pieChart.tsx 5.2 KB

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