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.
- package/CHANGE_LOG.md +54 -0
- package/README.md +115 -0
- package/SETUP_CHECKLIST.md +54 -0
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +11 -0
- package/dist/charts/line_chart.d.ts +14 -0
- package/dist/charts/line_chart.d.ts.map +1 -0
- package/dist/charts/line_chart.js +26 -0
- package/dist/charts/multi_line_chart.d.ts +13 -0
- package/dist/charts/multi_line_chart.d.ts.map +1 -0
- package/dist/charts/multi_line_chart.js +32 -0
- package/dist/hooks/use_data_query.d.ts +11 -0
- package/dist/hooks/use_data_query.d.ts.map +1 -0
- package/dist/hooks/use_data_query.js +35 -0
- package/dist/index.client.d.ts +17 -0
- package/dist/index.client.d.ts.map +1 -0
- package/dist/index.client.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/lib/auto_encode.d.ts +24 -0
- package/dist/lib/auto_encode.d.ts.map +1 -0
- package/dist/lib/auto_encode.js +46 -0
- package/dist/lib/contract-version.d.ts +2 -0
- package/dist/lib/contract-version.d.ts.map +1 -0
- package/dist/lib/contract-version.js +1 -0
- package/dist/lib/format.d.ts +5 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +31 -0
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/picker.d.ts +4 -0
- package/dist/lib/picker.d.ts.map +1 -0
- package/dist/lib/picker.js +20 -0
- package/dist/lib/runners/in_memory.d.ts +4 -0
- package/dist/lib/runners/in_memory.d.ts.map +1 -0
- package/dist/lib/runners/in_memory.js +303 -0
- package/dist/lib/shape.d.ts +11 -0
- package/dist/lib/shape.d.ts.map +1 -0
- package/dist/lib/shape.js +27 -0
- package/dist/schema/descriptor.d.ts +162 -0
- package/dist/schema/descriptor.d.ts.map +1 -0
- package/dist/schema/descriptor.js +56 -0
- package/dist/schema/query.d.ts +96 -0
- package/dist/schema/query.d.ts.map +1 -0
- package/dist/schema/query.js +37 -0
- package/dist/testing/fixtures.d.ts +163 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +59 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/types/index.d.ts +62 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/visualizers/area.d.ts +3 -0
- package/dist/visualizers/area.d.ts.map +1 -0
- package/dist/visualizers/area.js +66 -0
- package/dist/visualizers/bar.d.ts +3 -0
- package/dist/visualizers/bar.d.ts.map +1 -0
- package/dist/visualizers/bar.js +41 -0
- package/dist/visualizers/builtins.d.ts +3 -0
- package/dist/visualizers/builtins.d.ts.map +1 -0
- package/dist/visualizers/builtins.js +7 -0
- package/dist/visualizers/chart_frame.d.ts +97 -0
- package/dist/visualizers/chart_frame.d.ts.map +1 -0
- package/dist/visualizers/chart_frame.js +146 -0
- package/dist/visualizers/index.d.ts +7 -0
- package/dist/visualizers/index.d.ts.map +1 -0
- package/dist/visualizers/index.js +4 -0
- package/dist/visualizers/line.d.ts +3 -0
- package/dist/visualizers/line.d.ts.map +1 -0
- package/dist/visualizers/line.js +66 -0
- package/dist/visualizers/palette.d.ts +13 -0
- package/dist/visualizers/palette.d.ts.map +1 -0
- package/dist/visualizers/palette.js +24 -0
- package/dist/visualizers/single_stat.d.ts +3 -0
- package/dist/visualizers/single_stat.d.ts.map +1 -0
- package/dist/visualizers/single_stat.js +60 -0
- package/dist/visualizers/stacked_bar.d.ts +3 -0
- package/dist/visualizers/stacked_bar.d.ts.map +1 -0
- package/dist/visualizers/stacked_bar.js +75 -0
- package/dist/visualizers/table.d.ts +3 -0
- package/dist/visualizers/table.d.ts.map +1 -0
- package/dist/visualizers/table.js +55 -0
- package/dist/visualizers/visualizer_view.d.ts +12 -0
- package/dist/visualizers/visualizer_view.d.ts.map +1 -0
- package/dist/visualizers/visualizer_view.js +6 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|