@maxhealth.tech/prefab 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.d.ts CHANGED
@@ -9,7 +9,7 @@ import type { ComponentJSON } from './core/component.js';
9
9
  import type { Action, ActionJSON } from './actions/types.js';
10
10
  import type { PipeFn } from './rx/pipes.js';
11
11
  /** Package version — injected by build script, updated at release time. */
12
- export declare const VERSION = "0.2.4";
12
+ export declare const VERSION = "0.2.6";
13
13
  export interface Theme {
14
14
  light?: Record<string, string>;
15
15
  dark?: Record<string, string>;
package/dist/app.js CHANGED
@@ -7,7 +7,7 @@
7
7
  import {} from './core/component.js';
8
8
  import { drainAutoState } from './rx/state-collector.js';
9
9
  /** Package version — injected by build script, updated at release time. */
10
- export const VERSION = '0.2.4';
10
+ export const VERSION = '0.2.6';
11
11
  export class PrefabApp {
12
12
  title;
13
13
  view;
package/dist/prefab.css CHANGED
@@ -736,6 +736,27 @@
736
736
  .pf-chart { position: relative; }
737
737
  .pf-chart svg { display: block; width: 100%; height: auto; }
738
738
 
739
+ .pf-chart-tooltip {
740
+ position: absolute;
741
+ pointer-events: none;
742
+ z-index: 50;
743
+ background: var(--popover, #fff);
744
+ color: var(--popover-foreground, #09090b);
745
+ border: 1px solid var(--border, #e5e7eb);
746
+ border-radius: var(--radius, 0.375rem);
747
+ padding: 0.375rem 0.625rem;
748
+ font-size: 0.75rem;
749
+ line-height: 1.25rem;
750
+ box-shadow: 0 2px 8px rgb(0 0 0 / 0.12);
751
+ white-space: nowrap;
752
+ opacity: 0;
753
+ transition: opacity 0.15s ease;
754
+ }
755
+ .pf-chart-tooltip.pf-visible { opacity: 1; }
756
+ .pf-chart-tooltip-label { font-weight: 500; margin-bottom: 0.125rem; }
757
+ .pf-chart-tooltip-row { display: flex; align-items: center; gap: 0.375rem; }
758
+ .pf-chart-tooltip-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
759
+
739
760
  .pf-chart-legend { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 0.75rem; font-size: 0.75rem; }
740
761
  .pf-chart-legend-item { display: flex; align-items: center; gap: 0.375rem; }
741
762
 
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Chart tooltip — hit-zone overlays, crosshair, data dots, and tooltip popup.
3
+ *
4
+ * Extracted from charts.ts to keep each file under 700 LOC.
5
+ */
6
+ import type { ChartLayout, SeriesEntry } from './charts.js';
7
+ export interface TooltipEntry {
8
+ label: string;
9
+ value: string | number;
10
+ color: string;
11
+ }
12
+ export interface TooltipCtx {
13
+ wrapper: HTMLElement;
14
+ tooltip: HTMLElement;
15
+ svgEl: SVGSVGElement;
16
+ }
17
+ /** Create and attach a tooltip div to the chart wrapper. */
18
+ export declare function createTooltip(wrapper: HTMLElement, svg: SVGSVGElement): TooltipCtx;
19
+ /** Show the tooltip at a position relative to the SVG. */
20
+ export declare function showTooltipAt(ctx: TooltipCtx, svgX: number, _svgY: number, categoryLabel: string | undefined, entries: TooltipEntry[]): void;
21
+ /** Attach hover hit-zones for bar chart columns. */
22
+ export declare function addBarTooltipZones(ctx: TooltipCtx, svg: SVGSVGElement, data: Record<string, unknown>[], series: SeriesEntry[], layout: ChartLayout, xAxisKey: string | undefined, formatValue: (raw: unknown, s: SeriesEntry, si: number) => string): void;
23
+ /** Attach hover hit-zones for line/area chart data points. */
24
+ export declare function addLineTooltipZones(ctx: TooltipCtx, svg: SVGSVGElement, data: Record<string, unknown>[], allSeries: SeriesEntry[], layout: ChartLayout, xAxisKey: string | undefined, formatValue: (raw: unknown, s: SeriesEntry, si: number) => string, crosshair?: SVGLineElement, dotGroups?: SVGCircleElement[][]): void;
25
+ //# sourceMappingURL=chart-tooltip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chart-tooltip.d.ts","sourceRoot":"","sources":["../../../src/renderer/components/chart-tooltip.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAI3D,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,WAAW,CAAA;IACpB,OAAO,EAAE,WAAW,CAAA;IACpB,KAAK,EAAE,aAAa,CAAA;CACrB;AAED,4DAA4D;AAC5D,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,aAAa,GAAG,UAAU,CAUlF;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAC3B,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,OAAO,EAAE,YAAY,EAAE,GACtB,IAAI,CA8BN;AAMD,oDAAoD;AACpD,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,UAAU,EACf,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC/B,MAAM,EAAE,WAAW,EAAE,EACrB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,WAAW,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,KAAK,MAAM,GAChE,IAAI,CA6BN;AAED,8DAA8D;AAC9D,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,UAAU,EACf,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC/B,SAAS,EAAE,WAAW,EAAE,EACxB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,WAAW,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,KAAK,MAAM,EACjE,SAAS,CAAC,EAAE,cAAc,EAC1B,SAAS,CAAC,EAAE,gBAAgB,EAAE,EAAE,GAC/B,IAAI,CAuDN"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Chart tooltip — hit-zone overlays, crosshair, data dots, and tooltip popup.
3
+ *
4
+ * Extracted from charts.ts to keep each file under 700 LOC.
5
+ */
6
+ const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'];
7
+ /** Create and attach a tooltip div to the chart wrapper. */
8
+ export function createTooltip(wrapper, svg) {
9
+ const tooltip = document.createElement('div');
10
+ tooltip.className = 'pf-chart-tooltip';
11
+ wrapper.appendChild(tooltip);
12
+ wrapper.addEventListener('mouseleave', () => {
13
+ tooltip.classList.remove('pf-visible');
14
+ });
15
+ return { wrapper, tooltip, svgEl: svg };
16
+ }
17
+ /** Show the tooltip at a position relative to the SVG. */
18
+ export function showTooltipAt(ctx, svgX, _svgY, categoryLabel, entries) {
19
+ const { tooltip, svgEl, wrapper } = ctx;
20
+ if (entries.length === 0)
21
+ return;
22
+ let html = '';
23
+ if (categoryLabel) {
24
+ html += `<div class="pf-chart-tooltip-label">${esc(categoryLabel)}</div>`;
25
+ }
26
+ for (const e of entries) {
27
+ html += `<div class="pf-chart-tooltip-row">`;
28
+ html += `<span class="pf-chart-tooltip-dot" style="background:${esc(e.color)}"></span>`;
29
+ html += `<span>${esc(e.label)}:</span> <strong>${esc(String(e.value))}</strong>`;
30
+ html += `</div>`;
31
+ }
32
+ tooltip.innerHTML = html;
33
+ const svgRect = svgEl.getBoundingClientRect();
34
+ const wrapRect = wrapper.getBoundingClientRect();
35
+ const viewBox = svgEl.viewBox.baseVal;
36
+ const scaleX = svgRect.width / (viewBox.width || 1);
37
+ const pixelX = svgRect.left - wrapRect.left + svgX * scaleX;
38
+ const tooltipW = tooltip.offsetWidth || 100;
39
+ let left = pixelX + 12;
40
+ if (left + tooltipW > wrapRect.width) {
41
+ left = pixelX - tooltipW - 12;
42
+ }
43
+ tooltip.style.left = `${Math.max(0, left)}px`;
44
+ tooltip.style.top = '4px';
45
+ tooltip.classList.add('pf-visible');
46
+ }
47
+ function esc(s) {
48
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
49
+ }
50
+ /** Attach hover hit-zones for bar chart columns. */
51
+ export function addBarTooltipZones(ctx, svg, data, series, layout, xAxisKey, formatValue) {
52
+ const groupW = layout.plotWidth / data.length;
53
+ for (let di = 0; di < data.length; di++) {
54
+ const x = layout.plotLeft + di * groupW;
55
+ const zone = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
56
+ zone.setAttribute('x', String(x));
57
+ zone.setAttribute('y', String(layout.plotTop));
58
+ zone.setAttribute('width', String(groupW));
59
+ zone.setAttribute('height', String(layout.plotHeight));
60
+ zone.setAttribute('fill', 'transparent');
61
+ zone.style.cursor = 'default';
62
+ const rawLabel = xAxisKey ? data[di][xAxisKey] : undefined;
63
+ const label = rawLabel != null ? String(rawLabel) : undefined;
64
+ const entries = series.map((s, si) => ({
65
+ label: s.label ?? s.dataKey,
66
+ value: formatValue(data[di][s.dataKey], s, si),
67
+ color: s.color ?? COLORS[si % COLORS.length],
68
+ }));
69
+ const centerX = x + groupW / 2;
70
+ const show = () => showTooltipAt(ctx, centerX, layout.plotTop, label, entries);
71
+ const hide = () => ctx.tooltip.classList.remove('pf-visible');
72
+ zone.addEventListener('mouseenter', show);
73
+ zone.addEventListener('mouseleave', hide);
74
+ zone.addEventListener('touchstart', show, { passive: true });
75
+ zone.addEventListener('touchend', hide, { passive: true });
76
+ svg.appendChild(zone);
77
+ }
78
+ }
79
+ /** Attach hover hit-zones for line/area chart data points. */
80
+ export function addLineTooltipZones(ctx, svg, data, allSeries, layout, xAxisKey, formatValue, crosshair, dotGroups) {
81
+ const step = data.length === 1 ? layout.plotWidth : layout.plotWidth / (data.length - 1);
82
+ for (let di = 0; di < data.length; di++) {
83
+ const centerX = data.length === 1
84
+ ? (layout.plotLeft + layout.plotRight) / 2
85
+ : layout.plotLeft + (di / (data.length - 1)) * layout.plotWidth;
86
+ const zoneX = data.length === 1 ? layout.plotLeft : centerX - step / 2;
87
+ const zoneW = data.length === 1 ? layout.plotWidth : step;
88
+ const zone = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
89
+ zone.setAttribute('x', String(Math.max(layout.plotLeft, zoneX)));
90
+ zone.setAttribute('y', String(layout.plotTop));
91
+ zone.setAttribute('width', String(Math.min(zoneW, layout.plotRight - Math.max(layout.plotLeft, zoneX))));
92
+ zone.setAttribute('height', String(layout.plotHeight));
93
+ zone.setAttribute('fill', 'transparent');
94
+ zone.style.cursor = 'default';
95
+ const rawLabel = xAxisKey ? data[di][xAxisKey] : undefined;
96
+ const label = rawLabel != null ? String(rawLabel) : undefined;
97
+ const entries = allSeries.map((s, si) => ({
98
+ label: s.label ?? s.dataKey,
99
+ value: formatValue(data[di][s.dataKey], s, si),
100
+ color: s.color ?? COLORS[si % COLORS.length],
101
+ }));
102
+ const showHover = () => {
103
+ showTooltipAt(ctx, centerX, layout.plotTop, label, entries);
104
+ if (crosshair) {
105
+ crosshair.setAttribute('x1', String(centerX));
106
+ crosshair.setAttribute('x2', String(centerX));
107
+ crosshair.setAttribute('opacity', '1');
108
+ }
109
+ if (dotGroups) {
110
+ for (const dots of dotGroups) {
111
+ for (const dot of dots)
112
+ dot.setAttribute('data-visible', 'false');
113
+ dots[di]?.setAttribute('data-visible', 'true');
114
+ }
115
+ }
116
+ };
117
+ const hideHover = () => {
118
+ ctx.tooltip.classList.remove('pf-visible');
119
+ if (crosshair)
120
+ crosshair.setAttribute('opacity', '0');
121
+ if (dotGroups) {
122
+ for (const dots of dotGroups) {
123
+ for (const dot of dots)
124
+ dot.setAttribute('data-visible', 'false');
125
+ }
126
+ }
127
+ };
128
+ zone.addEventListener('mouseenter', showHover);
129
+ zone.addEventListener('mouseleave', hideHover);
130
+ zone.addEventListener('touchstart', showHover, { passive: true });
131
+ zone.addEventListener('touchend', hideHover, { passive: true });
132
+ svg.appendChild(zone);
133
+ }
134
+ }
135
+ //# sourceMappingURL=chart-tooltip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chart-tooltip.js","sourceRoot":"","sources":["../../../src/renderer/components/chart-tooltip.ts"],"names":[],"mappings":"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"}
@@ -5,4 +5,20 @@
5
5
  * For production use, these can be enhanced with a charting library.
6
6
  */
7
7
  export declare function registerChartComponents(): void;
8
+ export interface SeriesEntry {
9
+ dataKey: string;
10
+ label?: string;
11
+ color?: string;
12
+ yAxisId?: 'left' | 'right';
13
+ }
14
+ export interface ChartLayout {
15
+ /** Usable plot area after axis padding */
16
+ plotLeft: number;
17
+ plotRight: number;
18
+ plotTop: number;
19
+ plotBottom: number;
20
+ plotWidth: number;
21
+ plotHeight: number;
22
+ }
23
+ export declare function formatYValue(value: number, format?: string): string;
8
24
  //# sourceMappingURL=charts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"charts.d.ts","sourceRoot":"","sources":["../../../src/renderer/components/charts.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,wBAAgB,uBAAuB,IAAI,IAAI,CAS9C"}
1
+ {"version":3,"file":"charts.d.ts","sourceRoot":"","sources":["../../../src/renderer/components/charts.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,wBAAgB,uBAAuB,IAAI,IAAI,CAS9C;AAOD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAC3B;AAID,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAuID,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAMnE"}
@@ -5,6 +5,7 @@
5
5
  * For production use, these can be enhanced with a charting library.
6
6
  */
7
7
  import { registerComponent, resolveValue, el } from '../engine.js';
8
+ import { createTooltip, addBarTooltipZones, addLineTooltipZones, showTooltipAt } from './chart-tooltip.js';
8
9
  export function registerChartComponents() {
9
10
  registerComponent('BarChart', renderBarChart);
10
11
  registerComponent('LineChart', renderLineChart);
@@ -119,7 +120,7 @@ function drawBaseline(svg, layout) {
119
120
  line.setAttribute('stroke-width', '1');
120
121
  svg.appendChild(line);
121
122
  }
122
- function formatYValue(value, format) {
123
+ export function formatYValue(value, format) {
123
124
  if (format === 'currency')
124
125
  return `$${value.toLocaleString()}`;
125
126
  if (format === 'percent')
@@ -130,6 +131,15 @@ function formatYValue(value, format) {
130
131
  return `${(value / 1_000).toFixed(1)}K`;
131
132
  return String(value);
132
133
  }
134
+ /** Create a format callback for tooltip entries that handles per-axis formats + null. */
135
+ function makeTooltipFormatter(yAxisFormat, yAxisRightFormat) {
136
+ return (raw, s) => {
137
+ if (raw === null || raw === undefined)
138
+ return '—';
139
+ const fmt = s.yAxisId === 'right' ? yAxisRightFormat : yAxisFormat;
140
+ return formatYValue(Number(raw), fmt);
141
+ };
142
+ }
133
143
  function renderBarChart(node, ctx) {
134
144
  const wrapper = el('div', 'pf-chart pf-bar-chart');
135
145
  const data = resolveValue(node.data, ctx) ?? [];
@@ -142,6 +152,7 @@ function renderBarChart(node, ctx) {
142
152
  const showYAxis = node.showYAxis !== false;
143
153
  const showGrid = node.showGrid === true;
144
154
  const showYAxisRight = node.showYAxisRight === true;
155
+ const showTooltipProp = node.showTooltip !== false;
145
156
  const xAxisKey = node.xAxis;
146
157
  const leftSeries = series.filter(s => s.yAxisId !== 'right');
147
158
  const rightSeries = series.filter(s => s.yAxisId === 'right');
@@ -154,7 +165,7 @@ function renderBarChart(node, ctx) {
154
165
  : 1;
155
166
  const w = 400;
156
167
  const layout = chartLayout(w, height, showYAxis, hasRight);
157
- const svg = createSvg(w, height);
168
+ const svg = createSvg(w, height, 'Bar');
158
169
  // Axes + grid (behind bars)
159
170
  if (showYAxis)
160
171
  drawYAxis(svg, layout, leftMax, showGrid, node.yAxisFormat);
@@ -167,9 +178,12 @@ function renderBarChart(node, ctx) {
167
178
  for (let di = 0; di < data.length; di++) {
168
179
  for (let si = 0; si < series.length; si++) {
169
180
  const s = series[si];
181
+ const raw = data[di][s.dataKey];
182
+ if (raw === null || raw === undefined)
183
+ continue; // skip null bars
170
184
  const isRight = s.yAxisId === 'right';
171
185
  const max = isRight ? rightMax : leftMax;
172
- const val = Number(data[di][s.dataKey] ?? 0);
186
+ const val = Number(raw);
173
187
  const h = Math.max(0, (val / max) * layout.plotHeight);
174
188
  const x = layout.plotLeft + di * barGroupWidth + si * barWidth + barWidth / 2;
175
189
  const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
@@ -188,6 +202,12 @@ function renderBarChart(node, ctx) {
188
202
  return layout.plotLeft + i * barGroupWidth + barGroupWidth / 2;
189
203
  }, layout.plotBottom);
190
204
  }
205
+ // Tooltip hit-zones (on top of bars)
206
+ if (showTooltipProp) {
207
+ const ttCtx = createTooltip(wrapper, svg);
208
+ const fmt = makeTooltipFormatter(node.yAxisFormat, node.yAxisRightFormat);
209
+ addBarTooltipZones(ttCtx, svg, data, series, layout, xAxisKey, fmt);
210
+ }
191
211
  addLegend(wrapper, series, node.showLegend);
192
212
  wrapper.appendChild(svg);
193
213
  return wrapper;
@@ -204,6 +224,7 @@ function renderLineChart(node, ctx) {
204
224
  const showYAxis = node.showYAxis !== false;
205
225
  const showGrid = node.showGrid === true;
206
226
  const showYAxisRight = node.showYAxisRight === true;
227
+ const showTooltipProp = node.showTooltip !== false;
207
228
  const xAxisKey = node.xAxis;
208
229
  // Split series by axis
209
230
  const leftSeries = allSeries.filter(s => s.yAxisId !== 'right');
@@ -218,7 +239,7 @@ function renderLineChart(node, ctx) {
218
239
  : 1;
219
240
  const w = 400;
220
241
  const layout = chartLayout(w, height, showYAxis, hasRight);
221
- const svg = createSvg(w, height);
242
+ const svg = createSvg(w, height, node.type === 'AreaChart' ? 'Area' : 'Line');
222
243
  const isArea = node.type === 'AreaChart';
223
244
  // Draw grid + axes (behind data)
224
245
  if (showYAxis)
@@ -226,39 +247,100 @@ function renderLineChart(node, ctx) {
226
247
  if (hasRight)
227
248
  drawYAxisRight(svg, layout, rightMax, node.yAxisRightFormat);
228
249
  drawBaseline(svg, layout);
229
- // Draw series
250
+ // Draw series (with null-gap handling)
251
+ const dotGroups = []; // one array of dots per series
230
252
  for (let si = 0; si < allSeries.length; si++) {
231
253
  const s = allSeries[si];
232
254
  const isRight = s.yAxisId === 'right';
233
255
  const max = isRight ? rightMax : leftMax;
256
+ const color = s.color ?? COLORS[si % COLORS.length];
234
257
  const points = data.map((d, i) => {
258
+ const raw = d[s.dataKey];
259
+ const isNull = raw === null || raw === undefined;
235
260
  const x = data.length === 1
236
261
  ? (layout.plotLeft + layout.plotRight) / 2
237
262
  : layout.plotLeft + (i / (data.length - 1)) * layout.plotWidth;
238
- const y = layout.plotBottom - (Number(d[s.dataKey] ?? 0) / max) * layout.plotHeight;
239
- return { x, y };
263
+ const y = isNull ? layout.plotBottom : layout.plotBottom - (Number(raw) / max) * layout.plotHeight;
264
+ return { x, y, isNull };
240
265
  });
241
- const color = s.color ?? COLORS[si % COLORS.length];
242
- if (isArea && points.length > 0) {
243
- const areaPath = `M ${points[0].x},${layout.plotBottom} ` +
244
- points.map(p => `L ${p.x},${p.y}`).join(' ') +
245
- ` L ${points[points.length - 1].x},${layout.plotBottom} Z`;
246
- const area = document.createElementNS('http://www.w3.org/2000/svg', 'path');
247
- area.setAttribute('d', areaPath);
248
- area.setAttribute('fill', color);
249
- area.setAttribute('opacity', '0.15');
250
- svg.appendChild(area);
266
+ // Area fill (skip null segments)
267
+ if (isArea) {
268
+ let seg = [];
269
+ const flushArea = () => {
270
+ if (seg.length < 2) {
271
+ seg = [];
272
+ return;
273
+ }
274
+ const d = `M ${seg[0].x},${layout.plotBottom} ` +
275
+ seg.map(p => `L ${p.x},${p.y}`).join(' ') +
276
+ ` L ${seg[seg.length - 1].x},${layout.plotBottom} Z`;
277
+ const area = document.createElementNS('http://www.w3.org/2000/svg', 'path');
278
+ area.setAttribute('d', d);
279
+ area.setAttribute('fill', color);
280
+ area.setAttribute('opacity', '0.15');
281
+ svg.appendChild(area);
282
+ seg = [];
283
+ };
284
+ for (const p of points) {
285
+ if (p.isNull) {
286
+ flushArea();
287
+ continue;
288
+ }
289
+ seg.push(p);
290
+ }
291
+ flushArea();
292
+ }
293
+ // Line path with gap handling: break into segments on null
294
+ let linePath = '';
295
+ for (const p of points) {
296
+ if (p.isNull) { /* gap — next valid point starts a new M */
297
+ continue;
298
+ }
299
+ // Check if previous point was null → start new segment
300
+ const idx = points.indexOf(p);
301
+ const prevNull = idx === 0 || points[idx - 1].isNull;
302
+ linePath += `${prevNull ? 'M' : 'L'} ${p.x},${p.y} `;
303
+ }
304
+ if (linePath) {
305
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
306
+ line.setAttribute('d', linePath.trim());
307
+ line.setAttribute('fill', 'none');
308
+ line.setAttribute('stroke', color);
309
+ line.setAttribute('stroke-width', '2');
310
+ if (isRight)
311
+ line.setAttribute('stroke-dasharray', '6 3');
312
+ svg.appendChild(line);
313
+ }
314
+ // Data-point dots (hidden by default, shown on hover)
315
+ const seriesDots = [];
316
+ for (const p of points) {
317
+ const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
318
+ dot.setAttribute('cx', String(p.x));
319
+ dot.setAttribute('cy', String(p.y));
320
+ dot.setAttribute('r', '4');
321
+ dot.setAttribute('fill', color);
322
+ dot.setAttribute('class', 'pf-data-dot');
323
+ dot.setAttribute('data-visible', 'false');
324
+ dot.style.pointerEvents = 'none';
325
+ if (p.isNull)
326
+ dot.style.display = 'none';
327
+ svg.appendChild(dot);
328
+ seriesDots.push(dot);
251
329
  }
252
- const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`).join(' ');
253
- const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
254
- line.setAttribute('d', linePath);
255
- line.setAttribute('fill', 'none');
256
- line.setAttribute('stroke', color);
257
- line.setAttribute('stroke-width', '2');
258
- if (isRight)
259
- line.setAttribute('stroke-dasharray', '6 3');
260
- svg.appendChild(line);
330
+ dotGroups.push(seriesDots);
261
331
  }
332
+ // Crosshair line (hidden by default)
333
+ const crosshair = document.createElementNS('http://www.w3.org/2000/svg', 'line');
334
+ crosshair.setAttribute('class', 'pf-crosshair');
335
+ crosshair.setAttribute('x1', String(layout.plotLeft));
336
+ crosshair.setAttribute('x2', String(layout.plotLeft));
337
+ crosshair.setAttribute('y1', String(layout.plotTop));
338
+ crosshair.setAttribute('y2', String(layout.plotBottom));
339
+ crosshair.setAttribute('stroke', AXIS_COLOR);
340
+ crosshair.setAttribute('stroke-width', '1');
341
+ crosshair.setAttribute('stroke-dasharray', '3 3');
342
+ crosshair.setAttribute('opacity', '0');
343
+ svg.appendChild(crosshair);
262
344
  // X-axis labels
263
345
  if (xAxisKey) {
264
346
  drawXAxisLabels(svg, data, xAxisKey, (i) => {
@@ -267,6 +349,12 @@ function renderLineChart(node, ctx) {
267
349
  : layout.plotLeft + (i / (data.length - 1)) * layout.plotWidth;
268
350
  }, layout.plotBottom);
269
351
  }
352
+ // Tooltip hit-zones (on top of lines)
353
+ if (showTooltipProp) {
354
+ const ttCtx = createTooltip(wrapper, svg);
355
+ const fmt = makeTooltipFormatter(node.yAxisFormat, node.yAxisRightFormat);
356
+ addLineTooltipZones(ttCtx, svg, data, allSeries, layout, xAxisKey, fmt, crosshair, dotGroups);
357
+ }
270
358
  addLegend(wrapper, allSeries, node.showLegend);
271
359
  wrapper.appendChild(svg);
272
360
  return wrapper;
@@ -277,16 +365,18 @@ function renderPieChart(node, ctx) {
277
365
  const series = node.series ?? [];
278
366
  const height = node.height ?? 300;
279
367
  const size = Math.min(height, 300);
368
+ const showTooltipProp = node.showTooltip !== false;
280
369
  if (data.length === 0 || series.length === 0) {
281
370
  wrapper.textContent = 'No chart data';
282
371
  return wrapper;
283
372
  }
284
- const svg = createSvg(size, size);
373
+ const svg = createSvg(size, size, 'Pie');
285
374
  const cx = size / 2;
286
375
  const cy = size / 2;
287
376
  const r = size / 2 - 10;
288
377
  // Use first series key, each data point is a slice
289
378
  const key = series[0].dataKey;
379
+ const xAxisKey = node.xAxis;
290
380
  const values = data.map(d => Number(d[key] ?? 0));
291
381
  const total = values.reduce((a, b) => a + b, 0);
292
382
  if (total === 0) {
@@ -294,6 +384,7 @@ function renderPieChart(node, ctx) {
294
384
  wrapper.appendChild(svg);
295
385
  return wrapper;
296
386
  }
387
+ const ttCtx = showTooltipProp ? createTooltip(wrapper, svg) : undefined;
297
388
  let startAngle = -Math.PI / 2;
298
389
  for (let i = 0; i < values.length; i++) {
299
390
  const angle = (values[i] / total) * 2 * Math.PI;
@@ -303,9 +394,34 @@ function renderPieChart(node, ctx) {
303
394
  const y1 = cy + r * Math.sin(startAngle);
304
395
  const x2 = cx + r * Math.cos(endAngle);
305
396
  const y2 = cy + r * Math.sin(endAngle);
397
+ const color = COLORS[i % COLORS.length];
306
398
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
307
399
  path.setAttribute('d', `M ${cx},${cy} L ${x1},${y1} A ${r},${r} 0 ${largeArc} 1 ${x2},${y2} Z`);
308
- path.setAttribute('fill', COLORS[i % COLORS.length]);
400
+ path.setAttribute('fill', color);
401
+ path.style.cursor = 'default';
402
+ if (ttCtx) {
403
+ const rawSlice = xAxisKey ? data[i][xAxisKey] : undefined;
404
+ const sliceLabel = rawSlice != null ? String(rawSlice) : `Slice ${i + 1}`;
405
+ const pct = `${((values[i] / total) * 100).toFixed(1)}%`;
406
+ const midAngle = startAngle + angle / 2;
407
+ const tipX = cx + (r * 0.6) * Math.cos(midAngle);
408
+ path.addEventListener('mouseenter', () => {
409
+ showTooltipAt(ttCtx, tipX, cy, sliceLabel, [
410
+ { label: series[0].label ?? key, value: `${values[i]} (${pct})`, color },
411
+ ]);
412
+ });
413
+ path.addEventListener('mouseleave', () => {
414
+ ttCtx.tooltip.classList.remove('pf-visible');
415
+ });
416
+ path.addEventListener('touchstart', () => {
417
+ showTooltipAt(ttCtx, tipX, cy, sliceLabel, [
418
+ { label: series[0].label ?? key, value: `${values[i]} (${pct})`, color },
419
+ ]);
420
+ }, { passive: true });
421
+ path.addEventListener('touchend', () => {
422
+ ttCtx.tooltip.classList.remove('pf-visible');
423
+ }, { passive: true });
424
+ }
309
425
  svg.appendChild(path);
310
426
  startAngle = endAngle;
311
427
  }
@@ -322,11 +438,15 @@ function renderFallbackChart(node, _ctx) {
322
438
  return e;
323
439
  }
324
440
  // ── Helpers ──────────────────────────────────────────────────────────────────
325
- function createSvg(width, height) {
441
+ function createSvg(width, height, chartType) {
326
442
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
327
443
  svg.setAttribute('width', '100%');
328
444
  svg.setAttribute('height', String(height));
329
445
  svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
446
+ svg.setAttribute('role', 'img');
447
+ if (chartType) {
448
+ svg.setAttribute('aria-label', `${chartType} chart`);
449
+ }
330
450
  svg.style.overflow = 'visible';
331
451
  return svg;
332
452
  }
@@ -380,14 +500,10 @@ function renderHistogram(node, ctx) {
380
500
  const W = 300;
381
501
  const H = height;
382
502
  const barW = W / binCount;
383
- const ns = 'http://www.w3.org/2000/svg';
384
- const svg = document.createElementNS(ns, 'svg');
385
- svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
386
- svg.setAttribute('width', '100%');
387
- svg.setAttribute('height', String(H));
503
+ const svg = createSvg(W, H, 'Histogram');
388
504
  for (let i = 0; i < binCount; i++) {
389
505
  const barH = maxBin > 0 ? (bins[i] / maxBin) * H : 0;
390
- const rect = document.createElementNS(ns, 'rect');
506
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
391
507
  rect.setAttribute('x', String(i * barW));
392
508
  rect.setAttribute('y', String(H - barH));
393
509
  rect.setAttribute('width', String(barW - 1));