Operator screen
Asset Detail
A single asset with vibration zones, a spectrum, permissives and its maintenance record.
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>Asset Detail - 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.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="asset-tag">P-2101A</span>
<span class="asset-name">Feed pump A · centrifugal, 55 kW</span>
</div>
<div class="asset-chips">
<span class="chip chip-state" id="chipState">RUNNING</span>
<span class="chip" id="chipMode">AUTO</span>
<span class="chip" id="chipHealth">HEALTH 92%</span>
<span class="chip chip-muted" id="chipRuntime">—</span>
</div>
<div class="hmi-header-actions">
<button class="hmi-theme-toggle" id="themeToggle" type="button">Light</button>
</div>
</header>
<section class="hmi-body">
<div class="hmi-panel hmi-panel-instruments">
<div class="hmi-panel-header"><span>Live measurements</span></div>
<div class="hmi-panel-content">
<div class="instrument-cluster">
<div class="instrument">
<span class="instrument-tag">PT-2101</span>
<smart-gauge id="dischargeGauge"></smart-gauge>
<span class="instrument-desc">Discharge pressure</span>
</div>
<div class="instrument">
<span class="instrument-tag">FT-2104</span>
<smart-gauge id="flowGauge"></smart-gauge>
<span class="instrument-desc">Flow</span>
</div>
<div class="instrument">
<span class="instrument-tag">VT-2101</span>
<smart-gauge id="vibrationGauge"></smart-gauge>
<span class="instrument-desc">Bearing vibration</span>
</div>
</div>
<div class="setpoint-block">
<div class="setpoint-header">
<span>Speed setpoint</span>
<span class="setpoint-value" id="setpointValue">--<small>%</small></span>
</div>
<smart-slider id="speedSetpoint"></smart-slider>
<p class="setpoint-note">
Writes go through the control layer. The slider only raises a <code>change</code> event.
</p>
</div>
</div>
</div>
<div class="hmi-panel hmi-panel-detail">
<smart-tabs id="detailTabs" animation="none">
<smart-tab-item label="Trend">
<div class="tab-body">
<smart-chart id="assetChart"></smart-chart>
</div>
</smart-tab-item>
<smart-tab-item label="Properties">
<div class="tab-body tab-body-scroll">
<smart-table id="propertiesTable"></smart-table>
</div>
</smart-tab-item>
<smart-tab-item label="Maintenance">
<div class="tab-body tab-body-scroll">
<ul class="event-list" id="eventList"></ul>
</div>
</smart-tab-item>
</smart-tabs>
</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.tabs.js"></script>
<script type="module" src="node_modules/smart-webcomponents/source/modules/smart.table.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" />
/* Asset detail screen: one piece of equipment, its live measurements, its
* setpoint, and the records an engineer reaches for when something looks off. */
const TREND_SAMPLES = 90;
const palette = Smart.Industrial.palette;
const SERIES = [
{ field: 'discharge', label: 'Discharge (bar)', min: 0, max: 10, color: 0 },
{ field: 'flow', label: 'Flow (L/min)', min: 0, max: 500, color: 2 },
{ field: 'vibration', label: 'Vibration (mm/s)', min: 0, max: 10, color: 3 }
];
function scale(series, value) {
return (value - series.min) / (series.max - series.min) * 100;
}
const trendData = [];
for (let i = 0; i < TREND_SAMPLES; i++) {
const phase = i / TREND_SAMPLES * Math.PI * 2;
trendData.push({
sample: i - TREND_SAMPLES,
discharge: scale(SERIES[0], 6.4 + Math.sin(phase) * 0.7),
flow: scale(SERIES[1], 312 + Math.sin(phase * 1.6) * 40),
vibration: scale(SERIES[2], 2.1 + Math.sin(phase * 2.3) * 0.5)
});
}
let sampleIndex = 0;
window.Smart('#assetChart', class {
get properties() {
return {
animation: 'none',
caption: '',
description: '',
showLegend: true,
showBorderLine: false,
backgroundColor: 'transparent',
padding: { left: 5, top: 10, right: 10, bottom: 5 },
titlePadding: { left: 0, top: 0, right: 0, bottom: 0 },
dataSource: trendData,
xAxis: {
dataField: 'sample',
valuesOnTicks: true,
unitInterval: 15,
gridLines: { visible: true, dashStyle: '2,2' },
labels: { formatFunction: (value) => value + 's' }
},
seriesGroups: [
{
type: 'line',
valueAxis: {
title: { text: '% of range' },
minValue: 0,
maxValue: 100,
unitInterval: 20
},
series: SERIES.map((series) => ({
dataField: series.field,
displayText: series.label,
lineWidth: 2,
symbolSize: 0
}))
}
]
};
}
});
window.Smart('#propertiesTable', class {
get properties() {
return {
dataSource: new window.Smart.DataAdapter({
dataSource: AssetData.properties,
dataFields: [
'property: string',
'value: string',
'source: string'
]
}),
columns: [
{ label: 'Property', dataField: 'property', width: 220 },
{ label: 'Value', dataField: 'value', width: 220 },
{ label: 'Source', dataField: 'source' }
]
};
}
});
window.onload = function () {
const Industrial = Smart.Industrial;
Industrial.apply(document.getElementById('dischargeGauge'), 'pressure-gauge');
Industrial.apply(document.getElementById('flowGauge'), 'flow-meter');
/* The zones of ISO 10816-3 for this machine - Group 2 (15-300 kW) on a rigid foundation:
A/B 1.4, B/C 2.8, C/D 4.5 mm/s RMS. The vibration-gauge preset's own bands (7, 11.25
and 17.5 mm/s) are not the zones of any column of the standard, and with them a pump
already in zone D showed as normal. */
Industrial.apply(document.getElementById('vibrationGauge'), 'vibration-gauge', {
min: 0,
max: 10,
ranges: Industrial.bands(0, 10, [[0, .28, 'normal'], [.28, .45, 'warning'], [.45, 1, 'critical']])
});
Industrial.apply(document.getElementById('speedSetpoint'), 'setpoint-slider', {
min: 40,
max: 100,
value: AssetData.snapshot().speed,
interval: 1
});
const gauges = {
discharge: document.getElementById('dischargeGauge'),
flow: document.getElementById('flowGauge'),
vibration: document.getElementById('vibrationGauge')
};
const chart = document.getElementById('assetChart'),
setpoint = document.getElementById('speedSetpoint'),
setpointValue = document.getElementById('setpointValue'),
chipState = document.getElementById('chipState'),
chipHealth = document.getElementById('chipHealth'),
chipRuntime = document.getElementById('chipRuntime'),
eventList = document.getElementById('eventList'),
themeToggle = document.getElementById('themeToggle');
/* ── Theme ───────────────────────────────────────────────────────── */
function applyTheme(theme) {
document.body.setAttribute('theme', theme);
const elements = document.querySelectorAll('smart-gauge, smart-slider, smart-chart, smart-tabs, smart-table');
for (let i = 0; i < elements.length; i++) {
elements[i].theme = theme;
}
themeToggle.textContent = theme === 'industrial-dark' ? 'Light' : 'Dark';
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[SERIES[i].color];
series[i].lineColor = colors[SERIES[i].color];
}
chart.refresh();
}
themeToggle.addEventListener('click', function () {
applyTheme(document.body.getAttribute('theme') === 'industrial-dark' ? 'industrial' : 'industrial-dark');
});
/* ── Setpoint ────────────────────────────────────────────────────── */
function showSetpoint(value) {
setpointValue.innerHTML = Number(value).toFixed(0) + '<small>%</small>';
}
setpoint.addEventListener('change', function (event) {
const value = event.detail.value;
AssetData.setSpeed(value);
showSetpoint(value);
});
showSetpoint(AssetData.snapshot().speed);
/* ── Maintenance history ─────────────────────────────────────────── */
eventList.innerHTML = AssetData.events.map(function (item) {
return '<li class="event-row event-' + item.type + '">' +
'<div class="event-meta">' +
'<span class="event-date">' + item.date + '</span>' +
'<span class="event-type">' + item.type + '</span>' +
'</div>' +
'<div class="event-body">' +
'<span class="event-title">' + item.title + '</span>' +
'<span class="event-detail">' + item.detail + '</span>' +
'<span class="event-by">' + item.by + '</span>' +
'</div>' +
'</li>';
}).join('');
/* ── Live values ─────────────────────────────────────────────────── */
applyTheme('industrial-dark');
AssetData.subscribe(function (state) {
gauges.discharge.value = state.discharge;
gauges.flow.value = state.flow;
gauges.vibration.value = state.vibration;
/* Vibration drives the asset's headline condition - ISO 10816-3 zones for a 55 kW
machine on a rigid foundation (Group 2, rigid). 4.5 and 7.1 had been used here,
which is the flexible-support column: a pump reading 5 mm/s, in zone D, showed as
only elevated. Zone C (above 2.8) is elevated, zone D (above 4.5) is high. */
let condition = 'running';
if (state.vibration > 4.5) {
condition = 'alarm';
}
else if (state.vibration > 2.8) {
condition = 'warning';
}
chipState.textContent = condition === 'alarm' ? 'HIGH VIBRATION'
: condition === 'warning' ? 'ELEVATED VIBRATION'
: 'RUNNING';
chipState.className = 'chip chip-state chip-' + condition;
chipHealth.textContent = 'HEALTH ' + Math.round(state.health) + '%';
chipRuntime.textContent = Math.round(state.runtimeHours).toLocaleString() + ' h runtime';
trendData.shift();
trendData.push({
sample: sampleIndex++,
discharge: scale(SERIES[0], state.discharge),
flow: scale(SERIES[1], state.flow),
vibration: scale(SERIES[2], state.vibration)
});
chart.update();
});
AssetData.start(1000);
};
styles.css
/* Asset detail screen. Colours come from the industrial theme tokens. */
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 1fr;
gap: 10px;
padding: 10px;
min-height: 100%;
box-sizing: border-box;
}
/* ── Header ──────────────────────────────────────────────────────────── */
.hmi-header {
display: flex;
align-items: center;
gap: 24px;
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;
}
.asset-tag {
font-family: Consolas, "Courier New", monospace;
font-size: 18px;
font-weight: 600;
letter-spacing: .06em;
}
.asset-name {
color: var(--smart-text-tertiary-color);
font-size: 11px;
}
.asset-chips {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.hmi-header-actions {
margin-left: auto;
}
.chip {
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
font-size: 10px;
letter-spacing: .07em;
padding: 4px 10px;
white-space: nowrap;
}
.chip-muted {
border-color: transparent;
color: var(--smart-text-tertiary-color);
}
.chip-running {
border-color: var(--smart-industrial-ok);
color: var(--smart-industrial-ok-text, var(--smart-industrial-ok));
}
.chip-warning {
border-color: var(--smart-industrial-alarm-warning);
color: var(--smart-industrial-alarm-warning-text, var(--smart-industrial-alarm-warning));
}
.chip-alarm {
background: var(--smart-industrial-alarm-critical);
border-color: var(--smart-industrial-alarm-critical);
color: var(--smart-industrial-alarm-critical-color);
}
.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);
}
/* ── Body ────────────────────────────────────────────────────────────── */
.hmi-body {
display: grid;
grid-template-columns: minmax(320px, 420px) minmax(0, 1fr);
gap: 10px;
min-height: 0;
}
.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;
min-height: 0;
}
.hmi-panel-header {
padding: 8px 12px;
background: var(--smart-industrial-panel-header-background);
border-bottom: 1px solid var(--smart-industrial-panel-border);
color: var(--smart-text-secondary-color);
font-size: 11.5px;
font-weight: 600;
letter-spacing: .06em;
text-transform: uppercase;
}
.hmi-panel-content {
flex: 1;
padding: 10px;
min-height: 0;
overflow: auto;
}
/* ── Instrument cluster ──────────────────────────────────────────────── */
.instrument-cluster {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
/* Vibration is the headline condition indicator for a rotating machine, so it
gets the full width rather than leaving a hole in the grid. */
.instrument-cluster .instrument:last-child {
grid-column: 1 / -1;
}
.instrument {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 6px;
border: 1px solid var(--smart-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
}
/* Square box: the gauge derives its radius from the smaller side. */
.instrument smart-gauge {
width: 180px;
height: 180px;
}
.instrument-tag {
align-self: flex-start;
font-family: Consolas, "Courier New", monospace;
font-size: 11px;
color: var(--smart-text-secondary-color);
}
.instrument-desc {
color: var(--smart-text-tertiary-color);
font-size: 11px;
}
/* ── Setpoint ────────────────────────────────────────────────────────── */
.setpoint-block {
margin-top: 14px;
padding: 12px;
border: 1px solid var(--smart-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
}
.setpoint-header {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 10px;
}
.setpoint-header > span:first-child {
color: var(--smart-text-tertiary-color);
font-size: 11px;
letter-spacing: .05em;
text-transform: uppercase;
}
.setpoint-value {
font-size: 26px;
font-weight: 300;
}
.setpoint-value small {
font-size: 12px;
margin-left: 3px;
color: var(--smart-text-tertiary-color);
}
.setpoint-block smart-slider {
width: 100%;
}
.setpoint-note {
color: var(--smart-text-tertiary-color);
font-size: 11px;
margin: 10px 0 0;
}
.setpoint-note code {
font-family: Consolas, "Courier New", monospace;
background: var(--smart-ui-state-hover);
padding: 1px 4px;
}
/* ── Detail tabs ─────────────────────────────────────────────────────── */
.hmi-panel-detail {
padding: 0;
}
.hmi-panel-detail smart-tabs {
width: 100%;
height: 100%;
min-height: 460px;
border: none;
}
.tab-body {
height: 100%;
box-sizing: border-box;
padding: 8px;
}
.tab-body-scroll {
overflow: auto;
}
.tab-body smart-chart {
width: 100%;
height: 100%;
min-height: 380px;
}
.tab-body smart-table {
width: 100%;
}
/* ── Maintenance history ─────────────────────────────────────────────── */
.event-list {
list-style: none;
margin: 0;
padding: 0;
}
.event-row {
display: grid;
grid-template-columns: 110px 1fr;
gap: 14px;
padding: 12px 8px;
border-bottom: 1px solid var(--smart-border);
border-left: 3px solid var(--smart-border);
}
.event-row:last-child {
border-bottom: none;
}
.event-alarm {
border-left-color: var(--smart-industrial-alarm-critical);
}
.event-maintenance {
border-left-color: var(--smart-industrial-maintenance);
}
.event-inspection {
border-left-color: var(--smart-industrial-manual);
}
.event-meta {
display: flex;
flex-direction: column;
gap: 3px;
}
.event-date {
font-family: Consolas, "Courier New", monospace;
font-size: 11px;
color: var(--smart-text-secondary-color);
}
.event-type {
font-size: 10px;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--smart-text-tertiary-color);
}
.event-body {
display: flex;
flex-direction: column;
gap: 3px;
}
.event-title {
font-weight: 500;
}
.event-detail {
color: var(--smart-text-secondary-color);
}
.event-by {
color: var(--smart-text-tertiary-color);
font-size: 11px;
}
/* ── Responsive ──────────────────────────────────────────────────────── */
@media (max-width: 1100px) {
.hmi-body {
grid-template-columns: 1fr;
}
}