@maxhealth.tech/prefab 0.2.5 → 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 +1 -1
- package/dist/app.js +1 -1
- package/dist/prefab.css +21 -0
- package/dist/renderer/components/chart-tooltip.d.ts +25 -0
- package/dist/renderer/components/chart-tooltip.d.ts.map +1 -0
- package/dist/renderer/components/chart-tooltip.js +135 -0
- package/dist/renderer/components/chart-tooltip.js.map +1 -0
- package/dist/renderer/components/charts.d.ts +16 -0
- package/dist/renderer/components/charts.d.ts.map +1 -1
- package/dist/renderer/components/charts.js +151 -35
- package/dist/renderer/components/charts.js.map +1 -1
- package/dist/renderer.auto.min.js +28 -28
- package/dist/renderer.min.js +28 -28
- package/package.json +1 -1
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.
|
|
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.
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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;
|
|
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(
|
|
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(
|
|
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
|
-
|
|
242
|
-
if (isArea
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
|
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(
|
|
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));
|