Operator screen
KPI Wall
OEE, count against target, downtime Pareto, SPC and alarm KPIs on one display.
index.htm as it is written with the smart-industrial package; industrial-demos.css is the screens' shared layout
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>KPI Wall - Smart.Industrial</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="industrial-demos.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial-dark">
<div class="hmi-screen kw-screen">
<header class="hmi-header">
<div class="hmi-identity">
<span class="hmi-plant">LINE 3 · KPI WALL</span>
<span class="hmi-area">Packaging · Day shift · <span id="shiftWindow">06:00 – 14:00</span></span>
</div>
<div class="hmi-header-actions">
<div class="kw-shift" aria-label="Shift progress">
<span class="kw-shift-label" id="shiftLabel">Shift 0 %</span>
<div class="kw-shift-bar"><div class="kw-shift-fill" id="shiftFill"></div></div>
</div>
<span class="hmi-clock" id="clock">--:--:--</span>
<button class="hmi-theme-toggle" id="themeToggle" type="button">Light</button>
</div>
</header>
<section class="kw-oee" aria-label="Overall equipment effectiveness">
<smart-status-tile id="tileOee" label="OEE" unit="%" precision-digits="1" target="85"></smart-status-tile>
<smart-status-tile id="tileAvailability" label="Availability" unit="%" precision-digits="1"></smart-status-tile>
<smart-status-tile id="tilePerformance" label="Performance" unit="%" precision-digits="1"></smart-status-tile>
<smart-status-tile id="tileQuality" label="Quality" unit="%" precision-digits="2"></smart-status-tile>
<div class="kw-count hmi-panel">
<span class="kw-count-label">Units this shift</span>
<smart-seven-segment id="count" digits="6" leading-zeros></smart-seven-segment>
<span class="kw-count-target">target <b id="target">--</b> · <span id="countGap">--</span></span>
</div>
<div class="kw-rate hmi-panel">
<span class="kw-count-label">Rate</span>
<smart-bar-graph id="rate" orientation="horizontal" min="0" max="1600" segments="24" unit="u/h" show-value peak-hold></smart-bar-graph>
<span class="kw-count-target">nominal <b>1,440 u/h</b></span>
</div>
</section>
<section class="kw-middle">
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Line status</span>
<span class="hmi-panel-note" id="lineNote">All stations running</span>
</div>
<div class="hmi-panel-content kw-stations" id="stations"></div>
</div>
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Production vs target</span>
<span class="hmi-panel-note">Units per hour, this shift</span>
</div>
<div class="hmi-panel-content kw-chart">
<smart-chart id="hourly"></smart-chart>
</div>
</div>
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Downtime</span>
<span class="hmi-panel-note" id="downtimeNote">0 min this shift</span>
</div>
<div class="hmi-panel-content kw-chart">
<smart-chart id="pareto"></smart-chart>
</div>
</div>
</section>
<section class="kw-bottom">
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Fill weight</span>
<span class="hmi-panel-note" id="spcNote">X̄-R, n = 5, limits from the first 20 subgroups</span>
</div>
<div class="hmi-panel-content kw-spc">
<smart-control-chart id="spc" type="xbar-r" sample-size="5" baseline-count="20" unit="g" precision-digits="2" show-capability show-stats show-violations label="Fill weight"></smart-control-chart>
</div>
</div>
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Alarms</span>
<span class="hmi-panel-note" id="alarmNote">No active alarms</span>
</div>
<div class="hmi-panel-content kw-alarms">
<smart-alarm-banner id="banner" max-visible="3" show-acknowledge="false" empty-message="No active alarms"></smart-alarm-banner>
<smart-annunciator id="annunciator" columns="4"></smart-annunciator>
</div>
</div>
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Energy & waste</span>
<span class="hmi-panel-note">Per 1,000 units</span>
</div>
<div class="hmi-panel-content kw-tiles">
<smart-status-tile id="tileEnergy" label="Electricity" unit="kWh" precision-digits="1" trend="flat" footnote="per 1,000 units"></smart-status-tile>
<smart-status-tile id="tileAir" label="Compressed air" unit="Nm³" precision-digits="1" footnote="per 1,000 units"></smart-status-tile>
<smart-status-tile id="tileScrap" label="Scrap" unit="%" precision-digits="2" footnote="rejects + rework"></smart-status-tile>
<smart-status-tile id="tileChangeover" label="Next changeover" footnote="IBU-400 → IBU-200"></smart-status-tile>
</div>
</div>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/smart.industrial.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.statustile.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.sevensegment.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.bargraph.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.multistateindicator.js"></script>
<script type="module" src="node_modules/smart-webcomponents/source/modules/smart.chart.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.controlchart.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.alarmbanner.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.annunciator.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
<script>
/* The site's light/dark switch is the reader's choice: a screen opened from the light site
in a new tab opens light too. Framed, the site's page sets the theme itself. */
addEventListener('load', function () {
var light = false,
toggle = document.querySelector('.hmi-theme-toggle');
try { light = window.top === window && localStorage.getItem('smart-industrial-dark') === 'false'; } catch (e) { }
/* after the screen's own onload, which is where its button starts listening */
if (light && toggle) { setTimeout(function () { if (document.body.getAttribute('theme') === 'industrial-dark') { toggle.click(); } }, 0); }
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* KPI wall.
*
* The display on the wall at the end of the line, read from ten metres by people walking
* past: OEE and its three factors, the count against the shift target, the rate, which
* stations are running, the hourly production, where the downtime went, whether the fill
* weight is in control, and whatever is in alarm. Nothing on it is operated - it is the
* plant's own numbers, big, and a wall that says the right thing when it says nothing.
*
* Everything is simulated from a shift clock so the wall is alive in the demo; every
* number would come from the line's MES and the historian. */
window.onload = function () {
const Industrial = window.Smart.Industrial,
el = function (id) {
return document.getElementById(id);
};
const SHIFT_START = 6 * 3600, /* 06:00 */
SHIFT_LENGTH = 8 * 3600,
NOMINAL_RATE = 1440, /* units per hour */
TARGET = 10800; /* units per shift at 94 % */
/* Where the shift is now, compressed: the demo runs a shift in a few minutes. */
const started = Date.now();
function shiftSeconds() {
const real = (Date.now() - started) / 1000;
return Math.min(SHIFT_LENGTH, 4 * 3600 + real * 20);
}
/* ── The stations ───────────────────────────────────────────────────── */
const STATES = [
{ value: 'running', label: 'Running', severity: 'ok' },
{ value: 'starved', label: 'Starved', severity: 'advisory' },
{ value: 'blocked', label: 'Blocked', severity: 'warning' },
{ value: 'fault', label: 'Fault', severity: 'critical', blink: true },
{ value: 'changeover', label: 'Changeover', severity: 'neutral' }
];
const stations = [
{ id: 'unscrambler', label: 'Unscrambler', state: 'running' },
{ id: 'filler', label: 'Filler', state: 'running' },
{ id: 'capper', label: 'Capper', state: 'running' },
{ id: 'labeller', label: 'Labeller', state: 'running' },
{ id: 'cartoner', label: 'Cartoner', state: 'running' },
{ id: 'palletiser', label: 'Palletiser', state: 'running' }
];
const stationHost = el('stations');
stations.forEach(function (station) {
const card = document.createElement('div'),
indicator = document.createElement('smart-multi-state-indicator'),
label = document.createElement('span'),
rate = document.createElement('span');
card.className = 'kw-station';
indicator.states = STATES;
indicator.value = station.state;
indicator.showLabel = true;
indicator.id = 'station-' + station.id;
label.className = 'kw-station-label';
label.textContent = station.label;
rate.className = 'kw-station-rate';
rate.id = 'rate-' + station.id;
card.appendChild(label);
card.appendChild(indicator);
card.appendChild(rate);
stationHost.appendChild(card);
station.indicator = indicator;
station.rateNode = rate;
});
/* ── The counters ───────────────────────────────────────────────────── */
let produced = 6120,
rejected = 41,
downtimeMinutes = { 'Label reel change': 14, 'Capper jam': 9, 'Starved (upstream)': 11, 'Filler CIP overrun': 6, 'Cartoner fault': 4 },
fault = null,
hourly = [],
fills = [];
for (let h = 0; h < 4; h++) {
hourly.push({ hour: (6 + h) + ':00', produced: 1310 + Math.round(Math.random() * 120), target: 1350 });
}
/* A fill weight process: 10.00 g nominal, sigma 0.03, a drift late in the shift. */
function fillSubgroup(index) {
const drift = index > 30 ? (index - 30) * 0.004 : 0,
group = [];
for (let i = 0; i < 5; i++) {
let u = 0;
for (let k = 0; k < 12; k++) {
u += Math.random();
}
group.push(Number((10 + drift + (u - 6) * 0.03).toFixed(3)));
}
return group;
}
for (let i = 0; i < 28; i++) {
fills.push(fillSubgroup(i));
}
el('spc').data = fills.slice();
el('spc').specLimits = { lsl: 9.9, usl: 10.1, target: 10 };
/* ── The charts ─────────────────────────────────────────────────────── */
const palette = Industrial.paletteFor(document.body);
function chartBase() {
return {
animation: 'none',
caption: '',
description: '',
showLegend: false,
showBorderLine: false,
backgroundColor: 'transparent',
padding: { left: 5, top: 10, right: 10, bottom: 5 },
titlePadding: { left: 0, top: 0, right: 0, bottom: 0 }
};
}
const hourlyChart = el('hourly'),
paretoChart = el('pareto');
function renderHourly() {
hourlyChart.dataSource = hourly.slice();
hourlyChart.xAxis = { dataField: 'hour', gridLines: { visible: false } };
hourlyChart.seriesGroups = [{
type: 'column',
columnsGapPercent: 40,
seriesGapPercent: 0,
valueAxis: { minValue: 0, maxValue: 1600, unitInterval: 400, gridLines: { visible: true, dashStyle: '2,2' } },
series: [
{ dataField: 'produced', displayText: 'Produced', fillColor: palette.series[0], lineColor: palette.series[0] },
{ dataField: 'target', displayText: 'Target', fillColor: 'transparent', lineColor: palette.series[1], lineWidth: 2, opacity: 0.9 }
]
}];
hourlyChart.update();
}
function renderPareto() {
const rows = Object.keys(downtimeMinutes).map(function (cause) {
return { cause: cause, minutes: downtimeMinutes[cause] };
}).sort(function (a, b) {
return b.minutes - a.minutes;
});
let total = 0;
rows.forEach(function (row) {
total += row.minutes;
});
let running = 0;
rows.forEach(function (row) {
running += row.minutes;
row.cumulative = Math.round(running / total * 100);
});
paretoChart.dataSource = rows;
paretoChart.xAxis = { dataField: 'cause', gridLines: { visible: false }, labels: { angle: 0 } };
paretoChart.seriesGroups = [{
type: 'column',
columnsGapPercent: 45,
valueAxis: { minValue: 0, unitInterval: 5, description: 'min', gridLines: { visible: true, dashStyle: '2,2' } },
series: [{ dataField: 'minutes', displayText: 'Minutes', fillColor: palette.series[2], lineColor: palette.series[2] }]
}, {
type: 'line',
valueAxis: { minValue: 0, maxValue: 100, unitInterval: 25, position: 'right', description: '% cumulative' },
series: [{ dataField: 'cumulative', displayText: 'Cumulative', lineColor: palette.series[3], lineWidth: 2, symbolType: 'circle', symbolSize: 6 }]
}];
paretoChart.update();
el('downtimeNote').textContent = total + ' min this shift · ' + rows[0].cause + ' first';
}
Object.assign(hourlyChart, chartBase());
Object.assign(paretoChart, chartBase());
renderHourly();
renderPareto();
/* ── Alarms ─────────────────────────────────────────────────────────── */
const banner = el('banner'),
annunciator = el('annunciator');
let alarms = [];
function renderAlarms() {
banner.alarms = alarms.slice();
annunciator.tiles = stations.map(function (station) {
return {
id: station.id,
label: station.label,
state: station.state === 'fault' ? 'alarm' : station.state === 'blocked' || station.state === 'starved' ? 'ack' : 'normal'
};
});
el('alarmNote').textContent = alarms.length ? alarms.length + ' active' : 'No active alarms';
}
renderAlarms();
/* ── The shift, running ─────────────────────────────────────────────── */
let lastTick = shiftSeconds(),
faultUntil = 0,
starveUntil = 0;
setInterval(function () {
const now = shiftSeconds(),
elapsed = Math.max(0, now - lastTick),
hour = Math.floor(now / 3600);
lastTick = now;
/* Random events: a capper jam, an upstream starve. */
if (!fault && Math.random() < 0.012) {
fault = 'Capper jam';
faultUntil = now + 240;
stations[2].state = 'fault';
stations[3].state = 'starved';
stations[4].state = 'starved';
stations[0].state = 'blocked';
stations[1].state = 'blocked';
alarms = [{ id: 'capper-' + now, tag: 'CAP-301', message: 'Capper jam - chuck 4', priority: 1, timestamp: Date.now(), acknowledged: false, active: true }];
renderAlarms();
}
if (fault && now > faultUntil) {
downtimeMinutes[fault] = (downtimeMinutes[fault] || 0) + Math.round((faultUntil - (faultUntil - 240)) / 60);
fault = null;
stations.forEach(function (station) {
station.state = 'running';
});
alarms = [];
renderAlarms();
renderPareto();
}
if (!fault && !starveUntil && Math.random() < 0.006) {
starveUntil = now + 150;
stations[0].state = 'starved';
stations[1].state = 'starved';
alarms = [{ id: 'starve-' + now, tag: 'LINE-3', message: 'Unscrambler starved - bottle supply', priority: 3, timestamp: Date.now(), acknowledged: true, active: true }];
renderAlarms();
}
if (starveUntil && now > starveUntil) {
downtimeMinutes['Starved (upstream)'] += 2;
starveUntil = 0;
stations[0].state = 'running';
stations[1].state = 'running';
alarms = [];
renderAlarms();
renderPareto();
}
const running = !fault,
rate = running ? (starveUntil ? NOMINAL_RATE * 0.55 : NOMINAL_RATE * (0.93 + Math.random() * 0.08)) : 0;
produced += rate * elapsed / 3600;
if (Math.random() < 0.02) {
rejected += 1;
}
/* Station readouts. */
stations.forEach(function (station) {
station.indicator.value = station.state;
station.rateNode.textContent = station.state === 'running' ? Math.round(rate) + ' u/h' : station.state;
});
el('lineNote').textContent = fault ? 'Line down - ' + fault : starveUntil ? 'Running starved' : 'All stations running';
/* Hourly. */
while (hourly.length < hour - 6 + 1 && hourly.length < 8) {
hourly.push({ hour: (6 + hourly.length) + ':00', produced: 0, target: 1350 });
}
if (hourly[hour - 6]) {
hourly[hour - 6].produced = Math.round(hourly[hour - 6].produced + rate * elapsed / 3600);
}
/* OEE from the shift so far. */
const shiftFraction = now / SHIFT_LENGTH,
plannedTime = now,
downtime = Object.keys(downtimeMinutes).reduce(function (sum, key) {
return sum + downtimeMinutes[key];
}, 0) * 60,
availability = Math.max(0, (plannedTime - downtime) / plannedTime) * 100,
performance = Math.min(98.5, produced / ((plannedTime - downtime) / 3600 * NOMINAL_RATE) * 100),
quality = (produced - rejected) / produced * 100,
oee = availability * performance * quality / 10000;
el('tileOee').value = oee;
el('tileOee').state = oee >= 85 ? 'ok' : oee >= 75 ? 'warning' : 'critical';
el('tileAvailability').value = availability;
el('tileAvailability').state = availability >= 90 ? 'ok' : 'warning';
el('tilePerformance').value = performance;
el('tilePerformance').state = performance >= 90 ? 'ok' : 'warning';
el('tileQuality').value = quality;
el('tileQuality').state = quality >= 99.5 ? 'ok' : 'warning';
el('tileOee').trend = oee >= 85 ? 'up' : 'down';
el('count').value = Math.floor(produced);
el('target').textContent = TARGET.toLocaleString('en-US');
const expected = TARGET * shiftFraction,
gap = Math.floor(produced - expected);
el('countGap').textContent = (gap >= 0 ? '+' : '') + gap.toLocaleString('en-US') + ' vs plan';
el('countGap').className = gap >= 0 ? 'hmi-ok' : 'hmi-warning';
el('rate').value = Math.round(rate);
el('shiftFill').style.width = (shiftFraction * 100) + '%';
el('shiftLabel').textContent = 'Shift ' + Math.round(shiftFraction * 100) + ' %';
el('tileEnergy').value = 38.2 + (rate < NOMINAL_RATE * 0.7 ? 6 : 0) + Math.random() * 0.4;
el('tileEnergy').trend = rate < NOMINAL_RATE * 0.7 ? 'up' : 'flat';
el('tileAir').value = 12.4 + Math.random() * 0.2;
el('tileScrap').value = rejected / produced * 100;
el('tileScrap').state = rejected / produced > 0.008 ? 'warning' : 'ok';
el('tileChangeover').value = clockAt(SHIFT_START + SHIFT_LENGTH - 45 * 60) + ' (' + Math.max(0, Math.round((SHIFT_LENGTH - 45 * 60 - now) / 60)) + ' min)';
el('clock').textContent = clockAt(SHIFT_START + now);
}, 500);
/* A new fill weight subgroup every few seconds. */
setInterval(function () {
fills.push(fillSubgroup(fills.length));
if (fills.length > 60) {
fills.shift();
}
el('spc').data = fills.slice();
const violations = el('spc').violations ? el('spc').violations() : [];
el('spcNote').textContent = 'X̄-R, n = 5 · ' + fills.length + ' subgroups · ' +
(violations.length ? violations.length + ' rule violation' + (violations.length === 1 ? '' : 's') : 'in control');
}, 4000);
setInterval(renderHourly, 5000);
function clockAt(seconds) {
const s = Math.floor(seconds) % 86400;
return ('0' + Math.floor(s / 3600)).slice(-2) + ':' + ('0' + Math.floor((s % 3600) / 60)).slice(-2);
}
/* ── Theme ──────────────────────────────────────────────────────────── */
const toggle = el('themeToggle');
toggle.addEventListener('click', function () {
const dark = document.body.getAttribute('theme') === 'industrial-dark',
theme = dark ? 'industrial' : 'industrial-dark';
document.body.setAttribute('theme', theme);
toggle.textContent = dark ? 'Dark' : 'Light';
document.querySelectorAll('smart-chart').forEach(function (chart) {
chart.theme = theme;
});
});
};
styles.css
/* KPI wall. Read from ten metres: every number is large, every label is short, and the
only saturated colour belongs to a station that is not running or a KPI below target. */
html,
body {
height: 100%;
margin: 0;
}
body.viewport {
background: var(--smart-industrial-canvas, var(--smart-body-background-color));
color: var(--smart-text-color);
font-family: var(--smart-font-family);
font-size: 14px;
}
.kw-screen {
gap: 12px;
}
/* ── Shift progress in the header ────────────────────────────────────── */
.kw-shift {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 200px;
}
.kw-shift-label {
font-size: 11px;
letter-spacing: 0.05em;
text-transform: uppercase;
opacity: 0.8;
}
.kw-shift-bar {
height: 8px;
/* a groove in both themes (the readout background is a display's black) */
background: var(--smart-industrial-canvas);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
overflow: hidden;
}
.kw-shift-fill {
height: 100%;
width: 0;
background: var(--smart-industrial-running, var(--smart-primary));
}
/* ── OEE row ─────────────────────────────────────────────────────────── */
.kw-oee {
display: grid;
grid-template-columns: repeat(4, minmax(160px, 1fr)) minmax(260px, 1.4fr) minmax(200px, 1fr);
gap: 12px;
min-width: 0;
}
.kw-oee smart-status-tile {
--smart-status-tile-value-size: 44px;
font-size: 16px;
}
.kw-count,
.kw-rate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
}
.kw-count-label,
.kw-count-target {
font-size: 12px;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--smart-text-secondary-color);
}
.kw-count-target b {
font-variant-numeric: tabular-nums;
}
.kw-count smart-seven-segment {
width: 100%;
height: 72px;
}
.kw-rate smart-bar-graph {
--smart-bar-graph-height: 220px;
--smart-bar-graph-width: 28px;
}
/* ── Middle row ──────────────────────────────────────────────────────── */
.kw-middle,
.kw-bottom {
display: grid;
gap: 12px;
min-width: 0;
}
.kw-middle {
grid-template-columns: minmax(300px, 1fr) minmax(320px, 1.3fr) minmax(320px, 1.3fr);
}
.kw-bottom {
grid-template-columns: minmax(420px, 2fr) minmax(320px, 1.2fr) minmax(240px, 1fr);
}
/* Six tiles: two even rows of three, then three of two, then one column - never five and
an orphan, which is what auto-fit made of them at a laptop's width. */
@media (max-width: 1200px) {
.kw-oee {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.kw-middle,
.kw-bottom {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.kw-oee {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 440px) {
.kw-oee {
grid-template-columns: 1fr;
}
}
.kw-stations {
display: grid;
grid-template-columns: repeat(2, minmax(120px, 1fr));
gap: 10px;
}
.kw-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 10px 8px;
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
background: var(--smart-industrial-panel-header-background);
}
.kw-station-label {
font-size: 12px;
letter-spacing: 0.05em;
text-transform: uppercase;
opacity: 0.85;
}
.kw-station smart-multi-state-indicator {
font-size: 15px;
font-weight: 700;
}
.kw-station-rate {
font-size: 13px;
font-variant-numeric: tabular-nums;
opacity: 0.8;
}
.kw-chart smart-chart {
width: 100%;
height: 260px;
}
.kw-spc smart-control-chart {
width: 100%;
height: 340px;
}
.kw-alarms {
display: flex;
flex-direction: column;
gap: 10px;
}
.kw-tiles {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}
Components on this screen
Alarm BannerAnnunciatorBar GraphControl ChartMulti-State IndicatorSeven SegmentStatus Tile