chart.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import moment from 'moment';
  2. import MarkLine from 'app/components/charts/components/markLine';
  3. import MarkPoint from 'app/components/charts/components/markPoint';
  4. import LineChart, {LineChartSeries} from 'app/components/charts/lineChart';
  5. import {t} from 'app/locale';
  6. import space from 'app/styles/space';
  7. import theme from 'app/utils/theme';
  8. import {Trigger} from 'app/views/alerts/incidentRules/types';
  9. import closedSymbol from './closedSymbol';
  10. import startedSymbol from './startedSymbol';
  11. type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T;
  12. function truthy<T>(value: T): value is Truthy<T> {
  13. return !!value;
  14. }
  15. type Data = [number, {count: number}[]];
  16. /**
  17. * So we'll have to see how this looks with real data, but echarts requires
  18. * an explicit (x,y) value to draw a symbol (incident started/closed bubble).
  19. *
  20. * This uses the closest date *without* going over.
  21. *
  22. * AFAICT we can't give it an x-axis value and have it draw on the line,
  23. * so we probably need to calculate the y-axis value ourselves if we want it placed
  24. * at the exact time.
  25. *
  26. * @param data Data array
  27. * @param needle the target timestamp
  28. */
  29. function getNearbyIndex(data: Data[], needle: number) {
  30. // `data` is sorted, return the first index whose value (timestamp) is > `needle`
  31. const index = data.findIndex(([ts]) => ts > needle);
  32. // this shouldn't happen, as we try to buffer dates before start/end dates
  33. if (index === 0) {
  34. return 0;
  35. }
  36. return index !== -1 ? index - 1 : data.length - 1;
  37. }
  38. type Props = {
  39. data: Data[];
  40. aggregate: string;
  41. started: string;
  42. closed?: string;
  43. triggers?: Trigger[];
  44. resolveThreshold?: number | '' | null;
  45. };
  46. const Chart = (props: Props) => {
  47. const {aggregate, data, started, closed, triggers, resolveThreshold} = props;
  48. const startedTs = started && moment.utc(started).unix();
  49. const closedTs = closed && moment.utc(closed).unix();
  50. const chartData = data.map(([ts, val]) => [
  51. ts * 1000,
  52. val.length ? val.reduce((acc, {count} = {count: 0}) => acc + count, 0) : 0,
  53. ]);
  54. const startedCoordinate = startedTs
  55. ? chartData[getNearbyIndex(data, startedTs)]
  56. : undefined;
  57. const showClosedMarker =
  58. data && closedTs && data[data.length - 1] && data[data.length - 1][0] >= closedTs
  59. ? true
  60. : false;
  61. const closedCoordinate =
  62. closedTs && showClosedMarker ? chartData[getNearbyIndex(data, closedTs)] : undefined;
  63. const seriesName = aggregate;
  64. const warningTrigger = triggers?.find(trig => trig.label === 'warning');
  65. const criticalTrigger = triggers?.find(trig => trig.label === 'critical');
  66. const warningTriggerAlertThreshold =
  67. typeof warningTrigger?.alertThreshold === 'number'
  68. ? warningTrigger?.alertThreshold
  69. : undefined;
  70. const criticalTriggerAlertThreshold =
  71. typeof criticalTrigger?.alertThreshold === 'number'
  72. ? criticalTrigger?.alertThreshold
  73. : undefined;
  74. const alertResolveThreshold =
  75. typeof resolveThreshold === 'number' ? resolveThreshold : undefined;
  76. const marklinePrecision = Math.max(
  77. ...[
  78. warningTriggerAlertThreshold,
  79. criticalTriggerAlertThreshold,
  80. alertResolveThreshold,
  81. ].map(decimal => {
  82. if (!decimal || !isFinite(decimal)) return 0;
  83. let e = 1;
  84. let p = 0;
  85. while (Math.round(decimal * e) / e !== decimal) {
  86. e *= 10;
  87. p += 1;
  88. }
  89. return p;
  90. })
  91. );
  92. const lineSeries: LineChartSeries[] = [
  93. {
  94. // e.g. Events or Users
  95. seriesName,
  96. dataArray: chartData,
  97. data: [],
  98. markPoint: MarkPoint({
  99. data: [
  100. {
  101. labelForValue: seriesName,
  102. seriesName,
  103. symbol: `image://${startedSymbol}`,
  104. name: t('Alert Triggered'),
  105. coord: startedCoordinate,
  106. },
  107. ...(closedTs
  108. ? [
  109. {
  110. labelForValue: seriesName,
  111. seriesName,
  112. symbol: `image://${closedSymbol}`,
  113. symbolSize: 24,
  114. name: t('Alert Resolved'),
  115. coord: closedCoordinate,
  116. },
  117. ]
  118. : []),
  119. ] as any, // TODO(ts): data on this type is likely incomplete (needs @types/echarts@4.6.2)
  120. }),
  121. },
  122. warningTrigger &&
  123. warningTriggerAlertThreshold && {
  124. seriesName: 'Warning Alert',
  125. type: 'line',
  126. markLine: MarkLine({
  127. silent: true,
  128. lineStyle: {color: theme.yellow300},
  129. data: [
  130. {
  131. yAxis: warningTriggerAlertThreshold,
  132. } as any, // TODO(ts): data on this type is likely incomplete (needs @types/echarts@4.6.2)
  133. ],
  134. precision: marklinePrecision,
  135. label: {
  136. show: true,
  137. position: 'insideEndTop',
  138. formatter: 'WARNING',
  139. color: theme.yellow300,
  140. fontSize: 10,
  141. } as any, // TODO(ts): Color is not an exposed option for label,
  142. }),
  143. data: [],
  144. },
  145. criticalTrigger &&
  146. criticalTriggerAlertThreshold && {
  147. seriesName: 'Critical Alert',
  148. type: 'line',
  149. markLine: MarkLine({
  150. silent: true,
  151. lineStyle: {color: theme.red200},
  152. data: [
  153. {
  154. yAxis: criticalTriggerAlertThreshold,
  155. } as any, // TODO(ts): data on this type is likely incomplete (needs @types/echarts@4.6.2)
  156. ],
  157. precision: marklinePrecision,
  158. label: {
  159. show: true,
  160. position: 'insideEndTop',
  161. formatter: 'CRITICAL',
  162. color: theme.red300,
  163. fontSize: 10,
  164. } as any, // TODO(ts): Color is not an exposed option for label,
  165. }),
  166. data: [],
  167. },
  168. criticalTrigger &&
  169. alertResolveThreshold && {
  170. seriesName: 'Critical Resolve',
  171. type: 'line',
  172. markLine: MarkLine({
  173. silent: true,
  174. lineStyle: {color: theme.gray200},
  175. data: [
  176. {
  177. yAxis: alertResolveThreshold,
  178. } as any, // TODO(ts): data on this type is likely incomplete (needs @types/echarts@4.6.2)
  179. ],
  180. precision: marklinePrecision,
  181. label: {
  182. show: true,
  183. position: 'insideEndBottom',
  184. formatter: 'CRITICAL RESOLUTION',
  185. color: theme.gray200,
  186. fontSize: 10,
  187. } as any, // TODO(ts): Color is not an option for label,
  188. }),
  189. data: [],
  190. },
  191. ].filter(truthy);
  192. return (
  193. <LineChart
  194. isGroupedByDate
  195. showTimeInTooltip
  196. grid={{
  197. left: 0,
  198. right: 0,
  199. top: space(2),
  200. bottom: 0,
  201. }}
  202. series={lineSeries}
  203. />
  204. );
  205. };
  206. export default Chart;