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 @@ + + + + + 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 @@