Component demo · Test and measurement
Intensity Graph
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>Intensity Graph 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.IntensityGraph displays a matrix as colour on an x and a y axis, with a colour ramp that says what the colours mean and cursors that read the cell under them.</h2>
<p>
It is the display a spectrogram, a thermal image, a die map or a sensor array ends in. In graph mode the matrix is assigned whole. In chart mode columns are pushed as they arrive and scroll along x, so a spectrogram builds up with the signal. Colour comes from a named map or from the application's own stops, over a z range that follows the data or is fixed. A cursor reads x, y and z, so a value is taken from the display by its coordinates and not by its colour.
</p>
</section>
<section>
<h2>A spectrogram of a machine run-up, as it happens</h2>
<p>
A motor runs up from 600 to 3,000 rpm over half a minute while a bearing tone at a fixed frequency and its second harmonic sit underneath. Each block of samples goes through <code>Smart.DSP.spectrum</code> and its magnitudes are pushed as one column. The 1x order climbs as the speed rises; the bearing tone does not move. Click the plot to read a cell; the arrow keys move the cursor a cell at a time.
</p>
<smart-intensity-graph id="spectrogram" label="ST-2112 run-up" mode="chart" history-length="240" x-label="Time" x-unit="s" y-label="Frequency" y-unit="Hz" z-label="Level" z-unit="dB" color-map="inferno"></smart-intensity-graph>
<div class="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="restart">Restart the run-up</button>
<label>Colour map <select id="map">
<option value="inferno">inferno</option>
<option value="viridis">viridis</option>
<option value="thermal">thermal</option>
<option value="jet">jet</option>
<option value="grey">grey</option>
</select></label>
<label><input type="checkbox" id="smooth" /> Interpolate</label>
<span id="specLog" class="demo-log">cursorChange and cellClick are listed here.</span>
</div>
</section>
<section>
<h2>A thermal image and a wafer map</h2>
<p>
Two matrices assigned whole. The thermal frame is 80 by 60 pixels from a camera on a switchgear panel, with the z range fixed to the alarm band of the connection and the temperatures read at two cursors. The wafer map is a pass/fail grid with the application's own stops, so a die is one of three colours and nothing in between.
</p>
<div class="row">
<smart-intensity-graph id="thermal" class="half" label="Panel 3, IR frame" x-label="Column" x-format="plain" y-label="Row" z-label="Temperature" z-unit="°C" color-map="thermal" interpolate></smart-intensity-graph>
<smart-intensity-graph id="wafer" class="half" label="Lot 4471, wafer 12" x-format="plain" z-label="Bin" show-grid></smart-intensity-graph>
</div>
<p class="hint">Keyboard: the plot is a tab stop. The arrow keys move the active cursor by one cell, with Shift by ten, and N makes the next cursor active. The cursor legend below each plot is text, so the readings are available to a screen reader.</p>
</section>
</div>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.intensitygraph.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" />
/* The spectrogram is fed from a generated signal so what appears can be checked against
what was put in: a 1x order that climbs with the speed, a bearing tone that does not. The
seam is push(column): a real acquisition pushes the magnitudes of each block it reads. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
window.onload = function () {
/* ── The run-up ────────────────────────────────────────────────────── */
const spectrogram = document.getElementById('spectrogram'),
specLog = document.getElementById('specLog'),
pause = document.getElementById('pause'),
restart = document.getElementById('restart'),
map = document.getElementById('map'),
smooth = document.getElementById('smooth');
const RATE = 1024, BLOCK = 512, BINS = BLOCK / 2 + 1, RUN_UP = 30, COLUMN_SECONDS = 0.25;
let elapsed = 0, phase1 = 0, phase2 = 0, timer = null;
spectrogram.yMin = 0;
spectrogram.yMax = RATE / 2;
spectrogram.zMin = -60;
spectrogram.zMax = 0;
spectrogram.cursors = [{ id: 'c1', label: 'C1', x: 8, y: 187 }];
function column() {
/* Speed runs 600 to 3000 rpm over the run-up: 10 to 50 Hz shaft, with a 2x, a
bearing tone at 187 Hz with its harmonic, and broadband noise. */
const rpm = 600 + Math.min(1, elapsed / RUN_UP) * 2400,
shaft = rpm / 60,
signal = new Float64Array(BLOCK);
for (let i = 0; i < BLOCK; i++) {
phase1 += 2 * Math.PI * shaft / RATE;
phase2 += 2 * Math.PI * 187 / RATE;
signal[i] = 1.0 * Math.sin(phase1) + 0.35 * Math.sin(2 * phase1) +
0.25 * Math.sin(phase2) + 0.1 * Math.sin(2 * phase2) +
(shaft > 42 ? 0.5 * Math.sin(3 * phase1) : 0) + 0.01 * gaussian();
}
const result = Smart.DSP.spectrum(signal, { sampleRate: RATE, window: 'hann' }),
levels = new Float64Array(BINS);
for (let i = 0; i < BINS; i++) {
levels[i] = 20 * Math.log10(Math.max(1e-6, result.magnitudes[i]));
}
return levels;
}
function tick() {
spectrogram.push(column(), elapsed);
elapsed += COLUMN_SECONDS;
}
function start() {
if (timer) {
clearInterval(timer);
}
timer = setInterval(tick, 100);
pause.textContent = 'Pause';
}
/* The first ten seconds at once, so the page opens with a picture in it. */
for (let i = 0; i < 40; i++) {
tick();
}
start();
pause.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
pause.textContent = 'Resume';
}
else {
start();
}
});
restart.addEventListener('click', function () {
elapsed = 0;
spectrogram.clear();
start();
});
map.addEventListener('change', function () {
spectrogram.colorMap = map.value;
});
smooth.addEventListener('change', function () {
spectrogram.interpolate = smooth.checked;
});
spectrogram.addEventListener('cursorChange', function (event) {
specLog.textContent = 'cursorChange: ' + event.detail.id + ' at ' + event.detail.x.toFixed(2) + ' s, ' +
event.detail.y.toFixed(1) + ' Hz reads ' + event.detail.z.toFixed(1) + ' dB';
});
spectrogram.addEventListener('cellClick', function (event) {
specLog.textContent = 'cellClick: column ' + event.detail.column + ', row ' + event.detail.row + ', ' + event.detail.z.toFixed(1) + ' dB';
});
/* ── The thermal frame ─────────────────────────────────────────────── */
const thermal = document.getElementById('thermal'),
frame = [];
for (let row = 0; row < 60; row++) {
const line = new Float64Array(80);
for (let col = 0; col < 80; col++) {
/* Ambient with a warm busbar across the middle and one hot connection. */
const busbar = Math.exp(-Math.pow((row - 30) / 6, 2)) * 22,
hot = Math.exp(-(Math.pow(col - 58, 2) + Math.pow(row - 31, 2)) / 18) * 48,
cool = Math.exp(-(Math.pow(col - 18, 2) + Math.pow(row - 12, 2)) / 60) * -4;
line[col] = 24 + busbar + hot + cool + gaussian() * 0.4;
}
frame.push(line);
}
thermal.data = frame;
thermal.zMin = 20;
thermal.zMax = 100;
thermal.cursors = [
{ id: 'hot', label: 'L2 lug', x: 58.5, y: 31.5 },
{ id: 'ref', label: 'busbar', x: 30.5, y: 30.5 }
];
/* ── The wafer map ─────────────────────────────────────────────────── */
const wafer = document.getElementById('wafer'),
dies = [],
SIZE = 26;
for (let row = 0; row < SIZE; row++) {
const line = [];
for (let col = 0; col < SIZE; col++) {
const dx = col - (SIZE - 1) / 2, dy = row - (SIZE - 1) / 2,
radius = Math.hypot(dx, dy);
if (radius > SIZE / 2 - 0.5) {
line.push(NaN);
continue;
}
/* Bin 1 pass, bin 2 marginal, bin 3 fail: an edge ring and one scratch fail. */
const edge = radius > SIZE / 2 - 2.2,
scratch = Math.abs(dy - 0.55 * dx + 3) < 0.9 && dx > -6 && dx < 5,
marginal = Math.random() < 0.06;
line.push(edge || scratch ? 3 : marginal ? 2 : 1);
}
dies.push(line);
}
wafer.data = dies;
wafer.zMin = 1;
wafer.zMax = 3;
wafer.colorStops = [[0, '#3f8f3f'], [0.49, '#3f8f3f'], [0.5, '#e08b1a'], [0.74, '#e08b1a'], [0.75, '#d62828'], [1, '#d62828']];
wafer.showRamp = false;
wafer.cursors = [{ id: 'die', label: 'die', x: 13.5, y: 8.5 }];
};
styles.css
smart-intensity-graph {
--smart-intensity-height: 380px;
}
.row {
display: flex;
flex-wrap: wrap;
gap: 18px;
}
smart-intensity-graph.half {
--smart-intensity-height: 340px;
flex: 1 1 420px;
min-width: 0;
width: auto;
}
/* ── 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.IntensityGraph displays a matrix as colour on an x and a y axis, with a colour ramp that says what the colours mean and cursors that read the cell under them.</h2>
<p>
It is the display a spectrogram, a thermal image, a die map or a sensor array ends in. In graph mode the matrix is assigned whole. In chart mode columns are pushed as they arrive and scroll along x, so a spectrogram builds up with the signal. Colour comes from a named map or from the application's own stops, over a z range that follows the data or is fixed. A cursor reads x, y and z, so a value is taken from the display by its coordinates and not by its colour.
</p>
</section>
<section>
<h2>A spectrogram of a machine run-up, as it happens</h2>
<p>
A motor runs up from 600 to 3,000 rpm over half a minute while a bearing tone at a fixed frequency and its second harmonic sit underneath. Each block of samples goes through <code>Smart.DSP.spectrum</code> and its magnitudes are pushed as one column. The 1x order climbs as the speed rises; the bearing tone does not move. Click the plot to read a cell; the arrow keys move the cursor a cell at a time.
</p>
<smart-intensity-graph id="spectrogram" label="ST-2112 run-up" mode="chart" [historyLength]="240" x-label="Time" x-unit="s" y-label="Frequency" y-unit="Hz" z-label="Level" z-unit="dB" color-map="inferno"></smart-intensity-graph>
<div class="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="restart">Restart the run-up</button>
<label>Colour map <select id="map">
<option value="inferno">inferno</option>
<option value="viridis">viridis</option>
<option value="thermal">thermal</option>
<option value="jet">jet</option>
<option value="grey">grey</option>
</select></label>
<label><input type="checkbox" id="smooth" /> Interpolate</label>
<span id="specLog" class="demo-log">cursorChange and cellClick are listed here.</span>
</div>
</section>
<section>
<h2>A thermal image and a wafer map</h2>
<p>
Two matrices assigned whole. The thermal frame is 80 by 60 pixels from a camera on a switchgear panel, with the z range fixed to the alarm band of the connection and the temperatures read at two cursors. The wafer map is a pass/fail grid with the application's own stops, so a die is one of three colours and nothing in between.
</p>
<div class="row">
<smart-intensity-graph id="thermal" class="half" label="Panel 3, IR frame" x-label="Column" x-format="plain" y-label="Row" z-label="Temperature" z-unit="°C" color-map="thermal" [interpolate]="true"></smart-intensity-graph>
<smart-intensity-graph id="wafer" class="half" label="Lot 4471, wafer 12" x-format="plain" z-label="Bin" [showGrid]="true"></smart-intensity-graph>
</div>
<p class="hint">Keyboard: the plot is a tab stop. The arrow keys move the active cursor by one cell, with Shift by ten, and N makes the next cursor active. The cursor legend below each plot is text, so the readings are available to a screen reader.</p>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { IntensityGraphModule } from 'smart-industrial/angular/intensitygraph';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - IntensityGraph 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: [IntensityGraphModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent implements AfterViewInit, OnDestroy {
private unmounted = false;
ngAfterViewInit(): void {
const root = document.querySelector('.angular-root') as HTMLElement,
pending: Promise<void>[] = [];
root.querySelectorAll('*').forEach((element: any) => {
if (typeof element.whenRendered === 'function' && !element.isRendered) {
pending.push(element.whenRendered());
}
});
Promise.all(pending).then(() => {
if (!this.unmounted) {
runDemo();
}
});
}
ngOnDestroy(): void {
this.unmounted = true;
}
}
angular/demo.ts
// @ts-nocheck
/* The HTML demo's script, as it was written: page code that feeds the elements, not a typed
API. The types are on the wrappers in app.component.ts. */
declare const Smart: any;
/* The spectrogram is fed from a generated signal so what appears can be checked against
what was put in: a 1x order that climbs with the speed, a bearing tone that does not. The
seam is push(column): a real acquisition pushes the magnitudes of each block it reads. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
export function runDemo(): void {
/* ── The run-up ────────────────────────────────────────────────────── */
const spectrogram = document.getElementById('spectrogram'),
specLog = document.getElementById('specLog'),
pause = document.getElementById('pause'),
restart = document.getElementById('restart'),
map = document.getElementById('map'),
smooth = document.getElementById('smooth');
const RATE = 1024, BLOCK = 512, BINS = BLOCK / 2 + 1, RUN_UP = 30, COLUMN_SECONDS = 0.25;
let elapsed = 0, phase1 = 0, phase2 = 0, timer = null;
spectrogram.yMin = 0;
spectrogram.yMax = RATE / 2;
spectrogram.zMin = -60;
spectrogram.zMax = 0;
spectrogram.cursors = [{ id: 'c1', label: 'C1', x: 8, y: 187 }];
function column() {
/* Speed runs 600 to 3000 rpm over the run-up: 10 to 50 Hz shaft, with a 2x, a
bearing tone at 187 Hz with its harmonic, and broadband noise. */
const rpm = 600 + Math.min(1, elapsed / RUN_UP) * 2400,
shaft = rpm / 60,
signal = new Float64Array(BLOCK);
for (let i = 0; i < BLOCK; i++) {
phase1 += 2 * Math.PI * shaft / RATE;
phase2 += 2 * Math.PI * 187 / RATE;
signal[i] = 1.0 * Math.sin(phase1) + 0.35 * Math.sin(2 * phase1) +
0.25 * Math.sin(phase2) + 0.1 * Math.sin(2 * phase2) +
(shaft > 42 ? 0.5 * Math.sin(3 * phase1) : 0) + 0.01 * gaussian();
}
const result = Smart.DSP.spectrum(signal, { sampleRate: RATE, window: 'hann' }),
levels = new Float64Array(BINS);
for (let i = 0; i < BINS; i++) {
levels[i] = 20 * Math.log10(Math.max(1e-6, result.magnitudes[i]));
}
return levels;
}
function tick() {
spectrogram.push(column(), elapsed);
elapsed += COLUMN_SECONDS;
}
function start() {
if (timer) {
clearInterval(timer);
}
timer = setInterval(tick, 100);
pause.textContent = 'Pause';
}
/* The first ten seconds at once, so the page opens with a picture in it. */
for (let i = 0; i < 40; i++) {
tick();
}
start();
pause.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
pause.textContent = 'Resume';
}
else {
start();
}
});
restart.addEventListener('click', function () {
elapsed = 0;
spectrogram.clear();
start();
});
map.addEventListener('change', function () {
spectrogram.colorMap = map.value;
});
smooth.addEventListener('change', function () {
spectrogram.interpolate = smooth.checked;
});
spectrogram.addEventListener('cursorChange', function (event) {
specLog.textContent = 'cursorChange: ' + event.detail.id + ' at ' + event.detail.x.toFixed(2) + ' s, ' +
event.detail.y.toFixed(1) + ' Hz reads ' + event.detail.z.toFixed(1) + ' dB';
});
spectrogram.addEventListener('cellClick', function (event) {
specLog.textContent = 'cellClick: column ' + event.detail.column + ', row ' + event.detail.row + ', ' + event.detail.z.toFixed(1) + ' dB';
});
/* ── The thermal frame ─────────────────────────────────────────────── */
const thermal = document.getElementById('thermal'),
frame = [];
for (let row = 0; row < 60; row++) {
const line = new Float64Array(80);
for (let col = 0; col < 80; col++) {
/* Ambient with a warm busbar across the middle and one hot connection. */
const busbar = Math.exp(-Math.pow((row - 30) / 6, 2)) * 22,
hot = Math.exp(-(Math.pow(col - 58, 2) + Math.pow(row - 31, 2)) / 18) * 48,
cool = Math.exp(-(Math.pow(col - 18, 2) + Math.pow(row - 12, 2)) / 60) * -4;
line[col] = 24 + busbar + hot + cool + gaussian() * 0.4;
}
frame.push(line);
}
thermal.data = frame;
thermal.zMin = 20;
thermal.zMax = 100;
thermal.cursors = [
{ id: 'hot', label: 'L2 lug', x: 58.5, y: 31.5 },
{ id: 'ref', label: 'busbar', x: 30.5, y: 30.5 }
];
/* ── The wafer map ─────────────────────────────────────────────────── */
const wafer = document.getElementById('wafer'),
dies = [],
SIZE = 26;
for (let row = 0; row < SIZE; row++) {
const line = [];
for (let col = 0; col < SIZE; col++) {
const dx = col - (SIZE - 1) / 2, dy = row - (SIZE - 1) / 2,
radius = Math.hypot(dx, dy);
if (radius > SIZE / 2 - 0.5) {
line.push(NaN);
continue;
}
/* Bin 1 pass, bin 2 marginal, bin 3 fail: an edge ring and one scratch fail. */
const edge = radius > SIZE / 2 - 2.2,
scratch = Math.abs(dy - 0.55 * dx + 3) < 0.9 && dx > -6 && dx < 5,
marginal = Math.random() < 0.06;
line.push(edge || scratch ? 3 : marginal ? 2 : 1);
}
dies.push(line);
}
wafer.data = dies;
wafer.zMin = 1;
wafer.zMax = 3;
wafer.colorStops = [[0, '#3f8f3f'], [0.49, '#3f8f3f'], [0.5, '#e08b1a'], [0.74, '#e08b1a'], [0.75, '#d62828'], [1, '#d62828']];
wafer.showRamp = false;
wafer.cursors = [{ id: 'die', label: 'die', x: 13.5, y: 8.5 }];
}
angular/app.component.css
smart-intensity-graph {
--smart-intensity-height: 380px;
}
.row {
display: flex;
flex-wrap: wrap;
gap: 18px;
}
smart-intensity-graph.half {
--smart-intensity-height: 340px;
flex: 1 1 420px;
min-width: 0;
width: auto;
}
/* ── 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 { IntensityGraph } from 'smart-industrial/react/intensitygraph';
const Smart = window.Smart;
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* The spectrogram is fed from a generated signal so what appears can be checked against
what was put in: a 1x order that climbs with the speed, a bearing tone that does not. The
seam is push(column): a real acquisition pushes the magnitudes of each block it reads. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
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;
}
/* ── The run-up ────────────────────────────────────────────────────── */
const spectrogram = document.getElementById('spectrogram'),
specLog = document.getElementById('specLog'),
pause = document.getElementById('pause'),
restart = document.getElementById('restart'),
map = document.getElementById('map'),
smooth = document.getElementById('smooth');
const RATE = 1024, BLOCK = 512, BINS = BLOCK / 2 + 1, RUN_UP = 30, COLUMN_SECONDS = 0.25;
let elapsed = 0, phase1 = 0, phase2 = 0, timer = null;
spectrogram.yMin = 0;
spectrogram.yMax = RATE / 2;
spectrogram.zMin = -60;
spectrogram.zMax = 0;
spectrogram.cursors = [{ id: 'c1', label: 'C1', x: 8, y: 187 }];
function column() {
/* Speed runs 600 to 3000 rpm over the run-up: 10 to 50 Hz shaft, with a 2x, a
bearing tone at 187 Hz with its harmonic, and broadband noise. */
const rpm = 600 + Math.min(1, elapsed / RUN_UP) * 2400,
shaft = rpm / 60,
signal = new Float64Array(BLOCK);
for (let i = 0; i < BLOCK; i++) {
phase1 += 2 * Math.PI * shaft / RATE;
phase2 += 2 * Math.PI * 187 / RATE;
signal[i] = 1.0 * Math.sin(phase1) + 0.35 * Math.sin(2 * phase1) +
0.25 * Math.sin(phase2) + 0.1 * Math.sin(2 * phase2) +
(shaft > 42 ? 0.5 * Math.sin(3 * phase1) : 0) + 0.01 * gaussian();
}
const result = Smart.DSP.spectrum(signal, { sampleRate: RATE, window: 'hann' }),
levels = new Float64Array(BINS);
for (let i = 0; i < BINS; i++) {
levels[i] = 20 * Math.log10(Math.max(1e-6, result.magnitudes[i]));
}
return levels;
}
function tick() {
spectrogram.push(column(), elapsed);
elapsed += COLUMN_SECONDS;
}
function start() {
if (timer) {
clearInterval(timer);
}
timer = setInterval(tick, 100);
pause.textContent = 'Pause';
}
/* The first ten seconds at once, so the page opens with a picture in it. */
for (let i = 0; i < 40; i++) {
tick();
}
start();
pause.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
pause.textContent = 'Resume';
}
else {
start();
}
});
restart.addEventListener('click', function () {
elapsed = 0;
spectrogram.clear();
start();
});
map.addEventListener('change', function () {
spectrogram.colorMap = map.value;
});
smooth.addEventListener('change', function () {
spectrogram.interpolate = smooth.checked;
});
spectrogram.addEventListener('cursorChange', function (event) {
specLog.textContent = 'cursorChange: ' + event.detail.id + ' at ' + event.detail.x.toFixed(2) + ' s, ' +
event.detail.y.toFixed(1) + ' Hz reads ' + event.detail.z.toFixed(1) + ' dB';
});
spectrogram.addEventListener('cellClick', function (event) {
specLog.textContent = 'cellClick: column ' + event.detail.column + ', row ' + event.detail.row + ', ' + event.detail.z.toFixed(1) + ' dB';
});
/* ── The thermal frame ─────────────────────────────────────────────── */
const thermal = document.getElementById('thermal'),
frame = [];
for (let row = 0; row < 60; row++) {
const line = new Float64Array(80);
for (let col = 0; col < 80; col++) {
/* Ambient with a warm busbar across the middle and one hot connection. */
const busbar = Math.exp(-Math.pow((row - 30) / 6, 2)) * 22,
hot = Math.exp(-(Math.pow(col - 58, 2) + Math.pow(row - 31, 2)) / 18) * 48,
cool = Math.exp(-(Math.pow(col - 18, 2) + Math.pow(row - 12, 2)) / 60) * -4;
line[col] = 24 + busbar + hot + cool + gaussian() * 0.4;
}
frame.push(line);
}
thermal.data = frame;
thermal.zMin = 20;
thermal.zMax = 100;
thermal.cursors = [
{ id: 'hot', label: 'L2 lug', x: 58.5, y: 31.5 },
{ id: 'ref', label: 'busbar', x: 30.5, y: 30.5 }
];
/* ── The wafer map ─────────────────────────────────────────────────── */
const wafer = document.getElementById('wafer'),
dies = [],
SIZE = 26;
for (let row = 0; row < SIZE; row++) {
const line = [];
for (let col = 0; col < SIZE; col++) {
const dx = col - (SIZE - 1) / 2, dy = row - (SIZE - 1) / 2,
radius = Math.hypot(dx, dy);
if (radius > SIZE / 2 - 0.5) {
line.push(NaN);
continue;
}
/* Bin 1 pass, bin 2 marginal, bin 3 fail: an edge ring and one scratch fail. */
const edge = radius > SIZE / 2 - 2.2,
scratch = Math.abs(dy - 0.55 * dx + 3) < 0.9 && dx > -6 && dx < 5,
marginal = Math.random() < 0.06;
line.push(edge || scratch ? 3 : marginal ? 2 : 1);
}
dies.push(line);
}
wafer.data = dies;
wafer.zMin = 1;
wafer.zMax = 3;
wafer.colorStops = [[0, '#3f8f3f'], [0.49, '#3f8f3f'], [0.5, '#e08b1a'], [0.74, '#e08b1a'], [0.75, '#d62828'], [1, '#d62828']];
wafer.showRamp = false;
wafer.cursors = [{ id: 'die', label: 'die', x: 13.5, y: 8.5 }];
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.IntensityGraph displays a matrix as colour on an x and a y axis, with a colour ramp that says what the colours mean and cursors that read the cell under them.</h2>
<p>It is the display a spectrogram, a thermal image, a die map or a sensor array ends in. In graph mode the matrix is assigned whole. In chart mode columns are pushed as they arrive and scroll along x, so a spectrogram builds up with the signal. Colour comes from a named map or from the application's own stops, over a z range that follows the data or is fixed. A cursor reads x, y and z, so a value is taken from the display by its coordinates and not by its colour.</p>
</section>
<section>
<h2>A spectrogram of a machine run-up, as it happens</h2>
<p>A motor runs up from 600 to 3,000 rpm over half a minute while a bearing tone at a fixed frequency and its second harmonic sit underneath. Each block of samples goes through <code>Smart.DSP.spectrum</code> and its magnitudes are pushed as one column. The 1x order climbs as the speed rises; the bearing tone does not move. Click the plot to read a cell; the arrow keys move the cursor a cell at a time.</p>
<IntensityGraph id="spectrogram" label="ST-2112 run-up" mode="chart" historyLength={240} xLabel="Time" xUnit="s" yLabel="Frequency" yUnit="Hz" zLabel="Level" zUnit="dB" colorMap="inferno"></IntensityGraph>
<div className="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="restart">Restart the run-up</button>
<label>
Colour map
<select id="map"><option value="inferno">inferno</option> <option value="viridis">viridis</option> <option value="thermal">thermal</option> <option value="jet">jet</option> <option value="grey">grey</option></select>
</label>
<label><input type="checkbox" id="smooth" /> Interpolate</label>
<span id="specLog" className="demo-log">cursorChange and cellClick are listed here.</span>
</div>
</section>
<section>
<h2>A thermal image and a wafer map</h2>
<p>Two matrices assigned whole. The thermal frame is 80 by 60 pixels from a camera on a switchgear panel, with the z range fixed to the alarm band of the connection and the temperatures read at two cursors. The wafer map is a pass/fail grid with the application's own stops, so a die is one of three colours and nothing in between.</p>
<div className="row">
<IntensityGraph id="thermal" className="half" label="Panel 3, IR frame" xLabel="Column" xFormat="plain" yLabel="Row" zLabel="Temperature" zUnit="°C" colorMap="thermal" interpolate></IntensityGraph>
<IntensityGraph id="wafer" className="half" label="Lot 4471, wafer 12" xFormat="plain" zLabel="Bin" showGrid></IntensityGraph>
</div>
<p className="hint">Keyboard: the plot is a tab stop. The arrow keys move the active cursor by one cell, with Shift by ten, and N makes the next cursor active. The cursor legend below each plot is text, so the readings are available to a screen reader.</p>
</section>
</div>
</div>
);
};
export default App;
react/App.css
smart-intensity-graph {
--smart-intensity-height: 380px;
}
.row {
display: flex;
flex-wrap: wrap;
gap: 18px;
}
smart-intensity-graph.half {
--smart-intensity-height: 340px;
flex: 1 1 420px;
min-width: 0;
width: auto;
}
/* ── 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.IntensityGraph displays a matrix as colour on an x and a y axis, with a colour ramp that says what the colours mean and cursors that read the cell under them.</h2>
<p>
It is the display a spectrogram, a thermal image, a die map or a sensor array ends in. In graph mode the matrix is assigned whole. In chart mode columns are pushed as they arrive and scroll along x, so a spectrogram builds up with the signal. Colour comes from a named map or from the application's own stops, over a z range that follows the data or is fixed. A cursor reads x, y and z, so a value is taken from the display by its coordinates and not by its colour.
</p>
</section>
<section>
<h2>A spectrogram of a machine run-up, as it happens</h2>
<p>
A motor runs up from 600 to 3,000 rpm over half a minute while a bearing tone at a fixed frequency and its second harmonic sit underneath. Each block of samples goes through <code>Smart.DSP.spectrum</code> and its magnitudes are pushed as one column. The 1x order climbs as the speed rises; the bearing tone does not move. Click the plot to read a cell; the arrow keys move the cursor a cell at a time.
</p>
<smart-intensity-graph id="spectrogram" label="ST-2112 run-up" mode="chart" history-length="240" x-label="Time" x-unit="s" y-label="Frequency" y-unit="Hz" z-label="Level" z-unit="dB" color-map="inferno"></smart-intensity-graph>
<div class="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="restart">Restart the run-up</button>
<label>Colour map <select id="map">
<option value="inferno">inferno</option>
<option value="viridis">viridis</option>
<option value="thermal">thermal</option>
<option value="jet">jet</option>
<option value="grey">grey</option>
</select></label>
<label><input type="checkbox" id="smooth" /> Interpolate</label>
<span id="specLog" class="demo-log">cursorChange and cellClick are listed here.</span>
</div>
</section>
<section>
<h2>A thermal image and a wafer map</h2>
<p>
Two matrices assigned whole. The thermal frame is 80 by 60 pixels from a camera on a switchgear panel, with the z range fixed to the alarm band of the connection and the temperatures read at two cursors. The wafer map is a pass/fail grid with the application's own stops, so a die is one of three colours and nothing in between.
</p>
<div class="row">
<smart-intensity-graph id="thermal" class="half" label="Panel 3, IR frame" x-label="Column" x-format="plain" y-label="Row" z-label="Temperature" z-unit="°C" color-map="thermal" interpolate></smart-intensity-graph>
<smart-intensity-graph id="wafer" class="half" label="Lot 4471, wafer 12" x-format="plain" z-label="Bin" show-grid></smart-intensity-graph>
</div>
<p class="hint">Keyboard: the plot is a tab stop. The arrow keys move the active cursor by one cell, with Shift by ten, and N makes the next cursor active. The cursor legend below each plot is text, so the readings are available to a screen reader.</p>
</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.intensitygraph.js";
const Smart = window.Smart;
/* The spectrogram is fed from a generated signal so what appears can be checked against
what was put in: a 1x order that climbs with the speed, a bearing tone that does not. The
seam is push(column): a real acquisition pushes the magnitudes of each block it reads. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
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(() => {
/* ── The run-up ────────────────────────────────────────────────────── */
const spectrogram = document.getElementById('spectrogram'),
specLog = document.getElementById('specLog'),
pause = document.getElementById('pause'),
restart = document.getElementById('restart'),
map = document.getElementById('map'),
smooth = document.getElementById('smooth');
const RATE = 1024, BLOCK = 512, BINS = BLOCK / 2 + 1, RUN_UP = 30, COLUMN_SECONDS = 0.25;
let elapsed = 0, phase1 = 0, phase2 = 0, timer = null;
spectrogram.yMin = 0;
spectrogram.yMax = RATE / 2;
spectrogram.zMin = -60;
spectrogram.zMax = 0;
spectrogram.cursors = [{ id: 'c1', label: 'C1', x: 8, y: 187 }];
function column() {
/* Speed runs 600 to 3000 rpm over the run-up: 10 to 50 Hz shaft, with a 2x, a
bearing tone at 187 Hz with its harmonic, and broadband noise. */
const rpm = 600 + Math.min(1, elapsed / RUN_UP) * 2400,
shaft = rpm / 60,
signal = new Float64Array(BLOCK);
for (let i = 0; i < BLOCK; i++) {
phase1 += 2 * Math.PI * shaft / RATE;
phase2 += 2 * Math.PI * 187 / RATE;
signal[i] = 1.0 * Math.sin(phase1) + 0.35 * Math.sin(2 * phase1) +
0.25 * Math.sin(phase2) + 0.1 * Math.sin(2 * phase2) +
(shaft > 42 ? 0.5 * Math.sin(3 * phase1) : 0) + 0.01 * gaussian();
}
const result = Smart.DSP.spectrum(signal, { sampleRate: RATE, window: 'hann' }),
levels = new Float64Array(BINS);
for (let i = 0; i < BINS; i++) {
levels[i] = 20 * Math.log10(Math.max(1e-6, result.magnitudes[i]));
}
return levels;
}
function tick() {
spectrogram.push(column(), elapsed);
elapsed += COLUMN_SECONDS;
}
function start() {
if (timer) {
clearInterval(timer);
}
timer = setInterval(tick, 100);
pause.textContent = 'Pause';
}
/* The first ten seconds at once, so the page opens with a picture in it. */
for (let i = 0; i < 40; i++) {
tick();
}
start();
pause.addEventListener('click', function () {
if (timer) {
clearInterval(timer);
timer = null;
pause.textContent = 'Resume';
}
else {
start();
}
});
restart.addEventListener('click', function () {
elapsed = 0;
spectrogram.clear();
start();
});
map.addEventListener('change', function () {
spectrogram.colorMap = map.value;
});
smooth.addEventListener('change', function () {
spectrogram.interpolate = smooth.checked;
});
spectrogram.addEventListener('cursorChange', function (event) {
specLog.textContent = 'cursorChange: ' + event.detail.id + ' at ' + event.detail.x.toFixed(2) + ' s, ' +
event.detail.y.toFixed(1) + ' Hz reads ' + event.detail.z.toFixed(1) + ' dB';
});
spectrogram.addEventListener('cellClick', function (event) {
specLog.textContent = 'cellClick: column ' + event.detail.column + ', row ' + event.detail.row + ', ' + event.detail.z.toFixed(1) + ' dB';
});
/* ── The thermal frame ─────────────────────────────────────────────── */
const thermal = document.getElementById('thermal'),
frame = [];
for (let row = 0; row < 60; row++) {
const line = new Float64Array(80);
for (let col = 0; col < 80; col++) {
/* Ambient with a warm busbar across the middle and one hot connection. */
const busbar = Math.exp(-Math.pow((row - 30) / 6, 2)) * 22,
hot = Math.exp(-(Math.pow(col - 58, 2) + Math.pow(row - 31, 2)) / 18) * 48,
cool = Math.exp(-(Math.pow(col - 18, 2) + Math.pow(row - 12, 2)) / 60) * -4;
line[col] = 24 + busbar + hot + cool + gaussian() * 0.4;
}
frame.push(line);
}
thermal.data = frame;
thermal.zMin = 20;
thermal.zMax = 100;
thermal.cursors = [
{ id: 'hot', label: 'L2 lug', x: 58.5, y: 31.5 },
{ id: 'ref', label: 'busbar', x: 30.5, y: 30.5 }
];
/* ── The wafer map ─────────────────────────────────────────────────── */
const wafer = document.getElementById('wafer'),
dies = [],
SIZE = 26;
for (let row = 0; row < SIZE; row++) {
const line = [];
for (let col = 0; col < SIZE; col++) {
const dx = col - (SIZE - 1) / 2, dy = row - (SIZE - 1) / 2,
radius = Math.hypot(dx, dy);
if (radius > SIZE / 2 - 0.5) {
line.push(NaN);
continue;
}
/* Bin 1 pass, bin 2 marginal, bin 3 fail: an edge ring and one scratch fail. */
const edge = radius > SIZE / 2 - 2.2,
scratch = Math.abs(dy - 0.55 * dx + 3) < 0.9 && dx > -6 && dx < 5,
marginal = Math.random() < 0.06;
line.push(edge || scratch ? 3 : marginal ? 2 : 1);
}
dies.push(line);
}
wafer.data = dies;
wafer.zMin = 1;
wafer.zMax = 3;
wafer.colorStops = [[0, '#3f8f3f'], [0.49, '#3f8f3f'], [0.5, '#e08b1a'], [0.74, '#e08b1a'], [0.75, '#d62828'], [1, '#d62828']];
wafer.showRamp = false;
wafer.cursors = [{ id: 'die', label: 'die', x: 13.5, y: 8.5 }];
});
});
}
};
</script>
<style>
smart-intensity-graph {
--smart-intensity-height: 380px;
}
.row {
display: flex;
flex-wrap: wrap;
gap: 18px;
}
smart-intensity-graph.half {
--smart-intensity-height: 340px;
flex: 1 1 420px;
min-width: 0;
width: auto;
}
/* ── 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
- A spectrogram of a machine run-up, as it happens
- A thermal image and a wafer map
On the operator screens
Also in test and measurement
Bode PlotDigital WaveformOctave BandsPolar PlotResource PickerWaveform Graph