Operator screen
Overview
A plant overview display with unit status, key values and active alarms.
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>Plant Overview - 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">
<header class="hmi-header">
<div class="hmi-identity">
<span class="hmi-plant">NORTHFIELD PLANT</span>
<span class="hmi-area">Area 2 · Utilities & Process</span>
</div>
<div class="hmi-header-status">
<span class="hmi-conn" id="connectionState">
<span class="hmi-conn-dot" id="connectionDot"></span>
<span id="connectionLabel">Connected</span>
</span>
<span class="hmi-clock" id="clock">--:--:--</span>
<button class="hmi-theme-toggle" id="themeToggle" type="button">Light</button>
</div>
</header>
<section class="hmi-kpis" aria-label="Key performance indicators">
<div class="kpi-tile">
<span class="kpi-label">Overall Equipment Effectiveness</span>
<span class="kpi-value" id="kpiOee">--<small>%</small></span>
<smart-progress-bar id="oeeBar" min="0" max="100" value="0"></smart-progress-bar>
</div>
<div class="kpi-tile">
<span class="kpi-label">Throughput</span>
<span class="kpi-value" id="kpiThroughput">--<small>u/h</small></span>
<span class="kpi-sub" id="kpiThroughputTarget">target 1 200 u/h</span>
</div>
<div class="kpi-tile">
<span class="kpi-label">Specific Energy</span>
<span class="kpi-value" id="kpiEnergy">--<small>kWh/t</small></span>
<span class="kpi-sub" id="kpiEnergyTrend">—</span>
</div>
<div class="kpi-tile" id="alarmTile">
<span class="kpi-label">Active Alarms</span>
<span class="kpi-value" id="kpiAlarms">0</span>
<span class="kpi-sub" id="kpiAlarmsDetail">no unacknowledged alarms</span>
</div>
</section>
<section class="hmi-instruments" aria-label="Process instruments">
<div class="instrument">
<span class="instrument-tag">PT-2101</span>
<smart-gauge id="pressureGauge"></smart-gauge>
<span class="instrument-desc">Header pressure · 0-10 bar</span>
</div>
<div class="instrument">
<span class="instrument-tag">FT-2104</span>
<smart-gauge id="flowGauge"></smart-gauge>
<span class="instrument-desc">Feed flow · 0-500 L/min</span>
</div>
<div class="instrument">
<span class="instrument-tag">TT-2108</span>
<smart-gauge id="temperatureGauge"></smart-gauge>
<span class="instrument-desc">Reactor outlet · -20-150 °C</span>
</div>
<div class="instrument">
<span class="instrument-tag">ST-2112</span>
<smart-gauge id="rpmGauge"></smart-gauge>
<span class="instrument-desc">Main agitator · 0-6000 RPM</span>
</div>
</section>
<section class="hmi-body">
<div class="hmi-panel hmi-panel-trend">
<div class="hmi-panel-header">
<span>Process trend · last 60 samples</span>
<span class="hmi-panel-note">1 s update</span>
</div>
<div class="hmi-panel-content">
<smart-chart id="trendChart"></smart-chart>
</div>
</div>
<div class="hmi-panel hmi-panel-levels">
<div class="hmi-panel-header">
<span>Storage levels</span>
</div>
<div class="hmi-panel-content hmi-tanks">
<div class="tank-slot">
<smart-tank id="tank1"></smart-tank>
<span class="tank-tag">TK-01</span>
</div>
<div class="tank-slot">
<smart-tank id="tank2"></smart-tank>
<span class="tank-tag">TK-02</span>
</div>
<div class="tank-slot">
<smart-tank id="tank3"></smart-tank>
<span class="tank-tag">TK-03</span>
</div>
</div>
</div>
<div class="hmi-panel hmi-panel-equipment">
<div class="hmi-panel-header">
<span>Equipment status</span>
</div>
<div class="hmi-panel-content">
<ul class="equipment-list" id="equipmentList"></ul>
</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-webcomponents/source/modules/smart.chart.js"></script>
<script type="module" src="node_modules/smart-webcomponents/source/modules/smart.progressbar.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.multistateindicator.js"></script>
<script type="text/javascript" src="data.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" />
/* Plant overview screen.
*
* Instruments are configured entirely from Smart.Industrial presets - each gauge
* gets its range, units, precision and alarm bands from one call, so this file
* only deals with layout behaviour and incoming values. */
const TREND_SAMPLES = 60;
/* Pens carry different units, so each is trended as a percentage of its own
engineering range - the same pen scaling a historian trend uses. The gauges
above carry the engineering values. */
const PENS = [
{ field: 'temperature', tag: 'TT-2108', unit: '°C', min: -20, max: 150, color: 3 },
{ field: 'pressure', tag: 'PT-2101', unit: 'bar', min: 0, max: 10, color: 0 },
{ field: 'flow', tag: 'FT-2104', unit: 'L/min', min: 0, max: 500, color: 2 }
];
function scale(pen, value) {
return (value - pen.min) / (pen.max - pen.min) * 100;
}
/* Seeded with plausible history so the trend reads as a running process on the
first frame instead of filling in from an empty axis over a minute. */
const trendData = [];
for (let i = 0; i < TREND_SAMPLES; i++) {
const phase = i / TREND_SAMPLES * Math.PI * 2;
trendData.push({
sample: i - TREND_SAMPLES,
temperature: scale(PENS[0], 84 + Math.sin(phase * 0.8) * 9),
pressure: scale(PENS[1], 6.2 + Math.sin(phase) * 0.8),
flow: scale(PENS[2], 310 + Math.sin(phase * 1.7) * 45)
});
}
let sampleIndex = 0;
const palette = Smart.Industrial.palette;
window.Smart('#trendChart', class {
get properties() {
return {
animation: 'none',
caption: '',
description: '',
showLegend: true,
showBorderLine: false,
backgroundColor: 'transparent',
columnSeriesOverlap: true,
padding: { left: 5, top: 5, right: 5, bottom: 5 },
titlePadding: { left: 0, top: 0, right: 0, bottom: 0 },
dataSource: trendData,
xAxis: {
dataField: 'sample',
valuesOnTicks: true,
unitInterval: 10,
gridLines: { visible: true, dashStyle: '2,2' },
labels: { formatFunction: (value) => value + 's' }
},
seriesGroups: [
{
type: 'line',
valueAxis: {
title: { text: '% of range' },
minValue: 0,
maxValue: 100,
unitInterval: 25
},
series: PENS.map((pen) => ({
dataField: pen.field,
displayText: pen.tag + ' (' + pen.min + '-' + pen.max + ' ' + pen.unit + ')',
lineWidth: 2,
symbolSize: 0
}))
}
]
};
}
});
window.onload = function () {
const Industrial = Smart.Industrial;
/* ── Instruments: one line each ──────────────────────────────────── */
Industrial.apply(document.getElementById('pressureGauge'), 'pressure-gauge');
Industrial.apply(document.getElementById('flowGauge'), 'flow-meter');
Industrial.apply(document.getElementById('temperatureGauge'), 'temperature-gauge');
Industrial.apply(document.getElementById('rpmGauge'), 'rpm-gauge');
const tanks = [
Industrial.apply(document.getElementById('tank1'), 'level-tank'),
Industrial.apply(document.getElementById('tank2'), 'level-tank'),
Industrial.apply(document.getElementById('tank3'), 'level-tank')
];
/* One scale, three tanks.
Every tank drew its own end-point labels, so the panel carried six redundant
"100.0 %" / "0.0 %" strings that took more width than the bars did and squeezed each
bar to a few pixels. The three share a range, so one scale serves all of them - which
is also how a tank farm is drawn on a real display, and it gives the bars the width
that makes a level readable at a glance. */
tanks[1].labelsVisibility = 'none';
tanks[2].labelsVisibility = 'none';
const gauges = {
pressure: document.getElementById('pressureGauge'),
flow: document.getElementById('flowGauge'),
temperature: document.getElementById('temperatureGauge'),
rpm: document.getElementById('rpmGauge')
};
const chart = document.getElementById('trendChart'),
oeeBar = document.getElementById('oeeBar'),
kpiOee = document.getElementById('kpiOee'),
kpiThroughput = document.getElementById('kpiThroughput'),
kpiEnergy = document.getElementById('kpiEnergy'),
kpiEnergyTrend = document.getElementById('kpiEnergyTrend'),
kpiAlarms = document.getElementById('kpiAlarms'),
kpiAlarmsDetail = document.getElementById('kpiAlarmsDetail'),
alarmTile = document.getElementById('alarmTile'),
equipmentList = document.getElementById('equipmentList'),
clock = document.getElementById('clock'),
themeToggle = document.getElementById('themeToggle');
/* ── Theme switch: the same screen in both industrial themes ──────── */
function applyTheme(theme) {
document.body.setAttribute('theme', theme);
const elements = document.querySelectorAll('smart-gauge, smart-tank, smart-chart, smart-progress-bar, smart-multi-state-indicator');
for (let i = 0; i < elements.length; i++) {
elements[i].theme = theme;
}
themeToggle.textContent = theme === 'industrial-dark' ? 'Light' : 'Dark';
/* Chart series colours are set in JavaScript, not CSS, so they are
swapped from the palette rather than inherited from the theme.
Both `color` and `lineColor` are set so the legend swatch and the
line itself stay in step. */
const colors = theme === 'industrial-dark' ? palette.seriesDark : palette.series,
series = chart.seriesGroups[0].series;
for (let i = 0; i < series.length; i++) {
series[i].color = colors[PENS[i].color];
series[i].lineColor = colors[PENS[i].color];
}
chart.refresh();
}
themeToggle.addEventListener('click', function () {
applyTheme(document.body.getAttribute('theme') === 'industrial-dark' ? 'industrial' : 'industrial-dark');
});
applyTheme('industrial-dark');
/* ── Equipment list ──────────────────────────────────────────────── */
const EQUIPMENT_STATES = [
{ value: 'running', label: 'Running', severity: 'ok' },
{ value: 'stopped', label: 'Stopped', severity: 'off' },
{ value: 'manual', label: 'Manual', severity: 'manual' },
{ value: 'maintenance', label: 'Maintenance', severity: 'maintenance' },
{ value: 'fault', label: 'Fault', severity: 'critical', blink: true }
];
function renderEquipment(equipment) {
equipmentList.innerHTML = '';
equipment.forEach(function (item) {
const row = document.createElement('li'),
indicator = document.createElement('smart-multi-state-indicator'),
tag = document.createElement('span'),
name = document.createElement('span');
row.className = 'equipment-row';
indicator.states = EQUIPMENT_STATES;
indicator.value = item.state;
indicator.showLabel = true;
indicator.theme = document.body.getAttribute('theme');
tag.className = 'equipment-tag';
tag.textContent = item.tag;
name.className = 'equipment-name';
name.textContent = item.name;
row.appendChild(indicator);
row.appendChild(tag);
row.appendChild(name);
equipmentList.appendChild(row);
});
}
/* ── Clock ───────────────────────────────────────────────────────── */
function updateClock() {
clock.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false });
}
updateClock();
setInterval(updateClock, 1000);
/* ── Bind the feed ───────────────────────────────────────────────── */
renderEquipment(PlantData.snapshot().equipment);
PlantData.subscribe(function (state) {
gauges.pressure.value = state.pressure;
gauges.flow.value = state.flow;
gauges.temperature.value = state.temperature;
gauges.rpm.value = state.rpm;
for (let i = 0; i < tanks.length; i++) {
tanks[i].value = state.levels[i];
}
kpiOee.innerHTML = state.oee.toFixed(1) + '<small>%</small>';
oeeBar.value = state.oee;
kpiThroughput.innerHTML = Math.round(state.throughput) + '<small>u/h</small>';
kpiEnergy.innerHTML = state.energy.toFixed(1) + '<small>kWh/t</small>';
const delta = state.energy - state.energyPrevious;
kpiEnergyTrend.textContent = (delta >= 0 ? '▲ +' : '▼ ') + delta.toFixed(2) + ' vs previous';
kpiEnergyTrend.className = 'kpi-sub ' + (delta > 0 ? 'trend-up' : 'trend-down');
kpiAlarms.textContent = state.alarms;
alarmTile.classList.toggle('kpi-tile-abnormal', state.alarms > 0);
kpiAlarmsDetail.textContent = state.alarms > 0
? state.alarms + ' unacknowledged - see alarm screen'
: 'no unacknowledged alarms';
trendData.shift();
trendData.push({
sample: sampleIndex++,
temperature: scale(PENS[0], state.temperature),
pressure: scale(PENS[1], state.pressure),
flow: scale(PENS[2], state.flow)
});
chart.update();
});
PlantData.start(1000);
};
styles.css
/* Plant overview screen. Every colour comes from the industrial theme tokens,
so the layout follows whichever industrial theme is active. */
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: 13px;
overflow: auto;
}
.hmi-screen {
display: grid;
grid-template-rows: auto auto auto 1fr;
gap: 10px;
padding: 10px;
min-height: 100%;
box-sizing: border-box;
}
/* ── Header ──────────────────────────────────────────────────────────── */
.hmi-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
}
.hmi-identity {
display: flex;
flex-direction: column;
gap: 2px;
}
.hmi-plant {
font-size: 16px;
font-weight: 600;
letter-spacing: .08em;
}
.hmi-area {
color: var(--smart-text-tertiary-color);
font-size: 11px;
letter-spacing: .04em;
}
.hmi-header-status {
display: flex;
align-items: center;
gap: 18px;
}
.hmi-conn {
display: flex;
align-items: center;
gap: 8px;
color: var(--smart-text-secondary-color);
}
.hmi-conn-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--smart-industrial-ok);
}
.hmi-conn-dot.disconnected {
background: var(--smart-industrial-alarm-critical);
}
.hmi-clock {
font-family: Consolas, "Courier New", monospace;
font-size: 15px;
letter-spacing: .05em;
}
.hmi-theme-toggle {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
font-family: inherit;
font-size: 11px;
letter-spacing: .06em;
padding: 5px 12px;
text-transform: uppercase;
}
.hmi-theme-toggle:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
/* ── KPI tiles ───────────────────────────────────────────────────────── */
.hmi-kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.kpi-tile {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px 14px;
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-left: 3px solid var(--smart-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
}
.kpi-tile-abnormal {
background: var(--smart-industrial-abnormal-background);
border-left-color: var(--smart-industrial-alarm-critical);
}
.kpi-label {
color: var(--smart-text-tertiary-color);
font-size: 11px;
letter-spacing: .05em;
text-transform: uppercase;
}
.kpi-value {
font-size: 30px;
font-weight: 300;
line-height: 1;
}
.kpi-value small {
font-size: 13px;
margin-left: 4px;
color: var(--smart-text-tertiary-color);
}
.kpi-sub {
color: var(--smart-text-tertiary-color);
font-size: 11px;
}
.kpi-sub.trend-up {
color: var(--smart-industrial-alarm-warning-text, var(--smart-industrial-alarm-warning));
}
.kpi-sub.trend-down {
color: var(--smart-industrial-ok-text, var(--smart-industrial-ok));
}
#alarmTile.kpi-tile-abnormal .kpi-value {
color: var(--smart-industrial-alarm-critical-text, var(--smart-industrial-alarm-critical));
}
.kpi-tile smart-progress-bar {
height: 6px;
width: 100%;
}
/* ── Instrument row ──────────────────────────────────────────────────── */
.hmi-instruments {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.instrument {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px;
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
}
/* Square box: the gauge derives its radius from the smaller side, so a wide,
short tile would draw the scale larger than the needle can reach. */
.instrument smart-gauge {
width: 215px;
height: 215px;
}
.instrument-tag {
align-self: flex-start;
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
letter-spacing: .06em;
color: var(--smart-text-secondary-color);
}
.instrument-desc {
color: var(--smart-text-tertiary-color);
font-size: 11px;
}
/* ── Body: trend + tanks + equipment ─────────────────────────────────── */
.hmi-body {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(240px, 1fr) minmax(260px, 1fr);
gap: 10px;
min-height: 320px;
}
.hmi-panel {
display: flex;
flex-direction: column;
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
overflow: hidden;
min-width: 0;
}
.hmi-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--smart-industrial-panel-header-background);
border-bottom: 1px solid var(--smart-industrial-panel-border);
font-size: 11.5px;
font-weight: 600;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--smart-text-secondary-color);
}
.hmi-panel-note {
color: var(--smart-text-tertiary-color);
text-transform: none;
letter-spacing: 0;
}
.hmi-panel-content {
flex: 1;
padding: 10px;
min-height: 0;
overflow: auto;
}
.hmi-panel-trend smart-chart {
width: 100%;
height: 100%;
min-height: 260px;
}
/* ── Tanks ───────────────────────────────────────────────────────────── */
/* The first slot carries the shared scale, so it gets the extra width the labels
need; the other two are all bar. */
.hmi-tanks {
display: grid;
grid-template-columns: 1.45fr 1fr 1fr;
gap: 12px;
}
.tank-slot {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.tank-slot smart-tank {
width: 100%;
height: 240px;
}
.tank-tag {
font-family: Consolas, "Courier New", monospace;
font-size: 11px;
color: var(--smart-text-secondary-color);
}
/* ── Equipment list ──────────────────────────────────────────────────── */
.equipment-list {
list-style: none;
margin: 0;
padding: 0;
}
.equipment-row {
display: grid;
grid-template-columns: 118px 78px 1fr;
align-items: center;
gap: 10px;
padding: 9px 4px;
border-bottom: 1px solid var(--smart-border);
}
.equipment-row:last-child {
border-bottom: none;
}
.equipment-tag {
font-family: Consolas, "Courier New", monospace;
font-size: 11px;
color: var(--smart-text-secondary-color);
}
.equipment-name {
color: var(--smart-text-color);
}
/* ── Responsive ──────────────────────────────────────────────────────── */
@media (max-width: 1280px) {
.hmi-body {
grid-template-columns: 1fr 1fr;
}
.hmi-panel-trend {
grid-column: 1 / -1;
}
}
@media (max-width: 900px) {
.hmi-kpis,
.hmi-instruments {
grid-template-columns: repeat(2, 1fr);
}
.hmi-body {
grid-template-columns: 1fr;
}
}