Component demo · Alarms and status
Status Tile
StatusTile displays one process value with its label, unit, state and trend direction, for status walls and plant overview screens.
index.htm as it is written with the smart-industrial package
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Overview - Status Tile Custom Element | Javascript UI Components | htmlelements</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="styles.css" />
</head>
<body class="viewport" theme="industrial">
<div class="tile-demo">
<header class="tile-demo-head">
<h2>Status tile</h2>
<button class="tile-button" id="themeToggle" type="button">Dark</button>
</header>
<p class="tile-note">
Smart.StatusTile shows one process value with its label, its unit, its state and its trend direction. A tile answers whether a value is normal at a glance; a gauge shows where a value is in its range and needs a scale and a needle.
</p>
<h3>A plant overview strip</h3>
<div class="tile-strip" id="liveStrip"></div>
<p class="tile-note" id="liveNote">Values update every two seconds; the state follows the thresholds.</p>
<h3>Trend polarity</h3>
<p class="tile-note">
An arrow pointing up can be good or bad depending on the quantity. The <code>trendPolarity</code> property tells the tile whether a rising value is good, bad or neutral, and the arrow is coloured accordingly.
</p>
<div class="tile-strip">
<smart-status-tile label="First pass yield" value="96.2" unit="%" state="ok"
trend="up" trend-value="1.4" trend-polarity="higherIsBetter"
footnote="Rising is good"></smart-status-tile>
<smart-status-tile label="Scrap rate" value="3.1" unit="%" state="warning"
trend="up" trend-value="0.9" trend-polarity="lowerIsBetter"
footnote="Rising is bad"></smart-status-tile>
<smart-status-tile label="Ambient" value="21.4" unit="°C" state="neutral"
trend="up" trend-value="0.3" trend-polarity="neutral"
footnote="Rising is neither"></smart-status-tile>
</div>
<h3>Content</h3>
<div class="tile-strip">
<smart-status-tile label="Line state" value="Running" state="ok"
footnote="A name, not a number"></smart-status-tile>
<smart-status-tile label="Batch" value="B-2261-04" state="neutral"
footnote="Long text wraps and shrinks"></smart-status-tile>
<smart-status-tile label="Feed pressure" state="unknown"
footnote="No reading at all"></smart-status-tile>
<smart-status-tile label="Vessel level" value="62.8" unit="%" state="advisory"
target="60" id="sparkTile"></smart-status-tile>
</div>
<h3>Interactive tiles</h3>
<p class="tile-note">
The <code>interactive</code> property makes the tile a button: it can be focused with Tab, activated with Enter or Space, and is announced as a button by screen readers. It is off by default so that a tile that does nothing is not a tab stop.
</p>
<div class="tile-strip">
<smart-status-tile label="Open alarms" value="7" state="critical" interactive
trend="up" trend-value="3" trend-polarity="lowerIsBetter"
footnote="Press to open the alarm list" id="alarmTile"></smart-status-tile>
<smart-status-tile label="Work orders" value="12" state="advisory" interactive
footnote="Press to open the queue" id="woTile"></smart-status-tile>
</div>
<p class="tile-note" id="log">No tile pressed yet.</p>
<h3>Classifying a value</h3>
<p class="tile-note">
<code>StatusTile.stateFor(value, thresholds)</code> is a static method that returns the state for a value, so a screen can classify a value without a tile. Use <code>direction: 'below'</code> for quantities where a lower value is worse, such as capability indices, yields or remaining life.
</p>
<pre class="tile-code" id="stateForOut"></pre>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.statustile.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>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* Status tile overview.
*
* The live strip is built from a table of readings and thresholds rather than from
* markup, because that is how a real screen does it: the tags come from a configuration
* and the tile decides its own state from the numbers. Nothing here maps a value to a
* colour - StatusTile.stateFor does that, and it is the same call the screen would make
* to colour anything else. */
const TAGS = [
{ label: 'OEE', value: 78.4, unit: '%', digits: 1, target: 85,
thresholds: { advisory: 80, warning: 75, critical: 65, direction: 'below' },
polarity: 'higherIsBetter', drift: 0.9, history: true },
{ label: 'Feed pressure', value: 6.31, unit: 'bar', digits: 2, target: 6.2,
thresholds: { advisory: 7, warning: 8, critical: 9 },
polarity: 'neutral', drift: 0.09 },
{ label: 'Reactor temp', value: 84.2, unit: '°C', digits: 1,
thresholds: { advisory: 88, warning: 92, critical: 96 },
polarity: 'neutral', drift: 1.6 },
{ label: 'Open alarms', value: 4, unit: '', digits: 0,
thresholds: { advisory: 3, warning: 8, critical: 15 },
polarity: 'lowerIsBetter', drift: 1.4 },
{ label: 'Throughput', value: 412, unit: 't/h', digits: 0, target: 430,
thresholds: { advisory: 400, warning: 370, critical: 340, direction: 'below' },
polarity: 'higherIsBetter', drift: 9 }
];
function el(id) {
return document.getElementById(id);
}
window.onload = function () {
const strip = el('liveStrip'),
tiles = [];
TAGS.forEach(function (tag) {
const tile = document.createElement('smart-status-tile');
tile.label = tag.label;
tile.unit = tag.unit;
tile.precisionDigits = tag.digits;
if (tag.target !== undefined) {
tile.target = tag.target;
}
tile.trendPolarity = tag.polarity;
strip.appendChild(tile);
tiles.push({ tile: tile, tag: tag, previous: tag.value, history: [] });
});
function scan() {
tiles.forEach(function (entry) {
const tag = entry.tag,
next = Math.max(0, tag.value + (Math.random() - 0.5) * tag.drift * 2),
change = next - entry.previous;
tag.value = next;
entry.tile.value = next;
/* The tile classifies itself from the thresholds rather than the page picking
a colour - one rule, in one place, for every tile on the screen. */
entry.tile.state = window.Smart.StatusTile ?
window.Smart.StatusTile.stateFor(next, tag.thresholds) :
customElements.get('smart-status-tile').stateFor(next, tag.thresholds);
/* A trend of nothing is worse than no trend: below a threshold the reading is
noise, and an arrow that flickers every two seconds is unreadable. */
const meaningful = Math.abs(change) > tag.drift * 0.12;
entry.tile.trend = meaningful ? (change > 0 ? 'up' : 'down') : 'flat';
entry.tile.trendValue = meaningful ? change.toFixed(tag.digits) : null;
if (tag.history) {
entry.history.push(next);
if (entry.history.length > 24) {
entry.history.shift();
}
entry.tile.sparkline = entry.history.slice();
}
entry.previous = next;
});
}
scan();
setInterval(scan, 2000);
//A sparkline on a static tile, to show it is not tied to the live strip.
const spark = el('sparkTile');
if (spark) {
spark.sparkline = [58, 59.4, 61, 60.2, 62.5, 63.1, 62.8, 64, 63.4, 62.8];
}
['alarmTile', 'woTile'].forEach(function (id) {
el(id).addEventListener('activate', function (event) {
el('log').textContent = 'activate: ' + event.detail.label + ' = ' +
event.detail.value + ' (' + event.detail.state + ')';
});
});
//Show the classifier's answers, so the thresholds are not a black box.
const StatusTile = customElements.get('smart-status-tile'),
cpkThresholds = { advisory: 1.33, warning: 1.0, critical: 0.8, direction: 'below' },
lines = [1.62, 1.4, 1.33, 1.1, 0.95, 0.7, null].map(function (cpk) {
return 'stateFor(' + String(cpk).padStart(4) + ', {advisory: 1.33, warning: 1, ' +
'critical: 0.8, direction: \'below\'}) -> ' +
StatusTile.stateFor(cpk, cpkThresholds);
});
el('stateForOut').textContent = lines.join('\n');
const toggle = el('themeToggle');
toggle.addEventListener('click', function () {
const dark = document.body.getAttribute('theme') === 'industrial-dark';
document.body.setAttribute('theme', dark ? 'industrial' : 'industrial-dark');
toggle.textContent = dark ? 'Dark' : 'Light';
});
};
styles.css
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;
margin: 0;
padding: 16px;
}
.tile-demo {
max-width: 1120px;
}
.tile-demo-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.tile-demo h2 {
font-size: 17px;
font-weight: 600;
margin: 0 0 10px;
}
.tile-demo h3 {
font-size: 11px;
font-weight: 600;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--smart-text-tertiary-color);
margin: 26px 0 10px;
}
.tile-note {
font-size: 12px;
line-height: 1.6;
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 100ch;
}
.tile-note code {
font-family: Consolas, Monaco, monospace;
font-size: 11.5px;
color: var(--smart-industrial-accent-text);
}
/* Auto-fit rather than a fixed count: a KPI strip is read across a wall display and on a
laptop, and the tile should not care which. */
.tile-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.tile-strip smart-status-tile {
min-height: 104px;
}
.tile-button {
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;
height: 30px;
letter-spacing: .06em;
padding: 0 14px;
text-transform: uppercase;
}
.tile-button:focus-visible {
outline: 2px solid var(--smart-industrial-focus);
outline-offset: 2px;
box-shadow: 0 0 0 4px var(--smart-industrial-focus-halo);
}
.tile-code {
background: var(--smart-industrial-panel-background, var(--smart-surface));
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 3px;
color: var(--smart-text-color);
font-family: Consolas, Monaco, monospace;
font-size: 11.5px;
line-height: 1.6;
margin: 0;
overflow: auto;
padding: 10px 12px;
}
The same demo in Angular, generated from the HTML demo. The frameworks guide has the project setup.
angular/app.component.html
<div class="angular-root" theme="industrial">
<div class="tile-demo">
<header class="tile-demo-head">
<h2>Status tile</h2>
<button class="tile-button" id="themeToggle" type="button">Dark</button>
</header>
<p class="tile-note">
Smart.StatusTile shows one process value with its label, its unit, its state and its trend direction. A tile answers whether a value is normal at a glance; a gauge shows where a value is in its range and needs a scale and a needle.
</p>
<h3>A plant overview strip</h3>
<div class="tile-strip" id="liveStrip"></div>
<p class="tile-note" id="liveNote">Values update every two seconds; the state follows the thresholds.</p>
<h3>Trend polarity</h3>
<p class="tile-note">
An arrow pointing up can be good or bad depending on the quantity. The <code>trendPolarity</code> property tells the tile whether a rising value is good, bad or neutral, and the arrow is coloured accordingly.
</p>
<div class="tile-strip">
<smart-status-tile label="First pass yield" [value]="96.2" unit="%" state="ok"
trend="up" [trendValue]="1.4" trend-polarity="higherIsBetter"
footnote="Rising is good"></smart-status-tile>
<smart-status-tile label="Scrap rate" [value]="3.1" unit="%" state="warning"
trend="up" [trendValue]="0.9" trend-polarity="lowerIsBetter"
footnote="Rising is bad"></smart-status-tile>
<smart-status-tile label="Ambient" [value]="21.4" unit="°C" state="neutral"
trend="up" [trendValue]="0.3" trend-polarity="neutral"
footnote="Rising is neither"></smart-status-tile>
</div>
<h3>Content</h3>
<div class="tile-strip">
<smart-status-tile label="Line state" value="Running" state="ok"
footnote="A name, not a number"></smart-status-tile>
<smart-status-tile label="Batch" value="B-2261-04" state="neutral"
footnote="Long text wraps and shrinks"></smart-status-tile>
<smart-status-tile label="Feed pressure" state="unknown"
footnote="No reading at all"></smart-status-tile>
<smart-status-tile label="Vessel level" [value]="62.8" unit="%" state="advisory" [target]="60" id="sparkTile"></smart-status-tile>
</div>
<h3>Interactive tiles</h3>
<p class="tile-note">
The <code>interactive</code> property makes the tile a button: it can be focused with Tab, activated with Enter or Space, and is announced as a button by screen readers. It is off by default so that a tile that does nothing is not a tab stop.
</p>
<div class="tile-strip">
<smart-status-tile label="Open alarms" [value]="7" state="critical" [interactive]="true"
trend="up" [trendValue]="3" trend-polarity="lowerIsBetter"
footnote="Press to open the alarm list" id="alarmTile"></smart-status-tile>
<smart-status-tile label="Work orders" [value]="12" state="advisory" [interactive]="true"
footnote="Press to open the queue" id="woTile"></smart-status-tile>
</div>
<p class="tile-note" id="log">No tile pressed yet.</p>
<h3>Classifying a value</h3>
<p class="tile-note">
<code>StatusTile.stateFor(value, thresholds)</code> is a static method that returns the state for a value, so a screen can classify a value without a tile. Use <code>direction: 'below'</code> for quantities where a lower value is worse, such as capability indices, yields or remaining life.
</p>
<pre class="tile-code" id="stateForOut"></pre>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { StatusTileModule } from 'smart-industrial/angular/statustile';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - StatusTile in Angular.
*
* The template is the HTML demo's markup: the elements are the wrappers, so what is
* written on them is checked against their inputs. demo.ts drives them the way the
* plant's data would, once every element in the view has rendered.
*/
@Component({
selector: 'app-root',
standalone: true,
imports: [StatusTileModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent implements AfterViewInit, OnDestroy {
private unmounted = false;
ngAfterViewInit(): void {
const root = document.querySelector('.angular-root') as HTMLElement,
pending: Promise<void>[] = [];
root.querySelectorAll('*').forEach((element: any) => {
if (typeof element.whenRendered === 'function' && !element.isRendered) {
pending.push(element.whenRendered());
}
});
Promise.all(pending).then(() => {
if (!this.unmounted) {
runDemo();
}
});
}
ngOnDestroy(): void {
this.unmounted = true;
}
}
angular/demo.ts
// @ts-nocheck
/* The HTML demo's script, as it was written: page code that feeds the elements, not a typed
API. The types are on the wrappers in app.component.ts. */
declare const Smart: any;
/* Status tile overview.
*
* The live strip is built from a table of readings and thresholds rather than from
* markup, because that is how a real screen does it: the tags come from a configuration
* and the tile decides its own state from the numbers. Nothing here maps a value to a
* colour - StatusTile.stateFor does that, and it is the same call the screen would make
* to colour anything else. */
const TAGS = [
{ label: 'OEE', value: 78.4, unit: '%', digits: 1, target: 85,
thresholds: { advisory: 80, warning: 75, critical: 65, direction: 'below' },
polarity: 'higherIsBetter', drift: 0.9, history: true },
{ label: 'Feed pressure', value: 6.31, unit: 'bar', digits: 2, target: 6.2,
thresholds: { advisory: 7, warning: 8, critical: 9 },
polarity: 'neutral', drift: 0.09 },
{ label: 'Reactor temp', value: 84.2, unit: '°C', digits: 1,
thresholds: { advisory: 88, warning: 92, critical: 96 },
polarity: 'neutral', drift: 1.6 },
{ label: 'Open alarms', value: 4, unit: '', digits: 0,
thresholds: { advisory: 3, warning: 8, critical: 15 },
polarity: 'lowerIsBetter', drift: 1.4 },
{ label: 'Throughput', value: 412, unit: 't/h', digits: 0, target: 430,
thresholds: { advisory: 400, warning: 370, critical: 340, direction: 'below' },
polarity: 'higherIsBetter', drift: 9 }
];
function el(id) {
return document.getElementById(id);
}
export function runDemo(): void {
const strip = el('liveStrip'),
tiles = [];
TAGS.forEach(function (tag) {
const tile = document.createElement('smart-status-tile');
tile.label = tag.label;
tile.unit = tag.unit;
tile.precisionDigits = tag.digits;
if (tag.target !== undefined) {
tile.target = tag.target;
}
tile.trendPolarity = tag.polarity;
strip.appendChild(tile);
tiles.push({ tile: tile, tag: tag, previous: tag.value, history: [] });
});
function scan() {
tiles.forEach(function (entry) {
const tag = entry.tag,
next = Math.max(0, tag.value + (Math.random() - 0.5) * tag.drift * 2),
change = next - entry.previous;
tag.value = next;
entry.tile.value = next;
/* The tile classifies itself from the thresholds rather than the page picking
a colour - one rule, in one place, for every tile on the screen. */
entry.tile.state = window.Smart.StatusTile ?
window.Smart.StatusTile.stateFor(next, tag.thresholds) :
customElements.get('smart-status-tile').stateFor(next, tag.thresholds);
/* A trend of nothing is worse than no trend: below a threshold the reading is
noise, and an arrow that flickers every two seconds is unreadable. */
const meaningful = Math.abs(change) > tag.drift * 0.12;
entry.tile.trend = meaningful ? (change > 0 ? 'up' : 'down') : 'flat';
entry.tile.trendValue = meaningful ? change.toFixed(tag.digits) : null;
if (tag.history) {
entry.history.push(next);
if (entry.history.length > 24) {
entry.history.shift();
}
entry.tile.sparkline = entry.history.slice();
}
entry.previous = next;
});
}
scan();
setInterval(scan, 2000);
//A sparkline on a static tile, to show it is not tied to the live strip.
const spark = el('sparkTile');
if (spark) {
spark.sparkline = [58, 59.4, 61, 60.2, 62.5, 63.1, 62.8, 64, 63.4, 62.8];
}
['alarmTile', 'woTile'].forEach(function (id) {
el(id).addEventListener('activate', function (event) {
el('log').textContent = 'activate: ' + event.detail.label + ' = ' +
event.detail.value + ' (' + event.detail.state + ')';
});
});
//Show the classifier's answers, so the thresholds are not a black box.
const StatusTile = customElements.get('smart-status-tile'),
cpkThresholds = { advisory: 1.33, warning: 1.0, critical: 0.8, direction: 'below' },
lines = [1.62, 1.4, 1.33, 1.1, 0.95, 0.7, null].map(function (cpk) {
return 'stateFor(' + String(cpk).padStart(4) + ', {advisory: 1.33, warning: 1, ' +
'critical: 0.8, direction: \'below\'}) -> ' +
StatusTile.stateFor(cpk, cpkThresholds);
});
el('stateForOut').textContent = lines.join('\n');
const toggle = el('themeToggle');
toggle.addEventListener('click', function () {
const dark = document.body.getAttribute('theme') === 'industrial-dark';
document.body.setAttribute('theme', dark ? 'industrial' : 'industrial-dark');
toggle.textContent = dark ? 'Dark' : 'Light';
});
}
angular/app.component.css
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;
margin: 0;
padding: 16px;
}
.tile-demo {
max-width: 1120px;
}
.tile-demo-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.tile-demo h2 {
font-size: 17px;
font-weight: 600;
margin: 0 0 10px;
}
.tile-demo h3 {
font-size: 11px;
font-weight: 600;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--smart-text-tertiary-color);
margin: 26px 0 10px;
}
.tile-note {
font-size: 12px;
line-height: 1.6;
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 100ch;
}
.tile-note code {
font-family: Consolas, Monaco, monospace;
font-size: 11.5px;
color: var(--smart-industrial-accent-text);
}
/* Auto-fit rather than a fixed count: a KPI strip is read across a wall display and on a
laptop, and the tile should not care which. */
.tile-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.tile-strip smart-status-tile {
min-height: 104px;
}
.tile-button {
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;
height: 30px;
letter-spacing: .06em;
padding: 0 14px;
text-transform: uppercase;
}
.tile-button:focus-visible {
outline: 2px solid var(--smart-industrial-focus);
outline-offset: 2px;
box-shadow: 0 0 0 4px var(--smart-industrial-focus-halo);
}
.tile-code {
background: var(--smart-industrial-panel-background, var(--smart-surface));
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 3px;
color: var(--smart-text-color);
font-family: Consolas, Monaco, monospace;
font-size: 11.5px;
line-height: 1.6;
margin: 0;
overflow: auto;
padding: 10px 12px;
}
The same demo in React, generated from the HTML demo. The frameworks guide has the project setup.
react/App.jsx
import 'smart-industrial/source/styles/smart.default.css';
import 'smart-industrial/source/styles/smart.industrial-elements.css';
import 'smart-industrial/source/styles/smart.industrial.css';
import 'smart-industrial/source/styles/smart.industrial-dark.css';
import './App.css';
import React from 'react';
import { StatusTile } from 'smart-industrial/react/statustile';
const Smart = window.Smart;
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* Status tile overview.
*
* The live strip is built from a table of readings and thresholds rather than from
* markup, because that is how a real screen does it: the tags come from a configuration
* and the tile decides its own state from the numbers. Nothing here maps a value to a
* colour - StatusTile.stateFor does that, and it is the same call the screen would make
* to colour anything else. */
const TAGS = [
{ label: 'OEE', value: 78.4, unit: '%', digits: 1, target: 85,
thresholds: { advisory: 80, warning: 75, critical: 65, direction: 'below' },
polarity: 'higherIsBetter', drift: 0.9, history: true },
{ label: 'Feed pressure', value: 6.31, unit: 'bar', digits: 2, target: 6.2,
thresholds: { advisory: 7, warning: 8, critical: 9 },
polarity: 'neutral', drift: 0.09 },
{ label: 'Reactor temp', value: 84.2, unit: '°C', digits: 1,
thresholds: { advisory: 88, warning: 92, critical: 96 },
polarity: 'neutral', drift: 1.6 },
{ label: 'Open alarms', value: 4, unit: '', digits: 0,
thresholds: { advisory: 3, warning: 8, critical: 15 },
polarity: 'lowerIsBetter', drift: 1.4 },
{ label: 'Throughput', value: 412, unit: 't/h', digits: 0, target: 430,
thresholds: { advisory: 400, warning: 370, critical: 340, direction: 'below' },
polarity: 'higherIsBetter', drift: 9 }
];
function el(id) {
return document.getElementById(id);
}
const App = () => {
const root = React.useRef(null);
React.useEffect(() => {
/* The elements render after React has mounted them, and the demo's logic - written
for a page's load event - reads their parts, so it waits for every element in the
tree to have rendered first. */
const pending = Array.from(root.current.querySelectorAll('*'))
.filter((element) => typeof element.whenRendered === 'function' && !element.isRendered)
.map((element) => element.whenRendered());
let unmounted = false;
Promise.all(pending).then(() => {
if (unmounted) {
return;
}
const strip = el('liveStrip'),
tiles = [];
TAGS.forEach(function (tag) {
const tile = document.createElement('smart-status-tile');
tile.label = tag.label;
tile.unit = tag.unit;
tile.precisionDigits = tag.digits;
if (tag.target !== undefined) {
tile.target = tag.target;
}
tile.trendPolarity = tag.polarity;
strip.appendChild(tile);
tiles.push({ tile: tile, tag: tag, previous: tag.value, history: [] });
});
function scan() {
tiles.forEach(function (entry) {
const tag = entry.tag,
next = Math.max(0, tag.value + (Math.random() - 0.5) * tag.drift * 2),
change = next - entry.previous;
tag.value = next;
entry.tile.value = next;
/* The tile classifies itself from the thresholds rather than the page picking
a colour - one rule, in one place, for every tile on the screen. */
entry.tile.state = window.Smart.StatusTile ?
window.Smart.StatusTile.stateFor(next, tag.thresholds) :
customElements.get('smart-status-tile').stateFor(next, tag.thresholds);
/* A trend of nothing is worse than no trend: below a threshold the reading is
noise, and an arrow that flickers every two seconds is unreadable. */
const meaningful = Math.abs(change) > tag.drift * 0.12;
entry.tile.trend = meaningful ? (change > 0 ? 'up' : 'down') : 'flat';
entry.tile.trendValue = meaningful ? change.toFixed(tag.digits) : null;
if (tag.history) {
entry.history.push(next);
if (entry.history.length > 24) {
entry.history.shift();
}
entry.tile.sparkline = entry.history.slice();
}
entry.previous = next;
});
}
scan();
setInterval(scan, 2000);
//A sparkline on a static tile, to show it is not tied to the live strip.
const spark = el('sparkTile');
if (spark) {
spark.sparkline = [58, 59.4, 61, 60.2, 62.5, 63.1, 62.8, 64, 63.4, 62.8];
}
['alarmTile', 'woTile'].forEach(function (id) {
el(id).addEventListener('activate', function (event) {
el('log').textContent = 'activate: ' + event.detail.label + ' = ' +
event.detail.value + ' (' + event.detail.state + ')';
});
});
//Show the classifier's answers, so the thresholds are not a black box.
const StatusTile = customElements.get('smart-status-tile'),
cpkThresholds = { advisory: 1.33, warning: 1.0, critical: 0.8, direction: 'below' },
lines = [1.62, 1.4, 1.33, 1.1, 0.95, 0.7, null].map(function (cpk) {
return 'stateFor(' + String(cpk).padStart(4) + ', {advisory: 1.33, warning: 1, ' +
'critical: 0.8, direction: \'below\'}) -> ' +
StatusTile.stateFor(cpk, cpkThresholds);
});
el('stateForOut').textContent = lines.join('\n');
const toggle = el('themeToggle');
toggle.addEventListener('click', function () {
const dark = document.body.getAttribute('theme') === 'industrial-dark';
document.body.setAttribute('theme', dark ? 'industrial' : 'industrial-dark');
toggle.textContent = dark ? 'Dark' : 'Light';
});
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="tile-demo">
<header className="tile-demo-head">
<h2>Status tile</h2>
<button className="tile-button" id="themeToggle" type="button">Dark</button>
</header>
<p className="tile-note">Smart.StatusTile shows one process value with its label, its unit, its state and its trend direction. A tile answers whether a value is normal at a glance; a gauge shows where a value is in its range and needs a scale and a needle.</p>
<h3>A plant overview strip</h3>
<div className="tile-strip" id="liveStrip"></div>
<p className="tile-note" id="liveNote">Values update every two seconds; the state follows the thresholds.</p>
<h3>Trend polarity</h3>
<p className="tile-note">An arrow pointing up can be good or bad depending on the quantity. The <code>trendPolarity</code> property tells the tile whether a rising value is good, bad or neutral, and the arrow is coloured accordingly.</p>
<div className="tile-strip">
<StatusTile label="First pass yield" value={96.2} unit="%" state="ok" trend="up" trendValue={1.4} trendPolarity="higherIsBetter" footnote="Rising is good"></StatusTile>
<StatusTile label="Scrap rate" value={3.1} unit="%" state="warning" trend="up" trendValue={0.9} trendPolarity="lowerIsBetter" footnote="Rising is bad"></StatusTile>
<StatusTile label="Ambient" value={21.4} unit="°C" state="neutral" trend="up" trendValue={0.3} trendPolarity="neutral" footnote="Rising is neither"></StatusTile>
</div>
<h3>Content</h3>
<div className="tile-strip">
<StatusTile label="Line state" value="Running" state="ok" footnote="A name, not a number"></StatusTile>
<StatusTile label="Batch" value="B-2261-04" state="neutral" footnote="Long text wraps and shrinks"></StatusTile>
<StatusTile label="Feed pressure" state="unknown" footnote="No reading at all"></StatusTile>
<StatusTile label="Vessel level" value={62.8} unit="%" state="advisory" target={60} id="sparkTile"></StatusTile>
</div>
<h3>Interactive tiles</h3>
<p className="tile-note">The <code>interactive</code> property makes the tile a button: it can be focused with Tab, activated with Enter or Space, and is announced as a button by screen readers. It is off by default so that a tile that does nothing is not a tab stop.</p>
<div className="tile-strip">
<StatusTile label="Open alarms" value={7} state="critical" interactive trend="up" trendValue={3} trendPolarity="lowerIsBetter" footnote="Press to open the alarm list" id="alarmTile"></StatusTile>
<StatusTile label="Work orders" value={12} state="advisory" interactive footnote="Press to open the queue" id="woTile"></StatusTile>
</div>
<p className="tile-note" id="log">No tile pressed yet.</p>
<h3>Classifying a value</h3>
<p className="tile-note"><code>StatusTile.stateFor(value, thresholds)</code> is a static method that returns the state for a value, so a screen can classify a value without a tile. Use <code>direction: 'below'</code> for quantities where a lower value is worse, such as capability indices, yields or remaining life.</p>
<pre className="tile-code" id="stateForOut">{""}</pre>
</div>
</div>
);
};
export default App;
react/App.css
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;
margin: 0;
padding: 16px;
}
.tile-demo {
max-width: 1120px;
}
.tile-demo-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.tile-demo h2 {
font-size: 17px;
font-weight: 600;
margin: 0 0 10px;
}
.tile-demo h3 {
font-size: 11px;
font-weight: 600;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--smart-text-tertiary-color);
margin: 26px 0 10px;
}
.tile-note {
font-size: 12px;
line-height: 1.6;
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 100ch;
}
.tile-note code {
font-family: Consolas, Monaco, monospace;
font-size: 11.5px;
color: var(--smart-industrial-accent-text);
}
/* Auto-fit rather than a fixed count: a KPI strip is read across a wall display and on a
laptop, and the tile should not care which. */
.tile-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.tile-strip smart-status-tile {
min-height: 104px;
}
.tile-button {
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;
height: 30px;
letter-spacing: .06em;
padding: 0 14px;
text-transform: uppercase;
}
.tile-button:focus-visible {
outline: 2px solid var(--smart-industrial-focus);
outline-offset: 2px;
box-shadow: 0 0 0 4px var(--smart-industrial-focus-halo);
}
.tile-code {
background: var(--smart-industrial-panel-background, var(--smart-surface));
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 3px;
color: var(--smart-text-color);
font-family: Consolas, Monaco, monospace;
font-size: 11.5px;
line-height: 1.6;
margin: 0;
overflow: auto;
padding: 10px 12px;
}
The same demo in Vue, generated from the HTML demo. The frameworks guide has the project setup.
vue/App.vue
<template>
<div class="vue-root" theme="industrial" v-pre>
<div class="tile-demo">
<header class="tile-demo-head">
<h2>Status tile</h2>
<button class="tile-button" id="themeToggle" type="button">Dark</button>
</header>
<p class="tile-note">
Smart.StatusTile shows one process value with its label, its unit, its state and its trend direction. A tile answers whether a value is normal at a glance; a gauge shows where a value is in its range and needs a scale and a needle.
</p>
<h3>A plant overview strip</h3>
<div class="tile-strip" id="liveStrip"></div>
<p class="tile-note" id="liveNote">Values update every two seconds; the state follows the thresholds.</p>
<h3>Trend polarity</h3>
<p class="tile-note">
An arrow pointing up can be good or bad depending on the quantity. The <code>trendPolarity</code> property tells the tile whether a rising value is good, bad or neutral, and the arrow is coloured accordingly.
</p>
<div class="tile-strip">
<smart-status-tile label="First pass yield" value="96.2" unit="%" state="ok"
trend="up" trend-value="1.4" trend-polarity="higherIsBetter"
footnote="Rising is good"></smart-status-tile>
<smart-status-tile label="Scrap rate" value="3.1" unit="%" state="warning"
trend="up" trend-value="0.9" trend-polarity="lowerIsBetter"
footnote="Rising is bad"></smart-status-tile>
<smart-status-tile label="Ambient" value="21.4" unit="°C" state="neutral"
trend="up" trend-value="0.3" trend-polarity="neutral"
footnote="Rising is neither"></smart-status-tile>
</div>
<h3>Content</h3>
<div class="tile-strip">
<smart-status-tile label="Line state" value="Running" state="ok"
footnote="A name, not a number"></smart-status-tile>
<smart-status-tile label="Batch" value="B-2261-04" state="neutral"
footnote="Long text wraps and shrinks"></smart-status-tile>
<smart-status-tile label="Feed pressure" state="unknown"
footnote="No reading at all"></smart-status-tile>
<smart-status-tile label="Vessel level" value="62.8" unit="%" state="advisory"
target="60" id="sparkTile"></smart-status-tile>
</div>
<h3>Interactive tiles</h3>
<p class="tile-note">
The <code>interactive</code> property makes the tile a button: it can be focused with Tab, activated with Enter or Space, and is announced as a button by screen readers. It is off by default so that a tile that does nothing is not a tab stop.
</p>
<div class="tile-strip">
<smart-status-tile label="Open alarms" value="7" state="critical" interactive
trend="up" trend-value="3" trend-polarity="lowerIsBetter"
footnote="Press to open the alarm list" id="alarmTile"></smart-status-tile>
<smart-status-tile label="Work orders" value="12" state="advisory" interactive
footnote="Press to open the queue" id="woTile"></smart-status-tile>
</div>
<p class="tile-note" id="log">No tile pressed yet.</p>
<h3>Classifying a value</h3>
<p class="tile-note">
<code>StatusTile.stateFor(value, thresholds)</code> is a static method that returns the state for a value, so a screen can classify a value without a tile. Use <code>direction: 'below'</code> for quantities where a lower value is worse, such as capability indices, yields or remaining life.
</p>
<pre class="tile-code" id="stateForOut"></pre>
</div>
</div>
</template>
<script>
import { onMounted } from "vue";
import "smart-industrial/source/styles/smart.default.css";
import "smart-industrial/source/styles/smart.industrial-elements.css";
import "smart-industrial/source/styles/smart.industrial.css";
import "smart-industrial/source/styles/smart.industrial-dark.css";
import "smart-industrial/source/modules/smart.statustile.js";
const Smart = window.Smart;
/* Status tile overview.
*
* The live strip is built from a table of readings and thresholds rather than from
* markup, because that is how a real screen does it: the tags come from a configuration
* and the tile decides its own state from the numbers. Nothing here maps a value to a
* colour - StatusTile.stateFor does that, and it is the same call the screen would make
* to colour anything else. */
const TAGS = [
{ label: 'OEE', value: 78.4, unit: '%', digits: 1, target: 85,
thresholds: { advisory: 80, warning: 75, critical: 65, direction: 'below' },
polarity: 'higherIsBetter', drift: 0.9, history: true },
{ label: 'Feed pressure', value: 6.31, unit: 'bar', digits: 2, target: 6.2,
thresholds: { advisory: 7, warning: 8, critical: 9 },
polarity: 'neutral', drift: 0.09 },
{ label: 'Reactor temp', value: 84.2, unit: '°C', digits: 1,
thresholds: { advisory: 88, warning: 92, critical: 96 },
polarity: 'neutral', drift: 1.6 },
{ label: 'Open alarms', value: 4, unit: '', digits: 0,
thresholds: { advisory: 3, warning: 8, critical: 15 },
polarity: 'lowerIsBetter', drift: 1.4 },
{ label: 'Throughput', value: 412, unit: 't/h', digits: 0, target: 430,
thresholds: { advisory: 400, warning: 370, critical: 340, direction: 'below' },
polarity: 'higherIsBetter', drift: 9 }
];
function el(id) {
return document.getElementById(id);
}
export default {
name: "app",
setup() {
onMounted(() => {
/* The elements render after Vue has mounted them, and the demo's logic - written for
a page's load event - reads their parts, so it waits for every element in the tree
to have rendered first. */
const pending = Array.from(document.querySelector(".vue-root").querySelectorAll("*"))
.filter((element) => typeof element.whenRendered === "function" && !element.isRendered)
.map((element) => element.whenRendered());
Promise.all(pending).then(() => {
const strip = el('liveStrip'),
tiles = [];
TAGS.forEach(function (tag) {
const tile = document.createElement('smart-status-tile');
tile.label = tag.label;
tile.unit = tag.unit;
tile.precisionDigits = tag.digits;
if (tag.target !== undefined) {
tile.target = tag.target;
}
tile.trendPolarity = tag.polarity;
strip.appendChild(tile);
tiles.push({ tile: tile, tag: tag, previous: tag.value, history: [] });
});
function scan() {
tiles.forEach(function (entry) {
const tag = entry.tag,
next = Math.max(0, tag.value + (Math.random() - 0.5) * tag.drift * 2),
change = next - entry.previous;
tag.value = next;
entry.tile.value = next;
/* The tile classifies itself from the thresholds rather than the page picking
a colour - one rule, in one place, for every tile on the screen. */
entry.tile.state = window.Smart.StatusTile ?
window.Smart.StatusTile.stateFor(next, tag.thresholds) :
customElements.get('smart-status-tile').stateFor(next, tag.thresholds);
/* A trend of nothing is worse than no trend: below a threshold the reading is
noise, and an arrow that flickers every two seconds is unreadable. */
const meaningful = Math.abs(change) > tag.drift * 0.12;
entry.tile.trend = meaningful ? (change > 0 ? 'up' : 'down') : 'flat';
entry.tile.trendValue = meaningful ? change.toFixed(tag.digits) : null;
if (tag.history) {
entry.history.push(next);
if (entry.history.length > 24) {
entry.history.shift();
}
entry.tile.sparkline = entry.history.slice();
}
entry.previous = next;
});
}
scan();
setInterval(scan, 2000);
//A sparkline on a static tile, to show it is not tied to the live strip.
const spark = el('sparkTile');
if (spark) {
spark.sparkline = [58, 59.4, 61, 60.2, 62.5, 63.1, 62.8, 64, 63.4, 62.8];
}
['alarmTile', 'woTile'].forEach(function (id) {
el(id).addEventListener('activate', function (event) {
el('log').textContent = 'activate: ' + event.detail.label + ' = ' +
event.detail.value + ' (' + event.detail.state + ')';
});
});
//Show the classifier's answers, so the thresholds are not a black box.
const StatusTile = customElements.get('smart-status-tile'),
cpkThresholds = { advisory: 1.33, warning: 1.0, critical: 0.8, direction: 'below' },
lines = [1.62, 1.4, 1.33, 1.1, 0.95, 0.7, null].map(function (cpk) {
return 'stateFor(' + String(cpk).padStart(4) + ', {advisory: 1.33, warning: 1, ' +
'critical: 0.8, direction: \'below\'}) -> ' +
StatusTile.stateFor(cpk, cpkThresholds);
});
el('stateForOut').textContent = lines.join('\n');
const toggle = el('themeToggle');
toggle.addEventListener('click', function () {
const dark = document.body.getAttribute('theme') === 'industrial-dark';
document.body.setAttribute('theme', dark ? 'industrial' : 'industrial-dark');
toggle.textContent = dark ? 'Dark' : 'Light';
});
});
});
}
};
</script>
<style>
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;
margin: 0;
padding: 16px;
}
.tile-demo {
max-width: 1120px;
}
.tile-demo-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.tile-demo h2 {
font-size: 17px;
font-weight: 600;
margin: 0 0 10px;
}
.tile-demo h3 {
font-size: 11px;
font-weight: 600;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--smart-text-tertiary-color);
margin: 26px 0 10px;
}
.tile-note {
font-size: 12px;
line-height: 1.6;
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 100ch;
}
.tile-note code {
font-family: Consolas, Monaco, monospace;
font-size: 11.5px;
color: var(--smart-industrial-accent-text);
}
/* Auto-fit rather than a fixed count: a KPI strip is read across a wall display and on a
laptop, and the tile should not care which. */
.tile-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.tile-strip smart-status-tile {
min-height: 104px;
}
.tile-button {
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;
height: 30px;
letter-spacing: .06em;
padding: 0 14px;
text-transform: uppercase;
}
.tile-button:focus-visible {
outline: 2px solid var(--smart-industrial-focus);
outline-offset: 2px;
box-shadow: 0 0 0 4px var(--smart-industrial-focus-halo);
}
.tile-code {
background: var(--smart-industrial-panel-background, var(--smart-surface));
border: 1px solid var(--smart-industrial-panel-border, var(--smart-border));
border-radius: 3px;
color: var(--smart-text-color);
font-family: Consolas, Monaco, monospace;
font-size: 11.5px;
line-height: 1.6;
margin: 0;
overflow: auto;
padding: 10px 12px;
}
</style>
What the demo shows
- Status tile
- A plant overview strip
- Trend polarity
- Content
- Interactive tiles
- Classifying a value
On the operator screens
Alarm PerformanceAndonBatchConnectivityCopilotKPI WallLoginMaintenanceMobile SupervisorPackaging OeePump StationReportsShift HandoverUnitsVibration Analysis