Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 159 additions & 4 deletions packages/devextreme/js/__internal/viz/axes/base_axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ import { Deferred, when } from '@js/core/utils/deferred';
import { extend } from '@js/core/utils/extend';
import { adjust } from '@js/core/utils/math';
import {
isDefined, isFunction, isPlainObject, type,
isDate, isDefined, isFunction, isPlainObject, type,
} from '@js/core/utils/type';
import formatHelper from '@js/format_helper';
import { multiplyInExponentialForm } from '@ts/core/utils/m_math';
import constants from '@ts/viz/axes/axes_constants';
import { calculateCanvasMargins, measureLabels } from '@ts/viz/axes/axes_utils';
import createConstantLine from '@ts/viz/axes/constant_line';
Expand Down Expand Up @@ -70,6 +71,12 @@ const _isArray = Array.isArray;
const DEFAULT_AXIS_LABEL_SPACING = 5;
const MAX_GRID_BORDER_ADHENSION = 4;

const PANNING_CORRECTION_ITERATION_COUNT = 5;
const PANNING_CORRECTION_PRECISION = 1e-4;

const ZOOM_FACTOR_PRECISION = 2;
const ZOOM_FACTOR_MULTIPLIER = 10 ** ZOOM_FACTOR_PRECISION;

const TOP = constants.top;
const BOTTOM = constants.bottom;
const LEFT = constants.left;
Expand Down Expand Up @@ -1294,6 +1301,146 @@ Axis.prototype = {
return length;
},

_getTickIntervalValue() {
const tickInterval = this.getTickInterval();

if (!isDefined(tickInterval)) {
return 0;
}

return this._options.dataType === 'datetime' ? dateUtils.dateToMilliseconds(tickInterval) : tickInterval;
},

getWholeRangeBreaks() {
const businessRange = this._translator.getBusinessRange();
const { type } = this._options;

if (type === constants.discrete || type === constants.logarithmic
|| !isDefined(businessRange.min) || !isDefined(businessRange.max)) {
return [];
}

const interval = this._getTickIntervalValue();

return this._getBreaksForRange(businessRange.min, businessRange.max)
.reduce((result, scaleBreak) => {
const hidden = this._getHiddenDuration(scaleBreak, interval);
const shift = ((scaleBreak.to - scaleBreak.from) - hidden) / 2;

Comment thread
dmlvr marked this conversation as resolved.
return hidden ? result.concat(extend({}, scaleBreak, {
from: this._addToValue(scaleBreak.from, shift),
to: this._addToValue(scaleBreak.to, -shift),
cumulativeWidth: 0,
})) : result;
}, []);
},

_getBreaksForRange(minVisible, maxVisible) {
const viewport = minVisible > maxVisible
? { minVisible: maxVisible, maxVisible: minVisible }
: { minVisible, maxVisible };
const breaks = this._getScaleBreaks(this._options, viewport, this._series, this.isArgumentAxis);

return this._filterBreaks(breaks, viewport, this._options.breakStyle);
Comment thread
dmlvr marked this conversation as resolved.
},

_getHiddenDuration(scaleBreak, tickInterval) {
const duration = scaleBreak.to - scaleBreak.from;

return scaleBreak.gapSize ? duration : Math.max(duration - tickInterval, 0);
},

getVisualRangeLengthWithoutBreaks(range) {
const businessRange = range || this._translator.getBusinessRange();
const length = this.getVisualRangeLength(businessRange);
const options = this._options;

if (options.type === constants.discrete || options.type === constants.logarithmic
|| !isDefined(businessRange.minVisible) || !isDefined(businessRange.maxVisible)) {
return length;
}

const interval = this._getTickIntervalValue();

return this._getBreaksForRange(businessRange.minVisible, businessRange.maxVisible)
.reduce((result, scaleBreak) => result - this._getHiddenDuration(scaleBreak, interval), length);
},

_addToValue(value, diff) {
return isDate(value) ? new Date(value.getTime() + diff) : value + diff;
},

adjustPannedRange(range, anchor?: 'start' | 'end') {
const that = this;
const storedParams = that._storedZoomEndParams;
const { type } = that._options;

if (!storedParams || type === constants.discrete || type === constants.logarithmic) {
return range;
}

const { startRange } = storedParams;

if (!this._getBreaksForRange(range.startValue, range.endValue).length
&& !this._getBreaksForRange(startRange.startValue, startRange.endValue).length) {
return range;
}

const isReversed = range.startValue > range.endValue;

if (isReversed) {
const reordered = that.adjustPannedRange({ startValue: range.endValue, endValue: range.startValue }, anchor);

return { startValue: reordered.endValue, endValue: reordered.startValue };
}

const targetLength = that.getVisualRangeLengthWithoutBreaks({
minVisible: startRange.startValue,
maxVisible: startRange.endValue,
});

if (!targetLength) {
return range;
}

const tolerance = targetLength * PANNING_CORRECTION_PRECISION;
const bounds = that.getZoomBounds();
const keepsEndValue = anchor
? anchor === 'end'
: range.startValue > startRange.startValue || range.endValue > startRange.endValue;
let result = range;
let current = range;
let bestDeviation = Infinity;

for (let i = 0; i < PANNING_CORRECTION_ITERATION_COUNT; i += 1) {
const delta = that.getVisualRangeLengthWithoutBreaks({
minVisible: current.startValue,
maxVisible: current.endValue,
}) - targetLength;
const deviation = Math.abs(delta);

if (deviation < bestDeviation) {
bestDeviation = deviation;
result = current;
}

if (deviation <= tolerance) {
break;
}

current = keepsEndValue
? { startValue: that._addToValue(current.startValue, delta), endValue: current.endValue }
: { startValue: current.startValue, endValue: that._addToValue(current.endValue, -delta) };

if (current.startValue >= current.endValue
|| current.startValue < bounds.startValue || current.endValue > bounds.endValue) {
break;
}
}

return result;
},

getVisualRangeCenter(range, useMerge) {
const translator = this.getTranslator();
const businessRange = translator.getBusinessRange();
Expand Down Expand Up @@ -2468,9 +2615,17 @@ Axis.prototype = {
};
const typeIsNotChanged = that.getOptions().type === that._storedZoomEndParams.type;
const shift = typeIsNotChanged ? adjust(that.getVisualRangeCenter() - that.getVisualRangeCenter(previousBusinessRange, false)) : NaN;
const zoomFactor = typeIsNotChanged
// @ts-expect-error
? +`${Math.round(`${that.getVisualRangeLength(previousBusinessRange) / (that.getVisualRangeLength() || 1)}e+2`)}e-2` : NaN;
const calcZoomFactor = (): number => {
if (action === 'pan') {
return 1;
}

const currentLength = that.getVisualRangeLength() || 1;
const ratio = that.getVisualRangeLength(previousBusinessRange) / currentLength;

return Math.round(multiplyInExponentialForm(ratio, ZOOM_FACTOR_PRECISION)) / ZOOM_FACTOR_MULTIPLIER;
};
const zoomFactor = typeIsNotChanged ? calcZoomFactor() : NaN;
const zoomEndEvent = that._getZoomEndEventArg(previousRange, domEvent, action, zoomFactor, shift);

zoomEndEvent.cancel = that.checkZoomingLowerLimitOvercome(zoomFactor === 1 ? 'pan' : 'zoom', zoomFactor).stopInteraction;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,11 @@ export const BaseChart = BaseWidget.inherit({
zoomMaxArg = argBusinessRange.maxVisible;
}

this._scrollBar.init(argBusinessRange, !this._argumentAxes[0].getOptions().valueMarginsEnabled).setPosition(zoomMinArg, zoomMaxArg);
const argumentAxis = this._argumentAxes[0];

this._scrollBar
.init(argBusinessRange, !argumentAxis.getOptions().valueMarginsEnabled, argumentAxis.getWholeRangeBreaks())
.setPosition(zoomMinArg, zoomMaxArg);
}

this._updateTracker(trackerCanvases);
Expand Down
104 changes: 69 additions & 35 deletions packages/devextreme/js/__internal/viz/chart_components/scroll_bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ ScrollBar.prototype = {
const scrollElement = this._scroll.element;

eventsEngine.on(scrollElement, dragEventStart, (e) => {
this._dragStartOffset = this._offset;

fireEvent({
type: 'dxc-scroll-start',
originalEvent: e,
Expand All @@ -68,37 +70,63 @@ ScrollBar.prototype = {
});

eventsEngine.on(scrollElement, dragEventMove, (e) => {
const dX = -e.offset.x * this._scale;
const dY = -e.offset.y * this._scale;
const lx = this._offset - (this._layoutOptions.vertical ? dY : dX) / this._scale;
this._applyPosition(lx, lx + this._translator.canvasLength / this._scale);
const position = this._getDragPosition(e);
this._applyPosition(position, position + this._thumbLength);

fireEvent({
type: 'dxc-scroll-move',
originalEvent: e,
target: scrollElement,
// @ts-expect-error
offset: {
x: dX,
y: dY,
},
});
fireEvent(this._getDragEvent('dxc-scroll-move', e, scrollElement, position));
});

eventsEngine.on(scrollElement, dragEventEnd, (e) => {
fireEvent({
type: 'dxc-scroll-end',
originalEvent: e,
target: scrollElement,
// @ts-expect-error
offset: {
x: -e.offset.x * this._scale,
y: -e.offset.y * this._scale,
},
});
fireEvent(this._getDragEvent('dxc-scroll-end', e, scrollElement, this._getDragPosition(e)));
});
},

_getDragPosition(e) {
const offset = this._layoutOptions.vertical ? e.offset.y : e.offset.x;

return (this._dragStartOffset ?? this._offset) + offset;
},

_getDragEvent(type, e, target, position) {
return {
type,
originalEvent: e,
target,
offset: {
x: -e.offset.x * this._scale,
y: -e.offset.y * this._scale,
},
scrollRange: this._getRangeAtPosition(position),
};
},

_getBoundaryDirection() {
return this._translateWithOffset || (this._hasBreaks ? 1 : 0);
},

_getRangeAtPosition(position) {
const translator = this._translator;
const length = this._thumbLength;

if (!isFinite(position) || !isFinite(length)) {
return undefined;
}

const visibleArea = translator.getCanvasVisibleArea();
const lastPosition = _max(visibleArea.max - length, visibleArea.min);
const start = _min(_max(position, visibleArea.min), lastPosition);

// the inverse of setPosition: the same boundary directions, or the end coordinate of a
// discrete thumb would resolve to the next category and every drag would widen the range
const direction = this._getBoundaryDirection();
const from = translator.from(start, -direction);
const to = translator.from(start + length, direction);

return translator.isInverted()
? { startValue: to, endValue: from }
: { startValue: from, endValue: to };
},

update(options) {
const that = this;
let position = options.position;
Expand Down Expand Up @@ -129,18 +157,21 @@ ScrollBar.prototype = {
return that;
},

init(range, stick) {
init(range, stick, wholeRangeBreaks) {
const that = this;
const isDiscrete = range.axisType === 'discrete';
that._translateWithOffset = (isDiscrete && !stick && 1) || 0;
that._hasBreaks = !!wholeRangeBreaks?.length;
that._translator.update(extend({}, range, {
minVisible: null,
maxVisible: null,
visibleCategories: null,
breaks: wholeRangeBreaks?.length ? wholeRangeBreaks : null,
userBreaks: null,
}, isDiscrete && {
min: null,
max: null,
} || {}), that._canvas, { isHorizontal: !that._layoutOptions.vertical, stick });
} || {}), that._canvas, { isHorizontal: !that._layoutOptions.vertical, stick, breaksSize: 0 });
return that;
},

Expand Down Expand Up @@ -215,15 +246,18 @@ ScrollBar.prototype = {
// Axis like functions

setPosition(min, max) {
const that = this;
const translator = that._translator;
const minPoint = isDefined(min) ? translator.translate(min, -that._translateWithOffset) : translator.translate('canvas_position_start');
const maxPoint = isDefined(max) ? translator.translate(max, that._translateWithOffset) : translator.translate('canvas_position_end');

that._offset = _min(minPoint, maxPoint);
that._scale = translator.getScale(min, max);

that._applyPosition(_min(minPoint, maxPoint), _max(minPoint, maxPoint));
const translator = this._translator;
const direction = this._getBoundaryDirection();
const minPoint = isDefined(min) ? translator.translate(min, -direction) : translator.translate('canvas_position_start');
const maxPoint = isDefined(max) ? translator.translate(max, direction) : translator.translate('canvas_position_end');

this._offset = _min(minPoint, maxPoint);
this._thumbLength = Math.abs(maxPoint - minPoint);
this._scale = this._thumbLength
? translator.canvasLength / this._thumbLength
: translator.getScale(min, max);

this._applyPosition(_min(minPoint, maxPoint), _max(minPoint, maxPoint));
},

customPositionIsAvailable() {
Expand Down
Loading
Loading