@matthieumordrel/chart-studio 0.2.0 → 0.2.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.
- package/README.md +1 -1
- package/dist/core/chart-capabilities.d.mts +48 -0
- package/dist/core/chart-capabilities.mjs +55 -0
- package/dist/core/{colors.d.ts → colors.d.mts} +5 -3
- package/dist/core/colors.mjs +55 -0
- package/dist/core/config-utils.mjs +79 -0
- package/dist/core/date-utils.mjs +49 -0
- package/dist/core/define-chart-schema.d.mts +106 -0
- package/dist/core/define-chart-schema.mjs +47 -0
- package/dist/core/formatting.mjs +349 -0
- package/dist/core/infer-columns.d.mts +9 -0
- package/dist/core/infer-columns.mjs +481 -0
- package/dist/core/metric-utils.d.mts +13 -0
- package/dist/core/metric-utils.mjs +121 -0
- package/dist/core/pipeline-data-points.mjs +212 -0
- package/dist/core/pipeline-helpers.mjs +85 -0
- package/dist/core/{pipeline.d.ts → pipeline.d.mts} +21 -24
- package/dist/core/pipeline.mjs +153 -0
- package/dist/core/types.d.mts +957 -0
- package/dist/core/use-chart-options.d.mts +64 -0
- package/dist/core/use-chart-options.mjs +7 -0
- package/dist/core/use-chart-resolvers.mjs +34 -0
- package/dist/core/{use-chart.d.ts → use-chart.d.mts} +12 -9
- package/dist/core/use-chart.mjs +299 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +8 -0
- package/dist/ui/chart-axis-ticks.mjs +65 -0
- package/dist/ui/{chart-canvas.d.ts → chart-canvas.d.mts} +13 -6
- package/dist/ui/chart-canvas.mjs +461 -0
- package/dist/ui/chart-context.d.mts +92 -0
- package/dist/ui/chart-context.mjs +112 -0
- package/dist/ui/{chart-date-range-badge.d.ts → chart-date-range-badge.d.mts} +10 -4
- package/dist/ui/chart-date-range-badge.mjs +49 -0
- package/dist/ui/chart-date-range-panel.d.mts +18 -0
- package/dist/ui/chart-date-range-panel.mjs +208 -0
- package/dist/ui/{chart-date-range.d.ts → chart-date-range.d.mts} +10 -4
- package/dist/ui/chart-date-range.mjs +67 -0
- package/dist/ui/chart-debug.d.mts +17 -0
- package/dist/ui/chart-debug.mjs +169 -0
- package/dist/ui/chart-dropdown.mjs +92 -0
- package/dist/ui/{chart-filters-panel.d.ts → chart-filters-panel.d.mts} +12 -5
- package/dist/ui/chart-filters-panel.mjs +132 -0
- package/dist/ui/{chart-filters.d.ts → chart-filters.d.mts} +10 -4
- package/dist/ui/chart-filters.mjs +48 -0
- package/dist/ui/chart-group-by-selector.d.mts +14 -0
- package/dist/ui/chart-group-by-selector.mjs +29 -0
- package/dist/ui/{chart-metric-panel.d.ts → chart-metric-panel.d.mts} +12 -5
- package/dist/ui/chart-metric-panel.mjs +172 -0
- package/dist/ui/chart-metric-selector.d.mts +16 -0
- package/dist/ui/chart-metric-selector.mjs +50 -0
- package/dist/ui/chart-select.mjs +62 -0
- package/dist/ui/{chart-source-switcher.d.ts → chart-source-switcher.d.mts} +10 -4
- package/dist/ui/chart-source-switcher.mjs +54 -0
- package/dist/ui/chart-time-bucket-selector.d.mts +15 -0
- package/dist/ui/chart-time-bucket-selector.mjs +34 -0
- package/dist/ui/chart-toolbar-overflow.d.mts +28 -0
- package/dist/ui/chart-toolbar-overflow.mjs +209 -0
- package/dist/ui/chart-toolbar.d.mts +29 -0
- package/dist/ui/chart-toolbar.mjs +56 -0
- package/dist/ui/chart-type-selector.d.mts +14 -0
- package/dist/ui/chart-type-selector.mjs +33 -0
- package/dist/ui/chart-x-axis-selector.d.mts +14 -0
- package/dist/ui/chart-x-axis-selector.mjs +25 -0
- package/dist/ui/index.d.mts +19 -0
- package/dist/ui/index.mjs +18 -0
- package/dist/ui/toolbar-types.d.mts +7 -0
- package/dist/ui/toolbar-types.mjs +83 -0
- package/package.json +11 -10
- package/dist/core/chart-capabilities.d.ts +0 -60
- package/dist/core/chart-capabilities.d.ts.map +0 -1
- package/dist/core/chart-capabilities.js +0 -54
- package/dist/core/colors.d.ts.map +0 -1
- package/dist/core/colors.js +0 -54
- package/dist/core/config-utils.d.ts +0 -43
- package/dist/core/config-utils.d.ts.map +0 -1
- package/dist/core/config-utils.js +0 -80
- package/dist/core/date-utils.d.ts +0 -29
- package/dist/core/date-utils.d.ts.map +0 -1
- package/dist/core/date-utils.js +0 -58
- package/dist/core/define-chart-schema.d.ts +0 -105
- package/dist/core/define-chart-schema.d.ts.map +0 -1
- package/dist/core/define-chart-schema.js +0 -44
- package/dist/core/formatting.d.ts +0 -47
- package/dist/core/formatting.d.ts.map +0 -1
- package/dist/core/formatting.js +0 -396
- package/dist/core/index.d.ts +0 -17
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -12
- package/dist/core/infer-columns.d.ts +0 -6
- package/dist/core/infer-columns.d.ts.map +0 -1
- package/dist/core/infer-columns.js +0 -512
- package/dist/core/metric-utils.d.ts +0 -43
- package/dist/core/metric-utils.d.ts.map +0 -1
- package/dist/core/metric-utils.js +0 -141
- package/dist/core/pipeline-data-points.d.ts +0 -23
- package/dist/core/pipeline-data-points.d.ts.map +0 -1
- package/dist/core/pipeline-data-points.js +0 -235
- package/dist/core/pipeline-helpers.d.ts +0 -38
- package/dist/core/pipeline-helpers.d.ts.map +0 -1
- package/dist/core/pipeline-helpers.js +0 -97
- package/dist/core/pipeline.d.ts.map +0 -1
- package/dist/core/pipeline.js +0 -156
- package/dist/core/types.d.ts +0 -1109
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js +0 -14
- package/dist/core/use-chart-options.d.ts +0 -66
- package/dist/core/use-chart-options.d.ts.map +0 -1
- package/dist/core/use-chart-options.js +0 -4
- package/dist/core/use-chart-resolvers.d.ts +0 -14
- package/dist/core/use-chart-resolvers.d.ts.map +0 -1
- package/dist/core/use-chart-resolvers.js +0 -41
- package/dist/core/use-chart.d.ts.map +0 -1
- package/dist/core/use-chart.js +0 -265
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -35
- package/dist/ui/chart-axis-ticks.d.ts +0 -35
- package/dist/ui/chart-axis-ticks.d.ts.map +0 -1
- package/dist/ui/chart-axis-ticks.js +0 -79
- package/dist/ui/chart-canvas.d.ts.map +0 -1
- package/dist/ui/chart-canvas.js +0 -337
- package/dist/ui/chart-context.d.ts +0 -89
- package/dist/ui/chart-context.d.ts.map +0 -1
- package/dist/ui/chart-context.js +0 -128
- package/dist/ui/chart-date-range-badge.d.ts.map +0 -1
- package/dist/ui/chart-date-range-badge.js +0 -30
- package/dist/ui/chart-date-range-panel.d.ts +0 -25
- package/dist/ui/chart-date-range-panel.d.ts.map +0 -1
- package/dist/ui/chart-date-range-panel.js +0 -125
- package/dist/ui/chart-date-range.d.ts.map +0 -1
- package/dist/ui/chart-date-range.js +0 -37
- package/dist/ui/chart-debug.d.ts +0 -10
- package/dist/ui/chart-debug.d.ts.map +0 -1
- package/dist/ui/chart-debug.js +0 -126
- package/dist/ui/chart-dropdown.d.ts +0 -35
- package/dist/ui/chart-dropdown.d.ts.map +0 -1
- package/dist/ui/chart-dropdown.js +0 -76
- package/dist/ui/chart-filters-panel.d.ts.map +0 -1
- package/dist/ui/chart-filters-panel.js +0 -46
- package/dist/ui/chart-filters.d.ts.map +0 -1
- package/dist/ui/chart-filters.js +0 -26
- package/dist/ui/chart-group-by-selector.d.ts +0 -8
- package/dist/ui/chart-group-by-selector.d.ts.map +0 -1
- package/dist/ui/chart-group-by-selector.js +0 -19
- package/dist/ui/chart-metric-panel.d.ts.map +0 -1
- package/dist/ui/chart-metric-panel.js +0 -118
- package/dist/ui/chart-metric-selector.d.ts +0 -10
- package/dist/ui/chart-metric-selector.d.ts.map +0 -1
- package/dist/ui/chart-metric-selector.js +0 -27
- package/dist/ui/chart-select.d.ts +0 -25
- package/dist/ui/chart-select.d.ts.map +0 -1
- package/dist/ui/chart-select.js +0 -35
- package/dist/ui/chart-source-switcher.d.ts.map +0 -1
- package/dist/ui/chart-source-switcher.js +0 -31
- package/dist/ui/chart-time-bucket-selector.d.ts +0 -9
- package/dist/ui/chart-time-bucket-selector.d.ts.map +0 -1
- package/dist/ui/chart-time-bucket-selector.js +0 -25
- package/dist/ui/chart-toolbar-overflow.d.ts +0 -29
- package/dist/ui/chart-toolbar-overflow.d.ts.map +0 -1
- package/dist/ui/chart-toolbar-overflow.js +0 -109
- package/dist/ui/chart-toolbar.d.ts +0 -45
- package/dist/ui/chart-toolbar.d.ts.map +0 -1
- package/dist/ui/chart-toolbar.js +0 -44
- package/dist/ui/chart-type-selector.d.ts +0 -8
- package/dist/ui/chart-type-selector.d.ts.map +0 -1
- package/dist/ui/chart-type-selector.js +0 -22
- package/dist/ui/chart-x-axis-selector.d.ts +0 -8
- package/dist/ui/chart-x-axis-selector.d.ts.map +0 -1
- package/dist/ui/chart-x-axis-selector.js +0 -14
- package/dist/ui/index.d.ts +0 -25
- package/dist/ui/index.d.ts.map +0 -1
- package/dist/ui/index.js +0 -23
- package/dist/ui/toolbar-types.d.ts +0 -43
- package/dist/ui/toolbar-types.d.ts.map +0 -1
- package/dist/ui/toolbar-types.js +0 -50
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { isAggregateMetric } from "./metric-utils.mjs";
|
|
2
|
+
import { aggregate, dateBucketKey, dateBucketLabel, getStringValue } from "./pipeline-helpers.mjs";
|
|
3
|
+
//#region src/core/pipeline-data-points.ts
|
|
4
|
+
/**
|
|
5
|
+
* Bucket and aggregation steps for the chart transformation pipeline.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Build chart-ready data points by bucketing the X-axis, pivoting groups, and
|
|
9
|
+
* aggregating the selected metric.
|
|
10
|
+
*
|
|
11
|
+
* @param items - Filtered source data
|
|
12
|
+
* @param xColumn - Active X-axis column
|
|
13
|
+
* @param groupByColumn - Optional group-by column
|
|
14
|
+
* @param metric - Selected metric configuration
|
|
15
|
+
* @param numberColumns - Number columns available for metric lookup
|
|
16
|
+
* @param timeBucket - Time bucket used for date X-axes
|
|
17
|
+
* @returns Aggregated chart data points and group labels
|
|
18
|
+
*/
|
|
19
|
+
function buildDataPoints(items, xColumn, groupByColumn, metric, numberColumns, timeBucket) {
|
|
20
|
+
const groupSet = /* @__PURE__ */ new Set();
|
|
21
|
+
if (groupByColumn) for (const item of items) groupSet.add(getStringValue(item, groupByColumn));
|
|
22
|
+
const groups = groupByColumn ? [...groupSet].toSorted() : ["value"];
|
|
23
|
+
const metricColumn = isAggregateMetric(metric) ? numberColumns.find((column) => column.id === metric.columnId) ?? null : null;
|
|
24
|
+
if (xColumn.type === "date") return buildTimeBuckets(items, xColumn, groupByColumn, groups, metric, metricColumn, timeBucket);
|
|
25
|
+
return buildCategoryBuckets(items, xColumn, groupByColumn, groups, metric, metricColumn);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate a continuous sequence of date buckets from the minimum to maximum
|
|
29
|
+
* date found in the data.
|
|
30
|
+
*
|
|
31
|
+
* @param items - Filtered source data
|
|
32
|
+
* @param xColumn - Active date column
|
|
33
|
+
* @param bucket - Time bucket granularity
|
|
34
|
+
* @returns Every bucket between the first and last date in the dataset
|
|
35
|
+
*/
|
|
36
|
+
function generateBucketsFromData(items, xColumn, bucket) {
|
|
37
|
+
let min = null;
|
|
38
|
+
let max = null;
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
const rawValue = xColumn.accessor(item);
|
|
41
|
+
if (rawValue == null) continue;
|
|
42
|
+
const date = new Date(rawValue);
|
|
43
|
+
if (Number.isNaN(date.getTime())) continue;
|
|
44
|
+
if (!min || date < min) min = date;
|
|
45
|
+
if (!max || date > max) max = date;
|
|
46
|
+
}
|
|
47
|
+
if (!min || !max) return [];
|
|
48
|
+
const buckets = [];
|
|
49
|
+
const cursor = startOfBucket(min, bucket);
|
|
50
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
51
|
+
const maxBuckets = 500;
|
|
52
|
+
for (let bucketCount = 0; bucketCount < maxBuckets; bucketCount += 1) {
|
|
53
|
+
if (cursor > max) break;
|
|
54
|
+
const key = dateBucketKey(cursor, bucket);
|
|
55
|
+
if (!seenKeys.has(key)) {
|
|
56
|
+
seenKeys.add(key);
|
|
57
|
+
buckets.push({
|
|
58
|
+
key,
|
|
59
|
+
label: dateBucketLabel(key, bucket)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
advanceBucketCursor(cursor, bucket);
|
|
63
|
+
}
|
|
64
|
+
return buckets;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Normalize a date to the start of its containing bucket.
|
|
68
|
+
*
|
|
69
|
+
* @param date - Source date
|
|
70
|
+
* @param bucket - Time bucket granularity
|
|
71
|
+
* @returns Start boundary for the bucket containing the source date
|
|
72
|
+
*/
|
|
73
|
+
function startOfBucket(date, bucket) {
|
|
74
|
+
const normalized = new Date(date);
|
|
75
|
+
normalized.setHours(0, 0, 0, 0);
|
|
76
|
+
switch (bucket) {
|
|
77
|
+
case "day": return normalized;
|
|
78
|
+
case "week": {
|
|
79
|
+
const day = normalized.getDay();
|
|
80
|
+
normalized.setDate(normalized.getDate() - (day + 6) % 7);
|
|
81
|
+
return normalized;
|
|
82
|
+
}
|
|
83
|
+
case "month":
|
|
84
|
+
normalized.setDate(1);
|
|
85
|
+
return normalized;
|
|
86
|
+
case "quarter":
|
|
87
|
+
normalized.setMonth(Math.floor(normalized.getMonth() / 3) * 3, 1);
|
|
88
|
+
return normalized;
|
|
89
|
+
case "year":
|
|
90
|
+
normalized.setMonth(0, 1);
|
|
91
|
+
return normalized;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Advance a mutable cursor by a single bucket interval.
|
|
96
|
+
*
|
|
97
|
+
* @param cursor - Current date cursor
|
|
98
|
+
* @param bucket - Time bucket granularity
|
|
99
|
+
*/
|
|
100
|
+
function advanceBucketCursor(cursor, bucket) {
|
|
101
|
+
switch (bucket) {
|
|
102
|
+
case "day":
|
|
103
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
104
|
+
break;
|
|
105
|
+
case "week":
|
|
106
|
+
cursor.setDate(cursor.getDate() + 7);
|
|
107
|
+
break;
|
|
108
|
+
case "month":
|
|
109
|
+
cursor.setMonth(cursor.getMonth() + 1);
|
|
110
|
+
break;
|
|
111
|
+
case "quarter":
|
|
112
|
+
cursor.setMonth(cursor.getMonth() + 3);
|
|
113
|
+
break;
|
|
114
|
+
case "year":
|
|
115
|
+
cursor.setFullYear(cursor.getFullYear() + 1);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build data points for a date X-axis.
|
|
121
|
+
*
|
|
122
|
+
* @param items - Filtered source data
|
|
123
|
+
* @param xColumn - Active date column
|
|
124
|
+
* @param groupByColumn - Optional group-by column
|
|
125
|
+
* @param groups - Resolved group labels
|
|
126
|
+
* @param metric - Selected metric configuration
|
|
127
|
+
* @param metricColumn - Resolved numeric metric column
|
|
128
|
+
* @param timeBucket - Time bucket granularity
|
|
129
|
+
* @returns Aggregated date-bucketed data points
|
|
130
|
+
*/
|
|
131
|
+
function buildTimeBuckets(items, xColumn, groupByColumn, groups, metric, metricColumn, timeBucket) {
|
|
132
|
+
const allBuckets = generateBucketsFromData(items, xColumn, timeBucket);
|
|
133
|
+
const accumulator = /* @__PURE__ */ new Map();
|
|
134
|
+
for (const { key } of allBuckets) {
|
|
135
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
136
|
+
for (const group of groups) groupMap.set(group, []);
|
|
137
|
+
accumulator.set(key, groupMap);
|
|
138
|
+
}
|
|
139
|
+
for (const item of items) {
|
|
140
|
+
const rawValue = xColumn.accessor(item);
|
|
141
|
+
if (rawValue == null) continue;
|
|
142
|
+
const key = dateBucketKey(new Date(rawValue), timeBucket);
|
|
143
|
+
const groupMap = accumulator.get(key);
|
|
144
|
+
if (!groupMap) continue;
|
|
145
|
+
const group = groupByColumn ? getStringValue(item, groupByColumn) : "value";
|
|
146
|
+
const values = groupMap.get(group);
|
|
147
|
+
if (!values) continue;
|
|
148
|
+
if (metricColumn) {
|
|
149
|
+
const metricValue = metricColumn.accessor(item);
|
|
150
|
+
if (metricValue != null) values.push(metricValue);
|
|
151
|
+
} else values.push(1);
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
data: allBuckets.map(({ key, label }) => {
|
|
155
|
+
const point = {
|
|
156
|
+
xLabel: label,
|
|
157
|
+
xKey: key
|
|
158
|
+
};
|
|
159
|
+
const groupMap = accumulator.get(key);
|
|
160
|
+
for (const group of groups) point[group] = aggregate(groupMap.get(group) ?? [], metric.kind === "aggregate" ? metric.aggregate : "count", metric.kind === "aggregate" ? metric.includeZeros ?? true : true);
|
|
161
|
+
return point;
|
|
162
|
+
}),
|
|
163
|
+
groups
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Build data points for a categorical or boolean X-axis.
|
|
168
|
+
*
|
|
169
|
+
* @param items - Filtered source data
|
|
170
|
+
* @param xColumn - Active X-axis column
|
|
171
|
+
* @param groupByColumn - Optional group-by column
|
|
172
|
+
* @param groups - Resolved group labels
|
|
173
|
+
* @param metric - Selected metric configuration
|
|
174
|
+
* @param metricColumn - Resolved numeric metric column
|
|
175
|
+
* @returns Aggregated category-bucketed data points
|
|
176
|
+
*/
|
|
177
|
+
function buildCategoryBuckets(items, xColumn, groupByColumn, groups, metric, metricColumn) {
|
|
178
|
+
const xValues = /* @__PURE__ */ new Set();
|
|
179
|
+
for (const item of items) xValues.add(getStringValue(item, xColumn));
|
|
180
|
+
const accumulator = /* @__PURE__ */ new Map();
|
|
181
|
+
for (const xValue of xValues) {
|
|
182
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
183
|
+
for (const group of groups) groupMap.set(group, []);
|
|
184
|
+
accumulator.set(xValue, groupMap);
|
|
185
|
+
}
|
|
186
|
+
for (const item of items) {
|
|
187
|
+
const xValue = getStringValue(item, xColumn);
|
|
188
|
+
const groupMap = accumulator.get(xValue);
|
|
189
|
+
if (!groupMap) continue;
|
|
190
|
+
const group = groupByColumn ? getStringValue(item, groupByColumn) : "value";
|
|
191
|
+
const values = groupMap.get(group);
|
|
192
|
+
if (!values) continue;
|
|
193
|
+
if (metricColumn) {
|
|
194
|
+
const metricValue = metricColumn.accessor(item);
|
|
195
|
+
if (metricValue != null) values.push(metricValue);
|
|
196
|
+
} else values.push(1);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
data: [...xValues].map((xValue) => {
|
|
200
|
+
const point = {
|
|
201
|
+
xLabel: xValue,
|
|
202
|
+
xKey: xValue
|
|
203
|
+
};
|
|
204
|
+
const groupMap = accumulator.get(xValue);
|
|
205
|
+
for (const group of groups) point[group] = aggregate(groupMap.get(group) ?? [], metric.kind === "aggregate" ? metric.aggregate : "count", metric.kind === "aggregate" ? metric.includeZeros ?? true : true);
|
|
206
|
+
return point;
|
|
207
|
+
}),
|
|
208
|
+
groups
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
export { buildDataPoints };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { formatTimeBucketLabel } from "./formatting.mjs";
|
|
2
|
+
//#region src/core/pipeline-helpers.ts
|
|
3
|
+
/**
|
|
4
|
+
* Shared helpers for the chart transformation pipeline.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Format a stable key for a date bucket.
|
|
8
|
+
*
|
|
9
|
+
* @param date - Source date
|
|
10
|
+
* @param bucket - Time bucket granularity
|
|
11
|
+
* @returns Machine-friendly bucket key
|
|
12
|
+
*/
|
|
13
|
+
function dateBucketKey(date, bucket) {
|
|
14
|
+
const year = date.getFullYear();
|
|
15
|
+
const month = date.getMonth();
|
|
16
|
+
switch (bucket) {
|
|
17
|
+
case "day": return `${year}-${String(month + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
18
|
+
case "week": {
|
|
19
|
+
const day = date.getDay();
|
|
20
|
+
const monday = new Date(date);
|
|
21
|
+
monday.setDate(date.getDate() - (day + 6) % 7);
|
|
22
|
+
return `${monday.getFullYear()}-${String(monday.getMonth() + 1).padStart(2, "0")}-${String(monday.getDate()).padStart(2, "0")}`;
|
|
23
|
+
}
|
|
24
|
+
case "month": return `${year}-${String(month + 1).padStart(2, "0")}`;
|
|
25
|
+
case "quarter": return `${year}-Q${Math.floor(month / 3) + 1}`;
|
|
26
|
+
case "year": return `${year}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Format a bucket key into a human-readable label.
|
|
31
|
+
*
|
|
32
|
+
* @param key - Bucket key
|
|
33
|
+
* @param bucket - Time bucket granularity
|
|
34
|
+
* @returns Display label for the chart axis
|
|
35
|
+
*/
|
|
36
|
+
function dateBucketLabel(key, bucket) {
|
|
37
|
+
return formatTimeBucketLabel(key, bucket, "axis");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract a comparable string value from an item using a column definition.
|
|
41
|
+
*
|
|
42
|
+
* @param item - Raw data item
|
|
43
|
+
* @param column - Column definition to read from
|
|
44
|
+
* @returns String value used by filters and groups
|
|
45
|
+
*/
|
|
46
|
+
function getStringValue(item, column) {
|
|
47
|
+
switch (column.type) {
|
|
48
|
+
case "boolean": {
|
|
49
|
+
const value = column.accessor(item);
|
|
50
|
+
if (value === true) return column.trueLabel;
|
|
51
|
+
if (value === false) return column.falseLabel;
|
|
52
|
+
return "Unknown";
|
|
53
|
+
}
|
|
54
|
+
case "category": return column.accessor(item) ?? "Unknown";
|
|
55
|
+
case "date": {
|
|
56
|
+
const value = column.accessor(item);
|
|
57
|
+
return value != null ? String(value) : "Unknown";
|
|
58
|
+
}
|
|
59
|
+
case "number": {
|
|
60
|
+
const value = column.accessor(item);
|
|
61
|
+
return value != null ? String(value) : "Unknown";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Aggregate numeric values using the requested function.
|
|
67
|
+
*
|
|
68
|
+
* @param values - Numeric values to aggregate
|
|
69
|
+
* @param fn - Aggregation strategy
|
|
70
|
+
* @param includeZeros - Whether zero values should participate in avg/min/max
|
|
71
|
+
* @returns Aggregated numeric result
|
|
72
|
+
*/
|
|
73
|
+
function aggregate(values, fn, includeZeros = true) {
|
|
74
|
+
if (fn === "count") return values.length;
|
|
75
|
+
const effectiveValues = !includeZeros && fn !== "sum" ? values.filter((value) => value !== 0) : values;
|
|
76
|
+
if (effectiveValues.length === 0) return 0;
|
|
77
|
+
switch (fn) {
|
|
78
|
+
case "sum": return effectiveValues.reduce((sum, value) => sum + value, 0);
|
|
79
|
+
case "avg": return effectiveValues.reduce((sum, value) => sum + value, 0) / effectiveValues.length;
|
|
80
|
+
case "min": return Math.min(...effectiveValues);
|
|
81
|
+
case "max": return Math.max(...effectiveValues);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { aggregate, dateBucketKey, dateBucketLabel, getStringValue };
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Pure functions that transform raw data into chart-ready format.
|
|
5
|
-
* Pipeline: Raw Data -> Filter -> Bucket by X-axis -> Pivot by groupBy -> Aggregate -> Sort
|
|
6
|
-
*/
|
|
7
|
-
import type { AvailableFilter, ChartColumn, ChartSeries, FilterState, Metric, SortConfig, TimeBucket, TransformedDataPoint } from './types.js';
|
|
1
|
+
import { AvailableFilter, ChartColumn, ChartSeries, FilterState, Metric, SortConfig, TimeBucket, TransformedDataPoint } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/core/pipeline.d.ts
|
|
8
4
|
/**
|
|
9
5
|
* Input for the transformation pipeline.
|
|
10
6
|
*
|
|
@@ -17,15 +13,15 @@ import type { AvailableFilter, ChartColumn, ChartSeries, FilterState, Metric, So
|
|
|
17
13
|
* @property filters - Active filter state
|
|
18
14
|
* @property sorting - Sort configuration
|
|
19
15
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
type PipelineInput<T, TColumnId extends string = string> = {
|
|
17
|
+
data: readonly T[];
|
|
18
|
+
columns: readonly ChartColumn<T, TColumnId>[];
|
|
19
|
+
xAxisId: TColumnId;
|
|
20
|
+
groupById: TColumnId | null;
|
|
21
|
+
metric: Metric<TColumnId>;
|
|
22
|
+
timeBucket: TimeBucket;
|
|
23
|
+
filters: FilterState<TColumnId>;
|
|
24
|
+
sorting: SortConfig | null;
|
|
29
25
|
};
|
|
30
26
|
/**
|
|
31
27
|
* Output of the transformation pipeline.
|
|
@@ -34,10 +30,10 @@ export type PipelineInput<T, TColumnId extends string = string> = {
|
|
|
34
30
|
* @property series - Series definitions for rendering
|
|
35
31
|
* @property groups - Unique group labels
|
|
36
32
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
type PipelineOutput = {
|
|
34
|
+
data: TransformedDataPoint[];
|
|
35
|
+
series: ChartSeries[];
|
|
36
|
+
groups: string[];
|
|
41
37
|
};
|
|
42
38
|
/**
|
|
43
39
|
* Apply active filters to the raw data.
|
|
@@ -48,7 +44,7 @@ export type PipelineOutput = {
|
|
|
48
44
|
* @param filters - Active filter state
|
|
49
45
|
* @returns Filtered data items
|
|
50
46
|
*/
|
|
51
|
-
|
|
47
|
+
declare function applyFilters<T, TColumnId extends string>(data: readonly T[], columns: readonly ChartColumn<T, TColumnId>[], filters: FilterState<TColumnId>): T[];
|
|
52
48
|
/**
|
|
53
49
|
* Run the full transformation pipeline.
|
|
54
50
|
*
|
|
@@ -57,7 +53,7 @@ export declare function applyFilters<T, TColumnId extends string>(data: readonly
|
|
|
57
53
|
* @param input - Pipeline configuration and source data
|
|
58
54
|
* @returns Transformed data, series metadata, and group labels
|
|
59
55
|
*/
|
|
60
|
-
|
|
56
|
+
declare function runPipeline<T, TColumnId extends string>(input: PipelineInput<T, TColumnId>): PipelineOutput;
|
|
61
57
|
/**
|
|
62
58
|
* Extract available filter options from data for every category and boolean
|
|
63
59
|
* column.
|
|
@@ -66,5 +62,6 @@ export declare function runPipeline<T, TColumnId extends string>(input: Pipeline
|
|
|
66
62
|
* @param columns - Column definitions
|
|
67
63
|
* @returns Filter metadata and option counts per filterable column
|
|
68
64
|
*/
|
|
69
|
-
|
|
70
|
-
//#
|
|
65
|
+
declare function extractAvailableFilters<T, TColumnId extends string>(data: readonly T[], columns: readonly ChartColumn<T, TColumnId>[]): AvailableFilter<TColumnId>[];
|
|
66
|
+
//#endregion
|
|
67
|
+
export { PipelineInput, PipelineOutput, applyFilters, extractAvailableFilters, runPipeline };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { getSeriesColor } from "./colors.mjs";
|
|
2
|
+
import { getMetricLabel } from "./metric-utils.mjs";
|
|
3
|
+
import { formatChartValue } from "./formatting.mjs";
|
|
4
|
+
import { getStringValue } from "./pipeline-helpers.mjs";
|
|
5
|
+
import { buildDataPoints } from "./pipeline-data-points.mjs";
|
|
6
|
+
//#region src/core/pipeline.ts
|
|
7
|
+
/**
|
|
8
|
+
* Data transformation pipeline.
|
|
9
|
+
*
|
|
10
|
+
* Pure functions that transform raw data into chart-ready format.
|
|
11
|
+
* Pipeline: Raw Data -> Filter -> Bucket by X-axis -> Pivot by groupBy -> Aggregate -> Sort
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Apply active filters to the raw data.
|
|
15
|
+
* Filters are AND-combined across columns and OR-combined within a column.
|
|
16
|
+
*
|
|
17
|
+
* @param data - Raw data items
|
|
18
|
+
* @param columns - Column definitions used to read values
|
|
19
|
+
* @param filters - Active filter state
|
|
20
|
+
* @returns Filtered data items
|
|
21
|
+
*/
|
|
22
|
+
function applyFilters(data, columns, filters) {
|
|
23
|
+
if (filters.size === 0) return [...data];
|
|
24
|
+
return data.filter((item) => {
|
|
25
|
+
for (const [columnId, activeValues] of filters) {
|
|
26
|
+
if (activeValues.size === 0) continue;
|
|
27
|
+
const column = columns.find((candidate) => candidate.id === columnId);
|
|
28
|
+
if (!column) continue;
|
|
29
|
+
if (!activeValues.has(getStringValue(item, column))) return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Apply sorting to transformed data points.
|
|
36
|
+
*
|
|
37
|
+
* @param data - Aggregated chart data points
|
|
38
|
+
* @param sorting - Explicit sort configuration
|
|
39
|
+
* @param isTimeSeries - Whether the X-axis is date-based
|
|
40
|
+
* @returns Sorted chart data points
|
|
41
|
+
*/
|
|
42
|
+
function applySorting(data, sorting, isTimeSeries) {
|
|
43
|
+
if (isTimeSeries) return data.toSorted((a, b) => String(a["xKey"]).localeCompare(String(b["xKey"])));
|
|
44
|
+
if (!sorting) return data.toSorted((a, b) => getPointTotal(b) - getPointTotal(a));
|
|
45
|
+
const direction = sorting.direction === "asc" ? 1 : -1;
|
|
46
|
+
return data.toSorted((a, b) => {
|
|
47
|
+
const aValue = a[sorting.key] ?? 0;
|
|
48
|
+
const bValue = b[sorting.key] ?? 0;
|
|
49
|
+
if (typeof aValue === "number" && typeof bValue === "number") return (aValue - bValue) * direction;
|
|
50
|
+
return String(aValue).localeCompare(String(bValue)) * direction;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Sum all numeric series values for a transformed data point.
|
|
55
|
+
*
|
|
56
|
+
* @param point - Transformed chart data point
|
|
57
|
+
* @returns Combined numeric total across series keys
|
|
58
|
+
*/
|
|
59
|
+
function getPointTotal(point) {
|
|
60
|
+
return Object.entries(point).filter(([key]) => key !== "xLabel" && key !== "xKey").reduce((sum, [, value]) => sum + (typeof value === "number" ? value : 0), 0);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build recharts series definitions from group labels.
|
|
64
|
+
*
|
|
65
|
+
* @param groups - Group labels returned by the pivot step
|
|
66
|
+
* @param metricLabel - Human-readable label for the selected metric
|
|
67
|
+
* @param useShadcn - Whether to use shadcn-compatible color variables
|
|
68
|
+
* @returns Recharts series metadata
|
|
69
|
+
*/
|
|
70
|
+
function buildSeries(groups, metricLabel, useShadcn = true) {
|
|
71
|
+
return groups.map((group, index) => ({
|
|
72
|
+
dataKey: group,
|
|
73
|
+
label: group === "value" ? metricLabel : group,
|
|
74
|
+
color: getSeriesColor(index, useShadcn)
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Run the full transformation pipeline.
|
|
79
|
+
*
|
|
80
|
+
* Raw Data -> Filter -> Bucket -> Pivot -> Aggregate -> Sort -> Output
|
|
81
|
+
*
|
|
82
|
+
* @param input - Pipeline configuration and source data
|
|
83
|
+
* @returns Transformed data, series metadata, and group labels
|
|
84
|
+
*/
|
|
85
|
+
function runPipeline(input) {
|
|
86
|
+
const { data, columns, xAxisId, groupById, metric, timeBucket, filters, sorting } = input;
|
|
87
|
+
const xColumn = columns.find((column) => column.id === xAxisId);
|
|
88
|
+
if (!xColumn) return {
|
|
89
|
+
data: [],
|
|
90
|
+
series: [],
|
|
91
|
+
groups: []
|
|
92
|
+
};
|
|
93
|
+
const groupByColumn = groupById ? columns.find((column) => column.id === groupById) ?? null : null;
|
|
94
|
+
const numberColumns = columns.filter((column) => column.type === "number");
|
|
95
|
+
const { data: points, groups } = buildDataPoints(applyFilters(data, columns, filters), xColumn, groupByColumn, metric, numberColumns, timeBucket);
|
|
96
|
+
return {
|
|
97
|
+
data: applySorting(points, sorting, xColumn.type === "date"),
|
|
98
|
+
series: buildSeries(groups, getMetricLabel(metric, columns)),
|
|
99
|
+
groups
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Extract available filter options from data for every category and boolean
|
|
104
|
+
* column.
|
|
105
|
+
*
|
|
106
|
+
* @param data - Raw data items
|
|
107
|
+
* @param columns - Column definitions
|
|
108
|
+
* @returns Filter metadata and option counts per filterable column
|
|
109
|
+
*/
|
|
110
|
+
function extractAvailableFilters(data, columns) {
|
|
111
|
+
return columns.filter((column) => column.type === "category" || column.type === "boolean").map((column) => {
|
|
112
|
+
const counts = /* @__PURE__ */ new Map();
|
|
113
|
+
for (const item of data) {
|
|
114
|
+
const value = getStringValue(item, column);
|
|
115
|
+
const formattedLabel = formatFilterOptionLabel(item, column);
|
|
116
|
+
const existing = counts.get(value);
|
|
117
|
+
if (existing) {
|
|
118
|
+
counts.set(value, {
|
|
119
|
+
count: existing.count + 1,
|
|
120
|
+
label: existing.label
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
counts.set(value, {
|
|
125
|
+
count: 1,
|
|
126
|
+
label: formattedLabel
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
columnId: column.id,
|
|
131
|
+
label: column.label,
|
|
132
|
+
type: column.type,
|
|
133
|
+
options: [...counts.entries()].toSorted(([, left], [, right]) => right.count - left.count).map(([value, option]) => ({
|
|
134
|
+
value,
|
|
135
|
+
label: option.label,
|
|
136
|
+
count: option.count
|
|
137
|
+
}))
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Derive one human-facing filter option label from the typed column value while
|
|
143
|
+
* keeping the underlying filter state keyed by the stable string value.
|
|
144
|
+
*/
|
|
145
|
+
function formatFilterOptionLabel(item, column) {
|
|
146
|
+
return formatChartValue(column.accessor(item), {
|
|
147
|
+
column,
|
|
148
|
+
surface: "tooltip",
|
|
149
|
+
item
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
export { applyFilters, extractAvailableFilters, runPipeline };
|