diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index 645754254c1..9d35adfa6a2 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -34,18 +34,18 @@ export default defineConfig({ }], ['vuepress-plugin-code-copy', true], ['vuepress-plugin-typedoc', { - entryPoints: ['../../src/types/index.d.ts'], - hideInPageTOC: true, - tsconfig: path.resolve(__dirname, '../../tsconfig.json'), - }, + entryPoints: ['../../src/types/index.d.ts'], + hideInPageTOC: true, + tsconfig: path.resolve(__dirname, '../../tsconfig.json'), + }, ], ['@simonbrunel/vuepress-plugin-versions', { filters: { suffix: (tag) => tag ? ` (${tag})` : '', title: (v, vars) => { return window.location.href.includes('master') ? 'Development (master)' : - vars.tag === 'latest' ? 'Latest (' + v + ')' : - v + (vars.tag ? ` (${vars.tag})` : '') + ' (outdated)'; + vars.tag === 'latest' ? 'Latest (' + v + ')' : + v + (vars.tag ? ` (${vars.tag})` : '') + ' (outdated)'; }, }, menu: { @@ -219,6 +219,7 @@ export default defineConfig({ 'legend/point-style', 'legend/position', 'legend/title', + 'legend/navigation' ] }, { diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 1621f5a87d1..85bd1d281dc 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -26,6 +26,7 @@ The doughnut, pie, and polar area charts override the legend defaults. To change | `rtl` | `boolean` | | `true` for rendering the legends from right to left. | `textDirection` | `string` | canvas' default | This will force the text direction `'rtl'` or `'ltr'` on the canvas for rendering the legend, regardless of the css specified on the canvas | `title` | `object` | | See the [Legend Title Configuration](#legend-title-configuration) section below. +| `navigation` | `object` | | Handle large sets of legends with pagination. [more...](#legend-navigation-configuration) :::tip Note If you need more visual customizations, please use an [HTML legend](../samples/legend/html.md). @@ -86,6 +87,24 @@ Namespace: `options.plugins.legend.title` | `padding` | [`Padding`](../general/padding.md) | `0` | Padding around the title. | `text` | `string` | | The string title. +## Legend Navigation Configuration + +Namespace: `options.plugins.legend.navigation` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `display` | `string`\|`boolean` | `false` | Show/hide legend navigation. If `auto` is used, the navigation will be shown only if the legend overflows. +| `color` | [`Color`](../general/colors.md) | `Chart.defaults.color` | Color of the navigation page count label. +| `activeColor` | [`Color`](../general/colors.md) | `Chart.defaults.color` | Color of active navigation arrows. +| `inactiveColor` | [`Color`](../general/colors.md) | 40% opacity of the active color | Color of inactive navigation arrows. +| `arrowSize` | `number` | `12` | Size of navigation arrows. +| `maxCols` | `number` | `1` | Maximum number of columns, in vertical legends, for navigation to be activated. +| `maxRows` | `number` | `3` | Maximum number of rows, in horizontal legends, for navigation to be activated. +| `padding` | [`Padding`](../general/padding.md) | `{ x: 10, y: 10, top: 0 }` | Navigation buttons padding. +| `align` | `string` | `'start'` | Alignment of navigation buttons. Possible options are `start`, `center` and `end`. +| `grid` | `boolean`\|`object` | `true` | Align legends horizontally and vertically. Can be a `boolean` or an object containing `x` and `y` with boolean values ​​indicating whether the axis should be aligned. +| `font` | `Font` | `{ weight: 'bold', size: 14 }` | Font style of the navigation page count label. See [Fonts](../general/fonts.md) + ## Legend Item Interface Items passed to the legend `onClick` function are the ones returned from `labels.generateLabels`. These items must implement the following interface. diff --git a/docs/samples/legend/navigation.md b/docs/samples/legend/navigation.md new file mode 100644 index 00000000000..f57e4c86663 --- /dev/null +++ b/docs/samples/legend/navigation.md @@ -0,0 +1,124 @@ +# Navigation + +This sample shows how to use legend navigation to handle overflow. + +```js chart-editor + +// +const DATA_COUNT = 100; +const NUMBER_CFG = {count: DATA_COUNT, decimals: 0}; +const LABEL_CFG = {count: DATA_COUNT, prefix: 'Group ', min: 1, max: DATA_COUNT + 1}; + +const data = { + labels: Utils.labels(LABEL_CFG), + datasets: [{ + label: '# of Votes', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Object.values(Utils.CHART_COLORS), + }] +}; +// + +// +const config = { + type: 'pie', + data: data, + options: { + plugins: { + legend: { + position: 'right', + align: 'start', + title: { + display: true, + text: 'Chart.js Legend Navigation Example', + position: 'start', + }, + navigation: { + display: 'auto', + maxCols: 1, + maxRows: 3, + arrowSize: 12, + align: 'start', + grid: true + } + }, + } + } +}; +// + +// +const actions = [ + { + name: 'Toggle', + handler(chart) { + const {navigation} = chart.options.plugins.legend; + navigation.display = navigation.display ? false : 'auto'; + chart.update(); + } + }, + { + name: '+ Label', + handler(chart) { + console.log(chart); + const lastLabel = chart.data.labels[chart.data.labels.length - 1] || ''; + const lastIndex = +lastLabel.substring(6); + chart.data.labels.push(LABEL_CFG.prefix + (lastIndex + 1)); + chart.update(); + } + }, + { + name: '- Label', + handler(chart) { + chart.data.labels.pop(); + chart.update(); + } + }, + { + name: 'Position: left', + handler(chart) { + chart.options.plugins.legend.position = 'left'; + chart.update(); + } + }, + { + name: 'Position: top', + handler(chart) { + chart.options.plugins.legend.position = 'top'; + chart.update(); + } + }, + { + name: 'Position: right', + handler(chart) { + chart.options.plugins.legend.position = 'right'; + chart.update(); + } + }, + { + name: 'Position: bottom', + handler(chart) { + chart.options.plugins.legend.position = 'bottom'; + chart.update(); + } + }, + { + name: 'Toggle grid', + handler(chart) { + chart.options.plugins.legend.navigation.grid = !chart.options.plugins.legend.navigation.grid; + chart.update(); + } + }, +]; +// + +module.exports = { + config, + actions +}; +``` + +## Docs +* [Doughnut and Pie Charts](../../charts/doughnut.md) +* [Legend](../../configuration/legend.md) + * [Navigation](../../configuration/legend.md#legend-navigation-configuration) \ No newline at end of file diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 6ed99413536..e5d1639f47f 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -16,6 +16,7 @@ import { } from '../helpers/index.js'; import {_alignStartEnd, _textX, _toLeftRightCenter} from '../helpers/helpers.extras.js'; import {toTRBLCorners} from '../helpers/helpers.options.js'; +import {Color} from '@kurkle/color'; /** * @typedef { import('../types/index.js').ChartEvent } ChartEvent @@ -77,6 +78,7 @@ export class Legend extends Element { this.position = undefined; this.weight = undefined; this.fullSize = undefined; + this.navigation = undefined; } update(maxWidth, maxHeight, margins) { @@ -86,6 +88,7 @@ export class Legend extends Element { this.setDimensions(); this.buildLabels(); + this.buildNavigation(); this.fit(); } @@ -118,6 +121,296 @@ export class Legend extends Element { } this.legendItems = legendItems; + this._computeNavigation(); + } + + /** + * @private + */ + _initNavigation(override) { + if (!this.navigation) { + this.navigation = { + page: 0, + totalPages: 0, + itemWidth: 0, + itemHeight: 0, + navWidth: 0, + navHeight: 0, + maxBlocks: 0, + blocks: undefined, + text: undefined, + prev: undefined, + next: undefined, + legendItems: this.legendItems, + _width: 0, + _height: 0, + _maxWidth: 0, + _maxHeight: 0, + }; + } + + if (override) { + Object.assign(this.navigation, override); + } + } + + /** + * @private + */ + _computeNavigation() { + const {ctx, options} = this; + const {navigation: navOpts, labels: labelOpts} = options; + + if (!(navOpts && navOpts.display)) { + this.navigation = undefined; + return; + } + const isHorizontal = this.isHorizontal(); + + this._initNavigation({ + totalPages: 0, + _width: 0, + _height: 0, + _maxWidth: 0, + _maxHeight: 0, + itemWidth: 0, + itemHeight: 0, + maxBlocks: 0, + }); + + const labelFont = toFont(labelOpts.font); + const {boxWidth, itemHeight: _itemHeight} = getBoxSize(labelOpts, labelFont.size); + const font = toFont(navOpts.font); + + const padding = toPadding(navOpts.padding); + this.navigation.navHeight = Math.max(font.size, navOpts.arrowSize) + padding.height; + + const grid = getGridAxis(navOpts.grid); + + // Find the largest width to keep all items the same width + if (grid.x) { + this.navigation.itemWidth = this.legendItems.reduce((max, legendItem) => { + const width = calculateItemWidth(legendItem, boxWidth, labelFont, ctx); + return Math.max(max, width); + }, 0); + } + + // Find the greatest height to keep all items the same height + if (grid.y) { + this.navigation.itemHeight = this.legendItems.reduce((max, legendItem) => { + const height = calculateItemHeight(_itemHeight, legendItem, labelFont.lineHeight); + return Math.max(max, height); + }, 0); + } + + const titleHeight = this._computeTitleHeight(); + + if (isHorizontal) { + this._computeHorizontalNavigation(titleHeight, _itemHeight, boxWidth, labelFont); + } else { + this._computeVerticalNavigation(titleHeight, _itemHeight, boxWidth, labelFont); + } + + this.navigation.blocks.forEach((block) => { + this.navigation._maxWidth = Math.max(this.navigation._maxWidth, block.width); + this.navigation._maxHeight = Math.max(this.navigation._maxHeight, block.height); + }); + } + + /** + * @private + */ + _computeHorizontalNavigation(titleHeight, _itemHeight, boxWidth, labelFont) { + const {labels: labelOpts, navigation: navOpts, maxHeight = this.maxHeight} = this.options; + const {navHeight} = this.navigation; + + const widthLimit = this.maxWidth - labelOpts.padding; + const rows = this.navigation.blocks = [{start: 0, end: 0, height: 0, width: 0, bottom: 0}]; + let maxItemHeight = 0; + + this.legendItems.forEach((legendItem, i) => { + const {itemWidth, itemHeight} = this._getLegendItemSize(legendItem, boxWidth, _itemHeight, labelFont); + let row = rows[rows.length - 1]; + + if (row.width + itemWidth + labelOpts.padding > widthLimit) { + rows.push(row = {start: i, end: i, height: 0, width: 0, bottom: 0}); + } + + row.end = i + 1; + row.width += itemWidth + labelOpts.padding; + row.height = Math.max(row.height, itemHeight + labelOpts.padding); + row.bottom = (rows.length > 1 ? rows[rows.length - 2].bottom : 0) + row.height; + maxItemHeight = Math.max(maxItemHeight, row.height); + }); + + const totalRows = rows.length; + const maxRows = this.navigation.maxBlocks = Math.min( + totalRows, + navOpts.maxRows || Infinity, + maxHeight ? Math.floor((maxHeight - navHeight - labelOpts.padding) / (maxItemHeight + labelOpts.padding)) || 1 : Infinity + ); + + this.navigation.totalPages = Math.ceil(totalRows / maxRows); + + // Find minimum height required to fit any page + let height = 0; + for (let i = 0; i < rows.length; i += maxRows) { + const l = i > 0 ? rows[i - 1].bottom : 0; + const r = rows[Math.min(i + maxRows - 1, rows.length - 1)].bottom; + height = Math.max(height, r - l); + } + + this.navigation._height = titleHeight + labelOpts.padding + height + navHeight + 10; + } + + /** + * @private + */ + _computeVerticalNavigation(titleHeight, _itemHeight, boxWidth, labelFont) { + const {labels: labelOpts, navigation: navOpts, maxWidth = this.maxWidth} = this.options; + const {navHeight} = this.navigation; + + const heightLimit = this.maxHeight - titleHeight - navHeight - labelOpts.padding; + const columns = this.navigation.blocks = [{start: 0, end: 0, height: 0, width: 0, right: 0}]; + let maxItemWidth = 0; + + this.legendItems.forEach((legendItem, i) => { + const {itemWidth, itemHeight} = this._getLegendItemSize(legendItem, boxWidth, _itemHeight, labelFont); + let col = columns[columns.length - 1]; + + if (col.height + itemHeight + labelOpts.padding > heightLimit) { + columns.push(col = {start: i, end: i, height: 0, width: 0, right: 0}); + } + + col.end = i + 1; + col.height += itemHeight + labelOpts.padding; + col.width = Math.max(col.width, itemWidth + labelOpts.padding); + col.right = (columns.length > 1 ? columns[columns.length - 2].right : 0) + col.width; + maxItemWidth = Math.max(maxItemWidth, col.width); + }); + + const totalCols = columns.length; + + const maxCols = this.navigation.maxBlocks = Math.min( + totalCols, + navOpts.maxCols || Infinity, + maxWidth ? Math.floor((maxWidth - labelOpts.padding) / (maxItemWidth + labelOpts.padding)) || 1 : Infinity, + ); + + this.navigation.totalPages = Math.ceil(totalCols / maxCols); + + // Find minimum width required to fit any page + let width = 0; + for (let i = 0; i < columns.length; i += maxCols) { + const l = i > 0 ? columns[i - 1].right : 0; + const r = columns[Math.min(i + maxCols - 1, columns.length - 1)].right; + width = Math.max(width, r - l); + } + + const titleWidth = this._computeTitleWidth(); + this.navigation._width = Math.max(titleWidth, width) + 10; + } + + buildNavigation() { + if (!this.navigation) { + return; + } + const {ctx, options: {align, navigation: navOpts, labels: labelOpts}} = this; + const {totalPages, navHeight} = this.navigation; + + const font = toFont(navOpts.font); + + if (totalPages < 1 || (totalPages === 1 && navOpts.display === 'auto')) { + this.navigation = undefined; + return; + } + + const page = this.navigation.page = Math.max(0, Math.min(totalPages - 1, this.navigation.page)); + const text = this.navigation.text = `${page + 1}/${totalPages}`; + + ctx.save(); + ctx.font = font.string; + const textWidth = ctx.measureText(text).width; + ctx.restore(); + + const padding = toPadding(navOpts.padding); + const navWidth = this.navigation.navWidth = (navOpts.arrowSize * 2) + padding.width + textWidth + font.size; + + let left = this.left; + let right = this.right; + + if (this.isHorizontal()) { + const maxWidth = this.navigation._maxWidth + labelOpts.padding; + left = _alignStartEnd(align, this.left, right - maxWidth); + right = left + maxWidth; + } + + const prev = this.navigation.prev = { + x: _alignStartEnd(navOpts.align, left + padding.left, right - (navWidth - padding.left)), + y: (this.top || 0) + this.height - navHeight + padding.top, + width: navOpts.arrowSize, + height: navOpts.arrowSize + }; + this.navigation.next = { + x: prev.x + navOpts.arrowSize + textWidth + font.size, + y: prev.y, + width: navOpts.arrowSize, + height: navOpts.arrowSize + }; + + const {blocks: columns, maxBlocks: maxCols} = this.navigation; + const startIdx = Math.min(columns.length - 1, page * maxCols); + const endIdx = Math.min(columns.length - 1, startIdx + (maxCols - 1)); + const start = columns[startIdx].start; + const end = columns[endIdx].end; + this.navigation.legendItems = totalPages > 1 ? this.legendItems.slice(start, end) : this.legendItems; + } + + /** + * @private + */ + _getLegendItemSize(legendItem, boxWidth, _itemHeight, labelFont) { + const width = this._getLegendItemWidth(legendItem, boxWidth, labelFont); + const height = this._getLegendItemHeight(legendItem, _itemHeight, labelFont); + return {...width, ...height}; + } + + /** + * @private + */ + _getLegendItemWidth(legendItem, boxWidth, labelFont) { + const hitboxWidth = calculateItemWidth(legendItem, boxWidth, labelFont, this.ctx); + let itemWidth = hitboxWidth; + + if (this.navigation && this.navigation.itemWidth) { + itemWidth = this.navigation.itemWidth; + } + + return {itemWidth, hitboxWidth}; + } + + /** + * @private + */ + _getLegendItemHeight(legendItem, _itemHeight, labelFont) { + const hitboxHeight = calculateItemHeight(_itemHeight, legendItem, labelFont.lineHeight); + let itemHeight = hitboxHeight; + + if (this.navigation && this.navigation.itemHeight) { + itemHeight = this.navigation.itemHeight; + } + + return {itemHeight, hitboxHeight}; + } + + /** + * @private + */ + _getVisibleLegendItems() { + if (this.navigation) { + return this.navigation.legendItems; + } + return this.legendItems; } fit() { @@ -137,62 +430,83 @@ export class Legend extends Element { const fontSize = labelFont.size; const titleHeight = this._computeTitleHeight(); const {boxWidth, itemHeight} = getBoxSize(labelOpts, fontSize); + const isHorizontal = this.isHorizontal(); let width, height; ctx.font = labelFont.string; - if (this.isHorizontal()) { + if (isHorizontal) { width = this.maxWidth; // fill all the width - height = this._fitRows(titleHeight, fontSize, boxWidth, itemHeight) + 10; + height = this._fitRows(titleHeight, labelFont, boxWidth, itemHeight) + 10; + + if (this.navigation) { + height = this.navigation._height; + } } else { height = this.maxHeight; // fill all the height width = this._fitCols(titleHeight, labelFont, boxWidth, itemHeight) + 10; + + if (this.navigation) { + width = this.navigation._width; + } } - this.width = Math.min(width, options.maxWidth || this.maxWidth); - this.height = Math.min(height, options.maxHeight || this.maxHeight); + const maxWidth = isHorizontal ? this.maxWidth : (options.maxWidth || this.maxWidth); + const maxHeight = isHorizontal ? (options.maxHeight || this.maxHeight) : this.maxHeight; + this.width = Math.min(width, maxWidth); + this.height = Math.min(height, maxHeight); } /** * @private */ - _fitRows(titleHeight, fontSize, boxWidth, itemHeight) { + _fitRows(titleHeight, labelFont, boxWidth, _itemHeight) { const {ctx, maxWidth, options: {labels: {padding}}} = this; const hitboxes = this.legendHitBoxes = []; // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one const lineWidths = this.lineWidths = [0]; - const lineHeight = itemHeight + padding; let totalHeight = titleHeight; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; - let row = -1; - let top = -lineHeight; - this.legendItems.forEach((legendItem, i) => { - const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + let row = 0; + let top = 0; + let currentLineHeight = 0; - if (i === 0 || lineWidths[lineWidths.length - 1] + itemWidth + 2 * padding > maxWidth) { - totalHeight += lineHeight; - lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0; - top += lineHeight; + this._getVisibleLegendItems().forEach((legendItem, i) => { + const {itemWidth, itemHeight, hitboxWidth, hitboxHeight} = this._getLegendItemSize(legendItem, boxWidth, _itemHeight, labelFont); + + if (i > 0 && lineWidths[lineWidths.length - 1] + itemWidth + 2 * padding > maxWidth) { + lineWidths.push(0); + + totalHeight += currentLineHeight + padding; + top += currentLineHeight + padding; row++; + currentLineHeight = itemHeight; + } else { + currentLineHeight = Math.max(currentLineHeight, itemHeight); } - hitboxes[i] = {left: 0, top, row, width: itemWidth, height: itemHeight}; - + hitboxes[i] = {left: 0, top, row, width: hitboxWidth, height: hitboxHeight, offsetWidth: itemWidth}; lineWidths[lineWidths.length - 1] += itemWidth + padding; }); + totalHeight += currentLineHeight + padding; + return totalHeight; } _fitCols(titleHeight, labelFont, boxWidth, _itemHeight) { - const {ctx, maxHeight, options: {labels: {padding}}} = this; + const {maxHeight, options: {labels: {padding}}} = this; const hitboxes = this.legendHitBoxes = []; const columnSizes = this.columnSizes = []; - const heightLimit = maxHeight - titleHeight; + let heightLimit = maxHeight - titleHeight; + + if (this.navigation) { + heightLimit -= this.navigation.navHeight; + } let totalWidth = padding; let currentColWidth = 0; @@ -201,8 +515,8 @@ export class Legend extends Element { let left = 0; let col = 0; - this.legendItems.forEach((legendItem, i) => { - const {itemWidth, itemHeight} = calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight); + this._getVisibleLegendItems().forEach((legendItem, i) => { + const {itemWidth, itemHeight, hitboxWidth, hitboxHeight} = this._getLegendItemSize(legendItem, boxWidth, _itemHeight, labelFont); // If too tall, go to new column if (i > 0 && currentColHeight + itemHeight + 2 * padding > heightLimit) { @@ -214,7 +528,7 @@ export class Legend extends Element { } // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = {left, top: currentColHeight, col, width: itemWidth, height: itemHeight}; + hitboxes[i] = {left, top: currentColHeight, col, width: hitboxWidth, height: hitboxHeight, offsetHeight: itemHeight}; // Get max width currentColWidth = Math.max(currentColWidth, itemWidth); @@ -224,6 +538,9 @@ export class Legend extends Element { totalWidth += currentColWidth; columnSizes.push({width: currentColWidth, height: currentColHeight}); // previous column size + const titleWidth = this._computeTitleWidth(); + totalWidth = Math.max(totalWidth, titleWidth); + return totalWidth; } @@ -244,20 +561,26 @@ export class Legend extends Element { } hitbox.top += this.top + titleHeight + padding; hitbox.left = rtlHelper.leftForLtr(rtlHelper.x(left), hitbox.width); - left += hitbox.width + padding; + left += hitbox.offsetWidth + padding; } } else { + let bottom = this.bottom; + + if (this.navigation) { + bottom -= this.navigation.navHeight; + } + let col = 0; - let top = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - this.columnSizes[col].height); + let top = _alignStartEnd(align, this.top + titleHeight + padding, bottom - this.columnSizes[col].height); for (const hitbox of hitboxes) { if (hitbox.col !== col) { col = hitbox.col; - top = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - this.columnSizes[col].height); + top = _alignStartEnd(align, this.top + titleHeight + padding, bottom - this.columnSizes[col].height); } hitbox.top = top; hitbox.left += this.left + padding; hitbox.left = rtlHelper.leftForLtr(rtlHelper.x(hitbox.left), hitbox.width); - top += hitbox.height + padding; + top += hitbox.offsetHeight + padding; } } } @@ -374,6 +697,12 @@ export class Legend extends Element { // Horizontal const isHorizontal = this.isHorizontal(); const titleHeight = this._computeTitleHeight(); + let bottom = this.bottom; + + if (this.navigation) { + bottom -= this.navigation.navHeight; + } + if (isHorizontal) { cursor = { x: _alignStartEnd(align, this.left + padding, this.right - lineWidths[0]), @@ -383,21 +712,21 @@ export class Legend extends Element { } else { cursor = { x: this.left + padding, - y: _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - columnSizes[0].height), + y: _alignStartEnd(align, this.top + titleHeight + padding, bottom - columnSizes[0].height), line: 0 }; } overrideTextDirection(this.ctx, opts.textDirection); - const lineHeight = itemHeight + padding; - this.legendItems.forEach((legendItem, i) => { + let currentLineHeight = 0; + this._getVisibleLegendItems().forEach((legendItem, i) => { ctx.strokeStyle = legendItem.fontColor; // for strikethrough effect ctx.fillStyle = legendItem.fontColor; // render in correct colour - const textWidth = ctx.measureText(legendItem.text).width; const textAlign = rtlHelper.textAlign(legendItem.textAlign || (legendItem.textAlign = labelOpts.textAlign)); - const width = boxWidth + halfFontSize + textWidth; + const width = this._getLegendItemWidth(legendItem, boxWidth, labelFont).itemWidth; + const height = this._getLegendItemHeight(legendItem, itemHeight, labelFont).itemHeight + padding; let x = cursor.x; let y = cursor.y; @@ -405,14 +734,15 @@ export class Legend extends Element { if (isHorizontal) { if (i > 0 && x + width + padding > this.right) { - y = cursor.y += lineHeight; + y = cursor.y += currentLineHeight; cursor.line++; x = cursor.x = _alignStartEnd(align, this.left + padding, this.right - lineWidths[cursor.line]); + currentLineHeight = 0; } - } else if (i > 0 && y + lineHeight > this.bottom) { + } else if (i > 0 && y + height > bottom) { x = cursor.x = x + columnSizes[cursor.line].width + padding; cursor.line++; - y = cursor.y = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - columnSizes[cursor.line].height); + y = cursor.y = _alignStartEnd(align, this.top + titleHeight + padding, bottom - columnSizes[cursor.line].height); } const realX = rtlHelper.x(x); @@ -426,35 +756,84 @@ export class Legend extends Element { if (isHorizontal) { cursor.x += width + padding; - } else if (typeof legendItem.text !== 'string') { - const fontLineHeight = labelFont.lineHeight; - cursor.y += calculateLegendItemHeight(legendItem, fontLineHeight) + padding; } else { - cursor.y += lineHeight; + cursor.y += height; } + + currentLineHeight = Math.max(currentLineHeight, height); }); restoreTextDirection(this.ctx, opts.textDirection); + + this._drawNavigation(); + } + + /** + * @private + */ + _drawNavigation() { + if (!this.navigation) { + return; + } + const {ctx, options: {navigation: navOpts}} = this; + const {page, totalPages, maxBlocks, prev, next, text} = this.navigation; + + if (totalPages <= 1 && navOpts.display === 'auto') { + return; + } + + const {arrowSize} = navOpts; + const font = toFont(navOpts.font); + const fontSize = font.size; + const halfFontSize = fontSize / 2; + const isHorizontal = this.isHorizontal(); + + ctx.save(); + + const drawArrow = (x, y, rotation = 0, color = navOpts.color) => { + const [a, b, c] = getNavArrow(x, y, arrowSize, rotation); + + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + ctx.lineTo(b.x, b.y); + ctx.lineTo(c.x, c.y); + ctx.fill(); + }; + + const rotation = isHorizontal && maxBlocks === 1 || !isHorizontal && maxBlocks > 1 ? 90 : 0; + const hasPrev = page > 0; + const hasNext = page < totalPages - 1; + const colors = [navOpts.inactiveColor, navOpts.activeColor]; + drawArrow(prev.x + (arrowSize / 2), prev.y + (arrowSize / 2), 180 + rotation, colors[+hasPrev]); + drawArrow(next.x + (arrowSize / 2), next.y + (arrowSize / 2), rotation, colors[+hasNext]); + + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + ctx.strokeStyle = navOpts.color; + ctx.fillStyle = navOpts.color; + ctx.font = font.string; + + renderText(ctx, text, prev.x + arrowSize + halfFontSize, prev.y + (arrowSize / 2), font); + ctx.restore(); } /** * @protected */ drawTitle() { - const opts = this.options; - const titleOpts = opts.title; - const titleFont = toFont(titleOpts.font); - const titlePadding = toPadding(titleOpts.padding); + const {ctx, options} = this; + const {labels: labelOpts, title: titleOpts} = options; if (!titleOpts.display) { return; } - const rtlHelper = getRtlAdapter(opts.rtl, this.left, this.width); - const ctx = this.ctx; + const titleFont = toFont(titleOpts.font); + const titlePadding = toPadding(titleOpts.padding); + const rtlHelper = getRtlAdapter(options.rtl, this.left, this.width); const position = titleOpts.position; - const halfFontSize = titleFont.size / 2; - const topPaddingPlusHalfFontSize = titlePadding.top + halfFontSize; + const topPaddingPlusHalfFontSize = titlePadding.top + (titleFont.size / 2); let y; // These defaults are used when the legend is vertical. @@ -464,18 +843,26 @@ export class Legend extends Element { if (this.isHorizontal()) { // Move left / right so that the title is above the legend lines - maxWidth = Math.max(...this.lineWidths); + maxWidth = (this.navigation ? this.navigation._maxWidth : Math.max(...this.lineWidths)) + labelOpts.padding; y = this.top + topPaddingPlusHalfFontSize; - left = _alignStartEnd(opts.align, left, this.right - maxWidth); + left = _alignStartEnd(options.align, left, this.right - maxWidth); } else { // Move down so that the title is above the legend stack in every alignment - const maxHeight = this.columnSizes.reduce((acc, size) => Math.max(acc, size.height), 0); - y = topPaddingPlusHalfFontSize + _alignStartEnd(opts.align, this.top, this.bottom - maxHeight - opts.labels.padding - this._computeTitleHeight()); + let maxHeight; + + if (this.navigation) { + maxHeight = this.navigation._maxHeight + this.navigation.navHeight; + } else { + maxHeight = this.columnSizes.reduce((acc, size) => Math.max(acc, size.height), 0); + } + + maxHeight += labelOpts.padding + this._computeTitleHeight(); + y = topPaddingPlusHalfFontSize + _alignStartEnd(options.align, this.top, this.bottom - maxHeight); } // Now that we know the left edge of the inner legend box, compute the correct // X coordinate from the title alignment - const x = _alignStartEnd(position, left, left + maxWidth); + const x = _alignStartEnd(position, left + titlePadding.left, left + (maxWidth - titlePadding.width)); // Canvas setup ctx.textAlign = rtlHelper.textAlign(_toLeftRightCenter(position)); @@ -492,28 +879,92 @@ export class Legend extends Element { */ _computeTitleHeight() { const titleOpts = this.options.title; + + if (!titleOpts.display) { + return 0; + } + const titleFont = toFont(titleOpts.font); const titlePadding = toPadding(titleOpts.padding); - return titleOpts.display ? titleFont.lineHeight + titlePadding.height : 0; + const titleText = titleOpts.text; + + let titleHeight = titleFont.lineHeight; + if (titleText && typeof titleText !== 'string') { + titleHeight *= titleText.length; + } + + return titleHeight + titlePadding.height; } /** * @private */ - _getLegendItemAt(x, y) { - let i, hitBox, lh; + _computeTitleWidth() { + const titleOpts = this.options.title; + if (!titleOpts.display) { + return 0; + } + + const titleFont = toFont(titleOpts.font); + const titlePadding = toPadding(titleOpts.padding); + let titleLongestText = titleOpts.text; + + if (titleLongestText && typeof titleLongestText !== 'string') { + titleLongestText = titleLongestText.reduce((a, b) => a.length > b.length ? a : b); + } + + this.ctx.save(); + this.ctx.font = titleFont.string; + let titleWidth = this.ctx.measureText(titleLongestText).width; + this.ctx.restore(); + + return titleWidth + titlePadding.width; + } + + /** + * @private + */ + _getNavigationDirAt(x, y) { + if (!(this.navigation && this.navigation.prev && this.navigation.next)) { + return 0; + } + const {prev, next} = this.navigation; + + const {arrowSize} = this.options.navigation; + // Add a padding to the clickable area (30% of the arrow size) + const padding = arrowSize * 0.3; + + if (_isBetween(x, prev.x - padding, prev.x + arrowSize + (padding / 2)) + && _isBetween(y, prev.y - padding, prev.y + arrowSize + padding)) { + return -1; + } + if (_isBetween(x, next.x - (padding / 2), next.x + arrowSize + padding) + && _isBetween(y, next.y - padding, next.y + arrowSize + padding)) { + return 1; + } + + return 0; + } + + /** + * @private + */ + _getLegendItemAt(x, y) { if (_isBetween(x, this.left, this.right) && _isBetween(y, this.top, this.bottom)) { // See if we are touching one of the dataset boxes - lh = this.legendHitBoxes; - for (i = 0; i < lh.length; ++i) { + const lh = this.legendHitBoxes; + const legendItems = this._getVisibleLegendItems(); + let hitBox; + + for (let i = 0; i < lh.length; ++i) { hitBox = lh[i]; if (_isBetween(x, hitBox.left, hitBox.left + hitBox.width) && _isBetween(y, hitBox.top, hitBox.top + hitBox.height)) { // Touching an element - return this.legendItems[i]; + return legendItems[i]; } } } @@ -521,11 +972,39 @@ export class Legend extends Element { return null; } + /** + * @private + */ + _handleNavigationEvent(e) { + if (!this.navigation || this.navigation.totalPages < 2 || e.type !== 'click') { + return; + } + + const dir = this._getNavigationDirAt(e.x, e.y); + + if (!dir) { + return; + } + + const {page, totalPages} = this.navigation; + const lastPage = totalPages - 1; + const newPage = this.navigation.page = Math.max(0, Math.min(lastPage, this.navigation.page + dir)); + + if (newPage !== page) { + this.buildNavigation(); + this.fit(); + this.adjustHitBoxes(); + this.chart.render(); + } + } + /** * Handle an event * @param {ChartEvent} e - The event to handle */ handleEvent(e) { + this._handleNavigationEvent(e); + const opts = this.options; if (!isListened(e.type, opts)) { return; @@ -552,12 +1031,6 @@ export class Legend extends Element { } } -function calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight) { - const itemWidth = calculateItemWidth(legendItem, boxWidth, labelFont, ctx); - const itemHeight = calculateItemHeight(_itemHeight, legendItem, labelFont.lineHeight); - return {itemWidth, itemHeight}; -} - function calculateItemWidth(legendItem, boxWidth, labelFont, ctx) { let legendItemText = legendItem.text; if (legendItemText && typeof legendItemText !== 'string') { @@ -568,7 +1041,7 @@ function calculateItemWidth(legendItem, boxWidth, labelFont, ctx) { function calculateItemHeight(_itemHeight, legendItem, fontLineHeight) { let itemHeight = _itemHeight; - if (typeof legendItem.text !== 'string') { + if (legendItem.text && typeof legendItem.text !== 'string') { itemHeight = calculateLegendItemHeight(legendItem, fontLineHeight); } return itemHeight; @@ -589,6 +1062,49 @@ function isListened(type, opts) { return false; } +function getNavArrow(cx, cy, size, rotation = 0) { + const x1 = cx; const + y1 = cy + (size / 2); + const x2 = cx - size / 2; const + y2 = cy - (size / 2); + const x3 = cx + size / 2; const + y3 = y2; + + const result = [ + {x: x1, y: y1}, + {x: x2, y: y2}, + {x: x3, y: y3} + ]; + + const radians = (Math.PI / 180) * rotation; + const cos = Math.cos(radians); + const sin = Math.sin(radians); + + result.forEach(item => { + const nx = (cos * (item.x - cx)) + (sin * (item.y - cy)) + cx; + const ny = (cos * (item.y - cy)) - (sin * (item.x - cx)) + cy; + item.x = nx; + item.y = ny; + }); + + return result; +} + +function getGridAxis(grid) { + const result = {x: false, y: false}; + + if (grid) { + if (typeof grid === 'boolean') { + result.x = result.y = true; + } else { + result.x = !!grid.x; + result.y = !!grid.y; + } + } + + return result; +} + export default { id: 'legend', @@ -623,6 +1139,7 @@ export default { afterUpdate(chart) { const legend = chart.legend; legend.buildLabels(); + legend.buildNavigation(); legend.adjustHitBoxes(); }, @@ -657,6 +1174,27 @@ export default { onHover: null, onLeave: null, + navigation: { + color: (ctx) => ctx.chart.options.color, + display: false, + arrowSize: 12, + maxCols: 1, + maxRows: 3, + padding: { + x: 10, + y: 10, + top: 0 + }, + align: 'start', + grid: true, + activeColor: (ctx) => ctx.chart.options.color, + inactiveColor: (ctx) => new Color(ctx.chart.options.plugins.legend.navigation.activeColor).alpha(0.4).rgbString(), + font: { + weight: 'bold', + size: 14 + } + }, + labels: { color: (ctx) => ctx.chart.options.color, boxWidth: 40, diff --git a/src/types/index.d.ts b/src/types/index.d.ts index fcdd44fe06b..fe542750ebc 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -2339,10 +2339,18 @@ export interface LegendItem { textAlign?: TextAlign; } +export interface LegendNavigation { + active: boolean; + page: number; + totalPages: number; + legendItems?: LegendItem[]; +} + export interface LegendElement extends Element>, LayoutItem { chart: Chart; ctx: CanvasRenderingContext2D; legendItems?: LegendItem[]; + navigation: LegendNavigation; options: LegendOptions; fit(): void; } @@ -2499,6 +2507,71 @@ export interface LegendOptions { */ text: string; }; + + navigation: { + /** + * Show/hide legend navigation. + * + * If `auto` is used, the navigation will be shown only if the legend overflows. + * @default false + */ + display: 'auto' | boolean; + /** + * Color of the navigation page count label. + * @see Defaults.color + */ + color: Color; + /** + * Color of active navigation arrows. + * @see Defaults.color + */ + activeColor: Color; + /** + * Color of inactive navigation arrows. + * + * Defaults to 40% opacity of the active color. + */ + inactiveColor: Color; + /** + * Size of navigation arrows. + * @default 12 + */ + arrowSize: number; + /** + * Maximum number of columns, in vertical legends, for navigation to be activated. + * @default 1 + */ + maxCols: number; + /** + * Maximum number of rows, in horizontal legends, for navigation to be activated. + * @default 3 + */ + maxRows: number; + /** + * Navigation buttons padding. + * @default + * { x: 10, y: 10, top: 0 } + */ + padding: number | ChartArea; + /** + * Alignment of navigation buttons. + * @default 'start' + */ + align: 'start' | 'center' | 'end'; + /** + * Align legends horizontally and vertically. + * + * Fixes the width/height of all legends according to the widest/tallest legend, to form a grid and keep legends aligned when changing pages. + * @default true + */ + grid: boolean | { x?: boolean; y?: boolean; }, + /** + * Font style of the navigation page count label. + * @default + * { weight: 'bold', size: 14 } + */ + font: ScriptableAndScriptableOptions, ScriptableChartContext>; + } } export declare const SubTitle: Plugin; @@ -3725,7 +3798,7 @@ export type ChartType = keyof ChartTypeRegistry; export type ScaleOptionsByType = { [key in ScaleType]: { type: key } & ScaleTypeRegistry[key]['options'] }[TScale] -; + ; // Convenience alias for creating and manipulating scale options in user code export type ScaleOptions = DeepPartial>; diff --git a/test/fixtures/plugin.legend/legend-doughnut-horizontal-multiline-legend.json b/test/fixtures/plugin.legend/legend-doughnut-horizontal-multiline-legend.json new file mode 100644 index 00000000000..fe53960b362 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-horizontal-multiline-legend.json @@ -0,0 +1,29 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", ["", "", ""], "", "", "", "", "", "", "", "", "", ""], + "datasets": [ + { + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "center" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-horizontal-multiline-legend.png b/test/fixtures/plugin.legend/legend-doughnut-horizontal-multiline-legend.png new file mode 100644 index 00000000000..4e6db743b3d Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-horizontal-multiline-legend.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-hidden.json b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-hidden.json new file mode 100644 index 00000000000..e13b67365cd --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-hidden.json @@ -0,0 +1,34 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["Label 1", "Label 2", "Label 3", "Label 4", "Label 5"], + "datasets": [ + { + "data": [10, 20, 30, 40, 50], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "start", + "navigation": { + "display": "auto", + "maxRows": 1 + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-hidden.png b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-hidden.png new file mode 100644 index 00000000000..275a39f2069 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-hidden.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-visible.json b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-visible.json new file mode 100644 index 00000000000..67dd859aa3e --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-visible.json @@ -0,0 +1,45 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [ + "Label 1", + "Label 2", + "Label 3", + "Label 4", + "Label 5", + "Label 6", + "Label 7", + "Label 8", + "Label 9", + "Label 10" + ], + "datasets": [ + { + "data": [10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "start", + "navigation": { + "display": "auto", + "maxRows": 1 + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-visible.png b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-visible.png new file mode 100644 index 00000000000..45c8d0aa954 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-auto-visible.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-multiple-lines.json b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-multiple-lines.json new file mode 100644 index 00000000000..b908d2a9628 --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-multiple-lines.json @@ -0,0 +1,56 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [ + "Label 1", + ["Label 2", "Second line"], + "Label 3", + "Label 4", + "Label 5", + "Label 6", + "Label 7", + "Label 8", + "Label 9", + "Label 10", + "Label 11", + "Label 12", + "Label 13", + "Label 14", + "Label 15" + ], + "datasets": [ + { + "data": [ + 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150 + ], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "start", + "maxHeight": 300, + "navigation": { + "display": true, + "maxRows": 2, + "grid": { + "x": true + } + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-multiple-lines.png b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-multiple-lines.png new file mode 100644 index 00000000000..92b5fbc38a7 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-multiple-lines.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-center.json b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-center.json new file mode 100644 index 00000000000..3f70377a9a1 --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-center.json @@ -0,0 +1,35 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["Label 1", "Label 2", "Label 3"], + "datasets": [ + { + "data": [10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "center", + "navigation": { + "display": true, + "maxRows": 1, + "align": "center" + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-center.png b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-center.png new file mode 100644 index 00000000000..89b04c0dbb2 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-center.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-end.json b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-end.json new file mode 100644 index 00000000000..51a8097414d --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-end.json @@ -0,0 +1,35 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["Label 1", "Label 2", "Label 3"], + "datasets": [ + { + "data": [10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "center", + "navigation": { + "display": true, + "maxRows": 1, + "align": "end" + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-end.png b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-end.png new file mode 100644 index 00000000000..7ee46cabbf6 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-end.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-start.json b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-start.json new file mode 100644 index 00000000000..77455000aad --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-start.json @@ -0,0 +1,35 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["Label 1", "Label 2", "Label 3"], + "datasets": [ + { + "data": [10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "center", + "navigation": { + "display": true, + "maxRows": 1, + "align": "start" + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-start.png b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-start.png new file mode 100644 index 00000000000..698f8e3f4aa Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-center-start.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-center.json b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-center.json new file mode 100644 index 00000000000..6fc56488a1f --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-center.json @@ -0,0 +1,35 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["Label 1", "Label 2", "Label 3"], + "datasets": [ + { + "data": [10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "start", + "navigation": { + "display": true, + "maxRows": 1, + "align": "center" + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-center.png b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-center.png new file mode 100644 index 00000000000..6c4d321fc5c Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-center.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-end.json b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-end.json new file mode 100644 index 00000000000..16159b4aa33 --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-end.json @@ -0,0 +1,35 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["Label 1", "Label 2", "Label 3"], + "datasets": [ + { + "data": [10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "start", + "navigation": { + "display": true, + "maxRows": 1, + "align": "end" + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-end.png b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-end.png new file mode 100644 index 00000000000..c881042fd01 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-align-start-end.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-bottom.json b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-bottom.json new file mode 100644 index 00000000000..30941747662 --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-bottom.json @@ -0,0 +1,45 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [ + "Label 1", + "Label 2", + "Label 3", + "Label 4", + "Label 5", + "Label 6", + "Label 7", + "Label 8", + "Label 9", + "Label 10" + ], + "datasets": [ + { + "data": [10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "bottom", + "align": "start", + "navigation": { + "display": true, + "maxRows": 1 + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-bottom.png b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-bottom.png new file mode 100644 index 00000000000..d544dc8d79c Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-bottom.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-single.json b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-single.json new file mode 100644 index 00000000000..fd11a1995ff --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-single.json @@ -0,0 +1,34 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["Label 1", "Label 2", "Label 3", "Label 4", "Label 5"], + "datasets": [ + { + "data": [10, 20, 30, 40, 50], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "start", + "navigation": { + "display": true, + "maxRows": 1 + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-single.png b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-single.png new file mode 100644 index 00000000000..275a39f2069 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-single.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-title.json b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-title.json new file mode 100644 index 00000000000..ab40e0918c7 --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-title.json @@ -0,0 +1,49 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [ + "Label 1", + "Label 2", + "Label 3", + "Label 4", + "Label 5", + "Label 6", + "Label 7", + "Label 8", + "Label 9", + "Label 10" + ], + "datasets": [ + { + "data": [10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "start", + "title": { + "display": true, + "text": "Legend Title Text" + }, + "navigation": { + "display": true, + "maxRows": 1 + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-title.png b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-title.png new file mode 100644 index 00000000000..52e614e8bdb Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-horizontal-visible-title.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-cols.json b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-cols.json new file mode 100644 index 00000000000..7db2a9920ce --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-cols.json @@ -0,0 +1,61 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [ + "Label 1", + "Label 2", + "Label 3", + "Label 4", + "Label 5", + "Label 6", + "Label 7", + "Label 8", + "Label 9", + "Label 10", + "Label 11", + "Label 12", + "Label 13", + "Label 14", + "Label 15", + "Label 16", + "Label 17", + "Label 18", + "Label 19", + "Label 20", + "Label 21", + "Label 22", + "Label 23" + ], + "datasets": [ + { + "data": [ + 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, + 160, 170, 180, 190, 200, 210, 220, 230 + ], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "start", + "navigation": { + "display": true, + "maxCols": 2 + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-cols.png b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-cols.png new file mode 100644 index 00000000000..02cdd98d99d Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-cols.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-lines.json b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-lines.json new file mode 100644 index 00000000000..0b832948ba4 --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-lines.json @@ -0,0 +1,54 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [ + "Label 1", + ["Label 2", "Second line"], + "Label 3", + "Label 4", + "Label 5", + "Label 6", + "Label 7", + "Label 8", + "Label 9", + "Label 10", + "Label 11", + "Label 12", + "Label 13", + "Label 14", + "Label 15" + ], + "datasets": [ + { + "data": [ + 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150 + ], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "start", + "maxWidth": 300, + "navigation": { + "display": true, + "maxCols": 1, + "grid": false + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-lines.png b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-lines.png new file mode 100644 index 00000000000..accaadca920 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-multiple-lines.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-center.json b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-center.json new file mode 100644 index 00000000000..71d0ef89c67 --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-center.json @@ -0,0 +1,35 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["Label 1", "Label 2", "Label 3"], + "datasets": [ + { + "data": [10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "start", + "navigation": { + "display": true, + "maxCols": 1, + "align": "center" + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-center.png b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-center.png new file mode 100644 index 00000000000..97e5c704c35 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-center.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end-multiple-cols.json b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end-multiple-cols.json new file mode 100644 index 00000000000..17c7d0ac627 --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end-multiple-cols.json @@ -0,0 +1,53 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [ + "Label 1", + "Label 2", + "Label 3", + "Label 4", + "Label 5", + "Label 6", + "Label 7", + "Label 8", + "Label 9", + "Label 10", + "Label 11", + "Label 12", + "Label 13", + "Label 14", + "Label 15" + ], + "datasets": [ + { + "data": [ + 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150 + ], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "start", + "navigation": { + "display": true, + "maxCols": 2, + "align": "end" + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end-multiple-cols.png b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end-multiple-cols.png new file mode 100644 index 00000000000..893fd1ba9c7 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end-multiple-cols.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end.json b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end.json new file mode 100644 index 00000000000..7aa27addd12 --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end.json @@ -0,0 +1,35 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["Label 1", "Label 2", "Label 3"], + "datasets": [ + { + "data": [10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "start", + "navigation": { + "display": true, + "maxCols": 1, + "align": "end" + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end.png b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end.png new file mode 100644 index 00000000000..3da66a0bbbc Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-align-end.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-left.json b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-left.json new file mode 100644 index 00000000000..91ebf4a320d --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-left.json @@ -0,0 +1,45 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [ + "Label 1", + "Label 2", + "Label 3", + "Label 4", + "Label 5", + "Label 6", + "Label 7", + "Label 8", + "Label 9", + "Label 10" + ], + "datasets": [ + { + "data": [10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "left", + "align": "start", + "navigation": { + "display": true, + "maxCols": 1 + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-left.png b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-left.png new file mode 100644 index 00000000000..70e0397fb87 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-left.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-single.json b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-single.json new file mode 100644 index 00000000000..a22b37332db --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-single.json @@ -0,0 +1,45 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [ + "Label 1", + "Label 2", + "Label 3", + "Label 4", + "Label 5", + "Label 6", + "Label 7", + "Label 8", + "Label 9", + "Label 10" + ], + "datasets": [ + { + "data": [10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "start", + "navigation": { + "display": true, + "maxCols": 1 + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-single.png b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-single.png new file mode 100644 index 00000000000..d9899391564 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-single.png differ diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-title.json b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-title.json new file mode 100644 index 00000000000..dd1579b873c --- /dev/null +++ b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-title.json @@ -0,0 +1,49 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [ + "Label 1", + "Label 2", + "Label 3", + "Label 4", + "Label 5", + "Label 6", + "Label 7", + "Label 8", + "Label 9", + "Label 10" + ], + "datasets": [ + { + "data": [10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "start", + "title": { + "display": true, + "text": "Legend Title Text" + }, + "navigation": { + "display": true, + "maxCols": 1 + } + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-title.png b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-title.png new file mode 100644 index 00000000000..92931190660 Binary files /dev/null and b/test/fixtures/plugin.legend/navigation/legend-navigation-vertical-visible-title.png differ diff --git a/test/fixtures/plugin.legend/title/multiline.json b/test/fixtures/plugin.legend/title/multiline.json new file mode 100644 index 00000000000..4d155004a42 --- /dev/null +++ b/test/fixtures/plugin.legend/title/multiline.json @@ -0,0 +1,33 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", ""], + "datasets": [ + { + "data": [10, 20, 30, 40], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "center", + "title": { + "display": true, + "text": ["", "", ""] + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/title/multiline.png b/test/fixtures/plugin.legend/title/multiline.png new file mode 100644 index 00000000000..1de4d9b6ecd Binary files /dev/null and b/test/fixtures/plugin.legend/title/multiline.png differ diff --git a/test/fixtures/plugin.legend/title/padding-bottom.json b/test/fixtures/plugin.legend/title/padding-bottom.json new file mode 100644 index 00000000000..61a312e0460 --- /dev/null +++ b/test/fixtures/plugin.legend/title/padding-bottom.json @@ -0,0 +1,36 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", ""], + "datasets": [ + { + "data": [10, 20, 30, 40], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "center", + "title": { + "display": true, + "text": "Title Text Example", + "padding": { + "bottom": 50 + } + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/title/padding-bottom.png b/test/fixtures/plugin.legend/title/padding-bottom.png new file mode 100644 index 00000000000..6b70b75df2a Binary files /dev/null and b/test/fixtures/plugin.legend/title/padding-bottom.png differ diff --git a/test/fixtures/plugin.legend/title/padding-left.json b/test/fixtures/plugin.legend/title/padding-left.json new file mode 100644 index 00000000000..954469c24a8 --- /dev/null +++ b/test/fixtures/plugin.legend/title/padding-left.json @@ -0,0 +1,37 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", ""], + "datasets": [ + { + "data": [10, 20, 30, 40], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "center", + "title": { + "display": true, + "text": "Title Text Example", + "position": "start", + "padding": { + "left": 20 + } + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/title/padding-left.png b/test/fixtures/plugin.legend/title/padding-left.png new file mode 100644 index 00000000000..136f4766f6c Binary files /dev/null and b/test/fixtures/plugin.legend/title/padding-left.png differ diff --git a/test/fixtures/plugin.legend/title/padding-right.json b/test/fixtures/plugin.legend/title/padding-right.json new file mode 100644 index 00000000000..8769090aa52 --- /dev/null +++ b/test/fixtures/plugin.legend/title/padding-right.json @@ -0,0 +1,37 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", ""], + "datasets": [ + { + "data": [10, 20, 30, 40], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "center", + "title": { + "display": true, + "text": "Title Text Example", + "position": "end", + "padding": { + "right": 20 + } + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/title/padding-right.png b/test/fixtures/plugin.legend/title/padding-right.png new file mode 100644 index 00000000000..67957d60c2b Binary files /dev/null and b/test/fixtures/plugin.legend/title/padding-right.png differ diff --git a/test/fixtures/plugin.legend/title/padding-top.json b/test/fixtures/plugin.legend/title/padding-top.json new file mode 100644 index 00000000000..5d0a4a7479b --- /dev/null +++ b/test/fixtures/plugin.legend/title/padding-top.json @@ -0,0 +1,36 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", ""], + "datasets": [ + { + "data": [10, 20, 30, 40], + "backgroundColor": "#00ff00", + "borderWidth": 0 + } + ] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "center", + "title": { + "display": true, + "text": "Title Text Example", + "padding": { + "top": 50 + } + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/title/padding-top.png b/test/fixtures/plugin.legend/title/padding-top.png new file mode 100644 index 00000000000..a36bd45f289 Binary files /dev/null and b/test/fixtures/plugin.legend/title/padding-top.png differ diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index e0bed42c263..49793c37120 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -28,7 +28,28 @@ describe('Legend block tests', function() { display: false, position: 'center', text: '', - } + }, + + navigation: { + color: jasmine.any(Function), + display: false, + arrowSize: 12, + maxCols: 1, + maxRows: 3, + padding: { + x: 10, + y: 10, + top: 0 + }, + align: 'start', + grid: true, + activeColor: jasmine.any(Function), + inactiveColor: jasmine.any(Function), + font: { + weight: 'bold', + size: 14 + } + }, }); }); @@ -1053,7 +1074,8 @@ describe('Legend block tests', function() { Chart.defaults.plugins.legend, { labels: {color: Chart.defaults.color}, - title: {color: Chart.defaults.color} + title: {color: Chart.defaults.color}, + navigation: {color: Chart.defaults.color} } )); }); @@ -1182,4 +1204,1253 @@ describe('Legend block tests', function() { expect(clickItem).toBe(chart.legend.legendItems[0]); }); }); + + describe('navigation', function() { + it('should not change legendItems when navigation is active', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: true, + maxRows: 1 + } + } + }, + } + }); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.navigation.legendItems.length).toBe(4); + }); + + describe('horizontal', function() { + it('should not show navigation by default', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + maxHeight: 100 + } + } + } + }); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(10); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should not show navigation if display false', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: false + } + } + } + } + }); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(10); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should not show navigation if options is false', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: false + } + }, + } + }, + { + canvas: {width: 512, height: 512} + } + ); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(10); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should show navigation if display true', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3'], + datasets: [{ + data: [10, 20, 30] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: true, + maxRows: 1 + } + } + }, + } + }, + { + canvas: {width: 512, height: 512} + } + ); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(3); + expect(chart.legend.navigation.legendItems.length).toBe(3); + }); + + it('should not show navigation if display auto and enough space', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2'], + datasets: [{ + data: [10, 20] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: 'auto', + maxRows: 1 + } + } + }, + } + }, + { + canvas: {width: 512, height: 512} + } + ); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(2); + expect(chart.legend.legendItems.length).toBe(2); + }); + + it('should show navigation if display auto and low space', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: 'auto', + maxRows: 1 + } + } + }, + } + }); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + }); + + it('should change pages when clicking on the navigation', async function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: true, + maxRows: 1 + } + } + }, + } + }); + + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.navigation.page).toBe(0); + + const next = { + x: chart.legend.navigation.next.x + (chart.legend.navigation.next.width / 2), + y: chart.legend.navigation.next.y + (chart.legend.navigation.next.height / 2) + }; + + await jasmine.triggerMouseEvent(chart, 'click', next); + + expect(chart.legend.navigation.page).toBe(1); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + + await jasmine.triggerMouseEvent(chart, 'click', next); + + expect(chart.legend.navigation.page).toBe(2); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(2); + + await jasmine.triggerMouseEvent(chart, 'click', next); + + expect(chart.legend.navigation.page).toBe(2); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(2); + + const prev = { + x: chart.legend.navigation.prev.x + (chart.legend.navigation.prev.width / 2), + y: chart.legend.navigation.prev.y + (chart.legend.navigation.prev.height / 2) + }; + + await jasmine.triggerMouseEvent(chart, 'click', prev); + + expect(chart.legend.navigation.page).toBe(1); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + + await jasmine.triggerMouseEvent(chart, 'click', prev); + + expect(chart.legend.navigation.page).toBe(0); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + + await jasmine.triggerMouseEvent(chart, 'click', prev); + + expect(chart.legend.navigation.page).toBe(0); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + }); + + it('should show navigation after changing the options', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: false, + maxRows: 1 + } + } + }, + } + }, + { + canvas: {width: 512, height: 512} + } + ); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(10); + expect(chart.legend.legendItems.length).toBe(10); + + chart.options.plugins.legend.navigation.display = true; + chart.update(); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + }); + + it('should hide navigation after changing the options', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: true, + maxRows: 1 + } + } + }, + } + }, + { + canvas: {width: 512, height: 512} + } + ); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + + chart.options.plugins.legend.navigation.display = false; + chart.update(); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(10); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should show navigation after adding labels when display auto', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4'], + datasets: [{ + data: [10, 20, 30, 40] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: 'auto', + maxRows: 1 + } + } + }, + } + }); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(4); + + chart.data.labels.push('Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'); + chart.update(); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should hide navigation after removing labels when display auto', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: 'auto', + maxRows: 1 + } + } + }, + } + }); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(10); + + chart.data.labels.splice(4, 6); + chart.update(); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(4); + }); + + it('should respect max rows', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: 'auto', + maxRows: 1 + } + } + }, + } + }); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(10); + + chart.options.plugins.legend.navigation.maxRows = 2; + chart.update(); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(8); + expect(chart.legend.navigation.legendItems.length).toBe(8); + expect(chart.legend.legendItems.length).toBe(10); + + chart.options.plugins.legend.navigation.maxRows = 3; + chart.update(); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(10); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should respect horizontal grid', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['AAAAAAAAAAAAAAAAAAAA', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: true, + maxRows: 5, + grid: { + x: true + } + } + } + }, + } + }); + + expect(chart.legend.navigation).toBeTruthy(); + + const largestLabel = chart.legend.legendHitBoxes[0]; + + chart.legend.legendHitBoxes.forEach((box) => { + expect(box.offsetWidth).toBe(largestLabel.offsetWidth); + }); + + chart.options.plugins.legend.navigation.grid.x = false; + chart.update(); + + chart.legend.legendHitBoxes.slice(1).forEach((box) => { + expect(largestLabel.offsetWidth).toBeGreaterThan(box.offsetWidth); + }); + }); + + it('should respect vertical grid', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: [['Label 1', 'Multiline'], 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: true, + maxRows: 5, + grid: { + y: true + } + } + } + }, + } + }); + + expect(chart.legend.navigation).toBeTruthy(); + + const padding = chart.options.plugins.legend.labels.padding; + let highestLabel = chart.legend.legendHitBoxes[0]; + let topBlock = chart.legend.navigation.blocks[0]; + + expect(topBlock.height).toBe(highestLabel.height + padding); + + chart.legend.navigation.blocks.forEach((block, blockIndex) => { + for (let i = block.start; i < block.end; i++) { + const legend = chart.legend.legendHitBoxes[i]; + expect(legend.top - padding).toBe(topBlock.height * blockIndex); + } + }); + + chart.options.plugins.legend.navigation.grid.y = false; + chart.update(); + + highestLabel = chart.legend.legendHitBoxes[0]; + topBlock = chart.legend.navigation.blocks[0]; + + expect(topBlock.height).toBe(highestLabel.height + padding); + + chart.legend.navigation.blocks.slice(1).forEach((block) => { + for (let i = block.start; i < block.end; i++) { + const legend = chart.legend.legendHitBoxes[i]; + expect((legend.top + block.height) - topBlock.height - padding).toBeLessThan(topBlock.height); + } + }); + }); + + it('should show navigation on resize when display auto', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'top', + navigation: { + display: 'auto', + maxRows: 1 + } + } + }, + } + }, + { + canvas: {width: 800, height: 512} + } + ); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(8); + expect(chart.legend.legendItems.length).toBe(8); + + chart.resize(512, 512); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(5); + expect(chart.legend.navigation.legendItems.length).toBe(5); + expect(chart.legend.legendItems.length).toBe(8); + }); + }); + + describe('vertical', function() { + it('should not show navigation by default', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10', 'Label 11', 'Label 12', 'Label 13', 'Label 14', 'Label 15'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10, 20, 30, 40, 50, 60] + }] + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'right', + maxWidth: 100, + } + } + } + }, + { + canvas: {width: 512, height: 250} + } + ); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(15); + expect(chart.legend.legendItems.length).toBe(15); + }); + + it('should not show navigation if display false', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: false + } + } + } + } + }); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(10); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should not show navigation if options is false', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'right', + navigation: false + } + }, + } + }); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(10); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should show navigation if display true', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3'], + datasets: [{ + data: [10, 20, 30] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: true, + maxCols: 1 + } + } + }, + } + }); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(3); + expect(chart.legend.navigation.legendItems.length).toBe(3); + }); + + it('should not show navigation if display auto and enough space', function() { + const chart = acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2'], + datasets: [{ + data: [10, 20] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: 'auto', + maxCols: 1 + } + } + }, + } + }); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(2); + expect(chart.legend.legendItems.length).toBe(2); + }); + + it('should show navigation if display auto and low space', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: 'auto', + maxCols: 1 + } + } + }, + } + }, + { + canvas: {width: 512, height: 130} + } + ); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should change pages when clicking on the navigation', async function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: true, + maxCols: 1 + } + } + }, + } + }, + { + canvas: {width: 512, height: 130} + } + ); + + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.navigation.page).toBe(0); + + const next = { + x: chart.legend.navigation.next.x + (chart.legend.navigation.next.width / 2), + y: chart.legend.navigation.next.y + (chart.legend.navigation.next.height / 2) + }; + + await jasmine.triggerMouseEvent(chart, 'click', next); + + expect(chart.legend.navigation.page).toBe(1); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + + await jasmine.triggerMouseEvent(chart, 'click', next); + + expect(chart.legend.navigation.page).toBe(2); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(2); + + await jasmine.triggerMouseEvent(chart, 'click', next); + + expect(chart.legend.navigation.page).toBe(2); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(2); + + const prev = { + x: chart.legend.navigation.prev.x + (chart.legend.navigation.prev.width / 2), + y: chart.legend.navigation.prev.y + (chart.legend.navigation.prev.height / 2) + }; + + await jasmine.triggerMouseEvent(chart, 'click', prev); + + expect(chart.legend.navigation.page).toBe(1); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + + await jasmine.triggerMouseEvent(chart, 'click', prev); + + expect(chart.legend.navigation.page).toBe(0); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + + await jasmine.triggerMouseEvent(chart, 'click', prev); + + expect(chart.legend.navigation.page).toBe(0); + expect(chart.legend.legendItems.length).toBe(10); + expect(chart.legend.legendHitBoxes.length).toBe(4); + }); + + it('should show navigation after changing the options', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: false, + maxRows: 1 + } + } + }, + } + }, + { + canvas: {width: 512, height: 130} + } + ); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(10); + expect(chart.legend.legendItems.length).toBe(10); + + chart.options.plugins.legend.navigation.display = true; + chart.update(); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should hide navigation after changing the options', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: true, + maxRows: 1 + } + } + }, + } + }, + { + canvas: {width: 512, height: 130} + } + ); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(10); + + chart.options.plugins.legend.navigation.display = false; + chart.update(); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(10); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should show navigation after adding labels when display auto', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4'], + datasets: [{ + data: [10, 20, 30, 40] + }] + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: 'auto', + maxCols: 1 + } + } + }, + } + }, + { + canvas: {width: 512, height: 130} + } + ); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(4); + + chart.data.labels.push('Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'); + chart.update(); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should hide navigation after removing labels when display auto', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: 'auto', + maxCols: 1 + } + } + }, + } + }, + { + canvas: {width: 512, height: 130} + } + ); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(10); + + chart.data.labels.splice(4, 6); + chart.update(); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(4); + }); + + it('should respect max cols', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: 'auto', + maxCols: 1 + } + } + }, + } + }, + { + canvas: {width: 800, height: 130} + } + ); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(10); + + chart.options.plugins.legend.navigation.maxCols = 2; + chart.update(); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(8); + expect(chart.legend.navigation.legendItems.length).toBe(8); + expect(chart.legend.legendItems.length).toBe(10); + + chart.options.plugins.legend.navigation.maxCols = 3; + chart.update(); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(10); + expect(chart.legend.legendItems.length).toBe(10); + }); + + it('should respect horizontal grid', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['AAAAAAAAAAAAAAA', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'right', + maxWidth: 800, + navigation: { + display: true, + maxCols: 5, + grid: { + x: true + } + } + } + }, + } + }, + { + canvas: {width: 800, height: 130} + } + ); + + expect(chart.legend.navigation).toBeTruthy(); + + const padding = chart.options.plugins.legend.labels.padding; + let largestLabel = chart.legend.legendHitBoxes[0]; + let leftBlock = chart.legend.navigation.blocks[0]; + + expect(leftBlock.width).toBe(largestLabel.width + padding); + + chart.legend.navigation.blocks.forEach((block, blockIndex) => { + for (let i = block.start; i < block.end; i++) { + const legend = chart.legend.legendHitBoxes[i]; + expect(legend.left - padding).toBe(chart.legend.left + (leftBlock.width * blockIndex)); + } + }); + + chart.options.plugins.legend.navigation.grid.x = false; + chart.update(); + + largestLabel = chart.legend.legendHitBoxes[0]; + leftBlock = chart.legend.navigation.blocks[0]; + + expect(leftBlock.width).toBe(largestLabel.width + padding); + + let leftOffset = leftBlock.width; + chart.legend.navigation.blocks.slice(1).forEach((block) => { + for (let i = block.start; i < block.end; i++) { + const legend = chart.legend.legendHitBoxes[i]; + expect((legend.left - chart.legend.left + block.width) - leftOffset - padding).toBeLessThan(leftBlock.width); + } + leftOffset += block.width; + }); + }); + + it('should respect vertical grid', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: [['Label 1', 'Multiline'], 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8', 'Label 9', 'Label 10'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: true, + maxCols: 5, + grid: { + y: true + } + } + } + }, + } + }, + { + canvas: {width: 800, height: 200} + } + ); + + expect(chart.legend.navigation).toBeTruthy(); + + const highestLabel = chart.legend.legendHitBoxes[0]; + + chart.legend.legendHitBoxes.forEach((box) => { + expect(box.offsetHeight).toBe(highestLabel.offsetHeight); + }); + + chart.options.plugins.legend.navigation.grid.y = false; + chart.update(); + + chart.legend.legendHitBoxes.slice(1).forEach((box) => { + expect(highestLabel.offsetHeight).toBeGreaterThan(box.offsetHeight); + }); + }); + + it('should show navigation on resize when display auto', function() { + const chart = acquireChart( + { + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2', 'Label 3', 'Label 4', 'Label 5', 'Label 6', 'Label 7', 'Label 8'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70, 80, 90, 10] + }] + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'right', + navigation: { + display: 'auto', + maxCols: 1 + } + } + }, + } + }, + { + canvas: {width: 512, height: 512} + } + ); + + expect(chart.legend.navigation).toBeUndefined(); + expect(chart.legend.legendHitBoxes.length).toBe(8); + expect(chart.legend.legendItems.length).toBe(8); + + chart.resize(512, 130); + + expect(chart.legend.navigation).toBeTruthy(); + expect(chart.legend.legendHitBoxes.length).toBe(4); + expect(chart.legend.navigation.legendItems.length).toBe(4); + expect(chart.legend.legendItems.length).toBe(8); + }); + }); + }); });