Operator screen
Alarm Performance
An alarm journal measured against ISA-18.2 and EEMUA 191: rate, floods, bad actors, priority mix, standing and chattering 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>Alarm Performance - 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 ap-screen">
<header class="hmi-header">
<div class="hmi-identity">
<span class="hmi-plant">ALARM PERFORMANCE</span>
<span class="hmi-area">Line 3 control system · measured against ISA-18.2 and EEMUA 191</span>
</div>
<div class="hmi-header-status">
<div class="ap-range" role="group" aria-label="Period">
<button class="hmi-button" type="button" data-days="1" aria-pressed="true">Last 24 hours</button>
<button class="hmi-button" type="button" data-days="7" aria-pressed="false">Last 7 days</button>
</div>
</div>
<div class="hmi-header-actions">
<button class="hmi-theme-toggle" id="themeToggle" type="button">Light</button>
</div>
</header>
<section class="hmi-body ap-body">
<div class="ap-tiles">
<smart-status-tile id="tileRate" label="Per 10 min, average" precision-digits="2" target="≤ 1"></smart-status-tile>
<smart-status-tile id="tilePeak" label="Peak in 10 min" precision-digits="0" target="≤ 10"></smart-status-tile>
<smart-status-tile id="tileFlood" label="Time in flood" unit="%" precision-digits="1" target="< 1 %"></smart-status-tile>
<smart-status-tile id="tileTop10" label="Top 10 share" unit="%" precision-digits="0" target="< 20 %"></smart-status-tile>
<smart-status-tile id="tileStale" label="Standing > 24 h" precision-digits="0" target="≤ 5"></smart-status-tile>
<smart-status-tile id="tileChatter" label="Chattering" precision-digits="0" target="0"></smart-status-tile>
</div>
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Alarms per 10 minutes</span>
<span class="ap-hint" id="floodNote"></span>
</div>
<div class="hmi-panel-content">
<smart-trend id="rate" label="Alarm rate"></smart-trend>
</div>
</div>
<div class="ap-row">
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Bad actors</span>
<span class="ap-hint">The ten tags that raised the most alarms: fixing these is most of the work</span>
</div>
<div class="hmi-panel-content">
<smart-comparison-chart id="actors" label="Top 10 alarms by count" pareto precision-digits="0"></smart-comparison-chart>
</div>
</div>
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Priorities</span>
<span class="ap-hint">Aim for about 80 % low, 15 % medium, 5 % high</span>
</div>
<div class="hmi-panel-content">
<smart-comparison-chart id="priorities" label="Alarms by priority" kind="donut" unit="%" precision-digits="0"></smart-comparison-chart>
</div>
</div>
</div>
<div class="hmi-panel">
<div class="hmi-panel-header">
<span>Standing and chattering</span>
<span class="ap-hint">Alarms that have stopped meaning anything to the operator</span>
</div>
<div class="hmi-panel-content">
<smart-alarm-grid id="standing" timestamp-format="relative"></smart-alarm-grid>
</div>
</div>
</section>
</div>
<!-- scripts -->
<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.trend.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.comparisonchart.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.alarmgrid.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" />
/* Alarm performance.
*
* An alarm system is judged by what it asks of the operator. ISA-18.2 and EEMUA 191 put numbers
* on that: about one alarm in ten minutes is manageable, more than ten is a flood, a handful of
* tags usually raise most of the alarms, a standing alarm has stopped meaning anything, and one
* that chatters teaches the operator to ignore it. This screen measures a control system's
* alarm journal against those numbers. The journal here is a generated stand-in with the usual
* shape - a steady rate, two floods, a few bad actors - which a station would supply from its
* own alarm history. */
window.onload = function () {
const rate = document.getElementById('rate'),
actors = document.getElementById('actors'),
priorities = document.getElementById('priorities'),
standing = document.getElementById('standing'),
floodNote = document.getElementById('floodNote'),
themeToggle = document.getElementById('themeToggle'),
tiles = {
rate: document.getElementById('tileRate'),
peak: document.getElementById('tilePeak'),
flood: document.getElementById('tileFlood'),
top10: document.getElementById('tileTop10'),
stale: document.getElementById('tileStale'),
chatter: document.getElementById('tileChatter')
},
MIN = 60000,
H = 3600000,
SLOT = 10 * MIN,
NOW = Math.floor(Date.now() / SLOT) * SLOT,
FLOOD = 10;
/* ── The journal ────────────────────────────────────────────────────── */
/* the tags that alarm, how often, and at what priority: a few do most of it */
const TAGS = [
['LS-3021', 'Cap hopper level low', 2, 0.2], ['TT-3104', 'Glue unit temperature low', 3, 0.13], ['PS-3002', 'Air pressure switch low', 3, 0.11],
['ZS-3310', 'Palletiser door not closed', 2, 0.08], ['FT-3001', 'Filler flow deviation', 3, 0.06], ['PT-3001', 'Air pressure low', 3, 0.05],
['LT-3005', 'Product tank level high', 2, 0.04], ['VT-3202', 'Capper vibration high', 3, 0.035], ['TT-3002', 'Rinse water temperature low', 3, 0.03],
['XA-3400', 'Labeller glue empty', 2, 0.025], ['CW-3500', 'Checkweigher reject high', 1, 0.02], ['SS-3101', 'Emergency stop pressed', 1, 0.004]
];
/* and the long tail every real system has: a few hundred tags that alarm now and then */
const KINDS = [['TT', 'temperature deviation', 3], ['PT', 'pressure deviation', 3], ['LT', 'level deviation', 2], ['FT', 'flow deviation', 3], ['ZS', 'position not reached', 2], ['XA', 'drive fault', 1]];
for (let i = 0; i < 160; i++) {
const kind = KINDS[i % KINDS.length];
TAGS.push([kind[0] + '-3' + String(600 + i), kind[1], kind[2], 0.0024 + (i % 7) * 0.0004]);
}
const WEIGHT = TAGS.reduce((sum, tag) => sum + tag[3], 0);
const seed = (n) => { const x = Math.sin(n * 12.9898) * 43758.5453; return x - Math.floor(x); };
function journal(days) {
const slots = days * 144,
counts = [],
byTag = {},
byPriority = { 1: 0, 2: 0, 3: 0 };
for (let i = 0; i < slots; i++) {
const t = NOW - (slots - i) * SLOT,
hour = new Date(t).getHours(),
/* busier on the day shift, and two floods: a changeover and a trip */
base = 0.35 + (hour >= 6 && hour < 18 ? 0.35 : 0) + seed(i + days * 1000) * 0.8,
flood = (i === slots - 40 || i === slots - 39) ? 22 - (i === slots - 39 ? 9 : 0) : (days === 7 && (i === 300 || i === 612)) ? 14 : 0,
n = Math.round(base + flood);
counts.push({ timestamp: t, alarms: n, flood: FLOOD });
for (let k = 0; k < n; k++) {
let r = seed(i * 31 + k * 7 + days) * WEIGHT, pick = TAGS[TAGS.length - 1];
for (const tag of TAGS) {
if (r < tag[3]) { pick = tag; break; }
r -= tag[3];
}
byTag[pick[0]] = (byTag[pick[0]] || 0) + 1;
byPriority[pick[2]] += 1;
}
}
return { counts: counts, byTag: byTag, byPriority: byPriority };
}
function show(days) {
const j = journal(days),
total = j.counts.reduce((sum, c) => sum + c.alarms, 0),
average = total / j.counts.length,
peak = Math.max.apply(null, j.counts.map((c) => c.alarms)),
floods = j.counts.filter((c) => c.alarms > FLOOD).length,
ranked = Object.keys(j.byTag).sort((a, b) => j.byTag[b] - j.byTag[a]),
top10 = ranked.slice(0, 10).reduce((sum, tag) => sum + j.byTag[tag], 0),
chattering = ['LS-3021', 'PS-3002'];
tiles.rate.value = average;
tiles.rate.state = average <= 1 ? 'ok' : average <= 2 ? 'advisory' : 'warning';
tiles.peak.value = peak;
tiles.peak.state = peak <= FLOOD ? 'ok' : 'warning';
tiles.flood.value = floods / j.counts.length * 100;
tiles.flood.state = floods / j.counts.length < 0.01 ? 'ok' : 'warning';
tiles.top10.value = top10 / total * 100;
tiles.top10.state = top10 / total < 0.2 ? 'ok' : 'warning';
tiles.top10.footnote = 'of ' + total.toLocaleString('en') + ' alarms from ' + ranked.length + ' tags';
tiles.stale.value = 3;
tiles.stale.state = 'ok';
tiles.chatter.value = chattering.length;
tiles.chatter.state = chattering.length ? 'warning' : 'ok';
rate.clear();
rate.live = false;
rate.scaleMode = 'value';
rate.timeSpan = days * 24 * H;
rate.pens = [
{ field: 'alarms', label: 'Alarms in 10 min', unit: '', min: 0, max: 25, precision: 0 },
{ field: 'flood', label: 'Flood threshold', unit: '', min: 0, max: 25, precision: 0 }
];
rate.pushMany(j.counts);
rate.setWindow(NOW - days * 24 * H, NOW);
floodNote.textContent = floods ? floods + ' ten-minute period' + (floods === 1 ? '' : 's') + ' in flood (more than ' + FLOOD + ')' : 'No flood';
actors.items = ranked.slice(0, 10).map((tag) => ({ field: tag, label: tag + ' · ' + TAGS.find((t) => t[0] === tag)[1], value: j.byTag[tag] }));
priorities.items = [
{ field: 'p3', label: 'Low', value: Math.round(j.byPriority[3] / total * 100) },
{ field: 'p2', label: 'Medium', value: Math.round(j.byPriority[2] / total * 100) },
{ field: 'p1', label: 'High', value: Math.round(j.byPriority[1] / total * 100) }
];
standing.alarms = [
{ id: 's1', tag: 'LS-3021', priority: 2, message: 'Cap hopper level low - chattering: 41 times in the last hour', area: 'Capper', active: true, acknowledged: false, timestamp: NOW - 22 * MIN },
{ id: 's2', tag: 'PS-3002', priority: 3, message: 'Air pressure switch low - chattering: 18 times in the last hour', area: 'Utilities', active: true, acknowledged: true, timestamp: NOW - 48 * MIN },
{ id: 's3', tag: 'TT-3104', priority: 3, message: 'Glue unit temperature low - standing 31 h', area: 'Case packer', active: true, acknowledged: true, timestamp: NOW - 31 * H },
{ id: 's4', tag: 'LT-3005', priority: 2, message: 'Product tank level high - standing 26 h', area: 'Filler', active: true, acknowledged: true, timestamp: NOW - 26 * H },
{ id: 's5', tag: 'XA-3400', priority: 2, message: 'Labeller glue empty - standing 52 h, shelved', area: 'Labeller', active: true, acknowledged: true, timestamp: NOW - 52 * H, shelvedUntil: NOW + 4 * H }
];
}
document.querySelectorAll('.ap-range button').forEach(function (button) {
button.addEventListener('click', function () {
document.querySelectorAll('.ap-range button').forEach((b) => b.setAttribute('aria-pressed', b === button ? 'true' : 'false'));
show(Number(button.getAttribute('data-days')));
});
});
themeToggle.addEventListener('click', function () {
const theme = document.body.getAttribute('theme') === 'industrial-dark' ? 'industrial' : 'industrial-dark';
document.body.setAttribute('theme', theme);
themeToggle.textContent = theme === 'industrial-dark' ? 'Light' : 'Dark';
});
show(1);
};
styles.css
/* Alarm performance. The numbers the standards give, across the top; under them where the
alarms came from and which ones have stopped meaning anything. */
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;
}
.ap-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.ap-tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 12px;
}
.ap-row {
display: grid;
grid-template-columns: minmax(0, 1.6fr) minmax(320px, 1fr);
gap: 12px;
}
@media (max-width: 1100px) {
.ap-row {
grid-template-columns: 1fr;
}
}
.ap-hint {
font-size: 11px;
font-weight: 400;
text-transform: none;
letter-spacing: 0;
color: var(--smart-text-secondary-color);
}
.ap-range {
display: flex;
gap: 6px;
}
.ap-range button[aria-pressed="true"] {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
#rate {
width: 100%;
height: 240px;
}
smart-comparison-chart {
width: 100%;
height: 330px;
}
smart-alarm-grid {
width: 100%;
height: 300px;
}