Sync lovely-chart from upstream (#6959)
This commit is contained in:
parent
1c9d35c634
commit
ec9add5c67
@ -1,9 +1,14 @@
|
||||
import { GUTTER, AXES_FONT, X_AXIS_HEIGHT, X_AXIS_SHIFT_START, PLOT_TOP_PADDING } from './constants.js';
|
||||
import { GUTTER, AXES_FONT_STYLE, X_AXIS_HEIGHT, X_AXIS_SHIFT_START, PLOT_TOP_PADDING } from './constants.js';
|
||||
import { humanize } from './format.js';
|
||||
import { getCssColor } from './skin.js';
|
||||
import { applyXEdgeOpacity, applyYEdgeOpacity, xScaleLevelToStep, yScaleLevelToStep } from './formulas.js';
|
||||
import { toPixels } from './Projection.js';
|
||||
|
||||
function getAxesFont(context) {
|
||||
const fontFamily = getComputedStyle(context.canvas).fontFamily || 'sans-serif';
|
||||
return `${AXES_FONT_STYLE} ${fontFamily}`;
|
||||
}
|
||||
|
||||
export function createAxes(context, data, plotSize, colors) {
|
||||
function drawXAxis(state, projection) {
|
||||
context.clearRect(0, plotSize.height - X_AXIS_HEIGHT + 1, plotSize.width, X_AXIS_HEIGHT + 1);
|
||||
@ -13,7 +18,7 @@ export function createAxes(context, data, plotSize, colors) {
|
||||
const step = xScaleLevelToStep(scaleLevel);
|
||||
const opacityFactor = 1 - (state.xAxisScale - scaleLevel);
|
||||
|
||||
context.font = AXES_FONT;
|
||||
context.font = getAxesFont(context);
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'middle';
|
||||
|
||||
@ -126,7 +131,7 @@ export function createAxes(context, data, plotSize, colors) {
|
||||
const firstVisibleValue = Math.ceil(yMin / step) * step;
|
||||
const lastVisibleValue = Math.floor(yMax / step) * step;
|
||||
|
||||
context.font = AXES_FONT;
|
||||
context.font = getAxesFont(context);
|
||||
context.textAlign = isSecondary ? 'right' : 'left';
|
||||
context.textBaseline = 'bottom';
|
||||
|
||||
@ -171,7 +176,7 @@ export function createAxes(context, data, plotSize, colors) {
|
||||
const percentValues = [0, 0.25, 0.50, 0.75, 1];
|
||||
const [, height] = projection.getSize();
|
||||
|
||||
context.font = AXES_FONT;
|
||||
context.font = getAxesFont(context);
|
||||
context.textAlign = 'left';
|
||||
context.textBaseline = 'bottom';
|
||||
context.lineWidth = 1;
|
||||
@ -198,7 +203,7 @@ export function createAxes(context, data, plotSize, colors) {
|
||||
const firstVisibleValue = Math.ceil(yMin / step) * step;
|
||||
const lastVisibleValue = Math.floor(yMax / step) * step;
|
||||
|
||||
context.font = AXES_FONT;
|
||||
context.font = getAxesFont(context);
|
||||
context.textAlign = 'right';
|
||||
context.textBaseline = 'bottom';
|
||||
|
||||
|
||||
@ -167,9 +167,9 @@ function create(container, originalData) {
|
||||
}
|
||||
|
||||
function _setupGlobalListeners() {
|
||||
document.documentElement.addEventListener('darkmode', () => {
|
||||
new MutationObserver(() => {
|
||||
_stateManager.update();
|
||||
});
|
||||
}).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth !== _windowWidth) {
|
||||
|
||||
@ -23,11 +23,14 @@ export function createMinimap(container, data, colors, rangeCallback) {
|
||||
let _canvasSize;
|
||||
let _ruler;
|
||||
let _slider;
|
||||
let _limitMask;
|
||||
|
||||
let _capturedOffset;
|
||||
let _range = {};
|
||||
let _state;
|
||||
|
||||
const _limitBegin = data.limitBegin;
|
||||
|
||||
const _updateRulerOnRaf = throttleWithRaf(_updateRuler);
|
||||
|
||||
_setupLayout();
|
||||
@ -69,6 +72,7 @@ export function createMinimap(container, data, colors, rangeCallback) {
|
||||
|
||||
_setupCanvas();
|
||||
_setupRuler();
|
||||
_setupLimitMask();
|
||||
|
||||
container.appendChild(_element);
|
||||
|
||||
@ -139,6 +143,23 @@ export function createMinimap(container, data, colors, rangeCallback) {
|
||||
_element.appendChild(_ruler);
|
||||
}
|
||||
|
||||
function _setupLimitMask() {
|
||||
if (_limitBegin == null) return;
|
||||
|
||||
_limitMask = createElement();
|
||||
_limitMask.className = 'lovely-chart--minimap-limit-mask';
|
||||
_limitMask.style.width = `${_limitBegin * 100}%`;
|
||||
_limitMask.innerHTML =
|
||||
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5265 10.2173V7.54299C16.5265 5.08532 14.4958 3.08585 11.9997 3.08585C9.50365 3.08585 7.47293 5.08532 7.47293 7.54299V10.2173C6.2992 10.2173 5.36524 11.2011 5.42629 12.3733L5.60706 15.844C5.6879 17.3962 5.72833 18.1723 6.00269 18.7852C6.39058 19.6518 7.10506 20.33 7.9906 20.6723C8.61698 20.9144 9.39412 20.9144 10.9484 20.9144H13.051C14.6053 20.9144 15.3825 20.9144 16.0088 20.6723C16.8944 20.33 17.6089 19.6518 17.9967 18.7852C18.2711 18.1723 18.3115 17.3962 18.3924 15.844L18.5731 12.3733C18.6342 11.2011 17.7002 10.2173 16.5265 10.2173ZM11.9997 4.8687C10.5023 4.8687 9.28364 6.06857 9.28364 7.54299V10.2173H14.7158V7.54299C14.7158 6.06857 13.4972 4.8687 11.9997 4.8687Z" fill="currentColor"/>' +
|
||||
'</svg>';
|
||||
if (data.onLimitedRangeClick) {
|
||||
_limitMask.classList.add('lovely-chart--state-interactive');
|
||||
_limitMask.addEventListener('click', data.onLimitedRangeClick);
|
||||
}
|
||||
_element.appendChild(_limitMask);
|
||||
}
|
||||
|
||||
function _isStateChanged(newState) {
|
||||
if (!_state) {
|
||||
return true;
|
||||
@ -206,7 +227,8 @@ export function createMinimap(container, data, colors, rangeCallback) {
|
||||
}
|
||||
|
||||
function _onSliderDrag(moveEvent, captureEvent, { dragOffsetX }) {
|
||||
const minX1 = 0;
|
||||
const limitX = _limitBegin != null ? _limitBegin * _canvasSize.width : 0;
|
||||
const minX1 = limitX;
|
||||
const maxX1 = _canvasSize.width - _slider.offsetWidth;
|
||||
|
||||
const newX1 = Math.max(minX1, Math.min(_capturedOffset + dragOffsetX - MINIMAP_EAR_WIDTH, maxX1));
|
||||
@ -218,7 +240,8 @@ export function createMinimap(container, data, colors, rangeCallback) {
|
||||
}
|
||||
|
||||
function _onLeftEarDrag(moveEvent, captureEvent, { dragOffsetX }) {
|
||||
const minX1 = 0;
|
||||
const limitX = _limitBegin != null ? _limitBegin * _canvasSize.width : 0;
|
||||
const minX1 = limitX;
|
||||
const maxX1 = _slider.offsetLeft + _slider.offsetWidth - MINIMAP_EAR_WIDTH * 2;
|
||||
|
||||
const newX1 = Math.min(maxX1, Math.max(minX1, _capturedOffset + dragOffsetX));
|
||||
@ -244,6 +267,10 @@ export function createMinimap(container, data, colors, rangeCallback) {
|
||||
nextRange = _adjustDiscreteRange(nextRange);
|
||||
}
|
||||
|
||||
if (_limitBegin != null && nextRange.begin < _limitBegin) {
|
||||
nextRange.begin = _limitBegin;
|
||||
}
|
||||
|
||||
if (nextRange.begin === _range.begin && nextRange.end === _range.end) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { createTransitionManager } from './TransitionManager.js';
|
||||
import { throttleWithRaf, getMaxMin, mergeArrays, proxyMerge, sumArrays } from './utils.js';
|
||||
import { throttleWithRaf, getMaxMin, mergeArrays, proxyMerge } from './utils.js';
|
||||
import {
|
||||
AXES_MAX_COLUMN_WIDTH,
|
||||
AXES_MAX_ROW_HEIGHT,
|
||||
X_AXIS_HEIGHT,
|
||||
ANIMATE_PROPS,
|
||||
TRANSITION_DEFAULT_DURATION,
|
||||
Y_AXIS_ZERO_BASED_THRESHOLD,
|
||||
} from './constants.js';
|
||||
import { xStepToScaleLevel, yScaleLevelToStep, yStepToScaleLevel } from './formulas.js';
|
||||
@ -55,7 +56,7 @@ export function createStateManager(data, viewportSize, callback) {
|
||||
|
||||
function _buildTransitionConfig() {
|
||||
const transitionConfig = [];
|
||||
const datasetVisibilities = data.datasets.map(({ key }) => `opacity#${key} 300`);
|
||||
const datasetVisibilities = data.datasets.map(({ key }) => `opacity#${key} ${TRANSITION_DEFAULT_DURATION}`);
|
||||
|
||||
mergeArrays([
|
||||
ANIMATE_PROPS,
|
||||
@ -105,11 +106,11 @@ function calculateState(data, viewportSize, range, filter, focusOn, minimapDelta
|
||||
calculateYAxisScale(viewportSize.height, yRanges.yMinViewportSecond, yRanges.yMaxViewportSecond);
|
||||
|
||||
const yStep = yScaleLevelToStep(yAxisScale);
|
||||
yRanges.yMinViewport -= yRanges.yMinViewport % yStep;
|
||||
yRanges.yMinViewport = Math.floor(yRanges.yMinViewport / yStep) * yStep;
|
||||
|
||||
if (yAxisScaleSecond) {
|
||||
const yStepSecond = yScaleLevelToStep(yAxisScaleSecond);
|
||||
yRanges.yMinViewportSecond -= yRanges.yMinViewportSecond % yStepSecond;
|
||||
yRanges.yMinViewportSecond = Math.floor(yRanges.yMinViewportSecond / yStepSecond) * yStepSecond;
|
||||
}
|
||||
|
||||
const datasetsOpacity = {};
|
||||
@ -164,7 +165,9 @@ function calculateYRanges(data, filter, labelFromIndex, labelToIndex, prevState)
|
||||
function calculateYRangesForGroup(data, labelFromIndex, labelToIndex, prevState, datasets) {
|
||||
const { min: yMinMinimapReal = prevState.yMinMinimap, max: yMaxMinimap = prevState.yMaxMinimap }
|
||||
= getMaxMin(mergeArrays(datasets.map(({ yMax, yMin }) => [yMax, yMin])));
|
||||
const yMinMinimap = yMinMinimapReal / yMaxMinimap > Y_AXIS_ZERO_BASED_THRESHOLD ? yMinMinimapReal : 0;
|
||||
const yMinMinimap = yMinMinimapReal < 0
|
||||
? yMinMinimapReal
|
||||
: (yMinMinimapReal / yMaxMinimap > Y_AXIS_ZERO_BASED_THRESHOLD ? yMinMinimapReal : 0);
|
||||
|
||||
let yMinViewport;
|
||||
let yMaxViewport;
|
||||
@ -178,7 +181,9 @@ function calculateYRangesForGroup(data, labelFromIndex, labelToIndex, prevState,
|
||||
const viewportMaxMin = getMaxMin(mergeArrays(viewportValues));
|
||||
const yMinViewportReal = viewportMaxMin.min !== undefined ? viewportMaxMin.min : prevState.yMinViewport;
|
||||
yMaxViewport = viewportMaxMin.max !== undefined ? viewportMaxMin.max : prevState.yMaxViewport;
|
||||
yMinViewport = yMinViewportReal / yMaxViewport > Y_AXIS_ZERO_BASED_THRESHOLD ? yMinViewportReal : 0;
|
||||
yMinViewport = yMinViewportReal < 0
|
||||
? yMinViewportReal
|
||||
: (yMinViewportReal / yMaxViewport > Y_AXIS_ZERO_BASED_THRESHOLD ? yMinViewportReal : 0);
|
||||
}
|
||||
|
||||
return {
|
||||
@ -193,14 +198,26 @@ function calculateYRangesStacked(data, filter, labelFromIndex, labelToIndex, pre
|
||||
const filteredDatasets = data.datasets.filter((d) => filter[d.key]);
|
||||
const filteredValues = filteredDatasets.map(({ values }) => values);
|
||||
|
||||
const sums = filteredValues.length ? sumArrays(filteredValues) : [];
|
||||
const { max: yMaxMinimap = prevState.yMaxMinimap } = getMaxMin(sums);
|
||||
const { max: yMaxViewport = prevState.yMaxViewport } = getMaxMin(sums.slice(labelFromIndex, labelToIndex + 1));
|
||||
const length = filteredValues[0] ? filteredValues[0].length : 0;
|
||||
const posSums = new Array(length).fill(0);
|
||||
const negSums = new Array(length).fill(0);
|
||||
for (let i = 0; i < filteredValues.length; i++) {
|
||||
for (let j = 0; j < length; j++) {
|
||||
const v = filteredValues[i][j];
|
||||
if (v == null) continue;
|
||||
if (v >= 0) posSums[j] += v; else negSums[j] += v;
|
||||
}
|
||||
}
|
||||
|
||||
const { max: yMaxMinimap = prevState.yMaxMinimap } = getMaxMin(posSums);
|
||||
const { min: yMinMinimap = prevState.yMinMinimap } = getMaxMin(negSums);
|
||||
const { max: yMaxViewport = prevState.yMaxViewport } = getMaxMin(posSums.slice(labelFromIndex, labelToIndex + 1));
|
||||
const { min: yMinViewport = prevState.yMinViewport } = getMaxMin(negSums.slice(labelFromIndex, labelToIndex + 1));
|
||||
|
||||
return {
|
||||
yMinViewport: 0,
|
||||
yMinViewport,
|
||||
yMaxViewport,
|
||||
yMinMinimap: 0,
|
||||
yMinMinimap,
|
||||
yMaxMinimap,
|
||||
};
|
||||
}
|
||||
|
||||
@ -219,6 +219,8 @@ export function createTooltip(container, data, plotSize, colors, onZoom, onFocus
|
||||
|
||||
function _drawCircles(statistics, labelIndex) {
|
||||
statistics.forEach(({ value, key, hasOwnYAxis, originalIndex }) => {
|
||||
if (value == null) return;
|
||||
|
||||
const pointIndex = labelIndex - _state.labelFromIndex;
|
||||
const point = hasOwnYAxis ? _secondaryPoints[pointIndex] : _points[originalIndex][pointIndex];
|
||||
|
||||
@ -356,13 +358,8 @@ export function createTooltip(container, data, plotSize, colors, onZoom, onFocus
|
||||
|
||||
_renderPercentageValue(newDataSet, value, totalValue);
|
||||
|
||||
const totalText = dataSetContainer.querySelector(`[data-total="true"]`);
|
||||
if (totalText) {
|
||||
dataSetContainer.insertBefore(newDataSet, totalText);
|
||||
} else {
|
||||
dataSetContainer.appendChild(newDataSet);
|
||||
}
|
||||
}
|
||||
|
||||
function _updateDataSet(currentDataSet, { key, value } = {}, totalValue) {
|
||||
currentDataSet.setAttribute('data-present', 'true');
|
||||
@ -419,7 +416,7 @@ export function createTooltip(container, data, plotSize, colors, onZoom, onFocus
|
||||
|
||||
const totalValue = statistics.reduce((a, x) => a + x.value, 0);
|
||||
const pointerVector = getPointerVector();
|
||||
const filteredStatistics = statistics.filter(({ value }) => value !== 0);
|
||||
const filteredStatistics = statistics.filter(({ value }) => value !== 0 && value != null);
|
||||
const sortedStatistics = filteredStatistics.sort((a, b) => b.value - a.value);
|
||||
const limitedStatistics = sortedStatistics.slice(0, MAX_TOOLTIP_ITEMS);
|
||||
const finalStatistics = data.isPie ? limitedStatistics.filter(({ value }, index) => _isPieSectorSelected(statistics, value, totalValue, index, pointerVector)) : limitedStatistics;
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { SPEED_TEST_FAST_FPS, SPEED_TEST_INTERVAL, TRANSITION_DEFAULT_DURATION } from './constants.js';
|
||||
|
||||
function transition(t) {
|
||||
// faster
|
||||
// return -t * (t - 2);
|
||||
// easeOut
|
||||
return 1 - Math.pow(1 - t, 1.675);
|
||||
// iOS-style ease-out (no overshoot)
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
export function createTransitionManager(onTick) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export const DPR = window.devicePixelRatio || 1;
|
||||
|
||||
export const DEFAULT_RANGE = { begin: 0.8, end: 1 };
|
||||
export const TRANSITION_DEFAULT_DURATION = 300;
|
||||
export const TRANSITION_DEFAULT_DURATION = 400;
|
||||
export const LONG_PRESS_TIMEOUT = 500;
|
||||
|
||||
export const GUTTER = 10;
|
||||
@ -17,7 +17,7 @@ export const PIE_MINIMUM_VISIBLE_PERCENT = 0.02;
|
||||
export const BALLOON_OFFSET = 20;
|
||||
export const MAX_TOOLTIP_ITEMS = 12;
|
||||
|
||||
export const AXES_FONT = '300 10px Helvetica, Arial, sans-serif';
|
||||
export const AXES_FONT_STYLE = '300 10px';
|
||||
export const AXES_MAX_COLUMN_WIDTH = 45;
|
||||
export const AXES_MAX_ROW_HEIGHT = 50;
|
||||
export const X_AXIS_HEIGHT = 30;
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { getMaxMin } from './utils.js';
|
||||
import { statsFormatDay, statsFormatDayHour, statsFormatText, statsFormatMin } from './format.js';
|
||||
|
||||
const DEFAULT_COLORS = [
|
||||
'#3497ED', '#2373DB', '#9ED448', '#5FB641',
|
||||
'#F5BD25', '#F79E39', '#E65850', '#5D5CDC',
|
||||
];
|
||||
|
||||
const LABEL_TYPE_TO_FORMATTER = {
|
||||
'day': "statsFormat('day')",
|
||||
'hour': "statsFormat('hour')",
|
||||
@ -10,7 +15,7 @@ const LABEL_TYPE_TO_FORMATTER = {
|
||||
};
|
||||
|
||||
export function analyzeData(data) {
|
||||
const { title, labelFormatter: labelFormatterRaw, labelType, tooltipFormatter, isStacked, isPercentage, secondaryYAxis, hasSecondYAxis, onZoom, minimapRange, hideCaption, zoomOutLabel, valuePrefix, valueSuffix } = data;
|
||||
const { title, labelFormatter: labelFormatterRaw, labelType, tooltipFormatter, isStacked, isPercentage, secondaryYAxis, hasSecondYAxis, onZoom, minimapRange, hideCaption, zoomOutLabel, valuePrefix, valueSuffix, limitDate, onLimitedRangeClick } = data;
|
||||
const labelFormatter = labelFormatterRaw || (labelType && LABEL_TYPE_TO_FORMATTER[labelType]);
|
||||
const { datasets, labels } = prepareDatasets(data);
|
||||
|
||||
@ -46,6 +51,15 @@ export function analyzeData(data) {
|
||||
break;
|
||||
}
|
||||
|
||||
let limitBegin = null;
|
||||
if (limitDate != null) {
|
||||
const totalXWidth = labels.length - 1;
|
||||
const idx = labels.findIndex((l) => l >= limitDate);
|
||||
if (idx > 0) {
|
||||
limitBegin = idx / totalXWidth;
|
||||
}
|
||||
}
|
||||
|
||||
const analyzed = {
|
||||
title,
|
||||
labelFormatter,
|
||||
@ -70,6 +84,8 @@ export function analyzeData(data) {
|
||||
minimapRange,
|
||||
hideCaption,
|
||||
zoomOutLabel,
|
||||
limitBegin,
|
||||
onLimitedRangeClick,
|
||||
};
|
||||
|
||||
analyzed.shouldZoomToPie = !analyzed.onZoom && analyzed.isPercentage;
|
||||
@ -81,6 +97,8 @@ export function analyzeData(data) {
|
||||
function prepareDatasets(data) {
|
||||
const { type, labels, datasets, hasSecondYAxis } = data;
|
||||
|
||||
let nextDefaultColor = 0;
|
||||
|
||||
return {
|
||||
labels: cloneArray(labels),
|
||||
datasets: datasets.map(({ name, color, values }, i) => {
|
||||
@ -90,7 +108,7 @@ function prepareDatasets(data) {
|
||||
type,
|
||||
key: `y${i}`,
|
||||
name,
|
||||
color,
|
||||
color: color || DEFAULT_COLORS[nextDefaultColor++ % DEFAULT_COLORS.length],
|
||||
values: cloneArray(values),
|
||||
hasOwnYAxis: hasSecondYAxis && i === datasets.length - 1,
|
||||
yMin,
|
||||
|
||||
@ -84,20 +84,30 @@ function drawDataset(type, ...args) {
|
||||
function drawDatasetLine(context, points, projection, options) {
|
||||
context.beginPath();
|
||||
|
||||
let pixels = [];
|
||||
|
||||
const segments = [];
|
||||
let current = [];
|
||||
for (let j = 0, l = points.length; j < l; j++) {
|
||||
const { labelIndex, stackValue } = points[j];
|
||||
pixels.push(toPixels(projection, labelIndex, stackValue));
|
||||
const point = points[j];
|
||||
if (point.gap) {
|
||||
if (current.length) {
|
||||
segments.push(current);
|
||||
current = [];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
current.push(toPixels(projection, point.labelIndex, point.stackValue));
|
||||
}
|
||||
if (current.length) segments.push(current);
|
||||
|
||||
segments.forEach((segment) => {
|
||||
let pixels = segment;
|
||||
if (options.simplification) {
|
||||
const simplifierFn = simplify(pixels);
|
||||
pixels = simplifierFn(options.simplification).points;
|
||||
pixels = simplify(pixels)(options.simplification).points;
|
||||
}
|
||||
|
||||
pixels.forEach(([x, y]) => {
|
||||
context.lineTo(x, y);
|
||||
pixels.forEach(([x, y], k) => {
|
||||
if (k === 0) context.moveTo(x, y);
|
||||
else context.lineTo(x, y);
|
||||
});
|
||||
});
|
||||
|
||||
context.save();
|
||||
@ -119,6 +129,7 @@ function drawDatasetBars(context, points, projection, options) {
|
||||
context.fillStyle = options.color;
|
||||
|
||||
for (let j = 0, l = points.length; j < l; j++) {
|
||||
if (points[j].gap) continue;
|
||||
const { labelIndex, stackValue, stackOffset = 0 } = points[j];
|
||||
|
||||
const [, yFrom] = toPixels(projection, labelIndex, Math.max(stackOffset, yMin));
|
||||
@ -139,18 +150,29 @@ function drawDatasetBars(context, points, projection, options) {
|
||||
function drawDatasetSteps(context, points, projection, options) {
|
||||
context.beginPath();
|
||||
|
||||
let pixels = [];
|
||||
|
||||
const segments = [];
|
||||
let current = [];
|
||||
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),
|
||||
const point = points[j];
|
||||
if (point.gap) {
|
||||
if (current.length) {
|
||||
segments.push(current);
|
||||
current = [];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
current.push(
|
||||
toPixels(projection, point.labelIndex - PLOT_BARS_WIDTH_SHIFT, point.stackValue),
|
||||
toPixels(projection, point.labelIndex + PLOT_BARS_WIDTH_SHIFT, point.stackValue),
|
||||
);
|
||||
}
|
||||
if (current.length) segments.push(current);
|
||||
|
||||
pixels.forEach(([x, y]) => {
|
||||
context.lineTo(x, y);
|
||||
segments.forEach((segment) => {
|
||||
segment.forEach(([x, y], k) => {
|
||||
if (k === 0) context.moveTo(x, y);
|
||||
else context.lineTo(x, y);
|
||||
});
|
||||
});
|
||||
|
||||
context.save();
|
||||
@ -240,7 +262,8 @@ function drawDatasetPie(context, points, projection, options) {
|
||||
context.fill();
|
||||
|
||||
if (percent >= PIE_MINIMUM_VISIBLE_PERCENT) {
|
||||
context.font = `700 ${getPieTextSize(percent, radius)}px Helvetica, Arial, sans-serif`;
|
||||
const fontFamily = getComputedStyle(context.canvas).fontFamily || 'sans-serif';
|
||||
context.font = `700 ${getPieTextSize(percent, radius)}px ${fontFamily}`;
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'middle';
|
||||
context.fillStyle = 'white';
|
||||
|
||||
@ -41,10 +41,13 @@ export function statsFormatText(labels) {
|
||||
}
|
||||
|
||||
export function humanize(value, decimals = 1) {
|
||||
if (value >= 1e6) {
|
||||
return keepThreeDigits(value / 1e6, decimals) + 'M';
|
||||
} else if (value >= 1e3) {
|
||||
return keepThreeDigits(value / 1e3, decimals) + 'K';
|
||||
const abs = Math.abs(value);
|
||||
const sign = value < 0 ? '-' : '';
|
||||
|
||||
if (abs >= 1e6) {
|
||||
return sign + keepThreeDigits(abs / 1e6, decimals) + 'M';
|
||||
} else if (abs >= 1e3) {
|
||||
return sign + keepThreeDigits(abs / 1e3, decimals) + 'K';
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
3
src/lib/lovely-chart/icons/lock.svg
Normal file
3
src/lib/lovely-chart/icons/lock.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5265 10.2173V7.54299C16.5265 5.08532 14.4958 3.08585 11.9997 3.08585C9.50365 3.08585 7.47293 5.08532 7.47293 7.54299V10.2173C6.2992 10.2173 5.36524 11.2011 5.42629 12.3733L5.60706 15.844C5.6879 17.3962 5.72833 18.1723 6.00269 18.7852C6.39058 19.6518 7.10506 20.33 7.9906 20.6723C8.61698 20.9144 9.39412 20.9144 10.9484 20.9144H13.051C14.6053 20.9144 15.3825 20.9144 16.0088 20.6723C16.8944 20.33 17.6089 19.6518 17.9967 18.7852C18.2711 18.1723 18.3115 17.3962 18.3924 15.844L18.5731 12.3733C18.6342 11.2011 17.7002 10.2173 16.5265 10.2173ZM11.9997 4.8687C10.5023 4.8687 9.28364 6.06857 9.28364 7.54299V10.2173H14.7158V7.54299C14.7158 6.06857 13.4972 4.8687 11.9997 4.8687Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 845 B |
@ -11,9 +11,10 @@ export function preparePoints(data, datasets, range, visibilities, bounds, pieTo
|
||||
|
||||
const points = values.map((datasetValues, i) => (
|
||||
datasetValues.map((value, j) => {
|
||||
let visibleValue = value;
|
||||
const isGap = value == null;
|
||||
let visibleValue = isGap ? 0 : value;
|
||||
|
||||
if (data.isStacked) {
|
||||
if (data.isStacked && !isGap) {
|
||||
visibleValue *= visibilities[i];
|
||||
}
|
||||
|
||||
@ -23,6 +24,7 @@ export function preparePoints(data, datasets, range, visibilities, bounds, pieTo
|
||||
visibleValue,
|
||||
stackOffset: 0,
|
||||
stackValue: visibleValue,
|
||||
gap: isGap,
|
||||
};
|
||||
})
|
||||
));
|
||||
@ -57,17 +59,31 @@ function preparePercentage(points, bounds) {
|
||||
}
|
||||
|
||||
function prepareStacked(points) {
|
||||
const accum = [];
|
||||
const posAccum = [];
|
||||
const negAccum = [];
|
||||
|
||||
points.forEach((datasetPoints) => {
|
||||
datasetPoints.forEach((point, j) => {
|
||||
if (accum[j] === undefined) {
|
||||
accum[j] = 0;
|
||||
if (posAccum[j] === undefined) {
|
||||
posAccum[j] = 0;
|
||||
negAccum[j] = 0;
|
||||
}
|
||||
|
||||
point.stackOffset = accum[j];
|
||||
accum[j] += point.visibleValue;
|
||||
point.stackValue = accum[j];
|
||||
if (point.gap) {
|
||||
point.stackOffset = posAccum[j];
|
||||
point.stackValue = posAccum[j];
|
||||
return;
|
||||
}
|
||||
|
||||
if (point.visibleValue >= 0) {
|
||||
point.stackOffset = posAccum[j];
|
||||
posAccum[j] += point.visibleValue;
|
||||
point.stackValue = posAccum[j];
|
||||
} else {
|
||||
point.stackOffset = negAccum[j];
|
||||
negAccum[j] += point.visibleValue;
|
||||
point.stackValue = negAccum[j];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -39,9 +39,9 @@ styleElement.appendChild(document.createTextNode(''));
|
||||
document.head.appendChild(styleElement);
|
||||
const styleSheet = styleElement.sheet;
|
||||
|
||||
document.documentElement.addEventListener('darkmode', () => {
|
||||
new MutationObserver(() => {
|
||||
skin = detectSkin();
|
||||
});
|
||||
}).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
export function createColors(datasetColors) {
|
||||
const colors = {};
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
text-decoration: none;
|
||||
|
||||
background-color: transparent;
|
||||
|
||||
transition: opacity 150ms ease;
|
||||
|
||||
&:hover {
|
||||
@ -78,6 +77,7 @@
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -256 1792 1792' version='1.1'%0A%3E%3Cg transform='matrix(1,0,0,-1,7.5932203,1217.0847)' id='g3003'%3E%3Cpath d='m 1671,970 q 0,-40 -28,-68 L 919,178 783,42 Q 755,14 715,14 675,14 647,42 L 511,178 149,540 q -28,28 -28,68 0,40 28,68 l 136,136 q 28,28 68,28 40,0 68,-28 l 294,-295 656,657 q 28,28 68,28 40,0 68,-28 l 136,-136 q 28,-28 28,-68 z' style='fill:white'/%3E%3C/g%3E%3C/svg%3E");
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.lovely-chart--button-label {
|
||||
|
||||
@ -7,20 +7,11 @@
|
||||
--zoom-out-text: #108BE3;
|
||||
--tooltip-background: #ffffff;
|
||||
--tooltip-arrow: #D2D5D7;
|
||||
--minimap-limit-color: #616770;
|
||||
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
font: 300 13px '-apple-system', 'HelveticaNeue', Helvetica, Arial, sans-serif;
|
||||
color: #222222;
|
||||
text-align: left;
|
||||
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
|
||||
html.theme-dark & {
|
||||
--background-color: #242F3E;
|
||||
--text-color: #ffffff;
|
||||
@ -30,8 +21,19 @@
|
||||
--zoom-out-text: #48AAF0;
|
||||
--tooltip-background: #1c2533;
|
||||
--tooltip-arrow: #D2D5D7;
|
||||
--minimap-limit-color: #BFC0C2;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
font: 300 13px '-apple-system', 'HelveticaNeue', Helvetica, Arial, sans-serif;
|
||||
color: #222222;
|
||||
text-align: left;
|
||||
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
|
||||
// &.lovely-chart--state-invisible > * {
|
||||
// display: none;
|
||||
// }
|
||||
|
||||
@ -61,6 +61,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lovely-chart--minimap-limit-mask {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
|
||||
border-radius: 6px 0 0 6px;
|
||||
border-right: 2px dashed var(--minimap-limit-color);
|
||||
|
||||
color: var(--minimap-limit-color);
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
|
||||
&.lovely-chart--state-interactive {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
pointer-events: auto;
|
||||
|
||||
transition: opacity 200ms ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lovely-chart--minimap-slider {
|
||||
display: inline-block;
|
||||
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
// https://jsperf.com/finding-maximum-element-in-an-array
|
||||
export function getMaxMin(array) {
|
||||
const length = array.length;
|
||||
let max = array[0];
|
||||
let min = array[0];
|
||||
let max;
|
||||
let min;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const value = array[i];
|
||||
|
||||
if (value > max) {
|
||||
max = value;
|
||||
} else if (value < min) {
|
||||
min = value;
|
||||
}
|
||||
if (value == null) continue;
|
||||
if (max === undefined || value > max) max = value;
|
||||
if (min === undefined || value < min) min = value;
|
||||
}
|
||||
|
||||
return { max, min };
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user