Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
76 changes: 57 additions & 19 deletions app/components/PackageDownloadAnalytics.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,40 @@ const {
createdIso: string | null
}>()

const { accentColors, selectedAccentColor } = useAccentColor()
const colorMode = useColorMode()

const resolvedMode = ref<'light' | 'dark'>('light')

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

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

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

// oklh or css variables are not supported by vue-data-ui (for now)

const accentColorValueById = computed<Record<string, string>>(() => {
const map: Record<string, string> = {}
for (const item of accentColors) {
map[item.id] = item.value
}
return map
})

const accent = computed(() => {
const id = selectedAccentColor.value
return id ? (oklchToHex(accentColorValueById.value[id]!) ?? '#8A8A8A') : '#8A8A8A'
})

type ChartTimeGranularity = 'daily' | 'weekly' | 'monthly' | 'yearly'
type EvolutionData =
| DailyDownloadPoint[]
Expand Down Expand Up @@ -81,7 +115,7 @@ function formatXyDataset(
name: packageName,
type: 'line',
series: dataset.map(d => d.downloads),
color: '#8A8A8A',
color: accent.value,
},
],
dates: dataset.map(d => `${d.weekStart}\nto ${d.weekEnd}`),
Expand All @@ -94,7 +128,7 @@ function formatXyDataset(
name: packageName,
type: 'line',
series: dataset.map(d => d.downloads),
color: '#8A8A8A',
color: accent.value,
},
],
dates: dataset.map(d => d.day),
Expand All @@ -107,7 +141,7 @@ function formatXyDataset(
name: packageName,
type: 'line',
series: dataset.map(d => d.downloads),
color: '#8A8A8A',
color: accent.value,
},
],
dates: dataset.map(d => d.month),
Expand All @@ -120,7 +154,7 @@ function formatXyDataset(
name: packageName,
type: 'line',
series: dataset.map(d => d.downloads),
color: '#8A8A8A',
color: accent.value,
},
],
dates: dataset.map(d => d.year),
Expand Down Expand Up @@ -381,7 +415,7 @@ const chartData = computed<{ dataset: VueUiXyDatasetItem[] | null; dates: string
const formatter = ({ value }: { value: number }) => formatCompactNumber(value, { decimals: 1 })

const config = computed(() => ({
theme: 'dark',
theme: isDarkMode.value ? 'dark' : 'default',
chart: {
userOptions: {
buttons: {
Expand All @@ -392,8 +426,9 @@ const config = computed(() => ({
tooltip: false,
},
},
backgroundColor: '#0A0A0A', // current default dark mode theme,
backgroundColor: isDarkMode.value ? '#0A0A0A' : '#FFFFFF',
grid: {
stroke: isDarkMode.value ? '#4A4A4A' : '#a3a3a3',
labels: {
axis: {
yLabel: $t('package.downloads.y_axis_label', { granularity: selectedGranularity.value }),
Expand All @@ -419,7 +454,7 @@ const config = computed(() => ({
show: false, // As long as a single package is displayed
},
tooltip: {
borderColor: '#2A2A2A',
borderColor: 'transparent',
backdropFilter: false,
backgroundColor: 'transparent',
customFormat: ({
Expand All @@ -431,23 +466,25 @@ const config = computed(() => ({
}) => {
if (!datapoint) return ''
const displayValue = formatter({ value: datapoint[0]?.value ?? 0 })
return `<div class="flex flex-col font-mono text-xs p-3 bg-[#0A0A0A]/10 backdrop-blur-md">
return `<div class="flex flex-col font-mono text-xs p-3 border border-border rounded-md bg-white/10 dark:bg-[#0A0A0A]/10 backdrop-blur-md">
<span class="text-fg-subtle">${chartData.value?.dates[absoluteIndex]}</span>
<span class="text-xl">${displayValue}</span>
</div>
`
},
},
zoom: {
highlightColor: '#2A2A2A',
highlightColor: isDarkMode.value ? '#2A2A2A' : '#E1E5E8',
minimap: {
show: true,
lineColor: '#FAFAFA',
selectedColorOpacity: 0.1,
frameColor: '#3A3A3A',
selectedColor: accent.value,
selectedColorOpacity: 0.06,
frameColor: isDarkMode.value ? '#3A3A3A' : '#a3a3a3',
},
preview: {
fill: '#FAFAFA05',
fill: accent.value + 10,
stroke: accent.value + 60,
strokeWidth: 1,
strokeDasharray: 3,
},
Expand All @@ -471,12 +508,12 @@ const config = computed(() => ({
</label>

<div
class="flex items-center px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-fg/50)"
class="flex items-center px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-accent/30)"
>
<select
id="granularity"
v-model="selectedGranularity"
class="w-full bg-transparent font-mono text-sm text-fg outline-none"
class="w-full bg-transparent font-mono text-sm text-fg outline-none appearance-none"
>
<option value="daily">{{ $t('package.downloads.granularity_daily') }}</option>
<option value="weekly">{{ $t('package.downloads.granularity_weekly') }}</option>
Expand All @@ -496,7 +533,7 @@ const config = computed(() => ({
{{ $t('package.downloads.start_date') }}
</label>
<div
class="flex items-center gap-2 px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-fg/50)"
class="flex items-center gap-2 px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-accent/30)"
>
<span class="i-carbon-calendar w-4 h-4 text-fg-subtle shrink-0" aria-hidden="true" />
<input
Expand All @@ -516,7 +553,7 @@ const config = computed(() => ({
{{ $t('package.downloads.end_date') }}
</label>
<div
class="flex items-center gap-2 px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-fg/50)"
class="flex items-center gap-2 px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-accent/30)"
>
<span class="i-carbon-calendar w-4 h-4 text-fg-subtle shrink-0" aria-hidden="true" />
<input
Expand Down Expand Up @@ -650,16 +687,17 @@ const config = computed(() => ({

<style>
.vue-ui-pen-and-paper-actions {
background: #1a1a1a !important;
background: var(--bg-elevated) !important;
}

.vue-ui-pen-and-paper-action {
background: #1a1a1a !important;
background: var(--bg-elevated) !important;
border: none !important;
}

.vue-ui-pen-and-paper-action:hover {
background: #2a2a2a !important;
background: var(--bg-elevated) !important;
box-shadow: none !important;
}

.vue-data-ui-zoom {
Expand Down
122 changes: 80 additions & 42 deletions app/components/PackageWeeklyDownloadStats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,41 @@ const createdIso = computed(() => packument.value?.time?.created ?? null)

const { fetchPackageDownloadEvolution } = useCharts()

const { accentColors, selectedAccentColor } = useAccentColor()

const colorMode = useColorMode()

const resolvedMode = ref<'light' | 'dark'>('light')

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

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

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

const accentColorValueById = computed<Record<string, string>>(() => {
const map: Record<string, string> = {}
for (const item of accentColors) {
map[item.id] = item.value
}
return map
})

const accent = computed(() => {
const id = selectedAccentColor.value
return id ? (oklchToHex(accentColorValueById.value[id]!) ?? '#8A8A8A') : '#8A8A8A'
})

const pulseColor = computed(() => (selectedAccentColor.value ? accent.value : '#8A8A8A'))

const weeklyDownloads = ref<WeeklyDownloadPoint[]>([])

async function loadWeeklyDownloads() {
Expand Down Expand Up @@ -51,52 +86,55 @@ const dataset = computed(() =>

const lastDatapoint = computed(() => dataset.value.at(-1)?.period ?? '')

const config = computed(() => ({
theme: 'dark',
style: {
backgroundColor: 'transparent',
animation: { show: false },
area: {
color: 'oklch(0.5243 0 0)', // css variable doesn't seem to work here
useGradient: false,
opacity: 10,
},
dataLabel: {
offsetX: -10,
fontSize: 28,
bold: false,
color: 'var(--fg)',
},
line: {
color: 'var(--fg-subtle)',
pulse: {
show: true,
loop: true, // runs only once if false
radius: 2,
color: 'var(--fg-muted)',
easing: 'ease-in-out',
trail: {
// oklh or css variables are not supported by vue-data-ui (for now)
const config = computed(() => {
return {
theme: 'dark',
style: {
backgroundColor: 'transparent',
animation: { show: false },
area: {
color: '#6A6A6A',
useGradient: false,
opacity: 10,
},
dataLabel: {
offsetX: -10,
fontSize: 28,
bold: false,
color: isDarkMode.value ? '#8a8a8a' : '#696969',
},
line: {
color: '#696969',
pulse: {
show: true,
length: 6,
loop: true, // runs only once if false
radius: 2,
color: pulseColor.value,
easing: 'ease-in-out',
trail: {
show: true,
length: 6,
},
},
},
plot: {
radius: 6,
stroke: isDarkMode.value ? '#FAFAFA' : '#0A0A0A',
},
title: {
text: lastDatapoint.value,
fontSize: 12,
color: '#8a8a8a',
bold: false,
},
verticalIndicator: {
strokeDasharray: 0,
color: isDarkMode.value ? '#FAFAFA' : '#525252',
},
},
plot: {
radius: 6,
stroke: 'var(--fg)',
},
title: {
text: lastDatapoint.value,
fontSize: 12,
color: 'var(--fg)',
bold: false,
},
verticalIndicator: {
strokeDasharray: 0,
color: 'var(--fg-muted)',
},
},
}))
}
})
</script>

<template>
Expand Down
45 changes: 45 additions & 0 deletions app/utils/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Vue Data UI does not support CSS vars nor OKLCH for now
export function oklchToHex(color: string | undefined | null): string | undefined | null {
if (color == null) return color

const match = color.trim().match(/^oklch\(\s*([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\s*\)$/i)

if (!match) {
throw new Error('Invalid OKLCH color format')
}

const lightness = Number(match[1])
const chroma = Number(match[2])
const hue = Number(match[3])

const hRad = (hue * Math.PI) / 180

const a = chroma * Math.cos(hRad)
const b = chroma * Math.sin(hRad)

let l_ = lightness + 0.3963377774 * a + 0.2158037573 * b
let m_ = lightness - 0.1055613458 * a - 0.0638541728 * b
let s_ = lightness - 0.0894841775 * a - 1.291485548 * b

l_ = l_ ** 3
m_ = m_ ** 3
s_ = s_ ** 3

let r = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_
let g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_
let bRgb = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_

const toSrgb = (value: number): number =>
value <= 0.0031308 ? 12.92 * value : 1.055 * Math.pow(value, 1 / 2.4) - 0.055

r = toSrgb(r)
g = toSrgb(g)
bRgb = toSrgb(bRgb)

const toHex = (value: number): string =>
Math.round(Math.min(Math.max(0, value), 1) * 255)
.toString(16)
.padStart(2, '0')

return `#${toHex(r)}${toHex(g)}${toHex(bRgb)}`
}
Loading