| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| part of dart.vmstats; |
| |
| class BarGraph { |
| CanvasElement _canvas; |
| GraphModel _model; |
| List<Element> _elements; |
| double scaleHeight = 0; |
| |
| static const int SAMPLE_WIDTH = 5; |
| static const int LEFT_MARGIN = 50; |
| static const int RIGHT_MARGIN = 150; |
| static const int LEGEND_WIDTH = 130; |
| static const int LEGEND_Y = 20; |
| static const int INSIDE_MARGIN = 2; |
| static const int LINE_WIDTH = 2; |
| |
| static const int NUM_DIVIDERS = 5; |
| static const String FONT = "14px sans-serif"; |
| |
| BarGraph(this._canvas, this._elements) { |
| var maxElements = |
| (_canvas.width - LEFT_MARGIN - RIGHT_MARGIN) ~/ SAMPLE_WIDTH; |
| _model = new GraphModel(maxElements); |
| _model.addListener(drawGraph, null); |
| drawBarGraph(); |
| } |
| |
| void addSample(List<int> segments) { |
| if (segments.length != _elements.length) { |
| throw new RuntimeError('invalid sample size for graph'); |
| } |
| _model.addSample(segments); |
| } |
| |
| void drawBarGraph() { |
| // Draw chart's outer box. |
| var context = _canvas.context2D; |
| context.beginPath(); |
| context.strokeStyle = 'black'; |
| // The '2's are the width of the line, even though 1 is specified. |
| context.strokeRect( |
| LEFT_MARGIN - 2, 1, _canvas.width - LEFT_MARGIN - RIGHT_MARGIN + 2, |
| _canvas.height - 2, 1); |
| |
| // Draw legend. |
| var x = _canvas.width - LEGEND_WIDTH; |
| var y = LEGEND_Y; |
| context.font = FONT; |
| for (var i = _elements.length - 1; i >= 0; i--) { |
| context.fillStyle = _elements[i].color; |
| context.fillRect(x, y, 20, 20); |
| context.fillStyle = 'black'; |
| context.fillText(_elements[i].name, x + 30, y + 15); |
| y += 30; |
| } |
| } |
| |
| void drawGraph(GraphModel model) { |
| var graphHeight = model.maxTotal; |
| var width = _canvas.clientWidth; |
| var height = _canvas.clientHeight; |
| if (graphHeight >= scaleHeight) { |
| // Make scale height a bit higher to allow for growth, and |
| // round to nearest 100. |
| scaleHeight = graphHeight * 1.2; |
| scaleHeight = ((scaleHeight / 100).ceil() * 100); |
| } |
| var scale = height / scaleHeight; |
| drawValues(scaleHeight, scale); |
| drawChart(scaleHeight, scale); |
| } |
| |
| void drawChart(int maxHeight, double scale) { |
| var dividerHeight = maxHeight ~/ NUM_DIVIDERS; |
| var context = _canvas.context2D; |
| context.beginPath(); |
| var height = maxHeight.toInt(); |
| var scaledY = dividerHeight * scale; |
| |
| // Draw the vertical axis values and lines. |
| context.clearRect(0, 0, LEFT_MARGIN - INSIDE_MARGIN, maxHeight); |
| for (var i = 1; i < NUM_DIVIDERS; i++) { |
| height -= (dividerHeight ~/ 100) * 100; |
| context.font = FONT; |
| context.fillStyle = 'black'; |
| context.textAlign = 'right'; |
| context.textBaseline = 'middle'; |
| context.fillText(height.toString(), LEFT_MARGIN - 10, scaledY); |
| context.moveTo(LEFT_MARGIN - INSIDE_MARGIN, scaledY); |
| context.strokeStyle = 'grey'; |
| context.lineWidth = 0.5; |
| context.lineTo(_canvas.width - RIGHT_MARGIN, scaledY); |
| context.stroke(); |
| scaledY += dividerHeight * scale; |
| } |
| } |
| |
| void drawValues(int maxHeight, num scale) { |
| Iterator<Sample> iterator = _model.iterator; |
| var x = LEFT_MARGIN + INSIDE_MARGIN; |
| var y = INSIDE_MARGIN; |
| var w = _canvas.width - LEFT_MARGIN - RIGHT_MARGIN - INSIDE_MARGIN; |
| var h = (maxHeight * scale).ceil() - (2 * INSIDE_MARGIN); |
| _canvas.context2D.clearRect(x, y, w, h); |
| |
| while (iterator.moveNext()) { |
| Sample s = iterator.current; |
| var y = INSIDE_MARGIN; |
| if (s != null) { |
| var blankHeight = scaleHeight - s.total(); |
| drawVerticalSegment(x, y, SAMPLE_WIDTH, blankHeight, 'white', scale); |
| y += blankHeight; |
| for (int i = s.length - 1; i >= 0; i--) { |
| var h = s[i]; |
| drawVerticalSegment(x, y, SAMPLE_WIDTH, h, _elements[i].color, scale); |
| y += s[i]; |
| } |
| } else { |
| drawVerticalSegment(x, INSIDE_MARGIN, SAMPLE_WIDTH, |
| maxHeight, 'white', scale); |
| } |
| x += SAMPLE_WIDTH ; |
| } |
| } |
| |
| void drawVerticalSegment(int x, int y, int w, int h, String color, |
| num scale) { |
| var context = _canvas.context2D; |
| y = (y * scale).floor(); |
| h = (h * scale).ceil(); |
| context.beginPath(); |
| context.lineWidth = w; |
| context.fillStyle = color; |
| context.strokeStyle = color; |
| if (x < INSIDE_MARGIN) { |
| x = INSIDE_MARGIN; |
| } |
| if (y < INSIDE_MARGIN) { |
| y = INSIDE_MARGIN; |
| } |
| var max = _canvas.height - INSIDE_MARGIN; |
| if ((y + h) > max) { |
| h = max - y; |
| } |
| context.moveTo(x, y); |
| context.lineTo(x, y + h); |
| context.stroke(); |
| } |
| } |
| |
| class GraphModel extends ObservableModel { |
| List<Sample> _samples = new List<Sample>(); |
| int _maxSize; |
| |
| static const int _LARGE_LENGTH = 999999999; |
| |
| GraphModel(this._maxSize) {} |
| |
| void addSample(List<int> segments) { |
| var len = _samples.length; |
| if (_samples.length >= _maxSize) { |
| _samples.remove(_samples.first); |
| } |
| _samples.add(new Sample(segments)); |
| notifySuccess(); |
| } |
| |
| int get maxSize => _maxSize; |
| |
| Iterator<Sample> get iterator => _samples.iterator; |
| |
| Sample operator[](int i) => _samples[i]; |
| |
| /** |
| * Returns the minimum total from all the samples. |
| */ |
| int get minTotal { |
| var min = _LARGE_LENGTH; |
| _samples.forEach((Sample s) => min = (s.total() < min ? s.total() : min)); |
| return min; |
| } |
| |
| /** |
| * Returns the maximum total from all the samples. |
| */ |
| int get maxTotal { |
| var max = 1; // Must be non-zero. |
| _samples.forEach((Sample s) => max = (s.total() > max ? s.total() : max)); |
| return max; |
| } |
| } |
| |
| /** |
| * An element is a data type that gets charted. Each element has a name for |
| * the legend, and a color for the bar graph. The number of elements in a |
| * graph should match the number of segments in each sample. |
| */ |
| class Element { |
| final String name; |
| final String color; // Any description the DOM will accept, like "red". |
| |
| Element(this.name, this.color) {} |
| } |
| |
| /** |
| * A sample is a list of segment lengths. |
| */ |
| class Sample { |
| List<int> _segments; |
| |
| Sample(this._segments) {} |
| |
| int get length => _segments.length; |
| int operator[](int i) => _segments[i]; |
| |
| Iterator<int> get iterator => _segments.iterator; |
| |
| int total() { |
| return _segments.fold(0, (int prev, int element) => prev + element); |
| } |
| } |