Component demo · Trends and signals
Scope
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>Scope 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.Scope is an oscilloscope display with a trigger that keeps a repetitive signal still, a graticule with divisions, cursors and automatic measurements taken from the captured record.</h2>
<p>
A strip chart shows a signal passing by. A scope shows the same cycle of the signal on every capture, because every capture starts at the trigger point. Samples arrive in blocks through <code>push()</code> at the rate of the digitizer; the trigger runs over each block and the screen shows one capture around the trigger.
</p>
</section>
<section>
<h2>A 2 kHz PWM gate drive and the switched current at 1 MS/s</h2>
<p>
Channel 1 is the gate voltage, a 0 to 12 V square wave with a ringing edge. Channel 2 is the inductor current, a triangle wave that lags the gate. Trigger on the rising edge of the gate to hold the picture still. Move the trigger level above 12 V and nothing triggers: <em>auto</em> mode free-runs and <em>normal</em> mode keeps the last capture. Place cursors on the edge to read the rise time and compare it with the automatic measurement.
</p>
<smart-scope id="drive" sample-rate="1000000" time-per-division="0.0001" trigger-level="6"
trigger-hysteresis="0.5" label="Gate drive"></smart-scope>
<div class="demo-actions">
<label>Trigger
<select id="triggerMode">
<option value="auto" selected>auto</option>
<option value="normal">normal</option>
</select>
</label>
<button type="button" id="single">Single</button>
<button type="button" id="run">Run</button>
<label>Level <input type="range" id="level" min="-2" max="16" step="0.5" value="6" /> <span id="levelValue">6.0 V</span></label>
<label>Edge
<select id="edge">
<option value="rising" selected>rising</option>
<option value="falling">falling</option>
</select>
</label>
<label>Time/div
<select id="timebase">
<option value="0.00002">20 µs</option>
<option value="0.00005">50 µs</option>
<option value="0.0001" selected>100 µs</option>
<option value="0.0002">200 µs</option>
<option value="0.0005">500 µs</option>
</select>
</label>
<label><input type="checkbox" id="cursors" /> Cursors on the edge</label>
<button type="button" id="autoSet">Auto set</button>
</div>
<p class="hint">
Keyboard: the legend is a single tab stop. Use the arrow keys to move between channels and Enter or Space to show or hide one. Measurements are taken from the captured record, so they do not depend on whether the channel is displayed.
</p>
</section>
<section>
<h2>XY mode: a Lissajous figure</h2>
<p>
XY mode draws the second channel against the first. Two sine waves with a 3:2 frequency ratio draw a Lissajous figure, which was used to compare phase and frequency before counters were common. The figure drifts as the phase changes.
</p>
<smart-scope id="xy" mode="xy" sample-rate="100000" time-per-division="0.002" show-measurements="false"
trigger-mode="auto" label="XY"></smart-scope>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.scope.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 scopes are fed by generators with known content, so what the measurements report
can be checked against what was put in. The seam is push(block) - replace a generator
with a digitizer callback and nothing else on the page changes. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
window.onload = function () {
/* ── Gate drive ─────────────────────────────────────────────────────── */
const drive = document.getElementById('drive'),
triggerMode = document.getElementById('triggerMode'),
single = document.getElementById('single'),
run = document.getElementById('run'),
level = document.getElementById('level'),
levelValue = document.getElementById('levelValue'),
edge = document.getElementById('edge'),
timebase = document.getElementById('timebase'),
cursors = document.getElementById('cursors'),
autoSet = document.getElementById('autoSet');
const RATE = 1000000, BLOCK = 20000, PWM = 2000, DUTY = 0.35, RISE = 2.2e-6;
let t = 0;
drive.channels = [
{ field: 'gate', label: 'CH1 gate', unit: 'V', voltsPerDivision: 5, offset: -2 },
{ field: 'current', label: 'CH2 I', unit: 'A', voltsPerDivision: 2, offset: 1 }
];
drive.measurements = ['vpp', 'frequency', 'riseTime', 'dutyCycle'];
/* A square wave with an exponential edge and a damped ring after it; the current is
the triangle an inductor draws from it, lagging by the switching. */
function driveBlock() {
const gate = new Float64Array(BLOCK),
current = new Float64Array(BLOCK),
period = 1 / PWM;
for (let i = 0; i < BLOCK; i++) {
const time = t + i / RATE,
phase = (time % period) / period,
sinceRise = phase * period,
sinceFall = (phase - DUTY) * period;
let v;
if (phase < DUTY) {
v = 12 * (1 - Math.exp(-sinceRise / RISE)) +
2.5 * Math.exp(-sinceRise / 6e-6) * Math.sin(2 * Math.PI * 350000 * sinceRise);
}
else {
v = 12 * Math.exp(-sinceFall / RISE) -
1.5 * Math.exp(-sinceFall / 6e-6) * Math.sin(2 * Math.PI * 350000 * sinceFall);
}
gate[i] = v + gaussian() * 0.06;
current[i] = (phase < DUTY ? 1 + 4 * (phase / DUTY) : 5 - 4 * ((phase - DUTY) / (1 - DUTY))) +
gaussian() * 0.03;
}
t += BLOCK / RATE;
return { gate: gate, current: current };
}
setInterval(function () {
drive.push(driveBlock());
}, 50);
triggerMode.addEventListener('change', function () {
drive.triggerMode = triggerMode.value;
});
single.addEventListener('click', function () {
drive.single();
triggerMode.value = 'auto';
});
run.addEventListener('click', function () {
drive.triggerMode = triggerMode.value;
drive.run();
});
level.addEventListener('input', function () {
drive.triggerLevel = Number(level.value);
levelValue.textContent = Number(level.value).toFixed(1) + ' V';
});
edge.addEventListener('change', function () {
drive.triggerEdge = edge.value;
});
timebase.addEventListener('change', function () {
drive.timePerDivision = Number(timebase.value);
});
/* Cursors sit at the 10 % and 90 % points of the edge, which is where the rise time
is measured - so the cursor delta and the measurement should agree. */
cursors.addEventListener('change', function () {
drive.cursorA = cursors.checked ? -0.2e-6 : null;
drive.cursorB = cursors.checked ? 4.6e-6 : null;
});
autoSet.addEventListener('click', function () {
drive.autoSet();
timebase.value = String(drive.timePerDivision);
});
/* ── Lissajous ──────────────────────────────────────────────────────── */
const xy = document.getElementById('xy'),
XRATE = 100000, XBLOCK = 4000;
let xt = 0;
xy.channels = [
{ field: 'x', label: 'X', unit: 'V', voltsPerDivision: 1 },
{ field: 'y', label: 'Y', unit: 'V', voltsPerDivision: 1 }
];
function xyBlock() {
const x = new Float64Array(XBLOCK),
y = new Float64Array(XBLOCK);
for (let i = 0; i < XBLOCK; i++) {
const time = xt + i / XRATE;
x[i] = 3 * Math.sin(2 * Math.PI * 300 * time);
y[i] = 3 * Math.sin(2 * Math.PI * 200 * time + xt * 0.8);
}
xt += XBLOCK / XRATE;
return { x: x, y: y };
}
setInterval(function () {
xy.push(xyBlock());
}, 80);
};
styles.css
smart-scope {
width: 100%;
}
#drive {
--smart-scope-height: 440px;
}
#xy {
--smart-scope-height: 360px;
max-width: 520px;
}
.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.Scope is an oscilloscope display with a trigger that keeps a repetitive signal still, a graticule with divisions, cursors and automatic measurements taken from the captured record.</h2>
<p>
A strip chart shows a signal passing by. A scope shows the same cycle of the signal on every capture, because every capture starts at the trigger point. Samples arrive in blocks through <code>push()</code> at the rate of the digitizer; the trigger runs over each block and the screen shows one capture around the trigger.
</p>
</section>
<section>
<h2>A 2 kHz PWM gate drive and the switched current at 1 MS/s</h2>
<p>
Channel 1 is the gate voltage, a 0 to 12 V square wave with a ringing edge. Channel 2 is the inductor current, a triangle wave that lags the gate. Trigger on the rising edge of the gate to hold the picture still. Move the trigger level above 12 V and nothing triggers: <em>auto</em> mode free-runs and <em>normal</em> mode keeps the last capture. Place cursors on the edge to read the rise time and compare it with the automatic measurement.
</p>
<smart-scope id="drive" [sampleRate]="1000000" [timePerDivision]="0.0001" [triggerLevel]="6" [triggerHysteresis]="0.5" label="Gate drive"></smart-scope>
<div class="demo-actions">
<label>Trigger
<select id="triggerMode">
<option value="auto" selected>auto</option>
<option value="normal">normal</option>
</select>
</label>
<button type="button" id="single">Single</button>
<button type="button" id="run">Run</button>
<label>Level <input type="range" id="level" min="-2" max="16" step="0.5" value="6" /> <span id="levelValue">6.0 V</span></label>
<label>Edge
<select id="edge">
<option value="rising" selected>rising</option>
<option value="falling">falling</option>
</select>
</label>
<label>Time/div
<select id="timebase">
<option value="0.00002">20 µs</option>
<option value="0.00005">50 µs</option>
<option value="0.0001" selected>100 µs</option>
<option value="0.0002">200 µs</option>
<option value="0.0005">500 µs</option>
</select>
</label>
<label><input type="checkbox" id="cursors" /> Cursors on the edge</label>
<button type="button" id="autoSet">Auto set</button>
</div>
<p class="hint">
Keyboard: the legend is a single tab stop. Use the arrow keys to move between channels and Enter or Space to show or hide one. Measurements are taken from the captured record, so they do not depend on whether the channel is displayed.
</p>
</section>
<section>
<h2>XY mode: a Lissajous figure</h2>
<p>
XY mode draws the second channel against the first. Two sine waves with a 3:2 frequency ratio draw a Lissajous figure, which was used to compare phase and frequency before counters were common. The figure drifts as the phase changes.
</p>
<smart-scope id="xy" mode="xy" [sampleRate]="100000" [timePerDivision]="0.002" [showMeasurements]="false"
trigger-mode="auto" label="XY"></smart-scope>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { ScopeModule } from 'smart-industrial/angular/scope';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - Scope 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: [ScopeModule],
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 scopes are fed by generators with known content, so what the measurements report
can be checked against what was put in. The seam is push(block) - replace a generator
with a digitizer callback and nothing else on the page changes. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
export function runDemo(): void {
/* ── Gate drive ─────────────────────────────────────────────────────── */
const drive = document.getElementById('drive'),
triggerMode = document.getElementById('triggerMode'),
single = document.getElementById('single'),
run = document.getElementById('run'),
level = document.getElementById('level'),
levelValue = document.getElementById('levelValue'),
edge = document.getElementById('edge'),
timebase = document.getElementById('timebase'),
cursors = document.getElementById('cursors'),
autoSet = document.getElementById('autoSet');
const RATE = 1000000, BLOCK = 20000, PWM = 2000, DUTY = 0.35, RISE = 2.2e-6;
let t = 0;
drive.channels = [
{ field: 'gate', label: 'CH1 gate', unit: 'V', voltsPerDivision: 5, offset: -2 },
{ field: 'current', label: 'CH2 I', unit: 'A', voltsPerDivision: 2, offset: 1 }
];
drive.measurements = ['vpp', 'frequency', 'riseTime', 'dutyCycle'];
/* A square wave with an exponential edge and a damped ring after it; the current is
the triangle an inductor draws from it, lagging by the switching. */
function driveBlock() {
const gate = new Float64Array(BLOCK),
current = new Float64Array(BLOCK),
period = 1 / PWM;
for (let i = 0; i < BLOCK; i++) {
const time = t + i / RATE,
phase = (time % period) / period,
sinceRise = phase * period,
sinceFall = (phase - DUTY) * period;
let v;
if (phase < DUTY) {
v = 12 * (1 - Math.exp(-sinceRise / RISE)) +
2.5 * Math.exp(-sinceRise / 6e-6) * Math.sin(2 * Math.PI * 350000 * sinceRise);
}
else {
v = 12 * Math.exp(-sinceFall / RISE) -
1.5 * Math.exp(-sinceFall / 6e-6) * Math.sin(2 * Math.PI * 350000 * sinceFall);
}
gate[i] = v + gaussian() * 0.06;
current[i] = (phase < DUTY ? 1 + 4 * (phase / DUTY) : 5 - 4 * ((phase - DUTY) / (1 - DUTY))) +
gaussian() * 0.03;
}
t += BLOCK / RATE;
return { gate: gate, current: current };
}
setInterval(function () {
drive.push(driveBlock());
}, 50);
triggerMode.addEventListener('change', function () {
drive.triggerMode = triggerMode.value;
});
single.addEventListener('click', function () {
drive.single();
triggerMode.value = 'auto';
});
run.addEventListener('click', function () {
drive.triggerMode = triggerMode.value;
drive.run();
});
level.addEventListener('input', function () {
drive.triggerLevel = Number(level.value);
levelValue.textContent = Number(level.value).toFixed(1) + ' V';
});
edge.addEventListener('change', function () {
drive.triggerEdge = edge.value;
});
timebase.addEventListener('change', function () {
drive.timePerDivision = Number(timebase.value);
});
/* Cursors sit at the 10 % and 90 % points of the edge, which is where the rise time
is measured - so the cursor delta and the measurement should agree. */
cursors.addEventListener('change', function () {
drive.cursorA = cursors.checked ? -0.2e-6 : null;
drive.cursorB = cursors.checked ? 4.6e-6 : null;
});
autoSet.addEventListener('click', function () {
drive.autoSet();
timebase.value = String(drive.timePerDivision);
});
/* ── Lissajous ──────────────────────────────────────────────────────── */
const xy = document.getElementById('xy'),
XRATE = 100000, XBLOCK = 4000;
let xt = 0;
xy.channels = [
{ field: 'x', label: 'X', unit: 'V', voltsPerDivision: 1 },
{ field: 'y', label: 'Y', unit: 'V', voltsPerDivision: 1 }
];
function xyBlock() {
const x = new Float64Array(XBLOCK),
y = new Float64Array(XBLOCK);
for (let i = 0; i < XBLOCK; i++) {
const time = xt + i / XRATE;
x[i] = 3 * Math.sin(2 * Math.PI * 300 * time);
y[i] = 3 * Math.sin(2 * Math.PI * 200 * time + xt * 0.8);
}
xt += XBLOCK / XRATE;
return { x: x, y: y };
}
setInterval(function () {
xy.push(xyBlock());
}, 80);
}
angular/app.component.css
smart-scope {
width: 100%;
}
#drive {
--smart-scope-height: 440px;
}
#xy {
--smart-scope-height: 360px;
max-width: 520px;
}
.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 { Scope } from 'smart-industrial/react/scope';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* Both scopes are fed by generators with known content, so what the measurements report
can be checked against what was put in. The seam is push(block) - replace a generator
with a digitizer callback and nothing else on the page changes. */
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;
}
/* ── Gate drive ─────────────────────────────────────────────────────── */
const drive = document.getElementById('drive'),
triggerMode = document.getElementById('triggerMode'),
single = document.getElementById('single'),
run = document.getElementById('run'),
level = document.getElementById('level'),
levelValue = document.getElementById('levelValue'),
edge = document.getElementById('edge'),
timebase = document.getElementById('timebase'),
cursors = document.getElementById('cursors'),
autoSet = document.getElementById('autoSet');
const RATE = 1000000, BLOCK = 20000, PWM = 2000, DUTY = 0.35, RISE = 2.2e-6;
let t = 0;
drive.channels = [
{ field: 'gate', label: 'CH1 gate', unit: 'V', voltsPerDivision: 5, offset: -2 },
{ field: 'current', label: 'CH2 I', unit: 'A', voltsPerDivision: 2, offset: 1 }
];
drive.measurements = ['vpp', 'frequency', 'riseTime', 'dutyCycle'];
/* A square wave with an exponential edge and a damped ring after it; the current is
the triangle an inductor draws from it, lagging by the switching. */
function driveBlock() {
const gate = new Float64Array(BLOCK),
current = new Float64Array(BLOCK),
period = 1 / PWM;
for (let i = 0; i < BLOCK; i++) {
const time = t + i / RATE,
phase = (time % period) / period,
sinceRise = phase * period,
sinceFall = (phase - DUTY) * period;
let v;
if (phase < DUTY) {
v = 12 * (1 - Math.exp(-sinceRise / RISE)) +
2.5 * Math.exp(-sinceRise / 6e-6) * Math.sin(2 * Math.PI * 350000 * sinceRise);
}
else {
v = 12 * Math.exp(-sinceFall / RISE) -
1.5 * Math.exp(-sinceFall / 6e-6) * Math.sin(2 * Math.PI * 350000 * sinceFall);
}
gate[i] = v + gaussian() * 0.06;
current[i] = (phase < DUTY ? 1 + 4 * (phase / DUTY) : 5 - 4 * ((phase - DUTY) / (1 - DUTY))) +
gaussian() * 0.03;
}
t += BLOCK / RATE;
return { gate: gate, current: current };
}
setInterval(function () {
drive.push(driveBlock());
}, 50);
triggerMode.addEventListener('change', function () {
drive.triggerMode = triggerMode.value;
});
single.addEventListener('click', function () {
drive.single();
triggerMode.value = 'auto';
});
run.addEventListener('click', function () {
drive.triggerMode = triggerMode.value;
drive.run();
});
level.addEventListener('input', function () {
drive.triggerLevel = Number(level.value);
levelValue.textContent = Number(level.value).toFixed(1) + ' V';
});
edge.addEventListener('change', function () {
drive.triggerEdge = edge.value;
});
timebase.addEventListener('change', function () {
drive.timePerDivision = Number(timebase.value);
});
/* Cursors sit at the 10 % and 90 % points of the edge, which is where the rise time
is measured - so the cursor delta and the measurement should agree. */
cursors.addEventListener('change', function () {
drive.cursorA = cursors.checked ? -0.2e-6 : null;
drive.cursorB = cursors.checked ? 4.6e-6 : null;
});
autoSet.addEventListener('click', function () {
drive.autoSet();
timebase.value = String(drive.timePerDivision);
});
/* ── Lissajous ──────────────────────────────────────────────────────── */
const xy = document.getElementById('xy'),
XRATE = 100000, XBLOCK = 4000;
let xt = 0;
xy.channels = [
{ field: 'x', label: 'X', unit: 'V', voltsPerDivision: 1 },
{ field: 'y', label: 'Y', unit: 'V', voltsPerDivision: 1 }
];
function xyBlock() {
const x = new Float64Array(XBLOCK),
y = new Float64Array(XBLOCK);
for (let i = 0; i < XBLOCK; i++) {
const time = xt + i / XRATE;
x[i] = 3 * Math.sin(2 * Math.PI * 300 * time);
y[i] = 3 * Math.sin(2 * Math.PI * 200 * time + xt * 0.8);
}
xt += XBLOCK / XRATE;
return { x: x, y: y };
}
setInterval(function () {
xy.push(xyBlock());
}, 80);
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.Scope is an oscilloscope display with a trigger that keeps a repetitive signal still, a graticule with divisions, cursors and automatic measurements taken from the captured record.</h2>
<p>A strip chart shows a signal passing by. A scope shows the same cycle of the signal on every capture, because every capture starts at the trigger point. Samples arrive in blocks through <code>push()</code> at the rate of the digitizer; the trigger runs over each block and the screen shows one capture around the trigger.</p>
</section>
<section>
<h2>A 2 kHz PWM gate drive and the switched current at 1 MS/s</h2>
<p>Channel 1 is the gate voltage, a 0 to 12 V square wave with a ringing edge. Channel 2 is the inductor current, a triangle wave that lags the gate. Trigger on the rising edge of the gate to hold the picture still. Move the trigger level above 12 V and nothing triggers: <em>auto</em> mode free-runs and <em>normal</em> mode keeps the last capture. Place cursors on the edge to read the rise time and compare it with the automatic measurement.</p>
<Scope id="drive" sampleRate={1000000} timePerDivision={0.0001} triggerLevel={6} triggerHysteresis={0.5} label="Gate drive"></Scope>
<div className="demo-actions">
<label>
Trigger
<select id="triggerMode" defaultValue="auto"><option value="auto">auto</option> <option value="normal">normal</option></select>
</label>
<button type="button" id="single">Single</button>
<button type="button" id="run">Run</button>
<label>Level <input type="range" id="level" min="-2" max="16" step="0.5" defaultValue="6" /> <span id="levelValue">6.0 V</span></label>
<label>
Edge
<select id="edge" defaultValue="rising"><option value="rising">rising</option> <option value="falling">falling</option></select>
</label>
<label>
Time/div
<select id="timebase" defaultValue="0.0001"><option value="0.00002">20 µs</option> <option value="0.00005">50 µs</option> <option value="0.0001">100 µs</option> <option value="0.0002">200 µs</option> <option value="0.0005">500 µs</option></select>
</label>
<label><input type="checkbox" id="cursors" /> Cursors on the edge</label>
<button type="button" id="autoSet">Auto set</button>
</div>
<p className="hint">Keyboard: the legend is a single tab stop. Use the arrow keys to move between channels and Enter or Space to show or hide one. Measurements are taken from the captured record, so they do not depend on whether the channel is displayed.</p>
</section>
<section>
<h2>XY mode: a Lissajous figure</h2>
<p>XY mode draws the second channel against the first. Two sine waves with a 3:2 frequency ratio draw a Lissajous figure, which was used to compare phase and frequency before counters were common. The figure drifts as the phase changes.</p>
<Scope id="xy" mode="xy" sampleRate={100000} timePerDivision={0.002} showMeasurements={false} triggerMode="auto" label="XY"></Scope>
</section>
</div>
</div>
);
};
export default App;
react/App.css
smart-scope {
width: 100%;
}
#drive {
--smart-scope-height: 440px;
}
#xy {
--smart-scope-height: 360px;
max-width: 520px;
}
.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.Scope is an oscilloscope display with a trigger that keeps a repetitive signal still, a graticule with divisions, cursors and automatic measurements taken from the captured record.</h2>
<p>
A strip chart shows a signal passing by. A scope shows the same cycle of the signal on every capture, because every capture starts at the trigger point. Samples arrive in blocks through <code>push()</code> at the rate of the digitizer; the trigger runs over each block and the screen shows one capture around the trigger.
</p>
</section>
<section>
<h2>A 2 kHz PWM gate drive and the switched current at 1 MS/s</h2>
<p>
Channel 1 is the gate voltage, a 0 to 12 V square wave with a ringing edge. Channel 2 is the inductor current, a triangle wave that lags the gate. Trigger on the rising edge of the gate to hold the picture still. Move the trigger level above 12 V and nothing triggers: <em>auto</em> mode free-runs and <em>normal</em> mode keeps the last capture. Place cursors on the edge to read the rise time and compare it with the automatic measurement.
</p>
<smart-scope id="drive" sample-rate="1000000" time-per-division="0.0001" trigger-level="6"
trigger-hysteresis="0.5" label="Gate drive"></smart-scope>
<div class="demo-actions">
<label>Trigger
<select id="triggerMode">
<option value="auto" selected>auto</option>
<option value="normal">normal</option>
</select>
</label>
<button type="button" id="single">Single</button>
<button type="button" id="run">Run</button>
<label>Level <input type="range" id="level" min="-2" max="16" step="0.5" value="6" /> <span id="levelValue">6.0 V</span></label>
<label>Edge
<select id="edge">
<option value="rising" selected>rising</option>
<option value="falling">falling</option>
</select>
</label>
<label>Time/div
<select id="timebase">
<option value="0.00002">20 µs</option>
<option value="0.00005">50 µs</option>
<option value="0.0001" selected>100 µs</option>
<option value="0.0002">200 µs</option>
<option value="0.0005">500 µs</option>
</select>
</label>
<label><input type="checkbox" id="cursors" /> Cursors on the edge</label>
<button type="button" id="autoSet">Auto set</button>
</div>
<p class="hint">
Keyboard: the legend is a single tab stop. Use the arrow keys to move between channels and Enter or Space to show or hide one. Measurements are taken from the captured record, so they do not depend on whether the channel is displayed.
</p>
</section>
<section>
<h2>XY mode: a Lissajous figure</h2>
<p>
XY mode draws the second channel against the first. Two sine waves with a 3:2 frequency ratio draw a Lissajous figure, which was used to compare phase and frequency before counters were common. The figure drifts as the phase changes.
</p>
<smart-scope id="xy" mode="xy" sample-rate="100000" time-per-division="0.002" show-measurements="false"
trigger-mode="auto" label="XY"></smart-scope>
</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.scope.js";
/* Both scopes are fed by generators with known content, so what the measurements report
can be checked against what was put in. The seam is push(block) - replace a generator
with a digitizer callback and nothing else on the page changes. */
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(() => {
/* ── Gate drive ─────────────────────────────────────────────────────── */
const drive = document.getElementById('drive'),
triggerMode = document.getElementById('triggerMode'),
single = document.getElementById('single'),
run = document.getElementById('run'),
level = document.getElementById('level'),
levelValue = document.getElementById('levelValue'),
edge = document.getElementById('edge'),
timebase = document.getElementById('timebase'),
cursors = document.getElementById('cursors'),
autoSet = document.getElementById('autoSet');
const RATE = 1000000, BLOCK = 20000, PWM = 2000, DUTY = 0.35, RISE = 2.2e-6;
let t = 0;
drive.channels = [
{ field: 'gate', label: 'CH1 gate', unit: 'V', voltsPerDivision: 5, offset: -2 },
{ field: 'current', label: 'CH2 I', unit: 'A', voltsPerDivision: 2, offset: 1 }
];
drive.measurements = ['vpp', 'frequency', 'riseTime', 'dutyCycle'];
/* A square wave with an exponential edge and a damped ring after it; the current is
the triangle an inductor draws from it, lagging by the switching. */
function driveBlock() {
const gate = new Float64Array(BLOCK),
current = new Float64Array(BLOCK),
period = 1 / PWM;
for (let i = 0; i < BLOCK; i++) {
const time = t + i / RATE,
phase = (time % period) / period,
sinceRise = phase * period,
sinceFall = (phase - DUTY) * period;
let v;
if (phase < DUTY) {
v = 12 * (1 - Math.exp(-sinceRise / RISE)) +
2.5 * Math.exp(-sinceRise / 6e-6) * Math.sin(2 * Math.PI * 350000 * sinceRise);
}
else {
v = 12 * Math.exp(-sinceFall / RISE) -
1.5 * Math.exp(-sinceFall / 6e-6) * Math.sin(2 * Math.PI * 350000 * sinceFall);
}
gate[i] = v + gaussian() * 0.06;
current[i] = (phase < DUTY ? 1 + 4 * (phase / DUTY) : 5 - 4 * ((phase - DUTY) / (1 - DUTY))) +
gaussian() * 0.03;
}
t += BLOCK / RATE;
return { gate: gate, current: current };
}
setInterval(function () {
drive.push(driveBlock());
}, 50);
triggerMode.addEventListener('change', function () {
drive.triggerMode = triggerMode.value;
});
single.addEventListener('click', function () {
drive.single();
triggerMode.value = 'auto';
});
run.addEventListener('click', function () {
drive.triggerMode = triggerMode.value;
drive.run();
});
level.addEventListener('input', function () {
drive.triggerLevel = Number(level.value);
levelValue.textContent = Number(level.value).toFixed(1) + ' V';
});
edge.addEventListener('change', function () {
drive.triggerEdge = edge.value;
});
timebase.addEventListener('change', function () {
drive.timePerDivision = Number(timebase.value);
});
/* Cursors sit at the 10 % and 90 % points of the edge, which is where the rise time
is measured - so the cursor delta and the measurement should agree. */
cursors.addEventListener('change', function () {
drive.cursorA = cursors.checked ? -0.2e-6 : null;
drive.cursorB = cursors.checked ? 4.6e-6 : null;
});
autoSet.addEventListener('click', function () {
drive.autoSet();
timebase.value = String(drive.timePerDivision);
});
/* ── Lissajous ──────────────────────────────────────────────────────── */
const xy = document.getElementById('xy'),
XRATE = 100000, XBLOCK = 4000;
let xt = 0;
xy.channels = [
{ field: 'x', label: 'X', unit: 'V', voltsPerDivision: 1 },
{ field: 'y', label: 'Y', unit: 'V', voltsPerDivision: 1 }
];
function xyBlock() {
const x = new Float64Array(XBLOCK),
y = new Float64Array(XBLOCK);
for (let i = 0; i < XBLOCK; i++) {
const time = xt + i / XRATE;
x[i] = 3 * Math.sin(2 * Math.PI * 300 * time);
y[i] = 3 * Math.sin(2 * Math.PI * 200 * time + xt * 0.8);
}
xt += XBLOCK / XRATE;
return { x: x, y: y };
}
setInterval(function () {
xy.push(xyBlock());
}, 80);
});
});
}
};
</script>
<style>
smart-scope {
width: 100%;
}
#drive {
--smart-scope-height: 440px;
}
#xy {
--smart-scope-height: 360px;
max-width: 520px;
}
.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 2 kHz PWM gate drive and the switched current at 1 MS/s
- XY mode: a Lissajous figure