diff options
Diffstat (limited to 'web-timeplot/src/plot/timeplot-view.js')
| -rw-r--r-- | web-timeplot/src/plot/timeplot-view.js | 442 |
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 }); - } -} |
