Skip to content

Commit f0cf42e

Browse files
fix: use oklch colours in charts (#278)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent 3d3d04e commit f0cf42e

7 files changed

Lines changed: 466 additions & 122 deletions

File tree

app/components/PackageDownloadAnalytics.vue

Lines changed: 118 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { ref, computed, shallowRef, watch } from 'vue'
33
import type { VueUiXyDatasetItem } from 'vue-data-ui'
44
import { VueUiXy } from 'vue-data-ui/vue-ui-xy'
55
import { useDebounceFn, useElementSize } from '@vueuse/core'
6+
import { useCssVariables } from '../composables/useColors'
7+
import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch } from '../utils/colors'
68
79
const {
810
weeklyDownloads,
@@ -24,9 +26,19 @@ const rootEl = shallowRef<HTMLElement | null>(null)
2426
const { width } = useElementSize(rootEl)
2527
2628
onMounted(() => {
29+
rootEl.value = document.documentElement
2730
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
2831
})
2932
33+
const { colors } = useCssVariables(
34+
['--bg', '--bg-subtle', '--bg-elevated', '--fg-subtle', '--border', '--border-subtle'],
35+
{
36+
element: rootEl,
37+
watchHtmlAttributes: true,
38+
watchResize: false, // set to true only if a var changes color on resize
39+
},
40+
)
41+
3042
watch(
3143
() => colorMode.value,
3244
value => {
@@ -49,7 +61,9 @@ const accentColorValueById = computed<Record<string, string>>(() => {
4961
5062
const accent = computed(() => {
5163
const id = selectedAccentColor.value
52-
return id ? (oklchToHex(accentColorValueById.value[id]!) ?? '#8A8A8A') : '#8A8A8A'
64+
return id
65+
? (accentColorValueById.value[id] ?? colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK)
66+
: (colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK)
5367
})
5468
5569
const mobileBreakpointWidth = 640
@@ -58,10 +72,6 @@ const isMobile = computed(() => {
5872
return width.value > 0 && width.value < mobileBreakpointWidth
5973
})
6074
61-
onMounted(() => {
62-
rootEl.value = document.documentElement
63-
})
64-
6575
type ChartTimeGranularity = 'daily' | 'weekly' | 'monthly' | 'yearly'
6676
type EvolutionData =
6777
| DailyDownloadPoint[]
@@ -444,119 +454,121 @@ const loadFile = (link: string, filename: string) => {
444454
a.remove()
445455
}
446456
447-
const config = computed(() => ({
448-
theme: isDarkMode.value ? 'dark' : 'default',
449-
chart: {
450-
height: isMobile.value ? 850 : 600,
451-
userOptions: {
452-
buttons: {
453-
pdf: false,
454-
labels: false,
455-
fullscreen: false,
456-
table: false,
457-
tooltip: false,
458-
},
459-
callbacks: {
460-
img: ({ imageUri }: { imageUri: string }) => {
461-
loadFile(
462-
imageUri,
463-
`${packageName}-${selectedGranularity.value}_${startDate.value}_${endDate.value}.png`,
464-
)
465-
},
466-
csv: (csvStr: string) => {
467-
const blob = new Blob([csvStr.replace('data:text/csv;charset=utf-8,', '')])
468-
const url = URL.createObjectURL(blob)
469-
loadFile(
470-
url,
471-
`${packageName}-${selectedGranularity.value}_${startDate.value}_${endDate.value}.csv`,
472-
)
473-
URL.revokeObjectURL(url)
457+
const config = computed(() => {
458+
return {
459+
theme: isDarkMode.value ? 'dark' : 'default',
460+
chart: {
461+
height: isMobile.value ? 850 : 600,
462+
userOptions: {
463+
buttons: {
464+
pdf: false,
465+
labels: false,
466+
fullscreen: false,
467+
table: false,
468+
tooltip: false,
474469
},
475-
svg: ({ blob }: { blob: Blob }) => {
476-
const url = URL.createObjectURL(blob)
477-
loadFile(
478-
url,
479-
`${packageName}-${selectedGranularity.value}_${startDate.value}_${endDate.value}.svg`,
480-
)
481-
URL.revokeObjectURL(url)
470+
callbacks: {
471+
img: ({ imageUri }: { imageUri: string }) => {
472+
loadFile(
473+
imageUri,
474+
`${packageName}-${selectedGranularity.value}_${startDate.value}_${endDate.value}.png`,
475+
)
476+
},
477+
csv: (csvStr: string) => {
478+
const blob = new Blob([csvStr.replace('data:text/csv;charset=utf-8,', '')])
479+
const url = URL.createObjectURL(blob)
480+
loadFile(
481+
url,
482+
`${packageName}-${selectedGranularity.value}_${startDate.value}_${endDate.value}.csv`,
483+
)
484+
URL.revokeObjectURL(url)
485+
},
486+
svg: ({ blob }: { blob: Blob }) => {
487+
const url = URL.createObjectURL(blob)
488+
loadFile(
489+
url,
490+
`${packageName}-${selectedGranularity.value}_${startDate.value}_${endDate.value}.svg`,
491+
)
492+
URL.revokeObjectURL(url)
493+
},
482494
},
483495
},
484-
},
485-
backgroundColor: isDarkMode.value ? '#0A0A0A' : '#FFFFFF',
486-
grid: {
487-
stroke: isDarkMode.value ? '#4A4A4A' : '#a3a3a3',
488-
labels: {
489-
axis: {
490-
yLabel: $t('package.downloads.y_axis_label', {
491-
granularity: $t(`package.downloads.granularity_${selectedGranularity.value}`),
492-
}),
493-
xLabel: packageName,
494-
yLabelOffsetX: 12,
495-
fontSize: 24,
496-
},
497-
xAxisLabels: {
498-
values: chartData.value?.dates,
499-
showOnlyAtModulo: true,
500-
modulo: 12,
501-
},
502-
yAxis: {
503-
formatter,
504-
useNiceScale: true,
496+
backgroundColor: colors.value.bg,
497+
grid: {
498+
stroke: colors.value.border,
499+
labels: {
500+
axis: {
501+
yLabel: $t('package.downloads.y_axis_label', {
502+
granularity: $t(`package.downloads.granularity_${selectedGranularity.value}`),
503+
}),
504+
xLabel: packageName,
505+
yLabelOffsetX: 12,
506+
fontSize: 24,
507+
},
508+
xAxisLabels: {
509+
values: chartData.value?.dates,
510+
showOnlyAtModulo: true,
511+
modulo: 12,
512+
},
513+
yAxis: {
514+
formatter,
515+
useNiceScale: true,
516+
},
505517
},
506518
},
507-
},
508-
highlighter: {
509-
useLine: true,
510-
},
511-
legend: {
512-
show: false, // As long as a single package is displayed
513-
},
514-
tooltip: {
515-
borderColor: 'transparent',
516-
backdropFilter: false,
517-
backgroundColor: 'transparent',
518-
customFormat: ({
519-
absoluteIndex,
520-
datapoint,
521-
}: {
522-
absoluteIndex: number
523-
datapoint: Record<string, any>
524-
}) => {
525-
if (!datapoint) return ''
526-
const displayValue = formatter({ value: datapoint[0]?.value ?? 0 })
527-
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">
519+
highlighter: {
520+
useLine: true,
521+
},
522+
legend: {
523+
show: false, // As long as a single package is displayed
524+
},
525+
tooltip: {
526+
borderColor: 'transparent',
527+
backdropFilter: false,
528+
backgroundColor: 'transparent',
529+
customFormat: ({
530+
absoluteIndex,
531+
datapoint,
532+
}: {
533+
absoluteIndex: number
534+
datapoint: Record<string, any>
535+
}) => {
536+
if (!datapoint) return ''
537+
const displayValue = formatter({ value: datapoint[0]?.value ?? 0 })
538+
return `<div class="flex flex-col font-mono text-xs p-3 border border-border rounded-md bg-[var(--bg)]/10 backdrop-blur-md">
528539
<span class="text-fg-subtle">${chartData.value?.dates[absoluteIndex]}</span>
529540
<span class="text-xl">${displayValue}</span>
530541
</div>
531542
`
543+
},
532544
},
533-
},
534-
zoom: {
535-
maxWidth: 500,
536-
customFormat:
537-
displayedGranularity.value !== 'weekly'
538-
? undefined
539-
: ({ absoluteIndex, side }: { absoluteIndex: number; side: 'left' | 'right' }) => {
540-
const parts = extractDates(chartData.value.dates[absoluteIndex] ?? '')
541-
return side === 'left' ? parts[0] : parts.at(-1)
542-
},
543-
highlightColor: isDarkMode.value ? '#2A2A2A' : '#E1E5E8',
544-
minimap: {
545-
show: true,
546-
lineColor: '#FAFAFA',
547-
selectedColor: accent.value,
548-
selectedColorOpacity: 0.06,
549-
frameColor: isDarkMode.value ? '#3A3A3A' : '#a3a3a3',
550-
},
551-
preview: {
552-
fill: accent.value + 10,
553-
stroke: accent.value + 60,
554-
strokeWidth: 1,
555-
strokeDasharray: 3,
545+
zoom: {
546+
maxWidth: 500,
547+
customFormat:
548+
displayedGranularity.value !== 'weekly'
549+
? undefined
550+
: ({ absoluteIndex, side }: { absoluteIndex: number; side: 'left' | 'right' }) => {
551+
const parts = extractDates(chartData.value.dates[absoluteIndex] ?? '')
552+
return side === 'left' ? parts[0] : parts.at(-1)
553+
},
554+
highlightColor: colors.value.bgElevated,
555+
minimap: {
556+
show: true,
557+
lineColor: '#FAFAFA',
558+
selectedColor: accent.value,
559+
selectedColorOpacity: 0.06,
560+
frameColor: colors.value.border,
561+
},
562+
preview: {
563+
fill: transparentizeOklch(accent.value, isDarkMode.value ? 0.95 : 0.92),
564+
stroke: transparentizeOklch(accent.value, 0.5),
565+
strokeWidth: 1,
566+
strokeDasharray: 3,
567+
},
556568
},
557569
},
558-
},
559-
}))
570+
}
571+
})
560572
</script>
561573

562574
<template>

app/components/PackageWeeklyDownloadStats.vue

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<script setup lang="ts">
22
import { ref, computed, onMounted, watch } from 'vue'
33
import { VueUiSparkline } from 'vue-data-ui/vue-ui-sparkline'
4+
import { useCssVariables } from '../composables/useColors'
5+
import { OKLCH_NEUTRAL_FALLBACK, lightenOklch } from '../utils/colors'
46
57
const { packageName } = defineProps<{
68
packageName: string
@@ -19,7 +21,10 @@ const colorMode = useColorMode()
1921
2022
const resolvedMode = ref<'light' | 'dark'>('light')
2123
24+
const rootEl = shallowRef<HTMLElement | null>(null)
25+
2226
onMounted(() => {
27+
rootEl.value = document.documentElement
2328
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
2429
})
2530
@@ -31,6 +36,24 @@ watch(
3136
{ flush: 'sync' },
3237
)
3338
39+
const { colors } = useCssVariables(
40+
[
41+
'--bg',
42+
'--fg',
43+
'--bg-subtle',
44+
'--bg-elevated',
45+
'--border-hover',
46+
'--fg-subtle',
47+
'--border',
48+
'--border-subtle',
49+
],
50+
{
51+
element: rootEl,
52+
watchHtmlAttributes: true,
53+
watchResize: false, // set to true only if a var changes color on resize
54+
},
55+
)
56+
3457
const isDarkMode = computed(() => resolvedMode.value === 'dark')
3558
3659
const accentColorValueById = computed<Record<string, string>>(() => {
@@ -43,14 +66,16 @@ const accentColorValueById = computed<Record<string, string>>(() => {
4366
4467
const accent = computed(() => {
4568
const id = selectedAccentColor.value
46-
return id ? (oklchToHex(accentColorValueById.value[id]!) ?? '#8A8A8A') : '#8A8A8A'
69+
return id
70+
? (accentColorValueById.value[id] ?? colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK)
71+
: (colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK)
4772
})
4873
4974
const pulseColor = computed(() => {
5075
if (!selectedAccentColor.value) {
51-
return isDarkMode.value ? '#BFBFBF' : '#E0E0E0'
76+
return colors.value.fgSubtle
5277
}
53-
return isDarkMode.value ? accent.value : lightenHex(accent.value, 0.5)
78+
return isDarkMode.value ? accent.value : lightenOklch(accent.value, 0.5)
5479
})
5580
5681
const weeklyDownloads = ref<WeeklyDownloadPoint[]>([])
@@ -99,18 +124,18 @@ const config = computed(() => {
99124
backgroundColor: 'transparent',
100125
animation: { show: false },
101126
area: {
102-
color: '#6A6A6A',
127+
color: colors.value.borderHover,
103128
useGradient: false,
104129
opacity: 10,
105130
},
106131
dataLabel: {
107132
offsetX: -10,
108133
fontSize: 28,
109134
bold: false,
110-
color: isDarkMode.value ? '#8a8a8a' : '#696969',
135+
color: colors.value.fg,
111136
},
112137
line: {
113-
color: isDarkMode.value ? '#4a4a4a' : '#525252',
138+
color: colors.value.borderHover,
114139
pulse: {
115140
show: true,
116141
loop: true, // runs only once if false
@@ -125,17 +150,17 @@ const config = computed(() => {
125150
},
126151
plot: {
127152
radius: 6,
128-
stroke: isDarkMode.value ? '#FAFAFA' : '#0A0A0A',
153+
stroke: isDarkMode.value ? 'oklch(0.985 0 0)' : 'oklch(0.145 0 0)',
129154
},
130155
title: {
131156
text: lastDatapoint.value,
132157
fontSize: 12,
133-
color: isDarkMode.value ? '#8a8a8a' : '#696969',
158+
color: colors.value.fgSubtle,
134159
bold: false,
135160
},
136161
verticalIndicator: {
137162
strokeDasharray: 0,
138-
color: isDarkMode.value ? '#FAFAFA' : '#525252',
163+
color: isDarkMode.value ? 'oklch(0.985 0 0)' : colors.value.fgSubtle,
139164
},
140165
},
141166
}

0 commit comments

Comments
 (0)