chart.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import {browserHistory} from 'react-router';
  2. import {useTheme} from '@emotion/react';
  3. import type {LegendComponentOption, LineSeriesOption} from 'echarts';
  4. import ChartZoom from 'sentry/components/charts/chartZoom';
  5. import type {LineChartProps} from 'sentry/components/charts/lineChart';
  6. import {LineChart} from 'sentry/components/charts/lineChart';
  7. import TransitionChart from 'sentry/components/charts/transitionChart';
  8. import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
  9. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  10. import type {EventsStatsData, OrganizationSummary, Project} from 'sentry/types';
  11. import type {Series} from 'sentry/types/echarts';
  12. import {getUtcToLocalDateObject} from 'sentry/utils/dates';
  13. import {
  14. axisLabelFormatter,
  15. getDurationUnit,
  16. tooltipFormatter,
  17. } from 'sentry/utils/discover/charts';
  18. import {aggregateOutputType} from 'sentry/utils/discover/fields';
  19. import getDynamicText from 'sentry/utils/getDynamicText';
  20. import {decodeList} from 'sentry/utils/queryString';
  21. import {useLocation} from 'sentry/utils/useLocation';
  22. import useRouter from 'sentry/utils/useRouter';
  23. import {getIntervalLine} from 'sentry/views/performance/utils';
  24. import type {ViewProps} from '../types';
  25. import type {
  26. NormalizedTrendsTransaction,
  27. TrendChangeType,
  28. TrendFunctionField,
  29. TrendsStats,
  30. } from './types';
  31. import {
  32. generateTrendFunctionAsString,
  33. getCurrentTrendFunction,
  34. getCurrentTrendParameter,
  35. getUnselectedSeries,
  36. transformEventStatsSmoothed,
  37. trendToColor,
  38. } from './utils';
  39. type Props = ViewProps & {
  40. isLoading: boolean;
  41. organization: OrganizationSummary;
  42. projects: Project[];
  43. statsData: TrendsStats;
  44. trendChangeType: TrendChangeType;
  45. additionalSeries?: LineSeriesOption[];
  46. applyRegressionFormatToInterval?: boolean;
  47. disableLegend?: boolean;
  48. disableXAxis?: boolean;
  49. grid?: LineChartProps['grid'];
  50. height?: number;
  51. neutralColor?: boolean;
  52. transaction?: NormalizedTrendsTransaction;
  53. trendFunctionField?: TrendFunctionField;
  54. };
  55. export function transformEventStats(
  56. data: EventsStatsData,
  57. seriesName?: string
  58. ): Series[] {
  59. return [
  60. {
  61. seriesName: seriesName || 'Current',
  62. data: data.map(([timestamp, countsForTimestamp]) => ({
  63. name: timestamp * 1000,
  64. value: countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  65. })),
  66. },
  67. ];
  68. }
  69. function getLegend(trendFunction: string): LegendComponentOption {
  70. return {
  71. right: 10,
  72. top: 0,
  73. itemGap: 12,
  74. align: 'left',
  75. data: [
  76. {
  77. name: 'Baseline',
  78. icon: 'path://M180 1000 l0 -40 200 0 200 0 0 40 0 40 -200 0 -200 0 0 -40z, M810 1000 l0 -40 200 0 200 0 0 40 0 40 -200 0 -200 0 0 -40zm, M1440 1000 l0 -40 200 0 200 0 0 40 0 40 -200 0 -200 0 0 -40z',
  79. },
  80. {
  81. name: 'Releases',
  82. },
  83. {
  84. name: trendFunction,
  85. },
  86. ],
  87. };
  88. }
  89. export function Chart({
  90. trendChangeType,
  91. statsPeriod,
  92. transaction,
  93. statsData,
  94. isLoading,
  95. start: propsStart,
  96. end: propsEnd,
  97. trendFunctionField,
  98. disableXAxis,
  99. disableLegend,
  100. neutralColor,
  101. grid,
  102. height,
  103. projects,
  104. project,
  105. organization,
  106. additionalSeries,
  107. applyRegressionFormatToInterval = false,
  108. }: Props) {
  109. const location = useLocation();
  110. const router = useRouter();
  111. const theme = useTheme();
  112. const handleLegendSelectChanged = legendChange => {
  113. const {selected} = legendChange;
  114. const unselected = Object.keys(selected).filter(key => !selected[key]);
  115. const query = {
  116. ...location.query,
  117. };
  118. const queryKey = getUnselectedSeries(trendChangeType);
  119. query[queryKey] = unselected;
  120. const to = {
  121. ...location,
  122. query,
  123. };
  124. browserHistory.push(to);
  125. };
  126. const derivedTrendChangeType = organization.features.includes('performance-new-trends')
  127. ? transaction?.change
  128. : trendChangeType;
  129. const lineColor =
  130. trendToColor[neutralColor ? 'neutral' : derivedTrendChangeType || trendChangeType];
  131. const events =
  132. statsData && transaction?.project && transaction?.transaction
  133. ? statsData[[transaction.project, transaction.transaction].join(',')]
  134. : undefined;
  135. const data = events?.data ?? [];
  136. const trendFunction = getCurrentTrendFunction(location, trendFunctionField);
  137. const trendParameter = getCurrentTrendParameter(location, projects, project);
  138. const chartLabel = generateTrendFunctionAsString(
  139. trendFunction.field,
  140. trendParameter.column
  141. );
  142. const results = transformEventStats(data, chartLabel);
  143. const {smoothedResults, minValue, maxValue} = transformEventStatsSmoothed(
  144. results,
  145. chartLabel
  146. );
  147. const start = propsStart ? getUtcToLocalDateObject(propsStart) : null;
  148. const end = propsEnd ? getUtcToLocalDateObject(propsEnd) : null;
  149. const {utc} = normalizeDateTimeParams(location.query);
  150. const seriesSelection = decodeList(
  151. location.query[getUnselectedSeries(trendChangeType)]
  152. ).reduce((selection, metric) => {
  153. selection[metric] = false;
  154. return selection;
  155. }, {});
  156. const legend: LegendComponentOption = disableLegend
  157. ? {show: false}
  158. : {
  159. ...getLegend(chartLabel),
  160. selected: seriesSelection,
  161. };
  162. const loading = isLoading;
  163. const reloading = isLoading;
  164. const yMax = Math.max(
  165. maxValue,
  166. transaction?.aggregate_range_2 || 0,
  167. transaction?.aggregate_range_1 || 0
  168. );
  169. const yMin = Math.min(
  170. minValue,
  171. transaction?.aggregate_range_1 || Number.MAX_SAFE_INTEGER,
  172. transaction?.aggregate_range_2 || Number.MAX_SAFE_INTEGER
  173. );
  174. const smoothedSeries = smoothedResults
  175. ? smoothedResults.map(values => {
  176. return {
  177. ...values,
  178. color: lineColor.default,
  179. lineStyle: {
  180. opacity: 1,
  181. },
  182. };
  183. })
  184. : [];
  185. const needsLabel = true;
  186. const intervalSeries = getIntervalLine(
  187. theme,
  188. smoothedResults || [],
  189. 0.5,
  190. needsLabel,
  191. transaction,
  192. applyRegressionFormatToInterval
  193. );
  194. const yDiff = yMax - yMin;
  195. const yMargin = yDiff * 0.1;
  196. const series = [...smoothedSeries, ...intervalSeries];
  197. const durationUnit = getDurationUnit(series);
  198. const chartOptions: Omit<LineChartProps, 'series'> = {
  199. tooltip: {
  200. valueFormatter: (value, seriesName) => {
  201. return tooltipFormatter(value, aggregateOutputType(seriesName));
  202. },
  203. },
  204. yAxis: {
  205. min: Math.max(0, yMin - yMargin),
  206. max: yMax + yMargin,
  207. minInterval: durationUnit,
  208. axisLabel: {
  209. color: theme.chartLabel,
  210. formatter: (value: number) =>
  211. axisLabelFormatter(value, 'duration', undefined, durationUnit),
  212. },
  213. },
  214. };
  215. return (
  216. <ChartZoom
  217. router={router}
  218. period={statsPeriod}
  219. start={start}
  220. end={end}
  221. utc={utc === 'true'}
  222. >
  223. {zoomRenderProps => {
  224. return (
  225. <TransitionChart loading={loading} reloading={reloading}>
  226. <TransparentLoadingMask visible={reloading} />
  227. {getDynamicText({
  228. value: (
  229. <LineChart
  230. height={height}
  231. {...zoomRenderProps}
  232. {...chartOptions}
  233. additionalSeries={additionalSeries}
  234. onLegendSelectChanged={handleLegendSelectChanged}
  235. series={series}
  236. seriesOptions={{
  237. showSymbol: false,
  238. }}
  239. legend={legend}
  240. toolBox={{
  241. show: false,
  242. }}
  243. grid={
  244. grid ?? {
  245. left: '10px',
  246. right: '10px',
  247. top: '40px',
  248. bottom: '0px',
  249. }
  250. }
  251. xAxis={disableXAxis ? {show: false} : undefined}
  252. />
  253. ),
  254. fixed: 'Duration Chart',
  255. })}
  256. </TransitionChart>
  257. );
  258. }}
  259. </ChartZoom>
  260. );
  261. }
  262. export default Chart;