@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
|
@@ -22,14 +22,147 @@ import { forwardRef, useEffect, useMemo, useState, type ForwardedRef, type React
|
|
|
22
22
|
import { ChartFrame } from "@squadbase/vantage/components"
|
|
23
23
|
import { EChart } from "@squadbase/vantage/components"
|
|
24
24
|
import type { ChartCommonProps, ChartEventPayload, ChartSeriesColor } from "@squadbase/vantage/components"
|
|
25
|
+
import { useSyncExternalStore } from "react"
|
|
25
26
|
import { createContext, useCallback, useRef, type RefObject } from "react"
|
|
26
27
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
27
|
-
import {
|
|
28
|
+
import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
28
29
|
import type { ChartColor, ChartColors, ValueFormatter } from "@squadbase/vantage/components"
|
|
29
30
|
import type { EChartsOption } from "@squadbase/vantage/components"
|
|
30
31
|
import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
|
|
32
|
+
import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
|
|
31
33
|
import type { ChartThreshold, ChartAnnotation, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
|
|
32
34
|
|
|
35
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The display language of the parts and the defaults that follow it.
|
|
39
|
+
*
|
|
40
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
41
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
42
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
43
|
+
*
|
|
44
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
45
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
49
|
+
type UiLanguage = "ja" | "en"
|
|
50
|
+
|
|
51
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
52
|
+
const FALLBACK_LOCALE = "en-US"
|
|
53
|
+
|
|
54
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
55
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
56
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
60
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
61
|
+
const value = tag?.trim()
|
|
62
|
+
if (!value) return FALLBACK_LOCALE
|
|
63
|
+
try {
|
|
64
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
65
|
+
} catch {
|
|
66
|
+
return FALLBACK_LOCALE
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
72
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
73
|
+
*/
|
|
74
|
+
function documentLocale(): string {
|
|
75
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
76
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
80
|
+
function resolveLocale(locale?: string | null): string {
|
|
81
|
+
return locale || documentLocale()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Picks the wording table of the locale's language. */
|
|
85
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
86
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
90
|
+
function defaultCurrency(locale?: string | null): string {
|
|
91
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
95
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
96
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
100
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
101
|
+
|
|
102
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
103
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
107
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
108
|
+
* Monday for the rest.
|
|
109
|
+
*/
|
|
110
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
111
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
112
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
113
|
+
try {
|
|
114
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
115
|
+
getWeekInfo?: () => { firstDay: number }
|
|
116
|
+
weekInfo?: { firstDay: number }
|
|
117
|
+
}
|
|
118
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
119
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
120
|
+
} catch {
|
|
121
|
+
// Fall through to the table below.
|
|
122
|
+
}
|
|
123
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
127
|
+
|
|
128
|
+
const listeners = new Set<() => void>()
|
|
129
|
+
let observer: MutationObserver | null = null
|
|
130
|
+
|
|
131
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
132
|
+
function subscribe(onChange: () => void): () => void {
|
|
133
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
134
|
+
listeners.add(onChange)
|
|
135
|
+
if (!observer) {
|
|
136
|
+
observer = new MutationObserver(() => {
|
|
137
|
+
for (const listener of [...listeners]) listener()
|
|
138
|
+
})
|
|
139
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
140
|
+
}
|
|
141
|
+
return () => {
|
|
142
|
+
listeners.delete(onChange)
|
|
143
|
+
if (listeners.size === 0 && observer) {
|
|
144
|
+
observer.disconnect()
|
|
145
|
+
observer = null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
154
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
155
|
+
*/
|
|
156
|
+
function useDocumentLocale(): string {
|
|
157
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
161
|
+
function useLocale(locale?: string | null): string {
|
|
162
|
+
const page = useDocumentLocale()
|
|
163
|
+
return locale || page
|
|
164
|
+
}
|
|
165
|
+
|
|
33
166
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
34
167
|
|
|
35
168
|
/**
|
|
@@ -533,9 +666,10 @@ function resolveValueFormatter(
|
|
|
533
666
|
function resolveAxisFormatter(
|
|
534
667
|
valueFormatter: ValueFormatter | undefined,
|
|
535
668
|
format: ValueFormat | undefined,
|
|
536
|
-
|
|
669
|
+
localeArg?: string,
|
|
537
670
|
): ValueFormatter {
|
|
538
671
|
if (valueFormatter) return valueFormatter
|
|
672
|
+
const locale = resolveLocale(localeArg)
|
|
539
673
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
540
674
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
541
675
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -544,40 +678,75 @@ function resolveAxisFormatter(
|
|
|
544
678
|
return (value) => formatValue(value, short, locale)
|
|
545
679
|
}
|
|
546
680
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
{ char: "万", size: 1e4 },
|
|
552
|
-
] as const
|
|
681
|
+
interface CompactUnit {
|
|
682
|
+
token: string
|
|
683
|
+
size: number
|
|
684
|
+
}
|
|
553
685
|
|
|
554
|
-
const
|
|
686
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
690
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
691
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
692
|
+
*/
|
|
693
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
694
|
+
const cached = compactUnitCache.get(locale)
|
|
695
|
+
if (cached !== undefined) return cached
|
|
696
|
+
let units: CompactUnit[] | null = []
|
|
697
|
+
try {
|
|
698
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
699
|
+
const seen = new Set<string>()
|
|
700
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
701
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
702
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
703
|
+
if (at === -1) continue
|
|
704
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
705
|
+
units = null
|
|
706
|
+
break
|
|
707
|
+
}
|
|
708
|
+
const token = parts[at]!.value
|
|
709
|
+
if (seen.has(token)) continue
|
|
710
|
+
seen.add(token)
|
|
711
|
+
units.push({ token, size: 10 ** exp })
|
|
712
|
+
}
|
|
713
|
+
} catch {
|
|
714
|
+
units = null
|
|
715
|
+
}
|
|
716
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
717
|
+
compactUnitCache.set(locale, sorted)
|
|
718
|
+
return sorted
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
555
722
|
|
|
556
723
|
/**
|
|
557
724
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
558
|
-
* that largest tick, so
|
|
559
|
-
* Only the
|
|
560
|
-
* any other format —
|
|
725
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
726
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
727
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
728
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
561
729
|
*/
|
|
562
730
|
function resolveAxisTickFormatter(
|
|
563
731
|
valueFormatter: ValueFormatter | undefined,
|
|
564
732
|
format: ValueFormat | undefined,
|
|
565
|
-
|
|
733
|
+
localeArg?: string,
|
|
566
734
|
): (top: number) => ValueFormatter {
|
|
735
|
+
const locale = resolveLocale(localeArg)
|
|
567
736
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
568
737
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
if (!aligns) return () => base
|
|
738
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
739
|
+
if (!units) return () => base
|
|
572
740
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
741
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
573
742
|
return (top) => {
|
|
574
|
-
const unit =
|
|
743
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
575
744
|
if (!unit) return base
|
|
576
745
|
return (value) => {
|
|
577
746
|
const text = base(value)
|
|
578
747
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
579
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
580
|
-
return text.replace(
|
|
748
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
749
|
+
return text.replace(numberToken, () => scaled)
|
|
581
750
|
}
|
|
582
751
|
}
|
|
583
752
|
}
|
|
@@ -606,8 +775,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
606
775
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
607
776
|
const MAX_CHART_SERIES = 5
|
|
608
777
|
|
|
609
|
-
/** The series name the folded categories get. */
|
|
610
|
-
const
|
|
778
|
+
/** The series name the folded categories get, by language. */
|
|
779
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
780
|
+
|
|
781
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
782
|
+
function otherSeriesLabel(locale?: string): string {
|
|
783
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
787
|
+
|
|
788
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
789
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
790
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
791
|
+
}
|
|
611
792
|
|
|
612
793
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
613
794
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -627,7 +808,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
627
808
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
628
809
|
items: readonly Item[],
|
|
629
810
|
n: number = MAX_CHART_SERIES,
|
|
630
|
-
otherLabel: string =
|
|
811
|
+
otherLabel: string = otherSeriesLabel(),
|
|
631
812
|
): AggregatedItems<Item> {
|
|
632
813
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
633
814
|
const ranked = items
|
|
@@ -656,7 +837,7 @@ function aggregateSeries(
|
|
|
656
837
|
data: readonly Record<string, unknown>[],
|
|
657
838
|
categories: readonly string[],
|
|
658
839
|
n: number = MAX_CHART_SERIES,
|
|
659
|
-
otherLabel: string =
|
|
840
|
+
otherLabel: string = otherSeriesLabel(),
|
|
660
841
|
): AggregatedSeries {
|
|
661
842
|
const totals = categories.map((name) => ({
|
|
662
843
|
name,
|
|
@@ -688,12 +869,14 @@ function aggregateSeriesKeeping(
|
|
|
688
869
|
categories: readonly string[],
|
|
689
870
|
keep: string | undefined,
|
|
690
871
|
n: number = MAX_CHART_SERIES,
|
|
872
|
+
otherLabel: string = otherSeriesLabel(),
|
|
691
873
|
): AggregatedSeries {
|
|
692
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
874
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
693
875
|
const aggregated = aggregateSeries(
|
|
694
876
|
data,
|
|
695
877
|
categories.filter((c) => c !== keep),
|
|
696
878
|
n,
|
|
879
|
+
otherLabel,
|
|
697
880
|
)
|
|
698
881
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
699
882
|
}
|
|
@@ -729,7 +912,7 @@ function seriesColorsWithComparison(
|
|
|
729
912
|
* Colour per drawn series: the record's token for that category, else the
|
|
730
913
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
731
914
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
732
|
-
* data itself names
|
|
915
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
733
916
|
*/
|
|
734
917
|
function seriesColorsFor(
|
|
735
918
|
categories: readonly string[],
|
|
@@ -741,7 +924,7 @@ function seriesColorsFor(
|
|
|
741
924
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
742
925
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
743
926
|
return categories.map((category, i) => {
|
|
744
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
927
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
745
928
|
const fromRecord = record?.[category]
|
|
746
929
|
if (fromRecord) return fromRecord
|
|
747
930
|
if (list && list.length > 0) {
|
|
@@ -788,7 +971,7 @@ interface DonutSlicesConfig {
|
|
|
788
971
|
*/
|
|
789
972
|
function donutSlices(
|
|
790
973
|
data: readonly Record<string, unknown>[],
|
|
791
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
974
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
792
975
|
): DonutSlice[] {
|
|
793
976
|
const items = data
|
|
794
977
|
.map((row, i) => {
|
|
@@ -801,7 +984,8 @@ function donutSlices(
|
|
|
801
984
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
802
985
|
name: item.name,
|
|
803
986
|
value: item.value,
|
|
804
|
-
|
|
987
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
988
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
805
989
|
isOther: false,
|
|
806
990
|
row: item.row,
|
|
807
991
|
}))
|
|
@@ -874,7 +1058,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
874
1058
|
*/
|
|
875
1059
|
function barListRows<Row extends object>(
|
|
876
1060
|
data: readonly Row[],
|
|
877
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
1061
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
878
1062
|
): BarListRow<Row>[] {
|
|
879
1063
|
const items = data.map((row): BarListRow<Row> => {
|
|
880
1064
|
const cells = row as Record<string, unknown>
|
|
@@ -920,19 +1104,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
920
1104
|
|
|
921
1105
|
/**
|
|
922
1106
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
923
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
1107
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
924
1108
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
925
1109
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
926
1110
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
927
1111
|
* keeps its contrast.
|
|
928
1112
|
*/
|
|
929
1113
|
function barListFills(
|
|
930
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
1114
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
931
1115
|
color: ChartColor,
|
|
932
1116
|
otherColor: string,
|
|
933
1117
|
): string[] {
|
|
934
1118
|
return rows.map((row) =>
|
|
935
|
-
row.isOther
|
|
1119
|
+
row.isOther || isOtherName(row.name)
|
|
1120
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
1121
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
936
1122
|
)
|
|
937
1123
|
}
|
|
938
1124
|
|
|
@@ -1148,11 +1334,30 @@ interface ChartSummaryInput {
|
|
|
1148
1334
|
format?: ValueFormat
|
|
1149
1335
|
/** Wins over `format`. */
|
|
1150
1336
|
valueFormatter?: ValueFormatter
|
|
1337
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
1338
|
+
locale?: string
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const SUMMARY_TEXT = {
|
|
1342
|
+
ja: {
|
|
1343
|
+
up: "、前の点より増加",
|
|
1344
|
+
down: "、前の点より減少",
|
|
1345
|
+
flat: "、前の点と横ばい",
|
|
1346
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
1347
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
1348
|
+
},
|
|
1349
|
+
en: {
|
|
1350
|
+
up: ", up from the previous point",
|
|
1351
|
+
down: ", down from the previous point",
|
|
1352
|
+
flat: ", flat from the previous point",
|
|
1353
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
1354
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
1355
|
+
},
|
|
1151
1356
|
}
|
|
1152
1357
|
|
|
1153
1358
|
/**
|
|
1154
1359
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
1155
|
-
* 「<title>: 最新 <index> は
|
|
1360
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
1156
1361
|
* computed from the last two non-empty points of the first series — never
|
|
1157
1362
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
1158
1363
|
* no usable point.
|
|
@@ -1168,30 +1373,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
1168
1373
|
}
|
|
1169
1374
|
const latest = points[points.length - 1]
|
|
1170
1375
|
if (!latest) return undefined
|
|
1171
|
-
const
|
|
1376
|
+
const locale = resolveLocale(input.locale)
|
|
1377
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
1378
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
1172
1379
|
const prev = points[points.length - 2]
|
|
1173
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
1380
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
1174
1381
|
const head = input.title ? `${input.title}: ` : ""
|
|
1175
|
-
return `${head}
|
|
1382
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
1176
1383
|
}
|
|
1177
1384
|
|
|
1178
1385
|
/**
|
|
1179
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
1180
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
1386
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
1387
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
1181
1388
|
*/
|
|
1182
1389
|
function buildShareSummary(
|
|
1183
1390
|
slices: readonly { name: string; value: number }[],
|
|
1184
1391
|
title?: string,
|
|
1392
|
+
locale?: string,
|
|
1185
1393
|
): string | undefined {
|
|
1186
1394
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
1187
1395
|
const top = slices[0]
|
|
1188
1396
|
if (!top || total <= 0) return undefined
|
|
1189
1397
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
1190
|
-
return `${title ? `${title}: ` : ""}
|
|
1398
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
1191
1399
|
}
|
|
1192
1400
|
|
|
1193
1401
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
|
|
1194
1402
|
|
|
1403
|
+
/** The built-in words of the option builders, by language. */
|
|
1404
|
+
const CHART_TEXT = {
|
|
1405
|
+
ja: {
|
|
1406
|
+
target: "目標",
|
|
1407
|
+
forecast: "予測",
|
|
1408
|
+
total: "合計",
|
|
1409
|
+
provisional: "速報",
|
|
1410
|
+
noComparison: "比較なし",
|
|
1411
|
+
share: (share: string) => `(${share})`,
|
|
1412
|
+
targetRate: (rate: string) => `(達成率 ${rate})`,
|
|
1413
|
+
provisionalTitle: (title: string, label: string) => `${title}(${label})`,
|
|
1414
|
+
},
|
|
1415
|
+
en: {
|
|
1416
|
+
target: "Target",
|
|
1417
|
+
forecast: "Forecast",
|
|
1418
|
+
total: "Total",
|
|
1419
|
+
provisional: "Preliminary",
|
|
1420
|
+
noComparison: "No comparison",
|
|
1421
|
+
share: (share: string) => ` (${share})`,
|
|
1422
|
+
targetRate: (rate: string) => ` (${rate} achieved)`,
|
|
1423
|
+
provisionalTitle: (title: string, label: string) => `${title} (${label})`,
|
|
1424
|
+
},
|
|
1425
|
+
} satisfies Record<UiLanguage, unknown>
|
|
1426
|
+
|
|
1427
|
+
const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
|
|
1428
|
+
|
|
1195
1429
|
type CartesianChartKind = "area" | "bar" | "line"
|
|
1196
1430
|
type CartesianStackType = "default" | "stacked" | "percent"
|
|
1197
1431
|
type CartesianCurveType = "linear" | "smooth" | "step"
|
|
@@ -1376,7 +1610,7 @@ interface CartesianChartConfig {
|
|
|
1376
1610
|
* all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
|
|
1377
1611
|
*/
|
|
1378
1612
|
axisTicks?: (top: number) => (value: number) => string
|
|
1379
|
-
/** Locale of the time-axis labels. Default
|
|
1613
|
+
/** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
|
|
1380
1614
|
locale?: string
|
|
1381
1615
|
type?: CartesianStackType
|
|
1382
1616
|
/** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
|
|
@@ -1614,11 +1848,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
|
|
|
1614
1848
|
*/
|
|
1615
1849
|
function timeAxisLabels(
|
|
1616
1850
|
values: readonly unknown[],
|
|
1617
|
-
|
|
1851
|
+
localeArg?: string,
|
|
1618
1852
|
shown: (i: number) => boolean = () => true,
|
|
1619
1853
|
): string[] | null {
|
|
1620
1854
|
const parsed = parseTimeIndex(values)
|
|
1621
1855
|
if (!parsed) return null
|
|
1856
|
+
const locale = resolveLocale(localeArg)
|
|
1622
1857
|
const { granularity, times } = parsed
|
|
1623
1858
|
let prevYear: number | null = null
|
|
1624
1859
|
return times.map((t, i) => {
|
|
@@ -1641,9 +1876,10 @@ function timeAxisLabels(
|
|
|
1641
1876
|
}
|
|
1642
1877
|
|
|
1643
1878
|
/** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
|
|
1644
|
-
function timeTooltipLabels(values: readonly unknown[],
|
|
1879
|
+
function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
|
|
1645
1880
|
const parsed = parseTimeIndex(values)
|
|
1646
1881
|
if (!parsed) return null
|
|
1882
|
+
const locale = resolveLocale(localeArg)
|
|
1647
1883
|
const options: Intl.DateTimeFormatOptions =
|
|
1648
1884
|
parsed.granularity === "year"
|
|
1649
1885
|
? { year: "numeric" }
|
|
@@ -1783,7 +2019,9 @@ function tooltipHtml(
|
|
|
1783
2019
|
rows: readonly TooltipRow[],
|
|
1784
2020
|
valueFormatter: (value: number) => string,
|
|
1785
2021
|
notes: readonly string[] = [],
|
|
2022
|
+
locale?: string,
|
|
1786
2023
|
): string {
|
|
2024
|
+
const t = chartText(locale)
|
|
1787
2025
|
const lines = rows.map(
|
|
1788
2026
|
(row) =>
|
|
1789
2027
|
`<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
|
|
@@ -1792,7 +2030,7 @@ function tooltipHtml(
|
|
|
1792
2030
|
`<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
|
|
1793
2031
|
row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
|
|
1794
2032
|
}${
|
|
1795
|
-
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px"
|
|
2033
|
+
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
|
|
1796
2034
|
}</span></div>` +
|
|
1797
2035
|
(row.note === undefined
|
|
1798
2036
|
? ""
|
|
@@ -1806,8 +2044,10 @@ function tooltipHtml(
|
|
|
1806
2044
|
|
|
1807
2045
|
/* ------------------------------------------------- per-bar targets / notes */
|
|
1808
2046
|
|
|
1809
|
-
/** Default name of the per-bar target marker (legend, tooltip). */
|
|
1810
|
-
|
|
2047
|
+
/** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
|
|
2048
|
+
function barTargetLabel(locale?: string): string {
|
|
2049
|
+
return chartText(locale).target
|
|
2050
|
+
}
|
|
1811
2051
|
|
|
1812
2052
|
/** How far (px) the target marker reaches past each side of its bar. */
|
|
1813
2053
|
const TARGET_MARKER_OVERHANG = 4
|
|
@@ -1858,16 +2098,17 @@ function achievementRate(value: number | null | undefined, target: number | null
|
|
|
1858
2098
|
return (value / target) * 100
|
|
1859
2099
|
}
|
|
1860
2100
|
|
|
1861
|
-
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3
|
|
2101
|
+
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
|
|
1862
2102
|
function barTargetNote(
|
|
1863
2103
|
label: string,
|
|
1864
2104
|
value: number | null,
|
|
1865
2105
|
target: number | null,
|
|
1866
2106
|
valueFormatter: (value: number) => string,
|
|
2107
|
+
locale?: string,
|
|
1867
2108
|
): string | null {
|
|
1868
2109
|
if (target === null) return null
|
|
1869
2110
|
const rate = achievementRate(value, target)
|
|
1870
|
-
return `${label} ${valueFormatter(target)}${rate === null ? "" :
|
|
2111
|
+
return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
|
|
1871
2112
|
}
|
|
1872
2113
|
|
|
1873
2114
|
/** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
|
|
@@ -1959,11 +2200,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
|
|
|
1959
2200
|
/** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
|
|
1960
2201
|
const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
|
|
1961
2202
|
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2203
|
+
/**
|
|
2204
|
+
* The label at the top of a forecast's boundary line: the forecast's name (as in the legend) and an arrow toward the forecast side.
|
|
2205
|
+
* Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
|
|
2206
|
+
*/
|
|
2207
|
+
function forecastBoundaryText(label?: string, locale?: string): string {
|
|
2208
|
+
return `${label ?? chartText(locale).forecast} →`
|
|
1967
2209
|
}
|
|
1968
2210
|
|
|
1969
2211
|
/** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
|
|
@@ -2003,8 +2245,10 @@ interface AxisExtent {
|
|
|
2003
2245
|
max: number
|
|
2004
2246
|
}
|
|
2005
2247
|
|
|
2006
|
-
/** The label of a stack's total (highlight and tooltip). */
|
|
2007
|
-
|
|
2248
|
+
/** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
|
|
2249
|
+
function stackTotalLabel(locale?: string): string {
|
|
2250
|
+
return chartText(locale).total
|
|
2251
|
+
}
|
|
2008
2252
|
|
|
2009
2253
|
/** Per point, the sum of the stacked series; `null` where every series is missing. */
|
|
2010
2254
|
function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
|
|
@@ -2126,8 +2370,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
|
|
|
2126
2370
|
|
|
2127
2371
|
/**
|
|
2128
2372
|
* The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
|
|
2129
|
-
* comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2130
|
-
* either value is missing or the baseline is 0.
|
|
2373
|
+
* comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2374
|
+
* "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
|
|
2131
2375
|
*/
|
|
2132
2376
|
function comparisonNote(
|
|
2133
2377
|
current: number | null,
|
|
@@ -2135,16 +2379,18 @@ function comparisonNote(
|
|
|
2135
2379
|
comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
|
|
2136
2380
|
grain?: PeriodGrain,
|
|
2137
2381
|
format?: ValueFormat,
|
|
2138
|
-
|
|
2382
|
+
localeArg?: string,
|
|
2139
2383
|
): string {
|
|
2384
|
+
const locale = resolveLocale(localeArg)
|
|
2140
2385
|
const result = computeComparison(
|
|
2141
2386
|
current,
|
|
2142
2387
|
comparison.vs === "custom"
|
|
2143
2388
|
? { previous, vs: "custom", label: comparison.label ?? "", grain }
|
|
2144
2389
|
: { previous, vs: comparison.vs, label: comparison.label, grain },
|
|
2390
|
+
{ locale },
|
|
2145
2391
|
)
|
|
2146
2392
|
const text = formatChange(result, defaultChangeDisplay(format), locale, format)
|
|
2147
|
-
if (text === null) return
|
|
2393
|
+
if (text === null) return `${chartText(locale).noComparison} ${result.label}`
|
|
2148
2394
|
const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
|
|
2149
2395
|
return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
|
|
2150
2396
|
}
|
|
@@ -2187,7 +2433,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2187
2433
|
toneColors,
|
|
2188
2434
|
valueFormatter,
|
|
2189
2435
|
axisFormatter = valueFormatter,
|
|
2190
|
-
locale = DEFAULT_LOCALE,
|
|
2191
2436
|
type = "default",
|
|
2192
2437
|
layout = "horizontal",
|
|
2193
2438
|
curveType = "linear",
|
|
@@ -2224,7 +2469,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2224
2469
|
chartOptions,
|
|
2225
2470
|
reserveActions = false,
|
|
2226
2471
|
} = cfg
|
|
2227
|
-
const
|
|
2472
|
+
const locale = resolveLocale(cfg.locale)
|
|
2473
|
+
const text = chartText(locale)
|
|
2474
|
+
const provisionalLabel = cfg.provisionalLabel ?? text.provisional
|
|
2228
2475
|
|
|
2229
2476
|
const stacked = kind !== "line" && type !== "default"
|
|
2230
2477
|
const percent = kind !== "line" && type === "percent"
|
|
@@ -2373,9 +2620,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2373
2620
|
// `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
|
|
2374
2621
|
const perBarTarget = kind === "bar" && Boolean(targetKey)
|
|
2375
2622
|
const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
|
|
2376
|
-
const targetText = hasTarget ? (targetLabel ??
|
|
2623
|
+
const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
|
|
2377
2624
|
const highlightText = (value: number) =>
|
|
2378
|
-
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${
|
|
2625
|
+
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
|
|
2379
2626
|
/** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
|
|
2380
2627
|
const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
|
|
2381
2628
|
if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
|
|
@@ -2401,7 +2648,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2401
2648
|
// One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
|
|
2402
2649
|
const barTargets: (number | null)[] | null =
|
|
2403
2650
|
perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
|
|
2404
|
-
const barTargetName = targetLabel ??
|
|
2651
|
+
const barTargetName = targetLabel ?? text.target
|
|
2405
2652
|
const barTargetColor = labelColor ?? guideColor
|
|
2406
2653
|
|
|
2407
2654
|
const markPointFor = (c: number, color: string | undefined) =>
|
|
@@ -2615,7 +2862,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2615
2862
|
}
|
|
2616
2863
|
|
|
2617
2864
|
/* -- forecast: dashed continuation (+ optional band) of one series */
|
|
2618
|
-
const forecastLabel = forecast?.label ??
|
|
2865
|
+
const forecastLabel = forecast?.label ?? text.forecast
|
|
2619
2866
|
const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
|
|
2620
2867
|
const forecastValues: (number | null)[] = []
|
|
2621
2868
|
if (forecast) {
|
|
@@ -3091,7 +3338,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3091
3338
|
})
|
|
3092
3339
|
}
|
|
3093
3340
|
if (stackTotals && i <= lastActual && rows.length > 1) {
|
|
3094
|
-
rows.push({ name:
|
|
3341
|
+
rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3095
3342
|
}
|
|
3096
3343
|
const provisional = provStart !== -1 && i >= provStart && i <= lastActual
|
|
3097
3344
|
if (provisional) {
|
|
@@ -3107,19 +3354,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3107
3354
|
...noteFor(at),
|
|
3108
3355
|
})
|
|
3109
3356
|
})
|
|
3110
|
-
if (stackTotals && rows.length > 1) rows.push({ name:
|
|
3357
|
+
if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3111
3358
|
}
|
|
3112
3359
|
const title = tooltipTitles[i] ?? labels[i] ?? ""
|
|
3113
3360
|
const lines: string[] = []
|
|
3114
3361
|
if (barTargets && i <= lastActual) {
|
|
3115
3362
|
const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
|
|
3116
|
-
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
|
|
3363
|
+
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
|
|
3117
3364
|
if (note) lines.push(note)
|
|
3118
3365
|
}
|
|
3119
3366
|
for (const note of notes) {
|
|
3120
3367
|
if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
|
|
3121
3368
|
}
|
|
3122
|
-
return tooltipHtml(provisional ?
|
|
3369
|
+
return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
|
|
3123
3370
|
},
|
|
3124
3371
|
},
|
|
3125
3372
|
xAxis: horizontalBars ? valueAxis : categoryAxis,
|
|
@@ -3211,8 +3458,6 @@ interface DonutOptionSlice {
|
|
|
3211
3458
|
/** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
|
|
3212
3459
|
const DONUT_OUTSIDE_LABELS = 2
|
|
3213
3460
|
|
|
3214
|
-
const DEFAULT_TOTAL_LABEL = "合計"
|
|
3215
|
-
|
|
3216
3461
|
const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
|
|
3217
3462
|
|
|
3218
3463
|
/** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
|
|
@@ -3245,15 +3490,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
|
|
|
3245
3490
|
}
|
|
3246
3491
|
|
|
3247
3492
|
/**
|
|
3248
|
-
* The default center label: what is totalled (「合計」), the formatted total
|
|
3493
|
+
* The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
|
|
3249
3494
|
* (`amount`, the big line), the unit (a small line under it) and both as one
|
|
3250
3495
|
* string (`value`, 「9.2万 セッション」).
|
|
3251
3496
|
*/
|
|
3252
3497
|
function donutCenterText(
|
|
3253
3498
|
total: number,
|
|
3254
3499
|
valueFormatter: (value: number) => string,
|
|
3255
|
-
{ totalLabel
|
|
3500
|
+
{ totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
|
|
3256
3501
|
): { label: string; value: string; amount: string; unit?: string } {
|
|
3502
|
+
totalLabel ??= chartText(locale).total
|
|
3257
3503
|
const amount = valueFormatter(total)
|
|
3258
3504
|
return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
|
|
3259
3505
|
}
|
|
@@ -3481,8 +3727,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
|
|
|
3481
3727
|
|
|
3482
3728
|
/* ------------------------------------------------------------ provisional */
|
|
3483
3729
|
|
|
3484
|
-
/** The default label for points that are not final yet. */
|
|
3485
|
-
|
|
3730
|
+
/** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
|
|
3731
|
+
function defaultProvisionalLabel(locale?: string): string {
|
|
3732
|
+
return chartText(locale).provisional
|
|
3733
|
+
}
|
|
3486
3734
|
|
|
3487
3735
|
/**
|
|
3488
3736
|
* Position of the first provisional point: the first entry of `indexValues`
|
|
@@ -3549,6 +3797,11 @@ export interface DonutChartProps<T extends Record<string, unknown> = Record<stri
|
|
|
3549
3797
|
loading?: boolean
|
|
3550
3798
|
}
|
|
3551
3799
|
|
|
3800
|
+
const TEXT = {
|
|
3801
|
+
ja: { item: "項目", value: "値", chart: (shares: readonly string[]) => `構成比のドーナツグラフ(${shares.join("、")})` },
|
|
3802
|
+
en: { item: "Item", value: "Value", chart: (shares: readonly string[]) => `Donut chart of shares (${shares.join(", ")})` },
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3552
3805
|
/** The element's width, kept current (`null` until measured). */
|
|
3553
3806
|
function useElementWidth(): [(el: HTMLDivElement | null) => void, number | null] {
|
|
3554
3807
|
const [node, setNode] = useState<HTMLDivElement | null>(null)
|
|
@@ -3604,12 +3857,15 @@ function DonutChartImpl<T extends Record<string, unknown>>(
|
|
|
3604
3857
|
const center = resolveDonutCenter(donutOptions?.centerLabel, { showLabel: donutOptions?.showLabel ?? showLabel, label })
|
|
3605
3858
|
const maxSlices = donutOptions?.maxSlices
|
|
3606
3859
|
const showLegend = donutOptions?.showLegend ?? true
|
|
3607
|
-
const
|
|
3860
|
+
const locale = useLocale()
|
|
3861
|
+
const t = TEXT[uiLanguage(locale)]
|
|
3862
|
+
const valueFormatter = useMemo(() => resolveValueFormatter(valueFormatterProp, format, locale), [valueFormatterProp, format, locale])
|
|
3608
3863
|
|
|
3609
3864
|
const { ref: frameRef, tokens } = useChartSurface(ref)
|
|
3610
3865
|
const slices = useMemo(
|
|
3611
|
-
() =>
|
|
3612
|
-
|
|
3866
|
+
() =>
|
|
3867
|
+
donutSlices(data, { index: labelKey, value: valueKey, colors, maxSlices, otherColor: CHART_OTHER_COLOR, otherLabel: otherSeriesLabel(locale) }),
|
|
3868
|
+
[data, labelKey, valueKey, colors, maxSlices, locale],
|
|
3613
3869
|
)
|
|
3614
3870
|
const palette = useMemo(() => resolveChartColors(slices.map((s) => chartFillName(s.color)), tokens), [slices, tokens])
|
|
3615
3871
|
const rows = useMemo(() => slices.map((s) => s.row), [slices])
|
|
@@ -3659,15 +3915,14 @@ function DonutChartImpl<T extends Record<string, unknown>>(
|
|
|
3659
3915
|
let centerLabel: ReactNode = null
|
|
3660
3916
|
if (center.kind === "custom") centerLabel = center.node
|
|
3661
3917
|
else if (center.kind === "total") {
|
|
3662
|
-
const text = donutCenterText(total, valueFormatter, { totalLabel: donutOptions?.totalLabel, unit: donutOptions?.unit })
|
|
3918
|
+
const text = donutCenterText(total, valueFormatter, { totalLabel: donutOptions?.totalLabel, unit: donutOptions?.unit, locale })
|
|
3663
3919
|
centerLabel = caption(text.label, text.amount, text.unit)
|
|
3664
3920
|
} else if (center.kind === "top") {
|
|
3665
3921
|
const top = donutTopText(slices, labels)
|
|
3666
3922
|
centerLabel = top && caption(top.label, top.amount)
|
|
3667
3923
|
}
|
|
3668
3924
|
// Built from the slices (names and shares), not the column keys.
|
|
3669
|
-
const a11yLabel =
|
|
3670
|
-
ariaLabel ?? `構成比のドーナツグラフ(${legend.map((item) => `${labelOf(item.name, labels)} ${item.share}`).join("、")})`
|
|
3925
|
+
const a11yLabel = ariaLabel ?? t.chart(legend.map((item) => `${labelOf(item.name, labels)} ${item.share}`))
|
|
3671
3926
|
const dataTable = useMemo(
|
|
3672
3927
|
() =>
|
|
3673
3928
|
chartDataTable(
|
|
@@ -3676,14 +3931,14 @@ function DonutChartImpl<T extends Record<string, unknown>>(
|
|
|
3676
3931
|
"name",
|
|
3677
3932
|
["value"],
|
|
3678
3933
|
valueFormatter,
|
|
3679
|
-
{ name: labels?.[labelKey] ??
|
|
3934
|
+
{ name: labels?.[labelKey] ?? t.item, value: labels?.[valueKey] ?? t.value },
|
|
3680
3935
|
),
|
|
3681
|
-
[rows, labelKey, valueKey, valueFormatter, labels],
|
|
3936
|
+
[rows, labelKey, valueKey, valueFormatter, labels, t],
|
|
3682
3937
|
)
|
|
3683
3938
|
|
|
3684
3939
|
const resolvedSummary = useMemo(
|
|
3685
|
-
() => (summary === "auto" ? buildShareSummary(slices, title) : summary),
|
|
3686
|
-
[summary, slices, title],
|
|
3940
|
+
() => (summary === "auto" ? buildShareSummary(slices, title, locale) : summary),
|
|
3941
|
+
[summary, slices, title, locale],
|
|
3687
3942
|
)
|
|
3688
3943
|
return (
|
|
3689
3944
|
<ChartFrame
|