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

SAP / ui5-webcomponents-react / 11380019916

17 Oct 2024 07:05AM CUT coverage: 87.181%. Remained the same
11380019916

Pull #6498

github

web-flow
Merge 38b871a88 into 76b925b48
Pull Request #6498: docs(Table): improve story

2884 of 3845 branches covered (75.01%)

5060 of 5804 relevant lines covered (87.18%)

97902.37 hits per line

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

81.11
/packages/charts/src/components/ComposedChart/index.tsx
1
'use client';
2

3
import { enrichEventWithDetails, ThemingParameters, useIsRTL, useSyncRef } from '@ui5/webcomponents-react-base';
4
import type { CSSProperties, FC } from 'react';
5
import { forwardRef, useCallback } from 'react';
6
import {
7
  Area,
8
  Bar,
9
  Brush,
10
  CartesianGrid,
11
  Cell,
12
  ComposedChart as ComposedChartLib,
13
  LabelList,
14
  Legend,
15
  Line,
16
  ReferenceLine,
17
  Tooltip,
18
  XAxis,
19
  YAxis
20
} from 'recharts';
21
import type { YAxisProps } from 'recharts';
22
import { getValueByDataKey } from 'recharts/lib/util/ChartUtils.js';
23
import { useChartMargin } from '../../hooks/useChartMargin.js';
24
import { useLabelFormatter } from '../../hooks/useLabelFormatter.js';
25
import { useLegendItemClick } from '../../hooks/useLegendItemClick.js';
26
import { useLongestYAxisLabel } from '../../hooks/useLongestYAxisLabel.js';
27
import { useObserveXAxisHeights } from '../../hooks/useObserveXAxisHeights.js';
28
import { useOnClickInternal } from '../../hooks/useOnClickInternal.js';
29
import { usePrepareDimensionsAndMeasures } from '../../hooks/usePrepareDimensionsAndMeasures.js';
30
import { useTooltipFormatter } from '../../hooks/useTooltipFormatter.js';
31
import type { IChartBaseProps } from '../../interfaces/IChartBaseProps.js';
32
import type { IChartDimension } from '../../interfaces/IChartDimension.js';
33
import type { IChartMeasure } from '../../interfaces/IChartMeasure.js';
34
import { ChartContainer } from '../../internal/ChartContainer.js';
35
import { ChartDataLabel } from '../../internal/ChartDataLabel.js';
36
import { defaultFormatter } from '../../internal/defaults.js';
37
import { brushProps, tickLineConfig, tooltipContentStyle, tooltipFillOpacity } from '../../internal/staticProps.js';
38
import { getCellColors, resolvePrimaryAndSecondaryMeasures } from '../../internal/Utils.js';
39
import { XAxisTicks } from '../../internal/XAxisTicks.js';
40
import { YAxisTicks } from '../../internal/YAxisTicks.js';
41
import { ComposedChartPlaceholder } from './Placeholder.js';
42

43
const dimensionDefaults = {
32✔
44
  formatter: defaultFormatter
45
};
46

47
const measureDefaults = {
32✔
48
  formatter: defaultFormatter,
49
  opacity: 1
50
};
51

52
interface MeasureConfig extends IChartMeasure {
53
  /**
54
   * width of the current chart element, defaults to `1` for `lines` and `20` for bars
55
   */
56
  width?: number;
57
  /**
58
   * Opacity
59
   * @default 1
60
   */
61
  opacity?: number;
62
  /**
63
   * Chart type
64
   */
65
  type: AvailableChartTypes;
66
  /**
67
   * bar Stack ID
68
   * @default undefined
69
   */
70
  stackId?: string;
71
  /**
72
   * Highlight color of defined elements
73
   * @param value {string | number} Current value of the highlighted measure
74
   * @param measure {IChartMeasure} Current measure object
75
   * @param dataElement {object} Current data element
76
   */
77
  highlightColor?: (value: number, measure: MeasureConfig, dataElement: Record<string, any>) => CSSProperties['color'];
78
}
79

80
interface DimensionConfig extends IChartDimension {
81
  interval?: YAxisProps['interval'];
82
}
83

84
export interface ComposedChartProps extends IChartBaseProps {
85
  /**
86
   * An array of config objects. Each object will define one dimension of the chart.
87
   *
88
   * **Required Properties**
89
   * - `accessor`: string containing the path to the dataset key the dimension should display. Supports object structures by using <code>'parent.child'</code>.
90
   *   Can also be a getter.
91
   *
92
   * **Optional Properties**
93
   * - `formatter`: function will be called for each data label and allows you to format it according to your needs
94
   * - `interval`: number that controls how many ticks are rendered on the x axis
95
   *
96
   */
97
  dimensions: DimensionConfig[];
98
  /**
99
   * An array of config objects. Each object is defining one element in the chart.
100
   *
101
   * **Required properties**
102
   * - `accessor`: string containing the path to the dataset key this element should display. Supports object structures by using <code>'parent.child'</code>.
103
   *   Can also be a getter.
104
   * - `type`: string which chart element to show. Possible values: `line`, `bar`, `area`.
105
   *
106
   * **Optional properties**
107
   *
108
   * - `label`: Label to display in legends or tooltips. Falls back to the <code>accessor</code> if not present.
109
   * - `color`: any valid CSS Color or CSS Variable. Defaults to the `sapChart_Ordinal` colors
110
   * - `formatter`: function will be called for each data label and allows you to format it according to your needs
111
   * - `hideDataLabel`: flag whether the data labels should be hidden in the chart for this element.
112
   * - `DataLabel`: a custom component to be used for the data label
113
   * - `width`: width of the current chart element, defaults to `1` for `lines` and `20` for bars
114
   * - `opacity`: element opacity, defaults to `1`
115
   * - `stackId`: bars with the same stackId will be stacked
116
   * - `highlightColor`: function will be called to define a custom color of a specific element which matches the
117
   *    defined condition. Overwrites code>color</code> of the element.
118
   *
119
   */
120
  measures: MeasureConfig[];
121
  /**
122
   * layout for showing measures. `horizontal` bars would equal the column chart, `vertical` would be a bar chart.
123
   * Default Value: `horizontal`
124
   *
125
   * @default `"horizontal"`
126
   */
127
  layout?: 'horizontal' | 'vertical';
128
}
129

130
const ChartTypes = {
32✔
131
  line: Line,
132
  bar: Bar,
133
  area: Area
134
};
135

136
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
137
type AvailableChartTypes = 'line' | 'bar' | 'area' | string;
138

139
/**
140
 * The `ComposedChart` enables you to combine different chart types in one chart, e.g. showing bars together with lines.
141
 */
142
const ComposedChart = forwardRef<HTMLDivElement, ComposedChartProps>((props, ref) => {
32✔
143
  const {
144
    loading,
145
    loadingDelay,
146
    dataset,
147
    onDataPointClick,
148
    noLegend,
149
    noAnimation,
150
    tooltipConfig,
151
    onLegendClick,
152
    onClick,
153
    layout = 'horizontal',
100✔
154
    style,
155
    className,
156
    slot,
157
    syncId,
158
    ChartPlaceholder,
159
    children,
160
    ...rest
161
  } = props;
114✔
162

163
  const [componentRef, chartRef] = useSyncRef<any>(ref);
100✔
164

165
  const chartConfig: ComposedChartProps['chartConfig'] = {
100✔
166
    yAxisLabelsVisible: true,
167
    yAxisVisible: false,
168
    xAxisVisible: true,
169
    gridStroke: ThemingParameters.sapList_BorderColor,
170
    gridHorizontal: true,
171
    gridVertical: false,
172
    legendPosition: 'bottom',
173
    legendHorizontalAlign: 'left',
174
    zoomingTool: false,
175
    resizeDebounce: 250,
176
    yAxisWidth: null,
177
    yAxisConfig: {},
178
    xAxisConfig: {},
179
    secondYAxisConfig: {},
180
    secondXAxisConfig: {},
181
    ...props.chartConfig
182
  };
183
  const { referenceLine } = chartConfig;
100✔
184

185
  const { dimensions, measures } = usePrepareDimensionsAndMeasures(
100✔
186
    props.dimensions,
187
    props.measures,
188
    dimensionDefaults,
189
    measureDefaults
190
  );
191

192
  const tooltipValueFormatter = useTooltipFormatter(measures);
100✔
193

194
  const primaryDimension = dimensions[0];
100✔
195
  const { primaryMeasure, secondaryMeasure } = resolvePrimaryAndSecondaryMeasures(
100✔
196
    measures,
197
    chartConfig?.secondYAxis?.dataKey
198
  );
199

200
  const labelFormatter = useLabelFormatter(primaryDimension);
100✔
201

202
  const dataKeys = measures.map(({ accessor }) => accessor);
196✔
203
  const colorSecondY = chartConfig.secondYAxis
100!
204
    ? dataKeys.findIndex((key) => key === chartConfig.secondYAxis?.dataKey)
×
205
    : 0;
206

207
  const valueAccessor =
208
    (attribute) =>
100✔
209
    ({ payload }) => {
84✔
210
      return getValueByDataKey(payload, attribute);
228✔
211
    };
212

213
  const onDataPointClickInternal = (payload, eventOrIndex, event) => {
100✔
214
    if (typeof onDataPointClick === 'function') {
12!
215
      if (typeof eventOrIndex === 'number') {
×
216
        const payloadValueLength = Array.isArray(payload?.value);
×
217
        onDataPointClick(
×
218
          enrichEventWithDetails(event, {
219
            value: payloadValueLength ? payload.value[1] - payload.value[0] : payload.value,
×
220
            dataIndex: payload.index ?? eventOrIndex,
×
221
            dataKey: payloadValueLength
×
222
              ? Object.keys(payload).filter((key) =>
223
                  payload.value.length
×
224
                    ? payload[key] === payload.value[1] - payload.value[0]
225
                    : payload[key] === payload.value && key !== 'value'
×
226
                )[0]
227
              : (payload.dataKey ??
×
228
                Object.keys(payload).find((key) => payload[key] && payload[key] === payload.value && key !== 'value')),
×
229
            payload: payload.payload
230
          })
231
        );
232
      } else {
233
        onDataPointClick(
×
234
          enrichEventWithDetails({} as any, {
235
            value: Array.isArray(eventOrIndex.value)
×
236
              ? eventOrIndex.value[1] - eventOrIndex.value[0]
237
              : eventOrIndex.value,
238
            dataKey: eventOrIndex.dataKey,
239
            dataIndex: eventOrIndex.index,
240
            payload: eventOrIndex.payload
241
          })
242
        );
243
      }
244
    }
245
  };
246

247
  const onItemLegendClick = useLegendItemClick(onLegendClick);
100✔
248
  const onClickInternal = useOnClickInternal(onClick);
100✔
249

250
  const isBigDataSet = dataset?.length > 30;
100✔
251
  const primaryDimensionAccessor = primaryDimension?.accessor;
100✔
252

253
  const [yAxisWidth, legendPosition] = useLongestYAxisLabel(
100✔
254
    dataset,
255
    layout === 'vertical' ? dimensions : measures,
100!
256
    chartConfig.legendPosition
257
  );
258

259
  const marginChart = useChartMargin(chartConfig.margin, chartConfig.zoomingTool);
100✔
260
  const xAxisHeights = useObserveXAxisHeights(chartRef, layout === 'vertical' ? 1 : props.dimensions.length);
100!
261

262
  const measureAxisProps = {
100✔
263
    axisLine: chartConfig.yAxisVisible,
264
    tickLine: tickLineConfig,
265
    tickFormatter: primaryMeasure?.formatter,
266
    interval: 0
267
  };
268

269
  const Placeholder = useCallback(() => {
100✔
270
    return <ComposedChartPlaceholder layout={layout} measures={measures} />;
18✔
271
  }, [layout, measures]);
272

273
  const { chartConfig: _0, dimensions: _1, measures: _2, ...propsWithoutOmitted } = rest;
100✔
274
  const isRTL = useIsRTL(chartRef);
100✔
275

276
  return (
100✔
277
    <ChartContainer
278
      ref={componentRef}
279
      loading={loading}
280
      loadingDelay={loadingDelay}
281
      dataset={dataset}
282
      Placeholder={ChartPlaceholder ?? Placeholder}
165✔
283
      style={style}
284
      className={className}
285
      slot={slot}
286
      resizeDebounce={chartConfig.resizeDebounce}
287
      {...propsWithoutOmitted}
288
    >
289
      {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
290
      {/*// @ts-ignore:: todo not yet compatible with React19*/}
291
      <ComposedChartLib
292
        syncId={syncId}
293
        onClick={onClickInternal}
294
        stackOffset="sign"
295
        margin={marginChart}
296
        data={dataset}
297
        layout={layout}
298
        accessibilityLayer={chartConfig.accessibilityLayer}
299
        className={
300
          typeof onDataPointClick === 'function' || typeof onClick === 'function' ? 'has-click-handler' : undefined
300✔
301
        }
302
      >
303
        <CartesianGrid
304
          vertical={chartConfig.gridVertical}
305
          horizontal={chartConfig.gridHorizontal}
306
          stroke={chartConfig.gridStroke}
307
        />
308
        {dimensions.map((dimension, index) => {
309
          let AxisComponent;
310
          const axisProps: any = {
84✔
311
            dataKey: dimension.accessor,
312
            interval: dimension?.interval ?? (isBigDataSet ? 'preserveStart' : 0),
84!
313
            tickLine: index < 1,
314
            axisLine: index < 1,
315
            allowDuplicatedCategory: index === 0
316
          };
317

318
          if (layout === 'vertical') {
84!
319
            axisProps.type = 'category';
×
320
            axisProps.visible = false;
×
321
            axisProps.hide = !chartConfig.yAxisVisible;
×
322
            axisProps.tick = <YAxisTicks config={dimension} />;
×
323
            axisProps.yAxisId = index;
×
324
            axisProps.width = chartConfig.yAxisWidth ?? yAxisWidth;
×
325
            AxisComponent = YAxis;
×
326
            axisProps.orientation = isRTL ? 'right' : 'left';
×
327
          } else {
328
            axisProps.dataKey = dimension.accessor;
84✔
329
            axisProps.tick = <XAxisTicks config={dimension} />;
84✔
330
            axisProps.hide = !chartConfig.xAxisVisible;
84✔
331
            axisProps.xAxisId = index;
84✔
332
            axisProps.height = xAxisHeights[index];
84✔
333
            AxisComponent = XAxis;
84✔
334
            axisProps.reversed = isRTL;
84✔
335
          }
336

337
          return <AxisComponent key={dimension.reactKey} {...axisProps} />;
84✔
338
        })}
339
        {layout === 'horizontal' && (
200✔
340
          <YAxis
341
            {...measureAxisProps}
342
            yAxisId="primary"
343
            width={chartConfig.yAxisWidth ?? yAxisWidth}
170✔
344
            orientation={isRTL ? 'right' : 'left'}
100!
345
            tick={chartConfig.yAxisLabelsVisible ? <YAxisTicks config={primaryMeasure} /> : false}
100✔
346
            {...chartConfig.yAxisConfig}
347
          />
348
        )}
349
        {layout === 'vertical' && (
100!
350
          <XAxis
351
            {...measureAxisProps}
352
            reversed={isRTL}
353
            xAxisId="primary"
354
            type="number"
355
            tick={<XAxisTicks config={primaryMeasure} />}
356
            {...chartConfig.xAxisConfig}
357
          />
358
        )}
359

360
        {chartConfig.secondYAxis?.dataKey && layout === 'horizontal' && (
100!
361
          <YAxis
362
            dataKey={chartConfig.secondYAxis.dataKey}
363
            axisLine={{
364
              stroke: chartConfig.secondYAxis.color ?? `var(--sapChart_OrderedColor_${(colorSecondY % 12) + 1})`
×
365
            }}
366
            tick={
367
              <YAxisTicks
368
                config={secondaryMeasure}
369
                secondYAxisConfig={{
370
                  color: chartConfig.secondYAxis.color ?? `var(--sapChart_OrderedColor_${(colorSecondY % 12) + 1})`
×
371
                }}
372
              />
373
            }
374
            tickLine={{
375
              stroke: chartConfig.secondYAxis.color ?? `var(--sapChart_OrderedColor_${(colorSecondY % 12) + 1})`
×
376
            }}
377
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
378
            // @ts-ignore
379
            label={{
380
              value: chartConfig.secondYAxis.name,
381
              offset: 2,
382
              angle: +90,
383
              position: 'center'
384
            }}
385
            orientation={isRTL ? 'left' : 'right'}
×
386
            interval={0}
387
            yAxisId="secondary"
388
            {...chartConfig.secondYAxisConfig}
389
          />
390
        )}
391
        {chartConfig.secondYAxis?.dataKey && layout === 'vertical' && (
100!
392
          <XAxis
393
            dataKey={chartConfig.secondYAxis.dataKey}
394
            axisLine={{
395
              stroke: chartConfig.secondYAxis.color ?? `var(--sapChart_OrderedColor_${(colorSecondY % 12) + 1})`
×
396
            }}
397
            tick={
398
              <XAxisTicks
399
                config={secondaryMeasure}
400
                secondYAxisConfig={{
401
                  color: chartConfig.secondYAxis.color ?? `var(--sapChart_OrderedColor_${(colorSecondY % 12) + 1})`
×
402
                }}
403
              />
404
            }
405
            tickLine={{
406
              stroke: chartConfig.secondYAxis.color ?? `var(--sapChart_OrderedColor_${(colorSecondY % 12) + 1})`
×
407
            }}
408
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
409
            // @ts-ignore
410
            label={{ value: chartConfig.secondYAxis.name, offset: 2, angle: +90, position: 'center' }}
411
            orientation="top"
412
            interval={0}
413
            xAxisId="secondary"
414
            type="number"
415
            {...chartConfig.secondXAxisConfig}
416
          />
417
        )}
418
        {referenceLine && (
100!
419
          <ReferenceLine
420
            {...referenceLine}
421
            stroke={referenceLine?.color ?? referenceLine?.stroke}
×
422
            y={referenceLine?.value ? (layout === 'horizontal' ? referenceLine?.value : undefined) : referenceLine?.y}
×
423
            x={referenceLine?.value ? (layout === 'vertical' ? referenceLine?.value : undefined) : referenceLine?.x}
×
424
            yAxisId={(referenceLine?.yAxisId ?? layout === 'horizontal') ? 'primary' : undefined}
×
425
            xAxisId={(referenceLine?.xAxisId ?? layout === 'vertical') ? 'primary' : undefined}
×
426
            label={referenceLine?.label}
427
          />
428
        )}
429
        {/*ToDo: remove conditional rendering once `active` is working again (https://github.com/recharts/recharts/issues/2703)*/}
430
        {tooltipConfig?.active !== false && (
200✔
431
          <Tooltip
432
            cursor={tooltipFillOpacity}
433
            formatter={tooltipValueFormatter}
434
            contentStyle={tooltipContentStyle}
435
            labelFormatter={labelFormatter}
436
            {...tooltipConfig}
437
          />
438
        )}
439
        {!noLegend && (
170✔
440
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
441
          // @ts-ignore
442
          <Legend
443
            verticalAlign={chartConfig.legendPosition}
444
            align={chartConfig.legendHorizontalAlign}
445
            onClick={onItemLegendClick}
446
            wrapperStyle={legendPosition}
447
            {...chartConfig.legendConfig}
448
          />
449
        )}
450
        {measures?.map((element, index) => {
451
          const ChartElement = ChartTypes[element.type] as any as FC<any>;
196✔
452

453
          const chartElementProps: any = {
196✔
454
            isAnimationActive: !noAnimation
455
          };
456
          let labelPosition = 'top';
196✔
457

458
          switch (element.type) {
196✔
459
            case 'line':
460
              chartElementProps.activeDot = {
84✔
461
                onClick: onDataPointClickInternal
462
              };
463
              chartElementProps.strokeWidth = element.width;
84✔
464
              chartElementProps.strokeOpacity = element.opacity;
84✔
465
              chartElementProps.dot = element.showDot ?? !isBigDataSet;
84✔
466
              break;
84✔
467
            case 'bar':
468
              chartElementProps.hide = element.hide;
84✔
469
              chartElementProps.fillOpacity = element.opacity;
84✔
470
              chartElementProps.strokeOpacity = element.opacity;
84✔
471
              chartElementProps.barSize = element.width;
84✔
472
              chartElementProps.onClick = onDataPointClickInternal;
84✔
473
              chartElementProps.stackId = element.stackId ?? undefined;
84✔
474
              chartElementProps.labelPosition = element.stackId ? 'insideTop' : 'top';
84!
475
              if (layout === 'vertical') {
84!
476
                labelPosition = 'insideRight';
×
477
              } else {
478
                labelPosition = 'insideTop';
84✔
479
              }
480
              break;
84✔
481
            case 'area':
482
              chartElementProps.dot = !isBigDataSet;
28✔
483
              chartElementProps.fillOpacity = 0.3;
28✔
484
              chartElementProps.strokeOpacity = element.opacity;
28✔
485
              chartElementProps.strokeWidth = element.width;
28✔
486
              chartElementProps.activeDot = {
28✔
487
                onClick: onDataPointClickInternal
488
              };
489
              break;
28✔
490
          }
491

492
          if (layout === 'vertical') {
196!
493
            chartElementProps.xAxisId = chartConfig.secondYAxis?.dataKey === element.accessor ? 'secondary' : 'primary';
×
494
          } else {
495
            chartElementProps.yAxisId = chartConfig.secondYAxis?.dataKey === element.accessor ? 'secondary' : 'primary';
196!
496
          }
497
          return (
196✔
498
            <ChartElement
499
              key={element.reactKey}
500
              name={element.label ?? element.accessor}
196!
501
              label={
502
                element.type === 'bar' || isBigDataSet ? undefined : (
504✔
503
                  <ChartDataLabel config={element} chartType={element.type} position={labelPosition} />
504
                )
505
              }
506
              stroke={element.color ?? `var(--sapChart_OrderedColor_${(index % 12) + 1})`}
336✔
507
              fill={element.color ?? `var(--sapChart_OrderedColor_${(index % 12) + 1})`}
336✔
508
              type="monotone"
509
              dataKey={element.accessor}
510
              {...chartElementProps}
511
            >
512
              {element.type === 'bar' && (
280✔
513
                <>
514
                  <LabelList
515
                    data={dataset}
516
                    valueAccessor={valueAccessor(element.accessor)}
517
                    content={<ChartDataLabel config={element} chartType="column" position={'insideTop'} />}
518
                  />
519
                  {dataset.map((data, i) => {
520
                    return (
1,008✔
521
                      <Cell
522
                        key={i}
523
                        fill={getCellColors(element, data, index)}
524
                        stroke={getCellColors(element, data, index)}
525
                      />
526
                    );
527
                  })}
528
                </>
529
              )}
530
            </ChartElement>
531
          );
532
        })}
533
        {!!chartConfig.zoomingTool && (
116✔
534
          <Brush
535
            dataKey={primaryDimensionAccessor}
536
            tickFormatter={primaryDimension?.formatter}
537
            {...brushProps}
538
            {...(typeof chartConfig.zoomingTool === 'object' ? chartConfig.zoomingTool : {})}
16✔
539
          />
540
        )}
541
        {children}
542
      </ComposedChartLib>
543
    </ChartContainer>
544
  );
545
});
546

547
ComposedChart.displayName = 'ComposedChart';
32✔
548

549
export { ComposedChart };
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