Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 189 additions & 111 deletions app/components/Package/TrendsChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1345,12 +1345,54 @@ function drawSvgPrintLegend(svg: Record<string, any>) {
return seriesNames.join('\n')
}

const showCorrectionControls = shallowRef(false)
const isResizing = shallowRef(false)

const chartHeight = computed(() => {
if (isMobile.value) {
return 950
}
return showCorrectionControls.value && props.inModal ? 494 : 600
})

const timeoutId = shallowRef<ReturnType<typeof setTimeout> | null>(null)

function pauseChartTransitions() {
if (timeoutId.value) {
clearTimeout(timeoutId.value)
}

isResizing.value = true

timeoutId.value = setTimeout(() => {
isResizing.value = false
timeoutId.value = null
}, 200)
}

onBeforeUnmount(() => {
if (timeoutId.value) {
clearTimeout(timeoutId.value)
}
})

watch(
chartHeight,
(newH, oldH) => {
if (newH !== oldH) {
// Avoids triggering chart line transitions when the chart is resized
pauseChartTransitions()
}
},
{ immediate: true },
)

// VueUiXy chart component configuration
const chartConfig = computed<VueUiXyConfig>(() => {
return {
theme: isDarkMode.value ? 'dark' : ('' as VueDataUiTheme),
chart: {
height: isMobile.value ? 950 : 600,
height: chartHeight.value,
backgroundColor: colors.value.bg,
padding: { bottom: displayedGranularity.value === 'yearly' ? 84 : 64, right: 128 }, // padding right is set to leave space of last datapoint label(s)
userOptions: {
Expand Down Expand Up @@ -1554,7 +1596,6 @@ const chartConfig = computed<VueUiXyConfig>(() => {
})

const isDownloadsMetric = computed(() => selectedMetric.value === 'downloads')
const showCorrectionControls = shallowRef(false)

const packageAnomalies = computed(() => getAnomaliesForPackages(effectivePackageNames.value))
const hasAnomalies = computed(() => packageAnomalies.value.length > 0)
Expand Down Expand Up @@ -1674,116 +1715,139 @@ watch(selectedMetric, value => {
/>
{{ $t('package.trends.data_correction') }}
</button>
<div v-if="showCorrectionControls" class="grid grid-cols-2 sm:flex items-end gap-3">
<label class="flex flex-col gap-1 flex-1">
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
{{ $t('package.trends.average_window') }}
<span class="text-fg-muted">({{ settings.chartFilter.averageWindow }})</span>
</span>
<input
v-model.number="settings.chartFilter.averageWindow"
type="range"
min="0"
max="20"
step="1"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
</label>
<label class="flex flex-col gap-1 flex-1">
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
{{ $t('package.trends.smoothing') }}
<span class="text-fg-muted">({{ settings.chartFilter.smoothingTau }})</span>
</span>
<input
v-model.number="settings.chartFilter.smoothingTau"
type="range"
min="0"
max="20"
step="1"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
</label>
<label class="flex flex-col gap-1 flex-1">
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
{{ $t('package.trends.prediction') }}
<span class="text-fg-muted">({{ settings.chartFilter.predictionPoints }})</span>
</span>
<input
v-model.number="settings.chartFilter.predictionPoints"
type="range"
min="0"
max="30"
step="1"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
</label>
<div class="flex flex-col gap-1 shrink-0">
<span
class="text-2xs font-mono text-fg-subtle tracking-wide uppercase flex items-center justify-between"
>
{{ $t('package.trends.known_anomalies') }}
<TooltipApp interactive :to="inModal ? '#chart-modal' : undefined">
<button
type="button"
class="i-lucide:info w-3.5 h-3.5 text-fg-muted cursor-help"
:aria-label="$t('package.trends.known_anomalies')"

<div
class="overflow-hidden transition-[opacity] duration-200 ease-out"
:class="
showCorrectionControls
? 'max-h-[220px] opacity-100'
: 'max-h-0 opacity-0 pointer-events-none'
"
>
<div class="pt-1 min-h-[160px] sm:min-h-[76px]">
<div class="grid grid-cols-2 sm:flex items-end gap-3">
<label class="flex flex-col gap-1 flex-1">
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
{{ $t('package.trends.average_window') }}
<span class="text-fg-muted">({{ settings.chartFilter.averageWindow }})</span>
</span>
<input
v-model.number="settings.chartFilter.averageWindow"
type="range"
min="0"
max="20"
step="1"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
<template #content>
<div class="flex flex-col gap-3">
<p class="text-xs text-fg-muted">
{{ $t('package.trends.known_anomalies_description') }}
</p>
<div v-if="hasAnomalies">
<p class="text-xs text-fg-subtle font-medium">
{{ $t('package.trends.known_anomalies_ranges') }}
</p>
<ul class="text-xs text-fg-subtle list-disc list-inside">
<li v-for="a in packageAnomalies" :key="`${a.packageName}-${a.start}`">
</label>

<label class="flex flex-col gap-1 flex-1">
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
{{ $t('package.trends.smoothing') }}
<span class="text-fg-muted">({{ settings.chartFilter.smoothingTau }})</span>
</span>
<input
v-model.number="settings.chartFilter.smoothingTau"
type="range"
min="0"
max="20"
step="1"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
</label>

<label class="flex flex-col gap-1 flex-1">
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
{{ $t('package.trends.prediction') }}
<span class="text-fg-muted">({{ settings.chartFilter.predictionPoints }})</span>
</span>
<input
v-model.number="settings.chartFilter.predictionPoints"
type="range"
min="0"
max="30"
step="1"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
</label>

<div class="flex flex-col gap-1 shrink-0">
<span
class="text-2xs font-mono text-fg-subtle tracking-wide uppercase flex items-center justify-between"
>
{{ $t('package.trends.known_anomalies') }}
<TooltipApp interactive :to="inModal ? '#chart-modal' : undefined">
<button
type="button"
class="i-lucide:info w-3.5 h-3.5 text-fg-muted cursor-help"
:aria-label="$t('package.trends.known_anomalies')"
/>
<template #content>
<div class="flex flex-col gap-3">
<p class="text-xs text-fg-muted">
{{ $t('package.trends.known_anomalies_description') }}
</p>

<div v-if="hasAnomalies">
<p class="text-xs text-fg-subtle font-medium">
{{ $t('package.trends.known_anomalies_ranges') }}
</p>
<ul class="text-xs text-fg-subtle list-disc list-inside">
<li v-for="a in packageAnomalies" :key="`${a.packageName}-${a.start}`">
{{
isMultiPackageMode
? $t('package.trends.known_anomalies_range_named', {
packageName: a.packageName,
start: formatAnomalyDate(a.start),
end: formatAnomalyDate(a.end),
})
: $t('package.trends.known_anomalies_range', {
start: formatAnomalyDate(a.start),
end: formatAnomalyDate(a.end),
})
}}
</li>
</ul>
</div>

<p v-else class="text-xs text-fg-muted">
{{
isMultiPackageMode
? $t('package.trends.known_anomalies_range_named', {
packageName: a.packageName,
start: formatAnomalyDate(a.start),
end: formatAnomalyDate(a.end),
})
: $t('package.trends.known_anomalies_range', {
start: formatAnomalyDate(a.start),
end: formatAnomalyDate(a.end),
})
$t('package.trends.known_anomalies_none', effectivePackageNames.length)
}}
</li>
</ul>
</div>
<p v-else class="text-xs text-fg-muted">
{{ $t('package.trends.known_anomalies_none', effectivePackageNames.length) }}
</p>
<div class="flex justify-end">
<LinkBase
to="https://github.com/npmx-dev/npmx.dev/edit/main/app/utils/download-anomalies.data.ts"
class="text-xs text-accent"
>
{{ $t('package.trends.known_anomalies_contribute') }}
</LinkBase>
</div>
</div>
</template>
</TooltipApp>
</span>
<label
class="flex items-center gap-1.5 text-2xs font-mono text-fg-subtle cursor-pointer h-4"
:class="{ 'opacity-50 pointer-events-none': !hasAnomalies }"
>
<input
:checked="settings.chartFilter.anomaliesFixed && hasAnomalies"
@change="
settings.chartFilter.anomaliesFixed = ($event.target as HTMLInputElement).checked
"
type="checkbox"
:disabled="!hasAnomalies"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
{{ $t('package.trends.apply_correction') }}
</label>
</p>

<div class="flex justify-end">
<LinkBase
to="https://github.com/npmx-dev/npmx.dev/edit/main/app/utils/download-anomalies.data.ts"
class="text-xs text-accent"
>
{{ $t('package.trends.known_anomalies_contribute') }}
</LinkBase>
</div>
</div>
</template>
</TooltipApp>
</span>

<label
class="flex items-center gap-1.5 text-2xs font-mono text-fg-subtle cursor-pointer h-4"
:class="{ 'opacity-50 pointer-events-none': !hasAnomalies }"
>
<input
:checked="settings.chartFilter.anomaliesFixed && hasAnomalies"
@change="
settings.chartFilter.anomaliesFixed = (
$event.target as HTMLInputElement
).checked
"
type="checkbox"
:disabled="!hasAnomalies"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
{{ $t('package.trends.apply_correction') }}
</label>
</div>
</div>
</div>
</div>
</div>
Expand All @@ -1802,14 +1866,23 @@ watch(selectedMetric, value => {
<div
role="region"
aria-labelledby="trends-chart-title"
:class="isMobile === false && width > 0 ? 'min-h-[567px]' : 'min-h-[260px]'"
:class="
isMobile === false && width > 0
? showCorrectionControls
? 'h-[491px]'
: 'h-[567px]'
: 'min-h-[260px]'
"
>
<ClientOnly v-if="chartData.dataset">
<div :data-pending="pending" :data-minimap-visible="maxDatapoints > 6">
<VueUiXy
:dataset="normalisedDataset"
:config="chartConfig"
class="[direction:ltr]"
:class="{
'[direction:ltr]': true,
'no-transition': isResizing,
}"
@zoomStart="setIsZoom"
@zoomEnd="setIsZoom"
@zoomReset="isZoomed = false"
Expand Down Expand Up @@ -2105,4 +2178,9 @@ watch(selectedMetric, value => {
[data-minimap-visible='false'] .vue-data-ui-watermark {
top: calc(100% - 2rem) !important;
}

.no-transition line,
.no-transition circle {
transition: none !important;
}
</style>
Loading