@contractspec/lib.presentation-runtime-core 3.7.6 → 3.8.3
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 +60 -54
- package/dist/browser/index.js +473 -1
- package/dist/browser/table.js +21 -0
- package/dist/browser/visualization.echarts.js +148 -0
- package/dist/browser/visualization.js +307 -0
- package/dist/browser/visualization.model.builders.js +290 -0
- package/dist/browser/visualization.model.helpers.js +199 -0
- package/dist/browser/visualization.model.js +306 -0
- package/dist/browser/visualization.types.js +0 -0
- package/dist/browser/visualization.utils.js +65 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +473 -1
- package/dist/node/index.js +473 -1
- package/dist/node/table.js +21 -0
- package/dist/node/visualization.echarts.js +148 -0
- package/dist/node/visualization.js +307 -0
- package/dist/node/visualization.model.builders.js +290 -0
- package/dist/node/visualization.model.helpers.js +199 -0
- package/dist/node/visualization.model.js +306 -0
- package/dist/node/visualization.types.js +0 -0
- package/dist/node/visualization.utils.js +65 -0
- package/dist/table.d.ts +99 -0
- package/dist/table.js +22 -0
- package/dist/visualization.d.ts +3 -0
- package/dist/visualization.echarts.d.ts +3 -0
- package/dist/visualization.echarts.js +149 -0
- package/dist/visualization.js +308 -0
- package/dist/visualization.model.builders.d.ts +20 -0
- package/dist/visualization.model.builders.js +291 -0
- package/dist/visualization.model.d.ts +3 -0
- package/dist/visualization.model.helpers.d.ts +60 -0
- package/dist/visualization.model.helpers.js +200 -0
- package/dist/visualization.model.js +307 -0
- package/dist/visualization.model.test.d.ts +1 -0
- package/dist/visualization.types.d.ts +74 -0
- package/dist/visualization.types.js +1 -0
- package/dist/visualization.utils.d.ts +5 -0
- package/dist/visualization.utils.js +66 -0
- package/package.json +121 -5
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/visualization.utils.ts
|
|
3
|
+
function formatVisualizationValue(value, format) {
|
|
4
|
+
if (value == null)
|
|
5
|
+
return "\u2014";
|
|
6
|
+
if (format === "currency" && typeof value === "number") {
|
|
7
|
+
return new Intl.NumberFormat(undefined, {
|
|
8
|
+
style: "currency",
|
|
9
|
+
currency: "USD"
|
|
10
|
+
}).format(value);
|
|
11
|
+
}
|
|
12
|
+
if (format === "percentage" && typeof value === "number") {
|
|
13
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
14
|
+
}
|
|
15
|
+
if ((format === "date" || format === "dateTime") && value) {
|
|
16
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
17
|
+
return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(undefined, {
|
|
18
|
+
dateStyle: "medium",
|
|
19
|
+
timeStyle: format === "dateTime" ? "short" : undefined
|
|
20
|
+
}).format(date);
|
|
21
|
+
}
|
|
22
|
+
return String(value);
|
|
23
|
+
}
|
|
24
|
+
function resolveVisualizationRows(data, resultPath) {
|
|
25
|
+
const value = resultPath ? getAtPath(data, resultPath) : data;
|
|
26
|
+
const candidate = value ?? data;
|
|
27
|
+
if (Array.isArray(candidate))
|
|
28
|
+
return candidate.map(asRow);
|
|
29
|
+
if (Array.isArray(getAtPath(candidate, "items"))) {
|
|
30
|
+
return getAtPath(candidate, "items").map(asRow);
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(getAtPath(candidate, "rows"))) {
|
|
33
|
+
return getAtPath(candidate, "rows").map(asRow);
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(getAtPath(candidate, "data"))) {
|
|
36
|
+
return getAtPath(candidate, "data").map(asRow);
|
|
37
|
+
}
|
|
38
|
+
return candidate && typeof candidate === "object" ? [asRow(candidate)] : [];
|
|
39
|
+
}
|
|
40
|
+
function getAtPath(source, path) {
|
|
41
|
+
if (!path || source == null)
|
|
42
|
+
return source;
|
|
43
|
+
return path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean).reduce((current, segment) => {
|
|
44
|
+
if (current == null || typeof current !== "object")
|
|
45
|
+
return;
|
|
46
|
+
return current[segment];
|
|
47
|
+
}, source);
|
|
48
|
+
}
|
|
49
|
+
function toNumber(value) {
|
|
50
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
51
|
+
return value;
|
|
52
|
+
if (typeof value === "string" && value.trim()) {
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function asRow(value) {
|
|
59
|
+
return value && typeof value === "object" ? value : { value };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/visualization.model.helpers.ts
|
|
63
|
+
function createVisualizationMaps(spec) {
|
|
64
|
+
return {
|
|
65
|
+
dimensions: new Map((spec.visualization.dimensions ?? []).map((dimension) => [
|
|
66
|
+
dimension.key,
|
|
67
|
+
dimension
|
|
68
|
+
])),
|
|
69
|
+
measures: new Map((spec.visualization.measures ?? []).map((measure) => [
|
|
70
|
+
measure.key,
|
|
71
|
+
measure
|
|
72
|
+
]))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function createVisualizationBaseModel(spec, rows, maps) {
|
|
76
|
+
const config = spec.visualization;
|
|
77
|
+
return {
|
|
78
|
+
kind: config.kind,
|
|
79
|
+
title: config.title ?? spec.meta.title,
|
|
80
|
+
description: config.description ?? spec.meta.description,
|
|
81
|
+
summary: `${spec.meta.title ?? spec.meta.key}: ${rows.length} row${rows.length === 1 ? "" : "s"}`,
|
|
82
|
+
warnings: rows.length === 0 ? ["No visualization rows available."] : [],
|
|
83
|
+
palette: config.palette,
|
|
84
|
+
legend: config.legend,
|
|
85
|
+
tooltip: config.tooltip,
|
|
86
|
+
drilldown: config.drilldown,
|
|
87
|
+
thresholds: config.thresholds ?? [],
|
|
88
|
+
annotations: (config.annotations ?? []).map((annotation) => resolveAnnotation(annotation, rows[0] ?? {})),
|
|
89
|
+
table: createTable(config.table?.caption, config, rows)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function createMeasureSeries(measureKey, measures, rows, dimension, config) {
|
|
93
|
+
const measure = measures.get(measureKey);
|
|
94
|
+
if (!measure)
|
|
95
|
+
return null;
|
|
96
|
+
return {
|
|
97
|
+
key: config?.key ?? measure.key,
|
|
98
|
+
label: config?.label ?? measure.label,
|
|
99
|
+
type: config?.type,
|
|
100
|
+
color: config?.color ?? measure.color,
|
|
101
|
+
stack: config?.stack,
|
|
102
|
+
smooth: config?.smooth,
|
|
103
|
+
points: rows.map((row, index) => ({
|
|
104
|
+
id: `${measure.key}-${index}`,
|
|
105
|
+
raw: row,
|
|
106
|
+
x: dimension ? readDimensionValue(row, dimension) : index,
|
|
107
|
+
y: numericValue(row, measure),
|
|
108
|
+
value: numericValue(row, measure)
|
|
109
|
+
}))
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function defaultSeries(keys) {
|
|
113
|
+
return keys.map((key) => ({ key, label: key, measure: key }));
|
|
114
|
+
}
|
|
115
|
+
function createAxis(dimension) {
|
|
116
|
+
if (!dimension)
|
|
117
|
+
return;
|
|
118
|
+
return {
|
|
119
|
+
key: dimension.key,
|
|
120
|
+
label: dimension.label,
|
|
121
|
+
type: dimension.type === "time" ? "time" : dimension.type === "number" ? "number" : "category",
|
|
122
|
+
format: dimension.format
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function createValueAxis(key, measures) {
|
|
126
|
+
const measure = key ? measures.get(key) : undefined;
|
|
127
|
+
if (!measure)
|
|
128
|
+
return;
|
|
129
|
+
return {
|
|
130
|
+
key: measure.key,
|
|
131
|
+
label: measure.label,
|
|
132
|
+
type: "number",
|
|
133
|
+
format: measure.format
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function readValue(row, measure) {
|
|
137
|
+
return measure ? getAtPath(row, measure.dataPath) : undefined;
|
|
138
|
+
}
|
|
139
|
+
function readDimensionValue(row, dimension) {
|
|
140
|
+
return dimension ? getAtPath(row, dimension.dataPath) : undefined;
|
|
141
|
+
}
|
|
142
|
+
function numericValue(row, measure) {
|
|
143
|
+
return toNumber(readValue(row, measure));
|
|
144
|
+
}
|
|
145
|
+
function numericPathValue(row, dimension) {
|
|
146
|
+
return toNumber(readDimensionValue(row, dimension));
|
|
147
|
+
}
|
|
148
|
+
function stringValue(row, dimension) {
|
|
149
|
+
const value = readDimensionValue(row, dimension);
|
|
150
|
+
return value == null ? undefined : String(value);
|
|
151
|
+
}
|
|
152
|
+
function createTable(caption, config, rows) {
|
|
153
|
+
const dimensions = config.dimensions ?? [];
|
|
154
|
+
const measures = config.measures ?? [];
|
|
155
|
+
return {
|
|
156
|
+
caption,
|
|
157
|
+
columns: [
|
|
158
|
+
...dimensions.map((dimension) => ({
|
|
159
|
+
key: dimension.key,
|
|
160
|
+
label: dimension.label
|
|
161
|
+
})),
|
|
162
|
+
...measures.map((measure) => ({
|
|
163
|
+
key: measure.key,
|
|
164
|
+
label: measure.label
|
|
165
|
+
}))
|
|
166
|
+
],
|
|
167
|
+
rows: rows.map((row) => ({
|
|
168
|
+
...Object.fromEntries(dimensions.map((dimension) => [
|
|
169
|
+
dimension.key,
|
|
170
|
+
readDimensionValue(row, dimension)
|
|
171
|
+
])),
|
|
172
|
+
...Object.fromEntries(measures.map((measure) => [measure.key, readValue(row, measure)]))
|
|
173
|
+
}))
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function resolveAnnotation(annotation, row) {
|
|
177
|
+
return {
|
|
178
|
+
key: annotation.key,
|
|
179
|
+
label: annotation.label,
|
|
180
|
+
kind: annotation.kind,
|
|
181
|
+
color: annotation.color,
|
|
182
|
+
x: annotation.xDataPath ? getAtPath(row, annotation.xDataPath) : undefined,
|
|
183
|
+
y: annotation.yDataPath ? toNumber(getAtPath(row, annotation.yDataPath)) : undefined,
|
|
184
|
+
start: annotation.startDataPath ? getAtPath(row, annotation.startDataPath) : undefined,
|
|
185
|
+
end: annotation.endDataPath ? getAtPath(row, annotation.endDataPath) : undefined
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/visualization.model.builders.ts
|
|
190
|
+
function buildMetricModel(base, config, rows, maps) {
|
|
191
|
+
const measure = maps.measures.get(config.measure);
|
|
192
|
+
const comparison = config.comparisonMeasure ? maps.measures.get(config.comparisonMeasure) : undefined;
|
|
193
|
+
const currentRow = rows.at(-1) ?? {};
|
|
194
|
+
const sparkline = config.sparkline ? createMeasureSeries(config.sparkline.measure ?? config.measure, maps.measures, rows, maps.dimensions.get(config.sparkline.dimension)) : null;
|
|
195
|
+
return {
|
|
196
|
+
...base,
|
|
197
|
+
series: sparkline ? [sparkline] : [],
|
|
198
|
+
metric: {
|
|
199
|
+
label: measure?.label ?? config.measure,
|
|
200
|
+
value: readValue(currentRow, measure),
|
|
201
|
+
comparisonValue: comparison ? readValue(currentRow, comparison) : undefined,
|
|
202
|
+
format: measure?.format
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function buildCartesianModel(base, config, rows, maps) {
|
|
207
|
+
const series = (config.series ?? defaultSeries(config.yMeasures ?? [])).map((seriesItem) => createMeasureSeries(seriesItem.measure, maps.measures, rows, maps.dimensions.get(config.xDimension), seriesItem)).filter((item) => Boolean(item));
|
|
208
|
+
return {
|
|
209
|
+
...base,
|
|
210
|
+
series,
|
|
211
|
+
xAxis: createAxis(maps.dimensions.get(config.xDimension)),
|
|
212
|
+
yAxis: createValueAxis(config.yMeasures?.[0], maps.measures)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function buildDistributionModel(base, config, rows, maps) {
|
|
216
|
+
const nameDimension = maps.dimensions.get(config.nameDimension);
|
|
217
|
+
const valueMeasure = maps.measures.get(config.valueMeasure);
|
|
218
|
+
return {
|
|
219
|
+
...base,
|
|
220
|
+
series: [
|
|
221
|
+
{
|
|
222
|
+
key: valueMeasure?.key ?? config.valueMeasure,
|
|
223
|
+
label: valueMeasure?.label ?? config.valueMeasure,
|
|
224
|
+
type: config.kind,
|
|
225
|
+
points: rows.map((row, index) => ({
|
|
226
|
+
id: `${config.kind}-${index}`,
|
|
227
|
+
raw: row,
|
|
228
|
+
name: stringValue(row, nameDimension),
|
|
229
|
+
value: numericValue(row, valueMeasure)
|
|
230
|
+
}))
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function buildHeatmapModel(base, config, rows, maps) {
|
|
236
|
+
const xDimension = maps.dimensions.get(config.xDimension);
|
|
237
|
+
const yDimension = maps.dimensions.get(config.yDimension);
|
|
238
|
+
const valueMeasure = maps.measures.get(config.valueMeasure);
|
|
239
|
+
return {
|
|
240
|
+
...base,
|
|
241
|
+
series: [
|
|
242
|
+
{
|
|
243
|
+
key: config.valueMeasure,
|
|
244
|
+
label: valueMeasure?.label ?? config.valueMeasure,
|
|
245
|
+
type: "heatmap",
|
|
246
|
+
points: rows.map((row, index) => ({
|
|
247
|
+
id: `heatmap-${index}`,
|
|
248
|
+
raw: row,
|
|
249
|
+
name: stringValue(row, yDimension),
|
|
250
|
+
x: readDimensionValue(row, xDimension),
|
|
251
|
+
y: numericValue(row, valueMeasure),
|
|
252
|
+
value: numericValue(row, valueMeasure)
|
|
253
|
+
}))
|
|
254
|
+
}
|
|
255
|
+
],
|
|
256
|
+
xAxis: createAxis(xDimension),
|
|
257
|
+
yAxis: createAxis(yDimension)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function buildGeoModel(base, config, rows, maps) {
|
|
261
|
+
return {
|
|
262
|
+
...base,
|
|
263
|
+
series: [
|
|
264
|
+
{
|
|
265
|
+
key: "geo",
|
|
266
|
+
label: "Geo",
|
|
267
|
+
type: config.variant ?? "scatter",
|
|
268
|
+
points: rows.map((row, index) => ({
|
|
269
|
+
id: `geo-${index}`,
|
|
270
|
+
raw: row,
|
|
271
|
+
name: config.nameDimension ? stringValue(row, maps.dimensions.get(config.nameDimension)) : undefined,
|
|
272
|
+
value: config.valueMeasure ? numericValue(row, maps.measures.get(config.valueMeasure)) : undefined,
|
|
273
|
+
latitude: numericPathValue(row, maps.dimensions.get(config.latitudeDimension ?? "")),
|
|
274
|
+
longitude: numericPathValue(row, maps.dimensions.get(config.longitudeDimension ?? ""))
|
|
275
|
+
}))
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
geo: {
|
|
279
|
+
mode: config.mode ?? "chart",
|
|
280
|
+
variant: config.variant ?? "scatter",
|
|
281
|
+
geoJson: config.geoJson
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
export {
|
|
286
|
+
buildMetricModel,
|
|
287
|
+
buildHeatmapModel,
|
|
288
|
+
buildGeoModel,
|
|
289
|
+
buildDistributionModel,
|
|
290
|
+
buildCartesianModel
|
|
291
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { VisualizationSpec } from '@contractspec/lib.contracts-spec/visualizations';
|
|
2
|
+
import type { ContractVisualizationRenderModel } from './visualization.types';
|
|
3
|
+
export declare function createVisualizationModel(spec: VisualizationSpec, data: unknown): ContractVisualizationRenderModel;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { VisualizationDimension, VisualizationMeasure, VisualizationSpec } from '@contractspec/lib.contracts-spec/visualizations';
|
|
2
|
+
import type { ContractVisualizationAnnotationModel, ContractVisualizationAxisModel, ContractVisualizationTableAlternative } from './visualization.types';
|
|
3
|
+
export interface VisualizationModelMaps {
|
|
4
|
+
dimensions: Map<string, VisualizationDimension>;
|
|
5
|
+
measures: Map<string, VisualizationMeasure>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createVisualizationMaps(spec: VisualizationSpec): VisualizationModelMaps;
|
|
8
|
+
export declare function createVisualizationBaseModel(spec: VisualizationSpec, rows: Record<string, unknown>[], maps: VisualizationModelMaps): {
|
|
9
|
+
kind: "heatmap" | "metric" | "cartesian" | "pie" | "funnel" | "geo";
|
|
10
|
+
title: string | undefined;
|
|
11
|
+
description: string;
|
|
12
|
+
summary: string;
|
|
13
|
+
warnings: string[];
|
|
14
|
+
palette: string[] | undefined;
|
|
15
|
+
legend: boolean | undefined;
|
|
16
|
+
tooltip: boolean | undefined;
|
|
17
|
+
drilldown: import("@contractspec/lib.contracts-spec").VisualizationDrilldown | undefined;
|
|
18
|
+
thresholds: import("@contractspec/lib.contracts-spec").VisualizationThreshold[];
|
|
19
|
+
annotations: ContractVisualizationAnnotationModel[];
|
|
20
|
+
table: ContractVisualizationTableAlternative;
|
|
21
|
+
};
|
|
22
|
+
export declare function createMeasureSeries(measureKey: string, measures: Map<string, VisualizationMeasure>, rows: Record<string, unknown>[], dimension?: VisualizationDimension, config?: {
|
|
23
|
+
key: string;
|
|
24
|
+
label: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
color?: string;
|
|
27
|
+
stack?: string;
|
|
28
|
+
smooth?: boolean;
|
|
29
|
+
}): {
|
|
30
|
+
key: string;
|
|
31
|
+
label: string;
|
|
32
|
+
type: string | undefined;
|
|
33
|
+
color: string | undefined;
|
|
34
|
+
stack: string | undefined;
|
|
35
|
+
smooth: boolean | undefined;
|
|
36
|
+
points: {
|
|
37
|
+
id: string;
|
|
38
|
+
raw: Record<string, unknown>;
|
|
39
|
+
x: unknown;
|
|
40
|
+
y: number | null;
|
|
41
|
+
value: number | null;
|
|
42
|
+
}[];
|
|
43
|
+
} | null;
|
|
44
|
+
export declare function defaultSeries(keys: string[]): {
|
|
45
|
+
key: string;
|
|
46
|
+
label: string;
|
|
47
|
+
measure: string;
|
|
48
|
+
}[];
|
|
49
|
+
export declare function createAxis(dimension?: VisualizationDimension): ContractVisualizationAxisModel | undefined;
|
|
50
|
+
export declare function createValueAxis(key: string | undefined, measures: Map<string, VisualizationMeasure>): {
|
|
51
|
+
key: string;
|
|
52
|
+
label: string;
|
|
53
|
+
type: "number";
|
|
54
|
+
format: import("@contractspec/lib.contracts-spec").VisualizationValueFormat | undefined;
|
|
55
|
+
} | undefined;
|
|
56
|
+
export declare function readValue(row: Record<string, unknown>, measure?: VisualizationMeasure): unknown;
|
|
57
|
+
export declare function readDimensionValue(row: Record<string, unknown>, dimension?: VisualizationDimension): unknown;
|
|
58
|
+
export declare function numericValue(row: Record<string, unknown>, measure?: VisualizationMeasure): number | null;
|
|
59
|
+
export declare function numericPathValue(row: Record<string, unknown>, dimension?: VisualizationDimension): number | null;
|
|
60
|
+
export declare function stringValue(row: Record<string, unknown>, dimension?: VisualizationDimension): string | undefined;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/visualization.utils.ts
|
|
3
|
+
function formatVisualizationValue(value, format) {
|
|
4
|
+
if (value == null)
|
|
5
|
+
return "\u2014";
|
|
6
|
+
if (format === "currency" && typeof value === "number") {
|
|
7
|
+
return new Intl.NumberFormat(undefined, {
|
|
8
|
+
style: "currency",
|
|
9
|
+
currency: "USD"
|
|
10
|
+
}).format(value);
|
|
11
|
+
}
|
|
12
|
+
if (format === "percentage" && typeof value === "number") {
|
|
13
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
14
|
+
}
|
|
15
|
+
if ((format === "date" || format === "dateTime") && value) {
|
|
16
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
17
|
+
return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(undefined, {
|
|
18
|
+
dateStyle: "medium",
|
|
19
|
+
timeStyle: format === "dateTime" ? "short" : undefined
|
|
20
|
+
}).format(date);
|
|
21
|
+
}
|
|
22
|
+
return String(value);
|
|
23
|
+
}
|
|
24
|
+
function resolveVisualizationRows(data, resultPath) {
|
|
25
|
+
const value = resultPath ? getAtPath(data, resultPath) : data;
|
|
26
|
+
const candidate = value ?? data;
|
|
27
|
+
if (Array.isArray(candidate))
|
|
28
|
+
return candidate.map(asRow);
|
|
29
|
+
if (Array.isArray(getAtPath(candidate, "items"))) {
|
|
30
|
+
return getAtPath(candidate, "items").map(asRow);
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(getAtPath(candidate, "rows"))) {
|
|
33
|
+
return getAtPath(candidate, "rows").map(asRow);
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(getAtPath(candidate, "data"))) {
|
|
36
|
+
return getAtPath(candidate, "data").map(asRow);
|
|
37
|
+
}
|
|
38
|
+
return candidate && typeof candidate === "object" ? [asRow(candidate)] : [];
|
|
39
|
+
}
|
|
40
|
+
function getAtPath(source, path) {
|
|
41
|
+
if (!path || source == null)
|
|
42
|
+
return source;
|
|
43
|
+
return path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean).reduce((current, segment) => {
|
|
44
|
+
if (current == null || typeof current !== "object")
|
|
45
|
+
return;
|
|
46
|
+
return current[segment];
|
|
47
|
+
}, source);
|
|
48
|
+
}
|
|
49
|
+
function toNumber(value) {
|
|
50
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
51
|
+
return value;
|
|
52
|
+
if (typeof value === "string" && value.trim()) {
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function asRow(value) {
|
|
59
|
+
return value && typeof value === "object" ? value : { value };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/visualization.model.helpers.ts
|
|
63
|
+
function createVisualizationMaps(spec) {
|
|
64
|
+
return {
|
|
65
|
+
dimensions: new Map((spec.visualization.dimensions ?? []).map((dimension) => [
|
|
66
|
+
dimension.key,
|
|
67
|
+
dimension
|
|
68
|
+
])),
|
|
69
|
+
measures: new Map((spec.visualization.measures ?? []).map((measure) => [
|
|
70
|
+
measure.key,
|
|
71
|
+
measure
|
|
72
|
+
]))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function createVisualizationBaseModel(spec, rows, maps) {
|
|
76
|
+
const config = spec.visualization;
|
|
77
|
+
return {
|
|
78
|
+
kind: config.kind,
|
|
79
|
+
title: config.title ?? spec.meta.title,
|
|
80
|
+
description: config.description ?? spec.meta.description,
|
|
81
|
+
summary: `${spec.meta.title ?? spec.meta.key}: ${rows.length} row${rows.length === 1 ? "" : "s"}`,
|
|
82
|
+
warnings: rows.length === 0 ? ["No visualization rows available."] : [],
|
|
83
|
+
palette: config.palette,
|
|
84
|
+
legend: config.legend,
|
|
85
|
+
tooltip: config.tooltip,
|
|
86
|
+
drilldown: config.drilldown,
|
|
87
|
+
thresholds: config.thresholds ?? [],
|
|
88
|
+
annotations: (config.annotations ?? []).map((annotation) => resolveAnnotation(annotation, rows[0] ?? {})),
|
|
89
|
+
table: createTable(config.table?.caption, config, rows)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function createMeasureSeries(measureKey, measures, rows, dimension, config) {
|
|
93
|
+
const measure = measures.get(measureKey);
|
|
94
|
+
if (!measure)
|
|
95
|
+
return null;
|
|
96
|
+
return {
|
|
97
|
+
key: config?.key ?? measure.key,
|
|
98
|
+
label: config?.label ?? measure.label,
|
|
99
|
+
type: config?.type,
|
|
100
|
+
color: config?.color ?? measure.color,
|
|
101
|
+
stack: config?.stack,
|
|
102
|
+
smooth: config?.smooth,
|
|
103
|
+
points: rows.map((row, index) => ({
|
|
104
|
+
id: `${measure.key}-${index}`,
|
|
105
|
+
raw: row,
|
|
106
|
+
x: dimension ? readDimensionValue(row, dimension) : index,
|
|
107
|
+
y: numericValue(row, measure),
|
|
108
|
+
value: numericValue(row, measure)
|
|
109
|
+
}))
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function defaultSeries(keys) {
|
|
113
|
+
return keys.map((key) => ({ key, label: key, measure: key }));
|
|
114
|
+
}
|
|
115
|
+
function createAxis(dimension) {
|
|
116
|
+
if (!dimension)
|
|
117
|
+
return;
|
|
118
|
+
return {
|
|
119
|
+
key: dimension.key,
|
|
120
|
+
label: dimension.label,
|
|
121
|
+
type: dimension.type === "time" ? "time" : dimension.type === "number" ? "number" : "category",
|
|
122
|
+
format: dimension.format
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function createValueAxis(key, measures) {
|
|
126
|
+
const measure = key ? measures.get(key) : undefined;
|
|
127
|
+
if (!measure)
|
|
128
|
+
return;
|
|
129
|
+
return {
|
|
130
|
+
key: measure.key,
|
|
131
|
+
label: measure.label,
|
|
132
|
+
type: "number",
|
|
133
|
+
format: measure.format
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function readValue(row, measure) {
|
|
137
|
+
return measure ? getAtPath(row, measure.dataPath) : undefined;
|
|
138
|
+
}
|
|
139
|
+
function readDimensionValue(row, dimension) {
|
|
140
|
+
return dimension ? getAtPath(row, dimension.dataPath) : undefined;
|
|
141
|
+
}
|
|
142
|
+
function numericValue(row, measure) {
|
|
143
|
+
return toNumber(readValue(row, measure));
|
|
144
|
+
}
|
|
145
|
+
function numericPathValue(row, dimension) {
|
|
146
|
+
return toNumber(readDimensionValue(row, dimension));
|
|
147
|
+
}
|
|
148
|
+
function stringValue(row, dimension) {
|
|
149
|
+
const value = readDimensionValue(row, dimension);
|
|
150
|
+
return value == null ? undefined : String(value);
|
|
151
|
+
}
|
|
152
|
+
function createTable(caption, config, rows) {
|
|
153
|
+
const dimensions = config.dimensions ?? [];
|
|
154
|
+
const measures = config.measures ?? [];
|
|
155
|
+
return {
|
|
156
|
+
caption,
|
|
157
|
+
columns: [
|
|
158
|
+
...dimensions.map((dimension) => ({
|
|
159
|
+
key: dimension.key,
|
|
160
|
+
label: dimension.label
|
|
161
|
+
})),
|
|
162
|
+
...measures.map((measure) => ({
|
|
163
|
+
key: measure.key,
|
|
164
|
+
label: measure.label
|
|
165
|
+
}))
|
|
166
|
+
],
|
|
167
|
+
rows: rows.map((row) => ({
|
|
168
|
+
...Object.fromEntries(dimensions.map((dimension) => [
|
|
169
|
+
dimension.key,
|
|
170
|
+
readDimensionValue(row, dimension)
|
|
171
|
+
])),
|
|
172
|
+
...Object.fromEntries(measures.map((measure) => [measure.key, readValue(row, measure)]))
|
|
173
|
+
}))
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function resolveAnnotation(annotation, row) {
|
|
177
|
+
return {
|
|
178
|
+
key: annotation.key,
|
|
179
|
+
label: annotation.label,
|
|
180
|
+
kind: annotation.kind,
|
|
181
|
+
color: annotation.color,
|
|
182
|
+
x: annotation.xDataPath ? getAtPath(row, annotation.xDataPath) : undefined,
|
|
183
|
+
y: annotation.yDataPath ? toNumber(getAtPath(row, annotation.yDataPath)) : undefined,
|
|
184
|
+
start: annotation.startDataPath ? getAtPath(row, annotation.startDataPath) : undefined,
|
|
185
|
+
end: annotation.endDataPath ? getAtPath(row, annotation.endDataPath) : undefined
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
stringValue,
|
|
190
|
+
readValue,
|
|
191
|
+
readDimensionValue,
|
|
192
|
+
numericValue,
|
|
193
|
+
numericPathValue,
|
|
194
|
+
defaultSeries,
|
|
195
|
+
createVisualizationMaps,
|
|
196
|
+
createVisualizationBaseModel,
|
|
197
|
+
createValueAxis,
|
|
198
|
+
createMeasureSeries,
|
|
199
|
+
createAxis
|
|
200
|
+
};
|