@cfasim-ui/docs 0.4.11 → 0.4.12
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/charts/BarChart/BarChart.md +54 -1
- package/charts/BarChart/BarChart.vue +180 -28
- package/charts/ChoroplethMap/ChoroplethMap.md +8 -2
- package/charts/ChoroplethMap/ChoroplethMap.vue +89 -7
- package/charts/LineChart/LineChart.md +138 -9
- package/charts/LineChart/LineChart.vue +295 -87
- package/charts/_shared/ChartAnnotations.vue +56 -17
- package/charts/_shared/annotations.ts +14 -9
- package/charts/_shared/chartProps.ts +39 -0
- package/charts/_shared/dateAxis.ts +507 -0
- package/charts/_shared/index.ts +22 -0
- package/charts/_shared/useChartFoundation.ts +3 -0
- package/charts/_shared/useChartPadding.ts +53 -5
- package/charts/hsa-mapping.ts +1 -0
- package/charts/index.ts +0 -1
- package/index.json +1 -1
- package/package.json +1 -1
- package/shared/useUrlParams.ts +203 -40
|
@@ -14,7 +14,23 @@ import {
|
|
|
14
14
|
makeTooltipValueFormatter,
|
|
15
15
|
ChartAnnotations,
|
|
16
16
|
INLINE_LEGEND_ROW_HEIGHT,
|
|
17
|
+
TITLE_LINE_HEIGHT,
|
|
18
|
+
TITLE_FONT_SIZE,
|
|
19
|
+
TITLE_FONT_WEIGHT,
|
|
20
|
+
AXIS_LABEL_FONT_SIZE,
|
|
21
|
+
TICK_LABEL_FONT_SIZE,
|
|
22
|
+
TICK_LABEL_OPACITY,
|
|
23
|
+
LEGEND_FONT_SIZE,
|
|
24
|
+
resolveLabelStyle,
|
|
25
|
+
parseDate,
|
|
26
|
+
isAllDates,
|
|
27
|
+
pickDateTicks,
|
|
28
|
+
formatDate,
|
|
17
29
|
type ChartData,
|
|
30
|
+
type LabelStyle,
|
|
31
|
+
type DateFormat,
|
|
32
|
+
type DateTickUnit,
|
|
33
|
+
type DateTimezone,
|
|
18
34
|
type ChartCommonProps,
|
|
19
35
|
type ChartHoverPayload,
|
|
20
36
|
type ChartTooltipBaseProps,
|
|
@@ -29,6 +45,13 @@ import {
|
|
|
29
45
|
*/
|
|
30
46
|
export type LineChartData = ChartData;
|
|
31
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Accepted shapes for an `x` array. Numeric arrays (typed or plain) plot
|
|
50
|
+
* as numbers; string-or-`Date` arrays trigger date-axis mode when every
|
|
51
|
+
* element parses. See the `timezone` prop and `dateAxis` module.
|
|
52
|
+
*/
|
|
53
|
+
export type LineChartXInput = LineChartData | readonly (string | Date)[];
|
|
54
|
+
|
|
32
55
|
export interface Series {
|
|
33
56
|
/**
|
|
34
57
|
* Y-values. One of `y` or `data` must be supplied; `y` wins if both
|
|
@@ -40,9 +63,10 @@ export interface Series {
|
|
|
40
63
|
/**
|
|
41
64
|
* Optional x-values, parallel to `y`/`data`. When set, the chart
|
|
42
65
|
* plots points at the given x positions (irregular spacing supported).
|
|
43
|
-
* When omitted, points are plotted at indices 0, 1, 2, ...
|
|
66
|
+
* When omitted, points are plotted at indices 0, 1, 2, ... Accepts
|
|
67
|
+
* date strings or `Date` objects to enable date-axis mode.
|
|
44
68
|
*/
|
|
45
|
-
x?:
|
|
69
|
+
x?: LineChartXInput;
|
|
46
70
|
color?: string;
|
|
47
71
|
dashed?: boolean;
|
|
48
72
|
strokeWidth?: number;
|
|
@@ -78,7 +102,7 @@ export interface Area {
|
|
|
78
102
|
upper: LineChartData;
|
|
79
103
|
lower: LineChartData;
|
|
80
104
|
/** Optional x-values parallel to `upper`/`lower`. See `Series.x`. */
|
|
81
|
-
x?:
|
|
105
|
+
x?: LineChartXInput;
|
|
82
106
|
color?: string;
|
|
83
107
|
opacity?: number;
|
|
84
108
|
}
|
|
@@ -104,6 +128,17 @@ export interface AreaSection {
|
|
|
104
128
|
dashed?: boolean;
|
|
105
129
|
/** Label placement: "below" (default) renders below chart, "inline" renders in legend row, false hides label */
|
|
106
130
|
legend?: "inline" | "below" | false;
|
|
131
|
+
/**
|
|
132
|
+
* Style for the area section's primary label text. Defaults: font-size
|
|
133
|
+
* 11, font-weight 600, color taken from the section's `color`.
|
|
134
|
+
*/
|
|
135
|
+
inlineLabelStyle?: LabelStyle;
|
|
136
|
+
/**
|
|
137
|
+
* Style for the area section's secondary description text. Defaults:
|
|
138
|
+
* font-size 11, currentColor at 0.6 opacity. Providing `color` drops
|
|
139
|
+
* the default opacity.
|
|
140
|
+
*/
|
|
141
|
+
inlineDescriptionStyle?: LabelStyle;
|
|
107
142
|
}
|
|
108
143
|
|
|
109
144
|
interface LineChartProps extends ChartCommonProps {
|
|
@@ -115,8 +150,9 @@ interface LineChartProps extends ChartCommonProps {
|
|
|
115
150
|
* Optional x-values paired with `y`/`data`. When provided, points
|
|
116
151
|
* are plotted at the given x positions instead of at their indices.
|
|
117
152
|
* Ignored when `series` is used — set `x` on each `Series` instead.
|
|
153
|
+
* Accepts date strings or `Date` objects to enable date-axis mode.
|
|
118
154
|
*/
|
|
119
|
-
x?:
|
|
155
|
+
x?: LineChartXInput;
|
|
120
156
|
series?: Series[];
|
|
121
157
|
areas?: Area[];
|
|
122
158
|
areaSections?: AreaSection[];
|
|
@@ -149,12 +185,24 @@ interface LineChartProps extends ChartCommonProps {
|
|
|
149
185
|
*/
|
|
150
186
|
yTicks?: number | number[];
|
|
151
187
|
/**
|
|
152
|
-
* Formatter for x-axis tick labels.
|
|
153
|
-
* format string, or a function
|
|
154
|
-
* is also supported for index-based
|
|
155
|
-
* `@cfasim-ui/shared
|
|
188
|
+
* Formatter for x-axis tick labels. On a numeric axis, accepts a
|
|
189
|
+
* preset name, a printf-style format string, or a function (the
|
|
190
|
+
* two-arg `(value, index)` form is also supported for index-based
|
|
191
|
+
* labels — see `formatNumber` in `@cfasim-ui/shared`). On a date
|
|
192
|
+
* axis (auto-detected when every x value parses as a date), accepts
|
|
193
|
+
* a `DateFormat` instead — see the `dateAxis` module.
|
|
194
|
+
*/
|
|
195
|
+
xTickFormat?:
|
|
196
|
+
| NumberFormat
|
|
197
|
+
| ((value: number, index: number) => string)
|
|
198
|
+
| DateFormat;
|
|
199
|
+
/**
|
|
200
|
+
* Timezone used when parsing offset-less ISO strings and rendering
|
|
201
|
+
* date-axis tick labels. `"utc"` (default) keeps visuals consistent
|
|
202
|
+
* across viewers; `"local"` uses the browser timezone. Ignored on a
|
|
203
|
+
* numeric axis.
|
|
156
204
|
*/
|
|
157
|
-
|
|
205
|
+
timezone?: DateTimezone;
|
|
158
206
|
/**
|
|
159
207
|
* Formatter for y-axis tick labels. Accepts a preset name, a printf-style
|
|
160
208
|
* format string, or a function. See `formatNumber` in `@cfasim-ui/shared`.
|
|
@@ -186,15 +234,49 @@ defineSlots<{
|
|
|
186
234
|
}>();
|
|
187
235
|
|
|
188
236
|
/**
|
|
189
|
-
* Internal series shape where `data` (y-values) is always resolved
|
|
190
|
-
* `
|
|
237
|
+
* Internal series shape where `data` (y-values) is always resolved and
|
|
238
|
+
* `x` is narrowed to numeric (date inputs have already been parsed to
|
|
239
|
+
* epoch-ms). `Series.y` takes precedence over `Series.data`.
|
|
191
240
|
*/
|
|
192
|
-
type ResolvedSeries = Omit<Series, "data" | "y"> & {
|
|
241
|
+
type ResolvedSeries = Omit<Series, "data" | "y" | "x"> & {
|
|
242
|
+
data: LineChartData;
|
|
243
|
+
x?: LineChartData;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/** Same shape as `Area` but with `x` narrowed to numeric post-parse. */
|
|
247
|
+
type ResolvedArea = Omit<Area, "x"> & { x?: LineChartData };
|
|
193
248
|
|
|
194
249
|
const EMPTY_DATA: readonly number[] = [];
|
|
195
250
|
|
|
196
|
-
|
|
197
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Yields every user-supplied `x` array on the component (top-level,
|
|
253
|
+
* per-series, per-area). Used both for date auto-detection and for the
|
|
254
|
+
* single-pass resolution that converts strings/Dates to numeric ms.
|
|
255
|
+
*/
|
|
256
|
+
function* xInputs(p: LineChartProps): Iterable<LineChartXInput> {
|
|
257
|
+
if (p.x && p.x.length > 0) yield p.x;
|
|
258
|
+
if (p.series) {
|
|
259
|
+
for (const s of p.series) if (s.x && s.x.length > 0) yield s.x;
|
|
260
|
+
}
|
|
261
|
+
if (p.areas) {
|
|
262
|
+
for (const a of p.areas) if (a.x && a.x.length > 0) yield a.x;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Convert an x input to numeric. Pre-parsed when `isDate` is true. */
|
|
267
|
+
function toNumericX(
|
|
268
|
+
x: LineChartXInput | undefined,
|
|
269
|
+
isDate: boolean,
|
|
270
|
+
tz: DateTimezone,
|
|
271
|
+
): LineChartData | undefined {
|
|
272
|
+
if (!x) return undefined;
|
|
273
|
+
if (!isDate) return x as LineChartData;
|
|
274
|
+
const out = new Float64Array(x.length);
|
|
275
|
+
for (let i = 0; i < x.length; i++) {
|
|
276
|
+
const v = parseDate(x[i], tz);
|
|
277
|
+
out[i] = v ?? NaN;
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
198
280
|
}
|
|
199
281
|
|
|
200
282
|
const formatTooltipValue = makeTooltipValueFormatter(
|
|
@@ -202,15 +284,58 @@ const formatTooltipValue = makeTooltipValueFormatter(
|
|
|
202
284
|
() => props.yTickFormat,
|
|
203
285
|
);
|
|
204
286
|
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
287
|
+
const tz = computed<DateTimezone>(() => props.timezone ?? "utc");
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Single-pass resolution of x-axis mode + resolved series and areas.
|
|
291
|
+
* Date auto-detection (`isAllDates` per input array) and date→ms
|
|
292
|
+
* conversion (`toNumericX`) both call `parseDate`; this computed
|
|
293
|
+
* makes sure each user-supplied x value is parsed exactly once per
|
|
294
|
+
* render even though we have multiple consumers.
|
|
295
|
+
*/
|
|
296
|
+
const resolvedXAxis = computed<{
|
|
297
|
+
isDate: boolean;
|
|
298
|
+
series: ResolvedSeries[];
|
|
299
|
+
areas: ResolvedArea[];
|
|
300
|
+
}>(() => {
|
|
301
|
+
const z = tz.value;
|
|
302
|
+
let isDate = false;
|
|
303
|
+
let any = false;
|
|
304
|
+
for (const x of xInputs(props)) {
|
|
305
|
+
any = true;
|
|
306
|
+
if (!isAllDates(x, z)) {
|
|
307
|
+
isDate = false;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
isDate = true;
|
|
311
|
+
}
|
|
312
|
+
if (!any) isDate = false;
|
|
313
|
+
|
|
314
|
+
const series: ResolvedSeries[] =
|
|
315
|
+
props.series && props.series.length > 0
|
|
316
|
+
? props.series.map((s) => ({
|
|
317
|
+
...s,
|
|
318
|
+
data: s.y ?? s.data ?? EMPTY_DATA,
|
|
319
|
+
x: toNumericX(s.x, isDate, z),
|
|
320
|
+
}))
|
|
321
|
+
: (() => {
|
|
322
|
+
const topY = props.y ?? props.data;
|
|
323
|
+
return topY
|
|
324
|
+
? [{ data: topY, x: toNumericX(props.x, isDate, z) }]
|
|
325
|
+
: [];
|
|
326
|
+
})();
|
|
327
|
+
|
|
328
|
+
const areas: ResolvedArea[] = (props.areas ?? []).map((a) => ({
|
|
329
|
+
...a,
|
|
330
|
+
x: toNumericX(a.x, isDate, z),
|
|
331
|
+
}));
|
|
332
|
+
|
|
333
|
+
return { isDate, series, areas };
|
|
211
334
|
});
|
|
212
335
|
|
|
213
|
-
const
|
|
336
|
+
const xIsDate = computed(() => resolvedXAxis.value.isDate);
|
|
337
|
+
const allSeries = computed<ResolvedSeries[]>(() => resolvedXAxis.value.series);
|
|
338
|
+
const allAreas = computed<ResolvedArea[]>(() => resolvedXAxis.value.areas);
|
|
214
339
|
|
|
215
340
|
const maxLen = computed(() => {
|
|
216
341
|
let m = 0;
|
|
@@ -237,7 +362,7 @@ function seriesXAt(s: { x?: LineChartData }, i: number): number {
|
|
|
237
362
|
}
|
|
238
363
|
|
|
239
364
|
/** Data-space x value for the i-th point of an area. */
|
|
240
|
-
function areaXAt(a:
|
|
365
|
+
function areaXAt(a: ResolvedArea, i: number): number {
|
|
241
366
|
return a.x ? Number(a.x[i]) : i;
|
|
242
367
|
}
|
|
243
368
|
|
|
@@ -346,7 +471,7 @@ function toPoints(s: ResolvedSeries): { x: number; y: number }[] {
|
|
|
346
471
|
return pts;
|
|
347
472
|
}
|
|
348
473
|
|
|
349
|
-
function toAreaPath(a:
|
|
474
|
+
function toAreaPath(a: ResolvedArea): string {
|
|
350
475
|
const len = Math.min(a.upper.length, a.lower.length);
|
|
351
476
|
if (len === 0) return "";
|
|
352
477
|
// Collect contiguous segments where both upper/lower and x are finite
|
|
@@ -435,6 +560,8 @@ interface PositionedSectionLabel {
|
|
|
435
560
|
row: number;
|
|
436
561
|
color: string;
|
|
437
562
|
fillOpacity: number;
|
|
563
|
+
labelStyle: ReturnType<typeof resolveLabelStyle>;
|
|
564
|
+
descStyle: ReturnType<typeof resolveLabelStyle>;
|
|
438
565
|
}
|
|
439
566
|
|
|
440
567
|
const sectionLabels = computed<{
|
|
@@ -467,6 +594,16 @@ const sectionLabels = computed<{
|
|
|
467
594
|
(sec.seriesIndex != null
|
|
468
595
|
? (allSeries.value[sec.seriesIndex]?.color ?? "currentColor")
|
|
469
596
|
: "#999");
|
|
597
|
+
// Section labels default to the section's data-driven color/weight;
|
|
598
|
+
// user-supplied styles override piece-by-piece.
|
|
599
|
+
const labelStyle = resolveLabelStyle(
|
|
600
|
+
{ color, fontWeight: 600, ...sec.inlineLabelStyle },
|
|
601
|
+
{ fontSize: 11 },
|
|
602
|
+
);
|
|
603
|
+
const descStyle = resolveLabelStyle(sec.inlineDescriptionStyle, {
|
|
604
|
+
fontSize: 11,
|
|
605
|
+
fillOpacity: 0.6,
|
|
606
|
+
});
|
|
470
607
|
items.push({
|
|
471
608
|
cx,
|
|
472
609
|
labelText,
|
|
@@ -475,6 +612,8 @@ const sectionLabels = computed<{
|
|
|
475
612
|
row: 0,
|
|
476
613
|
color,
|
|
477
614
|
fillOpacity: sec.opacity ?? 0.15,
|
|
615
|
+
labelStyle,
|
|
616
|
+
descStyle,
|
|
478
617
|
});
|
|
479
618
|
}
|
|
480
619
|
|
|
@@ -581,43 +720,68 @@ const yTickItems = computed(() => {
|
|
|
581
720
|
return values.map((v) => ({ value: fmt(v), y: snap(yPixel(v)) }));
|
|
582
721
|
});
|
|
583
722
|
|
|
723
|
+
/**
|
|
724
|
+
* Format a single x-axis value into its display label. Used by both
|
|
725
|
+
* tick rendering and the tooltip x-label so the two stay in sync. On
|
|
726
|
+
* a date axis, `unit` (when supplied) drives the default formatter's
|
|
727
|
+
* preset choice (year-tick → "year", month-tick → "month-year", etc.);
|
|
728
|
+
* the tooltip path leaves it undefined and gets the ISO default.
|
|
729
|
+
*/
|
|
730
|
+
function formatXValue(v: number, i: number, unit?: DateTickUnit): string {
|
|
731
|
+
const xf = props.xTickFormat;
|
|
732
|
+
if (xIsDate.value) {
|
|
733
|
+
if (typeof xf === "function") {
|
|
734
|
+
return (xf as (v: number, i: number) => string)(v, i);
|
|
735
|
+
}
|
|
736
|
+
return formatDate(v, xf as DateFormat | undefined, tz.value, unit);
|
|
737
|
+
}
|
|
738
|
+
const display = v + xDisplayOffset.value;
|
|
739
|
+
if (xf !== undefined) {
|
|
740
|
+
return typeof xf === "function"
|
|
741
|
+
? (xf as (v: number, i: number) => string)(display, i)
|
|
742
|
+
: formatNumber(display, xf as NumberFormat);
|
|
743
|
+
}
|
|
744
|
+
if (
|
|
745
|
+
!hasExplicitX.value &&
|
|
746
|
+
props.xLabels &&
|
|
747
|
+
Number.isInteger(v) &&
|
|
748
|
+
v >= 0 &&
|
|
749
|
+
v < props.xLabels.length
|
|
750
|
+
) {
|
|
751
|
+
return props.xLabels[v];
|
|
752
|
+
}
|
|
753
|
+
return formatTick(display);
|
|
754
|
+
}
|
|
755
|
+
|
|
584
756
|
const xTickItems = computed(() => {
|
|
585
757
|
const { min: xMin, max: xMax } = xExtent.value;
|
|
586
758
|
if (xMin === xMax) return [];
|
|
587
|
-
const
|
|
759
|
+
const isDate = xIsDate.value;
|
|
760
|
+
// `xMin` (display offset) is meaningless on a date axis — every x
|
|
761
|
+
// value already lives in absolute ms.
|
|
762
|
+
const offset = isDate ? 0 : xDisplayOffset.value;
|
|
588
763
|
const len = maxLen.value;
|
|
589
|
-
|
|
590
|
-
// Tick values are in data-space; display labels add `xDisplayOffset`.
|
|
591
|
-
const fmt = (v: number, i: number) => {
|
|
592
|
-
const display = v + offset;
|
|
593
|
-
const xf = props.xTickFormat;
|
|
594
|
-
if (xf !== undefined) {
|
|
595
|
-
return typeof xf === "function"
|
|
596
|
-
? xf(display, i)
|
|
597
|
-
: formatNumber(display, xf);
|
|
598
|
-
}
|
|
599
|
-
if (
|
|
600
|
-
!hasExplicitX.value &&
|
|
601
|
-
props.xLabels &&
|
|
602
|
-
Number.isInteger(v) &&
|
|
603
|
-
v >= 0 &&
|
|
604
|
-
v < props.xLabels.length
|
|
605
|
-
) {
|
|
606
|
-
return props.xLabels[v];
|
|
607
|
-
}
|
|
608
|
-
return formatTick(display);
|
|
609
|
-
};
|
|
764
|
+
const targetTickCount = innerW.value / 80;
|
|
610
765
|
|
|
611
766
|
let values: number[];
|
|
612
|
-
|
|
767
|
+
let unit: DateTickUnit | undefined;
|
|
768
|
+
if (isDate) {
|
|
769
|
+
const picked = pickDateTicks(xMin, xMax, targetTickCount, tz.value);
|
|
770
|
+
unit = picked.unit;
|
|
771
|
+
// Run through computeTickValues so the clip-to-range semantics
|
|
772
|
+
// match the numeric path. xMin is already 0, so `ticks` is used
|
|
773
|
+
// directly.
|
|
774
|
+
values = computeTickValues({ min: xMin, max: xMax, ticks: picked.values });
|
|
775
|
+
} else if (
|
|
613
776
|
props.xTicks == null &&
|
|
614
777
|
!hasExplicitX.value &&
|
|
615
778
|
props.xLabels &&
|
|
616
779
|
props.xLabels.length === len
|
|
617
780
|
) {
|
|
618
781
|
// xLabels fallback: pick evenly-spaced index ticks so every label
|
|
619
|
-
// bucket gets at most one tick.
|
|
620
|
-
|
|
782
|
+
// bucket gets at most one tick. Date mode supersedes xLabels — if
|
|
783
|
+
// both are supplied, only the date axis is used.
|
|
784
|
+
const targetTicks = Math.max(3, Math.floor(targetTickCount));
|
|
621
785
|
const step = Math.max(1, Math.round((len - 1) / targetTicks));
|
|
622
786
|
values = [];
|
|
623
787
|
for (let i = 0; i < len; i += step) values.push(i);
|
|
@@ -626,7 +790,7 @@ const xTickItems = computed(() => {
|
|
|
626
790
|
min: xMin,
|
|
627
791
|
max: xMax,
|
|
628
792
|
ticks: props.xTicks,
|
|
629
|
-
targetTickCount
|
|
793
|
+
targetTickCount,
|
|
630
794
|
displayOffset: offset,
|
|
631
795
|
});
|
|
632
796
|
}
|
|
@@ -639,7 +803,7 @@ const xTickItems = computed(() => {
|
|
|
639
803
|
let anchor: "start" | "middle" | "end" = "middle";
|
|
640
804
|
if (x - leftEdge <= edgeSnapPx) anchor = "start";
|
|
641
805
|
else if (rightEdge - x <= edgeSnapPx) anchor = "end";
|
|
642
|
-
return { value:
|
|
806
|
+
return { value: formatXValue(v, i, unit), x, anchor };
|
|
643
807
|
});
|
|
644
808
|
});
|
|
645
809
|
|
|
@@ -706,18 +870,8 @@ const hoverSlotProps = computed(() => {
|
|
|
706
870
|
const idx = hoverIndex.value;
|
|
707
871
|
const targetX = hoverDataX.value;
|
|
708
872
|
if (idx === null || targetX === null) return null;
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
let xLabel: string | undefined;
|
|
712
|
-
const xf = props.xTickFormat;
|
|
713
|
-
if (xf !== undefined) {
|
|
714
|
-
xLabel =
|
|
715
|
-
typeof xf === "function" ? xf(displayX, idx) : formatNumber(displayX, xf);
|
|
716
|
-
} else if (!hasExplicitX.value) {
|
|
717
|
-
xLabel = props.xLabels?.[idx];
|
|
718
|
-
} else {
|
|
719
|
-
xLabel = formatTick(displayX);
|
|
720
|
-
}
|
|
873
|
+
// Single source of truth for label dispatch — same as the x-axis ticks.
|
|
874
|
+
const xLabel = formatXValue(targetX, idx);
|
|
721
875
|
const series = allSeries.value;
|
|
722
876
|
const values: ChartTooltipValue[] = [];
|
|
723
877
|
for (let i = 0; i < series.length; i++) {
|
|
@@ -783,6 +937,7 @@ const {
|
|
|
783
937
|
width: () => props.width,
|
|
784
938
|
height: () => props.height,
|
|
785
939
|
title: () => props.title,
|
|
940
|
+
titleStyle: () => props.titleStyle,
|
|
786
941
|
xLabel: () => props.xLabel,
|
|
787
942
|
yLabel: () => props.yLabel,
|
|
788
943
|
debounce: () => props.debounce,
|
|
@@ -800,6 +955,46 @@ const {
|
|
|
800
955
|
extraBelowHeight: () => sectionLabels.value.extraHeight,
|
|
801
956
|
});
|
|
802
957
|
|
|
958
|
+
/** Resolved style for the x/y axis labels. */
|
|
959
|
+
const axisLabelResolved = computed(() =>
|
|
960
|
+
resolveLabelStyle(props.axisLabelStyle, { fontSize: AXIS_LABEL_FONT_SIZE }),
|
|
961
|
+
);
|
|
962
|
+
/** Resolved style for the axis tick labels. */
|
|
963
|
+
const tickLabelResolved = computed(() =>
|
|
964
|
+
resolveLabelStyle(props.tickLabelStyle, {
|
|
965
|
+
fontSize: TICK_LABEL_FONT_SIZE,
|
|
966
|
+
fillOpacity: TICK_LABEL_OPACITY,
|
|
967
|
+
}),
|
|
968
|
+
);
|
|
969
|
+
/** Resolved style for inline legend item labels. */
|
|
970
|
+
const legendResolved = computed(() =>
|
|
971
|
+
resolveLabelStyle(props.legendStyle, { fontSize: LEGEND_FONT_SIZE }),
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
/** Resolved title style with defaults applied. */
|
|
975
|
+
const titleResolved = computed(() => {
|
|
976
|
+
const s = props.titleStyle;
|
|
977
|
+
const align = s?.align ?? "left";
|
|
978
|
+
const b = bounds.value;
|
|
979
|
+
const x =
|
|
980
|
+
align === "left"
|
|
981
|
+
? b.left
|
|
982
|
+
: align === "right"
|
|
983
|
+
? b.right
|
|
984
|
+
: b.left + b.width / 2;
|
|
985
|
+
const anchor =
|
|
986
|
+
align === "left" ? "start" : align === "right" ? "end" : "middle";
|
|
987
|
+
return {
|
|
988
|
+
lines: (props.title ?? "").split("\n"),
|
|
989
|
+
fontSize: s?.fontSize ?? TITLE_FONT_SIZE,
|
|
990
|
+
lineHeight: s?.lineHeight ?? TITLE_LINE_HEIGHT,
|
|
991
|
+
fontWeight: s?.fontWeight ?? TITLE_FONT_WEIGHT,
|
|
992
|
+
color: s?.color ?? "currentColor",
|
|
993
|
+
x,
|
|
994
|
+
anchor,
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
|
|
803
998
|
/**
|
|
804
999
|
* Legend items joined with their wrapped pixel positions. `x` is the
|
|
805
1000
|
* left edge of the indicator; `y` is the center of the row.
|
|
@@ -833,14 +1028,21 @@ const positionedLegendItems = computed(() => {
|
|
|
833
1028
|
<!-- title -->
|
|
834
1029
|
<text
|
|
835
1030
|
v-if="title"
|
|
836
|
-
:x="
|
|
837
|
-
:y="
|
|
838
|
-
text-anchor="
|
|
839
|
-
font-size="
|
|
840
|
-
font-weight="
|
|
841
|
-
fill="
|
|
1031
|
+
:x="titleResolved.x"
|
|
1032
|
+
:y="titleResolved.lineHeight"
|
|
1033
|
+
:text-anchor="titleResolved.anchor"
|
|
1034
|
+
:font-size="titleResolved.fontSize"
|
|
1035
|
+
:font-weight="titleResolved.fontWeight"
|
|
1036
|
+
:fill="titleResolved.color"
|
|
842
1037
|
>
|
|
843
|
-
|
|
1038
|
+
<tspan
|
|
1039
|
+
v-for="(line, i) in titleResolved.lines"
|
|
1040
|
+
:key="i"
|
|
1041
|
+
:x="titleResolved.x"
|
|
1042
|
+
:dy="i === 0 ? 0 : titleResolved.lineHeight"
|
|
1043
|
+
>
|
|
1044
|
+
{{ line }}
|
|
1045
|
+
</tspan>
|
|
844
1046
|
</text>
|
|
845
1047
|
<!-- inline legend -->
|
|
846
1048
|
<g v-if="positionedLegendItems.length > 0">
|
|
@@ -870,8 +1072,9 @@ const positionedLegendItems = computed(() => {
|
|
|
870
1072
|
<text
|
|
871
1073
|
:x="item.x + 18"
|
|
872
1074
|
:y="item.y + 4"
|
|
873
|
-
font-size="
|
|
874
|
-
fill="
|
|
1075
|
+
:font-size="legendResolved.fontSize"
|
|
1076
|
+
:fill="legendResolved.fill"
|
|
1077
|
+
:font-weight="legendResolved.fontWeight"
|
|
875
1078
|
>
|
|
876
1079
|
{{ item.label }}
|
|
877
1080
|
</text>
|
|
@@ -929,9 +1132,10 @@ const positionedLegendItems = computed(() => {
|
|
|
929
1132
|
:y="tick.y"
|
|
930
1133
|
text-anchor="end"
|
|
931
1134
|
dominant-baseline="middle"
|
|
932
|
-
font-size="
|
|
933
|
-
fill="
|
|
934
|
-
|
|
1135
|
+
:font-size="tickLabelResolved.fontSize"
|
|
1136
|
+
:fill="tickLabelResolved.fill"
|
|
1137
|
+
:font-weight="tickLabelResolved.fontWeight"
|
|
1138
|
+
:fill-opacity="tickLabelResolved.fillOpacity"
|
|
935
1139
|
>
|
|
936
1140
|
{{ tick.value }}
|
|
937
1141
|
</text>
|
|
@@ -942,8 +1146,9 @@ const positionedLegendItems = computed(() => {
|
|
|
942
1146
|
:y="0"
|
|
943
1147
|
:transform="`translate(14, ${padding.top + innerH / 2}) rotate(-90)`"
|
|
944
1148
|
text-anchor="middle"
|
|
945
|
-
font-size="
|
|
946
|
-
fill="
|
|
1149
|
+
:font-size="axisLabelResolved.fontSize"
|
|
1150
|
+
:fill="axisLabelResolved.fill"
|
|
1151
|
+
:font-weight="axisLabelResolved.fontWeight"
|
|
947
1152
|
>
|
|
948
1153
|
{{ yLabel }}
|
|
949
1154
|
</text>
|
|
@@ -955,9 +1160,10 @@ const positionedLegendItems = computed(() => {
|
|
|
955
1160
|
:x="tick.x"
|
|
956
1161
|
:y="padding.top + innerH + 16"
|
|
957
1162
|
:text-anchor="tick.anchor"
|
|
958
|
-
font-size="
|
|
959
|
-
fill="
|
|
960
|
-
|
|
1163
|
+
:font-size="tickLabelResolved.fontSize"
|
|
1164
|
+
:fill="tickLabelResolved.fill"
|
|
1165
|
+
:font-weight="tickLabelResolved.fontWeight"
|
|
1166
|
+
:fill-opacity="tickLabelResolved.fillOpacity"
|
|
961
1167
|
>
|
|
962
1168
|
{{ tick.value }}
|
|
963
1169
|
</text>
|
|
@@ -967,8 +1173,9 @@ const positionedLegendItems = computed(() => {
|
|
|
967
1173
|
:x="padding.left + innerW / 2"
|
|
968
1174
|
:y="height - 4"
|
|
969
1175
|
text-anchor="middle"
|
|
970
|
-
font-size="
|
|
971
|
-
fill="
|
|
1176
|
+
:font-size="axisLabelResolved.fontSize"
|
|
1177
|
+
:fill="axisLabelResolved.fill"
|
|
1178
|
+
:font-weight="axisLabelResolved.fontWeight"
|
|
972
1179
|
>
|
|
973
1180
|
{{ xLabel }}
|
|
974
1181
|
</text>
|
|
@@ -1134,9 +1341,9 @@ const positionedLegendItems = computed(() => {
|
|
|
1134
1341
|
v-if="item.labelText"
|
|
1135
1342
|
:x="item.cx - item.textWidth / 2 + 8"
|
|
1136
1343
|
:y="sectionLabelBaseY + item.row * SECTION_LABEL_ROW_HEIGHT + 8"
|
|
1137
|
-
font-size="
|
|
1138
|
-
font-weight="
|
|
1139
|
-
:fill="item.
|
|
1344
|
+
:font-size="item.labelStyle.fontSize"
|
|
1345
|
+
:font-weight="item.labelStyle.fontWeight"
|
|
1346
|
+
:fill="item.labelStyle.fill"
|
|
1140
1347
|
>
|
|
1141
1348
|
{{ item.labelText }}
|
|
1142
1349
|
</text>
|
|
@@ -1144,9 +1351,10 @@ const positionedLegendItems = computed(() => {
|
|
|
1144
1351
|
v-if="item.descText"
|
|
1145
1352
|
:x="item.cx - item.textWidth / 2 + 8"
|
|
1146
1353
|
:y="sectionLabelBaseY + item.row * SECTION_LABEL_ROW_HEIGHT + 22"
|
|
1147
|
-
font-size="
|
|
1148
|
-
|
|
1149
|
-
fill
|
|
1354
|
+
:font-size="item.descStyle.fontSize"
|
|
1355
|
+
:font-weight="item.descStyle.fontWeight"
|
|
1356
|
+
:fill="item.descStyle.fill"
|
|
1357
|
+
:fill-opacity="item.descStyle.fillOpacity"
|
|
1150
1358
|
>
|
|
1151
1359
|
{{ item.descText }}
|
|
1152
1360
|
</text>
|