discover.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. import type {SeriesOption} from 'echarts';
  2. import isArray from 'lodash/isArray';
  3. import max from 'lodash/max';
  4. import XAxis from 'sentry/components/charts/components/xAxis';
  5. import AreaSeries from 'sentry/components/charts/series/areaSeries';
  6. import BarSeries from 'sentry/components/charts/series/barSeries';
  7. import LineSeries from 'sentry/components/charts/series/lineSeries';
  8. import MapSeries from 'sentry/components/charts/series/mapSeries';
  9. import {lightenHexToRgb} from 'sentry/components/charts/utils';
  10. import * as countryCodesMap from 'sentry/data/countryCodesMap';
  11. import {t} from 'sentry/locale';
  12. import {EventsGeoData, EventsStats} from 'sentry/types';
  13. import {lightTheme as theme} from 'sentry/utils/theme';
  14. import {
  15. DEFAULT_FONT_FAMILY,
  16. slackChartDefaults,
  17. slackChartSize,
  18. slackGeoChartSize,
  19. } from './slack';
  20. import {ChartType, RenderDescriptor} from './types';
  21. const discoverxAxis = XAxis({
  22. theme,
  23. // @ts-expect-error Not sure whats wrong with boundryGap type
  24. boundaryGap: true,
  25. splitNumber: 3,
  26. isGroupedByDate: true,
  27. axisLabel: {fontSize: 11, fontFamily: DEFAULT_FONT_FAMILY},
  28. });
  29. export const discoverCharts: RenderDescriptor<ChartType>[] = [];
  30. discoverCharts.push({
  31. key: ChartType.SLACK_DISCOVER_TOTAL_PERIOD,
  32. getOption: (
  33. data:
  34. | {seriesName: string; stats: EventsStats}
  35. | {stats: Record<string, EventsStats>; seriesName?: string}
  36. ) => {
  37. if (isArray(data.stats.data)) {
  38. const color = theme.charts.getColorPalette(data.stats.data.length - 2);
  39. const areaSeries = AreaSeries({
  40. name: data.seriesName,
  41. data: data.stats.data.map(([timestamp, countsForTimestamp]) => [
  42. timestamp * 1000,
  43. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  44. ]),
  45. lineStyle: {color: color?.[0], opacity: 1, width: 0.4},
  46. areaStyle: {color: color?.[0], opacity: 1},
  47. });
  48. return {
  49. ...slackChartDefaults,
  50. useUTC: true,
  51. color,
  52. series: [areaSeries],
  53. };
  54. }
  55. const stats = Object.keys(data.stats).map(key =>
  56. Object.assign({}, {key}, data.stats[key])
  57. );
  58. const color = theme.charts.getColorPalette(stats.length - 2);
  59. const series = stats
  60. .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
  61. .map((s, i) =>
  62. AreaSeries({
  63. name: s.key,
  64. stack: 'area',
  65. data: s.data.map(([timestamp, countsForTimestamp]) => [
  66. timestamp * 1000,
  67. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  68. ]),
  69. lineStyle: {color: color?.[i], opacity: 1, width: 0.4},
  70. areaStyle: {color: color?.[i], opacity: 1},
  71. })
  72. );
  73. return {
  74. ...slackChartDefaults,
  75. xAxis: discoverxAxis,
  76. useUTC: true,
  77. color,
  78. series,
  79. };
  80. },
  81. ...slackChartSize,
  82. });
  83. discoverCharts.push({
  84. key: ChartType.SLACK_DISCOVER_TOTAL_DAILY,
  85. getOption: (
  86. data:
  87. | {seriesName: string; stats: EventsStats}
  88. | {stats: Record<string, EventsStats>; seriesName?: string}
  89. ) => {
  90. if (isArray(data.stats.data)) {
  91. const color = theme.charts.getColorPalette(data.stats.data.length - 2);
  92. const barSeries = BarSeries({
  93. name: data.seriesName,
  94. data: data.stats.data.map(([timestamp, countsForTimestamp]) => ({
  95. value: [
  96. timestamp * 1000,
  97. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  98. ],
  99. })),
  100. itemStyle: {color: color?.[0], opacity: 1},
  101. });
  102. return {
  103. ...slackChartDefaults,
  104. xAxis: discoverxAxis,
  105. useUTC: true,
  106. color,
  107. series: [barSeries],
  108. };
  109. }
  110. const stats = Object.keys(data.stats).map(key =>
  111. Object.assign({}, {key}, data.stats[key])
  112. );
  113. const color = theme.charts.getColorPalette(stats.length - 2);
  114. const series = stats
  115. .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
  116. .map((s, i) =>
  117. BarSeries({
  118. name: s.key,
  119. stack: 'area',
  120. data: s.data.map(([timestamp, countsForTimestamp]) => ({
  121. value: [
  122. timestamp * 1000,
  123. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  124. ],
  125. })),
  126. itemStyle: {color: color?.[i], opacity: 1},
  127. })
  128. );
  129. return {
  130. ...slackChartDefaults,
  131. xAxis: discoverxAxis,
  132. useUTC: true,
  133. color,
  134. series,
  135. };
  136. },
  137. ...slackChartSize,
  138. });
  139. discoverCharts.push({
  140. key: ChartType.SLACK_DISCOVER_TOP5_PERIOD,
  141. getOption: (
  142. data: {stats: Record<string, EventsStats>} | {stats: EventsStats; seriesName?: string}
  143. ) => {
  144. if (isArray(data.stats.data)) {
  145. const color = theme.charts.getColorPalette(data.stats.data.length - 2);
  146. const areaSeries = AreaSeries({
  147. data: data.stats.data.map(([timestamp, countsForTimestamp]) => [
  148. timestamp * 1000,
  149. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  150. ]),
  151. lineStyle: {color: color?.[0], opacity: 1, width: 0.4},
  152. areaStyle: {color: color?.[0], opacity: 1},
  153. });
  154. return {
  155. ...slackChartDefaults,
  156. useUTC: true,
  157. color,
  158. series: [areaSeries],
  159. };
  160. }
  161. const stats = Object.values(data.stats);
  162. const hasOther = Object.keys(data.stats).includes('Other');
  163. const color = theme.charts.getColorPalette(stats.length - 2 - (hasOther ? 1 : 0));
  164. if (hasOther) {
  165. color.push(theme.chartOther);
  166. }
  167. const series = stats
  168. .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
  169. .map((topSeries, i) =>
  170. AreaSeries({
  171. stack: 'area',
  172. data: topSeries.data.map(([timestamp, countsForTimestamp]) => [
  173. timestamp * 1000,
  174. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  175. ]),
  176. lineStyle: {color: color?.[i], opacity: 1, width: 0.4},
  177. areaStyle: {color: color?.[i], opacity: 1},
  178. })
  179. );
  180. return {
  181. ...slackChartDefaults,
  182. xAxis: discoverxAxis,
  183. useUTC: true,
  184. color,
  185. series,
  186. };
  187. },
  188. ...slackChartSize,
  189. });
  190. discoverCharts.push({
  191. key: ChartType.SLACK_DISCOVER_TOP5_PERIOD_LINE,
  192. getOption: (
  193. data: {stats: Record<string, EventsStats>} | {stats: EventsStats; seriesName?: string}
  194. ) => {
  195. if (isArray(data.stats.data)) {
  196. const color = theme.charts.getColorPalette(data.stats.data.length - 2);
  197. const lineSeries = LineSeries({
  198. data: data.stats.data.map(([timestamp, countsForTimestamp]) => [
  199. timestamp * 1000,
  200. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  201. ]),
  202. lineStyle: {color: color?.[0], opacity: 1},
  203. itemStyle: {color: color?.[0]},
  204. });
  205. return {
  206. ...slackChartDefaults,
  207. useUTC: true,
  208. color,
  209. series: [lineSeries],
  210. };
  211. }
  212. const stats = Object.values(data.stats);
  213. const hasOther = Object.keys(data.stats).includes('Other');
  214. const color = theme.charts.getColorPalette(stats.length - 2 - (hasOther ? 1 : 0));
  215. if (hasOther) {
  216. color.push(theme.chartOther);
  217. }
  218. const series = stats
  219. .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
  220. .map((topSeries, i) =>
  221. LineSeries({
  222. data: topSeries.data.map(([timestamp, countsForTimestamp]) => [
  223. timestamp * 1000,
  224. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  225. ]),
  226. lineStyle: {color: color?.[i], opacity: 1},
  227. itemStyle: {color: color?.[i]},
  228. })
  229. );
  230. return {
  231. ...slackChartDefaults,
  232. xAxis: discoverxAxis,
  233. useUTC: true,
  234. color,
  235. series,
  236. };
  237. },
  238. ...slackChartSize,
  239. });
  240. discoverCharts.push({
  241. key: ChartType.SLACK_DISCOVER_TOP5_DAILY,
  242. getOption: (
  243. data: {stats: Record<string, EventsStats>} | {stats: EventsStats; seriesName?: string}
  244. ) => {
  245. if (isArray(data.stats.data)) {
  246. const color = theme.charts.getColorPalette(data.stats.data.length - 2);
  247. const areaSeries = AreaSeries({
  248. data: data.stats.data.map(([timestamp, countsForTimestamp]) => [
  249. timestamp * 1000,
  250. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  251. ]),
  252. lineStyle: {color: color?.[0], opacity: 1, width: 0.4},
  253. areaStyle: {color: color?.[0], opacity: 1},
  254. });
  255. return {
  256. ...slackChartDefaults,
  257. useUTC: true,
  258. color,
  259. series: [areaSeries],
  260. };
  261. }
  262. const stats = Object.values(data.stats);
  263. const hasOther = Object.keys(data.stats).includes('Other');
  264. const color = theme.charts.getColorPalette(stats.length - 2 - (hasOther ? 1 : 0));
  265. if (hasOther) {
  266. color.push(theme.chartOther);
  267. }
  268. const series = stats
  269. .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
  270. .map((topSeries, i) =>
  271. BarSeries({
  272. stack: 'area',
  273. data: topSeries.data.map(([timestamp, countsForTimestamp]) => [
  274. timestamp * 1000,
  275. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  276. ]),
  277. itemStyle: {color: color?.[i], opacity: 1},
  278. })
  279. );
  280. return {
  281. ...slackChartDefaults,
  282. xAxis: discoverxAxis,
  283. useUTC: true,
  284. color,
  285. series,
  286. };
  287. },
  288. ...slackChartSize,
  289. });
  290. discoverCharts.push({
  291. key: ChartType.SLACK_DISCOVER_PREVIOUS_PERIOD,
  292. getOption: (
  293. data:
  294. | {seriesName: string; stats: EventsStats}
  295. | {stats: Record<string, EventsStats>; seriesName?: string}
  296. ) => {
  297. if (isArray(data.stats.data)) {
  298. const dataMiddleIndex = Math.floor(data.stats.data.length / 2);
  299. const current = data.stats.data.slice(dataMiddleIndex);
  300. const previous = data.stats.data.slice(0, dataMiddleIndex);
  301. const color = theme.charts.getColorPalette(data.stats.data.length - 2);
  302. const areaSeries = AreaSeries({
  303. name: data.seriesName,
  304. data: current.map(([timestamp, countsForTimestamp]) => [
  305. timestamp * 1000,
  306. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  307. ]),
  308. lineStyle: {color: color?.[0], opacity: 1, width: 0.4},
  309. areaStyle: {color: color?.[0], opacity: 1},
  310. });
  311. const previousPeriod = LineSeries({
  312. name: t('previous %s', data.seriesName),
  313. data: previous.map(([_, countsForTimestamp], i) => [
  314. current[i][0] * 1000,
  315. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  316. ]),
  317. lineStyle: {color: theme.gray200, type: 'dotted'},
  318. itemStyle: {color: theme.gray200},
  319. });
  320. return {
  321. ...slackChartDefaults,
  322. useUTC: true,
  323. color,
  324. series: [areaSeries, previousPeriod],
  325. };
  326. }
  327. const stats = Object.keys(data.stats).map(key =>
  328. Object.assign({}, {key}, data.stats[key])
  329. );
  330. const color = theme.charts.getColorPalette(stats.length - 2);
  331. const previousPeriodColor = lightenHexToRgb(color);
  332. const areaSeries: SeriesOption[] = [];
  333. const lineSeries: SeriesOption[] = [];
  334. stats
  335. .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
  336. .map((s, i) => {
  337. const dataMiddleIndex = Math.floor(s.data.length / 2);
  338. const current = s.data.slice(dataMiddleIndex);
  339. const previous = s.data.slice(0, dataMiddleIndex);
  340. areaSeries.push(
  341. AreaSeries({
  342. name: s.key,
  343. stack: 'area',
  344. data: s.data
  345. .slice(dataMiddleIndex)
  346. .map(([timestamp, countsForTimestamp]) => [
  347. timestamp * 1000,
  348. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  349. ]),
  350. lineStyle: {color: color?.[i], opacity: 1, width: 0.4},
  351. areaStyle: {color: color?.[i], opacity: 1},
  352. })
  353. );
  354. lineSeries.push(
  355. LineSeries({
  356. name: t('previous %s', s.key),
  357. stack: 'previous',
  358. data: previous.map(([_, countsForTimestamp], index) => [
  359. current[index][0] * 1000,
  360. countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  361. ]),
  362. lineStyle: {color: previousPeriodColor?.[i], type: 'dotted'},
  363. itemStyle: {color: previousPeriodColor?.[i]},
  364. })
  365. );
  366. });
  367. return {
  368. ...slackChartDefaults,
  369. xAxis: discoverxAxis,
  370. useUTC: true,
  371. color,
  372. series: [...areaSeries, ...lineSeries],
  373. };
  374. },
  375. ...slackChartSize,
  376. });
  377. discoverCharts.push({
  378. key: ChartType.SLACK_DISCOVER_WORLDMAP,
  379. getOption: (data: {seriesName: string; stats: {data: EventsGeoData}}) => {
  380. const mapSeries = MapSeries({
  381. map: 'sentryWorld',
  382. name: data.seriesName,
  383. data: data.stats.data.map(country => ({
  384. name: country['geo.country_code'],
  385. value: country.count,
  386. })),
  387. nameMap: countryCodesMap.default,
  388. aspectScale: 0.85,
  389. zoom: 1.1,
  390. center: [10.97, 9.71],
  391. itemStyle: {
  392. areaColor: theme.gray200,
  393. borderColor: theme.backgroundSecondary,
  394. },
  395. });
  396. // For absolute values, we want min/max to based on min/max of series
  397. // Otherwise it should be 0-100
  398. const maxValue = max(data.stats.data.map(value => value.count)) || 1;
  399. return {
  400. backgroundColor: theme.background,
  401. visualMap: [
  402. {
  403. left: 'right',
  404. min: 0,
  405. max: maxValue,
  406. inRange: {
  407. color: [theme.purple200, theme.purple300],
  408. },
  409. text: ['High', 'Low'],
  410. textStyle: {
  411. color: theme.textColor,
  412. },
  413. // Whether show handles, which can be dragged to adjust "selected range".
  414. // False because the handles are pretty ugly
  415. calculable: false,
  416. },
  417. ],
  418. series: [mapSeries],
  419. };
  420. },
  421. ...slackGeoChartSize,
  422. });