@squadbase/vantage 0.6.2-dev1.54bde93 → 0.6.2-dev1.5c79ef5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build-A23M4GCH.js → build-U7GO26HR.js} +3 -3
- package/dist/{build-A23M4GCH.js.map → build-U7GO26HR.js.map} +1 -1
- package/dist/{check-FBHT6W6J.js → check-MRVAIHG5.js} +3 -3
- package/dist/{check-FBHT6W6J.js.map → check-MRVAIHG5.js.map} +1 -1
- package/dist/{chunk-57GHR5LN.js → chunk-3RJEOP3J.js} +29 -18
- package/dist/chunk-3RJEOP3J.js.map +1 -0
- package/dist/{chunk-QOXXOFGL.js → chunk-77CISBMG.js} +994 -464
- package/dist/chunk-77CISBMG.js.map +1 -0
- package/dist/{chunk-JXFCSF3K.js → chunk-EG4PX726.js} +12 -7
- package/dist/chunk-EG4PX726.js.map +1 -0
- package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
- package/dist/chunk-NZ5QIIG4.js.map +1 -0
- package/dist/{chunk-5C2SEDPH.js → chunk-PJ4SPXHS.js} +4 -4
- package/dist/{chunk-5C2SEDPH.js.map → chunk-PJ4SPXHS.js.map} +1 -1
- package/dist/{chunk-7S52M2SL.js → chunk-QNOX7WLZ.js} +3 -3
- package/dist/{chunk-7S52M2SL.js.map → chunk-QNOX7WLZ.js.map} +1 -1
- package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
- package/dist/chunk-ROHNXP7J.js.map +1 -0
- package/dist/{chunk-6OTTGAPO.js → chunk-TRMHNMMZ.js} +24 -7
- package/dist/chunk-TRMHNMMZ.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/components/index.d.ts +49 -29
- package/dist/components/index.js +5 -5
- package/dist/{delta-text-CvwBGJ95.d.ts → delta-text-CbrOaW83.d.ts} +4 -2
- package/dist/{dev-H4I2JL63.js → dev-O42JFQ64.js} +3 -3
- package/dist/{dev-H4I2JL63.js.map → dev-O42JFQ64.js.map} +1 -1
- package/dist/doc/index.d.ts +14 -14
- package/dist/doc/index.js +198 -84
- package/dist/doc/index.js.map +1 -1
- package/dist/{doctor-B2O3GJDU.js → doctor-YD3XAAOT.js} +3 -3
- package/dist/{doctor-B2O3GJDU.js.map → doctor-YD3XAAOT.js.map} +1 -1
- package/dist/{format-U0cQl069.d.ts → format-DihQkL1N.d.ts} +17 -11
- package/dist/slide/index.d.ts +2 -2
- package/dist/slide/index.js +95 -44
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +6 -6
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +5 -5
- package/docs/en/agent-skills.md +1 -1
- package/docs/ja/agent-skills.md +1 -1
- package/package.json +2 -2
- package/registry/ui/app-shell.tsx +138 -1
- package/registry/ui/area-chart.tsx +344 -85
- package/registry/ui/attention-list.tsx +177 -15
- package/registry/ui/bar-chart.tsx +341 -85
- package/registry/ui/bar-list.tsx +332 -77
- package/registry/ui/box-plot-chart.tsx +405 -97
- package/registry/ui/category-bar.tsx +248 -40
- package/registry/ui/chart-frame.tsx +257 -46
- package/registry/ui/data-table.tsx +229 -37
- package/registry/ui/date-range-picker.tsx +190 -36
- package/registry/ui/delta.tsx +171 -12
- package/registry/ui/donut-chart.tsx +339 -84
- package/registry/ui/echart.tsx +142 -4
- package/registry/ui/filter-bar.tsx +179 -20
- package/registry/ui/funnel-steps.tsx +354 -80
- package/registry/ui/heatmap.tsx +293 -52
- package/registry/ui/histogram-chart.tsx +395 -97
- package/registry/ui/kpi-card.tsx +245 -44
- package/registry/ui/legend.tsx +330 -76
- package/registry/ui/line-chart.tsx +344 -85
- package/registry/ui/multi-select.tsx +159 -9
- package/registry/ui/page-shell.tsx +141 -3
- package/registry/ui/paired-chart.tsx +349 -90
- package/registry/ui/progress-circle.tsx +205 -39
- package/registry/ui/sample-data.tsx +153 -9
- package/registry/ui/scatter-chart.tsx +391 -100
- package/registry/ui/searchable-select.tsx +145 -7
- package/registry/ui/states.tsx +170 -13
- package/registry/ui/status-badge.tsx +193 -28
- package/registry/ui/tracker.tsx +205 -39
- package/registry/ui/trend-indicator.tsx +149 -6
- package/registry/ui/updated-at.tsx +170 -16
- package/registry/ui/waterfall-chart.tsx +415 -102
- package/skills/vantage-design/SKILL.md +25 -27
- package/skills/vantage-design/references/extract.md +10 -13
- package/skills/vantage-design/references/format.md +12 -173
- package/skills/vantage-design/references/presets.md +0 -10
- package/templates/AGENTS.md +29 -15
- package/templates/agents/page.md +1 -1
- package/templates/agents/slide.md +7 -1
- package/dist/chunk-2N72FKLB.js.map +0 -1
- package/dist/chunk-57GHR5LN.js.map +0 -1
- package/dist/chunk-6OTTGAPO.js.map +0 -1
- package/dist/chunk-JXFCSF3K.js.map +0 -1
- package/dist/chunk-PDOBHMUQ.js.map +0 -1
- package/dist/chunk-QOXXOFGL.js.map +0 -1
|
@@ -13,11 +13,143 @@ import { cn } from "@squadbase/vantage/ui"
|
|
|
13
13
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@squadbase/vantage/ui"
|
|
14
14
|
import type { ValueFormat } from "@squadbase/vantage/components"
|
|
15
15
|
import type { ChartColor, ValueFormatter } from "@squadbase/vantage/components"
|
|
16
|
+
import { useSyncExternalStore } from "react"
|
|
16
17
|
import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef, type RefObject } from "react"
|
|
17
18
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
18
|
-
import {
|
|
19
|
+
import { EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
|
|
19
20
|
import type { ChartColors, ChartSeriesColor } from "@squadbase/vantage/components"
|
|
20
21
|
|
|
22
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The display language of the parts and the defaults that follow it.
|
|
26
|
+
*
|
|
27
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
28
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
29
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
30
|
+
*
|
|
31
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
32
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
36
|
+
type UiLanguage = "ja" | "en"
|
|
37
|
+
|
|
38
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
39
|
+
const FALLBACK_LOCALE = "en-US"
|
|
40
|
+
|
|
41
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
42
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
43
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
47
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
48
|
+
const value = tag?.trim()
|
|
49
|
+
if (!value) return FALLBACK_LOCALE
|
|
50
|
+
try {
|
|
51
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
52
|
+
} catch {
|
|
53
|
+
return FALLBACK_LOCALE
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
59
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
60
|
+
*/
|
|
61
|
+
function documentLocale(): string {
|
|
62
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
63
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
67
|
+
function resolveLocale(locale?: string | null): string {
|
|
68
|
+
return locale || documentLocale()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Picks the wording table of the locale's language. */
|
|
72
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
73
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
77
|
+
function defaultCurrency(locale?: string | null): string {
|
|
78
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
82
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
83
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
87
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
88
|
+
|
|
89
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
90
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
94
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
95
|
+
* Monday for the rest.
|
|
96
|
+
*/
|
|
97
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
98
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
99
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
100
|
+
try {
|
|
101
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
102
|
+
getWeekInfo?: () => { firstDay: number }
|
|
103
|
+
weekInfo?: { firstDay: number }
|
|
104
|
+
}
|
|
105
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
106
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
107
|
+
} catch {
|
|
108
|
+
// Fall through to the table below.
|
|
109
|
+
}
|
|
110
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
114
|
+
|
|
115
|
+
const listeners = new Set<() => void>()
|
|
116
|
+
let observer: MutationObserver | null = null
|
|
117
|
+
|
|
118
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
119
|
+
function subscribe(onChange: () => void): () => void {
|
|
120
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
121
|
+
listeners.add(onChange)
|
|
122
|
+
if (!observer) {
|
|
123
|
+
observer = new MutationObserver(() => {
|
|
124
|
+
for (const listener of [...listeners]) listener()
|
|
125
|
+
})
|
|
126
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
127
|
+
}
|
|
128
|
+
return () => {
|
|
129
|
+
listeners.delete(onChange)
|
|
130
|
+
if (listeners.size === 0 && observer) {
|
|
131
|
+
observer.disconnect()
|
|
132
|
+
observer = null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
141
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
142
|
+
*/
|
|
143
|
+
function useDocumentLocale(): string {
|
|
144
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
148
|
+
function useLocale(locale?: string | null): string {
|
|
149
|
+
const page = useDocumentLocale()
|
|
150
|
+
return locale || page
|
|
151
|
+
}
|
|
152
|
+
|
|
21
153
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
22
154
|
|
|
23
155
|
/**
|
|
@@ -521,9 +653,10 @@ function resolveValueFormatter(
|
|
|
521
653
|
function resolveAxisFormatter(
|
|
522
654
|
valueFormatter: ValueFormatter | undefined,
|
|
523
655
|
format: ValueFormat | undefined,
|
|
524
|
-
|
|
656
|
+
localeArg?: string,
|
|
525
657
|
): ValueFormatter {
|
|
526
658
|
if (valueFormatter) return valueFormatter
|
|
659
|
+
const locale = resolveLocale(localeArg)
|
|
527
660
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
528
661
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
529
662
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -532,40 +665,75 @@ function resolveAxisFormatter(
|
|
|
532
665
|
return (value) => formatValue(value, short, locale)
|
|
533
666
|
}
|
|
534
667
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
{ char: "万", size: 1e4 },
|
|
540
|
-
] as const
|
|
668
|
+
interface CompactUnit {
|
|
669
|
+
token: string
|
|
670
|
+
size: number
|
|
671
|
+
}
|
|
541
672
|
|
|
542
|
-
const
|
|
673
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
677
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
678
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
679
|
+
*/
|
|
680
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
681
|
+
const cached = compactUnitCache.get(locale)
|
|
682
|
+
if (cached !== undefined) return cached
|
|
683
|
+
let units: CompactUnit[] | null = []
|
|
684
|
+
try {
|
|
685
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
686
|
+
const seen = new Set<string>()
|
|
687
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
688
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
689
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
690
|
+
if (at === -1) continue
|
|
691
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
692
|
+
units = null
|
|
693
|
+
break
|
|
694
|
+
}
|
|
695
|
+
const token = parts[at]!.value
|
|
696
|
+
if (seen.has(token)) continue
|
|
697
|
+
seen.add(token)
|
|
698
|
+
units.push({ token, size: 10 ** exp })
|
|
699
|
+
}
|
|
700
|
+
} catch {
|
|
701
|
+
units = null
|
|
702
|
+
}
|
|
703
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
704
|
+
compactUnitCache.set(locale, sorted)
|
|
705
|
+
return sorted
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
543
709
|
|
|
544
710
|
/**
|
|
545
711
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
546
|
-
* that largest tick, so
|
|
547
|
-
* Only the
|
|
548
|
-
* any other format —
|
|
712
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
713
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
714
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
715
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
549
716
|
*/
|
|
550
717
|
function resolveAxisTickFormatter(
|
|
551
718
|
valueFormatter: ValueFormatter | undefined,
|
|
552
719
|
format: ValueFormat | undefined,
|
|
553
|
-
|
|
720
|
+
localeArg?: string,
|
|
554
721
|
): (top: number) => ValueFormatter {
|
|
722
|
+
const locale = resolveLocale(localeArg)
|
|
555
723
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
556
724
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
if (!aligns) return () => base
|
|
725
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
726
|
+
if (!units) return () => base
|
|
560
727
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
728
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
561
729
|
return (top) => {
|
|
562
|
-
const unit =
|
|
730
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
563
731
|
if (!unit) return base
|
|
564
732
|
return (value) => {
|
|
565
733
|
const text = base(value)
|
|
566
734
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
567
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
568
|
-
return text.replace(
|
|
735
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
736
|
+
return text.replace(numberToken, () => scaled)
|
|
569
737
|
}
|
|
570
738
|
}
|
|
571
739
|
}
|
|
@@ -594,8 +762,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
594
762
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
595
763
|
const MAX_CHART_SERIES = 5
|
|
596
764
|
|
|
597
|
-
/** The series name the folded categories get. */
|
|
598
|
-
const
|
|
765
|
+
/** The series name the folded categories get, by language. */
|
|
766
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
767
|
+
|
|
768
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
769
|
+
function otherSeriesLabel(locale?: string): string {
|
|
770
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
774
|
+
|
|
775
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
776
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
777
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
778
|
+
}
|
|
599
779
|
|
|
600
780
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
601
781
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -615,7 +795,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
615
795
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
616
796
|
items: readonly Item[],
|
|
617
797
|
n: number = MAX_CHART_SERIES,
|
|
618
|
-
otherLabel: string =
|
|
798
|
+
otherLabel: string = otherSeriesLabel(),
|
|
619
799
|
): AggregatedItems<Item> {
|
|
620
800
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
621
801
|
const ranked = items
|
|
@@ -644,7 +824,7 @@ function aggregateSeries(
|
|
|
644
824
|
data: readonly Record<string, unknown>[],
|
|
645
825
|
categories: readonly string[],
|
|
646
826
|
n: number = MAX_CHART_SERIES,
|
|
647
|
-
otherLabel: string =
|
|
827
|
+
otherLabel: string = otherSeriesLabel(),
|
|
648
828
|
): AggregatedSeries {
|
|
649
829
|
const totals = categories.map((name) => ({
|
|
650
830
|
name,
|
|
@@ -676,12 +856,14 @@ function aggregateSeriesKeeping(
|
|
|
676
856
|
categories: readonly string[],
|
|
677
857
|
keep: string | undefined,
|
|
678
858
|
n: number = MAX_CHART_SERIES,
|
|
859
|
+
otherLabel: string = otherSeriesLabel(),
|
|
679
860
|
): AggregatedSeries {
|
|
680
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
861
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
681
862
|
const aggregated = aggregateSeries(
|
|
682
863
|
data,
|
|
683
864
|
categories.filter((c) => c !== keep),
|
|
684
865
|
n,
|
|
866
|
+
otherLabel,
|
|
685
867
|
)
|
|
686
868
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
687
869
|
}
|
|
@@ -717,7 +899,7 @@ function seriesColorsWithComparison(
|
|
|
717
899
|
* Colour per drawn series: the record's token for that category, else the
|
|
718
900
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
719
901
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
720
|
-
* data itself names
|
|
902
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
721
903
|
*/
|
|
722
904
|
function seriesColorsFor(
|
|
723
905
|
categories: readonly string[],
|
|
@@ -729,7 +911,7 @@ function seriesColorsFor(
|
|
|
729
911
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
730
912
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
731
913
|
return categories.map((category, i) => {
|
|
732
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
914
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
733
915
|
const fromRecord = record?.[category]
|
|
734
916
|
if (fromRecord) return fromRecord
|
|
735
917
|
if (list && list.length > 0) {
|
|
@@ -776,7 +958,7 @@ interface DonutSlicesConfig {
|
|
|
776
958
|
*/
|
|
777
959
|
function donutSlices(
|
|
778
960
|
data: readonly Record<string, unknown>[],
|
|
779
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
961
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
780
962
|
): DonutSlice[] {
|
|
781
963
|
const items = data
|
|
782
964
|
.map((row, i) => {
|
|
@@ -789,7 +971,8 @@ function donutSlices(
|
|
|
789
971
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
790
972
|
name: item.name,
|
|
791
973
|
value: item.value,
|
|
792
|
-
|
|
974
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
975
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
793
976
|
isOther: false,
|
|
794
977
|
row: item.row,
|
|
795
978
|
}))
|
|
@@ -862,7 +1045,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
862
1045
|
*/
|
|
863
1046
|
function barListRows<Row extends object>(
|
|
864
1047
|
data: readonly Row[],
|
|
865
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
1048
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
866
1049
|
): BarListRow<Row>[] {
|
|
867
1050
|
const items = data.map((row): BarListRow<Row> => {
|
|
868
1051
|
const cells = row as Record<string, unknown>
|
|
@@ -908,19 +1091,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
908
1091
|
|
|
909
1092
|
/**
|
|
910
1093
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
911
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
1094
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
912
1095
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
913
1096
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
914
1097
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
915
1098
|
* keeps its contrast.
|
|
916
1099
|
*/
|
|
917
1100
|
function barListFills(
|
|
918
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
1101
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
919
1102
|
color: ChartColor,
|
|
920
1103
|
otherColor: string,
|
|
921
1104
|
): string[] {
|
|
922
1105
|
return rows.map((row) =>
|
|
923
|
-
row.isOther
|
|
1106
|
+
row.isOther || isOtherName(row.name)
|
|
1107
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
1108
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
924
1109
|
)
|
|
925
1110
|
}
|
|
926
1111
|
|
|
@@ -1136,11 +1321,30 @@ interface ChartSummaryInput {
|
|
|
1136
1321
|
format?: ValueFormat
|
|
1137
1322
|
/** Wins over `format`. */
|
|
1138
1323
|
valueFormatter?: ValueFormatter
|
|
1324
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
1325
|
+
locale?: string
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const SUMMARY_TEXT = {
|
|
1329
|
+
ja: {
|
|
1330
|
+
up: "、前の点より増加",
|
|
1331
|
+
down: "、前の点より減少",
|
|
1332
|
+
flat: "、前の点と横ばい",
|
|
1333
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
1334
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
1335
|
+
},
|
|
1336
|
+
en: {
|
|
1337
|
+
up: ", up from the previous point",
|
|
1338
|
+
down: ", down from the previous point",
|
|
1339
|
+
flat: ", flat from the previous point",
|
|
1340
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
1341
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
1342
|
+
},
|
|
1139
1343
|
}
|
|
1140
1344
|
|
|
1141
1345
|
/**
|
|
1142
1346
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
1143
|
-
* 「<title>: 最新 <index> は
|
|
1347
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
1144
1348
|
* computed from the last two non-empty points of the first series — never
|
|
1145
1349
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
1146
1350
|
* no usable point.
|
|
@@ -1156,26 +1360,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
1156
1360
|
}
|
|
1157
1361
|
const latest = points[points.length - 1]
|
|
1158
1362
|
if (!latest) return undefined
|
|
1159
|
-
const
|
|
1363
|
+
const locale = resolveLocale(input.locale)
|
|
1364
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
1365
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
1160
1366
|
const prev = points[points.length - 2]
|
|
1161
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
1367
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
1162
1368
|
const head = input.title ? `${input.title}: ` : ""
|
|
1163
|
-
return `${head}
|
|
1369
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
1164
1370
|
}
|
|
1165
1371
|
|
|
1166
1372
|
/**
|
|
1167
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
1168
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
1373
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
1374
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
1169
1375
|
*/
|
|
1170
1376
|
function buildShareSummary(
|
|
1171
1377
|
slices: readonly { name: string; value: number }[],
|
|
1172
1378
|
title?: string,
|
|
1379
|
+
locale?: string,
|
|
1173
1380
|
): string | undefined {
|
|
1174
1381
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
1175
1382
|
const top = slices[0]
|
|
1176
1383
|
if (!top || total <= 0) return undefined
|
|
1177
1384
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
1178
|
-
return `${title ? `${title}: ` : ""}
|
|
1385
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
1179
1386
|
}
|
|
1180
1387
|
|
|
1181
1388
|
/* ---------- end of inlined code ---------- */
|
|
@@ -1215,7 +1422,8 @@ export const CategoryBar = forwardRef<HTMLDivElement, CategoryBarProps>(function
|
|
|
1215
1422
|
) {
|
|
1216
1423
|
// The bar's width thins the running totals that would print over each other.
|
|
1217
1424
|
const { ref, width } = useChartSurface(forwardedRef, null)
|
|
1218
|
-
const
|
|
1425
|
+
const locale = useLocale()
|
|
1426
|
+
const valueFormatter = useMemo(() => resolveAxisFormatter(valueFormatterProp, format, locale), [valueFormatterProp, format, locale])
|
|
1219
1427
|
const segments = categoryBarSegments(values)
|
|
1220
1428
|
const markerPosition = marker ? categoryBarMarkerPosition(marker.value, values) : 0
|
|
1221
1429
|
const allLabels = showLabels ? categoryBarLabels(values, undefined, marker ? markerPosition : undefined) : []
|