Component demo · Trends and signals
Spectrum
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>Spectrum 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.Spectrum is a spectrum analyzer display. Blocks of samples are pushed to the component and shown as their frequency content, with the controls of an instrument front panel.</h2>
<p>
The component provides a logarithmic frequency axis, levels in dB against a stated reference, averaging that settles the noise floor, max hold, peak markers that read frequency and level, and a THD readout. The FFT and the window functions are provided by <code>Smart.DSP</code>, which applies the amplitude correction of the selected window, so the levels shown are correct for every window.
</p>
</section>
<section>
<h2>A 1 kHz tone with distortion and noise at 48 kS/s</h2>
<p>
Blocks of 4096 samples arrive twenty times per second. The tone is 0.5 V peak with second and third harmonics at −40 and −46 dB, which gives a THD of about 1.1%. Compare this with the THD readout. Enable averaging to settle the noise floor, and enable max hold and sweep the tone frequency.
</p>
<smart-spectrum id="audio" sample-rate="48000" unit="V" show-thd show-peaks="4" min-prominence="15" min-level="-110" max-level="0"></smart-spectrum>
<div class="demo-actions">
<label>Averaging
<select id="averaging">
<option value="none" selected>none</option>
<option value="linear">linear, 16</option>
<option value="exponential">exponential, 16</option>
</select>
</label>
<label>Window
<select id="window">
<option value="rectangular">rectangular</option>
<option value="hann" selected>hann</option>
<option value="hamming">hamming</option>
<option value="blackman">blackman</option>
<option value="blackman-harris">blackman-harris</option>
<option value="flat-top">flat-top</option>
</select>
</label>
<label>Axis
<select id="axis">
<option value="linear" selected>linear</option>
<option value="log">log</option>
</select>
</label>
<label><input type="checkbox" id="hold" /> Max hold</label>
<label>Tone <input type="range" id="tone" min="200" max="8000" step="10" value="1000" /> <span id="toneValue">1000 Hz</span></label>
<button type="button" id="pause">Pause</button>
<button type="button" id="clear">Clear</button>
</div>
<p class="hint">
Change the window and observe the peak reading. A flat-top window reads amplitude to within 0.01 dB but spreads the peak in frequency; a rectangular window does the opposite. The readout reflects the selected window in both cases.
</p>
</section>
<section>
<h2>Bearing vibration on a logarithmic axis with the shaft speed marked</h2>
<p>
A motor at 1490 rpm (24.8 Hz) with an outer-race bearing defect at 3.57 times the shaft speed and its harmonics, over 1/f noise. Markers supplied by the application are placed at the shaft frequency and the defect frequency, so the peaks can be compared with the expected frequencies. Peaks are found on the averaged trace by prominence, so a single strong line is reported once.
</p>
<smart-spectrum id="vibration" sample-rate="10240" unit="g" frequency-scale="log" min-frequency="5"
averaging="exponential" averages="8" show-peaks="4" min-prominence="8"
min-level="-90" max-level="-10" label="Bearing DE, radial"></smart-spectrum>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.spectrum.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" />
/* Both analyzers are fed by generators here, with known content, so what they report can
be checked against what was put in. The seam is push(block) - replace a generator with
a DAQ callback, a WebSocket delivering sample blocks or a file reader and nothing else
on the page changes. */
/* Gaussian noise from two uniforms. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
window.onload = function () {
/* ── A distorted tone at 48 kS/s ────────────────────────────────────── */
const audio = document.getElementById('audio'),
averaging = document.getElementById('averaging'),
windowSelect = document.getElementById('window'),
axis = document.getElementById('axis'),
hold = document.getElementById('hold'),
tone = document.getElementById('tone'),
toneValue = document.getElementById('toneValue'),
pause = document.getElementById('pause'),
clear = document.getElementById('clear');
const RATE = 48000, BLOCK = 4096;
let toneHz = 1000,
phase = 0;
/* 0.5 V peak fundamental, second harmonic at -40 dB, third at -46 dB: a THD of
sqrt(0.01^2 + 0.005^2) = 1.12 %. Noise at -80 dB or so per bin. */
function audioBlock() {
const block = new Float64Array(BLOCK),
step = 2 * Math.PI * toneHz / RATE;
for (let i = 0; i < BLOCK; i++) {
block[i] = 0.5 * Math.sin(phase) +
0.5 * 0.01 * Math.sin(2 * phase) +
0.5 * 0.005 * Math.sin(3 * phase) +
gaussian() * 0.0008;
phase += step;
}
phase %= 2 * Math.PI;
return block;
}
setInterval(function () {
audio.push(audioBlock());
}, 50);
averaging.addEventListener('change', function () {
audio.averages = 16;
audio.averaging = averaging.value;
});
windowSelect.addEventListener('change', function () {
audio.window = windowSelect.value;
});
axis.addEventListener('change', function () {
audio.frequencyScale = axis.value;
});
hold.addEventListener('change', function () {
audio.peakHold = hold.checked;
});
tone.addEventListener('input', function () {
toneHz = Number(tone.value);
toneValue.textContent = toneHz + ' Hz';
});
pause.addEventListener('click', function () {
audio.paused = !audio.paused;
pause.textContent = audio.paused ? 'Resume' : 'Pause';
});
clear.addEventListener('click', function () {
audio.clear();
});
/* ── Bearing vibration at 10.24 kS/s ──────────────────────────────── */
const vibration = document.getElementById('vibration'),
VRATE = 10240, VBLOCK = 8192,
shaft = 1490 / 60,
bpfo = 3.57 * shaft;
let vphase = 0;
vibration.markers = [
{ frequency: shaft, label: '1× ' + shaft.toFixed(1) + ' Hz' },
{ frequency: bpfo, label: 'BPFO ' + bpfo.toFixed(1) + ' Hz' }
];
/* Shaft imbalance at 1x, a defect at BPFO with three harmonics that decay, and a
coloured noise floor built from a one-pole filter over white noise. */
function vibrationBlock() {
const block = new Float64Array(VBLOCK),
dt = 1 / VRATE;
let coloured = 0;
for (let i = 0; i < VBLOCK; i++) {
const t = vphase + i * dt;
coloured = coloured * 0.97 + gaussian() * 0.004;
block[i] = 0.05 * Math.sin(2 * Math.PI * shaft * t) +
0.02 * Math.sin(2 * Math.PI * bpfo * t) +
0.012 * Math.sin(2 * Math.PI * 2 * bpfo * t + 0.7) +
0.006 * Math.sin(2 * Math.PI * 3 * bpfo * t + 1.9) +
coloured + gaussian() * 0.0006;
}
vphase += VBLOCK * dt;
return block;
}
setInterval(function () {
vibration.push(vibrationBlock());
}, 200);
};
styles.css
smart-spectrum {
width: 100%;
}
#audio {
--smart-spectrum-height: 380px;
}
.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;
}
.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.Spectrum is a spectrum analyzer display. Blocks of samples are pushed to the component and shown as their frequency content, with the controls of an instrument front panel.</h2>
<p>
The component provides a logarithmic frequency axis, levels in dB against a stated reference, averaging that settles the noise floor, max hold, peak markers that read frequency and level, and a THD readout. The FFT and the window functions are provided by <code>Smart.DSP</code>, which applies the amplitude correction of the selected window, so the levels shown are correct for every window.
</p>
</section>
<section>
<h2>A 1 kHz tone with distortion and noise at 48 kS/s</h2>
<p>
Blocks of 4096 samples arrive twenty times per second. The tone is 0.5 V peak with second and third harmonics at −40 and −46 dB, which gives a THD of about 1.1%. Compare this with the THD readout. Enable averaging to settle the noise floor, and enable max hold and sweep the tone frequency.
</p>
<smart-spectrum id="audio" [sampleRate]="48000" unit="V" [showThd]="true" [showPeaks]="4" [minProminence]="15" [minLevel]="-110" [maxLevel]="0"></smart-spectrum>
<div class="demo-actions">
<label>Averaging
<select id="averaging">
<option value="none" selected>none</option>
<option value="linear">linear, 16</option>
<option value="exponential">exponential, 16</option>
</select>
</label>
<label>Window
<select id="window">
<option value="rectangular">rectangular</option>
<option value="hann" selected>hann</option>
<option value="hamming">hamming</option>
<option value="blackman">blackman</option>
<option value="blackman-harris">blackman-harris</option>
<option value="flat-top">flat-top</option>
</select>
</label>
<label>Axis
<select id="axis">
<option value="linear" selected>linear</option>
<option value="log">log</option>
</select>
</label>
<label><input type="checkbox" id="hold" /> Max hold</label>
<label>Tone <input type="range" id="tone" min="200" max="8000" step="10" value="1000" /> <span id="toneValue">1000 Hz</span></label>
<button type="button" id="pause">Pause</button>
<button type="button" id="clear">Clear</button>
</div>
<p class="hint">
Change the window and observe the peak reading. A flat-top window reads amplitude to within 0.01 dB but spreads the peak in frequency; a rectangular window does the opposite. The readout reflects the selected window in both cases.
</p>
</section>
<section>
<h2>Bearing vibration on a logarithmic axis with the shaft speed marked</h2>
<p>
A motor at 1490 rpm (24.8 Hz) with an outer-race bearing defect at 3.57 times the shaft speed and its harmonics, over 1/f noise. Markers supplied by the application are placed at the shaft frequency and the defect frequency, so the peaks can be compared with the expected frequencies. Peaks are found on the averaged trace by prominence, so a single strong line is reported once.
</p>
<smart-spectrum id="vibration" [sampleRate]="10240" unit="g" frequency-scale="log" [minFrequency]="5"
averaging="exponential" [averages]="8" [showPeaks]="4" [minProminence]="8" [minLevel]="-90" [maxLevel]="-10" label="Bearing DE, radial"></smart-spectrum>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { SpectrumModule } from 'smart-industrial/angular/spectrum';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - Spectrum 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: [SpectrumModule],
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. */
/* Both analyzers are fed by generators here, with known content, so what they report can
be checked against what was put in. The seam is push(block) - replace a generator with
a DAQ callback, a WebSocket delivering sample blocks or a file reader and nothing else
on the page changes. */
/* Gaussian noise from two uniforms. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
export function runDemo(): void {
/* ── A distorted tone at 48 kS/s ────────────────────────────────────── */
const audio = document.getElementById('audio'),
averaging = document.getElementById('averaging'),
windowSelect = document.getElementById('window'),
axis = document.getElementById('axis'),
hold = document.getElementById('hold'),
tone = document.getElementById('tone'),
toneValue = document.getElementById('toneValue'),
pause = document.getElementById('pause'),
clear = document.getElementById('clear');
const RATE = 48000, BLOCK = 4096;
let toneHz = 1000,
phase = 0;
/* 0.5 V peak fundamental, second harmonic at -40 dB, third at -46 dB: a THD of
sqrt(0.01^2 + 0.005^2) = 1.12 %. Noise at -80 dB or so per bin. */
function audioBlock() {
const block = new Float64Array(BLOCK),
step = 2 * Math.PI * toneHz / RATE;
for (let i = 0; i < BLOCK; i++) {
block[i] = 0.5 * Math.sin(phase) +
0.5 * 0.01 * Math.sin(2 * phase) +
0.5 * 0.005 * Math.sin(3 * phase) +
gaussian() * 0.0008;
phase += step;
}
phase %= 2 * Math.PI;
return block;
}
setInterval(function () {
audio.push(audioBlock());
}, 50);
averaging.addEventListener('change', function () {
audio.averages = 16;
audio.averaging = averaging.value;
});
windowSelect.addEventListener('change', function () {
audio.window = windowSelect.value;
});
axis.addEventListener('change', function () {
audio.frequencyScale = axis.value;
});
hold.addEventListener('change', function () {
audio.peakHold = hold.checked;
});
tone.addEventListener('input', function () {
toneHz = Number(tone.value);
toneValue.textContent = toneHz + ' Hz';
});
pause.addEventListener('click', function () {
audio.paused = !audio.paused;
pause.textContent = audio.paused ? 'Resume' : 'Pause';
});
clear.addEventListener('click', function () {
audio.clear();
});
/* ── Bearing vibration at 10.24 kS/s ──────────────────────────────── */
const vibration = document.getElementById('vibration'),
VRATE = 10240, VBLOCK = 8192,
shaft = 1490 / 60,
bpfo = 3.57 * shaft;
let vphase = 0;
vibration.markers = [
{ frequency: shaft, label: '1× ' + shaft.toFixed(1) + ' Hz' },
{ frequency: bpfo, label: 'BPFO ' + bpfo.toFixed(1) + ' Hz' }
];
/* Shaft imbalance at 1x, a defect at BPFO with three harmonics that decay, and a
coloured noise floor built from a one-pole filter over white noise. */
function vibrationBlock() {
const block = new Float64Array(VBLOCK),
dt = 1 / VRATE;
let coloured = 0;
for (let i = 0; i < VBLOCK; i++) {
const t = vphase + i * dt;
coloured = coloured * 0.97 + gaussian() * 0.004;
block[i] = 0.05 * Math.sin(2 * Math.PI * shaft * t) +
0.02 * Math.sin(2 * Math.PI * bpfo * t) +
0.012 * Math.sin(2 * Math.PI * 2 * bpfo * t + 0.7) +
0.006 * Math.sin(2 * Math.PI * 3 * bpfo * t + 1.9) +
coloured + gaussian() * 0.0006;
}
vphase += VBLOCK * dt;
return block;
}
setInterval(function () {
vibration.push(vibrationBlock());
}, 200);
}
angular/app.component.css
smart-spectrum {
width: 100%;
}
#audio {
--smart-spectrum-height: 380px;
}
.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;
}
.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 { Spectrum } from 'smart-industrial/react/spectrum';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* Both analyzers are fed by generators here, with known content, so what they report can
be checked against what was put in. The seam is push(block) - replace a generator with
a DAQ callback, a WebSocket delivering sample blocks or a file reader and nothing else
on the page changes. */
/* Gaussian noise from two uniforms. */
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;
}
/* ── A distorted tone at 48 kS/s ────────────────────────────────────── */
const audio = document.getElementById('audio'),
averaging = document.getElementById('averaging'),
windowSelect = document.getElementById('window'),
axis = document.getElementById('axis'),
hold = document.getElementById('hold'),
tone = document.getElementById('tone'),
toneValue = document.getElementById('toneValue'),
pause = document.getElementById('pause'),
clear = document.getElementById('clear');
const RATE = 48000, BLOCK = 4096;
let toneHz = 1000,
phase = 0;
/* 0.5 V peak fundamental, second harmonic at -40 dB, third at -46 dB: a THD of
sqrt(0.01^2 + 0.005^2) = 1.12 %. Noise at -80 dB or so per bin. */
function audioBlock() {
const block = new Float64Array(BLOCK),
step = 2 * Math.PI * toneHz / RATE;
for (let i = 0; i < BLOCK; i++) {
block[i] = 0.5 * Math.sin(phase) +
0.5 * 0.01 * Math.sin(2 * phase) +
0.5 * 0.005 * Math.sin(3 * phase) +
gaussian() * 0.0008;
phase += step;
}
phase %= 2 * Math.PI;
return block;
}
setInterval(function () {
audio.push(audioBlock());
}, 50);
averaging.addEventListener('change', function () {
audio.averages = 16;
audio.averaging = averaging.value;
});
windowSelect.addEventListener('change', function () {
audio.window = windowSelect.value;
});
axis.addEventListener('change', function () {
audio.frequencyScale = axis.value;
});
hold.addEventListener('change', function () {
audio.peakHold = hold.checked;
});
tone.addEventListener('input', function () {
toneHz = Number(tone.value);
toneValue.textContent = toneHz + ' Hz';
});
pause.addEventListener('click', function () {
audio.paused = !audio.paused;
pause.textContent = audio.paused ? 'Resume' : 'Pause';
});
clear.addEventListener('click', function () {
audio.clear();
});
/* ── Bearing vibration at 10.24 kS/s ──────────────────────────────── */
const vibration = document.getElementById('vibration'),
VRATE = 10240, VBLOCK = 8192,
shaft = 1490 / 60,
bpfo = 3.57 * shaft;
let vphase = 0;
vibration.markers = [
{ frequency: shaft, label: '1× ' + shaft.toFixed(1) + ' Hz' },
{ frequency: bpfo, label: 'BPFO ' + bpfo.toFixed(1) + ' Hz' }
];
/* Shaft imbalance at 1x, a defect at BPFO with three harmonics that decay, and a
coloured noise floor built from a one-pole filter over white noise. */
function vibrationBlock() {
const block = new Float64Array(VBLOCK),
dt = 1 / VRATE;
let coloured = 0;
for (let i = 0; i < VBLOCK; i++) {
const t = vphase + i * dt;
coloured = coloured * 0.97 + gaussian() * 0.004;
block[i] = 0.05 * Math.sin(2 * Math.PI * shaft * t) +
0.02 * Math.sin(2 * Math.PI * bpfo * t) +
0.012 * Math.sin(2 * Math.PI * 2 * bpfo * t + 0.7) +
0.006 * Math.sin(2 * Math.PI * 3 * bpfo * t + 1.9) +
coloured + gaussian() * 0.0006;
}
vphase += VBLOCK * dt;
return block;
}
setInterval(function () {
vibration.push(vibrationBlock());
}, 200);
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.Spectrum is a spectrum analyzer display. Blocks of samples are pushed to the component and shown as their frequency content, with the controls of an instrument front panel.</h2>
<p>The component provides a logarithmic frequency axis, levels in dB against a stated reference, averaging that settles the noise floor, max hold, peak markers that read frequency and level, and a THD readout. The FFT and the window functions are provided by <code>Smart.DSP</code>, which applies the amplitude correction of the selected window, so the levels shown are correct for every window.</p>
</section>
<section>
<h2>A 1 kHz tone with distortion and noise at 48 kS/s</h2>
<p>Blocks of 4096 samples arrive twenty times per second. The tone is 0.5 V peak with second and third harmonics at −40 and −46 dB, which gives a THD of about 1.1%. Compare this with the THD readout. Enable averaging to settle the noise floor, and enable max hold and sweep the tone frequency.</p>
<Spectrum id="audio" sampleRate={48000} unit="V" showThd showPeaks={4} minProminence={15} minLevel={-110} maxLevel={0}></Spectrum>
<div className="demo-actions">
<label>
Averaging
<select id="averaging" defaultValue="none"><option value="none">none</option> <option value="linear">linear, 16</option> <option value="exponential">exponential, 16</option></select>
</label>
<label>
Window
<select id="window" defaultValue="hann"><option value="rectangular">rectangular</option> <option value="hann">hann</option> <option value="hamming">hamming</option> <option value="blackman">blackman</option> <option value="blackman-harris">blackman-harris</option> <option value="flat-top">flat-top</option></select>
</label>
<label>
Axis
<select id="axis" defaultValue="linear"><option value="linear">linear</option> <option value="log">log</option></select>
</label>
<label><input type="checkbox" id="hold" /> Max hold</label>
<label>Tone <input type="range" id="tone" min="200" max="8000" step="10" defaultValue="1000" /> <span id="toneValue">1000 Hz</span></label>
<button type="button" id="pause">Pause</button>
<button type="button" id="clear">Clear</button>
</div>
<p className="hint">Change the window and observe the peak reading. A flat-top window reads amplitude to within 0.01 dB but spreads the peak in frequency; a rectangular window does the opposite. The readout reflects the selected window in both cases.</p>
</section>
<section>
<h2>Bearing vibration on a logarithmic axis with the shaft speed marked</h2>
<p>A motor at 1490 rpm (24.8 Hz) with an outer-race bearing defect at 3.57 times the shaft speed and its harmonics, over 1/f noise. Markers supplied by the application are placed at the shaft frequency and the defect frequency, so the peaks can be compared with the expected frequencies. Peaks are found on the averaged trace by prominence, so a single strong line is reported once.</p>
<Spectrum id="vibration" sampleRate={10240} unit="g" frequencyScale="log" minFrequency={5} averaging="exponential" averages={8} showPeaks={4} minProminence={8} minLevel={-90} maxLevel={-10} label="Bearing DE, radial"></Spectrum>
</section>
</div>
</div>
);
};
export default App;
react/App.css
smart-spectrum {
width: 100%;
}
#audio {
--smart-spectrum-height: 380px;
}
.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;
}
.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.Spectrum is a spectrum analyzer display. Blocks of samples are pushed to the component and shown as their frequency content, with the controls of an instrument front panel.</h2>
<p>
The component provides a logarithmic frequency axis, levels in dB against a stated reference, averaging that settles the noise floor, max hold, peak markers that read frequency and level, and a THD readout. The FFT and the window functions are provided by <code>Smart.DSP</code>, which applies the amplitude correction of the selected window, so the levels shown are correct for every window.
</p>
</section>
<section>
<h2>A 1 kHz tone with distortion and noise at 48 kS/s</h2>
<p>
Blocks of 4096 samples arrive twenty times per second. The tone is 0.5 V peak with second and third harmonics at −40 and −46 dB, which gives a THD of about 1.1%. Compare this with the THD readout. Enable averaging to settle the noise floor, and enable max hold and sweep the tone frequency.
</p>
<smart-spectrum id="audio" sample-rate="48000" unit="V" show-thd show-peaks="4" min-prominence="15" min-level="-110" max-level="0"></smart-spectrum>
<div class="demo-actions">
<label>Averaging
<select id="averaging">
<option value="none" selected>none</option>
<option value="linear">linear, 16</option>
<option value="exponential">exponential, 16</option>
</select>
</label>
<label>Window
<select id="window">
<option value="rectangular">rectangular</option>
<option value="hann" selected>hann</option>
<option value="hamming">hamming</option>
<option value="blackman">blackman</option>
<option value="blackman-harris">blackman-harris</option>
<option value="flat-top">flat-top</option>
</select>
</label>
<label>Axis
<select id="axis">
<option value="linear" selected>linear</option>
<option value="log">log</option>
</select>
</label>
<label><input type="checkbox" id="hold" /> Max hold</label>
<label>Tone <input type="range" id="tone" min="200" max="8000" step="10" value="1000" /> <span id="toneValue">1000 Hz</span></label>
<button type="button" id="pause">Pause</button>
<button type="button" id="clear">Clear</button>
</div>
<p class="hint">
Change the window and observe the peak reading. A flat-top window reads amplitude to within 0.01 dB but spreads the peak in frequency; a rectangular window does the opposite. The readout reflects the selected window in both cases.
</p>
</section>
<section>
<h2>Bearing vibration on a logarithmic axis with the shaft speed marked</h2>
<p>
A motor at 1490 rpm (24.8 Hz) with an outer-race bearing defect at 3.57 times the shaft speed and its harmonics, over 1/f noise. Markers supplied by the application are placed at the shaft frequency and the defect frequency, so the peaks can be compared with the expected frequencies. Peaks are found on the averaged trace by prominence, so a single strong line is reported once.
</p>
<smart-spectrum id="vibration" sample-rate="10240" unit="g" frequency-scale="log" min-frequency="5"
averaging="exponential" averages="8" show-peaks="4" min-prominence="8"
min-level="-90" max-level="-10" label="Bearing DE, radial"></smart-spectrum>
</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.spectrum.js";
/* Both analyzers are fed by generators here, with known content, so what they report can
be checked against what was put in. The seam is push(block) - replace a generator with
a DAQ callback, a WebSocket delivering sample blocks or a file reader and nothing else
on the page changes. */
/* Gaussian noise from two uniforms. */
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(() => {
/* ── A distorted tone at 48 kS/s ────────────────────────────────────── */
const audio = document.getElementById('audio'),
averaging = document.getElementById('averaging'),
windowSelect = document.getElementById('window'),
axis = document.getElementById('axis'),
hold = document.getElementById('hold'),
tone = document.getElementById('tone'),
toneValue = document.getElementById('toneValue'),
pause = document.getElementById('pause'),
clear = document.getElementById('clear');
const RATE = 48000, BLOCK = 4096;
let toneHz = 1000,
phase = 0;
/* 0.5 V peak fundamental, second harmonic at -40 dB, third at -46 dB: a THD of
sqrt(0.01^2 + 0.005^2) = 1.12 %. Noise at -80 dB or so per bin. */
function audioBlock() {
const block = new Float64Array(BLOCK),
step = 2 * Math.PI * toneHz / RATE;
for (let i = 0; i < BLOCK; i++) {
block[i] = 0.5 * Math.sin(phase) +
0.5 * 0.01 * Math.sin(2 * phase) +
0.5 * 0.005 * Math.sin(3 * phase) +
gaussian() * 0.0008;
phase += step;
}
phase %= 2 * Math.PI;
return block;
}
setInterval(function () {
audio.push(audioBlock());
}, 50);
averaging.addEventListener('change', function () {
audio.averages = 16;
audio.averaging = averaging.value;
});
windowSelect.addEventListener('change', function () {
audio.window = windowSelect.value;
});
axis.addEventListener('change', function () {
audio.frequencyScale = axis.value;
});
hold.addEventListener('change', function () {
audio.peakHold = hold.checked;
});
tone.addEventListener('input', function () {
toneHz = Number(tone.value);
toneValue.textContent = toneHz + ' Hz';
});
pause.addEventListener('click', function () {
audio.paused = !audio.paused;
pause.textContent = audio.paused ? 'Resume' : 'Pause';
});
clear.addEventListener('click', function () {
audio.clear();
});
/* ── Bearing vibration at 10.24 kS/s ──────────────────────────────── */
const vibration = document.getElementById('vibration'),
VRATE = 10240, VBLOCK = 8192,
shaft = 1490 / 60,
bpfo = 3.57 * shaft;
let vphase = 0;
vibration.markers = [
{ frequency: shaft, label: '1× ' + shaft.toFixed(1) + ' Hz' },
{ frequency: bpfo, label: 'BPFO ' + bpfo.toFixed(1) + ' Hz' }
];
/* Shaft imbalance at 1x, a defect at BPFO with three harmonics that decay, and a
coloured noise floor built from a one-pole filter over white noise. */
function vibrationBlock() {
const block = new Float64Array(VBLOCK),
dt = 1 / VRATE;
let coloured = 0;
for (let i = 0; i < VBLOCK; i++) {
const t = vphase + i * dt;
coloured = coloured * 0.97 + gaussian() * 0.004;
block[i] = 0.05 * Math.sin(2 * Math.PI * shaft * t) +
0.02 * Math.sin(2 * Math.PI * bpfo * t) +
0.012 * Math.sin(2 * Math.PI * 2 * bpfo * t + 0.7) +
0.006 * Math.sin(2 * Math.PI * 3 * bpfo * t + 1.9) +
coloured + gaussian() * 0.0006;
}
vphase += VBLOCK * dt;
return block;
}
setInterval(function () {
vibration.push(vibrationBlock());
}, 200);
});
});
}
};
</script>
<style>
smart-spectrum {
width: 100%;
}
#audio {
--smart-spectrum-height: 380px;
}
.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;
}
.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
- A 1 kHz tone with distortion and noise at 48 kS/s
- Bearing vibration on a logarithmic axis with the shaft speed marked