Component demo · Production floor
State Timeline
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>StateTimeline Overview Demo</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="smart-demo-container">
<section id="overview">
<h2>Smart.StateTimeline displays the state history of each machine on a production line along a time axis.</h2>
<p>
Each row is a machine and each segment is a state (running, starved, blocked, stopped, fault, changeover) drawn for the time it lasted, with the reason recorded by the plant. The open segment at the right is the current state. The summary under each machine name shows the time spent in each state over the displayed window. Compared with a downtime Pareto chart, the timeline shows when each state occurred, in what order, and what the neighbouring machines were doing at the same time.
</p>
</section>
<section>
<h2>Line 2, current shift</h2>
<smart-state-timeline id="line" label="Line 2" show-summary></smart-state-timeline>
<div class="demo-actions">
<button type="button" id="feed">Pause the feed</button>
<button type="button" id="summary">Hide the summaries</button>
<label>Window <select id="window">
<option value="14400000">4 h</option>
<option value="28800000" selected>8 h</option>
<option value="43200000">12 h</option>
</select></label>
</div>
<p class="demo-log" id="log">Use Tab to focus the timeline. The left and right arrow keys move along a row, the up and down arrow keys move to the machine above or below at the same time, and Enter opens a segment.</p>
</section>
<section>
<h2>Custom states</h2>
<p>
The <code>states</code> property defines the names and colours of the states recorded by the plant. A state that is not in the list is drawn in a neutral colour with its own id, so a new state from the controller is still visible.
</p>
<smart-state-timeline id="furnace" label="Furnace 1" show-reasons></smart-state-timeline>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.statetimeline.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" />
/* State timeline overview.
*
* The record is written by hand here in the shape a historian's state table takes: one
* row per machine, each a list of { state, from, to, reason }. A plant's comes from its
* historian or MES as JSON and is assigned to `rows` unchanged; state changes after that
* arrive through push(). */
window.onload = function () {
const line = document.getElementById('line'),
furnace = document.getElementById('furnace'),
feedButton = document.getElementById('feed'),
summaryButton = document.getElementById('summary'),
windowSelect = document.getElementById('window'),
log = document.getElementById('log'),
MIN = 60000,
H = 3600000,
now = Date.now(),
start = now - 8 * H;
/* ── The shift so far ───────────────────────────────────────────────── */
function at(minutes) {
return start + minutes * MIN;
}
line.rows = [
{ id: 'filler', label: 'Filler', segments: [
{ state: 'changeover', from: at(-20), to: at(35), reason: 'Format 500 ml → 1 l' },
{ state: 'running', from: at(35), to: at(160) },
{ state: 'fault', from: at(160), to: at(196), reason: 'Jam at infeed star' },
{ state: 'running', from: at(196), to: at(300) },
{ state: 'starved', from: at(300), to: at(318), reason: 'Bottle supply' },
{ state: 'running', from: at(318) }
] },
{ id: 'capper', label: 'Capper', segments: [
{ state: 'changeover', from: at(-20), to: at(38) },
{ state: 'running', from: at(38), to: at(163) },
{ state: 'starved', from: at(163), to: at(199), reason: 'Filler fault' },
{ state: 'running', from: at(199), to: at(250) },
{ state: 'fault', from: at(250), to: at(262), reason: 'Cap feeder' },
{ state: 'running', from: at(262) }
] },
{ id: 'labeller', label: 'Labeller', segments: [
{ state: 'changeover', from: at(-20), to: at(41) },
{ state: 'running', from: at(41), to: at(166) },
{ state: 'starved', from: at(166), to: at(203), reason: 'Upstream' },
{ state: 'running', from: at(203), to: at(390) },
{ state: 'blocked', from: at(390), to: at(412), reason: 'Packer full' },
{ state: 'running', from: at(412) }
] },
{ id: 'packer', label: 'Case packer', segments: [
{ state: 'maintenance', from: at(-60), to: at(45), reason: 'Planned: gripper service' },
{ state: 'running', from: at(45), to: at(170) },
{ state: 'starved', from: at(170), to: at(206) },
{ state: 'running', from: at(206), to: at(388) },
{ state: 'fault', from: at(388), to: at(414), reason: 'Glue unit temperature' },
{ state: 'running', from: at(414) }
] }
];
/* ── The feed: a state change somewhere on the line every few seconds ─ */
const ids = ['filler', 'capper', 'labeller', 'packer'],
stories = [
{ state: 'starved', reason: 'Upstream' },
{ state: 'running' },
{ state: 'blocked', reason: 'Downstream full' },
{ state: 'running' },
{ state: 'fault', reason: 'Guard door' },
{ state: 'running' }
];
let step = 0,
timer = null;
function tick() {
const id = ids[step % ids.length],
story = stories[(step * 7) % stories.length];
line.push(id, { state: story.state, from: Date.now(), reason: story.reason });
step++;
}
function startFeed() {
timer = setInterval(tick, 5000);
feedButton.textContent = 'Pause the feed';
}
feedButton.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
feedButton.textContent = 'Resume the feed';
}
else {
startFeed();
}
});
summaryButton.addEventListener('click', function () {
line.showSummary = !line.showSummary;
summaryButton.textContent = line.showSummary ? 'Hide the summaries' : 'Show the summaries';
});
windowSelect.addEventListener('change', function () {
line.timeSpan = Number(windowSelect.value);
});
line.addEventListener('segmentClick', function (event) {
const segment = event.detail.segment,
minutes = Math.round((event.detail.to - event.detail.from) / MIN);
log.textContent = event.detail.row + ': ' + event.detail.state + ' for ' + minutes + ' min' + (segment.reason ? ' - ' + segment.reason : '') + '. A screen would open the downtime record here.';
});
startFeed();
/* ── The plant's vocabulary ─────────────────────────────────────────── */
furnace.states = [
{ id: 'heating', label: 'Heating', color: 'warning' },
{ id: 'soak', label: 'Soak', color: 'ok' },
{ id: 'cooling', label: 'Cooling', color: 'accent' },
{ id: 'load', label: 'Load / unload', color: 'neutral' },
{ id: 'trip', label: 'Trip', color: 'critical' }
];
furnace.timeSpan = 12 * H;
furnace.segments = [
{ state: 'load', from: now - 12 * H, to: now - 11.3 * H },
{ state: 'heating', from: now - 11.3 * H, to: now - 9.6 * H, reason: 'Ramp 5 °C/min' },
{ state: 'soak', from: now - 9.6 * H, to: now - 6.4 * H, reason: '850 °C' },
{ state: 'cooling', from: now - 6.4 * H, to: now - 4.1 * H },
{ state: 'load', from: now - 4.1 * H, to: now - 3.4 * H },
{ state: 'heating', from: now - 3.4 * H, to: now - 2.2 * H },
{ state: 'trip', from: now - 2.2 * H, to: now - 1.9 * H, reason: 'Burner flame failure' },
{ state: 'purge', from: now - 1.9 * H, to: now - 1.6 * H, reason: 'Purge before relight' },
{ state: 'heating', from: now - 1.6 * H, to: now - 0.5 * H },
{ state: 'soak', from: now - 0.5 * H }
];
};
styles.css
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions 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;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
smart-state-timeline {
--smart-timeline-label-width: 230px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
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="smart-demo-container">
<section id="overview">
<h2>Smart.StateTimeline displays the state history of each machine on a production line along a time axis.</h2>
<p>
Each row is a machine and each segment is a state (running, starved, blocked, stopped, fault, changeover) drawn for the time it lasted, with the reason recorded by the plant. The open segment at the right is the current state. The summary under each machine name shows the time spent in each state over the displayed window. Compared with a downtime Pareto chart, the timeline shows when each state occurred, in what order, and what the neighbouring machines were doing at the same time.
</p>
</section>
<section>
<h2>Line 2, current shift</h2>
<smart-state-timeline id="line" label="Line 2" [showSummary]="true"></smart-state-timeline>
<div class="demo-actions">
<button type="button" id="feed">Pause the feed</button>
<button type="button" id="summary">Hide the summaries</button>
<label>Window <select id="window">
<option value="14400000">4 h</option>
<option value="28800000" selected>8 h</option>
<option value="43200000">12 h</option>
</select></label>
</div>
<p class="demo-log" id="log">Use Tab to focus the timeline. The left and right arrow keys move along a row, the up and down arrow keys move to the machine above or below at the same time, and Enter opens a segment.</p>
</section>
<section>
<h2>Custom states</h2>
<p>
The <code>states</code> property defines the names and colours of the states recorded by the plant. A state that is not in the list is drawn in a neutral colour with its own id, so a new state from the controller is still visible.
</p>
<smart-state-timeline id="furnace" label="Furnace 1" [showReasons]="true"></smart-state-timeline>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { StateTimelineModule } from 'smart-industrial/angular/statetimeline';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - StateTimeline 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: [StateTimelineModule],
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. */
/* State timeline overview.
*
* The record is written by hand here in the shape a historian's state table takes: one
* row per machine, each a list of { state, from, to, reason }. A plant's comes from its
* historian or MES as JSON and is assigned to `rows` unchanged; state changes after that
* arrive through push(). */
export function runDemo(): void {
const line = document.getElementById('line'),
furnace = document.getElementById('furnace'),
feedButton = document.getElementById('feed'),
summaryButton = document.getElementById('summary'),
windowSelect = document.getElementById('window'),
log = document.getElementById('log'),
MIN = 60000,
H = 3600000,
now = Date.now(),
start = now - 8 * H;
/* ── The shift so far ───────────────────────────────────────────────── */
function at(minutes) {
return start + minutes * MIN;
}
line.rows = [
{ id: 'filler', label: 'Filler', segments: [
{ state: 'changeover', from: at(-20), to: at(35), reason: 'Format 500 ml → 1 l' },
{ state: 'running', from: at(35), to: at(160) },
{ state: 'fault', from: at(160), to: at(196), reason: 'Jam at infeed star' },
{ state: 'running', from: at(196), to: at(300) },
{ state: 'starved', from: at(300), to: at(318), reason: 'Bottle supply' },
{ state: 'running', from: at(318) }
] },
{ id: 'capper', label: 'Capper', segments: [
{ state: 'changeover', from: at(-20), to: at(38) },
{ state: 'running', from: at(38), to: at(163) },
{ state: 'starved', from: at(163), to: at(199), reason: 'Filler fault' },
{ state: 'running', from: at(199), to: at(250) },
{ state: 'fault', from: at(250), to: at(262), reason: 'Cap feeder' },
{ state: 'running', from: at(262) }
] },
{ id: 'labeller', label: 'Labeller', segments: [
{ state: 'changeover', from: at(-20), to: at(41) },
{ state: 'running', from: at(41), to: at(166) },
{ state: 'starved', from: at(166), to: at(203), reason: 'Upstream' },
{ state: 'running', from: at(203), to: at(390) },
{ state: 'blocked', from: at(390), to: at(412), reason: 'Packer full' },
{ state: 'running', from: at(412) }
] },
{ id: 'packer', label: 'Case packer', segments: [
{ state: 'maintenance', from: at(-60), to: at(45), reason: 'Planned: gripper service' },
{ state: 'running', from: at(45), to: at(170) },
{ state: 'starved', from: at(170), to: at(206) },
{ state: 'running', from: at(206), to: at(388) },
{ state: 'fault', from: at(388), to: at(414), reason: 'Glue unit temperature' },
{ state: 'running', from: at(414) }
] }
];
/* ── The feed: a state change somewhere on the line every few seconds ─ */
const ids = ['filler', 'capper', 'labeller', 'packer'],
stories = [
{ state: 'starved', reason: 'Upstream' },
{ state: 'running' },
{ state: 'blocked', reason: 'Downstream full' },
{ state: 'running' },
{ state: 'fault', reason: 'Guard door' },
{ state: 'running' }
];
let step = 0,
timer = null;
function tick() {
const id = ids[step % ids.length],
story = stories[(step * 7) % stories.length];
line.push(id, { state: story.state, from: Date.now(), reason: story.reason });
step++;
}
function startFeed() {
timer = setInterval(tick, 5000);
feedButton.textContent = 'Pause the feed';
}
feedButton.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
feedButton.textContent = 'Resume the feed';
}
else {
startFeed();
}
});
summaryButton.addEventListener('click', function () {
line.showSummary = !line.showSummary;
summaryButton.textContent = line.showSummary ? 'Hide the summaries' : 'Show the summaries';
});
windowSelect.addEventListener('change', function () {
line.timeSpan = Number(windowSelect.value);
});
line.addEventListener('segmentClick', function (event) {
const segment = event.detail.segment,
minutes = Math.round((event.detail.to - event.detail.from) / MIN);
log.textContent = event.detail.row + ': ' + event.detail.state + ' for ' + minutes + ' min' + (segment.reason ? ' - ' + segment.reason : '') + '. A screen would open the downtime record here.';
});
startFeed();
/* ── The plant's vocabulary ─────────────────────────────────────────── */
furnace.states = [
{ id: 'heating', label: 'Heating', color: 'warning' },
{ id: 'soak', label: 'Soak', color: 'ok' },
{ id: 'cooling', label: 'Cooling', color: 'accent' },
{ id: 'load', label: 'Load / unload', color: 'neutral' },
{ id: 'trip', label: 'Trip', color: 'critical' }
];
furnace.timeSpan = 12 * H;
furnace.segments = [
{ state: 'load', from: now - 12 * H, to: now - 11.3 * H },
{ state: 'heating', from: now - 11.3 * H, to: now - 9.6 * H, reason: 'Ramp 5 °C/min' },
{ state: 'soak', from: now - 9.6 * H, to: now - 6.4 * H, reason: '850 °C' },
{ state: 'cooling', from: now - 6.4 * H, to: now - 4.1 * H },
{ state: 'load', from: now - 4.1 * H, to: now - 3.4 * H },
{ state: 'heating', from: now - 3.4 * H, to: now - 2.2 * H },
{ state: 'trip', from: now - 2.2 * H, to: now - 1.9 * H, reason: 'Burner flame failure' },
{ state: 'purge', from: now - 1.9 * H, to: now - 1.6 * H, reason: 'Purge before relight' },
{ state: 'heating', from: now - 1.6 * H, to: now - 0.5 * H },
{ state: 'soak', from: now - 0.5 * H }
];
}
angular/app.component.css
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions 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;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
smart-state-timeline {
--smart-timeline-label-width: 230px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
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 { StateTimeline } from 'smart-industrial/react/statetimeline';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* State timeline overview.
*
* The record is written by hand here in the shape a historian's state table takes: one
* row per machine, each a list of { state, from, to, reason }. A plant's comes from its
* historian or MES as JSON and is assigned to `rows` unchanged; state changes after that
* arrive through push(). */
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 line = document.getElementById('line'),
furnace = document.getElementById('furnace'),
feedButton = document.getElementById('feed'),
summaryButton = document.getElementById('summary'),
windowSelect = document.getElementById('window'),
log = document.getElementById('log'),
MIN = 60000,
H = 3600000,
now = Date.now(),
start = now - 8 * H;
/* ── The shift so far ───────────────────────────────────────────────── */
function at(minutes) {
return start + minutes * MIN;
}
line.rows = [
{ id: 'filler', label: 'Filler', segments: [
{ state: 'changeover', from: at(-20), to: at(35), reason: 'Format 500 ml → 1 l' },
{ state: 'running', from: at(35), to: at(160) },
{ state: 'fault', from: at(160), to: at(196), reason: 'Jam at infeed star' },
{ state: 'running', from: at(196), to: at(300) },
{ state: 'starved', from: at(300), to: at(318), reason: 'Bottle supply' },
{ state: 'running', from: at(318) }
] },
{ id: 'capper', label: 'Capper', segments: [
{ state: 'changeover', from: at(-20), to: at(38) },
{ state: 'running', from: at(38), to: at(163) },
{ state: 'starved', from: at(163), to: at(199), reason: 'Filler fault' },
{ state: 'running', from: at(199), to: at(250) },
{ state: 'fault', from: at(250), to: at(262), reason: 'Cap feeder' },
{ state: 'running', from: at(262) }
] },
{ id: 'labeller', label: 'Labeller', segments: [
{ state: 'changeover', from: at(-20), to: at(41) },
{ state: 'running', from: at(41), to: at(166) },
{ state: 'starved', from: at(166), to: at(203), reason: 'Upstream' },
{ state: 'running', from: at(203), to: at(390) },
{ state: 'blocked', from: at(390), to: at(412), reason: 'Packer full' },
{ state: 'running', from: at(412) }
] },
{ id: 'packer', label: 'Case packer', segments: [
{ state: 'maintenance', from: at(-60), to: at(45), reason: 'Planned: gripper service' },
{ state: 'running', from: at(45), to: at(170) },
{ state: 'starved', from: at(170), to: at(206) },
{ state: 'running', from: at(206), to: at(388) },
{ state: 'fault', from: at(388), to: at(414), reason: 'Glue unit temperature' },
{ state: 'running', from: at(414) }
] }
];
/* ── The feed: a state change somewhere on the line every few seconds ─ */
const ids = ['filler', 'capper', 'labeller', 'packer'],
stories = [
{ state: 'starved', reason: 'Upstream' },
{ state: 'running' },
{ state: 'blocked', reason: 'Downstream full' },
{ state: 'running' },
{ state: 'fault', reason: 'Guard door' },
{ state: 'running' }
];
let step = 0,
timer = null;
function tick() {
const id = ids[step % ids.length],
story = stories[(step * 7) % stories.length];
line.push(id, { state: story.state, from: Date.now(), reason: story.reason });
step++;
}
function startFeed() {
timer = setInterval(tick, 5000);
feedButton.textContent = 'Pause the feed';
}
feedButton.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
feedButton.textContent = 'Resume the feed';
}
else {
startFeed();
}
});
summaryButton.addEventListener('click', function () {
line.showSummary = !line.showSummary;
summaryButton.textContent = line.showSummary ? 'Hide the summaries' : 'Show the summaries';
});
windowSelect.addEventListener('change', function () {
line.timeSpan = Number(windowSelect.value);
});
line.addEventListener('segmentClick', function (event) {
const segment = event.detail.segment,
minutes = Math.round((event.detail.to - event.detail.from) / MIN);
log.textContent = event.detail.row + ': ' + event.detail.state + ' for ' + minutes + ' min' + (segment.reason ? ' - ' + segment.reason : '') + '. A screen would open the downtime record here.';
});
startFeed();
/* ── The plant's vocabulary ─────────────────────────────────────────── */
furnace.states = [
{ id: 'heating', label: 'Heating', color: 'warning' },
{ id: 'soak', label: 'Soak', color: 'ok' },
{ id: 'cooling', label: 'Cooling', color: 'accent' },
{ id: 'load', label: 'Load / unload', color: 'neutral' },
{ id: 'trip', label: 'Trip', color: 'critical' }
];
furnace.timeSpan = 12 * H;
furnace.segments = [
{ state: 'load', from: now - 12 * H, to: now - 11.3 * H },
{ state: 'heating', from: now - 11.3 * H, to: now - 9.6 * H, reason: 'Ramp 5 °C/min' },
{ state: 'soak', from: now - 9.6 * H, to: now - 6.4 * H, reason: '850 °C' },
{ state: 'cooling', from: now - 6.4 * H, to: now - 4.1 * H },
{ state: 'load', from: now - 4.1 * H, to: now - 3.4 * H },
{ state: 'heating', from: now - 3.4 * H, to: now - 2.2 * H },
{ state: 'trip', from: now - 2.2 * H, to: now - 1.9 * H, reason: 'Burner flame failure' },
{ state: 'purge', from: now - 1.9 * H, to: now - 1.6 * H, reason: 'Purge before relight' },
{ state: 'heating', from: now - 1.6 * H, to: now - 0.5 * H },
{ state: 'soak', from: now - 0.5 * H }
];
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.StateTimeline displays the state history of each machine on a production line along a time axis.</h2>
<p>Each row is a machine and each segment is a state (running, starved, blocked, stopped, fault, changeover) drawn for the time it lasted, with the reason recorded by the plant. The open segment at the right is the current state. The summary under each machine name shows the time spent in each state over the displayed window. Compared with a downtime Pareto chart, the timeline shows when each state occurred, in what order, and what the neighbouring machines were doing at the same time.</p>
</section>
<section>
<h2>Line 2, current shift</h2>
<StateTimeline id="line" label="Line 2" showSummary></StateTimeline>
<div className="demo-actions">
<button type="button" id="feed">Pause the feed</button>
<button type="button" id="summary">Hide the summaries</button>
<label>
Window
<select id="window" defaultValue="28800000"><option value="14400000">4 h</option> <option value="28800000">8 h</option> <option value="43200000">12 h</option></select>
</label>
</div>
<p className="demo-log" id="log">Use Tab to focus the timeline. The left and right arrow keys move along a row, the up and down arrow keys move to the machine above or below at the same time, and Enter opens a segment.</p>
</section>
<section>
<h2>Custom states</h2>
<p>The <code>states</code> property defines the names and colours of the states recorded by the plant. A state that is not in the list is drawn in a neutral colour with its own id, so a new state from the controller is still visible.</p>
<StateTimeline id="furnace" label="Furnace 1" showReasons></StateTimeline>
</section>
</div>
</div>
);
};
export default App;
react/App.css
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions 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;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
smart-state-timeline {
--smart-timeline-label-width: 230px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
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="smart-demo-container">
<section id="overview">
<h2>Smart.StateTimeline displays the state history of each machine on a production line along a time axis.</h2>
<p>
Each row is a machine and each segment is a state (running, starved, blocked, stopped, fault, changeover) drawn for the time it lasted, with the reason recorded by the plant. The open segment at the right is the current state. The summary under each machine name shows the time spent in each state over the displayed window. Compared with a downtime Pareto chart, the timeline shows when each state occurred, in what order, and what the neighbouring machines were doing at the same time.
</p>
</section>
<section>
<h2>Line 2, current shift</h2>
<smart-state-timeline id="line" label="Line 2" show-summary></smart-state-timeline>
<div class="demo-actions">
<button type="button" id="feed">Pause the feed</button>
<button type="button" id="summary">Hide the summaries</button>
<label>Window <select id="window">
<option value="14400000">4 h</option>
<option value="28800000" selected>8 h</option>
<option value="43200000">12 h</option>
</select></label>
</div>
<p class="demo-log" id="log">Use Tab to focus the timeline. The left and right arrow keys move along a row, the up and down arrow keys move to the machine above or below at the same time, and Enter opens a segment.</p>
</section>
<section>
<h2>Custom states</h2>
<p>
The <code>states</code> property defines the names and colours of the states recorded by the plant. A state that is not in the list is drawn in a neutral colour with its own id, so a new state from the controller is still visible.
</p>
<smart-state-timeline id="furnace" label="Furnace 1" show-reasons></smart-state-timeline>
</section>
</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.statetimeline.js";
/* State timeline overview.
*
* The record is written by hand here in the shape a historian's state table takes: one
* row per machine, each a list of { state, from, to, reason }. A plant's comes from its
* historian or MES as JSON and is assigned to `rows` unchanged; state changes after that
* arrive through push(). */
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 line = document.getElementById('line'),
furnace = document.getElementById('furnace'),
feedButton = document.getElementById('feed'),
summaryButton = document.getElementById('summary'),
windowSelect = document.getElementById('window'),
log = document.getElementById('log'),
MIN = 60000,
H = 3600000,
now = Date.now(),
start = now - 8 * H;
/* ── The shift so far ───────────────────────────────────────────────── */
function at(minutes) {
return start + minutes * MIN;
}
line.rows = [
{ id: 'filler', label: 'Filler', segments: [
{ state: 'changeover', from: at(-20), to: at(35), reason: 'Format 500 ml → 1 l' },
{ state: 'running', from: at(35), to: at(160) },
{ state: 'fault', from: at(160), to: at(196), reason: 'Jam at infeed star' },
{ state: 'running', from: at(196), to: at(300) },
{ state: 'starved', from: at(300), to: at(318), reason: 'Bottle supply' },
{ state: 'running', from: at(318) }
] },
{ id: 'capper', label: 'Capper', segments: [
{ state: 'changeover', from: at(-20), to: at(38) },
{ state: 'running', from: at(38), to: at(163) },
{ state: 'starved', from: at(163), to: at(199), reason: 'Filler fault' },
{ state: 'running', from: at(199), to: at(250) },
{ state: 'fault', from: at(250), to: at(262), reason: 'Cap feeder' },
{ state: 'running', from: at(262) }
] },
{ id: 'labeller', label: 'Labeller', segments: [
{ state: 'changeover', from: at(-20), to: at(41) },
{ state: 'running', from: at(41), to: at(166) },
{ state: 'starved', from: at(166), to: at(203), reason: 'Upstream' },
{ state: 'running', from: at(203), to: at(390) },
{ state: 'blocked', from: at(390), to: at(412), reason: 'Packer full' },
{ state: 'running', from: at(412) }
] },
{ id: 'packer', label: 'Case packer', segments: [
{ state: 'maintenance', from: at(-60), to: at(45), reason: 'Planned: gripper service' },
{ state: 'running', from: at(45), to: at(170) },
{ state: 'starved', from: at(170), to: at(206) },
{ state: 'running', from: at(206), to: at(388) },
{ state: 'fault', from: at(388), to: at(414), reason: 'Glue unit temperature' },
{ state: 'running', from: at(414) }
] }
];
/* ── The feed: a state change somewhere on the line every few seconds ─ */
const ids = ['filler', 'capper', 'labeller', 'packer'],
stories = [
{ state: 'starved', reason: 'Upstream' },
{ state: 'running' },
{ state: 'blocked', reason: 'Downstream full' },
{ state: 'running' },
{ state: 'fault', reason: 'Guard door' },
{ state: 'running' }
];
let step = 0,
timer = null;
function tick() {
const id = ids[step % ids.length],
story = stories[(step * 7) % stories.length];
line.push(id, { state: story.state, from: Date.now(), reason: story.reason });
step++;
}
function startFeed() {
timer = setInterval(tick, 5000);
feedButton.textContent = 'Pause the feed';
}
feedButton.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
feedButton.textContent = 'Resume the feed';
}
else {
startFeed();
}
});
summaryButton.addEventListener('click', function () {
line.showSummary = !line.showSummary;
summaryButton.textContent = line.showSummary ? 'Hide the summaries' : 'Show the summaries';
});
windowSelect.addEventListener('change', function () {
line.timeSpan = Number(windowSelect.value);
});
line.addEventListener('segmentClick', function (event) {
const segment = event.detail.segment,
minutes = Math.round((event.detail.to - event.detail.from) / MIN);
log.textContent = event.detail.row + ': ' + event.detail.state + ' for ' + minutes + ' min' + (segment.reason ? ' - ' + segment.reason : '') + '. A screen would open the downtime record here.';
});
startFeed();
/* ── The plant's vocabulary ─────────────────────────────────────────── */
furnace.states = [
{ id: 'heating', label: 'Heating', color: 'warning' },
{ id: 'soak', label: 'Soak', color: 'ok' },
{ id: 'cooling', label: 'Cooling', color: 'accent' },
{ id: 'load', label: 'Load / unload', color: 'neutral' },
{ id: 'trip', label: 'Trip', color: 'critical' }
];
furnace.timeSpan = 12 * H;
furnace.segments = [
{ state: 'load', from: now - 12 * H, to: now - 11.3 * H },
{ state: 'heating', from: now - 11.3 * H, to: now - 9.6 * H, reason: 'Ramp 5 °C/min' },
{ state: 'soak', from: now - 9.6 * H, to: now - 6.4 * H, reason: '850 °C' },
{ state: 'cooling', from: now - 6.4 * H, to: now - 4.1 * H },
{ state: 'load', from: now - 4.1 * H, to: now - 3.4 * H },
{ state: 'heating', from: now - 3.4 * H, to: now - 2.2 * H },
{ state: 'trip', from: now - 2.2 * H, to: now - 1.9 * H, reason: 'Burner flame failure' },
{ state: 'purge', from: now - 1.9 * H, to: now - 1.6 * H, reason: 'Purge before relight' },
{ state: 'heating', from: now - 1.6 * H, to: now - 0.5 * H },
{ state: 'soak', from: now - 0.5 * H }
];
});
});
}
};
</script>
<style>
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions 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;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
smart-state-timeline {
--smart-timeline-label-width: 230px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
</style>
What the demo shows
- Line 2, current shift
- Custom states