Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eb5edbe
chore: bump vue-data-ui from 3.15.10 to 3.15.11
graphieros Mar 7, 2026
77aa146
chore: centralise some shared chart utils
graphieros Mar 7, 2026
fea11d6
chore: use common shared chart utils
graphieros Mar 7, 2026
87ddf4a
feat: add bar chart component for facet comparisons
graphieros Mar 7, 2026
f46d93b
chore: add translations
graphieros Mar 7, 2026
ab5c8d5
fix: remove unnecessary iterator & add key
graphieros Mar 7, 2026
fb40da5
fix: follow the rabbit
graphieros Mar 7, 2026
bb8a4d0
fix: follow the rabbit
graphieros Mar 7, 2026
a0f85a6
fix: rabbit errors
graphieros Mar 7, 2026
9e20fcb
fix: follow the rabbit
graphieros Mar 7, 2026
2217d65
fix: remove commented code
graphieros Mar 7, 2026
d68d25d
fix: pet the rabbit
graphieros Mar 7, 2026
aa64eda
fix: add watermark on compare chart bar prints
graphieros Mar 7, 2026
80bd26c
fix: force ltr on chart component
graphieros Mar 7, 2026
542d4be
fix: add missing watermark section in chart alt text
graphieros Mar 7, 2026
603017f
chore: add french translations
graphieros Mar 7, 2026
df47c0b
Merge branch 'main' into main
alexdln Mar 7, 2026
379630a
fix: enable tooltip
graphieros Mar 8, 2026
b887ba6
fix: enable tooltip
graphieros Mar 8, 2026
4c6751d
fix: hide tooltip toggle from chart context menu
graphieros Mar 8, 2026
988bddc
fix: increase name labels slightly
graphieros Mar 8, 2026
294bb52
fix: disable tooltip on mobile
graphieros Mar 8, 2026
c7be95b
fix: use implicit return
graphieros Mar 8, 2026
447e003
fix: rename function
graphieros Mar 8, 2026
52b83f4
fix: use proper tabindex value
graphieros Mar 8, 2026
e1012ce
fix: use proper tabindex value
graphieros Mar 8, 2026
081c9b1
fix: remove log
graphieros Mar 8, 2026
a657211
fix: rename function
graphieros Mar 8, 2026
da87583
fix: use ButtonGroup
graphieros Mar 8, 2026
7c269ca
fix: remove ButtonGroup
graphieros Mar 8, 2026
eaf50c7
fix: show tabs only when chartable facets are selected
graphieros Mar 8, 2026
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
267 changes: 267 additions & 0 deletions app/components/Compare/FacetBarChart.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { VueUiHorizontalBar } from 'vue-data-ui/vue-ui-horizontal-bar'
import type { VueUiHorizontalBarConfig, VueUiHorizontalBarDatasetItem } from 'vue-data-ui'
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
import {
loadFile,
insertLineBreaks,
sanitise,
applyEllipsis,
copyAltTextForCompareFacetBarChart,
} from '~/utils/charts'

import('vue-data-ui/style.css')

const props = defineProps<{
values: (FacetValue | null | undefined)[]
packages: string[]
label: string
description: string
facetLoading?: boolean
}>()

// const { accentColors, selectedAccentColor } = useAccentColor()
const colorMode = useColorMode()
const resolvedMode = shallowRef<'light' | 'dark'>('light')
const rootEl = shallowRef<HTMLElement | null>(null)
const { width } = useElementSize(rootEl)
const { copy, copied } = useClipboard()

const mobileBreakpointWidth = 640
const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth)

const chartKey = ref(0)

const { colors } = useCssVariables(
[
'--bg',
'--fg',
'--bg-subtle',
'--bg-elevated',
'--fg-subtle',
'--fg-muted',
'--border',
'--border-subtle',
],
{
element: rootEl,
watchHtmlAttributes: true,
watchResize: false,
},
)

onMounted(async () => {
rootEl.value = document.documentElement
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
})

watch(
() => colorMode.value,
value => {
resolvedMode.value = value === 'dark' ? 'dark' : 'light'
},
{ flush: 'sync' },
)

watch(
() => props.packages,
(newP, oldP) => {
if (newP.length !== oldP.length) return
chartKey.value += 1
},
)

const isDarkMode = computed(() => resolvedMode.value === 'dark')

const dataset = computed<VueUiHorizontalBarDatasetItem[]>(() => {
if (props.facetLoading) return []

return props.packages.map((name, index) => {
return {
name: insertLineBreaks(applyEllipsis(name)),
value: (props.values[index]?.raw ?? 0) as number,
color: isListedFramework(name) ? getFrameworkColor(name) : undefined,
formattedValue: props.values[index]?.display,
}
})
})
Comment thread
graphieros marked this conversation as resolved.

const skeletonDataset = computed(() =>
props.packages.map((pkg, i) => {
return {
name: '_',
value: i + 1,
color: colors.value.border,
}
}),
)

function buildExportFilename(extension: string): string {
const sanitized = props.packages.map(p => sanitise(p).slice(0, 10)).join('_')
const comparisonLabel = $t('compare.packages.section_comparison')
return `${props.label}_${comparisonLabel}_${sanitized}.${extension}`
Comment thread
graphieros marked this conversation as resolved.
Outdated
}

const config = computed<VueUiHorizontalBarConfig>(() => {
return {
theme: isDarkMode.value ? 'dark' : '',
userOptions: {
buttons: {
pdf: false,
fullscreen: false,
sort: false,
annotator: false,
table: false,
csv: false,
altCopy: true,
},
buttonTitle: {
img: $t('package.trends.download_file', { fileType: 'PNG' }),
svg: $t('package.trends.download_file', { fileType: 'SVG' }),
altCopy: $t('package.trends.copy_alt.button_label'),
},
callbacks: {
img: args => {
const imageUri = args?.imageUri
if (!imageUri) return
loadFile(imageUri, buildExportFilename('png'))
},
svg: args => {
const blob = args?.blob
if (!blob) return
const url = URL.createObjectURL(blob)
loadFile(url, buildExportFilename('svg'))
URL.revokeObjectURL(url)
},
altCopy: ({ dataset: dst, config: cfg }) => {
copyAltTextForCompareFacetBarChart({
dataset: dst,
config: {
...cfg,
facet: props.label,
description: props.description,
copy,
$t,
},
})
},
},
},
skeletonDataset: skeletonDataset.value,
skeletonConfig: {
style: {
chart: {
backgroundColor: colors.value.bg,
},
},
},
style: {
chart: {
backgroundColor: colors.value.bg,
height: 56 * props.packages.length,
layout: {
bars: {
rowColor: isDarkMode.value ? colors.value.borderSubtle : colors.value.bgSubtle,
rowRadius: 4,
borderRadius: 4,
dataLabels: {
fontSize: isMobile.value ? 12 : 18,
percentage: { show: false },
offsetX: 12,
bold: false,
color: colors.value.fg,
value: {
formatter: ({ config }) => {
return config.datapoint.formattedValue
},
},
Comment thread
graphieros marked this conversation as resolved.
},
nameLabels: {
fontSize: isMobile.value ? 12 : 16,
color: colors.value.fgSubtle,
},
underlayerColor: colors.value.bg,
},
highlighter: { opacity: 0 },
},
legend: {
show: false,
},
title: {
fontSize: 16,
bold: false,
text: props.label,
color: colors.value.fg,
subtitle: {
text: props.description,
fontSize: 12,
color: colors.value.fgSubtle,
},
},
tooltip: { show: false },
},
},
}
})
</script>

<template>
<div class="font-mono facet-bar">
<ClientOnly v-if="dataset">
<VueUiHorizontalBar :key="chartKey" :dataset :config>
<template #menuIcon="{ isOpen }">
<span v-if="isOpen" class="i-lucide:x w-6 h-6" aria-hidden="true" />
<span v-else class="i-lucide:ellipsis-vertical w-6 h-6" aria-hidden="true" />
</template>
<template #optionCsv>
<span class="text-fg-subtle font-mono pointer-events-none">CSV</span>
</template>
<template #optionImg>
<span class="text-fg-subtle font-mono pointer-events-none">PNG</span>
</template>
<template #optionSvg>
<span class="text-fg-subtle font-mono pointer-events-none">SVG</span>
</template>
<template #optionAltCopy>
<span
class="w-6 h-6"
:class="
copied ? 'i-lucide:check text-accent' : 'i-lucide:person-standing text-fg-subtle'
"
style="pointer-events: none"
aria-hidden="true"
/>
</template>
</VueUiHorizontalBar>

<template #fallback>
<div class="flex flex-col gap-2 justify-center items-center mb-2">
<SkeletonInline class="h-4 w-16" />
<SkeletonInline class="h-4 w-28" />
</div>
<div class="flex flex-col gap-1">
<SkeletonInline class="h-7 w-full" v-for="(_, i) in packages" />

Check failure on line 244 in app/components/Compare/FacetBarChart.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'i' is declared but its value is never read.
</div>
</template>
</ClientOnly>

<template v-else>
<div class="flex flex-col gap-2 justify-center items-center mb-2">
<SkeletonInline class="h-4 w-16" />
<SkeletonInline class="h-4 w-28" />
</div>
<div class="flex flex-col gap-1">
<SkeletonInline class="h-7 w-full" v-for="(_, i) in packages" />

Check failure on line 255 in app/components/Compare/FacetBarChart.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'i' is declared but its value is never read.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</div>
</template>
</div>
Comment thread
graphieros marked this conversation as resolved.
</template>

<style>
.facet-bar .atom-subtitle {
width: 80% !important;
margin: 0 auto;
height: 2rem;
}
</style>
16 changes: 1 addition & 15 deletions app/components/Package/TrendsChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
import { DATE_INPUT_MAX } from '~/utils/input'
import { applyDataCorrection } from '~/utils/chart-data-correction'
import { applyBlocklistCorrection, getAnomaliesForPackages } from '~/utils/download-anomalies'
import { copyAltTextForTrendLineChart } from '~/utils/charts'
import { copyAltTextForTrendLineChart, sanitise, loadFile } from '~/utils/charts'

import('vue-data-ui/style.css')

Expand Down Expand Up @@ -1085,14 +1085,6 @@ const maxDatapoints = computed(() =>
Math.max(0, ...(chartData.value.dataset ?? []).map(d => d.series.length)),
)

const loadFile = (link: string, filename: string) => {
const a = document.createElement('a')
a.href = link
a.download = filename
a.click()
a.remove()
}

const datetimeFormatterOptions = computed(() => {
return {
daily: { year: 'yyyy-MM-dd', month: 'yyyy-MM-dd', day: 'yyyy-MM-dd' },
Expand All @@ -1113,12 +1105,6 @@ const tooltipDateFormatter = computed(() => {
})
})

const sanitise = (value: string) =>
value
.replace(/^@/, '')
.replace(/[\\/:"*?<>|]/g, '-')
.replace(/\//g, '-')

function buildExportFilename(extension: string): string {
const g = selectedGranularity.value
const range = `${startDate.value}_${endDate.value}`
Expand Down
16 changes: 1 addition & 15 deletions app/components/Package/VersionDistribution.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
drawNpmxLogoAndTaglineWatermark,
} from '~/composables/useChartWatermark'
import TooltipApp from '~/components/Tooltip/App.vue'
import { copyAltTextForVersionsBarChart } from '~/utils/charts'
import { copyAltTextForVersionsBarChart, sanitise, loadFile } from '~/utils/charts'

import('vue-data-ui/style.css')

Expand Down Expand Up @@ -89,20 +89,6 @@ const compactNumberFormatter = useCompactNumberFormatter()
// Show loading indicator immediately to maintain stable layout
const showLoadingIndicator = computed(() => pending.value)

const loadFile = (link: string, filename: string) => {
const a = document.createElement('a')
a.href = link
a.download = filename
a.click()
a.remove()
}

const sanitise = (value: string) =>
value
.replace(/^@/, '')
.replace(/[\\/:"*?<>|]/g, '-')
.replace(/\//g, '-')

const { locale } = useI18n()
function formatDate(date: Date) {
return date.toLocaleString(locale.value, {
Expand Down
Loading
Loading