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
package/CHANGE_LOG.md ADDED
@@ -0,0 +1,54 @@
1
+ # hazo_dataviz — Change Log
2
+
3
+ ## 0.3.0 — 2026-06-13 (unreleased)
4
+
5
+ "Studio" visual restyle — a warm, editorial light theme applied to every shipped
6
+ visualizer and to the test-app showroom.
7
+
8
+ - **Shared chart theme** (`visualizers/chart_frame.tsx`): new `CHART_THEME` tokens
9
+ (warm ink/muted/hairline/surface), `axisProps` + `gridProps` (hairline horizontal
10
+ grid, no tick marks), `legendProps`, `CHART_MARGIN`, and a `makeTooltip(labelFmt,
11
+ valueFmt)` factory that renders a warm floating card with a colored swatch + tabular
12
+ value per series. SVG `font-family` is intentionally left unset so charts inherit the
13
+ host page font. `EmptyState` restyled with an icon + "No data to display".
14
+ - **Palette**: `DEFAULT_PALETTE` replaced with the Studio ramp (coral, teal, marigold,
15
+ plum, ocean, olive, rose, amber-brown) — warm/cool alternation so adjacent series stay
16
+ distinguishable; coral + teal lead as the default complementary pair.
17
+ - **Visualizers restyled** to consume the shared theme: `line` (2.5px monotone stroke,
18
+ hover active-dot, custom tooltip), `area` (per-series `<linearGradient>` fills),
19
+ `bar` (rounded `radius`, `maxBarSize`, category gap), `stacked_bar` (rounded top
20
+ segment), `single_stat` (uppercase label, coral accent bar, 52px tabular figure),
21
+ `table` (warm sticky header, hairline rows, right-aligned tabular measures).
22
+ - **stacked_bar.accepts** relaxed: now scores `0.95` for any 2-dimension + ≥1-measure
23
+ query with ≥1 categorical dimension (either dim ordering), so the canonical
24
+ "measure by category over time" shape (temporal x, categorical series) is accepted.
25
+ Previously required `dimensions[0]` itself to be categorical. Jest + autotest aligned.
26
+ - **Test-app "Studio" showroom**: `next/font` wiring (Bricolage Grotesque display +
27
+ Hanken Grotesk body), warm-canvas `globals.css` with CSS-var design tokens and a
28
+ layered radial-glow background, redesigned sidebar (gradient brand mark, active-state
29
+ highlight via `usePathname`), an editorial gallery (eyebrow + display headline,
30
+ per-chart icon cards with hover lift), and a real overview landing page.
31
+
32
+ ## 0.2.0 — 2026-06-13 (unreleased)
33
+
34
+ Build 02: Bespoke chart components + recharts visualizers.
35
+
36
+ - `LineChart` — bespoke single-series line chart (recharts) exported from `./client`; accepts `data: ChartDataSeries`, `dates: string[]`, `color`, optional `width`/`height`/`unit`/`showTooltip`/`className`; null values render as gaps (`connectNulls={false}`); responsive by default (130 px height), fixed-size when `width` is given; empty data renders a placeholder without throwing
37
+ - `MultiLineChart` — bespoke multi-series line chart (recharts) exported from `./client`; accepts `series: MultiSeries[]` (each with `label`/`color`/`data`), `dates`; optional `width`/`height`/`showTooltip`/`showLegend`/`className`; same null-gap + responsive behaviour as `LineChart`; empty/all-null series render a placeholder without throwing
38
+ - New types (`ChartDataPoint`, `ChartDataSeries`, `MultiSeries`) added to `src/types/index.ts` and exported from `./client`
39
+ - `charts_smoke.test.tsx` added (describe.skip — same recharts dual-React ESM limitation as render_smoke.test.tsx; real coverage via test-app)
40
+
41
+ ## 0.1.0 — 2026-06-13 (unreleased)
42
+
43
+ Build 01: Scaffold + Headless Data Foundation.
44
+
45
+ - Package scaffold: ESM, `moduleResolution: node16`, jest (ESM), test-app (Next 16 stub)
46
+ - `DatasetDescriptor` + `Query` Zod schemas with `parseDescriptor` / `parseQuery`; coded `HAZO_DATAVIZ_INVALID_*` errors via `hazo_core.fromZodValidation`
47
+ - `derivePickerOptions(descriptor)` — pure function returning all valid measures/dims/aggs
48
+ - `createInMemoryRunner(descriptor, rows)` — full filter→time-bucket→group→aggregate→sort→limit pipeline; SQL null semantics; ISO-8601 week bucketing; `last` agg
49
+ - `formatValue` / `formatPercent` / `formatCompact` — delegate currency/number/date to `hazo_core`; `en-US` locale pinned for SSR safety
50
+ - `useDataQuery(descriptor, query, runner)` — React hook; loading→success/error; cancel-on-unmount
51
+ - `./testing` export — `prices`, `sector_breakdown`, `sparse`, `badDescriptorInput` fixtures + `createInMemoryRunner` re-export
52
+ - 70 Jest tests, all green; `audit:structure` + `audit:versions` pass
53
+
54
+ **Deferred to next session:** server-SQL runner (blocked: `hazo_connect` has no GROUP BY/aggregate support — candidate FR); Explorer UI / recharts; saved views / DDL.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # hazo_dataviz
2
+
3
+ Headless data-visualization primitives for hazo apps. A descriptor-driven data layer (schema, in-memory runner, React hook, formatters) plus a set of built-in recharts visualizers that auto-select the right chart for a given query shape — all styled with a warm, self-contained **"Studio"** theme out of the box.
4
+
5
+ ## Status
6
+
7
+ **v0.3.0** — in development (not yet published to npm). Data layer + built-in visualizers + auto-selection complete. The server-SQL runner is deferred (blocked on `hazo_connect` GROUP BY support — see FR-003).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install hazo_dataviz hazo_core
13
+ ```
14
+
15
+ Peer deps: `hazo_core` (required); `hazo_connect` / `hazo_ui` / `hazo_api` (optional). The visualizers depend on `recharts` and `react` (recharts is a direct dep; react is a peer).
16
+
17
+ ## Quick start — data layer
18
+
19
+ ```ts
20
+ import {
21
+ parseDescriptor,
22
+ createInMemoryRunner,
23
+ useDataQuery,
24
+ } from 'hazo_dataviz';
25
+
26
+ const descriptor = parseDescriptor({
27
+ name: 'sales',
28
+ version: '1',
29
+ grain: ['date', 'region'],
30
+ fields: {
31
+ date: { role: 'dimension', type: 'time', timeGrains: ['month', 'year'] },
32
+ region: { role: 'dimension', type: 'category' },
33
+ revenue: { role: 'measure', agg: 'sum', format: { kind: 'currency', ccy: 'USD' } },
34
+ },
35
+ });
36
+
37
+ const rows = [/* your data */];
38
+ const runner = createInMemoryRunner(descriptor, rows);
39
+
40
+ // In a React component:
41
+ const { result, status, error } = useDataQuery(descriptor, {
42
+ measures: [{ field: 'revenue' }],
43
+ dimensions: ['date'],
44
+ timeGrain: 'month',
45
+ }, runner);
46
+ ```
47
+
48
+ ## Quick start — auto-selected visualizer
49
+
50
+ `<VisualizerView>` scores every built-in visualizer against the descriptor + query
51
+ and renders the best fit (argmax). It is client-only — import from `hazo_dataviz/visualizers`.
52
+
53
+ ```tsx
54
+ 'use client';
55
+ import { VisualizerView } from 'hazo_dataviz/visualizers';
56
+
57
+ // `result` comes from a runner (e.g. createInMemoryRunner above)
58
+ <VisualizerView descriptor={descriptor} query={query} result={result} opts={{ height: 300 }} />;
59
+ ```
60
+
61
+ Need manual control instead of auto-selection?
62
+
63
+ ```tsx
64
+ import { builtinVisualizers, selectVisualizer, deriveEncoding } from 'hazo_dataviz/visualizers';
65
+
66
+ const { visualizer } = selectVisualizer(descriptor, query, result);
67
+ const encoding = deriveEncoding(query, result);
68
+ const el = visualizer.render(result, encoding, { height: 300, colors: ['#f5563d', '#14a098'] });
69
+ ```
70
+
71
+ ### Built-in visualizers & how they score
72
+
73
+ `accepts(descriptor, query) → number` (0 = can't render, higher = better fit). The winner is the highest score:
74
+
75
+ | id | When it fits | Score |
76
+ |---|---|---|
77
+ | `single_stat` | 0 dimensions + exactly 1 measure | 0.9 |
78
+ | `line` | ≥1 temporal dimension + ≥1 measure | 0.9 |
79
+ | `bar` | exactly 1 categorical dimension + ≥1 measure | 0.9 |
80
+ | `stacked_bar` | 2 dimensions + ≥1 measure + ≥1 categorical dim | 0.95 (categorical x) / 0.6 (temporal x) |
81
+ | `area` | ≥1 temporal dimension + ≥1 measure | 0.5 (never beats `line`) |
82
+ | `table` | anything | 0.1 (universal fallback) |
83
+
84
+ `stacked_bar` is graduated so a multi-series time series (temporal x) still defaults to `line` while a category-by-category split (categorical x) picks the stacked bar.
85
+
86
+ ### Styling (the "Studio" theme)
87
+
88
+ Charts ship pre-styled — warm hairline grid, custom tooltip, rounded bars, gradient
89
+ area fills, tabular figures — with no Tailwind or external CSS required (everything is
90
+ inline SVG/JS so it renders correctly in any consumer). The series palette and theme
91
+ tokens live in `visualizers/palette.ts` and `visualizers/chart_frame.tsx`; override
92
+ colors per-render via `opts.colors`. SVG `font-family` is intentionally left unset so
93
+ charts inherit your app's font.
94
+
95
+ ## Bespoke chart primitives
96
+
97
+ For hand-built charts (not query-driven), `hazo_dataviz/client` exports lightweight
98
+ recharts wrappers: `LineChart` (single series) and `MultiLineChart` (multi series),
99
+ both with null-gap handling and responsive sizing.
100
+
101
+ ## Exports
102
+
103
+ | Entry point | Exports |
104
+ |---|---|
105
+ | `hazo_dataviz` (server) / `hazo_dataviz/client` | `parseDescriptor`, `parseQuery`, `derivePickerOptions`, `createInMemoryRunner`, `useDataQuery`, `formatValue` / `formatPercent` / `formatCompact`, `LineChart`, `MultiLineChart`, `CONTRACT_VERSION`, + types |
106
+ | `hazo_dataviz/visualizers` | `VisualizerView`, `builtinVisualizers`, `selectVisualizer`, `deriveEncoding`, `classifyShape`, + `Visualizer` / `VisualizerOpts` / `QueryShape` types |
107
+ | `hazo_dataviz/testing` | Fixtures (`prices`, `sector_breakdown`, `sparse`) + `createInMemoryRunner` re-export |
108
+ | `hazo_dataviz/api` | HTTP route handler factories (`hazo_api`-based) |
109
+
110
+ `hazo_dataviz/visualizers` and `hazo_dataviz/client` are client-only (`'use client'`); the
111
+ root `hazo_dataviz` import is server-safe and re-exports the client-safe data layer.
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,54 @@
1
+ # hazo_dataviz — Setup Checklist
2
+
3
+ Follow these steps when adding hazo_dataviz to a consuming application.
4
+
5
+ ## 1. Install
6
+
7
+ ```bash
8
+ npm install hazo_dataviz hazo_core recharts
9
+ ```
10
+
11
+ - `hazo_core` is a required peer dependency.
12
+ - `recharts` is required by the built-in visualizers and `./client` chart primitives.
13
+ - `react` / `react-dom` are peer deps (^18 or ^19).
14
+ - `hazo_connect` / `hazo_ui` / `hazo_api` are optional peers — only needed if you use
15
+ the (future) server-SQL runner or the `./api` route handlers.
16
+
17
+ ## 2. Configure
18
+
19
+ Nothing to configure. hazo_dataviz is headless and stateless — there are **no env vars,
20
+ no config files, and no database tables**. The in-memory runner operates on rows you
21
+ pass in; the visualizers are pure given a `Result` + `Encoding`.
22
+
23
+ ## 3. Wire it up
24
+
25
+ 1. Build a `DatasetDescriptor` with `parseDescriptor()` (throws a coded `HazoError` on
26
+ invalid shape).
27
+ 2. Create a runner with `createInMemoryRunner(descriptor, rows)` and query it (or use the
28
+ `useDataQuery` React hook).
29
+ 3. Render results with `<VisualizerView descriptor query result />` (auto-selects the
30
+ chart) from `hazo_dataviz/visualizers`.
31
+
32
+ > **Client boundary:** import visualizers from `hazo_dataviz/visualizers` (or chart
33
+ > primitives from `hazo_dataviz/client`) inside a `'use client'` component. The root
34
+ > `hazo_dataviz` import is server-safe and exposes only the client-safe data layer.
35
+
36
+ ## 4. Verify
37
+
38
+ A quick smoke test using the bundled fixtures (no app data required):
39
+
40
+ ```tsx
41
+ 'use client';
42
+ import { VisualizerView } from 'hazo_dataviz/visualizers';
43
+ import { pricesDescriptor, pricesRows, createInMemoryRunner } from 'hazo_dataviz/testing';
44
+
45
+ const runner = createInMemoryRunner(pricesDescriptor, pricesRows);
46
+ const query = { measures: [{ field: 'close' }], dimensions: ['date', 'symbol'] };
47
+ const result = await runner(query);
48
+
49
+ // Renders a multi-series line chart (the auto-selected fit for a temporal query):
50
+ <VisualizerView descriptor={pricesDescriptor} query={query} result={result} opts={{ height: 300 }} />;
51
+ ```
52
+
53
+ If the chart renders with a warm hairline grid and a coral/teal series palette, the
54
+ package is wired correctly.
@@ -0,0 +1,4 @@
1
+ export declare function createExampleHandler(options: {
2
+ getHazoConnect: () => Promise<unknown> | unknown;
3
+ }): () => Promise<Response>;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAEA,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAAE,cAAc,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;CAAE,2BAQjG"}
@@ -0,0 +1,11 @@
1
+ // hazo_dataviz/src/api/index.ts — HTTP route handler factories.
2
+ // Inject hazo_connect; never import it at module top-level.
3
+ export function createExampleHandler(options) {
4
+ // const adapter = await options.getHazoConnect();
5
+ void options;
6
+ return async function handler() {
7
+ return new Response(JSON.stringify({ ok: true }), {
8
+ headers: { 'content-type': 'application/json' },
9
+ });
10
+ };
11
+ }
@@ -0,0 +1,14 @@
1
+ import * as React from 'react';
2
+ import type { ChartDataSeries } from '../types/index.js';
3
+ export interface LineChartProps {
4
+ data: ChartDataSeries;
5
+ dates: string[];
6
+ color: string;
7
+ width?: number;
8
+ height?: number;
9
+ unit?: string;
10
+ showTooltip?: boolean;
11
+ className?: string;
12
+ }
13
+ export declare function LineChart({ data, dates, color, width, height, unit, showTooltip, className, }: LineChartProps): React.ReactElement;
14
+ //# sourceMappingURL=line_chart.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"line_chart.d.ts","sourceRoot":"","sources":["../../src/charts/line_chart.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAU/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,eAAe,CAAC;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,KAAK,EACL,KAAK,EACL,KAAK,EACL,MAAM,EACN,IAAS,EACT,WAAkB,EAClB,SAAS,GACV,EAAE,cAAc,GAAG,KAAK,CAAC,YAAY,CA2DrC"}
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { LineChart as RechartsLineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from 'recharts';
4
+ export function LineChart({ data, dates, color, width, height, unit = '', showTooltip = true, className, }) {
5
+ // Empty data guard — render a placeholder, never throw
6
+ if (!data || data.length === 0) {
7
+ return (_jsx("div", { className: className, style: {
8
+ display: 'flex',
9
+ alignItems: 'center',
10
+ justifyContent: 'center',
11
+ height: height ?? 130,
12
+ color: '#9ca3af',
13
+ fontSize: 14,
14
+ }, children: "No data" }));
15
+ }
16
+ const chartData = dates.map((d, i) => ({ x: d, value: data[i] ?? null }));
17
+ const chartHeight = height ?? 130;
18
+ const tooltipFormatter = unit
19
+ ? (v) => [`${v}${unit}`, '']
20
+ : undefined;
21
+ const inner = (_jsxs(RechartsLineChart, { data: chartData, width: width !== undefined ? width : undefined, height: width !== undefined ? chartHeight : undefined, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "x" }), _jsx(YAxis, { tickFormatter: (v) => unit ? `${v}${unit}` : String(v) }), showTooltip && _jsx(Tooltip, { formatter: tooltipFormatter }), _jsx(Line, { type: "monotone", dataKey: "value", stroke: color, dot: false, connectNulls: false, isAnimationActive: false })] }));
22
+ if (width !== undefined) {
23
+ return (_jsx("div", { style: { width, height: chartHeight, overflow: 'hidden' }, className: className, children: inner }));
24
+ }
25
+ return (_jsx("div", { className: className, children: _jsx(ResponsiveContainer, { width: "100%", height: chartHeight, children: inner }) }));
26
+ }
@@ -0,0 +1,13 @@
1
+ import * as React from 'react';
2
+ import type { MultiSeries } from '../types/index.js';
3
+ export interface MultiLineChartProps {
4
+ series: MultiSeries[];
5
+ dates: string[];
6
+ width?: number;
7
+ height?: number;
8
+ showTooltip?: boolean;
9
+ showLegend?: boolean;
10
+ className?: string;
11
+ }
12
+ export declare function MultiLineChart({ series, dates, width, height, showTooltip, showLegend, className, }: MultiLineChartProps): React.ReactElement;
13
+ //# sourceMappingURL=multi_line_chart.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multi_line_chart.d.ts","sourceRoot":"","sources":["../../src/charts/multi_line_chart.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAW/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,KAAK,EACL,KAAK,EACL,MAAM,EACN,WAAkB,EAClB,UAAiB,EACjB,SAAS,GACV,EAAE,mBAAmB,GAAG,KAAK,CAAC,YAAY,CAuE1C"}
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { LineChart as RechartsLineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts';
4
+ export function MultiLineChart({ series, dates, width, height, showTooltip = true, showLegend = true, className, }) {
5
+ const chartHeight = height ?? 130;
6
+ // Empty series or all-empty guard — render a placeholder, never throw
7
+ const hasData = series.length > 0 &&
8
+ series.some((s) => s.data.some((v) => v !== null));
9
+ if (!hasData) {
10
+ return (_jsx("div", { className: className, style: {
11
+ display: 'flex',
12
+ alignItems: 'center',
13
+ justifyContent: 'center',
14
+ height: chartHeight,
15
+ color: '#9ca3af',
16
+ fontSize: 14,
17
+ }, children: "No data" }));
18
+ }
19
+ // Build wide rows: { x: date, [seriesLabel]: value | null, ... }
20
+ const chartData = dates.map((d, i) => {
21
+ const row = { x: d };
22
+ for (const s of series) {
23
+ row[s.label] = s.data[i] ?? null;
24
+ }
25
+ return row;
26
+ });
27
+ const inner = (_jsxs(RechartsLineChart, { data: chartData, width: width !== undefined ? width : undefined, height: width !== undefined ? chartHeight : undefined, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "x" }), _jsx(YAxis, {}), showTooltip && _jsx(Tooltip, {}), showLegend && _jsx(Legend, {}), series.map((s) => (_jsx(Line, { type: "monotone", dataKey: s.label, stroke: s.color, dot: false, connectNulls: false, isAnimationActive: false }, s.label)))] }));
28
+ if (width !== undefined) {
29
+ return (_jsx("div", { style: { width, height: chartHeight, overflow: 'hidden' }, className: className, children: inner }));
30
+ }
31
+ return (_jsx("div", { className: className, children: _jsx(ResponsiveContainer, { width: "100%", height: chartHeight, children: inner }) }));
32
+ }
@@ -0,0 +1,11 @@
1
+ import type { DatasetDescriptor } from '../schema/descriptor.js';
2
+ import type { Query } from '../schema/query.js';
3
+ import type { Result, QueryRunner } from '../types/index.js';
4
+ export type QueryStatus = 'idle' | 'loading' | 'success' | 'error';
5
+ export interface UseDataQueryResult {
6
+ result: Result | undefined;
7
+ status: QueryStatus;
8
+ error: unknown;
9
+ }
10
+ export declare function useDataQuery(descriptor: DatasetDescriptor, query: Query, runner: QueryRunner): UseDataQueryResult;
11
+ //# sourceMappingURL=use_data_query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use_data_query.d.ts","sourceRoot":"","sources":["../../src/hooks/use_data_query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE7D,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnE,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,wBAAgB,YAAY,CAC1B,UAAU,EAAE,iBAAiB,EAC7B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,WAAW,GAClB,kBAAkB,CAqCpB"}
@@ -0,0 +1,35 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ export function useDataQuery(descriptor, query, runner) {
3
+ const [result, setResult] = useState(undefined);
4
+ const [status, setStatus] = useState('idle');
5
+ const [error, setError] = useState(undefined);
6
+ // Use a ref to track abort between renders
7
+ const cancelledRef = useRef(false);
8
+ // Serialize query + descriptor for dependency comparison
9
+ const queryKey = JSON.stringify(query);
10
+ const descriptorKey = descriptor.name + '@' + descriptor.version;
11
+ useEffect(() => {
12
+ cancelledRef.current = false;
13
+ setStatus('loading');
14
+ setResult(undefined);
15
+ setError(undefined);
16
+ runner(query)
17
+ .then((res) => {
18
+ if (cancelledRef.current)
19
+ return;
20
+ setResult(res);
21
+ setStatus('success');
22
+ })
23
+ .catch((err) => {
24
+ if (cancelledRef.current)
25
+ return;
26
+ setError(err);
27
+ setStatus('error');
28
+ });
29
+ return () => {
30
+ cancelledRef.current = true;
31
+ };
32
+ // eslint-disable-next-line react-hooks/exhaustive-deps
33
+ }, [queryKey, descriptorKey, runner]);
34
+ return { result, status, error };
35
+ }
@@ -0,0 +1,17 @@
1
+ export { CONTRACT_VERSION } from './lib/contract-version.js';
2
+ export { parseDescriptor } from './schema/descriptor.js';
3
+ export { parseQuery } from './schema/query.js';
4
+ export { derivePickerOptions } from './lib/picker.js';
5
+ export { createInMemoryRunner } from './lib/runners/in_memory.js';
6
+ export { formatValue, formatPercent, formatCompact } from './lib/format.js';
7
+ export { useDataQuery } from './hooks/use_data_query.js';
8
+ export type { DatasetDescriptor, Field, Agg, TimeGrain, Format, JoinSpec, } from './schema/descriptor.js';
9
+ export type { Query, MeasureRef, Filter, FilterOp, Sort } from './schema/query.js';
10
+ export type { Result, Column, Encoding, QueryRunner, PickerOptions } from './types/index.js';
11
+ export type { QueryStatus, UseDataQueryResult } from './hooks/use_data_query.js';
12
+ export { LineChart } from './charts/line_chart.js';
13
+ export { MultiLineChart } from './charts/multi_line_chart.js';
14
+ export type { LineChartProps } from './charts/line_chart.js';
15
+ export type { MultiLineChartProps } from './charts/multi_line_chart.js';
16
+ export type { ChartDataPoint, ChartDataSeries, MultiSeries } from './types/index.js';
17
+ //# sourceMappingURL=index.client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.client.d.ts","sourceRoot":"","sources":["../src/index.client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,YAAY,EACV,iBAAiB,EACjB,KAAK,EACL,GAAG,EACH,SAAS,EACT,MAAM,EACN,QAAQ,GACT,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACnF,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC7F,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAGjF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACxE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,11 @@
1
+ // Client-safe entry — no Node.js builtins.
2
+ export { CONTRACT_VERSION } from './lib/contract-version.js';
3
+ export { parseDescriptor } from './schema/descriptor.js';
4
+ export { parseQuery } from './schema/query.js';
5
+ export { derivePickerOptions } from './lib/picker.js';
6
+ export { createInMemoryRunner } from './lib/runners/in_memory.js';
7
+ export { formatValue, formatPercent, formatCompact } from './lib/format.js';
8
+ export { useDataQuery } from './hooks/use_data_query.js';
9
+ // Bespoke chart components (recharts-based)
10
+ export { LineChart } from './charts/line_chart.js';
11
+ export { MultiLineChart } from './charts/multi_line_chart.js';
@@ -0,0 +1,2 @@
1
+ export * from './index.client.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // Server entry — re-exports client surface and will add server-only exports later.
2
+ export * from './index.client.js';
@@ -0,0 +1,24 @@
1
+ import type { DatasetDescriptor } from '../schema/descriptor.js';
2
+ import type { Query } from '../schema/query.js';
3
+ import type { Result, Encoding, Visualizer } from '../types/index.js';
4
+ /**
5
+ * Derives an Encoding from a query and result.
6
+ * - x = query.dimensions[0] (or '' if none)
7
+ * - series = query.dimensions[1] if present
8
+ * - y = measure column keys from result.columns where role==='measure' (preserve order)
9
+ *
10
+ * When result is undefined, derive from query alone.
11
+ */
12
+ export declare function deriveEncoding(query: Query, result?: Result): Encoding;
13
+ export interface SelectVisualizerResult {
14
+ visualizer: Visualizer;
15
+ encoding: Encoding;
16
+ candidates: Visualizer[];
17
+ }
18
+ /**
19
+ * Score every builtin visualizer and return the best match with encoding.
20
+ * candidates = those with score > 0 sorted descending (stable; on tie keep builtins order).
21
+ * visualizer = top candidate (guaranteed non-empty because table always scores 0.1).
22
+ */
23
+ export declare function selectVisualizer(descriptor: DatasetDescriptor, query: Query, result?: Result): SelectVisualizerResult;
24
+ //# sourceMappingURL=auto_encode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto_encode.d.ts","sourceRoot":"","sources":["../../src/lib/auto_encode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGtE;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAYtE;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,UAAU,EAAE,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,iBAAiB,EAC7B,KAAK,EAAE,KAAK,EACZ,MAAM,CAAC,EAAE,MAAM,GACd,sBAAsB,CAuBxB"}
@@ -0,0 +1,46 @@
1
+ import { builtinVisualizers } from '../visualizers/builtins.js';
2
+ /**
3
+ * Derives an Encoding from a query and result.
4
+ * - x = query.dimensions[0] (or '' if none)
5
+ * - series = query.dimensions[1] if present
6
+ * - y = measure column keys from result.columns where role==='measure' (preserve order)
7
+ *
8
+ * When result is undefined, derive from query alone.
9
+ */
10
+ export function deriveEncoding(query, result) {
11
+ const x = query.dimensions[0] ?? '';
12
+ const series = query.dimensions.length >= 2 ? query.dimensions[1] : undefined;
13
+ let y;
14
+ if (result) {
15
+ y = result.columns.filter((c) => c.role === 'measure').map((c) => c.key);
16
+ }
17
+ else {
18
+ y = query.measures.map((m) => m.field);
19
+ }
20
+ return { x, series, y };
21
+ }
22
+ /**
23
+ * Score every builtin visualizer and return the best match with encoding.
24
+ * candidates = those with score > 0 sorted descending (stable; on tie keep builtins order).
25
+ * visualizer = top candidate (guaranteed non-empty because table always scores 0.1).
26
+ */
27
+ export function selectVisualizer(descriptor, query, result) {
28
+ const scored = builtinVisualizers.map((v, idx) => ({
29
+ visualizer: v,
30
+ score: v.accepts(descriptor, query, result),
31
+ idx,
32
+ }));
33
+ // Keep only those with score > 0
34
+ const positive = scored.filter((s) => s.score > 0);
35
+ // Stable sort descending by score (preserve original order on tie)
36
+ positive.sort((a, b) => {
37
+ if (b.score !== a.score)
38
+ return b.score - a.score;
39
+ return a.idx - b.idx;
40
+ });
41
+ // Table always scores 0.1 so candidates is never empty
42
+ const candidates = positive.map((s) => s.visualizer);
43
+ const visualizer = candidates[0];
44
+ const encoding = deriveEncoding(query, result);
45
+ return { visualizer, encoding, candidates };
46
+ }
@@ -0,0 +1,2 @@
1
+ export declare const CONTRACT_VERSION: "0.1";
2
+ //# sourceMappingURL=contract-version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-version.d.ts","sourceRoot":"","sources":["../../src/lib/contract-version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,EAAG,KAAc,CAAC"}
@@ -0,0 +1 @@
1
+ export const CONTRACT_VERSION = '0.1';
@@ -0,0 +1,5 @@
1
+ import type { Format } from '../schema/descriptor.js';
2
+ export declare function formatValue(value: unknown, format: Format): string;
3
+ export declare function formatPercent(value: number, decimals?: number): string;
4
+ export declare function formatCompact(value: number): string;
5
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/lib/format.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAItD,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAclE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAMjE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnD"}
@@ -0,0 +1,31 @@
1
+ import { formatCurrency, formatNumber, format_date } from 'hazo_core';
2
+ const LOCALE = 'en-US';
3
+ export function formatValue(value, format) {
4
+ if (value === null || value === undefined)
5
+ return '';
6
+ switch (format.kind) {
7
+ case 'currency':
8
+ return formatCurrency(Number(value), format.ccy);
9
+ case 'number':
10
+ return formatNumber(Number(value), format.decimals);
11
+ case 'percent':
12
+ return formatPercent(Number(value));
13
+ case 'compact':
14
+ return formatCompact(Number(value));
15
+ case 'date':
16
+ return format_date(String(value), format.dateFormat ?? 'dmy');
17
+ }
18
+ }
19
+ export function formatPercent(value, decimals = 2) {
20
+ return new Intl.NumberFormat(LOCALE, {
21
+ style: 'percent',
22
+ minimumFractionDigits: decimals,
23
+ maximumFractionDigits: decimals,
24
+ }).format(value / 100);
25
+ }
26
+ export function formatCompact(value) {
27
+ return new Intl.NumberFormat(LOCALE, {
28
+ notation: 'compact',
29
+ maximumFractionDigits: 1,
30
+ }).format(value);
31
+ }
@@ -0,0 +1,3 @@
1
+ export { CONTRACT_VERSION } from './contract-version.js';
2
+ export { derivePickerOptions } from './picker.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { CONTRACT_VERSION } from './contract-version.js';
2
+ export { derivePickerOptions } from './picker.js';
@@ -0,0 +1,4 @@
1
+ import type { DatasetDescriptor } from '../schema/descriptor.js';
2
+ import type { PickerOptions } from '../types/index.js';
3
+ export declare function derivePickerOptions(descriptor: DatasetDescriptor): PickerOptions;
4
+ //# sourceMappingURL=picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../src/lib/picker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAO,MAAM,yBAAyB,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,iBAAiB,GAAG,aAAa,CAmBhF"}
@@ -0,0 +1,20 @@
1
+ const ALL_AGGS = ['sum', 'avg', 'min', 'max', 'last', 'count'];
2
+ export function derivePickerOptions(descriptor) {
3
+ const measures = [];
4
+ const dimensions = [];
5
+ const filterableFields = [];
6
+ for (const [field, def] of Object.entries(descriptor.fields)) {
7
+ filterableFields.push(field);
8
+ if (def.role === 'measure') {
9
+ measures.push({ field, aggs: ALL_AGGS });
10
+ }
11
+ else {
12
+ dimensions.push({
13
+ field,
14
+ type: def.type,
15
+ timeGrains: def.timeGrains,
16
+ });
17
+ }
18
+ }
19
+ return { measures, dimensions, filterableFields };
20
+ }
@@ -0,0 +1,4 @@
1
+ import type { DatasetDescriptor } from '../../schema/descriptor.js';
2
+ import type { QueryRunner } from '../../types/index.js';
3
+ export declare function createInMemoryRunner(descriptor: DatasetDescriptor, rows: Record<string, unknown>[]): QueryRunner;
4
+ //# sourceMappingURL=in_memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in_memory.d.ts","sourceRoot":"","sources":["../../../src/lib/runners/in_memory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAkB,MAAM,4BAA4B,CAAC;AAEpF,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAmGxE,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,iBAAiB,EAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAC9B,WAAW,CAkMb"}