Skip to content

Commit 76c09c0

Browse files
Abdullah KhanAbdullah Khan
authored andcommitted
feat(explore-suspect-attrs): Iterating on series data, sorting or data and tooltip
1 parent 8f27719 commit 76c09c0

File tree

1 file changed

+140
-6
lines changed
  • static/app/views/explore/components/suspectTags

1 file changed

+140
-6
lines changed

static/app/views/explore/components/suspectTags/charts.tsx

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import {useLayoutEffect, useRef, useState} from 'react';
1+
import {useCallback, useLayoutEffect, useMemo, useRef, useState} from 'react';
22
import type {Theme} from '@emotion/react';
33
import {useTheme} from '@emotion/react';
44
import styled from '@emotion/styled';
5+
import type {TooltipComponentFormatterCallbackParams} from 'echarts';
6+
import type {CallbackDataParams} from 'echarts/types/dist/shared';
57

68
import BaseChart from 'sentry/components/charts/baseChart';
79
import {space} from 'sentry/styles/space';
@@ -29,6 +31,75 @@ export function Charts({rankedAttributes}: Props) {
2931
);
3032
}
3133

34+
type CohortData = SuspectAttributesResult['rankedAttributes'][number]['cohort1'];
35+
36+
function cohortsToSeriesData(
37+
cohort1: CohortData,
38+
cohort2: CohortData
39+
): {
40+
[BASELINE_SERIES_NAME]: Array<{label: string; value: string}>;
41+
[SELECTED_SERIES_NAME]: Array<{label: string; value: string}>;
42+
} {
43+
const cohort1Map = new Map(cohort1.map(({label, value}) => [label, value]));
44+
const cohort2Map = new Map(cohort2.map(({label, value}) => [label, value]));
45+
46+
const uniqueLabels = new Set([
47+
...cohort1.map(c => c.label),
48+
...cohort2.map(c => c.label),
49+
]);
50+
51+
// From the unique labels, we create two series data objects, one for the selected cohort and one for the baseline cohort.
52+
// If a label isn't present in either of the cohorts, we assign a value of 0, to that label in the respective series.
53+
const seriesData = Array.from(uniqueLabels).map(label => {
54+
const selectedVal = cohort1Map.get(label) ?? '0';
55+
const baselineVal = cohort2Map.get(label) ?? '0';
56+
57+
// We sort by descending value of the selected cohort
58+
const sortVal = Number(selectedVal);
59+
60+
return {
61+
label,
62+
selectedValue: selectedVal,
63+
baselineValue: baselineVal,
64+
sortValue: sortVal,
65+
};
66+
});
67+
68+
seriesData.sort((a, b) => b.sortValue - a.sortValue);
69+
70+
const selectedSeriesData = seriesData.map(({label, selectedValue}) => ({
71+
label,
72+
value: selectedValue,
73+
}));
74+
75+
const baselineSeriesData = seriesData.map(({label, baselineValue}) => ({
76+
label,
77+
value: baselineValue,
78+
}));
79+
80+
return {
81+
[SELECTED_SERIES_NAME]: selectedSeriesData,
82+
[BASELINE_SERIES_NAME]: baselineSeriesData,
83+
};
84+
}
85+
86+
// TODO Abdullah Khan: This is a temporary function to get the totals of the cohorts. Will be removed
87+
// once the backend returns the totals.
88+
function cohortTotals(
89+
cohort1: CohortData,
90+
cohort2: CohortData
91+
): {
92+
[BASELINE_SERIES_NAME]: number;
93+
[SELECTED_SERIES_NAME]: number;
94+
} {
95+
const cohort1Total = cohort1.reduce((acc, curr) => acc + Number(curr.value), 0);
96+
const cohort2Total = cohort2.reduce((acc, curr) => acc + Number(curr.value), 0);
97+
return {
98+
[SELECTED_SERIES_NAME]: cohort1Total,
99+
[BASELINE_SERIES_NAME]: cohort2Total,
100+
};
101+
}
102+
32103
function Chart({
33104
attribute,
34105
theme,
@@ -42,6 +113,68 @@ function Chart({
42113
const cohort1Color = theme.chart.getColorPalette(0)?.[0];
43114
const cohort2Color = '#dddddd';
44115

116+
const seriesData = useMemo(
117+
() => cohortsToSeriesData(attribute.cohort1, attribute.cohort2),
118+
[attribute.cohort1, attribute.cohort2]
119+
);
120+
121+
const seriesTotals = useMemo(
122+
() => cohortTotals(attribute.cohort1, attribute.cohort2),
123+
[attribute.cohort1, attribute.cohort2]
124+
);
125+
126+
const valueFormatter = useCallback(
127+
(_value: number, label?: string, seriesParams?: CallbackDataParams) => {
128+
const data = Number(seriesParams?.data);
129+
const total = seriesTotals[label as keyof typeof seriesTotals];
130+
const percentage = (data / total) * 100;
131+
return `${percentage.toFixed(1)}%`;
132+
},
133+
[seriesTotals]
134+
);
135+
136+
const formatAxisLabel = useCallback(
137+
(
138+
_value: number,
139+
_isTimestamp: boolean,
140+
_utc: boolean,
141+
_showTimeInTooltip: boolean,
142+
_addSecondsToTimeFormat: boolean,
143+
_bucketSize: number | undefined,
144+
seriesParamsOrParam: TooltipComponentFormatterCallbackParams
145+
) => {
146+
if (!Array.isArray(seriesParamsOrParam)) {
147+
throw new Error('seriesParamsOrParam is not an array in formatAxisLabel');
148+
}
149+
150+
const selectedParam = seriesParamsOrParam[0];
151+
const baselineParam = seriesParamsOrParam[1];
152+
153+
if (!selectedParam || !baselineParam) {
154+
throw new Error('selectedParam or baselineParam is not defined');
155+
}
156+
157+
const selectedTotal =
158+
seriesTotals[selectedParam?.seriesName as keyof typeof seriesTotals];
159+
const selectedData = Number(selectedParam?.data);
160+
const selectedPercentage = (selectedData / selectedTotal) * 100;
161+
162+
const baselineTotal =
163+
seriesTotals[baselineParam?.seriesName as keyof typeof seriesTotals];
164+
const baselineData = Number(baselineParam?.data);
165+
const baselinePercentage = (baselineData / baselineTotal) * 100;
166+
167+
const isDifferent = selectedPercentage.toFixed(1) !== baselinePercentage.toFixed(1);
168+
169+
const status = isDifferent
170+
? {adjective: 'different', message: 'This is suspicious.'}
171+
: {adjective: 'similar', message: 'Nothing unusual here.'};
172+
173+
return `<div style="max-width: 200px; white-space: normal; word-wrap: break-word; line-height: 1.2;">${selectedParam?.name} <span style="color: ${theme.textColor};">is <strong>${status.adjective}</strong> ${isDifferent ? 'between' : 'across'} selected and baseline data. ${status.message}</span></div>`;
174+
},
175+
[seriesTotals, theme.textColor]
176+
);
177+
45178
useLayoutEffect(() => {
46179
const chartContainer = chartRef.current?.getEchartsInstance().getDom();
47180
if (!chartContainer) return;
@@ -69,8 +202,9 @@ function Chart({
69202
autoHeightResize
70203
isGroupedByDate={false}
71204
tooltip={{
72-
trigger: 'axis',
73-
confine: true,
205+
renderMode: 'html',
206+
valueFormatter,
207+
formatAxisLabel,
74208
}}
75209
grid={{
76210
left: 2,
@@ -80,7 +214,7 @@ function Chart({
80214
xAxis={{
81215
show: true,
82216
type: 'category',
83-
data: attribute.cohort1.map(cohort => cohort.label),
217+
data: seriesData[SELECTED_SERIES_NAME].map(cohort => cohort.label),
84218
truncate: 14,
85219
axisLabel: hideLabels
86220
? {show: false}
@@ -103,7 +237,7 @@ function Chart({
103237
series={[
104238
{
105239
type: 'bar',
106-
data: attribute.cohort1.map(cohort => cohort.value),
240+
data: seriesData[SELECTED_SERIES_NAME].map(cohort => cohort.value),
107241
name: SELECTED_SERIES_NAME,
108242
itemStyle: {
109243
color: cohort1Color,
@@ -113,7 +247,7 @@ function Chart({
113247
},
114248
{
115249
type: 'bar',
116-
data: attribute.cohort2.map(cohort => cohort.value),
250+
data: seriesData[BASELINE_SERIES_NAME].map(cohort => cohort.value),
117251
name: BASELINE_SERIES_NAME,
118252
itemStyle: {
119253
color: cohort2Color,

0 commit comments

Comments
 (0)