chart.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import {browserHistory, withRouter, WithRouterProps} from 'react-router';
  2. import {useTheme} from '@emotion/react';
  3. import type {LegendComponentOption} from 'echarts';
  4. import ChartZoom from 'sentry/components/charts/chartZoom';
  5. import {
  6. LineChart,
  7. LineChartProps,
  8. LineChartSeries,
  9. } from 'sentry/components/charts/lineChart';
  10. import TransitionChart from 'sentry/components/charts/transitionChart';
  11. import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
  12. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  13. import {t} from 'sentry/locale';
  14. import {EventsStatsData, OrganizationSummary, Project} from 'sentry/types';
  15. import {Series} from 'sentry/types/echarts';
  16. import {getUtcToLocalDateObject} from 'sentry/utils/dates';
  17. import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
  18. import getDynamicText from 'sentry/utils/getDynamicText';
  19. import {decodeList} from 'sentry/utils/queryString';
  20. import {Theme} from 'sentry/utils/theme';
  21. import {ViewProps} from '../types';
  22. import {
  23. NormalizedTrendsTransaction,
  24. TrendChangeType,
  25. TrendFunctionField,
  26. TrendsStats,
  27. } from './types';
  28. import {
  29. generateTrendFunctionAsString,
  30. getCurrentTrendFunction,
  31. getCurrentTrendParameter,
  32. getUnselectedSeries,
  33. transformEventStatsSmoothed,
  34. trendToColor,
  35. } from './utils';
  36. type Props = WithRouterProps &
  37. ViewProps & {
  38. isLoading: boolean;
  39. location: Location;
  40. organization: OrganizationSummary;
  41. projects: Project[];
  42. statsData: TrendsStats;
  43. trendChangeType: TrendChangeType;
  44. disableLegend?: boolean;
  45. disableXAxis?: boolean;
  46. grid?: LineChartProps['grid'];
  47. height?: number;
  48. transaction?: NormalizedTrendsTransaction;
  49. trendFunctionField?: TrendFunctionField;
  50. };
  51. function transformEventStats(data: EventsStatsData, seriesName?: string): Series[] {
  52. return [
  53. {
  54. seriesName: seriesName || 'Current',
  55. data: data.map(([timestamp, countsForTimestamp]) => ({
  56. name: timestamp * 1000,
  57. value: countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  58. })),
  59. },
  60. ];
  61. }
  62. function getLegend(trendFunction: string): LegendComponentOption {
  63. return {
  64. right: 10,
  65. top: 0,
  66. itemGap: 12,
  67. align: 'left',
  68. data: [
  69. {
  70. name: 'Baseline',
  71. 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',
  72. },
  73. {
  74. name: 'Releases',
  75. },
  76. {
  77. name: trendFunction,
  78. },
  79. ],
  80. };
  81. }
  82. function getIntervalLine(
  83. theme: Theme,
  84. series: Series[],
  85. intervalRatio: number,
  86. transaction?: NormalizedTrendsTransaction
  87. ): LineChartSeries[] {
  88. if (!transaction || !series.length || !series[0].data || !series[0].data.length) {
  89. return [];
  90. }
  91. const seriesStart = parseInt(series[0].data[0].name as string, 10);
  92. const seriesEnd = parseInt(series[0].data.slice(-1)[0].name as string, 10);
  93. if (seriesEnd < seriesStart) {
  94. return [];
  95. }
  96. const periodLine: LineChartSeries = {
  97. data: [],
  98. color: theme.textColor,
  99. markLine: {
  100. data: [],
  101. label: {},
  102. lineStyle: {
  103. color: theme.textColor,
  104. type: 'dashed',
  105. width: 1,
  106. },
  107. symbol: ['none', 'none'],
  108. tooltip: {
  109. show: false,
  110. },
  111. },
  112. seriesName: 'Baseline',
  113. };
  114. const periodLineLabel = {
  115. fontSize: 11,
  116. show: true,
  117. color: theme.textColor,
  118. silent: true,
  119. };
  120. const previousPeriod = {
  121. ...periodLine,
  122. markLine: {...periodLine.markLine},
  123. seriesName: 'Baseline',
  124. };
  125. const currentPeriod = {
  126. ...periodLine,
  127. markLine: {...periodLine.markLine},
  128. seriesName: 'Baseline',
  129. };
  130. const periodDividingLine = {
  131. ...periodLine,
  132. markLine: {...periodLine.markLine},
  133. seriesName: 'Period split',
  134. };
  135. const seriesDiff = seriesEnd - seriesStart;
  136. const seriesLine = seriesDiff * intervalRatio + seriesStart;
  137. previousPeriod.markLine.data = [
  138. [
  139. {value: 'Past', coord: [seriesStart, transaction.aggregate_range_1]},
  140. {coord: [seriesLine, transaction.aggregate_range_1]},
  141. ],
  142. ];
  143. previousPeriod.markLine.tooltip = {
  144. formatter: () => {
  145. return [
  146. '<div class="tooltip-series tooltip-series-solo">',
  147. '<div>',
  148. `<span class="tooltip-label"><strong>${t('Past Baseline')}</strong></span>`,
  149. // p50() coerces the axis to be time based
  150. tooltipFormatter(transaction.aggregate_range_1, 'p50()'),
  151. '</div>',
  152. '</div>',
  153. '<div class="tooltip-arrow"></div>',
  154. ].join('');
  155. },
  156. };
  157. currentPeriod.markLine.data = [
  158. [
  159. {value: 'Present', coord: [seriesLine, transaction.aggregate_range_2]},
  160. {coord: [seriesEnd, transaction.aggregate_range_2]},
  161. ],
  162. ];
  163. currentPeriod.markLine.tooltip = {
  164. formatter: () => {
  165. return [
  166. '<div class="tooltip-series tooltip-series-solo">',
  167. '<div>',
  168. `<span class="tooltip-label"><strong>${t('Present Baseline')}</strong></span>`,
  169. // p50() coerces the axis to be time based
  170. tooltipFormatter(transaction.aggregate_range_2, 'p50()'),
  171. '</div>',
  172. '</div>',
  173. '<div class="tooltip-arrow"></div>',
  174. ].join('');
  175. },
  176. };
  177. periodDividingLine.markLine = {
  178. data: [
  179. {
  180. xAxis: seriesLine,
  181. },
  182. ],
  183. label: {show: false},
  184. lineStyle: {
  185. color: theme.textColor,
  186. type: 'solid',
  187. width: 2,
  188. },
  189. symbol: ['none', 'none'],
  190. tooltip: {
  191. show: false,
  192. },
  193. silent: true,
  194. };
  195. previousPeriod.markLine.label = {
  196. ...periodLineLabel,
  197. formatter: 'Past',
  198. position: 'insideStartBottom',
  199. };
  200. currentPeriod.markLine.label = {
  201. ...periodLineLabel,
  202. formatter: 'Present',
  203. position: 'insideEndBottom',
  204. };
  205. const additionalLineSeries = [previousPeriod, currentPeriod, periodDividingLine];
  206. return additionalLineSeries;
  207. }
  208. export function Chart({
  209. trendChangeType,
  210. router,
  211. statsPeriod,
  212. transaction,
  213. statsData,
  214. isLoading,
  215. location,
  216. start: propsStart,
  217. end: propsEnd,
  218. trendFunctionField,
  219. disableXAxis,
  220. disableLegend,
  221. grid,
  222. height,
  223. projects,
  224. project,
  225. }: Props) {
  226. const theme = useTheme();
  227. const handleLegendSelectChanged = legendChange => {
  228. const {selected} = legendChange;
  229. const unselected = Object.keys(selected).filter(key => !selected[key]);
  230. const query = {
  231. ...location.query,
  232. };
  233. const queryKey = getUnselectedSeries(trendChangeType);
  234. query[queryKey] = unselected;
  235. const to = {
  236. ...location,
  237. query,
  238. };
  239. browserHistory.push(to);
  240. };
  241. const lineColor = trendToColor[trendChangeType || ''];
  242. const events =
  243. statsData && transaction?.project && transaction?.transaction
  244. ? statsData[[transaction.project, transaction.transaction].join(',')]
  245. : undefined;
  246. const data = events?.data ?? [];
  247. const trendFunction = getCurrentTrendFunction(location, trendFunctionField);
  248. const trendParameter = getCurrentTrendParameter(location, projects, project);
  249. const chartLabel = generateTrendFunctionAsString(
  250. trendFunction.field,
  251. trendParameter.column
  252. );
  253. const results = transformEventStats(data, chartLabel);
  254. const {smoothedResults, minValue, maxValue} = transformEventStatsSmoothed(
  255. results,
  256. chartLabel
  257. );
  258. const start = propsStart ? getUtcToLocalDateObject(propsStart) : null;
  259. const end = propsEnd ? getUtcToLocalDateObject(propsEnd) : null;
  260. const {utc} = normalizeDateTimeParams(location.query);
  261. const seriesSelection = decodeList(
  262. location.query[getUnselectedSeries(trendChangeType)]
  263. ).reduce((selection, metric) => {
  264. selection[metric] = false;
  265. return selection;
  266. }, {});
  267. const legend: LegendComponentOption = disableLegend
  268. ? {show: false}
  269. : {
  270. ...getLegend(chartLabel),
  271. selected: seriesSelection,
  272. };
  273. const loading = isLoading;
  274. const reloading = isLoading;
  275. const yMax = Math.max(
  276. maxValue,
  277. transaction?.aggregate_range_2 || 0,
  278. transaction?.aggregate_range_1 || 0
  279. );
  280. const yMin = Math.min(
  281. minValue,
  282. transaction?.aggregate_range_1 || Number.MAX_SAFE_INTEGER,
  283. transaction?.aggregate_range_2 || Number.MAX_SAFE_INTEGER
  284. );
  285. const yDiff = yMax - yMin;
  286. const yMargin = yDiff * 0.1;
  287. const chartOptions: Omit<LineChartProps, 'series'> = {
  288. tooltip: {
  289. valueFormatter: (value, seriesName) => {
  290. return tooltipFormatter(value, seriesName);
  291. },
  292. },
  293. yAxis: {
  294. min: Math.max(0, yMin - yMargin),
  295. max: yMax + yMargin,
  296. axisLabel: {
  297. color: theme.chartLabel,
  298. // p50() coerces the axis to be time based
  299. formatter: (value: number) => axisLabelFormatter(value, 'p50()'),
  300. },
  301. },
  302. };
  303. return (
  304. <ChartZoom
  305. router={router}
  306. period={statsPeriod}
  307. start={start}
  308. end={end}
  309. utc={utc === 'true'}
  310. >
  311. {zoomRenderProps => {
  312. const smoothedSeries = smoothedResults
  313. ? smoothedResults.map(values => {
  314. return {
  315. ...values,
  316. color: lineColor.default,
  317. lineStyle: {
  318. opacity: 1,
  319. },
  320. };
  321. })
  322. : [];
  323. const intervalSeries = getIntervalLine(
  324. theme,
  325. smoothedResults || [],
  326. 0.5,
  327. transaction
  328. );
  329. return (
  330. <TransitionChart loading={loading} reloading={reloading}>
  331. <TransparentLoadingMask visible={reloading} />
  332. {getDynamicText({
  333. value: (
  334. <LineChart
  335. height={height}
  336. {...zoomRenderProps}
  337. {...chartOptions}
  338. onLegendSelectChanged={handleLegendSelectChanged}
  339. series={[...smoothedSeries, ...intervalSeries]}
  340. seriesOptions={{
  341. showSymbol: false,
  342. }}
  343. legend={legend}
  344. toolBox={{
  345. show: false,
  346. }}
  347. grid={
  348. grid ?? {
  349. left: '10px',
  350. right: '10px',
  351. top: '40px',
  352. bottom: '0px',
  353. }
  354. }
  355. xAxis={disableXAxis ? {show: false} : undefined}
  356. />
  357. ),
  358. fixed: 'Duration Chart',
  359. })}
  360. </TransitionChart>
  361. );
  362. }}
  363. </ChartZoom>
  364. );
  365. }
  366. export default withRouter(Chart);