hazo_dataviz 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGE_LOG.md +54 -0
- package/README.md +115 -0
- package/SETUP_CHECKLIST.md +54 -0
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +11 -0
- package/dist/charts/line_chart.d.ts +14 -0
- package/dist/charts/line_chart.d.ts.map +1 -0
- package/dist/charts/line_chart.js +26 -0
- package/dist/charts/multi_line_chart.d.ts +13 -0
- package/dist/charts/multi_line_chart.d.ts.map +1 -0
- package/dist/charts/multi_line_chart.js +32 -0
- package/dist/hooks/use_data_query.d.ts +11 -0
- package/dist/hooks/use_data_query.d.ts.map +1 -0
- package/dist/hooks/use_data_query.js +35 -0
- package/dist/index.client.d.ts +17 -0
- package/dist/index.client.d.ts.map +1 -0
- package/dist/index.client.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/lib/auto_encode.d.ts +24 -0
- package/dist/lib/auto_encode.d.ts.map +1 -0
- package/dist/lib/auto_encode.js +46 -0
- package/dist/lib/contract-version.d.ts +2 -0
- package/dist/lib/contract-version.d.ts.map +1 -0
- package/dist/lib/contract-version.js +1 -0
- package/dist/lib/format.d.ts +5 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +31 -0
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/picker.d.ts +4 -0
- package/dist/lib/picker.d.ts.map +1 -0
- package/dist/lib/picker.js +20 -0
- package/dist/lib/runners/in_memory.d.ts +4 -0
- package/dist/lib/runners/in_memory.d.ts.map +1 -0
- package/dist/lib/runners/in_memory.js +303 -0
- package/dist/lib/shape.d.ts +11 -0
- package/dist/lib/shape.d.ts.map +1 -0
- package/dist/lib/shape.js +27 -0
- package/dist/schema/descriptor.d.ts +162 -0
- package/dist/schema/descriptor.d.ts.map +1 -0
- package/dist/schema/descriptor.js +56 -0
- package/dist/schema/query.d.ts +96 -0
- package/dist/schema/query.d.ts.map +1 -0
- package/dist/schema/query.js +37 -0
- package/dist/testing/fixtures.d.ts +163 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +59 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/types/index.d.ts +62 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/visualizers/area.d.ts +3 -0
- package/dist/visualizers/area.d.ts.map +1 -0
- package/dist/visualizers/area.js +66 -0
- package/dist/visualizers/bar.d.ts +3 -0
- package/dist/visualizers/bar.d.ts.map +1 -0
- package/dist/visualizers/bar.js +41 -0
- package/dist/visualizers/builtins.d.ts +3 -0
- package/dist/visualizers/builtins.d.ts.map +1 -0
- package/dist/visualizers/builtins.js +7 -0
- package/dist/visualizers/chart_frame.d.ts +97 -0
- package/dist/visualizers/chart_frame.d.ts.map +1 -0
- package/dist/visualizers/chart_frame.js +146 -0
- package/dist/visualizers/index.d.ts +7 -0
- package/dist/visualizers/index.d.ts.map +1 -0
- package/dist/visualizers/index.js +4 -0
- package/dist/visualizers/line.d.ts +3 -0
- package/dist/visualizers/line.d.ts.map +1 -0
- package/dist/visualizers/line.js +66 -0
- package/dist/visualizers/palette.d.ts +13 -0
- package/dist/visualizers/palette.d.ts.map +1 -0
- package/dist/visualizers/palette.js +24 -0
- package/dist/visualizers/single_stat.d.ts +3 -0
- package/dist/visualizers/single_stat.d.ts.map +1 -0
- package/dist/visualizers/single_stat.js +60 -0
- package/dist/visualizers/stacked_bar.d.ts +3 -0
- package/dist/visualizers/stacked_bar.d.ts.map +1 -0
- package/dist/visualizers/stacked_bar.js +75 -0
- package/dist/visualizers/table.d.ts +3 -0
- package/dist/visualizers/table.d.ts.map +1 -0
- package/dist/visualizers/table.js +55 -0
- package/dist/visualizers/visualizer_view.d.ts +12 -0
- package/dist/visualizers/visualizer_view.d.ts.map +1 -0
- package/dist/visualizers/visualizer_view.js +6 -0
- package/package.json +88 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { HazoError } from 'hazo_core';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Time-grain bucketing
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
function isoWeek(d) {
|
|
6
|
+
// ISO 8601: week starts Monday; week 1 = week containing first Thursday
|
|
7
|
+
const thursday = new Date(d);
|
|
8
|
+
thursday.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7) + 3);
|
|
9
|
+
const yearStart = new Date(Date.UTC(thursday.getUTCFullYear(), 0, 1));
|
|
10
|
+
const weekNum = Math.ceil(((thursday.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
|
11
|
+
return `${thursday.getUTCFullYear()}-W${String(weekNum).padStart(2, '0')}`;
|
|
12
|
+
}
|
|
13
|
+
function bucketDate(value, grain) {
|
|
14
|
+
const d = value instanceof Date ? value : new Date(String(value));
|
|
15
|
+
if (isNaN(d.getTime()))
|
|
16
|
+
return String(value);
|
|
17
|
+
const y = d.getUTCFullYear();
|
|
18
|
+
const m = d.getUTCMonth(); // 0-based
|
|
19
|
+
switch (grain) {
|
|
20
|
+
case 'day':
|
|
21
|
+
return `${y}-${String(m + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
|
|
22
|
+
case 'week':
|
|
23
|
+
return isoWeek(d);
|
|
24
|
+
case 'month':
|
|
25
|
+
return `${y}-${String(m + 1).padStart(2, '0')}`;
|
|
26
|
+
case 'quarter':
|
|
27
|
+
return `${y}-Q${Math.floor(m / 3) + 1}`;
|
|
28
|
+
case 'year':
|
|
29
|
+
return String(y);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Aggregation helpers (SQL null semantics — nulls are skipped)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
function numericValues(vals) {
|
|
36
|
+
return vals.filter((v) => v !== null && v !== undefined && typeof v === 'number' && !isNaN(v));
|
|
37
|
+
}
|
|
38
|
+
function aggregate(vals, agg, rawVals) {
|
|
39
|
+
if (agg === 'count')
|
|
40
|
+
return vals.filter((v) => v !== null && v !== undefined).length;
|
|
41
|
+
const nums = numericValues(vals);
|
|
42
|
+
if (nums.length === 0)
|
|
43
|
+
return null;
|
|
44
|
+
switch (agg) {
|
|
45
|
+
case 'sum': return nums.reduce((a, b) => a + b, 0);
|
|
46
|
+
case 'avg': return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
47
|
+
case 'min': return Math.min(...nums);
|
|
48
|
+
case 'max': return Math.max(...nums);
|
|
49
|
+
case 'last': {
|
|
50
|
+
// last = value at max raw value in the group (rawVals = temporal dim values)
|
|
51
|
+
if (!rawVals || rawVals.length === 0)
|
|
52
|
+
return nums[nums.length - 1] ?? null;
|
|
53
|
+
let maxRaw = rawVals[0];
|
|
54
|
+
let lastVal = vals[0];
|
|
55
|
+
for (let i = 1; i < rawVals.length; i++) {
|
|
56
|
+
const rv = rawVals[i];
|
|
57
|
+
const curr = typeof rv === 'string' || typeof rv === 'number' ? rv : String(rv);
|
|
58
|
+
const max = typeof maxRaw === 'string' || typeof maxRaw === 'number' ? maxRaw : String(maxRaw);
|
|
59
|
+
if (curr > max) {
|
|
60
|
+
maxRaw = rv;
|
|
61
|
+
lastVal = vals[i];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return lastVal ?? null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Filter application
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
function applyFilter(row, filter) {
|
|
72
|
+
const val = row[filter.field];
|
|
73
|
+
const { op, value } = filter;
|
|
74
|
+
switch (op) {
|
|
75
|
+
case 'is_null': return val === null || val === undefined;
|
|
76
|
+
case 'eq': return val === value;
|
|
77
|
+
case 'neq': return val !== value;
|
|
78
|
+
case 'gt': return typeof val === 'number' && typeof value === 'number' && val > value;
|
|
79
|
+
case 'gte': return typeof val === 'number' && typeof value === 'number' && val >= value;
|
|
80
|
+
case 'lt': return typeof val === 'number' && typeof value === 'number' && val < value;
|
|
81
|
+
case 'lte': return typeof val === 'number' && typeof value === 'number' && val <= value;
|
|
82
|
+
case 'in': return Array.isArray(value) && value.includes(val);
|
|
83
|
+
case 'between': {
|
|
84
|
+
if (!Array.isArray(value) || value.length !== 2)
|
|
85
|
+
return false;
|
|
86
|
+
const [lo, hi] = value;
|
|
87
|
+
if (typeof val !== 'number' || typeof lo !== 'number' || typeof hi !== 'number')
|
|
88
|
+
return false;
|
|
89
|
+
return val >= lo && val <= hi;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Runner factory
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
export function createInMemoryRunner(descriptor, rows) {
|
|
97
|
+
return async function runQuery(query) {
|
|
98
|
+
// --- Validate query fields against descriptor ---
|
|
99
|
+
const allDimFields = Object.entries(descriptor.fields)
|
|
100
|
+
.filter(([, f]) => f.role === 'dimension')
|
|
101
|
+
.map(([k]) => k);
|
|
102
|
+
const allMeasureFields = Object.entries(descriptor.fields)
|
|
103
|
+
.filter(([, f]) => f.role === 'measure')
|
|
104
|
+
.map(([k]) => k);
|
|
105
|
+
for (const dimField of query.dimensions) {
|
|
106
|
+
if (!(dimField in descriptor.fields)) {
|
|
107
|
+
throw new HazoError({
|
|
108
|
+
code: 'HAZO_DATAVIZ_UNKNOWN_FIELD',
|
|
109
|
+
pkg: 'hazo_dataviz',
|
|
110
|
+
message: `Unknown field: ${dimField}`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (!allDimFields.includes(dimField)) {
|
|
114
|
+
throw new HazoError({
|
|
115
|
+
code: 'HAZO_DATAVIZ_FIELD_ROLE_MISMATCH',
|
|
116
|
+
pkg: 'hazo_dataviz',
|
|
117
|
+
message: `Field "${dimField}" is a measure, cannot be used as a dimension`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const ref of query.measures) {
|
|
122
|
+
if (!(ref.field in descriptor.fields)) {
|
|
123
|
+
throw new HazoError({
|
|
124
|
+
code: 'HAZO_DATAVIZ_UNKNOWN_FIELD',
|
|
125
|
+
pkg: 'hazo_dataviz',
|
|
126
|
+
message: `Unknown field: ${ref.field}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (!allMeasureFields.includes(ref.field)) {
|
|
130
|
+
throw new HazoError({
|
|
131
|
+
code: 'HAZO_DATAVIZ_FIELD_ROLE_MISMATCH',
|
|
132
|
+
pkg: 'hazo_dataviz',
|
|
133
|
+
message: `Field "${ref.field}" is a dimension, cannot be used as a measure`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// --- Validate timeGrain ---
|
|
138
|
+
let temporalDimField = null;
|
|
139
|
+
if (query.timeGrain) {
|
|
140
|
+
const temporalDims = query.dimensions.filter((d) => {
|
|
141
|
+
const f = descriptor.fields[d];
|
|
142
|
+
return f?.role === 'dimension' && f.type === 'time';
|
|
143
|
+
});
|
|
144
|
+
if (temporalDims.length === 0) {
|
|
145
|
+
throw new HazoError({
|
|
146
|
+
code: 'HAZO_DATAVIZ_INVALID_TIME_GRAIN',
|
|
147
|
+
pkg: 'hazo_dataviz',
|
|
148
|
+
message: 'timeGrain requires a temporal dimension in the query',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
temporalDimField = temporalDims[0];
|
|
152
|
+
const fieldDef = descriptor.fields[temporalDimField];
|
|
153
|
+
if (fieldDef.role === 'dimension' && fieldDef.type === 'time') {
|
|
154
|
+
const declared = fieldDef.timeGrains ?? [];
|
|
155
|
+
if (!declared.includes(query.timeGrain)) {
|
|
156
|
+
throw new HazoError({
|
|
157
|
+
code: 'HAZO_DATAVIZ_INVALID_TIME_GRAIN',
|
|
158
|
+
pkg: 'hazo_dataviz',
|
|
159
|
+
message: `Time grain "${query.timeGrain}" not declared for field "${temporalDimField}". Declared: ${declared.join(', ')}`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// --- 1. Filter ---
|
|
165
|
+
let filtered = rows;
|
|
166
|
+
if (query.filters && query.filters.length > 0) {
|
|
167
|
+
filtered = rows.filter((row) => query.filters.every((f) => applyFilter(row, f)));
|
|
168
|
+
}
|
|
169
|
+
// Empty result after filter — not an error
|
|
170
|
+
if (filtered.length === 0) {
|
|
171
|
+
const columns = buildColumns(query, descriptor);
|
|
172
|
+
return { rows: [], columns, query };
|
|
173
|
+
}
|
|
174
|
+
// --- 2. Time-grain bucket ---
|
|
175
|
+
if (query.timeGrain && temporalDimField) {
|
|
176
|
+
const grain = query.timeGrain;
|
|
177
|
+
const field = temporalDimField;
|
|
178
|
+
filtered = filtered.map((row) => ({
|
|
179
|
+
...row,
|
|
180
|
+
[field]: bucketDate(row[field], grain),
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
// --- 3. Group ---
|
|
184
|
+
const groups = new Map();
|
|
185
|
+
// Find temporal dim for 'last' semantics — use the raw (pre-bucketed) values per group
|
|
186
|
+
// We capture raw values before bucketing by referencing the original rows in parallel
|
|
187
|
+
const rawRows = rows; // original, pre-filter; we need pre-bucket temporal values
|
|
188
|
+
// Recompute filtered indices for raw temporal capture
|
|
189
|
+
let filteredIndices = [];
|
|
190
|
+
if (query.filters && query.filters.length > 0) {
|
|
191
|
+
rows.forEach((row, i) => {
|
|
192
|
+
if (query.filters.every((f) => applyFilter(row, f)))
|
|
193
|
+
filteredIndices.push(i);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
filteredIndices = rows.map((_, i) => i);
|
|
198
|
+
}
|
|
199
|
+
filteredIndices.forEach((origIdx, filteredIdx) => {
|
|
200
|
+
const row = filtered[filteredIdx];
|
|
201
|
+
const rawRow = rawRows[origIdx];
|
|
202
|
+
const groupKey = query.dimensions.map((d) => `${d}=${String(row[d])}`).join('|');
|
|
203
|
+
if (!groups.has(groupKey)) {
|
|
204
|
+
const dimValues = {};
|
|
205
|
+
for (const d of query.dimensions) {
|
|
206
|
+
dimValues[d] = row[d];
|
|
207
|
+
}
|
|
208
|
+
const measureArrays = {};
|
|
209
|
+
const rawTemporalValues = {};
|
|
210
|
+
for (const ref of query.measures) {
|
|
211
|
+
measureArrays[ref.field] = [];
|
|
212
|
+
rawTemporalValues[ref.field] = [];
|
|
213
|
+
}
|
|
214
|
+
groups.set(groupKey, { dimValues, measureArrays, rawTemporalValues });
|
|
215
|
+
}
|
|
216
|
+
const group = groups.get(groupKey);
|
|
217
|
+
for (const ref of query.measures) {
|
|
218
|
+
group.measureArrays[ref.field].push(row[ref.field]);
|
|
219
|
+
// Capture raw temporal value for 'last' agg
|
|
220
|
+
if (temporalDimField) {
|
|
221
|
+
group.rawTemporalValues[ref.field].push(rawRow[temporalDimField]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
// --- 4. Aggregate ---
|
|
226
|
+
let resultRows = [];
|
|
227
|
+
for (const group of groups.values()) {
|
|
228
|
+
const resultRow = { ...group.dimValues };
|
|
229
|
+
for (const ref of query.measures) {
|
|
230
|
+
const fieldDef = descriptor.fields[ref.field];
|
|
231
|
+
const agg = ref.agg ?? (fieldDef.role === 'measure' ? fieldDef.agg : 'count');
|
|
232
|
+
const vals = group.measureArrays[ref.field] ?? [];
|
|
233
|
+
const rawTemporalVals = group.rawTemporalValues[ref.field];
|
|
234
|
+
resultRow[ref.field] = aggregate(vals, agg, agg === 'last' ? rawTemporalVals : undefined);
|
|
235
|
+
}
|
|
236
|
+
resultRows.push(resultRow);
|
|
237
|
+
}
|
|
238
|
+
// --- 5. Sort + Limit ---
|
|
239
|
+
if (query.sort && query.sort.length > 0) {
|
|
240
|
+
resultRows.sort((a, b) => {
|
|
241
|
+
for (const s of query.sort) {
|
|
242
|
+
const av = a[s.field];
|
|
243
|
+
const bv = b[s.field];
|
|
244
|
+
const av2 = av === null || av === undefined ? '' : String(av);
|
|
245
|
+
const bv2 = bv === null || bv === undefined ? '' : String(bv);
|
|
246
|
+
if (av2 < bv2)
|
|
247
|
+
return s.dir === 'asc' ? -1 : 1;
|
|
248
|
+
if (av2 > bv2)
|
|
249
|
+
return s.dir === 'asc' ? 1 : -1;
|
|
250
|
+
}
|
|
251
|
+
return 0;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// Default: temporal dim asc if present, else first measure desc
|
|
256
|
+
if (temporalDimField) {
|
|
257
|
+
const f = temporalDimField;
|
|
258
|
+
resultRows.sort((a, b) => {
|
|
259
|
+
const av = String(a[f] ?? '');
|
|
260
|
+
const bv = String(b[f] ?? '');
|
|
261
|
+
return av < bv ? -1 : av > bv ? 1 : 0;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
else if (query.measures.length > 0) {
|
|
265
|
+
const firstMeasure = query.measures[0].field;
|
|
266
|
+
resultRows.sort((a, b) => {
|
|
267
|
+
const av = typeof a[firstMeasure] === 'number' ? a[firstMeasure] : 0;
|
|
268
|
+
const bv = typeof b[firstMeasure] === 'number' ? b[firstMeasure] : 0;
|
|
269
|
+
return bv - av; // desc
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (query.limit !== undefined) {
|
|
274
|
+
resultRows = resultRows.slice(0, query.limit);
|
|
275
|
+
}
|
|
276
|
+
const columns = buildColumns(query, descriptor);
|
|
277
|
+
return { rows: resultRows, columns, query };
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function buildColumns(query, descriptor) {
|
|
281
|
+
const columns = [];
|
|
282
|
+
for (const dimField of query.dimensions) {
|
|
283
|
+
const fieldDef = descriptor.fields[dimField];
|
|
284
|
+
columns.push({
|
|
285
|
+
key: dimField,
|
|
286
|
+
role: 'dimension',
|
|
287
|
+
label: dimField,
|
|
288
|
+
format: fieldDef?.role === 'dimension' ? fieldDef.format : undefined,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
for (const ref of query.measures) {
|
|
292
|
+
const fieldDef = descriptor.fields[ref.field];
|
|
293
|
+
const agg = ref.agg ?? (fieldDef?.role === 'measure' ? fieldDef.agg : undefined);
|
|
294
|
+
columns.push({
|
|
295
|
+
key: ref.field,
|
|
296
|
+
role: 'measure',
|
|
297
|
+
label: ref.field,
|
|
298
|
+
format: fieldDef?.role === 'measure' ? fieldDef.format : undefined,
|
|
299
|
+
agg,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return columns;
|
|
303
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DatasetDescriptor } from '../schema/descriptor.js';
|
|
2
|
+
import type { Query } from '../schema/query.js';
|
|
3
|
+
export interface QueryShape {
|
|
4
|
+
temporalDims: string[];
|
|
5
|
+
categoricalDims: string[];
|
|
6
|
+
dimCount: number;
|
|
7
|
+
measureCount: number;
|
|
8
|
+
splitDim?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function classifyShape(descriptor: DatasetDescriptor, query: Query): QueryShape;
|
|
11
|
+
//# sourceMappingURL=shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shape.d.ts","sourceRoot":"","sources":["../../src/lib/shape.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAEhD,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,iBAAiB,EAAE,KAAK,EAAE,KAAK,GAAG,UAAU,CA2BrF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function classifyShape(descriptor, query) {
|
|
2
|
+
const temporalDims = [];
|
|
3
|
+
const categoricalDims = [];
|
|
4
|
+
for (const dim of query.dimensions) {
|
|
5
|
+
const field = descriptor.fields[dim];
|
|
6
|
+
if (field?.role === 'dimension') {
|
|
7
|
+
if (field.type === 'time') {
|
|
8
|
+
temporalDims.push(dim);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
categoricalDims.push(dim);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
// Field not found or not a dimension — treat as categorical (defensive)
|
|
16
|
+
categoricalDims.push(dim);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const splitDim = query.dimensions.length >= 2 ? query.dimensions[1] : undefined;
|
|
20
|
+
return {
|
|
21
|
+
temporalDims,
|
|
22
|
+
categoricalDims,
|
|
23
|
+
dimCount: query.dimensions.length,
|
|
24
|
+
measureCount: query.measures.length,
|
|
25
|
+
splitDim,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const AggSchema: z.ZodEnum<{
|
|
3
|
+
sum: "sum";
|
|
4
|
+
avg: "avg";
|
|
5
|
+
min: "min";
|
|
6
|
+
max: "max";
|
|
7
|
+
last: "last";
|
|
8
|
+
count: "count";
|
|
9
|
+
}>;
|
|
10
|
+
export type Agg = z.infer<typeof AggSchema>;
|
|
11
|
+
export declare const TimeGrainSchema: z.ZodEnum<{
|
|
12
|
+
day: "day";
|
|
13
|
+
week: "week";
|
|
14
|
+
month: "month";
|
|
15
|
+
quarter: "quarter";
|
|
16
|
+
year: "year";
|
|
17
|
+
}>;
|
|
18
|
+
export type TimeGrain = z.infer<typeof TimeGrainSchema>;
|
|
19
|
+
export declare const FormatSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
20
|
+
kind: z.ZodLiteral<"currency">;
|
|
21
|
+
ccy: z.ZodString;
|
|
22
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
23
|
+
kind: z.ZodLiteral<"percent">;
|
|
24
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
25
|
+
kind: z.ZodLiteral<"number">;
|
|
26
|
+
decimals: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
28
|
+
kind: z.ZodLiteral<"compact">;
|
|
29
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
30
|
+
kind: z.ZodLiteral<"date">;
|
|
31
|
+
dateFormat: z.ZodOptional<z.ZodString>;
|
|
32
|
+
}, z.core.$strip>], "kind">;
|
|
33
|
+
export type Format = z.infer<typeof FormatSchema>;
|
|
34
|
+
export declare const FieldSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
35
|
+
role: z.ZodLiteral<"measure">;
|
|
36
|
+
agg: z.ZodEnum<{
|
|
37
|
+
sum: "sum";
|
|
38
|
+
avg: "avg";
|
|
39
|
+
min: "min";
|
|
40
|
+
max: "max";
|
|
41
|
+
last: "last";
|
|
42
|
+
count: "count";
|
|
43
|
+
}>;
|
|
44
|
+
format: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
45
|
+
kind: z.ZodLiteral<"currency">;
|
|
46
|
+
ccy: z.ZodString;
|
|
47
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
48
|
+
kind: z.ZodLiteral<"percent">;
|
|
49
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
50
|
+
kind: z.ZodLiteral<"number">;
|
|
51
|
+
decimals: z.ZodOptional<z.ZodNumber>;
|
|
52
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
53
|
+
kind: z.ZodLiteral<"compact">;
|
|
54
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
55
|
+
kind: z.ZodLiteral<"date">;
|
|
56
|
+
dateFormat: z.ZodOptional<z.ZodString>;
|
|
57
|
+
}, z.core.$strip>], "kind">>;
|
|
58
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
59
|
+
role: z.ZodLiteral<"dimension">;
|
|
60
|
+
type: z.ZodEnum<{
|
|
61
|
+
category: "category";
|
|
62
|
+
time: "time";
|
|
63
|
+
}>;
|
|
64
|
+
timeGrains: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
65
|
+
day: "day";
|
|
66
|
+
week: "week";
|
|
67
|
+
month: "month";
|
|
68
|
+
quarter: "quarter";
|
|
69
|
+
year: "year";
|
|
70
|
+
}>>>;
|
|
71
|
+
format: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
72
|
+
kind: z.ZodLiteral<"currency">;
|
|
73
|
+
ccy: z.ZodString;
|
|
74
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
75
|
+
kind: z.ZodLiteral<"percent">;
|
|
76
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
77
|
+
kind: z.ZodLiteral<"number">;
|
|
78
|
+
decimals: z.ZodOptional<z.ZodNumber>;
|
|
79
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
80
|
+
kind: z.ZodLiteral<"compact">;
|
|
81
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
82
|
+
kind: z.ZodLiteral<"date">;
|
|
83
|
+
dateFormat: z.ZodOptional<z.ZodString>;
|
|
84
|
+
}, z.core.$strip>], "kind">>;
|
|
85
|
+
}, z.core.$strip>], "role">;
|
|
86
|
+
export type Field = z.infer<typeof FieldSchema>;
|
|
87
|
+
declare const JoinSpecSchema: z.ZodObject<{
|
|
88
|
+
key: z.ZodString;
|
|
89
|
+
with: z.ZodOptional<z.ZodString>;
|
|
90
|
+
}, z.core.$strip>;
|
|
91
|
+
export type JoinSpec = z.infer<typeof JoinSpecSchema>;
|
|
92
|
+
export declare const DatasetDescriptorSchema: z.ZodObject<{
|
|
93
|
+
name: z.ZodString;
|
|
94
|
+
version: z.ZodString;
|
|
95
|
+
grain: z.ZodArray<z.ZodString>;
|
|
96
|
+
fields: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
97
|
+
role: z.ZodLiteral<"measure">;
|
|
98
|
+
agg: z.ZodEnum<{
|
|
99
|
+
sum: "sum";
|
|
100
|
+
avg: "avg";
|
|
101
|
+
min: "min";
|
|
102
|
+
max: "max";
|
|
103
|
+
last: "last";
|
|
104
|
+
count: "count";
|
|
105
|
+
}>;
|
|
106
|
+
format: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
107
|
+
kind: z.ZodLiteral<"currency">;
|
|
108
|
+
ccy: z.ZodString;
|
|
109
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
110
|
+
kind: z.ZodLiteral<"percent">;
|
|
111
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
112
|
+
kind: z.ZodLiteral<"number">;
|
|
113
|
+
decimals: z.ZodOptional<z.ZodNumber>;
|
|
114
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
115
|
+
kind: z.ZodLiteral<"compact">;
|
|
116
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
117
|
+
kind: z.ZodLiteral<"date">;
|
|
118
|
+
dateFormat: z.ZodOptional<z.ZodString>;
|
|
119
|
+
}, z.core.$strip>], "kind">>;
|
|
120
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
121
|
+
role: z.ZodLiteral<"dimension">;
|
|
122
|
+
type: z.ZodEnum<{
|
|
123
|
+
category: "category";
|
|
124
|
+
time: "time";
|
|
125
|
+
}>;
|
|
126
|
+
timeGrains: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
127
|
+
day: "day";
|
|
128
|
+
week: "week";
|
|
129
|
+
month: "month";
|
|
130
|
+
quarter: "quarter";
|
|
131
|
+
year: "year";
|
|
132
|
+
}>>>;
|
|
133
|
+
format: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
134
|
+
kind: z.ZodLiteral<"currency">;
|
|
135
|
+
ccy: z.ZodString;
|
|
136
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
137
|
+
kind: z.ZodLiteral<"percent">;
|
|
138
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
139
|
+
kind: z.ZodLiteral<"number">;
|
|
140
|
+
decimals: z.ZodOptional<z.ZodNumber>;
|
|
141
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
142
|
+
kind: z.ZodLiteral<"compact">;
|
|
143
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
144
|
+
kind: z.ZodLiteral<"date">;
|
|
145
|
+
dateFormat: z.ZodOptional<z.ZodString>;
|
|
146
|
+
}, z.core.$strip>], "kind">>;
|
|
147
|
+
}, z.core.$strip>], "role">>;
|
|
148
|
+
defaults: z.ZodOptional<z.ZodObject<{
|
|
149
|
+
measures: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
150
|
+
dimensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
151
|
+
split: z.ZodOptional<z.ZodString>;
|
|
152
|
+
viz: z.ZodOptional<z.ZodString>;
|
|
153
|
+
}, z.core.$strip>>;
|
|
154
|
+
joinable: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
155
|
+
key: z.ZodString;
|
|
156
|
+
with: z.ZodOptional<z.ZodString>;
|
|
157
|
+
}, z.core.$strip>>>;
|
|
158
|
+
}, z.core.$strip>;
|
|
159
|
+
export type DatasetDescriptor = z.infer<typeof DatasetDescriptorSchema>;
|
|
160
|
+
export declare function parseDescriptor(input: unknown): DatasetDescriptor;
|
|
161
|
+
export {};
|
|
162
|
+
//# sourceMappingURL=descriptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"descriptor.d.ts","sourceRoot":"","sources":["../../src/schema/descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,SAAS;;;;;;;EAAwD,CAAC;AAC/E,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAE5C,eAAO,MAAM,eAAe;;;;;;EAAsD,CAAC;AACnF,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAQxD,eAAO,MAAM,YAAY;;;;;;;;;;;;;2BAMvB,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAelD,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAA2E,CAAC;AACpG,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,QAAA,MAAM,cAAc;;;iBAGlB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAStD,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAOlC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CASjE"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { fromZodValidation } from 'hazo_core';
|
|
3
|
+
export const AggSchema = z.enum(['sum', 'avg', 'min', 'max', 'last', 'count']);
|
|
4
|
+
export const TimeGrainSchema = z.enum(['day', 'week', 'month', 'quarter', 'year']);
|
|
5
|
+
const FormatCurrencySchema = z.object({ kind: z.literal('currency'), ccy: z.string().min(1) });
|
|
6
|
+
const FormatPercentSchema = z.object({ kind: z.literal('percent') });
|
|
7
|
+
const FormatNumberSchema = z.object({ kind: z.literal('number'), decimals: z.number().int().min(0).optional() });
|
|
8
|
+
const FormatCompactSchema = z.object({ kind: z.literal('compact') });
|
|
9
|
+
const FormatDateSchema = z.object({ kind: z.literal('date'), dateFormat: z.string().optional() });
|
|
10
|
+
export const FormatSchema = z.discriminatedUnion('kind', [
|
|
11
|
+
FormatCurrencySchema,
|
|
12
|
+
FormatPercentSchema,
|
|
13
|
+
FormatNumberSchema,
|
|
14
|
+
FormatCompactSchema,
|
|
15
|
+
FormatDateSchema,
|
|
16
|
+
]);
|
|
17
|
+
const MeasureFieldSchema = z.object({
|
|
18
|
+
role: z.literal('measure'),
|
|
19
|
+
agg: AggSchema,
|
|
20
|
+
format: FormatSchema.optional(),
|
|
21
|
+
});
|
|
22
|
+
const DimensionFieldSchema = z.object({
|
|
23
|
+
role: z.literal('dimension'),
|
|
24
|
+
type: z.enum(['category', 'time']),
|
|
25
|
+
timeGrains: z.array(TimeGrainSchema).optional(),
|
|
26
|
+
format: FormatSchema.optional(),
|
|
27
|
+
});
|
|
28
|
+
export const FieldSchema = z.discriminatedUnion('role', [MeasureFieldSchema, DimensionFieldSchema]);
|
|
29
|
+
const JoinSpecSchema = z.object({
|
|
30
|
+
key: z.string().min(1),
|
|
31
|
+
with: z.string().optional(),
|
|
32
|
+
});
|
|
33
|
+
const DefaultsSchema = z.object({
|
|
34
|
+
measures: z.array(z.string()).optional(),
|
|
35
|
+
dimensions: z.array(z.string()).optional(),
|
|
36
|
+
split: z.string().optional(),
|
|
37
|
+
viz: z.string().optional(),
|
|
38
|
+
});
|
|
39
|
+
export const DatasetDescriptorSchema = z.object({
|
|
40
|
+
name: z.string().min(1),
|
|
41
|
+
version: z.string().min(1),
|
|
42
|
+
grain: z.array(z.string()).min(1),
|
|
43
|
+
fields: z.record(z.string(), FieldSchema),
|
|
44
|
+
defaults: DefaultsSchema.optional(),
|
|
45
|
+
joinable: z.array(JoinSpecSchema).optional(),
|
|
46
|
+
});
|
|
47
|
+
export function parseDescriptor(input) {
|
|
48
|
+
const result = DatasetDescriptorSchema.safeParse(input);
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
throw fromZodValidation(result.error, {
|
|
51
|
+
pkg: 'hazo_dataviz',
|
|
52
|
+
code: 'HAZO_DATAVIZ_INVALID_DESCRIPTOR',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return result.data;
|
|
56
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const FilterOpSchema: z.ZodEnum<{
|
|
3
|
+
in: "in";
|
|
4
|
+
eq: "eq";
|
|
5
|
+
neq: "neq";
|
|
6
|
+
gt: "gt";
|
|
7
|
+
gte: "gte";
|
|
8
|
+
lt: "lt";
|
|
9
|
+
lte: "lte";
|
|
10
|
+
between: "between";
|
|
11
|
+
is_null: "is_null";
|
|
12
|
+
}>;
|
|
13
|
+
export type FilterOp = z.infer<typeof FilterOpSchema>;
|
|
14
|
+
export declare const MeasureRefSchema: z.ZodObject<{
|
|
15
|
+
field: z.ZodString;
|
|
16
|
+
agg: z.ZodOptional<z.ZodEnum<{
|
|
17
|
+
sum: "sum";
|
|
18
|
+
avg: "avg";
|
|
19
|
+
min: "min";
|
|
20
|
+
max: "max";
|
|
21
|
+
last: "last";
|
|
22
|
+
count: "count";
|
|
23
|
+
}>>;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
export type MeasureRef = z.infer<typeof MeasureRefSchema>;
|
|
26
|
+
export declare const FilterSchema: z.ZodObject<{
|
|
27
|
+
field: z.ZodString;
|
|
28
|
+
op: z.ZodEnum<{
|
|
29
|
+
in: "in";
|
|
30
|
+
eq: "eq";
|
|
31
|
+
neq: "neq";
|
|
32
|
+
gt: "gt";
|
|
33
|
+
gte: "gte";
|
|
34
|
+
lt: "lt";
|
|
35
|
+
lte: "lte";
|
|
36
|
+
between: "between";
|
|
37
|
+
is_null: "is_null";
|
|
38
|
+
}>;
|
|
39
|
+
value: z.ZodUnknown;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
export type Filter = z.infer<typeof FilterSchema>;
|
|
42
|
+
export declare const SortSchema: z.ZodObject<{
|
|
43
|
+
field: z.ZodString;
|
|
44
|
+
dir: z.ZodEnum<{
|
|
45
|
+
asc: "asc";
|
|
46
|
+
desc: "desc";
|
|
47
|
+
}>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
export type Sort = z.infer<typeof SortSchema>;
|
|
50
|
+
export declare const QuerySchema: z.ZodObject<{
|
|
51
|
+
measures: z.ZodArray<z.ZodObject<{
|
|
52
|
+
field: z.ZodString;
|
|
53
|
+
agg: z.ZodOptional<z.ZodEnum<{
|
|
54
|
+
sum: "sum";
|
|
55
|
+
avg: "avg";
|
|
56
|
+
min: "min";
|
|
57
|
+
max: "max";
|
|
58
|
+
last: "last";
|
|
59
|
+
count: "count";
|
|
60
|
+
}>>;
|
|
61
|
+
}, z.core.$strip>>;
|
|
62
|
+
dimensions: z.ZodArray<z.ZodString>;
|
|
63
|
+
filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
64
|
+
field: z.ZodString;
|
|
65
|
+
op: z.ZodEnum<{
|
|
66
|
+
in: "in";
|
|
67
|
+
eq: "eq";
|
|
68
|
+
neq: "neq";
|
|
69
|
+
gt: "gt";
|
|
70
|
+
gte: "gte";
|
|
71
|
+
lt: "lt";
|
|
72
|
+
lte: "lte";
|
|
73
|
+
between: "between";
|
|
74
|
+
is_null: "is_null";
|
|
75
|
+
}>;
|
|
76
|
+
value: z.ZodUnknown;
|
|
77
|
+
}, z.core.$strip>>>;
|
|
78
|
+
timeGrain: z.ZodOptional<z.ZodEnum<{
|
|
79
|
+
day: "day";
|
|
80
|
+
week: "week";
|
|
81
|
+
month: "month";
|
|
82
|
+
quarter: "quarter";
|
|
83
|
+
year: "year";
|
|
84
|
+
}>>;
|
|
85
|
+
sort: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
86
|
+
field: z.ZodString;
|
|
87
|
+
dir: z.ZodEnum<{
|
|
88
|
+
asc: "asc";
|
|
89
|
+
desc: "desc";
|
|
90
|
+
}>;
|
|
91
|
+
}, z.core.$strip>>>;
|
|
92
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
93
|
+
}, z.core.$strip>;
|
|
94
|
+
export type Query = z.infer<typeof QuerySchema>;
|
|
95
|
+
export declare function parseQuery(input: unknown): Query;
|
|
96
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/schema/query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,cAAc;;;;;;;;;;EAEzB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD,eAAO,MAAM,gBAAgB;;;;;;;;;;iBAG3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,YAAY;;;;;;;;;;;;;;iBAIvB,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAElD,eAAO,MAAM,UAAU;;;;;;iBAGrB,CAAC;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAE9C,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAUvB,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAShD"}
|