Skip to content
297 changes: 186 additions & 111 deletions app/components/Package/TrendsChart.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { Theme as VueDataUiTheme, VueUiXyConfig, VueUiXyDatasetItem } from 'vue-data-ui'
import { VueUiXy } from 'vue-data-ui/vue-ui-xy'
import { useDebounceFn, useElementSize } from '@vueuse/core'
import { useDebounceFn, useElementSize, useTimeoutFn } from '@vueuse/core'
import { useCssVariables } from '~/composables/useColors'
import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch, lightenOklch } from '~/utils/colors'
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
Expand Down Expand Up @@ -1350,12 +1350,46 @@ 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 { 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 },
)

// 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 @@ -1559,7 +1593,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 @@ -1648,6 +1681,8 @@ watch(selectedMetric, value => {

<button
v-if="showResetButton"
:aria-expanded="showCorrectionControls"
aria-controls="trends-correction-controls"
type="button"
aria-label="Reset date range"
class="self-end flex items-center justify-center px-2.5 py-2.25 border border-transparent rounded-md text-fg-subtle hover:text-fg transition-colors hover:border-border focus-visible:outline-accent/70 sm:mb-0"
Expand All @@ -1671,115 +1706,141 @@ 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"
id="trends-correction-controls"
:aria-hidden="!showCorrectionControls"
:inert="!showCorrectionControls"
:class="
showCorrectionControls
? 'max-h-[220px] opacity-100'
: 'max-h-0 opacity-0 pointer-events-none'
Comment on lines +1715 to +1717
Copy link
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"
:disabled="!showCorrectionControls"
type="range"
min="0"
max="20"
step="1"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
Copy link
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"
:disabled="!showCorrectionControls"
type="range"
min="0"
max="20"
step="1"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
Copy link
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"
:disabled="!showCorrectionControls"
type="range"
min="0"
max="30"
step="1"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
Copy link
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"
v-if="showCorrectionControls"
>
<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': !hasAnomalies }"
>
<input
:checked="settings.chartFilter.anomaliesFixed"
@change="
settings.chartFilter.anomaliesFixed = ($event.target as HTMLInputElement).checked
"
type="checkbox"
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': !hasAnomalies }"
>
<input
:checked="settings.chartFilter.anomaliesFixed"
:disabled="!showCorrectionControls"
@change="
settings.chartFilter.anomaliesFixed = (
$event.target as HTMLInputElement
).checked
"
type="checkbox"
class="accent-[var(--accent-color,var(--fg-subtle))]"
/>
Copy link
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 @@ -1798,14 +1859,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 @@ -2102,6 +2172,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