TelegramPWA/src/lib/lovely-chart/drawDatasets.js
2022-04-09 01:18:22 +02:00

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();
}