UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

612 lines (607 loc) • 25.8 kB
/** * DevExtreme (esm/viz/translators/translator2d.js) * Version: 24.2.6 * Build date: Mon Mar 17 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import { extend } from "../../core/utils/extend"; import { each } from "../../core/utils/iterator"; import { Range } from "./range"; import categoryTranslator from "./category_translator"; import intervalTranslator from "./interval_translator"; import datetimeTranslator from "./datetime_translator"; import logarithmicTranslator from "./logarithmic_translator"; import { getLogExt as getLog, getPower, raiseToExt, getCategoriesInfo } from "../core/utils"; import { isDefined, isDate } from "../../core/utils/type"; import { adjust } from "../../core/utils/math"; import dateUtils from "../../core/utils/date"; const _abs = Math.abs; const CANVAS_PROP = ["width", "height", "left", "top", "bottom", "right"]; const dummyTranslator = { to(value) { const coord = this._canvasOptions.startPoint + (this._options.conversionValue ? value : Math.round(value)); return coord > this._canvasOptions.endPoint ? this._canvasOptions.endPoint : coord }, from(value) { return value - this._canvasOptions.startPoint } }; const validateCanvas = function(canvas) { each(CANVAS_PROP, (function(_, prop) { canvas[prop] = parseInt(canvas[prop]) || 0 })); return canvas }; const makeCategoriesToPoints = function(categories) { const categoriesToPoints = {}; categories.forEach((function(item, i) { categoriesToPoints[item.valueOf()] = i })); return categoriesToPoints }; const validateBusinessRange = function(businessRange) { if (!(businessRange instanceof Range)) { businessRange = new Range(businessRange) } function validate(valueSelector, baseValueSelector) { if (!isDefined(businessRange[valueSelector]) && isDefined(businessRange[baseValueSelector])) { businessRange[valueSelector] = businessRange[baseValueSelector] } } validate("minVisible", "min"); validate("maxVisible", "max"); return businessRange }; function prepareBreaks(breaks, range) { const transform = "logarithmic" === range.axisType ? function(value) { return getLog(value, range.base) } : function(value) { return value }; const array = []; let br; let transformFrom; let transformTo; let i; const length = breaks.length; let sum = 0; for (i = 0; i < length; i++) { br = breaks[i]; transformFrom = transform(br.from); transformTo = transform(br.to); sum += transformTo - transformFrom; array.push({ trFrom: transformFrom, trTo: transformTo, from: br.from, to: br.to, length: sum, cumulativeWidth: br.cumulativeWidth }) } return array } function getCanvasBounds(range) { let min = range.min; let max = range.max; let minVisible = range.minVisible; let maxVisible = range.maxVisible; const isLogarithmic = "logarithmic" === range.axisType; if (isLogarithmic) { maxVisible = getLog(maxVisible, range.base, range.allowNegatives, range.linearThreshold); minVisible = getLog(minVisible, range.base, range.allowNegatives, range.linearThreshold); min = getLog(min, range.base, range.allowNegatives, range.linearThreshold); max = getLog(max, range.base, range.allowNegatives, range.linearThreshold) } return { base: range.base, rangeMin: min, rangeMax: max, rangeMinVisible: minVisible, rangeMaxVisible: maxVisible } } function getCheckingMethodsAboutBreaks(inverted) { return { isStartSide: !inverted ? function(pos, breaks, start, end) { return pos < breaks[0][start] } : function(pos, breaks, start, end) { return pos <= breaks[breaks.length - 1][end] }, isEndSide: !inverted ? function(pos, breaks, start, end) { return pos >= breaks[breaks.length - 1][end] } : function(pos, breaks, start, end) { return pos > breaks[0][start] }, isInBreak: !inverted ? function(pos, br, start, end) { return pos >= br[start] && pos < br[end] } : function(pos, br, start, end) { return pos > br[end] && pos <= br[start] }, isBetweenBreaks: !inverted ? function(pos, br, prevBreak, start, end) { return pos < br[start] && pos >= prevBreak[end] } : function(pos, br, prevBreak, start, end) { return pos >= br[end] && pos < prevBreak[start] }, getLength: !inverted ? function(br) { return br.length } : function(br, lastBreak) { return lastBreak.length - br.length }, getBreaksSize: !inverted ? function(br) { return br.cumulativeWidth } : function(br, lastBreak) { return lastBreak.cumulativeWidth - br.cumulativeWidth } } } const _Translator2d = function(businessRange, canvas, options) { this.update(businessRange, canvas, options) }; _Translator2d.prototype = { constructor: _Translator2d, reinit: function() { const that = this; const options = that._options; const range = that._businessRange; const categories = range.categories || []; let script = {}; const canvasOptions = that._prepareCanvasOptions(); const visibleCategories = getCategoriesInfo(categories, range.minVisible, range.maxVisible).categories; const categoriesLength = visibleCategories.length; if (range.isEmpty()) { script = dummyTranslator } else { switch (range.axisType) { case "logarithmic": script = logarithmicTranslator; break; case "semidiscrete": script = intervalTranslator; canvasOptions.ratioOfCanvasRange = canvasOptions.canvasLength / (dateUtils.addInterval(canvasOptions.rangeMaxVisible, options.interval) - canvasOptions.rangeMinVisible); break; case "discrete": script = categoryTranslator; that._categories = categories; canvasOptions.interval = that._getDiscreteInterval(options.addSpiderCategory ? categoriesLength + 1 : categoriesLength, canvasOptions); that._categoriesToPoints = makeCategoriesToPoints(categories); if (categoriesLength) { canvasOptions.startPointIndex = that._categoriesToPoints[visibleCategories[0].valueOf()]; that.visibleCategories = visibleCategories } break; default: if ("datetime" === range.dataType) { script = datetimeTranslator } } }(that._oldMethods || []).forEach((function(methodName) { delete that[methodName] })); that._oldMethods = Object.keys(script); extend(that, script); that._conversionValue = options.conversionValue ? value => value : (value, skipRound) => skipRound ? value : Math.round(value); that.sc = {}; that._checkingMethodsAboutBreaks = [getCheckingMethodsAboutBreaks(false), getCheckingMethodsAboutBreaks(that.isInverted())]; that._translateBreaks(); that._calculateSpecialValues() }, _translateBreaks: function() { const breaks = this._breaks; const size = this._options.breaksSize; let i; let b; let end; let length; if (void 0 === breaks) { return } for (i = 0, length = breaks.length; i < length; i++) { b = breaks[i]; end = this.translate(b.to); b.end = end; b.start = !b.gapSize ? !this.isInverted() ? end - size : end + size : end } }, _checkValueAboutBreaks: function(breaks, pos, start, end, methods) { let i; let length; let prop = { length: 0, breaksSize: void 0, inBreak: false }; let br; let prevBreak; const lastBreak = breaks[breaks.length - 1]; if (methods.isStartSide(pos, breaks, start, end)) { return prop } else if (methods.isEndSide(pos, breaks, start, end)) { return { length: lastBreak.length, breaksSize: lastBreak.cumulativeWidth, inBreak: false } } for (i = 0, length = breaks.length; i < length; i++) { br = breaks[i]; prevBreak = breaks[i - 1]; if (methods.isInBreak(pos, br, start, end)) { prop.inBreak = true; prop.break = br; break } if (prevBreak && methods.isBetweenBreaks(pos, br, prevBreak, start, end)) { prop = { length: methods.getLength(prevBreak, lastBreak), breaksSize: methods.getBreaksSize(prevBreak, lastBreak), inBreak: false }; break } } return prop }, isInverted: function() { return !(this._options.isHorizontal ^ this._businessRange.invert) }, _getDiscreteInterval: function(categoriesLength, canvasOptions) { const correctedCategoriesCount = categoriesLength - (this._options.stick ? 1 : 0); return correctedCategoriesCount > 0 ? canvasOptions.canvasLength / correctedCategoriesCount : canvasOptions.canvasLength }, _prepareCanvasOptions() { const businessRange = this._businessRange; const canvasOptions = this._canvasOptions = getCanvasBounds(businessRange); const canvas = this._canvas; const breaks = this._breaks; let length; canvasOptions.startPadding = canvas.startPadding || 0; canvasOptions.endPadding = canvas.endPadding || 0; if (this._options.isHorizontal) { canvasOptions.startPoint = canvas.left + canvasOptions.startPadding; length = canvas.width; canvasOptions.endPoint = canvas.width - canvas.right - canvasOptions.endPadding; canvasOptions.invert = businessRange.invert } else { canvasOptions.startPoint = canvas.top + canvasOptions.startPadding; length = canvas.height; canvasOptions.endPoint = canvas.height - canvas.bottom - canvasOptions.endPadding; canvasOptions.invert = !businessRange.invert } this.canvasLength = canvasOptions.canvasLength = canvasOptions.endPoint - canvasOptions.startPoint; canvasOptions.rangeDoubleError = Math.pow(10, getPower(canvasOptions.rangeMax - canvasOptions.rangeMin) - getPower(length) - 2); canvasOptions.ratioOfCanvasRange = canvasOptions.canvasLength / (canvasOptions.rangeMaxVisible - canvasOptions.rangeMinVisible); if (void 0 !== breaks) { const visibleRangeLength = canvasOptions.rangeMaxVisible - canvasOptions.rangeMinVisible - breaks[breaks.length - 1].length; if (0 !== visibleRangeLength) { canvasOptions.ratioOfCanvasRange = (canvasOptions.canvasLength - breaks[breaks.length - 1].cumulativeWidth) / visibleRangeLength } } return canvasOptions }, updateCanvas: function(canvas) { this._canvas = validateCanvas(canvas); this.reinit() }, updateBusinessRange: function(businessRange) { const breaks = businessRange.breaks || []; this._userBreaks = businessRange.userBreaks || []; this._businessRange = validateBusinessRange(businessRange); this._breaks = breaks.length ? prepareBreaks(breaks, this._businessRange) : void 0; this.reinit() }, update: function(businessRange, canvas, options) { this._options = extend(this._options || {}, options); this._canvas = validateCanvas(canvas); this.updateBusinessRange(businessRange) }, getBusinessRange: function() { return this._businessRange }, getEventScale: function(zoomEvent) { return zoomEvent.deltaScale || 1 }, getCanvasVisibleArea: function() { return { min: this._canvasOptions.startPoint, max: this._canvasOptions.endPoint } }, _calculateSpecialValues: function() { const that = this; const canvasOptions = that._canvasOptions; const startPoint = canvasOptions.startPoint - canvasOptions.startPadding; const endPoint = canvasOptions.endPoint + canvasOptions.endPadding; const range = that._businessRange; const minVisible = range.minVisible; const maxVisible = range.maxVisible; const canvas_position_center_middle = startPoint + canvasOptions.canvasLength / 2; let canvas_position_default; if (minVisible < 0 && maxVisible > 0 && minVisible !== maxVisible) { canvas_position_default = that.translate(0, 1) } if (!isDefined(canvas_position_default)) { const invert = range.invert ^ (minVisible < 0 && maxVisible <= 0); if (that._options.isHorizontal) { canvas_position_default = invert ? endPoint : startPoint } else { canvas_position_default = invert ? startPoint : endPoint } } that.sc = { canvas_position_default: canvas_position_default, canvas_position_left: startPoint, canvas_position_top: startPoint, canvas_position_center: canvas_position_center_middle, canvas_position_middle: canvas_position_center_middle, canvas_position_right: endPoint, canvas_position_bottom: endPoint, canvas_position_start: canvasOptions.invert ? endPoint : startPoint, canvas_position_end: canvasOptions.invert ? startPoint : endPoint } }, translateSpecialCase(value) { return this.sc[value] }, _calculateProjection: function(distance) { const canvasOptions = this._canvasOptions; return canvasOptions.invert ? canvasOptions.endPoint - distance : canvasOptions.startPoint + distance }, _calculateUnProjection: function(distance) { const canvasOptions = this._canvasOptions; "datetime" === this._businessRange.dataType && (distance = Math.round(distance)); return canvasOptions.invert ? canvasOptions.rangeMaxVisible.valueOf() - distance : canvasOptions.rangeMinVisible.valueOf() + distance }, getMinBarSize: function(minBarSize) { const visibleArea = this.getCanvasVisibleArea(); const minValue = this.from(visibleArea.min + minBarSize); return _abs(this.from(visibleArea.min) - (!isDefined(minValue) ? this.from(visibleArea.max) : minValue)) }, checkMinBarSize: function(value, minShownValue) { return _abs(value) < minShownValue ? value >= 0 ? minShownValue : -minShownValue : value }, translate(bp, direction, skipRound) { const specialValue = this.translateSpecialCase(bp); if (isDefined(specialValue)) { return Math.round(specialValue) } if (isNaN(bp)) { return null } return this.to(bp, direction, skipRound) }, getInterval: function(interval) { const canvasOptions = this._canvasOptions; interval = interval ?? this._businessRange.interval; if (interval) { return Math.round(canvasOptions.ratioOfCanvasRange * interval) } return Math.round(canvasOptions.endPoint - canvasOptions.startPoint) }, zoom(translate, scale, wholeRange) { const canvasOptions = this._canvasOptions; if (canvasOptions.rangeMinVisible.valueOf() === canvasOptions.rangeMaxVisible.valueOf() && 0 !== translate) { return this.zoomZeroLengthRange(translate, scale) } const startPoint = canvasOptions.startPoint; const endPoint = canvasOptions.endPoint; const isInverted = this.isInverted(); let newStart = (startPoint + translate) / scale; let newEnd = (endPoint + translate) / scale; wholeRange = wholeRange || {}; const minPoint = this.to(isInverted ? wholeRange.endValue : wholeRange.startValue); const maxPoint = this.to(isInverted ? wholeRange.startValue : wholeRange.endValue); let min; let max; if (minPoint > newStart) { newEnd -= newStart - minPoint; newStart = minPoint; min = isInverted ? wholeRange.endValue : wholeRange.startValue } if (maxPoint < newEnd) { newStart -= newEnd - maxPoint; newEnd = maxPoint; max = isInverted ? wholeRange.startValue : wholeRange.endValue } if (maxPoint - minPoint < newEnd - newStart) { newStart = minPoint; newEnd = maxPoint } translate = (endPoint - startPoint) * newStart / (newEnd - newStart) - startPoint; scale = (startPoint + translate) / newStart || 1; min = isDefined(min) ? min : adjust(this.from(newStart, 1)); max = isDefined(max) ? max : adjust(this.from(newEnd, -1)); if (scale <= 1) { min = this._correctValueAboutBreaks(min, 1 === scale ? translate : -1); max = this._correctValueAboutBreaks(max, 1 === scale ? translate : 1) } if (min > max) { min = min > wholeRange.endValue ? wholeRange.endValue : min; max = max < wholeRange.startValue ? wholeRange.startValue : max } else { min = min < wholeRange.startValue ? wholeRange.startValue : min; max = max > wholeRange.endValue ? wholeRange.endValue : max } return { min: min, max: max, translate: adjust(translate), scale: adjust(scale) } }, _correctValueAboutBreaks(value, direction) { const br = this._userBreaks.filter((br => value >= br.from && value <= br.to)); if (br.length) { return direction > 0 ? br[0].to : br[0].from } else { return value } }, zoomZeroLengthRange(translate, scale) { const canvasOptions = this._canvasOptions; const min = canvasOptions.rangeMin; const max = canvasOptions.rangeMax; const correction = (max.valueOf() !== min.valueOf() ? max.valueOf() - min.valueOf() : _abs(canvasOptions.rangeMinVisible.valueOf() - min.valueOf())) / canvasOptions.canvasLength; const isDateTime = isDate(max) || isDate(min); const isLogarithmic = "logarithmic" === this._businessRange.axisType; let newMin = canvasOptions.rangeMinVisible.valueOf() - correction; let newMax = canvasOptions.rangeMaxVisible.valueOf() + correction; newMin = isLogarithmic ? adjust(raiseToExt(newMin, canvasOptions.base)) : isDateTime ? new Date(newMin) : newMin; newMax = isLogarithmic ? adjust(raiseToExt(newMax, canvasOptions.base)) : isDateTime ? new Date(newMax) : newMax; return { min: newMin, max: newMax, translate: translate, scale: scale } }, getMinScale: function(zoom) { const { dataType: dataType, interval: interval } = this._businessRange; if ("datetime" === dataType && 1 === interval) { return this.getDateTimeMinScale(zoom) } return zoom ? 1.1 : .9 }, getDateTimeMinScale(zoom) { const canvasOptions = this._canvasOptions; let length = canvasOptions.canvasLength / canvasOptions.ratioOfCanvasRange; length += (parseInt(.1 * length) || 1) * (zoom ? -2 : 2); return canvasOptions.canvasLength / (Math.max(length, 1) * canvasOptions.ratioOfCanvasRange) }, getScale: function(val1, val2) { const canvasOptions = this._canvasOptions; if (canvasOptions.rangeMax === canvasOptions.rangeMin) { return 1 } val1 = isDefined(val1) ? this.fromValue(val1) : canvasOptions.rangeMin; val2 = isDefined(val2) ? this.fromValue(val2) : canvasOptions.rangeMax; return (canvasOptions.rangeMax - canvasOptions.rangeMin) / Math.abs(val1 - val2) }, isValid: function(value) { const co = this._canvasOptions; value = this.fromValue(value); return null !== value && !isNaN(value) && value.valueOf() + co.rangeDoubleError >= co.rangeMin && value.valueOf() - co.rangeDoubleError <= co.rangeMax }, getCorrectValue: function(value, direction) { const that = this; const breaks = that._breaks; let prop; value = that.fromValue(value); if (that._breaks) { prop = that._checkValueAboutBreaks(breaks, value, "trFrom", "trTo", that._checkingMethodsAboutBreaks[0]); if (true === prop.inBreak) { return that.toValue(direction > 0 ? prop.break.trTo : prop.break.trFrom) } } return that.toValue(value) }, to: function(bp, direction, skipRound) { const range = this.getBusinessRange(); if (isDefined(range.maxVisible) && isDefined(range.minVisible) && range.maxVisible.valueOf() === range.minVisible.valueOf()) { if (!isDefined(bp) || range.maxVisible.valueOf() !== bp.valueOf()) { return null } return this.translateSpecialCase(0 === bp && this._options.shiftZeroValue ? "canvas_position_default" : "canvas_position_middle") } bp = this.fromValue(bp); const that = this; const canvasOptions = that._canvasOptions; const breaks = that._breaks; let prop = { length: 0 }; let commonBreakSize = 0; if (void 0 !== breaks) { prop = that._checkValueAboutBreaks(breaks, bp, "trFrom", "trTo", that._checkingMethodsAboutBreaks[0]); commonBreakSize = isDefined(prop.breaksSize) ? prop.breaksSize : 0 } if (true === prop.inBreak) { if (direction > 0) { return prop.break.start } else if (direction < 0) { return prop.break.end } else { return null } } return that._conversionValue(that._calculateProjection((bp - canvasOptions.rangeMinVisible - prop.length) * canvasOptions.ratioOfCanvasRange + commonBreakSize), skipRound) }, from: function(pos, direction) { const that = this; const breaks = that._breaks; let prop = { length: 0 }; const canvasOptions = that._canvasOptions; const startPoint = canvasOptions.startPoint; let commonBreakSize = 0; if (void 0 !== breaks) { prop = that._checkValueAboutBreaks(breaks, pos, "start", "end", that._checkingMethodsAboutBreaks[1]); commonBreakSize = isDefined(prop.breaksSize) ? prop.breaksSize : 0 } if (true === prop.inBreak) { if (direction > 0) { return that.toValue(prop.break.trTo) } else if (direction < 0) { return that.toValue(prop.break.trFrom) } else { return null } } return that.toValue(that._calculateUnProjection((pos - startPoint - commonBreakSize) / canvasOptions.ratioOfCanvasRange + prop.length)) }, isValueProlonged: false, getRange: function() { return [this.toValue(this._canvasOptions.rangeMin), this.toValue(this._canvasOptions.rangeMax)] }, getScreenRange: function() { return [this._canvasOptions.startPoint, this._canvasOptions.endPoint] }, add: function(value, diff, dir) { return this._add(value, diff, (this._businessRange.invert ? -1 : 1) * dir) }, _add: function(value, diff, coeff) { return this.toValue(this.fromValue(value) + diff * coeff) }, fromValue: function(value) { return null !== value ? Number(value) : null }, toValue: function(value) { return null !== value ? Number(value) : null }, ratioOfCanvasRange() { return this._canvasOptions.ratioOfCanvasRange }, convert: value => value, getRangeByMinZoomValue(minZoom, visualRange) { if (visualRange.minVisible + minZoom <= this._businessRange.max) { return [visualRange.minVisible, visualRange.minVisible + minZoom] } else { return [visualRange.maxVisible - minZoom, visualRange.maxVisible] } } }; export { _Translator2d as Translator2D };