baseChart.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. import 'echarts/lib/component/grid';
  2. import 'echarts/lib/component/graphic';
  3. import 'echarts/lib/component/toolbox';
  4. import 'zrender/lib/svg/svg';
  5. import {forwardRef, useMemo} from 'react';
  6. import {css, Global, useTheme} from '@emotion/react';
  7. import styled from '@emotion/styled';
  8. import type {
  9. AxisPointerComponentOption,
  10. ECharts,
  11. EChartsOption,
  12. GridComponentOption,
  13. LegendComponentOption,
  14. LineSeriesOption,
  15. SeriesOption,
  16. TooltipComponentFormatterCallback,
  17. TooltipComponentFormatterCallbackParams,
  18. TooltipComponentOption,
  19. VisualMapComponentOption,
  20. XAXisComponentOption,
  21. YAXisComponentOption,
  22. } from 'echarts';
  23. import {AriaComponent} from 'echarts/components';
  24. import * as echarts from 'echarts/core';
  25. import ReactEchartsCore from 'echarts-for-react/lib/core';
  26. import MarkLine from 'sentry/components/charts/components/markLine';
  27. import {IS_ACCEPTANCE_TEST} from 'sentry/constants';
  28. import space from 'sentry/styles/space';
  29. import {
  30. EChartChartReadyHandler,
  31. EChartClickHandler,
  32. EChartDataZoomHandler,
  33. EChartEventHandler,
  34. EChartFinishedHandler,
  35. EChartHighlightHandler,
  36. EChartMouseOutHandler,
  37. EChartMouseOverHandler,
  38. EChartRenderedHandler,
  39. EChartRestoreHandler,
  40. ReactEchartsRef,
  41. Series,
  42. } from 'sentry/types/echarts';
  43. import {defined} from 'sentry/utils';
  44. import type {Theme} from 'sentry/utils/theme';
  45. import Grid from './components/grid';
  46. import Legend from './components/legend';
  47. import Tooltip, {TooltipSubLabel} from './components/tooltip';
  48. import XAxis from './components/xAxis';
  49. import YAxis from './components/yAxis';
  50. import LineSeries from './series/lineSeries';
  51. import {
  52. getDiffInMinutes,
  53. getDimensionValue,
  54. lightenHexToRgb,
  55. useEchartsAriaLabels,
  56. } from './utils';
  57. // TODO(ts): What is the series type? EChartOption.Series's data cannot have
  58. // `onClick` since it's typically an array.
  59. //
  60. // Handle series item clicks (e.g. Releases mark line or a single series
  61. // item) This is different than when you hover over an "axis" line on a chart
  62. // (e.g. if there are 2 series for an axis and you're not directly hovered
  63. // over an item)
  64. //
  65. // Calls "onClick" inside of series data
  66. const handleClick = (clickSeries: any, instance: ECharts) => {
  67. if (clickSeries.data) {
  68. clickSeries.data.onClick?.(clickSeries, instance);
  69. }
  70. };
  71. echarts.use(AriaComponent);
  72. type ReactEchartProps = React.ComponentProps<typeof ReactEchartsCore>;
  73. type ReactEChartOpts = NonNullable<ReactEchartProps['opts']>;
  74. /**
  75. * Used for some properties that can be truncated
  76. */
  77. type Truncateable = {
  78. /**
  79. * Truncate the label / value some number of characters.
  80. * If true is passed, it will use truncate based on a default length.
  81. */
  82. truncate?: number | boolean;
  83. };
  84. interface TooltipOption
  85. extends Omit<TooltipComponentOption, 'valueFormatter'>,
  86. Truncateable {
  87. filter?: (value: number, seriesParam: TooltipComponentOption['formatter']) => boolean;
  88. formatAxisLabel?: (
  89. value: number,
  90. isTimestamp: boolean,
  91. utc: boolean,
  92. showTimeInTooltip: boolean,
  93. addSecondsToTimeFormat: boolean,
  94. bucketSize: number | undefined,
  95. seriesParamsOrParam: TooltipComponentFormatterCallbackParams
  96. ) => string;
  97. markerFormatter?: (marker: string, label?: string) => string;
  98. nameFormatter?: (name: string) => string;
  99. /**
  100. * Array containing data that is used to display indented sublabels.
  101. */
  102. subLabels?: TooltipSubLabel[];
  103. valueFormatter?: (
  104. value: number,
  105. label?: string,
  106. seriesParams?: TooltipComponentFormatterCallback<any>
  107. ) => string;
  108. }
  109. type Props = {
  110. /**
  111. * Additional Chart Series
  112. * This is to pass series to BaseChart bypassing the wrappers like LineChart, AreaChart etc.
  113. */
  114. additionalSeries?: LineSeriesOption[];
  115. /**
  116. * If true, ignores height value and auto-scales chart to fit container height.
  117. */
  118. autoHeightResize?: boolean;
  119. /**
  120. * Axis pointer options
  121. */
  122. axisPointer?: AxisPointerComponentOption;
  123. /**
  124. * Bucket size to display time range in chart tooltip
  125. */
  126. bucketSize?: number;
  127. /**
  128. * Array of color codes to use in charts. May also take a function which is
  129. * provided with the current theme
  130. */
  131. colors?: string[] | ((theme: Theme) => string[]);
  132. 'data-test-id'?: string;
  133. /**
  134. * DataZoom (allows for zooming of chart)
  135. */
  136. dataZoom?: EChartsOption['dataZoom'];
  137. devicePixelRatio?: ReactEChartOpts['devicePixelRatio'];
  138. /**
  139. * theme name
  140. * example theme: https://github.com/apache/incubator-echarts/blob/master/theme/dark.js
  141. */
  142. echartsTheme?: ReactEchartProps['theme'];
  143. /**
  144. * optional, used to determine how xAxis is formatted if `isGroupedByDate == true`
  145. */
  146. end?: Date;
  147. /**
  148. * Forwarded Ref
  149. */
  150. forwardedRef?: React.Ref<ReactEchartsCore>;
  151. /**
  152. * Graphic options
  153. */
  154. graphic?: EChartsOption['graphic'];
  155. /**
  156. * ECharts Grid options. multiple grids allow multiple sub-graphs.
  157. */
  158. grid?: GridComponentOption | GridComponentOption[];
  159. /**
  160. * Chart height
  161. */
  162. height?: ReactEChartOpts['height'];
  163. /**
  164. * If data is grouped by date; then apply default date formatting to x-axis
  165. * and tooltips.
  166. */
  167. isGroupedByDate?: boolean;
  168. /**
  169. * states whether not to update chart immediately
  170. */
  171. lazyUpdate?: boolean;
  172. /**
  173. * Chart legend
  174. */
  175. legend?: LegendComponentOption & Truncateable;
  176. /**
  177. * optional, threshold in minutes used to add seconds to the xAxis datetime format if `isGroupedByDate == true`
  178. */
  179. minutesThresholdToDisplaySeconds?: number;
  180. /**
  181. * states whether or not to merge with previous `option`
  182. */
  183. notMerge?: boolean;
  184. onChartReady?: EChartChartReadyHandler;
  185. onClick?: EChartClickHandler;
  186. onDataZoom?: EChartDataZoomHandler;
  187. onFinished?: EChartFinishedHandler;
  188. onHighlight?: EChartHighlightHandler;
  189. onLegendSelectChanged?: EChartEventHandler<{
  190. name: string;
  191. selected: Record<string, boolean>;
  192. type: 'legendselectchanged';
  193. }>;
  194. onMouseOut?: EChartMouseOutHandler;
  195. onMouseOver?: EChartMouseOverHandler;
  196. onRendered?: EChartRenderedHandler;
  197. /**
  198. * One example of when this is called is restoring chart from zoom levels
  199. */
  200. onRestore?: EChartRestoreHandler;
  201. options?: EChartsOption;
  202. /**
  203. * optional, used to determine how xAxis is formatted if `isGroupedByDate == true`
  204. */
  205. period?: string | null;
  206. /**
  207. * Custom chart props that are implemented by us (and not a feature of eCharts)
  208. *
  209. * Display previous period as a LineSeries
  210. */
  211. previousPeriod?: Series[];
  212. /**
  213. * Use `canvas` when dealing with large datasets
  214. * See: https://ecomfe.github.io/echarts-doc/public/en/tutorial.html#Render%20by%20Canvas%20or%20SVG
  215. */
  216. renderer?: ReactEChartOpts['renderer'];
  217. /**
  218. * Chart Series
  219. * This is different than the interface to higher level charts, these need to
  220. * be an array of ECharts "Series" components.
  221. */
  222. series?: SeriesOption[];
  223. /**
  224. * Format timestamp with date AND time
  225. */
  226. showTimeInTooltip?: boolean;
  227. /**
  228. * optional, used to determine how xAxis is formatted if `isGroupedByDate == true`
  229. */
  230. start?: Date;
  231. /**
  232. * Inline styles
  233. */
  234. style?: React.CSSProperties;
  235. /**
  236. * Toolbox options
  237. */
  238. toolBox?: EChartsOption['toolbox'];
  239. /**
  240. * Tooltip options
  241. */
  242. tooltip?: TooltipOption;
  243. /**
  244. * If true and there's only one datapoint in series.data, we show a bar chart to increase the visibility.
  245. * Especially useful with line / area charts, because you can't draw line with single data point and one alone point is hard to spot.
  246. */
  247. transformSinglePointToBar?: boolean;
  248. /**
  249. * If true and there's only one datapoint in series.data, we show a horizontal line to increase the visibility
  250. * Similarly to single point bar in area charts a flat line for line charts makes it easy to spot the single data point.
  251. */
  252. transformSinglePointToLine?: boolean;
  253. /**
  254. * Use short date formatting for xAxis
  255. */
  256. useShortDate?: boolean;
  257. /**
  258. * Formats dates as UTC?
  259. */
  260. utc?: boolean;
  261. /**
  262. * ECharts Visual Map Options.
  263. */
  264. visualMap?: VisualMapComponentOption | VisualMapComponentOption[];
  265. /**
  266. * Chart width
  267. */
  268. width?: ReactEChartOpts['width'];
  269. /**
  270. * Pass `true` to have 2 x-axes with default properties. Can pass an array
  271. * of multiple objects to customize xAxis properties
  272. */
  273. xAxes?: true | Props['xAxis'][];
  274. /**
  275. * Must be explicitly `null` to disable xAxis
  276. *
  277. * Additionally a `truncate` option
  278. */
  279. xAxis?: (XAXisComponentOption & Truncateable) | null;
  280. /**
  281. * Pass `true` to have 2 y-axes with default properties. Can pass an array of
  282. * objects to customize yAxis properties
  283. */
  284. yAxes?: true | Props['yAxis'][];
  285. /**
  286. * Must be explicitly `null` to disable yAxis
  287. */
  288. yAxis?: YAXisComponentOption | null;
  289. };
  290. function BaseChartUnwrapped({
  291. colors,
  292. grid,
  293. tooltip,
  294. legend,
  295. dataZoom,
  296. toolBox,
  297. graphic,
  298. axisPointer,
  299. previousPeriod,
  300. echartsTheme,
  301. devicePixelRatio,
  302. minutesThresholdToDisplaySeconds,
  303. showTimeInTooltip,
  304. useShortDate,
  305. start,
  306. end,
  307. period,
  308. utc,
  309. yAxes,
  310. xAxes,
  311. style,
  312. forwardedRef,
  313. onClick,
  314. onLegendSelectChanged,
  315. onHighlight,
  316. onMouseOut,
  317. onMouseOver,
  318. onDataZoom,
  319. onRestore,
  320. onFinished,
  321. onRendered,
  322. options = {},
  323. series = [],
  324. additionalSeries = [],
  325. yAxis = {},
  326. xAxis = {},
  327. autoHeightResize = false,
  328. height = 200,
  329. width,
  330. renderer = 'svg',
  331. notMerge = true,
  332. lazyUpdate = false,
  333. isGroupedByDate = false,
  334. transformSinglePointToBar = false,
  335. transformSinglePointToLine = false,
  336. onChartReady = () => {},
  337. 'data-test-id': dataTestId,
  338. }: Props) {
  339. const theme = useTheme();
  340. const hasSinglePoints = (series as LineSeriesOption[] | undefined)?.every(
  341. s => Array.isArray(s.data) && s.data.length <= 1
  342. );
  343. const resolveColors =
  344. colors !== undefined ? (Array.isArray(colors) ? colors : colors(theme)) : null;
  345. const color =
  346. resolveColors ||
  347. (series.length ? theme.charts.getColorPalette(series.length) : theme.charts.colors);
  348. const previousPeriodColors =
  349. previousPeriod && previousPeriod.length > 1 ? lightenHexToRgb(color) : undefined;
  350. const transformedSeries =
  351. (hasSinglePoints && transformSinglePointToBar
  352. ? (series as LineSeriesOption[] | undefined)?.map(s => ({
  353. ...s,
  354. type: 'bar',
  355. barWidth: 40,
  356. barGap: 0,
  357. itemStyle: {...(s.areaStyle ?? {})},
  358. }))
  359. : hasSinglePoints && transformSinglePointToLine
  360. ? (series as LineSeriesOption[] | undefined)?.map(s => ({
  361. ...s,
  362. type: 'line',
  363. itemStyle: {...(s.lineStyle ?? {})},
  364. markLine:
  365. s?.data?.[0]?.[1] !== undefined
  366. ? MarkLine({
  367. silent: true,
  368. lineStyle: {
  369. type: 'solid',
  370. width: 1.5,
  371. },
  372. data: [{yAxis: s?.data?.[0]?.[1]}],
  373. label: {
  374. show: false,
  375. },
  376. })
  377. : undefined,
  378. }))
  379. : series) ?? [];
  380. const transformedPreviousPeriod =
  381. previousPeriod?.map((previous, seriesIndex) =>
  382. LineSeries({
  383. name: previous.seriesName,
  384. data: previous.data.map(({name, value}) => [name, value]),
  385. lineStyle: {
  386. color: previousPeriodColors ? previousPeriodColors[seriesIndex] : theme.gray200,
  387. type: 'dotted',
  388. },
  389. itemStyle: {
  390. color: previousPeriodColors ? previousPeriodColors[seriesIndex] : theme.gray200,
  391. },
  392. stack: 'previous',
  393. animation: false,
  394. })
  395. ) ?? [];
  396. const resolvedSeries = !previousPeriod
  397. ? [...transformedSeries, ...additionalSeries]
  398. : [...transformedSeries, ...transformedPreviousPeriod, ...additionalSeries];
  399. const defaultAxesProps = {theme};
  400. const yAxisOrCustom = !yAxes
  401. ? yAxis !== null
  402. ? YAxis({theme, ...yAxis})
  403. : undefined
  404. : Array.isArray(yAxes)
  405. ? yAxes.map(axis => YAxis({...axis, theme}))
  406. : [YAxis(defaultAxesProps), YAxis(defaultAxesProps)];
  407. /**
  408. * If true seconds will be added to the time format in the tooltips and chart xAxis
  409. */
  410. const addSecondsToTimeFormat =
  411. isGroupedByDate && defined(minutesThresholdToDisplaySeconds)
  412. ? getDiffInMinutes({start, end, period}) <= minutesThresholdToDisplaySeconds
  413. : false;
  414. const xAxisOrCustom = !xAxes
  415. ? xAxis !== null
  416. ? XAxis({
  417. ...xAxis,
  418. theme,
  419. useShortDate,
  420. start,
  421. end,
  422. period,
  423. isGroupedByDate,
  424. addSecondsToTimeFormat,
  425. utc,
  426. })
  427. : undefined
  428. : Array.isArray(xAxes)
  429. ? xAxes.map(axis =>
  430. XAxis({
  431. ...axis,
  432. theme,
  433. useShortDate,
  434. start,
  435. end,
  436. period,
  437. isGroupedByDate,
  438. addSecondsToTimeFormat,
  439. utc,
  440. })
  441. )
  442. : [XAxis(defaultAxesProps), XAxis(defaultAxesProps)];
  443. const seriesData =
  444. Array.isArray(series?.[0]?.data) && series[0].data.length > 1
  445. ? series[0].data
  446. : undefined;
  447. const bucketSize = seriesData ? seriesData[1][0] - seriesData[0][0] : undefined;
  448. const isTooltipPortalled = tooltip?.appendToBody;
  449. const tooltipOrNone =
  450. tooltip !== null
  451. ? Tooltip({
  452. showTimeInTooltip,
  453. isGroupedByDate,
  454. addSecondsToTimeFormat,
  455. utc,
  456. bucketSize,
  457. ...tooltip,
  458. className: isTooltipPortalled
  459. ? `${tooltip?.className ?? ''} chart-tooltip-portal`
  460. : tooltip?.className,
  461. })
  462. : undefined;
  463. const aria = useEchartsAriaLabels(
  464. {
  465. ...options,
  466. series: resolvedSeries,
  467. useUTC: utc,
  468. },
  469. isGroupedByDate
  470. );
  471. const chartOption = {
  472. ...options,
  473. animation: IS_ACCEPTANCE_TEST ? false : options.animation ?? true,
  474. useUTC: utc,
  475. color,
  476. grid: Array.isArray(grid) ? grid.map(Grid) : Grid(grid),
  477. tooltip: tooltipOrNone,
  478. legend: legend ? Legend({theme, ...legend}) : undefined,
  479. yAxis: yAxisOrCustom,
  480. xAxis: xAxisOrCustom,
  481. series: resolvedSeries,
  482. toolbox: toolBox,
  483. axisPointer,
  484. dataZoom,
  485. graphic,
  486. aria,
  487. };
  488. const chartStyles = {
  489. height: autoHeightResize ? '100%' : getDimensionValue(height),
  490. width: getDimensionValue(width),
  491. ...style,
  492. };
  493. // XXX(epurkhiser): Echarts can become unhappy if one of these event handlers
  494. // causes the chart to re-render and be passed a whole different instance of
  495. // event handlers.
  496. //
  497. // We use React.useMemo to keep the value across renders
  498. //
  499. const eventsMap = useMemo(
  500. () =>
  501. ({
  502. click: (props, instance) => {
  503. handleClick(props, instance);
  504. onClick?.(props, instance);
  505. },
  506. highlight: (props, instance) => onHighlight?.(props, instance),
  507. mouseout: (props, instance) => onMouseOut?.(props, instance),
  508. mouseover: (props, instance) => onMouseOver?.(props, instance),
  509. datazoom: (props, instance) => onDataZoom?.(props, instance),
  510. restore: (props, instance) => onRestore?.(props, instance),
  511. finished: (props, instance) => onFinished?.(props, instance),
  512. rendered: (props, instance) => onRendered?.(props, instance),
  513. legendselectchanged: (props, instance) =>
  514. onLegendSelectChanged?.(props, instance),
  515. } as ReactEchartProps['onEvents']),
  516. [
  517. onClick,
  518. onHighlight,
  519. onLegendSelectChanged,
  520. onMouseOut,
  521. onMouseOver,
  522. onDataZoom,
  523. onRestore,
  524. onFinished,
  525. onRendered,
  526. ]
  527. );
  528. return (
  529. <ChartContainer autoHeightResize={autoHeightResize} data-test-id={dataTestId}>
  530. {isTooltipPortalled && <Global styles={getPortalledTooltipStyles({theme})} />}
  531. <ReactEchartsCore
  532. ref={forwardedRef}
  533. echarts={echarts}
  534. notMerge={notMerge}
  535. lazyUpdate={lazyUpdate}
  536. theme={echartsTheme}
  537. onChartReady={onChartReady}
  538. onEvents={eventsMap}
  539. style={chartStyles}
  540. opts={{
  541. height: autoHeightResize ? undefined : height,
  542. width,
  543. renderer,
  544. devicePixelRatio,
  545. }}
  546. option={chartOption}
  547. />
  548. </ChartContainer>
  549. );
  550. }
  551. // Tooltip styles shared for regular and portalled tooltips
  552. const getTooltipStyles = (p: {theme: Theme}) => css`
  553. /* Tooltip styling */
  554. .tooltip-series,
  555. .tooltip-date {
  556. color: ${p.theme.subText};
  557. font-family: ${p.theme.text.family};
  558. font-variant-numeric: tabular-nums;
  559. padding: ${space(1)} ${space(2)};
  560. border-radius: ${p.theme.borderRadius} ${p.theme.borderRadius} 0 0;
  561. }
  562. .tooltip-series {
  563. border-bottom: none;
  564. }
  565. .tooltip-series-solo {
  566. border-radius: ${p.theme.borderRadius};
  567. }
  568. .tooltip-label {
  569. margin-right: ${space(1)};
  570. }
  571. .tooltip-label strong {
  572. font-weight: normal;
  573. color: ${p.theme.textColor};
  574. }
  575. .tooltip-label-indent {
  576. margin-left: 18px;
  577. }
  578. .tooltip-series > div {
  579. display: flex;
  580. justify-content: space-between;
  581. align-items: baseline;
  582. }
  583. .tooltip-date {
  584. border-top: solid 1px ${p.theme.innerBorder};
  585. text-align: center;
  586. position: relative;
  587. width: auto;
  588. border-radius: ${p.theme.borderRadiusBottom};
  589. }
  590. .tooltip-arrow {
  591. top: 100%;
  592. left: 50%;
  593. position: absolute;
  594. pointer-events: none;
  595. border-left: 8px solid transparent;
  596. border-right: 8px solid transparent;
  597. border-top: 8px solid ${p.theme.backgroundElevated};
  598. margin-left: -8px;
  599. &:before {
  600. border-left: 8px solid transparent;
  601. border-right: 8px solid transparent;
  602. border-top: 8px solid ${p.theme.translucentBorder};
  603. content: '';
  604. display: block;
  605. position: absolute;
  606. top: -7px;
  607. left: -8px;
  608. z-index: -1;
  609. }
  610. }
  611. /* Tooltip description styling */
  612. .tooltip-description {
  613. color: ${p.theme.white};
  614. border-radius: ${p.theme.borderRadius};
  615. background: #000;
  616. opacity: 0.9;
  617. padding: 5px 10px;
  618. position: relative;
  619. font-weight: bold;
  620. font-size: ${p.theme.fontSizeSmall};
  621. line-height: 1.4;
  622. font-family: ${p.theme.text.family};
  623. max-width: 230px;
  624. min-width: 230px;
  625. white-space: normal;
  626. text-align: center;
  627. :after {
  628. content: '';
  629. position: absolute;
  630. top: 100%;
  631. left: 50%;
  632. width: 0;
  633. height: 0;
  634. border-left: 5px solid transparent;
  635. border-right: 5px solid transparent;
  636. border-top: 5px solid #000;
  637. transform: translateX(-50%);
  638. }
  639. }
  640. `;
  641. // Contains styling for chart elements as we can't easily style those
  642. // elements directly
  643. const ChartContainer = styled('div')<{autoHeightResize: boolean}>`
  644. ${p => p.autoHeightResize && 'height: 100%;'}
  645. .echarts-for-react div:first-of-type {
  646. width: 100% !important;
  647. }
  648. .echarts-for-react text {
  649. font-variant-numeric: tabular-nums !important;
  650. }
  651. ${p => getTooltipStyles(p)}
  652. `;
  653. const getPortalledTooltipStyles = (p: {theme: Theme}) => css`
  654. .chart-tooltip-portal {
  655. ${getTooltipStyles(p)};
  656. }
  657. `;
  658. const BaseChart = forwardRef<ReactEchartsRef, Props>((props, ref) => (
  659. <BaseChartUnwrapped forwardedRef={ref} {...props} />
  660. ));
  661. BaseChart.displayName = 'forwardRef(BaseChart)';
  662. export default BaseChart;