lineChartWidgetVisualization.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import {useRef} from 'react';
  2. import {useNavigate} from 'react-router-dom';
  3. import {useTheme} from '@emotion/react';
  4. import type {
  5. TooltipFormatterCallback,
  6. TopLevelFormatterParams,
  7. } from 'echarts/types/dist/shared';
  8. import BaseChart from 'sentry/components/charts/baseChart';
  9. import {getFormatter} from 'sentry/components/charts/components/tooltip';
  10. import LineSeries from 'sentry/components/charts/series/lineSeries';
  11. import {useChartZoom} from 'sentry/components/charts/useChartZoom';
  12. import {isChartHovered} from 'sentry/components/charts/utils';
  13. import type {ReactEchartsRef, Series} from 'sentry/types/echarts';
  14. import {defined} from 'sentry/utils';
  15. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import {ReleaseSeries} from '../common/releaseSeries';
  18. import type {Meta, Release, TimeseriesData} from '../common/types';
  19. import {formatChartValue} from './formatChartValue';
  20. import {splitSeriesIntoCompleteAndIncomplete} from './splitSeriesIntoCompleteAndIncomplete';
  21. export interface LineChartWidgetVisualizationProps {
  22. timeseries: TimeseriesData[];
  23. dataCompletenessDelay?: number;
  24. meta?: Meta;
  25. releases?: Release[];
  26. utc?: boolean;
  27. }
  28. export function LineChartWidgetVisualization(props: LineChartWidgetVisualizationProps) {
  29. const chartRef = useRef<ReactEchartsRef>(null);
  30. const {meta} = props;
  31. const dataCompletenessDelay = props.dataCompletenessDelay ?? 0;
  32. const theme = useTheme();
  33. const organization = useOrganization();
  34. const navigate = useNavigate();
  35. let releaseSeries: Series | undefined = undefined;
  36. if (props.releases) {
  37. const onClick = (release: Release) => {
  38. navigate(
  39. normalizeUrl({
  40. pathname: `/organizations/${
  41. organization.slug
  42. }/releases/${encodeURIComponent(release.version)}/`,
  43. })
  44. );
  45. };
  46. releaseSeries = ReleaseSeries(theme, props.releases, onClick, props.utc ?? false);
  47. }
  48. const chartZoomProps = useChartZoom({
  49. saveOnZoom: true,
  50. });
  51. let completeSeries: TimeseriesData[] = props.timeseries;
  52. const incompleteSeries: TimeseriesData[] = [];
  53. if (dataCompletenessDelay > 0) {
  54. completeSeries = [];
  55. props.timeseries.forEach(timeserie => {
  56. const [completeSerie, incompleteSerie] = splitSeriesIntoCompleteAndIncomplete(
  57. timeserie,
  58. dataCompletenessDelay
  59. );
  60. if (completeSerie && completeSerie.data.length > 0) {
  61. completeSeries.push(completeSerie);
  62. }
  63. if (incompleteSerie && incompleteSerie.data.length > 0) {
  64. incompleteSeries.push(incompleteSerie);
  65. }
  66. });
  67. }
  68. // TODO: There's a TypeScript indexing error here. This _could_ in theory be
  69. // `undefined`. We need to guard against this in the parent component, and
  70. // show an error.
  71. const firstSeries = props.timeseries[0];
  72. // TODO: Raise error if attempting to plot series of different types or units
  73. const firstSeriesField = firstSeries?.field;
  74. const type = meta?.fields?.[firstSeriesField] ?? 'number';
  75. const unit = meta?.units?.[firstSeriesField] ?? undefined;
  76. const formatter: TooltipFormatterCallback<TopLevelFormatterParams> = (
  77. params,
  78. asyncTicket
  79. ) => {
  80. // Only show the tooltip of the current chart. Otherwise, all tooltips
  81. // in the chart group appear.
  82. if (!isChartHovered(chartRef?.current)) {
  83. return '';
  84. }
  85. let deDupedParams = params;
  86. if (Array.isArray(params)) {
  87. // We split each series into a complete and incomplete series, and they
  88. // have the same name. The two series overlap at one point on the chart,
  89. // to create a continuous line. This code prevents both series from
  90. // showing up on the tooltip
  91. const uniqueSeries = new Set<string>();
  92. deDupedParams = params.filter(param => {
  93. // Filter null values from tooltip
  94. if (param.value[1] === null) {
  95. return false;
  96. }
  97. if (uniqueSeries.has(param.seriesName)) {
  98. return false;
  99. }
  100. uniqueSeries.add(param.seriesName);
  101. return true;
  102. });
  103. }
  104. return getFormatter({
  105. isGroupedByDate: true,
  106. showTimeInTooltip: true,
  107. truncate: true,
  108. utc: props.utc ?? false,
  109. })(deDupedParams, asyncTicket);
  110. };
  111. return (
  112. <BaseChart
  113. ref={chartRef}
  114. autoHeightResize
  115. series={[
  116. ...completeSeries.map(timeserie => {
  117. return LineSeries({
  118. name: timeserie.field,
  119. color: timeserie.color,
  120. animation: false,
  121. data: timeserie.data.map(datum => {
  122. return [datum.timestamp, datum.value];
  123. }),
  124. });
  125. }),
  126. ...incompleteSeries.map(timeserie => {
  127. return LineSeries({
  128. name: timeserie.field,
  129. color: timeserie.color,
  130. animation: false,
  131. data: timeserie.data.map(datum => {
  132. return [datum.timestamp, datum.value];
  133. }),
  134. lineStyle: {
  135. type: 'dotted',
  136. },
  137. silent: true,
  138. });
  139. }),
  140. releaseSeries &&
  141. LineSeries({
  142. ...releaseSeries,
  143. name: releaseSeries.seriesName,
  144. data: [],
  145. }),
  146. ].filter(defined)}
  147. utc={props.utc}
  148. legend={{
  149. top: 0,
  150. left: 0,
  151. }}
  152. tooltip={{
  153. trigger: 'axis',
  154. axisPointer: {
  155. type: 'cross',
  156. },
  157. formatter,
  158. valueFormatter: value => {
  159. return formatChartValue(value, type, unit);
  160. },
  161. }}
  162. yAxis={{
  163. axisLabel: {
  164. formatter(value: number) {
  165. return formatChartValue(value, type, unit);
  166. },
  167. },
  168. axisPointer: {
  169. type: 'line',
  170. snap: false,
  171. lineStyle: {
  172. type: 'solid',
  173. width: 0.5,
  174. },
  175. label: {
  176. show: false,
  177. },
  178. },
  179. }}
  180. {...chartZoomProps}
  181. isGroupedByDate
  182. />
  183. );
  184. }