diff --git a/static/app/components/charts/baseChart.tsx b/static/app/components/charts/baseChart.tsx index b39db877acde4d..014c422559e912 100644 --- a/static/app/components/charts/baseChart.tsx +++ b/static/app/components/charts/baseChart.tsx @@ -69,6 +69,11 @@ type Props = { * be an array of ECharts "Series" components. */ series?: EChartOption.Series[]; + /** + * Additional Chart Series + * This is to pass series to BaseChart bypassing the wrappers like LineChart, AreaChart etc. + */ + additionalSeries?: EChartOption.SeriesLine[]; /** * Array of color codes to use in charts. May also take a function which is * provided with the current theme @@ -112,6 +117,7 @@ type Props = { seriesParams?: EChartOption.Tooltip.Format ) => string | number; nameFormatter?: (name: string) => string; + markerFormatter?: (marker: string, label?: string) => string; /** * Array containing seriesNames that need to be indented */ @@ -283,6 +289,7 @@ function BaseChartUnwrapped({ options = {}, series = [], + additionalSeries = [], yAxis = {}, xAxis = {}, @@ -337,8 +344,8 @@ function BaseChartUnwrapped({ ) ?? []; const resolvedSeries = !previousPeriod - ? transformedSeries - : [...transformedSeries, ...transformedPreviousPeriod]; + ? [...transformedSeries, ...additionalSeries] + : [...transformedSeries, ...transformedPreviousPeriod, ...additionalSeries]; const defaultAxesProps = {theme}; diff --git a/static/app/components/charts/components/tooltip.tsx b/static/app/components/charts/components/tooltip.tsx index af73e5dfaf770d..15ab6894d3f9e5 100644 --- a/static/app/components/charts/components/tooltip.tsx +++ b/static/app/components/charts/components/tooltip.tsx @@ -60,6 +60,10 @@ function defaultNameFormatter(value: string) { return value; } +function defaultMarkerFormatter(value: string) { + return value; +} + function getSeriesValue(series: EChartOption.Tooltip.Format, offset: number) { if (!series.data) { return undefined; @@ -81,7 +85,8 @@ type TooltipFormatters = | 'filter' | 'formatAxisLabel' | 'valueFormatter' - | 'nameFormatter'; + | 'nameFormatter' + | 'markerFormatter'; type FormatterOptions = Pick, TooltipFormatters> & Pick & { @@ -105,6 +110,7 @@ function getFormatter({ bucketSize, valueFormatter = defaultValueFormatter, nameFormatter = defaultNameFormatter, + markerFormatter = defaultMarkerFormatter, indentLabels = [], addSecondsToTimeFormat = false, }: FormatterOptions) { @@ -203,11 +209,13 @@ function getFormatter({ ); const value = valueFormatter(getSeriesValue(s, 1), s.seriesName, s); + const marker = markerFormatter(s.marker ?? '', s.seriesName); + const className = indentLabels.includes(formattedLabel) ? 'tooltip-label tooltip-label-indent' : 'tooltip-label'; - return `
${s.marker} ${formattedLabel} ${value}
`; + return `
${marker} ${formattedLabel} ${value}
`; }) .join(''), '', @@ -235,6 +243,7 @@ export default function Tooltip({ formatAxisLabel, valueFormatter, nameFormatter, + markerFormatter, hideDelay, indentLabels, ...props @@ -252,6 +261,7 @@ export default function Tooltip({ formatAxisLabel, valueFormatter, nameFormatter, + markerFormatter, indentLabels, }); diff --git a/static/app/components/charts/eventsRequest.tsx b/static/app/components/charts/eventsRequest.tsx index 8dfeff961818dc..42270cbaab1389 100644 --- a/static/app/components/charts/eventsRequest.tsx +++ b/static/app/components/charts/eventsRequest.tsx @@ -69,7 +69,7 @@ type DefaultProps = { */ interval: string; /** - * Time delta for comparing intervals alert metrics, value in minutes + * Time delta for comparing intervals of alert metrics, in seconds */ comparisonDelta?: number; /** diff --git a/static/app/views/alerts/incidentRules/triggers/chart/index.tsx b/static/app/views/alerts/incidentRules/triggers/chart/index.tsx index 6cd8134bf8b5ac..93744e374a013d 100644 --- a/static/app/views/alerts/incidentRules/triggers/chart/index.tsx +++ b/static/app/views/alerts/incidentRules/triggers/chart/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import styled from '@emotion/styled'; +import capitalize from 'lodash/capitalize'; import chunk from 'lodash/chunk'; import maxBy from 'lodash/maxBy'; import minBy from 'lodash/minBy'; @@ -31,6 +32,7 @@ import { } from 'app/utils/sessions'; import theme from 'app/utils/theme'; import withApi from 'app/utils/withApi'; +import {COMPARISON_DELTA_OPTIONS} from 'app/views/alerts/incidentRules/constants'; import {isSessionAggregate, SESSION_AGGREGATE_TO_FIELD} from 'app/views/alerts/utils'; import {AlertWizardAlertNames} from 'app/views/alerts/wizard/options'; import {getAlertTypeFromAggregateDataset} from 'app/views/alerts/wizard/utils'; @@ -218,6 +220,13 @@ class TriggersChart extends React.PureComponent { return period; }; + get comparisonSeriesName() { + return capitalize( + COMPARISON_DELTA_OPTIONS.find(({value}) => value === this.props.comparisonDelta) + ?.label || '' + ); + } + getComparisonMarkLines( timeseriesData: Series[] = [], comparisonTimeseriesData: Series[] = [] @@ -362,6 +371,7 @@ class TriggersChart extends React.PureComponent { timeseriesData: Series[] = [], isLoading: boolean, isReloading: boolean, + comparisonData?: Series[], comparisonMarkLines?: LineChartSeries[], minutesThresholdToDisplaySeconds?: number ) { @@ -389,6 +399,8 @@ class TriggersChart extends React.PureComponent { minValue={minBy(timeseriesData[0]?.data, ({value}) => value)?.value} maxValue={maxBy(timeseriesData[0]?.data, ({value}) => value)?.value} data={timeseriesData} + comparisonData={comparisonData ?? []} + comparisonSeriesName={this.comparisonSeriesName} comparisonMarkLines={comparisonMarkLines ?? []} hideThresholdLines={comparisonType === AlertRuleComparisonType.CHANGE} triggers={triggers} @@ -475,6 +487,7 @@ class TriggersChart extends React.PureComponent { loading, reloading, undefined, + undefined, MINUTES_THRESHOLD_TO_DISPLAY_SECONDS ); }} @@ -490,7 +503,7 @@ class TriggersChart extends React.PureComponent { environment={environment ? [environment] : undefined} project={projects.map(({id}) => Number(id))} interval={`${timeWindow}m`} - comparisonDelta={comparisonDelta} + comparisonDelta={comparisonDelta && comparisonDelta * 60} period={period} yAxis={aggregate} includePrevious={false} @@ -544,6 +557,7 @@ class TriggersChart extends React.PureComponent { timeseriesData, loading, reloading, + comparisonTimeseriesData, comparisonMarkLines ); }} diff --git a/static/app/views/alerts/incidentRules/triggers/chart/thresholdsChart.tsx b/static/app/views/alerts/incidentRules/triggers/chart/thresholdsChart.tsx index 7676ef48a5b1db..bb284eb5f22504 100644 --- a/static/app/views/alerts/incidentRules/triggers/chart/thresholdsChart.tsx +++ b/static/app/views/alerts/incidentRules/triggers/chart/thresholdsChart.tsx @@ -5,6 +5,8 @@ import flatten from 'lodash/flatten'; import AreaChart, {AreaChartSeries} from 'app/components/charts/areaChart'; import Graphic from 'app/components/charts/components/graphic'; +import {LineChartSeries} from 'app/components/charts/lineChart'; +import LineSeries from 'app/components/charts/series/lineSeries'; import space from 'app/styles/space'; import {GlobalSelection} from 'app/types'; import {ReactEchartsRef, Series} from 'app/types/echarts'; @@ -21,7 +23,8 @@ import {AlertRuleThresholdType, IncidentRule, Trigger} from '../../types'; type DefaultProps = { data: Series[]; - comparisonMarkLines: AreaChartSeries[]; + comparisonData: Series[]; + comparisonMarkLines: LineChartSeries[]; }; type Props = DefaultProps & { @@ -33,6 +36,7 @@ type Props = DefaultProps & { minutesThresholdToDisplaySeconds?: number; maxValue?: number; minValue?: number; + comparisonSeriesName?: string; } & Partial; type State = { @@ -63,6 +67,7 @@ const COLOR = { export default class ThresholdsChart extends PureComponent { static defaultProps: DefaultProps = { data: [], + comparisonData: [], comparisonMarkLines: [], }; @@ -81,6 +86,7 @@ export default class ThresholdsChart extends PureComponent { if ( this.props.triggers !== prevProps.triggers || this.props.data !== prevProps.data || + this.props.comparisonData !== prevProps.comparisonData || this.props.comparisonMarkLines !== prevProps.comparisonMarkLines ) { this.handleUpdateChartAxis(); @@ -295,6 +301,8 @@ export default class ThresholdsChart extends PureComponent { triggers, period, aggregate, + comparisonData, + comparisonSeriesName, comparisonMarkLines, minutesThresholdToDisplaySeconds, } = this.props; @@ -306,6 +314,13 @@ export default class ThresholdsChart extends PureComponent { }) ); + const comparisonDataWithoutRecentBucket = comparisonData?.map( + ({data: eventData, ...restOfData}) => ({ + ...restOfData, + data: eventData.slice(0, -1), + }) + ); + // Disable all lines by default but the 1st one const selected: Record = dataWithoutRecentBucket.reduce( (acc, {seriesName}, index) => { @@ -323,8 +338,17 @@ export default class ThresholdsChart extends PureComponent { const chartOptions = { tooltip: { - valueFormatter: (value: number, seriesName?: string) => - alertTooltipValueFormatter(value, seriesName ?? '', aggregate), + // use the main aggregate for all series (main, min, max, avg, comparison) + // to format all values similarly + valueFormatter: (value: number) => + alertTooltipValueFormatter(value, aggregate, aggregate), + + markerFormatter: (marker: string, seriesName?: string) => { + if (seriesName === comparisonSeriesName) { + return ''; + } + return marker; + }, }, yAxis: { min: this.state.yAxisMin ?? undefined, @@ -355,6 +379,19 @@ export default class ThresholdsChart extends PureComponent { ), })} series={[...dataWithoutRecentBucket, ...comparisonMarkLines]} + additionalSeries={[ + ...comparisonDataWithoutRecentBucket.map(({data: _data, ...otherSeriesProps}) => + LineSeries({ + name: comparisonSeriesName, + data: _data.map(({name, value}) => [name, value]), + lineStyle: {color: theme.gray200, type: 'dashed', width: 1}, + animation: false, + animationThreshold: 1, + animationDuration: 0, + ...otherSeriesProps, + }) + ), + ]} onFinished={() => { // We want to do this whenever the chart finishes re-rendering so that we can update the dimensions of // any graphics related to the triggers (e.g. the threshold areas + boundaries)