/* * Copyright (C) 2019 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ // GaugeChart creates a semi-circle gauge chart with colored segments. // // Initialize the chart with a semi-circle height, stroke width, and segments. // The class names you provide for the segments will allow you to style them // and the limit (0 - 100) is the upper percentage value where that segment // ends. You can update the chart with new current needle value at any time. // // SVG: // // - There is a path for each segment. Note there is a small includes a // buffer between segments, so they should be more than a few # apart. // - There is a single polygon for the needle value. // //
// // // // ... // // //
WI.GaugeChart = class GaugeChart extends WI.View { constructor({height, strokeWidth, segments}) { super(); strokeWidth = strokeWidth || 10; this._needleValue = null; const needleOverhangSpace = 10; // Distance the needle goes past the outer circle edge. const needleUnderhangSpace = 8; // Space allowed beneath the graph so a horizontal needle lines up. this._center = height - needleUnderhangSpace; this._radius = height - needleUnderhangSpace - needleOverhangSpace - 1; this._innerRadius = Math.floor(this._radius - strokeWidth); let width = (this._radius + needleOverhangSpace + 1) * 2; this._size = new WI.Size(width, height); console.assert(!this._segments, "Set segments only once"); console.assert(segments.length >= 1, "Need at least one segment"); console.assert(this._validateSegments(segments)); this._segments = segments; this.element.classList.add("gauge-chart"); this._chartElement = this.element.appendChild(createSVGElement("svg")); this._chartElement.setAttribute("width", width); this._chartElement.setAttribute("height", height); this._chartElement.setAttribute("viewBox", `0 0 ${width} ${height}`); this._needleElement = null; } // Public get size() { return this._size; } get segments() { return this._segments; } get value() { return this._needleValue; } set value(value) { console.assert(value >= 0 && value <= 100, "value should be between 0 and 100.", value); this._needleValue = value; } clear() { this._needleValue = null; } // Protected initialLayout() { super.initialLayout(); let startAngle = Math.PI; const onePercentAngle = Math.PI / 100; for (let {className, limit} of this._segments) { let offset = limit === 100 ? 0 : 1; let endAngle = Math.PI + (((limit - offset) / 100) * Math.PI); let pathElement = this._chartElement.appendChild(createSVGElement("path")); pathElement.classList.add("segment", className); pathElement.setAttribute("d", this._createSegmentPathData(this._center, startAngle, endAngle, this._radius, this._innerRadius)); startAngle = endAngle + onePercentAngle; } const needlePointExtraDraw = 0.5; // Draw a fat tip to the needle. const needleBaseExtraDraw = 4.5; // Draw a fat base to the needle. const needleUnderhangDraw = 6; // Draw the needle underhanging the base of the graph. let midX = this.size.width / 2; let midY = this._center; this._needleElement = this._chartElement.appendChild(createSVGElement("polygon")); this._needleElement.classList.add("needle"); this._needleElement.setAttribute("points", `0,${midY + needlePointExtraDraw}, 0,${midY - needlePointExtraDraw} ${midX + needleUnderhangDraw},${midY - needleBaseExtraDraw} ${midX + needleUnderhangDraw},${midY + needleBaseExtraDraw}`); this._needleElement.style.transformOrigin = `${midX}px ${midY}px`; } layout() { super.layout(); if (this.layoutReason === WI.View.LayoutReason.Resize) return; let empty = this._needleValue === null; this.element.classList.toggle("empty", empty); let value = empty ? 0 : this._needleValue; let degrees = 180 * (value / 100); // 0-100% mapped to 0-180deg. this._needleElement.style.transform = `rotate(${degrees}deg)`; } // Private _validateSegments(segments) { let lastLimit = -1; for (let {className, limit} of segments) { console.assert(limit >= 1 && limit <= 100, "limit should be between 1 and 100", limit); console.assert(limit >= (lastLimit + 1), "limits should always increase between segments"); lastLimit = limit; } return true; } _createSegmentPathData(c, a1, a2, r1, r2) { const startIndicatorUnderhang = 7; let r3 = (r2 - startIndicatorUnderhang); let onePercentArc = Math.PI / 100; let largeArcFlag = ((a2 - a1) % (Math.PI * 2)) > Math.PI ? 1 : 0; let x1 = c + Math.cos(a1) * r1, y1 = c + Math.sin(a1) * r1, x2 = c + Math.cos(a2) * r1, y2 = c + Math.sin(a2) * r1, x3 = c + Math.cos(a2) * r2, y3 = c + Math.sin(a2) * r2, x4 = c + Math.cos(a1 + onePercentArc) * r2, y4 = c + Math.sin(a1 + onePercentArc) * r2, x5 = c + Math.cos(a1 + onePercentArc) * r3, y5 = c + Math.sin(a1 + onePercentArc) * r3, x6 = c + Math.cos(a1) * r3, y6 = c + Math.sin(a1) * r3; return [ "M", x1, y1, // Starting position. "A", r1, r1, 0, largeArcFlag, 1, x2, y2, // Draw outer arc. "L", x3, y3, // Connect outer and inner arcs. "A", r2, r2, 0, largeArcFlag, 0, x4, y4, // Draw inner arc. "L", x5, y5, // Extend inner arc to center for start indicator. "A", r3, r3, 0, largeArcFlag, 0, x6, y6, // Draw final inner arc for start indicator. "Z" // Close path. ].join(" "); } };