diff --git a/karma.conf.js b/karma.conf.js index d19bfc62..f814d2a0 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -62,6 +62,7 @@ module.exports = function(karma) { {pattern: 'node_modules/chart.js/dist/chart.js'}, {pattern: 'node_modules/hammer-simulator/index.js'}, {pattern: 'node_modules/hammerjs/hammer.js'}, + {pattern: 'node_modules/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.js'}, {pattern: 'test/index.js'}, {pattern: 'src/index.js'}, {pattern: 'test/specs/**/*.js'} diff --git a/src/core.js b/src/core.js index 56817ca4..d64cbb4c 100644 --- a/src/core.js +++ b/src/core.js @@ -77,8 +77,17 @@ export function resetZoom(chart) { } function panScale(scale, delta, panOptions, limits) { + const {panDelta} = getState(scale.chart); + // Add possible cumulative delta from previous pan attempts where scale did not change + delta += panDelta[scale.id] || 0; const fn = panFunctions[scale.type] || panFunctions.default; - call(fn, [scale, delta, panOptions, limits]); + if (call(fn, [scale, delta, panOptions, limits])) { + // The scale changed, reset cumulative delta + panDelta[scale.id] = 0; + } else { + // The scale did not change, store cumulative delta + panDelta[scale.id] = delta; + } } export function doPan(chart, pan, enabledScales) { diff --git a/src/hammer.js b/src/hammer.js index f5274306..ed71d35d 100644 --- a/src/hammer.js +++ b/src/hammer.js @@ -80,7 +80,6 @@ function endPinch(chart, state, e) { } } - function handlePan(chart, state, e) { const delta = state.delta; if (delta !== null) { diff --git a/src/scale.types.js b/src/scale.types.js index 2102991f..329c30ce 100644 --- a/src/scale.types.js +++ b/src/scale.types.js @@ -39,12 +39,14 @@ function updateRange(scale, {min, max}, limits, zoom = false) { } scaleOpts.min = min; scaleOpts.max = max; + // return true if the scale range is changed + return scale.parse(min) !== scale.min || scale.parse(max) !== scale.max; } function zoomNumericalScale(scale, zoom, center, limits) { const delta = zoomDelta(scale, zoom, center); const newRange = {min: scale.min + delta.min, max: scale.max - delta.max}; - updateRange(scale, newRange, limits, true); + return updateRange(scale, newRange, limits, true); } const integerChange = (v) => v === 0 || isNaN(v) ? 0 : v < 0 ? Math.min(Math.round(v), -1) : Math.max(Math.round(v), 1); @@ -61,7 +63,7 @@ function zoomCategoryScale(scale, zoom, center, limits) { } const delta = zoomDelta(scale, zoom, center); const newRange = {min: scale.min + integerChange(delta.min), max: scale.max - integerChange(delta.max)}; - updateRange(scale, newRange, limits, true); + return updateRange(scale, newRange, limits, true); } const categoryDelta = new WeakMap(); @@ -80,14 +82,27 @@ function panCategoryScale(scale, delta, panOptions, limits) { categoryDelta.set(scale, minIndex !== scaleMin ? 0 : cumDelta); - updateRange(scale, {min: minIndex, max: maxIndex}, limits); + return updateRange(scale, {min: minIndex, max: maxIndex}, limits); } +const OFFSETS = { + second: 500, // 500 ms + minute: 30 * 1000, // 30 s + hour: 30 * 60 * 1000, // 30 m + day: 12 * 60 * 60 * 1000, // 12 h + week: 3.5 * 24 * 60 * 60 * 1000, // 3.5 d + month: 15 * 24 * 60 * 60 * 1000, // 15 d + quarter: 60 * 24 * 60 * 60 * 1000, // 60 d + year: 182 * 24 * 60 * 60 * 1000 // 182 d +}; + function panNumericalScale(scale, delta, panOptions, limits) { - const {min: prevStart, max: prevEnd} = scale; - const newMin = scale.getValueForPixel(scale.getPixelForValue(prevStart) - delta); - const newMax = scale.getValueForPixel(scale.getPixelForValue(prevEnd) - delta); - updateRange(scale, {min: newMin, max: newMax}, limits); + const {min: prevStart, max: prevEnd, options} = scale; + const round = options.time && options.time.round; + const offset = OFFSETS[round] || 0; + const newMin = scale.getValueForPixel(scale.getPixelForValue(prevStart + offset) - delta); + const newMax = scale.getValueForPixel(scale.getPixelForValue(prevEnd + offset) - delta); + return updateRange(scale, {min: newMin, max: newMax}, limits); } export const zoomFunctions = { diff --git a/src/state.js b/src/state.js index 2223c0ca..23ba9761 100644 --- a/src/state.js +++ b/src/state.js @@ -6,6 +6,7 @@ export function getState(chart) { state = { originalScaleLimits: {}, handlers: {}, + panDelta: {} }; chartStates.set(chart, state); } diff --git a/test/fixtures/pan/time-day-left.js b/test/fixtures/pan/time-day-left.js new file mode 100644 index 00000000..d39468e8 --- /dev/null +++ b/test/fixtures/pan/time-day-left.js @@ -0,0 +1,66 @@ +const canvas = document.createElement('canvas'); +canvas.width = 512; +canvas.height = 512; +const ctx = canvas.getContext('2d'); + +module.exports = { + config: { + type: 'bar', + data: { + labels: [new Date(2020, 0, 2), new Date(2020, 0, 3), new Date(2020, 0, 4), new Date(2020, 0, 5)], + datasets: [ + { + backgroundColor: ['red', 'green', 'blue', 'orange'], + data: [1, 2, 3, 4] + } + ] + }, + options: { + animation: false, + events: [], + scales: { + y: { + display: false, + max: 5 + }, + x: { + type: 'time', + min: new Date(2020, 0, 3), + max: new Date(2020, 0, 5), + time: { + unit: 'day', + round: 'day', + } + } + }, + plugins: { + legend: false, + zoom: { + pan: { + enabled: true, + mode: 'x', + } + } + } + } + }, + options: { + spriteText: true, + run(chart) { + const steps = 4; + const n = Math.sqrt(steps); + const side = 512 / n; + for (let i = 0; i < steps; i++) { + const col = i % n; + const row = Math.floor(i / n); + if (i > 0) { + chart.pan({x: 150}); + chart.update(); + } + ctx.drawImage(chart.canvas, col * side, row * side, side, side); + } + Chart.helpers.clearCanvas(chart.canvas); + chart.ctx.drawImage(canvas, 0, 0); + } + } +}; diff --git a/test/fixtures/pan/time-day-left.png b/test/fixtures/pan/time-day-left.png new file mode 100644 index 00000000..9cd37070 Binary files /dev/null and b/test/fixtures/pan/time-day-left.png differ diff --git a/test/fixtures/pan/time-day-right.js b/test/fixtures/pan/time-day-right.js new file mode 100644 index 00000000..6ba12019 --- /dev/null +++ b/test/fixtures/pan/time-day-right.js @@ -0,0 +1,66 @@ +const canvas = document.createElement('canvas'); +canvas.width = 512; +canvas.height = 512; +const ctx = canvas.getContext('2d'); + +module.exports = { + config: { + type: 'bar', + data: { + labels: [new Date(2020, 0, 2), new Date(2020, 0, 3), new Date(2020, 0, 4), new Date(2020, 0, 5)], + datasets: [ + { + backgroundColor: ['red', 'green', 'blue', 'orange'], + data: [1, 2, 3, 4] + } + ] + }, + options: { + animation: false, + events: [], + scales: { + y: { + display: false, + max: 5 + }, + x: { + type: 'time', + min: new Date(2020, 0, 1), + max: new Date(2020, 0, 3), + time: { + unit: 'day', + round: 'day', + } + } + }, + plugins: { + legend: false, + zoom: { + pan: { + enabled: true, + mode: 'x', + } + } + } + } + }, + options: { + spriteText: true, + run(chart) { + const steps = 4; + const n = Math.sqrt(steps); + const side = 512 / n; + for (let i = 0; i < steps; i++) { + const col = i % n; + const row = Math.floor(i / n); + if (i > 0) { + chart.pan({x: -150}); + chart.update(); + } + ctx.drawImage(chart.canvas, col * side, row * side, side, side); + } + Chart.helpers.clearCanvas(chart.canvas); + chart.ctx.drawImage(canvas, 0, 0); + } + } +}; diff --git a/test/fixtures/pan/time-day-right.png b/test/fixtures/pan/time-day-right.png new file mode 100644 index 00000000..f3c60eb4 Binary files /dev/null and b/test/fixtures/pan/time-day-right.png differ