Skip to content

Commit e5ccbff

Browse files
authored
feat: show registered anomalies as dashed segments on download charts (#1744)
1 parent 8199c1f commit e5ccbff

File tree

3 files changed

+63
-16
lines changed

3 files changed

+63
-16
lines changed

app/components/Package/TrendsChart.vue

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ function formatXyDataset(
219219
color: accent.value,
220220
temperatureColors,
221221
useArea: true,
222+
dashIndices: dataset
223+
.map((item, index) => (item.hasAnomaly ? index : -1))
224+
.filter(index => index !== -1),
222225
}
223226
224227
if (selectedGranularity === 'weekly' && isWeeklyDataset(dataset)) {
@@ -275,19 +278,26 @@ function formatXyDataset(
275278
function extractSeriesPoints(
276279
selectedGranularity: ChartTimeGranularity,
277280
dataset: EvolutionData,
278-
): Array<{ timestamp: number; value: number }> {
281+
): Array<{ timestamp: number; value: number; hasAnomaly: boolean }> {
279282
if (selectedGranularity === 'weekly' && isWeeklyDataset(dataset)) {
280-
return dataset.map(d => ({ timestamp: d.timestampEnd, value: d.value }))
283+
return dataset.map(d => ({
284+
timestamp: d.timestampEnd,
285+
value: d.value,
286+
hasAnomaly: !!d.hasAnomaly,
287+
}))
281288
}
282289
if (
283290
(selectedGranularity === 'daily' && isDailyDataset(dataset)) ||
284291
(selectedGranularity === 'monthly' && isMonthlyDataset(dataset)) ||
285292
(selectedGranularity === 'yearly' && isYearlyDataset(dataset))
286293
) {
287-
return (dataset as Array<{ timestamp: number; value: number }>).map(d => ({
288-
timestamp: d.timestamp,
289-
value: d.value,
290-
}))
294+
return (dataset as Array<{ timestamp: number; value: number; hasAnomaly?: boolean }>).map(
295+
d => ({
296+
timestamp: d.timestamp,
297+
value: d.value,
298+
hasAnomaly: !!d.hasAnomaly,
299+
}),
300+
)
291301
}
292302
return []
293303
}
@@ -380,6 +390,11 @@ const isEstimationGranularity = computed(
380390
const supportsEstimation = computed(
381391
() => isEstimationGranularity.value && selectedMetric.value !== 'contributors',
382392
)
393+
394+
const hasDownloadAnomalies = computed(() =>
395+
normalisedDataset.value?.some(datapoint => !!datapoint.dashIndices.length),
396+
)
397+
383398
const shouldRenderEstimationOverlay = computed(() => !pending.value && supportsEstimation.value)
384399
385400
const startDate = usePermalink<string>('start', '', {
@@ -955,11 +970,13 @@ const effectiveDataSingle = computed<EvolutionData>(() => {
955970
granularity: displayedGranularity.value,
956971
})
957972
}
973+
958974
return applyDataCorrection(
959975
data as Array<{ value: number }>,
960976
settings.value.chartFilter,
961977
) as EvolutionData
962978
}
979+
963980
return data
964981
})
965982
@@ -991,7 +1008,10 @@ const chartData = computed<{
9911008
const granularity = displayedGranularity.value
9921009
9931010
const timestampSet = new Set<number>()
994-
const pointsByPackage = new Map<string, Array<{ timestamp: number; value: number }>>()
1011+
const pointsByPackage = new Map<
1012+
string,
1013+
Array<{ timestamp: number; value: number; hasAnomaly?: boolean }>
1014+
>()
9951015
9961016
for (const pkg of names) {
9971017
let data = state.evolutionsByPackage[pkg] ?? []
@@ -1005,6 +1025,7 @@ const chartData = computed<{
10051025
) as EvolutionData
10061026
}
10071027
const points = extractSeriesPoints(granularity, data)
1028+
10081029
pointsByPackage.set(pkg, points)
10091030
for (const p of points) timestampSet.add(p.timestamp)
10101031
}
@@ -1014,15 +1035,23 @@ const chartData = computed<{
10141035
10151036
const dataset: VueUiXyDatasetItem[] = names.map(pkg => {
10161037
const points = pointsByPackage.get(pkg) ?? []
1017-
const map = new Map<number, number>()
1018-
for (const p of points) map.set(p.timestamp, p.value)
1038+
const valueByTimestamp = new Map<number, number>()
1039+
const anomalyTimestamps = new Set<number>()
1040+
for (const p of points) {
1041+
valueByTimestamp.set(p.timestamp, p.value)
1042+
if (p.hasAnomaly) anomalyTimestamps.add(p.timestamp)
1043+
}
10191044
1020-
const series = dates.map(t => map.get(t) ?? 0)
1045+
const series = dates.map(t => valueByTimestamp.get(t) ?? 0)
1046+
const dashIndices = dates
1047+
.map((t, index) => (anomalyTimestamps.has(t) ? index : -1))
1048+
.filter(index => index !== -1)
10211049
10221050
const item: VueUiXyDatasetItem = {
10231051
name: pkg,
10241052
type: 'line',
10251053
series,
1054+
dashIndices,
10261055
} as VueUiXyDatasetItem
10271056
10281057
if (isListedFramework(pkg)) {
@@ -1045,6 +1074,7 @@ const normalisedDataset = computed(() => {
10451074
return {
10461075
...d,
10471076
series: [...d.series.slice(0, -1), projectedLastValue],
1077+
dashIndices: d.dashIndices ?? [],
10481078
}
10491079
})
10501080
})
@@ -1408,7 +1438,10 @@ function drawSvgPrintLegend(svg: Record<string, any>) {
14081438
})
14091439
14101440
// Inject the estimation legend item when necessary
1411-
if (supportsEstimation.value && !isEndDateOnPeriodEnd.value && !isZoomed.value) {
1441+
if (
1442+
(supportsEstimation.value && !isEndDateOnPeriodEnd.value && !isZoomed.value) ||
1443+
hasDownloadAnomalies.value
1444+
) {
14121445
seriesNames.push(`
14131446
<line
14141447
x1="${svg.drawingArea.left + 12}"
@@ -1955,7 +1988,10 @@ watch(selectedMetric, value => {
19551988
</template>
19561989

19571990
<!-- Estimation extra legend item -->
1958-
<div class="flex gap-1 place-items-center" v-if="supportsEstimation">
1991+
<div
1992+
class="flex gap-1 place-items-center"
1993+
v-if="supportsEstimation || hasDownloadAnomalies"
1994+
>
19591995
<svg viewBox="0 0 20 2" width="20">
19601996
<line
19611997
x1="0"

app/types/chart.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,28 @@ export type DateRangeFields = {
55
endDate?: string
66
}
77

8-
export type DailyDataPoint = { value: number; day: string; timestamp: number }
8+
export type DailyDataPoint = { value: number; day: string; timestamp: number; hasAnomaly?: boolean }
99
export type WeeklyDataPoint = {
1010
value: number
1111
weekKey: string
1212
weekStart: string
1313
weekEnd: string
1414
timestampStart: number
1515
timestampEnd: number
16+
hasAnomaly?: boolean
17+
}
18+
export type MonthlyDataPoint = {
19+
value: number
20+
month: string
21+
timestamp: number
22+
hasAnomaly?: boolean
23+
}
24+
export type YearlyDataPoint = {
25+
value: number
26+
year: string
27+
timestamp: number
28+
hasAnomaly?: boolean
1629
}
17-
export type MonthlyDataPoint = { value: number; month: string; timestamp: number }
18-
export type YearlyDataPoint = { value: number; year: string; timestamp: number }
1930

2031
export type EvolutionData =
2132
| DailyDataPoint[]

app/utils/download-anomalies.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ export function applyBlocklistCorrection(opts: {
113113
for (let i = 0; i < count; i++) {
114114
const t = (i + 1) / (count + 1)
115115
result[affectedIndices[i]!]!.value = Math.round(startVal + t * (endVal - startVal))
116+
result[affectedIndices[i]!]!.hasAnomaly = true
116117
}
117118
}
118-
119119
return result as EvolutionData
120120
}

0 commit comments

Comments
 (0)