Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7d41c44
chore: remove quadrant chart
graphieros Apr 11, 2026
ba3042e
chore: bump vue-data-ui from 3.17.11 to 3.17.12
graphieros Apr 11, 2026
c8d7026
feat: add compare scatter chart
graphieros Apr 11, 2026
6bba1b8
fix: improve legend in compare downloads chart
graphieros Apr 11, 2026
28fd394
chore: update translations
graphieros Apr 11, 2026
d36cdfa
Merge branch 'main' into compare-scatter-with-selectable-axes
graphieros Apr 11, 2026
c297522
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 11, 2026
fbaa807
fix: remove unused translations
graphieros Apr 11, 2026
86e9e4d
Merge branch 'compare-scatter-with-selectable-axes' of https://github…
graphieros Apr 11, 2026
eeb0efc
fix: remove unused import
graphieros Apr 11, 2026
c522660
fix: follow the rabbit
graphieros Apr 11, 2026
b041e9d
fix: typo
graphieros Apr 11, 2026
8aa7684
chore: bump vue-data-ui from 3.17.12 to 3.17.13
graphieros Apr 11, 2026
ecab11e
fix: remove empty translation key added by a robot
graphieros Apr 11, 2026
d611dc6
fix: add skeleton in chart fallback slot
graphieros Apr 11, 2026
07d374b
fix: use proper css var for focus-visible overrides
graphieros Apr 11, 2026
a0ed79d
fix: skeleton CLS for facet bars
graphieros Apr 11, 2026
e9c51e6
fix: do not use empty auto-closing div
graphieros Apr 12, 2026
bc140c5
fix: improve axis translations for facet inputs
graphieros Apr 12, 2026
92d3844
feat: highlight axes when hovering/focusing related facet inputs
graphieros Apr 12, 2026
b6d4a9c
Merge branch 'main' into compare-scatter-with-selectable-axes
graphieros Apr 12, 2026
d91a576
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 12, 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
547 changes: 0 additions & 547 deletions app/components/Compare/FacetQuadrantChart.vue

This file was deleted.

490 changes: 490 additions & 0 deletions app/components/Compare/FacetScatterChart.vue

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/components/Package/TrendsChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1994,7 +1994,7 @@ const isSparklineLayout = computed({

<!-- Custom legend for multiple series -->
<template #legend="{ legend }">
<div class="flex gap-4 flex-wrap justify-center">
<div class="flex gap-x-6 gap-y-2 flex-wrap justify-center text-sm">
<template v-if="isMultiPackageMode">
<button
v-for="datapoint in legend"
Expand Down
16 changes: 12 additions & 4 deletions app/composables/useChartWatermark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,27 +132,35 @@ export function drawSmallNpmxLogoAndTaglineWatermark({
colors,
translateFn,
logoWidth = 36,
taglineFontSize = 8,
offsetYTagline = 0,
offsetXTagline = 0,
offsetYLogo = 0,
}: {
svg: Record<string, any>
colors: WatermarkColors
translateFn: (key: string) => string
logoWidth?: number
taglineFontSize?: number
offsetYTagline?: number
offsetXTagline?: number
offsetYLogo?: number
}) {
if (!svg.height) return

const npmxLogoWidthToHeight = 2.64
const npmxLogoHeight = logoWidth / npmxLogoWidthToHeight
const offsetX = 6
const watermarkY = svg.height - npmxLogoHeight
const watermarkY = svg.height - npmxLogoHeight + offsetYLogo
const taglineY = svg.height - 3

return `
${generateWatermarkLogo({ x: offsetX, y: watermarkY, width: logoWidth, height: npmxLogoHeight, fill: colors.fg })}
<text
fill="${colors.fgSubtle}"
x="${logoWidth + offsetX * 2}"
y="${taglineY}"
font-size="8"
x="${logoWidth + offsetX * 2 + offsetXTagline}"
y="${taglineY + offsetYTagline}"
font-size="${taglineFontSize}"
text-anchor="start"
>
${translateFn('tagline')}
Expand Down
40 changes: 38 additions & 2 deletions app/composables/useFacetSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export interface FacetInfoWithLabels extends Omit<FacetInfo, 'id'> {
label: string
description: string
chartable: boolean
chartable_scatter: boolean
formatter?: (value: number) => string
}

// Get facets in a category (excluding coming soon)
Expand All @@ -21,73 +23,104 @@ function getFacetsInCategory(category: string): ComparisonFacet[] {
*/
export function useFacetSelection(queryParam = 'facets') {
const { t } = useI18n()
const compactNumberFormatter = useCompactNumberFormatter()
const bytesFormatter = useBytesFormatter()

const facetLabels = computed(
(): Record<ComparisonFacet, { label: string; description: string; chartable: boolean }> => ({
(): Record<
ComparisonFacet,
{
label: string
description: string
chartable: boolean
chartable_scatter: boolean
formatter?: (value: number) => string
}
> => ({
downloads: {
label: t(`compare.facets.items.downloads.label`),
description: t(`compare.facets.items.downloads.description`),
chartable: true,
chartable: true, // TODO: rename to chartable_bar
chartable_scatter: true,
formatter: v => compactNumberFormatter.value.format(v),
},
totalLikes: {
label: t(`compare.facets.items.totalLikes.label`),
description: t(`compare.facets.items.totalLikes.description`),
chartable: true,
chartable_scatter: true,
formatter: v => compactNumberFormatter.value.format(v),
},
packageSize: {
label: t(`compare.facets.items.packageSize.label`),
description: t(`compare.facets.items.packageSize.description`),
chartable: true,
chartable_scatter: true,
formatter: v => bytesFormatter.format(v),
},
installSize: {
label: t(`compare.facets.items.installSize.label`),
description: t(`compare.facets.items.installSize.description`),
chartable: true,
chartable_scatter: true,
formatter: v => bytesFormatter.format(v),
},
moduleFormat: {
label: t(`compare.facets.items.moduleFormat.label`),
description: t(`compare.facets.items.moduleFormat.description`),
chartable: false,
chartable_scatter: false,
},
types: {
label: t(`compare.facets.items.types.label`),
description: t(`compare.facets.items.types.description`),
chartable: false,
chartable_scatter: false,
},
engines: {
label: t(`compare.facets.items.engines.label`),
description: t(`compare.facets.items.engines.description`),
chartable: false,
chartable_scatter: false,
},
vulnerabilities: {
label: t(`compare.facets.items.vulnerabilities.label`),
description: t(`compare.facets.items.vulnerabilities.description`),
chartable: false,
chartable_scatter: true,
formatter: v => compactNumberFormatter.value.format(v),
},
lastUpdated: {
label: t(`compare.facets.items.lastUpdated.label`),
description: t(`compare.facets.items.lastUpdated.description`),
chartable: false,
chartable_scatter: true,
},
license: {
label: t(`compare.facets.items.license.label`),
description: t(`compare.facets.items.license.description`),
chartable: false,
chartable_scatter: false,
},
dependencies: {
label: t(`compare.facets.items.dependencies.label`),
description: t(`compare.facets.items.dependencies.description`),
chartable: true,
chartable_scatter: true,
formatter: v => compactNumberFormatter.value.format(v),
},
totalDependencies: {
label: t(`compare.facets.items.totalDependencies.label`),
description: t(`compare.facets.items.totalDependencies.description`),
chartable: true,
chartable_scatter: true,
formatter: v => compactNumberFormatter.value.format(v),
},
deprecated: {
label: t(`compare.facets.items.deprecated.label`),
description: t(`compare.facets.items.deprecated.description`),
chartable: false,
chartable_scatter: false,
},
}),
)
Expand All @@ -100,6 +133,8 @@ export function useFacetSelection(queryParam = 'facets') {
label: facetLabels.value[facet].label,
description: facetLabels.value[facet].description,
chartable: facetLabels.value[facet].chartable,
chartable_scatter: facetLabels.value[facet].chartable_scatter,
formatter: facetLabels.value[facet].formatter ?? undefined,
}
}

Expand Down Expand Up @@ -224,6 +259,7 @@ export function useFacetSelection(queryParam = 'facets') {
isAllSelected,
isNoneSelected,
allFacets: ALL_FACETS,
facetLabels,
// Facet info with i18n
getCategoryLabel,
facetsByCategory,
Expand Down
8 changes: 4 additions & 4 deletions app/pages/compare.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import { NO_DEPENDENCY_ID } from '~/composables/usePackageComparison'
import { useRouteQuery } from '@vueuse/router'
import FacetBarChart from '~/components/Compare/FacetBarChart.vue'
import FacetQuadrantChart from '~/components/Compare/FacetQuadrantChart.vue'
import type { CommandPaletteContextCommandInput } from '~/types/command-palette'
import FacetScatterChart from '~/components/Compare/FacetScatterChart.vue'

definePageMeta({
name: 'compare',
Expand Down Expand Up @@ -424,7 +424,7 @@ useSeoMeta({
</div>
</TabPanel>

<!-- Charts: per-facet bars & quadrant -->
<!-- Charts: per-facet bars & scatter -->
<TabPanel value="charts" panel-id="comparison-panel-charts">
<div
v-if="selectedFacets.some(facet => facet.chartable)"
Expand All @@ -447,8 +447,8 @@ useSeoMeta({
<p v-else class="py-12 text-center text-fg-subtle">
{{ $t('compare.packages.no_chartable_data') }}
</p>
<div class="max-w-[450px] mx-auto">
<FacetQuadrantChart
<div>
<FacetScatterChart
v-if="packages.length"
:packages-data="packagesData"
:packages="packages.filter(p => p !== NO_DEPENDENCY_ID)"
Expand Down
96 changes: 44 additions & 52 deletions app/utils/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type {
AltCopyArgs,
VueUiHorizontalBarConfig,
VueUiHorizontalBarDatapoint,
VueUiQuadrantConfig,
VueUiQuadrantDatapoint,
VueUiScatterConfig,
VueUiScatterSeries,
VueUiXyConfig,
VueUiXyDatasetBarItem,
VueUiXyDatasetLineItem,
Expand Down Expand Up @@ -638,78 +638,70 @@ export async function copyAltTextForCompareFacetBarChart({
await config.copy(altText)
}

type CompareQuadrantChartConfig = VueUiQuadrantConfig & {
type CompareScatterChartConfig = VueUiScatterConfig & {
copy: (text: string) => Promise<void>
$t: TrendTranslateFunction
x: {
label: string
formatter: (v: number) => string
}
y: {
label: string
formatter: (v: number) => string
}
}

// Used for FacetQuadrantChart.vue
export function createAltTextForCompareQuadrantChart({
// Used for FacetScatterChart.vue
export function createAltTextForCompareScatterChart({
dataset,
config,
}: AltCopyArgs<VueUiQuadrantDatapoint[], CompareQuadrantChartConfig>) {
}: AltCopyArgs<VueUiScatterSeries[], CompareScatterChartConfig>) {
if (!dataset) return ''

const packages = {
topRight: dataset.filter(d => d.quadrant === 'TOP_RIGHT'),
topLeft: dataset.filter(d => d.quadrant === 'TOP_LEFT'),
bottomRight: dataset.filter(d => d.quadrant === 'BOTTOM_RIGHT'),
bottomLeft: dataset.filter(d => d.quadrant === 'BOTTOM_LEFT'),
}
const { x, y } = config
const { label: labelX, formatter: formatterX } = x
const { label: labelY, formatter: formatterY } = y

const descriptions = {
topRight: '',
topLeft: '',
bottomRight: '',
bottomLeft: '',
}
const datapoints = dataset.map(d => {
const rawX = d.values?.[0]?.x ?? 0
const rawY = d.values?.[0]?.y ?? 0
const name = d.fullName ?? ''

if (packages.topRight.length) {
descriptions.topRight = config.$t('compare.quadrant_chart.copy_alt.side_analysis_top_right', {
packages: packages.topRight.map(p => p.fullname).join(', '),
})
}

if (packages.topLeft.length) {
descriptions.topLeft = config.$t('compare.quadrant_chart.copy_alt.side_analysis_top_left', {
packages: packages.topLeft.map(p => p.fullname).join(', '),
})
}

if (packages.bottomRight.length) {
descriptions.bottomRight = config.$t(
'compare.quadrant_chart.copy_alt.side_analysis_bottom_right',
{
packages: packages.bottomRight.map(p => p.fullname).join(', '),
},
)
}
return {
x: formatterX(rawX),
y: formatterY(rawY),
name,
}
})

if (packages.bottomLeft.length) {
descriptions.bottomLeft = config.$t(
'compare.quadrant_chart.copy_alt.side_analysis_bottom_left',
{
packages: packages.bottomLeft.map(p => p.fullname).join(', '),
},
const analysis = datapoints
.map(d =>
config.$t('compare.scatter_chart.copy_alt.analysis', {
package: d.name,
x_name: labelX,
y_name: labelY,
x_value: d.x,
y_value: d.y,
}),
)
}

const analysis = Object.values(descriptions).filter(Boolean).join('. ')
.join(', ')

const altText = config.$t('compare.quadrant_chart.copy_alt.description', {
packages: dataset.map(p => p.fullname).join(', '),
const altText = config.$t('compare.scatter_chart.copy_alt.description', {
x_name: labelX,
y_name: labelY,
packages: datapoints.map(d => d.name).join(', '),
analysis,
watermark: config.$t('package.trends.copy_alt.watermark'),
})

return altText
}

export async function copyAltTextForCompareQuadrantChart({
export async function copyAltTextForCompareScatterChart({
dataset,
config,
}: AltCopyArgs<VueUiQuadrantDatapoint[], any>) {
const altText = createAltTextForCompareQuadrantChart({ dataset, config })
}: AltCopyArgs<VueUiScatterSeries[], CompareScatterChartConfig>) {
const altText = createAltTextForCompareScatterChart({ dataset, config })
await config.copy(altText)
}

Expand Down
Loading
Loading