@squadbase/vantage 0.6.2-dev1.48b34e1 → 0.6.2-dev1.56da87f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{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 +3 -2
- 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
package/registry/ui/bar-list.tsx
CHANGED
|
@@ -31,15 +31,148 @@ import { cn } from "@squadbase/vantage/ui"
|
|
|
31
31
|
import { ChartFrame } from "@squadbase/vantage/components"
|
|
32
32
|
import { toneForValue } from "@squadbase/vantage/components"
|
|
33
33
|
import type { ChartColor, ChartCommonProps, ChartSeriesColor, ChartThreshold } from "@squadbase/vantage/components"
|
|
34
|
+
import { useSyncExternalStore } from "react"
|
|
34
35
|
import { createContext, useCallback, useRef, type RefObject } from "react"
|
|
35
36
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
36
|
-
import {
|
|
37
|
+
import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
|
|
37
38
|
import type { ChartColors, ValueFormatter } from "@squadbase/vantage/components"
|
|
38
39
|
import { useMemo, type ReactNode } from "react"
|
|
39
40
|
import type { EChartsOption } from "@squadbase/vantage/components"
|
|
40
41
|
import { computeComparison, defaultChangeDisplay, formatChange, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
|
|
42
|
+
import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
|
|
41
43
|
import type { ChartAnnotation, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
|
|
42
44
|
|
|
45
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The display language of the parts and the defaults that follow it.
|
|
49
|
+
*
|
|
50
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
51
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
52
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
53
|
+
*
|
|
54
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
55
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
59
|
+
type UiLanguage = "ja" | "en"
|
|
60
|
+
|
|
61
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
62
|
+
const FALLBACK_LOCALE = "en-US"
|
|
63
|
+
|
|
64
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
65
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
66
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
70
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
71
|
+
const value = tag?.trim()
|
|
72
|
+
if (!value) return FALLBACK_LOCALE
|
|
73
|
+
try {
|
|
74
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
75
|
+
} catch {
|
|
76
|
+
return FALLBACK_LOCALE
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
82
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
83
|
+
*/
|
|
84
|
+
function documentLocale(): string {
|
|
85
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
86
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
90
|
+
function resolveLocale(locale?: string | null): string {
|
|
91
|
+
return locale || documentLocale()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Picks the wording table of the locale's language. */
|
|
95
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
96
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
100
|
+
function defaultCurrency(locale?: string | null): string {
|
|
101
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
105
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
106
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
110
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
111
|
+
|
|
112
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
113
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
117
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
118
|
+
* Monday for the rest.
|
|
119
|
+
*/
|
|
120
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
121
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
122
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
123
|
+
try {
|
|
124
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
125
|
+
getWeekInfo?: () => { firstDay: number }
|
|
126
|
+
weekInfo?: { firstDay: number }
|
|
127
|
+
}
|
|
128
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
129
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
130
|
+
} catch {
|
|
131
|
+
// Fall through to the table below.
|
|
132
|
+
}
|
|
133
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
137
|
+
|
|
138
|
+
const listeners = new Set<() => void>()
|
|
139
|
+
let observer: MutationObserver | null = null
|
|
140
|
+
|
|
141
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
142
|
+
function subscribe(onChange: () => void): () => void {
|
|
143
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
144
|
+
listeners.add(onChange)
|
|
145
|
+
if (!observer) {
|
|
146
|
+
observer = new MutationObserver(() => {
|
|
147
|
+
for (const listener of [...listeners]) listener()
|
|
148
|
+
})
|
|
149
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
150
|
+
}
|
|
151
|
+
return () => {
|
|
152
|
+
listeners.delete(onChange)
|
|
153
|
+
if (listeners.size === 0 && observer) {
|
|
154
|
+
observer.disconnect()
|
|
155
|
+
observer = null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
164
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
165
|
+
*/
|
|
166
|
+
function useDocumentLocale(): string {
|
|
167
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
171
|
+
function useLocale(locale?: string | null): string {
|
|
172
|
+
const page = useDocumentLocale()
|
|
173
|
+
return locale || page
|
|
174
|
+
}
|
|
175
|
+
|
|
43
176
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
44
177
|
|
|
45
178
|
/**
|
|
@@ -543,9 +676,10 @@ function resolveValueFormatter(
|
|
|
543
676
|
function resolveAxisFormatter(
|
|
544
677
|
valueFormatter: ValueFormatter | undefined,
|
|
545
678
|
format: ValueFormat | undefined,
|
|
546
|
-
|
|
679
|
+
localeArg?: string,
|
|
547
680
|
): ValueFormatter {
|
|
548
681
|
if (valueFormatter) return valueFormatter
|
|
682
|
+
const locale = resolveLocale(localeArg)
|
|
549
683
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
550
684
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
551
685
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -554,40 +688,75 @@ function resolveAxisFormatter(
|
|
|
554
688
|
return (value) => formatValue(value, short, locale)
|
|
555
689
|
}
|
|
556
690
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
]
|
|
691
|
+
interface CompactUnit {
|
|
692
|
+
token: string
|
|
693
|
+
size: number
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
700
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
701
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
702
|
+
*/
|
|
703
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
704
|
+
const cached = compactUnitCache.get(locale)
|
|
705
|
+
if (cached !== undefined) return cached
|
|
706
|
+
let units: CompactUnit[] | null = []
|
|
707
|
+
try {
|
|
708
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
709
|
+
const seen = new Set<string>()
|
|
710
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
711
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
712
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
713
|
+
if (at === -1) continue
|
|
714
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
715
|
+
units = null
|
|
716
|
+
break
|
|
717
|
+
}
|
|
718
|
+
const token = parts[at]!.value
|
|
719
|
+
if (seen.has(token)) continue
|
|
720
|
+
seen.add(token)
|
|
721
|
+
units.push({ token, size: 10 ** exp })
|
|
722
|
+
}
|
|
723
|
+
} catch {
|
|
724
|
+
units = null
|
|
725
|
+
}
|
|
726
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
727
|
+
compactUnitCache.set(locale, sorted)
|
|
728
|
+
return sorted
|
|
729
|
+
}
|
|
563
730
|
|
|
564
|
-
const
|
|
731
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
565
732
|
|
|
566
733
|
/**
|
|
567
734
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
568
|
-
* that largest tick, so
|
|
569
|
-
* Only the
|
|
570
|
-
* any other format —
|
|
735
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
736
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
737
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
738
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
571
739
|
*/
|
|
572
740
|
function resolveAxisTickFormatter(
|
|
573
741
|
valueFormatter: ValueFormatter | undefined,
|
|
574
742
|
format: ValueFormat | undefined,
|
|
575
|
-
|
|
743
|
+
localeArg?: string,
|
|
576
744
|
): (top: number) => ValueFormatter {
|
|
745
|
+
const locale = resolveLocale(localeArg)
|
|
577
746
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
578
747
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
if (!aligns) return () => base
|
|
748
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
749
|
+
if (!units) return () => base
|
|
582
750
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
751
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
583
752
|
return (top) => {
|
|
584
|
-
const unit =
|
|
753
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
585
754
|
if (!unit) return base
|
|
586
755
|
return (value) => {
|
|
587
756
|
const text = base(value)
|
|
588
757
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
589
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
590
|
-
return text.replace(
|
|
758
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
759
|
+
return text.replace(numberToken, () => scaled)
|
|
591
760
|
}
|
|
592
761
|
}
|
|
593
762
|
}
|
|
@@ -616,8 +785,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
616
785
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
617
786
|
const MAX_CHART_SERIES = 5
|
|
618
787
|
|
|
619
|
-
/** The series name the folded categories get. */
|
|
620
|
-
const
|
|
788
|
+
/** The series name the folded categories get, by language. */
|
|
789
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
790
|
+
|
|
791
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
792
|
+
function otherSeriesLabel(locale?: string): string {
|
|
793
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
797
|
+
|
|
798
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
799
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
800
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
801
|
+
}
|
|
621
802
|
|
|
622
803
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
623
804
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -637,7 +818,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
637
818
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
638
819
|
items: readonly Item[],
|
|
639
820
|
n: number = MAX_CHART_SERIES,
|
|
640
|
-
otherLabel: string =
|
|
821
|
+
otherLabel: string = otherSeriesLabel(),
|
|
641
822
|
): AggregatedItems<Item> {
|
|
642
823
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
643
824
|
const ranked = items
|
|
@@ -666,7 +847,7 @@ function aggregateSeries(
|
|
|
666
847
|
data: readonly Record<string, unknown>[],
|
|
667
848
|
categories: readonly string[],
|
|
668
849
|
n: number = MAX_CHART_SERIES,
|
|
669
|
-
otherLabel: string =
|
|
850
|
+
otherLabel: string = otherSeriesLabel(),
|
|
670
851
|
): AggregatedSeries {
|
|
671
852
|
const totals = categories.map((name) => ({
|
|
672
853
|
name,
|
|
@@ -698,12 +879,14 @@ function aggregateSeriesKeeping(
|
|
|
698
879
|
categories: readonly string[],
|
|
699
880
|
keep: string | undefined,
|
|
700
881
|
n: number = MAX_CHART_SERIES,
|
|
882
|
+
otherLabel: string = otherSeriesLabel(),
|
|
701
883
|
): AggregatedSeries {
|
|
702
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
884
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
703
885
|
const aggregated = aggregateSeries(
|
|
704
886
|
data,
|
|
705
887
|
categories.filter((c) => c !== keep),
|
|
706
888
|
n,
|
|
889
|
+
otherLabel,
|
|
707
890
|
)
|
|
708
891
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
709
892
|
}
|
|
@@ -739,7 +922,7 @@ function seriesColorsWithComparison(
|
|
|
739
922
|
* Colour per drawn series: the record's token for that category, else the
|
|
740
923
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
741
924
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
742
|
-
* data itself names
|
|
925
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
743
926
|
*/
|
|
744
927
|
function seriesColorsFor(
|
|
745
928
|
categories: readonly string[],
|
|
@@ -751,7 +934,7 @@ function seriesColorsFor(
|
|
|
751
934
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
752
935
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
753
936
|
return categories.map((category, i) => {
|
|
754
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
937
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
755
938
|
const fromRecord = record?.[category]
|
|
756
939
|
if (fromRecord) return fromRecord
|
|
757
940
|
if (list && list.length > 0) {
|
|
@@ -798,7 +981,7 @@ interface DonutSlicesConfig {
|
|
|
798
981
|
*/
|
|
799
982
|
function donutSlices(
|
|
800
983
|
data: readonly Record<string, unknown>[],
|
|
801
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
984
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
802
985
|
): DonutSlice[] {
|
|
803
986
|
const items = data
|
|
804
987
|
.map((row, i) => {
|
|
@@ -811,7 +994,8 @@ function donutSlices(
|
|
|
811
994
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
812
995
|
name: item.name,
|
|
813
996
|
value: item.value,
|
|
814
|
-
|
|
997
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
998
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
815
999
|
isOther: false,
|
|
816
1000
|
row: item.row,
|
|
817
1001
|
}))
|
|
@@ -884,7 +1068,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
884
1068
|
*/
|
|
885
1069
|
function barListRows<Row extends object>(
|
|
886
1070
|
data: readonly Row[],
|
|
887
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
1071
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
888
1072
|
): BarListRow<Row>[] {
|
|
889
1073
|
const items = data.map((row): BarListRow<Row> => {
|
|
890
1074
|
const cells = row as Record<string, unknown>
|
|
@@ -930,19 +1114,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
930
1114
|
|
|
931
1115
|
/**
|
|
932
1116
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
933
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
1117
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
934
1118
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
935
1119
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
936
1120
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
937
1121
|
* keeps its contrast.
|
|
938
1122
|
*/
|
|
939
1123
|
function barListFills(
|
|
940
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
1124
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
941
1125
|
color: ChartColor,
|
|
942
1126
|
otherColor: string,
|
|
943
1127
|
): string[] {
|
|
944
1128
|
return rows.map((row) =>
|
|
945
|
-
row.isOther
|
|
1129
|
+
row.isOther || isOtherName(row.name)
|
|
1130
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
1131
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
946
1132
|
)
|
|
947
1133
|
}
|
|
948
1134
|
|
|
@@ -1158,11 +1344,30 @@ interface ChartSummaryInput {
|
|
|
1158
1344
|
format?: ValueFormat
|
|
1159
1345
|
/** Wins over `format`. */
|
|
1160
1346
|
valueFormatter?: ValueFormatter
|
|
1347
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
1348
|
+
locale?: string
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const SUMMARY_TEXT = {
|
|
1352
|
+
ja: {
|
|
1353
|
+
up: "、前の点より増加",
|
|
1354
|
+
down: "、前の点より減少",
|
|
1355
|
+
flat: "、前の点と横ばい",
|
|
1356
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
1357
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
1358
|
+
},
|
|
1359
|
+
en: {
|
|
1360
|
+
up: ", up from the previous point",
|
|
1361
|
+
down: ", down from the previous point",
|
|
1362
|
+
flat: ", flat from the previous point",
|
|
1363
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
1364
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
1365
|
+
},
|
|
1161
1366
|
}
|
|
1162
1367
|
|
|
1163
1368
|
/**
|
|
1164
1369
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
1165
|
-
* 「<title>: 最新 <index> は
|
|
1370
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
1166
1371
|
* computed from the last two non-empty points of the first series — never
|
|
1167
1372
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
1168
1373
|
* no usable point.
|
|
@@ -1178,30 +1383,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
1178
1383
|
}
|
|
1179
1384
|
const latest = points[points.length - 1]
|
|
1180
1385
|
if (!latest) return undefined
|
|
1181
|
-
const
|
|
1386
|
+
const locale = resolveLocale(input.locale)
|
|
1387
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
1388
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
1182
1389
|
const prev = points[points.length - 2]
|
|
1183
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
1390
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
1184
1391
|
const head = input.title ? `${input.title}: ` : ""
|
|
1185
|
-
return `${head}
|
|
1392
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
1186
1393
|
}
|
|
1187
1394
|
|
|
1188
1395
|
/**
|
|
1189
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
1190
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
1396
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
1397
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
1191
1398
|
*/
|
|
1192
1399
|
function buildShareSummary(
|
|
1193
1400
|
slices: readonly { name: string; value: number }[],
|
|
1194
1401
|
title?: string,
|
|
1402
|
+
locale?: string,
|
|
1195
1403
|
): string | undefined {
|
|
1196
1404
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
1197
1405
|
const top = slices[0]
|
|
1198
1406
|
if (!top || total <= 0) return undefined
|
|
1199
1407
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
1200
|
-
return `${title ? `${title}: ` : ""}
|
|
1408
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
1201
1409
|
}
|
|
1202
1410
|
|
|
1203
1411
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
|
|
1204
1412
|
|
|
1413
|
+
/** The built-in words of the option builders, by language. */
|
|
1414
|
+
const CHART_TEXT = {
|
|
1415
|
+
ja: {
|
|
1416
|
+
target: "目標",
|
|
1417
|
+
forecast: "予測",
|
|
1418
|
+
total: "合計",
|
|
1419
|
+
provisional: "速報",
|
|
1420
|
+
noComparison: "比較なし",
|
|
1421
|
+
share: (share: string) => `(${share})`,
|
|
1422
|
+
targetRate: (rate: string) => `(達成率 ${rate})`,
|
|
1423
|
+
provisionalTitle: (title: string, label: string) => `${title}(${label})`,
|
|
1424
|
+
},
|
|
1425
|
+
en: {
|
|
1426
|
+
target: "Target",
|
|
1427
|
+
forecast: "Forecast",
|
|
1428
|
+
total: "Total",
|
|
1429
|
+
provisional: "Preliminary",
|
|
1430
|
+
noComparison: "No comparison",
|
|
1431
|
+
share: (share: string) => ` (${share})`,
|
|
1432
|
+
targetRate: (rate: string) => ` (${rate} achieved)`,
|
|
1433
|
+
provisionalTitle: (title: string, label: string) => `${title} (${label})`,
|
|
1434
|
+
},
|
|
1435
|
+
} satisfies Record<UiLanguage, unknown>
|
|
1436
|
+
|
|
1437
|
+
const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
|
|
1438
|
+
|
|
1205
1439
|
type CartesianChartKind = "area" | "bar" | "line"
|
|
1206
1440
|
type CartesianStackType = "default" | "stacked" | "percent"
|
|
1207
1441
|
type CartesianCurveType = "linear" | "smooth" | "step"
|
|
@@ -1386,7 +1620,7 @@ interface CartesianChartConfig {
|
|
|
1386
1620
|
* all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
|
|
1387
1621
|
*/
|
|
1388
1622
|
axisTicks?: (top: number) => (value: number) => string
|
|
1389
|
-
/** Locale of the time-axis labels. Default
|
|
1623
|
+
/** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
|
|
1390
1624
|
locale?: string
|
|
1391
1625
|
type?: CartesianStackType
|
|
1392
1626
|
/** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
|
|
@@ -1624,11 +1858,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
|
|
|
1624
1858
|
*/
|
|
1625
1859
|
function timeAxisLabels(
|
|
1626
1860
|
values: readonly unknown[],
|
|
1627
|
-
|
|
1861
|
+
localeArg?: string,
|
|
1628
1862
|
shown: (i: number) => boolean = () => true,
|
|
1629
1863
|
): string[] | null {
|
|
1630
1864
|
const parsed = parseTimeIndex(values)
|
|
1631
1865
|
if (!parsed) return null
|
|
1866
|
+
const locale = resolveLocale(localeArg)
|
|
1632
1867
|
const { granularity, times } = parsed
|
|
1633
1868
|
let prevYear: number | null = null
|
|
1634
1869
|
return times.map((t, i) => {
|
|
@@ -1651,9 +1886,10 @@ function timeAxisLabels(
|
|
|
1651
1886
|
}
|
|
1652
1887
|
|
|
1653
1888
|
/** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
|
|
1654
|
-
function timeTooltipLabels(values: readonly unknown[],
|
|
1889
|
+
function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
|
|
1655
1890
|
const parsed = parseTimeIndex(values)
|
|
1656
1891
|
if (!parsed) return null
|
|
1892
|
+
const locale = resolveLocale(localeArg)
|
|
1657
1893
|
const options: Intl.DateTimeFormatOptions =
|
|
1658
1894
|
parsed.granularity === "year"
|
|
1659
1895
|
? { year: "numeric" }
|
|
@@ -1793,7 +2029,9 @@ function tooltipHtml(
|
|
|
1793
2029
|
rows: readonly TooltipRow[],
|
|
1794
2030
|
valueFormatter: (value: number) => string,
|
|
1795
2031
|
notes: readonly string[] = [],
|
|
2032
|
+
locale?: string,
|
|
1796
2033
|
): string {
|
|
2034
|
+
const t = chartText(locale)
|
|
1797
2035
|
const lines = rows.map(
|
|
1798
2036
|
(row) =>
|
|
1799
2037
|
`<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
|
|
@@ -1802,7 +2040,7 @@ function tooltipHtml(
|
|
|
1802
2040
|
`<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
|
|
1803
2041
|
row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
|
|
1804
2042
|
}${
|
|
1805
|
-
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px"
|
|
2043
|
+
row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
|
|
1806
2044
|
}</span></div>` +
|
|
1807
2045
|
(row.note === undefined
|
|
1808
2046
|
? ""
|
|
@@ -1816,8 +2054,10 @@ function tooltipHtml(
|
|
|
1816
2054
|
|
|
1817
2055
|
/* ------------------------------------------------- per-bar targets / notes */
|
|
1818
2056
|
|
|
1819
|
-
/** Default name of the per-bar target marker (legend, tooltip). */
|
|
1820
|
-
|
|
2057
|
+
/** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
|
|
2058
|
+
function barTargetLabel(locale?: string): string {
|
|
2059
|
+
return chartText(locale).target
|
|
2060
|
+
}
|
|
1821
2061
|
|
|
1822
2062
|
/** How far (px) the target marker reaches past each side of its bar. */
|
|
1823
2063
|
const TARGET_MARKER_OVERHANG = 4
|
|
@@ -1868,16 +2108,17 @@ function achievementRate(value: number | null | undefined, target: number | null
|
|
|
1868
2108
|
return (value / target) * 100
|
|
1869
2109
|
}
|
|
1870
2110
|
|
|
1871
|
-
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3
|
|
2111
|
+
/** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
|
|
1872
2112
|
function barTargetNote(
|
|
1873
2113
|
label: string,
|
|
1874
2114
|
value: number | null,
|
|
1875
2115
|
target: number | null,
|
|
1876
2116
|
valueFormatter: (value: number) => string,
|
|
2117
|
+
locale?: string,
|
|
1877
2118
|
): string | null {
|
|
1878
2119
|
if (target === null) return null
|
|
1879
2120
|
const rate = achievementRate(value, target)
|
|
1880
|
-
return `${label} ${valueFormatter(target)}${rate === null ? "" :
|
|
2121
|
+
return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
|
|
1881
2122
|
}
|
|
1882
2123
|
|
|
1883
2124
|
/** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
|
|
@@ -1969,11 +2210,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
|
|
|
1969
2210
|
/** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
|
|
1970
2211
|
const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
|
|
1971
2212
|
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2213
|
+
/**
|
|
2214
|
+
* 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.
|
|
2215
|
+
* Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
|
|
2216
|
+
*/
|
|
2217
|
+
function forecastBoundaryText(label?: string, locale?: string): string {
|
|
2218
|
+
return `${label ?? chartText(locale).forecast} →`
|
|
1977
2219
|
}
|
|
1978
2220
|
|
|
1979
2221
|
/** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
|
|
@@ -2013,8 +2255,10 @@ interface AxisExtent {
|
|
|
2013
2255
|
max: number
|
|
2014
2256
|
}
|
|
2015
2257
|
|
|
2016
|
-
/** The label of a stack's total (highlight and tooltip). */
|
|
2017
|
-
|
|
2258
|
+
/** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
|
|
2259
|
+
function stackTotalLabel(locale?: string): string {
|
|
2260
|
+
return chartText(locale).total
|
|
2261
|
+
}
|
|
2018
2262
|
|
|
2019
2263
|
/** Per point, the sum of the stacked series; `null` where every series is missing. */
|
|
2020
2264
|
function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
|
|
@@ -2136,8 +2380,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
|
|
|
2136
2380
|
|
|
2137
2381
|
/**
|
|
2138
2382
|
* The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
|
|
2139
|
-
* comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2140
|
-
* either value is missing or the baseline is 0.
|
|
2383
|
+
* comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
|
|
2384
|
+
* "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
|
|
2141
2385
|
*/
|
|
2142
2386
|
function comparisonNote(
|
|
2143
2387
|
current: number | null,
|
|
@@ -2145,16 +2389,18 @@ function comparisonNote(
|
|
|
2145
2389
|
comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
|
|
2146
2390
|
grain?: PeriodGrain,
|
|
2147
2391
|
format?: ValueFormat,
|
|
2148
|
-
|
|
2392
|
+
localeArg?: string,
|
|
2149
2393
|
): string {
|
|
2394
|
+
const locale = resolveLocale(localeArg)
|
|
2150
2395
|
const result = computeComparison(
|
|
2151
2396
|
current,
|
|
2152
2397
|
comparison.vs === "custom"
|
|
2153
2398
|
? { previous, vs: "custom", label: comparison.label ?? "", grain }
|
|
2154
2399
|
: { previous, vs: comparison.vs, label: comparison.label, grain },
|
|
2400
|
+
{ locale },
|
|
2155
2401
|
)
|
|
2156
2402
|
const text = formatChange(result, defaultChangeDisplay(format), locale, format)
|
|
2157
|
-
if (text === null) return
|
|
2403
|
+
if (text === null) return `${chartText(locale).noComparison} ${result.label}`
|
|
2158
2404
|
const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
|
|
2159
2405
|
return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
|
|
2160
2406
|
}
|
|
@@ -2197,7 +2443,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2197
2443
|
toneColors,
|
|
2198
2444
|
valueFormatter,
|
|
2199
2445
|
axisFormatter = valueFormatter,
|
|
2200
|
-
locale = DEFAULT_LOCALE,
|
|
2201
2446
|
type = "default",
|
|
2202
2447
|
layout = "horizontal",
|
|
2203
2448
|
curveType = "linear",
|
|
@@ -2234,7 +2479,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2234
2479
|
chartOptions,
|
|
2235
2480
|
reserveActions = false,
|
|
2236
2481
|
} = cfg
|
|
2237
|
-
const
|
|
2482
|
+
const locale = resolveLocale(cfg.locale)
|
|
2483
|
+
const text = chartText(locale)
|
|
2484
|
+
const provisionalLabel = cfg.provisionalLabel ?? text.provisional
|
|
2238
2485
|
|
|
2239
2486
|
const stacked = kind !== "line" && type !== "default"
|
|
2240
2487
|
const percent = kind !== "line" && type === "percent"
|
|
@@ -2383,9 +2630,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2383
2630
|
// `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
|
|
2384
2631
|
const perBarTarget = kind === "bar" && Boolean(targetKey)
|
|
2385
2632
|
const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
|
|
2386
|
-
const targetText = hasTarget ? (targetLabel ??
|
|
2633
|
+
const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
|
|
2387
2634
|
const highlightText = (value: number) =>
|
|
2388
|
-
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${
|
|
2635
|
+
shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
|
|
2389
2636
|
/** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
|
|
2390
2637
|
const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
|
|
2391
2638
|
if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
|
|
@@ -2411,7 +2658,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2411
2658
|
// One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
|
|
2412
2659
|
const barTargets: (number | null)[] | null =
|
|
2413
2660
|
perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
|
|
2414
|
-
const barTargetName = targetLabel ??
|
|
2661
|
+
const barTargetName = targetLabel ?? text.target
|
|
2415
2662
|
const barTargetColor = labelColor ?? guideColor
|
|
2416
2663
|
|
|
2417
2664
|
const markPointFor = (c: number, color: string | undefined) =>
|
|
@@ -2625,7 +2872,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
2625
2872
|
}
|
|
2626
2873
|
|
|
2627
2874
|
/* -- forecast: dashed continuation (+ optional band) of one series */
|
|
2628
|
-
const forecastLabel = forecast?.label ??
|
|
2875
|
+
const forecastLabel = forecast?.label ?? text.forecast
|
|
2629
2876
|
const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
|
|
2630
2877
|
const forecastValues: (number | null)[] = []
|
|
2631
2878
|
if (forecast) {
|
|
@@ -3101,7 +3348,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3101
3348
|
})
|
|
3102
3349
|
}
|
|
3103
3350
|
if (stackTotals && i <= lastActual && rows.length > 1) {
|
|
3104
|
-
rows.push({ name:
|
|
3351
|
+
rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3105
3352
|
}
|
|
3106
3353
|
const provisional = provStart !== -1 && i >= provStart && i <= lastActual
|
|
3107
3354
|
if (provisional) {
|
|
@@ -3117,19 +3364,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
|
|
|
3117
3364
|
...noteFor(at),
|
|
3118
3365
|
})
|
|
3119
3366
|
})
|
|
3120
|
-
if (stackTotals && rows.length > 1) rows.push({ name:
|
|
3367
|
+
if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
|
|
3121
3368
|
}
|
|
3122
3369
|
const title = tooltipTitles[i] ?? labels[i] ?? ""
|
|
3123
3370
|
const lines: string[] = []
|
|
3124
3371
|
if (barTargets && i <= lastActual) {
|
|
3125
3372
|
const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
|
|
3126
|
-
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
|
|
3373
|
+
const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
|
|
3127
3374
|
if (note) lines.push(note)
|
|
3128
3375
|
}
|
|
3129
3376
|
for (const note of notes) {
|
|
3130
3377
|
if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
|
|
3131
3378
|
}
|
|
3132
|
-
return tooltipHtml(provisional ?
|
|
3379
|
+
return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
|
|
3133
3380
|
},
|
|
3134
3381
|
},
|
|
3135
3382
|
xAxis: horizontalBars ? valueAxis : categoryAxis,
|
|
@@ -3221,8 +3468,6 @@ interface DonutOptionSlice {
|
|
|
3221
3468
|
/** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
|
|
3222
3469
|
const DONUT_OUTSIDE_LABELS = 2
|
|
3223
3470
|
|
|
3224
|
-
const DEFAULT_TOTAL_LABEL = "合計"
|
|
3225
|
-
|
|
3226
3471
|
const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
|
|
3227
3472
|
|
|
3228
3473
|
/** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
|
|
@@ -3255,15 +3500,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
|
|
|
3255
3500
|
}
|
|
3256
3501
|
|
|
3257
3502
|
/**
|
|
3258
|
-
* The default center label: what is totalled (「合計」), the formatted total
|
|
3503
|
+
* The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
|
|
3259
3504
|
* (`amount`, the big line), the unit (a small line under it) and both as one
|
|
3260
3505
|
* string (`value`, 「9.2万 セッション」).
|
|
3261
3506
|
*/
|
|
3262
3507
|
function donutCenterText(
|
|
3263
3508
|
total: number,
|
|
3264
3509
|
valueFormatter: (value: number) => string,
|
|
3265
|
-
{ totalLabel
|
|
3510
|
+
{ totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
|
|
3266
3511
|
): { label: string; value: string; amount: string; unit?: string } {
|
|
3512
|
+
totalLabel ??= chartText(locale).total
|
|
3267
3513
|
const amount = valueFormatter(total)
|
|
3268
3514
|
return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
|
|
3269
3515
|
}
|
|
@@ -3491,8 +3737,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
|
|
|
3491
3737
|
|
|
3492
3738
|
/* ------------------------------------------------------------ provisional */
|
|
3493
3739
|
|
|
3494
|
-
/** The default label for points that are not final yet. */
|
|
3495
|
-
|
|
3740
|
+
/** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
|
|
3741
|
+
function defaultProvisionalLabel(locale?: string): string {
|
|
3742
|
+
return chartText(locale).provisional
|
|
3743
|
+
}
|
|
3496
3744
|
|
|
3497
3745
|
/**
|
|
3498
3746
|
* Position of the first provisional point: the first entry of `indexValues`
|
|
@@ -3572,6 +3820,11 @@ export interface BarListProps<T extends object = BarListItem>
|
|
|
3572
3820
|
ariaLabel?: string
|
|
3573
3821
|
}
|
|
3574
3822
|
|
|
3823
|
+
const TEXT = {
|
|
3824
|
+
ja: { item: "項目", value: "値" },
|
|
3825
|
+
en: { item: "Item", value: "Value" },
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3575
3828
|
/** Row height (h-8) + gap (space-y-1.5): the skeleton / empty height for `n` rows. */
|
|
3576
3829
|
const listHeight = (n: number) => n * 32 + Math.max(0, n - 1) * 6
|
|
3577
3830
|
|
|
@@ -3614,17 +3867,19 @@ function BarListImpl<T extends object>(
|
|
|
3614
3867
|
return () => cancelAnimationFrame(frame)
|
|
3615
3868
|
}, [showAnimation])
|
|
3616
3869
|
|
|
3870
|
+
const locale = useLocale()
|
|
3871
|
+
const t = TEXT[uiLanguage(locale)]
|
|
3617
3872
|
const maxItems = barListOptions?.maxItems ?? MAX_CHART_SERIES
|
|
3618
3873
|
const token = Array.isArray(colors) ? colors[0] : colors
|
|
3619
3874
|
const barColor = token ?? color ?? "chart-1"
|
|
3620
3875
|
|
|
3621
|
-
const formatter = resolveValueFormatter(valueFormatter ?? formatValue, format)
|
|
3876
|
+
const formatter = resolveValueFormatter(valueFormatter ?? formatValue, format, locale)
|
|
3622
3877
|
const baseRows = barListRows(data, {
|
|
3623
3878
|
index,
|
|
3624
3879
|
category: categories?.[0],
|
|
3625
3880
|
sortOrder: barListOptions?.sortOrder ?? sortOrder ?? "descending",
|
|
3626
3881
|
maxItems,
|
|
3627
|
-
otherLabel: barListOptions?.otherLabel,
|
|
3882
|
+
otherLabel: barListOptions?.otherLabel ?? otherSeriesLabel(locale),
|
|
3628
3883
|
exclude: barListOptions?.exclude,
|
|
3629
3884
|
})
|
|
3630
3885
|
const rows = baseRows.map((row) => {
|
|
@@ -3643,7 +3898,7 @@ function BarListImpl<T extends object>(
|
|
|
3643
3898
|
"name",
|
|
3644
3899
|
["value"],
|
|
3645
3900
|
formatter,
|
|
3646
|
-
{ name: (index && labels?.[index]) ||
|
|
3901
|
+
{ name: (index && labels?.[index]) || t.item, value: (categories?.[0] && labels?.[categories[0]]) || t.value },
|
|
3647
3902
|
)
|
|
3648
3903
|
|
|
3649
3904
|
const list = (
|