-
-
Notifications
You must be signed in to change notification settings - Fork 379
feat: compare download charts with sparklines #2273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
graphieros
merged 38 commits into
main
from
2270-line-charts-multiple-series---propose-a-split-view-option-to-display-series-in-individual-sparklines
Mar 27, 2026
Merged
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
834aebb
chore: add translations
graphieros f716beb
feat: create split parklines component
graphieros f0e5637
feat: implement split view for compare download charts
graphieros 1407af3
[autofix.ci] apply automated fixes
autofix-ci[bot] 509914e
fix: follow the rabbit
graphieros d395997
[autofix.ci] apply automated fixes
autofix-ci[bot] 08ace72
fix: add missing translation for aria-label
graphieros a58127f
fix: follow the rabbit
graphieros 22a4208
chore: bump vue-data-ui from 3.16.1 to 3.16.2
graphieros 02eaa7d
feat: apply dashed estimation segments on compare sparklines
graphieros d037a79
[autofix.ci] apply automated fixes
autofix-ci[bot] 9ff2919
fix: add missing prop in test
graphieros d7ba1fa
[autofix.ci] apply automated fixes
autofix-ci[bot] 965a99c
Merge branch 'main' into 2270-line-charts-multiple-series---propose-a…
graphieros 19b6bba
fix: use treeshaken import
graphieros 8f07f9d
chore: bump vue-data-ui from 3.16.3 to 3.16.4
graphieros ebb9831
fix: unify imports
graphieros e60df0a
[autofix.ci] apply automated fixes
autofix-ci[bot] a3bf620
chore: bump vue-data-ui from 3.16.4 to 3.16.5
graphieros a72ddb6
chore: remove test vue-data-ui tgz file
graphieros 7fb1a2a
fix: reset selectedIndex on esc
graphieros 102a96d
[autofix.ci] apply automated fixes
autofix-ci[bot] 315eda2
feat(ui): add reusable Tab component and wire URL persistence
Adebesin-Cell d321354
Merge branch 'main' into 2270-line-charts-multiple-series---propose-a…
graphieros 8221712
test: add Tab component tests and a11y coverage
Adebesin-Cell 8518b37
chore: remove comments from Tab components
Adebesin-Cell 8187c56
fix: always persist chart layout tab state in URL
Adebesin-Cell cdec42f
feat: add Tab component + tests (#2282)
graphieros c4a4d9c
fix: resolve lint errors and type check failures
Adebesin-Cell d5872e2
fix: resolve lint errors and type check failures (#2285)
graphieros a057d6d
[autofix.ci] apply automated fixes
autofix-ci[bot] 2ced70f
feat: add subtle gradient on sparklines in dark mode
graphieros dc4d902
[autofix.ci] apply automated fixes
autofix-ci[bot] ffb5ead
fix: use future proof series colour
graphieros 67db049
chore: add comment
graphieros e698ad1
fix: apply suggested refactoring
graphieros 2127638
[autofix.ci] apply automated fixes
autofix-ci[bot] ecb8ca5
fix: error message
graphieros File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,254 @@ | ||
| <script setup lang="ts"> | ||
| import { VueUiSparkline } from 'vue-data-ui/vue-ui-sparkline' | ||
| import { useCssVariables } from '~/composables/useColors' | ||
| import { | ||
| type VueUiSparklineConfig, | ||
| type VueUiSparklineDatasetItem, | ||
| type VueUiXyDatasetItem, | ||
| } from 'vue-data-ui' | ||
| import { getPalette, lightenColor } from 'vue-data-ui/utils' | ||
|
|
||
| import('vue-data-ui/style.css') | ||
|
|
||
| const props = defineProps<{ | ||
| dataset?: Array< | ||
| VueUiXyDatasetItem & { | ||
| color?: string | ||
| series: number[] | ||
| dashIndices?: number[] | ||
| } | ||
| > | ||
| dates: number[] | ||
| datetimeFormatterOptions: { | ||
| year: string | ||
| month: string | ||
| day: string | ||
| } | ||
| showLastDatapointEstimation: boolean | ||
| }>() | ||
|
|
||
| const { locale } = useI18n() | ||
| const colorMode = useColorMode() | ||
| const resolvedMode = shallowRef<'light' | 'dark'>('light') | ||
| const rootEl = shallowRef<HTMLElement | null>(null) | ||
| const palette = getPalette('') | ||
|
|
||
| const step = ref(0) | ||
|
|
||
| onMounted(() => { | ||
| rootEl.value = document.documentElement | ||
| }) | ||
|
|
||
| watch( | ||
| () => colorMode.value, | ||
| value => { | ||
| resolvedMode.value = value === 'dark' ? 'dark' : 'light' | ||
| }, | ||
| { flush: 'sync', immediate: true }, | ||
| ) | ||
|
|
||
graphieros marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const { colors } = useCssVariables( | ||
| [ | ||
| '--bg', | ||
| '--fg', | ||
| '--bg-subtle', | ||
| '--bg-elevated', | ||
| '--border-hover', | ||
| '--fg-subtle', | ||
| '--border', | ||
| '--border-subtle', | ||
| ], | ||
| { | ||
| element: rootEl, | ||
| watchHtmlAttributes: true, | ||
| watchResize: false, // set to true only if a var changes color on resize | ||
| }, | ||
| ) | ||
|
|
||
| const isDarkMode = computed(() => resolvedMode.value === 'dark') | ||
|
|
||
| const datasets = computed<VueUiSparklineDatasetItem[][]>(() => { | ||
| return (props.dataset ?? []).map(unit => { | ||
| return props.dates.map((period, i) => { | ||
| return { | ||
| period, | ||
| value: unit.series[i] ?? 0, | ||
| } | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| const selectedIndex = ref<number | undefined | null>(null) | ||
|
|
||
| function hoverIndex({ index }: { index: number | undefined | null }) { | ||
| if (typeof index === 'number') { | ||
| selectedIndex.value = index | ||
| } | ||
| } | ||
|
|
||
| function resetHover() { | ||
| selectedIndex.value = null | ||
| step.value += 1 // required to reset all chart instances | ||
| } | ||
|
|
||
| const configs = computed(() => { | ||
| return (props.dataset || []).map<VueUiSparklineConfig>((unit, i) => { | ||
| const lastIndex = unit.series.length - 1 | ||
| const dashIndices = props.showLastDatapointEstimation | ||
| ? Array.from(new Set([...(unit.dashIndices ?? []), lastIndex])) | ||
| : unit.dashIndices | ||
|
|
||
| // Ensure we loop through available palette colours when the series count is higher than the avalable palette | ||
| const fallbackColor = palette[i] ?? palette[i % palette.length] ?? palette[0]! | ||
| const seriesColor = unit.color ?? fallbackColor | ||
| const lightenedSeriesColor: string = unit.color | ||
| ? (lightenOklch(unit.color, 0.5) ?? seriesColor) | ||
| : (lightenColor(seriesColor, 0.5) ?? seriesColor) // palette uses hex colours | ||
|
|
||
| return { | ||
| a11y: { | ||
| translations: { | ||
| keyboardNavigation: $t( | ||
| 'package.trends.chart_assistive_text.keyboard_navigation_horizontal', | ||
| ), | ||
| tableAvailable: $t('package.trends.chart_assistive_text.table_available'), | ||
| tableCaption: $t('package.trends.chart_assistive_text.table_caption'), | ||
| }, | ||
| }, | ||
| theme: isDarkMode.value ? 'dark' : '', | ||
| temperatureColors: { | ||
| show: isDarkMode.value, | ||
| colors: [lightenedSeriesColor, seriesColor], | ||
| }, | ||
| skeletonConfig: { | ||
| style: { | ||
| backgroundColor: 'transparent', | ||
| dataLabel: { | ||
| show: true, | ||
| color: 'transparent', | ||
| }, | ||
| area: { | ||
| color: colors.value.borderHover, | ||
| useGradient: false, | ||
| opacity: 10, | ||
| }, | ||
| line: { | ||
| color: colors.value.borderHover, | ||
| }, | ||
| }, | ||
| }, | ||
| skeletonDataset: Array.from({ length: unit.series.length }, () => 0), | ||
| style: { | ||
| backgroundColor: 'transparent', | ||
| animation: { show: false }, | ||
| area: { | ||
| color: colors.value.borderHover, | ||
| useGradient: false, | ||
| opacity: 10, | ||
| }, | ||
| dataLabel: { | ||
| offsetX: -12, | ||
| fontSize: 24, | ||
| bold: false, | ||
| color: colors.value.fg, | ||
| datetimeFormatter: { | ||
| enable: true, | ||
| locale: locale.value, | ||
| useUTC: true, | ||
| options: props.datetimeFormatterOptions, | ||
| }, | ||
| }, | ||
| line: { | ||
| color: seriesColor, | ||
| dashIndices, | ||
| dashArray: 3, | ||
| }, | ||
| plot: { | ||
| radius: 6, | ||
| stroke: isDarkMode.value ? 'oklch(0.985 0 0)' : 'oklch(0.145 0 0)', | ||
| }, | ||
| title: { | ||
| fontSize: 12, | ||
| color: colors.value.fgSubtle, | ||
| bold: false, | ||
| }, | ||
|
|
||
| verticalIndicator: { | ||
| strokeDasharray: 0, | ||
| color: colors.value.fgSubtle, | ||
| }, | ||
| padding: { | ||
| left: 0, | ||
| right: 0, | ||
| top: 0, | ||
| bottom: 0, | ||
| }, | ||
| }, | ||
| } | ||
| }) | ||
| }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="grid gap-8 sm:grid-cols-2"> | ||
| <ClientOnly v-for="(config, i) in configs" :key="`config_${i}`"> | ||
| <div @mouseleave="resetHover" @keydown.esc="resetHover" class="w-full max-w-[400px] mx-auto"> | ||
| <div class="flex gap-2 place-items-center"> | ||
| <div class="h-3 w-3"> | ||
| <svg viewBox="0 0 2 2" class="w-full"> | ||
| <rect | ||
| x="0" | ||
| y="0" | ||
| width="2" | ||
| height="2" | ||
| rx="0.3" | ||
| :fill="dataset?.[i]?.color ?? palette[i]" | ||
| /> | ||
| </svg> | ||
| </div> | ||
| {{ applyEllipsis(dataset?.[i]?.name ?? '', 28) }} | ||
| </div> | ||
| <VueUiSparkline | ||
| :key="`${i}_${step}`" | ||
| :config | ||
| :dataset="datasets?.[i]" | ||
| :selectedIndex | ||
| @hoverIndex="hoverIndex" | ||
| > | ||
| <!-- Keyboard navigation hint --> | ||
| <template #hint="{ isVisible }"> | ||
| <p v-if="isVisible" class="text-accent text-xs text-center mt-2" aria-hidden="true"> | ||
| {{ $t('package.downloads.sparkline_nav_hint') }} | ||
| </p> | ||
| </template> | ||
|
|
||
| <template #skeleton> | ||
| <!-- This empty div overrides the default built-in scanning animation on load --> | ||
| <div /> | ||
| </template> | ||
| </VueUiSparkline> | ||
| </div> | ||
|
|
||
| <template #fallback> | ||
| <!-- Skeleton matching VueUiSparkline layout (title 24px + SVG aspect 500:80) --> | ||
| <div class="max-w-xs"> | ||
| <!-- Title row: fontSize * 2 = 24px --> | ||
| <div class="h-6 flex items-center ps-3"> | ||
| <SkeletonInline class="h-3 w-36" /> | ||
| </div> | ||
| <!-- Chart area: matches SVG viewBox 500:80 --> | ||
| <div class="aspect-[500/80] flex items-center"> | ||
| <!-- Data label (covers ~42% width, matching dataLabel.offsetX) --> | ||
| <div class="w-[42%] flex items-center ps-0.5"> | ||
| <SkeletonInline class="h-7 w-24" /> | ||
| </div> | ||
| <!-- Sparkline line placeholder --> | ||
| <div class="flex-1 flex items-end pe-3"> | ||
| <SkeletonInline class="h-px w-full" /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </template> | ||
| </ClientOnly> | ||
| </div> | ||
| </template> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.