UNPKG

@progress/kendo-charts

Version:

Kendo UI platform-independent Charts library

452 lines (354 loc) 14.1 kB
import { geometry as geo, drawing } from '@progress/kendo-drawing'; import { setDefaultOptions, deepExtend, defined, round, getSpacing } from '../../common'; import { BLACK, COORD_PRECISION } from '../../common/constants'; import { autoMajorUnit, Box, NumericAxis } from '../../core'; import { buildLabelElement, getRange } from '../utils'; import { DEGREE, DEFAULT_LINE_WIDTH, INSIDE, OUTSIDE } from '../constants'; const GEO_ARC_ADJUST_ANGLE = 180; const { Arc, Path, Group } = drawing; function drawTicks(arc, tickAngles, unit, tickOptions) { const ticks = new Group(); const center = arc.center; const radius = arc.getRadiusX(); if (tickOptions.visible) { for (let i = 0; i < tickAngles.length; i++) { const tickStart = arc.pointAt(tickAngles[i]); const tickEnd = new geo.Point(center.x + radius - tickOptions.size, center.y).rotate(tickAngles[i], center); ticks.append(new Path({ stroke: { color: tickOptions.color, width: tickOptions.width } }).moveTo(tickStart).lineTo(tickEnd)); } } return ticks; } function rangeSegment(from, to, color, opacity) { return { from: from, to: to, color: color, opacity: opacity }; } class RadialScale extends NumericAxis { constructor(options, service) { super(0, 1, options, service); } initUserOptions(options) { const scaleOptions = deepExtend({}, this.options, options); scaleOptions.majorUnit = scaleOptions.majorUnit || autoMajorUnit(scaleOptions.min, scaleOptions.max); scaleOptions.minorUnit = scaleOptions.minorUnit || scaleOptions.majorUnit / 10; return scaleOptions; } initFields() { } render(center, radius) { const arc = this.renderArc(center, radius); this.bbox = arc.bbox(); this.labelElements = this.renderLabels(); this.ticks = this.renderTicks(); this.ranges = this.renderRanges(); } reflow(bbox) { const center = bbox.center(); const radius = Math.min(bbox.height(), bbox.width()) / 2; if (defined(this.bbox)) { this.bbox = this.arc.bbox(); this.radius(this.arc.getRadiusX()); this.repositionRanges(); this.renderLabels(); } else { return this.render(center, radius); } } slotAngle(value) { const { min, max, reverse, startAngle, endAngle } = this.options; const angle = endAngle - startAngle; let result; if (reverse) { result = endAngle - (value - min) / (max - min) * angle; } else { result = ((value - min) / (max - min) * angle) + startAngle; } return result + GEO_ARC_ADJUST_ANGLE; } hasRanges() { const ranges = this.options.ranges; return ranges && ranges.length; } ticksSize() { const { majorTicks, minorTicks } = this.options; let size = 0; if (majorTicks.visible) { size = majorTicks.size; } if (minorTicks.visible) { size = Math.max(minorTicks.size, size); } return size; } labelsCount() { let count = super.labelsCount(); const options = this.options; const angle = options.endAngle - options.startAngle; if (angle >= 360 && (options.max % options.majorUnit === 0)) { count -= 1; } return count; } renderLabels() { const options = this.options; const arc = this.arc.clone(); let radius = arc.getRadiusX(); const tickAngles = this.tickAngles(arc, options.majorUnit); const rangeSize = options.rangeSize = options.rangeSize || radius * 0.1; const labelsGroup = new Group(); let rangeDistance = radius * 0.05; if (defined(options.rangeDistance)) { rangeDistance = options.rangeDistance; } else { options.rangeDistance = rangeDistance; } const labelsOptions = options.labels; const isInside = labelsOptions.position === INSIDE; const hasLabelElements = defined(this.labelElements); if (isInside) { radius -= this.ticksSize(); if (this.hasRanges() && !hasLabelElements) { radius -= rangeSize + rangeDistance; } arc.setRadiusX(radius).setRadiusY(radius); } const labels = this.labels; const count = labels.length; const padding = getSpacing(labelsOptions.padding); const paddingWidth = (padding.left + padding.right) / 2; const paddingHeight = (padding.top + padding.bottom) / 2; for (let i = 0; i < count; i++) { const label = labels[i]; const halfWidth = label.box.width() / 2; const halfHeight = label.box.height() / 2; const angle = tickAngles[i]; const labelAngle = (angle - GEO_ARC_ADJUST_ANGLE) * DEGREE; const lp = arc.pointAt(angle); const cx = lp.x + (Math.cos(labelAngle) * (halfWidth + paddingWidth) * (isInside ? 1 : -1)); const cy = lp.y + (Math.sin(labelAngle) * (halfHeight + paddingHeight) * (isInside ? 1 : -1)); label.reflow(new Box(cx - halfWidth, cy - halfHeight, cx + halfWidth, cy + halfHeight)); const labelPos = new geo.Point(label.box.x1, label.box.y1); let labelElement; if (!hasLabelElements) { labelElement = buildLabelElement(label, options.labels); labelsGroup.append(labelElement); } else { labelElement = this.labelElements.children[i]; const prevLabelPos = labelElement.bbox().origin; const labelTransform = labelElement.transform() || geo.transform(); labelTransform.translate(labelPos.x - prevLabelPos.x, labelPos.y - prevLabelPos.y); labelElement.transform(labelTransform); } this.bbox = geo.Rect.union(this.bbox, labelElement.bbox()); } return labelsGroup; } repositionRanges() { const ranges = this.ranges.children; if (ranges.length > 0) { const { rangeDistance, rangeSize } = this.options; let rangeRadius = this.getRangeRadius(); if (this.options.labels.position === INSIDE) { rangeRadius += rangeSize + rangeDistance; } const newRadius = rangeRadius + (rangeSize / 2); for (let i = 0; i < ranges.length; i++) { ranges[i]._geometry.setRadiusX(newRadius).setRadiusY(newRadius); } this.bbox = geo.Rect.union(this.bbox, this.ranges.bbox()); } } renderRanges() { const segments = this.rangeSegments(); const segmentsCount = segments.length; const result = new Group(); if (segmentsCount) { const { rangeSize, reverse, rangeDistance } = this.options; const rangeRadius = this.getRangeRadius(); // move the ticks with a range distance and a range size this.radius(this.radius() - rangeSize - rangeDistance); for (let i = 0; i < segmentsCount; i++) { const segment = segments[i]; const from = this.slotAngle(segment[reverse ? "to" : "from"]); const to = this.slotAngle(segment[!reverse ? "to" : "from"]); if (to - from !== 0) { result.append(this.createRange(from, to, rangeRadius, segment)); } } } return result; } createRange(startAngle, endAngle, rangeRadius, options) { const rangeSize = this.options.rangeSize; const rangeGeom = new geo.Arc(this.arc.center, { radiusX: rangeRadius + (rangeSize / 2), radiusY: rangeRadius + (rangeSize / 2), startAngle: startAngle, endAngle: endAngle }); return new Arc(rangeGeom, { stroke: { width: rangeSize, color: options.color, opacity: options.opacity, lineCap: options.lineCap } }); } rangeSegments() { const options = this.options; const ranges = options.ranges || []; const count = ranges.length; const segments = []; if (count) { const { min, max, rangePlaceholderColor: defaultColor } = options; segments.push(rangeSegment(min, max, defaultColor)); for (let i = 0; i < count; i++) { const range = getRange(ranges[i], min, max); const segmentsCount = segments.length; for (let j = 0; j < segmentsCount; j++) { const segment = segments[j]; if (segment.from <= range.from && range.from <= segment.to) { segments.push(rangeSegment(range.from, range.to, range.color, range.opacity)); if (segment.from <= range.to && range.to <= segment.to) { segments.push(rangeSegment(range.to, segment.to, defaultColor, range.opacity)); } segment.to = range.from; break; } } } } return segments; } getRangeRadius() { const { arc, options } = this; const { rangeSize, rangeDistance, majorTicks: { size: majorTickSize } } = options; let radius; if (options.labels.position === OUTSIDE) { radius = arc.getRadiusX() - majorTickSize - rangeDistance - rangeSize; } else { radius = arc.getRadiusX() - rangeSize; } return radius; } renderArc(center, radius) { const options = this.options; const arc = this.arc = new geo.Arc(center, { radiusX: radius, radiusY: radius, startAngle: options.startAngle + GEO_ARC_ADJUST_ANGLE, endAngle: options.endAngle + GEO_ARC_ADJUST_ANGLE }); return arc; } renderTicks() { const { arc, options } = this; const tickArc = arc.clone(); this.majorTickAngles = this.tickAngles(arc, options.majorUnit); this.majorTicks = drawTicks(tickArc, this.majorTickAngles, options.majorUnit, options.majorTicks); const allTicks = new Group(); allTicks.append(this.majorTicks); const majorTickSize = options.majorTicks.size; const minorTickSize = options.minorTicks.size; this._tickDifference = majorTickSize - minorTickSize; if (options.labels.position === OUTSIDE) { const radius = tickArc.getRadiusX(); tickArc.setRadiusX(radius - majorTickSize + minorTickSize) .setRadiusY(radius - majorTickSize + minorTickSize); } this.minorTickAngles = this.normalizeTickAngles(this.tickAngles(arc, options.minorUnit)); this.minorTicks = drawTicks(tickArc, this.minorTickAngles, options.minorUnit, options.minorTicks); allTicks.append(this.minorTicks); return allTicks; } normalizeTickAngles(angles) { const options = this.options; const skip = options.majorUnit / options.minorUnit; for (let i = angles.length - 1; i >= 0; i--) { if (i % skip === 0) { angles.splice(i, 1); } } return angles; } tickAngles(ring, stepValue) { const options = this.options; const reverse = options.reverse; const range = options.max - options.min; const angle = ring.endAngle - ring.startAngle; let tickCount = range / stepValue; let pos = ring.startAngle; let step = angle / tickCount; if (reverse) { pos += angle; step = -step; } if (angle >= 360 && (options.max % stepValue === 0)) { tickCount -= 1; } const positions = []; for (let i = 0; i < tickCount; i++) { positions.push(round(pos, COORD_PRECISION)); pos += step; } if (round(pos) <= ring.endAngle) { positions.push(pos); } return positions; } radius(value) { if (value) { this.arc.setRadiusX(value).setRadiusY(value); this.repositionTicks(this.majorTicks.children, this.majorTickAngles); this.repositionTicks(this.minorTicks.children, this.minorTickAngles, true); } else { return this.arc.getRadiusX(); } } repositionTicks(ticks, tickAngles, minor) { const diff = minor ? (this._tickDifference || 0) : 0; let tickArc = this.arc; const radius = tickArc.getRadiusX(); if (minor && this.options.labels.position === OUTSIDE && diff !== 0) { tickArc = this.arc.clone(); tickArc.setRadiusX(radius - diff).setRadiusY(radius - diff); } for (let i = 0; i < ticks.length; i++) { const newPoint = tickArc.pointAt(tickAngles[i]); const segments = ticks[i].segments; const xDiff = newPoint.x - segments[0].anchor().x; const yDiff = newPoint.y - segments[0].anchor().y; ticks[i].transform(new geo.transform().translate(xDiff, yDiff)); } } } setDefaultOptions(RadialScale, { min: 0, max: 100, majorTicks: { size: 15, align: INSIDE, color: BLACK, width: DEFAULT_LINE_WIDTH, visible: true }, minorTicks: { size: 10, align: INSIDE, color: BLACK, width: DEFAULT_LINE_WIDTH, visible: true }, startAngle: -30, endAngle: 210, labels: { position: INSIDE, padding: 2 } }); export default RadialScale;