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