Skip to content

Commit f0a386d

Browse files
committed
fix: chart enhancements from feedback
1 parent 20e8826 commit f0a386d

File tree

4 files changed

+272
-68
lines changed

4 files changed

+272
-68
lines changed

app/components/Package/DownloadAnalytics.vue

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useDebounceFn, useElementSize } from '@vueuse/core'
55
import { useCssVariables } from '~/composables/useColors'
66
import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch } from '~/utils/colors'
77
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
8+
import { drawNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
89
910
const props = defineProps<{
1011
// For single package downloads history
@@ -97,6 +98,12 @@ const accent = computed(() => {
9798
: (colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK)
9899
})
99100
101+
const watermarkColors = computed(() => ({
102+
fg: colors.value.fg ?? OKLCH_NEUTRAL_FALLBACK,
103+
bg: colors.value.bg ?? OKLCH_NEUTRAL_FALLBACK,
104+
fgSubtle: colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK,
105+
}))
106+
100107
const mobileBreakpointWidth = 640
101108
const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth)
102109
@@ -1389,31 +1396,6 @@ function drawSvgPrintLegend(svg: Record<string, any>) {
13891396
return seriesNames.join('\n')
13901397
}
13911398
1392-
/**
1393-
* Build and return npmx svg logo and tagline, to be injected during PNG & SVG exports
1394-
*/
1395-
function drawNpmxLogoAndTaglineWatermark(svg: Record<string, any>) {
1396-
if (!svg?.drawingArea) return ''
1397-
const npmxLogoWidthToHeight = 2.64
1398-
const npmxLogoWidth = 100
1399-
const npmxLogoHeight = npmxLogoWidth / npmxLogoWidthToHeight
1400-
1401-
return `
1402-
<svg x="${svg.drawingArea.left + svg.drawingArea.width / 2 - npmxLogoWidth / 2 - 3}" y="${svg.height - npmxLogoHeight}" width="${npmxLogoWidth}" height="${npmxLogoHeight}" viewBox="0 0 330 125" fill="none" xmlns="http://www.w3.org/2000/svg">
1403-
<path d="M22.848 97V85.288H34.752V97H22.848ZM56.4105 107.56L85.5945 25H93.2745L64.0905 107.56H56.4105ZM121.269 97V46.12H128.661L128.949 59.08L127.989 58.216C128.629 55.208 129.781 52.744 131.445 50.824C133.173 48.84 135.221 47.368 137.589 46.408C139.957 45.448 142.453 44.968 145.077 44.968C148.981 44.968 152.213 45.832 154.773 47.56C157.397 49.288 159.381 51.624 160.725 54.568C162.069 57.448 162.741 60.68 162.741 64.264V97H154.677V66.568C154.677 61.832 153.749 58.248 151.893 55.816C150.037 53.32 147.189 52.072 143.349 52.072C140.725 52.072 138.357 52.648 136.245 53.8C134.133 54.888 132.437 56.52 131.157 58.696C129.941 60.808 129.333 63.432 129.333 66.568V97H121.269ZM173.647 111.4V46.12H181.135L181.327 57.64L180.175 57.064C181.455 53.096 183.568 50.088 186.512 48.04C189.519 45.992 192.976 44.968 196.88 44.968C201.936 44.968 206.064 46.216 209.264 48.712C212.528 51.208 214.928 54.472 216.464 58.504C218 62.536 218.767 66.888 218.767 71.56C218.767 76.232 218 80.584 216.464 84.616C214.928 88.648 212.528 91.912 209.264 94.408C206.064 96.904 201.936 98.152 196.88 98.152C194.256 98.152 191.792 97.704 189.487 96.808C187.247 95.912 185.327 94.664 183.727 93.064C182.191 91.464 181.135 89.576 180.559 87.4L181.711 86.056V111.4H173.647ZM196.111 90.472C200.528 90.472 203.984 88.808 206.48 85.48C209.04 82.152 210.319 77.512 210.319 71.56C210.319 65.608 209.04 60.968 206.48 57.64C203.984 54.312 200.528 52.648 196.111 52.648C193.167 52.648 190.607 53.352 188.431 54.76C186.319 56.168 184.655 58.28 183.439 61.096C182.287 63.912 181.711 67.4 181.711 71.56C181.711 75.72 182.287 79.208 183.439 82.024C184.591 84.84 186.255 86.952 188.431 88.36C190.607 89.768 193.167 90.472 196.111 90.472ZM222.57 97V46.12H229.962L230.25 57.448L229.29 57.256C229.866 53.48 231.082 50.504 232.938 48.328C234.858 46.088 237.29 44.968 240.234 44.968C243.242 44.968 245.546 46.056 247.146 48.232C248.81 50.408 249.834 53.608 250.218 57.832H249.258C249.834 53.864 251.114 50.728 253.098 48.424C255.146 46.12 257.706 44.968 260.778 44.968C264.874 44.968 267.85 46.376 269.706 49.192C271.562 52.008 272.49 56.68 272.49 63.208V97H264.426V64.36C264.426 59.816 263.946 56.648 262.986 54.856C262.026 53 260.522 52.072 258.474 52.072C257.13 52.072 255.946 52.52 254.922 53.416C253.898 54.248 253.066 55.592 252.426 57.448C251.85 59.304 251.562 61.672 251.562 64.552V97H243.498V64.36C243.498 60.008 243.018 56.872 242.058 54.952C241.162 53.032 239.658 52.072 237.546 52.072C236.202 52.072 235.018 52.52 233.994 53.416C232.97 54.248 232.138 55.592 231.498 57.448C230.922 59.304 230.634 61.672 230.634 64.552V97H222.57ZM276.676 97L295.396 70.888L277.636 46.12H287.044L300.388 65.32L313.444 46.12H323.044L305.38 71.08L323.908 97H314.5L300.388 76.456L286.276 97H276.676Z" fill="${colors.value.fg}"/>
1404-
</svg>
1405-
<text
1406-
fill="${colors.value.fgMuted}"
1407-
x="${svg.drawingArea.left + svg.drawingArea.width / 2}"
1408-
y="${svg.height - npmxLogoHeight - 6}"
1409-
font-size="12"
1410-
text-anchor="middle"
1411-
>
1412-
${$t('tagline')}
1413-
</text>
1414-
`
1415-
}
1416-
14171399
// VueUiXy chart component configuration
14181400
const chartConfig = computed(() => {
14191401
return {
@@ -1715,7 +1697,7 @@ const chartConfig = computed(() => {
17151697
<!-- Inject npmx logo & tagline during SVG and PNG print -->
17161698
<g
17171699
v-if="svg.isPrintingSvg || svg.isPrintingImg"
1718-
v-html="drawNpmxLogoAndTaglineWatermark(svg)"
1700+
v-html="drawNpmxLogoAndTaglineWatermark(svg, watermarkColors, $t, 'bottom')"
17191701
/>
17201702

17211703
<!-- Overlay covering the chart area to hide line resizing when switching granularities recalculates VueUiXy scaleMax when estimation lines are necessary -->

app/components/Package/VersionDistribution.vue

Lines changed: 173 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import type {
99
import { useElementSize } from '@vueuse/core'
1010
import { useCssVariables } from '~/composables/useColors'
1111
import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch } from '~/utils/colors'
12+
import {
13+
drawSvgPrintLegend,
14+
drawNpmxLogoAndTaglineWatermark,
15+
} from '~/composables/useChartWatermark'
1216
import TooltipApp from '~/components/Tooltip/App.vue'
1317
1418
type TooltipParams = MinimalCustomFormatParams<VueUiXyDatapointItem[]> & {
@@ -64,6 +68,12 @@ const accent = computed(() => {
6468
: (colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK)
6569
})
6670
71+
const watermarkColors = computed(() => ({
72+
fg: colors.value.fg ?? OKLCH_NEUTRAL_FALLBACK,
73+
bg: colors.value.bg ?? OKLCH_NEUTRAL_FALLBACK,
74+
fgSubtle: colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK,
75+
}))
76+
6777
const { width } = useElementSize(rootEl)
6878
const mobileBreakpointWidth = 640
6979
const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth)
@@ -80,6 +90,9 @@ const {
8090
8191
const compactNumberFormatter = useCompactNumberFormatter()
8292
93+
// Show loading indicator immediately to maintain stable layout
94+
const showLoadingIndicator = computed(() => pending.value)
95+
8396
const chartConfig = computed(() => {
8497
return {
8598
theme: isDarkMode.value ? 'dark' : 'default',
@@ -89,11 +102,17 @@ const chartConfig = computed(() => {
89102
padding: {
90103
top: 24,
91104
right: 24,
92-
bottom: xAxisLabels.value.length > 10 ? 100 : 72, // More space for rotated labels
105+
bottom: xAxisLabels.value.length > 10 ? 100 : 88, // Space for rotated labels + watermark
93106
left: isMobile.value ? 60 : 80,
94107
},
95108
userOptions: {
96-
buttons: { pdf: false, labels: false, fullscreen: false, table: false, tooltip: false },
109+
buttons: {
110+
pdf: false,
111+
labels: false,
112+
fullscreen: false,
113+
table: false,
114+
tooltip: false,
115+
},
97116
},
98117
grid: {
99118
stroke: colors.value.border,
@@ -102,7 +121,6 @@ const chartConfig = computed(() => {
102121
color: pending.value ? colors.value.border : colors.value.fgSubtle,
103122
axis: {
104123
yLabel: 'Downloads',
105-
xLabel: props.packageName,
106124
yLabelOffsetX: 12,
107125
fontSize: isMobile.value ? 32 : 24,
108126
},
@@ -115,17 +133,14 @@ const chartConfig = computed(() => {
115133
xAxisLabels: {
116134
show: xAxisLabels.value.length <= 25,
117135
values: xAxisLabels.value,
118-
fontSize: isMobile.value ? 14 : 12,
136+
fontSize: isMobile.value ? 16 : 14,
119137
color: colors.value.fgSubtle,
120138
rotation: xAxisLabels.value.length > 10 ? 45 : 0,
121139
},
122140
},
123141
},
124-
timeTag: {
125-
show: false,
126-
},
127142
highlighter: { useLine: false },
128-
legend: { show: false },
143+
legend: { show: false, position: 'top' },
129144
bar: {
130145
periodGap: 16,
131146
innerGap: 8,
@@ -184,9 +199,6 @@ const chartConfig = computed(() => {
184199
},
185200
},
186201
},
187-
userOptions: {
188-
show: false,
189-
},
190202
table: {
191203
show: false,
192204
},
@@ -199,7 +211,7 @@ const xyDataset = computed<VueUiXyDatasetItem[]>(() => {
199211
200212
return [
201213
{
202-
name: 'Downloads',
214+
name: props.packageName,
203215
series: chartDataset.value.map(item => item.downloads),
204216
type: 'bar' as const,
205217
color: accent.value,
@@ -247,8 +259,7 @@ const endDate = computed(() => {
247259

248260
<template>
249261
<div
250-
class="w-full relative"
251-
:class="isMobile ? 'min-h-[600px]' : 'min-h-[500px]'"
262+
class="w-full flex flex-col"
252263
id="version-distribution"
253264
:aria-busy="pending ? 'true' : 'false'"
254265
>
@@ -395,44 +406,165 @@ const endDate = computed(() => {
395406
<div
396407
role="region"
397408
aria-labelledby="version-distribution-title"
398-
class="relative flex items-center justify-center"
409+
class="relative"
399410
:class="isMobile ? 'min-h-[500px]' : 'min-h-[400px]'"
400411
>
401-
<div
402-
v-if="pending"
403-
role="status"
404-
aria-live="polite"
405-
class="text-xs text-fg-subtle font-mono bg-bg/70 backdrop-blur px-3 py-2 rounded-md border border-border"
406-
>
407-
{{ $t('common.loading') }}
412+
<!-- Chart content -->
413+
<ClientOnly v-if="xyDataset.length > 0 && !error">
414+
<div class="chart-container w-full" :key="groupingMode">
415+
<VueUiXy :dataset="xyDataset" :config="chartConfig" class="[direction:ltr]">
416+
<!-- Injecting custom svg elements -->
417+
<template #svg="{ svg }">
418+
<!-- Inject legend during SVG print only -->
419+
<g v-if="svg.isPrintingSvg" v-html="drawSvgPrintLegend(svg, watermarkColors)" />
420+
421+
<!-- Inject npmx logo & tagline during SVG and PNG print -->
422+
<g
423+
v-if="svg.isPrintingSvg || svg.isPrintingImg"
424+
v-html="
425+
drawNpmxLogoAndTaglineWatermark(svg, watermarkColors, $t, 'belowDrawingArea')
426+
"
427+
/>
428+
</template>
429+
430+
<!-- Custom legend for single series (non-interactive) -->
431+
<template #legend="{ legend }">
432+
<div class="flex gap-4 flex-wrap justify-center">
433+
<template v-if="legend.length > 0">
434+
<div class="flex gap-1 place-items-center">
435+
<div class="h-3 w-3">
436+
<svg viewBox="0 0 2 2" class="w-full">
437+
<rect x="0" y="0" width="2" height="2" rx="0.3" :fill="legend[0]?.color" />
438+
</svg>
439+
</div>
440+
<span>
441+
{{ legend[0]?.name }}
442+
</span>
443+
</div>
444+
</template>
445+
</div>
446+
</template>
447+
448+
<!-- Contextual menu icon -->
449+
<template #menuIcon="{ isOpen }">
450+
<span v-if="isOpen" class="i-carbon:close w-6 h-6" aria-hidden="true" />
451+
<span v-else class="i-carbon:overflow-menu-vertical w-6 h-6" aria-hidden="true" />
452+
</template>
453+
454+
<!-- Export options -->
455+
<template #optionCsv>
456+
<span
457+
class="i-carbon:csv w-6 h-6 text-fg-subtle"
458+
style="pointer-events: none"
459+
aria-hidden="true"
460+
/>
461+
</template>
462+
463+
<template #optionImg>
464+
<span
465+
class="i-carbon:png w-6 h-6 text-fg-subtle"
466+
style="pointer-events: none"
467+
aria-hidden="true"
468+
/>
469+
</template>
470+
471+
<template #optionSvg>
472+
<span
473+
class="i-carbon:svg w-6 h-6 text-fg-subtle"
474+
style="pointer-events: none"
475+
aria-hidden="true"
476+
/>
477+
</template>
478+
479+
<!-- Annotator action icons -->
480+
<template #annotator-action-close>
481+
<span
482+
class="i-carbon:close w-6 h-6 text-fg-subtle"
483+
style="pointer-events: none"
484+
aria-hidden="true"
485+
/>
486+
</template>
487+
488+
<template #annotator-action-color="{ color }">
489+
<span class="i-carbon:color-palette w-6 h-6" :style="{ color }" aria-hidden="true" />
490+
</template>
491+
492+
<template #annotator-action-undo>
493+
<span
494+
class="i-carbon:undo w-6 h-6 text-fg-subtle"
495+
style="pointer-events: none"
496+
aria-hidden="true"
497+
/>
498+
</template>
499+
500+
<template #annotator-action-redo>
501+
<span
502+
class="i-carbon:redo w-6 h-6 text-fg-subtle"
503+
style="pointer-events: none"
504+
aria-hidden="true"
505+
/>
506+
</template>
507+
508+
<template #annotator-action-delete>
509+
<span
510+
class="i-carbon:trash-can w-6 h-6 text-fg-subtle"
511+
style="pointer-events: none"
512+
aria-hidden="true"
513+
/>
514+
</template>
515+
516+
<template #optionAnnotator="{ isAnnotator }">
517+
<span
518+
v-if="isAnnotator"
519+
class="i-carbon:edit-off w-6 h-6 text-fg-subtle"
520+
style="pointer-events: none"
521+
aria-hidden="true"
522+
/>
523+
<span
524+
v-else
525+
class="i-carbon:edit w-6 h-6 text-fg-subtle"
526+
style="pointer-events: none"
527+
aria-hidden="true"
528+
/>
529+
</template>
530+
</VueUiXy>
531+
</div>
532+
533+
<template #fallback>
534+
<div />
535+
</template>
536+
</ClientOnly>
537+
538+
<!-- No-data state -->
539+
<div v-if="!hasData && !pending && !error" class="flex items-center justify-center h-full">
540+
<div class="text-sm text-fg-subtle font-mono text-center flex flex-col items-center gap-2">
541+
<span class="i-carbon:data-vis-4 w-8 h-8" />
542+
<p>{{ $t('package.trends.no_data') }}</p>
543+
</div>
408544
</div>
409545

410-
<div
411-
v-else-if="error"
412-
class="text-sm text-fg-subtle font-mono text-center flex flex-col items-center gap-2"
413-
role="alert"
414-
>
415-
<span class="i-carbon:warning-hex w-8 h-8 text-red-400" />
416-
<p>{{ error.message }}</p>
417-
<p class="text-xs">Package: {{ packageName }}</p>
546+
<!-- Error state -->
547+
<div v-if="error" class="flex items-center justify-center h-full" role="alert">
548+
<div class="text-sm text-fg-subtle font-mono text-center flex flex-col items-center gap-2">
549+
<span class="i-carbon:warning-hex w-8 h-8 text-red-400" />
550+
<p>{{ error.message }}</p>
551+
<p class="text-xs">Package: {{ packageName }}</p>
552+
</div>
418553
</div>
419554

555+
<!-- Loading indicator as true overlay -->
420556
<div
421-
v-else-if="!hasData"
422-
class="text-sm text-fg-subtle font-mono text-center flex flex-col items-center gap-2"
557+
v-if="showLoadingIndicator"
558+
role="status"
559+
aria-live="polite"
560+
class="absolute top-1/2 inset-is-1/2 -translate-x-1/2 -translate-y-1/2"
423561
>
424-
<span class="i-carbon:data-vis-4 w-8 h-8" />
425-
<p>{{ $t('package.trends.no_data') }}</p>
426-
</div>
427-
428-
<ClientOnly v-else-if="xyDataset.length > 0">
429562
<div
430-
class="chart-container w-full h-[400px] sm:h-[400px]"
431-
:class="{ 'h-[500px]': isMobile }"
563+
class="text-xs text-fg-subtle font-mono bg-bg/70 backdrop-blur px-3 py-2 rounded-md border border-border"
432564
>
433-
<VueUiXy :dataset="xyDataset" :config="chartConfig" class="[direction:ltr]" />
565+
{{ $t('common.loading') }}
434566
</div>
435-
</ClientOnly>
567+
</div>
436568
</div>
437569
</div>
438570
</template>

app/components/Package/Versions.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
821821
<!-- Avoids CLS when the dialog has transitioned -->
822822
<div
823823
v-if="!hasDistributionModalTransitioned"
824-
class="w-full aspect-[136/391] sm:aspect-[359/278]"
824+
class="w-full aspect-[272/609] sm:aspect-[671/516]"
825825
/>
826826
</PackageChartModal>
827827
</template>

0 commit comments

Comments
 (0)