@@ -12,7 +12,52 @@ const chartPayload = JSON.stringify({ data, valueFormat })
1212---
1313
1414<div class =" comparison-chart-container" >
15- <h3 class =" comparison-chart-title" >{ title } </h3 >
15+ <div class =" comparison-chart-header" >
16+ <h3 class =" comparison-chart-title" >{ title } </h3 >
17+ <details class =" sort-dropdown" >
18+ <summary class =" sort-trigger" >
19+ <svg
20+ class =" sort-icon"
21+ width =" 14"
22+ height =" 14"
23+ viewBox =" 0 0 14 14"
24+ fill =" none"
25+ aria-hidden =" true"
26+ >
27+ <path
28+ d =" M10 5L7 2 4 5"
29+ stroke =" currentColor"
30+ stroke-width =" 1.5"
31+ stroke-linecap =" round"
32+ stroke-linejoin =" round" ></path >
33+ <path
34+ d =" M4 9l3 3 3-3"
35+ stroke =" currentColor"
36+ stroke-width =" 1.5"
37+ stroke-linecap =" round"
38+ stroke-linejoin =" round" ></path >
39+ </svg >
40+ <span class =" sort-label" >Default</span >
41+ </summary >
42+ <menu class =" sort-menu" >
43+ <li >
44+ <button class =" sort-option active" data-sort =" default" type =" button"
45+ >Default</button
46+ >
47+ </li >
48+ <li >
49+ <button class =" sort-option" data-sort =" asc" type =" button"
50+ >↑ Low–High</button
51+ >
52+ </li >
53+ <li >
54+ <button class =" sort-option" data-sort =" desc" type =" button"
55+ >↓ High–Low</button
56+ >
57+ </li >
58+ </menu >
59+ </details >
60+ </div >
1661 <div class =" comparison-chart-wrapper" data-comparison ={ chartPayload } >
1762 <svg class =" comparison-bar-chart" role =" img" aria-label ={ title } ></svg >
1863 </div >
@@ -23,15 +68,139 @@ const chartPayload = JSON.stringify({ data, valueFormat })
2368 .comparison-chart-container {
2469 position: relative;
2570 width: 100%;
26- margin-top: 1em ;
71+ margin-top: 0 ;
2772 margin-bottom: 2em;
2873 }
2974
75+ .comparison-chart-header {
76+ display: flex;
77+ align-items: center;
78+ justify-content: space-between;
79+ gap: 1em;
80+ margin-bottom: 0.75em;
81+ flex-wrap: wrap;
82+ }
83+
3084 .comparison-chart-title {
3185 font-size: 16px;
3286 font-weight: 600;
3387 color: var(--ft-text);
34- margin: 0 0 0.75em 0;
88+ margin: 0;
89+ }
90+
91+ .sort-dropdown {
92+ position: relative;
93+ }
94+
95+ .sort-trigger {
96+ display: flex;
97+ align-items: center;
98+ gap: 5px;
99+ padding: 5px 10px;
100+ font-size: 12px;
101+ font-weight: 500;
102+ color: var(--ft-muted);
103+ background: transparent;
104+ border: 1px solid var(--ft-border);
105+ border-radius: 6px;
106+ cursor: pointer;
107+ font-family: inherit;
108+ list-style: none;
109+ user-select: none;
110+ transition:
111+ color 0.2s,
112+ border-color 0.2s;
113+ }
114+
115+ .sort-trigger::-webkit-details-marker {
116+ display: none;
117+ }
118+
119+ .sort-trigger::marker {
120+ display: none;
121+ }
122+
123+ .sort-trigger:hover {
124+ color: var(--ft-text);
125+ border-color: var(--ft-accent);
126+ }
127+
128+ .sort-trigger:focus-visible {
129+ outline: 2px solid var(--ft-accent);
130+ outline-offset: 2px;
131+ }
132+
133+ .sort-dropdown[open] .sort-trigger {
134+ color: var(--ft-accent);
135+ border-color: var(--ft-accent);
136+ }
137+
138+ .sort-icon {
139+ flex-shrink: 0;
140+ }
141+
142+ .sort-menu {
143+ position: absolute;
144+ top: calc(100% + 6px);
145+ right: 0;
146+ z-index: 50;
147+ min-width: 120px;
148+ background: var(--ft-bg);
149+ border: 1px solid var(--ft-border);
150+ border-radius: 8px;
151+ padding: 4px;
152+ margin: 0;
153+ list-style: none;
154+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
155+ opacity: 1;
156+ transform: translateY(0);
157+ transition:
158+ opacity 0.15s ease,
159+ transform 0.15s ease;
160+ }
161+
162+ @starting-style {
163+ .sort-dropdown[open] .sort-menu {
164+ opacity: 0;
165+ transform: translateY(-6px);
166+ }
167+ }
168+
169+ :global(html.dark) .sort-menu {
170+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
171+ }
172+
173+ .sort-option {
174+ display: block;
175+ width: 100%;
176+ padding: 6px 10px;
177+ font-size: 12px;
178+ font-weight: 500;
179+ color: var(--ft-muted);
180+ background: transparent;
181+ border: none;
182+ border-radius: 6px;
183+ cursor: pointer;
184+ font-family: inherit;
185+ text-align: left;
186+ transition:
187+ color 0.15s,
188+ background-color 0.15s;
189+ }
190+
191+ .sort-option:hover {
192+ color: var(--ft-text);
193+ background: var(--ft-bg-muted);
194+ }
195+
196+ .sort-option:focus-visible {
197+ outline: 2px solid var(--ft-accent);
198+ outline-offset: -2px;
199+ }
200+
201+ .sort-option.active {
202+ color: var(--ft-accent);
203+ font-weight: 600;
35204 }
36205
37206 .comparison-chart-wrapper {
@@ -118,6 +287,7 @@ const chartPayload = JSON.stringify({ data, valueFormat })
118287 HTMLElement,
119288 { width: number; height: number }
120289 >()
290+ const chartResizeObserverCache = new WeakMap<HTMLElement, ResizeObserver>()
121291
122292 function formatValue(
123293 value: number,
@@ -144,12 +314,20 @@ const chartPayload = JSON.stringify({ data, valueFormat })
144314 tooltip.replaceChildren(titleDiv, valueDiv)
145315 }
146316
317+ function getSortedData(data: ChartDatum[], sortMode: string): ChartDatum[] {
318+ if (sortMode === 'asc') return [...data].sort((a, b) => a.value - b.value)
319+ if (sortMode === 'desc') return [...data].sort((a, b) => b.value - a.value)
320+ return data
321+ }
322+
147323 function initComparisonChart(wrapper: HTMLElement) {
148324 const svg = wrapper.querySelector('.comparison-bar-chart') as SVGSVGElement
149- const container = wrapper.closest('.comparison-chart-container')
150- const tooltip = container?.querySelector(
325+ const container = wrapper.closest<HTMLElement>(
326+ '.comparison-chart-container',
327+ )
328+ const tooltip = container?.querySelector<HTMLElement>(
151329 '.comparison-chart-tooltip',
152- ) as HTMLElement
330+ )
153331 if (!svg || !tooltip) return
154332
155333 const dataAttr = wrapper.dataset.comparison
@@ -167,10 +345,14 @@ const chartPayload = JSON.stringify({ data, valueFormat })
167345 const rawData = payload.data
168346 if (!rawData?.length) return
169347
170- const data: ChartDatum[] = rawData.map((d) => ({
171- name: String(d?.name ?? ''),
172- value: Math.max(0, Number(d.value) || 0),
173- }))
348+ const sortMode = container?.dataset.sortMode ?? 'default'
349+ const data: ChartDatum[] = getSortedData(
350+ rawData.map((d) => ({
351+ name: String(d?.name ?? ''),
352+ value: Math.max(0, Number(d.value) || 0),
353+ })),
354+ sortMode,
355+ )
174356
175357 const margin = { top: 20, right: 40, bottom: 80, left: 50 }
176358 const rect = wrapper.getBoundingClientRect()
@@ -322,6 +504,7 @@ const chartPayload = JSON.stringify({ data, valueFormat })
322504 chartDimensionsCache.set(wrapper, { width, height })
323505
324506 if (typeof ResizeObserver !== 'undefined') {
507+ chartResizeObserverCache.get(wrapper)?.disconnect()
325508 const ro = new ResizeObserver(() => {
326509 const r = wrapper.getBoundingClientRect()
327510 const w = r.width || 0
@@ -331,17 +514,62 @@ const chartPayload = JSON.stringify({ data, valueFormat })
331514 const lastH = last?.height ?? 0
332515 if (w > 0 && h > 0 && (w !== lastW || h !== lastH)) {
333516 ro.disconnect()
517+ chartResizeObserverCache.delete(wrapper)
334518 initComparisonChart(wrapper)
335519 }
336520 })
337521 ro.observe(wrapper)
522+ chartResizeObserverCache.set(wrapper, ro)
338523 }
339524 }
340525
526+ function initSortControls() {
527+ document
528+ .querySelectorAll<HTMLDetailsElement>('.sort-dropdown')
529+ .forEach((dropdown) => {
530+ const container = dropdown.closest<HTMLElement>(
531+ '.comparison-chart-container',
532+ )
533+ const wrapper = container?.querySelector<HTMLElement>(
534+ '.comparison-chart-wrapper',
535+ )
536+ if (!container || !wrapper) return
537+
538+ const label = dropdown.querySelector<HTMLElement>('.sort-label')
539+ const options =
540+ dropdown.querySelectorAll<HTMLButtonElement>('.sort-option')
541+
542+ options.forEach((btn) => {
543+ btn.addEventListener('click', () => {
544+ options.forEach((b) => b.classList.remove('active'))
545+ btn.classList.add('active')
546+ if (label) label.textContent = btn.textContent
547+ container.dataset.sortMode = btn.dataset.sort ?? 'default'
548+ dropdown.open = false
549+ initComparisonChart(wrapper)
550+ })
551+ })
552+
553+ // Close on outside click
554+ document.addEventListener('click', (e) => {
555+ if (!dropdown.contains(e.target as Node)) dropdown.open = false
556+ })
557+
558+ // Close on Escape
559+ dropdown.addEventListener('keydown', (e) => {
560+ if (e.key === 'Escape') {
561+ dropdown.open = false
562+ dropdown.querySelector<HTMLElement>('.sort-trigger')?.focus()
563+ }
564+ })
565+ })
566+ }
567+
341568 function init() {
342569 document.querySelectorAll('.comparison-chart-wrapper').forEach((el) => {
343570 initComparisonChart(el as HTMLElement)
344571 })
572+ initSortControls()
345573 }
346574
347575 if (document.readyState === 'loading') {
0 commit comments