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