Skip to content

Commit d5f5f40

Browse files
committed
Fix: scrollable vertical legend for long lists
1 parent c745de1 commit d5f5f40

File tree

1 file changed

+142
-1
lines changed

1 file changed

+142
-1
lines changed

src/plugins/plugin.legend.js

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {_alignStartEnd, _textX, _toLeftRightCenter} from '../helpers/helpers.ext
1818
import {toTRBLCorners} from '../helpers/helpers.options.js';
1919

2020

21-
// @ts-ignore
2221

2322
/**
2423
* @typedef { import('../types/index.js').ChartEvent } ChartEvent
@@ -122,13 +121,24 @@ export class Legend extends Element {
122121

123122
this.legendItems = legendItems;
124123
}
124+
fit() {
125+
const { options, ctx } = this;
126+
if (!options.display) {
127+
this.width = this.height = 0;
128+
return;
129+
}
125130
fit() {
126131
const { options, ctx } = this;
127132
if (!options.display) {
128133
this.width = this.height = 0;
129134
return;
130135
}
131136

137+
const labelOpts = options.labels;
138+
const labelFont = toFont(labelOpts.font);
139+
const fontSize = labelFont.size;
140+
const titleHeight = this._computeTitleHeight();
141+
const { boxWidth, itemHeight } = getBoxSize(labelOpts, fontSize);
132142
const labelOpts = options.labels;
133143
const labelFont = toFont(labelOpts.font);
134144
const fontSize = labelFont.size;
@@ -137,7 +147,27 @@ fit() {
137147

138148
let width, height;
139149
ctx.font = labelFont.string;
150+
let width, height;
151+
ctx.font = labelFont.string;
152+
153+
if (this.isHorizontal()) {
154+
width = this.maxWidth;
155+
height = this._fitRows(titleHeight, fontSize, boxWidth, itemHeight) + 10;
156+
} else {
157+
height = this.maxHeight;
158+
width = this._fitCols(titleHeight, labelFont, boxWidth, itemHeight) + 10;
159+
}
160+
161+
// --- handle scroll height limit ---
162+
const scrollOpts = options.scroll || {};
163+
if (scrollOpts.enabled && scrollOpts.maxItems) {
164+
const singleItemHeight = itemHeight + labelOpts.padding;
165+
const visibleHeight = singleItemHeight * scrollOpts.maxItems + titleHeight + labelOpts.padding * 2;
166+
this.height = Math.min(this.height, visibleHeight);
140167

168+
// wrap legend in scroll container
169+
this._wrapLegendScroll(visibleHeight);
170+
}
141171
if (this.isHorizontal()) {
142172
width = this.maxWidth;
143173
height = this._fitRows(titleHeight, fontSize, boxWidth, itemHeight) + 10;
@@ -161,6 +191,39 @@ fit() {
161191
this.height = Math.min(height, options.maxHeight || this.maxHeight);
162192
}
163193

194+
/**
195+
* Private helper to wrap the <ul> legend in a scrollable container
196+
*/
197+
_wrapLegendScroll(visibleHeight) {
198+
const canvasContainer = this.chart.canvas.parentNode;
199+
if (!canvasContainer) return;
200+
201+
const legendUL = canvasContainer.querySelector('ul.chartjs-legend');
202+
if (!legendUL) return;
203+
204+
// Skip if already wrapped
205+
if (canvasContainer.querySelector('.legend-scroll-wrapper')) return;
206+
207+
const legendWrapper = document.createElement('div');
208+
legendWrapper.className = 'legend-scroll-wrapper';
209+
legendWrapper.style.overflowY = 'auto';
210+
legendWrapper.style.maxHeight = `${visibleHeight}px`;
211+
legendWrapper.style.display = 'inline-block';
212+
213+
// Force single-column layout
214+
legendUL.style.display = 'flex';
215+
legendUL.style.flexDirection = 'column';
216+
legendUL.style.margin = '0';
217+
legendUL.style.padding = '0';
218+
219+
legendUL.parentNode.insertBefore(legendWrapper, legendUL);
220+
legendWrapper.appendChild(legendUL);
221+
}
222+
223+
this.width = Math.min(width, options.maxWidth || this.maxWidth);
224+
this.height = Math.min(height, options.maxHeight || this.maxHeight);
225+
}
226+
164227
/**
165228
* Private helper to wrap the <ul> legend in a scrollable container
166229
*/
@@ -232,6 +295,13 @@ _wrapLegendScroll(visibleHeight) {
232295
const columnSizes = this.columnSizes = [];
233296

234297
const heightLimit = maxHeight - titleHeight;
298+
_fitCols(titleHeight, labelFont, boxWidth, _itemHeight) {
299+
const {ctx, maxHeight, options: {labels: {padding}}} = this;
300+
const scrollOpts = this.options.scroll || {};
301+
const hitboxes = this.legendHitBoxes = [];
302+
const columnSizes = this.columnSizes = [];
303+
304+
const heightLimit = maxHeight - titleHeight;
235305

236306
// --- SCROLL ENABLED: force single column ---
237307
if (scrollOpts.enabled && scrollOpts.maxItems) {
@@ -258,10 +328,44 @@ _wrapLegendScroll(visibleHeight) {
258328
let currentColHeight = 0;
259329
let left = 0;
260330
let col = 0;
331+
// --- SCROLL ENABLED: force single column ---
332+
if (scrollOpts.enabled && scrollOpts.maxItems) {
333+
let totalWidth = 0;
334+
let top = 0;
335+
let maxItemWidth = 0;
336+
337+
this.legendItems.forEach((legendItem, i) => {
338+
const {itemWidth, itemHeight} = calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight);
339+
hitboxes[i] = {left: 0, top, col: 0, width: itemWidth, height: itemHeight};
340+
top += itemHeight + padding;
341+
maxItemWidth = Math.max(maxItemWidth, itemWidth);
342+
});
343+
344+
columnSizes.push({width: maxItemWidth, height: top});
345+
totalWidth = maxItemWidth + 2 * padding;
346+
347+
return totalWidth;
348+
}
261349

350+
// --- SCROLL DISABLED: original multi-column logic ---
351+
let totalWidth = padding;
352+
let currentColWidth = 0;
353+
let currentColHeight = 0;
354+
let left = 0;
355+
let col = 0;
356+
357+
this.legendItems.forEach((legendItem, i) => {
358+
const {itemWidth, itemHeight} = calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight);
262359
this.legendItems.forEach((legendItem, i) => {
263360
const {itemWidth, itemHeight} = calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight);
264361

362+
if (i > 0 && currentColHeight + itemHeight + 2 * padding > heightLimit) {
363+
totalWidth += currentColWidth + padding;
364+
columnSizes.push({width: currentColWidth, height: currentColHeight});
365+
left += currentColWidth + padding;
366+
col++;
367+
currentColWidth = currentColHeight = 0;
368+
}
265369
if (i > 0 && currentColHeight + itemHeight + 2 * padding > heightLimit) {
266370
totalWidth += currentColWidth + padding;
267371
columnSizes.push({width: currentColWidth, height: currentColHeight});
@@ -270,15 +374,23 @@ _wrapLegendScroll(visibleHeight) {
270374
currentColWidth = currentColHeight = 0;
271375
}
272376

377+
hitboxes[i] = {left, top: currentColHeight, col, width: itemWidth, height: itemHeight};
273378
hitboxes[i] = {left, top: currentColHeight, col, width: itemWidth, height: itemHeight};
274379

275380
currentColWidth = Math.max(currentColWidth, itemWidth);
276381
currentColHeight += itemHeight + padding;
277382
});
383+
currentColWidth = Math.max(currentColWidth, itemWidth);
384+
currentColHeight += itemHeight + padding;
385+
});
278386

387+
totalWidth += currentColWidth;
388+
columnSizes.push({width: currentColWidth, height: currentColHeight});
279389
totalWidth += currentColWidth;
280390
columnSizes.push({width: currentColWidth, height: currentColHeight});
281391

392+
return totalWidth;
393+
}
282394
return totalWidth;
283395
}
284396
adjustHitBoxes() {
@@ -447,6 +559,12 @@ _wrapLegendScroll(visibleHeight) {
447559
const lineHeight = itemHeight + padding;
448560
const scrollOpts = this.options.scroll || {};
449561
let legendItems = this.legendItems;
562+
if (scrollOpts.enabled && scrollOpts.maxItems) {
563+
legendItems = legendItems.slice(0, scrollOpts.maxItems);
564+
}
565+
legendItems.forEach((legendItem, i) => {
566+
const scrollOpts = this.options.scroll || {};
567+
let legendItems = this.legendItems;
450568
if (scrollOpts.enabled && scrollOpts.maxItems) {
451569
legendItems = legendItems.slice(0, scrollOpts.maxItems);
452570
}
@@ -620,6 +738,15 @@ _wrapLegendScroll(visibleHeight) {
620738

621739

622740

741+
742+
743+
744+
745+
746+
747+
748+
749+
623750
function calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight) {
624751
const itemWidth = calculateItemWidth(legendItem, boxWidth, labelFont, ctx);
625752
const itemHeight = calculateItemHeight(_itemHeight, legendItem, labelFont.lineHeight);
@@ -705,6 +832,20 @@ export default {
705832
console.log('Legend wrapper created inside afterUpdate!');
706833
}
707834
}
835+
}
836+
const scrollOpts = legend.options.scroll || {};
837+
if (scrollOpts.enabled && scrollOpts.maxItems) {
838+
const canvasContainer = chart.canvas.parentNode;
839+
if (canvasContainer) {
840+
const legendUL = canvasContainer.querySelector('ul.chartjs-legend');
841+
if (legendUL && !canvasContainer.querySelector('.legend-scroll-wrapper')) {
842+
const singleItemHeight = legend.legendHitBoxes[0].height + legend.options.labels.padding;
843+
const titleHeight = legend._computeTitleHeight();
844+
const visibleHeight = singleItemHeight * scrollOpts.maxItems + titleHeight + legend.options.labels.padding * 2;
845+
legend._wrapLegendScroll(visibleHeight);
846+
console.log('Legend wrapper created inside afterUpdate!');
847+
}
848+
}
708849
}
709850
},
710851

0 commit comments

Comments
 (0)