mviz 1.6.3 → 1.6.6

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 CHANGED
@@ -156,7 +156,7 @@ title: My Report
156
156
  ## Section Name
157
157
 
158
158
  ```big_value size=[4,2]
159
- {"value": 125000, "label": "Revenue", "format": "usd0m"}
159
+ {"value": 125000, "label": "Revenue", "format": "currency0m"}
160
160
  ```
161
161
  ```delta size=[4,2]
162
162
  {"value": 0.15, "label": "vs Last Month", "format": "pct0"}
@@ -200,7 +200,7 @@ Tables support inline sparkline columns for trend visualization:
200
200
  "type": "table",
201
201
  "columns": [
202
202
  {"id": "product", "title": "Product"},
203
- {"id": "sales", "title": "Sales", "fmt": "usd"},
203
+ {"id": "sales", "title": "Sales", "fmt": "currency"},
204
204
  {"id": "trend", "title": "Trend", "type": "sparkline", "sparkType": "line"},
205
205
  {"id": "progress", "title": "Goal", "type": "sparkline", "sparkType": "pct_bar", "width": 100}
206
206
  ],
@@ -217,10 +217,10 @@ Sparkline types: `line`, `bar`, `area`, `pct_bar` (progress bar), `dumbbell` (be
217
217
  | Format | Output | Description |
218
218
  |--------|--------|-------------|
219
219
  | `auto` | 1.000m, 10.00k | **Smart auto-format (default)** |
220
- | `usd_auto` | $1.000m, $10.00k | Smart auto-format with $ |
221
- | `usd0m` | $1.2m | Millions |
222
- | `usd0k` | $125k | Compact thousands |
223
- | `usd` | $1,250,000 | Full dollars |
220
+ | `currency_auto` | $1.000m, $10.00k | Smart auto-format with currency symbol |
221
+ | `currency0m` | $1.2m | Millions |
222
+ | `currency0k` | $125k | Compact thousands |
223
+ | `currency` | $1,250,000 | Full currency |
224
224
  | `pct0` | 15% | Percentage integer |
225
225
  | `pct` | 15.0% | Percentage with decimal |
226
226
  | `pct1` | 15.0% | Percentage with 1 decimal |
@@ -234,7 +234,7 @@ Chart axes automatically detect the appropriate format based on field names:
234
234
 
235
235
  | Field Pattern | Auto Format | Example |
236
236
  |---------------|-------------|---------|
237
- | revenue, sales, price, cost, profit | `usd_auto` | $1.250m |
237
+ | revenue, sales, price, cost, profit | `currency_auto` | $1.250m |
238
238
  | pct, percent, rate, ratio | `pct` or `pct0` | 15.0% |
239
239
  | All other fields | `auto` | 1.250m |
240
240
 
@@ -375,7 +375,7 @@ The skill is automatically available when working in this project directory.
375
375
 
376
376
  ## Dependencies
377
377
 
378
- - Node.js 20+
378
+ - Node.js 24.12+
379
379
 
380
380
  ## Design Philosophy
381
381
 
@@ -4,7 +4,7 @@
4
4
  import { FONT_SIZE_XXS, LEGEND_ITEM_WIDTH, LEGEND_ITEM_HEIGHT, LEGEND_ITEM_GAP, DEFAULT_CHART_HEIGHT, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
5
5
  import { wrapHtml } from '../core/serializer.js';
6
6
  import { registerChart, registerOptions } from './registry.js';
7
- import { inferFormat, getAxisFormatterJs, inferAxisType, resolveCurrency } from '../core/formatting.js';
7
+ import { inferFormat, getAxisFormatterJs, inferAxisType, resolveCurrency, shouldPctMultiply, collectNumericFieldValues, } from '../core/formatting.js';
8
8
  /**
9
9
  * Build ECharts options for an area chart
10
10
  */
@@ -24,7 +24,8 @@ export function buildAreaOptions(spec) {
24
24
  const sampleValue = data.length > 0 && data[0] ? data[0][firstYKey] ?? 0 : 0;
25
25
  const valueFormat = spec.format ?? inferFormat(firstYKey, sampleValue);
26
26
  const currency = resolveCurrency(spec.currency);
27
- const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale);
27
+ const pctMultiply = shouldPctMultiply(valueFormat, collectNumericFieldValues(data, yKeys));
28
+ const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
28
29
  // Detect x-axis type: time, value, or category
29
30
  const xAxisType = spec.xAxisType ?? inferAxisType(categories);
30
31
  // Get axis label config based on axis type
@@ -4,7 +4,7 @@
4
4
  import { BAR_MAX_WIDTH, LEGEND_ITEM_WIDTH, LEGEND_ITEM_HEIGHT, LEGEND_ITEM_GAP, FONT_SIZE_TINY, FONT_SIZE_XXS, DEFAULT_CHART_HEIGHT, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
5
5
  import { wrapHtml } from '../core/serializer.js';
6
6
  import { registerChart, registerOptions } from './registry.js';
7
- import { inferFormat, getLabelFormatterJs, getAxisFormatterJs, inferAxisType, resolveCurrency } from '../core/formatting.js';
7
+ import { inferFormat, getLabelFormatterJs, getAxisFormatterJs, inferAxisType, resolveCurrency, shouldPctMultiply, collectNumericFieldValues, } from '../core/formatting.js';
8
8
  /**
9
9
  * Build ECharts options for a bar chart
10
10
  */
@@ -24,8 +24,9 @@ export function buildBarOptions(spec) {
24
24
  const sampleValue = data.length > 0 && data[0] ? data[0][firstYKey] ?? 0 : 0;
25
25
  const valueFormat = spec.format ?? inferFormat(firstYKey, sampleValue);
26
26
  const currency = resolveCurrency(spec.currency);
27
- const labelFormatter = getLabelFormatterJs(valueFormat, currency.symbol, currency.locale);
28
- const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale);
27
+ const pctMultiply = shouldPctMultiply(valueFormat, collectNumericFieldValues(data, yKeys));
28
+ const labelFormatter = getLabelFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
29
+ const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
29
30
  // Detect category axis type (this is x-axis for vertical, y-axis for horizontal)
30
31
  const categoryAxisType = spec.xAxisType ?? inferAxisType(categories);
31
32
  // For horizontal bars, x-axis is always 'value'; for vertical it's the category axis type
@@ -4,7 +4,7 @@
4
4
  import { FONT_SIZE_TINY, DEFAULT_CHART_HEIGHT, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
5
5
  import { wrapHtml } from '../core/serializer.js';
6
6
  import { registerChart, registerOptions } from './registry.js';
7
- import { inferFormat, getAxisFormatterJs, formatValue, resolveCurrency } from '../core/formatting.js';
7
+ import { inferFormat, getAxisFormatterJs, formatValue, resolveCurrency, shouldPctMultiply, collectNumericFieldValues, } from '../core/formatting.js';
8
8
  import { buildPointLabelConfig, groupDataBySeries, buildSeriesLegend } from '../core/chart-helpers.js';
9
9
  /** Min/max bubble pixel sizes */
10
10
  const BUBBLE_SIZE_MIN = 10;
@@ -91,10 +91,10 @@ function buildBubbleSeries(data, ctx, seriesField, showLabels, palette, colors)
91
91
  /**
92
92
  * Build tooltip config for bubble chart.
93
93
  */
94
- function buildBubbleTooltip(ctx, seriesField, xFormat, yFormat, currency, colors) {
94
+ function buildBubbleTooltip(ctx, seriesField, xFormat, yFormat, currency, colors, xPctMultiply = true, yPctMultiply = true) {
95
95
  const labelIdx = ctx.labelField ? BUBBLE_LABEL_INDEX : -1;
96
- const xFormatExpr = formatValue(xFormat, currency.symbol, currency.locale);
97
- const yFormatExpr = formatValue(yFormat, currency.symbol, currency.locale);
96
+ const xFormatExpr = formatValue(xFormat, currency.symbol, currency.locale, xPctMultiply);
97
+ const yFormatExpr = formatValue(yFormat, currency.symbol, currency.locale, yPctMultiply);
98
98
  const seriesNameLine = seriesField
99
99
  ? "if (params.seriesName) result += '<strong>' + params.seriesName + '</strong><br/>';"
100
100
  : '';
@@ -157,6 +157,8 @@ export function buildBubbleOptions(spec) {
157
157
  const xFormat = spec.xFormat ?? inferFormat(xField, xSample);
158
158
  const yFormat = spec.yFormat ?? inferFormat(yField, ySample);
159
159
  const currency = resolveCurrency(spec.currency);
160
+ const xPctMultiply = shouldPctMultiply(xFormat, collectNumericFieldValues(data, [xField]));
161
+ const yPctMultiply = shouldPctMultiply(yFormat, collectNumericFieldValues(data, [yField]));
160
162
  // Build context for point construction
161
163
  const xAxis = detectAxisCategories(data, xField);
162
164
  const yAxis = detectAxisCategories(data, yField);
@@ -174,14 +176,14 @@ export function buildBubbleOptions(spec) {
174
176
  backgroundColor: 'transparent',
175
177
  animation: false,
176
178
  color: palette,
177
- tooltip: buildBubbleTooltip(ctx, seriesField, xFormat, yFormat, currency, colors),
179
+ tooltip: buildBubbleTooltip(ctx, seriesField, xFormat, yFormat, currency, colors, xPctMultiply, yPctMultiply),
178
180
  grid: {
179
181
  left: '6%', right: '8%',
180
182
  top: hasLegend ? '14%' : '12%',
181
183
  bottom: '14%', containLabel: true,
182
184
  },
183
- xAxis: buildBubbleAxis(xField, xAxis.isCategory, xAxis.categories, getAxisFormatterJs(xFormat, currency.symbol, currency.locale), 24, colors),
184
- yAxis: buildBubbleAxis(yField, yAxis.isCategory, yAxis.categories, getAxisFormatterJs(yFormat, currency.symbol, currency.locale), 32, colors),
185
+ xAxis: buildBubbleAxis(xField, xAxis.isCategory, xAxis.categories, getAxisFormatterJs(xFormat, currency.symbol, currency.locale, xPctMultiply), 24, colors),
186
+ yAxis: buildBubbleAxis(yField, yAxis.isCategory, yAxis.categories, getAxisFormatterJs(yFormat, currency.symbol, currency.locale, yPctMultiply), 32, colors),
185
187
  series: seriesList,
186
188
  };
187
189
  if (hasLegend) {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { FONT_SIZE_XXS, BAR_MAX_WIDTH, LEGEND_ITEM_WIDTH, LEGEND_ITEM_HEIGHT, LEGEND_ITEM_GAP, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
5
5
  import { wrapHtml } from '../core/serializer.js';
6
- import { inferAxisType, inferFormat, getAxisFormatterJs, resolveCurrency } from '../core/formatting.js';
6
+ import { inferAxisType, inferFormat, getAxisFormatterJs, resolveCurrency, shouldPctMultiply, collectNumericFieldValues, } from '../core/formatting.js';
7
7
  import { registerChart, registerOptions } from './registry.js';
8
8
  /**
9
9
  * Build ECharts options for a combo chart (bar + line with optional dual axis)
@@ -24,11 +24,13 @@ export function buildComboOptions(spec) {
24
24
  const barSample = data.length > 0 && data[0] && barKeys[0] ? data[0][barKeys[0]] ?? 0 : 0;
25
25
  const primaryFormat = spec.format ?? inferFormat(barKeys[0] ?? 'value', barSample);
26
26
  const currency = resolveCurrency(spec.currency);
27
- const primaryAxisFormatter = getAxisFormatterJs(primaryFormat, currency.symbol, currency.locale);
27
+ const primaryPctMultiply = shouldPctMultiply(primaryFormat, collectNumericFieldValues(data, barKeys));
28
+ const primaryAxisFormatter = getAxisFormatterJs(primaryFormat, currency.symbol, currency.locale, primaryPctMultiply);
28
29
  // Infer format for secondary y-axis (line if dual axis)
29
30
  const lineSample = data.length > 0 && data[0] && lineKeys[0] ? data[0][lineKeys[0]] ?? 0 : 0;
30
31
  const secondaryFormat = spec.secondaryFormat ?? inferFormat(lineKeys[0] ?? 'value', lineSample);
31
- const secondaryAxisFormatter = getAxisFormatterJs(secondaryFormat, currency.symbol, currency.locale);
32
+ const secondaryPctMultiply = shouldPctMultiply(secondaryFormat, collectNumericFieldValues(data, lineKeys));
33
+ const secondaryAxisFormatter = getAxisFormatterJs(secondaryFormat, currency.symbol, currency.locale, secondaryPctMultiply);
32
34
  // Detect x-axis type
33
35
  const xAxisType = inferAxisType(categories);
34
36
  // Get axis label config based on axis type
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { COLORS, FONT_SIZE_TINY, DUMBBELL_SYMBOL_SIZE, DEFAULT_CHART_HEIGHT, getThemeColors, } from '../core/themes.js';
8
8
  import { wrapHtml } from '../core/serializer.js';
9
- import { inferFormat, getAxisFormatterJs, formatValue, formatNumber, resolveCurrency, localeFixed } from '../core/formatting.js';
9
+ import { inferFormat, getAxisFormatterJs, formatValue, formatNumber, resolveCurrency, localeFixed, shouldPctMultiply, } from '../core/formatting.js';
10
10
  import { registerChart, registerOptions } from './registry.js';
11
11
  // Directional colors
12
12
  const POSITIVE_COLOR = COLORS.POSITIVE_GREEN;
@@ -50,7 +50,7 @@ const COMPACT_LABEL_CHAR_THRESHOLD = 5;
50
50
  * Compute the compact dumbbell label string for a given value.
51
51
  * Mirrors the JS formatter logic so we can measure character count at build time.
52
52
  */
53
- function compactFormatLabel(value, fmt, currencySymbol = '$', locale = 'en-US') {
53
+ function compactFormatLabel(value, fmt, currencySymbol = '$', locale = 'en-US', pctMultiply = true) {
54
54
  if (fmt === 'auto' || fmt === 'currency_auto') {
55
55
  const abs = Math.abs(value);
56
56
  const p = fmt === 'currency_auto' ? currencySymbol : '';
@@ -76,13 +76,13 @@ function compactFormatLabel(value, fmt, currencySymbol = '$', locale = 'en-US')
76
76
  }
77
77
  return value < 0 ? '(' + r + ')' : r;
78
78
  }
79
- return formatNumber(value, fmt);
79
+ return formatNumber(value, fmt, false, undefined, pctMultiply);
80
80
  }
81
81
  /**
82
82
  * Get the appropriate font size for a dumbbell dot label.
83
83
  */
84
- function dumbbellLabelFontSize(value, fmt, currencySymbol = '$', locale = 'en-US') {
85
- const label = compactFormatLabel(value, fmt, currencySymbol, locale);
84
+ function dumbbellLabelFontSize(value, fmt, currencySymbol = '$', locale = 'en-US', pctMultiply = true) {
85
+ const label = compactFormatLabel(value, fmt, currencySymbol, locale, pctMultiply);
86
86
  return label.length >= COMPACT_LABEL_CHAR_THRESHOLD
87
87
  ? DUMBBELL_LABEL_FONT_SMALL
88
88
  : DUMBBELL_LABEL_FONT;
@@ -92,7 +92,7 @@ function dumbbellLabelFontSize(value, fmt, currencySymbol = '$', locale = 'en-US
92
92
  * Dumbbell stores values as [value, categoryIndex], so we extract value from params.value[0].
93
93
  * Uses compact formatting (~4-5 chars max) to fit inside small dot symbols.
94
94
  */
95
- function buildDumbbellLabelFormatter(fmt, currencySymbol = '$', locale = 'en-US') {
95
+ function buildDumbbellLabelFormatter(fmt, currencySymbol = '$', locale = 'en-US', pctMultiply = true) {
96
96
  if (fmt === 'auto' || fmt === 'currency_auto') {
97
97
  const isCurrency = fmt === 'currency_auto' ? 'true' : 'false';
98
98
  const symbolStr = JSON.stringify(currencySymbol);
@@ -124,14 +124,14 @@ function buildDumbbellLabelFormatter(fmt, currencySymbol = '$', locale = 'en-US'
124
124
  return neg ? '(' + r + ')' : r;
125
125
  }`;
126
126
  }
127
- const expr = formatValue(fmt, currencySymbol, locale);
127
+ const expr = formatValue(fmt, currencySymbol, locale, pctMultiply);
128
128
  return `function(params) { var value = params.value[0]; if (value == null) return ''; return ${expr}; }`;
129
129
  }
130
130
  /**
131
131
  * Build a tooltip value formatting expression for dumbbell charts.
132
132
  * Tooltip can be more verbose than dot labels, so uses full smart formatting.
133
133
  */
134
- function buildDumbbellTooltipExpr(fmt, currencySymbol = '$', locale = 'en-US') {
134
+ function buildDumbbellTooltipExpr(fmt, currencySymbol = '$', locale = 'en-US', pctMultiply = true) {
135
135
  if (fmt === 'auto' || fmt === 'currency_auto') {
136
136
  const isCurrency = fmt === 'currency_auto' ? 'true' : 'false';
137
137
  const symbolStr = JSON.stringify(currencySymbol);
@@ -153,7 +153,7 @@ function buildDumbbellTooltipExpr(fmt, currencySymbol = '$', locale = 'en-US') {
153
153
  return neg ? '(' + r + ')' : r;
154
154
  })(val)`;
155
155
  }
156
- const expr = formatValue(fmt, currencySymbol, locale);
156
+ const expr = formatValue(fmt, currencySymbol, locale, pctMultiply);
157
157
  // formatValue uses 'value' as the variable name, replace with 'val' for tooltip context
158
158
  return expr.replace(/\bvalue\b/g, 'val');
159
159
  }
@@ -194,7 +194,8 @@ export function buildDumbbellOptions(spec) {
194
194
  const sampleValue = startValues[0] ?? 0;
195
195
  const valueFormat = spec.format ?? inferFormat(startField, sampleValue);
196
196
  const currency = resolveCurrency(spec.currency);
197
- const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale);
197
+ const pctMultiply = shouldPctMultiply(valueFormat, [...startValues, ...endValues]);
198
+ const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
198
199
  // Calculate axis limits
199
200
  const allValues = [...startValues, ...endValues];
200
201
  let xMin = spec.xMin;
@@ -215,7 +216,7 @@ export function buildDumbbellOptions(spec) {
215
216
  }
216
217
  }
217
218
  // Value formatter for labels inside dots (uses formatValue for all format types)
218
- const valueFormatterJs = buildDumbbellLabelFormatter(valueFormat, currency.symbol, currency.locale);
219
+ const valueFormatterJs = buildDumbbellLabelFormatter(valueFormat, currency.symbol, currency.locale, pctMultiply);
219
220
  // Build line data for custom series
220
221
  const lineData = startValues.map((s, i) => {
221
222
  const e = endValues[i];
@@ -326,7 +327,7 @@ export function buildDumbbellOptions(spec) {
326
327
  label: {
327
328
  show: showValues,
328
329
  position: 'inside',
329
- fontSize: dumbbellLabelFontSize(v, valueFormat, currency.symbol, currency.locale),
330
+ fontSize: dumbbellLabelFontSize(v, valueFormat, currency.symbol, currency.locale, pctMultiply),
330
331
  fontWeight: 'bold',
331
332
  color: rowColors[i],
332
333
  formatter: { _js_: valueFormatterJs },
@@ -349,7 +350,7 @@ export function buildDumbbellOptions(spec) {
349
350
  label: {
350
351
  show: showValues,
351
352
  position: 'inside',
352
- fontSize: dumbbellLabelFontSize(v, valueFormat, currency.symbol, currency.locale),
353
+ fontSize: dumbbellLabelFontSize(v, valueFormat, currency.symbol, currency.locale, pctMultiply),
353
354
  fontWeight: 'bold',
354
355
  color: '#ffffff',
355
356
  formatter: { _js_: valueFormatterJs },
@@ -363,7 +364,7 @@ export function buildDumbbellOptions(spec) {
363
364
  z: 10,
364
365
  });
365
366
  // Tooltip formatter with value formatting
366
- const tooltipValueExpr = buildDumbbellTooltipExpr(valueFormat, currency.symbol, currency.locale);
367
+ const tooltipValueExpr = buildDumbbellTooltipExpr(valueFormat, currency.symbol, currency.locale, pctMultiply);
367
368
  const tooltipFormatterJs = `function(params) {
368
369
  if (params.seriesName === 'range') return '';
369
370
  var cat = ${JSON.stringify(categories)}[params.data.value ? params.data.value[1] : params.data[1]];
@@ -3,14 +3,14 @@
3
3
  */
4
4
  import { FONT_SIZE_XXS, getThemeColors, getPaletteWithCustom } from '../core/themes.js';
5
5
  import { wrapHtml } from '../core/serializer.js';
6
- import { inferFormat, resolveCurrency } from '../core/formatting.js';
6
+ import { inferFormat, resolveCurrency, shouldPctMultiply } from '../core/formatting.js';
7
7
  import { registerChart, registerOptions } from './registry.js';
8
8
  /**
9
9
  * Get JavaScript formatter for funnel chart labels/tooltips
10
10
  * @param currencySymbol - Symbol to use for currency formats
11
11
  * @param locale - Locale for number formatting (defaults to 'en-US')
12
12
  */
13
- function getFunnelFormatterJs(fmt, currencySymbol = '$', locale = 'en-US') {
13
+ function getFunnelFormatterJs(fmt, currencySymbol = '$', locale = 'en-US', pctMultiply = true) {
14
14
  // Smart auto-formatting based on magnitude
15
15
  if (fmt === 'auto' || fmt === 'currency_auto' || fmt === null || fmt === undefined) {
16
16
  const isCurrency = fmt === 'currency_auto' ? 'true' : 'false';
@@ -66,14 +66,18 @@ function getFunnelFormatterJs(fmt, currencySymbol = '$', locale = 'en-US') {
66
66
  _js_: `function(params) { return params.name + ': ' + ${sym} + (params.value/1000000000).toLocaleString(${locStr}, {minimumFractionDigits:1, maximumFractionDigits:1}) + 'b'; }`,
67
67
  };
68
68
  case 'pct':
69
- case 'pct1':
69
+ case 'pct1': {
70
+ const scaleExpr = pctMultiply ? '(params.value * 100)' : 'params.value';
70
71
  return {
71
- _js_: "function(params) { return params.name + ': ' + (params.value * 100).toFixed(1) + '%'; }",
72
+ _js_: `function(params) { return params.name + ': ' + ${scaleExpr}.toFixed(1) + '%'; }`,
72
73
  };
73
- case 'pct0':
74
+ }
75
+ case 'pct0': {
76
+ const scaleExpr = pctMultiply ? '(params.value * 100)' : 'params.value';
74
77
  return {
75
- _js_: "function(params) { return params.name + ': ' + (params.value * 100).toFixed(0) + '%'; }",
78
+ _js_: `function(params) { return params.name + ': ' + ${scaleExpr}.toFixed(0) + '%'; }`,
76
79
  };
80
+ }
77
81
  case 'num0':
78
82
  return { _js_: "function(params) { return params.name + ': ' + params.value.toLocaleString(); }" };
79
83
  case 'num1':
@@ -107,8 +111,9 @@ export function buildFunnelOptions(spec) {
107
111
  const sampleValue = funnelData[0]?.value ?? 0;
108
112
  const valueFormat = spec.format ?? inferFormat(valueField, sampleValue);
109
113
  const currency = resolveCurrency(spec.currency);
114
+ const pctMultiply = shouldPctMultiply(valueFormat, funnelData.map((d) => d.value));
110
115
  // Get JavaScript formatter
111
- const formatter = getFunnelFormatterJs(valueFormat, currency.symbol, currency.locale);
116
+ const formatter = getFunnelFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
112
117
  return {
113
118
  backgroundColor: 'transparent',
114
119
  animation: false,
@@ -4,7 +4,7 @@
4
4
  import { FONT_SIZE_XXS, getThemeColors, getPaletteWithCustom } from '../core/themes.js';
5
5
  import { wrapHtml } from '../core/serializer.js';
6
6
  import { registerChart, registerOptions } from './registry.js';
7
- import { inferFormat, resolveCurrency } from '../core/formatting.js';
7
+ import { inferFormat, resolveCurrency, shouldPctMultiply } from '../core/formatting.js';
8
8
  /**
9
9
  * Parse a formatted string value like "1.9k" or "$2.5m" to a number.
10
10
  * Returns the original value if parsing fails.
@@ -38,7 +38,7 @@ function parseFormattedValue(value) {
38
38
  * @param currencySymbol - Symbol to use for currency formats
39
39
  * @param locale - Locale for number formatting (defaults to 'en-US')
40
40
  */
41
- function getHeatmapLabelFormatter(fmt, currencySymbol = '$', locale = 'en-US') {
41
+ function getHeatmapLabelFormatter(fmt, currencySymbol = '$', locale = 'en-US', pctMultiply = true) {
42
42
  if (fmt === null || fmt === undefined) {
43
43
  return null;
44
44
  }
@@ -80,13 +80,14 @@ function getHeatmapLabelFormatter(fmt, currencySymbol = '$', locale = 'en-US') {
80
80
  // Format-specific formatters (with null checks)
81
81
  const sym = JSON.stringify(currencySymbol);
82
82
  const locStr = JSON.stringify(locale);
83
+ const pctExpr = pctMultiply ? '(v * 100)' : 'v';
83
84
  const formatExprs = {
84
85
  currency: `${sym} + v.toLocaleString(${locStr})`,
85
86
  currency0k: `${sym} + (v/1000).toFixed(0) + 'k'`,
86
87
  currency0m: `${sym} + (v/1000000).toLocaleString(${locStr}, {minimumFractionDigits:1, maximumFractionDigits:1}) + 'm'`,
87
- pct: "(v * 100).toFixed(1) + '%'",
88
- pct0: "(v * 100).toFixed(0) + '%'",
89
- pct1: "(v * 100).toFixed(1) + '%'",
88
+ pct: `${pctExpr}.toFixed(1) + '%'`,
89
+ pct0: `${pctExpr}.toFixed(0) + '%'`,
90
+ pct1: `${pctExpr}.toFixed(1) + '%'`,
90
91
  num0: "v.toLocaleString()",
91
92
  num1: "v.toFixed(1)",
92
93
  num0k: "(v/1000).toFixed(0) + 'k'",
@@ -160,7 +161,8 @@ export function buildHeatmapOptions(spec) {
160
161
  const sampleValue = values[0] ?? 0;
161
162
  const valueFormat = spec.format ?? inferFormat('value', sampleValue);
162
163
  const currency = resolveCurrency(spec.currency);
163
- const labelFormatter = getHeatmapLabelFormatter(valueFormat, currency.symbol, currency.locale);
164
+ const pctMultiply = shouldPctMultiply(valueFormat, values);
165
+ const labelFormatter = getHeatmapLabelFormatter(valueFormat, currency.symbol, currency.locale, pctMultiply);
164
166
  // Build label config
165
167
  const labelConfig = { show: true, color: colors.text };
166
168
  if (labelFormatter) {
@@ -4,7 +4,7 @@
4
4
  import { FONT_SIZE_TINY, DEFAULT_CHART_HEIGHT, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
5
5
  import { wrapHtml } from '../core/serializer.js';
6
6
  import { registerChart, registerOptions } from './registry.js';
7
- import { inferFormat, getAxisFormatterJs, inferAxisType, resolveCurrency } from '../core/formatting.js';
7
+ import { inferFormat, getAxisFormatterJs, inferAxisType, resolveCurrency, shouldPctMultiply, collectNumericFieldValues, } from '../core/formatting.js';
8
8
  /**
9
9
  * Build ECharts options for a line chart
10
10
  */
@@ -23,7 +23,8 @@ export function buildLineOptions(spec) {
23
23
  const sampleValue = data.length > 0 && data[0] ? data[0][firstYKey] ?? 0 : 0;
24
24
  const valueFormat = spec.format ?? inferFormat(firstYKey, sampleValue);
25
25
  const currency = resolveCurrency(spec.currency);
26
- const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale);
26
+ const pctMultiply = shouldPctMultiply(valueFormat, collectNumericFieldValues(data, yKeys));
27
+ const axisFormatter = getAxisFormatterJs(valueFormat, currency.symbol, currency.locale, pctMultiply);
27
28
  // Detect x-axis type: time, value, or category
28
29
  // User can override with xAxisType in spec
29
30
  const xAxisType = spec.xAxisType ?? inferAxisType(categories);
@@ -4,7 +4,7 @@
4
4
  import { SCATTER_SYMBOL_SIZE, FONT_SIZE_TINY, DEFAULT_CHART_HEIGHT, getThemeColors, getPaletteWithCustom, } from '../core/themes.js';
5
5
  import { wrapHtml } from '../core/serializer.js';
6
6
  import { registerChart, registerOptions } from './registry.js';
7
- import { inferFormat, getAxisFormatterJs, formatValue, resolveCurrency } from '../core/formatting.js';
7
+ import { inferFormat, getAxisFormatterJs, formatValue, resolveCurrency, shouldPctMultiply, collectNumericFieldValues, } from '../core/formatting.js';
8
8
  import { buildPointLabelConfig, groupDataBySeries, buildSeriesLegend } from '../core/chart-helpers.js';
9
9
  /** Scatter point opacity */
10
10
  const SCATTER_OPACITY = 0.75;
@@ -38,8 +38,10 @@ export function buildScatterOptions(spec) {
38
38
  const xFormat = spec.xFormat ?? inferFormat(xField, xSample);
39
39
  const yFormat = spec.yFormat ?? inferFormat(yField, ySample);
40
40
  const currency = resolveCurrency(spec.currency);
41
- const xAxisFormatter = getAxisFormatterJs(xFormat, currency.symbol, currency.locale);
42
- const yAxisFormatter = getAxisFormatterJs(yFormat, currency.symbol, currency.locale);
41
+ const xPctMultiply = shouldPctMultiply(xFormat, collectNumericFieldValues(data, [xField]));
42
+ const yPctMultiply = shouldPctMultiply(yFormat, collectNumericFieldValues(data, [yField]));
43
+ const xAxisFormatter = getAxisFormatterJs(xFormat, currency.symbol, currency.locale, xPctMultiply);
44
+ const yAxisFormatter = getAxisFormatterJs(yFormat, currency.symbol, currency.locale, yPctMultiply);
43
45
  // Label config for persistent point labels
44
46
  const labelConfig = showLabels && labelField
45
47
  ? buildPointLabelConfig(SCATTER_LABEL_INDEX, colors)
@@ -77,8 +79,8 @@ export function buildScatterOptions(spec) {
77
79
  seriesList = [seriesObj];
78
80
  }
79
81
  // Build tooltip formatter with label support and value formatting
80
- const xFormatExpr = formatValue(xFormat, currency.symbol, currency.locale);
81
- const yFormatExpr = formatValue(yFormat, currency.symbol, currency.locale);
82
+ const xFormatExpr = formatValue(xFormat, currency.symbol, currency.locale, xPctMultiply);
83
+ const yFormatExpr = formatValue(yFormat, currency.symbol, currency.locale, yPctMultiply);
82
84
  const tooltipFormatterJs = `function(params) {
83
85
  var d = params.value || params.data;
84
86
  var result = '';
package/dist/cli.js CHANGED
@@ -177,22 +177,23 @@ async function main() {
177
177
  let errors = [];
178
178
  // Detect input type
179
179
  const trimmed = input.trim();
180
+ const lintMode = lintOnly ? 'lint' : 'generate';
180
181
  if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
181
182
  // JSON input - lint then generate
182
183
  const spec = JSON.parse(trimmed);
183
- lintSpec(spec);
184
+ lintSpec(spec, lintMode);
184
185
  html = await generateChartAsync(spec);
185
186
  }
186
187
  else if (trimmed.startsWith('---') || trimmed.includes('```')) {
187
188
  // Markdown input - parser handles linting internally (async for mermaid)
188
- const result = await parseMarkdownToDashboardAsync(input, 'light', baseDir, false, false, customTheme);
189
+ const result = await parseMarkdownToDashboardAsync(input, 'light', baseDir, false, false, customTheme, lintMode);
189
190
  html = result.html;
190
191
  errors = result.errors;
191
192
  }
192
193
  else {
193
194
  // Try JSON anyway - lint then generate
194
195
  const spec = JSON.parse(trimmed);
195
- lintSpec(spec);
196
+ lintSpec(spec, lintMode);
196
197
  html = await generateChartAsync(spec);
197
198
  }
198
199
  // Check for errors (unless --allow-errors is set)
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Table interactivity (sorting and filtering).
3
+ *
4
+ * Used by both standalone tables (`renderStandaloneTable`) and dashboard-embedded
5
+ * tables (`renderTable` in the layout parser). Produces the chrome (global filter
6
+ * input), the per-cell `data-sort` attribute for numeric sorting, and one
7
+ * vanilla-JS init function that wires up click/input handlers.
8
+ */
9
+ export interface TableInteractivity {
10
+ sortable: boolean;
11
+ filter: boolean;
12
+ }
13
+ /**
14
+ * Read sortable/filter options from a spec. Sort defaults ON, filter defaults OFF.
15
+ */
16
+ export declare function parseTableInteractivity(spec: Record<string, unknown>): TableInteractivity;
17
+ /**
18
+ * Generate an HTML attribute string for a sortable `<th>`.
19
+ * `colIdx` maps to the visual column index (including the row-number column if present).
20
+ */
21
+ export declare function sortableHeaderAttrs(colIdx: number, sortable: boolean): string;
22
+ /**
23
+ * Generate `data-sort="..."` for a cell so numeric sort doesn't depend on the
24
+ * rendered text (which may be formatted, contain SVG sparklines, etc.).
25
+ */
26
+ export declare function cellSortAttr(value: unknown): string;
27
+ /**
28
+ * Build the filter toolbar (global search input when `filter: true`).
29
+ */
30
+ export interface TableChrome {
31
+ toolbarHtml: string;
32
+ }
33
+ export declare function buildTableChrome(tableId: string, interactivity: TableInteractivity, colors: {
34
+ text: string;
35
+ textSecondary: string;
36
+ border: string;
37
+ background: string;
38
+ }): TableChrome;
39
+ /**
40
+ * Per-table init call. Invoked after the HTML is in the DOM.
41
+ */
42
+ export declare function tableInitCall(tableId: string): string;
43
+ /**
44
+ * Shared JS that defines `window.mvizTableInit`. Include once per output document.
45
+ * Responsible for sort clicks and the global filter input.
46
+ */
47
+ export declare function sharedTableScript(): string;
48
+ /**
49
+ * Shared CSS for sort indicators. Appended once per standalone document or
50
+ * injected once into dashboard mode alongside the shared script.
51
+ */
52
+ export declare function sharedTableCss(): string;
53
+ //# sourceMappingURL=table-interactivity.d.ts.map