chart.tsx 4.7 KB

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