/* * Copyright (C) 2015 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. */ WebInspector.ChartDetailsSectionRow = class ChartDetailsSectionRow extends WebInspector.DetailsSectionRow { constructor(delegate, chartSize, innerRadiusRatio) { super(WebInspector.UIString("No Chart Available")); innerRadiusRatio = innerRadiusRatio || 0; console.assert(chartSize > 0, chartSize); console.assert(innerRadiusRatio >= 0 && innerRadiusRatio < 1, innerRadiusRatio); this.element.classList.add("chart"); this._titleElement = document.createElement("div"); this._titleElement.className = "title"; this.element.appendChild(this._titleElement); let chartContentElement = document.createElement("div"); chartContentElement.className = "chart-content"; this.element.appendChild(chartContentElement); this._chartElement = createSVGElement("svg"); chartContentElement.appendChild(this._chartElement); this._legendElement = document.createElement("div"); this._legendElement.className = "legend"; chartContentElement.appendChild(this._legendElement); this._delegate = delegate; this._items = new Map; this._title = ""; this._chartSize = chartSize; this._radius = (this._chartSize / 2) - 1; // Subtract one to accomodate chart stroke width. this._innerRadius = innerRadiusRatio ? Math.floor(this._radius * innerRadiusRatio) : 0; this._total = 0; this._svgFiltersElement = document.createElement("svg"); this._svgFiltersElement.classList.add("defs-only"); this.element.append(this._svgFiltersElement); this._checkboxStyleElement = document.createElement("style"); this._checkboxStyleElement.id = "checkbox-styles"; document.getElementsByTagName("head")[0].append(this._checkboxStyleElement); function createEmptyChartPathData(c, r1, r2) { const a1 = 0; const a2 = Math.PI * 1.9999; 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) * r2, y4 = c + Math.sin(a1) * r2; return [ "M", x1, y1, // Starting position. "A", r1, r1, 0, 1, 1, x2, y2, // Draw outer arc. "Z", // Close path. "M", x3, y3, // Starting position. "A", r2, r2, 0, 1, 0, x4, y4, // Draw inner arc. "Z" // Close path. ].join(" "); } this._emptyChartPath = createSVGElement("path"); this._emptyChartPath.setAttribute("d", createEmptyChartPathData(this._chartSize / 2, this._radius, this._innerRadius)); this._emptyChartPath.classList.add("empty-chart"); this._chartElement.appendChild(this._emptyChartPath); } // Public get chartSize() { return this._chartSize; } set title(title) { if (this._title === title) return; this._title = title; this._titleElement.textContent = title; } get total() { return this._total; } addItem(id, label, value, color, checkbox, checked) { console.assert(!this._items.has(id), "Already added item with id: " + id); if (this._items.has(id)) return; console.assert(value >= 0, "Value cannot be negative."); if (value < 0) return; this._items.set(id, {label, value, color, checkbox, checked}); this._total += value; this._needsLayout(); } setItemValue(id, value) { let item = this._items.get(id); console.assert(item, "Cannot set value for invalid item id: " + id); if (!item) return; console.assert(value >= 0, "Value cannot be negative."); if (value < 0) return; if (item.value === value) return; this._total += value - item.value; item.value = value; this._needsLayout(); } clearItems() { for (let item of this._items.values()) { let path = item[WebInspector.ChartDetailsSectionRow.ChartSegmentPathSymbol]; if (path) path.remove(); } this._total = 0; this._items.clear(); this._needsLayout(); } // Private _addCheckboxColorFilter(id, r, g, b) { for (let i = 0; i < this._svgFiltersElement.childNodes.length; ++i) { if (this._svgFiltersElement.childNodes[i].id === id) return; } r /= 255; b /= 255; g /= 255; // Create an svg:filter element that approximates "background-blend-mode: color", for grayscale input. let filterElement = createSVGElement("filter"); filterElement.id = id; filterElement.setAttribute("color-interpolation-filters", "sRGB"); let values = [1 - r, 0, 0, 0, r, 1 - g, 0, 0, 0, g, 1 - b, 0, 0, 0, b, 0, 0, 0, 1, 0]; let colorMatrixPrimitive = createSVGElement("feColorMatrix"); colorMatrixPrimitive.setAttribute("type", "matrix"); colorMatrixPrimitive.setAttribute("values", values.join(" ")); function createGammaPrimitive(tagName, value) { let gammaPrimitive = createSVGElement(tagName); gammaPrimitive.setAttribute("type", "gamma"); gammaPrimitive.setAttribute("exponent", value); return gammaPrimitive; } let componentTransferPrimitive = createSVGElement("feComponentTransfer"); componentTransferPrimitive.append(createGammaPrimitive("feFuncR", 1.4), createGammaPrimitive("feFuncG", 1.4), createGammaPrimitive("feFuncB", 1.4)); filterElement.append(colorMatrixPrimitive, componentTransferPrimitive); this._svgFiltersElement.append(filterElement); let styleSheet = this._checkboxStyleElement.sheet; styleSheet.insertRule(".details-section > .content > .group > .row.chart > .chart-content > .legend > .legend-item > label > input[type=checkbox]." + id + " { filter: grayscale(1) url(#" + id + ") }", 0); } _updateLegend() { if (!this._items.size) { this._legendElement.removeChildren(); return; } function formatItemValue(item) { if (this._delegate && typeof this._delegate.formatChartValue === "function") return this._delegate.formatChartValue(item.value); return item.value; } for (let [id, item] of this._items) { if (item[WebInspector.ChartDetailsSectionRow.LegendItemValueElementSymbol]) { let valueElement = item[WebInspector.ChartDetailsSectionRow.LegendItemValueElementSymbol]; valueElement.textContent = formatItemValue.call(this, item); continue; } let labelElement = document.createElement("label"); let keyElement; if (item.checkbox) { let className = id.toLowerCase(); let rgb = item.color.substring(4, item.color.length - 1).replace(/ /g, "").split(","); if (rgb[0] === rgb[1] && rgb[1] === rgb[2]) rgb[0] = rgb[1] = rgb[2] = Math.min(160, rgb[0]); keyElement = document.createElement("input"); keyElement.type = "checkbox"; keyElement.classList.add(className); keyElement.checked = item.checked; keyElement[WebInspector.ChartDetailsSectionRow.DataItemIdSymbol] = id; keyElement.addEventListener("change", this._legendItemCheckboxValueChanged.bind(this)); this._addCheckboxColorFilter(className, rgb[0], rgb[1], rgb[2]); } else { keyElement = document.createElement("div"); keyElement.classList.add("color-key"); keyElement.style.backgroundColor = item.color; } labelElement.append(keyElement, item.label); let valueElement = document.createElement("div"); valueElement.classList.add("value"); valueElement.textContent = formatItemValue.call(this, item); item[WebInspector.ChartDetailsSectionRow.LegendItemValueElementSymbol] = valueElement; let legendItemElement = document.createElement("div"); legendItemElement.classList.add("legend-item"); legendItemElement.append(labelElement, valueElement); this._legendElement.append(legendItemElement); } } _legendItemCheckboxValueChanged(event) { let checkbox = event.target; let id = checkbox[WebInspector.ChartDetailsSectionRow.DataItemIdSymbol]; this.dispatchEventToListeners(WebInspector.ChartDetailsSectionRow.Event.LegendItemChecked, {id, checked: checkbox.checked}); } _needsLayout() { if (this._scheduledLayoutUpdateIdentifier) return; this._scheduledLayoutUpdateIdentifier = requestAnimationFrame(this._updateLayout.bind(this)); } _updateLayout() { if (this._scheduledLayoutUpdateIdentifier) { cancelAnimationFrame(this._scheduledLayoutUpdateIdentifier); this._scheduledLayoutUpdateIdentifier = undefined; } this._updateLegend(); this._chartElement.setAttribute("width", this._chartSize); this._chartElement.setAttribute("height", this._chartSize); this._chartElement.setAttribute("viewbox", "0 0 " + this._chartSize + " " + this._chartSize); function createSegmentPathData(c, a1, a2, r1, r2) { const 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) * r2, y4 = c + Math.sin(a1) * r2; return [ "M", x1, y1, // Starting position. "A", r1, r1, 0, largeArcFlag, 1, x2, y2, // Draw outer arc. "L", x3, y3, // Connect outer and innner arcs. "A", r2, r2, 0, largeArcFlag, 0, x4, y4, // Draw inner arc. "Z" // Close path. ].join(" "); } // Balance item values so that all non-zero chart segments are visible. const minimumDisplayValue = this._total * 0.015; let items = []; for (let item of this._items.values()) { item.displayValue = item.value ? Math.max(minimumDisplayValue, item.value) : 0; if (item.displayValue) items.push(item); } if (items.length > 1) { items.sort(function(a, b) { return a.value - b.value; }); let largeItemCount = items.length; let totalAdjustedValue = 0; for (let item of items) { if (item.value < minimumDisplayValue) { totalAdjustedValue += minimumDisplayValue - item.value; largeItemCount--; continue; } if (!totalAdjustedValue || !largeItemCount) break; const donatedValue = totalAdjustedValue / largeItemCount; if (item.displayValue - donatedValue >= minimumDisplayValue) { item.displayValue -= donatedValue; totalAdjustedValue -= donatedValue; } largeItemCount--; } } const center = this._chartSize / 2; let startAngle = -Math.PI / 2; let endAngle = 0; for (let [id, item] of this._items) { let path = item[WebInspector.ChartDetailsSectionRow.ChartSegmentPathSymbol]; if (!path) { path = createSVGElement("path"); path.classList.add("chart-segment"); path.setAttribute("fill", item.color); this._chartElement.appendChild(path); item[WebInspector.ChartDetailsSectionRow.ChartSegmentPathSymbol] = path; } if (!item.value) { path.classList.add("hidden"); continue; } const angle = (item.displayValue / this._total) * Math.PI * 2; endAngle = startAngle + angle; path.setAttribute("d", createSegmentPathData(center, startAngle, endAngle, this._radius, this._innerRadius)); path.classList.remove("hidden"); startAngle = endAngle; } } }; WebInspector.ChartDetailsSectionRow.DataItemIdSymbol = Symbol("chart-details-section-row-data-item-id"); WebInspector.ChartDetailsSectionRow.ChartSegmentPathSymbol = Symbol("chart-details-section-row-chart-segment-path"); WebInspector.ChartDetailsSectionRow.LegendItemValueElementSymbol = Symbol("chart-details-section-row-legend-item-value-element"); WebInspector.ChartDetailsSectionRow.Event = { LegendItemChecked: "chart-details-section-row-legend-item-checked" };