baseChart.tsx 21 KB

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