blob: b9deb1e6dea2fe4187418d7aa4f97ccb084dd1d1 [file] [log] [blame]
// 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 ArgumentError('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);
}
}