baseChart.tsx 19 KB

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