@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/heatmap.tsx
CHANGED
|
@@ -27,11 +27,143 @@ import { ChartFrame } from "@squadbase/vantage/components"
|
|
|
27
27
|
import { EChart } from "@squadbase/vantage/components"
|
|
28
28
|
import { EMPTY_VALUE, type ValueFormat } from "@squadbase/vantage/components"
|
|
29
29
|
import type { ChartActionsOptions, ChartEmptyState, ChartRootAttributes, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
|
|
30
|
+
import { useSyncExternalStore } from "react"
|
|
30
31
|
import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
|
|
31
32
|
import type { ChartTone } from "@squadbase/vantage/components"
|
|
32
|
-
import {
|
|
33
|
+
import { formatValue, toValueFormatter } from "@squadbase/vantage/components"
|
|
33
34
|
import type { ChartColor, ChartColors } from "@squadbase/vantage/components"
|
|
34
35
|
|
|
36
|
+
/* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The display language of the parts and the defaults that follow it.
|
|
40
|
+
*
|
|
41
|
+
* The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
|
|
42
|
+
* Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
|
|
43
|
+
* (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
|
|
44
|
+
*
|
|
45
|
+
* Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
|
|
46
|
+
* and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/** The language of the built-in wording: Japanese or English. */
|
|
50
|
+
type UiLanguage = "ja" | "en"
|
|
51
|
+
|
|
52
|
+
/** The locale used when `<html lang>` gives none (and on the server). */
|
|
53
|
+
const FALLBACK_LOCALE = "en-US"
|
|
54
|
+
|
|
55
|
+
/** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
|
|
56
|
+
function uiLanguage(locale: string | null | undefined): UiLanguage {
|
|
57
|
+
return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
|
|
61
|
+
function normalizeLocale(tag: string | null | undefined): string {
|
|
62
|
+
const value = tag?.trim()
|
|
63
|
+
if (!value) return FALLBACK_LOCALE
|
|
64
|
+
try {
|
|
65
|
+
return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
|
|
66
|
+
} catch {
|
|
67
|
+
return FALLBACK_LOCALE
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
|
|
73
|
+
* invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
|
|
74
|
+
*/
|
|
75
|
+
function documentLocale(): string {
|
|
76
|
+
if (typeof document === "undefined") return FALLBACK_LOCALE
|
|
77
|
+
return normalizeLocale(document.documentElement?.getAttribute("lang"))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `locale` when given, else the page's locale (`documentLocale()`). */
|
|
81
|
+
function resolveLocale(locale?: string | null): string {
|
|
82
|
+
return locale || documentLocale()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Picks the wording table of the locale's language. */
|
|
86
|
+
function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
|
|
87
|
+
return table[uiLanguage(resolveLocale(locale))]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
|
|
91
|
+
function defaultCurrency(locale?: string | null): string {
|
|
92
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
|
|
96
|
+
function defaultFiscalYearStartMonth(locale?: string | null): number {
|
|
97
|
+
return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
|
|
101
|
+
type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
102
|
+
|
|
103
|
+
/** Locales whose week starts on Sunday when the runtime has no week data. */
|
|
104
|
+
const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
|
|
108
|
+
* (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
|
|
109
|
+
* Monday for the rest.
|
|
110
|
+
*/
|
|
111
|
+
function weekStartsOn(locale?: string | null): WeekDay {
|
|
112
|
+
const tag = normalizeLocale(resolveLocale(locale))
|
|
113
|
+
if (uiLanguage(tag) === "ja") return 1
|
|
114
|
+
try {
|
|
115
|
+
const info = new Intl.Locale(tag) as Intl.Locale & {
|
|
116
|
+
getWeekInfo?: () => { firstDay: number }
|
|
117
|
+
weekInfo?: { firstDay: number }
|
|
118
|
+
}
|
|
119
|
+
const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
|
|
120
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
|
|
121
|
+
} catch {
|
|
122
|
+
// Fall through to the table below.
|
|
123
|
+
}
|
|
124
|
+
return SUNDAY_FIRST.has(tag) ? 0 : 1
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
|
|
128
|
+
|
|
129
|
+
const listeners = new Set<() => void>()
|
|
130
|
+
let observer: MutationObserver | null = null
|
|
131
|
+
|
|
132
|
+
/** One observer on `<html lang>` for every subscribed part. */
|
|
133
|
+
function subscribe(onChange: () => void): () => void {
|
|
134
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
|
|
135
|
+
listeners.add(onChange)
|
|
136
|
+
if (!observer) {
|
|
137
|
+
observer = new MutationObserver(() => {
|
|
138
|
+
for (const listener of [...listeners]) listener()
|
|
139
|
+
})
|
|
140
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
|
|
141
|
+
}
|
|
142
|
+
return () => {
|
|
143
|
+
listeners.delete(onChange)
|
|
144
|
+
if (listeners.size === 0 && observer) {
|
|
145
|
+
observer.disconnect()
|
|
146
|
+
observer = null
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const serverLocale = () => FALLBACK_LOCALE
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
|
|
155
|
+
* hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
|
|
156
|
+
*/
|
|
157
|
+
function useDocumentLocale(): string {
|
|
158
|
+
return useSyncExternalStore(subscribe, documentLocale, serverLocale)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
|
|
162
|
+
function useLocale(locale?: string | null): string {
|
|
163
|
+
const page = useDocumentLocale()
|
|
164
|
+
return locale || page
|
|
165
|
+
}
|
|
166
|
+
|
|
35
167
|
/* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
|
|
36
168
|
|
|
37
169
|
/**
|
|
@@ -535,9 +667,10 @@ function resolveValueFormatter(
|
|
|
535
667
|
function resolveAxisFormatter(
|
|
536
668
|
valueFormatter: ValueFormatter | undefined,
|
|
537
669
|
format: ValueFormat | undefined,
|
|
538
|
-
|
|
670
|
+
localeArg?: string,
|
|
539
671
|
): ValueFormatter {
|
|
540
672
|
if (valueFormatter) return valueFormatter
|
|
673
|
+
const locale = resolveLocale(localeArg)
|
|
541
674
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
542
675
|
if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
|
|
543
676
|
const currency = "currency" in spec ? spec.currency : undefined
|
|
@@ -546,40 +679,75 @@ function resolveAxisFormatter(
|
|
|
546
679
|
return (value) => formatValue(value, short, locale)
|
|
547
680
|
}
|
|
548
681
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
]
|
|
682
|
+
interface CompactUnit {
|
|
683
|
+
token: string
|
|
684
|
+
size: number
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const compactUnitCache = new Map<string, CompactUnit[] | null>()
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
|
|
691
|
+
* `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
|
|
692
|
+
* word (`1,5 Mio.`), where a token cannot be swapped in place.
|
|
693
|
+
*/
|
|
694
|
+
function compactUnits(locale: string): CompactUnit[] | null {
|
|
695
|
+
const cached = compactUnitCache.get(locale)
|
|
696
|
+
if (cached !== undefined) return cached
|
|
697
|
+
let units: CompactUnit[] | null = []
|
|
698
|
+
try {
|
|
699
|
+
const compact = new Intl.NumberFormat(locale, { notation: "compact" })
|
|
700
|
+
const seen = new Set<string>()
|
|
701
|
+
for (let exp = 3; exp <= 15; exp++) {
|
|
702
|
+
const parts = compact.formatToParts(10 ** exp)
|
|
703
|
+
const at = parts.findIndex((p) => p.type === "compact")
|
|
704
|
+
if (at === -1) continue
|
|
705
|
+
if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
|
|
706
|
+
units = null
|
|
707
|
+
break
|
|
708
|
+
}
|
|
709
|
+
const token = parts[at]!.value
|
|
710
|
+
if (seen.has(token)) continue
|
|
711
|
+
seen.add(token)
|
|
712
|
+
units.push({ token, size: 10 ** exp })
|
|
713
|
+
}
|
|
714
|
+
} catch {
|
|
715
|
+
units = null
|
|
716
|
+
}
|
|
717
|
+
const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
|
|
718
|
+
compactUnitCache.set(locale, sorted)
|
|
719
|
+
return sorted
|
|
720
|
+
}
|
|
555
721
|
|
|
556
|
-
const
|
|
722
|
+
const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
557
723
|
|
|
558
724
|
/**
|
|
559
725
|
* The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
|
|
560
|
-
* that largest tick, so
|
|
561
|
-
* Only the
|
|
562
|
-
* any other format —
|
|
726
|
+
* that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
|
|
727
|
+
* `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
|
|
728
|
+
* no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
|
|
729
|
+
* unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
|
|
563
730
|
*/
|
|
564
731
|
function resolveAxisTickFormatter(
|
|
565
732
|
valueFormatter: ValueFormatter | undefined,
|
|
566
733
|
format: ValueFormat | undefined,
|
|
567
|
-
|
|
734
|
+
localeArg?: string,
|
|
568
735
|
): (top: number) => ValueFormatter {
|
|
736
|
+
const locale = resolveLocale(localeArg)
|
|
569
737
|
const base = resolveAxisFormatter(valueFormatter, format, locale)
|
|
570
738
|
const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
if (!aligns) return () => base
|
|
739
|
+
const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
|
|
740
|
+
if (!units) return () => base
|
|
574
741
|
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
|
|
742
|
+
const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
|
|
575
743
|
return (top) => {
|
|
576
|
-
const unit =
|
|
744
|
+
const unit = units.find((u) => Math.abs(top) >= u.size)
|
|
577
745
|
if (!unit) return base
|
|
578
746
|
return (value) => {
|
|
579
747
|
const text = base(value)
|
|
580
748
|
if (value === 0 || !Number.isFinite(value)) return text
|
|
581
|
-
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.
|
|
582
|
-
return text.replace(
|
|
749
|
+
const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
|
|
750
|
+
return text.replace(numberToken, () => scaled)
|
|
583
751
|
}
|
|
584
752
|
}
|
|
585
753
|
}
|
|
@@ -608,8 +776,20 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
608
776
|
/** How many categories a chart colours individually; the rest fold into「その他」. */
|
|
609
777
|
const MAX_CHART_SERIES = 5
|
|
610
778
|
|
|
611
|
-
/** The series name the folded categories get. */
|
|
612
|
-
const
|
|
779
|
+
/** The series name the folded categories get, by language. */
|
|
780
|
+
const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
|
|
781
|
+
|
|
782
|
+
/** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
|
|
783
|
+
function otherSeriesLabel(locale?: string): string {
|
|
784
|
+
return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
|
|
788
|
+
|
|
789
|
+
/** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
|
|
790
|
+
function isOtherName(name: string | null | undefined): boolean {
|
|
791
|
+
return typeof name === "string" && OTHER_NAMES.has(name)
|
|
792
|
+
}
|
|
613
793
|
|
|
614
794
|
interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
615
795
|
/** The `n` largest items, in their given order. All items when there are `n` or fewer. */
|
|
@@ -629,7 +809,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
|
|
|
629
809
|
function aggregateTopN<Item extends { name: string; value: number }>(
|
|
630
810
|
items: readonly Item[],
|
|
631
811
|
n: number = MAX_CHART_SERIES,
|
|
632
|
-
otherLabel: string =
|
|
812
|
+
otherLabel: string = otherSeriesLabel(),
|
|
633
813
|
): AggregatedItems<Item> {
|
|
634
814
|
if (items.length <= n) return { top: [...items], rest: [], other: null }
|
|
635
815
|
const ranked = items
|
|
@@ -658,7 +838,7 @@ function aggregateSeries(
|
|
|
658
838
|
data: readonly Record<string, unknown>[],
|
|
659
839
|
categories: readonly string[],
|
|
660
840
|
n: number = MAX_CHART_SERIES,
|
|
661
|
-
otherLabel: string =
|
|
841
|
+
otherLabel: string = otherSeriesLabel(),
|
|
662
842
|
): AggregatedSeries {
|
|
663
843
|
const totals = categories.map((name) => ({
|
|
664
844
|
name,
|
|
@@ -690,12 +870,14 @@ function aggregateSeriesKeeping(
|
|
|
690
870
|
categories: readonly string[],
|
|
691
871
|
keep: string | undefined,
|
|
692
872
|
n: number = MAX_CHART_SERIES,
|
|
873
|
+
otherLabel: string = otherSeriesLabel(),
|
|
693
874
|
): AggregatedSeries {
|
|
694
|
-
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
|
|
875
|
+
if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
|
|
695
876
|
const aggregated = aggregateSeries(
|
|
696
877
|
data,
|
|
697
878
|
categories.filter((c) => c !== keep),
|
|
698
879
|
n,
|
|
880
|
+
otherLabel,
|
|
699
881
|
)
|
|
700
882
|
return { ...aggregated, categories: [...aggregated.categories, keep] }
|
|
701
883
|
}
|
|
@@ -731,7 +913,7 @@ function seriesColorsWithComparison(
|
|
|
731
913
|
* Colour per drawn series: the record's token for that category, else the
|
|
732
914
|
* array's token at the category's position in `allCategories` (cycled), else
|
|
733
915
|
* the token palette by drawn position. `otherCategory` — and a category the
|
|
734
|
-
* data itself names
|
|
916
|
+
* data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
|
|
735
917
|
*/
|
|
736
918
|
function seriesColorsFor(
|
|
737
919
|
categories: readonly string[],
|
|
@@ -743,7 +925,7 @@ function seriesColorsFor(
|
|
|
743
925
|
const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
|
|
744
926
|
const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
|
|
745
927
|
return categories.map((category, i) => {
|
|
746
|
-
if ((otherCategory !== null && category === otherCategory) || category
|
|
928
|
+
if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
|
|
747
929
|
const fromRecord = record?.[category]
|
|
748
930
|
if (fromRecord) return fromRecord
|
|
749
931
|
if (list && list.length > 0) {
|
|
@@ -790,7 +972,7 @@ interface DonutSlicesConfig {
|
|
|
790
972
|
*/
|
|
791
973
|
function donutSlices(
|
|
792
974
|
data: readonly Record<string, unknown>[],
|
|
793
|
-
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel =
|
|
975
|
+
{ index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
|
|
794
976
|
): DonutSlice[] {
|
|
795
977
|
const items = data
|
|
796
978
|
.map((row, i) => {
|
|
@@ -803,7 +985,8 @@ function donutSlices(
|
|
|
803
985
|
const slices: DonutSlice[] = top.map((item, rank) => ({
|
|
804
986
|
name: item.name,
|
|
805
987
|
value: item.value,
|
|
806
|
-
|
|
988
|
+
// A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
|
|
989
|
+
color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
|
|
807
990
|
isOther: false,
|
|
808
991
|
row: item.row,
|
|
809
992
|
}))
|
|
@@ -876,7 +1059,7 @@ interface BarListRowsConfig extends BarListOptions {
|
|
|
876
1059
|
*/
|
|
877
1060
|
function barListRows<Row extends object>(
|
|
878
1061
|
data: readonly Row[],
|
|
879
|
-
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel =
|
|
1062
|
+
{ index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
|
|
880
1063
|
): BarListRow<Row>[] {
|
|
881
1064
|
const items = data.map((row): BarListRow<Row> => {
|
|
882
1065
|
const cells = row as Record<string, unknown>
|
|
@@ -922,19 +1105,21 @@ const BAR_LIST_OTHER_TINT = 30
|
|
|
922
1105
|
|
|
923
1106
|
/**
|
|
924
1107
|
* The fill of each BarList row: all real rows in the same tint of `color`
|
|
925
|
-
* (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
|
|
1108
|
+
* (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
|
|
926
1109
|
* (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
|
|
927
1110
|
* ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
|
|
928
1111
|
* for that row. A tint, not the solid token, so the label printed over the bar
|
|
929
1112
|
* keeps its contrast.
|
|
930
1113
|
*/
|
|
931
1114
|
function barListFills(
|
|
932
|
-
rows: readonly Pick<BarListRow, "isOther" | "color">[],
|
|
1115
|
+
rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
|
|
933
1116
|
color: ChartColor,
|
|
934
1117
|
otherColor: string,
|
|
935
1118
|
): string[] {
|
|
936
1119
|
return rows.map((row) =>
|
|
937
|
-
row.isOther
|
|
1120
|
+
row.isOther || isOtherName(row.name)
|
|
1121
|
+
? tintCss(otherColor, BAR_LIST_OTHER_TINT)
|
|
1122
|
+
: tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
|
|
938
1123
|
)
|
|
939
1124
|
}
|
|
940
1125
|
|
|
@@ -1150,11 +1335,30 @@ interface ChartSummaryInput {
|
|
|
1150
1335
|
format?: ValueFormat
|
|
1151
1336
|
/** Wins over `format`. */
|
|
1152
1337
|
valueFormatter?: ValueFormatter
|
|
1338
|
+
/** Language of the sentence. Default the page's locale (`<html lang>`). */
|
|
1339
|
+
locale?: string
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const SUMMARY_TEXT = {
|
|
1343
|
+
ja: {
|
|
1344
|
+
up: "、前の点より増加",
|
|
1345
|
+
down: "、前の点より減少",
|
|
1346
|
+
flat: "、前の点と横ばい",
|
|
1347
|
+
latest: (label: string, value: string) => `最新 ${label} は ${value}`,
|
|
1348
|
+
top: (name: string, share: string) => `最大は ${name}(${share})`,
|
|
1349
|
+
},
|
|
1350
|
+
en: {
|
|
1351
|
+
up: ", up from the previous point",
|
|
1352
|
+
down: ", down from the previous point",
|
|
1353
|
+
flat: ", flat from the previous point",
|
|
1354
|
+
latest: (label: string, value: string) => `Latest ${label}: ${value}`,
|
|
1355
|
+
top: (name: string, share: string) => `Largest: ${name} (${share})`,
|
|
1356
|
+
},
|
|
1153
1357
|
}
|
|
1154
1358
|
|
|
1155
1359
|
/**
|
|
1156
1360
|
* One-sentence summary built from the rows, for `summary="auto"`:
|
|
1157
|
-
* 「<title>: 最新 <index> は
|
|
1361
|
+
* 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
|
|
1158
1362
|
* computed from the last two non-empty points of the first series — never
|
|
1159
1363
|
* written by hand, so it cannot contradict the data. `undefined` when there is
|
|
1160
1364
|
* no usable point.
|
|
@@ -1170,26 +1374,29 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
|
|
|
1170
1374
|
}
|
|
1171
1375
|
const latest = points[points.length - 1]
|
|
1172
1376
|
if (!latest) return undefined
|
|
1173
|
-
const
|
|
1377
|
+
const locale = resolveLocale(input.locale)
|
|
1378
|
+
const t = SUMMARY_TEXT[uiLanguage(locale)]
|
|
1379
|
+
const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
|
|
1174
1380
|
const prev = points[points.length - 2]
|
|
1175
|
-
const direction = !prev ? "" : latest.value > prev.value ?
|
|
1381
|
+
const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
|
|
1176
1382
|
const head = input.title ? `${input.title}: ` : ""
|
|
1177
|
-
return `${head}
|
|
1383
|
+
return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
|
|
1178
1384
|
}
|
|
1179
1385
|
|
|
1180
1386
|
/**
|
|
1181
|
-
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
|
|
1182
|
-
* (largest first). `undefined` when there is nothing to describe.
|
|
1387
|
+
* `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
|
|
1388
|
+
* (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
|
|
1183
1389
|
*/
|
|
1184
1390
|
function buildShareSummary(
|
|
1185
1391
|
slices: readonly { name: string; value: number }[],
|
|
1186
1392
|
title?: string,
|
|
1393
|
+
locale?: string,
|
|
1187
1394
|
): string | undefined {
|
|
1188
1395
|
const total = slices.reduce((sum, s) => sum + s.value, 0)
|
|
1189
1396
|
const top = slices[0]
|
|
1190
1397
|
if (!top || total <= 0) return undefined
|
|
1191
1398
|
const share = `${Math.round((top.value / total) * 1000) / 10}%`
|
|
1192
|
-
return `${title ? `${title}: ` : ""}
|
|
1399
|
+
return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
|
|
1193
1400
|
}
|
|
1194
1401
|
|
|
1195
1402
|
/* ---------- end of inlined code ---------- */
|
|
@@ -1455,6 +1662,25 @@ export function heatmapCellFontSize(
|
|
|
1455
1662
|
return (text * 10) / 12 + HEATMAP_CELL_PADDING <= cell ? 10 : null
|
|
1456
1663
|
}
|
|
1457
1664
|
|
|
1665
|
+
const TEXT = {
|
|
1666
|
+
ja: {
|
|
1667
|
+
join: "・",
|
|
1668
|
+
flagged: "要注意",
|
|
1669
|
+
item: "項目",
|
|
1670
|
+
top: (y: string, x: string, value: string) => `最大は ${y} ${x} の ${value}`,
|
|
1671
|
+
chart: (y: string, x: string) => `${y}×${x}のヒートマップ`,
|
|
1672
|
+
},
|
|
1673
|
+
en: {
|
|
1674
|
+
join: ", ",
|
|
1675
|
+
flagged: "Needs attention",
|
|
1676
|
+
item: "Item",
|
|
1677
|
+
top: (y: string, x: string, value: string) => `Highest: ${y} ${x} (${value})`,
|
|
1678
|
+
chart: (y: string, x: string) => `Heatmap of ${y} × ${x}`,
|
|
1679
|
+
},
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
const textOf = (locale?: string) => TEXT[uiLanguage(resolveLocale(locale))]
|
|
1683
|
+
|
|
1458
1684
|
export interface HeatmapOptionConfig {
|
|
1459
1685
|
model: HeatmapModel
|
|
1460
1686
|
x: string
|
|
@@ -1479,6 +1705,8 @@ export interface HeatmapOptionConfig {
|
|
|
1479
1705
|
/** The plot's width (px), when known: the numbers shrink (or go) when the cells are too narrow for them. */
|
|
1480
1706
|
plotWidth?: number | null
|
|
1481
1707
|
chartOptions?: EChartsOption
|
|
1708
|
+
/** Language of the tooltip. Default the page's locale (`<html lang>`). */
|
|
1709
|
+
locale?: string
|
|
1482
1710
|
}
|
|
1483
1711
|
|
|
1484
1712
|
/**
|
|
@@ -1490,6 +1718,7 @@ export interface HeatmapOptionConfig {
|
|
|
1490
1718
|
*/
|
|
1491
1719
|
export function buildHeatmapOption(config: HeatmapOptionConfig): EChartsOption {
|
|
1492
1720
|
const { model, labels, valueFormatter } = config
|
|
1721
|
+
const t = textOf(config.locale)
|
|
1493
1722
|
const label = (key: string) => labels?.[key] ?? key
|
|
1494
1723
|
const present = model.cells.filter((c) => c.value !== null)
|
|
1495
1724
|
const missing = model.cells.filter((c) => c.value === null)
|
|
@@ -1507,9 +1736,9 @@ export function buildHeatmapOption(config: HeatmapOptionConfig): EChartsOption {
|
|
|
1507
1736
|
const n = cell.row ? toChartNumber(cell.row[key]) : null
|
|
1508
1737
|
return `${escapeHtml(label(key))} ${n === null ? EMPTY_VALUE : escapeHtml(valueFormatter(n))}`
|
|
1509
1738
|
})
|
|
1510
|
-
.join(
|
|
1511
|
-
const flag = cell.flagged ?
|
|
1512
|
-
return `${head} ${main}${extras ?
|
|
1739
|
+
.join(t.join)
|
|
1740
|
+
const flag = cell.flagged ? `${t.join}${t.flagged}` : ""
|
|
1741
|
+
return `${head} ${main}${extras ? `${t.join}${extras}` : ""}${flag}`
|
|
1513
1742
|
}
|
|
1514
1743
|
|
|
1515
1744
|
const cellFont = config.plotWidth
|
|
@@ -1626,6 +1855,7 @@ export function heatmapDataTable(
|
|
|
1626
1855
|
names: { x: string; y: string },
|
|
1627
1856
|
formatter: ValueFormatter,
|
|
1628
1857
|
labels?: Record<string, string>,
|
|
1858
|
+
locale?: string,
|
|
1629
1859
|
): ChartDataTable {
|
|
1630
1860
|
const label = (key: string) => labels?.[key] ?? key
|
|
1631
1861
|
const rows = model.ys.map((yk, yi) => [
|
|
@@ -1635,17 +1865,26 @@ export function heatmapDataTable(
|
|
|
1635
1865
|
return v === null || v === undefined ? EMPTY_VALUE : formatter(v)
|
|
1636
1866
|
}),
|
|
1637
1867
|
])
|
|
1638
|
-
return { caption: `${label(names.y)}×${label(names.x)}`, columns: [labels?.[names.y] ??
|
|
1868
|
+
return { caption: `${label(names.y)}×${label(names.x)}`, columns: [labels?.[names.y] ?? textOf(locale).item, ...model.xs.map(label)], rows }
|
|
1639
1869
|
}
|
|
1640
1870
|
|
|
1641
|
-
/**
|
|
1642
|
-
|
|
1871
|
+
/**
|
|
1872
|
+
* `summary="auto"`: the strongest cell, e.g. 「最大は 月曜 9時台 の 12件」 / "Highest: Mon 9am (12)". `undefined` with no usable cell.
|
|
1873
|
+
* `locale` picks the language; default the page's (`<html lang>`).
|
|
1874
|
+
*/
|
|
1875
|
+
export function buildHeatmapSummary(
|
|
1876
|
+
model: HeatmapModel,
|
|
1877
|
+
formatter: ValueFormatter,
|
|
1878
|
+
title?: string,
|
|
1879
|
+
labels?: Record<string, string>,
|
|
1880
|
+
locale?: string,
|
|
1881
|
+
): string | undefined {
|
|
1643
1882
|
let best: HeatmapCell | undefined
|
|
1644
1883
|
for (const cell of model.cells) if (cell.value !== null && (!best || cell.value > (best.value as number))) best = cell
|
|
1645
1884
|
if (!best) return undefined
|
|
1646
1885
|
const label = (key: string) => labels?.[key] ?? key
|
|
1647
1886
|
const head = title ? `${title}: ` : ""
|
|
1648
|
-
return `${head}
|
|
1887
|
+
return `${head}${textOf(locale).top(label(model.ys[best.yi]!), label(model.xs[best.xi]!), formatter(best.value as number))}`
|
|
1649
1888
|
}
|
|
1650
1889
|
|
|
1651
1890
|
/* -------------------------------------------------------------- component */
|
|
@@ -1693,7 +1932,8 @@ function HeatmapImpl<T extends Record<string, unknown>>(
|
|
|
1693
1932
|
ref: ForwardedRef<HTMLDivElement>,
|
|
1694
1933
|
) {
|
|
1695
1934
|
const { ref: frameRef, surface, width: plotWidth, tokens } = useChartSurface(ref)
|
|
1696
|
-
const
|
|
1935
|
+
const locale = useLocale()
|
|
1936
|
+
const formatter = useMemo(() => resolveValueFormatter(valueFormatter, format ?? "number", locale), [valueFormatter, format, locale])
|
|
1697
1937
|
const model = useMemo(
|
|
1698
1938
|
() => buildHeatmapModel({ data, x, y, value, xLabels, yLabels, flag, scale, minValue, maxValue }),
|
|
1699
1939
|
[data, x, y, value, xLabels, yLabels, flag, scale?.min, scale?.max, minValue, maxValue],
|
|
@@ -1719,15 +1959,16 @@ function HeatmapImpl<T extends Record<string, unknown>>(
|
|
|
1719
1959
|
showTooltip,
|
|
1720
1960
|
plotWidth,
|
|
1721
1961
|
chartOptions,
|
|
1962
|
+
locale,
|
|
1722
1963
|
}),
|
|
1723
|
-
[model, x, y, value, colors, surface, tokens, labels, formatter, details, showValues, showTooltip, plotWidth, chartOptions],
|
|
1964
|
+
[model, x, y, value, colors, surface, tokens, labels, formatter, details, showValues, showTooltip, plotWidth, chartOptions, locale],
|
|
1724
1965
|
)
|
|
1725
|
-
const dataTable = useMemo(() => heatmapDataTable(model, { x, y }, formatter, labels), [model, x, y, formatter, labels])
|
|
1966
|
+
const dataTable = useMemo(() => heatmapDataTable(model, { x, y }, formatter, labels, locale), [model, x, y, formatter, labels, locale])
|
|
1726
1967
|
const resolvedSummary = useMemo(
|
|
1727
|
-
() => (summary === "auto" ? buildHeatmapSummary(model, formatter, title, labels) : summary),
|
|
1728
|
-
[summary, model, formatter, title, labels],
|
|
1968
|
+
() => (summary === "auto" ? buildHeatmapSummary(model, formatter, title, labels, locale) : summary),
|
|
1969
|
+
[summary, model, formatter, title, labels, locale],
|
|
1729
1970
|
)
|
|
1730
|
-
const label = ariaLabel ??
|
|
1971
|
+
const label = ariaLabel ?? textOf(locale).chart(labels?.[y] ?? y, labels?.[x] ?? x)
|
|
1731
1972
|
const legend = showLegend ? (
|
|
1732
1973
|
<HeatmapLegend model={model} formatter={formatter} tone={tone} />
|
|
1733
1974
|
) : null
|