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