baseChart.tsx 19 KB

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