@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
|
@@ -18,15 +18,148 @@ import { forwardRef, useMemo, type ForwardedRef, type ReactElement, type Ref } f
|
|
|
18
18
|
import { ChartFrame } from "@squadbase/vantage/components"
|
|
19
19
|
import { EChart } from "@squadbase/vantage/components"
|
|
20
20
|
import type { ChartThreshold } from "@squadbase/vantage/components"
|
|
21
|
+
import { useSyncExternalStore } from "react"
|
|
21
22
|
import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
|
|
22
23
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
23
|
-
import {
|
|
24
|
+
import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
24
25
|
import type { ChartColor, ChartColors, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
|
|
25
26
|
import { type ReactNode } from "react"
|
|
26
27
|
import type { EChartsOption } from "@squadbase/vantage/components"
|
|
27
28
|
import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
|
|
29
|
+
import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
|
|
28
30
|
import type { ChartAnnotation, ChartCommonProps, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
|
|
29
31
|
|
|
32
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The display language of the parts and the defaults that follow it.
|
|
36
|
+
*
|
|
37
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
38
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
39
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
40
|
+
*
|
|
41
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
42
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
46
|
+
type UiLanguage = "ja" | "en"
|
|
47
|
+
|
|
48
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
49
|
+
const FALLBACK_LOCALE = "en-US"
|
|
50
|
+
|
|
51
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
52
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
53
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
57
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
58
|
+
const value = tag?.trim()
|
|
59
|
+
if (!value) return FALLBACK_LOCALE
|
|
60
|
+
try {
|
|
61
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
62
|
+
} catch {
|
|
63
|
+
return FALLBACK_LOCALE
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
69
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
70
|
+
*/
|
|
71
|
+
function documentLocale(): string {
|
|
72
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
73
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
77
|
+
function resolveLocale(locale?: string | null): string {
|
|
78
|
+
return locale || documentLocale()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Picks the wording table of the locale's language. */
|
|
82
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
83
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
87
|
+
function defaultCurrency(locale?: string | null): string {
|
|
88
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
92
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
93
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
97
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
98
|
+
|
|
99
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
100
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
104
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
105
|
+
* Monday for the rest.
|
|
106
|
+
*/
|
|
107
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
108
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
109
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
110
|
+
try {
|
|
111
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
112
|
+
getWeekInfo?: () => { firstDay: number }
|
|
113
|
+
weekInfo?: { firstDay: number }
|
|
114
|
+
}
|
|
115
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
116
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
117
|
+
} catch {
|
|
118
|
+
// Fall through to the table below.
|
|
119
|
+
}
|
|
120
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
124
|
+
|
|
125
|
+
const listeners = new Set<() => void>()
|
|
126
|
+
let observer: MutationObserver | null = null
|
|
127
|
+
|
|
128
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
129
|
+
function subscribe(onChange: () => void): () => void {
|
|
130
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
131
|
+
listeners.add(onChange)
|
|
132
|
+
if (!observer) {
|
|
133
|
+
observer = new MutationObserver(() => {
|
|
134
|
+
for (const listener of [...listeners]) listener()
|
|
135
|
+
})
|
|
136
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
137
|
+
}
|
|
138
|
+
return () => {
|
|
139
|
+
listeners.delete(onChange)
|
|
140
|
+
if (listeners.size === 0 && observer) {
|
|
141
|
+
observer.disconnect()
|
|
142
|
+
observer = null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
151
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
152
|
+
*/
|
|
153
|
+
function useDocumentLocale(): string {
|
|
154
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
158
|
+
function useLocale(locale?: string | null): string {
|
|
159
|
+
const page = useDocumentLocale()
|
|
160
|
+
return locale || page
|
|
161
|
+
}
|
|
162
|
+
|
|
30
163
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
31
164
|
|
|
32
165
|
/**
|
|
@@ -530,9 +663,10 @@ function resolveValueFormatter(
|
|
|
530
663
|
function resolveAxisFormatter(
|
|
531
664
|
valueFormatter: ValueFormatter | undefined,
|
|
532
665
|
format: ValueFormat | undefined,
|
|
533
|
-
|
|
666
|
+
localeArg?: string,
|
|
534
667
|
): ValueFormatter {
|
|
535
668
|
if (valueFormatter) return valueFormatter
|
|
669
|
+
const locale = resolveLocale(localeArg)
|
|
536
670
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
537
671
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
538
672
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -541,40 +675,75 @@ function resolveAxisFormatter(
|
|
|
541
675
|
return (value) => formatValue(value, short, locale)
|
|
542
676
|
}
|
|
543
677
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
]
|
|
678
|
+
interface CompactUnit {
|
|
679
|
+
token: string
|
|
680
|
+
size: number
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
687
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
688
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
689
|
+
*/
|
|
690
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
691
|
+
const cached = compactUnitCache.get(locale)
|
|
692
|
+
if (cached !== undefined) return cached
|
|
693
|
+
let units: CompactUnit[] | null = []
|
|
694
|
+
try {
|
|
695
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
696
|
+
const seen = new Set<string>()
|
|
697
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
698
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
699
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
700
|
+
if (at === -1) continue
|
|
701
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
702
|
+
units = null
|
|
703
|
+
break
|
|
704
|
+
}
|
|
705
|
+
const token = parts[at]!.value
|
|
706
|
+
if (seen.has(token)) continue
|
|
707
|
+
seen.add(token)
|
|
708
|
+
units.push({ token, size: 10 ** exp })
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
units = null
|
|
712
|
+
}
|
|
713
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
714
|
+
compactUnitCache.set(locale, sorted)
|
|
715
|
+
return sorted
|
|
716
|
+
}
|
|
550
717
|
|
|
551
|
-
const
|
|
718
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
552
719
|
|
|
553
720
|
/**
|
|
554
721
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
555
|
-
* that largest tick, so
|
|
556
|
-
* Only the
|
|
557
|
-
* any other format —
|
|
722
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
723
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
724
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
725
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
558
726
|
*/
|
|
559
727
|
function resolveAxisTickFormatter(
|
|
560
728
|
valueFormatter: ValueFormatter | undefined,
|
|
561
729
|
format: ValueFormat | undefined,
|
|
562
|
-
|
|
730
|
+
localeArg?: string,
|
|
563
731
|
): (top: number) => ValueFormatter {
|
|
732
|
+
const locale = resolveLocale(localeArg)
|
|
564
733
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
565
734
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
if (!aligns) return () => base
|
|
735
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
736
|
+
if (!units) return () => base
|
|
569
737
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
738
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
570
739
|
return (top) => {
|
|
571
|
-
const unit =
|
|
740
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
572
741
|
if (!unit) return base
|
|
573
742
|
return (value) => {
|
|
574
743
|
const text = base(value)
|
|
575
744
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
576
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
577
|
-
return text.replace(
|
|
745
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
746
|
+
return text.replace(numberToken, () => scaled)
|
|
578
747
|
}
|
|
579
748
|
}
|
|
580
749
|
}
|
|
@@ -603,8 +772,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
603
772
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
604
773
|
const MAX_CHART_SERIES = 5
|
|
605
774
|
|
|
606
|
-
/** The series name the folded categories get. */
|
|
607
|
-
const
|
|
775
|
+
/** The series name the folded categories get, by language. */
|
|
776
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
777
|
+
|
|
778
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
779
|
+
function otherSeriesLabel(locale?: string): string {
|
|
780
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
784
|
+
|
|
785
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
786
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
787
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
788
|
+
}
|
|
608
789
|
|
|
609
790
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
610
791
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -624,7 +805,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
624
805
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
625
806
|
items: readonly Item[],
|
|
626
807
|
n: number = MAX_CHART_SERIES,
|
|
627
|
-
otherLabel: string =
|
|
808
|
+
otherLabel: string = otherSeriesLabel(),
|
|
628
809
|
): AggregatedItems<Item> {
|
|
629
810
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
630
811
|
const ranked = items
|
|
@@ -653,7 +834,7 @@ function aggregateSeries(
|
|
|
653
834
|
data: readonly Record<string, unknown>[],
|
|
654
835
|
categories: readonly string[],
|
|
655
836
|
n: number = MAX_CHART_SERIES,
|
|
656
|
-
otherLabel: string =
|
|
837
|
+
otherLabel: string = otherSeriesLabel(),
|
|
657
838
|
): AggregatedSeries {
|
|
658
839
|
const totals = categories.map((name) => ({
|
|
659
840
|
name,
|
|
@@ -685,12 +866,14 @@ function aggregateSeriesKeeping(
|
|
|
685
866
|
categories: readonly string[],
|
|
686
867
|
keep: string | undefined,
|
|
687
868
|
n: number = MAX_CHART_SERIES,
|
|
869
|
+
otherLabel: string = otherSeriesLabel(),
|
|
688
870
|
): AggregatedSeries {
|
|
689
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
871
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
690
872
|
const aggregated = aggregateSeries(
|
|
691
873
|
data,
|
|
692
874
|
categories.filter((c) => c !== keep),
|
|
693
875
|
n,
|
|
876
|
+
otherLabel,
|
|
694
877
|
)
|
|
695
878
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
696
879
|
}
|
|
@@ -726,7 +909,7 @@ function seriesColorsWithComparison(
|
|
|
726
909
|
* Colour per drawn series: the record's token for that category, else the
|
|
727
910
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
728
911
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
729
|
-
* data itself names
|
|
912
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
730
913
|
*/
|
|
731
914
|
function seriesColorsFor(
|
|
732
915
|
categories: readonly string[],
|
|
@@ -738,7 +921,7 @@ function seriesColorsFor(
|
|
|
738
921
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
739
922
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
740
923
|
return categories.map((category, i) => {
|
|
741
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
924
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
742
925
|
const fromRecord = record?.[category]
|
|
743
926
|
if (fromRecord) return fromRecord
|
|
744
927
|
if (list && list.length > 0) {
|
|
@@ -785,7 +968,7 @@ interface DonutSlicesConfig {
|
|
|
785
968
|
*/
|
|
786
969
|
function donutSlices(
|
|
787
970
|
data: readonly Record<string, unknown>[],
|
|
788
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
971
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
789
972
|
): DonutSlice[] {
|
|
790
973
|
const items = data
|
|
791
974
|
.map((row, i) => {
|
|
@@ -798,7 +981,8 @@ function donutSlices(
|
|
|
798
981
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
799
982
|
name: item.name,
|
|
800
983
|
value: item.value,
|
|
801
|
-
|
|
984
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
985
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
802
986
|
isOther: false,
|
|
803
987
|
row: item.row,
|
|
804
988
|
}))
|
|
@@ -871,7 +1055,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
871
1055
|
*/
|
|
872
1056
|
function barListRows<Row extends object>(
|
|
873
1057
|
data: readonly Row[],
|
|
874
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
1058
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
875
1059
|
): BarListRow<Row>[] {
|
|
876
1060
|
const items = data.map((row): BarListRow<Row> => {
|
|
877
1061
|
const cells = row as Record<string, unknown>
|
|
@@ -917,19 +1101,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
917
1101
|
|
|
918
1102
|
/**
|
|
919
1103
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
920
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
1104
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
921
1105
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
922
1106
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
923
1107
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
924
1108
|
* keeps its contrast.
|
|
925
1109
|
*/
|
|
926
1110
|
function barListFills(
|
|
927
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
1111
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
928
1112
|
color: ChartColor,
|
|
929
1113
|
otherColor: string,
|
|
930
1114
|
): string[] {
|
|
931
1115
|
return rows.map((row) =>
|
|
932
|
-
row.isOther
|
|
1116
|
+
row.isOther || isOtherName(row.name)
|
|
1117
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
1118
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
933
1119
|
)
|
|
934
1120
|
}
|
|
935
1121
|
|
|
@@ -1145,11 +1331,30 @@ interface ChartSummaryInput {
|
|
|
1145
1331
|
format?: ValueFormat
|
|
1146
1332
|
/** Wins over `format`. */
|
|
1147
1333
|
valueFormatter?: ValueFormatter
|
|
1334
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
1335
|
+
locale?: string
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const SUMMARY_TEXT = {
|
|
1339
|
+
ja: {
|
|
1340
|
+
up: "、前の点より増加",
|
|
1341
|
+
down: "、前の点より減少",
|
|
1342
|
+
flat: "、前の点と横ばい",
|
|
1343
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
1344
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
1345
|
+
},
|
|
1346
|
+
en: {
|
|
1347
|
+
up: ", up from the previous point",
|
|
1348
|
+
down: ", down from the previous point",
|
|
1349
|
+
flat: ", flat from the previous point",
|
|
1350
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
1351
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
1352
|
+
},
|
|
1148
1353
|
}
|
|
1149
1354
|
|
|
1150
1355
|
/**
|
|
1151
1356
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
1152
|
-
* 「<title>: 最新 <index> は
|
|
1357
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
1153
1358
|
* computed from the last two non-empty points of the first series — never
|
|
1154
1359
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
1155
1360
|
* no usable point.
|
|
@@ -1165,30 +1370,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
1165
1370
|
}
|
|
1166
1371
|
const latest = points[points.length - 1]
|
|
1167
1372
|
if (!latest) return undefined
|
|
1168
|
-
const
|
|
1373
|
+
const locale = resolveLocale(input.locale)
|
|
1374
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
1375
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
1169
1376
|
const prev = points[points.length - 2]
|
|
1170
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
1377
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
1171
1378
|
const head = input.title ? `${input.title}: ` : ""
|
|
1172
|
-
return `${head}
|
|
1379
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
1173
1380
|
}
|
|
1174
1381
|
|
|
1175
1382
|
/**
|
|
1176
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
1177
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
1383
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
1384
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
1178
1385
|
*/
|
|
1179
1386
|
function buildShareSummary(
|
|
1180
1387
|
slices: readonly { name: string; value: number }[],
|
|
1181
1388
|
title?: string,
|
|
1389
|
+
locale?: string,
|
|
1182
1390
|
): string | undefined {
|
|
1183
1391
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
1184
1392
|
const top = slices[0]
|
|
1185
1393
|
if (!top || total <= 0) return undefined
|
|
1186
1394
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
1187
|
-
return `${title ? `${title}: ` : ""}
|
|
1395
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
1188
1396
|
}
|
|
1189
1397
|
|
|
1190
1398
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
|
|
1191
1399
|
|
|
1400
|
+
/** The built-in words of the option builders, by language. */
|
|
1401
|
+
const CHART_TEXT = {
|
|
1402
|
+
ja: {
|
|
1403
|
+
target: "目標",
|
|
1404
|
+
forecast: "予測",
|
|
1405
|
+
total: "合計",
|
|
1406
|
+
provisional: "速報",
|
|
1407
|
+
noComparison: "比較なし",
|
|
1408
|
+
share: (share: string) => `(${share})`,
|
|
1409
|
+
targetRate: (rate: string) => `(達成率 ${rate})`,
|
|
1410
|
+
provisionalTitle: (title: string, label: string) => `${title}(${label})`,
|
|
1411
|
+
},
|
|
1412
|
+
en: {
|
|
1413
|
+
target: "Target",
|
|
1414
|
+
forecast: "Forecast",
|
|
1415
|
+
total: "Total",
|
|
1416
|
+
provisional: "Preliminary",
|
|
1417
|
+
noComparison: "No comparison",
|
|
1418
|
+
share: (share: string) => ` (${share})`,
|
|
1419
|
+
targetRate: (rate: string) => ` (${rate} achieved)`,
|
|
1420
|
+
provisionalTitle: (title: string, label: string) => `${title} (${label})`,
|
|
1421
|
+
},
|
|
1422
|
+
} satisfies Record<UiLanguage, unknown>
|
|
1423
|
+
|
|
1424
|
+
const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
|
|
1425
|
+
|
|
1192
1426
|
type CartesianChartKind = "area" | "bar" | "line"
|
|
1193
1427
|
type CartesianStackType = "default" | "stacked" | "percent"
|
|
1194
1428
|
type CartesianCurveType = "linear" | "smooth" | "step"
|
|
@@ -1373,7 +1607,7 @@ interface CartesianChartConfig {
|
|
|
1373
1607
|
* all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
|
|
1374
1608
|
*/
|
|
1375
1609
|
axisTicks?: (top: number) => (value: number) => string
|
|
1376
|
-
/** Locale of the time-axis labels. Default
|
|
1610
|
+
/** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
|
|
1377
1611
|
locale?: string
|
|
1378
1612
|
type?: CartesianStackType
|
|
1379
1613
|
/** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
|
|
@@ -1611,11 +1845,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
|
|
|
1611
1845
|
*/
|
|
1612
1846
|
function timeAxisLabels(
|
|
1613
1847
|
values: readonly unknown[],
|
|
1614
|
-
|
|
1848
|
+
localeArg?: string,
|
|
1615
1849
|
shown: (i: number) => boolean = () => true,
|
|
1616
1850
|
): string[] | null {
|
|
1617
1851
|
const parsed = parseTimeIndex(values)
|
|
1618
1852
|
if (!parsed) return null
|
|
1853
|
+
const locale = resolveLocale(localeArg)
|
|
1619
1854
|
const { granularity, times } = parsed
|
|
1620
1855
|
let prevYear: number | null = null
|
|
1621
1856
|
return times.map((t, i) => {
|
|
@@ -1638,9 +1873,10 @@ function timeAxisLabels(
|
|
|
1638
1873
|
}
|
|
1639
1874
|
|
|
1640
1875
|
/** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
|
|
1641
|
-
function timeTooltipLabels(values: readonly unknown[],
|
|
1876
|
+
function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
|
|
1642
1877
|
const parsed = parseTimeIndex(values)
|
|
1643
1878
|
if (!parsed) return null
|
|
1879
|
+
const locale = resolveLocale(localeArg)
|
|
1644
1880
|
const options: Intl.DateTimeFormatOptions =
|
|
1645
1881
|
parsed.granularity === "year"
|
|
1646
1882
|
? { year: "numeric" }
|
|
@@ -1780,7 +2016,9 @@ function tooltipHtml(
|
|
|
1780
2016
|
rows: readonly TooltipRow[],
|
|
1781
2017
|
valueFormatter: (value: number) => string,
|
|
1782
2018
|
notes: readonly string[] = [],
|
|
2019
|
+
locale?: string,
|
|
1783
2020
|
): string {
|
|
2021
|
+
const t = chartText(locale)
|
|
1784
2022
|
const lines = rows.map(
|
|
1785
2023
|
(row) =>
|
|
1786
2024
|
`<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
|
|
@@ -1789,7 +2027,7 @@ function tooltipHtml(
|
|
|
1789
2027
|
`<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
|
|
1790
2028
|
row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
|
|
1791
2029
|
}${
|
|
1792
|
-
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px"
|
|
2030
|
+
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
|
|
1793
2031
|
}</span></div>` +
|
|
1794
2032
|
(row.note === undefined
|
|
1795
2033
|
? ""
|
|
@@ -1803,8 +2041,10 @@ function tooltipHtml(
|
|
|
1803
2041
|
|
|
1804
2042
|
/* ------------------------------------------------- per-bar targets / notes */
|
|
1805
2043
|
|
|
1806
|
-
/** Default name of the per-bar target marker (legend, tooltip). */
|
|
1807
|
-
|
|
2044
|
+
/** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
|
|
2045
|
+
function barTargetLabel(locale?: string): string {
|
|
2046
|
+
return chartText(locale).target
|
|
2047
|
+
}
|
|
1808
2048
|
|
|
1809
2049
|
/** How far (px) the target marker reaches past each side of its bar. */
|
|
1810
2050
|
const TARGET_MARKER_OVERHANG = 4
|
|
@@ -1855,16 +2095,17 @@ function achievementRate(value: number | null | undefined, target: number | null
|
|
|
1855
2095
|
return (value / target) * 100
|
|
1856
2096
|
}
|
|
1857
2097
|
|
|
1858
|
-
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3
|
|
2098
|
+
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
|
|
1859
2099
|
function barTargetNote(
|
|
1860
2100
|
label: string,
|
|
1861
2101
|
value: number | null,
|
|
1862
2102
|
target: number | null,
|
|
1863
2103
|
valueFormatter: (value: number) => string,
|
|
2104
|
+
locale?: string,
|
|
1864
2105
|
): string | null {
|
|
1865
2106
|
if (target === null) return null
|
|
1866
2107
|
const rate = achievementRate(value, target)
|
|
1867
|
-
return `${label} ${valueFormatter(target)}${rate === null ? "" :
|
|
2108
|
+
return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
|
|
1868
2109
|
}
|
|
1869
2110
|
|
|
1870
2111
|
/** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
|
|
@@ -1956,11 +2197,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
|
|
|
1956
2197
|
/** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
|
|
1957
2198
|
const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
|
|
1958
2199
|
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2200
|
+
/**
|
|
2201
|
+
* 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.
|
|
2202
|
+
* Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
|
|
2203
|
+
*/
|
|
2204
|
+
function forecastBoundaryText(label?: string, locale?: string): string {
|
|
2205
|
+
return `${label ?? chartText(locale).forecast} →`
|
|
1964
2206
|
}
|
|
1965
2207
|
|
|
1966
2208
|
/** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
|
|
@@ -2000,8 +2242,10 @@ interface AxisExtent {
|
|
|
2000
2242
|
max: number
|
|
2001
2243
|
}
|
|
2002
2244
|
|
|
2003
|
-
/** The label of a stack's total (highlight and tooltip). */
|
|
2004
|
-
|
|
2245
|
+
/** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
|
|
2246
|
+
function stackTotalLabel(locale?: string): string {
|
|
2247
|
+
return chartText(locale).total
|
|
2248
|
+
}
|
|
2005
2249
|
|
|
2006
2250
|
/** Per point, the sum of the stacked series; `null` where every series is missing. */
|
|
2007
2251
|
function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
|
|
@@ -2123,8 +2367,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
|
|
|
2123
2367
|
|
|
2124
2368
|
/**
|
|
2125
2369
|
* The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
|
|
2126
|
-
* comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2127
|
-
* either value is missing or the baseline is 0.
|
|
2370
|
+
* comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2371
|
+
* "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
|
|
2128
2372
|
*/
|
|
2129
2373
|
function comparisonNote(
|
|
2130
2374
|
current: number | null,
|
|
@@ -2132,16 +2376,18 @@ function comparisonNote(
|
|
|
2132
2376
|
comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
|
|
2133
2377
|
grain?: PeriodGrain,
|
|
2134
2378
|
format?: ValueFormat,
|
|
2135
|
-
|
|
2379
|
+
localeArg?: string,
|
|
2136
2380
|
): string {
|
|
2381
|
+
const locale = resolveLocale(localeArg)
|
|
2137
2382
|
const result = computeComparison(
|
|
2138
2383
|
current,
|
|
2139
2384
|
comparison.vs === "custom"
|
|
2140
2385
|
? { previous, vs: "custom", label: comparison.label ?? "", grain }
|
|
2141
2386
|
: { previous, vs: comparison.vs, label: comparison.label, grain },
|
|
2387
|
+
{ locale },
|
|
2142
2388
|
)
|
|
2143
2389
|
const text = formatChange(result, defaultChangeDisplay(format), locale, format)
|
|
2144
|
-
if (text === null) return
|
|
2390
|
+
if (text === null) return `${chartText(locale).noComparison} ${result.label}`
|
|
2145
2391
|
const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
|
|
2146
2392
|
return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
|
|
2147
2393
|
}
|
|
@@ -2184,7 +2430,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2184
2430
|
toneColors,
|
|
2185
2431
|
valueFormatter,
|
|
2186
2432
|
axisFormatter = valueFormatter,
|
|
2187
|
-
locale = DEFAULT_LOCALE,
|
|
2188
2433
|
type = "default",
|
|
2189
2434
|
layout = "horizontal",
|
|
2190
2435
|
curveType = "linear",
|
|
@@ -2221,7 +2466,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2221
2466
|
chartOptions,
|
|
2222
2467
|
reserveActions = false,
|
|
2223
2468
|
} = cfg
|
|
2224
|
-
const
|
|
2469
|
+
const locale = resolveLocale(cfg.locale)
|
|
2470
|
+
const text = chartText(locale)
|
|
2471
|
+
const provisionalLabel = cfg.provisionalLabel ?? text.provisional
|
|
2225
2472
|
|
|
2226
2473
|
const stacked = kind !== "line" && type !== "default"
|
|
2227
2474
|
const percent = kind !== "line" && type === "percent"
|
|
@@ -2370,9 +2617,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2370
2617
|
// `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
|
|
2371
2618
|
const perBarTarget = kind === "bar" && Boolean(targetKey)
|
|
2372
2619
|
const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
|
|
2373
|
-
const targetText = hasTarget ? (targetLabel ??
|
|
2620
|
+
const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
|
|
2374
2621
|
const highlightText = (value: number) =>
|
|
2375
|
-
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${
|
|
2622
|
+
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
|
|
2376
2623
|
/** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
|
|
2377
2624
|
const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
|
|
2378
2625
|
if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
|
|
@@ -2398,7 +2645,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2398
2645
|
// One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
|
|
2399
2646
|
const barTargets: (number | null)[] | null =
|
|
2400
2647
|
perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
|
|
2401
|
-
const barTargetName = targetLabel ??
|
|
2648
|
+
const barTargetName = targetLabel ?? text.target
|
|
2402
2649
|
const barTargetColor = labelColor ?? guideColor
|
|
2403
2650
|
|
|
2404
2651
|
const markPointFor = (c: number, color: string | undefined) =>
|
|
@@ -2612,7 +2859,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2612
2859
|
}
|
|
2613
2860
|
|
|
2614
2861
|
/* -- forecast: dashed continuation (+ optional band) of one series */
|
|
2615
|
-
const forecastLabel = forecast?.label ??
|
|
2862
|
+
const forecastLabel = forecast?.label ?? text.forecast
|
|
2616
2863
|
const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
|
|
2617
2864
|
const forecastValues: (number | null)[] = []
|
|
2618
2865
|
if (forecast) {
|
|
@@ -3088,7 +3335,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3088
3335
|
})
|
|
3089
3336
|
}
|
|
3090
3337
|
if (stackTotals && i <= lastActual && rows.length > 1) {
|
|
3091
|
-
rows.push({ name:
|
|
3338
|
+
rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3092
3339
|
}
|
|
3093
3340
|
const provisional = provStart !== -1 && i >= provStart && i <= lastActual
|
|
3094
3341
|
if (provisional) {
|
|
@@ -3104,19 +3351,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3104
3351
|
...noteFor(at),
|
|
3105
3352
|
})
|
|
3106
3353
|
})
|
|
3107
|
-
if (stackTotals && rows.length > 1) rows.push({ name:
|
|
3354
|
+
if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3108
3355
|
}
|
|
3109
3356
|
const title = tooltipTitles[i] ?? labels[i] ?? ""
|
|
3110
3357
|
const lines: string[] = []
|
|
3111
3358
|
if (barTargets && i <= lastActual) {
|
|
3112
3359
|
const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
|
|
3113
|
-
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
|
|
3360
|
+
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
|
|
3114
3361
|
if (note) lines.push(note)
|
|
3115
3362
|
}
|
|
3116
3363
|
for (const note of notes) {
|
|
3117
3364
|
if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
|
|
3118
3365
|
}
|
|
3119
|
-
return tooltipHtml(provisional ?
|
|
3366
|
+
return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
|
|
3120
3367
|
},
|
|
3121
3368
|
},
|
|
3122
3369
|
xAxis: horizontalBars ? valueAxis : categoryAxis,
|
|
@@ -3208,8 +3455,6 @@ interface DonutOptionSlice {
|
|
|
3208
3455
|
/** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
|
|
3209
3456
|
const DONUT_OUTSIDE_LABELS = 2
|
|
3210
3457
|
|
|
3211
|
-
const DEFAULT_TOTAL_LABEL = "合計"
|
|
3212
|
-
|
|
3213
3458
|
const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
|
|
3214
3459
|
|
|
3215
3460
|
/** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
|
|
@@ -3242,15 +3487,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
|
|
|
3242
3487
|
}
|
|
3243
3488
|
|
|
3244
3489
|
/**
|
|
3245
|
-
* The default center label: what is totalled (「合計」), the formatted total
|
|
3490
|
+
* The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
|
|
3246
3491
|
* (`amount`, the big line), the unit (a small line under it) and both as one
|
|
3247
3492
|
* string (`value`, 「9.2万 セッション」).
|
|
3248
3493
|
*/
|
|
3249
3494
|
function donutCenterText(
|
|
3250
3495
|
total: number,
|
|
3251
3496
|
valueFormatter: (value: number) => string,
|
|
3252
|
-
{ totalLabel
|
|
3497
|
+
{ totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
|
|
3253
3498
|
): { label: string; value: string; amount: string; unit?: string } {
|
|
3499
|
+
totalLabel ??= chartText(locale).total
|
|
3254
3500
|
const amount = valueFormatter(total)
|
|
3255
3501
|
return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
|
|
3256
3502
|
}
|
|
@@ -3478,8 +3724,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
|
|
|
3478
3724
|
|
|
3479
3725
|
/* ------------------------------------------------------------ provisional */
|
|
3480
3726
|
|
|
3481
|
-
/** The default label for points that are not final yet. */
|
|
3482
|
-
|
|
3727
|
+
/** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
|
|
3728
|
+
function defaultProvisionalLabel(locale?: string): string {
|
|
3729
|
+
return chartText(locale).provisional
|
|
3730
|
+
}
|
|
3483
3731
|
|
|
3484
3732
|
/**
|
|
3485
3733
|
* Position of the first provisional point: the first entry of `indexValues`
|
|
@@ -3531,6 +3779,11 @@ export interface BarChartProps<T extends Record<string, unknown> = Record<string
|
|
|
3531
3779
|
barCategoryGap?: string | number
|
|
3532
3780
|
}
|
|
3533
3781
|
|
|
3782
|
+
const TEXT = {
|
|
3783
|
+
ja: { item: "項目", chart: (categories: readonly string[]) => `${categories.join("・")}の棒グラフ` },
|
|
3784
|
+
en: { item: "Item", chart: (categories: readonly string[]) => `Bar chart of ${categories.join(", ")}` },
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3534
3787
|
/** Rows not given yet (a draft fills props in steps): draw the empty state, keep the same array between renders. */
|
|
3535
3788
|
const NO_ROWS: never[] = []
|
|
3536
3789
|
|
|
@@ -3583,12 +3836,14 @@ function BarChartImpl<T extends Record<string, unknown>>(
|
|
|
3583
3836
|
) {
|
|
3584
3837
|
// Highlight labels are backed by the surface the chart sits on (a card, the page).
|
|
3585
3838
|
const { ref: frameRef, surface, width: plotWidth, tokens } = useChartSurface(ref)
|
|
3839
|
+
const locale = useLocale()
|
|
3840
|
+
const t = TEXT[uiLanguage(locale)]
|
|
3586
3841
|
// Ticks and on-plot labels read short (¥1億); the tooltip reads the value as `format` says.
|
|
3587
|
-
const tooltipFormatter = useMemo(() => resolveValueFormatter(valueFormatter, format), [valueFormatter, format])
|
|
3588
|
-
const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatter, format), [valueFormatter, format])
|
|
3842
|
+
const tooltipFormatter = useMemo(() => resolveValueFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
|
|
3843
|
+
const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
|
|
3589
3844
|
// Ticks share one unit — the largest tick's (¥0.5億 / ¥1億).
|
|
3590
|
-
const axisTicks = useMemo(() => resolveAxisTickFormatter(valueFormatter, format), [valueFormatter, format])
|
|
3591
|
-
const series = useMemo(() => aggregateSeries(data, categories), [data, categories])
|
|
3845
|
+
const axisTicks = useMemo(() => resolveAxisTickFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
|
|
3846
|
+
const series = useMemo(() => aggregateSeries(data, categories, undefined, otherSeriesLabel(locale)), [data, categories, locale])
|
|
3592
3847
|
const palette = useMemo(
|
|
3593
3848
|
() =>
|
|
3594
3849
|
resolveChartColors(
|
|
@@ -3650,24 +3905,25 @@ function BarChartImpl<T extends Record<string, unknown>>(
|
|
|
3650
3905
|
plotWidth,
|
|
3651
3906
|
plotHeight: typeof height === "number" ? height : undefined,
|
|
3652
3907
|
chartOptions,
|
|
3908
|
+
locale,
|
|
3653
3909
|
}),
|
|
3654
3910
|
[
|
|
3655
3911
|
series, index, palette, labels, thresholds, toneColors, tooltipFormatter, axisFormatter, axisTicks, stackType, barLayout, gap, showZeroValues,
|
|
3656
3912
|
showLegend, showTooltip, showGridLines, showXAxis, showYAxis, startEndOnly, yAxisWidth,
|
|
3657
3913
|
maxValue, allowDecimals, xAxisLabel, yAxisLabel, active,
|
|
3658
|
-
provisionalFrom, provisionalLabel, actions, highlight, target, targetLabel, barOptions?.targetKey, barOptions?.highlight, annotations, height, tokens, surface, plotWidth, chartOptions,
|
|
3914
|
+
provisionalFrom, provisionalLabel, actions, highlight, target, targetLabel, barOptions?.targetKey, barOptions?.highlight, annotations, height, tokens, surface, plotWidth, chartOptions, locale,
|
|
3659
3915
|
],
|
|
3660
3916
|
)
|
|
3661
3917
|
// The screen-reader table: exact values (tooltip format), dates read as dates, 「その他」 as its own column.
|
|
3662
3918
|
const dataTable = useMemo(() => {
|
|
3663
|
-
const table = chartDataTable(labelRows(series.data, index, labels), index, series.categories, tooltipFormatter, labels,
|
|
3664
|
-
const times = timeTooltipLabels(series.data.map((row) => row[index]))
|
|
3919
|
+
const table = chartDataTable(labelRows(series.data, index, labels), index, series.categories, tooltipFormatter, labels, t.item)
|
|
3920
|
+
const times = timeTooltipLabels(series.data.map((row) => row[index]), locale)
|
|
3665
3921
|
return times ? { ...table, rows: table.rows.map((row, i) => [times[i] ?? row[0] ?? "", ...row.slice(1)]) } : table
|
|
3666
|
-
}, [series, index, tooltipFormatter, labels])
|
|
3667
|
-
const label = ariaLabel ??
|
|
3922
|
+
}, [series, index, tooltipFormatter, labels, t, locale])
|
|
3923
|
+
const label = ariaLabel ?? t.chart(categories)
|
|
3668
3924
|
const resolvedSummary = useMemo(
|
|
3669
|
-
() => (summary === "auto" ? buildChartSummary({ data, index, categories, title, format, valueFormatter }) : summary),
|
|
3670
|
-
[summary, data, index, categories, title, format, valueFormatter],
|
|
3925
|
+
() => (summary === "auto" ? buildChartSummary({ data, index, categories, title, format, valueFormatter, locale }) : summary),
|
|
3926
|
+
[summary, data, index, categories, title, format, valueFormatter, locale],
|
|
3671
3927
|
)
|
|
3672
3928
|
return (
|
|
3673
3929
|
<ChartFrame
|