Component demo · Instrument readouts and controls
Panel Meter
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>Panel Meter 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.PanelMeter draws the moving-coil instrument screwed into the front of a rack: a shallow arc of scale and a long needle pivoted low in a rectangular case.</h2>
<p>
It is not a gauge with its arc cut down. A gauge is a dial — a circle of scale read against a short hand near the middle. A panel meter's geometry exists because the pointer is a physical arm: the pivot sits low, the arm reaches almost the full height of the case, and the scale is the arc that arm can sweep. With that geometry come the things a dial has no use for: the anti-parallax mirror band, end stops and an over-range flag, the damping of the coil, and the centre-zero movement.
</p>
</section>
<section>
<h2>A bench supply and a bus</h2>
<p>
<code>min</code>, <code>max</code> and <code>value</code> are the scale and the reading; <code>unit</code> and <code>label</code> are printed on the plate. <code>sweepAngle</code> sets how far the needle travels — a moving-coil movement swings about 90 to 120 degrees.
</p>
<div class="demo-row">
<smart-panel-meter id="bus" label="Bus" unit="V DC" min="0" max="30" value="24.1" show-value precision-digits="1"></smart-panel-meter>
<smart-panel-meter id="current" label="Load" unit="A" min="0" max="5" value="3.2" show-value precision-digits="2" sweep-angle="120"></smart-panel-meter>
<smart-panel-meter id="narrow" label="Line" unit="Hz" min="49" max="51" value="50.02" show-value precision-digits="2" sweep-angle="90"></smart-panel-meter>
</div>
</section>
<section>
<h2>The mirror band</h2>
<p>
<code>mirror</code> draws the anti-parallax strip under the scale — the band an operator lines the needle up with its own reflection in, which is how a panel meter is read accurately at all. It costs nothing on screen and it is what makes the instrument recognisable to anybody who has used one.
</p>
<div class="demo-row">
<smart-panel-meter label="Without" unit="mA" min="0" max="100" value="62" show-value precision-digits="0"></smart-panel-meter>
<smart-panel-meter label="With the mirror" unit="mA" min="0" max="100" value="62" mirror show-value precision-digits="0"></smart-panel-meter>
</div>
</section>
<section>
<h2>Centre zero, and the end stops</h2>
<p>
<code>centerZero</code> marks the rest position and draws the zero line of a ± movement. Past the ends of the scale the needle pins against the stop and the case raises an over-range flag, instead of a hand sweeping on round a dial: the reading is still reported, and the instrument says it is off scale.
</p>
<div class="demo-row">
<smart-panel-meter id="balance" label="Balance" unit="%" min="-50" max="50" value="0" center-zero sweep-angle="120" show-value precision-digits="1"></smart-panel-meter>
<div class="demo-controls">
<label>Balance <input type="range" id="balanceSet" min="-70" max="70" step="0.5" value="0" /></label>
<p class="demo-log" id="balanceLog">Push it past ±50 to pin the needle.</p>
</div>
</div>
</section>
<section>
<h2>Damping</h2>
<p>
<code>damping</code> is the inertia of the coil, in milliseconds to settle. It is off by default, because a needle that lags is showing something the process is not doing, and that is the host's decision to make. The digital reading is never damped: it is the value, and the needle is the picture of it, so the instrument cannot be caught telling two stories about the same instant.
</p>
<div class="demo-row">
<smart-panel-meter id="undamped" label="Undamped" unit="mV" min="0" max="500" value="250" show-value precision-digits="0"></smart-panel-meter>
<smart-panel-meter id="damped" label="Damped 600 ms" unit="mV" min="0" max="500" value="250" damping="600" show-value precision-digits="0"></smart-panel-meter>
<div class="demo-controls">
<button type="button" id="noise">Feed a noisy signal</button>
<p class="demo-log">The same samples go to both. One is readable.</p>
</div>
</div>
</section>
<section>
<h2>Alarm bands, a set pointer and reading quality</h2>
<p>
<code>ranges</code> paints the bands on the plate and the needle takes the band's colour; <code>setpoint</code> with <code>showSetpoint</code> clips the red set pointer to the scale. <code>quality</code> is carried by every component that extends the scale engine: a bad reading is struck through and a stale or uncertain one is dimmed.
</p>
<div class="demo-row">
<smart-panel-meter id="pressure" label="PT-2101" unit="bar" min="0" max="16" value="11.4" setpoint="12" show-setpoint mirror show-value precision-digits="1"></smart-panel-meter>
<smart-panel-meter label="uncertain" unit="bar" min="0" max="16" value="11.4" quality="uncertain" show-value precision-digits="1"></smart-panel-meter>
<smart-panel-meter label="bad" unit="bar" min="0" max="16" value="11.4" quality="bad" show-value precision-digits="1"></smart-panel-meter>
</div>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.panelmeter.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" />
/* Panel meter overview.
*
* The noise demo is the argument for damping: the same samples go to both instruments, and
* only the damped one can be read. The digital display under each is the undamped value, so
* the two never disagree about what the process is doing - only about how fast the needle
* is allowed to say it. */
window.onload = function () {
const balance = document.getElementById('balance'),
balanceSet = document.getElementById('balanceSet'),
balanceLog = document.getElementById('balanceLog'),
undamped = document.getElementById('undamped'),
damped = document.getElementById('damped'),
noise = document.getElementById('noise'),
pressure = document.getElementById('pressure');
pressure.ranges = [
{ startValue: 0, endValue: 10, className: 'ok' },
{ startValue: 10, endValue: 13.5, className: 'warning' },
{ startValue: 13.5, endValue: 16, className: 'critical' }
];
pressure.showRanges = true;
balanceSet.addEventListener('input', function () {
balance.value = Number(balanceSet.value);
const over = Number(balanceSet.value) > 50,
under = Number(balanceSet.value) < -50;
balanceLog.textContent = over || under
? 'Needle against the ' + (over ? 'upper' : 'lower') + ' stop. The reading is still ' +
balanceSet.value + ' %; the instrument says it is off scale.'
: 'Reading ' + Number(balanceSet.value).toFixed(1) + ' %.';
});
/* A signal with a burst of noise on it, the kind that makes an undamped needle useless. */
let timer = null,
step = 0;
noise.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
noise.textContent = 'Feed a noisy signal';
return;
}
step = 0;
noise.textContent = 'Stop';
timer = setInterval(function () {
step += 1;
const base = 250 + 120 * Math.sin(step / 22),
jitter = (Math.random() - 0.5) * 140,
sample = Math.max(0, Math.min(500, base + jitter));
undamped.value = Math.round(sample);
damped.value = Math.round(sample);
if (step > 400) {
clearInterval(timer);
timer = null;
noise.textContent = 'Feed a noisy signal';
}
}, 60);
});
};
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-row {
display: flex;
flex-wrap: wrap;
gap: 40px;
align-items: flex-start;
padding: 18px 22px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-column {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 26px;
padding: 18px 22px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-instrument {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-controls {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 240px;
}
.demo-controls label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.demo-controls input[type="range"] {
flex: 1;
}
.demo-controls button {
align-self: flex-start;
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-controls 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 {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-panel-meter {
--smart-panel-meter-width: 210px;
}
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.PanelMeter draws the moving-coil instrument screwed into the front of a rack: a shallow arc of scale and a long needle pivoted low in a rectangular case.</h2>
<p>
It is not a gauge with its arc cut down. A gauge is a dial — a circle of scale read against a short hand near the middle. A panel meter's geometry exists because the pointer is a physical arm: the pivot sits low, the arm reaches almost the full height of the case, and the scale is the arc that arm can sweep. With that geometry come the things a dial has no use for: the anti-parallax mirror band, end stops and an over-range flag, the damping of the coil, and the centre-zero movement.
</p>
</section>
<section>
<h2>A bench supply and a bus</h2>
<p>
<code>min</code>, <code>max</code> and <code>value</code> are the scale and the reading; <code>unit</code> and <code>label</code> are printed on the plate. <code>sweepAngle</code> sets how far the needle travels — a moving-coil movement swings about 90 to 120 degrees.
</p>
<div class="demo-row">
<smart-panel-meter id="bus" label="Bus" unit="V DC" [min]="0" [max]="30" value="24.1" [showValue]="true" [precisionDigits]="1"></smart-panel-meter>
<smart-panel-meter id="current" label="Load" unit="A" [min]="0" [max]="5" value="3.2" [showValue]="true" [precisionDigits]="2" [sweepAngle]="120"></smart-panel-meter>
<smart-panel-meter id="narrow" label="Line" unit="Hz" [min]="49" [max]="51" value="50.02" [showValue]="true" [precisionDigits]="2" [sweepAngle]="90"></smart-panel-meter>
</div>
</section>
<section>
<h2>The mirror band</h2>
<p>
<code>mirror</code> draws the anti-parallax strip under the scale — the band an operator lines the needle up with its own reflection in, which is how a panel meter is read accurately at all. It costs nothing on screen and it is what makes the instrument recognisable to anybody who has used one.
</p>
<div class="demo-row">
<smart-panel-meter label="Without" unit="mA" [min]="0" [max]="100" value="62" [showValue]="true" [precisionDigits]="0"></smart-panel-meter>
<smart-panel-meter label="With the mirror" unit="mA" [min]="0" [max]="100" value="62" [mirror]="true" [showValue]="true" [precisionDigits]="0"></smart-panel-meter>
</div>
</section>
<section>
<h2>Centre zero, and the end stops</h2>
<p>
<code>centerZero</code> marks the rest position and draws the zero line of a ± movement. Past the ends of the scale the needle pins against the stop and the case raises an over-range flag, instead of a hand sweeping on round a dial: the reading is still reported, and the instrument says it is off scale.
</p>
<div class="demo-row">
<smart-panel-meter id="balance" label="Balance" unit="%" [min]="-50" [max]="50" value="0" [centerZero]="true" [sweepAngle]="120" [showValue]="true" [precisionDigits]="1"></smart-panel-meter>
<div class="demo-controls">
<label>Balance <input type="range" id="balanceSet" min="-70" max="70" step="0.5" value="0" /></label>
<p class="demo-log" id="balanceLog">Push it past ±50 to pin the needle.</p>
</div>
</div>
</section>
<section>
<h2>Damping</h2>
<p>
<code>damping</code> is the inertia of the coil, in milliseconds to settle. It is off by default, because a needle that lags is showing something the process is not doing, and that is the host's decision to make. The digital reading is never damped: it is the value, and the needle is the picture of it, so the instrument cannot be caught telling two stories about the same instant.
</p>
<div class="demo-row">
<smart-panel-meter id="undamped" label="Undamped" unit="mV" [min]="0" [max]="500" value="250" [showValue]="true" [precisionDigits]="0"></smart-panel-meter>
<smart-panel-meter id="damped" label="Damped 600 ms" unit="mV" [min]="0" [max]="500" value="250" [damping]="600" [showValue]="true" [precisionDigits]="0"></smart-panel-meter>
<div class="demo-controls">
<button type="button" id="noise">Feed a noisy signal</button>
<p class="demo-log">The same samples go to both. One is readable.</p>
</div>
</div>
</section>
<section>
<h2>Alarm bands, a set pointer and reading quality</h2>
<p>
<code>ranges</code> paints the bands on the plate and the needle takes the band's colour; <code>setpoint</code> with <code>showSetpoint</code> clips the red set pointer to the scale. <code>quality</code> is carried by every component that extends the scale engine: a bad reading is struck through and a stale or uncertain one is dimmed.
</p>
<div class="demo-row">
<smart-panel-meter id="pressure" label="PT-2101" unit="bar" [min]="0" [max]="16" value="11.4" [setpoint]="12" [showSetpoint]="true" [mirror]="true" [showValue]="true" [precisionDigits]="1"></smart-panel-meter>
<smart-panel-meter label="uncertain" unit="bar" [min]="0" [max]="16" value="11.4" quality="uncertain" [showValue]="true" [precisionDigits]="1"></smart-panel-meter>
<smart-panel-meter label="bad" unit="bar" [min]="0" [max]="16" value="11.4" quality="bad" [showValue]="true" [precisionDigits]="1"></smart-panel-meter>
</div>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { PanelMeterModule } from 'smart-industrial/angular/panelmeter';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - PanelMeter 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: [PanelMeterModule],
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. */
/* Panel meter overview.
*
* The noise demo is the argument for damping: the same samples go to both instruments, and
* only the damped one can be read. The digital display under each is the undamped value, so
* the two never disagree about what the process is doing - only about how fast the needle
* is allowed to say it. */
export function runDemo(): void {
const balance = document.getElementById('balance'),
balanceSet = document.getElementById('balanceSet'),
balanceLog = document.getElementById('balanceLog'),
undamped = document.getElementById('undamped'),
damped = document.getElementById('damped'),
noise = document.getElementById('noise'),
pressure = document.getElementById('pressure');
pressure.ranges = [
{ startValue: 0, endValue: 10, className: 'ok' },
{ startValue: 10, endValue: 13.5, className: 'warning' },
{ startValue: 13.5, endValue: 16, className: 'critical' }
];
pressure.showRanges = true;
balanceSet.addEventListener('input', function () {
balance.value = Number(balanceSet.value);
const over = Number(balanceSet.value) > 50,
under = Number(balanceSet.value) < -50;
balanceLog.textContent = over || under
? 'Needle against the ' + (over ? 'upper' : 'lower') + ' stop. The reading is still ' +
balanceSet.value + ' %; the instrument says it is off scale.'
: 'Reading ' + Number(balanceSet.value).toFixed(1) + ' %.';
});
/* A signal with a burst of noise on it, the kind that makes an undamped needle useless. */
let timer = null,
step = 0;
noise.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
noise.textContent = 'Feed a noisy signal';
return;
}
step = 0;
noise.textContent = 'Stop';
timer = setInterval(function () {
step += 1;
const base = 250 + 120 * Math.sin(step / 22),
jitter = (Math.random() - 0.5) * 140,
sample = Math.max(0, Math.min(500, base + jitter));
undamped.value = Math.round(sample);
damped.value = Math.round(sample);
if (step > 400) {
clearInterval(timer);
timer = null;
noise.textContent = 'Feed a noisy signal';
}
}, 60);
});
}
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-row {
display: flex;
flex-wrap: wrap;
gap: 40px;
align-items: flex-start;
padding: 18px 22px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-column {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 26px;
padding: 18px 22px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-instrument {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-controls {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 240px;
}
.demo-controls label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.demo-controls input[type="range"] {
flex: 1;
}
.demo-controls button {
align-self: flex-start;
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-controls 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 {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-panel-meter {
--smart-panel-meter-width: 210px;
}
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 { PanelMeter } from 'smart-industrial/react/panelmeter';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* Panel meter overview.
*
* The noise demo is the argument for damping: the same samples go to both instruments, and
* only the damped one can be read. The digital display under each is the undamped value, so
* the two never disagree about what the process is doing - only about how fast the needle
* is allowed to say it. */
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 balance = document.getElementById('balance'),
balanceSet = document.getElementById('balanceSet'),
balanceLog = document.getElementById('balanceLog'),
undamped = document.getElementById('undamped'),
damped = document.getElementById('damped'),
noise = document.getElementById('noise'),
pressure = document.getElementById('pressure');
pressure.ranges = [
{ startValue: 0, endValue: 10, className: 'ok' },
{ startValue: 10, endValue: 13.5, className: 'warning' },
{ startValue: 13.5, endValue: 16, className: 'critical' }
];
pressure.showRanges = true;
balanceSet.addEventListener('input', function () {
balance.value = Number(balanceSet.value);
const over = Number(balanceSet.value) > 50,
under = Number(balanceSet.value) < -50;
balanceLog.textContent = over || under
? 'Needle against the ' + (over ? 'upper' : 'lower') + ' stop. The reading is still ' +
balanceSet.value + ' %; the instrument says it is off scale.'
: 'Reading ' + Number(balanceSet.value).toFixed(1) + ' %.';
});
/* A signal with a burst of noise on it, the kind that makes an undamped needle useless. */
let timer = null,
step = 0;
noise.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
noise.textContent = 'Feed a noisy signal';
return;
}
step = 0;
noise.textContent = 'Stop';
timer = setInterval(function () {
step += 1;
const base = 250 + 120 * Math.sin(step / 22),
jitter = (Math.random() - 0.5) * 140,
sample = Math.max(0, Math.min(500, base + jitter));
undamped.value = Math.round(sample);
damped.value = Math.round(sample);
if (step > 400) {
clearInterval(timer);
timer = null;
noise.textContent = 'Feed a noisy signal';
}
}, 60);
});
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.PanelMeter draws the moving-coil instrument screwed into the front of a rack: a shallow arc of scale and a long needle pivoted low in a rectangular case.</h2>
<p>It is not a gauge with its arc cut down. A gauge is a dial — a circle of scale read against a short hand near the middle. A panel meter's geometry exists because the pointer is a physical arm: the pivot sits low, the arm reaches almost the full height of the case, and the scale is the arc that arm can sweep. With that geometry come the things a dial has no use for: the anti-parallax mirror band, end stops and an over-range flag, the damping of the coil, and the centre-zero movement.</p>
</section>
<section>
<h2>A bench supply and a bus</h2>
<p><code>min</code>, <code>max</code> and <code>value</code> are the scale and the reading; <code>unit</code> and <code>label</code> are printed on the plate. <code>sweepAngle</code> sets how far the needle travels — a moving-coil movement swings about 90 to 120 degrees.</p>
<div className="demo-row">
<PanelMeter id="bus" label="Bus" unit="V DC" min={0} max={30} value="24.1" showValue precisionDigits={1}></PanelMeter>
<PanelMeter id="current" label="Load" unit="A" min={0} max={5} value="3.2" showValue precisionDigits={2} sweepAngle={120}></PanelMeter>
<PanelMeter id="narrow" label="Line" unit="Hz" min={49} max={51} value="50.02" showValue precisionDigits={2} sweepAngle={90}></PanelMeter>
</div>
</section>
<section>
<h2>The mirror band</h2>
<p><code>mirror</code> draws the anti-parallax strip under the scale — the band an operator lines the needle up with its own reflection in, which is how a panel meter is read accurately at all. It costs nothing on screen and it is what makes the instrument recognisable to anybody who has used one.</p>
<div className="demo-row">
<PanelMeter label="Without" unit="mA" min={0} max={100} value="62" showValue precisionDigits={0}></PanelMeter>
<PanelMeter label="With the mirror" unit="mA" min={0} max={100} value="62" mirror showValue precisionDigits={0}></PanelMeter>
</div>
</section>
<section>
<h2>Centre zero, and the end stops</h2>
<p><code>centerZero</code> marks the rest position and draws the zero line of a ± movement. Past the ends of the scale the needle pins against the stop and the case raises an over-range flag, instead of a hand sweeping on round a dial: the reading is still reported, and the instrument says it is off scale.</p>
<div className="demo-row">
<PanelMeter id="balance" label="Balance" unit="%" min={-50} max={50} value="0" centerZero sweepAngle={120} showValue precisionDigits={1}></PanelMeter>
<div className="demo-controls">
<label>Balance <input type="range" id="balanceSet" min="-70" max="70" step="0.5" defaultValue="0" /></label>
<p className="demo-log" id="balanceLog">Push it past ±50 to pin the needle.</p>
</div>
</div>
</section>
<section>
<h2>Damping</h2>
<p><code>damping</code> is the inertia of the coil, in milliseconds to settle. It is off by default, because a needle that lags is showing something the process is not doing, and that is the host's decision to make. The digital reading is never damped: it is the value, and the needle is the picture of it, so the instrument cannot be caught telling two stories about the same instant.</p>
<div className="demo-row">
<PanelMeter id="undamped" label="Undamped" unit="mV" min={0} max={500} value="250" showValue precisionDigits={0}></PanelMeter>
<PanelMeter id="damped" label="Damped 600 ms" unit="mV" min={0} max={500} value="250" damping={600} showValue precisionDigits={0}></PanelMeter>
<div className="demo-controls">
<button type="button" id="noise">Feed a noisy signal</button>
<p className="demo-log">The same samples go to both. One is readable.</p>
</div>
</div>
</section>
<section>
<h2>Alarm bands, a set pointer and reading quality</h2>
<p><code>ranges</code> paints the bands on the plate and the needle takes the band's colour; <code>setpoint</code> with <code>showSetpoint</code> clips the red set pointer to the scale. <code>quality</code> is carried by every component that extends the scale engine: a bad reading is struck through and a stale or uncertain one is dimmed.</p>
<div className="demo-row">
<PanelMeter id="pressure" label="PT-2101" unit="bar" min={0} max={16} value="11.4" setpoint={12} showSetpoint mirror showValue precisionDigits={1}></PanelMeter>
<PanelMeter label="uncertain" unit="bar" min={0} max={16} value="11.4" quality="uncertain" showValue precisionDigits={1}></PanelMeter>
<PanelMeter label="bad" unit="bar" min={0} max={16} value="11.4" quality="bad" showValue precisionDigits={1}></PanelMeter>
</div>
</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-row {
display: flex;
flex-wrap: wrap;
gap: 40px;
align-items: flex-start;
padding: 18px 22px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-column {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 26px;
padding: 18px 22px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-instrument {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-controls {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 240px;
}
.demo-controls label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.demo-controls input[type="range"] {
flex: 1;
}
.demo-controls button {
align-self: flex-start;
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-controls 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 {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-panel-meter {
--smart-panel-meter-width: 210px;
}
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.PanelMeter draws the moving-coil instrument screwed into the front of a rack: a shallow arc of scale and a long needle pivoted low in a rectangular case.</h2>
<p>
It is not a gauge with its arc cut down. A gauge is a dial — a circle of scale read against a short hand near the middle. A panel meter's geometry exists because the pointer is a physical arm: the pivot sits low, the arm reaches almost the full height of the case, and the scale is the arc that arm can sweep. With that geometry come the things a dial has no use for: the anti-parallax mirror band, end stops and an over-range flag, the damping of the coil, and the centre-zero movement.
</p>
</section>
<section>
<h2>A bench supply and a bus</h2>
<p>
<code>min</code>, <code>max</code> and <code>value</code> are the scale and the reading; <code>unit</code> and <code>label</code> are printed on the plate. <code>sweepAngle</code> sets how far the needle travels — a moving-coil movement swings about 90 to 120 degrees.
</p>
<div class="demo-row">
<smart-panel-meter id="bus" label="Bus" unit="V DC" min="0" max="30" value="24.1" show-value precision-digits="1"></smart-panel-meter>
<smart-panel-meter id="current" label="Load" unit="A" min="0" max="5" value="3.2" show-value precision-digits="2" sweep-angle="120"></smart-panel-meter>
<smart-panel-meter id="narrow" label="Line" unit="Hz" min="49" max="51" value="50.02" show-value precision-digits="2" sweep-angle="90"></smart-panel-meter>
</div>
</section>
<section>
<h2>The mirror band</h2>
<p>
<code>mirror</code> draws the anti-parallax strip under the scale — the band an operator lines the needle up with its own reflection in, which is how a panel meter is read accurately at all. It costs nothing on screen and it is what makes the instrument recognisable to anybody who has used one.
</p>
<div class="demo-row">
<smart-panel-meter label="Without" unit="mA" min="0" max="100" value="62" show-value precision-digits="0"></smart-panel-meter>
<smart-panel-meter label="With the mirror" unit="mA" min="0" max="100" value="62" mirror show-value precision-digits="0"></smart-panel-meter>
</div>
</section>
<section>
<h2>Centre zero, and the end stops</h2>
<p>
<code>centerZero</code> marks the rest position and draws the zero line of a ± movement. Past the ends of the scale the needle pins against the stop and the case raises an over-range flag, instead of a hand sweeping on round a dial: the reading is still reported, and the instrument says it is off scale.
</p>
<div class="demo-row">
<smart-panel-meter id="balance" label="Balance" unit="%" min="-50" max="50" value="0" center-zero sweep-angle="120" show-value precision-digits="1"></smart-panel-meter>
<div class="demo-controls">
<label>Balance <input type="range" id="balanceSet" min="-70" max="70" step="0.5" value="0" /></label>
<p class="demo-log" id="balanceLog">Push it past ±50 to pin the needle.</p>
</div>
</div>
</section>
<section>
<h2>Damping</h2>
<p>
<code>damping</code> is the inertia of the coil, in milliseconds to settle. It is off by default, because a needle that lags is showing something the process is not doing, and that is the host's decision to make. The digital reading is never damped: it is the value, and the needle is the picture of it, so the instrument cannot be caught telling two stories about the same instant.
</p>
<div class="demo-row">
<smart-panel-meter id="undamped" label="Undamped" unit="mV" min="0" max="500" value="250" show-value precision-digits="0"></smart-panel-meter>
<smart-panel-meter id="damped" label="Damped 600 ms" unit="mV" min="0" max="500" value="250" damping="600" show-value precision-digits="0"></smart-panel-meter>
<div class="demo-controls">
<button type="button" id="noise">Feed a noisy signal</button>
<p class="demo-log">The same samples go to both. One is readable.</p>
</div>
</div>
</section>
<section>
<h2>Alarm bands, a set pointer and reading quality</h2>
<p>
<code>ranges</code> paints the bands on the plate and the needle takes the band's colour; <code>setpoint</code> with <code>showSetpoint</code> clips the red set pointer to the scale. <code>quality</code> is carried by every component that extends the scale engine: a bad reading is struck through and a stale or uncertain one is dimmed.
</p>
<div class="demo-row">
<smart-panel-meter id="pressure" label="PT-2101" unit="bar" min="0" max="16" value="11.4" setpoint="12" show-setpoint mirror show-value precision-digits="1"></smart-panel-meter>
<smart-panel-meter label="uncertain" unit="bar" min="0" max="16" value="11.4" quality="uncertain" show-value precision-digits="1"></smart-panel-meter>
<smart-panel-meter label="bad" unit="bar" min="0" max="16" value="11.4" quality="bad" show-value precision-digits="1"></smart-panel-meter>
</div>
</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.panelmeter.js";
/* Panel meter overview.
*
* The noise demo is the argument for damping: the same samples go to both instruments, and
* only the damped one can be read. The digital display under each is the undamped value, so
* the two never disagree about what the process is doing - only about how fast the needle
* is allowed to say it. */
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 balance = document.getElementById('balance'),
balanceSet = document.getElementById('balanceSet'),
balanceLog = document.getElementById('balanceLog'),
undamped = document.getElementById('undamped'),
damped = document.getElementById('damped'),
noise = document.getElementById('noise'),
pressure = document.getElementById('pressure');
pressure.ranges = [
{ startValue: 0, endValue: 10, className: 'ok' },
{ startValue: 10, endValue: 13.5, className: 'warning' },
{ startValue: 13.5, endValue: 16, className: 'critical' }
];
pressure.showRanges = true;
balanceSet.addEventListener('input', function () {
balance.value = Number(balanceSet.value);
const over = Number(balanceSet.value) > 50,
under = Number(balanceSet.value) < -50;
balanceLog.textContent = over || under
? 'Needle against the ' + (over ? 'upper' : 'lower') + ' stop. The reading is still ' +
balanceSet.value + ' %; the instrument says it is off scale.'
: 'Reading ' + Number(balanceSet.value).toFixed(1) + ' %.';
});
/* A signal with a burst of noise on it, the kind that makes an undamped needle useless. */
let timer = null,
step = 0;
noise.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
noise.textContent = 'Feed a noisy signal';
return;
}
step = 0;
noise.textContent = 'Stop';
timer = setInterval(function () {
step += 1;
const base = 250 + 120 * Math.sin(step / 22),
jitter = (Math.random() - 0.5) * 140,
sample = Math.max(0, Math.min(500, base + jitter));
undamped.value = Math.round(sample);
damped.value = Math.round(sample);
if (step > 400) {
clearInterval(timer);
timer = null;
noise.textContent = 'Feed a noisy signal';
}
}, 60);
});
});
});
}
};
</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-row {
display: flex;
flex-wrap: wrap;
gap: 40px;
align-items: flex-start;
padding: 18px 22px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-column {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 26px;
padding: 18px 22px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-instrument {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-controls {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 240px;
}
.demo-controls label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.demo-controls input[type="range"] {
flex: 1;
}
.demo-controls button {
align-self: flex-start;
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-controls 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 {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-panel-meter {
--smart-panel-meter-width: 210px;
}
</style>
What the demo shows
- A bench supply and a bus
- The mirror band
- Centre zero, and the end stops
- Damping
- Alarm bands, a set pointer and reading quality
Also in instrument readouts and controls
Bar GraphBit FieldJog PanelKnobMomentary ButtonNumeric KeypadSelector SwitchSeven SegmentThermometerTouch Keyboard