mviz 1.6.6 → 1.6.7
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/dist/charts/area.js +8 -37
- package/dist/charts/bar.js +8 -27
- package/dist/charts/combo.js +17 -41
- package/dist/charts/line.js +6 -28
- package/dist/components/big_value.d.ts +23 -1
- package/dist/components/big_value.js +84 -25
- package/dist/components/delta.d.ts +24 -1
- package/dist/components/delta.js +63 -17
- package/dist/components/table-interactivity.d.ts +17 -1
- package/dist/components/table-interactivity.js +34 -1
- package/dist/components/table.js +1 -1
- package/dist/core/chart-helpers.d.ts +59 -5
- package/dist/core/chart-helpers.js +84 -5
- package/dist/core/formatting.d.ts +33 -0
- package/dist/core/formatting.js +135 -0
- package/dist/core/linter.d.ts +3 -0
- package/dist/core/linter.js +52 -8
- package/dist/layout/block-loader.d.ts +31 -0
- package/dist/layout/block-loader.js +143 -0
- package/dist/layout/layout-resolver.d.ts +33 -0
- package/dist/layout/layout-resolver.js +73 -0
- package/dist/layout/markdown-parser.d.ts +34 -0
- package/dist/layout/markdown-parser.js +395 -0
- package/dist/layout/parser-types.d.ts +116 -0
- package/dist/layout/parser-types.js +11 -0
- package/dist/layout/parser.d.ts +30 -21
- package/dist/layout/parser.js +116 -1067
- package/dist/layout/renderer.d.ts +33 -0
- package/dist/layout/renderer.js +450 -0
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/schema/mviz.v1.schema.json +359 -33
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* input), the per-cell `data-sort` attribute for numeric sorting, and one
|
|
7
7
|
* vanilla-JS init function that wires up click/input handlers.
|
|
8
8
|
*/
|
|
9
|
+
import type { FormatType } from '../types.js';
|
|
9
10
|
export interface TableInteractivity {
|
|
10
11
|
sortable: boolean;
|
|
11
12
|
filter: boolean;
|
|
@@ -22,8 +23,23 @@ export declare function sortableHeaderAttrs(colIdx: number, sortable: boolean):
|
|
|
22
23
|
/**
|
|
23
24
|
* Generate `data-sort="..."` for a cell so numeric sort doesn't depend on the
|
|
24
25
|
* rendered text (which may be formatted, contain SVG sparklines, etc.).
|
|
26
|
+
*
|
|
27
|
+
* When the column declares a `format`, the sort key is derived from that
|
|
28
|
+
* declaration — `date` → epoch ms, `duration` → seconds, numeric formats →
|
|
29
|
+
* raw number. This is the preferred path because format removes ambiguity
|
|
30
|
+
* (e.g. is `"5m"` 5 minutes or 5 million dollars?).
|
|
31
|
+
*
|
|
32
|
+
* For untyped columns, two unambiguous string shapes still get a numeric
|
|
33
|
+
* key as a courtesy: ISO-8601 dates (`YYYY-MM-DD`) and duration strings
|
|
34
|
+
* that include a seconds segment (`10s`, `1m7s`, `1h2m3s`). Anything else —
|
|
35
|
+
* including bare `5m` or `1h`, which is ambiguous (5 minutes or 5 million?) —
|
|
36
|
+
* falls back to locale string sort. Columns that explicitly declare
|
|
37
|
+
* `fmt: "duration"` still get lenient parsing of `5m`/`1h`; the strict form
|
|
38
|
+
* only applies to the untyped courtesy path. Pre-formatted currency strings
|
|
39
|
+
* are intentionally not parsed — pass numeric values and declare
|
|
40
|
+
* `format: "currency_auto"` (etc.) instead.
|
|
25
41
|
*/
|
|
26
|
-
export declare function cellSortAttr(value: unknown): string;
|
|
42
|
+
export declare function cellSortAttr(value: unknown, format?: FormatType): string;
|
|
27
43
|
/**
|
|
28
44
|
* Build the filter toolbar (global search input when `filter: true`).
|
|
29
45
|
*/
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* input), the per-cell `data-sort` attribute for numeric sorting, and one
|
|
7
7
|
* vanilla-JS init function that wires up click/input handlers.
|
|
8
8
|
*/
|
|
9
|
+
import { formatSortKey, parseDurationString, parseIsoDateString, } from '../core/formatting.js';
|
|
9
10
|
/**
|
|
10
11
|
* Read sortable/filter options from a spec. Sort defaults ON, filter defaults OFF.
|
|
11
12
|
*/
|
|
@@ -27,10 +28,33 @@ export function sortableHeaderAttrs(colIdx, sortable) {
|
|
|
27
28
|
/**
|
|
28
29
|
* Generate `data-sort="..."` for a cell so numeric sort doesn't depend on the
|
|
29
30
|
* rendered text (which may be formatted, contain SVG sparklines, etc.).
|
|
31
|
+
*
|
|
32
|
+
* When the column declares a `format`, the sort key is derived from that
|
|
33
|
+
* declaration — `date` → epoch ms, `duration` → seconds, numeric formats →
|
|
34
|
+
* raw number. This is the preferred path because format removes ambiguity
|
|
35
|
+
* (e.g. is `"5m"` 5 minutes or 5 million dollars?).
|
|
36
|
+
*
|
|
37
|
+
* For untyped columns, two unambiguous string shapes still get a numeric
|
|
38
|
+
* key as a courtesy: ISO-8601 dates (`YYYY-MM-DD`) and duration strings
|
|
39
|
+
* that include a seconds segment (`10s`, `1m7s`, `1h2m3s`). Anything else —
|
|
40
|
+
* including bare `5m` or `1h`, which is ambiguous (5 minutes or 5 million?) —
|
|
41
|
+
* falls back to locale string sort. Columns that explicitly declare
|
|
42
|
+
* `fmt: "duration"` still get lenient parsing of `5m`/`1h`; the strict form
|
|
43
|
+
* only applies to the untyped courtesy path. Pre-formatted currency strings
|
|
44
|
+
* are intentionally not parsed — pass numeric values and declare
|
|
45
|
+
* `format: "currency_auto"` (etc.) instead.
|
|
30
46
|
*/
|
|
31
|
-
export function cellSortAttr(value) {
|
|
47
|
+
export function cellSortAttr(value, format) {
|
|
32
48
|
if (value === null || value === undefined)
|
|
33
49
|
return '';
|
|
50
|
+
// Format-driven sort (preferred when declared).
|
|
51
|
+
if (format) {
|
|
52
|
+
const key = formatSortKey(value, format);
|
|
53
|
+
if (key !== null)
|
|
54
|
+
return ` data-sort="${key}"`;
|
|
55
|
+
// Format declared but value can't be coerced — fall through to the
|
|
56
|
+
// untyped heuristics so sort still does *something* sensible.
|
|
57
|
+
}
|
|
34
58
|
if (typeof value === 'number' && !isNaN(value)) {
|
|
35
59
|
return ` data-sort="${value}"`;
|
|
36
60
|
}
|
|
@@ -39,6 +63,15 @@ export function cellSortAttr(value) {
|
|
|
39
63
|
if (!isNaN(n) && String(n) === value.trim()) {
|
|
40
64
|
return ` data-sort="${n}"`;
|
|
41
65
|
}
|
|
66
|
+
const dateKey = parseIsoDateString(value);
|
|
67
|
+
if (dateKey !== null)
|
|
68
|
+
return ` data-sort="${dateKey}"`;
|
|
69
|
+
// Strict: require a seconds segment so bare "5m" / "1h" don't get parsed
|
|
70
|
+
// as durations. Those shapes are ambiguous in an untyped column; declare
|
|
71
|
+
// `fmt: "duration"` to opt into lenient parsing.
|
|
72
|
+
const durKey = parseDurationString(value, { requireSeconds: true });
|
|
73
|
+
if (durKey !== null)
|
|
74
|
+
return ` data-sort="${durKey}"`;
|
|
42
75
|
return ` data-sort="${escapeAttr(value)}"`;
|
|
43
76
|
}
|
|
44
77
|
// Objects/arrays (e.g., sparkline data) are not sortable; omit the attribute.
|
package/dist/components/table.js
CHANGED
|
@@ -498,7 +498,7 @@ function buildDataRows(cfg, inlineStyles = false) {
|
|
|
498
498
|
const cellResult = formatCell(value, col, cfg.colors, cfg.heatmapRanges, cfg.dumbbellRanges, cfg.defaultHeatmapColors, undefined, cfg.currency, cfg.pctMultiplyByColumn);
|
|
499
499
|
// Unwrap `{value, bold, italic}` overrides so numeric sort sees the real value.
|
|
500
500
|
const sortRaw = extractCellValue(value, col).value;
|
|
501
|
-
const sortAttr = cfg.interactivity.sortable ? cellSortAttr(sortRaw) : '';
|
|
501
|
+
const sortAttr = cfg.interactivity.sortable ? cellSortAttr(sortRaw, col.fmt) : '';
|
|
502
502
|
if (inlineStyles) {
|
|
503
503
|
const border = isLast ? `border-bottom: 1px solid ${cfg.colors.text};` : '';
|
|
504
504
|
const cellBg = cellResult.bgColor ?? rowBg;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared helpers for
|
|
2
|
+
* Shared helpers for chart builders.
|
|
3
3
|
*
|
|
4
|
-
* Consolidates
|
|
5
|
-
*
|
|
4
|
+
* Consolidates patterns that were duplicated across multiple chart builders:
|
|
5
|
+
* - scatter-family series grouping/labels (scatter, bubble)
|
|
6
|
+
* - value-axis format context (bar, line, area, combo)
|
|
7
|
+
* - x-axis label config (line, area, combo, bar)
|
|
8
|
+
* - yMin/yMax application (bar, line, area, combo)
|
|
9
|
+
* - multi-series legend (bar, area, combo)
|
|
6
10
|
*/
|
|
7
|
-
import type { DataPoint, ThemeColors } from '../types.js';
|
|
11
|
+
import type { ChartSpec, CurrencyConfig, DataPoint, FormatType, ThemeColors, AxisType } from '../types.js';
|
|
8
12
|
/**
|
|
9
13
|
* Build ECharts label config for persistent point labels.
|
|
10
14
|
* @param labelIndex - Index of the label value in the point array
|
|
@@ -15,7 +19,57 @@ export declare function buildPointLabelConfig(labelIndex: number, colors: ThemeC
|
|
|
15
19
|
*/
|
|
16
20
|
export declare function groupDataBySeries<T>(data: DataPoint[], seriesField: string, buildPoint: (d: DataPoint) => T): Map<string, T[]>;
|
|
17
21
|
/**
|
|
18
|
-
* Build ECharts legend config for multi-series charts.
|
|
22
|
+
* Build ECharts legend config for scatter-style multi-series charts.
|
|
23
|
+
* Uses small text and no item-width override.
|
|
19
24
|
*/
|
|
20
25
|
export declare function buildSeriesLegend(legendData: string[], colors: ThemeColors): Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Build ECharts legend config for bar/area/combo style charts.
|
|
28
|
+
* Uses narrow line-style swatches matching the rest of the chart palette.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildMultiSeriesLegend(legendData: string[], colors: ThemeColors): Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Resolved format context for a chart's value axis.
|
|
33
|
+
*
|
|
34
|
+
* Wraps the recurring (format, currency, pctMultiply, formatter) tuple
|
|
35
|
+
* so chart builders don't have to re-derive each piece individually.
|
|
36
|
+
*/
|
|
37
|
+
export interface ValueAxisFormatContext {
|
|
38
|
+
valueFormat: FormatType;
|
|
39
|
+
currency: CurrencyConfig;
|
|
40
|
+
pctMultiply: boolean;
|
|
41
|
+
axisFormatter: {
|
|
42
|
+
_js_: string;
|
|
43
|
+
} | null;
|
|
44
|
+
labelFormatter?: {
|
|
45
|
+
_js_: string;
|
|
46
|
+
} | null;
|
|
47
|
+
}
|
|
48
|
+
export interface BuildValueAxisFormatContextOptions {
|
|
49
|
+
/** Override the inferred format (e.g. combo's secondaryFormat). */
|
|
50
|
+
formatOverride?: FormatType;
|
|
51
|
+
/** Also build a label formatter (used by bar). */
|
|
52
|
+
withLabelFormatter?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve format/currency/pctMultiply and produce axis (and optionally label)
|
|
56
|
+
* formatters for a chart's value axis.
|
|
57
|
+
*
|
|
58
|
+
* Used by bar, line, area, and combo. Combo calls this twice (once per axis).
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildValueAxisFormatContext(spec: ChartSpec, data: DataPoint[], yKeys: string[], opts?: BuildValueAxisFormatContextOptions): ValueAxisFormatContext;
|
|
61
|
+
/**
|
|
62
|
+
* Build an ECharts xAxis label config based on the resolved axis type.
|
|
63
|
+
*
|
|
64
|
+
* - `category`: 45° rotation with truncation for long labels
|
|
65
|
+
* - `time`: month-day formatter ("Jan 5")
|
|
66
|
+
* - `value`: just hideOverlap
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildXAxisLabelConfig(xAxisType: AxisType): Record<string, unknown>;
|
|
69
|
+
/**
|
|
70
|
+
* Apply yMin/yMax from a spec onto a value-axis object in place.
|
|
71
|
+
* The caller passes the right axis object (xAxis for horizontal bars,
|
|
72
|
+
* yAxis or yAxis[0] for everything else).
|
|
73
|
+
*/
|
|
74
|
+
export declare function applyValueAxisRange(axisObj: Record<string, unknown>, spec: ChartSpec): void;
|
|
21
75
|
//# sourceMappingURL=chart-helpers.d.ts.map
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared helpers for
|
|
2
|
+
* Shared helpers for chart builders.
|
|
3
3
|
*
|
|
4
|
-
* Consolidates
|
|
5
|
-
*
|
|
4
|
+
* Consolidates patterns that were duplicated across multiple chart builders:
|
|
5
|
+
* - scatter-family series grouping/labels (scatter, bubble)
|
|
6
|
+
* - value-axis format context (bar, line, area, combo)
|
|
7
|
+
* - x-axis label config (line, area, combo, bar)
|
|
8
|
+
* - yMin/yMax application (bar, line, area, combo)
|
|
9
|
+
* - multi-series legend (bar, area, combo)
|
|
6
10
|
*/
|
|
7
|
-
import { FONT_SIZE_TINY } from './themes.js';
|
|
11
|
+
import { FONT_SIZE_TINY, FONT_SIZE_XXS, LEGEND_ITEM_WIDTH, LEGEND_ITEM_HEIGHT, LEGEND_ITEM_GAP, } from './themes.js';
|
|
12
|
+
import { inferFormat, resolveCurrency, shouldPctMultiply, collectNumericFieldValues, getAxisFormatterJs, getLabelFormatterJs, } from './formatting.js';
|
|
8
13
|
/**
|
|
9
14
|
* Build ECharts label config for persistent point labels.
|
|
10
15
|
* @param labelIndex - Index of the label value in the point array
|
|
@@ -33,7 +38,8 @@ export function groupDataBySeries(data, seriesField, buildPoint) {
|
|
|
33
38
|
return groups;
|
|
34
39
|
}
|
|
35
40
|
/**
|
|
36
|
-
* Build ECharts legend config for multi-series charts.
|
|
41
|
+
* Build ECharts legend config for scatter-style multi-series charts.
|
|
42
|
+
* Uses small text and no item-width override.
|
|
37
43
|
*/
|
|
38
44
|
export function buildSeriesLegend(legendData, colors) {
|
|
39
45
|
return {
|
|
@@ -43,4 +49,77 @@ export function buildSeriesLegend(legendData, colors) {
|
|
|
43
49
|
textStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_TINY },
|
|
44
50
|
};
|
|
45
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Build ECharts legend config for bar/area/combo style charts.
|
|
54
|
+
* Uses narrow line-style swatches matching the rest of the chart palette.
|
|
55
|
+
*/
|
|
56
|
+
export function buildMultiSeriesLegend(legendData, colors) {
|
|
57
|
+
return {
|
|
58
|
+
data: legendData,
|
|
59
|
+
top: 0,
|
|
60
|
+
textStyle: { color: colors.textSecondary, fontSize: FONT_SIZE_XXS },
|
|
61
|
+
itemWidth: LEGEND_ITEM_WIDTH,
|
|
62
|
+
itemHeight: LEGEND_ITEM_HEIGHT,
|
|
63
|
+
itemGap: LEGEND_ITEM_GAP,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve format/currency/pctMultiply and produce axis (and optionally label)
|
|
68
|
+
* formatters for a chart's value axis.
|
|
69
|
+
*
|
|
70
|
+
* Used by bar, line, area, and combo. Combo calls this twice (once per axis).
|
|
71
|
+
*/
|
|
72
|
+
export function buildValueAxisFormatContext(spec, data, yKeys, opts = {}) {
|
|
73
|
+
const firstYKey = yKeys[0] ?? 'value';
|
|
74
|
+
const sampleValue = data.length > 0 && data[0] ? data[0][firstYKey] ?? 0 : 0;
|
|
75
|
+
const valueFormat = opts.formatOverride ?? spec.format ?? inferFormat(firstYKey, sampleValue);
|
|
76
|
+
const currency = resolveCurrency(spec.currency);
|
|
77
|
+
const pctMultiply = shouldPctMultiply(valueFormat, collectNumericFieldValues(data, yKeys));
|
|
78
|
+
const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
|
|
79
|
+
const ctx = { valueFormat, currency, pctMultiply, axisFormatter };
|
|
80
|
+
if (opts.withLabelFormatter) {
|
|
81
|
+
ctx.labelFormatter = getLabelFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
|
|
82
|
+
}
|
|
83
|
+
return ctx;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Month-day formatter used for time-axis labels (e.g. "Jan 5").
|
|
87
|
+
* Extracted to a constant so all chart builders share the same JS source.
|
|
88
|
+
*/
|
|
89
|
+
const TIME_AXIS_MONTH_DAY_FORMATTER = {
|
|
90
|
+
_js_: `function(value) {
|
|
91
|
+
var d = new Date(value);
|
|
92
|
+
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
93
|
+
return months[d.getMonth()] + ' ' + d.getDate();
|
|
94
|
+
}`,
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Build an ECharts xAxis label config based on the resolved axis type.
|
|
98
|
+
*
|
|
99
|
+
* - `category`: 45° rotation with truncation for long labels
|
|
100
|
+
* - `time`: month-day formatter ("Jan 5")
|
|
101
|
+
* - `value`: just hideOverlap
|
|
102
|
+
*/
|
|
103
|
+
export function buildXAxisLabelConfig(xAxisType) {
|
|
104
|
+
if (xAxisType === 'category') {
|
|
105
|
+
return { interval: 0, rotate: 45, overflow: 'truncate', ellipsis: '...', width: 100 };
|
|
106
|
+
}
|
|
107
|
+
if (xAxisType === 'time') {
|
|
108
|
+
return { hideOverlap: true, formatter: TIME_AXIS_MONTH_DAY_FORMATTER };
|
|
109
|
+
}
|
|
110
|
+
return { hideOverlap: true };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Apply yMin/yMax from a spec onto a value-axis object in place.
|
|
114
|
+
* The caller passes the right axis object (xAxis for horizontal bars,
|
|
115
|
+
* yAxis or yAxis[0] for everything else).
|
|
116
|
+
*/
|
|
117
|
+
export function applyValueAxisRange(axisObj, spec) {
|
|
118
|
+
if (spec.yMin !== undefined) {
|
|
119
|
+
axisObj.min = spec.yMin;
|
|
120
|
+
}
|
|
121
|
+
if (spec.yMax !== undefined) {
|
|
122
|
+
axisObj.max = spec.yMax;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
46
125
|
//# sourceMappingURL=chart-helpers.js.map
|
|
@@ -26,6 +26,39 @@ export declare function localeFixed(value: number, decimals: number, locale: str
|
|
|
26
26
|
* values are in percentage units (via detectPctMultiply).
|
|
27
27
|
*/
|
|
28
28
|
export declare function formatNumber(value: number | null | undefined, format?: FormatType, showSign?: boolean, currency?: CurrencyConfig, pctMultiply?: boolean): string;
|
|
29
|
+
/**
|
|
30
|
+
* Convert a cell value to a numeric (or string) sort key based on the
|
|
31
|
+
* declared column format. The result is what the table emits as
|
|
32
|
+
* `data-sort` so the client-side compare always sees comparable values.
|
|
33
|
+
*
|
|
34
|
+
* - `date` → epoch ms (accepts Date, ISO string, or numeric ms)
|
|
35
|
+
* - `duration` → total seconds (accepts number-of-seconds or `1m7s` style)
|
|
36
|
+
* - currency / pct / num / auto → underlying numeric value
|
|
37
|
+
*
|
|
38
|
+
* Returns null when the value can't be coerced for the declared format —
|
|
39
|
+
* the caller falls back to its untyped heuristic chain.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatSortKey(value: unknown, format?: FormatType): number | string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Parse a duration string like `"10s"`, `"1m7s"`, `"25m39s"`, `"1h2m3s"`
|
|
44
|
+
* into total seconds. Returns null when the input doesn't match.
|
|
45
|
+
* Exported for the untyped fallback path in `cellSortAttr`.
|
|
46
|
+
*
|
|
47
|
+
* When `requireSeconds` is true, only strings that contain an `s` segment
|
|
48
|
+
* match — bare `5m` or `1h` are rejected. The untyped fallback uses this
|
|
49
|
+
* stricter form because a bare magnitude like `5m` is ambiguous (5 minutes
|
|
50
|
+
* or 5 million?); the lenient form is for columns that explicitly declare
|
|
51
|
+
* `fmt: "duration"`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseDurationString(s: string, options?: {
|
|
54
|
+
requireSeconds?: boolean;
|
|
55
|
+
}): number | null;
|
|
56
|
+
/**
|
|
57
|
+
* Parse an ISO-8601 date string (`YYYY-MM-DD` with optional time/timezone)
|
|
58
|
+
* into epoch ms. Returns null for non-ISO strings so they fall back to
|
|
59
|
+
* locale sort instead of mis-parsing through `Date.parse`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseIsoDateString(s: string): number | null;
|
|
29
62
|
/**
|
|
30
63
|
* Detect whether a series of values needs to be multiplied by 100 for percent formatting.
|
|
31
64
|
* Returns true when the series looks fractional (max |v| <= 1). Returns false when any
|
package/dist/core/formatting.js
CHANGED
|
@@ -92,6 +92,12 @@ export function formatNumber(value, format, showSign = false, currency, pctMulti
|
|
|
92
92
|
case 'num0b':
|
|
93
93
|
result = formatNum0b(value);
|
|
94
94
|
break;
|
|
95
|
+
case 'duration':
|
|
96
|
+
result = formatDuration(value);
|
|
97
|
+
break;
|
|
98
|
+
case 'date':
|
|
99
|
+
result = formatDateValue(value);
|
|
100
|
+
break;
|
|
95
101
|
default:
|
|
96
102
|
result = smartFormatNumber(value, false, cur);
|
|
97
103
|
}
|
|
@@ -198,6 +204,135 @@ function formatCurrency0b(value, cur) {
|
|
|
198
204
|
const formatted = cur.symbol + localeFixed(absValue / 1_000_000_000, 1, cur.locale) + 'b';
|
|
199
205
|
return isNegative ? `(${formatted})` : formatted;
|
|
200
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Render a number of seconds as a compact duration string: `"1m7s"`,
|
|
209
|
+
* `"25m39s"`, `"1h2m3s"`. Sub-second values render as `"0.5s"`. Negatives
|
|
210
|
+
* are wrapped in parens like the rest of the formatters.
|
|
211
|
+
*/
|
|
212
|
+
function formatDuration(seconds) {
|
|
213
|
+
if (seconds === 0)
|
|
214
|
+
return '0s';
|
|
215
|
+
const neg = seconds < 0;
|
|
216
|
+
let s = Math.abs(seconds);
|
|
217
|
+
const h = Math.floor(s / 3600);
|
|
218
|
+
s -= h * 3600;
|
|
219
|
+
const m = Math.floor(s / 60);
|
|
220
|
+
s -= m * 60;
|
|
221
|
+
let body = '';
|
|
222
|
+
if (h)
|
|
223
|
+
body += `${h}h`;
|
|
224
|
+
if (m)
|
|
225
|
+
body += `${m}m`;
|
|
226
|
+
if (s || (!h && !m)) {
|
|
227
|
+
const secStr = Number.isInteger(s) ? `${s}` : s.toFixed(2).replace(/\.?0+$/, '');
|
|
228
|
+
body += `${secStr}s`;
|
|
229
|
+
}
|
|
230
|
+
return neg ? `(${body})` : body;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Render an epoch-ms number as an ISO `YYYY-MM-DD` date string.
|
|
234
|
+
* String inputs are handled separately by the renderer (passed through),
|
|
235
|
+
* so this only fires when the value is numeric.
|
|
236
|
+
*/
|
|
237
|
+
function formatDateValue(value) {
|
|
238
|
+
const d = new Date(value);
|
|
239
|
+
if (isNaN(d.getTime()))
|
|
240
|
+
return String(value);
|
|
241
|
+
const yyyy = d.getUTCFullYear();
|
|
242
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
243
|
+
const dd = String(d.getUTCDate()).padStart(2, '0');
|
|
244
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Convert a cell value to a numeric (or string) sort key based on the
|
|
248
|
+
* declared column format. The result is what the table emits as
|
|
249
|
+
* `data-sort` so the client-side compare always sees comparable values.
|
|
250
|
+
*
|
|
251
|
+
* - `date` → epoch ms (accepts Date, ISO string, or numeric ms)
|
|
252
|
+
* - `duration` → total seconds (accepts number-of-seconds or `1m7s` style)
|
|
253
|
+
* - currency / pct / num / auto → underlying numeric value
|
|
254
|
+
*
|
|
255
|
+
* Returns null when the value can't be coerced for the declared format —
|
|
256
|
+
* the caller falls back to its untyped heuristic chain.
|
|
257
|
+
*/
|
|
258
|
+
export function formatSortKey(value, format) {
|
|
259
|
+
if (value === null || value === undefined)
|
|
260
|
+
return null;
|
|
261
|
+
if (!format)
|
|
262
|
+
return null;
|
|
263
|
+
if (format === 'date') {
|
|
264
|
+
if (value instanceof Date) {
|
|
265
|
+
const t = value.getTime();
|
|
266
|
+
return isNaN(t) ? null : t;
|
|
267
|
+
}
|
|
268
|
+
if (typeof value === 'number' && !isNaN(value))
|
|
269
|
+
return value;
|
|
270
|
+
if (typeof value === 'string') {
|
|
271
|
+
const t = Date.parse(value);
|
|
272
|
+
return isNaN(t) ? null : t;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
if (format === 'duration') {
|
|
277
|
+
if (typeof value === 'number' && !isNaN(value))
|
|
278
|
+
return value;
|
|
279
|
+
if (typeof value === 'string') {
|
|
280
|
+
return parseDurationString(value);
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
// Numeric-typed formats (currency*, pct*, num*, auto): pass numeric values
|
|
285
|
+
// through, parse simple numeric strings. Pre-formatted strings are *not*
|
|
286
|
+
// parsed — pass numeric values for typed columns.
|
|
287
|
+
if (typeof value === 'number' && !isNaN(value))
|
|
288
|
+
return value;
|
|
289
|
+
if (typeof value === 'string') {
|
|
290
|
+
const trimmed = value.trim();
|
|
291
|
+
const n = parseFloat(trimmed);
|
|
292
|
+
if (!isNaN(n) && String(n) === trimmed)
|
|
293
|
+
return n;
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Parse a duration string like `"10s"`, `"1m7s"`, `"25m39s"`, `"1h2m3s"`
|
|
299
|
+
* into total seconds. Returns null when the input doesn't match.
|
|
300
|
+
* Exported for the untyped fallback path in `cellSortAttr`.
|
|
301
|
+
*
|
|
302
|
+
* When `requireSeconds` is true, only strings that contain an `s` segment
|
|
303
|
+
* match — bare `5m` or `1h` are rejected. The untyped fallback uses this
|
|
304
|
+
* stricter form because a bare magnitude like `5m` is ambiguous (5 minutes
|
|
305
|
+
* or 5 million?); the lenient form is for columns that explicitly declare
|
|
306
|
+
* `fmt: "duration"`.
|
|
307
|
+
*/
|
|
308
|
+
export function parseDurationString(s, options = {}) {
|
|
309
|
+
const trimmed = s.trim();
|
|
310
|
+
const m = trimmed.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+(?:\.\d+)?)s)?$/);
|
|
311
|
+
if (!m)
|
|
312
|
+
return null;
|
|
313
|
+
const [, h, mm, ss] = m;
|
|
314
|
+
if (h === undefined && mm === undefined && ss === undefined)
|
|
315
|
+
return null;
|
|
316
|
+
if (options.requireSeconds && ss === undefined)
|
|
317
|
+
return null;
|
|
318
|
+
const hours = h ? parseInt(h, 10) : 0;
|
|
319
|
+
const mins = mm ? parseInt(mm, 10) : 0;
|
|
320
|
+
const secs = ss ? parseFloat(ss) : 0;
|
|
321
|
+
return hours * 3600 + mins * 60 + secs;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Parse an ISO-8601 date string (`YYYY-MM-DD` with optional time/timezone)
|
|
325
|
+
* into epoch ms. Returns null for non-ISO strings so they fall back to
|
|
326
|
+
* locale sort instead of mis-parsing through `Date.parse`.
|
|
327
|
+
*/
|
|
328
|
+
export function parseIsoDateString(s) {
|
|
329
|
+
const trimmed = s.trim();
|
|
330
|
+
const iso = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
331
|
+
if (!iso.test(trimmed))
|
|
332
|
+
return null;
|
|
333
|
+
const t = Date.parse(trimmed);
|
|
334
|
+
return isNaN(t) ? null : t;
|
|
335
|
+
}
|
|
201
336
|
/**
|
|
202
337
|
* Detect whether a series of values needs to be multiplied by 100 for percent formatting.
|
|
203
338
|
* Returns true when the series looks fractional (max |v| <= 1). Returns false when any
|
package/dist/core/linter.d.ts
CHANGED
|
@@ -16,6 +16,9 @@ export declare class SpecValidationError extends Error {
|
|
|
16
16
|
* Lint a single spec - validates against schema and performs data validation.
|
|
17
17
|
* Throws SpecValidationError on failure. Logs warnings to stderr.
|
|
18
18
|
*
|
|
19
|
+
* Columnar specs (`{columns, rows}`) are normalized to the standard
|
|
20
|
+
* `{data}` shape before validation, so callers do not need to convert first.
|
|
21
|
+
*
|
|
19
22
|
* @param mode - 'generate' (default) skips lint-only rules so HTML rendering
|
|
20
23
|
* stays quiet. 'lint' (used by `mviz --lint`) runs every rule, including
|
|
21
24
|
* advisory ones scoped to the lint command.
|
package/dist/core/linter.js
CHANGED
|
@@ -10,7 +10,8 @@ import { readFileSync } from 'node:fs';
|
|
|
10
10
|
import { join, dirname } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { executeLintRules, buildLintContext, findSchemaDir } from './lint-rules/index.js';
|
|
13
|
-
import { TYPE_TO_SCHEMA_DEF, VALID_TYPES } from './lint-rules/schema-fields.js';
|
|
13
|
+
import { TYPE_TO_SCHEMA_DEF, VALID_TYPES, VALID_FIELDS } from './lint-rules/schema-fields.js';
|
|
14
|
+
import { convertColumnarFormat } from '../layout/converter.js';
|
|
14
15
|
// Handle default exports for CommonJS/ESM interop
|
|
15
16
|
const Ajv2020 = Ajv2020Module;
|
|
16
17
|
const addFormats = addFormatsModule;
|
|
@@ -93,12 +94,37 @@ function filterErrorsNoType(errors) {
|
|
|
93
94
|
/**
|
|
94
95
|
* Filter errors to only those relevant to a specific schema definition
|
|
95
96
|
*/
|
|
96
|
-
function filterErrorsForDef(errors, defName) {
|
|
97
|
+
function filterErrorsForDef(errors, defName, specType) {
|
|
97
98
|
const relevantErrors = errors.filter((e) => e.schemaPath.includes(`/$defs/${defName}/`));
|
|
99
|
+
// Also keep enum errors from shared $defs (like FormatOption) when the
|
|
100
|
+
// failing property belongs to this spec's root — otherwise invalid enum
|
|
101
|
+
// values silently pass because the error lives under `/$defs/FormatOption/enum`
|
|
102
|
+
// rather than the type's own def.
|
|
103
|
+
const sharedEnumErrors = findSharedEnumErrors(errors, specType);
|
|
98
104
|
if (relevantErrors.length === 0) {
|
|
99
|
-
|
|
105
|
+
const additionalPropErrors = errors.filter((e) => e.keyword === 'additionalProperties' && e.schemaPath.includes(`/$defs/${defName}`));
|
|
106
|
+
return [...additionalPropErrors, ...sharedEnumErrors];
|
|
100
107
|
}
|
|
101
|
-
return relevantErrors;
|
|
108
|
+
return [...relevantErrors, ...sharedEnumErrors];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Find enum errors whose instancePath points to a top-level field of the
|
|
112
|
+
* spec. These come from shared $defs (e.g. FormatOption) and would otherwise
|
|
113
|
+
* be dropped by the schemaPath-based filter.
|
|
114
|
+
*/
|
|
115
|
+
function findSharedEnumErrors(errors, specType) {
|
|
116
|
+
const validFields = VALID_FIELDS[specType];
|
|
117
|
+
if (!validFields)
|
|
118
|
+
return [];
|
|
119
|
+
return errors.filter((e) => {
|
|
120
|
+
if (e.keyword !== 'enum')
|
|
121
|
+
return false;
|
|
122
|
+
// instancePath like "/format" — first segment is the field name.
|
|
123
|
+
const m = e.instancePath.match(/^\/([^/]+)(?:\/|$)/);
|
|
124
|
+
if (!m || !m[1])
|
|
125
|
+
return false;
|
|
126
|
+
return validFields.has(m[1]);
|
|
127
|
+
});
|
|
102
128
|
}
|
|
103
129
|
/**
|
|
104
130
|
* Filter schema errors to only show relevant errors for the detected type.
|
|
@@ -113,7 +139,7 @@ function filterSchemaErrors(errors, specType) {
|
|
|
113
139
|
if (!defName) {
|
|
114
140
|
return [createUnknownTypeError(specType)];
|
|
115
141
|
}
|
|
116
|
-
return filterErrorsForDef(errors, defName);
|
|
142
|
+
return filterErrorsForDef(errors, defName, specType);
|
|
117
143
|
}
|
|
118
144
|
/**
|
|
119
145
|
* Format a schema error into a readable message
|
|
@@ -185,20 +211,38 @@ function logWarnings(warnings) {
|
|
|
185
211
|
}
|
|
186
212
|
console.error('');
|
|
187
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Normalize a spec for validation.
|
|
216
|
+
*
|
|
217
|
+
* Currently this just folds the columnar `{columns, rows}` shape into the
|
|
218
|
+
* standard `{data}` shape. Linting always runs against the normalized spec so
|
|
219
|
+
* every entry point (CLI, markdown parser, programmatic) sees the same
|
|
220
|
+
* contract.
|
|
221
|
+
*/
|
|
222
|
+
function normalizeForLint(spec) {
|
|
223
|
+
if (spec && typeof spec === 'object' && 'type' in spec) {
|
|
224
|
+
return convertColumnarFormat(spec);
|
|
225
|
+
}
|
|
226
|
+
return spec;
|
|
227
|
+
}
|
|
188
228
|
/**
|
|
189
229
|
* Lint a single spec - validates against schema and performs data validation.
|
|
190
230
|
* Throws SpecValidationError on failure. Logs warnings to stderr.
|
|
191
231
|
*
|
|
232
|
+
* Columnar specs (`{columns, rows}`) are normalized to the standard
|
|
233
|
+
* `{data}` shape before validation, so callers do not need to convert first.
|
|
234
|
+
*
|
|
192
235
|
* @param mode - 'generate' (default) skips lint-only rules so HTML rendering
|
|
193
236
|
* stays quiet. 'lint' (used by `mviz --lint`) runs every rule, including
|
|
194
237
|
* advisory ones scoped to the lint command.
|
|
195
238
|
*/
|
|
196
239
|
export function lintSpec(spec, mode = 'generate') {
|
|
240
|
+
const normalized = normalizeForLint(spec);
|
|
197
241
|
// Schema validation
|
|
198
|
-
validateAgainstSchema(
|
|
242
|
+
validateAgainstSchema(normalized);
|
|
199
243
|
// Modular lint rules validation
|
|
200
|
-
if (
|
|
201
|
-
const context = buildLintContext(
|
|
244
|
+
if (normalized && typeof normalized === 'object' && 'type' in normalized) {
|
|
245
|
+
const context = buildLintContext(normalized);
|
|
202
246
|
const result = executeLintRules(context, mode);
|
|
203
247
|
// Log warnings to stderr
|
|
204
248
|
if (result.warnings.length > 0) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2: Load + convert + validate.
|
|
3
|
+
*
|
|
4
|
+
* Walks a raw IR (from markdown-parser) and resolves every `RawCodeBlockItem`
|
|
5
|
+
* into a `ParsedItem` by:
|
|
6
|
+
* - reading any `file=` reference (CSV or JSON)
|
|
7
|
+
* - parsing inline JSON
|
|
8
|
+
* - merging file data with inline options
|
|
9
|
+
* - applying columnar -> standard format conversion
|
|
10
|
+
* - injecting frontmatter currency / theme defaults
|
|
11
|
+
* - linting the resulting spec
|
|
12
|
+
*
|
|
13
|
+
* Errors collected during this phase are returned as a string list so the
|
|
14
|
+
* caller can report them in non-strict mode or short-circuit in strict mode.
|
|
15
|
+
*/
|
|
16
|
+
import type { ParsedFrontmatter, RawSection, Section } from './parser-types.js';
|
|
17
|
+
export interface LoadOptions {
|
|
18
|
+
baseDir: string | undefined;
|
|
19
|
+
strict: boolean;
|
|
20
|
+
lintMode: 'generate' | 'lint';
|
|
21
|
+
frontmatter: ParsedFrontmatter;
|
|
22
|
+
}
|
|
23
|
+
export interface LoadResult {
|
|
24
|
+
sections: Section[];
|
|
25
|
+
errors: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve all raw items into parsed items. Walks every section/row/item.
|
|
29
|
+
*/
|
|
30
|
+
export declare function loadSections(rawSections: RawSection[], options: LoadOptions): LoadResult;
|
|
31
|
+
//# sourceMappingURL=block-loader.d.ts.map
|