3import { Margins, Range, WorkerMessage } from "./WorkerTypes";
5// Helper function to find a nice integer tick interval
6function getNiceTickInterval(range: number, maxTicks: number): number {
7 const minInterval = Math.ceil(range / maxTicks);
8 if (minInterval <= 1) return 1;
10 const magnitude = Math.pow(10, Math.floor(Math.log10(minInterval)));
11 const niceIntervals = [1, 2, 5, 10];
13 for (const interval of niceIntervals) {
14 const tickInterval = interval * magnitude;
15 if (tickInterval >= minInterval) {
16 return Math.ceil(tickInterval);
17 }
18 }
19 return Math.ceil(niceIntervals[niceIntervals.length - 1] * magnitude * 10);
20}
22// Helper function to get tick positions
23function getTickPositions(
24 range: Range,
25 width: number,
26): { value: number; x: number }[] {
27 const pixelsPerTick = 20; // Minimum pixels between ticks
28 const maxTicks = Math.floor(width / pixelsPerTick);
29 const tickInterval = getNiceTickInterval(range.max - range.min, maxTicks);
31 const firstTick = Math.ceil(range.min);
32 const lastTick = Math.floor(range.max);
34 const ticks: { value: number; x: number }[] = [];
35 for (let value = firstTick; value <= lastTick; value += tickInterval) {
36 const x = (value - range.min) / (range.max - range.min);
37 if (Number.isInteger(value)) {
38 ticks.push({ value, x });
39 }
40 }
42 return ticks;
43}
45let canvas: OffscreenCanvas | null = null;
46let ctx: OffscreenCanvasRenderingContext2D | null = null;
48function renderTimeseries(
49 timeseries: number[],
50 width: number,
51 height: number,
52 margins: Margins,
53 xRange: Range,
54 yRange: Range,
55) {
56 if (!ctx || !canvas) return;
58 const context = ctx; // Create a stable reference to satisfy TypeScript
60 // Clear canvas
61 context.clearRect(0, 0, width, height);
63 // Draw axes
64 context.strokeStyle = "#666666";
65 context.lineWidth = 1;
66 context.beginPath();
68 // Y axis
69 context.moveTo(margins.left, margins.top);
70 context.lineTo(margins.left, height - margins.bottom);
72 // X axis
73 context.moveTo(margins.left, height - margins.bottom);
74 context.lineTo(width - margins.right, height - margins.bottom);
76 context.stroke();
78 // Calculate the drawing area dimensions
79 const drawingWidth = width - margins.left - margins.right;
80 const drawingHeight = height - margins.top - margins.bottom;
82 // Set up clipping region for timeseries
83 context.save();
84 context.beginPath();
85 context.rect(margins.left, margins.top, drawingWidth, drawingHeight);
86 context.clip();
88 // Set up drawing style for timeseries
89 context.strokeStyle = "#2196f3";
90 context.lineWidth = 2;
91 context.beginPath();
93 // Calculate scaling factors
94 const xScale = drawingWidth / (xRange.max - xRange.min);
95 const yScale = drawingHeight / (yRange.max - yRange.min);
97 // Draw the path
98 let isFirst = true;
99 for (let i = Math.floor(xRange.min); i <= Math.ceil(xRange.max); i++) {
100 if (i < 0 || i >= timeseries.length) continue;
101 const value = timeseries[i];
102 const x = margins.left + (i - xRange.min) * xScale;
103 const y = margins.top + drawingHeight - (value - yRange.min) * yScale;
104 if (isFirst) {
105 context.moveTo(x, y);
106 isFirst = false;
107 } else {
108 context.lineTo(x, y);
109 }
110 }
112 context.stroke();
114 // Remove clipping before drawing ticks
115 context.restore();
117 // Draw Y-axis ticks and labels
118 const yTicks = getTickPositions(yRange, drawingHeight);
120 context.textAlign = "right";
121 context.textBaseline = "middle";
122 context.fillStyle = "#666666";
123 context.font = "12px Arial";
125 yTicks.forEach((tick) => {
126 const y = margins.top + drawingHeight - tick.x * drawingHeight;
128 // Draw tick mark
129 context.beginPath();
130 context.moveTo(margins.left - 6, y);
131 context.lineTo(margins.left, y);
132 context.stroke();
134 // Draw label
135 context.fillText(tick.value.toString(), margins.left - 8, y);
136 });
138 // Draw X-axis ticks and labels
139 const ticks = getTickPositions(xRange, drawingWidth);
141 context.textAlign = "center";
142 context.textBaseline = "top";
143 context.fillStyle = "#666666";
144 context.font = "12px Arial";
146 ticks.forEach((tick) => {
147 const x = margins.left + tick.x * drawingWidth;
149 // Draw tick mark
150 context.beginPath();
151 context.moveTo(x, height - margins.bottom);
152 context.lineTo(x, height - margins.bottom + 6);
153 context.stroke();
155 // Draw label
156 context.fillText(tick.value.toString(), x, height - margins.bottom + 8);
157 });
158}
160self.onmessage = (evt: MessageEvent) => {
161 const message = evt.data as WorkerMessage;
163 if (message.type === "initialize") {
164 canvas = message.canvas;
165 ctx = canvas.getContext("2d");
166 if (!ctx) {
167 self.postMessage({
168 type: "error",
169 error: "Failed to get canvas context",
170 });
171 return;
172 }
173 self.postMessage({ type: "initialized" });
174 return;
175 }
177 if (message.type === "render") {
178 const { timeseries, width, height, margins, xRange, yRange } = message;
179 renderTimeseries(timeseries, width, height, margins, xRange, yRange);
180 self.postMessage({ type: "render_complete" });
181 return;
182 }
183};
185export {}; // Needed for TypeScript modules