• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SAP / ui5-webcomponents-react / 12295412387

12 Dec 2024 11:24AM CUT coverage: 87.137% (-0.02%) from 87.155%
12295412387

Pull #6738

github

web-flow
Merge bb6f5b3c7 into 6c377c04e
Pull Request #6738: chore: update monorepo to React19

2921 of 3885 branches covered (75.19%)

5108 of 5862 relevant lines covered (87.14%)

51403.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.79
/packages/charts/src/components/PieChart/PieChart.tsx
1
'use client';
2

3
import { enrichEventWithDetails, useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base';
4
import { clsx } from 'clsx';
5
import type { CSSProperties } from 'react';
6
import { cloneElement, forwardRef, isValidElement, useCallback, useMemo } from 'react';
7
import {
8
  Cell,
9
  Label as RechartsLabel,
10
  Legend,
11
  Pie,
12
  PieChart as PieChartLib,
13
  Sector,
14
  Text as RechartsText,
15
  Tooltip
16
} from 'recharts';
17
import { getValueByDataKey } from 'recharts/lib/util/ChartUtils.js';
18
import { useLegendItemClick } from '../../hooks/useLegendItemClick.js';
19
import { useOnClickInternal } from '../../hooks/useOnClickInternal.js';
20
import type { IChartBaseProps } from '../../interfaces/IChartBaseProps.js';
21
import type { IChartDimension } from '../../interfaces/IChartDimension.js';
22
import type { IChartMeasure } from '../../interfaces/IChartMeasure.js';
23
import type { IPolarChartConfig } from '../../interfaces/IPolarChartConfig.js';
24
import { ChartContainer } from '../../internal/ChartContainer.js';
25
import { defaultFormatter } from '../../internal/defaults.js';
26
import { tooltipContentStyle, tooltipFillOpacity } from '../../internal/staticProps.js';
27
import { classNames, styleData } from './PieChart.module.css.js';
28
import { PieChartPlaceholder } from './Placeholder.js';
29

30
interface MeasureConfig extends Omit<IChartMeasure, 'accessor' | 'label' | 'color' | 'hideDataLabel'> {
31
  /**
32
   * A string containing the path to the dataset key this pie should display.
33
   * Supports object structures by using `'parent.child'`. Can also be a getter.
34
   */
35
  accessor: string;
36
  /**
37
   * array of any valid CSS Color or CSS Variable. Defaults to the `sapChart_OrderedColor_` colors
38
   */
39
  colors?: CSSProperties['color'][];
40
  /**
41
   * Flag whether the data labels should be hidden in the chart for this segment.
42
   * When passed a function it will be called for each data label with the corresponding chart properties.
43
   */
44
  hideDataLabel?: boolean | ((chartConfig: any) => boolean);
45
}
46

47
export interface PieChartProps extends Omit<IChartBaseProps<IPolarChartConfig>, 'dimensions' | 'measures'> {
48
  /**
49
   * A label to display in the center of the `PieChart`.
50
   * If you use this prop to display a text, we recommend to increase `chartConfig.innerRadius` to have some free
51
   * space for the text.
52
   */
53
  centerLabel?: string;
54
  /**
55
   * A object which contains the configuration of the dimension.
56
   *
57
   * **Required Properties**
58
   * - `accessor`: string containing the path to the dataset key the dimension should display. Supports object structures by using <code>'parent.child'</code>.
59
   *   Can also be a getter.
60
   *
61
   * **Optional Properties**
62
   * - `formatter`: function will be called for each data label and allows you to format it according to your needs
63
   *
64
   */
65
  dimension: IChartDimension;
66
  /**
67
   * A object which contains the configuration of the measure. The object is defining one pie in the chart.
68
   *
69
   * **Required properties**
70
   * - `accessor`: string containing the path to the dataset key this pie should display. Supports object structures by using <code>'parent.child'</code>.
71
   *
72
   * **Optional properties**
73
   *
74
   * - `formatter`: function will be called for each data label and allows you to format it according to your needs
75
   * - `DataLabel`: a custom component to be used for the data label
76
   * - `colors`: array of any valid CSS Color or CSS Variable. Defaults to the `sapChart_OrderedColor_` colors
77
   * - `hideDataLabel`: flag whether the data labels should be hidden in the chart for this segment. When passed a function it will be called for each data label with the corresponding chart properties.
78
   */
79
  measure: MeasureConfig;
80
}
81

82
const tooltipItemDefaultStyle = { color: 'var (--sapTextColor)' };
31✔
83

84
/**
85
 * A Pie Chart is a type of graph that displays data in a circular graph.
86
 * The pieces of the graph are proportional to the fraction of the whole in each category.
87
 *
88
 * In other words, each slice of the pie is relative to the size of that category in the group as a whole.
89
 * The entire “pie” represents 100 percent of a whole, while the pie “slices” represent portions of the whole.
90
 */
91
const PieChart = forwardRef<HTMLDivElement, PieChartProps>((props, ref) => {
31✔
92
  const {
93
    loading,
94
    loadingDelay,
95
    dataset,
96
    noLegend,
97
    noAnimation,
98
    tooltipConfig,
99
    onDataPointClick,
100
    onLegendClick,
101
    onClick,
102
    centerLabel,
103
    style,
104
    className,
105
    slot,
106
    ChartPlaceholder,
107
    children,
108
    ...rest
109
  } = props;
94✔
110

111
  useStylesheet(styleData, PieChart.displayName);
94✔
112
  const [componentRef, chartRef] = useSyncRef(ref);
94✔
113
  const isDonutChart = props['data-component-name'] === 'DonutChart';
94✔
114

115
  const chartConfig: PieChartProps['chartConfig'] = {
94✔
116
    margin: { right: 30, left: 30, bottom: 30, top: 30, ...(props.chartConfig?.margin ?? {}) },
188✔
117
    legendPosition: 'bottom',
118
    legendHorizontalAlign: 'center',
119
    paddingAngle: 0,
120
    outerRadius: '80%',
121
    resizeDebounce: 250,
122
    tooltipItemStyle: tooltipItemDefaultStyle,
123
    ...props.chartConfig
124
  };
125

126
  const showActiveSegmentDataLabel = chartConfig.showActiveSegmentDataLabel ?? true;
94✔
127

128
  const dimension: IChartDimension = useMemo(
94✔
129
    () => ({
47✔
130
      formatter: defaultFormatter,
131
      ...props.dimension
132
    }),
133
    [props.dimension]
134
  );
135

136
  const measure: MeasureConfig = useMemo(
94✔
137
    () => ({
47✔
138
      formatter: defaultFormatter,
139
      ...props.measure
140
    }),
141
    [props.measure]
142
  );
143

144
  const dataLabel = (props) => {
94✔
145
    const hideDataLabel =
146
      typeof measure.hideDataLabel === 'function' ? measure.hideDataLabel(props) : measure.hideDataLabel;
744!
147
    if (hideDataLabel || chartConfig.activeSegment === props.index) return null;
744!
148

149
    if (isValidElement(measure.DataLabel)) {
744✔
150
      return cloneElement(measure.DataLabel, { ...props, config: measure });
72✔
151
    }
152

153
    return (
672✔
154
      <RechartsText {...props} alignmentBaseline="middle" className="recharts-pie-label-text">
155
        {measure.formatter(props.value)}
156
      </RechartsText>
157
    );
158
  };
159

160
  const tooltipValueFormatter = useCallback(
94✔
161
    (value, name) => [measure.formatter(value), dimension.formatter(name)],
11✔
162
    [measure.formatter, dimension.formatter]
163
  );
164

165
  const onItemLegendClick = useLegendItemClick(onLegendClick, () => measure.accessor);
94✔
166
  const onClickInternal = useOnClickInternal(onClick);
94✔
167

168
  const onDataPointClickInternal = useCallback(
94✔
169
    (payload, dataIndex, event) => {
170
      if (payload && payload && typeof onDataPointClick === 'function') {
11✔
171
        onDataPointClick(
11✔
172
          enrichEventWithDetails(event, {
173
            value: payload.value,
174
            dataKey: payload.tooltipPayload?.[0]?.dataKey,
175
            name: payload.name,
176
            payload: payload.payload,
177
            dataIndex
178
          })
179
        );
180
      }
181
    },
182
    [onDataPointClick]
183
  );
184

185
  const renderActiveShape = useCallback(
94✔
186
    (props) => {
187
      const RADIAN = Math.PI / 180;
×
188
      const { cx, cy, midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, payload, percent, value } = props;
×
189
      const sin = Math.sin(-RADIAN * midAngle);
×
190
      const cos = Math.cos(-RADIAN * midAngle);
×
191
      const sx = cx + (outerRadius + 10) * cos;
×
192
      const sy = cy + (outerRadius + 10) * sin;
×
193
      const mx = cx + (outerRadius + 30) * cos;
×
194
      const my = cy + (outerRadius + 30) * sin;
×
195
      const ex = mx + (cos >= 0 ? 1 : -1) * 22;
×
196
      const ey = my;
×
197
      const textAnchor = cos >= 0 ? 'start' : 'end';
×
198
      const activeLegendItem = chartRef.current?.querySelector<HTMLLIElement>(
×
199
        `.legend-item-${chartConfig.activeSegment}`
200
      );
201
      if (!activeLegendItem?.dataset.activeLegend) {
×
202
        const allLegendItems = chartRef.current?.querySelectorAll('.recharts-legend-item');
×
203

204
        allLegendItems.forEach((item) => item.removeAttribute('data-active-legend'));
×
205
        activeLegendItem.setAttribute('data-active-legend', 'true');
×
206
      }
207

208
      return (
×
209
        <g>
210
          {isDonutChart && (
×
211
            <text x={cx} y={cy} dy={8} textAnchor="middle" fill={fill}>
212
              {payload.name}
213
            </text>
214
          )}
215
          <Sector
216
            cx={cx}
217
            cy={cy}
218
            innerRadius={innerRadius}
219
            outerRadius={outerRadius}
220
            startAngle={startAngle}
221
            endAngle={endAngle}
222
            fill={fill}
223
          />
224
          <Sector
225
            cx={cx}
226
            cy={cy}
227
            startAngle={startAngle}
228
            endAngle={endAngle}
229
            innerRadius={outerRadius + 6}
230
            outerRadius={outerRadius + 10}
231
            fill={fill}
232
          />
233
          {showActiveSegmentDataLabel && (
×
234
            <>
235
              <path d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`} stroke={fill} fill="none" />
236
              <circle cx={ex} cy={ey} r={2} fill={fill} stroke="none" />
237
              <text x={ex + (cos >= 0 ? 1 : -1) * 12} y={ey} textAnchor={textAnchor} fill={fill}>
×
238
                {measure.formatter(value)}
239
              </text>
240
              <text x={ex + (cos >= 0 ? 1 : -1) * 12} y={ey} dy={18} textAnchor={textAnchor} fill={fill}>
×
241
                {`(${(percent * 100).toFixed(2)}%)`}
242
              </text>
243
            </>
244
          )}
245
        </g>
246
      );
247
    },
248
    [showActiveSegmentDataLabel, chartConfig.activeSegment, isDonutChart]
249
  );
250

251
  const renderLabelLine = useCallback(
94✔
252
    (props) => {
253
      const hideDataLabel =
254
        typeof measure.hideDataLabel === 'function' ? measure.hideDataLabel(props) : measure.hideDataLabel;
744!
255
      if (hideDataLabel || chartConfig.activeSegment === props.index) return null;
744!
256
      return Pie.renderLabelLineItem({}, props, undefined);
744✔
257
    },
258
    [chartConfig.activeSegment, measure.hideDataLabel]
259
  );
260

261
  const legendWrapperStyle = useMemo(() => {
94✔
262
    if (chartConfig.activeSegment != null && showActiveSegmentDataLabel) {
47!
263
      if (chartConfig.legendPosition === 'bottom') {
×
264
        return {
×
265
          paddingBlockStart: '30px'
266
        };
267
      } else if (chartConfig.legendPosition === 'top') {
×
268
        return {
×
269
          paddingBlockEnd: '30px'
270
        };
271
      }
272
    }
273

274
    return null;
47✔
275
  }, [showActiveSegmentDataLabel, chartConfig.activeSegment, chartConfig.legendPosition]);
276

277
  const { chartConfig: _0, dimension: _1, measure: _2, ...propsWithoutOmitted } = rest;
94✔
278

279
  return (
94✔
280
    <ChartContainer
281
      dataset={dataset}
282
      ref={componentRef}
283
      loading={loading}
284
      loadingDelay={loadingDelay}
285
      Placeholder={ChartPlaceholder ?? PieChartPlaceholder}
188✔
286
      style={style}
287
      className={className}
288
      slot={slot}
289
      resizeDebounce={chartConfig.resizeDebounce}
290
      {...propsWithoutOmitted}
291
    >
292
      {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
293
      {/*// @ts-ignore:: todo not yet compatible with React19*/}
294
      <PieChartLib
295
        onClick={onClickInternal}
296
        margin={chartConfig.margin}
297
        accessibilityLayer={chartConfig.accessibilityLayer}
298
        className={clsx(
299
          typeof onDataPointClick === 'function' || typeof onClick === 'function' ? 'has-click-handler' : undefined,
260✔
300
          classNames.piechart
301
        )}
302
      >
303
        <Pie
304
          onClick={onDataPointClickInternal}
305
          innerRadius={chartConfig.innerRadius}
306
          outerRadius={chartConfig.outerRadius}
307
          paddingAngle={chartConfig.paddingAngle}
308
          nameKey={dimension.accessor}
309
          dataKey={measure.accessor}
310
          data={dataset}
311
          animationBegin={0}
312
          isAnimationActive={!noAnimation}
313
          labelLine={renderLabelLine}
314
          label={dataLabel}
315
          activeIndex={chartConfig.activeSegment}
316
          activeShape={chartConfig.activeSegment != null && renderActiveShape}
94!
317
          rootTabIndex={-1}
318
        >
319
          {centerLabel && <RechartsLabel position="center">{centerLabel}</RechartsLabel>}
94!
320
          {dataset &&
174✔
321
            dataset.map((data, index) => (
322
              <Cell
744✔
323
                key={index}
324
                name={dimension.formatter(getValueByDataKey(data, dimension.accessor, ''))}
325
                fill={measure.colors?.[index] ?? `var(--sapChart_OrderedColor_${(index % 12) + 1})`}
1,488✔
326
              />
327
            ))}
328
        </Pie>
329
        {tooltipConfig?.active !== false && (
188✔
330
          <Tooltip
331
            cursor={tooltipFillOpacity}
332
            formatter={tooltipValueFormatter}
333
            contentStyle={tooltipContentStyle}
334
            itemStyle={chartConfig.tooltipItemStyle}
335
            labelStyle={chartConfig.tooltipLabelStyle}
336
            {...tooltipConfig}
337
          />
338
        )}
339
        {!noLegend && (
188✔
340
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
341
          // @ts-ignore
342
          <Legend
343
            verticalAlign={chartConfig.legendPosition}
344
            align={chartConfig.legendHorizontalAlign}
345
            onClick={onItemLegendClick}
346
            wrapperStyle={legendWrapperStyle}
347
            {...chartConfig.legendConfig}
348
          />
349
        )}
350
        {children}
351
      </PieChartLib>
352
    </ChartContainer>
353
  );
354
});
355

356
PieChart.displayName = 'PieChart';
31✔
357

358
export { PieChart };
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc