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 { humanize } from './format.js';
|
||||||
import { getCssColor } from './skin.js';
|
import { getCssColor } from './skin.js';
|
||||||
import { applyXEdgeOpacity, applyYEdgeOpacity, xScaleLevelToStep, yScaleLevelToStep } from './formulas.js';
|
import { applyXEdgeOpacity, applyYEdgeOpacity, xScaleLevelToStep, yScaleLevelToStep } from './formulas.js';
|
||||||
import { toPixels } from './Projection.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) {
|
export function createAxes(context, data, plotSize, colors) {
|
||||||
function drawXAxis(state, projection) {
|
function drawXAxis(state, projection) {
|
||||||
context.clearRect(0, plotSize.height - X_AXIS_HEIGHT + 1, plotSize.width, X_AXIS_HEIGHT + 1);
|
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 step = xScaleLevelToStep(scaleLevel);
|
||||||
const opacityFactor = 1 - (state.xAxisScale - scaleLevel);
|
const opacityFactor = 1 - (state.xAxisScale - scaleLevel);
|
||||||
|
|
||||||
context.font = AXES_FONT;
|
context.font = getAxesFont(context);
|
||||||
context.textAlign = 'center';
|
context.textAlign = 'center';
|
||||||
context.textBaseline = 'middle';
|
context.textBaseline = 'middle';
|
||||||
|
|
||||||
@ -126,7 +131,7 @@ export function createAxes(context, data, plotSize, colors) {
|
|||||||
const firstVisibleValue = Math.ceil(yMin / step) * step;
|
const firstVisibleValue = Math.ceil(yMin / step) * step;
|
||||||
const lastVisibleValue = Math.floor(yMax / step) * step;
|
const lastVisibleValue = Math.floor(yMax / step) * step;
|
||||||
|
|
||||||
context.font = AXES_FONT;
|
context.font = getAxesFont(context);
|
||||||
context.textAlign = isSecondary ? 'right' : 'left';
|
context.textAlign = isSecondary ? 'right' : 'left';
|
||||||
context.textBaseline = 'bottom';
|
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 percentValues = [0, 0.25, 0.50, 0.75, 1];
|
||||||
const [, height] = projection.getSize();
|
const [, height] = projection.getSize();
|
||||||
|
|
||||||
context.font = AXES_FONT;
|
context.font = getAxesFont(context);
|
||||||
context.textAlign = 'left';
|
context.textAlign = 'left';
|
||||||
context.textBaseline = 'bottom';
|
context.textBaseline = 'bottom';
|
||||||
context.lineWidth = 1;
|
context.lineWidth = 1;
|
||||||
@ -198,7 +203,7 @@ export function createAxes(context, data, plotSize, colors) {
|
|||||||
const firstVisibleValue = Math.ceil(yMin / step) * step;
|
const firstVisibleValue = Math.ceil(yMin / step) * step;
|
||||||
const lastVisibleValue = Math.floor(yMax / step) * step;
|
const lastVisibleValue = Math.floor(yMax / step) * step;
|
||||||
|
|
||||||
context.font = AXES_FONT;
|
context.font = getAxesFont(context);
|
||||||
context.textAlign = 'right';
|
context.textAlign = 'right';
|
||||||
context.textBaseline = 'bottom';
|
context.textBaseline = 'bottom';
|
||||||
|
|
||||||
|
|||||||
@ -167,9 +167,9 @@ function create(container, originalData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _setupGlobalListeners() {
|
function _setupGlobalListeners() {
|
||||||
document.documentElement.addEventListener('darkmode', () => {
|
new MutationObserver(() => {
|
||||||
_stateManager.update();
|
_stateManager.update();
|
||||||
});
|
}).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
if (window.innerWidth !== _windowWidth) {
|
if (window.innerWidth !== _windowWidth) {
|
||||||
|
|||||||
@ -23,11 +23,14 @@ export function createMinimap(container, data, colors, rangeCallback) {
|
|||||||
let _canvasSize;
|
let _canvasSize;
|
||||||
let _ruler;
|
let _ruler;
|
||||||
let _slider;
|
let _slider;
|
||||||
|
let _limitMask;
|
||||||
|
|
||||||
let _capturedOffset;
|
let _capturedOffset;
|
||||||
let _range = {};
|
let _range = {};
|
||||||
let _state;
|
let _state;
|
||||||
|
|
||||||
|
const _limitBegin = data.limitBegin;
|
||||||
|
|
||||||
const _updateRulerOnRaf = throttleWithRaf(_updateRuler);
|
const _updateRulerOnRaf = throttleWithRaf(_updateRuler);
|
||||||
|
|
||||||
_setupLayout();
|
_setupLayout();
|
||||||
@ -69,6 +72,7 @@ export function createMinimap(container, data, colors, rangeCallback) {
|
|||||||
|
|
||||||
_setupCanvas();
|
_setupCanvas();
|
||||||
_setupRuler();
|
_setupRuler();
|
||||||
|
_setupLimitMask();
|
||||||
|
|
||||||
container.appendChild(_element);
|
container.appendChild(_element);
|
||||||
|
|
||||||
@ -139,6 +143,23 @@ export function createMinimap(container, data, colors, rangeCallback) {
|
|||||||
_element.appendChild(_ruler);
|
_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) {
|
function _isStateChanged(newState) {
|
||||||
if (!_state) {
|
if (!_state) {
|
||||||
return true;
|
return true;
|
||||||
@ -206,7 +227,8 @@ export function createMinimap(container, data, colors, rangeCallback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _onSliderDrag(moveEvent, captureEvent, { dragOffsetX }) {
|
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 maxX1 = _canvasSize.width - _slider.offsetWidth;
|
||||||
|
|
||||||
const newX1 = Math.max(minX1, Math.min(_capturedOffset + dragOffsetX - MINIMAP_EAR_WIDTH, maxX1));
|
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 }) {
|
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 maxX1 = _slider.offsetLeft + _slider.offsetWidth - MINIMAP_EAR_WIDTH * 2;
|
||||||
|
|
||||||
const newX1 = Math.min(maxX1, Math.max(minX1, _capturedOffset + dragOffsetX));
|
const newX1 = Math.min(maxX1, Math.max(minX1, _capturedOffset + dragOffsetX));
|
||||||
@ -244,6 +267,10 @@ export function createMinimap(container, data, colors, rangeCallback) {
|
|||||||
nextRange = _adjustDiscreteRange(nextRange);
|
nextRange = _adjustDiscreteRange(nextRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_limitBegin != null && nextRange.begin < _limitBegin) {
|
||||||
|
nextRange.begin = _limitBegin;
|
||||||
|
}
|
||||||
|
|
||||||
if (nextRange.begin === _range.begin && nextRange.end === _range.end) {
|
if (nextRange.begin === _range.begin && nextRange.end === _range.end) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { createTransitionManager } from './TransitionManager.js';
|
import { createTransitionManager } from './TransitionManager.js';
|
||||||
import { throttleWithRaf, getMaxMin, mergeArrays, proxyMerge, sumArrays } from './utils.js';
|
import { throttleWithRaf, getMaxMin, mergeArrays, proxyMerge } from './utils.js';
|
||||||
import {
|
import {
|
||||||
AXES_MAX_COLUMN_WIDTH,
|
AXES_MAX_COLUMN_WIDTH,
|
||||||
AXES_MAX_ROW_HEIGHT,
|
AXES_MAX_ROW_HEIGHT,
|
||||||
X_AXIS_HEIGHT,
|
X_AXIS_HEIGHT,
|
||||||
ANIMATE_PROPS,
|
ANIMATE_PROPS,
|
||||||
|
TRANSITION_DEFAULT_DURATION,
|
||||||
Y_AXIS_ZERO_BASED_THRESHOLD,
|
Y_AXIS_ZERO_BASED_THRESHOLD,
|
||||||
} from './constants.js';
|
} from './constants.js';
|
||||||
import { xStepToScaleLevel, yScaleLevelToStep, yStepToScaleLevel } from './formulas.js';
|
import { xStepToScaleLevel, yScaleLevelToStep, yStepToScaleLevel } from './formulas.js';
|
||||||
@ -55,7 +56,7 @@ export function createStateManager(data, viewportSize, callback) {
|
|||||||
|
|
||||||
function _buildTransitionConfig() {
|
function _buildTransitionConfig() {
|
||||||
const transitionConfig = [];
|
const transitionConfig = [];
|
||||||
const datasetVisibilities = data.datasets.map(({ key }) => `opacity#${key} 300`);
|
const datasetVisibilities = data.datasets.map(({ key }) => `opacity#${key} ${TRANSITION_DEFAULT_DURATION}`);
|
||||||
|
|
||||||
mergeArrays([
|
mergeArrays([
|
||||||
ANIMATE_PROPS,
|
ANIMATE_PROPS,
|
||||||
@ -105,11 +106,11 @@ function calculateState(data, viewportSize, range, filter, focusOn, minimapDelta
|
|||||||
calculateYAxisScale(viewportSize.height, yRanges.yMinViewportSecond, yRanges.yMaxViewportSecond);
|
calculateYAxisScale(viewportSize.height, yRanges.yMinViewportSecond, yRanges.yMaxViewportSecond);
|
||||||
|
|
||||||
const yStep = yScaleLevelToStep(yAxisScale);
|
const yStep = yScaleLevelToStep(yAxisScale);
|
||||||
yRanges.yMinViewport -= yRanges.yMinViewport % yStep;
|
yRanges.yMinViewport = Math.floor(yRanges.yMinViewport / yStep) * yStep;
|
||||||
|
|
||||||
if (yAxisScaleSecond) {
|
if (yAxisScaleSecond) {
|
||||||
const yStepSecond = yScaleLevelToStep(yAxisScaleSecond);
|
const yStepSecond = yScaleLevelToStep(yAxisScaleSecond);
|
||||||
yRanges.yMinViewportSecond -= yRanges.yMinViewportSecond % yStepSecond;
|
yRanges.yMinViewportSecond = Math.floor(yRanges.yMinViewportSecond / yStepSecond) * yStepSecond;
|
||||||
}
|
}
|
||||||
|
|
||||||
const datasetsOpacity = {};
|
const datasetsOpacity = {};
|
||||||
@ -164,7 +165,9 @@ function calculateYRanges(data, filter, labelFromIndex, labelToIndex, prevState)
|
|||||||
function calculateYRangesForGroup(data, labelFromIndex, labelToIndex, prevState, datasets) {
|
function calculateYRangesForGroup(data, labelFromIndex, labelToIndex, prevState, datasets) {
|
||||||
const { min: yMinMinimapReal = prevState.yMinMinimap, max: yMaxMinimap = prevState.yMaxMinimap }
|
const { min: yMinMinimapReal = prevState.yMinMinimap, max: yMaxMinimap = prevState.yMaxMinimap }
|
||||||
= getMaxMin(mergeArrays(datasets.map(({ yMax, yMin }) => [yMax, yMin])));
|
= 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 yMinViewport;
|
||||||
let yMaxViewport;
|
let yMaxViewport;
|
||||||
@ -178,7 +181,9 @@ function calculateYRangesForGroup(data, labelFromIndex, labelToIndex, prevState,
|
|||||||
const viewportMaxMin = getMaxMin(mergeArrays(viewportValues));
|
const viewportMaxMin = getMaxMin(mergeArrays(viewportValues));
|
||||||
const yMinViewportReal = viewportMaxMin.min !== undefined ? viewportMaxMin.min : prevState.yMinViewport;
|
const yMinViewportReal = viewportMaxMin.min !== undefined ? viewportMaxMin.min : prevState.yMinViewport;
|
||||||
yMaxViewport = viewportMaxMin.max !== undefined ? viewportMaxMin.max : prevState.yMaxViewport;
|
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 {
|
return {
|
||||||
@ -193,14 +198,26 @@ function calculateYRangesStacked(data, filter, labelFromIndex, labelToIndex, pre
|
|||||||
const filteredDatasets = data.datasets.filter((d) => filter[d.key]);
|
const filteredDatasets = data.datasets.filter((d) => filter[d.key]);
|
||||||
const filteredValues = filteredDatasets.map(({ values }) => values);
|
const filteredValues = filteredDatasets.map(({ values }) => values);
|
||||||
|
|
||||||
const sums = filteredValues.length ? sumArrays(filteredValues) : [];
|
const length = filteredValues[0] ? filteredValues[0].length : 0;
|
||||||
const { max: yMaxMinimap = prevState.yMaxMinimap } = getMaxMin(sums);
|
const posSums = new Array(length).fill(0);
|
||||||
const { max: yMaxViewport = prevState.yMaxViewport } = getMaxMin(sums.slice(labelFromIndex, labelToIndex + 1));
|
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 {
|
return {
|
||||||
yMinViewport: 0,
|
yMinViewport,
|
||||||
yMaxViewport,
|
yMaxViewport,
|
||||||
yMinMinimap: 0,
|
yMinMinimap,
|
||||||
yMaxMinimap,
|
yMaxMinimap,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -219,6 +219,8 @@ export function createTooltip(container, data, plotSize, colors, onZoom, onFocus
|
|||||||
|
|
||||||
function _drawCircles(statistics, labelIndex) {
|
function _drawCircles(statistics, labelIndex) {
|
||||||
statistics.forEach(({ value, key, hasOwnYAxis, originalIndex }) => {
|
statistics.forEach(({ value, key, hasOwnYAxis, originalIndex }) => {
|
||||||
|
if (value == null) return;
|
||||||
|
|
||||||
const pointIndex = labelIndex - _state.labelFromIndex;
|
const pointIndex = labelIndex - _state.labelFromIndex;
|
||||||
const point = hasOwnYAxis ? _secondaryPoints[pointIndex] : _points[originalIndex][pointIndex];
|
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);
|
_renderPercentageValue(newDataSet, value, totalValue);
|
||||||
|
|
||||||
const totalText = dataSetContainer.querySelector(`[data-total="true"]`);
|
|
||||||
if (totalText) {
|
|
||||||
dataSetContainer.insertBefore(newDataSet, totalText);
|
|
||||||
} else {
|
|
||||||
dataSetContainer.appendChild(newDataSet);
|
dataSetContainer.appendChild(newDataSet);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function _updateDataSet(currentDataSet, { key, value } = {}, totalValue) {
|
function _updateDataSet(currentDataSet, { key, value } = {}, totalValue) {
|
||||||
currentDataSet.setAttribute('data-present', 'true');
|
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 totalValue = statistics.reduce((a, x) => a + x.value, 0);
|
||||||
const pointerVector = getPointerVector();
|
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 sortedStatistics = filteredStatistics.sort((a, b) => b.value - a.value);
|
||||||
const limitedStatistics = sortedStatistics.slice(0, MAX_TOOLTIP_ITEMS);
|
const limitedStatistics = sortedStatistics.slice(0, MAX_TOOLTIP_ITEMS);
|
||||||
const finalStatistics = data.isPie ? limitedStatistics.filter(({ value }, index) => _isPieSectorSelected(statistics, value, totalValue, index, pointerVector)) : limitedStatistics;
|
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';
|
import { SPEED_TEST_FAST_FPS, SPEED_TEST_INTERVAL, TRANSITION_DEFAULT_DURATION } from './constants.js';
|
||||||
|
|
||||||
function transition(t) {
|
function transition(t) {
|
||||||
// faster
|
// iOS-style ease-out (no overshoot)
|
||||||
// return -t * (t - 2);
|
return 1 - Math.pow(1 - t, 3);
|
||||||
// easeOut
|
|
||||||
return 1 - Math.pow(1 - t, 1.675);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTransitionManager(onTick) {
|
export function createTransitionManager(onTick) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
export const DPR = window.devicePixelRatio || 1;
|
export const DPR = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
export const DEFAULT_RANGE = { begin: 0.8, end: 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 LONG_PRESS_TIMEOUT = 500;
|
||||||
|
|
||||||
export const GUTTER = 10;
|
export const GUTTER = 10;
|
||||||
@ -17,7 +17,7 @@ export const PIE_MINIMUM_VISIBLE_PERCENT = 0.02;
|
|||||||
export const BALLOON_OFFSET = 20;
|
export const BALLOON_OFFSET = 20;
|
||||||
export const MAX_TOOLTIP_ITEMS = 12;
|
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_COLUMN_WIDTH = 45;
|
||||||
export const AXES_MAX_ROW_HEIGHT = 50;
|
export const AXES_MAX_ROW_HEIGHT = 50;
|
||||||
export const X_AXIS_HEIGHT = 30;
|
export const X_AXIS_HEIGHT = 30;
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { getMaxMin } from './utils.js';
|
import { getMaxMin } from './utils.js';
|
||||||
import { statsFormatDay, statsFormatDayHour, statsFormatText, statsFormatMin } from './format.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 = {
|
const LABEL_TYPE_TO_FORMATTER = {
|
||||||
'day': "statsFormat('day')",
|
'day': "statsFormat('day')",
|
||||||
'hour': "statsFormat('hour')",
|
'hour': "statsFormat('hour')",
|
||||||
@ -10,7 +15,7 @@ const LABEL_TYPE_TO_FORMATTER = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function analyzeData(data) {
|
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 labelFormatter = labelFormatterRaw || (labelType && LABEL_TYPE_TO_FORMATTER[labelType]);
|
||||||
const { datasets, labels } = prepareDatasets(data);
|
const { datasets, labels } = prepareDatasets(data);
|
||||||
|
|
||||||
@ -46,6 +51,15 @@ export function analyzeData(data) {
|
|||||||
break;
|
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 = {
|
const analyzed = {
|
||||||
title,
|
title,
|
||||||
labelFormatter,
|
labelFormatter,
|
||||||
@ -70,6 +84,8 @@ export function analyzeData(data) {
|
|||||||
minimapRange,
|
minimapRange,
|
||||||
hideCaption,
|
hideCaption,
|
||||||
zoomOutLabel,
|
zoomOutLabel,
|
||||||
|
limitBegin,
|
||||||
|
onLimitedRangeClick,
|
||||||
};
|
};
|
||||||
|
|
||||||
analyzed.shouldZoomToPie = !analyzed.onZoom && analyzed.isPercentage;
|
analyzed.shouldZoomToPie = !analyzed.onZoom && analyzed.isPercentage;
|
||||||
@ -81,6 +97,8 @@ export function analyzeData(data) {
|
|||||||
function prepareDatasets(data) {
|
function prepareDatasets(data) {
|
||||||
const { type, labels, datasets, hasSecondYAxis } = data;
|
const { type, labels, datasets, hasSecondYAxis } = data;
|
||||||
|
|
||||||
|
let nextDefaultColor = 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels: cloneArray(labels),
|
labels: cloneArray(labels),
|
||||||
datasets: datasets.map(({ name, color, values }, i) => {
|
datasets: datasets.map(({ name, color, values }, i) => {
|
||||||
@ -90,7 +108,7 @@ function prepareDatasets(data) {
|
|||||||
type,
|
type,
|
||||||
key: `y${i}`,
|
key: `y${i}`,
|
||||||
name,
|
name,
|
||||||
color,
|
color: color || DEFAULT_COLORS[nextDefaultColor++ % DEFAULT_COLORS.length],
|
||||||
values: cloneArray(values),
|
values: cloneArray(values),
|
||||||
hasOwnYAxis: hasSecondYAxis && i === datasets.length - 1,
|
hasOwnYAxis: hasSecondYAxis && i === datasets.length - 1,
|
||||||
yMin,
|
yMin,
|
||||||
|
|||||||
@ -84,20 +84,30 @@ function drawDataset(type, ...args) {
|
|||||||
function drawDatasetLine(context, points, projection, options) {
|
function drawDatasetLine(context, points, projection, options) {
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
|
|
||||||
let pixels = [];
|
const segments = [];
|
||||||
|
let current = [];
|
||||||
for (let j = 0, l = points.length; j < l; j++) {
|
for (let j = 0, l = points.length; j < l; j++) {
|
||||||
const { labelIndex, stackValue } = points[j];
|
const point = points[j];
|
||||||
pixels.push(toPixels(projection, labelIndex, stackValue));
|
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) {
|
if (options.simplification) {
|
||||||
const simplifierFn = simplify(pixels);
|
pixels = simplify(pixels)(options.simplification).points;
|
||||||
pixels = simplifierFn(options.simplification).points;
|
|
||||||
}
|
}
|
||||||
|
pixels.forEach(([x, y], k) => {
|
||||||
pixels.forEach(([x, y]) => {
|
if (k === 0) context.moveTo(x, y);
|
||||||
context.lineTo(x, y);
|
else context.lineTo(x, y);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
@ -119,6 +129,7 @@ function drawDatasetBars(context, points, projection, options) {
|
|||||||
context.fillStyle = options.color;
|
context.fillStyle = options.color;
|
||||||
|
|
||||||
for (let j = 0, l = points.length; j < l; j++) {
|
for (let j = 0, l = points.length; j < l; j++) {
|
||||||
|
if (points[j].gap) continue;
|
||||||
const { labelIndex, stackValue, stackOffset = 0 } = points[j];
|
const { labelIndex, stackValue, stackOffset = 0 } = points[j];
|
||||||
|
|
||||||
const [, yFrom] = toPixels(projection, labelIndex, Math.max(stackOffset, yMin));
|
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) {
|
function drawDatasetSteps(context, points, projection, options) {
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
|
|
||||||
let pixels = [];
|
const segments = [];
|
||||||
|
let current = [];
|
||||||
for (let j = 0, l = points.length; j < l; j++) {
|
for (let j = 0, l = points.length; j < l; j++) {
|
||||||
const { labelIndex, stackValue } = points[j];
|
const point = points[j];
|
||||||
pixels.push(
|
if (point.gap) {
|
||||||
toPixels(projection, labelIndex - PLOT_BARS_WIDTH_SHIFT, stackValue),
|
if (current.length) {
|
||||||
toPixels(projection, labelIndex + PLOT_BARS_WIDTH_SHIFT, stackValue),
|
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]) => {
|
segments.forEach((segment) => {
|
||||||
context.lineTo(x, y);
|
segment.forEach(([x, y], k) => {
|
||||||
|
if (k === 0) context.moveTo(x, y);
|
||||||
|
else context.lineTo(x, y);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
@ -240,7 +262,8 @@ function drawDatasetPie(context, points, projection, options) {
|
|||||||
context.fill();
|
context.fill();
|
||||||
|
|
||||||
if (percent >= PIE_MINIMUM_VISIBLE_PERCENT) {
|
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.textAlign = 'center';
|
||||||
context.textBaseline = 'middle';
|
context.textBaseline = 'middle';
|
||||||
context.fillStyle = 'white';
|
context.fillStyle = 'white';
|
||||||
|
|||||||
@ -41,10 +41,13 @@ export function statsFormatText(labels) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function humanize(value, decimals = 1) {
|
export function humanize(value, decimals = 1) {
|
||||||
if (value >= 1e6) {
|
const abs = Math.abs(value);
|
||||||
return keepThreeDigits(value / 1e6, decimals) + 'M';
|
const sign = value < 0 ? '-' : '';
|
||||||
} else if (value >= 1e3) {
|
|
||||||
return keepThreeDigits(value / 1e3, decimals) + 'K';
|
if (abs >= 1e6) {
|
||||||
|
return sign + keepThreeDigits(abs / 1e6, decimals) + 'M';
|
||||||
|
} else if (abs >= 1e3) {
|
||||||
|
return sign + keepThreeDigits(abs / 1e3, decimals) + 'K';
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
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) => (
|
const points = values.map((datasetValues, i) => (
|
||||||
datasetValues.map((value, j) => {
|
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];
|
visibleValue *= visibilities[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ export function preparePoints(data, datasets, range, visibilities, bounds, pieTo
|
|||||||
visibleValue,
|
visibleValue,
|
||||||
stackOffset: 0,
|
stackOffset: 0,
|
||||||
stackValue: visibleValue,
|
stackValue: visibleValue,
|
||||||
|
gap: isGap,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
@ -57,17 +59,31 @@ function preparePercentage(points, bounds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function prepareStacked(points) {
|
function prepareStacked(points) {
|
||||||
const accum = [];
|
const posAccum = [];
|
||||||
|
const negAccum = [];
|
||||||
|
|
||||||
points.forEach((datasetPoints) => {
|
points.forEach((datasetPoints) => {
|
||||||
datasetPoints.forEach((point, j) => {
|
datasetPoints.forEach((point, j) => {
|
||||||
if (accum[j] === undefined) {
|
if (posAccum[j] === undefined) {
|
||||||
accum[j] = 0;
|
posAccum[j] = 0;
|
||||||
|
negAccum[j] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
point.stackOffset = accum[j];
|
if (point.gap) {
|
||||||
accum[j] += point.visibleValue;
|
point.stackOffset = posAccum[j];
|
||||||
point.stackValue = accum[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);
|
document.head.appendChild(styleElement);
|
||||||
const styleSheet = styleElement.sheet;
|
const styleSheet = styleElement.sheet;
|
||||||
|
|
||||||
document.documentElement.addEventListener('darkmode', () => {
|
new MutationObserver(() => {
|
||||||
skin = detectSkin();
|
skin = detectSkin();
|
||||||
});
|
}).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
|
||||||
export function createColors(datasetColors) {
|
export function createColors(datasetColors) {
|
||||||
const colors = {};
|
const colors = {};
|
||||||
|
|||||||
@ -15,7 +15,6 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
transition: opacity 150ms ease;
|
transition: opacity 150ms ease;
|
||||||
|
|
||||||
&:hover {
|
&: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-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%;
|
background-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lovely-chart--button-label {
|
.lovely-chart--button-label {
|
||||||
|
|||||||
@ -7,20 +7,11 @@
|
|||||||
--zoom-out-text: #108BE3;
|
--zoom-out-text: #108BE3;
|
||||||
--tooltip-background: #ffffff;
|
--tooltip-background: #ffffff;
|
||||||
--tooltip-arrow: #D2D5D7;
|
--tooltip-arrow: #D2D5D7;
|
||||||
|
--minimap-limit-color: #616770;
|
||||||
|
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
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 & {
|
html.theme-dark & {
|
||||||
--background-color: #242F3E;
|
--background-color: #242F3E;
|
||||||
--text-color: #ffffff;
|
--text-color: #ffffff;
|
||||||
@ -30,8 +21,19 @@
|
|||||||
--zoom-out-text: #48AAF0;
|
--zoom-out-text: #48AAF0;
|
||||||
--tooltip-background: #1c2533;
|
--tooltip-background: #1c2533;
|
||||||
--tooltip-arrow: #D2D5D7;
|
--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 > * {
|
// &.lovely-chart--state-invisible > * {
|
||||||
// display: none;
|
// 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 {
|
.lovely-chart--minimap-slider {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
// https://jsperf.com/finding-maximum-element-in-an-array
|
// https://jsperf.com/finding-maximum-element-in-an-array
|
||||||
export function getMaxMin(array) {
|
export function getMaxMin(array) {
|
||||||
const length = array.length;
|
const length = array.length;
|
||||||
let max = array[0];
|
let max;
|
||||||
let min = array[0];
|
let min;
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
const value = array[i];
|
const value = array[i];
|
||||||
|
|
||||||
if (value > max) {
|
if (value == null) continue;
|
||||||
max = value;
|
if (max === undefined || value > max) max = value;
|
||||||
} else if (value < min) {
|
if (min === undefined || value < min) min = value;
|
||||||
min = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { max, min };
|
return { max, min };
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user