From: Jukka Kurkela Date: Tue, 30 Mar 2021 14:12:07 +0000 (+0300) Subject: Legend: adjust lifecycle and event handling (#8753) X-Git-Tag: v3.0.0-rc.7~15 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=88c585b11e82ab9fa8d83f1878543ab293c8f0fa;p=thirdparty%2FChart.js.git Legend: adjust lifecycle and event handling (#8753) --- diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 18db9229b..30f2507d6 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -27,6 +27,8 @@ const getBoxSize = (labelOpts, fontSize) => { }; }; +const itemsEqual = (a, b) => a !== null && b !== null && a.datasetIndex === b.datasetIndex && a.index === b.index; + export class Legend extends Element { /** @@ -154,44 +156,51 @@ export class Legend extends Element { */ _fitRows(titleHeight, fontSize, boxWidth, itemHeight) { const me = this; - const {ctx, maxWidth} = me; - const padding = me.options.labels.padding; + const {ctx, maxWidth, options: {labels: {padding}}} = me; const hitboxes = me.legendHitBoxes = []; // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one const lineWidths = me.lineWidths = [0]; + const lineHeight = itemHeight + padding; let totalHeight = titleHeight; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; + let row = -1; + let top = -lineHeight; me.legendItems.forEach((legendItem, i) => { const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; if (i === 0 || lineWidths[lineWidths.length - 1] + itemWidth + 2 * padding > maxWidth) { - totalHeight += itemHeight + padding; + totalHeight += lineHeight; lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0; + top += lineHeight; + row++; } - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = {left: 0, top: 0, width: itemWidth, height: itemHeight}; + hitboxes[i] = {left: 0, top, row, width: itemWidth, height: itemHeight}; lineWidths[lineWidths.length - 1] += itemWidth + padding; - }); + return totalHeight; } _fitCols(titleHeight, fontSize, boxWidth, itemHeight) { const me = this; - const {ctx, maxHeight} = me; - const padding = me.options.labels.padding; + const {ctx, maxHeight, options: {labels: {padding}}} = me; const hitboxes = me.legendHitBoxes = []; const columnSizes = me.columnSizes = []; + const heightLimit = maxHeight - titleHeight; + let totalWidth = padding; let currentColWidth = 0; let currentColHeight = 0; - const heightLimit = maxHeight - titleHeight; + let left = 0; + let top = 0; + let col = 0; + me.legendItems.forEach((legendItem, i) => { const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; @@ -199,6 +208,9 @@ export class Legend extends Element { if (i > 0 && currentColHeight + fontSize + 2 * padding > heightLimit) { totalWidth += currentColWidth + padding; columnSizes.push({width: currentColWidth, height: currentColHeight}); // previous column size + left += currentColWidth + padding; + col++; + top = 0; currentColWidth = currentColHeight = 0; } @@ -207,7 +219,8 @@ export class Legend extends Element { currentColHeight += fontSize + padding; // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = {left: 0, top: 0, width: itemWidth, height: itemHeight}; + hitboxes[i] = {left, top, col, width: itemWidth, height: itemHeight}; + top += itemHeight + padding; }); totalWidth += currentColWidth; @@ -216,6 +229,40 @@ export class Legend extends Element { return totalWidth; } + adjustHitBoxes() { + const me = this; + if (!me.options.display) { + return; + } + const titleHeight = me._computeTitleHeight(); + const {legendHitBoxes: hitboxes, options: {align, labels: {padding}}} = me; + if (this.isHorizontal()) { + let row = 0; + let left = _alignStartEnd(align, me.left + padding, me.right - me.lineWidths[row]); + for (const hitbox of hitboxes) { + if (row !== hitbox.row) { + row = hitbox.row; + left = _alignStartEnd(align, me.left + padding, me.right - me.lineWidths[row]); + } + hitbox.top += me.top + titleHeight + padding; + hitbox.left = left; + left += hitbox.width + padding; + } + } else { + let col = 0; + let top = _alignStartEnd(align, me.top + titleHeight + padding, me.bottom - me.columnSizes[col].height); + for (const hitbox of hitboxes) { + if (hitbox.col !== col) { + col = hitbox.col; + top = _alignStartEnd(align, me.top + titleHeight + padding, me.bottom - me.columnSizes[col].height); + } + hitbox.top = top; + hitbox.left += me.left + padding; + top += hitbox.height + padding; + } + } + } + isHorizontal() { return this.options.position === 'top' || this.options.position === 'bottom'; } @@ -237,7 +284,7 @@ export class Legend extends Element { */ _draw() { const me = this; - const {options: opts, columnSizes, lineWidths, ctx, legendHitBoxes} = me; + const {options: opts, columnSizes, lineWidths, ctx} = me; const {align, labels: labelOpts} = opts; const defaultColor = defaults.color; const rtlHelper = getRtlAdapter(opts.rtl, me.left, me.width); @@ -358,9 +405,6 @@ export class Legend extends Element { drawLegendBox(realX, y, legendItem); - legendHitBoxes[i].left = rtlHelper.leftForLtr(realX, legendHitBoxes[i].width); - legendHitBoxes[i].top = y; - x = _textX(textAlign, x + boxWidth + halfFontSize, me.right); // Fill the actual label @@ -476,13 +520,14 @@ export class Legend extends Element { if (e.type === 'mousemove') { const previous = me._hoveredItem; - if (previous && previous !== hoveredItem) { + const sameItem = itemsEqual(previous, hoveredItem); + if (previous && !sameItem) { call(opts.onLeave, [e, previous, me], me); } me._hoveredItem = hoveredItem; - if (hoveredItem) { + if (hoveredItem && !sameItem) { call(opts.onHover, [e, hoveredItem, me], me); } } else if (hoveredItem) { @@ -533,7 +578,9 @@ export default { // The labels need to be built after datasets are updated to ensure that colors // and other styling are correct. See https://github.com/chartjs/Chart.js/issues/6968 afterUpdate(chart) { - chart.legend.buildLabels(); + const legend = chart.legend; + legend.buildLabels(); + legend.adjustHitBoxes(); },