mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-03 15:04:03 +01:00
417 lines
14 KiB
JavaScript
417 lines
14 KiB
JavaScript
import defaults from './core.defaults';
|
|
import {each, isObject} from '../helpers/helpers.core';
|
|
import {toPadding} from '../helpers/helpers.options';
|
|
|
|
/**
|
|
* @typedef { import("./core.controller").default } Chart
|
|
*/
|
|
|
|
const STATIC_POSITIONS = ['left', 'top', 'right', 'bottom'];
|
|
|
|
function filterByPosition(array, position) {
|
|
return array.filter(v => v.pos === position);
|
|
}
|
|
|
|
function filterDynamicPositionByAxis(array, axis) {
|
|
return array.filter(v => STATIC_POSITIONS.indexOf(v.pos) === -1 && v.box.axis === axis);
|
|
}
|
|
|
|
function sortByWeight(array, reverse) {
|
|
return array.sort((a, b) => {
|
|
const v0 = reverse ? b : a;
|
|
const v1 = reverse ? a : b;
|
|
return v0.weight === v1.weight ?
|
|
v0.index - v1.index :
|
|
v0.weight - v1.weight;
|
|
});
|
|
}
|
|
|
|
function wrapBoxes(boxes) {
|
|
const layoutBoxes = [];
|
|
let i, ilen, box;
|
|
|
|
for (i = 0, ilen = (boxes || []).length; i < ilen; ++i) {
|
|
box = boxes[i];
|
|
layoutBoxes.push({
|
|
index: i,
|
|
box,
|
|
pos: box.position,
|
|
horizontal: box.isHorizontal(),
|
|
weight: box.weight
|
|
});
|
|
}
|
|
return layoutBoxes;
|
|
}
|
|
|
|
function setLayoutDims(layouts, params) {
|
|
let i, ilen, layout;
|
|
for (i = 0, ilen = layouts.length; i < ilen; ++i) {
|
|
layout = layouts[i];
|
|
// store dimensions used instead of available chartArea in fitBoxes
|
|
if (layout.horizontal) {
|
|
layout.width = layout.box.fullSize && params.availableWidth;
|
|
layout.height = params.hBoxMaxHeight;
|
|
} else {
|
|
layout.width = params.vBoxMaxWidth;
|
|
layout.height = layout.box.fullSize && params.availableHeight;
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildLayoutBoxes(boxes) {
|
|
const layoutBoxes = wrapBoxes(boxes);
|
|
const fullSize = sortByWeight(layoutBoxes.filter(wrap => wrap.box.fullSize), true);
|
|
const left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true);
|
|
const right = sortByWeight(filterByPosition(layoutBoxes, 'right'));
|
|
const top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true);
|
|
const bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom'));
|
|
const centerHorizontal = filterDynamicPositionByAxis(layoutBoxes, 'x');
|
|
const centerVertical = filterDynamicPositionByAxis(layoutBoxes, 'y');
|
|
|
|
return {
|
|
fullSize,
|
|
leftAndTop: left.concat(top),
|
|
rightAndBottom: right.concat(centerVertical).concat(bottom).concat(centerHorizontal),
|
|
chartArea: filterByPosition(layoutBoxes, 'chartArea'),
|
|
vertical: left.concat(right).concat(centerVertical),
|
|
horizontal: top.concat(bottom).concat(centerHorizontal)
|
|
};
|
|
}
|
|
|
|
function getCombinedMax(maxPadding, chartArea, a, b) {
|
|
return Math.max(maxPadding[a], chartArea[a]) + Math.max(maxPadding[b], chartArea[b]);
|
|
}
|
|
|
|
function updateMaxPadding(maxPadding, boxPadding) {
|
|
maxPadding.top = Math.max(maxPadding.top, boxPadding.top);
|
|
maxPadding.left = Math.max(maxPadding.left, boxPadding.left);
|
|
maxPadding.bottom = Math.max(maxPadding.bottom, boxPadding.bottom);
|
|
maxPadding.right = Math.max(maxPadding.right, boxPadding.right);
|
|
}
|
|
|
|
function updateDims(chartArea, params, layout) {
|
|
const box = layout.box;
|
|
const maxPadding = chartArea.maxPadding;
|
|
|
|
// dynamically placed boxes size is not considered
|
|
if (!isObject(layout.pos)) {
|
|
if (layout.size) {
|
|
// this layout was already counted for, lets first reduce old size
|
|
chartArea[layout.pos] -= layout.size;
|
|
}
|
|
layout.size = layout.horizontal ? box.height : box.width;
|
|
chartArea[layout.pos] += layout.size;
|
|
}
|
|
|
|
if (box.getPadding) {
|
|
updateMaxPadding(maxPadding, box.getPadding());
|
|
}
|
|
|
|
const newWidth = Math.max(0, params.outerWidth - getCombinedMax(maxPadding, chartArea, 'left', 'right'));
|
|
const newHeight = Math.max(0, params.outerHeight - getCombinedMax(maxPadding, chartArea, 'top', 'bottom'));
|
|
const widthChanged = newWidth !== chartArea.w;
|
|
const heightChanged = newHeight !== chartArea.h;
|
|
chartArea.w = newWidth;
|
|
chartArea.h = newHeight;
|
|
|
|
// return booleans on the changes per direction
|
|
return layout.horizontal
|
|
? {same: widthChanged, other: heightChanged}
|
|
: {same: heightChanged, other: widthChanged};
|
|
}
|
|
|
|
function handleMaxPadding(chartArea) {
|
|
const maxPadding = chartArea.maxPadding;
|
|
|
|
function updatePos(pos) {
|
|
const change = Math.max(maxPadding[pos] - chartArea[pos], 0);
|
|
chartArea[pos] += change;
|
|
return change;
|
|
}
|
|
chartArea.y += updatePos('top');
|
|
chartArea.x += updatePos('left');
|
|
updatePos('right');
|
|
updatePos('bottom');
|
|
}
|
|
|
|
function getMargins(horizontal, chartArea) {
|
|
const maxPadding = chartArea.maxPadding;
|
|
|
|
function marginForPositions(positions) {
|
|
const margin = {left: 0, top: 0, right: 0, bottom: 0};
|
|
positions.forEach((pos) => {
|
|
margin[pos] = Math.max(chartArea[pos], maxPadding[pos]);
|
|
});
|
|
return margin;
|
|
}
|
|
|
|
return horizontal
|
|
? marginForPositions(['left', 'right'])
|
|
: marginForPositions(['top', 'bottom']);
|
|
}
|
|
|
|
function fitBoxes(boxes, chartArea, params) {
|
|
const refitBoxes = [];
|
|
let i, ilen, layout, box, refit, changed;
|
|
|
|
for (i = 0, ilen = boxes.length, refit = 0; i < ilen; ++i) {
|
|
layout = boxes[i];
|
|
box = layout.box;
|
|
|
|
box.update(
|
|
layout.width || chartArea.w,
|
|
layout.height || chartArea.h,
|
|
getMargins(layout.horizontal, chartArea)
|
|
);
|
|
const {same, other} = updateDims(chartArea, params, layout);
|
|
|
|
// Dimensions changed and there were non full width boxes before this
|
|
// -> we have to refit those
|
|
refit |= same && refitBoxes.length;
|
|
|
|
// Chart area changed in the opposite direction
|
|
changed = changed || other;
|
|
|
|
if (!box.fullSize) { // fullSize boxes don't need to be re-fitted in any case
|
|
refitBoxes.push(layout);
|
|
}
|
|
}
|
|
|
|
return refit && fitBoxes(refitBoxes, chartArea, params) || changed;
|
|
}
|
|
|
|
function placeBoxes(boxes, chartArea, params) {
|
|
const userPadding = params.padding;
|
|
let x = chartArea.x;
|
|
let y = chartArea.y;
|
|
let i, ilen, layout, box;
|
|
|
|
for (i = 0, ilen = boxes.length; i < ilen; ++i) {
|
|
layout = boxes[i];
|
|
box = layout.box;
|
|
if (layout.horizontal) {
|
|
box.left = box.fullSize ? userPadding.left : chartArea.left;
|
|
box.right = box.fullSize ? params.outerWidth - userPadding.right : chartArea.left + chartArea.w;
|
|
box.top = y;
|
|
box.bottom = y + box.height;
|
|
box.width = box.right - box.left;
|
|
y = box.bottom;
|
|
} else {
|
|
box.left = x;
|
|
box.right = x + box.width;
|
|
box.top = box.fullSize ? userPadding.top : chartArea.top;
|
|
box.bottom = box.fullSize ? params.outerHeight - userPadding.right : chartArea.top + chartArea.h;
|
|
box.height = box.bottom - box.top;
|
|
x = box.right;
|
|
}
|
|
}
|
|
|
|
chartArea.x = x;
|
|
chartArea.y = y;
|
|
}
|
|
|
|
defaults.set('layout', {
|
|
padding: {
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @interface LayoutItem
|
|
* @typedef {object} LayoutItem
|
|
* @prop {string} position - The position of the item in the chart layout. Possible values are
|
|
* 'left', 'top', 'right', 'bottom', and 'chartArea'
|
|
* @prop {number} weight - The weight used to sort the item. Higher weights are further away from the chart area
|
|
* @prop {boolean} fullSize - if true, and the item is horizontal, then push vertical boxes down
|
|
* @prop {function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom)
|
|
* @prop {function} update - Takes two parameters: width and height. Returns size of item
|
|
* @prop {function} draw - Draws the element
|
|
* @prop {function} [getPadding] - Returns an object with padding on the edges
|
|
* @prop {number} width - Width of item. Must be valid after update()
|
|
* @prop {number} height - Height of item. Must be valid after update()
|
|
* @prop {number} left - Left edge of the item. Set by layout system and cannot be used in update
|
|
* @prop {number} top - Top edge of the item. Set by layout system and cannot be used in update
|
|
* @prop {number} right - Right edge of the item. Set by layout system and cannot be used in update
|
|
* @prop {number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update
|
|
*/
|
|
|
|
// The layout service is very self explanatory. It's responsible for the layout within a chart.
|
|
// Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need
|
|
// It is this service's responsibility of carrying out that layout.
|
|
export default {
|
|
|
|
/**
|
|
* Register a box to a chart.
|
|
* A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title.
|
|
* @param {Chart} chart - the chart to use
|
|
* @param {LayoutItem} item - the item to add to be laid out
|
|
*/
|
|
addBox(chart, item) {
|
|
if (!chart.boxes) {
|
|
chart.boxes = [];
|
|
}
|
|
|
|
// initialize item with default values
|
|
item.fullSize = item.fullSize || false;
|
|
item.position = item.position || 'top';
|
|
item.weight = item.weight || 0;
|
|
// @ts-ignore
|
|
item._layers = item._layers || function() {
|
|
return [{
|
|
z: 0,
|
|
draw(chartArea) {
|
|
item.draw(chartArea);
|
|
}
|
|
}];
|
|
};
|
|
|
|
chart.boxes.push(item);
|
|
},
|
|
|
|
/**
|
|
* Remove a layoutItem from a chart
|
|
* @param {Chart} chart - the chart to remove the box from
|
|
* @param {LayoutItem} layoutItem - the item to remove from the layout
|
|
*/
|
|
removeBox(chart, layoutItem) {
|
|
const index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1;
|
|
if (index !== -1) {
|
|
chart.boxes.splice(index, 1);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets (or updates) options on the given `item`.
|
|
* @param {Chart} chart - the chart in which the item lives (or will be added to)
|
|
* @param {LayoutItem} item - the item to configure with the given options
|
|
* @param {object} options - the new item options.
|
|
*/
|
|
configure(chart, item, options) {
|
|
item.fullSize = options.fullSize;
|
|
item.position = options.position;
|
|
item.weight = options.weight;
|
|
},
|
|
|
|
/**
|
|
* Fits boxes of the given chart into the given size by having each box measure itself
|
|
* then running a fitting algorithm
|
|
* @param {Chart} chart - the chart
|
|
* @param {number} width - the width to fit into
|
|
* @param {number} height - the height to fit into
|
|
* @param {number} minPadding - minimum padding required for each side of chart area
|
|
*/
|
|
update(chart, width, height, minPadding) {
|
|
if (!chart) {
|
|
return;
|
|
}
|
|
|
|
const padding = toPadding(chart.options.layout.padding);
|
|
const availableWidth = Math.max(width - padding.width, 0);
|
|
const availableHeight = Math.max(height - padding.height, 0);
|
|
const boxes = buildLayoutBoxes(chart.boxes);
|
|
const verticalBoxes = boxes.vertical;
|
|
const horizontalBoxes = boxes.horizontal;
|
|
|
|
// Before any changes are made, notify boxes that an update is about to being
|
|
// This is used to clear any cached data (e.g. scale limits)
|
|
each(chart.boxes, box => {
|
|
if (typeof box.beforeLayout === 'function') {
|
|
box.beforeLayout();
|
|
}
|
|
});
|
|
|
|
// Essentially we now have any number of boxes on each of the 4 sides.
|
|
// Our canvas looks like the following.
|
|
// The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and
|
|
// B1 is the bottom axis
|
|
// There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays
|
|
// These locations are single-box locations only, when trying to register a chartArea location that is already taken,
|
|
// an error will be thrown.
|
|
//
|
|
// |----------------------------------------------------|
|
|
// | T1 (Full Width) |
|
|
// |----------------------------------------------------|
|
|
// | | | T2 | |
|
|
// | |----|-------------------------------------|----|
|
|
// | | | C1 | | C2 | |
|
|
// | | |----| |----| |
|
|
// | | | | |
|
|
// | L1 | L2 | ChartArea (C0) | R1 |
|
|
// | | | | |
|
|
// | | |----| |----| |
|
|
// | | | C3 | | C4 | |
|
|
// | |----|-------------------------------------|----|
|
|
// | | | B1 | |
|
|
// |----------------------------------------------------|
|
|
// | B2 (Full Width) |
|
|
// |----------------------------------------------------|
|
|
//
|
|
|
|
const visibleVerticalBoxCount = verticalBoxes.reduce((total, wrap) =>
|
|
wrap.box.options && wrap.box.options.display === false ? total : total + 1, 0) || 1;
|
|
|
|
const params = Object.freeze({
|
|
outerWidth: width,
|
|
outerHeight: height,
|
|
padding,
|
|
availableWidth,
|
|
availableHeight,
|
|
vBoxMaxWidth: availableWidth / 2 / visibleVerticalBoxCount,
|
|
hBoxMaxHeight: availableHeight / 2
|
|
});
|
|
const maxPadding = Object.assign({}, padding);
|
|
updateMaxPadding(maxPadding, toPadding(minPadding));
|
|
const chartArea = Object.assign({
|
|
maxPadding,
|
|
w: availableWidth,
|
|
h: availableHeight,
|
|
x: padding.left,
|
|
y: padding.top
|
|
}, padding);
|
|
|
|
setLayoutDims(verticalBoxes.concat(horizontalBoxes), params);
|
|
|
|
// First fit the fullSize boxes, to reduce probability of re-fitting.
|
|
fitBoxes(boxes.fullSize, chartArea, params);
|
|
|
|
// Then fit vertical boxes
|
|
fitBoxes(verticalBoxes, chartArea, params);
|
|
|
|
// Then fit horizontal boxes
|
|
if (fitBoxes(horizontalBoxes, chartArea, params)) {
|
|
// if the area changed, re-fit vertical boxes
|
|
fitBoxes(verticalBoxes, chartArea, params);
|
|
}
|
|
|
|
handleMaxPadding(chartArea);
|
|
|
|
// Finally place the boxes to correct coordinates
|
|
placeBoxes(boxes.leftAndTop, chartArea, params);
|
|
|
|
// Move to opposite side of chart
|
|
chartArea.x += chartArea.w;
|
|
chartArea.y += chartArea.h;
|
|
|
|
placeBoxes(boxes.rightAndBottom, chartArea, params);
|
|
|
|
chart.chartArea = {
|
|
left: chartArea.left,
|
|
top: chartArea.top,
|
|
right: chartArea.left + chartArea.w,
|
|
bottom: chartArea.top + chartArea.h,
|
|
height: chartArea.h,
|
|
width: chartArea.w,
|
|
};
|
|
|
|
// Finally update boxes in chartArea (radial scale for example)
|
|
each(boxes.chartArea, (layout) => {
|
|
const box = layout.box;
|
|
Object.assign(box, chart.chartArea);
|
|
box.update(chartArea.w, chartArea.h);
|
|
});
|
|
}
|
|
};
|