Skip to content
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
})
Comment thread
graphieros marked this conversation as resolved.

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)
}
})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can probably simplify timeout logic with useTimeoutFn() (ref. https://vueuse.org/shared/useTimeoutFn/). Calling start() resets the timer countdown from the beginning (playground), so this should provide the same behavior (not tested yet).

Suggested change
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)
}
})
const isResizing = shallowRef(false)
const chartHeight = computed(() => {
if (isMobile.value) {
return 950
}
return showCorrectionControls.value && props.inModal ? 494 : 600
})
const { start } = useTimeoutFn(() => {
isResizing.value = false
}, 200, { immediate: false })
function pauseChartTransitions() {
isResizing.value = true
start()
}


watch(
chartHeight,
(newH, oldH) => {
if (newH !== oldH) {
// Avoids triggering chart line transitions when the chart is resized
pauseChartTransitions()
}
},
{ immediate: true },
)
Comment thread
graphieros marked this conversation as resolved.

// 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 @@ -1666,116 +1707,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'
Comment on lines +1715 to +1717
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since v-if="showCorrectionControls" was removed, input elements seems to be still focusable by keyboard when collapsed. This adds three invisible focuses and pops up tooltips when you navigates by typing Tab 😅

screenshot of collapsed controls but tooptip is shown for hidden element

If we need h-0, I think we could add disabled to avoid focusing on them.

"
>
<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))]"
/>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/>
:disabled="!showCorrectionControls"
/>

<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))]"
/>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/>
:disabled="!showCorrectionControls"
/>

</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))]"
/>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/>
:disabled="!showCorrectionControls"
/>

</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))]"
/>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/>
:disabled="!showCorrectionControls"
/>

{{ $t('package.trends.apply_correction') }}
</label>
</div>
</div>
</div>
</div>
</div>
Expand All @@ -1794,14 +1858,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 @@ -2098,6 +2171,11 @@ watch(selectedMetric, value => {
top: calc(100% - 2rem) !important;
}

.no-transition line,
.no-transition circle {
transition: none !important;
}

input::-webkit-date-and-time-value {
margin-inline: 4px;
}
Expand Down
Loading