259 lines
7.5 KiB
JavaScript
259 lines
7.5 KiB
JavaScript
import { getCssColor } from './skin';
|
|
import { mergeArrays } from './utils';
|
|
import { getPieRadius, getPieTextShift, getPieTextSize } from './formulas';
|
|
import { PLOT_BARS_WIDTH_SHIFT, PLOT_PIE_SHIFT, PIE_MINIMUM_VISIBLE_PERCENT } from './constants';
|
|
import { simplify } from './simplify';
|
|
import { toPixels } from './Projection';
|
|
|
|
export function drawDatasets(
|
|
context, state, data,
|
|
range, points, projection, secondaryPoints, secondaryProjection,
|
|
lineWidth, visibilities, colors, pieToBar, simplification,
|
|
) {
|
|
data.datasets.forEach(({ key, type, hasOwnYAxis }, i) => {
|
|
if (!visibilities[i]) {
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
color: getCssColor(colors, `dataset#${key}`),
|
|
lineWidth,
|
|
opacity: data.isStacked ? 1 : visibilities[i],
|
|
simplification,
|
|
};
|
|
|
|
const datasetType = type === 'pie' && pieToBar ? 'bar' : type;
|
|
let datasetPoints = hasOwnYAxis ? secondaryPoints : points[i];
|
|
let datasetProjection = hasOwnYAxis ? secondaryProjection : projection;
|
|
|
|
if (datasetType === 'area') {
|
|
const { yMin, yMax } = projection.getParams();
|
|
const yHeight = yMax - yMin;
|
|
const bottomLine = [
|
|
{ labelIndex: range.from, stackValue: 0 },
|
|
{ labelIndex: range.to, stackValue: 0 },
|
|
];
|
|
const topLine = [
|
|
{ labelIndex: range.to, stackValue: yHeight },
|
|
{ labelIndex: range.from, stackValue: yHeight },
|
|
];
|
|
|
|
datasetPoints = mergeArrays([points[i - 1] || bottomLine, topLine]);
|
|
}
|
|
|
|
if (datasetType === 'pie') {
|
|
options.center = projection.getCenter();
|
|
options.radius = getPieRadius(projection);
|
|
options.pointerVector = state.focusOn;
|
|
}
|
|
|
|
if (datasetType === 'bar') {
|
|
const [x0] = toPixels(projection, 0, 0);
|
|
const [x1] = toPixels(projection, 1, 0);
|
|
|
|
options.lineWidth = x1 - x0;
|
|
options.focusOn = state.focusOn;
|
|
}
|
|
|
|
drawDataset(datasetType, context, datasetPoints, datasetProjection, options);
|
|
});
|
|
|
|
if (state.focusOn && (data.isBars || data.isSteps)) {
|
|
const [x0] = toPixels(projection, 0, 0);
|
|
const [x1] = toPixels(projection, 1, 0);
|
|
|
|
drawBarsMask(context, projection, {
|
|
focusOn: state.focusOn,
|
|
color: getCssColor(colors, 'mask'),
|
|
lineWidth: data.isSteps ? x1 - x0 + lineWidth : x1 - x0,
|
|
});
|
|
}
|
|
}
|
|
|
|
function drawDataset(type, ...args) {
|
|
switch (type) {
|
|
case 'line':
|
|
return drawDatasetLine(...args);
|
|
case 'bar':
|
|
return drawDatasetBars(...args);
|
|
case 'step':
|
|
return drawDatasetSteps(...args);
|
|
case 'area':
|
|
return drawDatasetArea(...args);
|
|
case 'pie':
|
|
return drawDatasetPie(...args);
|
|
}
|
|
}
|
|
|
|
function drawDatasetLine(context, points, projection, options) {
|
|
context.beginPath();
|
|
|
|
let pixels = [];
|
|
|
|
for (let j = 0, l = points.length; j < l; j++) {
|
|
const { labelIndex, stackValue } = points[j];
|
|
pixels.push(toPixels(projection, labelIndex, stackValue));
|
|
}
|
|
|
|
if (options.simplification) {
|
|
const simplifierFn = simplify(pixels);
|
|
pixels = simplifierFn(options.simplification).points;
|
|
}
|
|
|
|
pixels.forEach(([x, y]) => {
|
|
context.lineTo(x, y);
|
|
});
|
|
|
|
context.save();
|
|
context.strokeStyle = options.color;
|
|
context.lineWidth = options.lineWidth;
|
|
context.globalAlpha = options.opacity;
|
|
context.lineJoin = 'bevel';
|
|
context.lineCap = 'butt';
|
|
context.stroke();
|
|
context.restore();
|
|
}
|
|
|
|
// TODO try areas
|
|
function drawDatasetBars(context, points, projection, options) {
|
|
const { yMin } = projection.getParams();
|
|
|
|
context.save();
|
|
context.globalAlpha = options.opacity;
|
|
context.fillStyle = options.color;
|
|
|
|
for (let j = 0, l = points.length; j < l; j++) {
|
|
const { labelIndex, stackValue, stackOffset = 0 } = points[j];
|
|
|
|
const [, yFrom] = toPixels(projection, labelIndex, Math.max(stackOffset, yMin));
|
|
const [x, yTo] = toPixels(projection, labelIndex, stackValue);
|
|
const rectX = x - options.lineWidth / 2;
|
|
const rectY = yTo;
|
|
const rectW = options.opacity === 1 ?
|
|
options.lineWidth + PLOT_BARS_WIDTH_SHIFT :
|
|
options.lineWidth + PLOT_BARS_WIDTH_SHIFT * options.opacity;
|
|
const rectH = yFrom - yTo;
|
|
|
|
context.fillRect(rectX, rectY, rectW, rectH);
|
|
}
|
|
|
|
context.restore();
|
|
}
|
|
|
|
function drawDatasetSteps(context, points, projection, options) {
|
|
context.beginPath();
|
|
|
|
let pixels = [];
|
|
|
|
for (let j = 0, l = points.length; j < l; j++) {
|
|
const { labelIndex, stackValue } = points[j];
|
|
pixels.push(
|
|
toPixels(projection, labelIndex - PLOT_BARS_WIDTH_SHIFT, stackValue),
|
|
toPixels(projection, labelIndex + PLOT_BARS_WIDTH_SHIFT, stackValue),
|
|
);
|
|
}
|
|
|
|
pixels.forEach(([x, y]) => {
|
|
context.lineTo(x, y);
|
|
});
|
|
|
|
context.save();
|
|
context.strokeStyle = options.color;
|
|
context.lineWidth = options.lineWidth;
|
|
context.globalAlpha = options.opacity;
|
|
context.stroke();
|
|
context.restore();
|
|
}
|
|
|
|
function drawBarsMask(context, projection, options) {
|
|
const [xCenter, yCenter] = projection.getCenter();
|
|
const [width, height] = projection.getSize();
|
|
|
|
const [x] = toPixels(projection, options.focusOn, 0);
|
|
|
|
context.fillStyle = options.color;
|
|
context.fillRect(xCenter - width / 2, yCenter - height / 2, x - options.lineWidth / 2 + PLOT_BARS_WIDTH_SHIFT, height);
|
|
context.fillRect(x + options.lineWidth / 2, yCenter - height / 2, width - (x + options.lineWidth / 2), height);
|
|
}
|
|
|
|
function drawDatasetArea(context, points, projection, options) {
|
|
context.beginPath();
|
|
|
|
let pixels = [];
|
|
|
|
for (let j = 0, l = points.length; j < l; j++) {
|
|
const { labelIndex, stackValue } = points[j];
|
|
pixels.push(toPixels(projection, labelIndex, stackValue));
|
|
}
|
|
|
|
if (options.simplification) {
|
|
const simplifierFn = simplify(pixels);
|
|
pixels = simplifierFn(options.simplification).points;
|
|
}
|
|
|
|
pixels.forEach(([x, y]) => {
|
|
context.lineTo(x, y);
|
|
});
|
|
|
|
context.save();
|
|
context.fillStyle = options.color;
|
|
context.lineWidth = options.lineWidth;
|
|
context.globalAlpha = options.opacity;
|
|
context.lineJoin = 'bevel';
|
|
context.lineCap = 'butt';
|
|
context.fill();
|
|
context.restore();
|
|
}
|
|
|
|
function drawDatasetPie(context, points, projection, options) {
|
|
const { visibleValue, stackValue, stackOffset = 0 } = points[0];
|
|
|
|
if (!visibleValue) {
|
|
return;
|
|
}
|
|
|
|
const { yMin, yMax } = projection.getParams();
|
|
const percentFactor = 1 / (yMax - yMin);
|
|
const percent = visibleValue * percentFactor;
|
|
|
|
const beginAngle = stackOffset * percentFactor * Math.PI * 2 - Math.PI / 2;
|
|
const endAngle = stackValue * percentFactor * Math.PI * 2 - Math.PI / 2;
|
|
|
|
const { radius = 120, center: [x, y], pointerVector } = options;
|
|
|
|
const shift = (
|
|
pointerVector &&
|
|
beginAngle <= pointerVector.angle &&
|
|
pointerVector.angle < endAngle &&
|
|
pointerVector.distance <= radius
|
|
) ? PLOT_PIE_SHIFT : 0;
|
|
|
|
const shiftAngle = (beginAngle + endAngle) / 2;
|
|
const directionX = Math.cos(shiftAngle);
|
|
const directionY = Math.sin(shiftAngle);
|
|
const shiftX = directionX * shift;
|
|
const shiftY = directionY * shift;
|
|
|
|
context.save();
|
|
|
|
context.beginPath();
|
|
context.fillStyle = options.color;
|
|
context.moveTo(x + shiftX, y + shiftY);
|
|
context.arc(x + shiftX, y + shiftY, radius, beginAngle, endAngle);
|
|
context.lineTo(x + shiftX, y + shiftY);
|
|
context.fill();
|
|
|
|
if (percent >= PIE_MINIMUM_VISIBLE_PERCENT) {
|
|
context.font = `700 ${getPieTextSize(percent, radius)}px Helvetica, Arial, sans-serif`;
|
|
context.textAlign = 'center';
|
|
context.textBaseline = 'middle';
|
|
context.fillStyle = 'white';
|
|
const textShift = getPieTextShift(percent, radius);
|
|
context.fillText(
|
|
`${Math.round(percent * 100)}%`, x + directionX * textShift + shiftX, y + directionY * textShift + shiftY,
|
|
);
|
|
}
|
|
|
|
context.restore();
|
|
}
|