diff --git a/app/components/Compare/FacetBarChart.vue b/app/components/Compare/FacetBarChart.vue
new file mode 100644
index 0000000000..de9619bca7
--- /dev/null
+++ b/app/components/Compare/FacetBarChart.vue
@@ -0,0 +1,313 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CSV
+
+
+ PNG
+
+
+ SVG
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/Package/TrendsChart.vue b/app/components/Package/TrendsChart.vue
index 663596da04..89d4670963 100644
--- a/app/components/Package/TrendsChart.vue
+++ b/app/components/Package/TrendsChart.vue
@@ -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')
@@ -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' },
@@ -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}`
@@ -1954,7 +1940,14 @@ watch(selectedMetric, value => {
diff --git a/app/components/Package/VersionDistribution.vue b/app/components/Package/VersionDistribution.vue
index 670b47bdf2..56322e2133 100644
--- a/app/components/Package/VersionDistribution.vue
+++ b/app/components/Package/VersionDistribution.vue
@@ -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')
@@ -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, {
@@ -469,7 +455,14 @@ const chartConfig = computed(() => {
diff --git a/app/composables/useChartWatermark.ts b/app/composables/useChartWatermark.ts
index 0cad48d641..48e6adf96c 100644
--- a/app/composables/useChartWatermark.ts
+++ b/app/composables/useChartWatermark.ts
@@ -47,15 +47,41 @@ export function drawSvgPrintLegend(svg: Record, colors: WatermarkCo
return seriesNames.join('')
}
+function generateWatermarkLogo({
+ x,
+ y,
+ width,
+ height,
+ fill,
+}: {
+ x: number
+ y: number
+ width: number
+ height: number
+ fill: string
+}) {
+ return `
+
+ `
+}
+
/**
* Build and return npmx svg logo and tagline, to be injected during PNG & SVG exports
+ * for VueUiXy instances
*/
-export function drawNpmxLogoAndTaglineWatermark(
- svg: Record,
- colors: WatermarkColors,
- translateFn: (key: string) => string,
- positioning: 'bottom' | 'belowDrawingArea' = 'bottom',
-) {
+export function drawNpmxLogoAndTaglineWatermark({
+ svg,
+ colors,
+ translateFn,
+ positioning = 'bottom',
+}: {
+ svg: Record
+ colors: WatermarkColors
+ translateFn: (key: string) => string
+ positioning?: 'bottom' | 'belowDrawingArea'
+}) {
if (!svg?.drawingArea) return ''
const npmxLogoWidthToHeight = 2.64
const npmxLogoWidth = 100
@@ -74,9 +100,7 @@ export function drawNpmxLogoAndTaglineWatermark(
const watermarkX = svg.width / 2 - npmxLogoWidth / 2
return `
-
+ ${generateWatermarkLogo({ x: watermarkX, y: watermarkY, width: npmxLogoWidth, height: npmxLogoHeight, fill: colors.fg })}
`
}
+
+/**
+ * Build and return npmx svg logo and tagline, to be injected during PNG & SVG exports
+ * for VueUiHorizontalBar instances
+ */
+export function drawSmallNpmxLogoAndTaglineWatermark({
+ svg,
+ colors,
+ translateFn,
+ logoWidth = 36,
+}: {
+ svg: Record
+ colors: WatermarkColors
+ translateFn: (key: string) => string
+ logoWidth?: number
+}) {
+ if (!svg.height) return
+
+ const npmxLogoWidthToHeight = 2.64
+ const npmxLogoHeight = logoWidth / npmxLogoWidthToHeight
+ const offsetX = 6
+ const watermarkY = svg.height - npmxLogoHeight
+ const taglineY = svg.height - 3
+
+ return `
+ ${generateWatermarkLogo({ x: offsetX, y: watermarkY, width: logoWidth, height: npmxLogoHeight, fill: colors.fg })}
+
+ ${translateFn('tagline')}
+
+ `
+}
diff --git a/app/composables/useFacetSelection.ts b/app/composables/useFacetSelection.ts
index ecb014b605..9af7befd3d 100644
--- a/app/composables/useFacetSelection.ts
+++ b/app/composables/useFacetSelection.ts
@@ -13,6 +13,7 @@ export interface FacetInfoWithLabels extends Omit {
id: ComparisonFacet
label: string
description: string
+ chartable: boolean
}
/**
@@ -24,58 +25,71 @@ export function useFacetSelection(queryParam = 'facets') {
const { t } = useI18n()
const facetLabels = computed(
- (): Record => ({
+ (): Record => ({
downloads: {
label: t(`compare.facets.items.downloads.label`),
description: t(`compare.facets.items.downloads.description`),
+ chartable: true,
},
totalLikes: {
label: t(`compare.facets.items.totalLikes.label`),
description: t(`compare.facets.items.totalLikes.description`),
+ chartable: true,
},
packageSize: {
label: t(`compare.facets.items.packageSize.label`),
description: t(`compare.facets.items.packageSize.description`),
+ chartable: true,
},
installSize: {
label: t(`compare.facets.items.installSize.label`),
description: t(`compare.facets.items.installSize.description`),
+ chartable: true,
},
moduleFormat: {
label: t(`compare.facets.items.moduleFormat.label`),
description: t(`compare.facets.items.moduleFormat.description`),
+ chartable: false,
},
types: {
label: t(`compare.facets.items.types.label`),
description: t(`compare.facets.items.types.description`),
+ chartable: false,
},
engines: {
label: t(`compare.facets.items.engines.label`),
description: t(`compare.facets.items.engines.description`),
+ chartable: false,
},
vulnerabilities: {
label: t(`compare.facets.items.vulnerabilities.label`),
description: t(`compare.facets.items.vulnerabilities.description`),
+ chartable: false,
},
lastUpdated: {
label: t(`compare.facets.items.lastUpdated.label`),
description: t(`compare.facets.items.lastUpdated.description`),
+ chartable: false,
},
license: {
label: t(`compare.facets.items.license.label`),
description: t(`compare.facets.items.license.description`),
+ chartable: false,
},
dependencies: {
label: t(`compare.facets.items.dependencies.label`),
description: t(`compare.facets.items.dependencies.description`),
+ chartable: true,
},
totalDependencies: {
label: t(`compare.facets.items.totalDependencies.label`),
description: t(`compare.facets.items.totalDependencies.description`),
+ chartable: true,
},
deprecated: {
label: t(`compare.facets.items.deprecated.label`),
description: t(`compare.facets.items.deprecated.description`),
+ chartable: false,
},
}),
)
@@ -87,6 +101,7 @@ export function useFacetSelection(queryParam = 'facets') {
...FACET_INFO[facet],
label: facetLabels.value[facet].label,
description: facetLabels.value[facet].description,
+ chartable: facetLabels.value[facet].chartable,
}
}
diff --git a/app/pages/compare.vue b/app/pages/compare.vue
index e8b33ea429..e38a34657d 100644
--- a/app/pages/compare.vue
+++ b/app/pages/compare.vue
@@ -1,6 +1,7 @@