anomalyChart.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import type {MarkAreaComponentOption} from 'echarts';
  2. import moment from 'moment-timezone';
  3. import type {AreaChartSeries} from 'sentry/components/charts/areaChart';
  4. import MarkLine from 'sentry/components/charts/components/markLine';
  5. import ConfigStore from 'sentry/stores/configStore';
  6. import {lightTheme as theme} from 'sentry/utils/theme';
  7. import type {Anomaly} from 'sentry/views/alerts/types';
  8. import {AnomalyType} from 'sentry/views/alerts/types';
  9. export interface AnomalyMarkerSeriesOptions {
  10. endDate?: Date;
  11. startDate?: Date;
  12. }
  13. export function getAnomalyMarkerSeries(
  14. anomalies: Anomaly[],
  15. opts: AnomalyMarkerSeriesOptions = {}
  16. ): AreaChartSeries[] {
  17. const series: AreaChartSeries[] = [];
  18. if (!Array.isArray(anomalies) || anomalies.length === 0) {
  19. return series;
  20. }
  21. const {startDate, endDate} = opts;
  22. const filterPredicate = (anomaly: Anomaly): boolean => {
  23. const timestamp = new Date(anomaly.timestamp).getTime();
  24. if (startDate && endDate) {
  25. return startDate.getTime() < timestamp && timestamp < endDate.getTime();
  26. }
  27. if (startDate) {
  28. return startDate.getTime() < timestamp;
  29. }
  30. if (endDate) {
  31. return timestamp < endDate.getTime();
  32. }
  33. return true;
  34. };
  35. const anomalyBlocks: MarkAreaComponentOption['data'] = [];
  36. let start: string | undefined;
  37. let end: string | undefined;
  38. anomalies
  39. .filter(item => filterPredicate(item))
  40. .forEach(item => {
  41. const {anomaly, timestamp} = item;
  42. if (
  43. [AnomalyType.HIGH_CONFIDENCE, AnomalyType.LOW_CONFIDENCE].includes(
  44. anomaly.anomaly_type
  45. )
  46. ) {
  47. if (!start) {
  48. // If this is the start of an anomaly, set start
  49. start = getDateForTimestamp(timestamp).toISOString();
  50. }
  51. // as long as we have an valid anomaly type - continue tracking until we've hit the end
  52. end = getDateForTimestamp(timestamp).toISOString();
  53. } else {
  54. if (start && end) {
  55. // If we've hit a non-anomaly type, push the block
  56. anomalyBlocks.push([
  57. {
  58. xAxis: start,
  59. },
  60. {
  61. xAxis: end,
  62. },
  63. ]);
  64. // Create a marker line for the start of the anomaly
  65. series.push(createAnomalyMarkerSeries(theme.purple300, start));
  66. }
  67. // reset the start/end to capture the next anomaly block
  68. start = undefined;
  69. end = undefined;
  70. }
  71. });
  72. if (start && end) {
  73. // push in the last block
  74. // Create a marker line for the start of the anomaly
  75. series.push(createAnomalyMarkerSeries(theme.purple300, start));
  76. anomalyBlocks.push([
  77. {
  78. xAxis: start,
  79. },
  80. {
  81. xAxis: end,
  82. },
  83. ]);
  84. }
  85. // NOTE: if timerange is too small - highlighted area will not be visible
  86. // Possibly provide a minimum window size if the time range is too large?
  87. series.push({
  88. seriesName: '',
  89. name: 'Anomaly',
  90. type: 'line',
  91. smooth: true,
  92. data: [],
  93. markArea: {
  94. itemStyle: {
  95. color: 'rgba(255, 173, 177, 0.4)',
  96. },
  97. silent: true, // potentially don't make this silent if we want to render the `anomaly detected` in the tooltip
  98. data: anomalyBlocks,
  99. },
  100. });
  101. return series;
  102. }
  103. function createAnomalyMarkerSeries(
  104. lineColor: string,
  105. timestamp: string
  106. ): AreaChartSeries {
  107. const formatter = ({value}: any) => {
  108. const time = formatTooltipDate(moment(value), 'MMM D, YYYY LT');
  109. return [
  110. `<div class="tooltip-series"><div>`,
  111. `</div>Anomaly Detected</div>`,
  112. `<div class="tooltip-footer">${time}</div>`,
  113. '<div class="tooltip-arrow"></div>',
  114. ].join('');
  115. };
  116. return {
  117. seriesName: 'Anomaly Line',
  118. type: 'line',
  119. markLine: MarkLine({
  120. silent: false,
  121. lineStyle: {color: lineColor, type: 'dashed'},
  122. label: {
  123. silent: true,
  124. show: false,
  125. },
  126. data: [
  127. {
  128. xAxis: timestamp,
  129. },
  130. ],
  131. tooltip: {
  132. formatter,
  133. },
  134. }),
  135. data: [],
  136. tooltip: {
  137. trigger: 'item',
  138. alwaysShowContent: true,
  139. formatter,
  140. },
  141. };
  142. }
  143. function getDateForTimestamp(timestamp: string | number): Date {
  144. return new Date(typeof timestamp === 'string' ? timestamp : timestamp * 1000);
  145. }
  146. function formatTooltipDate(date: moment.MomentInput, format: string): string {
  147. const {
  148. options: {timezone},
  149. } = ConfigStore.get('user');
  150. return moment.tz(date, timezone).format(format);
  151. }