@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, watch, computed, onMounted, getCurrentInstance } from "vue";
|
|
3
3
|
import { SliderRoot, SliderTrack, SliderRange, SliderThumb } from "reka-ui";
|
|
4
|
+
import { formatNumber, type NumberFormat } from "@cfasim-ui/shared";
|
|
4
5
|
import Hint from "../Hint/Hint.vue";
|
|
5
6
|
|
|
6
7
|
export type NumberRange = [number, number];
|
|
@@ -29,9 +30,17 @@ const props = defineProps<{
|
|
|
29
30
|
numberType?: "integer" | "float";
|
|
30
31
|
required?: boolean;
|
|
31
32
|
decimals?: number;
|
|
32
|
-
// Custom formatter for
|
|
33
|
-
//
|
|
34
|
-
//
|
|
33
|
+
// Custom formatter for the displayed value. Accepts a NumberFormat
|
|
34
|
+
// (preset name, printf string, or function) — see `formatNumber` in
|
|
35
|
+
// `@cfasim-ui/shared`. When set, overrides the default percent/decimal
|
|
36
|
+
// formatting for both the text input value and slider thumb/min/max
|
|
37
|
+
// labels. The model stays a raw number; only the display changes. Note
|
|
38
|
+
// that formats which add suffixes or scale the value (e.g.
|
|
39
|
+
// `"percent:1"`) may not round-trip through the text input — use
|
|
40
|
+
// `percent: true` for value scaling and `format` for display shaping.
|
|
41
|
+
format?: NumberFormat;
|
|
42
|
+
/** @deprecated Use `format` instead. Still honored for slider labels
|
|
43
|
+
* when `format` is unset, but will be removed in a future release. */
|
|
35
44
|
sliderDisplay?: (value: number) => string;
|
|
36
45
|
}>();
|
|
37
46
|
|
|
@@ -113,7 +122,11 @@ function roundToDecimals(v: number, d: number): number {
|
|
|
113
122
|
|
|
114
123
|
function formatSliderValue(v: number | undefined) {
|
|
115
124
|
if (v == null) return "";
|
|
116
|
-
|
|
125
|
+
// sliderDisplay (deprecated) is a function — i.e. already a valid
|
|
126
|
+
// NumberFormat — so route it through formatNumber. `format` wins when
|
|
127
|
+
// both are set.
|
|
128
|
+
const fmt = props.format ?? props.sliderDisplay;
|
|
129
|
+
if (fmt !== undefined) return formatNumber(v, fmt);
|
|
117
130
|
const d = displayDecimals.value;
|
|
118
131
|
if (props.percent) return (v * 100).toFixed(d) + "%";
|
|
119
132
|
return v.toLocaleString("en-US", {
|
|
@@ -149,6 +162,7 @@ function formatWithCommas(v: number | undefined): string {
|
|
|
149
162
|
|
|
150
163
|
function formatForDisplay(v: number | undefined): string {
|
|
151
164
|
if (v == null) return "";
|
|
165
|
+
if (props.format !== undefined) return formatNumber(v, props.format);
|
|
152
166
|
const d = displayDecimals.value;
|
|
153
167
|
if (d > 0) {
|
|
154
168
|
return v.toLocaleString("en-US", {
|
package/index.json
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { sprintf } from "sprintf-js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Named number-format presets, modelled on Streamlit's number column formats.
|
|
5
|
+
* - `plain` — `String(value)` (no grouping)
|
|
6
|
+
* - `localized` — `Intl.NumberFormat()` with the user's locale
|
|
7
|
+
* - `percent` — formats as a percent (value `0.5` → `"50%"`)
|
|
8
|
+
* - `compact` — short form (`1.2K`, `3.4M`)
|
|
9
|
+
* - `scientific` — scientific notation (`1.23E4`)
|
|
10
|
+
* - `engineering` — engineering notation (powers of 1000)
|
|
11
|
+
*
|
|
12
|
+
* Presets preserve the raw value's precision by default (no rounding).
|
|
13
|
+
* Append `:N` (a digit) to fix the number of fractional digits, e.g.
|
|
14
|
+
* `"percent:1"` → `"12.3%"`, `"localized:2"` → `"1,234.50"`.
|
|
15
|
+
*/
|
|
16
|
+
export type NumberFormatPreset =
|
|
17
|
+
| "plain"
|
|
18
|
+
| "localized"
|
|
19
|
+
| "percent"
|
|
20
|
+
| "compact"
|
|
21
|
+
| "scientific"
|
|
22
|
+
| "engineering";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A number format specifier:
|
|
26
|
+
* - A {@link NumberFormatPreset} name, optionally with `:N` digits suffix
|
|
27
|
+
* (e.g. `"percent:1"`, `"compact:2"`)
|
|
28
|
+
* - A printf-style format string (must contain a `%` placeholder, parsed
|
|
29
|
+
* by sprintf-js — e.g. `"%.2f"`, `"%05d"`)
|
|
30
|
+
* - A function `(value) => string` for full control
|
|
31
|
+
*
|
|
32
|
+
* Strings that contain no `%` and don't match a preset throw at format
|
|
33
|
+
* time, so typos surface immediately instead of silently rendering wrong.
|
|
34
|
+
*/
|
|
35
|
+
export type NumberFormat =
|
|
36
|
+
| NumberFormatPreset
|
|
37
|
+
| string
|
|
38
|
+
| ((value: number) => string);
|
|
39
|
+
|
|
40
|
+
const PRESET_NAMES = new Set<NumberFormatPreset>([
|
|
41
|
+
"plain",
|
|
42
|
+
"localized",
|
|
43
|
+
"percent",
|
|
44
|
+
"compact",
|
|
45
|
+
"scientific",
|
|
46
|
+
"engineering",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function isPreset(s: string): s is NumberFormatPreset {
|
|
50
|
+
return PRESET_NAMES.has(s as NumberFormatPreset);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse `"preset"` or `"preset:N"` into a name and optional digit count.
|
|
55
|
+
* Returns null if the string isn't a recognized preset.
|
|
56
|
+
*/
|
|
57
|
+
function parsePreset(
|
|
58
|
+
s: string,
|
|
59
|
+
): { preset: NumberFormatPreset; digits: number | undefined } | null {
|
|
60
|
+
const colon = s.indexOf(":");
|
|
61
|
+
if (colon === -1) {
|
|
62
|
+
return isPreset(s) ? { preset: s, digits: undefined } : null;
|
|
63
|
+
}
|
|
64
|
+
const name = s.slice(0, colon);
|
|
65
|
+
const rest = s.slice(colon + 1);
|
|
66
|
+
if (!isPreset(name)) return null;
|
|
67
|
+
// Digits must be a non-negative integer (matches Intl's 0..100 range).
|
|
68
|
+
if (!/^\d+$/.test(rest)) return null;
|
|
69
|
+
const digits = Number(rest);
|
|
70
|
+
if (digits > 100) return null;
|
|
71
|
+
return { preset: name, digits };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Covers JS double-precision (≤17 significant digits). When the caller
|
|
75
|
+
// hasn't asked for a specific digit count, we use this to preserve the
|
|
76
|
+
// raw value rather than letting Intl round to its default precision.
|
|
77
|
+
const RAW_PRECISION_DIGITS = 20;
|
|
78
|
+
|
|
79
|
+
function formatPreset(
|
|
80
|
+
value: number,
|
|
81
|
+
preset: NumberFormatPreset,
|
|
82
|
+
digits: number | undefined,
|
|
83
|
+
): string {
|
|
84
|
+
const fractionOpts: Intl.NumberFormatOptions =
|
|
85
|
+
digits !== undefined
|
|
86
|
+
? { minimumFractionDigits: digits, maximumFractionDigits: digits }
|
|
87
|
+
: { maximumFractionDigits: RAW_PRECISION_DIGITS };
|
|
88
|
+
switch (preset) {
|
|
89
|
+
case "plain":
|
|
90
|
+
return digits !== undefined ? value.toFixed(digits) : String(value);
|
|
91
|
+
case "localized":
|
|
92
|
+
return new Intl.NumberFormat(undefined, fractionOpts).format(value);
|
|
93
|
+
case "percent":
|
|
94
|
+
return new Intl.NumberFormat(undefined, {
|
|
95
|
+
style: "percent",
|
|
96
|
+
...fractionOpts,
|
|
97
|
+
}).format(value);
|
|
98
|
+
case "compact":
|
|
99
|
+
return new Intl.NumberFormat(undefined, {
|
|
100
|
+
notation: "compact",
|
|
101
|
+
...fractionOpts,
|
|
102
|
+
}).format(value);
|
|
103
|
+
case "scientific":
|
|
104
|
+
return new Intl.NumberFormat(undefined, {
|
|
105
|
+
notation: "scientific",
|
|
106
|
+
...fractionOpts,
|
|
107
|
+
}).format(value);
|
|
108
|
+
case "engineering":
|
|
109
|
+
return new Intl.NumberFormat(undefined, {
|
|
110
|
+
notation: "engineering",
|
|
111
|
+
...fractionOpts,
|
|
112
|
+
}).format(value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Format a number using a preset name, a printf-style format string, or a
|
|
118
|
+
* custom function. Non-finite values (NaN, ±Infinity) are returned as
|
|
119
|
+
* `String(value)`; if `format` is omitted, falls back to `String(value)`.
|
|
120
|
+
*
|
|
121
|
+
* Throws if `format` is a string that's neither a recognized preset (with
|
|
122
|
+
* an optional `:N` digit suffix) nor a printf string containing `%`.
|
|
123
|
+
*/
|
|
124
|
+
export function formatNumber(value: number, format?: NumberFormat): string {
|
|
125
|
+
if (!Number.isFinite(value)) return String(value);
|
|
126
|
+
if (format === undefined) return String(value);
|
|
127
|
+
if (typeof format === "function") return format(value);
|
|
128
|
+
// printf strings always contain a `%` placeholder; everything else must
|
|
129
|
+
// be a recognized preset (with an optional `:N` digits suffix).
|
|
130
|
+
if (format.includes("%")) return sprintf(format, value);
|
|
131
|
+
const parsed = parsePreset(format);
|
|
132
|
+
if (!parsed) {
|
|
133
|
+
const names = [...PRESET_NAMES].join(", ");
|
|
134
|
+
throw new Error(
|
|
135
|
+
`formatNumber: invalid format ${JSON.stringify(format)}. ` +
|
|
136
|
+
`Expected one of ${names} (optionally with ":N" digits), ` +
|
|
137
|
+
`a printf format string containing "%", or a function.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return formatPreset(value, parsed.preset, parsed.digits);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** True if `f` is a recognized {@link NumberFormat} value. */
|
|
144
|
+
export function isNumberFormat(f: unknown): f is NumberFormat {
|
|
145
|
+
return typeof f === "string" || typeof f === "function";
|
|
146
|
+
}
|
package/shared/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type {
|
|
|
14
14
|
ModelOutputsWire,
|
|
15
15
|
} from "./ModelOutput.js";
|
|
16
16
|
export { modelOutputToCSV } from "./csv.js";
|
|
17
|
+
export { formatNumber, isNumberFormat } from "./formatNumber.js";
|
|
18
|
+
export type { NumberFormat, NumberFormatPreset } from "./formatNumber.js";
|
|
17
19
|
export {
|
|
18
20
|
useUrlParams,
|
|
19
21
|
serialize,
|