chart.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import {Component} from 'react';
  2. import {InjectedRouter} from 'react-router';
  3. import {withTheme} from '@emotion/react';
  4. import max from 'lodash/max';
  5. import min from 'lodash/min';
  6. import AreaChart from 'app/components/charts/areaChart';
  7. import ChartZoom from 'app/components/charts/chartZoom';
  8. import {DateString} from 'app/types';
  9. import {Series} from 'app/types/echarts';
  10. import {axisLabelFormatter, tooltipFormatter} from 'app/utils/discover/charts';
  11. import {aggregateOutputType} from 'app/utils/discover/fields';
  12. import {Theme} from 'app/utils/theme';
  13. type Props = {
  14. theme: Theme;
  15. data: Series[];
  16. previousData?: Series;
  17. router: InjectedRouter;
  18. statsPeriod: string | undefined;
  19. start: DateString;
  20. end: DateString;
  21. utc: boolean;
  22. height?: number;
  23. grid?: AreaChart['props']['grid'];
  24. disableMultiAxis?: boolean;
  25. disableXAxis?: boolean;
  26. chartColors?: string[];
  27. loading: boolean;
  28. };
  29. // adapted from https://stackoverflow.com/questions/11397239/rounding-up-for-a-graph-maximum
  30. function computeAxisMax(data) {
  31. // assumes min is 0
  32. const valuesDict = data.map(value => value.data.map(point => point.value));
  33. const maxValue = max(valuesDict.map(max)) as number;
  34. if (maxValue <= 1) {
  35. return 1;
  36. }
  37. const power = Math.log10(maxValue);
  38. const magnitude = min([max([10 ** (power - Math.floor(power)), 0]), 10]) as number;
  39. let scale: number;
  40. if (magnitude <= 2.5) {
  41. scale = 0.2;
  42. } else if (magnitude <= 5) {
  43. scale = 0.5;
  44. } else if (magnitude <= 7.5) {
  45. scale = 1.0;
  46. } else {
  47. scale = 2.0;
  48. }
  49. const step = 10 ** Math.floor(power) * scale;
  50. return Math.round(Math.ceil(maxValue / step) * step);
  51. }
  52. class Chart extends Component<Props> {
  53. render() {
  54. const {
  55. theme,
  56. data,
  57. previousData,
  58. router,
  59. statsPeriod,
  60. start,
  61. end,
  62. utc,
  63. loading,
  64. height,
  65. grid,
  66. disableMultiAxis,
  67. disableXAxis,
  68. chartColors,
  69. } = this.props;
  70. if (!data || data.length <= 0) {
  71. return null;
  72. }
  73. const colors = chartColors ?? theme.charts.getColorPalette(4);
  74. const durationOnly = data.every(
  75. value => aggregateOutputType(value.seriesName) === 'duration'
  76. );
  77. const dataMax = durationOnly ? computeAxisMax(data) : undefined;
  78. const xAxes = disableMultiAxis
  79. ? undefined
  80. : [
  81. {
  82. gridIndex: 0,
  83. type: 'time' as const,
  84. },
  85. {
  86. gridIndex: 1,
  87. type: 'time' as const,
  88. },
  89. ];
  90. const yAxes = disableMultiAxis
  91. ? [
  92. {
  93. axisLabel: {
  94. color: theme.chartLabel,
  95. formatter(value: number) {
  96. return axisLabelFormatter(value, data[0].seriesName);
  97. },
  98. },
  99. },
  100. ]
  101. : [
  102. {
  103. gridIndex: 0,
  104. scale: true,
  105. max: dataMax,
  106. axisLabel: {
  107. color: theme.chartLabel,
  108. formatter(value: number) {
  109. return axisLabelFormatter(value, data[0].seriesName);
  110. },
  111. },
  112. },
  113. {
  114. gridIndex: 1,
  115. scale: true,
  116. max: dataMax,
  117. axisLabel: {
  118. color: theme.chartLabel,
  119. formatter(value: number) {
  120. return axisLabelFormatter(value, data[1].seriesName);
  121. },
  122. },
  123. },
  124. ];
  125. const axisPointer = disableMultiAxis
  126. ? undefined
  127. : {
  128. // Link the two series x-axis together.
  129. link: [{xAxisIndex: [0, 1]}],
  130. };
  131. const areaChartProps = {
  132. seriesOptions: {
  133. showSymbol: false,
  134. },
  135. grid: disableMultiAxis
  136. ? grid
  137. : [
  138. {
  139. top: '8px',
  140. left: '24px',
  141. right: '52%',
  142. bottom: '16px',
  143. },
  144. {
  145. top: '8px',
  146. left: '52%',
  147. right: '24px',
  148. bottom: '16px',
  149. },
  150. ],
  151. axisPointer,
  152. xAxes,
  153. yAxes,
  154. utc,
  155. isGroupedByDate: true,
  156. showTimeInTooltip: true,
  157. colors: [colors[0], colors[1]] as string[],
  158. tooltip: {
  159. valueFormatter: (value, seriesName) => {
  160. return tooltipFormatter(value, seriesName);
  161. },
  162. nameFormatter(value: string) {
  163. return value === 'epm()' ? 'tpm()' : value;
  164. },
  165. },
  166. };
  167. if (loading) {
  168. return <AreaChart height={height} series={[]} {...areaChartProps} />;
  169. }
  170. const series = data.map((values, i: number) => ({
  171. ...values,
  172. yAxisIndex: i,
  173. xAxisIndex: i,
  174. }));
  175. return (
  176. <ChartZoom
  177. router={router}
  178. period={statsPeriod}
  179. start={start}
  180. end={end}
  181. utc={utc}
  182. xAxisIndex={disableMultiAxis ? undefined : [0, 1]}
  183. >
  184. {zoomRenderProps => (
  185. <AreaChart
  186. height={height}
  187. {...zoomRenderProps}
  188. series={series}
  189. previousPeriod={previousData ? [previousData] : undefined}
  190. xAxis={disableXAxis ? {show: false} : undefined}
  191. {...areaChartProps}
  192. />
  193. )}
  194. </ChartZoom>
  195. );
  196. }
  197. }
  198. export default withTheme(Chart);