| /* |
| * 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. |
| // |
| // <div class="gauge-chart"> |
| // <svg width="204" height="110" viewBox="0 0 204 110"> |
| // <path class="segment segment-class-name-1" d="..."/> |
| // <path class="segment segment-class-name-2" d="..."/> |
| // ... |
| // <polygon class="needle" points="..."/> |
| // </svg> |
| // </div> |
| |
| 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(" "); |
| } |
| }; |