Component demo · Trends and signals
Strip Chart
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>Strip Chart 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.StripChart is a real-time chart for continuously acquired samples. The newest sample is drawn at the right and the oldest leaves at the left.</h2>
<p>
The buffer is a ring buffer: a push writes one slot and moves an index, so the cost of a sample does not grow with the history. Drawing is batched per animation frame, so the chart can accept samples at 10 kHz and still draw sixty times per second.
</p>
</section>
<section>
<h2>Four channels at 20 Hz</h2>
<smart-strip-chart id="acquisition"></smart-strip-chart>
<div class="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="clear">Clear</button>
<label>Rate
<select id="rate">
<option value="200">5 Hz</option>
<option value="50" selected>20 Hz</option>
<option value="16">60 Hz</option>
<option value="1">1 kHz</option>
</select>
</label>
<span id="stats" class="stats"></span>
</div>
<p class="hint">
Keyboard: the legend is a single tab stop. Use the arrow keys to move between pens and Enter or Space to show or hide one.
</p>
</section>
<section>
<h2>Scaling</h2>
<p>
Four signals with very different magnitudes. In <em>perPen</em> mode each pen uses the full height against its own range, so signals of different magnitudes can be compared by shape. In <em>shared</em> mode all pens use one range, so they can be compared by value.
</p>
<smart-strip-chart id="scaling"></smart-strip-chart>
<div class="demo-actions">
<label>Scale
<select id="scale">
<option value="perPen" selected>perPen</option>
<option value="shared">shared</option>
<option value="none">none (fixed 0–100)</option>
</select>
</label>
</div>
</section>
<section>
<h2>Modes</h2>
<p>
In <em>scroll</em> mode the trace moves and the newest sample stays at the right edge. In <em>sweep</em> mode the trace stays in place and a write cursor moves across it, like a persistence scope, which makes it easier to watch for a shape than for a trend.
</p>
<div class="two-up">
<div>
<h3>scroll</h3>
<smart-strip-chart id="scrollMode"></smart-strip-chart>
</div>
<div>
<h3>sweep</h3>
<smart-strip-chart id="sweepMode"></smart-strip-chart>
</div>
</div>
</section>
<section>
<h2>Time axis</h2>
<p>
In <em>time</em> mode the x position comes from the sample timestamp instead of the sample index, so an irregular feed, such as data from a plant historian, is spaced correctly instead of being drawn as if it were evenly sampled. With <code>showScales</code> the chart draws a y scale down the left and, here, seconds before now along the bottom - the two scales a classic waveform chart has.
</p>
<smart-strip-chart id="timed"></smart-strip-chart>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.stripchart.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" />
/* Every chart on this page is fed by one simulated acquisition loop. The seam between the
instrument and the display is push() - swap this generator for a WebSocket, an OPC-UA
subscription or a DAQ callback and nothing else on the page changes. */
function noise(amplitude) {
return (Math.random() - 0.5) * amplitude;
}
window.onload = function () {
/* ── Four channels at 20 Hz ──────────────────────────────────────── */
const acquisition = document.getElementById('acquisition'),
stats = document.getElementById('stats'),
pauseButton = document.getElementById('pause'),
rateSelect = document.getElementById('rate');
acquisition.historyLength = 600;
acquisition.min = -12;
acquisition.max = 12;
acquisition.showDigitalDisplay = true;
acquisition.precisionDigits = 3;
acquisition.pens = [
{ field: 'ai0', label: 'AI0', unit: 'V' },
{ field: 'ai1', label: 'AI1', unit: 'V' },
{ field: 'ai2', label: 'AI2', unit: 'V' },
{ field: 'ai3', label: 'AI3', unit: 'V' }
];
let phase = 0, timer = null;
function sample() {
phase += 0.06;
acquisition.push({
ai0: 8 * Math.sin(phase) + noise(0.4),
ai1: 5 * Math.sin(phase * 0.5 + 1) + noise(0.3),
ai2: 3 * Math.sin(phase * 2.3) + noise(0.8),
ai3: 10 * Math.sin(phase * 0.17) + noise(0.2)
});
}
function run(interval) {
if (timer) {
clearInterval(timer);
}
timer = setInterval(sample, Number(interval));
}
run(rateSelect.value);
rateSelect.addEventListener('change', function () {
run(rateSelect.value);
});
pauseButton.addEventListener('click', function () {
acquisition.paused = !acquisition.paused;
pauseButton.textContent = acquisition.paused ? 'Resume' : 'Pause';
});
document.getElementById('clear').addEventListener('click', function () {
acquisition.clear();
});
/* The chart reports what it took and what it had to drop, which is how a host knows
it is feeding faster than the buffer can hold. */
acquisition.addEventListener('pointsAppended', function (event) {
stats.textContent = event.detail.written.toLocaleString() + ' samples acquired, ' +
event.detail.length + ' held';
});
acquisition.addEventListener('penVisibilityChange', function (event) {
stats.textContent = event.detail.field + (event.detail.visible ? ' shown' : ' hidden');
});
/* ── Scaling ─────────────────────────────────────────────────────── */
const scaling = document.getElementById('scaling'),
scaleSelect = document.getElementById('scale');
scaling.historyLength = 400;
scaling.autoScale = 'perPen';
scaling.pens = [
{ field: 'flow', label: 'Flow', unit: 'L/min' },
{ field: 'pressure', label: 'Pressure', unit: 'bar' },
{ field: 'temperature', label: 'Temp', unit: '°C' },
{ field: 'speed', label: 'Speed', unit: 'RPM' }
];
scaleSelect.addEventListener('change', function () {
scaling.autoScale = scaleSelect.value;
});
let scalePhase = 0;
setInterval(function () {
scalePhase += 0.05;
scaling.push({
flow: 420 + 60 * Math.sin(scalePhase) + noise(8),
pressure: 6.2 + 0.8 * Math.sin(scalePhase * 1.7) + noise(0.1),
temperature: 68 + 6 * Math.sin(scalePhase * 0.4) + noise(0.5),
speed: 4400 + 500 * Math.sin(scalePhase * 0.9) + noise(40)
});
}, 50);
/* ── Modes ───────────────────────────────────────────────────────── */
const scrollMode = document.getElementById('scrollMode'),
sweepMode = document.getElementById('sweepMode');
[scrollMode, sweepMode].forEach(function (chart) {
chart.historyLength = 240;
chart.min = -1.4;
chart.max = 1.4;
chart.showLegend = false;
chart.pens = [{ field: 'v', label: 'Signal' }];
});
sweepMode.mode = 'sweep';
let modePhase = 0;
setInterval(function () {
modePhase += 0.12;
const value = Math.sin(modePhase) + noise(0.15);
scrollMode.push({ v: value });
sweepMode.push({ v: value });
}, 40);
/* ── A time axis ─────────────────────────────────────────────────── */
const timed = document.getElementById('timed');
timed.xAxisMode = 'time';
timed.timeSpan = 30000;
timed.historyLength = 500;
timed.autoScale = 'shared';
timed.showDigitalDisplay = true;
timed.showScales = true;
timed.pens = [
{ field: 'lt2201', label: 'LT-2201', unit: '%' },
{ field: 'tt2107', label: 'TT-2107', unit: '°C' }
];
/* Deliberately irregular: a real historian does not deliver on a metronome, and the
point of a time axis is that an uneven feed still lands in the right place. */
let timePhase = 0;
function irregular() {
timePhase += 0.1;
timed.push({
lt2201: 62 + 14 * Math.sin(timePhase) + noise(1.5),
tt2107: 68 + 9 * Math.sin(timePhase * 0.6) + noise(1)
});
setTimeout(irregular, 120 + Math.random() * 380);
}
irregular();
};
styles.css
smart-strip-chart {
width: 100%;
}
#acquisition {
--smart-strip-chart-height: 280px;
}
.two-up {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.two-up h3 {
margin: 0 0 6px;
font-size: 13px;
font-weight: 600;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.stats {
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.75;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
/* ── 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.StripChart is a real-time chart for continuously acquired samples. The newest sample is drawn at the right and the oldest leaves at the left.</h2>
<p>
The buffer is a ring buffer: a push writes one slot and moves an index, so the cost of a sample does not grow with the history. Drawing is batched per animation frame, so the chart can accept samples at 10 kHz and still draw sixty times per second.
</p>
</section>
<section>
<h2>Four channels at 20 Hz</h2>
<smart-strip-chart id="acquisition"></smart-strip-chart>
<div class="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="clear">Clear</button>
<label>Rate
<select id="rate">
<option value="200">5 Hz</option>
<option value="50" selected>20 Hz</option>
<option value="16">60 Hz</option>
<option value="1">1 kHz</option>
</select>
</label>
<span id="stats" class="stats"></span>
</div>
<p class="hint">
Keyboard: the legend is a single tab stop. Use the arrow keys to move between pens and Enter or Space to show or hide one.
</p>
</section>
<section>
<h2>Scaling</h2>
<p>
Four signals with very different magnitudes. In <em>perPen</em> mode each pen uses the full height against its own range, so signals of different magnitudes can be compared by shape. In <em>shared</em> mode all pens use one range, so they can be compared by value.
</p>
<smart-strip-chart id="scaling"></smart-strip-chart>
<div class="demo-actions">
<label>Scale
<select id="scale">
<option value="perPen" selected>perPen</option>
<option value="shared">shared</option>
<option value="none">none (fixed 0–100)</option>
</select>
</label>
</div>
</section>
<section>
<h2>Modes</h2>
<p>
In <em>scroll</em> mode the trace moves and the newest sample stays at the right edge. In <em>sweep</em> mode the trace stays in place and a write cursor moves across it, like a persistence scope, which makes it easier to watch for a shape than for a trend.
</p>
<div class="two-up">
<div>
<h3>scroll</h3>
<smart-strip-chart id="scrollMode"></smart-strip-chart>
</div>
<div>
<h3>sweep</h3>
<smart-strip-chart id="sweepMode"></smart-strip-chart>
</div>
</div>
</section>
<section>
<h2>Time axis</h2>
<p>
In <em>time</em> mode the x position comes from the sample timestamp instead of the sample index, so an irregular feed, such as data from a plant historian, is spaced correctly instead of being drawn as if it were evenly sampled. With <code>showScales</code> the chart draws a y scale down the left and, here, seconds before now along the bottom - the two scales a classic waveform chart has.
</p>
<smart-strip-chart id="timed"></smart-strip-chart>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { StripChartModule } from 'smart-industrial/angular/stripchart';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - StripChart 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: [StripChartModule],
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. */
/* Every chart on this page is fed by one simulated acquisition loop. The seam between the
instrument and the display is push() - swap this generator for a WebSocket, an OPC-UA
subscription or a DAQ callback and nothing else on the page changes. */
function noise(amplitude) {
return (Math.random() - 0.5) * amplitude;
}
export function runDemo(): void {
/* ── Four channels at 20 Hz ──────────────────────────────────────── */
const acquisition = document.getElementById('acquisition'),
stats = document.getElementById('stats'),
pauseButton = document.getElementById('pause'),
rateSelect = document.getElementById('rate');
acquisition.historyLength = 600;
acquisition.min = -12;
acquisition.max = 12;
acquisition.showDigitalDisplay = true;
acquisition.precisionDigits = 3;
acquisition.pens = [
{ field: 'ai0', label: 'AI0', unit: 'V' },
{ field: 'ai1', label: 'AI1', unit: 'V' },
{ field: 'ai2', label: 'AI2', unit: 'V' },
{ field: 'ai3', label: 'AI3', unit: 'V' }
];
let phase = 0, timer = null;
function sample() {
phase += 0.06;
acquisition.push({
ai0: 8 * Math.sin(phase) + noise(0.4),
ai1: 5 * Math.sin(phase * 0.5 + 1) + noise(0.3),
ai2: 3 * Math.sin(phase * 2.3) + noise(0.8),
ai3: 10 * Math.sin(phase * 0.17) + noise(0.2)
});
}
function run(interval) {
if (timer) {
clearInterval(timer);
}
timer = setInterval(sample, Number(interval));
}
run(rateSelect.value);
rateSelect.addEventListener('change', function () {
run(rateSelect.value);
});
pauseButton.addEventListener('click', function () {
acquisition.paused = !acquisition.paused;
pauseButton.textContent = acquisition.paused ? 'Resume' : 'Pause';
});
document.getElementById('clear').addEventListener('click', function () {
acquisition.clear();
});
/* The chart reports what it took and what it had to drop, which is how a host knows
it is feeding faster than the buffer can hold. */
acquisition.addEventListener('pointsAppended', function (event) {
stats.textContent = event.detail.written.toLocaleString() + ' samples acquired, ' +
event.detail.length + ' held';
});
acquisition.addEventListener('penVisibilityChange', function (event) {
stats.textContent = event.detail.field + (event.detail.visible ? ' shown' : ' hidden');
});
/* ── Scaling ─────────────────────────────────────────────────────── */
const scaling = document.getElementById('scaling'),
scaleSelect = document.getElementById('scale');
scaling.historyLength = 400;
scaling.autoScale = 'perPen';
scaling.pens = [
{ field: 'flow', label: 'Flow', unit: 'L/min' },
{ field: 'pressure', label: 'Pressure', unit: 'bar' },
{ field: 'temperature', label: 'Temp', unit: '°C' },
{ field: 'speed', label: 'Speed', unit: 'RPM' }
];
scaleSelect.addEventListener('change', function () {
scaling.autoScale = scaleSelect.value;
});
let scalePhase = 0;
setInterval(function () {
scalePhase += 0.05;
scaling.push({
flow: 420 + 60 * Math.sin(scalePhase) + noise(8),
pressure: 6.2 + 0.8 * Math.sin(scalePhase * 1.7) + noise(0.1),
temperature: 68 + 6 * Math.sin(scalePhase * 0.4) + noise(0.5),
speed: 4400 + 500 * Math.sin(scalePhase * 0.9) + noise(40)
});
}, 50);
/* ── Modes ───────────────────────────────────────────────────────── */
const scrollMode = document.getElementById('scrollMode'),
sweepMode = document.getElementById('sweepMode');
[scrollMode, sweepMode].forEach(function (chart) {
chart.historyLength = 240;
chart.min = -1.4;
chart.max = 1.4;
chart.showLegend = false;
chart.pens = [{ field: 'v', label: 'Signal' }];
});
sweepMode.mode = 'sweep';
let modePhase = 0;
setInterval(function () {
modePhase += 0.12;
const value = Math.sin(modePhase) + noise(0.15);
scrollMode.push({ v: value });
sweepMode.push({ v: value });
}, 40);
/* ── A time axis ─────────────────────────────────────────────────── */
const timed = document.getElementById('timed');
timed.xAxisMode = 'time';
timed.timeSpan = 30000;
timed.historyLength = 500;
timed.autoScale = 'shared';
timed.showDigitalDisplay = true;
timed.showScales = true;
timed.pens = [
{ field: 'lt2201', label: 'LT-2201', unit: '%' },
{ field: 'tt2107', label: 'TT-2107', unit: '°C' }
];
/* Deliberately irregular: a real historian does not deliver on a metronome, and the
point of a time axis is that an uneven feed still lands in the right place. */
let timePhase = 0;
function irregular() {
timePhase += 0.1;
timed.push({
lt2201: 62 + 14 * Math.sin(timePhase) + noise(1.5),
tt2107: 68 + 9 * Math.sin(timePhase * 0.6) + noise(1)
});
setTimeout(irregular, 120 + Math.random() * 380);
}
irregular();
}
angular/app.component.css
smart-strip-chart {
width: 100%;
}
#acquisition {
--smart-strip-chart-height: 280px;
}
.two-up {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.two-up h3 {
margin: 0 0 6px;
font-size: 13px;
font-weight: 600;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.stats {
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.75;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
/* ── 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 { StripChart } from 'smart-industrial/react/stripchart';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* Every chart on this page is fed by one simulated acquisition loop. The seam between the
instrument and the display is push() - swap this generator for a WebSocket, an OPC-UA
subscription or a DAQ callback and nothing else on the page changes. */
function noise(amplitude) {
return (Math.random() - 0.5) * amplitude;
}
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;
}
/* ── Four channels at 20 Hz ──────────────────────────────────────── */
const acquisition = document.getElementById('acquisition'),
stats = document.getElementById('stats'),
pauseButton = document.getElementById('pause'),
rateSelect = document.getElementById('rate');
acquisition.historyLength = 600;
acquisition.min = -12;
acquisition.max = 12;
acquisition.showDigitalDisplay = true;
acquisition.precisionDigits = 3;
acquisition.pens = [
{ field: 'ai0', label: 'AI0', unit: 'V' },
{ field: 'ai1', label: 'AI1', unit: 'V' },
{ field: 'ai2', label: 'AI2', unit: 'V' },
{ field: 'ai3', label: 'AI3', unit: 'V' }
];
let phase = 0, timer = null;
function sample() {
phase += 0.06;
acquisition.push({
ai0: 8 * Math.sin(phase) + noise(0.4),
ai1: 5 * Math.sin(phase * 0.5 + 1) + noise(0.3),
ai2: 3 * Math.sin(phase * 2.3) + noise(0.8),
ai3: 10 * Math.sin(phase * 0.17) + noise(0.2)
});
}
function run(interval) {
if (timer) {
clearInterval(timer);
}
timer = setInterval(sample, Number(interval));
}
run(rateSelect.value);
rateSelect.addEventListener('change', function () {
run(rateSelect.value);
});
pauseButton.addEventListener('click', function () {
acquisition.paused = !acquisition.paused;
pauseButton.textContent = acquisition.paused ? 'Resume' : 'Pause';
});
document.getElementById('clear').addEventListener('click', function () {
acquisition.clear();
});
/* The chart reports what it took and what it had to drop, which is how a host knows
it is feeding faster than the buffer can hold. */
acquisition.addEventListener('pointsAppended', function (event) {
stats.textContent = event.detail.written.toLocaleString() + ' samples acquired, ' +
event.detail.length + ' held';
});
acquisition.addEventListener('penVisibilityChange', function (event) {
stats.textContent = event.detail.field + (event.detail.visible ? ' shown' : ' hidden');
});
/* ── Scaling ─────────────────────────────────────────────────────── */
const scaling = document.getElementById('scaling'),
scaleSelect = document.getElementById('scale');
scaling.historyLength = 400;
scaling.autoScale = 'perPen';
scaling.pens = [
{ field: 'flow', label: 'Flow', unit: 'L/min' },
{ field: 'pressure', label: 'Pressure', unit: 'bar' },
{ field: 'temperature', label: 'Temp', unit: '°C' },
{ field: 'speed', label: 'Speed', unit: 'RPM' }
];
scaleSelect.addEventListener('change', function () {
scaling.autoScale = scaleSelect.value;
});
let scalePhase = 0;
setInterval(function () {
scalePhase += 0.05;
scaling.push({
flow: 420 + 60 * Math.sin(scalePhase) + noise(8),
pressure: 6.2 + 0.8 * Math.sin(scalePhase * 1.7) + noise(0.1),
temperature: 68 + 6 * Math.sin(scalePhase * 0.4) + noise(0.5),
speed: 4400 + 500 * Math.sin(scalePhase * 0.9) + noise(40)
});
}, 50);
/* ── Modes ───────────────────────────────────────────────────────── */
const scrollMode = document.getElementById('scrollMode'),
sweepMode = document.getElementById('sweepMode');
[scrollMode, sweepMode].forEach(function (chart) {
chart.historyLength = 240;
chart.min = -1.4;
chart.max = 1.4;
chart.showLegend = false;
chart.pens = [{ field: 'v', label: 'Signal' }];
});
sweepMode.mode = 'sweep';
let modePhase = 0;
setInterval(function () {
modePhase += 0.12;
const value = Math.sin(modePhase) + noise(0.15);
scrollMode.push({ v: value });
sweepMode.push({ v: value });
}, 40);
/* ── A time axis ─────────────────────────────────────────────────── */
const timed = document.getElementById('timed');
timed.xAxisMode = 'time';
timed.timeSpan = 30000;
timed.historyLength = 500;
timed.autoScale = 'shared';
timed.showDigitalDisplay = true;
timed.showScales = true;
timed.pens = [
{ field: 'lt2201', label: 'LT-2201', unit: '%' },
{ field: 'tt2107', label: 'TT-2107', unit: '°C' }
];
/* Deliberately irregular: a real historian does not deliver on a metronome, and the
point of a time axis is that an uneven feed still lands in the right place. */
let timePhase = 0;
function irregular() {
timePhase += 0.1;
timed.push({
lt2201: 62 + 14 * Math.sin(timePhase) + noise(1.5),
tt2107: 68 + 9 * Math.sin(timePhase * 0.6) + noise(1)
});
setTimeout(irregular, 120 + Math.random() * 380);
}
irregular();
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.StripChart is a real-time chart for continuously acquired samples. The newest sample is drawn at the right and the oldest leaves at the left.</h2>
<p>The buffer is a ring buffer: a push writes one slot and moves an index, so the cost of a sample does not grow with the history. Drawing is batched per animation frame, so the chart can accept samples at 10 kHz and still draw sixty times per second.</p>
</section>
<section>
<h2>Four channels at 20 Hz</h2>
<StripChart id="acquisition"></StripChart>
<div className="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="clear">Clear</button>
<label>
Rate
<select id="rate" defaultValue="50"><option value="200">5 Hz</option> <option value="50">20 Hz</option> <option value="16">60 Hz</option> <option value="1">1 kHz</option></select>
</label>
<span id="stats" className="stats"></span>
</div>
<p className="hint">Keyboard: the legend is a single tab stop. Use the arrow keys to move between pens and Enter or Space to show or hide one.</p>
</section>
<section>
<h2>Scaling</h2>
<p>Four signals with very different magnitudes. In <em>perPen</em> mode each pen uses the full height against its own range, so signals of different magnitudes can be compared by shape. In <em>shared</em> mode all pens use one range, so they can be compared by value.</p>
<StripChart id="scaling"></StripChart>
<div className="demo-actions">
<label>
Scale
<select id="scale" defaultValue="perPen"><option value="perPen">perPen</option> <option value="shared">shared</option> <option value="none">none (fixed 0–100)</option></select>
</label>
</div>
</section>
<section>
<h2>Modes</h2>
<p>In <em>scroll</em> mode the trace moves and the newest sample stays at the right edge. In <em>sweep</em> mode the trace stays in place and a write cursor moves across it, like a persistence scope, which makes it easier to watch for a shape than for a trend.</p>
<div className="two-up">
<div>
<h3>scroll</h3>
<StripChart id="scrollMode"></StripChart>
</div>
<div>
<h3>sweep</h3>
<StripChart id="sweepMode"></StripChart>
</div>
</div>
</section>
<section>
<h2>Time axis</h2>
<p>In <em>time</em> mode the x position comes from the sample timestamp instead of the sample index, so an irregular feed, such as data from a plant historian, is spaced correctly instead of being drawn as if it were evenly sampled. With <code>showScales</code> the chart draws a y scale down the left and, here, seconds before now along the bottom - the two scales a classic waveform chart has.</p>
<StripChart id="timed"></StripChart>
</section>
</div>
</div>
);
};
export default App;
react/App.css
smart-strip-chart {
width: 100%;
}
#acquisition {
--smart-strip-chart-height: 280px;
}
.two-up {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.two-up h3 {
margin: 0 0 6px;
font-size: 13px;
font-weight: 600;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.stats {
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.75;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
/* ── 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.StripChart is a real-time chart for continuously acquired samples. The newest sample is drawn at the right and the oldest leaves at the left.</h2>
<p>
The buffer is a ring buffer: a push writes one slot and moves an index, so the cost of a sample does not grow with the history. Drawing is batched per animation frame, so the chart can accept samples at 10 kHz and still draw sixty times per second.
</p>
</section>
<section>
<h2>Four channels at 20 Hz</h2>
<smart-strip-chart id="acquisition"></smart-strip-chart>
<div class="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="clear">Clear</button>
<label>Rate
<select id="rate">
<option value="200">5 Hz</option>
<option value="50" selected>20 Hz</option>
<option value="16">60 Hz</option>
<option value="1">1 kHz</option>
</select>
</label>
<span id="stats" class="stats"></span>
</div>
<p class="hint">
Keyboard: the legend is a single tab stop. Use the arrow keys to move between pens and Enter or Space to show or hide one.
</p>
</section>
<section>
<h2>Scaling</h2>
<p>
Four signals with very different magnitudes. In <em>perPen</em> mode each pen uses the full height against its own range, so signals of different magnitudes can be compared by shape. In <em>shared</em> mode all pens use one range, so they can be compared by value.
</p>
<smart-strip-chart id="scaling"></smart-strip-chart>
<div class="demo-actions">
<label>Scale
<select id="scale">
<option value="perPen" selected>perPen</option>
<option value="shared">shared</option>
<option value="none">none (fixed 0–100)</option>
</select>
</label>
</div>
</section>
<section>
<h2>Modes</h2>
<p>
In <em>scroll</em> mode the trace moves and the newest sample stays at the right edge. In <em>sweep</em> mode the trace stays in place and a write cursor moves across it, like a persistence scope, which makes it easier to watch for a shape than for a trend.
</p>
<div class="two-up">
<div>
<h3>scroll</h3>
<smart-strip-chart id="scrollMode"></smart-strip-chart>
</div>
<div>
<h3>sweep</h3>
<smart-strip-chart id="sweepMode"></smart-strip-chart>
</div>
</div>
</section>
<section>
<h2>Time axis</h2>
<p>
In <em>time</em> mode the x position comes from the sample timestamp instead of the sample index, so an irregular feed, such as data from a plant historian, is spaced correctly instead of being drawn as if it were evenly sampled. With <code>showScales</code> the chart draws a y scale down the left and, here, seconds before now along the bottom - the two scales a classic waveform chart has.
</p>
<smart-strip-chart id="timed"></smart-strip-chart>
</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.stripchart.js";
/* Every chart on this page is fed by one simulated acquisition loop. The seam between the
instrument and the display is push() - swap this generator for a WebSocket, an OPC-UA
subscription or a DAQ callback and nothing else on the page changes. */
function noise(amplitude) {
return (Math.random() - 0.5) * amplitude;
}
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(() => {
/* ── Four channels at 20 Hz ──────────────────────────────────────── */
const acquisition = document.getElementById('acquisition'),
stats = document.getElementById('stats'),
pauseButton = document.getElementById('pause'),
rateSelect = document.getElementById('rate');
acquisition.historyLength = 600;
acquisition.min = -12;
acquisition.max = 12;
acquisition.showDigitalDisplay = true;
acquisition.precisionDigits = 3;
acquisition.pens = [
{ field: 'ai0', label: 'AI0', unit: 'V' },
{ field: 'ai1', label: 'AI1', unit: 'V' },
{ field: 'ai2', label: 'AI2', unit: 'V' },
{ field: 'ai3', label: 'AI3', unit: 'V' }
];
let phase = 0, timer = null;
function sample() {
phase += 0.06;
acquisition.push({
ai0: 8 * Math.sin(phase) + noise(0.4),
ai1: 5 * Math.sin(phase * 0.5 + 1) + noise(0.3),
ai2: 3 * Math.sin(phase * 2.3) + noise(0.8),
ai3: 10 * Math.sin(phase * 0.17) + noise(0.2)
});
}
function run(interval) {
if (timer) {
clearInterval(timer);
}
timer = setInterval(sample, Number(interval));
}
run(rateSelect.value);
rateSelect.addEventListener('change', function () {
run(rateSelect.value);
});
pauseButton.addEventListener('click', function () {
acquisition.paused = !acquisition.paused;
pauseButton.textContent = acquisition.paused ? 'Resume' : 'Pause';
});
document.getElementById('clear').addEventListener('click', function () {
acquisition.clear();
});
/* The chart reports what it took and what it had to drop, which is how a host knows
it is feeding faster than the buffer can hold. */
acquisition.addEventListener('pointsAppended', function (event) {
stats.textContent = event.detail.written.toLocaleString() + ' samples acquired, ' +
event.detail.length + ' held';
});
acquisition.addEventListener('penVisibilityChange', function (event) {
stats.textContent = event.detail.field + (event.detail.visible ? ' shown' : ' hidden');
});
/* ── Scaling ─────────────────────────────────────────────────────── */
const scaling = document.getElementById('scaling'),
scaleSelect = document.getElementById('scale');
scaling.historyLength = 400;
scaling.autoScale = 'perPen';
scaling.pens = [
{ field: 'flow', label: 'Flow', unit: 'L/min' },
{ field: 'pressure', label: 'Pressure', unit: 'bar' },
{ field: 'temperature', label: 'Temp', unit: '°C' },
{ field: 'speed', label: 'Speed', unit: 'RPM' }
];
scaleSelect.addEventListener('change', function () {
scaling.autoScale = scaleSelect.value;
});
let scalePhase = 0;
setInterval(function () {
scalePhase += 0.05;
scaling.push({
flow: 420 + 60 * Math.sin(scalePhase) + noise(8),
pressure: 6.2 + 0.8 * Math.sin(scalePhase * 1.7) + noise(0.1),
temperature: 68 + 6 * Math.sin(scalePhase * 0.4) + noise(0.5),
speed: 4400 + 500 * Math.sin(scalePhase * 0.9) + noise(40)
});
}, 50);
/* ── Modes ───────────────────────────────────────────────────────── */
const scrollMode = document.getElementById('scrollMode'),
sweepMode = document.getElementById('sweepMode');
[scrollMode, sweepMode].forEach(function (chart) {
chart.historyLength = 240;
chart.min = -1.4;
chart.max = 1.4;
chart.showLegend = false;
chart.pens = [{ field: 'v', label: 'Signal' }];
});
sweepMode.mode = 'sweep';
let modePhase = 0;
setInterval(function () {
modePhase += 0.12;
const value = Math.sin(modePhase) + noise(0.15);
scrollMode.push({ v: value });
sweepMode.push({ v: value });
}, 40);
/* ── A time axis ─────────────────────────────────────────────────── */
const timed = document.getElementById('timed');
timed.xAxisMode = 'time';
timed.timeSpan = 30000;
timed.historyLength = 500;
timed.autoScale = 'shared';
timed.showDigitalDisplay = true;
timed.showScales = true;
timed.pens = [
{ field: 'lt2201', label: 'LT-2201', unit: '%' },
{ field: 'tt2107', label: 'TT-2107', unit: '°C' }
];
/* Deliberately irregular: a real historian does not deliver on a metronome, and the
point of a time axis is that an uneven feed still lands in the right place. */
let timePhase = 0;
function irregular() {
timePhase += 0.1;
timed.push({
lt2201: 62 + 14 * Math.sin(timePhase) + noise(1.5),
tt2107: 68 + 9 * Math.sin(timePhase * 0.6) + noise(1)
});
setTimeout(irregular, 120 + Math.random() * 380);
}
irregular();
});
});
}
};
</script>
<style>
smart-strip-chart {
width: 100%;
}
#acquisition {
--smart-strip-chart-height: 280px;
}
.two-up {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.two-up h3 {
margin: 0 0 6px;
font-size: 13px;
font-weight: 600;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.stats {
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.75;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
/* ── 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
- Four channels at 20 Hz
- Scaling
- Modes
- scroll
- sweep
- Time axis