UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

816 lines (811 loc) • 30.6 kB
/** * DevExtreme (esm/viz/gauges/bar_gauge.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/ */ const PI_DIV_180 = Math.PI / 180; const _abs = Math.abs; const _round = Math.round; const _floor = Math.floor; const _min = Math.min; const _max = Math.max; import registerComponent from "../../core/component_registrator"; import { clone } from "../../core/utils/object"; import { noop } from "../../core/utils/common"; import { overlapping } from "../../__internal/viz/chart_components/m_base_chart"; import { extend } from "../../core/utils/extend"; import { normalizeEnum as _normalizeEnum, convertAngleToRendererSpace, getCosAndSin, patchFontOptions, getVerticallyShiftedAngularCoords, normalizeArcParams, normalizeAngle } from "../core/utils"; import { BaseGauge, getSampleText, formatValue, compareArrays } from "./base_gauge"; import dxCircularGauge from "./circular_gauge"; import { plugin as pluginLegend } from "../components/legend"; import { plugins as centerTemplatePlugins } from "../core/center_template"; import { roundFloatPart } from "../../core/utils/math"; const _getSampleText = getSampleText; const _formatValue = formatValue; const _compareArrays = compareArrays; const _isArray = Array.isArray; const _convertAngleToRendererSpace = convertAngleToRendererSpace; const _getCosAndSin = getCosAndSin; const _patchFontOptions = patchFontOptions; const _Number = Number; const _isFinite = isFinite; const _noop = noop; const _extend = extend; const ARC_COORD_PREC = 5; const OPTION_VALUES = "values"; let BarWrapper; export const dxBarGauge = BaseGauge.inherit({ _rootClass: "dxbg-bar-gauge", _themeSection: "barGauge", _fontFields: ["label.font", "legend.font", "legend.title.font", "legend.title.subtitle.font"], _initCore: function() { const that = this; that.callBase.apply(that, arguments); that._barsGroup = that._renderer.g().attr({ class: "dxbg-bars" }).linkOn(that._renderer.root, "bars"); that._values = []; that._context = { renderer: that._renderer, translator: that._translator, tracker: that._tracker, group: that._barsGroup }; that._animateStep = function(pos) { const bars = that._bars; let i; let ii; for (i = 0, ii = bars.length; i < ii; ++i) { bars[i].animate(pos) } }; that._animateComplete = function() { that._bars.forEach((bar => bar.endAnimation())); that._checkOverlap() } }, _disposeCore: function() { this._barsGroup.linkOff(); this._barsGroup = this._values = this._context = this._animateStep = this._animateComplete = null; this.callBase.apply(this, arguments) }, _setupDomainCore: function() { let startValue = this.option("startValue"); let endValue = this.option("endValue"); _isFinite(startValue) || (startValue = 0); _isFinite(endValue) || (endValue = 100); this._translator.setDomain(startValue, endValue); this._baseValue = this._translator.adjust(this.option("baseValue")); _isFinite(this._baseValue) || (this._baseValue = startValue < endValue ? startValue : endValue) }, _getDefaultSize: function() { return { width: 300, height: 300 } }, _setupCodomain: dxCircularGauge.prototype._setupCodomain, _getApproximateScreenRange: function() { const sides = this._area.sides; const width = this._canvas.width / (sides.right - sides.left); const height = this._canvas.height / (sides.down - sides.up); const r = width < height ? width : height; return -this._translator.getCodomainRange() * r * PI_DIV_180 }, _setupAnimationSettings: function() { const that = this; that.callBase.apply(that, arguments); if (that._animationSettings) { that._animationSettings.step = that._animateStep; that._animationSettings.complete = that._animateComplete } }, _cleanContent: function() { this._barsGroup.linkRemove(); this._animationSettings && this._barsGroup.stopAnimation(); this._barsGroup.clear() }, _renderContent: function() { const that = this; let labelOptions = that.option("label"); const context = that._context; that._barsGroup.linkAppend(); context.textEnabled = void 0 === labelOptions || labelOptions && (!("visible" in labelOptions) || labelOptions.visible); if (context.textEnabled) { var _labelOptions, _labelOptions2; context.fontStyles = _patchFontOptions(_extend({}, that._themeManager.theme().label.font, null === (_labelOptions = labelOptions) || void 0 === _labelOptions ? void 0 : _labelOptions.font, { color: (null === (_labelOptions2 = labelOptions) || void 0 === _labelOptions2 || null === (_labelOptions2 = _labelOptions2.font) || void 0 === _labelOptions2 ? void 0 : _labelOptions2.color) || null })); labelOptions = _extend(true, {}, that._themeManager.theme().label, labelOptions); context.formatOptions = { format: void 0 !== labelOptions.format ? labelOptions.format : that._defaultFormatOptions, customizeText: labelOptions.customizeText }; context.textOptions = { align: "center" }; that._textIndent = labelOptions.indent > 0 ? _Number(labelOptions.indent) : 0; context.lineWidth = labelOptions.connectorWidth > 0 ? _Number(labelOptions.connectorWidth) : 0; context.lineColor = labelOptions.connectorColor || null; const text = that._renderer.text(_getSampleText(that._translator, context.formatOptions), 0, 0).attr(context.textOptions).css(context.fontStyles).append(that._barsGroup); const bBox = text.getBBox(); text.remove(); context.textY = bBox.y; context.textWidth = bBox.width; context.textHeight = bBox.height } dxCircularGauge.prototype._applyMainLayout.call(that); that._renderBars() }, _measureMainElements: function() { const result = { maxRadius: this._area.radius }; if (this._context.textEnabled) { result.horizontalMargin = this._context.textWidth; result.verticalMargin = this._context.textHeight; result.inverseHorizontalMargin = this._context.textWidth / 2; result.inverseVerticalMargin = this._context.textHeight / 2 } return result }, _renderBars: function() { const that = this; const options = _extend({}, that._themeManager.theme(), that.option()); let radius; const area = that._area; const relativeInnerRadius = options.relativeInnerRadius > 0 && options.relativeInnerRadius < 1 ? _Number(options.relativeInnerRadius) : .1; radius = area.radius; if (that._context.textEnabled) { that._textIndent = _round(_min(that._textIndent, radius / 2)); radius -= that._textIndent } that._outerRadius = _floor(radius); that._innerRadius = _floor(radius * relativeInnerRadius); that._barSpacing = options.barSpacing > 0 ? _Number(options.barSpacing) : 0; _extend(that._context, { backgroundColor: options.backgroundColor, x: area.x, y: area.y, startAngle: area.startCoord, endAngle: area.endCoord, baseAngle: that._translator.translate(that._baseValue) }); that._arrangeBars() }, _arrangeBars: function() { const that = this; let radius = that._outerRadius - that._innerRadius; const context = that._context; let i; const count = that._bars.length; that._beginValueChanging(); context.barSize = count > 0 ? _max((radius - (count - 1) * that._barSpacing) / count, 1) : 0; const spacing = count > 1 ? _max(_min((radius - count * context.barSize) / (count - 1), that._barSpacing), 0) : 0; const _count = _min(_floor((radius + spacing) / context.barSize), count); that._setBarsCount(count); radius = that._outerRadius; context.textRadius = radius; context.textIndent = that._textIndent; that._palette.reset(); const unitOffset = context.barSize + spacing; const colors = that._palette.generateColors(_count); for (i = 0; i < _count; ++i, radius -= unitOffset) { that._bars[i].arrange({ radius: radius, color: colors[i] }) } for (let i = _count; i < count; i++) { that._bars[i].hide() } if (that._animationSettings && !that._noAnimation) { that._animateBars() } else { that._updateBars() } that._endValueChanging() }, _setBarsCount: function() { const that = this; if (that._bars.length > 0) { if (that._dummyBackground) { that._dummyBackground.dispose(); that._dummyBackground = null } } else { if (!that._dummyBackground) { that._dummyBackground = that._renderer.arc().attr({ "stroke-linejoin": "round" }) } that._dummyBackground.attr({ x: that._context.x, y: that._context.y, outerRadius: that._outerRadius, innerRadius: that._innerRadius, startAngle: that._context.endAngle, endAngle: that._context.startAngle, fill: that._context.backgroundColor }).append(that._barsGroup) } }, _getCenter: function() { return { x: this._context.x, y: this._context.y } }, _updateBars: function() { this._bars.forEach((bar => bar.applyValue())); this._checkOverlap() }, _checkOverlap: function() { const that = this; const overlapStrategy = _normalizeEnum(that._getOption("resolveLabelOverlapping", true)); function shiftFunction(box, length) { return getVerticallyShiftedAngularCoords(box, -length, that._context) } if ("none" === overlapStrategy) { return } if ("shift" === overlapStrategy) { const newBars = that._dividePoints(); overlapping.resolveLabelOverlappingInOneDirection(newBars.left, that._canvas, false, false, shiftFunction); overlapping.resolveLabelOverlappingInOneDirection(newBars.right, that._canvas, false, false, shiftFunction); that._clearLabelsCrossTitle(); that._drawConnector() } else { that._clearOverlappingLabels() } }, _drawConnector() { const that = this; const bars = that._bars; const { connectorWidth: connectorWidth } = that._getOption("label"); bars.forEach((bar => { if (!bar._isLabelShifted) { return } const x = bar._bar.attr("x"); const y = bar._bar.attr("y"); const innerRadius = bar._bar.attr("innerRadius"); const outerRadius = bar._bar.attr("outerRadius"); const startAngle = bar._bar.attr("startAngle"); const endAngle = bar._bar.attr("endAngle"); const coordStart = getStartCoordsArc.apply(null, normalizeArcParams(x, y, innerRadius, outerRadius, startAngle, endAngle)); const { cos: cos, sin: sin } = _getCosAndSin(bar._angle); const xStart = coordStart.x - sin * connectorWidth / 2 - cos; const yStart = coordStart.y - cos * connectorWidth / 2 + sin; const box = bar._text.getBBox(); const lastCoords = bar._text._lastCoords; const indentFromLabel = that._context.textWidth / 2; const originalXLabelCoord = box.x + box.width / 2 + lastCoords.x; const originalPoints = [xStart, yStart, originalXLabelCoord, box.y + lastCoords.y]; if (bar._angle > 90) { originalPoints[2] += indentFromLabel } else { originalPoints[2] -= indentFromLabel } if (bar._angle <= 180 && bar._angle > 0) { originalPoints[3] += box.height } if (connectorWidth % 2) { const xDeviation = -sin / 2; const yDeviation = -cos / 2; if (bar._angle > 180) { originalPoints[0] -= xDeviation; originalPoints[1] -= yDeviation } else if (bar._angle > 0 && bar._angle <= 90) { originalPoints[0] += xDeviation; originalPoints[1] += yDeviation } } const points = originalPoints.map((coordinate => roundFloatPart(coordinate, 4))); bar._line.attr({ points: points }); bar._line.rotate(0); bar._isLabelShifted = false })) }, _dividePoints() { const bars = this._bars; return bars.reduce((function(stackBars, bar) { const angle = normalizeAngle(bar._angle); const isRightSide = angle <= 90 || angle >= 270; bar._text._lastCoords = { x: 0, y: 0 }; const barToExtend = isRightSide ? stackBars.right : stackBars.left; barToExtend.push({ series: { isStackedSeries: () => false, isFullStackedSeries: () => false }, getLabels: () => [{ isVisible: () => true, getBoundingRect: () => { const { height: height, width: width, x: x, y: y } = bar._text.getBBox(); const lastCoords = bar._text._lastCoords; return { x: x + lastCoords.x, y: y + lastCoords.y, width: width, height: height } }, shift: (x, y) => { const box = bar._text.getBBox(); bar._text._lastCoords = { x: x - box.x, y: y - box.y }; bar._text.attr({ translateX: x - box.x, translateY: y - box.y }); bar._isLabelShifted = true }, draw: () => bar.hideLabel(), getData: () => ({ value: bar.getValue() }), hideInsideLabel: () => false }] }); return stackBars }), { left: [], right: [] }) }, _clearOverlappingLabels() { const bars = this._bars; let currentIndex = 0; let nextIndex = 1; const sortedBars = bars.concat().sort(((a, b) => a.getValue() - b.getValue())); while (currentIndex < sortedBars.length && nextIndex < sortedBars.length) { const current = sortedBars[currentIndex]; const next = sortedBars[nextIndex]; if (current.checkIntersect(next)) { next.hideLabel(); nextIndex++ } else { currentIndex = nextIndex; nextIndex = currentIndex + 1 } } }, _clearLabelsCrossTitle() { const bars = this._bars; const titleCoords = this._title.getLayoutOptions() || { x: 0, y: 0, height: 0, width: 0 }; const minY = titleCoords.y + titleCoords.height; bars.forEach((bar => { const box = bar._text.getBBox(); const lastCoords = bar._text._lastCoords; if (minY > box.y + lastCoords.y) { bar.hideLabel() } })) }, _animateBars: function() { const that = this; let i; const ii = that._bars.length; if (ii > 0) { for (i = 0; i < ii; ++i) { that._bars[i].beginAnimation() } that._barsGroup.animate({ _: 0 }, that._animationSettings) } }, _buildNodes() { const that = this; const options = that._options.silent(); that._palette = that._themeManager.createPalette(options.palette, { useHighlight: true, extensionMode: options.paletteExtensionMode }); that._palette.reset(); that._bars = that._bars || []; that._animationSettings && that._barsGroup.stopAnimation(); const barValues = that._values.filter(_isFinite); const count = barValues.length; if (that._bars.length > count) { const ii = that._bars.length; for (let i = count; i < ii; ++i) { that._bars[i].dispose() } that._bars.splice(count, ii - count) } else if (that._bars.length < count) { for (let i = that._bars.length; i < count; ++i) { that._bars.push(new BarWrapper(i, that._context)) } } that._bars.forEach(((bar, index) => { bar.update({ color: that._palette.getNextColor(count), value: barValues[index] }) })) }, _updateValues: function(values) { const that = this; const list = _isArray(values) && values || _isFinite(values) && [values] || []; let i; const ii = list.length; let value; that._values.length = ii; for (i = 0; i < ii; ++i) { value = list[i]; that._values[i] = _Number(_isFinite(value) ? value : that._values[i]) } if (!that._resizing) { if (!_compareArrays(that._values, that.option("values"))) { that.option("values", that._values.slice()) } } this._change(["NODES"]) }, values: function(arg) { if (void 0 !== arg) { this._updateValues(arg); return this } else { return this._values.slice(0) } }, _optionChangesMap: { backgroundColor: "MOSTLY_TOTAL", relativeInnerRadius: "MOSTLY_TOTAL", barSpacing: "MOSTLY_TOTAL", label: "MOSTLY_TOTAL", resolveLabelOverlapping: "MOSTLY_TOTAL", palette: "MOSTLY_TOTAL", paletteExtensionMode: "MOSTLY_TOTAL", values: "VALUES" }, _change_VALUES: function() { this._updateValues(this.option("values")) }, _factory: clone(BaseGauge.prototype._factory), _optionChangesOrder: ["VALUES", "NODES"], _initialChanges: ["VALUES"], _change_NODES() { this._buildNodes() }, _change_MOSTLY_TOTAL: function() { this._change(["NODES"]); this.callBase() }, _proxyData: [], _getLegendData() { const that = this; const formatOptions = {}; const options = that._options.silent(); const labelFormatOptions = (options.label || {}).format; const legendFormatOptions = (options.legend || {}).itemTextFormat; if (legendFormatOptions) { formatOptions.format = legendFormatOptions } else { formatOptions.format = labelFormatOptions || that._defaultFormatOptions } return (this._bars || []).map((b => ({ id: b.index, item: { value: b.getValue(), color: b.getColor(), index: b.index }, text: _formatValue(b.getValue(), formatOptions), visible: true, states: { normal: { fill: b.getColor() } } }))) } }); BarWrapper = function(index, context) { this._context = context; this._tracker = context.renderer.arc().attr({ "stroke-linejoin": "round" }); this.index = index }; _extend(BarWrapper.prototype, { dispose: function() { const that = this; that._background.dispose(); that._bar.dispose(); if (that._context.textEnabled) { that._line.dispose(); that._text.dispose() } that._context.tracker.detach(that._tracker); that._context = that._settings = that._background = that._bar = that._line = that._text = that._tracker = null; return that }, arrange: function(options) { const that = this; const context = that._context; this._visible = true; context.tracker.attach(that._tracker, that, { index: that.index }); that._background = context.renderer.arc().attr({ "stroke-linejoin": "round", fill: context.backgroundColor }).append(context.group); that._settings = that._settings || { x: context.x, y: context.y, startAngle: context.baseAngle, endAngle: context.baseAngle }; that._bar = context.renderer.arc().attr(_extend({ "stroke-linejoin": "round" }, that._settings)).append(context.group); if (context.textEnabled) { that._line = context.renderer.path([], "line").attr({ "stroke-width": context.lineWidth }).append(context.group); that._text = context.renderer.text().css(context.fontStyles).attr(context.textOptions).append(context.group) } that._angle = isFinite(that._angle) ? that._angle : context.baseAngle; that._settings.outerRadius = options.radius; that._settings.innerRadius = options.radius - context.barSize; that._settings.x = context.x; that._settings.y = context.y; that._background.attr(_extend({}, that._settings, { startAngle: context.endAngle, endAngle: context.startAngle, fill: that._context.backgroundColor })); that._bar.attr({ x: context.x, y: context.y, outerRadius: that._settings.outerRadius, innerRadius: that._settings.innerRadius, fill: that._color }); that._tracker.attr(that._settings); if (context.textEnabled) { that._line.attr({ points: [context.x, context.y - that._settings.innerRadius, context.x, context.y - context.textRadius - context.textIndent], stroke: context.lineColor || that._color }).sharp(); that._text.css({ fill: context.fontStyles.fill || that._color }) } return that }, getTooltipParameters: function() { const cosSin = _getCosAndSin((this._angle + this._context.baseAngle) / 2); return { x: _round(this._context.x + (this._settings.outerRadius + this._settings.innerRadius) / 2 * cosSin.cos), y: _round(this._context.y - (this._settings.outerRadius + this._settings.innerRadius) / 2 * cosSin.sin), offset: 0, color: this._color, value: this._value } }, setAngle: function(angle) { const that = this; const context = that._context; const settings = that._settings; let cosSin; that._angle = angle; setAngles(settings, context.baseAngle, angle); that._bar.attr(settings); that._tracker.attr(settings); if (context.textEnabled) { cosSin = _getCosAndSin(angle); const indent = context.textIndent; const radius = context.textRadius + indent; let x = context.x + radius * cosSin.cos; let y = context.y - radius * cosSin.sin; const halfWidth = .5 * context.textWidth; const textHeight = context.textHeight; const textY = context.textY; if (_abs(x - context.x) > indent) { x += x < context.x ? -halfWidth : halfWidth } if (_abs(y - context.y) <= indent) { y -= textY + .5 * textHeight } else { y -= y < context.y ? textY + textHeight : textY } const text = _formatValue(that._value, context.formatOptions, { index: that.index }); const visibility = "" === text ? "hidden" : null; that._text.attr({ text: text, x: x, y: y, visibility: visibility }); that._line.attr({ visibility: visibility }); that._line.rotate(_convertAngleToRendererSpace(angle), context.x, context.y) } return that }, hideLabel: function() { this._text.attr({ visibility: "hidden" }); this._line.attr({ visibility: "hidden" }) }, checkIntersect: function(anotherBar) { const coords = this.calculateLabelCoords(); const anotherCoords = anotherBar.calculateLabelCoords(); if (!coords || !anotherCoords) { return false } const width = Math.max(0, Math.min(coords.bottomRight.x, anotherCoords.bottomRight.x) - Math.max(coords.topLeft.x, anotherCoords.topLeft.x)); const height = Math.max(0, Math.min(coords.bottomRight.y, anotherCoords.bottomRight.y) - Math.max(coords.topLeft.y, anotherCoords.topLeft.y)); return width * height !== 0 }, calculateLabelCoords: function() { if (!this._text) { return } const box = this._text.getBBox(); return { topLeft: { x: box.x, y: box.y }, bottomRight: { x: box.x + box.width, y: box.y + box.height } } }, _processValue: function(value) { return this._context.translator.translate(this._context.translator.adjust(value)) }, applyValue() { if (!this._visible) { return this } return this.setAngle(this._processValue(this.getValue())) }, update(_ref) { let { color: color, value: value } = _ref; this._color = color; this._value = value }, hide() { this._visible = false }, getColor() { return this._color }, getValue() { return this._value }, beginAnimation: function() { if (!this._visible) { return this } const that = this; const angle = this._processValue(this.getValue()); if (!compareFloats(that._angle, angle)) { that._start = that._angle; that._delta = angle - that._angle; that._tracker.attr({ visibility: "hidden" }); if (that._context.textEnabled) { that._line.attr({ visibility: "hidden" }); that._text.attr({ visibility: "hidden" }) } } else { that.animate = _noop; that.setAngle(that._angle) } }, animate: function(pos) { if (!this._visible) { return this } this._angle = this._start + this._delta * pos; setAngles(this._settings, this._context.baseAngle, this._angle); this._bar.attr(this._settings) }, endAnimation: function() { const that = this; if (void 0 !== that._delta) { if (compareFloats(that._angle, that._start + that._delta)) { that._tracker.attr({ visibility: null }); that.setAngle(that._angle) } } else { delete that.animate } delete that._start; delete that._delta } }); function setAngles(target, angle1, angle2) { target.startAngle = angle1 < angle2 ? angle1 : angle2; target.endAngle = angle1 < angle2 ? angle2 : angle1 } function compareFloats(value1, value2) { return _abs(value1 - value2) < 1e-4 } function getStartCoordsArc(x, y, innerR, outerR, startAngleCos, startAngleSin) { return { x: (x + outerR * startAngleCos).toFixed(5), y: (y - outerR * startAngleSin).toFixed(5) } } registerComponent("dxBarGauge", dxBarGauge); dxBarGauge.addPlugin(pluginLegend); dxBarGauge.addPlugin(centerTemplatePlugins.gauge);