hazo_dataviz 0.3.0

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.
Files changed (91) hide show
  1. package/CHANGE_LOG.md +54 -0
  2. package/README.md +115 -0
  3. package/SETUP_CHECKLIST.md +54 -0
  4. package/dist/api/index.d.ts +4 -0
  5. package/dist/api/index.d.ts.map +1 -0
  6. package/dist/api/index.js +11 -0
  7. package/dist/charts/line_chart.d.ts +14 -0
  8. package/dist/charts/line_chart.d.ts.map +1 -0
  9. package/dist/charts/line_chart.js +26 -0
  10. package/dist/charts/multi_line_chart.d.ts +13 -0
  11. package/dist/charts/multi_line_chart.d.ts.map +1 -0
  12. package/dist/charts/multi_line_chart.js +32 -0
  13. package/dist/hooks/use_data_query.d.ts +11 -0
  14. package/dist/hooks/use_data_query.d.ts.map +1 -0
  15. package/dist/hooks/use_data_query.js +35 -0
  16. package/dist/index.client.d.ts +17 -0
  17. package/dist/index.client.d.ts.map +1 -0
  18. package/dist/index.client.js +11 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +2 -0
  22. package/dist/lib/auto_encode.d.ts +24 -0
  23. package/dist/lib/auto_encode.d.ts.map +1 -0
  24. package/dist/lib/auto_encode.js +46 -0
  25. package/dist/lib/contract-version.d.ts +2 -0
  26. package/dist/lib/contract-version.d.ts.map +1 -0
  27. package/dist/lib/contract-version.js +1 -0
  28. package/dist/lib/format.d.ts +5 -0
  29. package/dist/lib/format.d.ts.map +1 -0
  30. package/dist/lib/format.js +31 -0
  31. package/dist/lib/index.d.ts +3 -0
  32. package/dist/lib/index.d.ts.map +1 -0
  33. package/dist/lib/index.js +2 -0
  34. package/dist/lib/picker.d.ts +4 -0
  35. package/dist/lib/picker.d.ts.map +1 -0
  36. package/dist/lib/picker.js +20 -0
  37. package/dist/lib/runners/in_memory.d.ts +4 -0
  38. package/dist/lib/runners/in_memory.d.ts.map +1 -0
  39. package/dist/lib/runners/in_memory.js +303 -0
  40. package/dist/lib/shape.d.ts +11 -0
  41. package/dist/lib/shape.d.ts.map +1 -0
  42. package/dist/lib/shape.js +27 -0
  43. package/dist/schema/descriptor.d.ts +162 -0
  44. package/dist/schema/descriptor.d.ts.map +1 -0
  45. package/dist/schema/descriptor.js +56 -0
  46. package/dist/schema/query.d.ts +96 -0
  47. package/dist/schema/query.d.ts.map +1 -0
  48. package/dist/schema/query.js +37 -0
  49. package/dist/testing/fixtures.d.ts +163 -0
  50. package/dist/testing/fixtures.d.ts.map +1 -0
  51. package/dist/testing/fixtures.js +59 -0
  52. package/dist/testing/index.d.ts +3 -0
  53. package/dist/testing/index.d.ts.map +1 -0
  54. package/dist/testing/index.js +2 -0
  55. package/dist/types/index.d.ts +62 -0
  56. package/dist/types/index.d.ts.map +1 -0
  57. package/dist/types/index.js +1 -0
  58. package/dist/visualizers/area.d.ts +3 -0
  59. package/dist/visualizers/area.d.ts.map +1 -0
  60. package/dist/visualizers/area.js +66 -0
  61. package/dist/visualizers/bar.d.ts +3 -0
  62. package/dist/visualizers/bar.d.ts.map +1 -0
  63. package/dist/visualizers/bar.js +41 -0
  64. package/dist/visualizers/builtins.d.ts +3 -0
  65. package/dist/visualizers/builtins.d.ts.map +1 -0
  66. package/dist/visualizers/builtins.js +7 -0
  67. package/dist/visualizers/chart_frame.d.ts +97 -0
  68. package/dist/visualizers/chart_frame.d.ts.map +1 -0
  69. package/dist/visualizers/chart_frame.js +146 -0
  70. package/dist/visualizers/index.d.ts +7 -0
  71. package/dist/visualizers/index.d.ts.map +1 -0
  72. package/dist/visualizers/index.js +4 -0
  73. package/dist/visualizers/line.d.ts +3 -0
  74. package/dist/visualizers/line.d.ts.map +1 -0
  75. package/dist/visualizers/line.js +66 -0
  76. package/dist/visualizers/palette.d.ts +13 -0
  77. package/dist/visualizers/palette.d.ts.map +1 -0
  78. package/dist/visualizers/palette.js +24 -0
  79. package/dist/visualizers/single_stat.d.ts +3 -0
  80. package/dist/visualizers/single_stat.d.ts.map +1 -0
  81. package/dist/visualizers/single_stat.js +60 -0
  82. package/dist/visualizers/stacked_bar.d.ts +3 -0
  83. package/dist/visualizers/stacked_bar.d.ts.map +1 -0
  84. package/dist/visualizers/stacked_bar.js +75 -0
  85. package/dist/visualizers/table.d.ts +3 -0
  86. package/dist/visualizers/table.d.ts.map +1 -0
  87. package/dist/visualizers/table.js +55 -0
  88. package/dist/visualizers/visualizer_view.d.ts +12 -0
  89. package/dist/visualizers/visualizer_view.d.ts.map +1 -0
  90. package/dist/visualizers/visualizer_view.js +6 -0
  91. package/package.json +88 -0
@@ -0,0 +1,146 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { formatValue } from '../lib/format.js';
4
+ /**
5
+ * "Studio" chart theme — a warm, editorial token set shared by every recharts
6
+ * visualizer so the package renders one coherent look out of the box.
7
+ *
8
+ * These are plain hex/number tokens (not Tailwind) on purpose: the charts are
9
+ * SVG and must look right in any consumer app regardless of its CSS pipeline.
10
+ * Font family is deliberately omitted so SVG text inherits the host page font.
11
+ */
12
+ export const CHART_THEME = {
13
+ /** Primary text — warm near-black. */
14
+ ink: '#2f2a25',
15
+ /** Axis ticks / secondary labels — warm grey. */
16
+ muted: '#9a9087',
17
+ /** Hairline grid + axis lines — warm sand. */
18
+ hairline: '#ece6dd',
19
+ /** Tooltip surface — warm white. */
20
+ surface: '#fffdf9',
21
+ /** Tooltip / card border. */
22
+ surfaceBorder: '#e7ddd0',
23
+ /** Base tick font size. */
24
+ fontSize: 12,
25
+ /** Default series stroke width. */
26
+ strokeWidth: 2.5,
27
+ };
28
+ /** Shared inner margin so axis labels never clip against a card edge. */
29
+ export const CHART_MARGIN = { top: 12, right: 16, bottom: 4, left: 4 };
30
+ /** Spread onto `<XAxis>` / `<YAxis>` for the Studio look (hairline, no tick marks). */
31
+ export const axisProps = {
32
+ tick: { fill: CHART_THEME.muted, fontSize: CHART_THEME.fontSize },
33
+ axisLine: { stroke: CHART_THEME.hairline },
34
+ tickLine: false,
35
+ tickMargin: 8,
36
+ };
37
+ /** Spread onto `<CartesianGrid>` — horizontal hairlines only, no vertical clutter. */
38
+ export const gridProps = {
39
+ stroke: CHART_THEME.hairline,
40
+ vertical: false,
41
+ };
42
+ /**
43
+ * Returns a recharts tick/value formatter for a given column.
44
+ * Uses formatValue when the column has a format, else a sane default.
45
+ */
46
+ export function makeFormatter(column) {
47
+ if (column?.format) {
48
+ const fmt = column.format;
49
+ return (v) => {
50
+ if (v === null || v === undefined)
51
+ return '';
52
+ return formatValue(v, fmt);
53
+ };
54
+ }
55
+ return (v) => {
56
+ if (v === null || v === undefined)
57
+ return '';
58
+ return String(v);
59
+ };
60
+ }
61
+ /**
62
+ * Returns the chart height to use. When opts has explicit height, use it;
63
+ * otherwise default to 300.
64
+ */
65
+ export function resolveChartDimensions(opts) {
66
+ return {
67
+ width: opts?.width,
68
+ height: opts?.height ?? 300,
69
+ };
70
+ }
71
+ /**
72
+ * Builds a recharts `content` renderer for `<Tooltip>` styled to match the
73
+ * Studio theme — a warm floating card with a colored swatch per series.
74
+ *
75
+ * @param labelFormatter formats the x value shown as the tooltip heading
76
+ * @param valueFormatter formats each series value
77
+ */
78
+ // Return type is intentionally `any`: recharts' `content` prop expects its own
79
+ // `ContentType`, and the workspace hoists a React 18 `@types/react` whose
80
+ // `ReactNode` disagrees with this package's React 19 types (`bigint`). Typing the
81
+ // boundary as `any` sidesteps that cross-version skew without weakening the
82
+ // component body, which stays fully typed via `TooltipRenderProps`.
83
+ export function makeTooltip(labelFormatter, valueFormatter) {
84
+ return function ChartTooltip(props) {
85
+ if (!props.active || !props.payload || props.payload.length === 0)
86
+ return null;
87
+ const heading = labelFormatter(props.label);
88
+ return (_jsxs("div", { style: {
89
+ background: CHART_THEME.surface,
90
+ border: `1px solid ${CHART_THEME.surfaceBorder}`,
91
+ borderRadius: 10,
92
+ padding: '8px 12px',
93
+ boxShadow: '0 6px 20px -8px rgba(60, 45, 30, 0.28)',
94
+ fontSize: 12,
95
+ lineHeight: 1.5,
96
+ minWidth: 120,
97
+ }, children: [heading && (_jsx("div", { style: {
98
+ color: CHART_THEME.muted,
99
+ fontSize: 11,
100
+ fontWeight: 600,
101
+ textTransform: 'uppercase',
102
+ letterSpacing: '0.04em',
103
+ marginBottom: 6,
104
+ }, children: heading })), props.payload.map((entry, i) => (_jsxs("div", { style: {
105
+ display: 'flex',
106
+ alignItems: 'center',
107
+ justifyContent: 'space-between',
108
+ gap: 16,
109
+ padding: '1px 0',
110
+ }, children: [_jsxs("span", { style: { display: 'flex', alignItems: 'center', gap: 7, color: CHART_THEME.muted }, children: [_jsx("span", { style: {
111
+ width: 9,
112
+ height: 9,
113
+ borderRadius: 3,
114
+ background: entry.color ?? CHART_THEME.ink,
115
+ flexShrink: 0,
116
+ } }), String(entry.name ?? entry.dataKey ?? '')] }), _jsx("span", { style: {
117
+ color: CHART_THEME.ink,
118
+ fontWeight: 600,
119
+ fontVariantNumeric: 'tabular-nums',
120
+ }, children: valueFormatter(entry.value) })] }, i)))] }));
121
+ };
122
+ }
123
+ /** Props spread onto `<Legend>` for the Studio look. */
124
+ export const legendProps = {
125
+ iconType: 'circle',
126
+ wrapperStyle: {
127
+ fontSize: 12,
128
+ color: CHART_THEME.muted,
129
+ paddingTop: 8,
130
+ },
131
+ };
132
+ /**
133
+ * Empty state element returned when result.rows is empty.
134
+ */
135
+ export function EmptyState() {
136
+ return (_jsxs("div", { style: {
137
+ display: 'flex',
138
+ flexDirection: 'column',
139
+ alignItems: 'center',
140
+ justifyContent: 'center',
141
+ gap: 6,
142
+ height: 120,
143
+ color: CHART_THEME.muted,
144
+ fontSize: 13,
145
+ }, children: [_jsxs("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: CHART_THEME.hairline, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M3 3v18h18" }), _jsx("path", { d: "M7 15l3-3 3 2 5-6" })] }), _jsx("span", { children: "No data to display" })] }));
146
+ }
@@ -0,0 +1,7 @@
1
+ export { builtinVisualizers } from './builtins.js';
2
+ export { selectVisualizer, deriveEncoding } from '../lib/auto_encode.js';
3
+ export { VisualizerView } from './visualizer_view.js';
4
+ export { classifyShape } from '../lib/shape.js';
5
+ export type { Visualizer, VisualizerOpts } from '../types/index.js';
6
+ export type { QueryShape } from '../lib/shape.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/visualizers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpE,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { builtinVisualizers } from './builtins.js';
2
+ export { selectVisualizer, deriveEncoding } from '../lib/auto_encode.js';
3
+ export { VisualizerView } from './visualizer_view.js';
4
+ export { classifyShape } from '../lib/shape.js';
@@ -0,0 +1,3 @@
1
+ import type { Visualizer } from '../types/index.js';
2
+ export declare const line: Visualizer;
3
+ //# sourceMappingURL=line.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"line.d.ts","sourceRoot":"","sources":["../../src/visualizers/line.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAoC,MAAM,mBAAmB,CAAC;AAiDtF,eAAO,MAAM,IAAI,EAAE,UAsElB,CAAC"}
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts';
4
+ import { classifyShape } from '../lib/shape.js';
5
+ import { pickColor } from './palette.js';
6
+ import { makeFormatter, makeTooltip, EmptyState, axisProps, gridProps, legendProps, CHART_MARGIN, CHART_THEME, } from './chart_frame.js';
7
+ function pivotData(result, encoding) {
8
+ if (!encoding.series) {
9
+ // No series split — data already in the right shape, y keys = encoding.y
10
+ return { data: result.rows, seriesKeys: encoding.y };
11
+ }
12
+ // Pivot long → wide: group by x, for each series value create a data key
13
+ const xKey = encoding.x;
14
+ const seriesKey = encoding.series;
15
+ const measureKey = encoding.y[0] ?? '';
16
+ const seriesValues = Array.from(new Set(result.rows.map((r) => String(r[seriesKey] ?? ''))));
17
+ const grouped = new Map();
18
+ for (const row of result.rows) {
19
+ const xVal = row[xKey];
20
+ if (!grouped.has(xVal)) {
21
+ grouped.set(xVal, { [xKey]: xVal });
22
+ }
23
+ const entry = grouped.get(xVal);
24
+ const sv = String(row[seriesKey] ?? '');
25
+ entry[sv] = row[measureKey];
26
+ }
27
+ return {
28
+ data: Array.from(grouped.values()),
29
+ seriesKeys: seriesValues,
30
+ };
31
+ }
32
+ export const line = {
33
+ id: 'line',
34
+ label: 'Line Chart',
35
+ accepts(descriptor, query) {
36
+ const shape = classifyShape(descriptor, query);
37
+ if (shape.temporalDims.length >= 1 && shape.measureCount >= 1)
38
+ return 0.9;
39
+ return 0;
40
+ },
41
+ render(result, encoding, opts) {
42
+ if (result.rows.length === 0)
43
+ return _jsx(EmptyState, {});
44
+ const xCol = result.columns.find((c) => c.key === encoding.x);
45
+ if (!xCol && encoding.x) {
46
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
47
+ console.warn(`[hazo_dataviz/line] encoding.x "${encoding.x}" not found in result.columns`);
48
+ }
49
+ return _jsx(EmptyState, {});
50
+ }
51
+ const { data, seriesKeys } = pivotData(result, encoding);
52
+ const xFormatter = makeFormatter(xCol);
53
+ const showLegend = opts?.showLegend ?? seriesKeys.length > 1;
54
+ const showTooltip = opts?.showTooltip ?? true;
55
+ // Get formatter for the first y column
56
+ const yCol = result.columns.find((c) => c.key === (encoding.series ? seriesKeys[0] : encoding.y[0]));
57
+ const yFormatter = makeFormatter(yCol ?? result.columns.find((c) => c.role === 'measure'));
58
+ const chartHeight = opts?.height ?? 300;
59
+ const chartWidth = opts?.width;
60
+ const chart = (_jsxs(LineChart, { data: data, width: chartWidth, height: chartWidth !== undefined ? chartHeight : undefined, margin: CHART_MARGIN, children: [_jsx(CartesianGrid, { ...gridProps }), _jsx(XAxis, { dataKey: encoding.x, tickFormatter: xFormatter, ...axisProps }), _jsx(YAxis, { tickFormatter: yFormatter, ...axisProps, width: 48 }), showTooltip && (_jsx(Tooltip, { cursor: { stroke: CHART_THEME.hairline, strokeWidth: 1.5 }, content: makeTooltip(xFormatter, yFormatter) })), showLegend && _jsx(Legend, { ...legendProps }), seriesKeys.map((key, i) => (_jsx(Line, { type: "monotone", dataKey: key, stroke: pickColor(i, opts?.colors), strokeWidth: CHART_THEME.strokeWidth, dot: false, activeDot: { r: 4, strokeWidth: 2, stroke: CHART_THEME.surface } }, key)))] }));
61
+ if (chartWidth !== undefined) {
62
+ return (_jsx("div", { style: { width: chartWidth, height: chartHeight, overflow: 'hidden' }, children: chart }));
63
+ }
64
+ return (_jsx(ResponsiveContainer, { width: "100%", height: chartHeight, children: chart }));
65
+ },
66
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * "Studio" palette — a warm, vivid-but-tasteful series ramp.
3
+ *
4
+ * Ordered so adjacent series stay distinguishable (warm / cool alternation)
5
+ * and the first two — coral + teal — read as a confident complementary pair,
6
+ * which is what most one- and two-series charts will actually use.
7
+ */
8
+ export declare const DEFAULT_PALETTE: string[];
9
+ /**
10
+ * Pick a color from the palette (or the override) by cycling modulo its length.
11
+ */
12
+ export declare function pickColor(index: number, colors?: string[]): string;
13
+ //# sourceMappingURL=palette.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"palette.d.ts","sourceRoot":"","sources":["../../src/visualizers/palette.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,EASnC,CAAC;AAEF;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAGlE"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * "Studio" palette — a warm, vivid-but-tasteful series ramp.
3
+ *
4
+ * Ordered so adjacent series stay distinguishable (warm / cool alternation)
5
+ * and the first two — coral + teal — read as a confident complementary pair,
6
+ * which is what most one- and two-series charts will actually use.
7
+ */
8
+ export const DEFAULT_PALETTE = [
9
+ '#f5563d', // coral
10
+ '#14a098', // teal
11
+ '#f5a524', // marigold
12
+ '#8b4a8f', // plum
13
+ '#3d6fb4', // ocean
14
+ '#84a13a', // olive
15
+ '#e0608a', // rose
16
+ '#c2691c', // amber-brown
17
+ ];
18
+ /**
19
+ * Pick a color from the palette (or the override) by cycling modulo its length.
20
+ */
21
+ export function pickColor(index, colors) {
22
+ const palette = colors && colors.length > 0 ? colors : DEFAULT_PALETTE;
23
+ return palette[index % palette.length];
24
+ }
@@ -0,0 +1,3 @@
1
+ import type { Visualizer } from '../types/index.js';
2
+ export declare const singleStat: Visualizer;
3
+ //# sourceMappingURL=single_stat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"single_stat.d.ts","sourceRoot":"","sources":["../../src/visualizers/single_stat.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAoC,MAAM,mBAAmB,CAAC;AAOtF,eAAO,MAAM,UAAU,EAAE,UAmFxB,CAAC"}
@@ -0,0 +1,60 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { classifyShape } from '../lib/shape.js';
4
+ import { makeFormatter, CHART_THEME } from './chart_frame.js';
5
+ import { pickColor } from './palette.js';
6
+ export const singleStat = {
7
+ id: 'single_stat',
8
+ label: 'Single Stat',
9
+ accepts(descriptor, query) {
10
+ const shape = classifyShape(descriptor, query);
11
+ if (shape.measureCount === 1 && shape.dimCount === 0)
12
+ return 0.9;
13
+ return 0;
14
+ },
15
+ render(result, encoding, opts) {
16
+ const measureCol = result.columns.find((c) => c.role === 'measure');
17
+ const formatter = makeFormatter(measureCol);
18
+ const accent = pickColor(0, opts?.colors);
19
+ // Determine the value: first row's first measure, or null if empty
20
+ let displayValue = '—';
21
+ if (result.rows.length > 0 && measureCol) {
22
+ const raw = result.rows[0][measureCol.key];
23
+ if (raw !== null && raw !== undefined) {
24
+ displayValue = formatter(raw);
25
+ }
26
+ }
27
+ const label = measureCol?.label ?? (encoding.y[0] ?? '');
28
+ return (_jsxs("div", { style: {
29
+ display: 'flex',
30
+ flexDirection: 'column',
31
+ alignItems: 'center',
32
+ justifyContent: 'center',
33
+ gap: 10,
34
+ padding: '28px 16px',
35
+ }, children: [label && (_jsx("div", { style: {
36
+ fontSize: 12,
37
+ fontWeight: 600,
38
+ color: CHART_THEME.muted,
39
+ textTransform: 'uppercase',
40
+ letterSpacing: '0.08em',
41
+ }, children: label })), _jsxs("div", { style: {
42
+ display: 'flex',
43
+ alignItems: 'baseline',
44
+ gap: 12,
45
+ }, children: [_jsx("span", { "aria-hidden": "true", style: {
46
+ width: 6,
47
+ height: 38,
48
+ borderRadius: 3,
49
+ background: accent,
50
+ alignSelf: 'center',
51
+ } }), _jsx("span", { style: {
52
+ fontSize: 52,
53
+ fontWeight: 700,
54
+ color: CHART_THEME.ink,
55
+ lineHeight: 1,
56
+ letterSpacing: '-0.02em',
57
+ fontVariantNumeric: 'tabular-nums',
58
+ }, children: displayValue })] })] }));
59
+ },
60
+ };
@@ -0,0 +1,3 @@
1
+ import type { Visualizer } from '../types/index.js';
2
+ export declare const stackedBar: Visualizer;
3
+ //# sourceMappingURL=stacked_bar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stacked_bar.d.ts","sourceRoot":"","sources":["../../src/visualizers/stacked_bar.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAoC,MAAM,mBAAmB,CAAC;AA8CtF,eAAO,MAAM,UAAU,EAAE,UAmFxB,CAAC"}
@@ -0,0 +1,75 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts';
4
+ import { classifyShape } from '../lib/shape.js';
5
+ import { pickColor } from './palette.js';
6
+ import { makeFormatter, makeTooltip, EmptyState, axisProps, gridProps, legendProps, CHART_MARGIN, } from './chart_frame.js';
7
+ function pivotData(result, encoding) {
8
+ if (!encoding.series) {
9
+ return { data: result.rows, seriesKeys: encoding.y };
10
+ }
11
+ const xKey = encoding.x;
12
+ const seriesKey = encoding.series;
13
+ const measureKey = encoding.y[0] ?? '';
14
+ const seriesValues = Array.from(new Set(result.rows.map((r) => String(r[seriesKey] ?? ''))));
15
+ const grouped = new Map();
16
+ for (const row of result.rows) {
17
+ const xVal = row[xKey];
18
+ if (!grouped.has(xVal)) {
19
+ grouped.set(xVal, { [xKey]: xVal });
20
+ }
21
+ const entry = grouped.get(xVal);
22
+ const sv = String(row[seriesKey] ?? '');
23
+ entry[sv] = row[measureKey];
24
+ }
25
+ return {
26
+ data: Array.from(grouped.values()),
27
+ seriesKeys: seriesValues,
28
+ };
29
+ }
30
+ export const stackedBar = {
31
+ id: 'stacked_bar',
32
+ label: 'Stacked Bar Chart',
33
+ accepts(descriptor, query) {
34
+ const shape = classifyShape(descriptor, query);
35
+ // A stacked bar needs two dimensions and at least one categorical dimension to
36
+ // stack by (the series, always query.dimensions[1]).
37
+ if (shape.dimCount === 2 &&
38
+ shape.measureCount >= 1 &&
39
+ shape.categoricalDims.length >= 1) {
40
+ // Full confidence when the x-axis (dimensions[0]) is itself categorical — the
41
+ // canonical "measure by category, split by category" stacked bar.
42
+ // When x is temporal (e.g. date) the same data reads better as a multi-series
43
+ // line, so score below line's 0.9 — stacked bar stays a candidate (> 0) but does
44
+ // not displace line as the default for time series.
45
+ const xIsCategorical = shape.categoricalDims.includes(query.dimensions[0]);
46
+ return xIsCategorical ? 0.95 : 0.6;
47
+ }
48
+ return 0;
49
+ },
50
+ render(result, encoding, opts) {
51
+ if (result.rows.length === 0)
52
+ return _jsx(EmptyState, {});
53
+ const xCol = result.columns.find((c) => c.key === encoding.x);
54
+ if (!xCol && encoding.x) {
55
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
56
+ console.warn(`[hazo_dataviz/stacked_bar] encoding.x "${encoding.x}" not found in result.columns`);
57
+ }
58
+ return _jsx(EmptyState, {});
59
+ }
60
+ const { data, seriesKeys } = pivotData(result, encoding);
61
+ const xFormatter = makeFormatter(xCol);
62
+ const showLegend = opts?.showLegend ?? true;
63
+ const showTooltip = opts?.showTooltip ?? true;
64
+ const yCol = result.columns.find((c) => c.role === 'measure');
65
+ const yFormatter = makeFormatter(yCol);
66
+ const chartHeight = opts?.height ?? 300;
67
+ const chartWidth = opts?.width;
68
+ const lastIdx = seriesKeys.length - 1;
69
+ const chart = (_jsxs(BarChart, { data: data, width: chartWidth, height: chartWidth !== undefined ? chartHeight : undefined, margin: CHART_MARGIN, barCategoryGap: "28%", children: [_jsx(CartesianGrid, { ...gridProps }), _jsx(XAxis, { dataKey: encoding.x, tickFormatter: xFormatter, ...axisProps }), _jsx(YAxis, { tickFormatter: yFormatter, ...axisProps, width: 48 }), showTooltip && (_jsx(Tooltip, { cursor: { fill: 'rgba(47, 42, 37, 0.05)' }, content: makeTooltip(xFormatter, yFormatter) })), showLegend && _jsx(Legend, { ...legendProps }), seriesKeys.map((key, i) => (_jsx(Bar, { dataKey: key, stackId: "a", fill: pickColor(i, opts?.colors), radius: i === lastIdx ? [6, 6, 0, 0] : [0, 0, 0, 0], maxBarSize: 64 }, key)))] }));
70
+ if (chartWidth !== undefined) {
71
+ return (_jsx("div", { style: { width: chartWidth, height: chartHeight, overflow: 'hidden' }, children: chart }));
72
+ }
73
+ return (_jsx(ResponsiveContainer, { width: "100%", height: chartHeight, children: chart }));
74
+ },
75
+ };
@@ -0,0 +1,3 @@
1
+ import type { Visualizer } from '../types/index.js';
2
+ export declare const table: Visualizer;
3
+ //# sourceMappingURL=table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../src/visualizers/table.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAoC,MAAM,mBAAmB,CAAC;AAKtF,eAAO,MAAM,KAAK,EAAE,UAiGnB,CAAC"}
@@ -0,0 +1,55 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { makeFormatter, CHART_THEME } from './chart_frame.js';
4
+ export const table = {
5
+ id: 'table',
6
+ label: 'Table',
7
+ accepts(_descriptor, _query) {
8
+ return 0.1;
9
+ },
10
+ render(result, _encoding, _opts) {
11
+ if (result.rows.length === 0) {
12
+ return (_jsx("div", { style: {
13
+ display: 'flex',
14
+ alignItems: 'center',
15
+ justifyContent: 'center',
16
+ height: 80,
17
+ color: CHART_THEME.muted,
18
+ fontSize: 13,
19
+ }, children: "No data to display" }));
20
+ }
21
+ const formatters = result.columns.map((col) => makeFormatter(col));
22
+ return (_jsx("div", { style: {
23
+ overflowX: 'auto',
24
+ overflowY: 'auto',
25
+ maxHeight: 400,
26
+ border: `1px solid ${CHART_THEME.hairline}`,
27
+ borderRadius: 10,
28
+ }, children: _jsxs("table", { style: {
29
+ borderCollapse: 'collapse',
30
+ width: '100%',
31
+ fontSize: 13,
32
+ }, children: [_jsx("thead", { children: _jsx("tr", { children: result.columns.map((col) => (_jsx("th", { style: {
33
+ padding: '10px 14px',
34
+ textAlign: col.role === 'measure' ? 'right' : 'left',
35
+ background: CHART_THEME.surface,
36
+ borderBottom: `1px solid ${CHART_THEME.surfaceBorder}`,
37
+ fontWeight: 600,
38
+ fontSize: 11,
39
+ textTransform: 'uppercase',
40
+ letterSpacing: '0.04em',
41
+ color: CHART_THEME.muted,
42
+ whiteSpace: 'nowrap',
43
+ position: 'sticky',
44
+ top: 0,
45
+ }, children: col.label }, col.key))) }) }), _jsx("tbody", { children: result.rows.map((row, rowIdx) => (_jsx("tr", { children: result.columns.map((col, colIdx) => (_jsx("td", { style: {
46
+ padding: '9px 14px',
47
+ textAlign: col.role === 'measure' ? 'right' : 'left',
48
+ borderBottom: rowIdx === result.rows.length - 1 ? 'none' : `1px solid ${CHART_THEME.hairline}`,
49
+ color: col.role === 'measure' ? CHART_THEME.ink : '#5c554d',
50
+ fontVariantNumeric: col.role === 'measure' ? 'tabular-nums' : 'normal',
51
+ fontWeight: col.role === 'measure' ? 600 : 400,
52
+ whiteSpace: 'nowrap',
53
+ }, children: formatters[colIdx](row[col.key]) }, col.key))) }, rowIdx))) })] }) }));
54
+ },
55
+ };
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ import type { DatasetDescriptor } from '../schema/descriptor.js';
3
+ import type { Query } from '../schema/query.js';
4
+ import type { Result, VisualizerOpts } from '../types/index.js';
5
+ export interface VisualizerViewProps {
6
+ descriptor: DatasetDescriptor;
7
+ query: Query;
8
+ result: Result;
9
+ opts?: VisualizerOpts;
10
+ }
11
+ export declare function VisualizerView({ descriptor, query, result, opts, }: VisualizerViewProps): React.ReactElement;
12
+ //# sourceMappingURL=visualizer_view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visualizer_view.d.ts","sourceRoot":"","sources":["../../src/visualizers/visualizer_view.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGhE,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,iBAAiB,CAAC;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB;AAED,wBAAgB,cAAc,CAAC,EAC7B,UAAU,EACV,KAAK,EACL,MAAM,EACN,IAAI,GACL,EAAE,mBAAmB,GAAG,KAAK,CAAC,YAAY,CAG1C"}
@@ -0,0 +1,6 @@
1
+ "use client";
2
+ import { selectVisualizer } from '../lib/auto_encode.js';
3
+ export function VisualizerView({ descriptor, query, result, opts, }) {
4
+ const { visualizer, encoding } = selectVisualizer(descriptor, query, result);
5
+ return visualizer.render(result, encoding, opts);
6
+ }
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "hazo_dataviz",
3
+ "version": "0.3.0",
4
+ "description": "Headless data visualization primitives for hazo apps",
5
+ "type": "module",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./dist/index.client.d.ts",
15
+ "import": "./dist/index.client.js"
16
+ },
17
+ "./visualizers": {
18
+ "types": "./dist/visualizers/index.d.ts",
19
+ "import": "./dist/visualizers/index.js"
20
+ },
21
+ "./api": {
22
+ "types": "./dist/api/index.d.ts",
23
+ "import": "./dist/api/index.js"
24
+ },
25
+ "./testing": {
26
+ "types": "./dist/testing/index.d.ts",
27
+ "import": "./dist/testing/index.js"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "CHANGE_LOG.md",
35
+ "SETUP_CHECKLIST.md"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.build.json",
39
+ "type-check": "tsc --noEmit",
40
+ "test": "node --experimental-vm-modules ../node_modules/jest/bin/jest.js --config jest.config.cjs",
41
+ "dev:test-app": "npm run build && cd test-app && npm run dev",
42
+ "build:test-app": "npm run build && cd test-app && npm run build"
43
+ },
44
+ "dependencies": {
45
+ "recharts": "^2.15.4",
46
+ "zod": "^4.1.12"
47
+ },
48
+ "peerDependencies": {
49
+ "hazo_api": "^2.4.0",
50
+ "hazo_connect": "^3.6.0",
51
+ "hazo_core": "^1.1.0",
52
+ "hazo_ui": "^4.0.0",
53
+ "react": "^18.0.0 || ^19.0.0",
54
+ "react-dom": "^18.0.0 || ^19.0.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "hazo_connect": {
58
+ "optional": true
59
+ },
60
+ "hazo_ui": {
61
+ "optional": true
62
+ },
63
+ "hazo_api": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "devDependencies": {
68
+ "@tailwindcss/postcss": "^4.2.4",
69
+ "@testing-library/jest-dom": "^6.0.0",
70
+ "@testing-library/react": "^16.0.0",
71
+ "@types/jest": "^30.0.0",
72
+ "@types/node": "^22.10.0",
73
+ "@types/react": "^19.0.0",
74
+ "@types/react-dom": "^19.0.0",
75
+ "jest": "^30.2.0",
76
+ "jest-environment-jsdom": "^30.2.0",
77
+ "jest-environment-node": "^30.2.0",
78
+ "postcss": "^8.4.49",
79
+ "react": "^19.0.0",
80
+ "react-dom": "^19.0.0",
81
+ "tailwindcss": "^4.2.4",
82
+ "ts-jest": "^29.4.5",
83
+ "typescript": "^5.7.2"
84
+ },
85
+ "keywords": [],
86
+ "author": "Pubs Abayasiri",
87
+ "license": "MIT"
88
+ }