@automattic/charts 4.2.0 → 4.3.0
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/CHANGELOG.md +20 -0
- package/dist/index.cjs +470 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +471 -122
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +42 -10
- package/src/charts/area-chart/private/validate-data.ts +2 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +72 -1
- package/src/charts/bar-chart/bar-chart.tsx +100 -83
- package/src/charts/bar-chart/index.ts +1 -1
- package/src/charts/bar-chart/private/band-highlight.tsx +3 -1
- package/src/charts/bar-chart/private/band-tooltip.tsx +1 -1
- package/src/charts/bar-chart/private/index.ts +1 -0
- package/src/charts/bar-chart/private/test/band-highlight.test.tsx +28 -2
- package/src/charts/bar-chart/private/test/value-domain.test.ts +100 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +12 -39
- package/src/charts/bar-chart/private/value-domain.ts +90 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +484 -9
- package/src/charts/bar-chart/types.ts +39 -0
- package/src/charts/line-chart/line-chart.tsx +148 -35
- package/src/charts/line-chart/private/index.ts +1 -0
- package/src/charts/line-chart/private/line-chart-glyph.tsx +17 -3
- package/src/charts/line-chart/private/nearest-pointer-events.tsx +155 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +604 -9
- package/src/charts/line-chart/types.ts +2 -1
- package/src/charts/private/readings.ts +34 -0
- package/src/charts/private/test/readings.test.ts +29 -0
- package/src/charts/private/test/whole-number-ticks.test.tsx +57 -0
- package/src/charts/private/whole-number-ticks.tsx +79 -0
- package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +124 -0
- package/src/components/legend/hooks/use-chart-legend-items.ts +60 -2
- package/src/components/legend/private/base-legend.tsx +36 -21
- package/src/components/legend/test/legend.test.tsx +84 -0
- package/src/components/legend/types.ts +2 -0
- package/src/components/legend/utils/index.ts +0 -1
- package/src/components/tooltip/accessible-tooltip.tsx +1 -1
- package/src/hooks/test/use-chart-margin.test.tsx +35 -0
- package/src/hooks/use-chart-margin.tsx +7 -2
- package/src/providers/chart-context/global-charts-provider.tsx +1 -1
- package/src/providers/chart-context/test/chart-context.test.tsx +4 -4
- package/src/types.ts +8 -0
- package/src/components/legend/utils/label-transform-factory.ts +0 -24
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { Text, getStringWidth } from "@visx/text";
|
|
|
12
12
|
import deepmerge from "deepmerge";
|
|
13
13
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
14
14
|
import { Tooltip, defaultStyles, useTooltip } from "@visx/tooltip";
|
|
15
|
-
import { createScale, getTicks, scaleBand, scaleOrdinal, scaleTime } from "@visx/scale";
|
|
15
|
+
import { createScale, getTicks, scaleBand, scaleCanBeZeroed, scaleOrdinal, scaleTime } from "@visx/scale";
|
|
16
16
|
import { Group } from "@visx/group";
|
|
17
17
|
import { LegendItem, LegendLabel, LegendOrdinal, LegendShape } from "@visx/legend";
|
|
18
18
|
import { IconButton, Stack, Text as Text$1 } from "@wordpress/ui";
|
|
@@ -21,6 +21,7 @@ import { LinearGradient } from "@visx/gradient";
|
|
|
21
21
|
import { curveCatmullRom, curveLinear, curveMonotoneX } from "@visx/curve";
|
|
22
22
|
import { useParentSize } from "@visx/responsive";
|
|
23
23
|
import { Annotation, CircleSubject, Connector, HtmlLabel, Label, LineSubject } from "@visx/annotation";
|
|
24
|
+
import { bisector } from "@visx/vendor/d3-array";
|
|
24
25
|
import { PatternCircles, PatternHexagons, PatternLines, PatternWaves } from "@visx/pattern";
|
|
25
26
|
import { Chart } from "react-google-charts";
|
|
26
27
|
import DOMPurify from "dompurify";
|
|
@@ -1074,7 +1075,7 @@ const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
|
|
|
1074
1075
|
return getChartColor(index, colorCache);
|
|
1075
1076
|
}, [colorCache, groupToColorMap]);
|
|
1076
1077
|
const getElementStyles = useCallback(({ data, index, overrideColor, legendShape }) => {
|
|
1077
|
-
const isSeriesData = data && typeof data === "object" && "data" in data
|
|
1078
|
+
const isSeriesData = data && typeof data === "object" && "data" in data;
|
|
1078
1079
|
const isPointPercentageData = data && typeof data === "object" && "value" in data && typeof data.value === "number" && !("data" in data);
|
|
1079
1080
|
return {
|
|
1080
1081
|
color: resolveColor({
|
|
@@ -1455,8 +1456,9 @@ const useChartMargin = (height, options, data, theme, horizontal = false) => {
|
|
|
1455
1456
|
const allDataPoints = data.flatMap((series) => series.data);
|
|
1456
1457
|
if (horizontal) return allDataPoints.map((d) => d.label || d.date?.getTime());
|
|
1457
1458
|
if (options.axis?.y?.tickValues?.length) return options.axis.y.tickValues;
|
|
1458
|
-
const
|
|
1459
|
-
const
|
|
1459
|
+
const values = allDataPoints.map((d) => d.value).filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
1460
|
+
const minY = values.length ? Math.min(...values) : 0;
|
|
1461
|
+
const maxY = values.length ? Math.max(...values) : 1;
|
|
1460
1462
|
const yScale = createScale({
|
|
1461
1463
|
...options.yScale,
|
|
1462
1464
|
domain: options.yScale?.domain ?? [minY, maxY],
|
|
@@ -1791,23 +1793,6 @@ function valueOrIdentityString(_) {
|
|
|
1791
1793
|
return String(valueOrIdentity(_));
|
|
1792
1794
|
}
|
|
1793
1795
|
//#endregion
|
|
1794
|
-
//#region src/components/legend/utils/label-transform-factory.ts
|
|
1795
|
-
/**
|
|
1796
|
-
* Returns a function which takes a Datum and index as input, and returns a formatted label object.
|
|
1797
|
-
* @param {object} root0 - The object to return the value of.
|
|
1798
|
-
* @param {AnyD3Scale} root0.scale - The scale to use.
|
|
1799
|
-
* @param {LabelFormatter<ScaleInput<Scale>>} root0.labelFormat - The label format to use.
|
|
1800
|
-
* @return {ItemTransformer<ScaleInput<Scale>, ReturnType<Scale>>} The label transform factory.
|
|
1801
|
-
*/
|
|
1802
|
-
function labelTransformFactory({ scale, labelFormat }) {
|
|
1803
|
-
return (d, i) => ({
|
|
1804
|
-
datum: d,
|
|
1805
|
-
index: i,
|
|
1806
|
-
text: `${labelFormat(d, i)}`,
|
|
1807
|
-
value: scale(d)
|
|
1808
|
-
});
|
|
1809
|
-
}
|
|
1810
|
-
//#endregion
|
|
1811
1796
|
//#region src/components/legend/private/base-legend.module.scss
|
|
1812
1797
|
var base_legend_module_default = {
|
|
1813
1798
|
"legend": "a8ccharts-04TogW-legend",
|
|
@@ -1849,7 +1834,7 @@ const getLegendItemAriaLabel = (text, value, visible, interactive) => {
|
|
|
1849
1834
|
if (visible) return;
|
|
1850
1835
|
return sprintf(_x("%s: hidden", "hidden non-interactive legend item", "jetpack-charts"), accessibleText);
|
|
1851
1836
|
};
|
|
1852
|
-
const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", alignment = "center", shape = "rect", fill = valueOrIdentityString, size = valueOrIdentityString, labelFormat = valueOrIdentity, labelTransform
|
|
1837
|
+
const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", alignment = "center", shape = "rect", fill = valueOrIdentityString, size = valueOrIdentityString, labelFormat = valueOrIdentity, labelTransform, itemStyles, itemClassName, labelStyles, labelClassName, shapeStyles, render, interactive = false, chartId }, ref) => {
|
|
1853
1838
|
const { margin: itemMargin = "0", flexDirection: itemDirection = "row" } = itemStyles ?? {};
|
|
1854
1839
|
const { justifyContent: labelJustifyContent = "flex-start", flex: labelFlex = "0 0 auto", margin: labelMargin = "0 4px", maxWidth, textOverflow = "wrap" } = labelStyles ?? {};
|
|
1855
1840
|
const { width: shapeWidth = 16, height: shapeHeight = 16, margin: shapeMargin = "2px 4px 2px 0" } = shapeStyles ?? {};
|
|
@@ -1857,11 +1842,17 @@ const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", a
|
|
|
1857
1842
|
const context = useContext(GlobalChartsContext);
|
|
1858
1843
|
const chartInstanceContext = useContext(ChartInstanceContext);
|
|
1859
1844
|
const standaloneScopeClass = useStandaloneScopeClass();
|
|
1845
|
+
const domain = items.map((item) => item.label);
|
|
1860
1846
|
const legendScale = scaleOrdinal({
|
|
1861
|
-
domain
|
|
1847
|
+
domain,
|
|
1862
1848
|
range: items.map((item) => item.color)
|
|
1863
1849
|
});
|
|
1864
|
-
const
|
|
1850
|
+
const defaultLabelTransform = useCallback(({ labelFormat: format }) => (datum, index) => ({
|
|
1851
|
+
datum,
|
|
1852
|
+
index,
|
|
1853
|
+
text: String(format(datum, index)),
|
|
1854
|
+
value: items[index].color
|
|
1855
|
+
}), [items]);
|
|
1865
1856
|
const getShapeStyle = useCallback(({ index }) => items[index]?.shapeStyle, [items]);
|
|
1866
1857
|
const handleLegendClick = useCallback((seriesLabels) => {
|
|
1867
1858
|
if (interactive && chartId && context) {
|
|
@@ -1882,24 +1873,26 @@ const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", a
|
|
|
1882
1873
|
chartInstanceContext,
|
|
1883
1874
|
context
|
|
1884
1875
|
]);
|
|
1885
|
-
const createClickHandler = useCallback((seriesLabels) => {
|
|
1886
|
-
if (!
|
|
1876
|
+
const createClickHandler = useCallback((seriesLabels, itemInteractive) => {
|
|
1877
|
+
if (!itemInteractive) return;
|
|
1887
1878
|
return () => handleLegendClick(seriesLabels);
|
|
1888
|
-
}, [
|
|
1889
|
-
const createKeyDownHandler = useCallback((seriesLabels) => {
|
|
1890
|
-
if (!
|
|
1879
|
+
}, [handleLegendClick]);
|
|
1880
|
+
const createKeyDownHandler = useCallback((seriesLabels, itemInteractive) => {
|
|
1881
|
+
if (!itemInteractive) return;
|
|
1891
1882
|
return (event) => {
|
|
1892
1883
|
if (event.key === "Enter" || event.key === " ") {
|
|
1893
1884
|
event.preventDefault();
|
|
1894
1885
|
handleLegendClick(seriesLabels);
|
|
1895
1886
|
}
|
|
1896
1887
|
};
|
|
1897
|
-
}, [
|
|
1888
|
+
}, [handleLegendClick]);
|
|
1898
1889
|
const flexAlignment = ALIGNMENT_TO_FLEX[alignment] ?? "center";
|
|
1890
|
+
const staticItemRole = interactive ? void 0 : "listitem";
|
|
1899
1891
|
return render ? render(items) : /* @__PURE__ */ jsx(LegendOrdinal, {
|
|
1900
1892
|
scale: legendScale,
|
|
1893
|
+
domain,
|
|
1901
1894
|
labelFormat,
|
|
1902
|
-
labelTransform,
|
|
1895
|
+
labelTransform: labelTransform ?? defaultLabelTransform,
|
|
1903
1896
|
children: (labels) => /* @__PURE__ */ jsx(Stack, {
|
|
1904
1897
|
ref,
|
|
1905
1898
|
direction: orientation === "vertical" ? "column" : "row",
|
|
@@ -1913,19 +1906,20 @@ const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", a
|
|
|
1913
1906
|
children: labels.map((label, i) => {
|
|
1914
1907
|
const matchedItem = items[i];
|
|
1915
1908
|
const seriesLabels = matchedItem?.seriesLabels?.length ? matchedItem.seriesLabels : [label.text];
|
|
1916
|
-
const visible = isSeriesVisible(seriesLabels[0]);
|
|
1917
|
-
const
|
|
1918
|
-
const
|
|
1909
|
+
const visible = matchedItem?.interactive === false || isSeriesVisible(seriesLabels[0]);
|
|
1910
|
+
const itemInteractive = interactive && matchedItem?.interactive !== false;
|
|
1911
|
+
const handleClick = createClickHandler(seriesLabels, itemInteractive);
|
|
1912
|
+
const handleKeyDown = createKeyDownHandler(seriesLabels, itemInteractive);
|
|
1919
1913
|
return /* @__PURE__ */ jsxs(LegendItem, {
|
|
1920
|
-
className: clsx("visx-legend-item", base_legend_module_default["legend-item"],
|
|
1914
|
+
className: clsx("visx-legend-item", base_legend_module_default["legend-item"], itemInteractive && base_legend_module_default["legend-item--interactive"], !visible && base_legend_module_default["legend-item--inactive"], itemClassName),
|
|
1921
1915
|
margin: itemMargin,
|
|
1922
1916
|
flexDirection: orientation === "vertical" && alignment === "end" ? "row-reverse" : itemDirection,
|
|
1923
1917
|
onClick: handleClick,
|
|
1924
1918
|
onKeyDown: handleKeyDown,
|
|
1925
|
-
role:
|
|
1926
|
-
tabIndex:
|
|
1927
|
-
"aria-pressed":
|
|
1928
|
-
"aria-label": getLegendItemAriaLabel(label.text, matchedItem?.value, visible,
|
|
1919
|
+
role: itemInteractive ? "button" : staticItemRole,
|
|
1920
|
+
tabIndex: itemInteractive ? 0 : void 0,
|
|
1921
|
+
"aria-pressed": itemInteractive ? visible : void 0,
|
|
1922
|
+
"aria-label": getLegendItemAriaLabel(label.text, matchedItem?.value, visible, itemInteractive),
|
|
1929
1923
|
children: [items[i]?.renderGlyph ? /* @__PURE__ */ jsx("svg", {
|
|
1930
1924
|
width: items[i]?.glyphSize * 2,
|
|
1931
1925
|
height: items[i]?.glyphSize * 2,
|
|
@@ -2127,6 +2121,32 @@ function processSeriesData(seriesData, getElementStyles, showValues, withGlyph,
|
|
|
2127
2121
|
});
|
|
2128
2122
|
}
|
|
2129
2123
|
/**
|
|
2124
|
+
* Builds the static item that names the comparison overlay, styled like its first series.
|
|
2125
|
+
* Skipped when a comparison series already has an item of its own, so nothing is listed twice.
|
|
2126
|
+
* @param seriesData - The series data to search for a comparison series
|
|
2127
|
+
* @param items - The legend items already built from the series
|
|
2128
|
+
* @param label - The item label
|
|
2129
|
+
* @param getElementStyles - Function to get element styles
|
|
2130
|
+
* @param legendShape - The shape type for legend items (string literal or React component)
|
|
2131
|
+
* @return The legend item, or null when it would add nothing
|
|
2132
|
+
*/
|
|
2133
|
+
function buildComparisonLegendItem(seriesData, items, label, getElementStyles, legendShape) {
|
|
2134
|
+
const itemLabels = new Set(items.map((item) => item.label));
|
|
2135
|
+
const index = seriesData.findIndex((series) => series.options?.type === "comparison" && !itemLabels.has(series.label));
|
|
2136
|
+
if (index === -1) return null;
|
|
2137
|
+
const { color, shapeStyles } = getElementStyles({
|
|
2138
|
+
data: seriesData[index],
|
|
2139
|
+
index,
|
|
2140
|
+
legendShape
|
|
2141
|
+
});
|
|
2142
|
+
return {
|
|
2143
|
+
label,
|
|
2144
|
+
color,
|
|
2145
|
+
shapeStyle: shapeStyles,
|
|
2146
|
+
interactive: false
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2130
2150
|
* Processes point data into legend items
|
|
2131
2151
|
* @param pointData - The point data to process
|
|
2132
2152
|
* @param getElementStyles - Function to get element styles
|
|
@@ -2162,11 +2182,16 @@ function processPointData(pointData, getElementStyles, showValues, legendValueDi
|
|
|
2162
2182
|
* @return Array of legend items ready for display
|
|
2163
2183
|
*/
|
|
2164
2184
|
function useChartLegendItems(data, options = {}, legendShape) {
|
|
2165
|
-
const { showValues = false, legendValueDisplay = "percentage", withGlyph = false, glyphSize = 8, collapseGroups = false, renderGlyph } = options;
|
|
2185
|
+
const { showValues = false, legendValueDisplay = "percentage", withGlyph = false, glyphSize = 8, collapseGroups = false, comparisonItem = false, renderGlyph } = options;
|
|
2166
2186
|
const { getElementStyles } = useGlobalChartsContext();
|
|
2167
2187
|
return useMemo(() => {
|
|
2168
2188
|
if (!data || !Array.isArray(data) || data.length === 0) return [];
|
|
2169
|
-
if ("data" in data[0])
|
|
2189
|
+
if ("data" in data[0]) {
|
|
2190
|
+
const seriesData = data;
|
|
2191
|
+
const items = processSeriesData(seriesData, getElementStyles, showValues, withGlyph, glyphSize, collapseGroups, renderGlyph, legendShape);
|
|
2192
|
+
const comparison = comparisonItem ? buildComparisonLegendItem(seriesData, items, typeof comparisonItem === "string" ? comparisonItem : __("Comparison period", "jetpack-charts"), getElementStyles, legendShape) : null;
|
|
2193
|
+
return comparison ? [...items, comparison] : items;
|
|
2194
|
+
}
|
|
2170
2195
|
return processPointData(data, getElementStyles, showValues, legendValueDisplay, withGlyph, glyphSize, renderGlyph, legendShape);
|
|
2171
2196
|
}, [
|
|
2172
2197
|
data,
|
|
@@ -2176,6 +2201,7 @@ function useChartLegendItems(data, options = {}, legendShape) {
|
|
|
2176
2201
|
withGlyph,
|
|
2177
2202
|
glyphSize,
|
|
2178
2203
|
collapseGroups,
|
|
2204
|
+
comparisonItem,
|
|
2179
2205
|
renderGlyph,
|
|
2180
2206
|
legendShape
|
|
2181
2207
|
]);
|
|
@@ -2639,7 +2665,7 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
|
|
|
2639
2665
|
tooltipContext?.showTooltip({
|
|
2640
2666
|
datum: tooltipItem.datum,
|
|
2641
2667
|
key: tooltipItem.seriesLabel,
|
|
2642
|
-
index: tooltipItem.
|
|
2668
|
+
index: tooltipItem.dataPointIndex
|
|
2643
2669
|
});
|
|
2644
2670
|
}
|
|
2645
2671
|
}
|
|
@@ -2962,6 +2988,31 @@ const DefaultGlyph = (props) => {
|
|
|
2962
2988
|
});
|
|
2963
2989
|
};
|
|
2964
2990
|
//#endregion
|
|
2991
|
+
//#region src/charts/private/readings.ts
|
|
2992
|
+
/**
|
|
2993
|
+
* Whether a value is a reading a scale can place.
|
|
2994
|
+
*
|
|
2995
|
+
* @param value - The point's value.
|
|
2996
|
+
* @return True for a finite number.
|
|
2997
|
+
*/
|
|
2998
|
+
const isReading = (value) => typeof value === "number" && Number.isFinite(value);
|
|
2999
|
+
/**
|
|
3000
|
+
* Whether a data point's value cannot be drawn.
|
|
3001
|
+
*
|
|
3002
|
+
* @param value - The point's value.
|
|
3003
|
+
* @param options - Validation options.
|
|
3004
|
+
* @param options.allowMissing - Whether `null` is a bucket with no reading rather than a fault.
|
|
3005
|
+
* @return True when the value is invalid.
|
|
3006
|
+
*/
|
|
3007
|
+
const isInvalidReading = (value, { allowMissing }) => value === null ? !allowMissing : value === void 0 || isNaN(value);
|
|
3008
|
+
/**
|
|
3009
|
+
* Formats a reading for a tooltip.
|
|
3010
|
+
*
|
|
3011
|
+
* @param value - The reading, or null when the bucket has none.
|
|
3012
|
+
* @return The formatted value.
|
|
3013
|
+
*/
|
|
3014
|
+
const formatReading = (value) => value == null ? __("No data", "jetpack-charts") : formatNumber(value);
|
|
3015
|
+
//#endregion
|
|
2965
3016
|
//#region src/charts/private/center/center.module.scss
|
|
2966
3017
|
var center_module_default = { "center": "a8ccharts-w3qxlG-center" };
|
|
2967
3018
|
//#endregion
|
|
@@ -3334,6 +3385,54 @@ const buildTimeAxisOptions = ({ dataSorted, width, axisOptions, scaleDomain, zoo
|
|
|
3334
3385
|
};
|
|
3335
3386
|
};
|
|
3336
3387
|
//#endregion
|
|
3388
|
+
//#region src/charts/private/whole-number-ticks.tsx
|
|
3389
|
+
/**
|
|
3390
|
+
* Whether every reading in the series is a whole number.
|
|
3391
|
+
*
|
|
3392
|
+
* @param series - The series visx renders.
|
|
3393
|
+
* @return True when at least one reading exists and none has a fraction.
|
|
3394
|
+
*/
|
|
3395
|
+
const hasOnlyWholeNumbers = (series) => {
|
|
3396
|
+
let sawReading = false;
|
|
3397
|
+
for (const { data } of series) for (const point of data) {
|
|
3398
|
+
const value = point?.value;
|
|
3399
|
+
if (!isReading(value)) continue;
|
|
3400
|
+
if (!Number.isInteger(value)) return false;
|
|
3401
|
+
sawReading = true;
|
|
3402
|
+
}
|
|
3403
|
+
return sawReading;
|
|
3404
|
+
};
|
|
3405
|
+
/**
|
|
3406
|
+
* The scale's ticks with the fractional ones dropped.
|
|
3407
|
+
*
|
|
3408
|
+
* d3 steps by 1, 2 or 5 times a power of ten, so every whole number inside the domain is one of its ticks.
|
|
3409
|
+
*
|
|
3410
|
+
* @param scale - The value scale visx built.
|
|
3411
|
+
* @param count - Ticks to ask the scale for; undefined uses the scale's own default, as visx does.
|
|
3412
|
+
* @return Whole-number ticks, or undefined when visx's own ticks need no change.
|
|
3413
|
+
*/
|
|
3414
|
+
const getWholeNumberTickValues = (scale, count) => {
|
|
3415
|
+
const ticks = scale?.ticks?.(count);
|
|
3416
|
+
if (!ticks?.every((tick) => typeof tick === "number")) return;
|
|
3417
|
+
const whole = ticks.filter((tick) => Number.isInteger(tick));
|
|
3418
|
+
return whole.length > 1 && whole.length < ticks.length ? whole : void 0;
|
|
3419
|
+
};
|
|
3420
|
+
/**
|
|
3421
|
+
* Hands its children the whole-number ticks of the chart's value scale.
|
|
3422
|
+
*
|
|
3423
|
+
* @param props - Component props.
|
|
3424
|
+
* @param props.axis - Which scale carries the values.
|
|
3425
|
+
* @param props.numTicks - Ticks to ask the scale for.
|
|
3426
|
+
* @param props.enabled - Whether the plotted values are all whole numbers and the caller pinned neither the ticks nor the value domain.
|
|
3427
|
+
* @param props.children - Renders the axis and grid with the tick values.
|
|
3428
|
+
* @return The rendered children.
|
|
3429
|
+
*/
|
|
3430
|
+
const WholeNumberTicks = ({ axis, numTicks, enabled, children }) => {
|
|
3431
|
+
const { xScale, yScale } = useContext(DataContext);
|
|
3432
|
+
const tickValues = enabled ? getWholeNumberTickValues(axis === "x" ? xScale : yScale, numTicks) : void 0;
|
|
3433
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: children(tickValues) });
|
|
3434
|
+
};
|
|
3435
|
+
//#endregion
|
|
3337
3436
|
//#region src/charts/private/with-responsive/with-responsive.module.scss
|
|
3338
3437
|
var with_responsive_module_default = {
|
|
3339
3438
|
"container": "a8ccharts-sP1gHa-container",
|
|
@@ -4000,11 +4099,15 @@ const toNumber$1 = (val) => {
|
|
|
4000
4099
|
const LineChartGlyph = ({ data, index, color, glyphStyle, renderGlyph, accessors, position }) => {
|
|
4001
4100
|
const { xScale, yScale } = useContext(DataContext) || {};
|
|
4002
4101
|
if (!xScale || !yScale) return null;
|
|
4003
|
-
|
|
4004
|
-
|
|
4102
|
+
const hasFiniteY = (datum) => {
|
|
4103
|
+
const scaledY = yScale(accessors.yAccessor(datum));
|
|
4104
|
+
return typeof scaledY === "number" && Number.isFinite(scaledY);
|
|
4105
|
+
};
|
|
4106
|
+
const point = position === "start" ? data.data.find(hasFiniteY) : data.data.findLast(hasFiniteY);
|
|
4107
|
+
if (!point) return null;
|
|
4005
4108
|
const x = xScale(accessors.xAccessor(point));
|
|
4006
4109
|
const y = yScale(accessors.yAccessor(point));
|
|
4007
|
-
if (typeof x !== "number" || typeof y !== "number") return null;
|
|
4110
|
+
if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
4008
4111
|
const size = Math.max(0, toNumber$1(glyphStyle?.radius) ?? 4);
|
|
4009
4112
|
return renderGlyph({
|
|
4010
4113
|
key: `${position}-glyph-${data.label}`,
|
|
@@ -4019,6 +4122,106 @@ const LineChartGlyph = ({ data, index, color, glyphStyle, renderGlyph, accessors
|
|
|
4019
4122
|
});
|
|
4020
4123
|
};
|
|
4021
4124
|
//#endregion
|
|
4125
|
+
//#region src/charts/line-chart/private/nearest-pointer-events.tsx
|
|
4126
|
+
/**
|
|
4127
|
+
* Find the datum nearest a pointer along x, in one series sorted by x.
|
|
4128
|
+
*
|
|
4129
|
+
* @param data - The series' registered data, in ascending x order.
|
|
4130
|
+
* @param xAccessor - Reads a datum's x value.
|
|
4131
|
+
* @param xScale - The chart's x scale.
|
|
4132
|
+
* @param x - Pointer x in SVG coordinates.
|
|
4133
|
+
* @return Index of the nearest datum and its distance, or index -1 when none has a position.
|
|
4134
|
+
*/
|
|
4135
|
+
function nearestByX(data, xAccessor, xScale, x) {
|
|
4136
|
+
return (typeof xScale.invert === "function" ? [bisector(xAccessor).left(data, xScale.invert(x))].flatMap((index) => [index - 1, index]) : data.map((_datum, index) => index)).filter((index) => index >= 0 && index < data.length).reduce((best, index) => {
|
|
4137
|
+
const distance = Math.abs(Number(xScale(xAccessor(data[index]))) - x);
|
|
4138
|
+
return distance < best.distance ? {
|
|
4139
|
+
index,
|
|
4140
|
+
distance
|
|
4141
|
+
} : best;
|
|
4142
|
+
}, {
|
|
4143
|
+
index: -1,
|
|
4144
|
+
distance: Infinity
|
|
4145
|
+
});
|
|
4146
|
+
}
|
|
4147
|
+
/**
|
|
4148
|
+
* Pick the candidate to report: the closest placed reading, else the closest bucket with no reading along x.
|
|
4149
|
+
*
|
|
4150
|
+
* @param candidates - One candidate per series.
|
|
4151
|
+
* @return The chosen candidate's params, or undefined when there are none.
|
|
4152
|
+
*/
|
|
4153
|
+
function pickNearest(candidates) {
|
|
4154
|
+
const placed = candidates.map(({ params }) => params).filter((params) => Number.isFinite(params.distanceY));
|
|
4155
|
+
const missing = candidates.filter(({ isMissing }) => isMissing).map(({ params }) => params);
|
|
4156
|
+
const distance = (params) => placed.length ? Math.hypot(params.distanceX ?? 0, params.distanceY ?? 0) : params.distanceX ?? 0;
|
|
4157
|
+
return (placed.length ? placed : missing).reduce((best, params) => !best || distance(params) < distance(best) ? params : best, void 0);
|
|
4158
|
+
}
|
|
4159
|
+
/**
|
|
4160
|
+
* Report pointer events at the nearest datum, including a bucket with no reading.
|
|
4161
|
+
*
|
|
4162
|
+
* visx's own nearest search measures y distance too, which is NaN for a null value, so a pointer
|
|
4163
|
+
* over a bucket where no series has a reading fires no event at all.
|
|
4164
|
+
*
|
|
4165
|
+
* @param props - Handlers to call.
|
|
4166
|
+
* @param props.onPointerDown - Receives the nearest datum on pointer down.
|
|
4167
|
+
* @param props.onPointerMove - Receives the nearest datum on pointer move.
|
|
4168
|
+
* @param props.onPointerUp - Receives the nearest datum on pointer up.
|
|
4169
|
+
* @return No visual content.
|
|
4170
|
+
*/
|
|
4171
|
+
function NearestPointerEvents({ onPointerDown, onPointerMove, onPointerUp }) {
|
|
4172
|
+
const { xScale, yScale, dataRegistry } = useContext(DataContext);
|
|
4173
|
+
const findNearest = useCallback((params) => {
|
|
4174
|
+
const point = params?.svgPoint;
|
|
4175
|
+
if (!point || !xScale || !yScale || !dataRegistry) return;
|
|
4176
|
+
const candidates = [];
|
|
4177
|
+
for (const key of dataRegistry.keys()) {
|
|
4178
|
+
const entry = dataRegistry.get(key);
|
|
4179
|
+
if (!entry) continue;
|
|
4180
|
+
const { index, distance } = nearestByX(entry.data, entry.xAccessor, xScale, point.x);
|
|
4181
|
+
if (index < 0) continue;
|
|
4182
|
+
const datum = entry.data[index];
|
|
4183
|
+
const value = entry.yAccessor(datum);
|
|
4184
|
+
candidates.push({
|
|
4185
|
+
params: {
|
|
4186
|
+
event: params.event,
|
|
4187
|
+
svgPoint: point,
|
|
4188
|
+
key,
|
|
4189
|
+
datum,
|
|
4190
|
+
index,
|
|
4191
|
+
distanceX: distance,
|
|
4192
|
+
distanceY: Math.abs(Number(yScale(value)) - point.y)
|
|
4193
|
+
},
|
|
4194
|
+
isMissing: value == null
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
4197
|
+
return pickNearest(candidates);
|
|
4198
|
+
}, [
|
|
4199
|
+
xScale,
|
|
4200
|
+
yScale,
|
|
4201
|
+
dataRegistry
|
|
4202
|
+
]);
|
|
4203
|
+
const handlers = useMemo(() => {
|
|
4204
|
+
const report = (handler) => handler && ((params) => {
|
|
4205
|
+
const nearest = findNearest(params);
|
|
4206
|
+
if (nearest) handler(nearest);
|
|
4207
|
+
});
|
|
4208
|
+
return {
|
|
4209
|
+
pointerdown: report(onPointerDown),
|
|
4210
|
+
pointermove: report(onPointerMove),
|
|
4211
|
+
pointerup: report(onPointerUp)
|
|
4212
|
+
};
|
|
4213
|
+
}, [
|
|
4214
|
+
findNearest,
|
|
4215
|
+
onPointerDown,
|
|
4216
|
+
onPointerMove,
|
|
4217
|
+
onPointerUp
|
|
4218
|
+
]);
|
|
4219
|
+
useEventEmitter("pointerdown", handlers.pointerdown);
|
|
4220
|
+
useEventEmitter("pointermove", handlers.pointermove);
|
|
4221
|
+
useEventEmitter("pointerup", handlers.pointerup);
|
|
4222
|
+
return null;
|
|
4223
|
+
}
|
|
4224
|
+
//#endregion
|
|
4022
4225
|
//#region src/charts/line-chart/line-chart.tsx
|
|
4023
4226
|
const defaultRenderGlyph = (props) => {
|
|
4024
4227
|
return /* @__PURE__ */ createElement(DefaultGlyph, {
|
|
@@ -4062,8 +4265,13 @@ const renderDefaultTooltip = (params, contentStyle) => {
|
|
|
4062
4265
|
if (!nearestDatum) return null;
|
|
4063
4266
|
const tooltipPoints = Object.entries(tooltipData?.datumByKey || {}).map(([key, { datum }]) => ({
|
|
4064
4267
|
key,
|
|
4065
|
-
value: datum.value
|
|
4066
|
-
})).sort((a, b) =>
|
|
4268
|
+
value: datum.value ?? null
|
|
4269
|
+
})).sort((a, b) => {
|
|
4270
|
+
if (a.value === null && b.value === null) return 0;
|
|
4271
|
+
if (a.value === null) return 1;
|
|
4272
|
+
if (b.value === null) return -1;
|
|
4273
|
+
return b.value - a.value;
|
|
4274
|
+
});
|
|
4067
4275
|
return /* @__PURE__ */ jsxs("div", {
|
|
4068
4276
|
className: line_chart_module_default["line-chart__tooltip"],
|
|
4069
4277
|
style: contentStyle,
|
|
@@ -4083,16 +4291,23 @@ const renderDefaultTooltip = (params, contentStyle) => {
|
|
|
4083
4291
|
children: [point.key, ":"]
|
|
4084
4292
|
}), /* @__PURE__ */ jsx("span", {
|
|
4085
4293
|
className: line_chart_module_default["line-chart__tooltip-value"],
|
|
4086
|
-
children:
|
|
4294
|
+
children: formatReading(point.value)
|
|
4087
4295
|
})]
|
|
4088
4296
|
}, point.key))]
|
|
4089
4297
|
});
|
|
4090
4298
|
};
|
|
4091
4299
|
const validateData$4 = (data) => {
|
|
4092
|
-
if (!data?.length) return "No data available";
|
|
4093
|
-
if (data.some((series) => series.data.some((point) =>
|
|
4300
|
+
if (!data?.length) return __("No data available", "jetpack-charts");
|
|
4301
|
+
if (data.some((series) => series.data.some((point) => isInvalidReading(point.value, { allowMissing: true }) || "date" in point && point.date && isNaN(point.date.getTime())))) return __("Invalid data", "jetpack-charts");
|
|
4094
4302
|
return null;
|
|
4095
4303
|
};
|
|
4304
|
+
const getFallbackYDomain = (readingExtent, isLogScale) => {
|
|
4305
|
+
const emptyDomain = isLogScale ? [1, 10] : [0, 1];
|
|
4306
|
+
if (!readingExtent) return emptyDomain;
|
|
4307
|
+
const [min, max] = readingExtent;
|
|
4308
|
+
if (min !== max || isLogScale) return;
|
|
4309
|
+
return min === 0 ? emptyDomain : [Math.min(0, min), Math.max(0, max)];
|
|
4310
|
+
};
|
|
4096
4311
|
const LineChartScalesRef = ({ chartRef, width, height, margin }) => {
|
|
4097
4312
|
const context = useContext(DataContext);
|
|
4098
4313
|
useImperativeHandle(chartRef, () => ({
|
|
@@ -4119,6 +4334,7 @@ const LineChartScalesRef = ({ chartRef, width, height, margin }) => {
|
|
|
4119
4334
|
const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, height, className, margin, withTooltips = true, withTooltipCrosshairs, showLegend = false, legend = {}, renderGlyph = defaultRenderGlyph, glyphStyle = {}, withLegendGlyph = false, withGradientFill = false, smoothing = true, curveType, renderTooltip = renderDefaultTooltip, tooltipPlacement, tooltipStyle, withStartGlyphs = false, withEndGlyphs = false, animation, options = {}, onPointerDown = void 0, onPointerUp = void 0, onPointerMove = void 0, onPointerOut = void 0, onDatumActivate = void 0, zoomable = false, rescaleYOnVisibilityChange = true, defaultHiddenSeries, children, gridVisibility, gap = "md" }, ref) => {
|
|
4120
4335
|
const legendInteractive = legend.interactive ?? false;
|
|
4121
4336
|
const legendCollapseGroups = legend.collapseGroups ?? false;
|
|
4337
|
+
const legendComparisonItem = legend.comparisonItem ?? false;
|
|
4122
4338
|
const legendShape = legend.shape ?? "line";
|
|
4123
4339
|
const legendPosition = legend.position ?? "bottom";
|
|
4124
4340
|
const formatting = useChartFormatting();
|
|
@@ -4165,19 +4381,32 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4165
4381
|
const allSeriesHidden = useMemo(() => {
|
|
4166
4382
|
return seriesWithVisibility.every(({ isVisible }) => !isVisible);
|
|
4167
4383
|
}, [seriesWithVisibility]);
|
|
4168
|
-
const
|
|
4169
|
-
|
|
4384
|
+
const yDomain = useMemo(() => {
|
|
4385
|
+
const includeZero = options?.yScale?.zero === true && scaleCanBeZeroed({ type: options.yScale.type ?? "linear" });
|
|
4386
|
+
if (rescaleYOnVisibilityChange && !includeZero) return;
|
|
4170
4387
|
let min = Infinity;
|
|
4171
4388
|
let max = -Infinity;
|
|
4172
|
-
for (const series of dataSorted)
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4389
|
+
for (const series of dataSorted) {
|
|
4390
|
+
if (rescaleYOnVisibilityChange && !isSeriesVisible(series.label)) continue;
|
|
4391
|
+
for (const point of series.data ?? []) {
|
|
4392
|
+
const value = point?.value;
|
|
4393
|
+
if (isReading(value)) {
|
|
4394
|
+
min = Math.min(min, value);
|
|
4395
|
+
max = Math.max(max, value);
|
|
4396
|
+
}
|
|
4177
4397
|
}
|
|
4178
4398
|
}
|
|
4399
|
+
if (includeZero) {
|
|
4400
|
+
if (min === Infinity) return;
|
|
4401
|
+
return max === 0 && min === 0 ? [0, 1] : [Math.min(0, min), Math.max(0, max)];
|
|
4402
|
+
}
|
|
4179
4403
|
return min < max ? [min, max] : void 0;
|
|
4180
|
-
}, [
|
|
4404
|
+
}, [
|
|
4405
|
+
rescaleYOnVisibilityChange,
|
|
4406
|
+
dataSorted,
|
|
4407
|
+
isSeriesVisible,
|
|
4408
|
+
options?.yScale
|
|
4409
|
+
]);
|
|
4181
4410
|
const activateSelectedPoint = useCallback((index) => {
|
|
4182
4411
|
const series = dataSorted[0];
|
|
4183
4412
|
const datum = series?.data[index];
|
|
@@ -4197,7 +4426,24 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4197
4426
|
onActivate: activateSelectedPoint,
|
|
4198
4427
|
preventTooltipScroll: tooltipPlacement === "below-axis"
|
|
4199
4428
|
});
|
|
4429
|
+
const visibleReadingExtent = useMemo(() => {
|
|
4430
|
+
let min = Infinity;
|
|
4431
|
+
let max = -Infinity;
|
|
4432
|
+
for (const series of dataSorted) {
|
|
4433
|
+
if (!isSeriesVisible(series.label)) continue;
|
|
4434
|
+
for (const point of series.data) {
|
|
4435
|
+
const value = point?.value;
|
|
4436
|
+
if (isReading(value)) {
|
|
4437
|
+
min = Math.min(min, value);
|
|
4438
|
+
max = Math.max(max, value);
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
return min <= max ? [min, max] : void 0;
|
|
4443
|
+
}, [dataSorted, isSeriesVisible]);
|
|
4444
|
+
const hasWholeNumberValues = useMemo(() => hasOnlyWholeNumbers(dataSorted.filter((series) => isSeriesVisible(series.label))), [dataSorted, isSeriesVisible]);
|
|
4200
4445
|
const chartOptions = useMemo(() => {
|
|
4446
|
+
const fallbackYDomain = getFallbackYDomain(visibleReadingExtent, options?.yScale?.type === "log");
|
|
4201
4447
|
return {
|
|
4202
4448
|
axis: {
|
|
4203
4449
|
x: buildTimeAxisOptions({
|
|
@@ -4226,7 +4472,8 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4226
4472
|
type: "linear",
|
|
4227
4473
|
nice: true,
|
|
4228
4474
|
zero: false,
|
|
4229
|
-
...
|
|
4475
|
+
...fallbackYDomain ? { domain: fallbackYDomain } : {},
|
|
4476
|
+
...yDomain ? { domain: yDomain } : {},
|
|
4230
4477
|
...options?.yScale
|
|
4231
4478
|
}
|
|
4232
4479
|
};
|
|
@@ -4235,7 +4482,8 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4235
4482
|
dataSorted,
|
|
4236
4483
|
width,
|
|
4237
4484
|
zoom.domain,
|
|
4238
|
-
|
|
4485
|
+
yDomain,
|
|
4486
|
+
visibleReadingExtent,
|
|
4239
4487
|
formatting,
|
|
4240
4488
|
isSeriesVisible
|
|
4241
4489
|
]);
|
|
@@ -4270,11 +4518,13 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4270
4518
|
withGlyph: withLegendGlyph,
|
|
4271
4519
|
glyphSize: Math.max(0, toNumber(glyphStyle?.radius) ?? 4),
|
|
4272
4520
|
collapseGroups: legendCollapseGroups,
|
|
4521
|
+
comparisonItem: legendComparisonItem,
|
|
4273
4522
|
renderGlyph
|
|
4274
4523
|
}), [
|
|
4275
4524
|
withLegendGlyph,
|
|
4276
4525
|
glyphStyle?.radius,
|
|
4277
4526
|
legendCollapseGroups,
|
|
4527
|
+
legendComparisonItem,
|
|
4278
4528
|
renderGlyph
|
|
4279
4529
|
]), legendShape);
|
|
4280
4530
|
const chartMetadata = useMemo(() => ({
|
|
@@ -4386,18 +4636,26 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4386
4636
|
},
|
|
4387
4637
|
xScale: chartOptions.xScale,
|
|
4388
4638
|
yScale: chartOptions.yScale,
|
|
4389
|
-
onPointerDown: zoom.handlers.onPointerDown,
|
|
4390
|
-
onPointerUp: zoom.handlers.onPointerUp,
|
|
4391
|
-
onPointerMove: zoom.handlers.onPointerMove,
|
|
4392
4639
|
onPointerOut,
|
|
4393
|
-
pointerEventsDataKey: "nearest",
|
|
4394
4640
|
children: [
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4641
|
+
/* @__PURE__ */ jsx(NearestPointerEvents, { ...zoom.handlers }),
|
|
4642
|
+
/* @__PURE__ */ jsx(WholeNumberTicks, {
|
|
4643
|
+
axis: "y",
|
|
4644
|
+
numTicks: chartOptions.axis.y.numTicks,
|
|
4645
|
+
enabled: hasWholeNumberValues && !chartOptions.axis.y.tickValues && !options?.yScale?.domain,
|
|
4646
|
+
children: (tickValues) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
4647
|
+
!allSeriesHidden && gridVisibility !== "none" && /* @__PURE__ */ jsx(Grid, {
|
|
4648
|
+
columns: false,
|
|
4649
|
+
numTicks: chartOptions.axis.y.numTicks,
|
|
4650
|
+
tickValues: tickValues ?? chartOptions.axis.y.tickValues
|
|
4651
|
+
}),
|
|
4652
|
+
!allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }),
|
|
4653
|
+
!allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ jsx(Axis, {
|
|
4654
|
+
...chartOptions.axis.y,
|
|
4655
|
+
...tickValues ? { tickValues } : {}
|
|
4656
|
+
})
|
|
4657
|
+
] })
|
|
4398
4658
|
}),
|
|
4399
|
-
!allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }),
|
|
4400
|
-
!allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.y }),
|
|
4401
4659
|
allSeriesHidden ? /* @__PURE__ */ jsx(SvgEmptyState, {
|
|
4402
4660
|
x: width / 2,
|
|
4403
4661
|
y: chartHeight / 2,
|
|
@@ -4538,7 +4796,7 @@ var area_chart_module_default = {
|
|
|
4538
4796
|
const validateData$3 = (data) => {
|
|
4539
4797
|
if (!data?.length) return __("No data available", "jetpack-charts");
|
|
4540
4798
|
if (data.some((series) => !series.data?.length)) return __("No data available", "jetpack-charts");
|
|
4541
|
-
if (data.some((series) => series.data.some((point) =>
|
|
4799
|
+
if (data.some((series) => series.data.some((point) => isInvalidReading(point.value, { allowMissing: false }) || "date" in point && point.date && isNaN(point.date.getTime())))) return __("Invalid data", "jetpack-charts");
|
|
4542
4800
|
return null;
|
|
4543
4801
|
};
|
|
4544
4802
|
//#endregion
|
|
@@ -4654,6 +4912,12 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4654
4912
|
}));
|
|
4655
4913
|
}, [dataSorted, hiddenSeries]);
|
|
4656
4914
|
const allSeriesHidden = useMemo(() => seriesWithVisibility.every(({ isVisible }) => !isVisible), [seriesWithVisibility]);
|
|
4915
|
+
const hasWholeNumberValues = useMemo(() => (!stacked || stackOffset === "none") && hasOnlyWholeNumbers(dataSorted.filter((series) => isSeriesVisible(series.label))), [
|
|
4916
|
+
dataSorted,
|
|
4917
|
+
isSeriesVisible,
|
|
4918
|
+
stacked,
|
|
4919
|
+
stackOffset
|
|
4920
|
+
]);
|
|
4657
4921
|
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
|
|
4658
4922
|
selectedIndex,
|
|
4659
4923
|
setSelectedIndex,
|
|
@@ -4746,8 +5010,9 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4746
5010
|
const legendItems = useChartLegendItems(dataSorted, useMemo(() => ({
|
|
4747
5011
|
withGlyph: false,
|
|
4748
5012
|
glyphSize: 0,
|
|
4749
|
-
collapseGroups: legend.collapseGroups ?? false
|
|
4750
|
-
|
|
5013
|
+
collapseGroups: legend.collapseGroups ?? false,
|
|
5014
|
+
comparisonItem: legend.comparisonItem ?? false
|
|
5015
|
+
}), [legend.collapseGroups, legend.comparisonItem]), legendShape);
|
|
4751
5016
|
const chartMetadata = useMemo(() => ({
|
|
4752
5017
|
stacked,
|
|
4753
5018
|
stackOffset,
|
|
@@ -4897,12 +5162,23 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4897
5162
|
onPointerOut,
|
|
4898
5163
|
pointerEventsDataKey: "nearest",
|
|
4899
5164
|
children: [
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
numTicks:
|
|
5165
|
+
/* @__PURE__ */ jsx(WholeNumberTicks, {
|
|
5166
|
+
axis: "y",
|
|
5167
|
+
numTicks: chartOptions.axis.y.numTicks,
|
|
5168
|
+
enabled: hasWholeNumberValues && !chartOptions.axis.y.tickValues && !options?.yScale?.domain,
|
|
5169
|
+
children: (tickValues) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
5170
|
+
!allSeriesHidden && gridVisibility !== "none" && /* @__PURE__ */ jsx(Grid, {
|
|
5171
|
+
columns: false,
|
|
5172
|
+
numTicks: chartOptions.axis.y.numTicks,
|
|
5173
|
+
tickValues: tickValues ?? chartOptions.axis.y.tickValues
|
|
5174
|
+
}),
|
|
5175
|
+
!allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }),
|
|
5176
|
+
!allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ jsx(Axis, {
|
|
5177
|
+
...chartOptions.axis.y,
|
|
5178
|
+
...tickValues ? { tickValues } : {}
|
|
5179
|
+
})
|
|
5180
|
+
] })
|
|
4903
5181
|
}),
|
|
4904
|
-
!allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }),
|
|
4905
|
-
!allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.y }),
|
|
4906
5182
|
allSeriesHidden ? /* @__PURE__ */ jsx(SvgEmptyState, {
|
|
4907
5183
|
x: width / 2,
|
|
4908
5184
|
y: chartHeight / 2,
|
|
@@ -5091,6 +5367,65 @@ const createTruncatedTickComponent = (axis) => (props) => {
|
|
|
5091
5367
|
const TruncatedXTickComponent = createTruncatedTickComponent("x");
|
|
5092
5368
|
const TruncatedYTickComponent = createTruncatedTickComponent("y");
|
|
5093
5369
|
//#endregion
|
|
5370
|
+
//#region src/charts/bar-chart/private/value-domain.ts
|
|
5371
|
+
/** Domain for a value axis that has nothing to scale, so its ticks still read 0 to 1. */
|
|
5372
|
+
const EMPTY_DOMAIN = [0, 1];
|
|
5373
|
+
/**
|
|
5374
|
+
* The value a bar is drawn at, preferring `visualValue` so a zero still shows a sliver.
|
|
5375
|
+
*
|
|
5376
|
+
* A missing reading is NaN, not null: `ComparisonBars` coerces the value with `Number()`,
|
|
5377
|
+
* which turns null into 0 and would draw a comparison shadow at the baseline.
|
|
5378
|
+
*
|
|
5379
|
+
* @param point - A data point handed to visx.
|
|
5380
|
+
* @return The bar value, or NaN when the bucket has no reading.
|
|
5381
|
+
*/
|
|
5382
|
+
const getBarValue = (point) => {
|
|
5383
|
+
const enhanced = point;
|
|
5384
|
+
return (enhanced?.visualValue !== void 0 ? enhanced.visualValue : point?.value) ?? NaN;
|
|
5385
|
+
};
|
|
5386
|
+
/**
|
|
5387
|
+
* Whether visx renders a bar for a point.
|
|
5388
|
+
*
|
|
5389
|
+
* @param point - A data point handed to visx.
|
|
5390
|
+
* @return True when the point has a finite bar value.
|
|
5391
|
+
*/
|
|
5392
|
+
const isBarRendered = (point) => Number.isFinite(getBarValue(point));
|
|
5393
|
+
/**
|
|
5394
|
+
* How many bars visx renders for a list of points.
|
|
5395
|
+
*
|
|
5396
|
+
* @param points - Data points handed to visx.
|
|
5397
|
+
* @return The number of points that get a bar.
|
|
5398
|
+
*/
|
|
5399
|
+
const countRenderedBars = (points) => points.filter(isBarRendered).length;
|
|
5400
|
+
/**
|
|
5401
|
+
* An explicit domain for the value scale, or null to let visx fit one to the data.
|
|
5402
|
+
*
|
|
5403
|
+
* Zero goes into the domain here rather than through the scale's `zero` flag: visx applies
|
|
5404
|
+
* `nice` before `zero`, which would leave the axis topping out at a raw maximum.
|
|
5405
|
+
*
|
|
5406
|
+
* @param data - Every series handed to the chart.
|
|
5407
|
+
* @param includeZero - Whether the domain must span zero.
|
|
5408
|
+
* @param isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it.
|
|
5409
|
+
* @return Value domain, or null when visx should fit its own.
|
|
5410
|
+
*/
|
|
5411
|
+
const getValueScaleDomain = (data, includeZero, isSeriesRendered) => {
|
|
5412
|
+
let min = Infinity;
|
|
5413
|
+
let max = -Infinity;
|
|
5414
|
+
for (const series of data) {
|
|
5415
|
+
if (!isSeriesRendered(series)) continue;
|
|
5416
|
+
for (const point of series.data) {
|
|
5417
|
+
const value = getBarValue(point);
|
|
5418
|
+
if (!Number.isFinite(value)) continue;
|
|
5419
|
+
min = Math.min(min, value);
|
|
5420
|
+
max = Math.max(max, value);
|
|
5421
|
+
}
|
|
5422
|
+
}
|
|
5423
|
+
if (min === Infinity) return [...EMPTY_DOMAIN];
|
|
5424
|
+
if (min === max) return min === 0 ? [...EMPTY_DOMAIN] : [Math.min(0, min), Math.max(0, max)];
|
|
5425
|
+
if (includeZero) return [Math.min(0, min), Math.max(0, max)];
|
|
5426
|
+
return null;
|
|
5427
|
+
};
|
|
5428
|
+
//#endregion
|
|
5094
5429
|
//#region src/charts/bar-chart/private/use-bar-chart-options.ts
|
|
5095
5430
|
/** Outer padding of the category band scale (space at the chart edges). */
|
|
5096
5431
|
const BASE_BAND_PADDING = .2;
|
|
@@ -5215,10 +5550,6 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
|
|
|
5215
5550
|
const tooltipDatumFormatter = hasLabels ? labelFormatter : byBucket(getTooltipFormatter(data, tickResolution, formatting));
|
|
5216
5551
|
const valueFormatter = formatNumberCompact;
|
|
5217
5552
|
const bandDomain = timeTickFormatter ? getBandDomain(data, isSeriesRendered) : null;
|
|
5218
|
-
const valueAccessor = (d) => {
|
|
5219
|
-
const enhancedPoint = d;
|
|
5220
|
-
return enhancedPoint?.visualValue !== void 0 ? enhancedPoint.visualValue : d?.value;
|
|
5221
|
-
};
|
|
5222
5553
|
return {
|
|
5223
5554
|
timeAxis: bandDomain && timeTickFormatter && {
|
|
5224
5555
|
domain: bandDomain,
|
|
@@ -5229,7 +5560,7 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
|
|
|
5229
5560
|
yTickFormat: valueFormatter,
|
|
5230
5561
|
tooltipLabelFormatter: tooltipDatumFormatter,
|
|
5231
5562
|
xAccessor: bucketAccessor,
|
|
5232
|
-
yAccessor:
|
|
5563
|
+
yAccessor: getBarValue,
|
|
5233
5564
|
gridVisibility: "x",
|
|
5234
5565
|
xScale: bandScale,
|
|
5235
5566
|
yScale: linearScale
|
|
@@ -5238,7 +5569,7 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
|
|
|
5238
5569
|
xTickFormat: valueFormatter,
|
|
5239
5570
|
yTickFormat: labelFormatter,
|
|
5240
5571
|
tooltipLabelFormatter: tooltipDatumFormatter,
|
|
5241
|
-
xAccessor:
|
|
5572
|
+
xAccessor: getBarValue,
|
|
5242
5573
|
yAccessor: bucketAccessor,
|
|
5243
5574
|
gridVisibility: "y",
|
|
5244
5575
|
xScale: linearScale,
|
|
@@ -5253,20 +5584,10 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
|
|
|
5253
5584
|
]);
|
|
5254
5585
|
return useMemo(() => {
|
|
5255
5586
|
const { xTickFormat: defaultXTickFormat, yTickFormat: defaultYTickFormat, tooltipLabelFormatter: defaultTooltipLabelFormatter, xAccessor, yAccessor, gridVisibility, xScale: baseXScale, yScale: baseYScale } = defaultOptions[horizontal ? "horizontal" : "vertical"];
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
data.forEach((series) => {
|
|
5261
|
-
series.data.forEach((d) => {
|
|
5262
|
-
const enhanced = d;
|
|
5263
|
-
const v = enhanced.visualValue !== void 0 ? enhanced.visualValue : d.value;
|
|
5264
|
-
if (typeof v === "number" && Number.isFinite(v)) allValues.push(v);
|
|
5265
|
-
});
|
|
5266
|
-
});
|
|
5267
|
-
if (allValues.length > 0) valueScaleDomainOverride = { domain: [Math.min(0, ...allValues), Math.max(0, ...allValues)] };
|
|
5268
|
-
}
|
|
5269
|
-
}
|
|
5587
|
+
const valueScaleOptions = horizontal ? stableOptions.xScale : stableOptions.yScale;
|
|
5588
|
+
const includeZero = data.some((s) => s.options?.type === "comparison") || valueScaleOptions?.zero !== false;
|
|
5589
|
+
const domain = valueScaleOptions?.domain ? null : getValueScaleDomain(data, includeZero, isSeriesRendered);
|
|
5590
|
+
const valueScaleDomainOverride = domain ? { domain } : {};
|
|
5270
5591
|
const xScale = {
|
|
5271
5592
|
...baseXScale,
|
|
5272
5593
|
...stableOptions.xScale || {},
|
|
@@ -5317,7 +5638,8 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
|
|
|
5317
5638
|
axisConfig,
|
|
5318
5639
|
stableOptions,
|
|
5319
5640
|
horizontal,
|
|
5320
|
-
data
|
|
5641
|
+
data,
|
|
5642
|
+
isSeriesRendered
|
|
5321
5643
|
]);
|
|
5322
5644
|
}
|
|
5323
5645
|
//#endregion
|
|
@@ -5482,6 +5804,8 @@ function BandHighlight({ visible, horizontal, onChange }) {
|
|
|
5482
5804
|
if (position === void 0 || !Number.isFinite(position)) return null;
|
|
5483
5805
|
return {
|
|
5484
5806
|
datum: nearest.datum,
|
|
5807
|
+
key: nearest.key,
|
|
5808
|
+
index: nearest.index,
|
|
5485
5809
|
x: horizontal ? margin?.left ?? 0 : position,
|
|
5486
5810
|
y: horizontal ? position : margin?.top ?? 0,
|
|
5487
5811
|
width: horizontal ? innerWidth : scale.bandwidth(),
|
|
@@ -5618,8 +5942,8 @@ function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointe
|
|
|
5618
5942
|
//#endregion
|
|
5619
5943
|
//#region src/charts/bar-chart/bar-chart.tsx
|
|
5620
5944
|
const validateData$2 = (data) => {
|
|
5621
|
-
if (!data?.length) return "No data available";
|
|
5622
|
-
if (data.some((series) => series.data.some((point) =>
|
|
5945
|
+
if (!data?.length) return __("No data available", "jetpack-charts");
|
|
5946
|
+
if (data.some((series) => series.data.some((point) => isInvalidReading(point.value, { allowMissing: true }) || !point.label && (!("date" in point && point.date) || isNaN(point.date.getTime()))))) return __("Invalid data", "jetpack-charts");
|
|
5623
5947
|
return null;
|
|
5624
5948
|
};
|
|
5625
5949
|
const getPatternId = (chartId, index) => `bar-pattern-${chartId}-${index}`;
|
|
@@ -5628,8 +5952,10 @@ const renderTooltipRow = (label, value) => /* @__PURE__ */ jsx("div", {
|
|
|
5628
5952
|
children: sprintf(__("%1$s: %2$s", "jetpack-charts"), label, value)
|
|
5629
5953
|
});
|
|
5630
5954
|
const BarChartInternal = ({ data, chartId: providedChartId, width, height, className, margin, withTooltips = false, showLegend = false, legend = {}, gridVisibility: gridVisibilityProp, renderTooltip, tooltipPlacement, tooltipAnchorTop, options = {}, orientation = "vertical", withPatterns = false, showZeroValues = false, withBandHighlight = false, onBandHighlightChange, defaultHiddenSeries, animation, children, gap = "md", onPointerDown, onPointerUp, onDatumActivate }) => {
|
|
5955
|
+
if (!withTooltips && (withBandHighlight || onBandHighlightChange)) warnOnce("bar-chart-band-highlight-without-tooltips", "BarChart: withBandHighlight and onBandHighlightChange require withTooltips.");
|
|
5631
5956
|
const legendInteractive = legend.interactive ?? false;
|
|
5632
5957
|
const legendCollapseGroups = legend.collapseGroups ?? false;
|
|
5958
|
+
const legendComparisonItem = legend.comparisonItem ?? false;
|
|
5633
5959
|
const horizontal = orientation === "horizontal";
|
|
5634
5960
|
const chartId = useChartId(providedChartId);
|
|
5635
5961
|
const hiddenSeries = useDefaultHiddenSeries(chartId, defaultHiddenSeries);
|
|
@@ -5640,10 +5966,21 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
5640
5966
|
enabled: showZeroValues,
|
|
5641
5967
|
valueAxisLength: horizontal ? width : height
|
|
5642
5968
|
});
|
|
5643
|
-
const legendItems = useChartLegendItems(dataSorted, useMemo(() => ({
|
|
5969
|
+
const legendItems = useChartLegendItems(dataSorted, useMemo(() => ({
|
|
5970
|
+
collapseGroups: legendCollapseGroups,
|
|
5971
|
+
comparisonItem: legendComparisonItem
|
|
5972
|
+
}), [legendCollapseGroups, legendComparisonItem]));
|
|
5644
5973
|
const { getElementStyles } = useGlobalChartsContext();
|
|
5645
5974
|
const isSeriesRendered = useCallback((series) => isSeriesVisible(series.label), [isSeriesVisible]);
|
|
5975
|
+
const hasWholeNumberValues = useMemo(() => hasOnlyWholeNumbers(dataSorted.filter(isSeriesRendered)), [dataSorted, isSeriesRendered]);
|
|
5646
5976
|
const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options, isSeriesRendered);
|
|
5977
|
+
const valueAxis = horizontal ? chartOptions.axis.x : chartOptions.axis.y;
|
|
5978
|
+
const callerValueDomain = horizontal ? options.xScale?.domain : options.yScale?.domain;
|
|
5979
|
+
const wholeNumberTicksProps = {
|
|
5980
|
+
axis: horizontal ? "x" : "y",
|
|
5981
|
+
numTicks: valueAxis.numTicks,
|
|
5982
|
+
enabled: hasWholeNumberValues && !valueAxis.tickValues && !callerValueDomain
|
|
5983
|
+
};
|
|
5647
5984
|
const defaultMargin = useChartMargin(height, chartOptions, dataSorted, theme, horizontal);
|
|
5648
5985
|
const chartRef = useRef(null);
|
|
5649
5986
|
const { legendChildren, nonLegendChildren } = useChartChildren(children, "BarChart");
|
|
@@ -5776,15 +6113,15 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
5776
6113
|
const p = point;
|
|
5777
6114
|
return nearestDatum.label != null ? p.label === nearestDatum.label : !!nearestDatum.date && !!p.date && p.date.getTime() === nearestDatum.date.getTime();
|
|
5778
6115
|
});
|
|
5779
|
-
if (comparisonEntry && comparisonDatum
|
|
6116
|
+
if (comparisonEntry && comparisonDatum) return /* @__PURE__ */ jsxs("div", {
|
|
5780
6117
|
className: bar_chart_module_default["bar-chart__tooltip"],
|
|
5781
6118
|
children: [
|
|
5782
6119
|
/* @__PURE__ */ jsx("div", {
|
|
5783
6120
|
className: bar_chart_module_default["bar-chart__tooltip-header"],
|
|
5784
6121
|
children: categoryLabel
|
|
5785
6122
|
}),
|
|
5786
|
-
renderTooltipRow(primaryKey,
|
|
5787
|
-
renderTooltipRow(comparisonEntry.series.label,
|
|
6123
|
+
renderTooltipRow(primaryKey, formatReading(nearestDatum.value)),
|
|
6124
|
+
renderTooltipRow(comparisonEntry.series.label, formatReading(comparisonDatum.value))
|
|
5788
6125
|
]
|
|
5789
6126
|
});
|
|
5790
6127
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -5792,7 +6129,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
5792
6129
|
children: [/* @__PURE__ */ jsx("div", {
|
|
5793
6130
|
className: bar_chart_module_default["bar-chart__tooltip-header"],
|
|
5794
6131
|
children: primaryKey
|
|
5795
|
-
}), renderTooltipRow(categoryLabel,
|
|
6132
|
+
}), renderTooltipRow(categoryLabel, formatReading(nearestDatum.value))]
|
|
5796
6133
|
});
|
|
5797
6134
|
}, [chartOptions.tooltip, comparisonEntries]);
|
|
5798
6135
|
const renderPattern = useCallback((index, color) => {
|
|
@@ -5843,15 +6180,15 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
5843
6180
|
const createKeyboardHighlightStyle = useCallback(() => {
|
|
5844
6181
|
if (selectedIndex === void 0) return "";
|
|
5845
6182
|
const primaryCount = primaryEntries.length;
|
|
5846
|
-
|
|
6183
|
+
if (!primaryCount) return "";
|
|
5847
6184
|
const dataPointIndex = Math.floor(selectedIndex / primaryCount);
|
|
5848
6185
|
const seriesIndex = selectedIndex % primaryCount;
|
|
5849
|
-
if (dataPointIndex >= maxDataPoints || seriesIndex >= primaryCount) return "";
|
|
5850
6186
|
const seriesData = primaryEntries[seriesIndex]?.series;
|
|
5851
|
-
|
|
5852
|
-
|
|
6187
|
+
const datum = seriesData?.data[dataPointIndex];
|
|
6188
|
+
if (!seriesData || !datum || !isBarRendered(datum)) return "";
|
|
6189
|
+
const domBarIndex = primaryEntries.slice(0, seriesIndex).reduce((count, { series }) => count + countRenderedBars(series.data), 0) + countRenderedBars(seriesData.data.slice(0, dataPointIndex));
|
|
5853
6190
|
return `
|
|
5854
|
-
.bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${
|
|
6191
|
+
.bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${domBarIndex + 1}) {
|
|
5855
6192
|
stroke: #005fcc;
|
|
5856
6193
|
stroke-width: 2px;
|
|
5857
6194
|
}
|
|
@@ -5945,17 +6282,20 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
5945
6282
|
horizontal,
|
|
5946
6283
|
onChange: onBandHighlightChange
|
|
5947
6284
|
}),
|
|
5948
|
-
!allSeriesHidden && /* @__PURE__ */
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
6285
|
+
!allSeriesHidden && /* @__PURE__ */ jsx(WholeNumberTicks, {
|
|
6286
|
+
...wholeNumberTicksProps,
|
|
6287
|
+
children: (valueTicks) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Grid, {
|
|
6288
|
+
columns: gridVisibility.includes("y"),
|
|
6289
|
+
rows: false,
|
|
6290
|
+
numTicks: chartOptions.axis.x.numTicks,
|
|
6291
|
+
tickValues: (horizontal ? valueTicks : void 0) ?? chartOptions.axis.x.tickValues
|
|
6292
|
+
}), /* @__PURE__ */ jsx(Grid, {
|
|
6293
|
+
columns: false,
|
|
6294
|
+
rows: gridVisibility.includes("x"),
|
|
6295
|
+
numTicks: chartOptions.axis.y.numTicks,
|
|
6296
|
+
tickValues: (horizontal ? void 0 : valueTicks) ?? chartOptions.axis.y.tickValues
|
|
6297
|
+
})] })
|
|
6298
|
+
}),
|
|
5959
6299
|
withPatterns && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("defs", { children: dataSorted.map((seriesData, index) => renderPattern(index, getElementStyles({
|
|
5960
6300
|
data: seriesData,
|
|
5961
6301
|
index
|
|
@@ -5998,7 +6338,16 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
5998
6338
|
onPointerDown,
|
|
5999
6339
|
onPointerUp
|
|
6000
6340
|
}),
|
|
6001
|
-
!allSeriesHidden && /* @__PURE__ */
|
|
6341
|
+
!allSeriesHidden && /* @__PURE__ */ jsx(WholeNumberTicks, {
|
|
6342
|
+
...wholeNumberTicksProps,
|
|
6343
|
+
children: (valueTicks) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Axis, {
|
|
6344
|
+
...chartOptions.axis.x,
|
|
6345
|
+
...horizontal && valueTicks ? { tickValues: valueTicks } : {}
|
|
6346
|
+
}), /* @__PURE__ */ jsx(Axis, {
|
|
6347
|
+
...chartOptions.axis.y,
|
|
6348
|
+
...!horizontal && valueTicks ? { tickValues: valueTicks } : {}
|
|
6349
|
+
})] })
|
|
6350
|
+
}),
|
|
6002
6351
|
withTooltips && /* @__PURE__ */ jsx(AccessibleTooltip, {
|
|
6003
6352
|
tooltipPlacement,
|
|
6004
6353
|
tooltipAnchorTop,
|