@automattic/charts 0.21.0 → 0.23.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +28 -6
  2. package/dist/cjs/components/bar-chart/bar-chart.js +28 -16
  3. package/dist/cjs/components/bar-chart/use-bar-chart-options.js +5 -1
  4. package/dist/cjs/components/leaderboard-chart/leaderboard-chart.js +31 -10
  5. package/dist/cjs/components/leaderboard-chart/leaderboard-chart.module.scss.js +1 -1
  6. package/dist/cjs/components/legend/index.js +5 -1
  7. package/dist/cjs/components/legend/legend.js +23 -0
  8. package/dist/cjs/components/legend/use-chart-legend-data.js +107 -0
  9. package/dist/cjs/components/line-chart/line-chart.js +24 -20
  10. package/dist/cjs/components/pie-chart/pie-chart.js +27 -17
  11. package/dist/cjs/components/pie-semi-circle-chart/pie-semi-circle-chart.js +22 -15
  12. package/dist/cjs/components/pie-semi-circle-chart/pie-semi-circle-chart.module.scss.js +1 -1
  13. package/dist/cjs/components/shared/use-zero-value-display.js +43 -0
  14. package/dist/cjs/components/shared/with-responsive.js +2 -2
  15. package/dist/cjs/index.js +9 -5
  16. package/dist/cjs/providers/chart-context/chart-context.js +12 -7
  17. package/dist/cjs/providers/chart-context/index.js +1 -0
  18. package/dist/cjs/providers/chart-context/utils.js +3 -2
  19. package/dist/cjs/providers/theme/themes.js +42 -0
  20. package/dist/cjs/style.css +1 -1
  21. package/dist/mjs/components/bar-chart/bar-chart.js +31 -19
  22. package/dist/mjs/components/bar-chart/use-bar-chart-options.js +5 -1
  23. package/dist/mjs/components/leaderboard-chart/leaderboard-chart.js +32 -11
  24. package/dist/mjs/components/leaderboard-chart/leaderboard-chart.module.scss.js +1 -1
  25. package/dist/mjs/components/legend/index.js +3 -1
  26. package/dist/mjs/components/legend/legend.js +21 -0
  27. package/dist/mjs/components/legend/use-chart-legend-data.js +105 -0
  28. package/dist/mjs/components/line-chart/line-chart.js +26 -22
  29. package/dist/mjs/components/pie-chart/pie-chart.js +29 -19
  30. package/dist/mjs/components/pie-semi-circle-chart/pie-semi-circle-chart.js +22 -15
  31. package/dist/mjs/components/pie-semi-circle-chart/pie-semi-circle-chart.module.scss.js +1 -1
  32. package/dist/mjs/components/shared/use-zero-value-display.js +41 -0
  33. package/dist/mjs/components/shared/with-responsive.js +2 -2
  34. package/dist/mjs/index.js +3 -1
  35. package/dist/mjs/providers/chart-context/chart-context.js +13 -9
  36. package/dist/mjs/providers/chart-context/index.js +1 -1
  37. package/dist/mjs/providers/chart-context/utils.js +3 -2
  38. package/dist/mjs/providers/theme/themes.js +42 -0
  39. package/dist/mjs/style.css +1 -1
  40. package/dist/types/components/bar-chart/bar-chart.d.ts +1 -0
  41. package/dist/types/components/conversion-funnel-chart/conversion-funnel-chart.d.ts +32 -0
  42. package/dist/types/components/legend/index.d.ts +4 -2
  43. package/dist/types/components/legend/legend.d.ts +9 -0
  44. package/dist/types/components/legend/types.d.ts +6 -2
  45. package/dist/types/components/legend/use-chart-legend-data.d.ts +19 -0
  46. package/dist/types/components/line-chart/line-chart.d.ts +5 -2
  47. package/dist/types/components/pie-chart/pie-chart.d.ts +1 -2
  48. package/dist/types/index.d.ts +8 -4
  49. package/dist/types/providers/chart-context/chart-context.d.ts +4 -2
  50. package/dist/types/providers/chart-context/index.d.ts +1 -1
  51. package/dist/types/types.d.ts +26 -0
  52. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,25 +5,45 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.23.0] - 2025-07-30
9
+ ### Added
10
+ - Add component ConversionFunnelChart. [#44433]
11
+ - Add showZeroValues option to BarChart to render zero-value with a small visible value. [#44443]
12
+
13
+ ### Fixed
14
+ - Charts: Fix TS errors related to missing React import in stories. [#44190]
15
+ - Fix top margin for semi cicle chart. [#44539]
16
+
17
+ ## [0.22.0] - 2025-07-28
18
+ ### Added
19
+ - Add a standalone chart legend component. [#44245]
20
+ - Add docs for various charts. [#44441] [#44465] [#44466] [#44467]
21
+
22
+ ### Changed
23
+ - Apply theme to the LeaderboardChart component. [#44419]
24
+
25
+ ### Fixed
26
+ - Fix issue where pie chart can be cut off. [#44490]
27
+
8
28
  ## [0.21.0] - 2025-07-25
9
29
  ### Added
10
- - Add Storybook docs introduction [#44427]
30
+ - Add Storybook docs introduction. [#44427]
11
31
 
12
32
  ### Changed
13
- - Fix export structure for charts [#44440]
14
- - Removed src and tests to optimize lib size [#44438]
33
+ - Fix export structure for charts. [#44440]
34
+ - Remove non-production files from built package. [#44438]
15
35
 
16
36
  ## [0.20.0] - 2025-07-23
17
37
  ### Added
18
- - Line Chart: Add documentation [#44410]
38
+ - Line Chart: Add documentation. [#44410]
19
39
 
20
40
  ## [0.19.1] - 2025-07-22
21
41
  ### Changed
22
- - Removed dependency on jetpack-components [#44411]
42
+ - Remove dependency on jetpack-components. [#44411]
23
43
 
24
44
  ## [0.19.0] - 2025-07-21
25
45
  ### Added
26
- - Added more exports for Woo Analytics to get rid of visx dependencies [#44390]
46
+ - Added more exports for Woo Analytics to get rid of visx dependencies. [#44390]
27
47
 
28
48
  ## [0.18.0] - 2025-07-21
29
49
  ### Added
@@ -316,6 +336,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
316
336
  - Fixed lints following ESLint rule changes for TS [#40584]
317
337
  - Fixing a bug in Chart storybook data. [#40640]
318
338
 
339
+ [0.23.0]: https://github.com/Automattic/charts/compare/v0.22.0...v0.23.0
340
+ [0.22.0]: https://github.com/Automattic/charts/compare/v0.21.0...v0.22.0
319
341
  [0.21.0]: https://github.com/Automattic/charts/compare/v0.20.0...v0.21.0
320
342
  [0.20.0]: https://github.com/Automattic/charts/compare/v0.19.1...v0.20.0
321
343
  [0.19.1]: https://github.com/Automattic/charts/compare/v0.19.0...v0.19.1
@@ -10,10 +10,13 @@ var react = require('react');
10
10
  var chartContext = require('../../providers/chart-context/chart-context.js');
11
11
  var utils = require('../../providers/chart-context/utils.js');
12
12
  var themeProvider = require('../../providers/theme/theme-provider.js');
13
- var baseLegend = require('../legend/base-legend.js');
13
+ var legend = require('../legend/legend.js');
14
+ require('../legend/base-legend.js');
15
+ var useChartLegendData = require('../legend/use-chart-legend-data.js');
14
16
  var useChartDataTransform = require('../shared/use-chart-data-transform.js');
15
17
  var useChartMargin = require('../shared/use-chart-margin.js');
16
18
  var useElementHeight = require('../shared/use-element-height.js');
19
+ var useZeroValueDisplay = require('../shared/use-zero-value-display.js');
17
20
  var withResponsive = require('../shared/with-responsive.js');
18
21
  var accessibleTooltip = require('../tooltip/accessible-tooltip.js');
19
22
  var barChart_module = require('./bar-chart.module.scss.js');
@@ -33,14 +36,21 @@ const validateData = (data) => {
33
36
  return null;
34
37
  };
35
38
  const getPatternId = (chartId, index) => `bar-pattern-${chartId}-${index}`;
36
- const BarChartInternal = ({ data, chartId: providedChartId, width, height = 400, className, margin, withTooltips = false, showLegend = false, legendOrientation = 'horizontal', legendAlignmentHorizontal = 'center', legendAlignmentVertical = 'bottom', legendShape = 'rect', gridVisibility: gridVisibilityProp, renderTooltip, options = {}, orientation = 'vertical', withPatterns = false, }) => {
39
+ const BarChartInternal = ({ data, chartId: providedChartId, width, height = 400, className, margin, withTooltips = false, showLegend = false, legendOrientation = 'horizontal', legendAlignmentHorizontal = 'center', legendAlignmentVertical = 'bottom', legendShape = 'rect', gridVisibility: gridVisibilityProp, renderTooltip, options = {}, orientation = 'vertical', withPatterns = false, showZeroValues = false, }) => {
37
40
  const horizontal = orientation === 'horizontal';
38
41
  // Generate a unique chart ID to avoid pattern conflicts with multiple charts
39
42
  const internalChartId = react.useId();
40
43
  const chartId = utils.useChartId(providedChartId);
44
+ const providerTheme = themeProvider.useChartTheme();
41
45
  const theme = themeProvider.useXYChartTheme(data);
42
46
  const dataSorted = useChartDataTransform.useChartDataTransform(data);
43
- const chartOptions = useBarChartOptions.useBarChartOptions(dataSorted, horizontal, options);
47
+ // Transform data to add a small value for zero bars to make them visible
48
+ const dataWithVisibleZeros = useZeroValueDisplay.useZeroValueDisplay(dataSorted, {
49
+ enabled: showZeroValues,
50
+ });
51
+ // Create legend items using the reusable hook
52
+ const legendItems = useChartLegendData.useChartLegendData(dataSorted, providerTheme);
53
+ const chartOptions = useBarChartOptions.useBarChartOptions(dataWithVisibleZeros, horizontal, options);
44
54
  const defaultMargin = useChartMargin.useChartMargin(height, chartOptions, dataSorted, theme, horizontal);
45
55
  const [legendRef, legendHeight] = useElementHeight.useElementHeight();
46
56
  const chartRef = react.useRef(null);
@@ -129,19 +139,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height = 400,
129
139
  // Validate data first
130
140
  const error = validateData(dataSorted);
131
141
  const isDataValid = !error;
132
- // Create legend items (hooks must be called in same order every render)
133
- const legendItems = react.useMemo(() => dataSorted.map((group, index) => ({
134
- label: group.label, // Label for each unique group
135
- value: '', // Empty string since we don't want to show a specific value
136
- color: getColor(group, index),
137
- shapeStyle: group?.options?.legendShapeStyle,
138
- })), [dataSorted, getColor]);
139
- // Register chart with context only if data is valid
140
- const providerTheme = themeProvider.useChartTheme();
141
- utils.useChartRegistration(chartId, legendItems, providerTheme, 'bar', isDataValid, {
142
+ // Memoize metadata to prevent unnecessary re-registration
143
+ const chartMetadata = react.useMemo(() => ({
142
144
  orientation,
143
145
  withPatterns,
144
- });
146
+ }), [orientation, withPatterns]);
147
+ // Register chart with context only if data is valid
148
+ utils.useChartRegistration(chartId, legendItems, providerTheme, 'bar', isDataValid, chartMetadata);
145
149
  if (error) {
146
150
  return jsxRuntime.jsx("div", { className: clsx('bar-chart', barChart_module.default['bar-chart']), children: error });
147
151
  }
@@ -158,9 +162,17 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height = 400,
158
162
  ...(showLegend && legendAlignmentVertical === 'top'
159
163
  ? { top: (defaultMargin.top || 0) + legendHeight }
160
164
  : {}),
161
- }, xScale: chartOptions.xScale, yScale: chartOptions.yScale, horizontal: horizontal, pointerEventsDataKey: "nearest", children: [jsxRuntime.jsx(xychart.Grid, { columns: gridVisibility.includes('y'), rows: gridVisibility.includes('x'), numTicks: 4 }), withPatterns && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("defs", { "data-testid": "bar-chart-patterns", children: dataSorted.map((seriesData, index) => renderPattern(index, getColor(seriesData, index))) }), jsxRuntime.jsx("style", { children: dataSorted.map((seriesData, index) => createPatternBorderStyle(index, getColor(seriesData, index))) })] })), highlightedBarStyle && jsxRuntime.jsx("style", { children: highlightedBarStyle }), jsxRuntime.jsx(xychart.BarGroup, { padding: chartOptions.barGroup.padding, children: dataSorted.map((seriesData, index) => (jsxRuntime.jsx(xychart.BarSeries, { dataKey: seriesData?.label, data: seriesData.data, yAccessor: chartOptions.accessors.yAccessor, xAccessor: chartOptions.accessors.xAccessor, colorAccessor: getBarBackground(index) }, seriesData?.label))) }), jsxRuntime.jsx(xychart.Axis, { ...chartOptions.axis.x }), jsxRuntime.jsx(xychart.Axis, { ...chartOptions.axis.y }), withTooltips && (jsxRuntime.jsx(accessibleTooltip.AccessibleTooltip, { detectBounds: true, snapTooltipToDatumX: true, snapTooltipToDatumY: true, renderTooltip: renderTooltip || renderDefaultTooltip, selectedIndex: selectedIndex, tooltipRef: tooltipRef, keyboardFocusedClassName: barChart_module.default['bar-chart__tooltip--keyboard-focused'], series: data, mode: "individual" }))] }), showLegend && (jsxRuntime.jsx(baseLegend.BaseLegend, { items: legendItems, orientation: legendOrientation, alignmentHorizontal: legendAlignmentHorizontal, alignmentVertical: legendAlignmentVertical, className: barChart_module.default['bar-chart__legend'], shape: legendShape, ref: legendRef }))] }));
165
+ }, xScale: chartOptions.xScale, yScale: chartOptions.yScale, horizontal: horizontal, pointerEventsDataKey: "nearest", children: [jsxRuntime.jsx(xychart.Grid, { columns: gridVisibility.includes('y'), rows: gridVisibility.includes('x'), numTicks: 4 }), withPatterns && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("defs", { "data-testid": "bar-chart-patterns", children: dataSorted.map((seriesData, index) => renderPattern(index, getColor(seriesData, index))) }), jsxRuntime.jsx("style", { children: dataSorted.map((seriesData, index) => createPatternBorderStyle(index, getColor(seriesData, index))) })] })), highlightedBarStyle && jsxRuntime.jsx("style", { children: highlightedBarStyle }), jsxRuntime.jsx(xychart.BarGroup, { padding: chartOptions.barGroup.padding, children: dataWithVisibleZeros.map((seriesData, index) => (jsxRuntime.jsx(xychart.BarSeries, { dataKey: seriesData?.label, data: seriesData.data, yAccessor: chartOptions.accessors.yAccessor, xAccessor: chartOptions.accessors.xAccessor, colorAccessor: getBarBackground(index) }, seriesData?.label))) }), jsxRuntime.jsx(xychart.Axis, { ...chartOptions.axis.x }), jsxRuntime.jsx(xychart.Axis, { ...chartOptions.axis.y }), withTooltips && (jsxRuntime.jsx(accessibleTooltip.AccessibleTooltip, { detectBounds: true, snapTooltipToDatumX: true, snapTooltipToDatumY: true, renderTooltip: renderTooltip || renderDefaultTooltip, selectedIndex: selectedIndex, tooltipRef: tooltipRef, keyboardFocusedClassName: barChart_module.default['bar-chart__tooltip--keyboard-focused'], series: data, mode: "individual" }))] }), showLegend && (jsxRuntime.jsx(legend.Legend, { items: legendItems, orientation: legendOrientation, alignmentHorizontal: legendAlignmentHorizontal, alignmentVertical: legendAlignmentVertical, className: barChart_module.default['bar-chart__legend'], shape: legendShape, ref: legendRef, chartId: chartId }))] }));
166
+ };
167
+ const BarChart = props => {
168
+ const existingContext = react.useContext(chartContext.ChartContext);
169
+ // If we're already in a ChartProvider context, don't create a new one
170
+ if (existingContext) {
171
+ return jsxRuntime.jsx(BarChartInternal, { ...props });
172
+ }
173
+ // Otherwise, create our own ChartProvider
174
+ return (jsxRuntime.jsx(chartContext.ChartProvider, { children: jsxRuntime.jsx(BarChartInternal, { ...props }) }));
162
175
  };
163
- const BarChart = props => (jsxRuntime.jsx(chartContext.ChartProvider, { children: jsxRuntime.jsx(BarChartInternal, { ...props }) }));
164
176
  BarChart.displayName = 'BarChart';
165
177
  var BarChart$1 = withResponsive.withResponsive(BarChart);
166
178
 
@@ -44,7 +44,11 @@ function useBarChartOptions(data, horizontal, options = {}) {
44
44
  : formatDateTick;
45
45
  const valueFormatter = numberFormatters.formatNumberCompact;
46
46
  const labelAccessor = (d) => d?.label || d?.date;
47
- const valueAccessor = (d) => d?.value;
47
+ const valueAccessor = (d) => {
48
+ // Use visualValue for bar rendering if available (for zero values), otherwise use value
49
+ const enhancedPoint = d;
50
+ return enhancedPoint?.visualValue !== undefined ? enhancedPoint.visualValue : d?.value;
51
+ };
48
52
  return {
49
53
  vertical: {
50
54
  xTickFormat: labelFormatter,
@@ -4,11 +4,24 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
  var components = require('@wordpress/components');
7
+ var element = require('@wordpress/element');
7
8
  var clsx = require('clsx');
8
9
  require('react');
10
+ var themeProvider = require('../../providers/theme/theme-provider.js');
9
11
  var formatMetricValue = require('../shared/format-metric-value.js');
10
12
  var leaderboardChart_module = require('./leaderboard-chart.module.scss.js');
11
13
 
14
+ /**
15
+ * Default settings for LeaderboardChart component
16
+ */
17
+ const DEFAULT_LEADERBOARD_SETTINGS = {
18
+ labelSpacing: 1.5,
19
+ rowGap: 12,
20
+ columnGap: 4,
21
+ primaryColor: '#3858E9',
22
+ secondaryColor: '#66BDFF',
23
+ deltaColors: ['#D63638', '#757575', '#008A20'],
24
+ };
12
25
  /**
13
26
  * Default value formatter using formatMetricValue
14
27
  *
@@ -49,25 +62,33 @@ const defaultDeltaFormatter = (value) => {
49
62
  * @param props.style - Custom styling for the chart container
50
63
  * @return JSX element representing the leaderboard chart
51
64
  */
52
- const LeaderboardChart = ({ data, withComparison = false, primaryColor = '#3858E9', secondaryColor = '#66BDFF', valueFormatter = defaultValueFormatter, deltaFormatter = defaultDeltaFormatter, loading = false, className, style, }) => {
53
- // TODO: Integrate with ThemeProvider:
54
- // 1. Use theme.colors for primaryColor/secondaryColor defaults
55
- // 2. Get delta sign colors from theme instead of hardcoding
56
- // 3. Add useChartTheme() hook like other chart components
57
- const signColors = ['#D63638', '#757575', '#008A20'];
65
+ const LeaderboardChart = ({ data, withComparison = false, primaryColor, secondaryColor, valueFormatter = defaultValueFormatter, deltaFormatter = defaultDeltaFormatter, loading = false, className, style, }) => {
66
+ const theme = themeProvider.useChartTheme();
67
+ // Get component settings from theme with fallbacks
68
+ const leaderboardSettings = theme.leaderboardChart;
69
+ const labelSpacing = leaderboardSettings?.labelSpacing ?? DEFAULT_LEADERBOARD_SETTINGS.labelSpacing;
70
+ const rowGap = leaderboardSettings?.rowGap ?? DEFAULT_LEADERBOARD_SETTINGS.rowGap;
71
+ const columnGap = leaderboardSettings?.columnGap ?? DEFAULT_LEADERBOARD_SETTINGS.columnGap;
72
+ // Use theme colors with prop overrides, fallback to defaults
73
+ const finalPrimaryColor = primaryColor || leaderboardSettings?.primaryColor || DEFAULT_LEADERBOARD_SETTINGS.primaryColor;
74
+ const finalSecondaryColor = secondaryColor ||
75
+ leaderboardSettings?.secondaryColor ||
76
+ DEFAULT_LEADERBOARD_SETTINGS.secondaryColor;
77
+ // Delta sign colors: negative, neutral, positive
78
+ const signColors = leaderboardSettings?.deltaColors ?? DEFAULT_LEADERBOARD_SETTINGS.deltaColors;
58
79
  const chartStyle = {
59
- '--primary-color': primaryColor,
60
- '--secondary-color': secondaryColor,
80
+ '--primary-color': finalPrimaryColor,
81
+ '--secondary-color': finalSecondaryColor,
61
82
  ...style,
62
83
  };
63
84
  // Handle empty or undefined data
64
85
  if (!data || data.length === 0) {
65
86
  return (jsxRuntime.jsx("div", { className: clsx(leaderboardChart_module.default.leaderboardChart, loading && leaderboardChart_module.default.loading, className), style: chartStyle, children: jsxRuntime.jsx("div", { className: leaderboardChart_module.default.emptyState, children: loading ? 'Loading...' : 'No data available' }) }));
66
87
  }
67
- return (jsxRuntime.jsx("div", { className: clsx(leaderboardChart_module.default.leaderboardChart, loading && leaderboardChart_module.default.loading, className), style: chartStyle, children: data.map(entry => {
88
+ return (jsxRuntime.jsx(components.__experimentalGrid, { className: clsx(leaderboardChart_module.default.leaderboardChart, loading && leaderboardChart_module.default.loading, className), templateColumns: "minmax(0, 1fr) auto", rowGap: rowGap, columnGap: columnGap, style: chartStyle, children: data.map(entry => {
68
89
  const colorIndex = Math.sign(entry.delta) + 1;
69
90
  const deltaColor = signColors[colorIndex];
70
- return (jsxRuntime.jsxs("div", { className: leaderboardChart_module.default.entryContainer, children: [jsxRuntime.jsxs("div", { className: leaderboardChart_module.default.labelContainer, children: [jsxRuntime.jsx("span", { className: leaderboardChart_module.default.entryLabel, children: entry.label }), jsxRuntime.jsxs("div", { className: leaderboardChart_module.default.progressContainer, children: [jsxRuntime.jsx(components.ProgressBar, { value: entry.currentShare, className: clsx(leaderboardChart_module.default.progressBar, leaderboardChart_module.default.primaryBar) }), withComparison && (jsxRuntime.jsx(components.ProgressBar, { value: entry.previousShare, className: clsx(leaderboardChart_module.default.progressBar, leaderboardChart_module.default.secondaryBar) }))] })] }), jsxRuntime.jsxs("div", { className: leaderboardChart_module.default.valueContainer, children: [jsxRuntime.jsx("span", { className: leaderboardChart_module.default.currentValue, children: valueFormatter(entry.currentValue) }), withComparison && (jsxRuntime.jsx("span", { className: leaderboardChart_module.default.deltaValue, style: { color: deltaColor }, children: deltaFormatter(entry.delta) }))] })] }, entry.id));
91
+ return (jsxRuntime.jsxs(element.Fragment, { children: [jsxRuntime.jsxs(components.__experimentalVStack, { spacing: labelSpacing, children: [jsxRuntime.jsx(components.__experimentalText, { children: entry.label }), jsxRuntime.jsxs("div", { className: leaderboardChart_module.default.progressContainer, children: [jsxRuntime.jsx(components.ProgressBar, { value: entry.currentShare, className: clsx(leaderboardChart_module.default.progressBar, leaderboardChart_module.default.primaryBar) }), withComparison && (jsxRuntime.jsx(components.ProgressBar, { value: entry.previousShare, className: clsx(leaderboardChart_module.default.progressBar, leaderboardChart_module.default.secondaryBar) }))] })] }), jsxRuntime.jsxs("div", { className: leaderboardChart_module.default.valueContainer, children: [jsxRuntime.jsx(components.__experimentalText, { children: valueFormatter(entry.currentValue) }), withComparison && (jsxRuntime.jsx(components.__experimentalText, { style: { color: deltaColor }, children: deltaFormatter(entry.delta) }))] })] }, entry.id));
71
92
  }) }));
72
93
  };
73
94
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var styles = {"leaderboardChart":"leaderboard-chart-module_leaderboardChart__zxakP","loading":"leaderboard-chart-module_loading__-AGv-","entryContainer":"leaderboard-chart-module_entryContainer__Os9tq","labelContainer":"leaderboard-chart-module_labelContainer__lt8CW","entryLabel":"leaderboard-chart-module_entryLabel__NPMEo","progressContainer":"leaderboard-chart-module_progressContainer__BZGbj","progressBar":"leaderboard-chart-module_progressBar__LAQaj","primaryBar":"leaderboard-chart-module_primaryBar__iybII","secondaryBar":"leaderboard-chart-module_secondaryBar__A7tLz","valueContainer":"leaderboard-chart-module_valueContainer__ZlLh4","currentValue":"leaderboard-chart-module_currentValue__I0aZw","deltaValue":"leaderboard-chart-module_deltaValue__zJpIW","emptyState":"leaderboard-chart-module_emptyState__0dkfy"};
5
+ var styles = {"leaderboardChart":"leaderboard-chart-module_leaderboardChart__zxakP","loading":"leaderboard-chart-module_loading__-AGv-","progressContainer":"leaderboard-chart-module_progressContainer__BZGbj","progressBar":"leaderboard-chart-module_progressBar__LAQaj","primaryBar":"leaderboard-chart-module_primaryBar__iybII","secondaryBar":"leaderboard-chart-module_secondaryBar__A7tLz","valueContainer":"leaderboard-chart-module_valueContainer__ZlLh4","emptyState":"leaderboard-chart-module_emptyState__0dkfy"};
6
6
 
7
7
  exports.default = styles;
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ var legend = require('./legend.js');
3
4
  var baseLegend = require('./base-legend.js');
5
+ var useChartLegendData = require('./use-chart-legend-data.js');
4
6
 
5
7
 
6
8
 
7
- exports.Legend = baseLegend.BaseLegend;
9
+ exports.Legend = legend.Legend;
10
+ exports.BaseLegend = baseLegend.BaseLegend;
11
+ exports.useChartLegendData = useChartLegendData.useChartLegendData;
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var react = require('react');
5
+ var chartContext = require('../../providers/chart-context/chart-context.js');
6
+ var baseLegend = require('./base-legend.js');
7
+
8
+ const Legend = react.forwardRef(({ chartId, items, ...props }, ref) => {
9
+ // Get context but don't throw if it doesn't exist
10
+ const context = react.useContext(chartContext.ChartContext);
11
+ // Use useMemo to ensure re-rendering when context changes
12
+ const contextItems = react.useMemo(() => {
13
+ return chartId && context ? context.getChartData(chartId)?.legendItems : undefined;
14
+ }, [chartId, context]);
15
+ // Use context items if available, otherwise fall back to provided items
16
+ const legendItems = (contextItems || items);
17
+ if (!legendItems) {
18
+ return null;
19
+ }
20
+ return jsxRuntime.jsx(baseLegend.BaseLegend, { ref: ref, items: legendItems, ...props });
21
+ });
22
+
23
+ exports.Legend = Legend;
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ /**
6
+ * Formats the value for a data point based on its type
7
+ * @param point - The data point to format
8
+ * @param showValues - Whether to show values or return empty string
9
+ * @return Formatted value string
10
+ */
11
+ function formatPointValue(point, showValues) {
12
+ if (!showValues) {
13
+ return '';
14
+ }
15
+ if ('percentage' in point) {
16
+ return `${point.percentage}%`;
17
+ }
18
+ else if ('value' in point) {
19
+ return point.value.toString();
20
+ }
21
+ return '';
22
+ }
23
+ /**
24
+ * Creates a base legend item with common properties
25
+ * @param label - The label for the legend item
26
+ * @param value - The value for the legend item
27
+ * @param color - The color for the legend item
28
+ * @return Base legend item object
29
+ */
30
+ function createBaseLegendItem(label, value, color) {
31
+ return {
32
+ label,
33
+ value,
34
+ color,
35
+ };
36
+ }
37
+ /**
38
+ * Processes SeriesData into legend items
39
+ * @param seriesData - The series data to process
40
+ * @param theme - The chart theme for colors
41
+ * @param showValues - Whether to show values in legend
42
+ * @param withGlyph - Whether to include glyph rendering
43
+ * @param glyphSize - Size of the glyph
44
+ * @param renderGlyph - Component to render the glyph
45
+ * @return Array of processed legend items
46
+ */
47
+ function processSeriesData(seriesData, theme, showValues, withGlyph, glyphSize, renderGlyph) {
48
+ const mapper = (series, index) => {
49
+ const baseItem = createBaseLegendItem(series.label, showValues ? series.data?.length?.toString() || '0' : '', theme.colors[index % theme.colors.length]);
50
+ if (withGlyph && renderGlyph) {
51
+ return {
52
+ ...baseItem,
53
+ glyphSize,
54
+ renderGlyph,
55
+ };
56
+ }
57
+ return baseItem;
58
+ };
59
+ return seriesData.map(mapper);
60
+ }
61
+ /**
62
+ * Processes point data into legend items
63
+ * @param pointData - The point data to process
64
+ * @param theme - The chart theme for colors
65
+ * @param showValues - Whether to show values in legend
66
+ * @param withGlyph - Whether to include glyph rendering
67
+ * @param glyphSize - Size of the glyph
68
+ * @param renderGlyph - Component to render the glyph
69
+ * @return Array of processed legend items
70
+ */
71
+ function processPointData(pointData, theme, showValues, withGlyph, glyphSize, renderGlyph) {
72
+ const mapper = (point, index) => {
73
+ const baseItem = createBaseLegendItem(point.label, formatPointValue(point, showValues), theme.colors[index % theme.colors.length]);
74
+ if (withGlyph && renderGlyph) {
75
+ return {
76
+ ...baseItem,
77
+ glyphSize,
78
+ renderGlyph,
79
+ };
80
+ }
81
+ return baseItem;
82
+ };
83
+ return pointData.map(mapper);
84
+ }
85
+ /**
86
+ * Hook to transform chart data into legend items
87
+ * @param data - The chart data to transform
88
+ * @param theme - The chart theme for colors
89
+ * @param options - Configuration options for legend generation
90
+ * @return Array of legend items ready for display
91
+ */
92
+ function useChartLegendData(data, theme, options = {}) {
93
+ const { showValues = false, withGlyph = false, glyphSize = 8, renderGlyph } = options;
94
+ return react.useMemo(() => {
95
+ if (!data || !Array.isArray(data) || data.length === 0) {
96
+ return [];
97
+ }
98
+ // Handle SeriesData (multiple series with data points)
99
+ if ('data' in data[0]) {
100
+ return processSeriesData(data, theme, showValues, withGlyph, glyphSize, renderGlyph);
101
+ }
102
+ // Handle DataPointDate or DataPointPercentage (single data points)
103
+ return processPointData(data, theme, showValues, withGlyph, glyphSize, renderGlyph);
104
+ }, [data, theme, showValues, withGlyph, glyphSize, renderGlyph]);
105
+ }
106
+
107
+ exports.useChartLegendData = useChartLegendData;
@@ -12,7 +12,9 @@ var clsx = require('clsx');
12
12
  var chartContext = require('../../providers/chart-context/chart-context.js');
13
13
  var utils = require('../../providers/chart-context/utils.js');
14
14
  var themeProvider = require('../../providers/theme/theme-provider.js');
15
- var baseLegend = require('../legend/base-legend.js');
15
+ var legend = require('../legend/legend.js');
16
+ require('../legend/base-legend.js');
17
+ var useChartLegendData = require('../legend/use-chart-legend-data.js');
16
18
  var defaultGlyph = require('../shared/default-glyph.js');
17
19
  var useChartDataTransform = require('../shared/use-chart-data-transform.js');
18
20
  var useChartMargin = require('../shared/use-chart-margin.js');
@@ -195,30 +197,24 @@ const LineChartInternal = react.forwardRef(({ data, chartId: providedChartId, wi
195
197
  const defaultMargin = useChartMargin.useChartMargin(height, chartOptions, dataSorted, theme);
196
198
  const error = validateData(dataSorted);
197
199
  const isDataValid = !error;
198
- // Create legend items (hooks must be called in same order every render)
199
- const legendItems = react.useMemo(() => dataSorted.map((group, index) => ({
200
- label: group.label, // Label for each unique group
201
- value: '', // Empty string since we don't want to show a specific value
202
- color: group?.options?.stroke ?? providerTheme.colors[index % providerTheme.colors.length],
203
- shapeStyle: group?.options?.legendShapeStyle,
204
- renderGlyph: withLegendGlyph ? providerTheme.glyphs?.[index] ?? renderGlyph : undefined,
200
+ // Memoize legend options to prevent unnecessary re-calculations
201
+ const legendOptions = react.useMemo(() => ({
202
+ withGlyph: withLegendGlyph,
205
203
  glyphSize: Math.max(0, toNumber(glyphStyle?.radius) ?? 4),
206
- })), [
207
- dataSorted,
208
- providerTheme.colors,
209
- providerTheme.glyphs,
210
- withLegendGlyph,
211
204
  renderGlyph,
212
- glyphStyle?.radius,
213
- ]);
214
- // Register chart with context only if data is valid
215
- utils.useChartRegistration(chartId, legendItems, providerTheme, 'line', isDataValid, {
205
+ }), [withLegendGlyph, glyphStyle?.radius, renderGlyph]);
206
+ // Create legend items using the reusable hook
207
+ const legendItems = useChartLegendData.useChartLegendData(dataSorted, providerTheme, legendOptions);
208
+ // Memoize metadata to prevent unnecessary re-registration
209
+ const chartMetadata = react.useMemo(() => ({
216
210
  withGradientFill,
217
211
  smoothing,
218
212
  curveType,
219
213
  withStartGlyphs,
220
214
  withLegendGlyph,
221
- });
215
+ }), [withGradientFill, smoothing, curveType, withStartGlyphs, withLegendGlyph]);
216
+ // Register chart with context only if data is valid
217
+ utils.useChartRegistration(chartId, legendItems, providerTheme, 'line', isDataValid, chartMetadata);
222
218
  const accessors = {
223
219
  xAccessor: (d) => d?.date,
224
220
  yAccessor: (d) => d?.value,
@@ -254,9 +250,17 @@ const LineChartInternal = react.forwardRef(({ data, chartId: providedChartId, wi
254
250
  return (jsxRuntime.jsxs("g", { children: [withStartGlyphs && (jsxRuntime.jsx(StartGlyph, { index: index, data: seriesData, color: stroke, renderGlyph: providerTheme.glyphs?.[index] ?? renderGlyph, accessors: accessors, glyphStyle: glyphStyle })), withGradientFill && (jsxRuntime.jsx(gradient.LinearGradient, { id: `area-gradient-${internalChartId}-${index + 1}`, from: stroke, fromOpacity: 0.4, toOpacity: 0.1, to: theme.backgroundColor, ...seriesData.options?.gradient, "data-testid": "line-gradient" })), jsxRuntime.jsx(xychart.AreaSeries, { dataKey: seriesData?.label, data: seriesData.data, ...accessors, fill: withGradientFill
255
251
  ? `url(#area-gradient-${internalChartId}-${index + 1})`
256
252
  : 'transparent', renderLine: true, curve: getCurveType(curveType, smoothing), lineProps: lineProps }, seriesData?.label)] }, seriesData?.label || index));
257
- }), withTooltips && (jsxRuntime.jsx(accessibleTooltip.AccessibleTooltip, { detectBounds: true, snapTooltipToDatumX: true, snapTooltipToDatumY: true, showSeriesGlyphs: true, renderTooltip: renderTooltip, renderGlyph: tooltipRenderGlyph, glyphStyle: glyphStyle, showVerticalCrosshair: withTooltipCrosshairs?.showVertical, showHorizontalCrosshair: withTooltipCrosshairs?.showHorizontal, selectedIndex: selectedIndex, tooltipRef: tooltipRef, keyboardFocusedClassName: lineChart_module.default['line-chart__tooltip--keyboard-focused'], series: dataSorted })), jsxRuntime.jsx(LineChartScalesRef, { chartRef: internalChartRef, width: width, height: height, margin: margin })] }) }), showLegend && (jsxRuntime.jsx(baseLegend.BaseLegend, { items: legendItems, orientation: legendOrientation, alignmentHorizontal: legendAlignmentHorizontal, alignmentVertical: legendAlignmentVertical, className: lineChart_module.default['line-chart-legend'], shape: legendShape, ref: legendRef })), children] }) }));
253
+ }), withTooltips && (jsxRuntime.jsx(accessibleTooltip.AccessibleTooltip, { detectBounds: true, snapTooltipToDatumX: true, snapTooltipToDatumY: true, showSeriesGlyphs: true, renderTooltip: renderTooltip, renderGlyph: tooltipRenderGlyph, glyphStyle: glyphStyle, showVerticalCrosshair: withTooltipCrosshairs?.showVertical, showHorizontalCrosshair: withTooltipCrosshairs?.showHorizontal, selectedIndex: selectedIndex, tooltipRef: tooltipRef, keyboardFocusedClassName: lineChart_module.default['line-chart__tooltip--keyboard-focused'], series: dataSorted })), jsxRuntime.jsx(LineChartScalesRef, { chartRef: internalChartRef, width: width, height: height, margin: margin })] }) }), showLegend && (jsxRuntime.jsx(legend.Legend, { items: legendItems, orientation: legendOrientation, alignmentHorizontal: legendAlignmentHorizontal, alignmentVertical: legendAlignmentVertical, className: lineChart_module.default['line-chart-legend'], shape: legendShape, chartId: chartId, ref: legendRef })), children] }) }));
254
+ });
255
+ const LineChart = react.forwardRef((props, ref) => {
256
+ const existingContext = react.useContext(chartContext.ChartContext);
257
+ // If we're already in a ChartProvider context, don't create a new one
258
+ if (existingContext) {
259
+ return jsxRuntime.jsx(LineChartInternal, { ...props, ref: ref });
260
+ }
261
+ // Otherwise, create our own ChartProvider
262
+ return (jsxRuntime.jsx(chartContext.ChartProvider, { children: jsxRuntime.jsx(LineChartInternal, { ...props, ref: ref }) }));
258
263
  });
259
- const LineChart = react.forwardRef((props, ref) => (jsxRuntime.jsx(chartContext.ChartProvider, { children: jsxRuntime.jsx(LineChartInternal, { ...props, ref: ref }) })));
260
264
  LineChart.displayName = 'LineChart';
261
265
  LineChart.AnnotationsOverlay = lineChartAnnotationsOverlay.default;
262
266
  LineChart.Annotation = lineChartAnnotation.default;
@@ -12,7 +12,9 @@ var chartContext = require('../../providers/chart-context/chart-context.js');
12
12
  var utils = require('../../providers/chart-context/utils.js');
13
13
  var themeProvider = require('../../providers/theme/theme-provider.js');
14
14
  var themes = require('../../providers/theme/themes.js');
15
- var baseLegend = require('../legend/base-legend.js');
15
+ var legend = require('../legend/legend.js');
16
+ require('../legend/base-legend.js');
17
+ var useChartLegendData = require('../legend/use-chart-legend-data.js');
16
18
  var useElementHeight = require('../shared/use-element-height.js');
17
19
  var withResponsive = require('../shared/with-responsive.js');
18
20
  var baseTooltip = require('../tooltip/base-tooltip.js');
@@ -53,30 +55,30 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
53
55
  const { onMouseMove, onMouseLeave, tooltipOpen, tooltipData, tooltipLeft, tooltipTop } = useChartMouseHandler.default({
54
56
  withTooltips,
55
57
  });
58
+ // Memoize legend options to prevent unnecessary re-calculations
59
+ const legendOptions = react.useMemo(() => ({ showValues: true }), []);
60
+ // Create legend items using the reusable hook
61
+ const legendItems = useChartLegendData.useChartLegendData(data, providerTheme, legendOptions);
56
62
  const { isValid, message } = validateData(data);
57
- // Create legend items (hooks must be called in same order every render)
58
- const legendItems = react.useMemo(() => data.map((item, index) => ({
59
- label: item.label,
60
- value: item.value.toString(),
61
- color: providerTheme.colors[index % providerTheme.colors.length],
62
- })), [data, providerTheme.colors]);
63
- // Register chart with context only if data is valid
64
- utils.useChartRegistration(chartId, legendItems, providerTheme, 'pie', isValid, {
63
+ // Memoize metadata to prevent unnecessary re-registration
64
+ const chartMetadata = react.useMemo(() => ({
65
65
  thickness,
66
66
  gapScale,
67
67
  cornerScale,
68
- });
68
+ }), [thickness, gapScale, cornerScale]);
69
+ // Register chart with context only if data is valid
70
+ utils.useChartRegistration(chartId, legendItems, providerTheme, 'pie', isValid, chartMetadata);
69
71
  if (!isValid) {
70
72
  return (jsxRuntime.jsx("div", { className: clsx('pie-chart', pieChart_module.default['pie-chart'], className), children: jsxRuntime.jsx("div", { className: pieChart_module.default['error-message'], children: message }) }));
71
73
  }
72
74
  const width = size;
73
75
  const height = size;
76
+ const adjustedHeight = showLegend && legendAlignmentVertical === 'top' ? height - legendHeight : height;
74
77
  // Calculate radius based on width/height
75
- const radius = Math.min(width, height) / 2;
76
- // Center the chart in the available space, adjusting for legend position
78
+ const radius = Math.min(width, adjustedHeight) / 2;
79
+ // Center the chart in the available space
77
80
  const centerX = width / 2;
78
- const legendOffset = showLegend && legendAlignmentVertical === 'top' ? legendHeight / 2 : 0;
79
- const centerY = height / 2 + legendOffset;
81
+ const centerY = adjustedHeight / 2;
80
82
  // Calculate the angle between each
81
83
  const padAngle = gapScale * ((2 * Math.PI) / data.length);
82
84
  const outerRadius = radius - padding;
@@ -96,7 +98,7 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
96
98
  return (jsxRuntime.jsxs("div", { className: clsx('pie-chart', pieChart_module.default['pie-chart'], className), style: {
97
99
  display: 'flex',
98
100
  flexDirection: showLegend && legendAlignmentVertical === 'top' ? 'column-reverse' : 'column',
99
- }, children: [jsxRuntime.jsx("svg", { viewBox: `0 0 ${size} ${size}`, preserveAspectRatio: "xMidYMid meet", width: size, height: size, children: jsxRuntime.jsxs(group.Group, { top: centerY, left: centerX, children: [jsxRuntime.jsx(shape.Pie, { data: dataWithIndex, pieValue: accessors.value, outerRadius: outerRadius, innerRadius: innerRadius, padAngle: padAngle, cornerRadius: cornerRadius, children: pie => {
101
+ }, children: [jsxRuntime.jsx("svg", { viewBox: `0 0 ${size} ${adjustedHeight}`, preserveAspectRatio: "xMidYMid meet", width: size, height: adjustedHeight, children: jsxRuntime.jsxs(group.Group, { top: centerY, left: centerX, children: [jsxRuntime.jsx(shape.Pie, { data: dataWithIndex, pieValue: accessors.value, outerRadius: outerRadius, innerRadius: innerRadius, padAngle: padAngle, cornerRadius: cornerRadius, children: pie => {
100
102
  return pie.arcs.map((arc, index) => {
101
103
  const [centroidX, centroidY] = pie.path.centroid(arc);
102
104
  const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.25;
@@ -111,11 +113,19 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
111
113
  }
112
114
  return (jsxRuntime.jsxs("g", { children: [jsxRuntime.jsx("path", { ...pathProps }), hasSpaceForLabel && (jsxRuntime.jsx("text", { x: centroidX, y: centroidY, dy: ".33em", fill: providerTheme.labelBackgroundColor || themes.defaultTheme.labelBackgroundColor, fontSize: 12, textAnchor: "middle", pointerEvents: "none", children: arc.data.label }))] }, `arc-${index}`));
113
115
  });
114
- } }), children] }) }), showLegend && (jsxRuntime.jsx(baseLegend.BaseLegend, { items: legendItems, orientation: legendOrientation, alignmentHorizontal: legendAlignmentHorizontal, alignmentVertical: legendAlignmentVertical, className: pieChart_module.default['pie-chart-legend'], shape: legendShape, ref: legendRef })), withTooltips && tooltipOpen && tooltipData && (jsxRuntime.jsx(baseTooltip.BaseTooltip, { data: tooltipData, top: tooltipTop || 0, left: tooltipLeft || 0, style: {
116
+ } }), children] }) }), showLegend && (jsxRuntime.jsx(legend.Legend, { items: legendItems, orientation: legendOrientation, alignmentHorizontal: legendAlignmentHorizontal, alignmentVertical: legendAlignmentVertical, className: pieChart_module.default['pie-chart-legend'], shape: legendShape, ref: legendRef, chartId: chartId })), withTooltips && tooltipOpen && tooltipData && (jsxRuntime.jsx(baseTooltip.BaseTooltip, { data: tooltipData, top: tooltipTop || 0, left: tooltipLeft || 0, style: {
115
117
  transform: 'translate(-50%, -100%)',
116
118
  } }))] }));
117
119
  };
118
- const PieChart = (props) => (jsxRuntime.jsx(chartContext.ChartProvider, { children: jsxRuntime.jsx(PieChartInternal, { ...props }) }));
120
+ const PieChart = (props) => {
121
+ const existingContext = react.useContext(chartContext.ChartContext);
122
+ // If we're already in a ChartProvider context, don't create a new one
123
+ if (existingContext) {
124
+ return jsxRuntime.jsx(PieChartInternal, { ...props });
125
+ }
126
+ // Otherwise, create our own ChartProvider
127
+ return (jsxRuntime.jsx(chartContext.ChartProvider, { children: jsxRuntime.jsx(PieChartInternal, { ...props }) }));
128
+ };
119
129
  PieChart.displayName = 'PieChart';
120
130
  var pieChart = withResponsive.withResponsive(PieChart);
121
131