Skip to content

Commit 1ea5504

Browse files
committed
feat: apply color scheme to charts
1 parent 6e1fc96 commit 1ea5504

3 files changed

Lines changed: 182 additions & 61 deletions

File tree

app/components/PackageDownloadAnalytics.vue

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,40 @@ const {
1616
createdIso: string | null
1717
}>()
1818
19+
const { accentColors, selectedAccentColor } = useAccentColor()
20+
const colorMode = useColorMode()
21+
22+
const resolvedMode = ref<'light' | 'dark'>('light')
23+
24+
onMounted(() => {
25+
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
26+
})
27+
28+
watch(
29+
() => colorMode.value,
30+
value => {
31+
resolvedMode.value = value === 'dark' ? 'dark' : 'light'
32+
},
33+
{ flush: 'sync' },
34+
)
35+
36+
const isDarkMode = computed(() => resolvedMode.value === 'dark')
37+
38+
// oklh or css variables are not supported by vue-data-ui (for now)
39+
40+
const accentColorValueById = computed<Record<string, string>>(() => {
41+
const map: Record<string, string> = {}
42+
for (const item of accentColors) {
43+
map[item.id] = item.value
44+
}
45+
return map
46+
})
47+
48+
const accent = computed(() => {
49+
const id = selectedAccentColor.value
50+
return id ? (oklchToHex(accentColorValueById.value[id]!) ?? '#8A8A8A') : '#8A8A8A'
51+
})
52+
1953
type ChartTimeGranularity = 'daily' | 'weekly' | 'monthly' | 'yearly'
2054
type EvolutionData =
2155
| DailyDownloadPoint[]
@@ -81,7 +115,7 @@ function formatXyDataset(
81115
name: packageName,
82116
type: 'line',
83117
series: dataset.map(d => d.downloads),
84-
color: '#8A8A8A',
118+
color: accent.value,
85119
},
86120
],
87121
dates: dataset.map(d => `${d.weekStart}\nto ${d.weekEnd}`),
@@ -94,7 +128,7 @@ function formatXyDataset(
94128
name: packageName,
95129
type: 'line',
96130
series: dataset.map(d => d.downloads),
97-
color: '#8A8A8A',
131+
color: accent.value,
98132
},
99133
],
100134
dates: dataset.map(d => d.day),
@@ -107,7 +141,7 @@ function formatXyDataset(
107141
name: packageName,
108142
type: 'line',
109143
series: dataset.map(d => d.downloads),
110-
color: '#8A8A8A',
144+
color: accent.value,
111145
},
112146
],
113147
dates: dataset.map(d => d.month),
@@ -120,7 +154,7 @@ function formatXyDataset(
120154
name: packageName,
121155
type: 'line',
122156
series: dataset.map(d => d.downloads),
123-
color: '#8A8A8A',
157+
color: accent.value,
124158
},
125159
],
126160
dates: dataset.map(d => d.year),
@@ -381,7 +415,7 @@ const chartData = computed<{ dataset: VueUiXyDatasetItem[] | null; dates: string
381415
const formatter = ({ value }: { value: number }) => formatCompactNumber(value, { decimals: 1 })
382416
383417
const config = computed(() => ({
384-
theme: 'dark',
418+
theme: isDarkMode.value ? 'dark' : 'default',
385419
chart: {
386420
userOptions: {
387421
buttons: {
@@ -392,8 +426,9 @@ const config = computed(() => ({
392426
tooltip: false,
393427
},
394428
},
395-
backgroundColor: '#0A0A0A', // current default dark mode theme,
429+
backgroundColor: isDarkMode.value ? '#0A0A0A' : '#FFFFFF',
396430
grid: {
431+
stroke: isDarkMode.value ? '#4A4A4A' : '#a3a3a3',
397432
labels: {
398433
axis: {
399434
yLabel: $t('package.downloads.y_axis_label', { granularity: selectedGranularity.value }),
@@ -419,7 +454,7 @@ const config = computed(() => ({
419454
show: false, // As long as a single package is displayed
420455
},
421456
tooltip: {
422-
borderColor: '#2A2A2A',
457+
borderColor: 'transparent',
423458
backdropFilter: false,
424459
backgroundColor: 'transparent',
425460
customFormat: ({
@@ -431,23 +466,25 @@ const config = computed(() => ({
431466
}) => {
432467
if (!datapoint) return ''
433468
const displayValue = formatter({ value: datapoint[0]?.value ?? 0 })
434-
return `<div class="flex flex-col font-mono text-xs p-3 bg-[#0A0A0A]/10 backdrop-blur-md">
469+
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">
435470
<span class="text-fg-subtle">${chartData.value?.dates[absoluteIndex]}</span>
436471
<span class="text-xl">${displayValue}</span>
437472
</div>
438473
`
439474
},
440475
},
441476
zoom: {
442-
highlightColor: '#2A2A2A',
477+
highlightColor: isDarkMode.value ? '#2A2A2A' : '#E1E5E8',
443478
minimap: {
444479
show: true,
445480
lineColor: '#FAFAFA',
446-
selectedColorOpacity: 0.1,
447-
frameColor: '#3A3A3A',
481+
selectedColor: accent.value,
482+
selectedColorOpacity: 0.06,
483+
frameColor: isDarkMode.value ? '#3A3A3A' : '#a3a3a3',
448484
},
449485
preview: {
450-
fill: '#FAFAFA05',
486+
fill: accent.value + 10,
487+
stroke: accent.value + 60,
451488
strokeWidth: 1,
452489
strokeDasharray: 3,
453490
},
@@ -471,12 +508,12 @@ const config = computed(() => ({
471508
</label>
472509

473510
<div
474-
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)"
511+
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)"
475512
>
476513
<select
477514
id="granularity"
478515
v-model="selectedGranularity"
479-
class="w-full bg-transparent font-mono text-sm text-fg outline-none"
516+
class="w-full bg-transparent font-mono text-sm text-fg outline-none appearance-none"
480517
>
481518
<option value="daily">{{ $t('package.downloads.granularity_daily') }}</option>
482519
<option value="weekly">{{ $t('package.downloads.granularity_weekly') }}</option>
@@ -496,7 +533,7 @@ const config = computed(() => ({
496533
{{ $t('package.downloads.start_date') }}
497534
</label>
498535
<div
499-
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)"
536+
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)"
500537
>
501538
<span class="i-carbon-calendar w-4 h-4 text-fg-subtle shrink-0" aria-hidden="true" />
502539
<input
@@ -516,7 +553,7 @@ const config = computed(() => ({
516553
{{ $t('package.downloads.end_date') }}
517554
</label>
518555
<div
519-
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)"
556+
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)"
520557
>
521558
<span class="i-carbon-calendar w-4 h-4 text-fg-subtle shrink-0" aria-hidden="true" />
522559
<input
@@ -650,16 +687,17 @@ const config = computed(() => ({
650687

651688
<style>
652689
.vue-ui-pen-and-paper-actions {
653-
background: #1a1a1a !important;
690+
background: var(--bg-elevated) !important;
654691
}
655692
656693
.vue-ui-pen-and-paper-action {
657-
background: #1a1a1a !important;
694+
background: var(--bg-elevated) !important;
658695
border: none !important;
659696
}
660697
661698
.vue-ui-pen-and-paper-action:hover {
662-
background: #2a2a2a !important;
699+
background: var(--bg-elevated) !important;
700+
box-shadow: none !important;
663701
}
664702
665703
.vue-data-ui-zoom {

app/components/PackageWeeklyDownloadStats.vue

Lines changed: 80 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,41 @@ const createdIso = computed(() => packument.value?.time?.created ?? null)
1313
1414
const { fetchPackageDownloadEvolution } = useCharts()
1515
16+
const { accentColors, selectedAccentColor } = useAccentColor()
17+
18+
const colorMode = useColorMode()
19+
20+
const resolvedMode = ref<'light' | 'dark'>('light')
21+
22+
onMounted(() => {
23+
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
24+
})
25+
26+
watch(
27+
() => colorMode.value,
28+
value => {
29+
resolvedMode.value = value === 'dark' ? 'dark' : 'light'
30+
},
31+
{ flush: 'sync' },
32+
)
33+
34+
const isDarkMode = computed(() => resolvedMode.value === 'dark')
35+
36+
const accentColorValueById = computed<Record<string, string>>(() => {
37+
const map: Record<string, string> = {}
38+
for (const item of accentColors) {
39+
map[item.id] = item.value
40+
}
41+
return map
42+
})
43+
44+
const accent = computed(() => {
45+
const id = selectedAccentColor.value
46+
return id ? (oklchToHex(accentColorValueById.value[id]!) ?? '#8A8A8A') : '#8A8A8A'
47+
})
48+
49+
const pulseColor = computed(() => (selectedAccentColor.value ? accent.value : '#8A8A8A'))
50+
1651
const weeklyDownloads = ref<WeeklyDownloadPoint[]>([])
1752
1853
async function loadWeeklyDownloads() {
@@ -51,52 +86,55 @@ const dataset = computed(() =>
5186
5287
const lastDatapoint = computed(() => dataset.value.at(-1)?.period ?? '')
5388
54-
const config = computed(() => ({
55-
theme: 'dark',
56-
style: {
57-
backgroundColor: 'transparent',
58-
animation: { show: false },
59-
area: {
60-
color: 'oklch(0.5243 0 0)', // css variable doesn't seem to work here
61-
useGradient: false,
62-
opacity: 10,
63-
},
64-
dataLabel: {
65-
offsetX: -10,
66-
fontSize: 28,
67-
bold: false,
68-
color: 'var(--fg)',
69-
},
70-
line: {
71-
color: 'var(--fg-subtle)',
72-
pulse: {
73-
show: true,
74-
loop: true, // runs only once if false
75-
radius: 2,
76-
color: 'var(--fg-muted)',
77-
easing: 'ease-in-out',
78-
trail: {
89+
// oklh or css variables are not supported by vue-data-ui (for now)
90+
const config = computed(() => {
91+
return {
92+
theme: 'dark',
93+
style: {
94+
backgroundColor: 'transparent',
95+
animation: { show: false },
96+
area: {
97+
color: '#6A6A6A',
98+
useGradient: false,
99+
opacity: 10,
100+
},
101+
dataLabel: {
102+
offsetX: -10,
103+
fontSize: 28,
104+
bold: false,
105+
color: isDarkMode.value ? '#8a8a8a' : '#696969',
106+
},
107+
line: {
108+
color: '#696969',
109+
pulse: {
79110
show: true,
80-
length: 6,
111+
loop: true, // runs only once if false
112+
radius: 2,
113+
color: pulseColor.value,
114+
easing: 'ease-in-out',
115+
trail: {
116+
show: true,
117+
length: 6,
118+
},
81119
},
82120
},
121+
plot: {
122+
radius: 6,
123+
stroke: isDarkMode.value ? '#FAFAFA' : '#0A0A0A',
124+
},
125+
title: {
126+
text: lastDatapoint.value,
127+
fontSize: 12,
128+
color: '#8a8a8a',
129+
bold: false,
130+
},
131+
verticalIndicator: {
132+
strokeDasharray: 0,
133+
color: isDarkMode.value ? '#FAFAFA' : '#525252',
134+
},
83135
},
84-
plot: {
85-
radius: 6,
86-
stroke: 'var(--fg)',
87-
},
88-
title: {
89-
text: lastDatapoint.value,
90-
fontSize: 12,
91-
color: 'var(--fg)',
92-
bold: false,
93-
},
94-
verticalIndicator: {
95-
strokeDasharray: 0,
96-
color: 'var(--fg-muted)',
97-
},
98-
},
99-
}))
136+
}
137+
})
100138
</script>
101139

102140
<template>

app/utils/colors.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Vue Data UI does not support CSS vars nor OKLCH for now
2+
export function oklchToHex(color: string | undefined | null): string | undefined | null {
3+
if (color == null) return color
4+
5+
const match = color.trim().match(/^oklch\(\s*([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\s*\)$/i)
6+
7+
if (!match) {
8+
throw new Error('Invalid OKLCH color format')
9+
}
10+
11+
const lightness = Number(match[1])
12+
const chroma = Number(match[2])
13+
const hue = Number(match[3])
14+
15+
const hRad = (hue * Math.PI) / 180
16+
17+
const a = chroma * Math.cos(hRad)
18+
const b = chroma * Math.sin(hRad)
19+
20+
let l_ = lightness + 0.3963377774 * a + 0.2158037573 * b
21+
let m_ = lightness - 0.1055613458 * a - 0.0638541728 * b
22+
let s_ = lightness - 0.0894841775 * a - 1.291485548 * b
23+
24+
l_ = l_ ** 3
25+
m_ = m_ ** 3
26+
s_ = s_ ** 3
27+
28+
let r = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_
29+
let g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_
30+
let bRgb = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_
31+
32+
const toSrgb = (value: number): number =>
33+
value <= 0.0031308 ? 12.92 * value : 1.055 * Math.pow(value, 1 / 2.4) - 0.055
34+
35+
r = toSrgb(r)
36+
g = toSrgb(g)
37+
bRgb = toSrgb(bRgb)
38+
39+
const toHex = (value: number): string =>
40+
Math.round(Math.min(Math.max(0, value), 1) * 255)
41+
.toString(16)
42+
.padStart(2, '0')
43+
44+
return `#${toHex(r)}${toHex(g)}${toHex(bRgb)}`
45+
}

0 commit comments

Comments
 (0)