blob: 1d7cad2c15b3e12ed22d4bb25e4d0446227983f3 [file] [log] [blame]
class BarGraphGroup {
constructor()
{
this._bars = [];
this._computeRangeLazily = new LazilyEvaluatedFunction(this._computeRange.bind(this));
this._showLabels = false;
}
addBar(values, valueLabels, mean, interval, barColor, meanIndicatorColor)
{
const bar = new BarGraph(this, values, valueLabels, mean, interval, barColor, meanIndicatorColor);
this._bars.push({bar, values, interval});
return bar;
}
showLabels() { return this._showLabels; }
setShowLabels(showLabels)
{
this._showLabels = showLabels;
for (const entry of this._bars)
entry.bar.enqueueToRender();
}
range()
{
return this._computeRangeLazily.evaluate(...this._bars);
}
_computeRange(...bars)
{
Instrumentation.startMeasuringTime('BarGraphGroup', 'updateGroup');
let min = Infinity;
let max = -Infinity;
for (const entry of bars) {
for (const value of entry.values) {
if (isNaN(value))
continue;
min = Math.min(min, value);
max = Math.max(max, value);
}
if (entry.interval) {
for (const value of entry.interval) {
if (isNaN(value))
continue;
min = Math.min(min, value);
max = Math.max(max, value);
}
}
}
const diff = max - min;
min -= diff * 0.1;
max += diff * 0.1;
const xForValue = (value, width) => (value - min) / (max - min) * width;
const barRangeForValue = (value, width) => [0, (value - min) / (max - min) * width];
Instrumentation.endMeasuringTime('BarGraphGroup', 'updateGroup');
return {min, max, xForValue, barRangeForValue};
}
}
class BarGraph extends ComponentBase {
constructor(group, values, valueLabels, mean, interval, barColor, meanIndicatorColor)
{
super('bar-graph');
this._group = group;
this._values = values;
this._valueLabels = valueLabels;
this._mean = mean;
this._interval = interval;
this._barColor = barColor;
this._meanIndicatorColor = meanIndicatorColor;
this._resizeCanvasLazily = new LazilyEvaluatedFunction(this._resizeCanvas.bind(this));
this._renderCanvasLazily = new LazilyEvaluatedFunction(this._renderCanvas.bind(this));
this._renderLabelsLazily = new LazilyEvaluatedFunction(this._renderLabels.bind(this));
}
render()
{
Instrumentation.startMeasuringTime('SingleBarGraph', 'render');
if (!this._values)
return false;
const range = this._group.range();
const showLabels = this._group.showLabels();
const canvas = this.content('graph');
const element = this.element();
const width = element.offsetWidth;
const height = element.offsetHeight;
const scale = this._resizeCanvasLazily.evaluate(canvas, width, height);
const step = this._renderCanvasLazily.evaluate(canvas, width, height, scale, this._values,
showLabels ? null : this._mean, showLabels ? null : this._interval, range);
this._renderLabelsLazily.evaluate(canvas, step, showLabels ? this._valueLabels : null);
Instrumentation.endMeasuringTime('SingleBarGraph', 'render');
}
_resizeCanvas(canvas, width, height)
{
const scale = window.devicePixelRatio;
canvas.width = width * scale;
canvas.height = height * scale;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
return scale;
}
_renderCanvas(canvas, width, height, scale, values, mean, interval, range)
{
const context = canvas.getContext('2d');
context.scale(scale, scale);
context.clearRect(0, 0, width, height);
context.fillStyle = this._barColor;
context.strokeStyle = this._meanIndicatorColor;
context.lineWidth = 1;
const step = Math.floor(height / values.length);
for (let i = 0; i < values.length; i++) {
const value = values[i];
if (isNaN(value))
continue;
const barWidth = range.xForValue(value, width);
const barRange = range.barRangeForValue(value, width);
const y = i * step;
context.fillRect(0, y, barWidth, step - 1);
}
const filteredValues = values.filter((value) => !isNaN(value));
if (mean) {
const x = range.xForValue(mean, width);
context.beginPath();
context.moveTo(x, 0);
context.lineTo(x, height);
context.stroke();
}
if (interval) {
const x1 = range.xForValue(interval[0], width);
const x2 = range.xForValue(interval[1], width);
const errorBarHeight = 10;
const endBarY1 = height / 2 - errorBarHeight / 2;
const endBarY2 = height / 2 + errorBarHeight / 2;
context.beginPath();
context.moveTo(x1, endBarY1);
context.lineTo(x1, endBarY2);
context.moveTo(x1, height / 2);
context.lineTo(x2, height / 2);
context.moveTo(x2, endBarY1);
context.lineTo(x2, endBarY2);
context.stroke();
}
return step;
}
_renderLabels(canvas, step, valueLabels)
{
if (!valueLabels)
valueLabels = [];
const element = ComponentBase.createElement;
this.renderReplace(this.content('labels'), valueLabels.map((label, i) => {
const labelElement = element('div', {class: 'label'}, label);
labelElement.style.top = (i + 0.5) * step + 'px';
return labelElement;
}));
canvas.style.opacity = valueLabels.length ? 0.5 : 1;
}
static htmlTemplate()
{
return `<canvas id="graph"></canvas><div id="labels"></div>`;
}
static cssTemplate()
{
return `
:host {
display: block !important;
overflow: hidden;
position: relative;
}
.label {
position: absolute;
left: 0;
width: 100%;
text-align: center;
font-size: 0.8rem;
line-height: 0.8rem;
margin-top: -0.4rem;
}
`;
}
}
ComponentBase.defineElement('bar-graph', BarGraph);