@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,51 @@
1
+ import { ChartType, ChartTypeConfig, DefinedChartSchema, FiltersConfig, GroupByConfig, InferableFieldKey, MetricConfig, ResolvedFilterColumnIdFromSchema, ResolvedGroupByColumnIdFromSchema, ResolvedMetricColumnIdFromSchema, ResolvedXAxisColumnIdFromSchema, TimeBucket, TimeBucketConfig, XAxisConfig } from "./types.mjs";
2
+ import { ColumnHelper, ColumnsFromEntries, MetricBuilder, MetricBuilderConfig, SchemaColumnEntry, SchemaFromBuilder, SelectableControlBuilder, SelectableControlBuilderConfig, ValidateColumnEntries } from "./schema-builder.types.mjs";
3
+ import { DATASET_CHART_METADATA, DatasetChartMetadata } from "./dataset-chart-metadata.mjs";
4
+
5
+ //#region src/core/dataset-builder.types.d.ts
6
+ type NonEmptyReadonlyArray<TValue> = readonly [TValue, ...TValue[]];
7
+ type DatasetColumnsContext<TColumns extends Record<string, unknown> | undefined> = [TColumns] extends [undefined] ? undefined : {
8
+ columns?: TColumns;
9
+ };
10
+ type DefinedDatasetChartSchema<TRow, TColumns extends Record<string, unknown> | undefined, TXAxis extends XAxisConfig<any> | undefined, TGroupBy extends GroupByConfig<any> | undefined, TFilters extends FiltersConfig<any> | undefined, TMetric extends MetricConfig<any> | undefined, TChartType extends ChartTypeConfig | undefined, TTimeBucket extends TimeBucketConfig | undefined, TConnectNulls extends boolean | undefined, TChartId extends string | undefined = undefined, TOwner = unknown> = DefinedChartSchema<TRow, SchemaFromBuilder<TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls>> & {
11
+ readonly [DATASET_CHART_METADATA]: DatasetChartMetadata<TChartId, TOwner>;
12
+ };
13
+ type DatasetKey<TRow> = NonEmptyReadonlyArray<InferableFieldKey<TRow>>;
14
+ type DefinedDataset<TRow, TColumns extends Record<string, unknown> | undefined = undefined, TKey extends DatasetKey<TRow> | undefined = undefined> = {
15
+ readonly key?: TKey;
16
+ readonly columns?: TColumns;
17
+ chart<const TChartId extends string | undefined = undefined>(id?: TChartId): DatasetChartBuilder<TRow, TColumns, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TChartId, DefinedDataset<TRow, TColumns, TKey>>;
18
+ validateData(data: readonly TRow[]): void;
19
+ build(): DefinedDataset<TRow, TColumns, TKey>;
20
+ readonly __datasetBrand: 'dataset-definition';
21
+ };
22
+ type DatasetDefinition<TRow, TColumns extends Record<string, unknown> | undefined = undefined, TKey extends DatasetKey<TRow> | undefined = undefined> = {
23
+ build(): DefinedDataset<TRow, TColumns, TKey>;
24
+ };
25
+ type ResolvedDatasetFromDefinition<TDataset> = TDataset extends DatasetDefinition<any, any, any> ? ReturnType<TDataset['build']> : never;
26
+ type DatasetRow<TDataset> = TDataset extends DefinedDataset<infer TRow, any, any> ? TRow : never;
27
+ type DatasetColumns<TDataset> = TDataset extends DefinedDataset<any, infer TColumns, any> ? TColumns : never;
28
+ type DatasetKeyIds<TDataset> = TDataset extends DefinedDataset<any, any, infer TKey> ? TKey : never;
29
+ type SingleDatasetKeyId<TDataset> = DatasetKeyIds<TDataset> extends readonly [infer TKeyId extends string] ? TKeyId : never;
30
+ type DatasetChartBuilder<TRow, TColumns extends Record<string, unknown> | undefined = undefined, TXAxis extends XAxisConfig<any> | undefined = undefined, TGroupBy extends GroupByConfig<any> | undefined = undefined, TFilters extends FiltersConfig<any> | undefined = undefined, TMetric extends MetricConfig<any> | undefined = undefined, TChartType extends ChartTypeConfig | undefined = undefined, TTimeBucket extends TimeBucketConfig | undefined = undefined, TConnectNulls extends boolean | undefined = undefined, TChartId extends string | undefined = undefined, TOwner = unknown> = {
31
+ xAxis<const TBuilder extends SelectableControlBuilder<ResolvedXAxisColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, true>>(defineXAxis: (xAxis: SelectableControlBuilder<ResolvedXAxisColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, true>) => TBuilder): DatasetChartBuilder<TRow, TColumns, SelectableControlBuilderConfig<TBuilder>, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
32
+ groupBy<const TBuilder extends SelectableControlBuilder<ResolvedGroupByColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, true>>(defineGroupBy: (groupBy: SelectableControlBuilder<ResolvedGroupByColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, true>) => TBuilder): DatasetChartBuilder<TRow, TColumns, TXAxis, SelectableControlBuilderConfig<TBuilder>, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
33
+ filters<const TBuilder extends SelectableControlBuilder<ResolvedFilterColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, false>>(defineFilters: (filters: SelectableControlBuilder<ResolvedFilterColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, false>) => TBuilder): DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, SelectableControlBuilderConfig<TBuilder>, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
34
+ metric<const TBuilder extends MetricBuilder<ResolvedMetricColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, any, any, any>>(defineMetric: (metric: MetricBuilder<ResolvedMetricColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>>) => TBuilder): DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, TFilters, MetricBuilderConfig<TBuilder>, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
35
+ chartType<const TBuilder extends SelectableControlBuilder<ChartType, true>>(defineChartType: (chartType: SelectableControlBuilder<ChartType, true>) => TBuilder): DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, SelectableControlBuilderConfig<TBuilder>, TTimeBucket, TConnectNulls, TChartId, TOwner>;
36
+ timeBucket<const TBuilder extends SelectableControlBuilder<TimeBucket, true>>(defineTimeBucket: (timeBucket: SelectableControlBuilder<TimeBucket, true>) => TBuilder): DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, SelectableControlBuilderConfig<TBuilder>, TConnectNulls, TChartId, TOwner>;
37
+ connectNulls<const TValue extends boolean>(value: TValue): DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TValue, TChartId, TOwner>;
38
+ build(): DefinedDatasetChartSchema<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
39
+ readonly [DATASET_CHART_METADATA]: DatasetChartMetadata<TChartId, TOwner>;
40
+ };
41
+ type DatasetChartDefinition<TRow, TColumns extends Record<string, unknown> | undefined = undefined, TXAxis extends XAxisConfig<any> | undefined = undefined, TGroupBy extends GroupByConfig<any> | undefined = undefined, TFilters extends FiltersConfig<any> | undefined = undefined, TMetric extends MetricConfig<any> | undefined = undefined, TChartType extends ChartTypeConfig | undefined = undefined, TTimeBucket extends TimeBucketConfig | undefined = undefined, TConnectNulls extends boolean | undefined = undefined, TChartId extends string | undefined = undefined, TOwner = unknown> = DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner> | DefinedDatasetChartSchema<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
42
+ interface DatasetBuilder<TRow, TColumns extends Record<string, unknown> | undefined = undefined, TKey extends DatasetKey<TRow> | undefined = undefined> extends DatasetDefinition<TRow, TColumns, TKey> {
43
+ key<const TFieldId extends InferableFieldKey<TRow>>(id: TFieldId): DatasetBuilder<TRow, TColumns, readonly [TFieldId]>;
44
+ key<const TKeyIds extends DatasetKey<TRow>>(ids: TKeyIds): DatasetBuilder<TRow, TColumns, TKeyIds>;
45
+ columns: TColumns extends undefined ? <const TEntries extends readonly SchemaColumnEntry<TRow>[]>(defineColumns: (columns: ColumnHelper<TRow>) => TEntries & ValidateColumnEntries<TEntries>) => DatasetBuilder<TRow, ColumnsFromEntries<TRow, TEntries>, TKey> : never;
46
+ chart<const TChartId extends string | undefined = undefined>(id?: TChartId): DatasetChartBuilder<TRow, TColumns, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TChartId, DefinedDataset<TRow, TColumns, TKey>>;
47
+ validateData(data: readonly TRow[]): void;
48
+ build(): DefinedDataset<TRow, TColumns, TKey>;
49
+ }
50
+ //#endregion
51
+ export { DatasetBuilder, DatasetChartBuilder, DatasetChartDefinition, DatasetColumns, DatasetDefinition, DatasetKey, DatasetKeyIds, DatasetRow, DefinedDataset, DefinedDatasetChartSchema, ResolvedDatasetFromDefinition, SingleDatasetKeyId };
@@ -0,0 +1,8 @@
1
+ //#region src/core/dataset-chart-metadata.d.ts
2
+ declare const DATASET_CHART_METADATA: unique symbol;
3
+ type DatasetChartMetadata<TChartId extends string | undefined = string | undefined, TOwner = unknown> = {
4
+ readonly dataset: TOwner;
5
+ readonly chartId: TChartId;
6
+ };
7
+ //#endregion
8
+ export { DATASET_CHART_METADATA, DatasetChartMetadata };
@@ -0,0 +1,4 @@
1
+ //#region src/core/dataset-chart-metadata.ts
2
+ const DATASET_CHART_METADATA = Symbol("dataset-chart-metadata");
3
+ //#endregion
4
+ export { DATASET_CHART_METADATA };
@@ -1,3 +1,5 @@
1
+ import { DateRangeFilter, TimeBucket } from "./types.mjs";
2
+
1
3
  //#region src/core/date-range-presets.d.ts
2
4
  /**
3
5
  * All recognised date range preset identifiers.
@@ -8,5 +10,45 @@
8
10
  * - Calendar presets — aligned to calendar boundaries
9
11
  */
10
12
  type DateRangePresetId = 'auto' | 'all-time' | 'last-7-days' | 'last-30-days' | 'last-3-months' | 'last-12-months' | 'quarter-to-date' | 'year-to-date' | 'last-year';
13
+ type DateRangePreset = {
14
+ id: DateRangePresetId;
15
+ label: string; /** Optional tooltip description shown on hover. */
16
+ description?: string; /** Build the filter for this preset. `null` = no date filtering (all time). */
17
+ buildFilter: () => DateRangeFilter | null;
18
+ };
19
+ /**
20
+ * Ordered list of all date range presets shown in the UI.
21
+ *
22
+ * Layout hint (2-column grid):
23
+ * Auto | All time
24
+ * Last 7 days | Last 30 days
25
+ * Last 3 months | Last 12 months
26
+ * Quarter to date | Year to date
27
+ * Last year |
28
+ */
29
+ declare const DATE_RANGE_PRESETS: readonly DateRangePreset[];
30
+ /**
31
+ * Map a time bucket to a sensible default date range.
32
+ *
33
+ * - `day` → last 30 days (enough for a meaningful daily trend)
34
+ * - `week` → last 3 months (~13 weeks)
35
+ * - `month` → last 12 months
36
+ * - `quarter` → all time (null)
37
+ * - `year` → all time (null)
38
+ */
39
+ declare function autoFilterForBucket(bucket: TimeBucket): DateRangeFilter | null;
40
+ /**
41
+ * Compute the effective `DateRangeFilter` for a given preset.
42
+ *
43
+ * For `'auto'`, this uses the provided `timeBucket` to derive the range.
44
+ * For all other presets, the filter is computed from the preset definition.
45
+ *
46
+ * @returns The resolved filter, or `null` for "all time".
47
+ */
48
+ declare function resolvePresetFilter(presetId: DateRangePresetId, timeBucket: TimeBucket): DateRangeFilter | null;
49
+ /**
50
+ * Get the human-readable label for a preset ID.
51
+ */
52
+ declare function getPresetLabel(presetId: DateRangePresetId): string;
11
53
  //#endregion
12
- export { DateRangePresetId };
54
+ export { DATE_RANGE_PRESETS, DateRangePreset, DateRangePresetId, autoFilterForBucket, getPresetLabel, resolvePresetFilter };
@@ -111,7 +111,7 @@ function autoFilterForBucket(bucket) {
111
111
  * @returns The resolved filter, or `null` for "all time".
112
112
  */
113
113
  function resolvePresetFilter(presetId, timeBucket) {
114
- if (presetId === "auto") return autoFilterForBucket(timeBucket);
114
+ if (presetId === "auto") return autoFilterForBucket(timeBucket) ?? null;
115
115
  return DATE_RANGE_PRESETS.find((p) => p.id === presetId)?.buildFilter() ?? null;
116
116
  }
117
117
  /**
@@ -149,4 +149,4 @@ function lastYear() {
149
149
  };
150
150
  }
151
151
  //#endregion
152
- export { DATE_RANGE_PRESETS, getPresetLabel, resolvePresetFilter };
152
+ export { DATE_RANGE_PRESETS, autoFilterForBucket, getPresetLabel, resolvePresetFilter };
@@ -0,0 +1,26 @@
1
+ import { DateColumn, DateRangeFilter } from "./types.mjs";
2
+
3
+ //#region src/core/date-utils.d.ts
4
+ /**
5
+ * Filter data by a date range on a specific date column.
6
+ * Both bounds are inclusive (to is extended to end of day).
7
+ *
8
+ * @param data - Raw data items
9
+ * @param dateColumn - The date column to filter on
10
+ * @param filter - Date range filter with from/to bounds
11
+ * @returns Filtered data items within the date range
12
+ */
13
+ declare function filterByDateRange<T>(data: readonly T[], dateColumn: DateColumn<T>, filter: DateRangeFilter): T[];
14
+ /**
15
+ * Compute the min/max date range from data for a given date column.
16
+ *
17
+ * @param data - Data items to scan
18
+ * @param dateColumn - The date column to extract dates from
19
+ * @returns Object with min and max dates (both null if no valid dates)
20
+ */
21
+ declare function computeDateRange<T>(data: readonly T[], dateColumn: DateColumn<T>): {
22
+ min: Date | null;
23
+ max: Date | null;
24
+ };
25
+ //#endregion
26
+ export { computeDateRange, filterByDateRange };
@@ -0,0 +1,8 @@
1
+ import { DataModelDefinition } from "./data-model.types.mjs";
2
+ import { DashboardBuilder, DashboardDefinition, DashboardInputModel, ResolvedDashboardFromDefinition } from "./dashboard.types.mjs";
3
+
4
+ //#region src/core/define-dashboard.d.ts
5
+ declare function resolveDashboardDefinition<TDashboard extends DashboardDefinition<any, any, any>>(dashboard: TDashboard): ResolvedDashboardFromDefinition<TDashboard>;
6
+ declare function defineDashboard<TModel extends DataModelDefinition<any, any, any, any>>(model: TModel): DashboardBuilder<DashboardInputModel<TModel>>;
7
+ //#endregion
8
+ export { defineDashboard, resolveDashboardDefinition };
@@ -0,0 +1,156 @@
1
+ import { DATASET_CHART_METADATA } from "./dataset-chart-metadata.mjs";
2
+ //#region src/core/define-dashboard.ts
3
+ function humanizeId(id) {
4
+ return id.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim().split(/\s+/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
5
+ }
6
+ function assertUniqueId(collection, kind, id) {
7
+ if (id in collection) throw new Error(`Duplicate ${kind} id: "${id}"`);
8
+ }
9
+ function getDatasetChartMetadata(chart) {
10
+ if (chart && typeof chart === "object" && DATASET_CHART_METADATA in chart) return chart[DATASET_CHART_METADATA];
11
+ if (chart && typeof chart === "object" && "build" in chart && typeof chart.build === "function") {
12
+ const built = chart.build();
13
+ if (built && typeof built === "object" && DATASET_CHART_METADATA in built) return built[DATASET_CHART_METADATA];
14
+ }
15
+ }
16
+ function findModelDatasetId(model, dataset) {
17
+ return Object.entries(model.datasets).find(([, candidate]) => candidate === dataset)?.[0];
18
+ }
19
+ function isMaterializedView(source) {
20
+ return !!source && typeof source === "object" && "__materializedViewBrand" in source && "materialization" in source && "build" in source && typeof source.build === "function";
21
+ }
22
+ function resolveChartDataSource(model, datasetOrView) {
23
+ const datasetId = findModelDatasetId(model, datasetOrView);
24
+ if (datasetId) return {
25
+ kind: "dataset",
26
+ datasetId
27
+ };
28
+ if (!isMaterializedView(datasetOrView)) return;
29
+ const view = datasetOrView;
30
+ const baseDatasetId = view.materialization.baseDataset;
31
+ if (!(baseDatasetId in model.datasets)) return;
32
+ return {
33
+ kind: "materialized-view",
34
+ datasetId: baseDatasetId,
35
+ view
36
+ };
37
+ }
38
+ function createDefinedDashboard(state) {
39
+ let cachedDashboard;
40
+ const build = () => {
41
+ if (cachedDashboard) return cachedDashboard;
42
+ const definedDashboard = {
43
+ model: state.model,
44
+ charts: state.charts,
45
+ sharedFilters: state.sharedFilters,
46
+ build() {
47
+ return definedDashboard;
48
+ },
49
+ __dashboardBrand: "dashboard-definition"
50
+ };
51
+ cachedDashboard = definedDashboard;
52
+ return definedDashboard;
53
+ };
54
+ return build();
55
+ }
56
+ function createDashboardBuilder(state) {
57
+ let cachedDashboard;
58
+ return {
59
+ chart(id, chart) {
60
+ assertUniqueId(state.charts, "dashboard chart", id);
61
+ const metadata = getDatasetChartMetadata(chart);
62
+ if (!metadata) throw new Error(`Dashboard chart "${id}" must come from defineDataset(...).chart(...).`);
63
+ if (metadata.chartId && metadata.chartId !== id) throw new Error(`Dashboard chart "${id}" does not match the chart authoring id "${metadata.chartId}".`);
64
+ const dataSource = resolveChartDataSource(state.model, metadata.dataset);
65
+ if (!dataSource) throw new Error(`Dashboard chart "${id}" references a dataset or materialized view that is not registered in this data model.`);
66
+ return createDashboardBuilder({
67
+ ...state,
68
+ charts: {
69
+ ...state.charts,
70
+ [id]: {
71
+ id,
72
+ datasetId: dataSource.datasetId,
73
+ schema: chart,
74
+ dataSource
75
+ }
76
+ }
77
+ });
78
+ },
79
+ sharedFilter(id, config) {
80
+ assertUniqueId(state.sharedFilters, "shared filter", id);
81
+ if (config === void 0) {
82
+ const attribute = state.model.attributes[id];
83
+ if (!attribute) throw new Error(`Unknown model attribute id: "${id}"`);
84
+ return createDashboardBuilder({
85
+ ...state,
86
+ sharedFilters: {
87
+ ...state.sharedFilters,
88
+ [id]: {
89
+ id,
90
+ kind: "select",
91
+ label: humanizeId(id),
92
+ source: {
93
+ kind: "attribute",
94
+ dataset: attribute.source.dataset,
95
+ key: attribute.source.key,
96
+ label: attribute.source.label
97
+ },
98
+ targets: attribute.targets,
99
+ attribute: id
100
+ }
101
+ }
102
+ });
103
+ }
104
+ if (config.kind === "select") return createDashboardBuilder({
105
+ ...state,
106
+ sharedFilters: {
107
+ ...state.sharedFilters,
108
+ [id]: {
109
+ id,
110
+ kind: "select",
111
+ label: config.label ?? humanizeId(id),
112
+ source: {
113
+ kind: "column",
114
+ dataset: config.source.dataset,
115
+ column: config.source.column
116
+ },
117
+ targets: config.targets ?? [{
118
+ dataset: config.source.dataset,
119
+ column: config.source.column
120
+ }]
121
+ }
122
+ }
123
+ });
124
+ if (!Array.isArray(config.targets) || config.targets.length === 0) throw new Error(`Dashboard shared date-range filter "${id}" requires at least one target.`);
125
+ return createDashboardBuilder({
126
+ ...state,
127
+ sharedFilters: {
128
+ ...state.sharedFilters,
129
+ [id]: {
130
+ id,
131
+ kind: "date-range",
132
+ label: config.label ?? humanizeId(id),
133
+ targets: config.targets
134
+ }
135
+ }
136
+ });
137
+ },
138
+ build() {
139
+ if (cachedDashboard) return cachedDashboard;
140
+ cachedDashboard = createDefinedDashboard(state);
141
+ return cachedDashboard;
142
+ }
143
+ };
144
+ }
145
+ function resolveDashboardDefinition(dashboard) {
146
+ return dashboard.build();
147
+ }
148
+ function defineDashboard(model) {
149
+ return createDashboardBuilder({
150
+ model: model.build(),
151
+ charts: {},
152
+ sharedFilters: {}
153
+ });
154
+ }
155
+ //#endregion
156
+ export { defineDashboard, resolveDashboardDefinition };
@@ -0,0 +1,11 @@
1
+ import { DataModelBuilder } from "./data-model.types.mjs";
2
+
3
+ //#region src/core/define-data-model.d.ts
4
+ /**
5
+ * Define one linked data model for dataset registration, relationships,
6
+ * associations, reusable model-level filter attributes, and explicit
7
+ * Phase 7 materialized views.
8
+ */
9
+ declare function defineDataModel(): DataModelBuilder<{}, {}, {}, {}>;
10
+ //#endregion
11
+ export { defineDataModel };
@@ -0,0 +1,327 @@
1
+ import { resolveDatasetDefinition, validateDatasetData } from "./define-dataset.mjs";
2
+ import { createMaterializationStartBuilder } from "./materialized-view.mjs";
3
+ import { attachModelRuntimeMetadata, getModelRuntimeMetadata, inferModelAttributes, inferModelRelationships, rewriteInferredRelationshipError } from "./model-inference.mjs";
4
+ import { compileModelChart } from "./model-chart.mjs";
5
+ //#region src/core/define-data-model.ts
6
+ function formatValue(value) {
7
+ if (value instanceof Date) return value.toISOString();
8
+ return String(value);
9
+ }
10
+ function buildFingerprint(value) {
11
+ if (value instanceof Date) return `date:${value.toISOString()}`;
12
+ return `${typeof value}:${String(value)}`;
13
+ }
14
+ function getSingleDatasetKey(dataset, datasetId) {
15
+ if (!dataset.key || dataset.key.length !== 1) throw new Error(`Dataset "${datasetId}" must declare exactly one key to participate in relationships or associations.`);
16
+ return dataset.key[0];
17
+ }
18
+ function assertUniqueId(collection, kind, id) {
19
+ if (id in collection) throw new Error(`Duplicate ${kind} id: "${id}"`);
20
+ }
21
+ function getDatasetOrThrow(datasets, datasetId) {
22
+ const dataset = datasets[datasetId];
23
+ if (!dataset) throw new Error(`Unknown dataset id: "${datasetId}"`);
24
+ return dataset;
25
+ }
26
+ function createKeyLookup(datasetId, keyId, rows) {
27
+ const values = /* @__PURE__ */ new Set();
28
+ rows.forEach((row, index) => {
29
+ const value = row[keyId];
30
+ if (value == null) throw new Error(`Dataset "${datasetId}" key "${keyId}" is missing a value at row ${index}.`);
31
+ values.add(buildFingerprint(value));
32
+ });
33
+ return {
34
+ keyId,
35
+ values
36
+ };
37
+ }
38
+ function validateRelationshipData(relationship, data, keyLookups) {
39
+ const toRows = data[relationship.to.dataset];
40
+ const sourceLookup = keyLookups[relationship.from.dataset]?.[relationship.from.key];
41
+ if (!sourceLookup) throw new Error(`Relationship "${relationship.id}" requires dataset "${relationship.from.dataset}" data.`);
42
+ if (!Array.isArray(toRows)) throw new Error(`Relationship "${relationship.id}" requires dataset "${relationship.to.dataset}" data.`);
43
+ toRows.forEach((row, index) => {
44
+ const value = row[relationship.to.column];
45
+ if (value == null) return;
46
+ if (!sourceLookup.values.has(buildFingerprint(value))) throw new Error(`Relationship "${relationship.id}" has an orphan foreign key "${formatValue(value)}" in dataset "${relationship.to.dataset}" column "${relationship.to.column}" at row ${index}.`);
47
+ });
48
+ }
49
+ function validateExplicitAssociationData(association, associationId, fromDatasetId, toDatasetId, fromLookup, toLookup) {
50
+ association.data.forEach((row, index) => {
51
+ const fromValue = row[association.columns.from];
52
+ const toValue = row[association.columns.to];
53
+ if (fromValue == null) throw new Error(`Association "${associationId}" is missing "${association.columns.from}" at edge row ${index}.`);
54
+ if (toValue == null) throw new Error(`Association "${associationId}" is missing "${association.columns.to}" at edge row ${index}.`);
55
+ if (!fromLookup.values.has(buildFingerprint(fromValue))) throw new Error(`Association "${associationId}" has an orphan "${association.columns.from}" value "${formatValue(fromValue)}" for dataset "${fromDatasetId}".`);
56
+ if (!toLookup.values.has(buildFingerprint(toValue))) throw new Error(`Association "${associationId}" has an orphan "${association.columns.to}" value "${formatValue(toValue)}" for dataset "${toDatasetId}".`);
57
+ });
58
+ }
59
+ function validateDerivedAssociationData(association, state, data, keyLookups) {
60
+ const derivedEdge = association.edge;
61
+ if (derivedEdge.kind !== "derived") return;
62
+ const deriveDatasetId = derivedEdge.deriveFrom.dataset;
63
+ const deriveRows = data[deriveDatasetId];
64
+ const deriveKeyId = getSingleDatasetKey(getDatasetOrThrow(state.datasets, deriveDatasetId), deriveDatasetId);
65
+ const oppositeDatasetId = deriveDatasetId === association.from.dataset ? association.to.dataset : association.from.dataset;
66
+ const oppositeKeyId = oppositeDatasetId === association.from.dataset ? association.from.key : association.to.key;
67
+ const oppositeLookup = keyLookups[oppositeDatasetId]?.[oppositeKeyId];
68
+ if (!oppositeLookup) throw new Error(`Association "${association.id}" requires dataset "${oppositeDatasetId}" data.`);
69
+ if (!Array.isArray(deriveRows)) throw new Error(`Association "${association.id}" requires dataset "${deriveDatasetId}" data.`);
70
+ deriveRows.forEach((row, index) => {
71
+ if (row[deriveKeyId] == null) throw new Error(`Association "${association.id}" source dataset "${deriveDatasetId}" is missing key "${deriveKeyId}" at row ${index}.`);
72
+ const rawValues = derivedEdge.deriveFrom.values(row) ?? [];
73
+ if (!Array.isArray(rawValues)) throw new Error(`Association "${association.id}" deriveFrom.values(...) must return an array.`);
74
+ rawValues.forEach((value) => {
75
+ if (value == null) throw new Error(`Association "${association.id}" deriveFrom.values(...) returned an empty key value.`);
76
+ if (!oppositeLookup.values.has(buildFingerprint(value))) throw new Error(`Association "${association.id}" has an orphan derived key "${formatValue(value)}" targeting dataset "${oppositeDatasetId}".`);
77
+ });
78
+ });
79
+ }
80
+ function validateAssociationData(association, state, data, keyLookups) {
81
+ const fromLookup = keyLookups[association.from.dataset]?.[association.from.key];
82
+ const toLookup = keyLookups[association.to.dataset]?.[association.to.key];
83
+ if (!fromLookup || !toLookup) throw new Error(`Association "${association.id}" requires both endpoint datasets to be present.`);
84
+ if (association.edge.kind === "explicit") {
85
+ validateExplicitAssociationData(association.edge, association.id, association.from.dataset, association.to.dataset, fromLookup, toLookup);
86
+ return;
87
+ }
88
+ validateDerivedAssociationData(association, state, data, keyLookups);
89
+ }
90
+ function validateModelRuntimeData(state, data) {
91
+ const keyLookups = {};
92
+ const requiredKeysByDataset = /* @__PURE__ */ new Map();
93
+ const addRequiredKey = (datasetId, keyId) => {
94
+ if (!keyId) return;
95
+ const existing = requiredKeysByDataset.get(datasetId);
96
+ if (existing) {
97
+ existing.add(keyId);
98
+ return;
99
+ }
100
+ requiredKeysByDataset.set(datasetId, new Set([keyId]));
101
+ };
102
+ Object.entries(state.datasets).forEach(([datasetId, dataset]) => {
103
+ const rows = data[datasetId];
104
+ if (!Array.isArray(rows)) throw new Error(`Missing dataset data for "${datasetId}".`);
105
+ validateDatasetData(dataset, rows, datasetId);
106
+ });
107
+ Object.values(state.relationships).forEach((relationship) => {
108
+ addRequiredKey(relationship.from.dataset, relationship.from.key);
109
+ });
110
+ Object.values(state.associations).forEach((association) => {
111
+ addRequiredKey(association.from.dataset, association.from.key);
112
+ addRequiredKey(association.to.dataset, association.to.key);
113
+ });
114
+ Object.values(state.attributes).forEach((attribute) => {
115
+ addRequiredKey(attribute.source.dataset, attribute.source.key);
116
+ });
117
+ Object.entries(state.datasets).forEach(([datasetId, dataset]) => {
118
+ if (dataset.key && dataset.key.length === 1) addRequiredKey(datasetId, dataset.key[0]);
119
+ });
120
+ requiredKeysByDataset.forEach((keyIds, datasetId) => {
121
+ const rows = data[datasetId];
122
+ if (!Array.isArray(rows)) throw new Error(`Missing dataset data for "${datasetId}".`);
123
+ keyLookups[datasetId] = {};
124
+ keyIds.forEach((keyId) => {
125
+ keyLookups[datasetId][keyId] = createKeyLookup(datasetId, keyId, rows);
126
+ });
127
+ });
128
+ Object.values(state.relationships).forEach((relationship) => {
129
+ validateRelationshipData(relationship, data, keyLookups);
130
+ });
131
+ Object.values(state.associations).forEach((association) => {
132
+ validateAssociationData(association, state, data, keyLookups);
133
+ });
134
+ }
135
+ function createDefinedDataModel(state) {
136
+ let cachedModel;
137
+ const build = () => {
138
+ if (cachedModel) return cachedModel;
139
+ const definedModel = {
140
+ datasets: state.datasets,
141
+ relationships: state.relationships,
142
+ associations: state.associations,
143
+ attributes: state.attributes,
144
+ materialize(id, defineView) {
145
+ return defineView(createMaterializationStartBuilder(id, definedModel));
146
+ },
147
+ chart(id, defineChart) {
148
+ return compileModelChart(definedModel, id, defineChart);
149
+ },
150
+ validateData(data) {
151
+ try {
152
+ validateModelRuntimeData(state, data);
153
+ } catch (error) {
154
+ rewriteInferredRelationshipError(getModelRuntimeMetadata(definedModel), error);
155
+ }
156
+ },
157
+ build() {
158
+ return definedModel;
159
+ },
160
+ __dataModelBrand: "data-model-definition"
161
+ };
162
+ attachModelRuntimeMetadata(definedModel, { inferredRelationships: new Map([...state.inferredRelationshipIds].map((relationshipId) => {
163
+ const relationship = state.relationships[relationshipId];
164
+ return [relationshipId, {
165
+ id: relationshipId,
166
+ fromDataset: relationship.from.dataset,
167
+ fromKey: relationship.from.key,
168
+ toDataset: relationship.to.dataset,
169
+ toColumn: relationship.to.column
170
+ }];
171
+ })) });
172
+ cachedModel = definedModel;
173
+ return definedModel;
174
+ };
175
+ return build();
176
+ }
177
+ function createDataModelBuilder(state = {
178
+ datasets: {},
179
+ relationships: {},
180
+ associations: {},
181
+ attributes: {},
182
+ inferredRelationshipIds: /* @__PURE__ */ new Set()
183
+ }) {
184
+ let cachedModel;
185
+ return {
186
+ dataset(id, dataset) {
187
+ assertUniqueId(state.datasets, "dataset", id);
188
+ const resolvedDataset = resolveDatasetDefinition(dataset);
189
+ if (!resolvedDataset.key || resolvedDataset.key.length === 0) throw new Error(`Dataset "${id}" must declare a .key() before being added to a data model.`);
190
+ return createDataModelBuilder({
191
+ ...state,
192
+ datasets: {
193
+ ...state.datasets,
194
+ [id]: resolvedDataset
195
+ }
196
+ });
197
+ },
198
+ relationship(id, config) {
199
+ assertUniqueId(state.relationships, "relationship", id);
200
+ const fromKeyId = getSingleDatasetKey(getDatasetOrThrow(state.datasets, config.from.dataset), config.from.dataset);
201
+ getDatasetOrThrow(state.datasets, config.to.dataset);
202
+ if (config.from.key !== fromKeyId) throw new Error(`Relationship "${id}" must use declared key "${fromKeyId}" from dataset "${config.from.dataset}".`);
203
+ return createDataModelBuilder({
204
+ ...state,
205
+ relationships: {
206
+ ...state.relationships,
207
+ [id]: {
208
+ kind: "relationship",
209
+ id,
210
+ from: config.from,
211
+ to: config.to,
212
+ reverse: {
213
+ dataset: config.to.dataset,
214
+ column: config.to.column,
215
+ to: {
216
+ dataset: config.from.dataset,
217
+ key: config.from.key
218
+ }
219
+ }
220
+ }
221
+ }
222
+ });
223
+ },
224
+ association(id, config) {
225
+ assertUniqueId(state.associations, "association", id);
226
+ const fromDataset = getDatasetOrThrow(state.datasets, config.from.dataset);
227
+ const toDataset = getDatasetOrThrow(state.datasets, config.to.dataset);
228
+ const fromKeyId = getSingleDatasetKey(fromDataset, config.from.dataset);
229
+ const toKeyId = getSingleDatasetKey(toDataset, config.to.dataset);
230
+ if (config.from.key !== fromKeyId) throw new Error(`Association "${id}" must use declared key "${fromKeyId}" from dataset "${config.from.dataset}".`);
231
+ if (config.to.key !== toKeyId) throw new Error(`Association "${id}" must use declared key "${toKeyId}" from dataset "${config.to.dataset}".`);
232
+ if ("deriveFrom" in config) {
233
+ if (config.deriveFrom.dataset !== config.from.dataset && config.deriveFrom.dataset !== config.to.dataset) throw new Error(`Association "${id}" deriveFrom.dataset must match either "${config.from.dataset}" or "${config.to.dataset}".`);
234
+ }
235
+ return createDataModelBuilder({
236
+ ...state,
237
+ associations: {
238
+ ...state.associations,
239
+ [id]: {
240
+ kind: "association",
241
+ id,
242
+ from: config.from,
243
+ to: config.to,
244
+ reverse: {
245
+ dataset: config.to.dataset,
246
+ key: config.to.key,
247
+ to: {
248
+ dataset: config.from.dataset,
249
+ key: config.from.key
250
+ }
251
+ },
252
+ edge: "deriveFrom" in config ? {
253
+ kind: "derived",
254
+ deriveFrom: config.deriveFrom
255
+ } : {
256
+ kind: "explicit",
257
+ data: config.data,
258
+ columns: config.columns
259
+ }
260
+ }
261
+ }
262
+ });
263
+ },
264
+ attribute(id, config) {
265
+ assertUniqueId(state.attributes, "attribute", id);
266
+ return createDataModelBuilder({
267
+ ...state,
268
+ attributes: {
269
+ ...state.attributes,
270
+ [id]: {
271
+ id,
272
+ kind: config.kind,
273
+ source: config.source,
274
+ targets: config.targets
275
+ }
276
+ }
277
+ });
278
+ },
279
+ infer(options) {
280
+ const nextRelationships = options.relationships === true ? inferModelRelationships(state.datasets, state.relationships, new Set(options.exclude ?? [])) : {
281
+ relationships: state.relationships,
282
+ metadata: { inferredRelationships: /* @__PURE__ */ new Map() }
283
+ };
284
+ const nextAttributes = options.attributes === true ? inferModelAttributes(state.datasets, nextRelationships.relationships, state.attributes) : state.attributes;
285
+ return createDataModelBuilder({
286
+ ...state,
287
+ relationships: nextRelationships.relationships,
288
+ attributes: nextAttributes,
289
+ inferredRelationshipIds: new Set([...state.inferredRelationshipIds, ...nextRelationships.metadata.inferredRelationships.keys()])
290
+ });
291
+ },
292
+ chart(id, defineChart) {
293
+ const model = cachedModel ?? createDefinedDataModel(state);
294
+ cachedModel = model;
295
+ return compileModelChart(model, id, defineChart);
296
+ },
297
+ materialize(id, defineView) {
298
+ const model = cachedModel ?? createDefinedDataModel(state);
299
+ cachedModel = model;
300
+ return defineView(createMaterializationStartBuilder(id, model));
301
+ },
302
+ validateData(data) {
303
+ try {
304
+ validateModelRuntimeData(state, data);
305
+ } catch (error) {
306
+ const model = cachedModel ?? createDefinedDataModel(state);
307
+ cachedModel = model;
308
+ rewriteInferredRelationshipError(getModelRuntimeMetadata(model), error);
309
+ }
310
+ },
311
+ build() {
312
+ if (cachedModel) return cachedModel;
313
+ cachedModel = createDefinedDataModel(state);
314
+ return cachedModel;
315
+ }
316
+ };
317
+ }
318
+ /**
319
+ * Define one linked data model for dataset registration, relationships,
320
+ * associations, reusable model-level filter attributes, and explicit
321
+ * Phase 7 materialized views.
322
+ */
323
+ function defineDataModel() {
324
+ return createDataModelBuilder();
325
+ }
326
+ //#endregion
327
+ export { defineDataModel };