chart.tsx 5.8 KB

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