a2896d8cd4249e7111a39ece7979b49e48f5bcf6
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ChartDetailsSectionRow.js
1 /*
2  * Copyright (C) 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.ChartDetailsSectionRow = class ChartDetailsSectionRow extends WebInspector.DetailsSectionRow
27 {
28     constructor(delegate)
29     {
30         super(WebInspector.UIString("No Chart Available"));
31
32         this.element.classList.add("chart");
33
34         this._titleElement = document.createElement("div");
35         this._titleElement.className = "title";
36         this.element.appendChild(this._titleElement);
37
38         var chartContentElement = document.createElement("div");
39         chartContentElement.className = "chart-content";
40         this.element.appendChild(chartContentElement);
41
42         this._canvas = document.createElement("canvas");
43         this._canvas.className = "chart";
44         chartContentElement.appendChild(this._canvas);
45
46         this._legendElement = document.createElement("div");
47         this._legendElement.className = "legend";
48         chartContentElement.appendChild(this._legendElement);
49
50         this._delegate = delegate;
51         this._items = new Map;
52         this._title = "";
53         this._innerLabel = "";
54         this._innerRadius = 0;
55         this._innerLabelFontSize = 11;
56         this._shadowColor = "rgba(0, 0, 0, 0.6)";
57         this._total = 0;
58
59         this._svgFiltersElement = document.createElement("svg");
60         this._svgFiltersElement.classList.add("defs-only");
61         this.element.append(this._svgFiltersElement);
62
63         this._checkboxStyleElement = document.createElement("style");
64         this._checkboxStyleElement.id = "checkbox-styles";
65         document.getElementsByTagName("head")[0].append(this._checkboxStyleElement);
66     }
67
68     // Public
69
70     set title(title)
71     {
72         if (this._title === title)
73             return;
74
75         this._title = title;
76         this._titleElement.textContent = title;
77     }
78
79     set innerLabel(label)
80     {
81         if (this._innerLabel === label)
82             return;
83
84         this._innerLabel = label;
85
86         this._needsLayout();
87     }
88
89     set innerRadius(radius)
90     {
91         if (this._innerRadius === radius)
92             return;
93
94         this._innerRadius = radius;
95
96         this._needsLayout();
97     }
98
99     get total()
100     {
101         return this._total;
102     }
103
104     addItem(id, label, value, color, checkbox, checked)
105     {
106         console.assert(!this._items.has(id), "Already added item with id: " + id);
107         if (this._items.has(id))
108             return;
109
110         console.assert(value >= 0, "Value cannot be negative.");
111         if (value < 0)
112             return;
113
114         this._items.set(id, {label, value, color, checkbox, checked});
115         this._total += value;
116
117         this._needsLayout();
118     }
119
120     setItemValue(id, value)
121     {
122         let item = this._items.get(id);
123         console.assert(item, "Cannot set value for invalid item id: " + id);
124         if (!item)
125             return;
126
127         console.assert(value >= 0, "Value cannot be negative.");
128         if (value < 0)
129             return;
130
131         if (item.value === value)
132             return;
133
134         this._total += value - item.value;
135         item.value = value;
136
137         this._needsLayout();
138     }
139
140     clearItems()
141     {
142         this._total = 0;
143         this._items.clear();
144
145         this._needsLayout();
146     }
147
148     // Private
149
150     _addCheckboxColorFilter(id, r, g, b)
151     {
152         for (let i = 0; i < this._svgFiltersElement.childNodes.length; ++i) {
153             if (this._svgFiltersElement.childNodes[i].id === id)
154                 return;
155         }
156
157         r /= 255;
158         b /= 255;
159         g /= 255;
160
161         // Create an svg:filter element that approximates "background-blend-mode: color", for grayscale input.
162         let filterElement = createSVGElement("filter");
163         filterElement.id = id;
164         filterElement.setAttribute("color-interpolation-filters", "sRGB");
165
166         let values = [1 - r, 0, 0, 0, r,
167                       1 - g, 0, 0, 0, g,
168                       1 - b, 0, 0, 0, b,
169                       0,     0, 0, 1, 0];
170
171         let colorMatrixPrimitive = createSVGElement("feColorMatrix");
172         colorMatrixPrimitive.setAttribute("type", "matrix");
173         colorMatrixPrimitive.setAttribute("values", values.join(" "));
174
175         function createGammaPrimitive(tagName, value)
176         {
177             let gammaPrimitive = createSVGElement(tagName);
178             gammaPrimitive.setAttribute("type", "gamma");
179             gammaPrimitive.setAttribute("value", value);
180             return gammaPrimitive;
181         }
182
183         let componentTransferPrimitive = createSVGElement("feComponentTransfer");
184         componentTransferPrimitive.append(createGammaPrimitive("feFuncR", 1.2), createGammaPrimitive("feFuncG", 1.2), createGammaPrimitive("feFuncB", 1.2));
185         filterElement.append(colorMatrixPrimitive, componentTransferPrimitive);
186
187         this._svgFiltersElement.append(filterElement);
188
189         let styleSheet = this._checkboxStyleElement.sheet;
190         styleSheet.insertRule(".details-section > .content > .group > .row.chart > .chart-content > .legend > .legend-item > label > input[type=checkbox]." + id + " { filter: grayscale(1) url(#" + id + ") }", 0);
191     }
192
193     _updateLegend()
194     {
195         if (!this._items.size) {
196             this._legendElement.removeChildren();
197             return;
198         }
199
200         function formatItemValue(item)
201         {
202             if (this._delegate && typeof this._delegate.formatChartValue === "function")
203                 return this._delegate.formatChartValue(item.value);
204             return item.value;
205         }
206
207         for (let [id, item] of this._items) {
208             if (item[WebInspector.ChartDetailsSectionRow.LegendItemValueElementSymbol]) {
209                 let valueElement = item[WebInspector.ChartDetailsSectionRow.LegendItemValueElementSymbol];
210                 valueElement.textContent = formatItemValue.call(this, item);
211                 continue;
212             }
213
214             let labelElement = document.createElement("label");
215             let keyElement;
216             if (item.checkbox) {
217                 let className = id.toLowerCase();
218                 let rgb = item.color.substring(4, item.color.length - 1).replace(/ /g, "").split(",");
219                 if (rgb[0] === rgb[1] && rgb[1] === rgb[2])
220                     rgb[0] = rgb[1] = rgb[2] = Math.min(160, rgb[0]);
221
222                 keyElement = document.createElement("input");
223                 keyElement.type = "checkbox";
224                 keyElement.classList.add(className);
225                 keyElement.checked = item.checked;
226                 keyElement[WebInspector.ChartDetailsSectionRow.DataItemIdSymbol] = id;
227
228                 keyElement.addEventListener("change", this._legendItemCheckboxValueChanged.bind(this));
229
230                 this._addCheckboxColorFilter(className, rgb[0], rgb[1], rgb[2]);
231             } else {
232                 keyElement = document.createElement("div");
233                 keyElement.classList.add("color-key");
234                 keyElement.style.backgroundColor = item.color;
235             }
236
237             labelElement.append(keyElement, item.label);
238
239             let valueElement = document.createElement("div");
240             valueElement.classList.add("value");
241             valueElement.textContent = formatItemValue.call(this, item);
242
243             item[WebInspector.ChartDetailsSectionRow.LegendItemValueElementSymbol] = valueElement;
244
245             let legendItemElement = document.createElement("div");
246             legendItemElement.classList.add("legend-item");
247             legendItemElement.append(labelElement, valueElement);
248
249             this._legendElement.append(legendItemElement);
250         }
251     }
252
253     _legendItemCheckboxValueChanged(event)
254     {
255         let checkbox = event.target;
256         let id = checkbox[WebInspector.ChartDetailsSectionRow.DataItemIdSymbol];
257         this.dispatchEventToListeners(WebInspector.ChartDetailsSectionRow.Event.LegendItemChecked, {id, checked: checkbox.checked});
258     }
259
260     _needsLayout()
261     {
262         if (this._scheduledLayoutUpdateIdentifier)
263             return;
264
265         this._scheduledLayoutUpdateIdentifier = requestAnimationFrame(this.updateLayout.bind(this));
266     }
267
268     updateLayout()
269     {
270         if (this._scheduledLayoutUpdateIdentifier) {
271             cancelAnimationFrame(this._scheduledLayoutUpdateIdentifier);
272             this._scheduledLayoutUpdateIdentifier = undefined;
273         }
274
275         this._updateLegend();
276
277         var width = this._canvas.clientWidth * window.devicePixelRatio;
278         var height = this._canvas.clientHeight * window.devicePixelRatio;
279         this._canvas.width = width;
280         this._canvas.height = height;
281
282         var context = this._canvas.getContext("2d");
283         context.clearRect(0, 0, width, height);
284
285         var x = Math.floor(width / 2);
286         var y = Math.floor(height / 2);
287         var radius = Math.floor(Math.min(x, y) * 0.96);   // Add a small margin to prevent clipping of the chart shadow.
288         var innerRadius = Math.floor(radius * this._innerRadius);
289         var startAngle = 1.5 * Math.PI;
290         var endAngle = startAngle;
291
292         function drawSlice(x, y, startAngle, endAngle, color)
293         {
294             context.beginPath();
295             context.moveTo(x, y);
296             context.arc(x, y, radius, startAngle, endAngle, false);
297             if (innerRadius > 0)
298                 context.arc(x, y, innerRadius, endAngle, startAngle, true);
299             context.fillStyle = color;
300             context.fill();
301         }
302
303         context.save();
304         context.shadowBlur = 2 * window.devicePixelRatio;
305         context.shadowOffsetY = window.devicePixelRatio;
306         context.shadowColor = this._shadowColor;
307         drawSlice(x, y, 0, 2.0 * Math.PI, "rgb(242, 242, 242)");
308         context.restore();
309
310         for (let [id, item] of this._items) {
311             if (item.value === 0)
312                 continue;
313             endAngle += (item.value / this._total) * 2.0 * Math.PI;
314             drawSlice(x, y, startAngle, endAngle, item.color);
315             startAngle = endAngle;
316         }
317
318         if (this._innerLabel) {
319             context.font = (this._innerLabelFontSize * window.devicePixelRatio) + "px sans-serif";
320             var metrics = context.measureText(this._innerLabel);
321             var offsetX = centerX - metrics.width / 2;
322             context.fillStyle = "rgb(68, 68, 68)";
323             context.fillText(this._innerLabel, offsetX, centerY);
324         }
325     }
326 };
327
328 WebInspector.ChartDetailsSectionRow.DataItemIdSymbol = Symbol("chart-details-section-row-data-item-id");
329 WebInspector.ChartDetailsSectionRow.LegendItemValueElementSymbol = Symbol("chart-details-section-row-legend-item-value-element");
330
331 WebInspector.ChartDetailsSectionRow.Event = {
332     LegendItemChecked: "chart-details-section-row-legend-item-checked"
333 };