blob: 84d31348f648421a9888c14cc5bfd72c0dfb366b [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright (c) 2015 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<link rel="import" href="/tracing/base/color_scheme.html">
<link rel="import" href="/tracing/base/math/range.html">
<link rel="import" href="/tracing/base/unit.html">
<script>
'use strict';
tr.exportTo('tr.ui.tracks', function() {
var ColorScheme = tr.b.ColorScheme;
var IDEAL_MAJOR_MARK_HEIGHT_PX = 30;
var AXIS_LABLE_MARGIN_PX = 10;
var AXIS_LABLE_FONT_SIZE_PX = 9;
var AXIS_LABLE_FONT = 'Arial';
/**
* A vertical axis for a (set of) chart series which maps an arbitrary range
* of values [min, max] to the unit range [0, 1].
*
* @constructor
*/
function ChartSeriesYAxis(opt_min, opt_max) {
this.guid_ = tr.b.GUID.allocateSimple();
this.bounds = new tr.b.math.Range();
if (opt_min !== undefined)
this.bounds.addValue(opt_min);
if (opt_max !== undefined)
this.bounds.addValue(opt_max);
}
ChartSeriesYAxis.prototype = {
get guid() {
return this.guid_;
},
valueToUnitRange: function(value) {
if (this.bounds.isEmpty)
throw new Error('Chart series y-axis bounds are empty');
var bounds = this.bounds;
if (bounds.range === 0)
return 0;
return (value - bounds.min) / bounds.range;
},
unitRangeToValue: function(unitRange) {
if (this.bounds.isEmpty) {
throw new Error('Chart series y-axis bounds are empty');
}
return unitRange * this.bounds.range + this.bounds.min;
},
/**
* Automatically set the y-axis bounds from the range of values of all
* series in a list.
*
* See the description of autoSetFromRange for the optional configuration
* argument flags.
*/
autoSetFromSeries: function(series, opt_config) {
var range = new tr.b.math.Range();
series.forEach(function(s) {
range.addRange(s.range);
}, this);
this.autoSetFromRange(range, opt_config);
},
/**
* Automatically set the y-axis bound from a range of values.
*
* The following four flags, which affect the behavior of this method with
* respect to already defined bounds, can be present in the optional
* configuration (a flag is assumed to be false if it is not provided or if
* the configuration is not provided):
*
* - expandMin: allow decreasing the min bound (if range.min < this.min)
* - shrinkMin: allow increasing the min bound (if range.min > this.min)
* - expandMax: allow increasing the max bound (if range.max > this.max)
* - shrinkMax: allow decreasing the max bound (if range.max < this.max)
*
* This method will ensure that the resulting bounds are defined and valid
* (i.e. min <= max) provided that they were valid or empty before and the
* value range is non-empty and valid.
*
* Note that unless expanding/shrinking a bound is explicitly enabled in
* the configuration, non-empty bounds will not be changed under any
* circumstances.
*
* Observe that if no configuration is provided (or all flags are set to
* false), this method will only modify the y-axis bounds if they are empty.
*/
autoSetFromRange: function(range, opt_config) {
if (range.isEmpty)
return;
var bounds = this.bounds;
if (bounds.isEmpty) {
bounds.addRange(range);
return;
}
if (!opt_config)
return;
var useRangeMin = (opt_config.expandMin && range.min < bounds.min ||
opt_config.shrinkMin && range.min > bounds.min);
var useRangeMax = (opt_config.expandMax && range.max > bounds.max ||
opt_config.shrinkMax && range.max < bounds.max);
// Neither bound is modified.
if (!useRangeMin && !useRangeMax)
return;
// Both bounds are modified. Assuming the range argument is a valid
// range, no extra checks are necessary.
if (useRangeMin && useRangeMax) {
bounds.min = range.min;
bounds.max = range.max;
return;
}
// Only one bound is modified. We must ensure that it doesn't go
// over/under the other (unmodified) bound.
if (useRangeMin) {
bounds.min = Math.min(range.min, bounds.max);
} else {
bounds.max = Math.max(range.max, bounds.min);
}
},
majorMarkHeightWorld_: function(transform, pixelRatio) {
var idealMajorMarkHeightPx = IDEAL_MAJOR_MARK_HEIGHT_PX * pixelRatio;
var idealMajorMarkHeightWorld =
transform.vectorToWorldDistance(idealMajorMarkHeightPx);
return tr.b.math.preferredNumberLargerThanMin(idealMajorMarkHeightWorld);
},
draw: function(ctx, transform, showYAxisLabels, showYGridLines) {
if (!showYAxisLabels && !showYGridLines) return;
var pixelRatio = transform.pixelRatio;
var viewTop = transform.outerTopViewY;
var worldTop = transform.viewYToWorldY(viewTop);
var viewBottom = transform.outerBottomViewY;
var viewHeight = viewBottom - viewTop;
var viewLeft = transform.leftViewX;
var viewRight = transform.rightViewX;
var labelLeft = transform.leftYLabel;
ctx.save();
ctx.lineWidth = pixelRatio;
ctx.fillStyle = ColorScheme.getColorForReservedNameAsString('black');
ctx.textAlign = 'left';
ctx.textBaseline = 'center';
ctx.font =
(AXIS_LABLE_FONT_SIZE_PX * pixelRatio) + 'px ' + AXIS_LABLE_FONT;
// Draw left edge of chart series.
ctx.beginPath();
ctx.strokeStyle = ColorScheme.getColorForReservedNameAsString('black');
tr.ui.b.drawLine(
ctx, viewLeft, viewTop, viewLeft, viewBottom, viewLeft);
ctx.stroke();
ctx.closePath();
// Draw y-axis ticks and gridlines.
ctx.beginPath();
ctx.strokeStyle = ColorScheme.getColorForReservedNameAsString('grey');
var majorMarkHeight = this.majorMarkHeightWorld_(transform, pixelRatio);
var maxMajorMark = Math.max(transform.viewYToWorldY(viewTop),
Math.abs(transform.viewYToWorldY(viewBottom)));
for (var curWorldY = 0;
curWorldY <= maxMajorMark;
curWorldY += majorMarkHeight) {
var roundedUnitValue = Math.floor(curWorldY * 1000000) / 1000000;
var curViewYPositive = transform.worldYToViewY(curWorldY);
if (curViewYPositive >= viewTop) {
if (showYAxisLabels) {
ctx.fillText(roundedUnitValue, viewLeft + AXIS_LABLE_MARGIN_PX,
curViewYPositive - AXIS_LABLE_MARGIN_PX);
}
if (showYGridLines) {
tr.ui.b.drawLine(
ctx, viewLeft, curViewYPositive, viewRight, curViewYPositive);
}
}
var curViewYNegative = transform.worldYToViewY(-1 * curWorldY);
if (curViewYNegative <= viewBottom) {
if (showYAxisLabels) {
ctx.fillText(roundedUnitValue, viewLeft + AXIS_LABLE_MARGIN_PX,
curViewYNegative - AXIS_LABLE_MARGIN_PX);
}
if (showYGridLines) {
tr.ui.b.drawLine(
ctx, viewLeft, curViewYNegative, viewRight, curViewYNegative);
}
}
}
ctx.stroke();
ctx.restore();
}
};
return {
ChartSeriesYAxis,
};
});
</script>