mviz 1.6.4 → 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/README.md +8 -8
- package/dist/charts/area.js +8 -36
- package/dist/charts/bar.js +8 -26
- package/dist/charts/bubble.js +9 -7
- package/dist/charts/combo.js +17 -39
- package/dist/charts/dumbbell.js +15 -14
- package/dist/charts/funnel.js +12 -7
- package/dist/charts/heatmap.js +8 -6
- package/dist/charts/line.js +6 -27
- package/dist/charts/scatter.js +7 -5
- package/dist/cli.js +4 -3
- 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 +69 -0
- package/dist/components/table-interactivity.js +216 -0
- package/dist/components/table.d.ts +6 -1
- package/dist/components/table.js +53 -12
- package/dist/core/chart-helpers.d.ts +59 -5
- package/dist/core/chart-helpers.js +84 -5
- package/dist/core/formatting.d.ts +61 -4
- package/dist/core/formatting.js +216 -17
- package/dist/core/lint-rules/registry.d.ts +4 -2
- package/dist/core/lint-rules/registry.js +6 -1
- package/dist/core/lint-rules/rules/index.d.ts +1 -0
- package/dist/core/lint-rules/rules/index.js +1 -0
- package/dist/core/lint-rules/rules/pct-scalar-gt-one.d.ts +13 -0
- package/dist/core/lint-rules/rules/pct-scalar-gt-one.js +46 -0
- package/dist/core/lint-rules/types.d.ts +12 -0
- package/dist/core/linter.d.ts +10 -2
- package/dist/core/linter.js +60 -12
- 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 +31 -22
- package/dist/layout/parser.js +118 -1006
- 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 +6 -6
- package/schema/mviz.v1.schema.json +402 -33
package/dist/core/formatting.js
CHANGED
|
@@ -40,8 +40,11 @@ export function localeFixed(value, decimals, locale) {
|
|
|
40
40
|
* Format a number according to the specified format type
|
|
41
41
|
* @param showSign - If true, prefix positive numbers with +
|
|
42
42
|
* @param currency - Optional currency config for currency formats (defaults to USD)
|
|
43
|
+
* @param pctMultiply - For pct formats: multiply the value by 100 before formatting.
|
|
44
|
+
* Default true. Set false when the caller has already determined that the series
|
|
45
|
+
* values are in percentage units (via detectPctMultiply).
|
|
43
46
|
*/
|
|
44
|
-
export function formatNumber(value, format, showSign = false, currency) {
|
|
47
|
+
export function formatNumber(value, format, showSign = false, currency, pctMultiply = true) {
|
|
45
48
|
if (value === null || value === undefined || isNaN(value)) {
|
|
46
49
|
return '';
|
|
47
50
|
}
|
|
@@ -69,10 +72,10 @@ export function formatNumber(value, format, showSign = false, currency) {
|
|
|
69
72
|
break;
|
|
70
73
|
case 'pct':
|
|
71
74
|
case 'pct1':
|
|
72
|
-
result = formatPct1(value);
|
|
75
|
+
result = formatPct1(value, pctMultiply);
|
|
73
76
|
break;
|
|
74
77
|
case 'pct0':
|
|
75
|
-
result = formatPct0(value);
|
|
78
|
+
result = formatPct0(value, pctMultiply);
|
|
76
79
|
break;
|
|
77
80
|
case 'num0':
|
|
78
81
|
result = formatNum0(value);
|
|
@@ -89,6 +92,12 @@ export function formatNumber(value, format, showSign = false, currency) {
|
|
|
89
92
|
case 'num0b':
|
|
90
93
|
result = formatNum0b(value);
|
|
91
94
|
break;
|
|
95
|
+
case 'duration':
|
|
96
|
+
result = formatDuration(value);
|
|
97
|
+
break;
|
|
98
|
+
case 'date':
|
|
99
|
+
result = formatDateValue(value);
|
|
100
|
+
break;
|
|
92
101
|
default:
|
|
93
102
|
result = smartFormatNumber(value, false, cur);
|
|
94
103
|
}
|
|
@@ -195,17 +204,199 @@ function formatCurrency0b(value, cur) {
|
|
|
195
204
|
const formatted = cur.symbol + localeFixed(absValue / 1_000_000_000, 1, cur.locale) + 'b';
|
|
196
205
|
return isNegative ? `(${formatted})` : formatted;
|
|
197
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
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Detect whether a series of values needs to be multiplied by 100 for percent formatting.
|
|
338
|
+
* Returns true when the series looks fractional (max |v| <= 1). Returns false when any
|
|
339
|
+
* value exceeds 1, indicating the series is already in percentage units (e.g., [15, 22, 38]).
|
|
340
|
+
*
|
|
341
|
+
* Empty / all-null series default to true (fractional), matching the legacy behavior.
|
|
342
|
+
*/
|
|
343
|
+
export function detectPctMultiply(values) {
|
|
344
|
+
let max = 0;
|
|
345
|
+
let sawAny = false;
|
|
346
|
+
for (const v of values) {
|
|
347
|
+
if (v === null || v === undefined || isNaN(v))
|
|
348
|
+
continue;
|
|
349
|
+
sawAny = true;
|
|
350
|
+
const abs = Math.abs(v);
|
|
351
|
+
if (abs > max)
|
|
352
|
+
max = abs;
|
|
353
|
+
}
|
|
354
|
+
if (!sawAny)
|
|
355
|
+
return true;
|
|
356
|
+
return max <= 1;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Series-aware pctMultiply for a chart/table column.
|
|
360
|
+
*
|
|
361
|
+
* For non-pct formats, always returns true (no effect — the flag is unused for those).
|
|
362
|
+
* For pct formats, returns false when the series contains a value > 1, so callers can
|
|
363
|
+
* format already-percent data like [15, 22, 38] as 15%, 22%, 38% instead of 1500% etc.
|
|
364
|
+
*/
|
|
365
|
+
export function shouldPctMultiply(format, values) {
|
|
366
|
+
if (format !== 'pct' && format !== 'pct0' && format !== 'pct1')
|
|
367
|
+
return true;
|
|
368
|
+
return detectPctMultiply(values);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Pull numeric values out of a data array for one or more field names.
|
|
372
|
+
* Used by charts to sample a y-series for pct auto-detection.
|
|
373
|
+
*/
|
|
374
|
+
export function collectNumericFieldValues(data, fields) {
|
|
375
|
+
const out = [];
|
|
376
|
+
if (!data)
|
|
377
|
+
return out;
|
|
378
|
+
for (const row of data) {
|
|
379
|
+
for (const f of fields) {
|
|
380
|
+
const v = row[f];
|
|
381
|
+
if (typeof v === 'number' && !isNaN(v))
|
|
382
|
+
out.push(v);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
198
387
|
/**
|
|
199
388
|
* Format as percentage with 1 decimal: 15.0%
|
|
200
389
|
*/
|
|
201
|
-
function formatPct1(value) {
|
|
202
|
-
|
|
390
|
+
function formatPct1(value, pctMultiply = true) {
|
|
391
|
+
const scaled = pctMultiply ? value * 100 : value;
|
|
392
|
+
return scaled.toFixed(1) + '%';
|
|
203
393
|
}
|
|
204
394
|
/**
|
|
205
395
|
* Format as percentage with 0 decimals: 15%
|
|
206
396
|
*/
|
|
207
|
-
function formatPct0(value) {
|
|
208
|
-
|
|
397
|
+
function formatPct0(value, pctMultiply = true) {
|
|
398
|
+
const scaled = pctMultiply ? value * 100 : value;
|
|
399
|
+
return Math.round(scaled) + '%';
|
|
209
400
|
}
|
|
210
401
|
/**
|
|
211
402
|
* Format as number with 0 decimals: 1,250
|
|
@@ -394,8 +585,9 @@ export function parseNumericString(value) {
|
|
|
394
585
|
* @param currencySymbol - Symbol to use for currency formats (defaults to '$')
|
|
395
586
|
* @param locale - Locale for number formatting (defaults to 'en-US')
|
|
396
587
|
*/
|
|
397
|
-
export function formatValue(fmt, currencySymbol = '$', locale = 'en-US') {
|
|
588
|
+
export function formatValue(fmt, currencySymbol = '$', locale = 'en-US', pctMultiply = true) {
|
|
398
589
|
const locStr = JSON.stringify(locale);
|
|
590
|
+
const pctExpr = pctMultiply ? '(value * 100)' : 'value';
|
|
399
591
|
switch (fmt) {
|
|
400
592
|
case 'currency':
|
|
401
593
|
return `'${currencySymbol}' + value.toLocaleString(${locStr})`;
|
|
@@ -407,9 +599,9 @@ export function formatValue(fmt, currencySymbol = '$', locale = 'en-US') {
|
|
|
407
599
|
return `'${currencySymbol}' + (value/1000000000).toLocaleString(${locStr}, {minimumFractionDigits:1, maximumFractionDigits:1}) + 'b'`;
|
|
408
600
|
case 'pct':
|
|
409
601
|
case 'pct1':
|
|
410
|
-
return
|
|
602
|
+
return `${pctExpr}.toFixed(1) + '%'`;
|
|
411
603
|
case 'pct0':
|
|
412
|
-
return
|
|
604
|
+
return `${pctExpr}.toFixed(0) + '%'`;
|
|
413
605
|
case 'num0':
|
|
414
606
|
return "value.toLocaleString()";
|
|
415
607
|
case 'num1':
|
|
@@ -430,7 +622,7 @@ export function formatValue(fmt, currencySymbol = '$', locale = 'en-US') {
|
|
|
430
622
|
* @param currencySymbol - Symbol to use for currency formats (defaults to '$')
|
|
431
623
|
* @param locale - Locale for number formatting (defaults to 'en-US')
|
|
432
624
|
*/
|
|
433
|
-
export function getLabelFormatterJs(fmt, currencySymbol = '$', locale = 'en-US') {
|
|
625
|
+
export function getLabelFormatterJs(fmt, currencySymbol = '$', locale = 'en-US', pctMultiply = true) {
|
|
434
626
|
if (fmt === null || fmt === undefined) {
|
|
435
627
|
return null;
|
|
436
628
|
}
|
|
@@ -473,7 +665,7 @@ export function getLabelFormatterJs(fmt, currencySymbol = '$', locale = 'en-US')
|
|
|
473
665
|
};
|
|
474
666
|
}
|
|
475
667
|
// Get the formatting expression for specific formats
|
|
476
|
-
const expr = formatValue(fmt, currencySymbol, locale);
|
|
668
|
+
const expr = formatValue(fmt, currencySymbol, locale, pctMultiply);
|
|
477
669
|
if (expr === 'value') {
|
|
478
670
|
return null;
|
|
479
671
|
}
|
|
@@ -488,7 +680,7 @@ export function getLabelFormatterJs(fmt, currencySymbol = '$', locale = 'en-US')
|
|
|
488
680
|
* @param currencySymbol - Symbol to use for currency formats (defaults to '$')
|
|
489
681
|
* @param locale - Locale for number formatting (defaults to 'en-US')
|
|
490
682
|
*/
|
|
491
|
-
export function getAxisFormatterJs(fmt, currencySymbol = '$', locale = 'en-US') {
|
|
683
|
+
export function getAxisFormatterJs(fmt, currencySymbol = '$', locale = 'en-US', pctMultiply = true) {
|
|
492
684
|
if (fmt === null || fmt === undefined) {
|
|
493
685
|
return null;
|
|
494
686
|
}
|
|
@@ -526,19 +718,26 @@ export function getAxisFormatterJs(fmt, currencySymbol = '$', locale = 'en-US')
|
|
|
526
718
|
}`,
|
|
527
719
|
};
|
|
528
720
|
}
|
|
529
|
-
// For percentage, multiply by 100
|
|
721
|
+
// For percentage, multiply by 100 unless the caller signals the series is already in
|
|
722
|
+
// percent units (pctMultiply === false).
|
|
530
723
|
if (fmt === 'pct' || fmt === 'pct1') {
|
|
724
|
+
const body = pctMultiply
|
|
725
|
+
? `return (value * 100).toFixed(1) + '%';`
|
|
726
|
+
: `return value.toFixed(1) + '%';`;
|
|
531
727
|
return {
|
|
532
|
-
_js_: `function(value) {
|
|
728
|
+
_js_: `function(value) { ${body} }`,
|
|
533
729
|
};
|
|
534
730
|
}
|
|
535
731
|
if (fmt === 'pct0') {
|
|
732
|
+
const body = pctMultiply
|
|
733
|
+
? `return (value * 100).toFixed(0) + '%';`
|
|
734
|
+
: `return value.toFixed(0) + '%';`;
|
|
536
735
|
return {
|
|
537
|
-
_js_: `function(value) {
|
|
736
|
+
_js_: `function(value) { ${body} }`,
|
|
538
737
|
};
|
|
539
738
|
}
|
|
540
739
|
// Get the formatting expression for specific formats
|
|
541
|
-
const expr = formatValue(fmt, currencySymbol, locale);
|
|
740
|
+
const expr = formatValue(fmt, currencySymbol, locale, pctMultiply);
|
|
542
741
|
if (expr === 'value') {
|
|
543
742
|
return null;
|
|
544
743
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Rules register themselves at module load time. The linter
|
|
5
5
|
* calls executeLintRules() to run all applicable rules.
|
|
6
6
|
*/
|
|
7
|
-
import type { LintContext, LintExecutionResult, LintRuleDefinition } from './types.js';
|
|
7
|
+
import type { LintContext, LintExecutionMode, LintExecutionResult, LintRuleDefinition } from './types.js';
|
|
8
8
|
/**
|
|
9
9
|
* Register a lint rule in the global registry.
|
|
10
10
|
* Called at module load time by rule files.
|
|
@@ -14,9 +14,11 @@ export declare function registerLintRule(definition: LintRuleDefinition): void;
|
|
|
14
14
|
* Execute all applicable lint rules for a given context.
|
|
15
15
|
*
|
|
16
16
|
* @param context - The lint context with spec, type, data, etc.
|
|
17
|
+
* @param mode - 'generate' (default) skips rules flagged `lintOnly: true`;
|
|
18
|
+
* 'lint' runs every applicable rule.
|
|
17
19
|
* @returns Execution result with errors, warnings, and validity
|
|
18
20
|
*/
|
|
19
|
-
export declare function executeLintRules(context: LintContext): LintExecutionResult;
|
|
21
|
+
export declare function executeLintRules(context: LintContext, mode?: LintExecutionMode): LintExecutionResult;
|
|
20
22
|
/**
|
|
21
23
|
* Get all registered rules (for testing/debugging)
|
|
22
24
|
*/
|
|
@@ -31,15 +31,20 @@ function ruleApplies(rule, type) {
|
|
|
31
31
|
* Execute all applicable lint rules for a given context.
|
|
32
32
|
*
|
|
33
33
|
* @param context - The lint context with spec, type, data, etc.
|
|
34
|
+
* @param mode - 'generate' (default) skips rules flagged `lintOnly: true`;
|
|
35
|
+
* 'lint' runs every applicable rule.
|
|
34
36
|
* @returns Execution result with errors, warnings, and validity
|
|
35
37
|
*/
|
|
36
|
-
export function executeLintRules(context) {
|
|
38
|
+
export function executeLintRules(context, mode = 'lint') {
|
|
37
39
|
const errors = [];
|
|
38
40
|
const warnings = [];
|
|
39
41
|
for (const ruleDef of ruleRegistry) {
|
|
40
42
|
if (!ruleApplies(ruleDef, context.type)) {
|
|
41
43
|
continue;
|
|
42
44
|
}
|
|
45
|
+
if (ruleDef.lintOnly && mode !== 'lint') {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
43
48
|
try {
|
|
44
49
|
const results = ruleDef.rule(context);
|
|
45
50
|
for (const result of results) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lint rule: pct-scalar-gt-one
|
|
3
|
+
*
|
|
4
|
+
* Warns when a scalar component (big_value / delta) uses a pct* format on a value
|
|
5
|
+
* whose absolute value is > 1. These formats always multiply by 100, so `value: 15.5`
|
|
6
|
+
* with `format: "pct"` renders as 1550.0%, which is almost never what was intended.
|
|
7
|
+
* The likely fix is to pass `value / 100` or to pass an already-fractional input.
|
|
8
|
+
*
|
|
9
|
+
* Scoped to scalars only. Charts and tables are series-aware and handle the
|
|
10
|
+
* already-percent case automatically (see shouldPctMultiply).
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=pct-scalar-gt-one.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lint rule: pct-scalar-gt-one
|
|
3
|
+
*
|
|
4
|
+
* Warns when a scalar component (big_value / delta) uses a pct* format on a value
|
|
5
|
+
* whose absolute value is > 1. These formats always multiply by 100, so `value: 15.5`
|
|
6
|
+
* with `format: "pct"` renders as 1550.0%, which is almost never what was intended.
|
|
7
|
+
* The likely fix is to pass `value / 100` or to pass an already-fractional input.
|
|
8
|
+
*
|
|
9
|
+
* Scoped to scalars only. Charts and tables are series-aware and handle the
|
|
10
|
+
* already-percent case automatically (see shouldPctMultiply).
|
|
11
|
+
*/
|
|
12
|
+
import { registerLintRule } from '../registry.js';
|
|
13
|
+
const PCT_FORMATS = new Set(['pct', 'pct0', 'pct1']);
|
|
14
|
+
registerLintRule({
|
|
15
|
+
id: 'pct-scalar-gt-one',
|
|
16
|
+
description: 'Warns when big_value/delta uses pct format with |value| > 1 (will render as value × 100)',
|
|
17
|
+
appliesTo: ['big_value', 'delta'],
|
|
18
|
+
defaultSeverity: 'warning',
|
|
19
|
+
// Advisory rule: only fire when the CLI is explicitly linting. Normal HTML
|
|
20
|
+
// rendering stays quiet so the dashboard output isn't noisy for end users.
|
|
21
|
+
lintOnly: true,
|
|
22
|
+
rule: checkPctScalar,
|
|
23
|
+
});
|
|
24
|
+
function checkPctScalar(context) {
|
|
25
|
+
const { spec } = context;
|
|
26
|
+
const format = spec.format;
|
|
27
|
+
if (typeof format !== 'string' || !PCT_FORMATS.has(format))
|
|
28
|
+
return [];
|
|
29
|
+
const value = spec.value;
|
|
30
|
+
if (typeof value !== 'number' || isNaN(value))
|
|
31
|
+
return [];
|
|
32
|
+
if (Math.abs(value) <= 1)
|
|
33
|
+
return [];
|
|
34
|
+
const scaled = (value * 100).toFixed(1).replace(/\.0$/, '');
|
|
35
|
+
const fractional = value / 100;
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
ruleId: 'pct-scalar-gt-one',
|
|
39
|
+
message: `"value": ${value} with "format": "${format}" will render as ${scaled}% (value × 100).`,
|
|
40
|
+
severity: 'warning',
|
|
41
|
+
path: '/value',
|
|
42
|
+
suggestion: `If ${value} is already a percentage, change it to ${fractional} (or pass the raw fraction). The pct* formats always multiply scalar values by 100.`,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=pct-scalar-gt-one.js.map
|
|
@@ -56,9 +56,21 @@ export interface LintRuleDefinition {
|
|
|
56
56
|
appliesTo: string[];
|
|
57
57
|
/** Default severity when rule triggers */
|
|
58
58
|
defaultSeverity: LintSeverity;
|
|
59
|
+
/**
|
|
60
|
+
* If true, this rule only fires when the CLI is explicitly linting
|
|
61
|
+
* (`mviz --lint …`). Normal HTML generation silently skips it.
|
|
62
|
+
* Use for checks that are advisory for authors but should not noise up
|
|
63
|
+
* the output when the user is just rendering a dashboard.
|
|
64
|
+
*/
|
|
65
|
+
lintOnly?: boolean;
|
|
59
66
|
/** The rule function */
|
|
60
67
|
rule: LintRule;
|
|
61
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Execution mode passed to the registry. `lint` runs every registered rule.
|
|
71
|
+
* `generate` skips rules flagged `lintOnly: true`.
|
|
72
|
+
*/
|
|
73
|
+
export type LintExecutionMode = 'lint' | 'generate';
|
|
62
74
|
/**
|
|
63
75
|
* Result of running all applicable lint rules
|
|
64
76
|
*/
|
package/dist/core/linter.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Validates specs against JSON Schema and performs data validation
|
|
5
5
|
* using modular lint rules. Throws errors on validation failure.
|
|
6
6
|
*/
|
|
7
|
+
import type { LintExecutionMode } from './lint-rules/types.js';
|
|
7
8
|
/**
|
|
8
9
|
* Error thrown when spec validation fails
|
|
9
10
|
*/
|
|
@@ -14,11 +15,18 @@ export declare class SpecValidationError extends Error {
|
|
|
14
15
|
/**
|
|
15
16
|
* Lint a single spec - validates against schema and performs data validation.
|
|
16
17
|
* Throws SpecValidationError on failure. Logs warnings to stderr.
|
|
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
|
+
*
|
|
22
|
+
* @param mode - 'generate' (default) skips lint-only rules so HTML rendering
|
|
23
|
+
* stays quiet. 'lint' (used by `mviz --lint`) runs every rule, including
|
|
24
|
+
* advisory ones scoped to the lint command.
|
|
17
25
|
*/
|
|
18
|
-
export declare function lintSpec(spec: unknown): void;
|
|
26
|
+
export declare function lintSpec(spec: unknown, mode?: LintExecutionMode): void;
|
|
19
27
|
/**
|
|
20
28
|
* Lint multiple specs (e.g., from a dashboard)
|
|
21
29
|
* Throws SpecValidationError on first failure.
|
|
22
30
|
*/
|
|
23
|
-
export declare function lintSpecs(specs: unknown[]): void;
|
|
31
|
+
export declare function lintSpecs(specs: unknown[], mode?: LintExecutionMode): void;
|
|
24
32
|
//# sourceMappingURL=linter.d.ts.map
|
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,17 +211,39 @@ 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.
|
|
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
|
+
*
|
|
235
|
+
* @param mode - 'generate' (default) skips lint-only rules so HTML rendering
|
|
236
|
+
* stays quiet. 'lint' (used by `mviz --lint`) runs every rule, including
|
|
237
|
+
* advisory ones scoped to the lint command.
|
|
191
238
|
*/
|
|
192
|
-
export function lintSpec(spec) {
|
|
239
|
+
export function lintSpec(spec, mode = 'generate') {
|
|
240
|
+
const normalized = normalizeForLint(spec);
|
|
193
241
|
// Schema validation
|
|
194
|
-
validateAgainstSchema(
|
|
242
|
+
validateAgainstSchema(normalized);
|
|
195
243
|
// Modular lint rules validation
|
|
196
|
-
if (
|
|
197
|
-
const context = buildLintContext(
|
|
198
|
-
const result = executeLintRules(context);
|
|
244
|
+
if (normalized && typeof normalized === 'object' && 'type' in normalized) {
|
|
245
|
+
const context = buildLintContext(normalized);
|
|
246
|
+
const result = executeLintRules(context, mode);
|
|
199
247
|
// Log warnings to stderr
|
|
200
248
|
if (result.warnings.length > 0) {
|
|
201
249
|
logWarnings(result.warnings);
|
|
@@ -217,9 +265,9 @@ export function lintSpec(spec) {
|
|
|
217
265
|
* Lint multiple specs (e.g., from a dashboard)
|
|
218
266
|
* Throws SpecValidationError on first failure.
|
|
219
267
|
*/
|
|
220
|
-
export function lintSpecs(specs) {
|
|
268
|
+
export function lintSpecs(specs, mode = 'generate') {
|
|
221
269
|
for (const spec of specs) {
|
|
222
|
-
lintSpec(spec);
|
|
270
|
+
lintSpec(spec, mode);
|
|
223
271
|
}
|
|
224
272
|
}
|
|
225
273
|
//# sourceMappingURL=linter.js.map
|
|
@@ -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
|