Skip to content

Commit 6e5af51

Browse files
committed
fix: handle CLS issues in chart modal
1 parent 3712560 commit 6e5af51

1 file changed

Lines changed: 189 additions & 111 deletions

File tree

app/components/Package/TrendsChart.vue

Lines changed: 189 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,12 +1345,54 @@ function drawSvgPrintLegend(svg: Record<string, any>) {
13451345
return seriesNames.join('\n')
13461346
}
13471347
1348+
const showCorrectionControls = shallowRef(false)
1349+
const isResizing = shallowRef(false)
1350+
1351+
const chartHeight = computed(() => {
1352+
if (isMobile.value) {
1353+
return 950
1354+
}
1355+
return showCorrectionControls.value && props.inModal ? 494 : 600
1356+
})
1357+
1358+
const timeoutId = shallowRef<ReturnType<typeof setTimeout> | null>(null)
1359+
1360+
function pauseChartTransitions() {
1361+
if (timeoutId.value) {
1362+
clearTimeout(timeoutId.value)
1363+
}
1364+
1365+
isResizing.value = true
1366+
1367+
timeoutId.value = setTimeout(() => {
1368+
isResizing.value = false
1369+
timeoutId.value = null
1370+
}, 200)
1371+
}
1372+
1373+
onBeforeUnmount(() => {
1374+
if (timeoutId.value) {
1375+
clearTimeout(timeoutId.value)
1376+
}
1377+
})
1378+
1379+
watch(
1380+
chartHeight,
1381+
(newH, oldH) => {
1382+
if (newH !== oldH) {
1383+
// Avoids triggering chart line transitions when the chart is resized
1384+
pauseChartTransitions()
1385+
}
1386+
},
1387+
{ immediate: true },
1388+
)
1389+
13481390
// VueUiXy chart component configuration
13491391
const chartConfig = computed<VueUiXyConfig>(() => {
13501392
return {
13511393
theme: isDarkMode.value ? 'dark' : ('' as VueDataUiTheme),
13521394
chart: {
1353-
height: isMobile.value ? 950 : 600,
1395+
height: chartHeight.value,
13541396
backgroundColor: colors.value.bg,
13551397
padding: { bottom: displayedGranularity.value === 'yearly' ? 84 : 64, right: 128 }, // padding right is set to leave space of last datapoint label(s)
13561398
userOptions: {
@@ -1554,7 +1596,6 @@ const chartConfig = computed<VueUiXyConfig>(() => {
15541596
})
15551597
15561598
const isDownloadsMetric = computed(() => selectedMetric.value === 'downloads')
1557-
const showCorrectionControls = shallowRef(false)
15581599
15591600
const packageAnomalies = computed(() => getAnomaliesForPackages(effectivePackageNames.value))
15601601
const hasAnomalies = computed(() => packageAnomalies.value.length > 0)
@@ -1674,116 +1715,139 @@ watch(selectedMetric, value => {
16741715
/>
16751716
{{ $t('package.trends.data_correction') }}
16761717
</button>
1677-
<div v-if="showCorrectionControls" class="grid grid-cols-2 sm:flex items-end gap-3">
1678-
<label class="flex flex-col gap-1 flex-1">
1679-
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1680-
{{ $t('package.trends.average_window') }}
1681-
<span class="text-fg-muted">({{ settings.chartFilter.averageWindow }})</span>
1682-
</span>
1683-
<input
1684-
v-model.number="settings.chartFilter.averageWindow"
1685-
type="range"
1686-
min="0"
1687-
max="20"
1688-
step="1"
1689-
class="accent-[var(--accent-color,var(--fg-subtle))]"
1690-
/>
1691-
</label>
1692-
<label class="flex flex-col gap-1 flex-1">
1693-
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1694-
{{ $t('package.trends.smoothing') }}
1695-
<span class="text-fg-muted">({{ settings.chartFilter.smoothingTau }})</span>
1696-
</span>
1697-
<input
1698-
v-model.number="settings.chartFilter.smoothingTau"
1699-
type="range"
1700-
min="0"
1701-
max="20"
1702-
step="1"
1703-
class="accent-[var(--accent-color,var(--fg-subtle))]"
1704-
/>
1705-
</label>
1706-
<label class="flex flex-col gap-1 flex-1">
1707-
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1708-
{{ $t('package.trends.prediction') }}
1709-
<span class="text-fg-muted">({{ settings.chartFilter.predictionPoints }})</span>
1710-
</span>
1711-
<input
1712-
v-model.number="settings.chartFilter.predictionPoints"
1713-
type="range"
1714-
min="0"
1715-
max="30"
1716-
step="1"
1717-
class="accent-[var(--accent-color,var(--fg-subtle))]"
1718-
/>
1719-
</label>
1720-
<div class="flex flex-col gap-1 shrink-0">
1721-
<span
1722-
class="text-2xs font-mono text-fg-subtle tracking-wide uppercase flex items-center justify-between"
1723-
>
1724-
{{ $t('package.trends.known_anomalies') }}
1725-
<TooltipApp interactive :to="inModal ? '#chart-modal' : undefined">
1726-
<button
1727-
type="button"
1728-
class="i-lucide:info w-3.5 h-3.5 text-fg-muted cursor-help"
1729-
:aria-label="$t('package.trends.known_anomalies')"
1718+
1719+
<div
1720+
class="overflow-hidden transition-[opacity] duration-200 ease-out"
1721+
:class="
1722+
showCorrectionControls
1723+
? 'max-h-[220px] opacity-100'
1724+
: 'max-h-0 opacity-0 pointer-events-none'
1725+
"
1726+
>
1727+
<div class="pt-1 min-h-[160px] sm:min-h-[76px]">
1728+
<div class="grid grid-cols-2 sm:flex items-end gap-3">
1729+
<label class="flex flex-col gap-1 flex-1">
1730+
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1731+
{{ $t('package.trends.average_window') }}
1732+
<span class="text-fg-muted">({{ settings.chartFilter.averageWindow }})</span>
1733+
</span>
1734+
<input
1735+
v-model.number="settings.chartFilter.averageWindow"
1736+
type="range"
1737+
min="0"
1738+
max="20"
1739+
step="1"
1740+
class="accent-[var(--accent-color,var(--fg-subtle))]"
17301741
/>
1731-
<template #content>
1732-
<div class="flex flex-col gap-3">
1733-
<p class="text-xs text-fg-muted">
1734-
{{ $t('package.trends.known_anomalies_description') }}
1735-
</p>
1736-
<div v-if="hasAnomalies">
1737-
<p class="text-xs text-fg-subtle font-medium">
1738-
{{ $t('package.trends.known_anomalies_ranges') }}
1739-
</p>
1740-
<ul class="text-xs text-fg-subtle list-disc list-inside">
1741-
<li v-for="a in packageAnomalies" :key="`${a.packageName}-${a.start}`">
1742+
</label>
1743+
1744+
<label class="flex flex-col gap-1 flex-1">
1745+
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1746+
{{ $t('package.trends.smoothing') }}
1747+
<span class="text-fg-muted">({{ settings.chartFilter.smoothingTau }})</span>
1748+
</span>
1749+
<input
1750+
v-model.number="settings.chartFilter.smoothingTau"
1751+
type="range"
1752+
min="0"
1753+
max="20"
1754+
step="1"
1755+
class="accent-[var(--accent-color,var(--fg-subtle))]"
1756+
/>
1757+
</label>
1758+
1759+
<label class="flex flex-col gap-1 flex-1">
1760+
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
1761+
{{ $t('package.trends.prediction') }}
1762+
<span class="text-fg-muted">({{ settings.chartFilter.predictionPoints }})</span>
1763+
</span>
1764+
<input
1765+
v-model.number="settings.chartFilter.predictionPoints"
1766+
type="range"
1767+
min="0"
1768+
max="30"
1769+
step="1"
1770+
class="accent-[var(--accent-color,var(--fg-subtle))]"
1771+
/>
1772+
</label>
1773+
1774+
<div class="flex flex-col gap-1 shrink-0">
1775+
<span
1776+
class="text-2xs font-mono text-fg-subtle tracking-wide uppercase flex items-center justify-between"
1777+
>
1778+
{{ $t('package.trends.known_anomalies') }}
1779+
<TooltipApp interactive :to="inModal ? '#chart-modal' : undefined">
1780+
<button
1781+
type="button"
1782+
class="i-lucide:info w-3.5 h-3.5 text-fg-muted cursor-help"
1783+
:aria-label="$t('package.trends.known_anomalies')"
1784+
/>
1785+
<template #content>
1786+
<div class="flex flex-col gap-3">
1787+
<p class="text-xs text-fg-muted">
1788+
{{ $t('package.trends.known_anomalies_description') }}
1789+
</p>
1790+
1791+
<div v-if="hasAnomalies">
1792+
<p class="text-xs text-fg-subtle font-medium">
1793+
{{ $t('package.trends.known_anomalies_ranges') }}
1794+
</p>
1795+
<ul class="text-xs text-fg-subtle list-disc list-inside">
1796+
<li v-for="a in packageAnomalies" :key="`${a.packageName}-${a.start}`">
1797+
{{
1798+
isMultiPackageMode
1799+
? $t('package.trends.known_anomalies_range_named', {
1800+
packageName: a.packageName,
1801+
start: formatAnomalyDate(a.start),
1802+
end: formatAnomalyDate(a.end),
1803+
})
1804+
: $t('package.trends.known_anomalies_range', {
1805+
start: formatAnomalyDate(a.start),
1806+
end: formatAnomalyDate(a.end),
1807+
})
1808+
}}
1809+
</li>
1810+
</ul>
1811+
</div>
1812+
1813+
<p v-else class="text-xs text-fg-muted">
17421814
{{
1743-
isMultiPackageMode
1744-
? $t('package.trends.known_anomalies_range_named', {
1745-
packageName: a.packageName,
1746-
start: formatAnomalyDate(a.start),
1747-
end: formatAnomalyDate(a.end),
1748-
})
1749-
: $t('package.trends.known_anomalies_range', {
1750-
start: formatAnomalyDate(a.start),
1751-
end: formatAnomalyDate(a.end),
1752-
})
1815+
$t('package.trends.known_anomalies_none', effectivePackageNames.length)
17531816
}}
1754-
</li>
1755-
</ul>
1756-
</div>
1757-
<p v-else class="text-xs text-fg-muted">
1758-
{{ $t('package.trends.known_anomalies_none', effectivePackageNames.length) }}
1759-
</p>
1760-
<div class="flex justify-end">
1761-
<LinkBase
1762-
to="https://github.com/npmx-dev/npmx.dev/edit/main/app/utils/download-anomalies.data.ts"
1763-
class="text-xs text-accent"
1764-
>
1765-
{{ $t('package.trends.known_anomalies_contribute') }}
1766-
</LinkBase>
1767-
</div>
1768-
</div>
1769-
</template>
1770-
</TooltipApp>
1771-
</span>
1772-
<label
1773-
class="flex items-center gap-1.5 text-2xs font-mono text-fg-subtle cursor-pointer h-4"
1774-
:class="{ 'opacity-50 pointer-events-none': !hasAnomalies }"
1775-
>
1776-
<input
1777-
:checked="settings.chartFilter.anomaliesFixed && hasAnomalies"
1778-
@change="
1779-
settings.chartFilter.anomaliesFixed = ($event.target as HTMLInputElement).checked
1780-
"
1781-
type="checkbox"
1782-
:disabled="!hasAnomalies"
1783-
class="accent-[var(--accent-color,var(--fg-subtle))]"
1784-
/>
1785-
{{ $t('package.trends.apply_correction') }}
1786-
</label>
1817+
</p>
1818+
1819+
<div class="flex justify-end">
1820+
<LinkBase
1821+
to="https://github.com/npmx-dev/npmx.dev/edit/main/app/utils/download-anomalies.data.ts"
1822+
class="text-xs text-accent"
1823+
>
1824+
{{ $t('package.trends.known_anomalies_contribute') }}
1825+
</LinkBase>
1826+
</div>
1827+
</div>
1828+
</template>
1829+
</TooltipApp>
1830+
</span>
1831+
1832+
<label
1833+
class="flex items-center gap-1.5 text-2xs font-mono text-fg-subtle cursor-pointer h-4"
1834+
:class="{ 'opacity-50 pointer-events-none': !hasAnomalies }"
1835+
>
1836+
<input
1837+
:checked="settings.chartFilter.anomaliesFixed && hasAnomalies"
1838+
@change="
1839+
settings.chartFilter.anomaliesFixed = (
1840+
$event.target as HTMLInputElement
1841+
).checked
1842+
"
1843+
type="checkbox"
1844+
:disabled="!hasAnomalies"
1845+
class="accent-[var(--accent-color,var(--fg-subtle))]"
1846+
/>
1847+
{{ $t('package.trends.apply_correction') }}
1848+
</label>
1849+
</div>
1850+
</div>
17871851
</div>
17881852
</div>
17891853
</div>
@@ -1802,14 +1866,23 @@ watch(selectedMetric, value => {
18021866
<div
18031867
role="region"
18041868
aria-labelledby="trends-chart-title"
1805-
:class="isMobile === false && width > 0 ? 'min-h-[567px]' : 'min-h-[260px]'"
1869+
:class="
1870+
isMobile === false && width > 0
1871+
? showCorrectionControls
1872+
? 'h-[491px]'
1873+
: 'h-[567px]'
1874+
: 'min-h-[260px]'
1875+
"
18061876
>
18071877
<ClientOnly v-if="chartData.dataset">
18081878
<div :data-pending="pending" :data-minimap-visible="maxDatapoints > 6">
18091879
<VueUiXy
18101880
:dataset="normalisedDataset"
18111881
:config="chartConfig"
1812-
class="[direction:ltr]"
1882+
:class="{
1883+
'[direction:ltr]': true,
1884+
'no-transition': isResizing,
1885+
}"
18131886
@zoomStart="setIsZoom"
18141887
@zoomEnd="setIsZoom"
18151888
@zoomReset="isZoomed = false"
@@ -2105,4 +2178,9 @@ watch(selectedMetric, value => {
21052178
[data-minimap-visible='false'] .vue-data-ui-watermark {
21062179
top: calc(100% - 2rem) !important;
21072180
}
2181+
2182+
.no-transition line,
2183+
.no-transition circle {
2184+
transition: none !important;
2185+
}
21082186
</style>

0 commit comments

Comments
 (0)