pieChart.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import {Component, createRef} from 'react';
  2. import {withTheme} from '@emotion/react';
  3. import type {PieSeriesOption} from 'echarts';
  4. import {ReactEchartsRef, Series} from 'sentry/types/echarts';
  5. import type {Theme} from 'sentry/utils/theme';
  6. import Legend from './components/legend';
  7. import PieSeries from './series/pieSeries';
  8. import BaseChart from './baseChart';
  9. type ChartProps = Omit<React.ComponentProps<typeof BaseChart>, 'css'>;
  10. export type PieChartSeries = Series & Omit<PieSeriesOption, 'data' | 'name'>;
  11. type Props = Omit<ChartProps, 'series'> & {
  12. series: PieChartSeries[];
  13. theme: Theme;
  14. selectOnRender?: boolean;
  15. };
  16. class PieChart extends Component<Props> {
  17. componentDidMount() {
  18. const {selectOnRender} = this.props;
  19. if (!selectOnRender) {
  20. return;
  21. }
  22. // Timeout is because we need to wait for rendering animation to complete
  23. // And I haven't found a callback for this
  24. this.highlightTimeout = window.setTimeout(() => this.highlight(0), 1000);
  25. }
  26. componentWillUnmount() {
  27. window.clearTimeout(this.highlightTimeout);
  28. }
  29. highlightTimeout: number | undefined = undefined;
  30. isInitialSelected = true;
  31. selected = 0;
  32. chart = createRef<ReactEchartsRef>();
  33. // Select a series to highlight (e.g. shows details of series)
  34. // This is the same event as when you hover over a series in the chart
  35. highlight = dataIndex => {
  36. if (!this.chart.current) {
  37. return;
  38. }
  39. this.chart.current.getEchartsInstance().dispatchAction({
  40. type: 'highlight',
  41. seriesIndex: 0,
  42. dataIndex,
  43. });
  44. };
  45. // Opposite of `highlight`
  46. downplay = dataIndex => {
  47. if (!this.chart.current) {
  48. return;
  49. }
  50. this.chart.current.getEchartsInstance().dispatchAction({
  51. type: 'downplay',
  52. seriesIndex: 0,
  53. dataIndex,
  54. });
  55. };
  56. // echarts Legend does not have access to percentages (but tooltip does :/)
  57. getSeriesPercentages = (series: PieChartSeries) => {
  58. const total = series.data.reduce((acc, {value}) => acc + value, 0);
  59. return series.data
  60. .map(({name, value}) => [name, Math.round((value / total) * 10000) / 100])
  61. .reduce(
  62. (acc, [name, value]) => ({
  63. ...acc,
  64. [name]: value,
  65. }),
  66. {}
  67. );
  68. };
  69. render() {
  70. const {series, theme, ...props} = this.props;
  71. if (!series || !series.length) {
  72. return null;
  73. }
  74. if (series.length > 1) {
  75. // eslint-disable-next-line no-console
  76. console.warn('PieChart only uses the first series!');
  77. }
  78. // Note, we only take the first series unit!
  79. const [firstSeries] = series;
  80. const seriesPercentages = this.getSeriesPercentages(firstSeries);
  81. return (
  82. <BaseChart
  83. ref={this.chart}
  84. colors={
  85. firstSeries &&
  86. firstSeries.data && [...theme.charts.getColorPalette(firstSeries.data.length)]
  87. }
  88. // when legend highlights it does NOT pass dataIndex :(
  89. onHighlight={({name}) => {
  90. if (
  91. !this.isInitialSelected ||
  92. !name ||
  93. firstSeries.data[this.selected].name === name
  94. ) {
  95. return;
  96. }
  97. // Unhighlight if not initial "highlight" event and
  98. // if name exists (i.e. not dispatched from cDM) and
  99. // highlighted series name is different than the initially selected series name
  100. this.downplay(this.selected);
  101. this.isInitialSelected = false;
  102. }}
  103. onMouseOver={({dataIndex}) => {
  104. if (!this.isInitialSelected) {
  105. return;
  106. }
  107. if (dataIndex === this.selected) {
  108. return;
  109. }
  110. this.downplay(this.selected);
  111. this.isInitialSelected = false;
  112. }}
  113. {...props}
  114. legend={Legend({
  115. theme,
  116. orient: 'vertical',
  117. align: 'left',
  118. show: true,
  119. left: 10,
  120. top: 10,
  121. bottom: 10,
  122. formatter: name =>
  123. `${name} ${
  124. typeof seriesPercentages[name] !== 'undefined'
  125. ? `(${seriesPercentages[name]}%)`
  126. : ''
  127. }`,
  128. })}
  129. tooltip={{
  130. formatter: data => {
  131. return [
  132. '<div class="tooltip-series">',
  133. `<div><span class="tooltip-label">${data.marker}<strong>${data.name}</strong></span> ${data.percent}%</div>`,
  134. '</div>',
  135. `<div class="tooltip-date">${data.value}</div>`,
  136. '</div>',
  137. '<div class="tooltip-arrow"></div>',
  138. ].join('');
  139. },
  140. }}
  141. series={[
  142. PieSeries({
  143. name: firstSeries.seriesName,
  144. data: firstSeries.data,
  145. avoidLabelOverlap: false,
  146. label: {
  147. formatter: ({name, percent}) => `${name}\n${percent}%`,
  148. show: false,
  149. position: 'center',
  150. fontSize: '18',
  151. },
  152. emphasis: {
  153. label: {
  154. show: true,
  155. },
  156. },
  157. labelLine: {
  158. show: false,
  159. },
  160. }),
  161. ]}
  162. xAxis={null}
  163. yAxis={null}
  164. />
  165. );
  166. }
  167. }
  168. export default withTheme(PieChart);