@matthieumordrel/chart-studio 0.3.0 → 0.5.2

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 (94) hide show
  1. package/README.md +10 -378
  2. package/dist/_internal.d.mts +9 -0
  3. package/dist/_internal.mjs +9 -0
  4. package/dist/core/chart-builder-controls.mjs +141 -0
  5. package/dist/core/chart-capabilities.d.mts +5 -0
  6. package/dist/core/chart-capabilities.mjs +9 -0
  7. package/dist/core/config-utils.mjs +2 -1
  8. package/dist/core/dashboard.types.d.mts +220 -0
  9. package/dist/core/data-label-defaults.d.mts +92 -0
  10. package/dist/core/data-label-defaults.mjs +78 -0
  11. package/dist/core/data-model.types.d.mts +196 -0
  12. package/dist/core/dataset-builder.types.d.mts +51 -0
  13. package/dist/core/dataset-chart-metadata.d.mts +8 -0
  14. package/dist/core/dataset-chart-metadata.mjs +4 -0
  15. package/dist/core/date-range-presets.d.mts +43 -1
  16. package/dist/core/date-range-presets.mjs +2 -2
  17. package/dist/core/date-utils.d.mts +26 -0
  18. package/dist/core/define-dashboard.d.mts +8 -0
  19. package/dist/core/define-dashboard.mjs +156 -0
  20. package/dist/core/define-data-model.d.mts +11 -0
  21. package/dist/core/define-data-model.mjs +327 -0
  22. package/dist/core/define-dataset.d.mts +13 -0
  23. package/dist/core/define-dataset.mjs +111 -0
  24. package/dist/core/formatting.d.mts +49 -0
  25. package/dist/core/formatting.mjs +32 -10
  26. package/dist/core/index.d.mts +19 -0
  27. package/dist/core/infer-columns.mjs +28 -2
  28. package/dist/core/materialized-view.mjs +580 -0
  29. package/dist/core/materialized-view.types.d.mts +223 -0
  30. package/dist/core/metric-utils.d.mts +18 -2
  31. package/dist/core/metric-utils.mjs +1 -1
  32. package/dist/core/model-chart.mjs +242 -0
  33. package/dist/core/model-chart.types.d.mts +199 -0
  34. package/dist/core/model-inference.mjs +169 -0
  35. package/dist/core/model-inference.types.d.mts +71 -0
  36. package/dist/core/pipeline.mjs +32 -1
  37. package/dist/core/schema-builder.mjs +28 -158
  38. package/dist/core/schema-builder.types.d.mts +2 -49
  39. package/dist/core/types.d.mts +61 -10
  40. package/dist/core/use-chart-options.d.mts +35 -8
  41. package/dist/core/use-chart-resolvers.mjs +13 -3
  42. package/dist/core/use-chart.d.mts +16 -12
  43. package/dist/core/use-chart.mjs +137 -35
  44. package/dist/core/use-dashboard.d.mts +190 -0
  45. package/dist/core/use-dashboard.mjs +551 -0
  46. package/dist/index.d.mts +14 -4
  47. package/dist/index.mjs +8 -2
  48. package/package.json +10 -41
  49. package/LICENSE +0 -21
  50. package/dist/core/define-chart-schema.d.mts +0 -38
  51. package/dist/core/define-chart-schema.mjs +0 -39
  52. package/dist/ui/chart-axis-ticks.mjs +0 -65
  53. package/dist/ui/chart-canvas.d.mts +0 -33
  54. package/dist/ui/chart-canvas.mjs +0 -779
  55. package/dist/ui/chart-context.d.mts +0 -99
  56. package/dist/ui/chart-context.mjs +0 -115
  57. package/dist/ui/chart-date-range-badge.d.mts +0 -20
  58. package/dist/ui/chart-date-range-badge.mjs +0 -49
  59. package/dist/ui/chart-date-range-panel.d.mts +0 -18
  60. package/dist/ui/chart-date-range-panel.mjs +0 -126
  61. package/dist/ui/chart-date-range.d.mts +0 -20
  62. package/dist/ui/chart-date-range.mjs +0 -67
  63. package/dist/ui/chart-debug.d.mts +0 -21
  64. package/dist/ui/chart-debug.mjs +0 -173
  65. package/dist/ui/chart-dropdown.mjs +0 -92
  66. package/dist/ui/chart-filters-panel.d.mts +0 -26
  67. package/dist/ui/chart-filters-panel.mjs +0 -132
  68. package/dist/ui/chart-filters.d.mts +0 -18
  69. package/dist/ui/chart-filters.mjs +0 -48
  70. package/dist/ui/chart-group-by-selector.d.mts +0 -16
  71. package/dist/ui/chart-group-by-selector.mjs +0 -32
  72. package/dist/ui/chart-metric-panel.d.mts +0 -25
  73. package/dist/ui/chart-metric-panel.mjs +0 -172
  74. package/dist/ui/chart-metric-selector.d.mts +0 -16
  75. package/dist/ui/chart-metric-selector.mjs +0 -50
  76. package/dist/ui/chart-select.mjs +0 -61
  77. package/dist/ui/chart-source-switcher.d.mts +0 -24
  78. package/dist/ui/chart-source-switcher.mjs +0 -56
  79. package/dist/ui/chart-time-bucket-selector.d.mts +0 -17
  80. package/dist/ui/chart-time-bucket-selector.mjs +0 -37
  81. package/dist/ui/chart-toolbar-overflow.d.mts +0 -28
  82. package/dist/ui/chart-toolbar-overflow.mjs +0 -231
  83. package/dist/ui/chart-toolbar.d.mts +0 -33
  84. package/dist/ui/chart-toolbar.mjs +0 -60
  85. package/dist/ui/chart-type-selector.d.mts +0 -19
  86. package/dist/ui/chart-type-selector.mjs +0 -168
  87. package/dist/ui/chart-x-axis-selector.d.mts +0 -16
  88. package/dist/ui/chart-x-axis-selector.mjs +0 -28
  89. package/dist/ui/index.d.mts +0 -19
  90. package/dist/ui/index.mjs +0 -18
  91. package/dist/ui/percent-stacked.mjs +0 -36
  92. package/dist/ui/theme.css +0 -67
  93. package/dist/ui/toolbar-types.d.mts +0 -7
  94. package/dist/ui/toolbar-types.mjs +0 -83
@@ -0,0 +1,223 @@
1
+ import { ResolvedColumnIdFromSchema } from "./types.mjs";
2
+ import { DatasetChartBuilder, DatasetColumns, DatasetKeyIds, DatasetRow, DefinedDataset, SingleDatasetKeyId } from "./dataset-builder.types.mjs";
3
+ import { AnyDefinedDataModel, DefinedDataModel, ModelAssociationDefinition, ModelAttributeDefinition, ModelDatasetId, ModelDatasets, ModelRelationshipDefinition } from "./data-model.types.mjs";
4
+
5
+ //#region src/core/materialized-view.types.d.ts
6
+ /**
7
+ * Phase 7 materialized-view types.
8
+ *
9
+ * These types keep cross-dataset chart grains explicit:
10
+ * a chart still executes against one flat row shape, but that row shape may now
11
+ * come from a reusable model-derived view when the author opts into
12
+ * materialization.
13
+ */
14
+ type Simplify<T> = { [TKey in keyof T]: T[TKey] } & {};
15
+ type UniqueId<TId extends string, TExisting extends string> = TId extends TExisting ? never : TId;
16
+ type MergeColumns<TLeft extends Record<string, unknown> | undefined, TRight extends Record<string, unknown>> = [TLeft] extends [undefined] ? TRight : Simplify<TLeft & TRight>;
17
+ type DeclaredDatasetColumns<TDataset> = Extract<DatasetColumns<TDataset>, Record<string, unknown>>;
18
+ type DeclaredVisibleDatasetColumnId<TDataset> = Extract<{ [TColumnId in keyof DeclaredDatasetColumns<TDataset>]: DeclaredDatasetColumns<TDataset>[TColumnId] extends false ? never : TColumnId }[keyof DeclaredDatasetColumns<TDataset>], string>;
19
+ /**
20
+ * Column IDs that a materialized view may project from one linked dataset.
21
+ *
22
+ * This reuses the dataset's own chart-visible column contract, so lookup and
23
+ * expansion steps stay strongly typed instead of falling back to loose strings.
24
+ */
25
+ type MaterializedProjectableColumnId<TDataset> = ResolvedColumnIdFromSchema<DatasetRow<TDataset>, TDataset extends {
26
+ columns?: Record<string, unknown> | undefined;
27
+ } ? TDataset : undefined>;
28
+ type DefaultLookupProjectionColumnId<TDataset> = Exclude<DeclaredVisibleDatasetColumnId<TDataset>, SingleDatasetKeyId<TDataset>>;
29
+ type DefaultExpandedProjectionColumnId<TDataset> = DeclaredVisibleDatasetColumnId<TDataset> | SingleDatasetKeyId<TDataset>;
30
+ type EffectiveProjectionColumnId<TDataset, TColumns extends readonly string[] | undefined, TIncludeKey extends boolean> = [TColumns] extends [readonly string[]] ? TColumns[number] | (TIncludeKey extends true ? SingleDatasetKeyId<TDataset> : never) : TIncludeKey extends true ? DefaultExpandedProjectionColumnId<TDataset> : DefaultLookupProjectionColumnId<TDataset>;
31
+ type DeclaredEffectiveProjectionColumnId<TDataset, TColumns extends readonly string[] | undefined, TIncludeKey extends boolean> = Extract<EffectiveProjectionColumnId<TDataset, TColumns, TIncludeKey>, DeclaredVisibleDatasetColumnId<TDataset>>;
32
+ type ProjectedColumnHintFromEntry<TEntry> = TEntry extends {
33
+ kind: 'derived';
34
+ type: infer TType extends string;
35
+ } ? Simplify<{
36
+ type: TType;
37
+ } & (TEntry extends {
38
+ format?: infer TFormat;
39
+ } ? {
40
+ format?: TFormat;
41
+ } : {}) & (TEntry extends {
42
+ label?: infer TLabel;
43
+ } ? {
44
+ label?: TLabel;
45
+ } : {}) & (TEntry extends {
46
+ trueLabel?: infer TTrueLabel;
47
+ } ? {
48
+ trueLabel?: TTrueLabel;
49
+ } : {}) & (TEntry extends {
50
+ falseLabel?: infer TFalseLabel;
51
+ } ? {
52
+ falseLabel?: TFalseLabel;
53
+ } : {})> : TEntry extends {
54
+ type?: infer TType extends string;
55
+ } ? Simplify<(undefined extends TType ? {} : {
56
+ type: TType;
57
+ }) & (TEntry extends {
58
+ format?: infer TFormat;
59
+ } ? {
60
+ format?: TFormat;
61
+ } : {}) & (TEntry extends {
62
+ label?: infer TLabel;
63
+ } ? {
64
+ label?: TLabel;
65
+ } : {}) & (TEntry extends {
66
+ trueLabel?: infer TTrueLabel;
67
+ } ? {
68
+ trueLabel?: TTrueLabel;
69
+ } : {}) & (TEntry extends {
70
+ falseLabel?: infer TFalseLabel;
71
+ } ? {
72
+ falseLabel?: TFalseLabel;
73
+ } : {})> : {};
74
+ type PrefixedColumnId<TAlias extends string, TColumnId extends string> = `${TAlias}${Capitalize<TColumnId>}`;
75
+ type ProjectedColumnsMap<TDataset, TAlias extends string, TColumns extends readonly string[] | undefined, TIncludeKey extends boolean> = Simplify<{ [TColumnId in DeclaredEffectiveProjectionColumnId<TDataset, TColumns, TIncludeKey> as PrefixedColumnId<TAlias, TColumnId>]: ProjectedColumnHintFromEntry<DeclaredDatasetColumns<TDataset>[TColumnId]> }>;
76
+ type RawProjectedColumnId<TDataset, TColumns extends readonly string[] | undefined, TIncludeKey extends boolean> = Extract<EffectiveProjectionColumnId<TDataset, TColumns, TIncludeKey>, Extract<keyof DatasetRow<TDataset>, string>>;
77
+ type DerivedProjectedColumnId<TDataset, TColumns extends readonly string[] | undefined, TIncludeKey extends boolean> = Extract<{ [TColumnId in DeclaredEffectiveProjectionColumnId<TDataset, TColumns, TIncludeKey>]: DeclaredDatasetColumns<TDataset>[TColumnId] extends {
78
+ kind: 'derived';
79
+ } ? TColumnId : never }[DeclaredEffectiveProjectionColumnId<TDataset, TColumns, TIncludeKey>], string>;
80
+ type ProjectedRowFields<TDataset, TAlias extends string, TColumns extends readonly string[] | undefined, TIncludeKey extends boolean, TNullable extends boolean> = Simplify<{ [TColumnId in RawProjectedColumnId<TDataset, TColumns, TIncludeKey> as PrefixedColumnId<TAlias, TColumnId>]: TNullable extends true ? DatasetRow<TDataset>[TColumnId] | null : DatasetRow<TDataset>[TColumnId] } & { [TColumnId in DerivedProjectedColumnId<TDataset, TColumns, TIncludeKey> as PrefixedColumnId<TAlias, TColumnId>]: DeclaredDatasetColumns<TDataset>[TColumnId] extends {
81
+ kind: 'derived';
82
+ accessor: (row: any) => infer TValue;
83
+ } ? TNullable extends true ? TValue | null : TValue : never }>;
84
+ type AppendExpandedKey<TBaseKey extends readonly string[] | undefined, TAlias extends string, TDataset> = TBaseKey extends readonly string[] ? readonly [...TBaseKey, PrefixedColumnId<TAlias, SingleDatasetKeyId<TDataset>>] : readonly [PrefixedColumnId<TAlias, SingleDatasetKeyId<TDataset>>];
85
+ type RelationshipLookupId<TRelationships extends Record<string, ModelRelationshipDefinition>, TBaseDatasetId extends string> = Extract<{ [TRelationshipId in keyof TRelationships]: TRelationships[TRelationshipId] extends ModelRelationshipDefinition<any, infer TToDatasetId extends string, any, any> ? TToDatasetId extends TBaseDatasetId ? TRelationshipId : never : never }[keyof TRelationships], string>;
86
+ type RelationshipExpansionId<TRelationships extends Record<string, ModelRelationshipDefinition>, TBaseDatasetId extends string> = Extract<{ [TRelationshipId in keyof TRelationships]: TRelationships[TRelationshipId] extends ModelRelationshipDefinition<infer TFromDatasetId extends string, any, any, any> ? TFromDatasetId extends TBaseDatasetId ? TRelationshipId : never : never }[keyof TRelationships], string>;
87
+ type RelationshipLookupTargetDatasetId<TRelationships extends Record<string, ModelRelationshipDefinition>, TRelationshipId extends string> = Extract<TRelationships[TRelationshipId] extends ModelRelationshipDefinition<infer TFromDatasetId extends string, any, any, any> ? TFromDatasetId : never, string>;
88
+ type RelationshipExpansionTargetDatasetId<TRelationships extends Record<string, ModelRelationshipDefinition>, TRelationshipId extends string> = Extract<TRelationships[TRelationshipId] extends ModelRelationshipDefinition<any, infer TToDatasetId extends string, any, any> ? TToDatasetId : never, string>;
89
+ type AssociationExpansionId<TAssociations extends Record<string, ModelAssociationDefinition>, TBaseDatasetId extends string> = Extract<{ [TAssociationId in keyof TAssociations]: TAssociations[TAssociationId] extends ModelAssociationDefinition<infer TFromDatasetId extends string, infer TToDatasetId extends string, any, any> ? TBaseDatasetId extends TFromDatasetId | TToDatasetId ? TAssociationId : never : never }[keyof TAssociations], string>;
90
+ type AssociationExpansionTargetDatasetId<TAssociations extends Record<string, ModelAssociationDefinition>, TAssociationId extends string, TBaseDatasetId extends string> = Extract<TAssociations[TAssociationId] extends ModelAssociationDefinition<infer TFromDatasetId extends string, infer TToDatasetId extends string, any, any> ? TBaseDatasetId extends TFromDatasetId ? TToDatasetId : TBaseDatasetId extends TToDatasetId ? TFromDatasetId : never : never, string>;
91
+ /**
92
+ * Serializable description of one materialization step.
93
+ *
94
+ * This metadata exists so callers can inspect a built view and understand
95
+ * whether it preserves grain via lookup projection or expands grain through a
96
+ * relationship/association traversal.
97
+ */
98
+ type MaterializedViewStepMetadata = {
99
+ readonly kind: 'join';
100
+ readonly alias: string;
101
+ readonly relationship: string;
102
+ readonly targetDataset: string;
103
+ readonly projectedColumns: readonly string[];
104
+ } | {
105
+ readonly kind: 'through-relationship';
106
+ readonly alias: string;
107
+ readonly relationship: string;
108
+ readonly targetDataset: string;
109
+ readonly projectedColumns: readonly string[];
110
+ } | {
111
+ readonly kind: 'through-association';
112
+ readonly alias: string;
113
+ readonly association: string;
114
+ readonly targetDataset: string;
115
+ readonly projectedColumns: readonly string[];
116
+ };
117
+ /**
118
+ * Public metadata attached to a built materialized view.
119
+ *
120
+ * This is the visible record of what the view is: which dataset owns the base
121
+ * grain, what that grain is called, and which traversals made the extra columns
122
+ * available.
123
+ */
124
+ type MaterializedViewMetadata<TId extends string = string, TBaseDatasetId extends string = string, TGrain extends string = string> = {
125
+ readonly id: TId;
126
+ readonly baseDataset: TBaseDatasetId;
127
+ readonly grain: TGrain;
128
+ readonly steps: readonly MaterializedViewStepMetadata[];
129
+ };
130
+ /**
131
+ * Resolved materialized view.
132
+ *
133
+ * It behaves like a reusable dataset, but also exposes explicit materialization
134
+ * metadata and the `materialize(data)` runtime that flattens model data into
135
+ * one chartable row array.
136
+ */
137
+ type DefinedMaterializedView<TRow, TColumns extends Record<string, unknown> | undefined, TKey extends readonly string[] | undefined, TModel extends AnyDefinedDataModel = AnyDefinedDataModel, TId extends string = string, TBaseDatasetId extends string = string, TGrain extends string = string> = Omit<DefinedDataset<TRow, TColumns, any>, 'build' | 'chart'> & {
138
+ readonly key?: TKey;
139
+ chart<const TChartId extends string | undefined = undefined>(id?: TChartId): DatasetChartBuilder<TRow, TColumns, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TChartId, DefinedMaterializedView<TRow, TColumns, TKey, TModel, TId, TBaseDatasetId, TGrain>>;
140
+ /**
141
+ * Materialize one explicit flat row array from linked model data.
142
+ *
143
+ * Callers remain in charge of when this happens. The model does not execute
144
+ * hidden joins at chart render time.
145
+ */
146
+ materialize(data: TModel extends DefinedDataModel<infer TDatasets, any, any, any> ? { readonly [TDatasetId in keyof TDatasets]: readonly DatasetRow<TDatasets[TDatasetId]>[] } : never): readonly TRow[];
147
+ /**
148
+ * Resolve the fluent view definition into its reusable dataset-like form.
149
+ */
150
+ build(): DefinedMaterializedView<TRow, TColumns, TKey, TModel, TId, TBaseDatasetId, TGrain>;
151
+ readonly materialization: MaterializedViewMetadata<TId, TBaseDatasetId, TGrain>;
152
+ readonly __materializedViewBrand: 'materialized-view-definition';
153
+ };
154
+ /**
155
+ * Public materialized view definition accepted across the chart-studio API.
156
+ *
157
+ * The definition is intentionally also a concrete materialized view instance so
158
+ * callers can reuse `.chart(...)`, `.materialize(...)`, and `.build()` without
159
+ * additional wrapping.
160
+ */
161
+ type MaterializedViewDefinition<TRow, TColumns extends Record<string, unknown> | undefined, TKey extends readonly string[] | undefined, TModel extends AnyDefinedDataModel = AnyDefinedDataModel, TId extends string = string, TBaseDatasetId extends string = string, TGrain extends string = string> = DefinedMaterializedView<TRow, TColumns, TKey, TModel, TId, TBaseDatasetId, TGrain>;
162
+ /**
163
+ * Entry point for authoring one model-derived materialized view.
164
+ *
165
+ * Authors must choose the base dataset first, because the rest of the builder
166
+ * is defined in relation to that one explicit row grain.
167
+ */
168
+ interface ModelMaterializationStartBuilder<TDatasets extends ModelDatasets, TRelationships extends Record<string, ModelRelationshipDefinition>, TAssociations extends Record<string, ModelAssociationDefinition>, TAttributes extends Record<string, ModelAttributeDefinition>, TViewId extends string = string> {
169
+ /**
170
+ * Start one materialized view from the declared base dataset.
171
+ *
172
+ * This dataset supplies the initial row grain before any lookup projection or
173
+ * explicit row-expanding traversal is added.
174
+ */
175
+ from<const TBaseDatasetId extends ModelDatasetId<TDatasets>>(dataset: TBaseDatasetId): ModelMaterializationBuilder<TDatasets, TRelationships, TAssociations, TAttributes, TViewId, TBaseDatasetId, DatasetRow<TDatasets[TBaseDatasetId]>, DatasetColumns<TDatasets[TBaseDatasetId]>, DatasetKeyIds<TDatasets[TBaseDatasetId]>, never, false>;
176
+ }
177
+ /**
178
+ * Fluent builder for one explicit model-derived view.
179
+ *
180
+ * The builder keeps three concerns separate:
181
+ * lookup projection, row-expanding traversal, and final grain declaration.
182
+ */
183
+ interface ModelMaterializationBuilder<TDatasets extends ModelDatasets, TRelationships extends Record<string, ModelRelationshipDefinition>, TAssociations extends Record<string, ModelAssociationDefinition>, TAttributes extends Record<string, ModelAttributeDefinition>, TViewId extends string, TBaseDatasetId extends ModelDatasetId<TDatasets>, TRow, TColumns extends Record<string, unknown> | undefined, TKey extends readonly string[] | undefined, TAliases extends string, THasExpansion extends boolean> {
184
+ /**
185
+ * Project columns from the far side of one lookup relationship.
186
+ *
187
+ * This preserves the base row grain and is the ergonomic path for common
188
+ * one-to-one or many-to-one lookups such as `jobs.ownerId -> owners.id`.
189
+ */
190
+ join<const TAlias extends string, const TRelationshipId extends RelationshipLookupId<TRelationships, TBaseDatasetId>, const TProjectedColumns extends readonly MaterializedProjectableColumnId<TDatasets[RelationshipLookupTargetDatasetId<TRelationships, TRelationshipId>]>[] | undefined = undefined>(alias: UniqueId<TAlias, TAliases>, config: {
191
+ readonly relationship: TRelationshipId;
192
+ readonly columns?: TProjectedColumns;
193
+ }): ModelMaterializationBuilder<TDatasets, TRelationships, TAssociations, TAttributes, TViewId, TBaseDatasetId, Simplify<TRow & ProjectedRowFields<TDatasets[RelationshipLookupTargetDatasetId<TRelationships, TRelationshipId>], TAlias, TProjectedColumns, false, true>>, MergeColumns<TColumns, ProjectedColumnsMap<TDatasets[RelationshipLookupTargetDatasetId<TRelationships, TRelationshipId>], TAlias, TProjectedColumns, false>>, TKey, TAliases | TAlias, THasExpansion>;
194
+ /**
195
+ * Expand the base grain across one declared one-to-many relationship.
196
+ *
197
+ * Use this when the chart genuinely needs the child-side grain to become part
198
+ * of the flat output rows.
199
+ */
200
+ throughRelationship<const TAlias extends string, const TRelationshipId extends RelationshipExpansionId<TRelationships, TBaseDatasetId>, const TProjectedColumns extends readonly MaterializedProjectableColumnId<TDatasets[RelationshipExpansionTargetDatasetId<TRelationships, TRelationshipId>]>[] | undefined = undefined>(alias: THasExpansion extends true ? never : UniqueId<TAlias, TAliases>, config: {
201
+ readonly relationship: TRelationshipId;
202
+ readonly columns?: TProjectedColumns;
203
+ }): ModelMaterializationBuilder<TDatasets, TRelationships, TAssociations, TAttributes, TViewId, TBaseDatasetId, Simplify<TRow & ProjectedRowFields<TDatasets[RelationshipExpansionTargetDatasetId<TRelationships, TRelationshipId>], TAlias, TProjectedColumns, true, false>>, MergeColumns<TColumns, ProjectedColumnsMap<TDatasets[RelationshipExpansionTargetDatasetId<TRelationships, TRelationshipId>], TAlias, TProjectedColumns, true>>, AppendExpandedKey<TKey, TAlias, TDatasets[RelationshipExpansionTargetDatasetId<TRelationships, TRelationshipId>]>, TAliases | TAlias, true>;
204
+ /**
205
+ * Expand the base grain across one explicit association.
206
+ *
207
+ * This is the visible many-to-many path. It always stays opt-in so row
208
+ * multiplication is never hidden.
209
+ */
210
+ throughAssociation<const TAlias extends string, const TAssociationId extends AssociationExpansionId<TAssociations, TBaseDatasetId>, const TProjectedColumns extends readonly MaterializedProjectableColumnId<TDatasets[AssociationExpansionTargetDatasetId<TAssociations, TAssociationId, TBaseDatasetId>]>[] | undefined = undefined>(alias: THasExpansion extends true ? never : UniqueId<TAlias, TAliases>, config: {
211
+ readonly association: TAssociationId;
212
+ readonly columns?: TProjectedColumns;
213
+ }): ModelMaterializationBuilder<TDatasets, TRelationships, TAssociations, TAttributes, TViewId, TBaseDatasetId, Simplify<TRow & ProjectedRowFields<TDatasets[AssociationExpansionTargetDatasetId<TAssociations, TAssociationId, TBaseDatasetId>], TAlias, TProjectedColumns, true, false>>, MergeColumns<TColumns, ProjectedColumnsMap<TDatasets[AssociationExpansionTargetDatasetId<TAssociations, TAssociationId, TBaseDatasetId>], TAlias, TProjectedColumns, true>>, AppendExpandedKey<TKey, TAlias, TDatasets[AssociationExpansionTargetDatasetId<TAssociations, TAssociationId, TBaseDatasetId>]>, TAliases | TAlias, true>;
214
+ /**
215
+ * Finalize the view with a human-readable grain label.
216
+ *
217
+ * Calling `grain(...)` is required so the flattened output shape always has a
218
+ * visible semantic name such as `'job'` or `'job-skill'`.
219
+ */
220
+ grain<const TGrain extends string>(grain: TGrain): MaterializedViewDefinition<TRow, TColumns, TKey, DefinedDataModel<TDatasets, TRelationships, TAssociations, TAttributes>, TViewId, TBaseDatasetId, TGrain>;
221
+ }
222
+ //#endregion
223
+ export { DefinedMaterializedView, MaterializedProjectableColumnId, MaterializedViewDefinition, MaterializedViewMetadata, MaterializedViewStepMetadata, ModelMaterializationBuilder, ModelMaterializationStartBuilder };
@@ -1,6 +1,22 @@
1
- import { ChartColumn, Metric } from "./types.mjs";
1
+ import { AggregateMetric, ChartColumn, CountMetric, Metric, NumericAggregateFunction } from "./types.mjs";
2
2
 
3
3
  //#region src/core/metric-utils.d.ts
4
+ /**
5
+ * Default metric used when no numeric aggregation is selected.
6
+ */
7
+ declare const DEFAULT_METRIC: CountMetric;
8
+ /**
9
+ * Type guard for aggregate metrics.
10
+ */
11
+ declare function isAggregateMetric<TColumnId extends string>(metric: Metric<TColumnId>): metric is AggregateMetric<TColumnId>;
12
+ /**
13
+ * Compare two metric definitions for semantic equality.
14
+ */
15
+ declare function isSameMetric<TColumnId extends string>(left: Metric<TColumnId>, right: Metric<TColumnId>): boolean;
16
+ /**
17
+ * Human-readable label for a numeric aggregate.
18
+ */
19
+ declare function getAggregateMetricLabel(columnLabel: string, aggregate: NumericAggregateFunction): string;
4
20
  /**
5
21
  * Human-readable label for a metric.
6
22
  */
@@ -10,4 +26,4 @@ declare function getMetricLabel<T, TColumnId extends string>(metric: Metric<TCol
10
26
  */
11
27
  declare function buildAvailableMetrics<T, TColumnId extends string>(columns: readonly ChartColumn<T, TColumnId>[]): Metric<TColumnId>[];
12
28
  //#endregion
13
- export { buildAvailableMetrics, getMetricLabel };
29
+ export { DEFAULT_METRIC, buildAvailableMetrics, getAggregateMetricLabel, getMetricLabel, isAggregateMetric, isSameMetric };
@@ -126,4 +126,4 @@ function resolveMetric(metric, columns, availableMetrics, configuredDefaultMetri
126
126
  return metric;
127
127
  }
128
128
  //#endregion
129
- export { DEFAULT_METRIC, buildAvailableMetrics, getMetricLabel, isAggregateMetric, isSameMetric, normalizeMetricAllowances, resolveMetric, restrictAvailableMetrics };
129
+ export { DEFAULT_METRIC, buildAvailableMetrics, getAggregateMetricLabel, getMetricLabel, isAggregateMetric, isSameMetric, normalizeMetricAllowances, resolveMetric, restrictAvailableMetrics };
@@ -0,0 +1,242 @@
1
+ import { createMetricBuilder, createSelectableControlBuilder, getMetricBuilderConfig, getSelectableControlConfig } from "./chart-builder-controls.mjs";
2
+ import { createDatasetChartBuilder } from "./schema-builder.mjs";
3
+ import { resolveRelationshipAlias } from "./model-inference.mjs";
4
+ //#region src/core/model-chart.ts
5
+ const MODEL_CHART_STATE = Symbol("model-chart-state");
6
+ function buildProjectedId(alias, columnId) {
7
+ return `${alias}${columnId.charAt(0).toUpperCase()}${columnId.slice(1)}`;
8
+ }
9
+ function getModelChartState(builder) {
10
+ return builder[MODEL_CHART_STATE];
11
+ }
12
+ function getQualifiedBaseDatasetId(model, fieldId) {
13
+ const [datasetId, ...rest] = fieldId.split(".");
14
+ if (!datasetId || rest.length === 0) return;
15
+ return datasetId in model.datasets ? datasetId : void 0;
16
+ }
17
+ function inferBaseDatasetId(model, chartId, fieldIds) {
18
+ const baseDatasetIds = [...new Set(fieldIds.map((fieldId) => getQualifiedBaseDatasetId(model, fieldId)).filter((datasetId) => !!datasetId))];
19
+ if (baseDatasetIds.length === 0) return;
20
+ if (baseDatasetIds.length > 1) throw new Error(`Model chart "${chartId}" references multiple base datasets (${baseDatasetIds.join(", ")}). Add .from(datasetId) with relative fields, or keep all qualified fields anchored to one dataset.`);
21
+ return baseDatasetIds[0];
22
+ }
23
+ function indexLookupRelationships(model) {
24
+ const indexed = /* @__PURE__ */ new Map();
25
+ Object.values(model.relationships).forEach((relationship) => {
26
+ const alias = resolveRelationshipAlias(relationship);
27
+ if (!alias) return;
28
+ const relationshipsForDataset = indexed.get(relationship.to.dataset) ?? /* @__PURE__ */ new Map();
29
+ const existing = relationshipsForDataset.get(alias);
30
+ if (existing && (existing.from.dataset !== relationship.from.dataset || existing.to.column !== relationship.to.column || existing.from.key !== relationship.from.key)) throw new Error(`Lookup alias "${alias}" is ambiguous on dataset "${relationship.to.dataset}". Add explicit relationships with distinct foreign-key column names.`);
31
+ relationshipsForDataset.set(alias, relationship);
32
+ indexed.set(relationship.to.dataset, relationshipsForDataset);
33
+ });
34
+ return indexed;
35
+ }
36
+ function resolveChartField(relationshipsByDataset, datasetId, fieldId) {
37
+ const segments = fieldId.split(".");
38
+ if (segments.length === 1) return { compiledId: fieldId };
39
+ if (segments.length !== 2) throw new Error(`Field path "${fieldId}" is not supported. Model charts allow one lookup hop such as "owner.name".`);
40
+ const [alias, columnId] = segments;
41
+ const relationship = relationshipsByDataset.get(datasetId)?.get(alias);
42
+ if (!relationship) throw new Error(`Cannot resolve lookup path "${fieldId}" from dataset "${datasetId}".`);
43
+ return {
44
+ compiledId: buildProjectedId(alias, columnId),
45
+ lookup: {
46
+ alias,
47
+ relationship,
48
+ columnId
49
+ }
50
+ };
51
+ }
52
+ function normalizeFieldId(model, baseDatasetId, fieldId) {
53
+ const qualifiedBaseDatasetId = getQualifiedBaseDatasetId(model, fieldId);
54
+ if (!qualifiedBaseDatasetId) return fieldId;
55
+ if (qualifiedBaseDatasetId !== baseDatasetId) throw new Error(`Field "${fieldId}" is anchored to dataset "${qualifiedBaseDatasetId}", but this chart compiles from "${baseDatasetId}".`);
56
+ return fieldId.slice(baseDatasetId.length + 1);
57
+ }
58
+ function collectConfigFieldIds(state) {
59
+ const fieldIds = /* @__PURE__ */ new Set();
60
+ const addSelectableConfig = (config) => {
61
+ config?.allowed?.forEach((fieldId) => fieldIds.add(fieldId));
62
+ config?.hidden?.forEach((fieldId) => fieldIds.add(fieldId));
63
+ if (config?.default) fieldIds.add(config.default);
64
+ };
65
+ const addMetricConfig = (config) => {
66
+ config?.allowed?.forEach((metric) => {
67
+ if (metric.kind === "aggregate") fieldIds.add(metric.columnId);
68
+ });
69
+ config?.hidden?.forEach((metric) => {
70
+ if (metric.kind === "aggregate") fieldIds.add(metric.columnId);
71
+ });
72
+ if (config?.default?.kind === "aggregate") fieldIds.add(config.default.columnId);
73
+ };
74
+ addSelectableConfig(state.xAxis);
75
+ addSelectableConfig(state.groupBy);
76
+ addSelectableConfig(state.filters);
77
+ addMetricConfig(state.metric);
78
+ return [...fieldIds];
79
+ }
80
+ function mapSelectableConfig(config, compileFieldId) {
81
+ if (!config) return;
82
+ return {
83
+ ...config.allowed ? { allowed: config.allowed.map(compileFieldId) } : {},
84
+ ...config.hidden ? { hidden: config.hidden.map(compileFieldId) } : {},
85
+ ...config.default ? { default: compileFieldId(config.default) } : {}
86
+ };
87
+ }
88
+ function mapMetricConfig(config, compileFieldId) {
89
+ if (!config) return;
90
+ const mapMetric = (metric) => metric.kind === "aggregate" ? {
91
+ ...metric,
92
+ columnId: compileFieldId(metric.columnId)
93
+ } : metric;
94
+ const mapMetricAllowance = (metric) => metric.kind === "aggregate" ? {
95
+ ...metric,
96
+ columnId: compileFieldId(metric.columnId)
97
+ } : metric;
98
+ return {
99
+ ...config.allowed ? { allowed: config.allowed.map(mapMetricAllowance) } : {},
100
+ ...config.hidden ? { hidden: config.hidden.map(mapMetric) } : {},
101
+ ...config.default ? { default: mapMetric(config.default) } : {}
102
+ };
103
+ }
104
+ function buildLookupSource(model, chartId, baseDatasetId, resolvedFields, hiddenViewIdPrefix) {
105
+ const lookups = /* @__PURE__ */ new Map();
106
+ resolvedFields.forEach((field) => {
107
+ if (!field.lookup) return;
108
+ const existing = lookups.get(field.lookup.alias) ?? {
109
+ relationship: field.lookup.relationship,
110
+ columns: /* @__PURE__ */ new Set()
111
+ };
112
+ existing.columns.add(field.lookup.columnId);
113
+ lookups.set(field.lookup.alias, existing);
114
+ });
115
+ if (lookups.size === 0) return {
116
+ dataset: model.datasets[baseDatasetId],
117
+ metadataTarget: model.datasets[baseDatasetId]
118
+ };
119
+ const view = model.materialize(`${hiddenViewIdPrefix}${chartId}`, (m) => {
120
+ let builder = m.from(baseDatasetId);
121
+ lookups.forEach(({ relationship, columns }, alias) => {
122
+ builder = builder.join(alias, {
123
+ relationship: relationship.id,
124
+ columns: [...columns]
125
+ });
126
+ });
127
+ return builder.grain(baseDatasetId);
128
+ });
129
+ return {
130
+ dataset: view,
131
+ metadataTarget: view
132
+ };
133
+ }
134
+ function createChartBuilderMembers(state, createNext) {
135
+ return {
136
+ xAxis(defineXAxis) {
137
+ const nextBuilder = defineXAxis(createSelectableControlBuilder({}, true));
138
+ return createNext({
139
+ ...state,
140
+ xAxis: getSelectableControlConfig(nextBuilder)
141
+ });
142
+ },
143
+ groupBy(defineGroupBy) {
144
+ const nextBuilder = defineGroupBy(createSelectableControlBuilder({}, true));
145
+ return createNext({
146
+ ...state,
147
+ groupBy: getSelectableControlConfig(nextBuilder)
148
+ });
149
+ },
150
+ filters(defineFilters) {
151
+ const nextBuilder = defineFilters(createSelectableControlBuilder({}, false));
152
+ return createNext({
153
+ ...state,
154
+ filters: getSelectableControlConfig(nextBuilder)
155
+ });
156
+ },
157
+ metric(defineMetric) {
158
+ const nextBuilder = defineMetric(createMetricBuilder());
159
+ return createNext({
160
+ ...state,
161
+ metric: getMetricBuilderConfig(nextBuilder)
162
+ });
163
+ },
164
+ chartType(defineChartType) {
165
+ const nextBuilder = defineChartType(createSelectableControlBuilder({}, true));
166
+ return createNext({
167
+ ...state,
168
+ chartType: getSelectableControlConfig(nextBuilder)
169
+ });
170
+ },
171
+ timeBucket(defineTimeBucket) {
172
+ const nextBuilder = defineTimeBucket(createSelectableControlBuilder({}, true));
173
+ return createNext({
174
+ ...state,
175
+ timeBucket: getSelectableControlConfig(nextBuilder)
176
+ });
177
+ },
178
+ connectNulls(value) {
179
+ return createNext({
180
+ ...state,
181
+ connectNulls: value
182
+ });
183
+ }
184
+ };
185
+ }
186
+ function createModelChartBuilder(state) {
187
+ const createNext = (nextState) => createModelChartBuilder(nextState);
188
+ const builder = createChartBuilderMembers(state, createNext);
189
+ Object.defineProperty(builder, MODEL_CHART_STATE, {
190
+ value: state,
191
+ enumerable: false,
192
+ configurable: false,
193
+ writable: false
194
+ });
195
+ return builder;
196
+ }
197
+ function createInferredModelChartBuilder(state) {
198
+ const createNext = (nextState) => createInferredModelChartBuilder(nextState);
199
+ const builder = createChartBuilderMembers(state, createNext);
200
+ Object.defineProperty(builder, MODEL_CHART_STATE, {
201
+ value: state,
202
+ enumerable: false,
203
+ configurable: false,
204
+ writable: false
205
+ });
206
+ return builder;
207
+ }
208
+ function createModelChartStartBuilder() {
209
+ return {
210
+ ...createInferredModelChartBuilder({}),
211
+ from(dataset) {
212
+ return createModelChartBuilder({ baseDatasetId: dataset });
213
+ }
214
+ };
215
+ }
216
+ function compileModelChartFromState(model, chartId, state, options = {}) {
217
+ const fieldIds = collectConfigFieldIds(state);
218
+ const baseDatasetId = state.baseDatasetId ?? inferBaseDatasetId(model, chartId, fieldIds);
219
+ if (!baseDatasetId) throw new Error("Model charts must choose a base dataset with .from(datasetId), or qualify all referenced fields from one dataset such as \"tests.takenAt\".");
220
+ const relationshipsByDataset = indexLookupRelationships(model);
221
+ const resolvedFields = fieldIds.map((fieldId) => resolveChartField(relationshipsByDataset, baseDatasetId, normalizeFieldId(model, baseDatasetId, fieldId)));
222
+ const compileFieldId = (fieldId) => resolveChartField(relationshipsByDataset, baseDatasetId, normalizeFieldId(model, baseDatasetId, fieldId)).compiledId;
223
+ const source = buildLookupSource(model, chartId, baseDatasetId, resolvedFields, options.hiddenViewIdPrefix ?? "__lookup_");
224
+ return createDatasetChartBuilder({
225
+ columns: source.dataset.columns,
226
+ xAxis: mapSelectableConfig(state.xAxis, compileFieldId),
227
+ groupBy: mapSelectableConfig(state.groupBy, compileFieldId),
228
+ filters: mapSelectableConfig(state.filters, compileFieldId),
229
+ metric: mapMetricConfig(state.metric, compileFieldId),
230
+ chartType: state.chartType,
231
+ timeBucket: state.timeBucket,
232
+ connectNulls: state.connectNulls
233
+ }, {
234
+ dataset: source.metadataTarget,
235
+ chartId
236
+ }).build();
237
+ }
238
+ function compileModelChart(model, chartId, defineChart, options = {}) {
239
+ return compileModelChartFromState(model, chartId, getModelChartState(defineChart(createModelChartStartBuilder())), options);
240
+ }
241
+ //#endregion
242
+ export { compileModelChart };