1/*
2 * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3 * Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1.  Redistributions of source code must retain the above copyright
10 *     notice, this list of conditions and the following disclaimer.
11 * 2.  Redistributions in binary form must reproduce the above copyright
12 *     notice, this list of conditions and the following disclaimer in the
13 *     documentation and/or other materials provided with the distribution.
14 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15 *     its contributors may be used to endorse or promote products derived
16 *     from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30WebInspector.SummaryBar = function(categories)
31{
32    this.categories = categories;
33
34    this.element = document.createElement("div");
35    this.element.className = "summary-bar";
36
37    this.graphElement = document.createElement("canvas");
38    this.graphElement.setAttribute("width", "450");
39    this.graphElement.setAttribute("height", "38");
40    this.graphElement.className = "summary-graph";
41    this.element.appendChild(this.graphElement);
42
43    this.legendElement = document.createElement("div");
44    this.legendElement.className = "summary-graph-legend";
45    this.element.appendChild(this.legendElement);
46}
47
48WebInspector.SummaryBar.prototype = {
49
50    get calculator() {
51        return this._calculator;
52    },
53
54    set calculator(x) {
55        this._calculator = x;
56    },
57
58    reset: function()
59    {
60        this.legendElement.removeChildren();
61        this._drawSummaryGraph();
62    },
63
64    update: function(data)
65    {
66        var graphInfo = this.calculator.computeSummaryValues(data);
67
68        var fillSegments = [];
69
70        this.legendElement.removeChildren();
71
72        for (var category in this.categories) {
73            var size = graphInfo.categoryValues[category];
74            if (!size)
75                continue;
76
77            var colorString = this.categories[category].color;
78
79            var fillSegment = {color: colorString, value: size};
80            fillSegments.push(fillSegment);
81
82            var legendLabel = this._makeLegendElement(this.categories[category].title, this.calculator.formatValue(size), colorString);
83            this.legendElement.appendChild(legendLabel);
84        }
85
86        if (graphInfo.total) {
87            var totalLegendLabel = this._makeLegendElement(WebInspector.UIString("Total"), this.calculator.formatValue(graphInfo.total));
88            totalLegendLabel.addStyleClass("total");
89            this.legendElement.appendChild(totalLegendLabel);
90        }
91
92        this._drawSummaryGraph(fillSegments);
93    },
94
95    _drawSwatch: function(canvas, color)
96    {
97        var ctx = canvas.getContext("2d");
98
99        function drawSwatchSquare() {
100            ctx.fillStyle = color;
101            ctx.fillRect(0, 0, 13, 13);
102
103            var gradient = ctx.createLinearGradient(0, 0, 13, 13);
104            gradient.addColorStop(0.0, "rgba(255, 255, 255, 0.2)");
105            gradient.addColorStop(1.0, "rgba(255, 255, 255, 0.0)");
106
107            ctx.fillStyle = gradient;
108            ctx.fillRect(0, 0, 13, 13);
109
110            gradient = ctx.createLinearGradient(13, 13, 0, 0);
111            gradient.addColorStop(0.0, "rgba(0, 0, 0, 0.2)");
112            gradient.addColorStop(1.0, "rgba(0, 0, 0, 0.0)");
113
114            ctx.fillStyle = gradient;
115            ctx.fillRect(0, 0, 13, 13);
116
117            ctx.strokeStyle = "rgba(0, 0, 0, 0.6)";
118            ctx.strokeRect(0.5, 0.5, 12, 12);
119        }
120
121        ctx.clearRect(0, 0, 13, 24);
122
123        drawSwatchSquare();
124
125        ctx.save();
126
127        ctx.translate(0, 25);
128        ctx.scale(1, -1);
129
130        drawSwatchSquare();
131
132        ctx.restore();
133
134        this._fadeOutRect(ctx, 0, 13, 13, 13, 0.5, 0.0);
135    },
136
137    _drawSummaryGraph: function(segments)
138    {
139        if (!segments || !segments.length) {
140            segments = [{color: "white", value: 1}];
141            this._showingEmptySummaryGraph = true;
142        } else
143            delete this._showingEmptySummaryGraph;
144
145        // Calculate the total of all segments.
146        var total = 0;
147        for (var i = 0; i < segments.length; ++i)
148            total += segments[i].value;
149
150        // Calculate the percentage of each segment, rounded to the nearest percent.
151        var percents = segments.map(function(s) { return Math.max(Math.round(100 * s.value / total), 1) });
152
153        // Calculate the total percentage.
154        var percentTotal = 0;
155        for (var i = 0; i < percents.length; ++i)
156            percentTotal += percents[i];
157
158        // Make sure our percentage total is not greater-than 100, it can be greater
159        // if we rounded up for a few segments.
160        while (percentTotal > 100) {
161            for (var i = 0; i < percents.length && percentTotal > 100; ++i) {
162                if (percents[i] > 1) {
163                    --percents[i];
164                    --percentTotal;
165                }
166            }
167        }
168
169        // Make sure our percentage total is not less-than 100, it can be less
170        // if we rounded down for a few segments.
171        while (percentTotal < 100) {
172            for (var i = 0; i < percents.length && percentTotal < 100; ++i) {
173                ++percents[i];
174                ++percentTotal;
175            }
176        }
177
178        var ctx = this.graphElement.getContext("2d");
179
180        var x = 0;
181        var y = 0;
182        var w = 450;
183        var h = 19;
184        var r = (h / 2);
185
186        function drawPillShadow()
187        {
188            // This draws a line with a shadow that is offset away from the line. The line is stroked
189            // twice with different X shadow offsets to give more feathered edges. Later we erase the
190            // line with destination-out 100% transparent black, leaving only the shadow. This only
191            // works if nothing has been drawn into the canvas yet.
192
193            ctx.beginPath();
194            ctx.moveTo(x + 4, y + h - 3 - 0.5);
195            ctx.lineTo(x + w - 4, y + h - 3 - 0.5);
196            ctx.closePath();
197
198            ctx.save();
199
200            ctx.shadowBlur = 2;
201            ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
202            ctx.shadowOffsetX = 3;
203            ctx.shadowOffsetY = 5;
204
205            ctx.strokeStyle = "white";
206            ctx.lineWidth = 1;
207
208            ctx.stroke();
209
210            ctx.shadowOffsetX = -3;
211
212            ctx.stroke();
213
214            ctx.restore();
215
216            ctx.save();
217
218            ctx.globalCompositeOperation = "destination-out";
219            ctx.strokeStyle = "rgba(0, 0, 0, 1)";
220            ctx.lineWidth = 1;
221
222            ctx.stroke();
223
224            ctx.restore();
225        }
226
227        function drawPill()
228        {
229            // Make a rounded rect path.
230            ctx.beginPath();
231            ctx.moveTo(x, y + r);
232            ctx.lineTo(x, y + h - r);
233            ctx.arc(x + r, y + h - r, r, Math.PI, Math.PI / 2, true);
234            ctx.lineTo(x + w - r, y + h);
235            ctx.arc(x + w - r, y + h - r, r, Math.PI / 2, 0, true);
236            ctx.lineTo(x + w, y + r);
237            ctx.arc(x + w - r, y + r, r, 0, 3 * Math.PI / 2, true);
238            ctx.lineTo(x + r, y);
239            ctx.arc(x + r, y + r, r, Math.PI / 2, Math.PI, true);
240            ctx.closePath();
241
242            // Clip to the rounded rect path.
243            ctx.save();
244            ctx.clip();
245
246            // Fill the segments with the associated color.
247            var previousSegmentsWidth = 0;
248            for (var i = 0; i < segments.length; ++i) {
249                var segmentWidth = Math.round(w * percents[i] / 100);
250                ctx.fillStyle = segments[i].color;
251                ctx.fillRect(x + previousSegmentsWidth, y, segmentWidth, h);
252                previousSegmentsWidth += segmentWidth;
253            }
254
255            // Draw the segment divider lines.
256            ctx.lineWidth = 1;
257            for (var i = 1; i < 20; ++i) {
258                ctx.beginPath();
259                ctx.moveTo(x + (i * Math.round(w / 20)) + 0.5, y);
260                ctx.lineTo(x + (i * Math.round(w / 20)) + 0.5, y + h);
261                ctx.closePath();
262
263                ctx.strokeStyle = "rgba(0, 0, 0, 0.2)";
264                ctx.stroke();
265
266                ctx.beginPath();
267                ctx.moveTo(x + (i * Math.round(w / 20)) + 1.5, y);
268                ctx.lineTo(x + (i * Math.round(w / 20)) + 1.5, y + h);
269                ctx.closePath();
270
271                ctx.strokeStyle = "rgba(255, 255, 255, 0.2)";
272                ctx.stroke();
273            }
274
275            // Draw the pill shading.
276            var lightGradient = ctx.createLinearGradient(x, y, x, y + (h / 1.5));
277            lightGradient.addColorStop(0.0, "rgba(220, 220, 220, 0.6)");
278            lightGradient.addColorStop(0.4, "rgba(220, 220, 220, 0.2)");
279            lightGradient.addColorStop(1.0, "rgba(255, 255, 255, 0.0)");
280
281            var darkGradient = ctx.createLinearGradient(x, y + (h / 3), x, y + h);
282            darkGradient.addColorStop(0.0, "rgba(0, 0, 0, 0.0)");
283            darkGradient.addColorStop(0.8, "rgba(0, 0, 0, 0.2)");
284            darkGradient.addColorStop(1.0, "rgba(0, 0, 0, 0.5)");
285
286            ctx.fillStyle = darkGradient;
287            ctx.fillRect(x, y, w, h);
288
289            ctx.fillStyle = lightGradient;
290            ctx.fillRect(x, y, w, h);
291
292            ctx.restore();
293        }
294
295        ctx.clearRect(x, y, w, (h * 2));
296
297        drawPillShadow();
298        drawPill();
299
300        ctx.save();
301
302        ctx.translate(0, (h * 2) + 1);
303        ctx.scale(1, -1);
304
305        drawPill();
306
307        ctx.restore();
308
309        this._fadeOutRect(ctx, x, y + h + 1, w, h, 0.5, 0.0);
310    },
311
312    _fadeOutRect: function(ctx, x, y, w, h, a1, a2)
313    {
314        ctx.save();
315
316        var gradient = ctx.createLinearGradient(x, y, x, y + h);
317        gradient.addColorStop(0.0, "rgba(0, 0, 0, " + (1.0 - a1) + ")");
318        gradient.addColorStop(0.8, "rgba(0, 0, 0, " + (1.0 - a2) + ")");
319        gradient.addColorStop(1.0, "rgba(0, 0, 0, 1.0)");
320
321        ctx.globalCompositeOperation = "destination-out";
322
323        ctx.fillStyle = gradient;
324        ctx.fillRect(x, y, w, h);
325
326        ctx.restore();
327    },
328
329    _makeLegendElement: function(label, value, color)
330    {
331        var legendElement = document.createElement("label");
332        legendElement.className = "summary-graph-legend-item";
333
334        if (color) {
335            var swatch = document.createElement("canvas");
336            swatch.className = "summary-graph-legend-swatch";
337            swatch.setAttribute("width", "13");
338            swatch.setAttribute("height", "24");
339
340            legendElement.appendChild(swatch);
341
342            this._drawSwatch(swatch, color);
343        }
344
345        var labelElement = document.createElement("div");
346        labelElement.className = "summary-graph-legend-label";
347        legendElement.appendChild(labelElement);
348
349        var headerElement = document.createElement("div");
350        headerElement.className = "summary-graph-legend-header";
351        headerElement.textContent = label;
352        labelElement.appendChild(headerElement);
353
354        var valueElement = document.createElement("div");
355        valueElement.className = "summary-graph-legend-value";
356        valueElement.textContent = value;
357        labelElement.appendChild(valueElement);
358
359        return legendElement;
360    }
361}
362
363WebInspector.SummaryBar.prototype.__proto__ = WebInspector.Object.prototype;
364