Skip to content

Commit 5da4549

Browse files
committed
feat: use patterns in tooltip markers
1 parent 438942c commit 5da4549

4 files changed

Lines changed: 167 additions & 16 deletions

File tree

app/components/Chart/PatternSlot.vue

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,9 @@
66
* series in the context of data visualisation.
77
*/
88
import { computed } from 'vue'
9-
import { createSeededSvgPattern } from '~/utils/charts'
9+
import { createSeededSvgPattern, type ChartPatternSlotProps } from '~/utils/charts'
1010
11-
const props = defineProps<{
12-
id: string
13-
seed: string | number
14-
color?: string
15-
foregroundColor: string
16-
fallbackColor: string
17-
maxSize: number
18-
minSize: number
19-
}>()
11+
const props = defineProps<ChartPatternSlotProps>()
2012
2113
const pattern = computed(() =>
2214
createSeededSvgPattern(props.seed, {

app/components/Compare/FacetBarChart.vue

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
33
import { VueUiHorizontalBar } from 'vue-data-ui/vue-ui-horizontal-bar'
44
import type { VueUiHorizontalBarConfig, VueUiHorizontalBarDatasetItem } from 'vue-data-ui'
55
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
6+
import { createChartPatternSlotMarkup } from '~/utils/charts'
67
import { drawSmallNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
78
89
import {
@@ -214,13 +215,40 @@ const config = computed<VueUiHorizontalBarConfig>(() => {
214215
backdropFilter: false,
215216
backgroundColor: 'transparent',
216217
customFormat: ({ datapoint }) => {
217-
const name = datapoint?.name?.replace(/\n/g, '<br>')
218-
return `
218+
const name = datapoint?.name?.replace(/\n/g, '<br>') ?? ''
219+
const safeSeriesIndex = (datapoint?.absoluteIndex as number) ?? 0
220+
const patternId = `tooltip-pattern-${safeSeriesIndex}`
221+
const usePattern = safeSeriesIndex !== 0
222+
223+
const patternMarkup = usePattern
224+
? createChartPatternSlotMarkup({
225+
id: patternId,
226+
seed: safeSeriesIndex,
227+
foregroundColor: colors.value.bg!,
228+
fallbackColor: 'transparent',
229+
maxSize: 24,
230+
minSize: 16,
231+
})
232+
: ''
233+
234+
const markerMarkup = usePattern
235+
? `
236+
<rect x="0" y="0" width="20" height="20" rx="3" fill="${datapoint?.color ?? 'transparent'}" />
237+
<rect x="0" y="0" width="20" height="20" rx="3" fill="url(#${patternId})" />
238+
`
239+
: `
240+
<rect x="0" y="0" width="20" height="20" rx="3" fill="${datapoint?.color ?? 'transparent'}" />
241+
`
242+
243+
return `
219244
<div class="font-mono p-3 border border-border rounded-md bg-[var(--bg)]/10 backdrop-blur-md">
220245
<div class="grid grid-cols-[12px_minmax(0,1fr)_max-content] items-center gap-x-3">
221246
<div class="w-3 h-3">
222-
<svg viewBox="0 0 2 2" class="w-full h-full">
223-
<rect x="0" y="0" width="2" height="2" rx="0.3" fill="${datapoint?.color}" />
247+
<svg viewBox="0 0 20 20" class="w-full h-full" aria-hidden="true">
248+
<defs>
249+
${patternMarkup}
250+
</defs>
251+
${markerMarkup}
224252
</svg>
225253
</div>
226254
<span class="text-3xs uppercase tracking-wide text-[var(--fg)]/70 truncate">
@@ -231,8 +259,8 @@ const config = computed<VueUiHorizontalBarConfig>(() => {
231259
</span>
232260
</div>
233261
</div>
234-
`
235-
},
262+
`
263+
}
236264
},
237265
},
238266
},

app/utils/charts.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,3 +1055,43 @@ export function createSeededSvgPattern(
10551055
contentMarkup,
10561056
}
10571057
}
1058+
1059+
export type ChartPatternSlotProps = {
1060+
id: string
1061+
seed: string | number
1062+
color?: string
1063+
foregroundColor: string
1064+
fallbackColor: string
1065+
maxSize: number
1066+
minSize: number
1067+
}
1068+
1069+
// Equivalent of the PatternSlot.vue component, to be used inside tooltip.customFormat in chart configs
1070+
export function createChartPatternSlotMarkup({
1071+
id,
1072+
seed,
1073+
color,
1074+
foregroundColor,
1075+
fallbackColor,
1076+
maxSize,
1077+
minSize,
1078+
}: ChartPatternSlotProps) {
1079+
const pattern = createSeededSvgPattern(seed, {
1080+
foregroundColor,
1081+
backgroundColor: color ?? fallbackColor,
1082+
minimumSize: minSize,
1083+
maximumSize: maxSize,
1084+
})
1085+
1086+
return `
1087+
<pattern
1088+
id="${id}"
1089+
patternUnits="userSpaceOnUse"
1090+
width="${pattern.width}"
1091+
height="${pattern.height}"
1092+
patternTransform="rotate(${pattern.rotation})"
1093+
>
1094+
${pattern.contentMarkup}
1095+
</pattern>
1096+
`
1097+
}

test/unit/app/utils/charts.spec.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
applyEllipsis,
1818
createSeedNumber,
1919
createSeededSvgPattern,
20+
createChartPatternSlotMarkup,
2021
type TrendLineConfig,
2122
type TrendLineDataset,
2223
type VersionsBarConfig,
@@ -1560,3 +1561,93 @@ describe('createSeededSvgPattern', () => {
15601561
expect(numericSeedResult).toEqual(stringSeedResult)
15611562
})
15621563
})
1564+
1565+
describe('createChartPatternSlotMarkup', () => {
1566+
it('returns a pattern element with the provided id', () => {
1567+
const result = createChartPatternSlotMarkup({
1568+
id: 'pattern-1',
1569+
seed: 7,
1570+
color: '#ff0000',
1571+
foregroundColor: '#ffffff',
1572+
fallbackColor: 'transparent',
1573+
maxSize: 24,
1574+
minSize: 16,
1575+
})
1576+
1577+
expect(result).toContain('<pattern')
1578+
expect(result).toContain('id="pattern-1"')
1579+
expect(result).toContain('patternUnits="userSpaceOnUse"')
1580+
expect(result).toContain('</pattern>')
1581+
})
1582+
1583+
it('includes width, height, rotation, and content markup from the generated pattern', () => {
1584+
const generatedPattern = createSeededSvgPattern(1, {
1585+
foregroundColor: '#000',
1586+
backgroundColor: 'transparent',
1587+
minimumSize: 16,
1588+
maximumSize: 24,
1589+
})
1590+
1591+
const result = createChartPatternSlotMarkup({
1592+
id: 'pattern-1',
1593+
seed: 1,
1594+
foregroundColor: '#000',
1595+
fallbackColor: 'transparent',
1596+
maxSize: 24,
1597+
minSize: 16,
1598+
})
1599+
1600+
expect(result).toContain(`width="${generatedPattern.width}"`)
1601+
expect(result).toContain(`height="${generatedPattern.height}"`)
1602+
expect(result).toContain(`patternTransform="rotate(${generatedPattern.rotation})"`)
1603+
expect(result).toContain(generatedPattern.contentMarkup)
1604+
})
1605+
1606+
it('is deterministic for the same inputs', () => {
1607+
const first = createChartPatternSlotMarkup({
1608+
id: 'pattern-stable',
1609+
seed: 'nuxt',
1610+
color: '#00ff00',
1611+
foregroundColor: '#000000',
1612+
fallbackColor: 'transparent',
1613+
maxSize: 40,
1614+
minSize: 10,
1615+
})
1616+
1617+
const second = createChartPatternSlotMarkup({
1618+
id: 'pattern-stable',
1619+
seed: 'nuxt',
1620+
color: '#00ff00',
1621+
foregroundColor: '#000000',
1622+
fallbackColor: 'transparent',
1623+
maxSize: 40,
1624+
minSize: 10,
1625+
})
1626+
1627+
expect(first).toBe(second)
1628+
})
1629+
1630+
it('changes when the id changes', () => {
1631+
const first = createChartPatternSlotMarkup({
1632+
id: 'pattern-a',
1633+
seed: 1,
1634+
color: '#00ff00',
1635+
foregroundColor: '#000000',
1636+
fallbackColor: 'transparent',
1637+
maxSize: 40,
1638+
minSize: 10,
1639+
})
1640+
1641+
const second = createChartPatternSlotMarkup({
1642+
id: 'pattern-b',
1643+
seed: 2,
1644+
color: '#00ff00',
1645+
foregroundColor: '#000000',
1646+
fallbackColor: 'transparent',
1647+
maxSize: 40,
1648+
minSize: 10,
1649+
})
1650+
1651+
expect(first).not.toBe(second)
1652+
})
1653+
})

0 commit comments

Comments
 (0)