@ifc-lite/charts 0.1.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.
@@ -0,0 +1,40 @@
1
+ import type { ChartDataset, ChartDatasetColumn } from './types.js';
2
+ /** The columns of a columnar entity table this adapter reads — structural, so
3
+ * any store shape that carries them (parsed, cached, server-hydrated) works. */
4
+ export interface ElementsEntityTable {
5
+ readonly count: number;
6
+ readonly expressId: ArrayLike<number>;
7
+ /** `EntityFlags` bits per row. */
8
+ readonly flags: ArrayLike<number>;
9
+ getName(expressId: number): string;
10
+ getTypeName(expressId: number): string;
11
+ }
12
+ export interface ElementsStore {
13
+ entities: ElementsEntityTable;
14
+ spatialHierarchy?: {
15
+ elementToStorey: ReadonlyMap<number, number>;
16
+ };
17
+ }
18
+ export declare const ELEMENT_COLUMNS: {
19
+ readonly ifcType: "IfcType";
20
+ readonly storey: "Storey";
21
+ readonly model: "Model";
22
+ readonly name: "Name";
23
+ };
24
+ export declare const ELEMENT_DATASET_COLUMNS: ChartDatasetColumn[];
25
+ export interface ElementsDatasetModel {
26
+ /** The parsed store (its `entities` + `spatialHierarchy` are read). */
27
+ store: ElementsStore;
28
+ /**
29
+ * Local express id → renderer (federated) global id, the host's canonical
30
+ * resolver (in the viewer, the store's `toGlobalId` over the federation
31
+ * registry). The package does no id arithmetic of its own.
32
+ */
33
+ toGlobalId: (expressId: number) => number;
34
+ /** Human model name for the `Model` column. */
35
+ name: string;
36
+ /** Keep only these express ids (a list scope, the visible set); all when absent. */
37
+ include?: ReadonlySet<number>;
38
+ }
39
+ export declare function elementsDataset(models: readonly ElementsDatasetModel[]): ChartDataset;
40
+ //# sourceMappingURL=elements-dataset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements-dataset.d.ts","sourceRoot":"","sources":["../src/elements-dataset.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAmB,MAAM,YAAY,CAAC;AAEpF;iFACiF;AACjF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACtC,kCAAkC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IAClC,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,gBAAgB,CAAC,EAAE;QAAE,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;CACrE;AAED,eAAO,MAAM,eAAe;;;;;CAKlB,CAAC;AAEX,eAAO,MAAM,uBAAuB,EAAE,kBAAkB,EAKvD,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,uEAAuE;IACvE,KAAK,EAAE,aAAa,CAAC;IACrB;;;;OAIG;IACH,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC/B;AAiBD,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,oBAAoB,EAAE,GAAG,YAAY,CA+BrF"}
@@ -0,0 +1,76 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * The `elements` dataset straight off a model's entity table: one row per
6
+ * element instance with its IFC type, storey and model — the three
7
+ * dimensions every overview chart wants, read from the columnar tables in
8
+ * one pass, no per-entity extraction. Property / quantity / material columns
9
+ * are the lists engine's job (a host merges those in through
10
+ * `ListDefinition` columns); this is the fast path that needs no list.
11
+ *
12
+ * Rows carry RENDERER ids through the host's own resolver (`toGlobalId`), so
13
+ * a bucket's ids can go straight to selection and visibility.
14
+ */
15
+ import { EntityFlags } from '@ifc-lite/data';
16
+ export const ELEMENT_COLUMNS = {
17
+ ifcType: 'IfcType',
18
+ storey: 'Storey',
19
+ model: 'Model',
20
+ name: 'Name',
21
+ };
22
+ export const ELEMENT_DATASET_COLUMNS = [
23
+ { id: ELEMENT_COLUMNS.ifcType, label: 'IFC type', kind: 'category' },
24
+ { id: ELEMENT_COLUMNS.storey, label: 'Storey', kind: 'category' },
25
+ { id: ELEMENT_COLUMNS.model, label: 'Model', kind: 'category' },
26
+ { id: ELEMENT_COLUMNS.name, label: 'Name', kind: 'category' },
27
+ ];
28
+ /** FNV-1a over the row identities, so two include sets of equal size differ. */
29
+ function fingerprintRows(ids) {
30
+ let h = 0x811c9dc5;
31
+ for (const id of ids) {
32
+ h ^= id;
33
+ h = Math.imul(h, 0x01000193) >>> 0;
34
+ }
35
+ return h.toString(16);
36
+ }
37
+ /** Only instances with geometry are chartable elements — types, styles, relationships are not. */
38
+ function isElementRow(flags) {
39
+ return (flags & EntityFlags.HAS_GEOMETRY) !== 0 && (flags & EntityFlags.IS_TYPE) === 0;
40
+ }
41
+ export function elementsDataset(models) {
42
+ const rows = [];
43
+ const fingerprintParts = [];
44
+ for (const model of models) {
45
+ const { entities, spatialHierarchy } = model.store;
46
+ const storeyNames = new Map();
47
+ const storeyOf = (expressId) => {
48
+ const storeyId = spatialHierarchy?.elementToStorey.get(expressId);
49
+ if (!storeyId)
50
+ return '';
51
+ let cached = storeyNames.get(storeyId);
52
+ if (cached === undefined) {
53
+ cached = entities.getName(storeyId) || '';
54
+ storeyNames.set(storeyId, cached);
55
+ }
56
+ return cached;
57
+ };
58
+ const rowIds = [];
59
+ for (let i = 0; i < entities.count; i++) {
60
+ if (!isElementRow(entities.flags[i]))
61
+ continue;
62
+ const expressId = entities.expressId[i];
63
+ if (model.include && !model.include.has(expressId))
64
+ continue;
65
+ const globalId = model.toGlobalId(expressId);
66
+ rowIds.push(globalId);
67
+ rows.push({
68
+ ids: [globalId],
69
+ values: [entities.getTypeName(expressId), storeyOf(expressId), model.name, entities.getName(expressId)],
70
+ });
71
+ }
72
+ fingerprintParts.push(`${model.name}:${rowIds.length}/${entities.count}#${fingerprintRows(rowIds)}`);
73
+ }
74
+ return { source: 'elements', columns: ELEMENT_DATASET_COLUMNS, rows, fingerprint: fingerprintParts.join('|') };
75
+ }
76
+ //# sourceMappingURL=elements-dataset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements-dataset.js","sourceRoot":"","sources":["../src/elements-dataset.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export * from './types.js';
2
+ export { aggregate, idsForCategories, categoriesForIds, idsForItems, itemsForIds, isoWeekKey, sturgesBins, type AggregateOptions, type AggregateResult } from './aggregate.js';
3
+ export { assignColors, emptyPalette, paletteColor, OTHER_BUCKET_KEY, OTHER_BUCKET_COLOR, type PaletteAssignment } from './palette.js';
4
+ export { buildEChartsOption, DEFAULT_THEME, type ChartTheme, type BuildOptionArgs, type EChartsOptionObject } from './echarts-option.js';
5
+ export { renderChartSvg, registerEChartsModules, type RenderSvgOptions } from './render-svg.js';
6
+ export { validateDashboardSpec, isDashboardSpec, isReportSpec, DASHBOARD_GRID_COLUMNS, type DashboardValidationError } from './validate.js';
7
+ export { elementsDataset, ELEMENT_COLUMNS, ELEMENT_DATASET_COLUMNS, type ElementsDatasetModel, type ElementsStore, type ElementsEntityTable } from './elements-dataset.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC/K,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtI,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AACzI,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAChG,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,YAAY,EAAE,sBAAsB,EAAE,KAAK,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAC5I,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,uBAAuB,EAAE,KAAK,oBAAoB,EAAE,KAAK,aAAa,EAAE,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ export * from './types.js';
5
+ export { aggregate, idsForCategories, categoriesForIds, idsForItems, itemsForIds, isoWeekKey, sturgesBins } from './aggregate.js';
6
+ export { assignColors, emptyPalette, paletteColor, OTHER_BUCKET_KEY, OTHER_BUCKET_COLOR } from './palette.js';
7
+ export { buildEChartsOption, DEFAULT_THEME } from './echarts-option.js';
8
+ export { renderChartSvg, registerEChartsModules } from './render-svg.js';
9
+ export { validateDashboardSpec, isDashboardSpec, isReportSpec, DASHBOARD_GRID_COLUMNS } from './validate.js';
10
+ export { elementsDataset, ELEMENT_COLUMNS, ELEMENT_DATASET_COLUMNS } from './elements-dataset.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAA+C,MAAM,gBAAgB,CAAC;AAC/K,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAA0B,MAAM,cAAc,CAAC;AACtI,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAmE,MAAM,qBAAqB,CAAC;AACzI,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAyB,MAAM,iBAAiB,CAAC;AAChG,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,YAAY,EAAE,sBAAsB,EAAiC,MAAM,eAAe,CAAC;AAC5I,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,uBAAuB,EAA2E,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,18 @@
1
+ /** label → colour, plus the next free slot so a new label never reuses one. */
2
+ export interface PaletteAssignment {
3
+ colors: Map<string, string>;
4
+ nextSlot: number;
5
+ }
6
+ /** Colour for the i-th slot: the 12 lens presets, then generated hues. */
7
+ export declare function paletteColor(slot: number): string;
8
+ /** `Bucket.key` of the synthetic folded tail; `aggregate` flags it, it never goes through the palette. */
9
+ export declare const OTHER_BUCKET_KEY = "__other__";
10
+ /** `Other` is always the same neutral grey, whatever slot it would have had. */
11
+ export declare const OTHER_BUCKET_COLOR = "#9E9E9E";
12
+ export declare function emptyPalette(): PaletteAssignment;
13
+ /**
14
+ * Extend `previous` with colours for `labels` (in the given order) and return
15
+ * the new assignment. `previous` is not mutated.
16
+ */
17
+ export declare function assignColors(labels: readonly string[], previous?: PaletteAssignment): PaletteAssignment;
18
+ //# sourceMappingURL=palette.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"palette.d.ts","sourceRoot":"","sources":["../src/palette.ts"],"names":[],"mappings":"AAiBA,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,0GAA0G;AAC1G,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAC5C,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C,wBAAgB,YAAY,IAAI,iBAAiB,CAEhD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,GAAE,iBAAkC,GAAG,iBAAiB,CASvH"}
@@ -0,0 +1,42 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * Bucket colours: the lens palette first (so a chart legend and a lens rule
6
+ * for the same category agree), then the lens's golden-angle generator for
7
+ * the long tail.
8
+ *
9
+ * Colours are assigned by LABEL and kept across re-aggregations: a bucket
10
+ * that drops from first to third place when the scope changes keeps its
11
+ * colour, otherwise every slice swaps hue on each 3D pick and the legend
12
+ * stops being readable as a key. A consumer threads the previous
13
+ * `PaletteAssignment` through; new labels take the lowest unused slot.
14
+ */
15
+ import { LENS_PALETTE, uniqueColor } from '@ifc-lite/lens';
16
+ /** Colour for the i-th slot: the 12 lens presets, then generated hues. */
17
+ export function paletteColor(slot) {
18
+ return slot < LENS_PALETTE.length ? LENS_PALETTE[slot] : uniqueColor(slot);
19
+ }
20
+ /** `Bucket.key` of the synthetic folded tail; `aggregate` flags it, it never goes through the palette. */
21
+ export const OTHER_BUCKET_KEY = '__other__';
22
+ /** `Other` is always the same neutral grey, whatever slot it would have had. */
23
+ export const OTHER_BUCKET_COLOR = '#9E9E9E';
24
+ export function emptyPalette() {
25
+ return { colors: new Map(), nextSlot: 0 };
26
+ }
27
+ /**
28
+ * Extend `previous` with colours for `labels` (in the given order) and return
29
+ * the new assignment. `previous` is not mutated.
30
+ */
31
+ export function assignColors(labels, previous = emptyPalette()) {
32
+ const colors = new Map(previous.colors);
33
+ let nextSlot = previous.nextSlot;
34
+ for (const label of labels) {
35
+ if (colors.has(label))
36
+ continue;
37
+ colors.set(label, paletteColor(nextSlot));
38
+ nextSlot += 1;
39
+ }
40
+ return { colors, nextSlot };
41
+ }
42
+ //# sourceMappingURL=palette.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"palette.js","sourceRoot":"","sources":["../src/palette.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;GAUG;AACH,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAQ3D,0EAA0E;AAC1E,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AAC7E,CAAC;AAED,0GAA0G;AAC1G,MAAM,CAAC,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAC5C,gFAAgF;AAChF,MAAM,CAAC,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE5C,MAAM,UAAU,YAAY;IAC1B,OAAO,EAAE,MAAM,EAAE,IAAI,GAAG,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;AAC5C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAyB,EAAE,WAA8B,YAAY,EAAE;IAClG,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,SAAS;QAChC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC1C,QAAQ,IAAI,CAAC,CAAC;IAChB,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { type BuildOptionArgs } from './echarts-option.js';
2
+ /** Register the chart types and components this package uses, once. */
3
+ export declare function registerEChartsModules(): void;
4
+ export interface RenderSvgOptions extends BuildOptionArgs {
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export declare function renderChartSvg(options: RenderSvgOptions): string;
9
+ //# sourceMappingURL=render-svg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-svg.d.ts","sourceRoot":"","sources":["../src/render-svg.ts"],"names":[],"mappings":"AAcA,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG/E,uEAAuE;AACvE,wBAAgB,sBAAsB,IAAI,IAAI,CAI7C;AAED,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAahE"}
@@ -0,0 +1,38 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * Vector rendering of a chart, off-screen — for the PDF report and for the
6
+ * CLI. ECharts' SVG renderer in SSR mode needs no DOM, so this runs in Node
7
+ * (and in a test) exactly as in the browser. One chart per SVG: ECharts'
8
+ * connected-canvas export is not available on the SVG renderer.
9
+ */
10
+ import { init, use } from 'echarts/core';
11
+ import { BarChart, PieChart, TreemapChart } from 'echarts/charts';
12
+ import { GridComponent, LegendComponent, TitleComponent, TooltipComponent } from 'echarts/components';
13
+ import { SVGRenderer } from 'echarts/renderers';
14
+ import { buildEChartsOption } from './echarts-option.js';
15
+ let registered = false;
16
+ /** Register the chart types and components this package uses, once. */
17
+ export function registerEChartsModules() {
18
+ if (registered)
19
+ return;
20
+ use([BarChart, PieChart, TreemapChart, GridComponent, LegendComponent, TitleComponent, TooltipComponent, SVGRenderer]);
21
+ registered = true;
22
+ }
23
+ export function renderChartSvg(options) {
24
+ registerEChartsModules();
25
+ const chart = init(null, null, { renderer: 'svg', ssr: true, width: options.width, height: options.height });
26
+ try {
27
+ // A font stack read off a stylesheet quotes family names (`"Segoe UI"`);
28
+ // zrender writes it into an attribute verbatim, and a double quote there
29
+ // makes the SVG malformed XML for any parser downstream (svg2pdf).
30
+ const theme = options.theme ? { ...options.theme, fontFamily: options.theme.fontFamily.replace(/"/g, "'") } : options.theme;
31
+ chart.setOption(buildEChartsOption({ ...options, theme, showTitle: options.showTitle ?? true }));
32
+ return chart.renderToSVGString();
33
+ }
34
+ finally {
35
+ chart.dispose();
36
+ }
37
+ }
38
+ //# sourceMappingURL=render-svg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-svg.js","sourceRoot":"","sources":["../src/render-svg.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;GAKG;AACH,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtG,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAwB,MAAM,qBAAqB,CAAC;AAE/E,IAAI,UAAU,GAAG,KAAK,CAAC;AACvB,uEAAuE;AACvE,MAAM,UAAU,sBAAsB;IACpC,IAAI,UAAU;QAAE,OAAO;IACvB,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,CAAC,CAAC,CAAC;IACvH,UAAU,GAAG,IAAI,CAAC;AACpB,CAAC;AAOD,MAAM,UAAU,cAAc,CAAC,OAAyB;IACtD,sBAAsB,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7G,IAAI,CAAC;QACH,yEAAyE;QACzE,yEAAyE;QACzE,mEAAmE;QACnE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;QAC5H,KAAK,CAAC,SAAS,CAAC,kBAAkB,CAAC,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;QACjG,OAAO,KAAK,CAAC,iBAAiB,EAAE,CAAC;IACnC,CAAC;YAAS,CAAC;QACT,KAAK,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC;AACH,CAAC"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The chart data contract (#3944).
3
+ *
4
+ * A chart is bound to a DATASET — rows that each carry the renderer ids of
5
+ * the elements they stand for — and a SPEC that says how to bucket those
6
+ * rows. Aggregation keeps every bucket's member ids, which is what makes the
7
+ * chart bidirectional with the 3D view: a bar is a set of element ids, and a
8
+ * 3D selection is a set of buckets. The chart library only ever sees
9
+ * `(seriesIndex, dataIndex)`; the id math lives here.
10
+ */
11
+ export type CellValue = string | number | boolean | null;
12
+ /** Where a dataset's rows come from; decides which adapter builds it. */
13
+ export type ChartSource = 'elements' | 'clash' | 'bcf' | 'schedule' | 'ids' | 'compare';
14
+ export interface ChartDatasetColumn {
15
+ id: string;
16
+ label: string;
17
+ kind: 'category' | 'number' | 'boolean' | 'date';
18
+ /** Display unit for a `number` column (`m²`, `m³`, `mm`), if any. */
19
+ unit?: string;
20
+ }
21
+ export interface ChartDatasetRow {
22
+ /** Renderer (federated) global ids of the elements this row stands for —
23
+ * one for an element, two for a clash pair, N for a schedule task. */
24
+ ids: ArrayLike<number>;
25
+ /** One value per dataset column, in column order. */
26
+ values: CellValue[];
27
+ }
28
+ export interface ChartDataset {
29
+ source: ChartSource;
30
+ columns: ChartDatasetColumn[];
31
+ rows: ChartDatasetRow[];
32
+ /** Changes whenever the rows do; lets a consumer skip re-aggregation. */
33
+ fingerprint: string;
34
+ }
35
+ export type ChartType = 'bar' | 'stackedBar' | 'pie' | 'treemap' | 'histogram' | 'timeline';
36
+ export type ChartAggregate = 'count' | 'sum';
37
+ export interface ChartMeasure {
38
+ agg: ChartAggregate;
39
+ /** The `number` column to sum; ignored for `count`. */
40
+ column?: string;
41
+ }
42
+ export interface ChartSpec {
43
+ id: string;
44
+ title: string;
45
+ source: ChartSource;
46
+ type: ChartType;
47
+ /** Column id to bucket by. A `date` column for `timeline`, a `number` column for `histogram`. */
48
+ dimension: string;
49
+ /** Second category column for `stackedBar`: one series per distinct value. */
50
+ stackBy?: string;
51
+ measure: ChartMeasure;
52
+ /** Bucket order; default `value` (largest first). `label` sorts by label, dates chronologically. */
53
+ sort?: 'value' | 'label';
54
+ /** Keep the first N buckets and fold the rest into one `Other` bucket. */
55
+ topN?: number;
56
+ /** Histogram bin count; Sturges' rule when absent. */
57
+ bins?: number;
58
+ }
59
+ /** What a dashboard aggregates over; resolved by the host into a dataset scope. */
60
+ export type ChartScope = {
61
+ kind: 'all';
62
+ } | {
63
+ kind: 'visible';
64
+ } | {
65
+ kind: 'basket';
66
+ } | {
67
+ kind: 'list';
68
+ listId: string;
69
+ };
70
+ export interface DashboardLayoutItem {
71
+ chartId: string;
72
+ x: number;
73
+ y: number;
74
+ w: number;
75
+ h: number;
76
+ }
77
+ export interface DashboardSpec {
78
+ version: 1;
79
+ id: string;
80
+ name: string;
81
+ scope: ChartScope;
82
+ charts: ChartSpec[];
83
+ layout: DashboardLayoutItem[];
84
+ }
85
+ export interface ReportPageSetup {
86
+ size: 'A4' | 'A3';
87
+ orientation: 'portrait' | 'landscape';
88
+ }
89
+ /** A dashboard plus what a printed report needs: page setup, title-block fields, snapshots. */
90
+ export interface ReportSpec extends DashboardSpec {
91
+ page: ReportPageSetup;
92
+ titleBlock: Record<string, string>;
93
+ /** Render a 3D snapshot of each chart's top bucket under it. */
94
+ snapshots: boolean;
95
+ }
96
+ export interface Bucket {
97
+ /** Stable key: the dimension value (stringified) or a bin/week key. */
98
+ key: string;
99
+ label: string;
100
+ /** The measure: count, or sum of the measure column. */
101
+ value: number;
102
+ /** Rows in the bucket, regardless of measure. */
103
+ count: number;
104
+ /** Renderer global ids of every element in the bucket, de-duplicated. */
105
+ ids: Uint32Array;
106
+ /** Hex colour, stable per label across re-aggregations. */
107
+ color: string;
108
+ }
109
+ /** One series of buckets — a plain chart has one, a stacked bar one per `stackBy` value. */
110
+ export interface BucketSeries {
111
+ key: string;
112
+ label: string;
113
+ buckets: Bucket[];
114
+ }
115
+ /** One selectable mark in a chart: a bucket of one series. A plain chart has one series. */
116
+ export interface ChartItem {
117
+ seriesIndex: number;
118
+ dataIndex: number;
119
+ }
120
+ export interface Aggregation {
121
+ spec: ChartSpec;
122
+ /** Category axis in display order; every series aligns to it. */
123
+ categories: Bucket[];
124
+ series: BucketSeries[];
125
+ total: number;
126
+ /** Rows the spec could not place (missing dimension value). */
127
+ unbucketed: number;
128
+ /** Element id → every index into `categories` it belongs to (a clash element under two rules is in two). */
129
+ categoryOf: Map<number, number[]>;
130
+ /** Display unit of the summed column, when the measure is a sum. */
131
+ unit?: string;
132
+ }
133
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;+DAE+D"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Structural validation of a `DashboardSpec` / `ReportSpec` read from
3
+ * outside — a saved `.ifclite-dashboard.json`, localStorage, a preset. The
4
+ * shape is small enough that a hand-written checker is clearer than a schema
5
+ * library, and it reports every problem at once with a JSON-pointer-ish path.
6
+ */
7
+ import type { DashboardSpec, ReportSpec } from './types.js';
8
+ export interface DashboardValidationError {
9
+ path: string;
10
+ message: string;
11
+ }
12
+ /** The dashboard layout's column count: every `DashboardLayoutItem` lives in a 12-column grid. */
13
+ export declare const DASHBOARD_GRID_COLUMNS = 12;
14
+ /** Every problem in a dashboard/report spec; an empty array means it is one. */
15
+ export declare function validateDashboardSpec(spec: unknown): DashboardValidationError[];
16
+ export declare function isDashboardSpec(spec: unknown): spec is DashboardSpec;
17
+ export declare function isReportSpec(spec: unknown): spec is ReportSpec;
18
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,OAAO,KAAK,EAA0B,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEpF,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,kGAAkG;AAClG,eAAO,MAAM,sBAAsB,KAAK,CAAC;AA8CzC,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,wBAAwB,EAAE,CA+D/E;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,aAAa,CAEpE;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,UAAU,CAE9D"}
@@ -0,0 +1,140 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /** The dashboard layout's column count: every `DashboardLayoutItem` lives in a 12-column grid. */
5
+ export const DASHBOARD_GRID_COLUMNS = 12;
6
+ const SOURCES = new Set(['elements', 'clash', 'bcf', 'schedule', 'ids', 'compare']);
7
+ const TYPES = new Set(['bar', 'stackedBar', 'pie', 'treemap', 'histogram', 'timeline']);
8
+ const SCOPES = new Set(['all', 'visible', 'basket', 'list']);
9
+ function isRecord(v) {
10
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
11
+ }
12
+ function str(errors, obj, key, path, optional = false) {
13
+ const v = obj[key];
14
+ if (v === undefined && optional)
15
+ return;
16
+ if (typeof v !== 'string' || v.length === 0)
17
+ errors.push({ path: `${path}.${key}`, message: 'expected a non-empty string' });
18
+ }
19
+ function num(errors, obj, key, path, optional = false) {
20
+ const v = obj[key];
21
+ if (v === undefined && optional)
22
+ return;
23
+ if (typeof v !== 'number' || !Number.isFinite(v))
24
+ errors.push({ path: `${path}.${key}`, message: 'expected a finite number' });
25
+ }
26
+ function validateChart(chart, path, errors) {
27
+ if (!isRecord(chart)) {
28
+ errors.push({ path, message: 'expected a chart object' });
29
+ return;
30
+ }
31
+ str(errors, chart, 'id', path);
32
+ str(errors, chart, 'title', path);
33
+ if (!SOURCES.has(String(chart.source)))
34
+ errors.push({ path: `${path}.source`, message: `expected one of ${[...SOURCES].join(', ')}` });
35
+ if (!TYPES.has(String(chart.type)))
36
+ errors.push({ path: `${path}.type`, message: `expected one of ${[...TYPES].join(', ')}` });
37
+ str(errors, chart, 'dimension', path);
38
+ str(errors, chart, 'stackBy', path, true);
39
+ if (chart.type === 'stackedBar' && typeof chart.stackBy !== 'string')
40
+ errors.push({ path: `${path}.stackBy`, message: 'a stackedBar needs stackBy' });
41
+ const measure = chart.measure;
42
+ if (!isRecord(measure) || (measure.agg !== 'count' && measure.agg !== 'sum')) {
43
+ errors.push({ path: `${path}.measure`, message: 'expected { agg: "count" | "sum", column? }' });
44
+ }
45
+ else if (measure.agg === 'sum') {
46
+ str(errors, measure, 'column', `${path}.measure`);
47
+ }
48
+ if (chart.sort !== undefined && chart.sort !== 'value' && chart.sort !== 'label')
49
+ errors.push({ path: `${path}.sort`, message: 'expected "value" or "label"' });
50
+ num(errors, chart, 'topN', path, true);
51
+ num(errors, chart, 'bins', path, true);
52
+ if (typeof chart.bins === 'number' && chart.bins < 1)
53
+ errors.push({ path: `${path}.bins`, message: 'expected at least 1 bin' });
54
+ }
55
+ /** Every problem in a dashboard/report spec; an empty array means it is one. */
56
+ export function validateDashboardSpec(spec) {
57
+ const errors = [];
58
+ if (!isRecord(spec))
59
+ return [{ path: '', message: 'expected a dashboard object' }];
60
+ if (spec.version !== 1)
61
+ errors.push({ path: '.version', message: 'expected version 1' });
62
+ str(errors, spec, 'id', '');
63
+ str(errors, spec, 'name', '');
64
+ const scope = spec.scope;
65
+ if (!isRecord(scope) || !SCOPES.has(String(scope.kind))) {
66
+ errors.push({ path: '.scope', message: `expected { kind: ${[...SCOPES].join(' | ')} }` });
67
+ }
68
+ else if (scope.kind === 'list') {
69
+ str(errors, scope, 'listId', '.scope');
70
+ }
71
+ if (!Array.isArray(spec.charts)) {
72
+ errors.push({ path: '.charts', message: 'expected an array of charts' });
73
+ }
74
+ else {
75
+ const ids = new Set();
76
+ spec.charts.forEach((chart, i) => {
77
+ validateChart(chart, `.charts[${i}]`, errors);
78
+ if (isRecord(chart) && typeof chart.id === 'string') {
79
+ if (ids.has(chart.id))
80
+ errors.push({ path: `.charts[${i}].id`, message: `duplicate chart id "${chart.id}"` });
81
+ ids.add(chart.id);
82
+ }
83
+ });
84
+ if (!Array.isArray(spec.layout)) {
85
+ errors.push({ path: '.layout', message: 'expected an array of layout items' });
86
+ }
87
+ else {
88
+ spec.layout.forEach((item, i) => {
89
+ const path = `.layout[${i}]`;
90
+ if (!isRecord(item)) {
91
+ errors.push({ path, message: 'expected a layout item' });
92
+ return;
93
+ }
94
+ str(errors, item, 'chartId', path);
95
+ if (typeof item.chartId === 'string' && !ids.has(item.chartId))
96
+ errors.push({ path: `${path}.chartId`, message: `no chart with id "${item.chartId}"` });
97
+ for (const key of ['x', 'y', 'w', 'h'])
98
+ num(errors, item, key, path);
99
+ // Grid cells: non-negative integers, at least one cell wide/high, inside the column count.
100
+ for (const key of ['x', 'y', 'w', 'h']) {
101
+ const v = item[key];
102
+ if (typeof v !== 'number' || !Number.isFinite(v))
103
+ continue;
104
+ if (!Number.isInteger(v) || v < 0)
105
+ errors.push({ path: `${path}.${key}`, message: 'expected a non-negative integer' });
106
+ else if ((key === 'w' || key === 'h') && v < 1)
107
+ errors.push({ path: `${path}.${key}`, message: 'expected at least 1' });
108
+ }
109
+ if (typeof item.x === 'number' && typeof item.w === 'number' && item.x + item.w > DASHBOARD_GRID_COLUMNS) {
110
+ errors.push({ path: `${path}.x`, message: `x + w exceeds the ${DASHBOARD_GRID_COLUMNS}-column grid` });
111
+ }
112
+ });
113
+ }
114
+ }
115
+ const page = spec.page;
116
+ if (page !== undefined) {
117
+ if (!isRecord(page) || (page.size !== 'A4' && page.size !== 'A3') || (page.orientation !== 'portrait' && page.orientation !== 'landscape')) {
118
+ errors.push({ path: '.page', message: 'expected { size: "A4" | "A3", orientation: "portrait" | "landscape" }' });
119
+ }
120
+ if (!isRecord(spec.titleBlock)) {
121
+ errors.push({ path: '.titleBlock', message: 'expected an object of title-block fields' });
122
+ }
123
+ else {
124
+ for (const [key, value] of Object.entries(spec.titleBlock)) {
125
+ if (typeof value !== 'string')
126
+ errors.push({ path: `.titleBlock.${key}`, message: 'expected a string' });
127
+ }
128
+ }
129
+ if (typeof spec.snapshots !== 'boolean')
130
+ errors.push({ path: '.snapshots', message: 'expected a boolean' });
131
+ }
132
+ return errors;
133
+ }
134
+ export function isDashboardSpec(spec) {
135
+ return validateDashboardSpec(spec).length === 0;
136
+ }
137
+ export function isReportSpec(spec) {
138
+ return isRecord(spec) && spec.page !== undefined && validateDashboardSpec(spec).length === 0;
139
+ }
140
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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"}