summaryrefslogtreecommitdiff
path: root/web-timeplot/src/plot/timeplot-view.js
diff options
context:
space:
mode:
Diffstat (limited to 'web-timeplot/src/plot/timeplot-view.js')
-rw-r--r--web-timeplot/src/plot/timeplot-view.js442
1 files changed, 0 insertions, 442 deletions
diff --git a/web-timeplot/src/plot/timeplot-view.js b/web-timeplot/src/plot/timeplot-view.js
deleted file mode 100644
index ce90a1f..0000000
--- a/web-timeplot/src/plot/timeplot-view.js
+++ /dev/null
@@ -1,442 +0,0 @@
-import { Application, Container, Graphics, Text } from 'pixi.js';
-import { formatDuration, formatValue, formatWallClock } from '../utils-format.js';
-
-function clamp(value, min, max) {
- return Math.min(max, Math.max(min, value));
-}
-
-function roundRect(graphics, x, y, width, height, radius, fill, stroke) {
- graphics.roundRect(x, y, width, height, radius);
- graphics.fill(fill);
- graphics.stroke(stroke);
-}
-
-export class TimeplotView {
- constructor({ host, panelId = 'primary', title = 'Vertical plot', subtitle = null, showReadouts = true, lineColor = 0x9fd1ff, pointColor = 0xe7f2ff }) {
- this.host = host;
- this.panelId = panelId;
- this.panelTitle = title;
- this.panelSubtitle = subtitle;
- this.showReadouts = showReadouts;
- this.lineColor = lineColor;
- this.pointColor = pointColor;
- this.app = new Application();
- this.container = new Container();
- this.background = new Graphics();
- this.grid = new Graphics();
- this.axes = new Graphics();
- this.line = new Graphics();
- this.points = new Graphics();
- this.crosshair = new Graphics();
- this.overlay = new Container();
- this.readoutBackground = new Graphics();
- this.axisLabelLayer = new Container();
- this.titleText = new Text({
- text: 'Plot viewport',
- style: {
- fill: 0xeef4ff,
- fontFamily: 'Inter, sans-serif',
- fontSize: 16,
- },
- });
- this.subtitleText = new Text({
- text: 'Synthetic data stream',
- style: {
- fill: 0x8ca3c7,
- fontFamily: 'Inter, sans-serif',
- fontSize: 12,
- },
- });
- this.realTimeText = new Text({
- text: '',
- style: {
- fill: 0xe8eef7,
- fontFamily: 'IBM Plex Mono, monospace',
- fontSize: 11,
- },
- });
- this.plotTimeText = new Text({
- text: '',
- style: {
- fill: 0xe8eef7,
- fontFamily: 'IBM Plex Mono, monospace',
- fontSize: 11,
- },
- });
- this.axisTitleText = new Text({
- text: '',
- style: {
- fill: 0x90a0b7,
- fontFamily: 'Inter, sans-serif',
- fontSize: 10,
- fontWeight: '600',
- letterSpacing: 1.5,
- },
- });
- this.screenPoints = [];
- this.bounds = { width: 100, height: 100 };
- this.hoverRadiusPx = 20;
- this.pointer = null;
- this.lastPointerEventAt = 0;
- this.axisLabels = [];
- }
-
- async init() {
- const rendererPreference = navigator.gpu ? 'webgpu' : 'webgl';
- await this.app.init({
- preference: rendererPreference,
- resizeTo: this.host,
- antialias: true,
- backgroundAlpha: 0,
- resolution: Math.min(window.devicePixelRatio || 1, 2),
- });
-
- this.app.stage.addChild(this.container);
- this.container.addChild(this.background);
- this.container.addChild(this.grid);
- this.container.addChild(this.axes);
- this.container.addChild(this.line);
- this.container.addChild(this.points);
- this.container.addChild(this.crosshair);
- this.container.addChild(this.overlay);
- this.overlay.addChild(this.readoutBackground);
- this.overlay.addChild(this.axisLabelLayer);
- this.overlay.addChild(this.titleText);
- this.overlay.addChild(this.subtitleText);
- this.overlay.addChild(this.realTimeText);
- this.overlay.addChild(this.plotTimeText);
- this.overlay.addChild(this.axisTitleText);
- this.host.appendChild(this.app.canvas);
- this.attachPointerListeners();
-
- return rendererPreference;
- }
-
- attachPointerListeners() {
- this.host.addEventListener('pointerleave', () => {
- this.pointer = null;
- this.lastPointerEventAt = performance.now();
- });
-
- this.host.addEventListener('pointermove', (event) => {
- const rect = this.host.getBoundingClientRect();
- this.pointer = {
- x: event.clientX - rect.left,
- y: event.clientY - rect.top,
- };
- this.lastPointerEventAt = performance.now();
- });
- }
-
- resize() {
- this.bounds = {
- width: this.host.clientWidth,
- height: this.host.clientHeight,
- };
- }
-
- render(state, points) {
- this.resize();
- this.renderFrame(state, points);
- this.clearHover();
- }
-
- clearHover() {
- this.crosshair.clear();
- }
-
- getHoverCandidate() {
- if (!this.pointer || this.screenPoints.length === 0) {
- return null;
- }
-
- let nearestPoint = null;
- let nearestDistance = Infinity;
-
- for (const point of this.screenPoints) {
- const dx = point.x - this.pointer.x;
- const dy = point.y - this.pointer.y;
- const distance = Math.sqrt(dx * dx + dy * dy);
- if (distance < nearestDistance) {
- nearestPoint = point;
- nearestDistance = distance;
- }
- }
-
- if (!nearestPoint || nearestDistance > this.hoverRadiusPx) {
- return null;
- }
-
- return {
- panelId: this.panelId,
- point: nearestPoint,
- x: clamp(nearestPoint.x, 0, this.bounds.width),
- y: clamp(nearestPoint.y, 0, this.bounds.height),
- pointerX: this.pointer.x,
- pointerY: this.pointer.y,
- distance: nearestDistance,
- lastPointerEventAt: this.lastPointerEventAt,
- };
- }
-
- hasPointer() {
- return this.pointer !== null;
- }
-
- findNearestScreenPointByTime(timeMs) {
- if (this.screenPoints.length === 0) {
- return null;
- }
-
- let nearestPoint = null;
- let nearestDelta = Infinity;
-
- for (const point of this.screenPoints) {
- const delta = Math.abs(point.timeMs - timeMs);
- if (delta < nearestDelta) {
- nearestPoint = point;
- nearestDelta = delta;
- }
- }
-
- return nearestPoint;
- }
-
- renderLinkedHover(hoverPoint) {
- this.crosshair.clear();
-
- if (!hoverPoint) {
- return;
- }
-
- const x = clamp(hoverPoint.x, 0, this.bounds.width);
- const y = clamp(hoverPoint.y, 0, this.bounds.height);
-
- this.crosshair.moveTo(x, 0);
- this.crosshair.lineTo(x, this.bounds.height);
- this.crosshair.moveTo(0, y);
- this.crosshair.lineTo(this.bounds.width, y);
- this.crosshair.stroke({ color: 0x8cb8ff, width: 1, alpha: 0.24 });
- this.crosshair.rect(x - 4, y - 4, 8, 8);
- this.crosshair.stroke({ color: 0xffffff, width: 1.5, alpha: 0.95 });
- }
-
- ensureAxisLabelCount(count) {
- while (this.axisLabels.length < count) {
- const label = new Text({
- text: '',
- style: {
- fill: 0x90a0b7,
- fontFamily: 'IBM Plex Mono, monospace',
- fontSize: 10,
- },
- });
- this.axisLabels.push(label);
- this.axisLabelLayer.addChild(label);
- }
-
- while (this.axisLabels.length > count) {
- const label = this.axisLabels.pop();
- this.axisLabelLayer.removeChild(label);
- label.destroy();
- }
- }
-
- renderAxes({ padding, plotWidth, plotHeight, minTime, maxTime, minValue, maxValue, width }) {
- const axisColor = 0x3e4c5f;
- const tickColor = 0x4f627a;
- const timeTickCount = 5;
- const valueTickCount = 5;
- const labels = [];
-
- this.axes.clear();
- this.axes.moveTo(padding.left, padding.top);
- this.axes.lineTo(padding.left, padding.top + plotHeight);
- this.axes.lineTo(padding.left + plotWidth, padding.top + plotHeight);
- this.axes.stroke({ color: axisColor, width: 1, alpha: 1 });
-
- for (let index = 0; index < timeTickCount; index += 1) {
- const ratio = timeTickCount === 1 ? 0 : index / (timeTickCount - 1);
- const y = padding.top + ratio * plotHeight;
- const timeMs = minTime + ratio * (maxTime - minTime);
-
- this.axes.moveTo(padding.left - 8, y);
- this.axes.lineTo(padding.left, y);
- this.axes.stroke({ color: tickColor, width: 1, alpha: 1 });
-
- labels.push({
- text: formatDuration(timeMs),
- x: 14,
- y: y - 7,
- anchorX: 0,
- });
- }
-
- for (let index = 0; index < valueTickCount; index += 1) {
- const ratio = valueTickCount === 1 ? 0 : index / (valueTickCount - 1);
- const x = padding.left + ratio * plotWidth;
- const value = minValue + ratio * (maxValue - minValue);
-
- this.axes.moveTo(x, padding.top + plotHeight);
- this.axes.lineTo(x, padding.top + plotHeight + 8);
- this.axes.stroke({ color: tickColor, width: 1, alpha: 1 });
-
- labels.push({
- text: formatValue(value),
- x,
- y: padding.top + plotHeight + 10,
- anchorX: 0.5,
- });
- }
-
- this.ensureAxisLabelCount(labels.length);
- labels.forEach((config, index) => {
- const label = this.axisLabels[index];
- label.text = config.text;
- label.x = config.x;
- label.y = config.y;
- label.anchor.set(config.anchorX, 0);
- });
-
- this.axisTitleText.text = 'TIME';
- this.axisTitleText.x = 18;
- this.axisTitleText.y = padding.top - 18;
- this.axisTitleText.rotation = 0;
-
- this.axes.moveTo(padding.left + plotWidth, padding.top + plotHeight);
- this.axes.lineTo(width - 14, padding.top + plotHeight);
- this.axes.stroke({ color: 0x202a35, width: 1, alpha: 1 });
- }
-
- renderReadouts(state, width) {
- if (!this.showReadouts) {
- this.readoutBackground.clear();
- this.realTimeText.text = '';
- this.plotTimeText.text = '';
- return;
- }
-
- const boxWidth = 168;
- const boxHeight = 22;
- const gap = 6;
- const left = width - boxWidth - 18;
- const top = 14;
-
- this.readoutBackground.clear();
- this.readoutBackground.rect(left, top, boxWidth, boxHeight);
- this.readoutBackground.fill({ color: 0x10161d, alpha: 1 });
- this.readoutBackground.stroke({ color: 0x2f3c4d, width: 1, alpha: 1 });
- this.readoutBackground.rect(left, top + boxHeight + gap, boxWidth, boxHeight);
- this.readoutBackground.fill({ color: 0x10161d, alpha: 1 });
- this.readoutBackground.stroke({ color: 0x2f3c4d, width: 1, alpha: 1 });
-
- this.realTimeText.text = `REAL ${formatWallClock(state.time.realNowMs)}`;
- this.realTimeText.x = left + 10;
- this.realTimeText.y = top + 5;
-
- this.plotTimeText.text = `PLOT ${formatDuration(state.time.plotTimeMs)}`;
- this.plotTimeText.x = left + 10;
- this.plotTimeText.y = top + boxHeight + gap + 5;
- }
-
- renderFrame(state, points) {
- const width = this.bounds.width;
- const height = this.bounds.height;
- const padding = { top: 72, right: 28, bottom: 46, left: 88 };
- const plotWidth = Math.max(10, width - padding.left - padding.right);
- const plotHeight = Math.max(10, height - padding.top - padding.bottom);
- const minTime = state.time.plotTimeMs - state.plot.windowDurationMs;
- const maxTime = Math.max(state.time.plotTimeMs, minTime + 1);
- const { min: minValue, max: maxValue } = state.plot.valueRange;
- const valueSpan = Math.max(0.001, maxValue - minValue);
-
- this.background.clear();
- roundRect(
- this.background,
- 0,
- 0,
- width,
- height,
- 6,
- { color: 0x05070b, alpha: 1 },
- { color: 0x2c3b4d, width: 1 },
- );
-
- this.grid.clear();
- if (state.plot.showGrid) {
- const gridColor = 0x21344a;
- for (let x = 0; x <= 6; x += 1) {
- const px = padding.left + (plotWidth * x) / 6;
- this.grid.moveTo(px, padding.top);
- this.grid.lineTo(px, padding.top + plotHeight);
- this.grid.stroke({ color: gridColor, width: 1, alpha: 0.85 });
- }
-
- for (let y = 0; y <= 8; y += 1) {
- const py = padding.top + (plotHeight * y) / 8;
- this.grid.moveTo(padding.left, py);
- this.grid.lineTo(padding.left + plotWidth, py);
- this.grid.stroke({ color: gridColor, width: 1, alpha: 0.85 });
- }
- }
-
- this.renderAxes({
- padding,
- plotWidth,
- plotHeight,
- minTime,
- maxTime,
- minValue,
- maxValue,
- width,
- });
-
- this.line.clear();
- this.points.clear();
- this.screenPoints = [];
-
- if (points.length > 0) {
- points.forEach((point, index) => {
- const x = padding.left + ((point.value - minValue) / valueSpan) * plotWidth;
- const y = padding.top + ((point.timeMs - minTime) / (maxTime - minTime)) * plotHeight;
-
- this.screenPoints.push({ ...point, x, y });
-
- if (index === 0) {
- this.line.moveTo(x, y);
- } else {
- this.line.lineTo(x, y);
- }
- });
-
- this.line.stroke({
- color: this.lineColor,
- width: 2,
- alpha: 0.96,
- cap: 'square',
- join: 'miter',
- });
-
- if (state.plot.showPoints) {
- for (const point of this.screenPoints) {
- this.points.rect(point.x - 2, point.y - 2, 4, 4);
- this.points.fill({ color: this.pointColor, alpha: 0.92 });
- }
- }
- }
-
- this.titleText.text = this.panelTitle;
- this.titleText.x = 20;
- this.titleText.y = 14;
-
- this.subtitleText.text = this.panelSubtitle ?? `value → ${state.source.preset} · ${state.source.sampleRateHz} hz · time ↓`;
- this.subtitleText.x = 20;
- this.subtitleText.y = 36;
-
- this.renderReadouts(state, width);
- }
-
- destroy() {
- this.app.destroy(true, { children: true });
- }
-}