diff --git a/src/lib/lovely-chart/Axes.js b/src/lib/lovely-chart/Axes.js
index 75bca5d11..fb214859c 100644
--- a/src/lib/lovely-chart/Axes.js
+++ b/src/lib/lovely-chart/Axes.js
@@ -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';
diff --git a/src/lib/lovely-chart/LovelyChart.js b/src/lib/lovely-chart/LovelyChart.js
index f55d01cf1..9f2e47ad2 100644
--- a/src/lib/lovely-chart/LovelyChart.js
+++ b/src/lib/lovely-chart/LovelyChart.js
@@ -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) {
diff --git a/src/lib/lovely-chart/Minimap.js b/src/lib/lovely-chart/Minimap.js
index a4e591263..a570df994 100644
--- a/src/lib/lovely-chart/Minimap.js
+++ b/src/lib/lovely-chart/Minimap.js
@@ -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 =
+ '';
+ 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;
}
diff --git a/src/lib/lovely-chart/StateManager.js b/src/lib/lovely-chart/StateManager.js
index 0437e432f..887467d9a 100644
--- a/src/lib/lovely-chart/StateManager.js
+++ b/src/lib/lovely-chart/StateManager.js
@@ -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,
};
}
diff --git a/src/lib/lovely-chart/Tooltip.js b/src/lib/lovely-chart/Tooltip.js
index 81d0aa966..51aacef8a 100644
--- a/src/lib/lovely-chart/Tooltip.js
+++ b/src/lib/lovely-chart/Tooltip.js
@@ -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,12 +358,7 @@ 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);
- }
+ dataSetContainer.appendChild(newDataSet);
}
function _updateDataSet(currentDataSet, { key, value } = {}, totalValue) {
@@ -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;
diff --git a/src/lib/lovely-chart/TransitionManager.js b/src/lib/lovely-chart/TransitionManager.js
index 625ee9de4..23f7b84b5 100644
--- a/src/lib/lovely-chart/TransitionManager.js
+++ b/src/lib/lovely-chart/TransitionManager.js
@@ -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) {
diff --git a/src/lib/lovely-chart/constants.js b/src/lib/lovely-chart/constants.js
index 198ff37df..17ebf0e87 100644
--- a/src/lib/lovely-chart/constants.js
+++ b/src/lib/lovely-chart/constants.js
@@ -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;
diff --git a/src/lib/lovely-chart/data.js b/src/lib/lovely-chart/data.js
index 8cef5ccd4..39d9946c0 100644
--- a/src/lib/lovely-chart/data.js
+++ b/src/lib/lovely-chart/data.js
@@ -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,
diff --git a/src/lib/lovely-chart/drawDatasets.js b/src/lib/lovely-chart/drawDatasets.js
index ce876c598..30d2ed380 100644
--- a/src/lib/lovely-chart/drawDatasets.js
+++ b/src/lib/lovely-chart/drawDatasets.js
@@ -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);
- if (options.simplification) {
- const simplifierFn = simplify(pixels);
- pixels = simplifierFn(options.simplification).points;
- }
-
- pixels.forEach(([x, y]) => {
- context.lineTo(x, y);
+ segments.forEach((segment) => {
+ let pixels = segment;
+ if (options.simplification) {
+ pixels = simplify(pixels)(options.simplification).points;
+ }
+ 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';
diff --git a/src/lib/lovely-chart/format.js b/src/lib/lovely-chart/format.js
index 1956802ec..6539c3745 100644
--- a/src/lib/lovely-chart/format.js
+++ b/src/lib/lovely-chart/format.js
@@ -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;
diff --git a/src/lib/lovely-chart/icons/lock.svg b/src/lib/lovely-chart/icons/lock.svg
new file mode 100644
index 000000000..2e6085f79
--- /dev/null
+++ b/src/lib/lovely-chart/icons/lock.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/lib/lovely-chart/preparePoints.js b/src/lib/lovely-chart/preparePoints.js
index b29f5fa18..30706ed78 100644
--- a/src/lib/lovely-chart/preparePoints.js
+++ b/src/lib/lovely-chart/preparePoints.js
@@ -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];
+ }
});
});
}
diff --git a/src/lib/lovely-chart/skin.js b/src/lib/lovely-chart/skin.js
index 7b7261d00..7451ad2bf 100644
--- a/src/lib/lovely-chart/skin.js
+++ b/src/lib/lovely-chart/skin.js
@@ -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 = {};
diff --git a/src/lib/lovely-chart/styles/_buttons.scss b/src/lib/lovely-chart/styles/_buttons.scss
index 49d843701..3abae9d46 100644
--- a/src/lib/lovely-chart/styles/_buttons.scss
+++ b/src/lib/lovely-chart/styles/_buttons.scss
@@ -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 {
diff --git a/src/lib/lovely-chart/styles/_common.scss b/src/lib/lovely-chart/styles/_common.scss
index 3cf3b44c7..6fc98d0f6 100644
--- a/src/lib/lovely-chart/styles/_common.scss
+++ b/src/lib/lovely-chart/styles/_common.scss
@@ -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;
// }
diff --git a/src/lib/lovely-chart/styles/_minimap.scss b/src/lib/lovely-chart/styles/_minimap.scss
index 0df4f4716..1fb4da892 100644
--- a/src/lib/lovely-chart/styles/_minimap.scss
+++ b/src/lib/lovely-chart/styles/_minimap.scss
@@ -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;
diff --git a/src/lib/lovely-chart/utils.js b/src/lib/lovely-chart/utils.js
index d13959c23..00c4171b1 100644
--- a/src/lib/lovely-chart/utils.js
+++ b/src/lib/lovely-chart/utils.js
@@ -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 };