-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathchart.zod.ts
More file actions
324 lines (281 loc) · 12.2 KB
/
Copy pathchart.zod.ts
File metadata and controls
324 lines (281 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod';
/**
* Unified Chart Type Taxonomy
*
* Shared by Dashboard and Report widgets.
* Provides a comprehensive set of chart types for data visualization.
*/
/**
* Chart Type Enum
* Categorized by visualization purpose
*/
import { lazySchema } from '../shared/lazy-schema';
export const ChartTypeSchema = lazySchema(() => z.enum([
// Comparison
'bar',
'horizontal-bar',
'column',
// Trend
'line',
'area',
// Distribution
'pie',
'donut',
'funnel',
// Relationship
'scatter',
// Composition
'treemap',
'sankey',
// Performance (single value — metric/kpi render a number; gauge/solid-gauge/
// bullet are honest single-value variants pending a real dial/target renderer)
'gauge',
'solid-gauge',
'metric',
'kpi',
'bullet',
// Advanced
'radar',
// Tabular
'table',
'pivot',
]));
// NOTE: the taxonomy lists only chart families the default Recharts renderer
// draws DISTINCTLY. Two groups are intentionally absent:
//
// 1. Families requiring data/dependencies the platform does not model — OHLC
// (candlestick/stock), per-record distributions (box-plot/violin), geo
// (choropleth/bubble-map/gl-map), or extra renderers (sunburst, heatmap,
// word-cloud, waterfall).
// 2. VARIANTS that only render as their base chart, so advertising them lies
// about the output: bi-polar-bar (→ bar), step-line / spline (→ line),
// pyramid (→ funnel), bubble (→ scatter, no size encoding).
//
// Both can return via an opt-in renderer once there is a real renderer and a
// data model to back them.
//
// Grouped/stacked bar and stacked area are absent for a DIFFERENT reason, and
// a better one: stacking is not a chart family, it is a property of the series
// (`ChartSeries.stack` — series sharing a group id stack, otherwise they
// group). One `bar` family plus a series-level stack group expresses all three
// without multiplying the taxonomy. The renderer honors it (objectui#2880);
// before that it did not, which is why they once sat in the list above.
//
// `metric`/`kpi` are kept as honest single-value synonyms; `gauge`/
// `solid-gauge`/`bullet` render a value today and gain a dial when a gauge
// renderer lands.
export type ChartType = z.infer<typeof ChartTypeSchema>;
/**
* Chart Axis Schema
* Definition for X and Y axes
*/
export const ChartAxisSchema = lazySchema(() => z.object({
/** Data field to map to this axis */
field: z.string().describe('Data field key'),
/** Axis title */
title: I18nLabelSchema.optional().describe('Axis display title'),
/** Value formatting (d3-format or similar) */
format: z.string().optional().describe('Value format string (e.g., "$0,0.00")'),
/** Axis scale settings */
min: z.number().optional().describe('Minimum value'),
max: z.number().optional().describe('Maximum value'),
stepSize: z.number().optional().describe('Step size for ticks'),
/** Appearance */
showGridLines: z.boolean().default(true),
position: z.enum(['left', 'right', 'top', 'bottom']).optional().describe('Axis position'),
/** Logarithmic scale */
logarithmic: z.boolean().default(false),
}));
/**
* Chart Series Schema
* Defines a single data series in the chart
*/
export const ChartSeriesSchema = lazySchema(() => z.object({
/** Field name for values */
name: z.string().describe('Field name or series identifier'),
/** Display label */
label: I18nLabelSchema.optional().describe('Series display label'),
/** Series type override (combo charts) */
type: ChartTypeSchema.optional().describe('Override chart type for this series'),
/** Specific color */
color: z.string().optional().describe('Series color (hex/rgb/token)'),
/** Stacking group */
stack: z.string().optional().describe('Stack identifier to group series'),
/** Axis binding */
yAxis: z.enum(['left', 'right']).default('left').describe('Bind to specific Y-Axis'),
/**
* Series role.
*
* - `'primary'` (default) — normal styling using the chart palette.
* - `'comparison'` — secondary period-over-period overlay; renderers
* render it muted (lower opacity, dashed stroke for line/area,
* lighter fill for bars) so it visually backgrounds against the
* primary series. Pair with `DashboardWidget.compareTo` on data-
* bound charts; for hand-authored series, set it directly.
*/
variant: z.enum(['primary', 'comparison']).default('primary').optional().describe('Series visual role'),
/** Override stroke dash pattern (e.g. "4 4" for dashed lines). */
dashArray: z.string().optional().describe('SVG stroke-dasharray override'),
/** Override series opacity (0–1). */
opacity: z.number().min(0).max(1).optional().describe('Series opacity override'),
}));
/**
* Chart Annotation Schema
* Static lines or regions to highlight data
*/
export const ChartAnnotationSchema = lazySchema(() => z.object({
type: z.enum(['line', 'region']).default('line'),
axis: z.enum(['x', 'y']).default('y'),
value: z.union([z.number(), z.string()]).describe('Start value'),
endValue: z.union([z.number(), z.string()]).optional().describe('End value for regions'),
color: z.string().optional(),
label: I18nLabelSchema.optional(),
style: z.enum(['solid', 'dashed', 'dotted']).default('dashed'),
}));
/**
* Chart Interaction Schema
*
* Both toggles are honored by the renderer. Two former members were removed in
* #3752 rather than left declared-but-inert (ADR-0078; the #1475 trim-vs-
* implement call), because each was redundant against something the platform
* already delivers:
*
* * `zoom` — no renderer had a zoom primitive behind it, and `brush` already
* narrows a range. Migration: `brush: true`.
* * `clickAction` — a chart segment click already has two owners that DO
* work: `drillDown` (opens the filtered records, which is what a segment
* click is almost always for) and, in the react tier, the host's own
* `onSegmentClick`. A third, silent one only invited authors to wire a
* click that never fired. Migration: `drillDown`, or handle it in React.
*/
export const ChartInteractionSchema = lazySchema(() => z.object({
tooltips: z.boolean().default(true).describe('Show the hover tooltip'),
brush: z.boolean().default(false).describe('Show the range selector under the plot'),
}));
/**
* Chart Configuration Base
* Common configuration for all chart types
*/
export const ChartConfigSchema = lazySchema(() => z.object({
/** Chart Type */
type: ChartTypeSchema,
/** Titles */
title: I18nLabelSchema.optional().describe('Chart title'),
subtitle: I18nLabelSchema.optional().describe('Chart subtitle'),
description: I18nLabelSchema.optional().describe('Accessibility description — announced to screen readers as the chart’s label'),
/** Axes Mapping */
xAxis: ChartAxisSchema.optional().describe('X-Axis configuration'),
yAxis: z.array(ChartAxisSchema).optional().describe('Y-Axis configuration (support dual axis)'),
/** Series Configuration */
series: z.array(ChartSeriesSchema).optional().describe('Defined series configuration'),
/** Appearance. Either a positional palette (string[]) applied per category in
* order, or a value→color map ({ value: color }, kanban-style). A value→color
* map — and a select/lookup dimension's option colors — take precedence over
* the positional palette per category, so semantic charts (health, status)
* paint their own colors instead of the generic palette. */
colors: z.union([
z.array(z.string()),
z.record(z.string(), z.string()),
]).optional().describe('Color palette (string[]) or value→color map ({ value: color })'),
height: z.number().optional().describe('Fixed plot height in pixels (overrides the container default)'),
/** Components */
showLegend: z.boolean().default(true).describe('Display legend'),
showDataLabels: z.boolean().default(false).describe('Display data labels'),
/** Annotations & Reference Lines */
annotations: z.array(ChartAnnotationSchema).optional()
.describe('Reference lines/bands drawn over the plot: { type: "line" | "region", axis: "x" | "y", value, endValue?, color?, label?, style? }'),
/** Interactions */
interaction: ChartInteractionSchema.optional()
.describe('Interaction toggles: { tooltips?, brush? }'),
/** ARIA accessibility attributes */
aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'),
}));
/**
* Object-bound chart aggregation
*
* A chart binds its data one of two ways: DATASET-bound (ADR-0021 — `dataset`
* + `dimensions` + `values`), or OBJECT-bound (`objectName` + the inline
* `aggregate` below, run as one ad-hoc `IDataEngine.aggregate()` call).
*
* The two key their result rows DIFFERENTLY, and that difference is the usual
* cause of a chart that draws axes and no data. `./chart-aggregate.ts` records
* the object-bound rule and exports the helpers that derive the columns
* (`chartAggregateResultKeys`) — read it before binding an axis.
*/
/**
* Aggregation functions an object-bound chart may ask for.
*
* A deliberate subset of the engine's `AggregationFunction`: these are the five
* the chart renderers implement in every path, including the client-side
* fallback. `count_distinct` / `array_agg` / `string_agg` are engine-level
* capabilities with no chart renderer behind them — advertising them here would
* be a declared-but-not-delivered claim (Prime Directive #10).
*/
export const ChartAggregateFunctionSchema = lazySchema(() =>
z.enum(['count', 'sum', 'avg', 'min', 'max']),
);
/**
* What the rows are grouped by — the chart's category axis.
*
* Either a bare field name, or the structured node the date-bucketing engine
* takes (`{ field, dateGranularity }`, see `data/query.zod.ts`
* `GroupByNodeSchema`). Mirrored rather than imported so the UI protocol does
* not depend on the query AST module; the two shapes are kept identical on
* purpose — the structured form is passed straight through to
* `IDataEngine.aggregate()`.
*/
export const ChartGroupBySchema = lazySchema(() =>
z.union([
z.string().describe('Field to group by'),
z.object({
field: z.string().describe('Field to group by'),
dateGranularity: z
.enum(['day', 'week', 'month', 'quarter', 'year'])
.optional()
.describe('Bucket date values into uniform periods'),
alias: z
.string()
.optional()
.describe('Alias for the projected group value (defaults to `field`) — this becomes the category column'),
}),
]),
);
/**
* Inline aggregation for an OBJECT-bound chart (`objectName` + `aggregate`).
*
* `field` is optional only because `count` counts rows rather than a column;
* every other function needs something to aggregate, which the refinement
* enforces rather than leaving to a renderer to shrug off (it used to reach the
* renderer as `sum(undefined)` and render blank).
*/
export const ChartAggregateSchema = lazySchema(() =>
z
.object({
field: z
.string()
.optional()
.describe('Field to aggregate — required for sum/avg/min/max, optional for count'),
function: ChartAggregateFunctionSchema.describe('Aggregation function'),
groupBy: ChartGroupBySchema.describe('Field the rows are grouped by — the chart category axis'),
})
.superRefine((agg, ctx) => {
if (agg.function !== 'count' && !agg.field) {
ctx.addIssue({
code: 'custom',
path: ['field'],
message: `aggregate.function "${agg.function}" needs a "field" to aggregate (only "count" may omit it).`,
});
}
})
.describe('Inline aggregation for an object-bound chart'),
);
export type ChartConfig = z.infer<typeof ChartConfigSchema>;
export type ChartAggregate = z.infer<typeof ChartAggregateSchema>;
export type ChartAggregateFunction = z.infer<typeof ChartAggregateFunctionSchema>;
export type ChartGroupBy = z.infer<typeof ChartGroupBySchema>;
export type ChartAxis = z.infer<typeof ChartAxisSchema>;
export type ChartSeries = z.infer<typeof ChartSeriesSchema>;
export type ChartAnnotation = z.infer<typeof ChartAnnotationSchema>;
export type ChartInteraction = z.infer<typeof ChartInteractionSchema>;