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