From 72713fa20c8f458c1cecc13733513207f55447d1 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 00:56:29 +0200 Subject: [PATCH 01/18] feat(example): add Eye Diagram Chart drawExample with NRZ signal generation Implements signal generation helpers (gaussianRandom, generateTrace, binTrace) and the SciChart heatmap accumulation loop for the eye diagram visualization. --- .../EyeDiagramChart/drawExample.ts | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts new file mode 100644 index 000000000..230356de3 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -0,0 +1,271 @@ +import { + EAutoRange, + HeatmapColorMap, + NumberRange, + NumericAxis, + SciChartSurface, + UniformHeatmapDataSeries, + UniformHeatmapRenderableSeries, +} from "scichart"; +import { appTheme } from "../../../theme"; + +// --------------------------------------------------------------------------- +// Signal generation helpers +// --------------------------------------------------------------------------- + +/** Box-Muller transform — returns a single standard-normal random value */ +function gaussianRandom(): number { + let u = 0; + let v = 0; + while (u === 0) u = Math.random(); + while (v === 0) v = Math.random(); + return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); +} + +/** + * Generates a 200-sample NRZ waveform for a 2-UI window. + * + * @param prev bit value (0 or 1) of the UI before the window + * @param curr bit value (0 or 1) of the central UI (UI 0–1) + * @param next bit value (0 or 1) of the UI after the window (UI 1–2) + * @returns Float32Array of 200 voltage samples + */ +export function generateTrace(prev: number, curr: number, next: number): Float32Array { + const SAMPLES_PER_UI = 100; + const TOTAL_SAMPLES = 200; + const TRANSITION_HALF = 10; // raised-cosine spans 20 samples (indices -10..+9) + const JITTER_SIGMA = 2.5; // samples + const NOISE_SIGMA = 0.05; // volts + + // Voltage levels: bit 1 → +1 V, bit 0 → −1 V + const vPrev = prev === 1 ? 1.0 : -1.0; + const vCurr = curr === 1 ? 1.0 : -1.0; + const vNext = next === 1 ? 1.0 : -1.0; + + const out = new Float32Array(TOTAL_SAMPLES); + + // Transition edge positions (in samples, relative to start of 2-UI window) + const jitter0 = Math.round(gaussianRandom() * JITTER_SIGMA); + const jitter1 = Math.round(gaussianRandom() * JITTER_SIGMA); + const edge0 = SAMPLES_PER_UI + jitter0; // transition prev→curr at sample 100 + const edge1 = 2 * SAMPLES_PER_UI + jitter1; // transition curr→next at sample 200 + + for (let i = 0; i < TOTAL_SAMPLES; i++) { + let v: number; + + // Determine voltage contribution from each transition using raised cosine + const d0 = i - edge0; // distance from first edge + const d1 = i - edge1; // distance from second edge + + if (Math.abs(d0) < TRANSITION_HALF) { + // Within raised-cosine transition zone for edge 0 (prev → curr) + const t = (d0 / (2 * TRANSITION_HALF)) + 0.5; // 0..1 + const rc = 0.5 * (1 - Math.cos(Math.PI * t)); + v = vPrev + (vCurr - vPrev) * rc; + } else if (Math.abs(d1) < TRANSITION_HALF) { + // Within raised-cosine transition zone for edge 1 (curr → next) + const t = (d1 / (2 * TRANSITION_HALF)) + 0.5; // 0..1 + const rc = 0.5 * (1 - Math.cos(Math.PI * t)); + v = vCurr + (vNext - vCurr) * rc; + } else if (i < edge0) { + v = vPrev; + } else if (i < edge1) { + v = vCurr; + } else { + v = vNext; + } + + // Gaussian amplitude noise + out[i] = v + gaussianRandom() * NOISE_SIGMA; + } + + return out; +} + +/** + * Bins a trace into a flat Float32Array accumulation grid. + * + * Grid layout: 400 columns × 200 rows, col-major → index = col * 200 + row + * - col maps to sample index (0..199) scaled to 0..399 + * - row maps to voltage (−1.5..+1.5) scaled to 0..199 + */ +export function binTrace(grid: Float32Array, trace: Float32Array): void { + const COLS = 400; + const ROWS = 200; + const V_MIN = -1.5; + const V_MAX = 1.5; + const V_RANGE = V_MAX - V_MIN; + + for (let i = 0; i < trace.length; i++) { + // Map sample index to column (0..COLS-1) + const col = Math.round((i / (trace.length - 1)) * (COLS - 1)); + + // Map voltage to row (0..ROWS-1); clamp to bounds + const rowF = ((trace[i] - V_MIN) / V_RANGE) * (ROWS - 1); + const row = Math.max(0, Math.min(ROWS - 1, Math.round(rowF))); + + grid[col * ROWS + row] += 1; + } +} + +// --------------------------------------------------------------------------- +// Main drawExample function +// --------------------------------------------------------------------------- + +export async function drawExample(rootElement: string | HTMLDivElement) { + const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, { + theme: appTheme.SciChartJsTheme, + }); + + // X Axis — time in UI units + sciChartSurface.xAxes.add( + new NumericAxis(wasmContext, { + axisTitle: "Time (UI)", + visibleRange: new NumberRange(0, 2), + autoRange: EAutoRange.Never, + drawMajorGridLines: false, + drawMinorGridLines: false, + }) + ); + + // Y Axis — voltage + sciChartSurface.yAxes.add( + new NumericAxis(wasmContext, { + axisTitle: "Voltage (V)", + visibleRange: new NumberRange(-1.5, 1.5), + autoRange: EAutoRange.Never, + drawMajorGridLines: false, + drawMinorGridLines: false, + }) + ); + + // Accumulation grid: 400 cols × 200 rows, col-major + const COLS = 400; + const ROWS = 200; + const grid = new Float32Array(COLS * ROWS); + + // zValuesLog: 200 rows × 400 cols (row-major), for setZValues + const zValuesLog: number[][] = Array.from({ length: ROWS }, () => new Array(COLS).fill(0)); + + // Heatmap data series + const dataSeries = new UniformHeatmapDataSeries(wasmContext, { + xStart: 0, + xStep: 2 / (COLS - 1), + yStart: -1.5, + yStep: 3 / (ROWS - 1), + zValues: zValuesLog, + }); + + // Heatmap renderable series + const renderableSeries = new UniformHeatmapRenderableSeries(wasmContext, { + dataSeries, + useLinearTextureFiltering: true, + colorMap: new HeatmapColorMap({ + minimum: 0, + maximum: 10, + gradientStops: [ + { offset: 0, color: "#000000" }, + { offset: 0.15, color: "#00008B" }, + { offset: 0.4, color: "#00FFFF" }, + { offset: 0.7, color: "#FFFF00" }, + { offset: 1, color: "#FFFFFF" }, + ], + }), + }); + + sciChartSurface.renderableSeries.add(renderableSeries); + + // Stats overlay + const statsDiv = document.createElement("div"); + statsDiv.style.cssText = [ + "position:absolute", + "top:8px", + "right:8px", + "font-family:monospace", + "font-size:12px", + "color:#ffffff", + "background:rgba(0,0,0,0.5)", + "padding:4px 8px", + "border-radius:4px", + "pointer-events:none", + "z-index:100", + ].join(";"); + statsDiv.textContent = "FPS: -- | Traces/s: -- | Total: 0"; + + const container = typeof rootElement === "string" + ? document.getElementById(rootElement) + : rootElement; + if (container) { + container.style.position = "relative"; + container.appendChild(statsDiv); + } + + // Animation state + let rafHandle: number | null = null; + let totalTraces = 0; + let frameCount = 0; + let lastStatsTime = performance.now(); + let lastFps = 0; + let lastTracesPerSec = 0; + + const TRACES_PER_FRAME = 50; + + function animate() { + const frameStart = performance.now(); + + // Generate and bin 50 traces this frame + for (let t = 0; t < TRACES_PER_FRAME; t++) { + const prev = Math.random() < 0.5 ? 0 : 1; + const curr = Math.random() < 0.5 ? 0 : 1; + const next = Math.random() < 0.5 ? 0 : 1; + const trace = generateTrace(prev, curr, next); + binTrace(grid, trace); + } + totalTraces += TRACES_PER_FRAME; + + // Update zValuesLog from grid (log1p scaling) + for (let col = 0; col < COLS; col++) { + for (let row = 0; row < ROWS; row++) { + zValuesLog[row][col] = Math.log1p(grid[col * ROWS + row]); + } + } + + dataSeries.setZValues(zValuesLog); + + // Update stats every 30 frames + frameCount++; + if (frameCount % 30 === 0) { + const now = performance.now(); + const elapsed = (now - lastStatsTime) / 1000; // seconds + lastFps = Math.round(30 / elapsed); + lastTracesPerSec = Math.round((30 * TRACES_PER_FRAME) / elapsed); + lastStatsTime = now; + statsDiv.textContent = `FPS: ${lastFps} | Traces/s: ${lastTracesPerSec} | Total: ${totalTraces.toLocaleString()}`; + } + + rafHandle = requestAnimationFrame(animate); + } + + function startAnimation() { + if (rafHandle !== null) return; + rafHandle = requestAnimationFrame(animate); + } + + function stopAnimation() { + if (rafHandle !== null) { + cancelAnimationFrame(rafHandle); + rafHandle = null; + } + } + + // Start automatically + startAnimation(); + + return { + sciChartSurface, + controls: { + startAnimation, + stopAnimation, + }, + }; +} From 9f37e7ffb8068f04c15cfb6de7b54a4b99ff3375 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 00:58:41 +0200 Subject: [PATCH 02/18] fix(example): correct raised-cosine transition range to exact 20-sample span --- .../ScientificCharts/EyeDiagramChart/drawExample.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts index 230356de3..379e63b5b 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -57,14 +57,14 @@ export function generateTrace(prev: number, curr: number, next: number): Float32 const d0 = i - edge0; // distance from first edge const d1 = i - edge1; // distance from second edge - if (Math.abs(d0) < TRANSITION_HALF) { + if (d0 >= -TRANSITION_HALF && d0 < TRANSITION_HALF) { // Within raised-cosine transition zone for edge 0 (prev → curr) - const t = (d0 / (2 * TRANSITION_HALF)) + 0.5; // 0..1 + const t = (d0 + TRANSITION_HALF) / (2 * TRANSITION_HALF); // 0..1 const rc = 0.5 * (1 - Math.cos(Math.PI * t)); v = vPrev + (vCurr - vPrev) * rc; - } else if (Math.abs(d1) < TRANSITION_HALF) { + } else if (d1 >= -TRANSITION_HALF && d1 < TRANSITION_HALF) { // Within raised-cosine transition zone for edge 1 (curr → next) - const t = (d1 / (2 * TRANSITION_HALF)) + 0.5; // 0..1 + const t = (d1 + TRANSITION_HALF) / (2 * TRANSITION_HALF); // 0..1 const rc = 0.5 * (1 - Math.cos(Math.PI * t)); v = vCurr + (vNext - vCurr) * rc; } else if (i < edge0) { From 123b4c654fad88e99b6f05fe205ffba2578acc9d Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 01:01:26 +0200 Subject: [PATCH 03/18] fix(example): add cleanup fn, remove dead stats vars in EyeDiagramChart --- .../EyeDiagramChart/drawExample.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts index 379e63b5b..f70b1253d 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -205,9 +205,6 @@ export async function drawExample(rootElement: string | HTMLDivElement) { let totalTraces = 0; let frameCount = 0; let lastStatsTime = performance.now(); - let lastFps = 0; - let lastTracesPerSec = 0; - const TRACES_PER_FRAME = 50; function animate() { @@ -237,10 +234,10 @@ export async function drawExample(rootElement: string | HTMLDivElement) { if (frameCount % 30 === 0) { const now = performance.now(); const elapsed = (now - lastStatsTime) / 1000; // seconds - lastFps = Math.round(30 / elapsed); - lastTracesPerSec = Math.round((30 * TRACES_PER_FRAME) / elapsed); + const fps = Math.round(30 / elapsed); + const tracesPerSec = Math.round((30 * TRACES_PER_FRAME) / elapsed); lastStatsTime = now; - statsDiv.textContent = `FPS: ${lastFps} | Traces/s: ${lastTracesPerSec} | Total: ${totalTraces.toLocaleString()}`; + statsDiv.textContent = `FPS: ${fps} | Traces/s: ${tracesPerSec} | Total: ${totalTraces.toLocaleString()}`; } rafHandle = requestAnimationFrame(animate); @@ -261,11 +258,18 @@ export async function drawExample(rootElement: string | HTMLDivElement) { // Start automatically startAnimation(); + function cleanup() { + stopAnimation(); + if (container) container.removeChild(statsDiv); + sciChartSurface.delete(); + } + return { sciChartSurface, controls: { startAnimation, stopAnimation, + cleanup, }, }; } From 6e239269e49c469afb68ec5df2e944d99bbc6224 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 01:02:17 +0200 Subject: [PATCH 04/18] feat(example): add EyeDiagramChart React wrapper component Create the React wrapper component for Eye Diagram Chart that: - Uses SciChartReact to mount the drawExample initialization - Calls startAnimation() on init to ensure animation starts after surface is ready - Calls cleanup() on delete to stop animation and teardown resources --- .../EyeDiagramChart/index.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/index.tsx diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/index.tsx b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/index.tsx new file mode 100644 index 000000000..3c3923799 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/index.tsx @@ -0,0 +1,20 @@ +import { SciChartReact, TResolvedReturnType } from "scichart-react"; +import commonClasses from "../../../styles/Examples.module.scss"; +import { drawExample } from "./drawExample"; + +export default function EyeDiagramChart() { + return ( +
+ drawExample(rootElementId)} + onInit={(initResult: TResolvedReturnType) => { + initResult.controls.startAnimation(); + }} + onDelete={(initResult: TResolvedReturnType) => { + initResult.controls.cleanup(); + }} + /> +
+ ); +} From 5456c71b39f2c66ab5b1dd0aa0a2d352c7a8ee82 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 01:08:08 +0200 Subject: [PATCH 05/18] feat(example): add Eye Diagram Chart exampleInfo metadata file Adds the metadata configuration file for the Eye Diagram example, defining framework descriptions, documentation links, and sandbox settings for the real-time persistence display demonstration. --- .../EyeDiagramChart/exampleInfo.tsx | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx new file mode 100644 index 000000000..ab7597216 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx @@ -0,0 +1,67 @@ +import { createExampleInfo } from "../../../exampleInfoUtils"; +import { IExampleMetadata } from "../../../IExampleMetadata"; + +const metaData: IExampleMetadata = + //// This metadata is computer generated - do not edit! + { + reactComponent: "EyeDiagramChart", + id: "featuredApps_scientificCharts_EyeDiagramChart", + imagePath: "javascript-eye-diagram-chart.jpg", + description: + "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Thousands of NRZ waveform traces accumulate per second into a 2D density grid, producing the iconic oscilloscope glow effect.", + tips: [], + frameworks: { + javascript: { + subtitle: + "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Thousands of NRZ waveform traces accumulate per second into a 2D density grid, producing the iconic oscilloscope glow effect.", + title: "Real-time Eye Diagram Chart Example", + pageTitle: "Real-time Eye Diagram (Persistence Display)", + metaDescription: + "See a real-time Eye Diagram rendered with SciChart.js. Thousands of NRZ waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", + markdownContent: null, + }, + react: { + subtitle: + "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Thousands of NRZ waveform traces accumulate per second into a 2D density grid, producing the iconic oscilloscope glow effect.", + title: "Real-time Eye Diagram Chart Example", + pageTitle: "Real-time Eye Diagram (Persistence Display)", + metaDescription: + "See a real-time Eye Diagram rendered with React and SciChart.js. Thousands of NRZ waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", + markdownContent: null, + }, + angular: { + subtitle: + "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Thousands of NRZ waveform traces accumulate per second into a 2D density grid, producing the iconic oscilloscope glow effect.", + title: "Real-time Eye Diagram Chart Example", + pageTitle: "Real-time Eye Diagram (Persistence Display)", + metaDescription: + "See a real-time Eye Diagram rendered with Angular and SciChart.js. Thousands of NRZ waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", + markdownContent: null, + }, + }, + documentationLinks: [ + { + href: "https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/", + title: "Uniform Heatmap Chart Type documentation", + linkTitle: "Uniform Heatmap Chart Documentation", + }, + ], + path: "eye-diagram-chart", + metaKeywords: "eye diagram, persistence, oscilloscope, NRZ, signal, heatmap, real-time, performance, javascript, webgl", + onWebsite: true, + filepath: "FeaturedApps/ScientificCharts/EyeDiagramChart", + thumbnailImage: "javascript-eye-diagram-chart.jpg", + sandboxConfig: { + infiniteLoopProtection: false, + hardReloadOnChange: false, + view: "browser", + }, + markdownContent: null, + pageLayout: "default", + extraDependencies: {}, + isNew: true, + }; +//// End of computer generated metadata + +export const eyeDiagramChartExampleInfo = createExampleInfo(metaData); +export default eyeDiagramChartExampleInfo; From 3aff7945c21cff7e07f6533eba5d86b35ae6c766 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 01:09:53 +0200 Subject: [PATCH 06/18] feat(examples): add vanilla and angular entry points for eye diagram chart Create entry point files for the Eye Diagram Chart example: - vanilla.ts: Standalone initialization with animation controls - angular.ts: Angular component wrapper with lifecycle hooks Both integrate with drawExample to render heatmap eye diagram with streaming NRZ signal traces and real-time statistics overlay. --- .../EyeDiagramChart/angular.ts | 32 +++++++++++++++++++ .../EyeDiagramChart/vanilla.ts | 14 ++++++++ 2 files changed, 46 insertions(+) create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/angular.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/vanilla.ts diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/angular.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/angular.ts new file mode 100644 index 000000000..a934336c9 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/angular.ts @@ -0,0 +1,32 @@ +import { Component } from "@angular/core"; +import { ScichartAngularComponent } from "scichart-angular"; +import { drawExample } from "./drawExample"; + +@Component({ + standalone: true, + imports: [ScichartAngularComponent], + selector: "app-eye-diagram-chart", + template: ` +
+ +
+ `, +}) +export class AppComponent { + initChart = drawExample; + private controls?: { startAnimation: () => void; stopAnimation: () => void; cleanup: () => void }; + + onInit(initResult: Awaited>) { + this.controls = initResult.controls; + this.controls.startAnimation(); + } + + onDelete(initResult: Awaited>) { + this.controls?.cleanup(); + } +} diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/vanilla.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/vanilla.ts new file mode 100644 index 000000000..7587d1a55 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/vanilla.ts @@ -0,0 +1,14 @@ +import { drawExample } from "./drawExample"; + +const create = async () => { + const { sciChartSurface, controls } = await drawExample("chart"); + controls.startAnimation(); + + const destructor = () => { + controls.cleanup(); + }; + + return destructor; +}; + +create(); From ff866361c68b39526b5de8bfe527b9a55a07a90a Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 01:11:14 +0200 Subject: [PATCH 07/18] feat(EyeDiagramChart): register eye diagram demo in routing and menu Register the EyeDiagramChart example in the AppRouter paths and Scientific Charts menu for routing discovery and navigation. --- Examples/src/components/AppRouter/examplePaths.ts | 1 + Examples/src/components/AppRouter/examples.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/Examples/src/components/AppRouter/examplePaths.ts b/Examples/src/components/AppRouter/examplePaths.ts index 7451b5a51..261512760 100644 --- a/Examples/src/components/AppRouter/examplePaths.ts +++ b/Examples/src/components/AppRouter/examplePaths.ts @@ -160,6 +160,7 @@ export default [ "../Examples/FeaturedApps/ScientificCharts/InteractiveWaterfallChart", "../Examples/FeaturedApps/ScientificCharts/LiDAR3DPointCloudDemo", "../Examples/FeaturedApps/ScientificCharts/PhasorDiagramChart", + "../Examples/FeaturedApps/ScientificCharts/EyeDiagramChart", "../Examples/FeaturedApps/ScientificCharts/Semiconductors", "../Examples/FeaturedApps/ScientificCharts/TenorCurves3D", "../Examples/FeaturedApps/ScientificCharts/WaferAnalysis", diff --git a/Examples/src/components/AppRouter/examples.ts b/Examples/src/components/AppRouter/examples.ts index 8036aa658..d3d63c95c 100644 --- a/Examples/src/components/AppRouter/examples.ts +++ b/Examples/src/components/AppRouter/examples.ts @@ -39,6 +39,7 @@ export const MENU_ITEMS_FEATURED_APPS: TMenuItem[] = [ EXAMPLES_PAGES.featuredApps_scientificCharts_AudioAnalyzerBarsDemo, EXAMPLES_PAGES.featuredApps_scientificCharts_WaterfallChartDemo, EXAMPLES_PAGES.featuredApps_scientificCharts_PhasorDiagramChart, + EXAMPLES_PAGES.featuredApps_scientificCharts_EyeDiagramChart, EXAMPLES_PAGES.featuredApps_scientificCharts_CorrelationPlot, EXAMPLES_PAGES.featuredApps_scientificCharts_Semiconductors, EXAMPLES_PAGES.featuredApps_scientificCharts_WaferAnalysis, From cdf708507545d17ea4b214afcd44eb92c3827ccc Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 01:12:04 +0200 Subject: [PATCH 08/18] chore(example): add placeholder thumbnail for EyeDiagramChart --- .../javascript-eye-diagram-chart.jpg | Bin 0 -> 22730 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/javascript-eye-diagram-chart.jpg diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/javascript-eye-diagram-chart.jpg b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/javascript-eye-diagram-chart.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e136dde2b94c7701602b2b7c7fd6fd47e0d53b2a GIT binary patch literal 22730 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 a0KDAv3TPFe?zbd#OHX?E2n3V=9R4S(&@EE{ literal 0 HcmV?d00001 From 516443444c81296fd7d26fcacbe433d7ceccde55 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 01:27:43 +0200 Subject: [PATCH 09/18] fix(example): increase trace resolution to 400 samples, add README, remove redundant startAnimation calls --- .../EyeDiagramChart/README.md | 30 +++++++++++++++++++ .../EyeDiagramChart/drawExample.ts | 6 ++-- .../EyeDiagramChart/index.tsx | 3 -- .../EyeDiagramChart/vanilla.ts | 3 +- 4 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md new file mode 100644 index 000000000..f822703c4 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md @@ -0,0 +1,30 @@ +## Real-time Eye Diagram (Persistence Display) + +### Overview + +An **eye diagram** is the standard oscilloscope tool for evaluating signal integrity in high-speed serial data links. It is produced by overlaying thousands of short waveform segments — each one a 2-UI (two bit-period) window sliced from a continuous data stream — on a single time axis. When many traces accumulate, the open central region (the "eye opening") becomes visible, along with the smearing caused by inter-symbol interference (ISI), timing jitter, and noise. + +This demo simulates a **Non-Return-to-Zero (NRZ)** serial data signal entirely in the browser and renders the accumulating eye diagram as a **2D density heatmap** using `UniformHeatmapRenderableSeries`. The result is an oscilloscope-style **persistence display**: frequently-traversed regions glow white/yellow, while rarely-visited edges fade to blue. + +### Technical Implementation + +**Signal generation** — Each trace is built from three random bits (prev, curr, next) to capture edge transitions on both sides of the 2-UI window: + +- Voltage levels: +1 V (high) and −1 V (low) +- **Raised-cosine transitions** spanning 20% of one UI for realistic edge shaping +- Per-transition **timing jitter** (Gaussian, σ ≈ 2.5% of UI) applied as a horizontal shift +- **Amplitude noise** (Gaussian, σ = 0.05 V) added to each sample + +**Heatmap accumulation** — Each trace's 400 samples are binned into a **400 × 200 accumulation grid** (time × voltage). Grid counts are log-scaled (`log1p`) before being passed to `UniformHeatmapDataSeries.setZValues()` each frame. The log scale keeps early traces visible while the hot centre saturates gracefully. + +**Colormap** — A custom `HeatmapColorMap` maps density to an oscilloscope palette: black → dark blue → cyan → yellow → white. + +**Performance** — 50 traces (20,000 samples) are generated and binned per animation frame. The heatmap is updated via a single `setZValues()` call per frame — one GPU texture upload regardless of how many traces have accumulated. A stats overlay shows live FPS, traces/second, and total accumulated traces. + +### Features + +- Real-time NRZ eye diagram with ISI, jitter, and noise +- Oscilloscope persistence display aesthetic +- 50 traces × 60 FPS = ~3,000 traces/second accumulation +- Stats overlay: FPS, Traces/s, Total traces +- No controls — pure performance display diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts index f70b1253d..99e62e391 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -28,11 +28,11 @@ function gaussianRandom(): number { * @param prev bit value (0 or 1) of the UI before the window * @param curr bit value (0 or 1) of the central UI (UI 0–1) * @param next bit value (0 or 1) of the UI after the window (UI 1–2) - * @returns Float32Array of 200 voltage samples + * @returns Float32Array of 400 voltage samples */ export function generateTrace(prev: number, curr: number, next: number): Float32Array { - const SAMPLES_PER_UI = 100; - const TOTAL_SAMPLES = 200; + const SAMPLES_PER_UI = 200; + const TOTAL_SAMPLES = 400; const TRANSITION_HALF = 10; // raised-cosine spans 20 samples (indices -10..+9) const JITTER_SIGMA = 2.5; // samples const NOISE_SIGMA = 0.05; // volts diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/index.tsx b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/index.tsx index 3c3923799..10426a96b 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/index.tsx +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/index.tsx @@ -8,9 +8,6 @@ export default function EyeDiagramChart() { drawExample(rootElementId)} - onInit={(initResult: TResolvedReturnType) => { - initResult.controls.startAnimation(); - }} onDelete={(initResult: TResolvedReturnType) => { initResult.controls.cleanup(); }} diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/vanilla.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/vanilla.ts index 7587d1a55..5e4b7fe5e 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/vanilla.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/vanilla.ts @@ -1,8 +1,7 @@ import { drawExample } from "./drawExample"; const create = async () => { - const { sciChartSurface, controls } = await drawExample("chart"); - controls.startAnimation(); + const { controls } = await drawExample("chart"); const destructor = () => { controls.cleanup(); From 1fda411cd4c251d79d90b82c884db9f683d1a82e Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 01:40:14 +0200 Subject: [PATCH 10/18] docs(example): add markdownContent overview for all frameworks in EyeDiagramChart --- .../ScientificCharts/EyeDiagramChart/exampleInfo.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx index ab7597216..25507dc43 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx @@ -18,7 +18,8 @@ const metaData: IExampleMetadata = pageTitle: "Real-time Eye Diagram (Persistence Display)", metaDescription: "See a real-time Eye Diagram rendered with SciChart.js. Thousands of NRZ waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", - markdownContent: null, + markdownContent: + "## Real-time Eye Diagram - JavaScript\n\n### Overview\nThis example demonstrates a **real-time Eye Diagram** (persistence display) using SciChart.js in JavaScript. An eye diagram is the standard tool for evaluating signal integrity in high-speed serial data links — it is produced by overlaying thousands of short NRZ waveform segments on a shared time axis, revealing the eye opening, jitter, noise, and inter-symbol interference (ISI) of the signal.\n\n### Technical Implementation\nThe chart uses [SciChartSurface.create()](https://www.scichart.com/documentation/js/v5/2d-charts/surface/new-scichart-surface/#scichartsurfacecreate) to initialise the surface. Each animation frame, 50 simulated NRZ traces are generated with raised-cosine edge shaping, per-transition timing jitter, and Gaussian amplitude noise. Trace samples are binned into a **400 × 200 accumulation grid** and passed to [UniformHeatmapDataSeries.setZValues()](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) each frame — a single WebGL texture upload regardless of how many traces have accumulated.\n\n### Features and Capabilities\nThe [UniformHeatmapRenderableSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) renders the density grid with a custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) (black → dark blue → cyan → yellow → white) that produces the classic oscilloscope persistence glow. A `log1p` scale keeps sparse edges visible while saturating the hot centre. A live stats overlay shows FPS, traces/second, and total accumulated traces.\n\n### Integration and Best Practices\nThe implementation uses a `requestAnimationFrame` loop with a clean start/stop/cleanup API. Resource cleanup (cancelling the rAF loop, removing the DOM overlay, calling `sciChartSurface.delete()`) is encapsulated in a single `cleanup()` function returned from `drawExample`, following the pattern used across all SciChart.js examples.", }, react: { subtitle: @@ -27,7 +28,8 @@ const metaData: IExampleMetadata = pageTitle: "Real-time Eye Diagram (Persistence Display)", metaDescription: "See a real-time Eye Diagram rendered with React and SciChart.js. Thousands of NRZ waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", - markdownContent: null, + markdownContent: + "## Real-time Eye Diagram - React\n\n### Overview\nThis React example demonstrates a **real-time Eye Diagram** (persistence display) built with SciChart.js. It simulates a high-speed NRZ serial data signal in the browser and renders the accumulating eye diagram as a 2D density heatmap — producing the iconic oscilloscope persistence glow that signal-integrity engineers use to evaluate jitter, noise, and eye opening.\n\n### Technical Implementation\nThe chart initialises via `` where `drawExample` creates a [UniformHeatmapDataSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) backed by a 400 × 200 accumulation grid. Each frame, 50 NRZ traces (with raised-cosine transitions, timing jitter, and amplitude noise) are generated and binned into the grid. The heatmap is updated via `setZValues()` — one GPU upload per frame. The `onDelete` prop calls `controls.cleanup()` which stops the animation loop and disposes the surface, following [React cleanup patterns for SciChart.js](https://www.scichart.com/documentation/js/v5/get-started/tutorials-react/tutorial-01-setting-up-project-with-scichart-react/).\n\n### Features and Capabilities\nA custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) (black → dark blue → cyan → yellow → white) with `log1p` density scaling produces the classic oscilloscope persistence palette. A live overlay shows FPS, traces/second, and total accumulated traces. The animation starts immediately on mount and cleans up automatically on unmount.\n\n### Integration and Best Practices\nThe component is intentionally minimal — no React state is needed because the animation lifecycle is entirely managed by the `controls` object returned from `drawExample`. This pattern keeps the React wrapper decoupled from the SciChart initialisation logic and makes the same `drawExample` function reusable across React, Angular, and vanilla JavaScript targets.", }, angular: { subtitle: @@ -36,7 +38,8 @@ const metaData: IExampleMetadata = pageTitle: "Real-time Eye Diagram (Persistence Display)", metaDescription: "See a real-time Eye Diagram rendered with Angular and SciChart.js. Thousands of NRZ waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", - markdownContent: null, + markdownContent: + "## Real-time Eye Diagram - Angular\n\n### Overview\nThis Angular example demonstrates a **real-time Eye Diagram** (persistence display) using SciChart.js and the [scichart-angular](https://www.npmjs.com/package/scichart-angular) package. It simulates a high-speed NRZ serial data signal and renders thousands of accumulating waveform traces as a heatmap density display — replicating the oscilloscope persistence effect used in signal-integrity analysis.\n\n### Technical Implementation\nThe standalone Angular component uses `` to initialise the SciChart surface. The `drawExample` function creates a [UniformHeatmapDataSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) backed by a 400 × 200 accumulation grid. Each animation frame, 50 NRZ traces (raised-cosine transitions, Gaussian jitter and noise) are generated and binned into the grid, then pushed to the GPU via `setZValues()`. Lifecycle is managed via `(onInit)` and `(onDelete)` event bindings, which start and clean up the animation loop.\n\n### Features and Capabilities\nA custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) with `log1p` density scaling maps the accumulation grid to a black → dark blue → cyan → yellow → white oscilloscope palette. Frequently-traversed regions glow white (the eye centre and bit rails), while sparse edge crossings fade to blue. A live stats overlay displays FPS, traces/second, and total accumulated traces.\n\n### Integration and Best Practices\nThe implementation follows Angular standalone component best practices. Resource cleanup (stopping the rAF loop, removing the DOM overlay, and calling `sciChartSurface.delete()`) is handled in a single `controls.cleanup()` call from the `(onDelete)` handler, preventing memory leaks when the component is destroyed.", }, }, documentationLinks: [ From b12174596350d3b385a622d2b9385567600a08bb Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 01:55:33 +0200 Subject: [PATCH 11/18] fix(example): reduce axis title font size to 11px in EyeDiagramChart --- .../ScientificCharts/EyeDiagramChart/drawExample.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts index 99e62e391..4aba500bf 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -121,6 +121,7 @@ export async function drawExample(rootElement: string | HTMLDivElement) { sciChartSurface.xAxes.add( new NumericAxis(wasmContext, { axisTitle: "Time (UI)", + axisTitleStyle: { fontSize: 11 }, visibleRange: new NumberRange(0, 2), autoRange: EAutoRange.Never, drawMajorGridLines: false, @@ -132,6 +133,7 @@ export async function drawExample(rootElement: string | HTMLDivElement) { sciChartSurface.yAxes.add( new NumericAxis(wasmContext, { axisTitle: "Voltage (V)", + axisTitleStyle: { fontSize: 11 }, visibleRange: new NumberRange(-1.5, 1.5), autoRange: EAutoRange.Never, drawMajorGridLines: false, From 04035363931e27ab8ae790f1abfdeaf06f965edb Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 01:56:40 +0200 Subject: [PATCH 12/18] fix(example): reduce axis label font size to 10px in EyeDiagramChart --- .../ScientificCharts/EyeDiagramChart/drawExample.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts index 4aba500bf..0ed92c410 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -122,6 +122,7 @@ export async function drawExample(rootElement: string | HTMLDivElement) { new NumericAxis(wasmContext, { axisTitle: "Time (UI)", axisTitleStyle: { fontSize: 11 }, + labelStyle: { fontSize: 10 }, visibleRange: new NumberRange(0, 2), autoRange: EAutoRange.Never, drawMajorGridLines: false, @@ -134,6 +135,7 @@ export async function drawExample(rootElement: string | HTMLDivElement) { new NumericAxis(wasmContext, { axisTitle: "Voltage (V)", axisTitleStyle: { fontSize: 11 }, + labelStyle: { fontSize: 10 }, visibleRange: new NumberRange(-1.5, 1.5), autoRange: EAutoRange.Never, drawMajorGridLines: false, From bd5d9b736e00eb367a975822b6a862a4623afa09 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 01:59:13 +0200 Subject: [PATCH 13/18] fix(example): switch to oscilloscope thermal colormap, widen transitions, scale jitter --- .../EyeDiagramChart/drawExample.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts index 0ed92c410..2785659aa 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -33,8 +33,8 @@ function gaussianRandom(): number { export function generateTrace(prev: number, curr: number, next: number): Float32Array { const SAMPLES_PER_UI = 200; const TOTAL_SAMPLES = 400; - const TRANSITION_HALF = 10; // raised-cosine spans 20 samples (indices -10..+9) - const JITTER_SIGMA = 2.5; // samples + const TRANSITION_HALF = 20; // raised-cosine spans 40 samples (~10% of 1 UI) + const JITTER_SIGMA = 5.0; // samples (~2.5% of 1 UI) const NOISE_SIGMA = 0.05; // volts // Voltage levels: bit 1 → +1 V, bit 0 → −1 V @@ -166,13 +166,14 @@ export async function drawExample(rootElement: string | HTMLDivElement) { useLinearTextureFiltering: true, colorMap: new HeatmapColorMap({ minimum: 0, - maximum: 10, + maximum: 8, gradientStops: [ - { offset: 0, color: "#000000" }, - { offset: 0.15, color: "#00008B" }, - { offset: 0.4, color: "#00FFFF" }, - { offset: 0.7, color: "#FFFF00" }, - { offset: 1, color: "#FFFFFF" }, + { offset: 0, color: "#000000" }, + { offset: 0.15, color: "#0000FF" }, + { offset: 0.4, color: "#00FFFF" }, + { offset: 0.6, color: "#00FF00" }, + { offset: 0.8, color: "#FFFF00" }, + { offset: 1, color: "#FF0000" }, ], }), }); From bb44a3626549b8589582a031643a7cb22a568c30 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 02:07:41 +0200 Subject: [PATCH 14/18] feat(example): add 3-crossing eye diagram model for authentic oscilloscope shape --- .../EyeDiagramChart/drawExample.ts | 76 ++++++++++--------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts index 2785659aa..ad553c20f 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -23,59 +23,60 @@ function gaussianRandom(): number { } /** - * Generates a 200-sample NRZ waveform for a 2-UI window. + * Generates a 400-sample NRZ waveform for a 2-UI window with crossings at t=0, 1UI, 2UI. + * Three visible crossing regions produce two eye openings matching a real oscilloscope display. * - * @param prev bit value (0 or 1) of the UI before the window - * @param curr bit value (0 or 1) of the central UI (UI 0–1) - * @param next bit value (0 or 1) of the UI after the window (UI 1–2) + * @param pprev bit value before the window start (drives crossing at t=0) + * @param prev bit value for the first half of the window (UI 0–1) + * @param curr bit value for the second half of the window (UI 1–2) + * @param next bit value after the window end (drives crossing at t=2UI) * @returns Float32Array of 400 voltage samples */ -export function generateTrace(prev: number, curr: number, next: number): Float32Array { +export function generateTrace(pprev: number, prev: number, curr: number, next: number): Float32Array { const SAMPLES_PER_UI = 200; const TOTAL_SAMPLES = 400; const TRANSITION_HALF = 20; // raised-cosine spans 40 samples (~10% of 1 UI) - const JITTER_SIGMA = 5.0; // samples (~2.5% of 1 UI) - const NOISE_SIGMA = 0.05; // volts + const JITTER_SIGMA = 4.0; // samples (~2% of 1 UI) + const NOISE_SIGMA = 0.012; // volts — kept low for clean, distinct crossing bands - // Voltage levels: bit 1 → +1 V, bit 0 → −1 V - const vPrev = prev === 1 ? 1.0 : -1.0; - const vCurr = curr === 1 ? 1.0 : -1.0; - const vNext = next === 1 ? 1.0 : -1.0; + const vPPrev = pprev === 1 ? 1.0 : -1.0; + const vPrev = prev === 1 ? 1.0 : -1.0; + const vCurr = curr === 1 ? 1.0 : -1.0; + const vNext = next === 1 ? 1.0 : -1.0; const out = new Float32Array(TOTAL_SAMPLES); - // Transition edge positions (in samples, relative to start of 2-UI window) - const jitter0 = Math.round(gaussianRandom() * JITTER_SIGMA); - const jitter1 = Math.round(gaussianRandom() * JITTER_SIGMA); - const edge0 = SAMPLES_PER_UI + jitter0; // transition prev→curr at sample 100 - const edge1 = 2 * SAMPLES_PER_UI + jitter1; // transition curr→next at sample 200 + // Three crossing edges: at t=0, t=1UI, t=2UI (each with independent jitter) + const edgeA = 0 + Math.round(gaussianRandom() * JITTER_SIGMA); // pprev→prev + const edgeB = SAMPLES_PER_UI + Math.round(gaussianRandom() * JITTER_SIGMA); // prev→curr + const edgeC = 2 * SAMPLES_PER_UI + Math.round(gaussianRandom() * JITTER_SIGMA); // curr→next for (let i = 0; i < TOTAL_SAMPLES; i++) { + const dA = i - edgeA; + const dB = i - edgeB; + const dC = i - edgeC; + let v: number; - // Determine voltage contribution from each transition using raised cosine - const d0 = i - edge0; // distance from first edge - const d1 = i - edge1; // distance from second edge - - if (d0 >= -TRANSITION_HALF && d0 < TRANSITION_HALF) { - // Within raised-cosine transition zone for edge 0 (prev → curr) - const t = (d0 + TRANSITION_HALF) / (2 * TRANSITION_HALF); // 0..1 - const rc = 0.5 * (1 - Math.cos(Math.PI * t)); - v = vPrev + (vCurr - vPrev) * rc; - } else if (d1 >= -TRANSITION_HALF && d1 < TRANSITION_HALF) { - // Within raised-cosine transition zone for edge 1 (curr → next) - const t = (d1 + TRANSITION_HALF) / (2 * TRANSITION_HALF); // 0..1 - const rc = 0.5 * (1 - Math.cos(Math.PI * t)); - v = vCurr + (vNext - vCurr) * rc; - } else if (i < edge0) { + if (dA >= -TRANSITION_HALF && dA < TRANSITION_HALF) { + const t = (dA + TRANSITION_HALF) / (2 * TRANSITION_HALF); + v = vPPrev + (vPrev - vPPrev) * 0.5 * (1 - Math.cos(Math.PI * t)); + } else if (dB >= -TRANSITION_HALF && dB < TRANSITION_HALF) { + const t = (dB + TRANSITION_HALF) / (2 * TRANSITION_HALF); + v = vPrev + (vCurr - vPrev) * 0.5 * (1 - Math.cos(Math.PI * t)); + } else if (dC >= -TRANSITION_HALF && dC < TRANSITION_HALF) { + const t = (dC + TRANSITION_HALF) / (2 * TRANSITION_HALF); + v = vCurr + (vNext - vCurr) * 0.5 * (1 - Math.cos(Math.PI * t)); + } else if (i < edgeA) { + v = vPPrev; + } else if (i < edgeB) { v = vPrev; - } else if (i < edge1) { + } else if (i < edgeC) { v = vCurr; } else { v = vNext; } - // Gaussian amplitude noise out[i] = v + gaussianRandom() * NOISE_SIGMA; } @@ -217,10 +218,11 @@ export async function drawExample(rootElement: string | HTMLDivElement) { // Generate and bin 50 traces this frame for (let t = 0; t < TRACES_PER_FRAME; t++) { - const prev = Math.random() < 0.5 ? 0 : 1; - const curr = Math.random() < 0.5 ? 0 : 1; - const next = Math.random() < 0.5 ? 0 : 1; - const trace = generateTrace(prev, curr, next); + const pprev = Math.random() < 0.5 ? 0 : 1; + const prev = Math.random() < 0.5 ? 0 : 1; + const curr = Math.random() < 0.5 ? 0 : 1; + const next = Math.random() < 0.5 ? 0 : 1; + const trace = generateTrace(pprev, prev, curr, next); binTrace(grid, trace); } totalTraces += TRACES_PER_FRAME; From 9ab9fb1ba50836ee662d4f980d9334a05126f634 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 02:15:19 +0200 Subject: [PATCH 15/18] feat(example): switch signal model to MLT-3 (three-level, two stacked eyes) --- .../EyeDiagramChart/drawExample.ts | 82 ++++++++----------- .../EyeDiagramChart/exampleInfo.tsx | 2 +- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts index ad553c20f..75d505bfa 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -23,63 +23,57 @@ function gaussianRandom(): number { } /** - * Generates a 400-sample NRZ waveform for a 2-UI window with crossings at t=0, 1UI, 2UI. - * Three visible crossing regions produce two eye openings matching a real oscilloscope display. + * Generates a 400-sample MLT-3 waveform for a 2-UI window. + * + * MLT-3 cycles through voltage levels [+1, 0, -1, 0] — the signal advances to the + * next level on each '1' bit and holds on each '0' bit. This produces three voltage + * rails and two stacked eye openings when overlaid. + * + * Three crossings are placed at t=0, t=1UI, t=2UI to produce the standard + * oscilloscope eye pattern (two openings per display window). * - * @param pprev bit value before the window start (drives crossing at t=0) - * @param prev bit value for the first half of the window (UI 0–1) - * @param curr bit value for the second half of the window (UI 1–2) - * @param next bit value after the window end (drives crossing at t=2UI) * @returns Float32Array of 400 voltage samples */ -export function generateTrace(pprev: number, prev: number, curr: number, next: number): Float32Array { +export function generateTrace(): Float32Array { const SAMPLES_PER_UI = 200; const TOTAL_SAMPLES = 400; const TRANSITION_HALF = 20; // raised-cosine spans 40 samples (~10% of 1 UI) const JITTER_SIGMA = 4.0; // samples (~2% of 1 UI) - const NOISE_SIGMA = 0.012; // volts — kept low for clean, distinct crossing bands + const NOISE_SIGMA = 0.012; // volts — low for clean, distinct crossing bands - const vPPrev = pprev === 1 ? 1.0 : -1.0; - const vPrev = prev === 1 ? 1.0 : -1.0; - const vCurr = curr === 1 ? 1.0 : -1.0; - const vNext = next === 1 ? 1.0 : -1.0; + // MLT-3 state machine: state index cycles 0→1→2→3→0, voltages [+1, 0, -1, 0] + const STATE_V = [1.0, 0.0, -1.0, 0.0] as const; + + // Start at a random state, then generate 4 consecutive levels (pprev, prev, curr, next) + // Each position: 50% chance to advance state (bit=1), 50% to hold (bit=0) + let state = Math.floor(Math.random() * 4); + const vLevels = Array.from({ length: 4 }, () => { + if (Math.random() < 0.5) state = (state + 1) % 4; + return STATE_V[state]; + }); + const [vPPrev, vPrev, vCurr, vNext] = vLevels; const out = new Float32Array(TOTAL_SAMPLES); - // Three crossing edges: at t=0, t=1UI, t=2UI (each with independent jitter) - const edgeA = 0 + Math.round(gaussianRandom() * JITTER_SIGMA); // pprev→prev - const edgeB = SAMPLES_PER_UI + Math.round(gaussianRandom() * JITTER_SIGMA); // prev→curr - const edgeC = 2 * SAMPLES_PER_UI + Math.round(gaussianRandom() * JITTER_SIGMA); // curr→next + // Three crossing edges at t=0, t=1UI, t=2UI with independent per-edge jitter + const edgeA = 0 + Math.round(gaussianRandom() * JITTER_SIGMA); + const edgeB = SAMPLES_PER_UI + Math.round(gaussianRandom() * JITTER_SIGMA); + const edgeC = 2 * SAMPLES_PER_UI + Math.round(gaussianRandom() * JITTER_SIGMA); - for (let i = 0; i < TOTAL_SAMPLES; i++) { - const dA = i - edgeA; - const dB = i - edgeB; - const dC = i - edgeC; + const rc = (d: number) => 0.5 * (1 - Math.cos(Math.PI * (d + TRANSITION_HALF) / (2 * TRANSITION_HALF))); + for (let i = 0; i < TOTAL_SAMPLES; i++) { + const dA = i - edgeA, dB = i - edgeB, dC = i - edgeC; let v: number; - - if (dA >= -TRANSITION_HALF && dA < TRANSITION_HALF) { - const t = (dA + TRANSITION_HALF) / (2 * TRANSITION_HALF); - v = vPPrev + (vPrev - vPPrev) * 0.5 * (1 - Math.cos(Math.PI * t)); - } else if (dB >= -TRANSITION_HALF && dB < TRANSITION_HALF) { - const t = (dB + TRANSITION_HALF) / (2 * TRANSITION_HALF); - v = vPrev + (vCurr - vPrev) * 0.5 * (1 - Math.cos(Math.PI * t)); - } else if (dC >= -TRANSITION_HALF && dC < TRANSITION_HALF) { - const t = (dC + TRANSITION_HALF) / (2 * TRANSITION_HALF); - v = vCurr + (vNext - vCurr) * 0.5 * (1 - Math.cos(Math.PI * t)); - } else if (i < edgeA) { - v = vPPrev; - } else if (i < edgeB) { - v = vPrev; - } else if (i < edgeC) { - v = vCurr; - } else { - v = vNext; - } - + if (dA >= -TRANSITION_HALF && dA < TRANSITION_HALF) v = vPPrev + (vPrev - vPPrev) * rc(dA); + else if (dB >= -TRANSITION_HALF && dB < TRANSITION_HALF) v = vPrev + (vCurr - vPrev) * rc(dB); + else if (dC >= -TRANSITION_HALF && dC < TRANSITION_HALF) v = vCurr + (vNext - vCurr) * rc(dC); + else if (i < edgeA) v = vPPrev; + else if (i < edgeB) v = vPrev; + else if (i < edgeC) v = vCurr; + else v = vNext; out[i] = v + gaussianRandom() * NOISE_SIGMA; } - return out; } @@ -218,11 +212,7 @@ export async function drawExample(rootElement: string | HTMLDivElement) { // Generate and bin 50 traces this frame for (let t = 0; t < TRACES_PER_FRAME; t++) { - const pprev = Math.random() < 0.5 ? 0 : 1; - const prev = Math.random() < 0.5 ? 0 : 1; - const curr = Math.random() < 0.5 ? 0 : 1; - const next = Math.random() < 0.5 ? 0 : 1; - const trace = generateTrace(pprev, prev, curr, next); + const trace = generateTrace(); binTrace(grid, trace); } totalTraces += TRACES_PER_FRAME; diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx index 25507dc43..c7a927cea 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx @@ -8,7 +8,7 @@ const metaData: IExampleMetadata = id: "featuredApps_scientificCharts_EyeDiagramChart", imagePath: "javascript-eye-diagram-chart.jpg", description: - "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Thousands of NRZ waveform traces accumulate per second into a 2D density grid, producing the iconic oscilloscope glow effect.", + "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Simulates an **MLT-3** signal (three voltage levels, used in 100BASE-TX Ethernet) — thousands of traces accumulate into a 2D density grid, producing the two-eye oscilloscope persistence pattern.", tips: [], frameworks: { javascript: { From 11280f4300b76460da5cd43eb4715dfbb1c030d3 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 02:20:38 +0200 Subject: [PATCH 16/18] fix(example): widen transitions to 35% UI, tighten Y range to match MLT-3 image --- .../ScientificCharts/EyeDiagramChart/drawExample.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts index 75d505bfa..aba9495c2 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -37,7 +37,7 @@ function gaussianRandom(): number { export function generateTrace(): Float32Array { const SAMPLES_PER_UI = 200; const TOTAL_SAMPLES = 400; - const TRANSITION_HALF = 20; // raised-cosine spans 40 samples (~10% of 1 UI) + const TRANSITION_HALF = 35; // raised-cosine spans 70 samples (~35% of 1 UI — matches real 100BASE-TX rise time) const JITTER_SIGMA = 4.0; // samples (~2% of 1 UI) const NOISE_SIGMA = 0.012; // volts — low for clean, distinct crossing bands @@ -131,7 +131,7 @@ export async function drawExample(rootElement: string | HTMLDivElement) { axisTitle: "Voltage (V)", axisTitleStyle: { fontSize: 11 }, labelStyle: { fontSize: 10 }, - visibleRange: new NumberRange(-1.5, 1.5), + visibleRange: new NumberRange(-1.15, 1.15), autoRange: EAutoRange.Never, drawMajorGridLines: false, drawMinorGridLines: false, @@ -150,8 +150,8 @@ export async function drawExample(rootElement: string | HTMLDivElement) { const dataSeries = new UniformHeatmapDataSeries(wasmContext, { xStart: 0, xStep: 2 / (COLS - 1), - yStart: -1.5, - yStep: 3 / (ROWS - 1), + yStart: -1.15, + yStep: 2.3 / (ROWS - 1), zValues: zValuesLog, }); From e25c9b4eb6d9cac82a35002a73df219fe96a2395 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Thu, 23 Apr 2026 12:05:23 +0200 Subject: [PATCH 17/18] docs(example): update eye diagram descriptions and signal realism Switch all documentation from NRZ to MLT-3 (README, subtitles, markdownContent for JS/React/Angular, metaKeywords). Add per-trace amplitude scaling, DC offset, and independent per-edge rise time variation to generateTrace() for a more realistic oscilloscope look. --- .../EyeDiagramChart/README.md | 20 +++++------ .../EyeDiagramChart/drawExample.ts | 34 +++++++++++++------ .../EyeDiagramChart/exampleInfo.tsx | 20 +++++------ 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md index f822703c4..7e70b5306 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md @@ -2,29 +2,29 @@ ### Overview -An **eye diagram** is the standard oscilloscope tool for evaluating signal integrity in high-speed serial data links. It is produced by overlaying thousands of short waveform segments — each one a 2-UI (two bit-period) window sliced from a continuous data stream — on a single time axis. When many traces accumulate, the open central region (the "eye opening") becomes visible, along with the smearing caused by inter-symbol interference (ISI), timing jitter, and noise. +An **eye diagram** is the standard oscilloscope tool for evaluating signal integrity in high-speed serial data links. It is produced by overlaying thousands of short waveform segments — each one a 2-UI (two bit-period) window sliced from a continuous data stream — on a single time axis. When many traces accumulate, the open central regions (the "eye openings") become visible, along with the smearing caused by inter-symbol interference (ISI), timing jitter, and noise. -This demo simulates a **Non-Return-to-Zero (NRZ)** serial data signal entirely in the browser and renders the accumulating eye diagram as a **2D density heatmap** using `UniformHeatmapRenderableSeries`. The result is an oscilloscope-style **persistence display**: frequently-traversed regions glow white/yellow, while rarely-visited edges fade to blue. +This demo simulates an **MLT-3 (Multi-Level Transmit 3)** serial data signal — the line code used in 100BASE-TX Ethernet — entirely in the browser and renders the accumulating eye diagram as a **2D density heatmap** using `UniformHeatmapRenderableSeries`. The result is an oscilloscope-style **persistence display**: frequently-traversed regions glow red/yellow, while rarely-visited edges fade to blue. MLT-3 cycles through three voltage levels (+1 V, 0 V, −1 V) via a four-state machine, producing **two stacked eye openings** per display window. ### Technical Implementation -**Signal generation** — Each trace is built from three random bits (prev, curr, next) to capture edge transitions on both sides of the 2-UI window: +**Signal generation** — Each trace is built from a random MLT-3 state sequence (pprev, prev, curr, next) to capture both transitions in the 2-UI window: -- Voltage levels: +1 V (high) and −1 V (low) -- **Raised-cosine transitions** spanning 20% of one UI for realistic edge shaping -- Per-transition **timing jitter** (Gaussian, σ ≈ 2.5% of UI) applied as a horizontal shift -- **Amplitude noise** (Gaussian, σ = 0.05 V) added to each sample +- Voltage levels: +1 V, 0 V, −1 V (four states: [+1, 0, −1, 0]) +- **Raised-cosine transitions** spanning ~35% of one UI — matching real 100BASE-TX rise time +- Three crossing edges at t = 0, t = 1 UI, t = 2 UI with independent per-edge **timing jitter** (Gaussian, σ = 4.0 samples ≈ 2% of UI) +- **Amplitude noise** (Gaussian, σ = 0.012 V) added to each sample **Heatmap accumulation** — Each trace's 400 samples are binned into a **400 × 200 accumulation grid** (time × voltage). Grid counts are log-scaled (`log1p`) before being passed to `UniformHeatmapDataSeries.setZValues()` each frame. The log scale keeps early traces visible while the hot centre saturates gracefully. -**Colormap** — A custom `HeatmapColorMap` maps density to an oscilloscope palette: black → dark blue → cyan → yellow → white. +**Colormap** — A custom `HeatmapColorMap` maps density to an oscilloscope thermal palette: black → dark blue → cyan → green → yellow → red. **Performance** — 50 traces (20,000 samples) are generated and binned per animation frame. The heatmap is updated via a single `setZValues()` call per frame — one GPU texture upload regardless of how many traces have accumulated. A stats overlay shows live FPS, traces/second, and total accumulated traces. ### Features -- Real-time NRZ eye diagram with ISI, jitter, and noise -- Oscilloscope persistence display aesthetic +- Real-time MLT-3 eye diagram with two stacked eye openings, ISI, jitter, and noise +- Oscilloscope thermal persistence display aesthetic (black → blue → cyan → yellow → red) - 50 traces × 60 FPS = ~3,000 traces/second accumulation - Stats overlay: FPS, Traces/s, Total traces - No controls — pure performance display diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts index aba9495c2..ebd236be6 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -37,19 +37,26 @@ function gaussianRandom(): number { export function generateTrace(): Float32Array { const SAMPLES_PER_UI = 200; const TOTAL_SAMPLES = 400; - const TRANSITION_HALF = 35; // raised-cosine spans 70 samples (~35% of 1 UI — matches real 100BASE-TX rise time) - const JITTER_SIGMA = 4.0; // samples (~2% of 1 UI) - const NOISE_SIGMA = 0.012; // volts — low for clean, distinct crossing bands + const BASE_RISE_HALF = 35; // raised-cosine half-width (~35% of 1 UI — real 100BASE-TX rise time) + const JITTER_SIGMA = 4.0; // samples (~2% of 1 UI) + const NOISE_SIGMA = 0.010; // volts — per-sample Gaussian noise + const AMPLITUDE_SIGMA = 0.025; // ±2.5% per-trace amplitude scaling + const DC_OFFSET_SIGMA = 0.015; // volts — per-trace DC offset + const RISE_SIGMA = 2.8; // samples — per-edge rise time variation // MLT-3 state machine: state index cycles 0→1→2→3→0, voltages [+1, 0, -1, 0] const STATE_V = [1.0, 0.0, -1.0, 0.0] as const; + // Per-trace capture variations — real scope traces drift slightly in amplitude and DC level + const amplitudeScale = 1.0 + gaussianRandom() * AMPLITUDE_SIGMA; + const dcOffset = gaussianRandom() * DC_OFFSET_SIGMA; + // Start at a random state, then generate 4 consecutive levels (pprev, prev, curr, next) // Each position: 50% chance to advance state (bit=1), 50% to hold (bit=0) let state = Math.floor(Math.random() * 4); const vLevels = Array.from({ length: 4 }, () => { if (Math.random() < 0.5) state = (state + 1) % 4; - return STATE_V[state]; + return STATE_V[state] * amplitudeScale + dcOffset; }); const [vPPrev, vPrev, vCurr, vNext] = vLevels; @@ -60,14 +67,21 @@ export function generateTrace(): Float32Array { const edgeB = SAMPLES_PER_UI + Math.round(gaussianRandom() * JITTER_SIGMA); const edgeC = 2 * SAMPLES_PER_UI + Math.round(gaussianRandom() * JITTER_SIGMA); - const rc = (d: number) => 0.5 * (1 - Math.cos(Math.PI * (d + TRANSITION_HALF) / (2 * TRANSITION_HALF))); + // Independent rise time per edge — each crossing X-pattern gets its own slew, producing + // realistic fuzz at the crossings instead of three identical curves stacked on top of each other + const clampRise = (r: number) => Math.max(26, Math.min(44, r)); + const riseA = clampRise(Math.round(BASE_RISE_HALF + gaussianRandom() * RISE_SIGMA)); + const riseB = clampRise(Math.round(BASE_RISE_HALF + gaussianRandom() * RISE_SIGMA)); + const riseC = clampRise(Math.round(BASE_RISE_HALF + gaussianRandom() * RISE_SIGMA)); + + const rc = (d: number, half: number) => 0.5 * (1 - Math.cos(Math.PI * (d + half) / (2 * half))); for (let i = 0; i < TOTAL_SAMPLES; i++) { const dA = i - edgeA, dB = i - edgeB, dC = i - edgeC; let v: number; - if (dA >= -TRANSITION_HALF && dA < TRANSITION_HALF) v = vPPrev + (vPrev - vPPrev) * rc(dA); - else if (dB >= -TRANSITION_HALF && dB < TRANSITION_HALF) v = vPrev + (vCurr - vPrev) * rc(dB); - else if (dC >= -TRANSITION_HALF && dC < TRANSITION_HALF) v = vCurr + (vNext - vCurr) * rc(dC); + if (dA >= -riseA && dA < riseA) v = vPPrev + (vPrev - vPPrev) * rc(dA, riseA); + else if (dB >= -riseB && dB < riseB) v = vPrev + (vCurr - vPrev) * rc(dB, riseB); + else if (dC >= -riseC && dC < riseC) v = vCurr + (vNext - vCurr) * rc(dC, riseC); else if (i < edgeA) v = vPPrev; else if (i < edgeB) v = vPrev; else if (i < edgeC) v = vCurr; @@ -115,7 +129,7 @@ export async function drawExample(rootElement: string | HTMLDivElement) { // X Axis — time in UI units sciChartSurface.xAxes.add( new NumericAxis(wasmContext, { - axisTitle: "Time (UI)", + axisTitle: "Time", axisTitleStyle: { fontSize: 11 }, labelStyle: { fontSize: 10 }, visibleRange: new NumberRange(0, 2), @@ -128,7 +142,7 @@ export async function drawExample(rootElement: string | HTMLDivElement) { // Y Axis — voltage sciChartSurface.yAxes.add( new NumericAxis(wasmContext, { - axisTitle: "Voltage (V)", + axisTitle: "Voltage", axisTitleStyle: { fontSize: 11 }, labelStyle: { fontSize: 10 }, visibleRange: new NumberRange(-1.15, 1.15), diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx index c7a927cea..8d733dbcc 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx @@ -13,33 +13,33 @@ const metaData: IExampleMetadata = frameworks: { javascript: { subtitle: - "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Thousands of NRZ waveform traces accumulate per second into a 2D density grid, producing the iconic oscilloscope glow effect.", + "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Simulates an **MLT-3** signal (three voltage levels, used in 100BASE-TX Ethernet) — thousands of traces accumulate into a 2D density grid, producing two stacked eye openings with the classic oscilloscope glow.", title: "Real-time Eye Diagram Chart Example", pageTitle: "Real-time Eye Diagram (Persistence Display)", metaDescription: - "See a real-time Eye Diagram rendered with SciChart.js. Thousands of NRZ waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", + "See a real-time Eye Diagram rendered with SciChart.js. Simulates an MLT-3 signal — thousands of waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", markdownContent: - "## Real-time Eye Diagram - JavaScript\n\n### Overview\nThis example demonstrates a **real-time Eye Diagram** (persistence display) using SciChart.js in JavaScript. An eye diagram is the standard tool for evaluating signal integrity in high-speed serial data links — it is produced by overlaying thousands of short NRZ waveform segments on a shared time axis, revealing the eye opening, jitter, noise, and inter-symbol interference (ISI) of the signal.\n\n### Technical Implementation\nThe chart uses [SciChartSurface.create()](https://www.scichart.com/documentation/js/v5/2d-charts/surface/new-scichart-surface/#scichartsurfacecreate) to initialise the surface. Each animation frame, 50 simulated NRZ traces are generated with raised-cosine edge shaping, per-transition timing jitter, and Gaussian amplitude noise. Trace samples are binned into a **400 × 200 accumulation grid** and passed to [UniformHeatmapDataSeries.setZValues()](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) each frame — a single WebGL texture upload regardless of how many traces have accumulated.\n\n### Features and Capabilities\nThe [UniformHeatmapRenderableSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) renders the density grid with a custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) (black → dark blue → cyan → yellow → white) that produces the classic oscilloscope persistence glow. A `log1p` scale keeps sparse edges visible while saturating the hot centre. A live stats overlay shows FPS, traces/second, and total accumulated traces.\n\n### Integration and Best Practices\nThe implementation uses a `requestAnimationFrame` loop with a clean start/stop/cleanup API. Resource cleanup (cancelling the rAF loop, removing the DOM overlay, calling `sciChartSurface.delete()`) is encapsulated in a single `cleanup()` function returned from `drawExample`, following the pattern used across all SciChart.js examples.", + "## Real-time Eye Diagram - JavaScript\n\n### Overview\nThis example demonstrates a **real-time Eye Diagram** (persistence display) using SciChart.js in JavaScript. An eye diagram is the standard tool for evaluating signal integrity in high-speed serial data links — it is produced by overlaying thousands of short waveform segments on a shared time axis, revealing the eye opening, jitter, noise, and inter-symbol interference (ISI) of the signal. This demo simulates an **MLT-3 (Multi-Level Transmit 3)** signal, the line code used in 100BASE-TX Ethernet, which cycles through three voltage levels (+1 V, 0 V, −1 V) to produce **two stacked eye openings** per display window.\n\n### Technical Implementation\nThe chart uses [SciChartSurface.create()](https://www.scichart.com/documentation/js/v5/2d-charts/surface/new-scichart-surface/#scichartsurfacecreate) to initialise the surface. Each animation frame, 50 simulated MLT-3 traces are generated with a four-state voltage machine, raised-cosine edge shaping (spanning ~35% of one UI — matching real 100BASE-TX rise time), per-crossing timing jitter (Gaussian, σ = 4 samples ≈ 2% UI), and Gaussian amplitude noise (σ = 0.012 V). Three crossing edges are placed at t = 0, t = 1 UI, and t = 2 UI. Trace samples are binned into a **400 × 200 accumulation grid** and passed to [UniformHeatmapDataSeries.setZValues()](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) each frame — a single WebGL texture upload regardless of how many traces have accumulated.\n\n### Features and Capabilities\nThe [UniformHeatmapRenderableSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) renders the density grid with a custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) using an oscilloscope thermal palette (black → dark blue → cyan → green → yellow → red) that produces the classic scope persistence glow. A `log1p` scale keeps sparse edges visible while saturating the hot centre. A live stats overlay shows FPS, traces/second, and total accumulated traces.\n\n### Integration and Best Practices\nThe implementation uses a `requestAnimationFrame` loop with a clean start/stop/cleanup API. Resource cleanup (cancelling the rAF loop, removing the DOM overlay, calling `sciChartSurface.delete()`) is encapsulated in a single `cleanup()` function returned from `drawExample`, following the pattern used across all SciChart.js examples.", }, react: { subtitle: - "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Thousands of NRZ waveform traces accumulate per second into a 2D density grid, producing the iconic oscilloscope glow effect.", + "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Simulates an **MLT-3** signal (three voltage levels, used in 100BASE-TX Ethernet) — thousands of traces accumulate into a 2D density grid, producing two stacked eye openings with the classic oscilloscope glow.", title: "Real-time Eye Diagram Chart Example", pageTitle: "Real-time Eye Diagram (Persistence Display)", metaDescription: - "See a real-time Eye Diagram rendered with React and SciChart.js. Thousands of NRZ waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", + "See a real-time Eye Diagram rendered with React and SciChart.js. Simulates an MLT-3 signal — thousands of waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", markdownContent: - "## Real-time Eye Diagram - React\n\n### Overview\nThis React example demonstrates a **real-time Eye Diagram** (persistence display) built with SciChart.js. It simulates a high-speed NRZ serial data signal in the browser and renders the accumulating eye diagram as a 2D density heatmap — producing the iconic oscilloscope persistence glow that signal-integrity engineers use to evaluate jitter, noise, and eye opening.\n\n### Technical Implementation\nThe chart initialises via `` where `drawExample` creates a [UniformHeatmapDataSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) backed by a 400 × 200 accumulation grid. Each frame, 50 NRZ traces (with raised-cosine transitions, timing jitter, and amplitude noise) are generated and binned into the grid. The heatmap is updated via `setZValues()` — one GPU upload per frame. The `onDelete` prop calls `controls.cleanup()` which stops the animation loop and disposes the surface, following [React cleanup patterns for SciChart.js](https://www.scichart.com/documentation/js/v5/get-started/tutorials-react/tutorial-01-setting-up-project-with-scichart-react/).\n\n### Features and Capabilities\nA custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) (black → dark blue → cyan → yellow → white) with `log1p` density scaling produces the classic oscilloscope persistence palette. A live overlay shows FPS, traces/second, and total accumulated traces. The animation starts immediately on mount and cleans up automatically on unmount.\n\n### Integration and Best Practices\nThe component is intentionally minimal — no React state is needed because the animation lifecycle is entirely managed by the `controls` object returned from `drawExample`. This pattern keeps the React wrapper decoupled from the SciChart initialisation logic and makes the same `drawExample` function reusable across React, Angular, and vanilla JavaScript targets.", + "## Real-time Eye Diagram - React\n\n### Overview\nThis React example demonstrates a **real-time Eye Diagram** (persistence display) built with SciChart.js. It simulates an **MLT-3 (Multi-Level Transmit 3)** serial data signal — the line code used in 100BASE-TX Ethernet — and renders the accumulating eye diagram as a 2D density heatmap, producing the oscilloscope persistence glow that signal-integrity engineers use to evaluate jitter, noise, and eye opening. MLT-3's three voltage levels (+1 V, 0 V, −1 V) create **two stacked eye openings** visible once enough traces have accumulated.\n\n### Technical Implementation\nThe chart initialises via `` where `drawExample` creates a [UniformHeatmapDataSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) backed by a 400 × 200 accumulation grid. Each frame, 50 MLT-3 traces (with raised-cosine transitions spanning ~35% of one UI, per-crossing timing jitter σ = 4 samples, and amplitude noise σ = 0.012 V) are generated and binned into the grid. The heatmap is updated via `setZValues()` — one GPU upload per frame. The `onDelete` prop calls `controls.cleanup()` which stops the animation loop and disposes the surface, following [React cleanup patterns for SciChart.js](https://www.scichart.com/documentation/js/v5/get-started/tutorials-react/tutorial-01-setting-up-project-with-scichart-react/).\n\n### Features and Capabilities\nA custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) using an oscilloscope thermal palette (black → dark blue → cyan → green → yellow → red) with `log1p` density scaling produces the classic scope persistence glow. A live overlay shows FPS, traces/second, and total accumulated traces. The animation starts immediately on mount and cleans up automatically on unmount.\n\n### Integration and Best Practices\nThe component is intentionally minimal — no React state is needed because the animation lifecycle is entirely managed by the `controls` object returned from `drawExample`. This pattern keeps the React wrapper decoupled from the SciChart initialisation logic and makes the same `drawExample` function reusable across React, Angular, and vanilla JavaScript targets.", }, angular: { subtitle: - "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Thousands of NRZ waveform traces accumulate per second into a 2D density grid, producing the iconic oscilloscope glow effect.", + "Demonstrates a real-time **Eye Diagram** (persistence display) using SciChart.js heatmap rendering. Simulates an **MLT-3** signal (three voltage levels, used in 100BASE-TX Ethernet) — thousands of traces accumulate into a 2D density grid, producing two stacked eye openings with the classic oscilloscope glow.", title: "Real-time Eye Diagram Chart Example", pageTitle: "Real-time Eye Diagram (Persistence Display)", metaDescription: - "See a real-time Eye Diagram rendered with Angular and SciChart.js. Thousands of NRZ waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", + "See a real-time Eye Diagram rendered with Angular and SciChart.js. Simulates an MLT-3 signal — thousands of waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", markdownContent: - "## Real-time Eye Diagram - Angular\n\n### Overview\nThis Angular example demonstrates a **real-time Eye Diagram** (persistence display) using SciChart.js and the [scichart-angular](https://www.npmjs.com/package/scichart-angular) package. It simulates a high-speed NRZ serial data signal and renders thousands of accumulating waveform traces as a heatmap density display — replicating the oscilloscope persistence effect used in signal-integrity analysis.\n\n### Technical Implementation\nThe standalone Angular component uses `` to initialise the SciChart surface. The `drawExample` function creates a [UniformHeatmapDataSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) backed by a 400 × 200 accumulation grid. Each animation frame, 50 NRZ traces (raised-cosine transitions, Gaussian jitter and noise) are generated and binned into the grid, then pushed to the GPU via `setZValues()`. Lifecycle is managed via `(onInit)` and `(onDelete)` event bindings, which start and clean up the animation loop.\n\n### Features and Capabilities\nA custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) with `log1p` density scaling maps the accumulation grid to a black → dark blue → cyan → yellow → white oscilloscope palette. Frequently-traversed regions glow white (the eye centre and bit rails), while sparse edge crossings fade to blue. A live stats overlay displays FPS, traces/second, and total accumulated traces.\n\n### Integration and Best Practices\nThe implementation follows Angular standalone component best practices. Resource cleanup (stopping the rAF loop, removing the DOM overlay, and calling `sciChartSurface.delete()`) is handled in a single `controls.cleanup()` call from the `(onDelete)` handler, preventing memory leaks when the component is destroyed.", + "## Real-time Eye Diagram - Angular\n\n### Overview\nThis Angular example demonstrates a **real-time Eye Diagram** (persistence display) using SciChart.js and the [scichart-angular](https://www.npmjs.com/package/scichart-angular) package. It simulates an **MLT-3 (Multi-Level Transmit 3)** serial data signal — the line code used in 100BASE-TX Ethernet — and renders thousands of accumulating waveform traces as a heatmap density display, replicating the oscilloscope persistence effect used in signal-integrity analysis. MLT-3's four-state voltage machine (+1 V → 0 V → −1 V → 0 V) produces **two stacked eye openings** per display window.\n\n### Technical Implementation\nThe standalone Angular component uses `` to initialise the SciChart surface. The `drawExample` function creates a [UniformHeatmapDataSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) backed by a 400 × 200 accumulation grid. Each animation frame, 50 MLT-3 traces (raised-cosine transitions spanning ~35% of one UI, Gaussian timing jitter σ = 4 samples, and amplitude noise σ = 0.012 V) are generated and binned into the grid, then pushed to the GPU via `setZValues()`. The animation starts automatically — lifecycle cleanup is managed via the `(onDelete)` event binding.\n\n### Features and Capabilities\nA custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) with `log1p` density scaling maps the accumulation grid to an oscilloscope thermal palette (black → dark blue → cyan → green → yellow → red). Frequently-traversed regions glow red (the eye centre and voltage rails), while sparse edge crossings fade to blue. A live stats overlay displays FPS, traces/second, and total accumulated traces.\n\n### Integration and Best Practices\nThe implementation follows Angular standalone component best practices. Resource cleanup (stopping the rAF loop, removing the DOM overlay, and calling `sciChartSurface.delete()`) is handled in a single `controls.cleanup()` call from the `(onDelete)` handler, preventing memory leaks when the component is destroyed.", }, }, documentationLinks: [ @@ -50,7 +50,7 @@ const metaData: IExampleMetadata = }, ], path: "eye-diagram-chart", - metaKeywords: "eye diagram, persistence, oscilloscope, NRZ, signal, heatmap, real-time, performance, javascript, webgl", + metaKeywords: "eye diagram, persistence, oscilloscope, MLT-3, signal integrity, heatmap, real-time, performance, javascript, webgl", onWebsite: true, filepath: "FeaturedApps/ScientificCharts/EyeDiagramChart", thumbnailImage: "javascript-eye-diagram-chart.jpg", From fcc983c6f163146052ee87a7069b16c131b7fa93 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Mon, 27 Apr 2026 12:46:16 +0200 Subject: [PATCH 18/18] feat(example): add live waveform panel above eye diagram heatmap Refactor eye diagram from a single heatmap to a two-panel layout using SciChartSubSurface. The top 28% shows the most recent trace as a cyan FastLineRenderableSeries; the bottom 72% retains the accumulating UniformHeatmapRenderableSeries. Uses createSingle + createSubSurface to share a single wasmContext, avoiding multi-surface init complexity. Also adds per-trace amplitude scaling, DC offset, and independent per-edge rise time variation for more realistic oscilloscope fuzz. Update docs, markdown content, and preview screenshot accordingly. --- .../EyeDiagramChart/README.md | 15 +- .../EyeDiagramChart/drawExample.ts | 214 +++++++++++------- .../EyeDiagramChart/exampleInfo.tsx | 6 +- .../javascript-eye-diagram-chart.jpg | Bin 22730 -> 36884 bytes 4 files changed, 147 insertions(+), 88 deletions(-) diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md index 7e70b5306..272affa35 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/README.md @@ -4,16 +4,25 @@ An **eye diagram** is the standard oscilloscope tool for evaluating signal integrity in high-speed serial data links. It is produced by overlaying thousands of short waveform segments — each one a 2-UI (two bit-period) window sliced from a continuous data stream — on a single time axis. When many traces accumulate, the open central regions (the "eye openings") become visible, along with the smearing caused by inter-symbol interference (ISI), timing jitter, and noise. -This demo simulates an **MLT-3 (Multi-Level Transmit 3)** serial data signal — the line code used in 100BASE-TX Ethernet — entirely in the browser and renders the accumulating eye diagram as a **2D density heatmap** using `UniformHeatmapRenderableSeries`. The result is an oscilloscope-style **persistence display**: frequently-traversed regions glow red/yellow, while rarely-visited edges fade to blue. MLT-3 cycles through three voltage levels (+1 V, 0 V, −1 V) via a four-state machine, producing **two stacked eye openings** per display window. +This demo simulates an **MLT-3 (Multi-Level Transmit 3)** serial data signal — the line code used in 100BASE-TX Ethernet — entirely in the browser and renders two panels in a single SciChart surface: + +- **Top panel** — a live `FastLineRenderableSeries` waveform showing the most recently generated trace +- **Bottom panel** — the accumulating `UniformHeatmapRenderableSeries` eye diagram, with an oscilloscope-style persistence display (red/yellow = frequently traversed, blue = rarely visited) + +MLT-3 cycles through three voltage levels (+1 V, 0 V, −1 V) via a four-state machine, producing **two stacked eye openings** per display window. ### Technical Implementation +**Two-panel layout** — Both panels live inside a single `SciChartSurface.createSingle` surface. Sub-charts are created with `SciChartSubSurface.createSubSurface`, positioned using `Rect` with `ESubSurfacePositionCoordinateMode.Relative`: the line chart occupies the top 28% (`Rect(0, 0, 1, 0.28)`) and the heatmap the bottom 72% (`Rect(0, 0.28, 1, 0.72)`). All axes, series, and data objects share the single `wasmContext` returned by `createSingle`. + **Signal generation** — Each trace is built from a random MLT-3 state sequence (pprev, prev, curr, next) to capture both transitions in the 2-UI window: - Voltage levels: +1 V, 0 V, −1 V (four states: [+1, 0, −1, 0]) +- Per-trace **amplitude scaling** (σ = 2.5%) and **DC offset** (σ = 15 mV) for realistic trace-to-trace variation - **Raised-cosine transitions** spanning ~35% of one UI — matching real 100BASE-TX rise time +- Independent per-edge **rise time variation** (σ = 2.8 samples) for realistic fuzz at crossings - Three crossing edges at t = 0, t = 1 UI, t = 2 UI with independent per-edge **timing jitter** (Gaussian, σ = 4.0 samples ≈ 2% of UI) -- **Amplitude noise** (Gaussian, σ = 0.012 V) added to each sample +- **Amplitude noise** (Gaussian, σ = 0.010 V) added to each sample **Heatmap accumulation** — Each trace's 400 samples are binned into a **400 × 200 accumulation grid** (time × voltage). Grid counts are log-scaled (`log1p`) before being passed to `UniformHeatmapDataSeries.setZValues()` each frame. The log scale keeps early traces visible while the hot centre saturates gracefully. @@ -23,7 +32,9 @@ This demo simulates an **MLT-3 (Multi-Level Transmit 3)** serial data signal — ### Features +- **Two-panel layout** using SciChart SubCharts API — live waveform on top, persistence heatmap on bottom - Real-time MLT-3 eye diagram with two stacked eye openings, ISI, jitter, and noise +- Per-trace realism: amplitude variation, DC offset, independent per-edge rise time variation - Oscilloscope thermal persistence display aesthetic (black → blue → cyan → yellow → red) - 50 traces × 60 FPS = ~3,000 traces/second accumulation - Stats overlay: FPS, Traces/s, Total traces diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts index ebd236be6..a1f9f6250 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/drawExample.ts @@ -1,11 +1,18 @@ import { EAutoRange, + ESubSurfacePositionCoordinateMode, + FastLineRenderableSeries, HeatmapColorMap, + I2DSubSurfaceOptions, NumberRange, NumericAxis, + Rect, + SciChartSubSurface, SciChartSurface, + Thickness, UniformHeatmapDataSeries, UniformHeatmapRenderableSeries, + XyDataSeries, } from "scichart"; import { appTheme } from "../../../theme"; @@ -122,46 +129,91 @@ export function binTrace(grid: Float32Array, trace: Float32Array): void { // --------------------------------------------------------------------------- export async function drawExample(rootElement: string | HTMLDivElement) { - const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, { - theme: appTheme.SciChartJsTheme, - }); - - // X Axis — time in UI units - sciChartSurface.xAxes.add( - new NumericAxis(wasmContext, { - axisTitle: "Time", - axisTitleStyle: { fontSize: 11 }, - labelStyle: { fontSize: 10 }, - visibleRange: new NumberRange(0, 2), - autoRange: EAutoRange.Never, - drawMajorGridLines: false, - drawMinorGridLines: false, - }) + // Single surface with two sub-charts avoids multi-surface init complexity + const { sciChartSurface: mainSurface, wasmContext } = await SciChartSurface.createSingle( + rootElement, + { theme: appTheme.SciChartJsTheme } ); - // Y Axis — voltage - sciChartSurface.yAxes.add( - new NumericAxis(wasmContext, { - axisTitle: "Voltage", - axisTitleStyle: { fontSize: 11 }, - labelStyle: { fontSize: 10 }, - visibleRange: new NumberRange(-1.15, 1.15), - autoRange: EAutoRange.Never, - drawMajorGridLines: false, - drawMinorGridLines: false, + // Main surface axes (required by SciChart even when hidden) + mainSurface.xAxes.add(new NumericAxis(wasmContext, { isVisible: false, id: "mainX" })); + mainSurface.yAxes.add(new NumericAxis(wasmContext, { isVisible: false, id: "mainY" })); + + // ── Top sub-chart: live waveform (top 28%) ──────────────────────────────── + + const lineSubOptions: I2DSubSurfaceOptions = { + theme: appTheme.SciChartJsTheme, + position: new Rect(0, 0, 1, 0.28), + coordinateMode: ESubSurfacePositionCoordinateMode.Relative, + padding: Thickness.fromNumber(2), + }; + const lineSurface = SciChartSubSurface.createSubSurface(mainSurface, lineSubOptions); + + lineSurface.xAxes.add(new NumericAxis(wasmContext, { + isVisible: false, + visibleRange: new NumberRange(0, 2), + autoRange: EAutoRange.Never, + })); + + lineSurface.yAxes.add(new NumericAxis(wasmContext, { + axisTitle: "Voltage (V)", + axisTitleStyle: { fontSize: 11 }, + labelStyle: { fontSize: 10 }, + visibleRange: new NumberRange(-1.5, 1.5), + autoRange: EAutoRange.Never, + drawMajorGridLines: true, + drawMinorGridLines: false, + })); + + const TRACE_LEN = 400; + const xTrace = Array.from({ length: TRACE_LEN }, (_, i) => (i / (TRACE_LEN - 1)) * 2); + const yBuffer = new Array(TRACE_LEN).fill(0); + + const lineDs = new XyDataSeries(wasmContext, { xValues: xTrace, yValues: yBuffer }); + lineSurface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: lineDs, + stroke: "#00E5FF", + strokeThickness: 1.5, }) ); - // Accumulation grid: 400 cols × 200 rows, col-major + // ── Bottom sub-chart: heatmap eye diagram (bottom 72%) ──────────────────── + + const heatSubOptions: I2DSubSurfaceOptions = { + theme: appTheme.SciChartJsTheme, + position: new Rect(0, 0.28, 1, 0.72), + coordinateMode: ESubSurfacePositionCoordinateMode.Relative, + padding: Thickness.fromNumber(2), + }; + const heatSurface = SciChartSubSurface.createSubSurface(mainSurface, heatSubOptions); + + heatSurface.xAxes.add(new NumericAxis(wasmContext, { + axisTitle: "Time (UI)", + axisTitleStyle: { fontSize: 11 }, + labelStyle: { fontSize: 10 }, + visibleRange: new NumberRange(0, 2), + autoRange: EAutoRange.Never, + drawMajorGridLines: false, + drawMinorGridLines: false, + })); + + heatSurface.yAxes.add(new NumericAxis(wasmContext, { + axisTitle: "Voltage (V)", + axisTitleStyle: { fontSize: 11 }, + labelStyle: { fontSize: 10 }, + visibleRange: new NumberRange(-1.15, 1.15), + autoRange: EAutoRange.Never, + drawMajorGridLines: false, + drawMinorGridLines: false, + })); + const COLS = 400; const ROWS = 200; const grid = new Float32Array(COLS * ROWS); - - // zValuesLog: 200 rows × 400 cols (row-major), for setZValues const zValuesLog: number[][] = Array.from({ length: ROWS }, () => new Array(COLS).fill(0)); - // Heatmap data series - const dataSeries = new UniformHeatmapDataSeries(wasmContext, { + const heatDs = new UniformHeatmapDataSeries(wasmContext, { xStart: 0, xStep: 2 / (COLS - 1), yStart: -1.15, @@ -169,52 +221,46 @@ export async function drawExample(rootElement: string | HTMLDivElement) { zValues: zValuesLog, }); - // Heatmap renderable series - const renderableSeries = new UniformHeatmapRenderableSeries(wasmContext, { - dataSeries, - useLinearTextureFiltering: true, - colorMap: new HeatmapColorMap({ - minimum: 0, - maximum: 8, - gradientStops: [ - { offset: 0, color: "#000000" }, - { offset: 0.15, color: "#0000FF" }, - { offset: 0.4, color: "#00FFFF" }, - { offset: 0.6, color: "#00FF00" }, - { offset: 0.8, color: "#FFFF00" }, - { offset: 1, color: "#FF0000" }, - ], - }), - }); + heatSurface.renderableSeries.add( + new UniformHeatmapRenderableSeries(wasmContext, { + dataSeries: heatDs, + useLinearTextureFiltering: true, + colorMap: new HeatmapColorMap({ + minimum: 0, + maximum: 8, + gradientStops: [ + { offset: 0, color: "#000000" }, + { offset: 0.15, color: "#0000FF" }, + { offset: 0.4, color: "#00FFFF" }, + { offset: 0.6, color: "#00FF00" }, + { offset: 0.8, color: "#FFFF00" }, + { offset: 1, color: "#FF0000" }, + ], + }), + }) + ); + + // ── Stats overlay ───────────────────────────────────────────────────────── + + const container = typeof rootElement === "string" + ? document.getElementById(rootElement) + : rootElement; - sciChartSurface.renderableSeries.add(renderableSeries); + if (container) container.style.position = "relative"; - // Stats overlay const statsDiv = document.createElement("div"); statsDiv.style.cssText = [ - "position:absolute", - "top:8px", - "right:8px", - "font-family:monospace", - "font-size:12px", - "color:#ffffff", - "background:rgba(0,0,0,0.5)", - "padding:4px 8px", - "border-radius:4px", - "pointer-events:none", - "z-index:100", + "position:absolute", "top:8px", "right:8px", + "font-family:monospace", "font-size:12px", "color:#ffffff", + "background:rgba(0,0,0,0.5)", "padding:4px 8px", + "border-radius:4px", "pointer-events:none", "z-index:100", ].join(";"); statsDiv.textContent = "FPS: -- | Traces/s: -- | Total: 0"; - const container = typeof rootElement === "string" - ? document.getElementById(rootElement) - : rootElement; - if (container) { - container.style.position = "relative"; - container.appendChild(statsDiv); - } + if (container) container.appendChild(statsDiv); + + // ── Animation loop ──────────────────────────────────────────────────────── - // Animation state let rafHandle: number | null = null; let totalTraces = 0; let frameCount = 0; @@ -222,29 +268,36 @@ export async function drawExample(rootElement: string | HTMLDivElement) { const TRACES_PER_FRAME = 50; function animate() { - const frameStart = performance.now(); + let lastTrace: Float32Array | null = null; - // Generate and bin 50 traces this frame for (let t = 0; t < TRACES_PER_FRAME; t++) { const trace = generateTrace(); binTrace(grid, trace); + lastTrace = trace; } totalTraces += TRACES_PER_FRAME; - // Update zValuesLog from grid (log1p scaling) + // Replace line chart data with the last trace from this batch + if (lastTrace) { + for (let i = 0; i < TRACE_LEN; i++) { + yBuffer[i] = lastTrace[i]; + } + lineDs.clear(); + lineDs.appendRange(xTrace, yBuffer); + } + + // Update heatmap for (let col = 0; col < COLS; col++) { for (let row = 0; row < ROWS; row++) { zValuesLog[row][col] = Math.log1p(grid[col * ROWS + row]); } } + heatDs.setZValues(zValuesLog); - dataSeries.setZValues(zValuesLog); - - // Update stats every 30 frames frameCount++; if (frameCount % 30 === 0) { const now = performance.now(); - const elapsed = (now - lastStatsTime) / 1000; // seconds + const elapsed = (now - lastStatsTime) / 1000; const fps = Math.round(30 / elapsed); const tracesPerSec = Math.round((30 * TRACES_PER_FRAME) / elapsed); lastStatsTime = now; @@ -266,21 +319,16 @@ export async function drawExample(rootElement: string | HTMLDivElement) { } } - // Start automatically startAnimation(); function cleanup() { stopAnimation(); if (container) container.removeChild(statsDiv); - sciChartSurface.delete(); + mainSurface.delete(); } return { - sciChartSurface, - controls: { - startAnimation, - stopAnimation, - cleanup, - }, + sciChartSurface: mainSurface, + controls: { startAnimation, stopAnimation, cleanup }, }; } diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx index 8d733dbcc..7bd94710b 100644 --- a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/exampleInfo.tsx @@ -19,7 +19,7 @@ const metaData: IExampleMetadata = metaDescription: "See a real-time Eye Diagram rendered with SciChart.js. Simulates an MLT-3 signal — thousands of waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", markdownContent: - "## Real-time Eye Diagram - JavaScript\n\n### Overview\nThis example demonstrates a **real-time Eye Diagram** (persistence display) using SciChart.js in JavaScript. An eye diagram is the standard tool for evaluating signal integrity in high-speed serial data links — it is produced by overlaying thousands of short waveform segments on a shared time axis, revealing the eye opening, jitter, noise, and inter-symbol interference (ISI) of the signal. This demo simulates an **MLT-3 (Multi-Level Transmit 3)** signal, the line code used in 100BASE-TX Ethernet, which cycles through three voltage levels (+1 V, 0 V, −1 V) to produce **two stacked eye openings** per display window.\n\n### Technical Implementation\nThe chart uses [SciChartSurface.create()](https://www.scichart.com/documentation/js/v5/2d-charts/surface/new-scichart-surface/#scichartsurfacecreate) to initialise the surface. Each animation frame, 50 simulated MLT-3 traces are generated with a four-state voltage machine, raised-cosine edge shaping (spanning ~35% of one UI — matching real 100BASE-TX rise time), per-crossing timing jitter (Gaussian, σ = 4 samples ≈ 2% UI), and Gaussian amplitude noise (σ = 0.012 V). Three crossing edges are placed at t = 0, t = 1 UI, and t = 2 UI. Trace samples are binned into a **400 × 200 accumulation grid** and passed to [UniformHeatmapDataSeries.setZValues()](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) each frame — a single WebGL texture upload regardless of how many traces have accumulated.\n\n### Features and Capabilities\nThe [UniformHeatmapRenderableSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) renders the density grid with a custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) using an oscilloscope thermal palette (black → dark blue → cyan → green → yellow → red) that produces the classic scope persistence glow. A `log1p` scale keeps sparse edges visible while saturating the hot centre. A live stats overlay shows FPS, traces/second, and total accumulated traces.\n\n### Integration and Best Practices\nThe implementation uses a `requestAnimationFrame` loop with a clean start/stop/cleanup API. Resource cleanup (cancelling the rAF loop, removing the DOM overlay, calling `sciChartSurface.delete()`) is encapsulated in a single `cleanup()` function returned from `drawExample`, following the pattern used across all SciChart.js examples.", + "## Real-time Eye Diagram - JavaScript\n\n### Overview\nThis example demonstrates a **real-time Eye Diagram** (persistence display) using SciChart.js in JavaScript. An eye diagram is the standard tool for evaluating signal integrity in high-speed serial data links — it is produced by overlaying thousands of short waveform segments on a shared time axis, revealing the eye opening, jitter, noise, and inter-symbol interference (ISI) of the signal. This demo simulates an **MLT-3 (Multi-Level Transmit 3)** signal, the line code used in 100BASE-TX Ethernet, which cycles through three voltage levels (+1 V, 0 V, −1 V) to produce **two stacked eye openings** per display window.\n\n### Technical Implementation\nThe layout uses [SciChartSurface.createSingle()](https://www.scichart.com/documentation/js/v5/2d-charts/surface/new-scichart-surface/#scichartsurfacecreatesingle) with two sub-charts created via `SciChartSubSurface.createSubSurface`. The top sub-chart (`Rect(0, 0, 1, 0.28)` relative) hosts a [FastLineRenderableSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/line-chart/) showing the live waveform; the bottom sub-chart (`Rect(0, 0.28, 1, 0.72)`) hosts the [UniformHeatmapRenderableSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) eye diagram. Both panels share a single `wasmContext`. Each animation frame, 50 simulated MLT-3 traces are generated with raised-cosine edge shaping (~35% of one UI), per-trace amplitude variation (σ = 2.5%) and DC offset (σ = 15 mV), independent per-edge timing jitter (σ = 4 samples ≈ 2% UI), and Gaussian amplitude noise (σ = 0.010 V). Trace samples are binned into a **400 × 200 accumulation grid** and passed to [UniformHeatmapDataSeries.setZValues()](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) each frame — a single WebGL texture upload regardless of how many traces have accumulated.\n\n### Features and Capabilities\nThe [UniformHeatmapRenderableSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) renders the density grid with a custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) using an oscilloscope thermal palette (black → dark blue → cyan → green → yellow → red) that produces the classic scope persistence glow. A `log1p` scale keeps sparse edges visible while saturating the hot centre. Per-trace realism (amplitude scaling, DC offset, independent rise time variation) produces authentic oscilloscope fuzz at crossings and thick voltage rails. A live stats overlay shows FPS, traces/second, and total accumulated traces.\n\n### Integration and Best Practices\nThe implementation uses a `requestAnimationFrame` loop with a clean start/stop/cleanup API. Using `createSingle` with sub-charts instead of multiple `SciChartSurface.create` calls avoids WASM context conflicts and eliminates fragile DOM measurement for panel sizing. Resource cleanup (cancelling the rAF loop, removing the DOM overlay, calling `mainSurface.delete()`) is encapsulated in a single `cleanup()` function returned from `drawExample`, following the pattern used across all SciChart.js examples.", }, react: { subtitle: @@ -29,7 +29,7 @@ const metaData: IExampleMetadata = metaDescription: "See a real-time Eye Diagram rendered with React and SciChart.js. Simulates an MLT-3 signal — thousands of waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", markdownContent: - "## Real-time Eye Diagram - React\n\n### Overview\nThis React example demonstrates a **real-time Eye Diagram** (persistence display) built with SciChart.js. It simulates an **MLT-3 (Multi-Level Transmit 3)** serial data signal — the line code used in 100BASE-TX Ethernet — and renders the accumulating eye diagram as a 2D density heatmap, producing the oscilloscope persistence glow that signal-integrity engineers use to evaluate jitter, noise, and eye opening. MLT-3's three voltage levels (+1 V, 0 V, −1 V) create **two stacked eye openings** visible once enough traces have accumulated.\n\n### Technical Implementation\nThe chart initialises via `` where `drawExample` creates a [UniformHeatmapDataSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) backed by a 400 × 200 accumulation grid. Each frame, 50 MLT-3 traces (with raised-cosine transitions spanning ~35% of one UI, per-crossing timing jitter σ = 4 samples, and amplitude noise σ = 0.012 V) are generated and binned into the grid. The heatmap is updated via `setZValues()` — one GPU upload per frame. The `onDelete` prop calls `controls.cleanup()` which stops the animation loop and disposes the surface, following [React cleanup patterns for SciChart.js](https://www.scichart.com/documentation/js/v5/get-started/tutorials-react/tutorial-01-setting-up-project-with-scichart-react/).\n\n### Features and Capabilities\nA custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) using an oscilloscope thermal palette (black → dark blue → cyan → green → yellow → red) with `log1p` density scaling produces the classic scope persistence glow. A live overlay shows FPS, traces/second, and total accumulated traces. The animation starts immediately on mount and cleans up automatically on unmount.\n\n### Integration and Best Practices\nThe component is intentionally minimal — no React state is needed because the animation lifecycle is entirely managed by the `controls` object returned from `drawExample`. This pattern keeps the React wrapper decoupled from the SciChart initialisation logic and makes the same `drawExample` function reusable across React, Angular, and vanilla JavaScript targets.", + "## Real-time Eye Diagram - React\n\n### Overview\nThis React example demonstrates a **real-time Eye Diagram** (persistence display) built with SciChart.js. It simulates an **MLT-3 (Multi-Level Transmit 3)** serial data signal — the line code used in 100BASE-TX Ethernet — and renders two panels: a live waveform line chart on top and an accumulating 2D density heatmap on the bottom, producing the oscilloscope persistence glow that signal-integrity engineers use to evaluate jitter, noise, and eye opening. MLT-3's three voltage levels (+1 V, 0 V, −1 V) create **two stacked eye openings** visible once enough traces have accumulated.\n\n### Technical Implementation\nThe chart initialises via `` where `drawExample` creates a single surface with `SciChartSurface.createSingle` and two sub-charts via `SciChartSubSurface.createSubSurface`. The top sub-chart (`Rect(0, 0, 1, 0.28)`, relative coordinates) hosts a [FastLineRenderableSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/line-chart/) for the live waveform; the bottom sub-chart (`Rect(0, 0.28, 1, 0.72)`) hosts a [UniformHeatmapDataSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) backed by a 400 × 200 accumulation grid. Each frame, 50 MLT-3 traces (raised-cosine transitions ~35% UI, per-trace amplitude variation σ = 2.5%, DC offset σ = 15 mV, timing jitter σ = 4 samples, amplitude noise σ = 0.010 V) are generated and binned. The heatmap is updated via `setZValues()` — one GPU upload per frame. The `onDelete` prop calls `controls.cleanup()` following [React cleanup patterns for SciChart.js](https://www.scichart.com/documentation/js/v5/get-started/tutorials-react/tutorial-01-setting-up-project-with-scichart-react/).\n\n### Features and Capabilities\nA custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) using an oscilloscope thermal palette (black → dark blue → cyan → green → yellow → red) with `log1p` density scaling produces the classic scope persistence glow. Per-trace realism (amplitude scaling, DC offset, independent per-edge rise time) adds authentic oscilloscope fuzz. A live overlay shows FPS, traces/second, and total accumulated traces.\n\n### Integration and Best Practices\nThe component is intentionally minimal — no React state is needed because the animation lifecycle is entirely managed by the `controls` object returned from `drawExample`. Using `createSingle` with sub-charts instead of multiple surfaces avoids WASM context conflicts and eliminates fragile DOM measurement for panel sizing. This pattern keeps the React wrapper decoupled from the SciChart initialisation logic and makes the same `drawExample` function reusable across React, Angular, and vanilla JavaScript targets.", }, angular: { subtitle: @@ -39,7 +39,7 @@ const metaData: IExampleMetadata = metaDescription: "See a real-time Eye Diagram rendered with Angular and SciChart.js. Simulates an MLT-3 signal — thousands of waveform traces accumulate into a heatmap density grid — like a real oscilloscope persistence display.", markdownContent: - "## Real-time Eye Diagram - Angular\n\n### Overview\nThis Angular example demonstrates a **real-time Eye Diagram** (persistence display) using SciChart.js and the [scichart-angular](https://www.npmjs.com/package/scichart-angular) package. It simulates an **MLT-3 (Multi-Level Transmit 3)** serial data signal — the line code used in 100BASE-TX Ethernet — and renders thousands of accumulating waveform traces as a heatmap density display, replicating the oscilloscope persistence effect used in signal-integrity analysis. MLT-3's four-state voltage machine (+1 V → 0 V → −1 V → 0 V) produces **two stacked eye openings** per display window.\n\n### Technical Implementation\nThe standalone Angular component uses `` to initialise the SciChart surface. The `drawExample` function creates a [UniformHeatmapDataSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) backed by a 400 × 200 accumulation grid. Each animation frame, 50 MLT-3 traces (raised-cosine transitions spanning ~35% of one UI, Gaussian timing jitter σ = 4 samples, and amplitude noise σ = 0.012 V) are generated and binned into the grid, then pushed to the GPU via `setZValues()`. The animation starts automatically — lifecycle cleanup is managed via the `(onDelete)` event binding.\n\n### Features and Capabilities\nA custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) with `log1p` density scaling maps the accumulation grid to an oscilloscope thermal palette (black → dark blue → cyan → green → yellow → red). Frequently-traversed regions glow red (the eye centre and voltage rails), while sparse edge crossings fade to blue. A live stats overlay displays FPS, traces/second, and total accumulated traces.\n\n### Integration and Best Practices\nThe implementation follows Angular standalone component best practices. Resource cleanup (stopping the rAF loop, removing the DOM overlay, and calling `sciChartSurface.delete()`) is handled in a single `controls.cleanup()` call from the `(onDelete)` handler, preventing memory leaks when the component is destroyed.", + "## Real-time Eye Diagram - Angular\n\n### Overview\nThis Angular example demonstrates a **real-time Eye Diagram** (persistence display) using SciChart.js and the [scichart-angular](https://www.npmjs.com/package/scichart-angular) package. It simulates an **MLT-3 (Multi-Level Transmit 3)** serial data signal — the line code used in 100BASE-TX Ethernet — and renders two panels: a live waveform line chart on top and an accumulating heatmap density display on the bottom, replicating the oscilloscope persistence effect used in signal-integrity analysis. MLT-3's four-state voltage machine (+1 V → 0 V → −1 V → 0 V) produces **two stacked eye openings** per display window.\n\n### Technical Implementation\nThe standalone Angular component uses `` to initialise the SciChart surface. The `drawExample` function creates a single surface with `SciChartSurface.createSingle` and two sub-charts via `SciChartSubSurface.createSubSurface`. The top sub-chart (`Rect(0, 0, 1, 0.28)`, relative coordinates) hosts a live [FastLineRenderableSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/line-chart/) waveform; the bottom sub-chart (`Rect(0, 0.28, 1, 0.72)`) hosts a [UniformHeatmapDataSeries](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) backed by a 400 × 200 accumulation grid. Each animation frame, 50 MLT-3 traces (raised-cosine transitions ~35% UI, per-trace amplitude variation σ = 2.5%, DC offset σ = 15 mV, timing jitter σ = 4 samples, amplitude noise σ = 0.010 V) are generated and binned, then pushed to the GPU via `setZValues()`. The animation starts automatically — lifecycle cleanup is managed via the `(onDelete)` event binding.\n\n### Features and Capabilities\nA custom [HeatmapColorMap](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/uniform-heatmap-renderable-series/uniform-heatmap-chart-type/) with `log1p` density scaling maps the accumulation grid to an oscilloscope thermal palette (black → dark blue → cyan → green → yellow → red). Frequently-traversed regions glow red (the eye centre and voltage rails), while sparse edge crossings fade to blue. Per-trace realism (amplitude scaling, DC offset, independent rise time variation) adds authentic oscilloscope fuzz at crossings. A live stats overlay displays FPS, traces/second, and total accumulated traces.\n\n### Integration and Best Practices\nThe implementation follows Angular standalone component best practices. Using `createSingle` with sub-charts instead of multiple surfaces avoids WASM context conflicts and eliminates fragile DOM measurement for panel sizing. Resource cleanup (stopping the rAF loop, removing the DOM overlay, and calling `mainSurface.delete()`) is handled in a single `controls.cleanup()` call from the `(onDelete)` handler, preventing memory leaks when the component is destroyed.", }, }, documentationLinks: [ diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/javascript-eye-diagram-chart.jpg b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/EyeDiagramChart/javascript-eye-diagram-chart.jpg index e136dde2b94c7701602b2b7c7fd6fd47e0d53b2a..8968d64e95c354979a771f197b65c593bafe7c60 100644 GIT binary patch literal 36884 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 raUZQJabIYzcSr}eMOIn`ir^dj8-0a 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%0Ad#0{n2nXz~=%BVW` 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 literal 22730 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 a0KDAv3TPFe?zbd#OHX?E2n3V=9R4S(&@EE{