chart.tsx 6.8 KB

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