@cfasim-ui/docs 0.4.6 → 0.4.8
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/charts/BarChart/BarChart.md +37 -2
- package/charts/BarChart/BarChart.vue +60 -17
- package/charts/ChartMenu/download.ts +68 -3
- package/charts/ChoroplethMap/ChoroplethMap.md +1 -1
- package/charts/ChoroplethMap/ChoroplethMap.vue +8 -6
- package/charts/DataTable/DataTable.md +48 -0
- package/charts/DataTable/DataTable.vue +28 -1
- package/charts/LineChart/LineChart.md +37 -3
- package/charts/LineChart/LineChart.vue +83 -67
- package/charts/_shared/ChartAnnotations.vue +54 -34
- package/charts/_shared/chartProps.ts +6 -4
- package/charts/_shared/index.ts +7 -0
- package/charts/_shared/scale.ts +86 -0
- package/charts/_shared/useChartFoundation.ts +5 -4
- package/components/NumberInput/NumberInput.md +22 -7
- package/components/NumberInput/NumberInput.vue +18 -4
- package/index.json +1 -1
- package/package.json +1 -1
- package/shared/formatNumber.ts +146 -0
- package/shared/index.ts +2 -0
|
@@ -160,6 +160,40 @@ Use `value-tick-format` to format the value-axis labels. `tooltip-value-format`
|
|
|
160
160
|
</template>
|
|
161
161
|
</ComponentDemo>
|
|
162
162
|
|
|
163
|
+
### Logarithmic value axis
|
|
164
|
+
|
|
165
|
+
Set `value-scale-type="log"` to switch the value axis to base-10 log
|
|
166
|
+
scaling. Non-positive values collapse to the axis floor, and
|
|
167
|
+
`valueMin <= 0` is ignored. With `layout="stacked"`, individual segment
|
|
168
|
+
sizes are no longer proportional to their raw values, but the cumulative
|
|
169
|
+
top still represents the sum.
|
|
170
|
+
|
|
171
|
+
<ComponentDemo>
|
|
172
|
+
<BarChart
|
|
173
|
+
:data="[3, 24, 180, 1450, 9800]"
|
|
174
|
+
:categories="['Wk1', 'Wk2', 'Wk3', 'Wk4', 'Wk5']"
|
|
175
|
+
value-scale-type="log"
|
|
176
|
+
:height="220"
|
|
177
|
+
x-label="Week"
|
|
178
|
+
y-label="Cases"
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<template #code>
|
|
182
|
+
|
|
183
|
+
```vue
|
|
184
|
+
<BarChart
|
|
185
|
+
:data="[3, 24, 180, 1450, 9800]"
|
|
186
|
+
:categories="['Wk1', 'Wk2', 'Wk3', 'Wk4', 'Wk5']"
|
|
187
|
+
value-scale-type="log"
|
|
188
|
+
:height="220"
|
|
189
|
+
x-label="Week"
|
|
190
|
+
y-label="Cases"
|
|
191
|
+
/>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
</template>
|
|
195
|
+
</ComponentDemo>
|
|
196
|
+
|
|
163
197
|
### Annotations
|
|
164
198
|
|
|
165
199
|
Pin callouts to specific bars with `annotations`. `x` is the category
|
|
@@ -286,7 +320,7 @@ anchor. `lineColor`, `lineWidth`, and `lineDash` style the line.
|
|
|
286
320
|
| `tooltipData` | `ArrayLike<unknown>` | No | — |
|
|
287
321
|
| `tooltipTrigger` | `"hover" \| "click"` | No | — |
|
|
288
322
|
| `tooltipClamp` | `"none" \| "chart" \| "window"` | No | `"chart"` |
|
|
289
|
-
| `tooltipValueFormat` | `
|
|
323
|
+
| `tooltipValueFormat` | `NumberFormat` | No | — |
|
|
290
324
|
| `csv` | `string \| (() => string)` | No | — |
|
|
291
325
|
| `filename` | `string` | No | — |
|
|
292
326
|
| `downloadLink` | `boolean \| string` | No | — |
|
|
@@ -299,8 +333,9 @@ anchor. `lineColor`, `lineWidth`, and `lineDash` style the line.
|
|
|
299
333
|
| `orientation` | `"vertical" \| "horizontal"` | No | `"vertical"` |
|
|
300
334
|
| `layout` | `"grouped" \| "stacked"` | No | `"grouped"` |
|
|
301
335
|
| `valueMin` | `number` | No | `0` |
|
|
336
|
+
| `valueScaleType` | `"linear" \| "log"` | No | `"linear"` |
|
|
302
337
|
| `valueTicks` | `number \| number[]` | No | — |
|
|
303
|
-
| `valueTickFormat` | `
|
|
338
|
+
| `valueTickFormat` | `NumberFormat` | No | — |
|
|
304
339
|
| `categoryFormat` | `(label: string, index: number) => string` | No | — |
|
|
305
340
|
| `barPadding` | `number` | No | `0.2` |
|
|
306
341
|
| `groupGap` | `number` | No | `1` |
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from "vue";
|
|
3
|
+
import { formatNumber, type NumberFormat } from "@cfasim-ui/shared";
|
|
3
4
|
import ChartMenu from "../ChartMenu/ChartMenu.vue";
|
|
4
5
|
import {
|
|
5
6
|
snap,
|
|
6
7
|
formatTick,
|
|
7
8
|
computeTickValues,
|
|
9
|
+
computeLogTickValues,
|
|
10
|
+
scaleFraction,
|
|
11
|
+
clampExtentForScale,
|
|
8
12
|
categoricalToCsv,
|
|
9
13
|
useChartFoundation,
|
|
10
14
|
makeTooltipValueFormatter,
|
|
@@ -45,13 +49,26 @@ interface BarChartProps extends ChartCommonProps {
|
|
|
45
49
|
layout?: "grouped" | "stacked";
|
|
46
50
|
/** Force the value axis to start at this value or lower (default 0). */
|
|
47
51
|
valueMin?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Scale type for the value axis. `"linear"` (default) maps values
|
|
54
|
+
* directly to pixels; `"log"` uses a base-10 log mapping. On a log
|
|
55
|
+
* axis, non-positive values collapse to the visible minimum, and
|
|
56
|
+
* `valueMin <= 0` is ignored. Stacked layout + log produces a
|
|
57
|
+
* cumulative axis but individual segment sizes are no longer
|
|
58
|
+
* proportional to their values.
|
|
59
|
+
*/
|
|
60
|
+
valueScaleType?: "linear" | "log";
|
|
48
61
|
/**
|
|
49
62
|
* Tick placement on the value axis (numeric). Number = interval,
|
|
50
63
|
* array = explicit values. When omitted, ticks are chosen automatically.
|
|
51
64
|
*/
|
|
52
65
|
valueTicks?: number | number[];
|
|
53
|
-
/**
|
|
54
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Formatter for value-axis tick labels. Accepts a preset name, a
|
|
68
|
+
* printf-style format string, or a function. See `formatNumber` in
|
|
69
|
+
* `@cfasim-ui/shared`.
|
|
70
|
+
*/
|
|
71
|
+
valueTickFormat?: NumberFormat;
|
|
55
72
|
/** Formatter for category-axis labels. Receives the resolved category string. */
|
|
56
73
|
categoryFormat?: (label: string, index: number) => string;
|
|
57
74
|
/**
|
|
@@ -76,6 +93,7 @@ const props = withDefaults(defineProps<BarChartProps>(), {
|
|
|
76
93
|
menu: true,
|
|
77
94
|
tooltipClamp: "chart",
|
|
78
95
|
valueGrid: true,
|
|
96
|
+
valueScaleType: "linear",
|
|
79
97
|
});
|
|
80
98
|
|
|
81
99
|
const emit = defineEmits<{
|
|
@@ -137,6 +155,10 @@ const isVertical = computed(() => props.orientation === "vertical");
|
|
|
137
155
|
const valueExtent = computed(() => {
|
|
138
156
|
let min = Infinity;
|
|
139
157
|
let max = -Infinity;
|
|
158
|
+
let smallestPositive = Infinity;
|
|
159
|
+
const visitPositive = (v: number) => {
|
|
160
|
+
if (v > 0 && v < smallestPositive) smallestPositive = v;
|
|
161
|
+
};
|
|
140
162
|
if (props.layout === "stacked") {
|
|
141
163
|
const n = categoryCount.value;
|
|
142
164
|
for (let i = 0; i < n; i++) {
|
|
@@ -146,6 +168,7 @@ const valueExtent = computed(() => {
|
|
|
146
168
|
if (i >= s.data.length) continue;
|
|
147
169
|
const v = Number(s.data[i]);
|
|
148
170
|
if (!isFinite(v)) continue;
|
|
171
|
+
visitPositive(v);
|
|
149
172
|
if (v >= 0) pos += v;
|
|
150
173
|
else neg += v;
|
|
151
174
|
}
|
|
@@ -157,6 +180,7 @@ const valueExtent = computed(() => {
|
|
|
157
180
|
for (const v of s.data) {
|
|
158
181
|
const n = Number(v);
|
|
159
182
|
if (!isFinite(n)) continue;
|
|
183
|
+
visitPositive(n);
|
|
160
184
|
if (n < min) min = n;
|
|
161
185
|
if (n > max) max = n;
|
|
162
186
|
}
|
|
@@ -165,11 +189,25 @@ const valueExtent = computed(() => {
|
|
|
165
189
|
if (!isFinite(min)) min = 0;
|
|
166
190
|
if (!isFinite(max)) max = 0;
|
|
167
191
|
// Extend the value axis down to valueMin (default 0) when data sits
|
|
168
|
-
// above it, so bars share a common baseline.
|
|
192
|
+
// above it, so bars share a common baseline. On log scales, only
|
|
193
|
+
// positive valueMin values are honored.
|
|
169
194
|
const floor = props.valueMin ?? 0;
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
195
|
+
if (props.valueScaleType === "log") {
|
|
196
|
+
if (floor > 0 && floor < min) min = floor;
|
|
197
|
+
} else if (floor < min) {
|
|
198
|
+
min = floor;
|
|
199
|
+
}
|
|
200
|
+
const clamped = clampExtentForScale(
|
|
201
|
+
min,
|
|
202
|
+
max,
|
|
203
|
+
props.valueScaleType,
|
|
204
|
+
smallestPositive,
|
|
205
|
+
);
|
|
206
|
+
return {
|
|
207
|
+
min: clamped.min,
|
|
208
|
+
max: clamped.max,
|
|
209
|
+
range: clamped.max - clamped.min || 1,
|
|
210
|
+
};
|
|
173
211
|
});
|
|
174
212
|
|
|
175
213
|
/** Size (in pixels) of the categorical axis. */
|
|
@@ -218,12 +256,12 @@ const groupedBaselinePixel = computed(() => {
|
|
|
218
256
|
|
|
219
257
|
/** Convert a data value to its pixel position along the value axis. */
|
|
220
258
|
function valuePixel(v: number): number {
|
|
221
|
-
const { min,
|
|
222
|
-
const
|
|
259
|
+
const { min, max } = valueExtent.value;
|
|
260
|
+
const frac = scaleFraction(v, min, max, props.valueScaleType);
|
|
223
261
|
if (isVertical.value) {
|
|
224
|
-
return padding.value.top + innerH.value -
|
|
262
|
+
return padding.value.top + innerH.value - frac * innerH.value;
|
|
225
263
|
}
|
|
226
|
-
return padding.value.left +
|
|
264
|
+
return padding.value.left + frac * innerW.value;
|
|
227
265
|
}
|
|
228
266
|
|
|
229
267
|
interface BarRect {
|
|
@@ -367,7 +405,9 @@ const formatTooltipValue = makeTooltipValueFormatter(
|
|
|
367
405
|
const valueTickItems = computed(() => {
|
|
368
406
|
const { min, max } = valueExtent.value;
|
|
369
407
|
const fmt = (v: number) =>
|
|
370
|
-
props.valueTickFormat
|
|
408
|
+
props.valueTickFormat !== undefined
|
|
409
|
+
? formatNumber(v, props.valueTickFormat)
|
|
410
|
+
: formatTick(v);
|
|
371
411
|
if (min === max) {
|
|
372
412
|
return [
|
|
373
413
|
{
|
|
@@ -377,12 +417,15 @@ const valueTickItems = computed(() => {
|
|
|
377
417
|
];
|
|
378
418
|
}
|
|
379
419
|
const targetTickPixels = isVertical.value ? 50 : 80;
|
|
380
|
-
const values =
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
420
|
+
const values =
|
|
421
|
+
props.valueScaleType === "log"
|
|
422
|
+
? computeLogTickValues({ min, max, ticks: props.valueTicks })
|
|
423
|
+
: computeTickValues({
|
|
424
|
+
min,
|
|
425
|
+
max,
|
|
426
|
+
ticks: props.valueTicks,
|
|
427
|
+
targetTickCount: valueSize.value / targetTickPixels,
|
|
428
|
+
});
|
|
386
429
|
return values.map((v) => ({
|
|
387
430
|
value: fmt(v),
|
|
388
431
|
pos: snap(valuePixel(v)),
|
|
@@ -7,16 +7,81 @@ export function downloadBlob(blob: Blob, name: string) {
|
|
|
7
7
|
URL.revokeObjectURL(url);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/** Inherited CSS properties propagated onto the cloned SVG root so text and
|
|
11
|
+
* `currentColor` strokes render with the same color/typography as the
|
|
12
|
+
* live chart when the SVG is opened standalone or rasterized to PNG. */
|
|
13
|
+
const ROOT_INHERITED_PROPS = [
|
|
14
|
+
"color",
|
|
15
|
+
"font-family",
|
|
16
|
+
"font-size",
|
|
17
|
+
"font-weight",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
/** Presentation attributes that may contain `var(--…)` references. The
|
|
21
|
+
* fallback inside `var(--name, #fff)` is the only thing that renders when
|
|
22
|
+
* the custom property is undefined outside the page context — so we
|
|
23
|
+
* resolve them against the document's CSS custom properties before
|
|
24
|
+
* serializing. */
|
|
25
|
+
const VAR_RESOLVABLE_ATTRS = ["fill", "stroke"] as const;
|
|
26
|
+
|
|
27
|
+
/** Resolve a single `var(--name)` or `var(--name, fallback)` expression.
|
|
28
|
+
* Looks the name up on `document.documentElement` (where theme tokens are
|
|
29
|
+
* declared); falls back to the in-expression fallback, then the original
|
|
30
|
+
* expression if neither resolves. */
|
|
31
|
+
function resolveVarExpression(expr: string): string {
|
|
32
|
+
const match = expr.match(
|
|
33
|
+
/^\s*var\(\s*(--[\w-]+)\s*(?:,\s*([^)]*?)\s*)?\)\s*$/,
|
|
34
|
+
);
|
|
35
|
+
if (!match) return expr;
|
|
36
|
+
const [, name, fallback] = match;
|
|
37
|
+
const value = window
|
|
38
|
+
.getComputedStyle(document.documentElement)
|
|
39
|
+
.getPropertyValue(name)
|
|
40
|
+
.trim();
|
|
41
|
+
if (value) return value;
|
|
42
|
+
if (fallback) return fallback.trim();
|
|
43
|
+
return expr;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Clone `svg` and inline the styles that don't survive a standalone render:
|
|
47
|
+
* inherited `color`/`font-*` on the root (so `currentColor` and unset
|
|
48
|
+
* font-family resolve), and any `var(--…)` references in fill/stroke
|
|
49
|
+
* attributes resolved against the document. */
|
|
50
|
+
export function prepareSvgForExport(svg: SVGSVGElement): SVGSVGElement {
|
|
11
51
|
const clone = svg.cloneNode(true) as SVGSVGElement;
|
|
12
52
|
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
53
|
+
|
|
54
|
+
const rootComputed = window.getComputedStyle(svg);
|
|
55
|
+
const rootStyleParts: string[] = [];
|
|
56
|
+
for (const prop of ROOT_INHERITED_PROPS) {
|
|
57
|
+
const value = rootComputed.getPropertyValue(prop);
|
|
58
|
+
if (value) rootStyleParts.push(`${prop}: ${value}`);
|
|
59
|
+
}
|
|
60
|
+
const existingRootStyle = clone.getAttribute("style") ?? "";
|
|
61
|
+
clone.setAttribute(
|
|
62
|
+
"style",
|
|
63
|
+
[existingRootStyle, ...rootStyleParts].filter(Boolean).join("; "),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
for (const target of clone.querySelectorAll<SVGElement>("*")) {
|
|
67
|
+
for (const attr of VAR_RESOLVABLE_ATTRS) {
|
|
68
|
+
const raw = target.getAttribute(attr);
|
|
69
|
+
if (!raw || !raw.includes("var(")) continue;
|
|
70
|
+
target.setAttribute(attr, resolveVarExpression(raw));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return clone;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function saveSvg(svg: SVGSVGElement, filename: string) {
|
|
78
|
+
const clone = prepareSvgForExport(svg);
|
|
13
79
|
const xml = new XMLSerializer().serializeToString(clone);
|
|
14
80
|
downloadBlob(new Blob([xml], { type: "image/svg+xml" }), `${filename}.svg`);
|
|
15
81
|
}
|
|
16
82
|
|
|
17
83
|
export function savePng(svg: SVGSVGElement, filename: string) {
|
|
18
|
-
const clone = svg
|
|
19
|
-
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
84
|
+
const clone = prepareSvgForExport(svg);
|
|
20
85
|
const xml = new XMLSerializer().serializeToString(clone);
|
|
21
86
|
const svgBlob = new Blob([xml], { type: "image/svg+xml;charset=utf-8" });
|
|
22
87
|
const url = URL.createObjectURL(svgBlob);
|
|
@@ -704,7 +704,7 @@ set `tooltip-trigger`.
|
|
|
704
704
|
id: string` | No | — |
|
|
705
705
|
| `name` | `string` | Yes | — |
|
|
706
706
|
| `value` | `number \| string` | No | — |
|
|
707
|
-
| `tooltipValueFormat` | `
|
|
707
|
+
| `tooltipValueFormat` | `NumberFormat` | No | — |
|
|
708
708
|
| `tooltipClamp` | `"none" \| "chart" \| "window"` | No | `"chart"` |
|
|
709
709
|
| `focus` | `FocusValue` | No | — |
|
|
710
710
|
| `focusZoomLevel` | `number` | No | `4` |
|
|
@@ -16,6 +16,7 @@ import { select } from "d3-selection";
|
|
|
16
16
|
import "d3-transition";
|
|
17
17
|
import { feature, mesh, merge } from "topojson-client";
|
|
18
18
|
import type { Topology, GeometryCollection } from "topojson-specification";
|
|
19
|
+
import { formatNumber, type NumberFormat } from "@cfasim-ui/shared";
|
|
19
20
|
import { fipsToHsa, hsaNames } from "./hsaMapping.js";
|
|
20
21
|
import ChartMenu from "../ChartMenu/ChartMenu.vue";
|
|
21
22
|
import type { ChartMenuItem } from "../ChartMenu/ChartMenu.vue";
|
|
@@ -129,11 +130,12 @@ const props = withDefaults(
|
|
|
129
130
|
value?: number | string;
|
|
130
131
|
}) => string;
|
|
131
132
|
/**
|
|
132
|
-
* Formatter for numeric values shown in the default tooltip.
|
|
133
|
-
*
|
|
134
|
-
* controls the entire tooltip in
|
|
133
|
+
* Formatter for numeric values shown in the default tooltip. Accepts a
|
|
134
|
+
* preset name, a printf-style format string, or a function. Ignored when
|
|
135
|
+
* `tooltipFormat` is provided (the caller controls the entire tooltip in
|
|
136
|
+
* that case). See `formatNumber` in `@cfasim-ui/shared`.
|
|
135
137
|
*/
|
|
136
|
-
tooltipValueFormat?:
|
|
138
|
+
tooltipValueFormat?: NumberFormat;
|
|
137
139
|
/**
|
|
138
140
|
* Boundary for tooltip flip/clamp. `"none"` always places to the right of
|
|
139
141
|
* the pointer with no clamping. `"chart"` (default) uses the map
|
|
@@ -889,8 +891,8 @@ const featureName = (
|
|
|
889
891
|
|
|
890
892
|
function formatTooltipValue(value: number | string | undefined): string {
|
|
891
893
|
if (value == null) return "";
|
|
892
|
-
if (typeof value === "number" && props.tooltipValueFormat) {
|
|
893
|
-
return props.tooltipValueFormat
|
|
894
|
+
if (typeof value === "number" && props.tooltipValueFormat !== undefined) {
|
|
895
|
+
return formatNumber(value, props.tooltipValueFormat);
|
|
894
896
|
}
|
|
895
897
|
return String(value);
|
|
896
898
|
}
|
|
@@ -56,6 +56,43 @@ A table for displaying columnar data. Accepts a plain record of arrays or a `Mod
|
|
|
56
56
|
</template>
|
|
57
57
|
</ComponentDemo>
|
|
58
58
|
|
|
59
|
+
### Formatting cell values
|
|
60
|
+
|
|
61
|
+
Use `format` on a column to override the default rendering. Accepts a
|
|
62
|
+
{@link NumberFormat} value — a preset name (optionally with `:N` digits,
|
|
63
|
+
e.g. `"percent:1"`), a printf-style format string, or a function
|
|
64
|
+
`(value, row) => string`. Formatted values are also used in CSV exports.
|
|
65
|
+
|
|
66
|
+
<ComponentDemo>
|
|
67
|
+
<DataTable
|
|
68
|
+
:data="{ day: [0, 1, 2, 3, 4], rate: [0.012, 0.234, 0.467, 0.512, 0.601], cases: [1, 21, 56, 101, 141] }"
|
|
69
|
+
:column-config="{
|
|
70
|
+
day: { label: 'Day', width: 'small' },
|
|
71
|
+
rate: { label: 'Attack rate', format: 'percent:1' },
|
|
72
|
+
cases: { label: 'Cases', format: '%05d' },
|
|
73
|
+
}"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<template #code>
|
|
77
|
+
|
|
78
|
+
```vue
|
|
79
|
+
<DataTable
|
|
80
|
+
:data="{
|
|
81
|
+
day: [0, 1, 2, 3, 4],
|
|
82
|
+
rate: [0.012, 0.234, 0.467, 0.512, 0.601],
|
|
83
|
+
cases: [1, 21, 56, 101, 141],
|
|
84
|
+
}"
|
|
85
|
+
:column-config="{
|
|
86
|
+
day: { label: 'Day', width: 'small' },
|
|
87
|
+
rate: { label: 'Attack rate', format: 'percent:1' },
|
|
88
|
+
cases: { label: 'Cases', format: '%05d' },
|
|
89
|
+
}"
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
</template>
|
|
94
|
+
</ComponentDemo>
|
|
95
|
+
|
|
59
96
|
### Cell class and max rows
|
|
60
97
|
|
|
61
98
|
<ComponentDemo>
|
|
@@ -169,5 +206,16 @@ interface ColumnConfig {
|
|
|
169
206
|
width?: "small" | "medium" | "large" | number;
|
|
170
207
|
align?: "left" | "center" | "right";
|
|
171
208
|
cellClass?: string;
|
|
209
|
+
format?: NumberFormat | ((value: CellValue, row: number) => string);
|
|
172
210
|
}
|
|
211
|
+
|
|
212
|
+
type CellValue = number | string | boolean;
|
|
213
|
+
|
|
214
|
+
type NumberFormat =
|
|
215
|
+
| NumberFormatPreset // "plain" | "localized" | "percent" | "compact" | "scientific" | "engineering" (optionally with ":N" digits, e.g. "percent:1")
|
|
216
|
+
| string // printf-style format, e.g. "%.2f", "%05d"
|
|
217
|
+
| ((value: number) => string);
|
|
173
218
|
```
|
|
219
|
+
|
|
220
|
+
See `formatNumber` in `@cfasim-ui/shared` for the underlying utility — you
|
|
221
|
+
can also call it directly in your own code.
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import type { CSSProperties } from "vue";
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
formatNumber,
|
|
6
|
+
type ModelOutput,
|
|
7
|
+
type NumberFormat,
|
|
8
|
+
} from "@cfasim-ui/shared";
|
|
5
9
|
import ChartMenu from "../ChartMenu/ChartMenu.vue";
|
|
6
10
|
import type { ChartMenuItem } from "../ChartMenu/ChartMenu.vue";
|
|
7
11
|
import { downloadCsv } from "../ChartMenu/download.js";
|
|
@@ -10,12 +14,23 @@ export type TableRecord = Record<string, ArrayLike<number | string | boolean>>;
|
|
|
10
14
|
export type TableData = TableRecord | ModelOutput;
|
|
11
15
|
export type ColumnWidth = "small" | "medium" | "large";
|
|
12
16
|
export type ColumnAlign = "left" | "center" | "right";
|
|
17
|
+
export type CellValue = number | string | boolean;
|
|
18
|
+
export type ColumnFormatter = (value: CellValue, row: number) => string;
|
|
13
19
|
|
|
14
20
|
export interface ColumnConfig {
|
|
15
21
|
label?: string;
|
|
16
22
|
width?: ColumnWidth | number;
|
|
17
23
|
align?: ColumnAlign;
|
|
18
24
|
cellClass?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Custom formatter for cell values in this column. Accepts a
|
|
27
|
+
* {@link NumberFormat} (preset name, printf-style string, or
|
|
28
|
+
* `(value) => string` function — see `formatNumber` in
|
|
29
|
+
* `@cfasim-ui/shared`) or a `(value, row) => string` function for full
|
|
30
|
+
* control. Number presets/sprintf only apply to numeric cells; other
|
|
31
|
+
* types fall back to default rendering. Used in CSV exports.
|
|
32
|
+
*/
|
|
33
|
+
format?: NumberFormat | ColumnFormatter;
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
const COLUMN_WIDTHS: Record<ColumnWidth, string> = {
|
|
@@ -102,6 +117,18 @@ const rowCount = computed(() => {
|
|
|
102
117
|
function cellValue(col: Column, row: number): string {
|
|
103
118
|
const v = col.values[row];
|
|
104
119
|
if (v === undefined || v === null) return "";
|
|
120
|
+
const format = props.columnConfig?.[col.name]?.format;
|
|
121
|
+
if (format !== undefined) {
|
|
122
|
+
// Function variant — either `(value: number) => string` (NumberFormat
|
|
123
|
+
// function) or `(value, row) => string` (ColumnFormatter). Both call
|
|
124
|
+
// sites are compatible; the narrower variant ignores `row`.
|
|
125
|
+
if (typeof format === "function") {
|
|
126
|
+
return (format as ColumnFormatter)(v, row);
|
|
127
|
+
}
|
|
128
|
+
// String preset/sprintf — only applies to numeric cells; other types
|
|
129
|
+
// fall through to default rendering.
|
|
130
|
+
if (typeof v === "number") return formatNumber(v, format);
|
|
131
|
+
}
|
|
105
132
|
if (col.enumLabels && typeof v === "number")
|
|
106
133
|
return col.enumLabels[v] ?? String(v);
|
|
107
134
|
if (typeof v === "number") {
|
|
@@ -180,6 +180,39 @@ Control tick placement with `x-ticks` and `y-ticks`. Pass a **number** for a fix
|
|
|
180
180
|
</template>
|
|
181
181
|
</ComponentDemo>
|
|
182
182
|
|
|
183
|
+
### Logarithmic y-axis
|
|
184
|
+
|
|
185
|
+
Set `y-scale-type="log"` to switch the y axis to base-10 log scaling. Useful
|
|
186
|
+
when data spans several orders of magnitude (e.g. epidemic case counts in
|
|
187
|
+
early exponential growth). Non-positive values collapse to the axis
|
|
188
|
+
floor, and `yMin <= 0` is ignored.
|
|
189
|
+
|
|
190
|
+
<ComponentDemo>
|
|
191
|
+
<LineChart
|
|
192
|
+
:data="[1, 3, 8, 22, 60, 165, 450, 1230, 3350]"
|
|
193
|
+
y-scale-type="log"
|
|
194
|
+
:height="220"
|
|
195
|
+
x-label="Day"
|
|
196
|
+
y-label="Cases"
|
|
197
|
+
y-grid
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
<template #code>
|
|
201
|
+
|
|
202
|
+
```vue
|
|
203
|
+
<LineChart
|
|
204
|
+
:data="[1, 3, 8, 22, 60, 165, 450, 1230, 3350]"
|
|
205
|
+
y-scale-type="log"
|
|
206
|
+
:height="220"
|
|
207
|
+
x-label="Day"
|
|
208
|
+
y-label="Cases"
|
|
209
|
+
y-grid
|
|
210
|
+
/>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
</template>
|
|
214
|
+
</ComponentDemo>
|
|
215
|
+
|
|
183
216
|
### Dashed baseline
|
|
184
217
|
|
|
185
218
|
<ComponentDemo>
|
|
@@ -647,7 +680,7 @@ until the user clicks Download:
|
|
|
647
680
|
| `tooltipData` | `ArrayLike<unknown>` | No | — |
|
|
648
681
|
| `tooltipTrigger` | `"hover" \| "click"` | No | — |
|
|
649
682
|
| `tooltipClamp` | `"none" \| "chart" \| "window"` | No | `"chart"` |
|
|
650
|
-
| `tooltipValueFormat` | `
|
|
683
|
+
| `tooltipValueFormat` | `NumberFormat` | No | — |
|
|
651
684
|
| `csv` | `string \| (() => string)` | No | — |
|
|
652
685
|
| `filename` | `string` | No | — |
|
|
653
686
|
| `downloadLink` | `boolean \| string` | No | — |
|
|
@@ -661,11 +694,12 @@ until the user clicks Download:
|
|
|
661
694
|
| `areaSections` | `AreaSection[]` | No | — |
|
|
662
695
|
| `lineOpacity` | `number` | No | `1` |
|
|
663
696
|
| `yMin` | `number` | No | — |
|
|
697
|
+
| `yScaleType` | `"linear" \| "log"` | No | `"linear"` |
|
|
664
698
|
| `xMin` | `number` | No | — |
|
|
665
699
|
| `xTicks` | `number \| number[]` | No | — |
|
|
666
700
|
| `yTicks` | `number \| number[]` | No | — |
|
|
667
|
-
| `xTickFormat` | `(value: number, index: number) => string` | No | — |
|
|
668
|
-
| `yTickFormat` | `
|
|
701
|
+
| `xTickFormat` | `NumberFormat \| ((value: number, index: number) => string)` | No | — |
|
|
702
|
+
| `yTickFormat` | `NumberFormat` | No | — |
|
|
669
703
|
| `xLabels` | `string[]` | No | — |
|
|
670
704
|
| `xGrid` | `boolean` | No | — |
|
|
671
705
|
| `yGrid` | `boolean` | No | — |
|