baseChart.tsx 22 KB

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