2014-10-03 02:43:56 +00:00
|
|
|
/*
|
|
|
|
|
|
|
|
The MIT License (MIT)
|
|
|
|
|
|
|
|
Copyright (c) 2014 Alex Yatskov
|
|
|
|
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
|
|
in the Software without restriction, including without limitation the rights
|
|
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
|
|
all copies or substantial portions of the Software.
|
|
|
|
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
|
THE SOFTWARE.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
(function(grapher) {
|
2014-09-28 07:58:56 +00:00
|
|
|
'use strict';
|
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
//
|
|
|
|
// Coord
|
|
|
|
//
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
function Coord(x, y) {
|
|
|
|
this.x = x;
|
|
|
|
this.y = y;
|
|
|
|
}
|
2014-09-27 01:05:48 +00:00
|
|
|
|
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
//
|
|
|
|
// Rect
|
|
|
|
//
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
function Rect(left, top, width, height) {
|
|
|
|
this.left = left;
|
|
|
|
this.top = top;
|
|
|
|
this.width = width;
|
|
|
|
this.height = height;
|
|
|
|
this.right = left + width;
|
|
|
|
this.bottom = top + height;
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
this.contains = function(coord) {
|
|
|
|
var contained =
|
|
|
|
coord.x >= this.left &&
|
|
|
|
coord.x < this.right &&
|
|
|
|
coord.y >= this.top &&
|
|
|
|
coord.y < this.bottom;
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
return contained;
|
|
|
|
};
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
this.intersection = function(rect) {
|
|
|
|
var left = Math.max(this.left, rect.left);
|
|
|
|
var top = Math.max(this.top, rect.top);
|
|
|
|
var right = Math.min(this.right, rect.right);
|
|
|
|
var bottom = Math.min(this.bottom, rect.bottom);
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
return new Rect(left, top, right - left, bottom - top);
|
|
|
|
};
|
|
|
|
}
|
2014-09-27 01:05:48 +00:00
|
|
|
|
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
//
|
|
|
|
// Range
|
|
|
|
//
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
function Range(start, end) {
|
|
|
|
this.start = start;
|
|
|
|
this.end = end;
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
this.containsPoint = function(value) {
|
|
|
|
return value >= this.start && value <= this.end;
|
|
|
|
};
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
this.getLength = function() {
|
|
|
|
return this.end - this.start;
|
|
|
|
};
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
this.clamp = function(value) {
|
|
|
|
if (value < this.start) {
|
|
|
|
return this.start;
|
|
|
|
}
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
if (value > this.end) {
|
|
|
|
return this.end;
|
|
|
|
}
|
2014-09-27 01:05:48 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
return value;
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-10-31 02:00:43 +00:00
|
|
|
|
|
|
|
this.include = function(range) {
|
|
|
|
this.start = Math.min(this.start, range.start);
|
|
|
|
this.end = Math.max(this.end, range.end);
|
|
|
|
};
|
2014-09-27 01:05:48 +00:00
|
|
|
}
|
2014-07-08 04:35:52 +00:00
|
|
|
|
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
//
|
|
|
|
// Column
|
|
|
|
//
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-10-31 02:15:51 +00:00
|
|
|
function Column(params) {
|
|
|
|
this.updateShapes = function() {
|
2014-10-31 07:08:47 +00:00
|
|
|
var gradient = this.canvas.gradient(this.decimateHints());
|
2014-10-31 03:11:48 +00:00
|
|
|
|
2014-10-31 06:58:25 +00:00
|
|
|
// function logger(e, x, y) {
|
|
|
|
// var rect = e.srcElement;
|
|
|
|
// console.log(rect.getBoundingClientRect());
|
|
|
|
// }
|
2014-10-31 06:25:59 +00:00
|
|
|
|
|
|
|
var backdrop = this.canvas.rect(
|
|
|
|
this.tickSize,
|
|
|
|
0,
|
|
|
|
this.width - (this.densitySize + this.tickSize),
|
|
|
|
this.height - this.panelSize
|
|
|
|
).attr({'stroke': '#d3d7cf', 'fill': '#eeeeec'});
|
|
|
|
|
|
|
|
var value = this.canvas.rect(
|
|
|
|
this.tickSize,
|
|
|
|
0,
|
|
|
|
this.width - (this.densitySize + this.tickSize),
|
|
|
|
50
|
|
|
|
).attr({'fill': '#cc0000'});
|
|
|
|
|
|
|
|
var density = this.canvas.rect(
|
|
|
|
this.width - this.densitySize,
|
|
|
|
0,
|
|
|
|
this.densitySize,
|
|
|
|
this.height - this.panelSize
|
|
|
|
).attr({'stroke': '#d3d7cf', 'fill': gradient});
|
|
|
|
|
|
|
|
var tick = this.canvas.line(
|
|
|
|
0,
|
|
|
|
(this.height - this.panelSize) / 2,
|
|
|
|
this.tickSize,
|
|
|
|
(this.height - this.panelSize) / 2
|
|
|
|
).attr({'stroke': '#888a85'});
|
|
|
|
|
|
|
|
var panel = this.canvas.rect(
|
|
|
|
this.tickSize,
|
|
|
|
this.height - this.panelSize,
|
|
|
|
this.width - this.tickSize,
|
|
|
|
this.panelSize
|
|
|
|
).attr({'fill': '#d3d7cf'});
|
|
|
|
|
|
|
|
var label = this.canvas.text(
|
|
|
|
this.tickSize + (this.width - this.tickSize) / 2,
|
|
|
|
this.height - this.panelSize / 2,
|
|
|
|
this.name
|
|
|
|
).attr({'dominant-baseline': 'middle', 'text-anchor': 'middle'});
|
|
|
|
|
|
|
|
var group = this.canvas.group(backdrop, value, density, panel, tick, label);
|
2014-10-31 06:58:25 +00:00
|
|
|
group.transform(Snap.format('t{x},{y}', {x: this.index * (this.width + this.padding), y: 0}));
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-10-31 07:08:47 +00:00
|
|
|
this.decimateHints = function() {
|
2014-10-31 06:58:25 +00:00
|
|
|
var colorStops = 'l(0,0,0,1)';
|
2014-10-31 07:08:47 +00:00
|
|
|
|
|
|
|
var groups = this.groupHints();
|
2014-10-31 02:15:51 +00:00
|
|
|
for (var i = 0, count = groups.length; i < count; ++i) {
|
|
|
|
var groupSize = groups[i];
|
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
var colorPercent = 0;
|
2014-10-31 07:08:47 +00:00
|
|
|
if (this.scale.getLength() > 0) {
|
|
|
|
colorPercent = Math.max(0, groupSize - this.scale.start) / this.scale.getLength();
|
2014-09-27 01:10:45 +00:00
|
|
|
}
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
var colorByte = 0xff - Math.min(0xff, Math.round(0xff * colorPercent));
|
|
|
|
var colorObj = tinycolor({ r: colorByte, g: colorByte, b: colorByte });
|
|
|
|
var colorStr = colorObj.toHexString();
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-10-31 06:58:25 +00:00
|
|
|
colorStops += colorStr;
|
|
|
|
if (i + 1 < count) {
|
|
|
|
colorStops += '-';
|
|
|
|
}
|
2014-10-31 02:15:51 +00:00
|
|
|
}
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
return colorStops;
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-10-31 07:08:47 +00:00
|
|
|
this.groupHints = function() {
|
|
|
|
var stepSize = this.range.getLength() / this.steps;
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
var hintGroups = [];
|
2014-10-31 07:08:47 +00:00
|
|
|
for (var i = 0; i < this.steps; ++i) {
|
2014-09-27 01:10:45 +00:00
|
|
|
var stepMax = this.range.end - stepSize * i;
|
|
|
|
var stepMin = stepMax - stepSize;
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
var hintCount = 0;
|
2014-10-31 07:08:47 +00:00
|
|
|
for (var j = 0, count = this.data.hints.length; j < count; ++j) {
|
|
|
|
var hint = this.data.hints[j];
|
2014-09-27 01:10:45 +00:00
|
|
|
if (hint.sample > stepMin && hint.sample <= stepMax) {
|
|
|
|
hintCount += hint.count;
|
|
|
|
}
|
2014-09-28 07:58:56 +00:00
|
|
|
}
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
hintGroups.push(hintCount);
|
|
|
|
}
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
return hintGroups;
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
this.setClampedValue = function(value, final) {
|
|
|
|
this.value = this.range.clamp(value);
|
|
|
|
this.updateShapes(final);
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
if (this.onValueChanged && final) {
|
|
|
|
this.onValueChanged(this.name, this.value);
|
|
|
|
}
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-10-31 02:15:51 +00:00
|
|
|
this.update = function(data, scale) {
|
|
|
|
this.data = data;
|
2014-09-27 01:10:45 +00:00
|
|
|
this.scale = scale;
|
|
|
|
this.updateShapes(true);
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
this.valueColorAdjust = function(color, offset) {
|
|
|
|
var colorObj = tinycolor(color);
|
|
|
|
var rangeEnd = this.value >= 0.0 ? this.range.end : this.range.start;
|
|
|
|
var rangeMid = (this.range.start + this.range.end) / 2.0;
|
|
|
|
var rangeRat = (this.value - rangeMid) / (rangeEnd - rangeMid);
|
|
|
|
var desatVal = Math.max(0.0, 1.0 - rangeRat + offset) * 100.0;
|
|
|
|
return colorObj.desaturate(desatVal).toHexString();
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-09-25 09:56:04 +00:00
|
|
|
|
2014-10-31 02:15:51 +00:00
|
|
|
this.computeFillColor = function() {
|
2014-09-27 01:10:45 +00:00
|
|
|
var color = this.value >= 0.0 ? this.fillColorPos : this.fillColorNeg;
|
|
|
|
return this.valueColorAdjust(color, this.desatOffset);
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-09-25 07:47:56 +00:00
|
|
|
|
2014-10-31 02:15:51 +00:00
|
|
|
this.computeHandleColor = function() {
|
2014-09-27 01:10:45 +00:00
|
|
|
var color = this.value >= 0.0 ? this.handleColorPos : this.handleColorNeg;
|
|
|
|
return this.valueColorAdjust(color, this.desatOffset);
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-09-25 07:47:56 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
this.State = {
|
|
|
|
NORMAL: 0,
|
|
|
|
HOVER: 1,
|
|
|
|
DRAG: 2
|
|
|
|
};
|
|
|
|
|
|
|
|
this.desatOffset = -0.3;
|
2014-10-31 06:25:59 +00:00
|
|
|
this.handleSize = 10;
|
|
|
|
this.densitySize = 10;
|
|
|
|
this.panelSize = 20;
|
|
|
|
this.tickSize = 5;
|
|
|
|
this.width = 125;
|
|
|
|
this.height = 500;
|
|
|
|
this.padding = 10;
|
2014-09-27 01:10:45 +00:00
|
|
|
this.emptyColor = '#eeeeec';
|
|
|
|
this.strokeColor = '#d3d7cf';
|
|
|
|
this.tickColor = '#888a85';
|
|
|
|
this.fillColorNeg = '#3465a4';
|
|
|
|
this.fillColorPos = '#cc0000';
|
|
|
|
this.handleColorNeg = '#204a87';
|
|
|
|
this.handleColorPos = '#a40000';
|
|
|
|
|
2014-10-31 02:15:51 +00:00
|
|
|
this.canvas = params.canvas;
|
|
|
|
this.name = params.name;
|
|
|
|
this.data = params.data;
|
|
|
|
this.scale = params.scale;
|
|
|
|
this.range = params.range;
|
2014-10-31 06:58:25 +00:00
|
|
|
this.steps = params.steps;
|
2014-10-31 06:25:59 +00:00
|
|
|
this.index = params.index;
|
2014-09-27 01:10:45 +00:00
|
|
|
this.state = this.State.NORMAL;
|
|
|
|
|
2014-10-31 02:15:51 +00:00
|
|
|
this.updateShapes();
|
2014-07-08 04:35:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
//
|
|
|
|
// Grapher
|
|
|
|
//
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-10-31 02:00:43 +00:00
|
|
|
grapher.Grapher = function(params) {
|
2014-09-27 01:10:45 +00:00
|
|
|
this.setColumns = function(columns) {
|
|
|
|
var scale = 0;
|
|
|
|
if (!useLocalScale) {
|
2014-10-31 02:00:43 +00:00
|
|
|
var hintData = _.pluck(columns, 'hints');
|
|
|
|
scale = this.computeGlobalScale(hintData);
|
2014-09-27 01:10:45 +00:00
|
|
|
}
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-10-31 06:25:59 +00:00
|
|
|
var index = 0;
|
2014-10-31 02:00:43 +00:00
|
|
|
for (var name in columns) {
|
2014-10-31 06:25:59 +00:00
|
|
|
var data = this.data[name] = columns[name];
|
2014-09-27 01:10:45 +00:00
|
|
|
if (useLocalScale) {
|
2014-10-31 02:51:11 +00:00
|
|
|
scale = this.computeLocalScale(data.hints);
|
2014-09-27 01:10:45 +00:00
|
|
|
}
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-10-31 02:00:43 +00:00
|
|
|
var column = this.columns[name];
|
|
|
|
if (column) {
|
2014-10-31 02:15:51 +00:00
|
|
|
column.update(data, scale);
|
2014-09-27 01:10:45 +00:00
|
|
|
}
|
2014-10-31 02:00:43 +00:00
|
|
|
else {
|
2014-10-31 02:51:11 +00:00
|
|
|
this.columns[name] = new Column({
|
2014-10-31 06:25:59 +00:00
|
|
|
canvas: this.canvas,
|
|
|
|
width: this.columnWidth,
|
|
|
|
height: this.columnHeight,
|
|
|
|
padding: this.columnPadding,
|
2014-10-31 06:58:25 +00:00
|
|
|
steps: this.steps,
|
2014-10-31 06:25:59 +00:00
|
|
|
range: this.range,
|
2014-10-31 06:58:25 +00:00
|
|
|
data: data,
|
2014-10-31 06:25:59 +00:00
|
|
|
name: name,
|
|
|
|
scale: scale,
|
|
|
|
index: index++,
|
2014-10-31 02:51:11 +00:00
|
|
|
});
|
2014-10-31 02:00:43 +00:00
|
|
|
}
|
|
|
|
}
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
this.setUseLocalScale = function(useLocalScale) {
|
|
|
|
if (useLocalScale != this.useLocalScale) {
|
|
|
|
this.useLocalScale = useLocalScale;
|
2014-10-31 06:58:25 +00:00
|
|
|
this.setColumns(this.data);
|
2014-09-27 01:10:45 +00:00
|
|
|
}
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
this.setUseRelativeScale = function(useRelativeScale) {
|
|
|
|
if (useRelativeScale != this.useRelativeScale) {
|
|
|
|
this.useRelativeScale = useRelativeScale;
|
2014-10-31 06:58:25 +00:00
|
|
|
this.setColumns(this.data);
|
2014-09-27 01:10:45 +00:00
|
|
|
}
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
this.setValueChangedListener = function(listener) {
|
2014-10-31 02:00:43 +00:00
|
|
|
for (var name in this.columns) {
|
|
|
|
this.columns[name].onValueChanged = listener;
|
|
|
|
}
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-10-31 02:00:43 +00:00
|
|
|
this.computeLocalScale = function(hints) {
|
2014-09-27 01:10:45 +00:00
|
|
|
var counts = _.pluck(hints, 'count');
|
|
|
|
var min = this.useRelativeScale ? _.min(counts) : 0;
|
|
|
|
return new Range(min, _.max(counts));
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-10-31 02:00:43 +00:00
|
|
|
this.computeGlobalScale = function(hintData) {
|
2014-09-27 01:10:45 +00:00
|
|
|
var globalScale = null;
|
2014-10-31 02:00:43 +00:00
|
|
|
for (var i = 0, count = hintData.length; i < count; ++i) {
|
|
|
|
var localScale = this.computeLocalScale(hintData[i]);
|
2014-09-27 01:10:45 +00:00
|
|
|
if (globalScale) {
|
2014-10-31 02:00:43 +00:00
|
|
|
globalScale.include(localScale);
|
2014-09-27 01:10:45 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
globalScale = localScale;
|
|
|
|
}
|
2014-10-31 02:00:43 +00:00
|
|
|
}
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-09-27 01:10:45 +00:00
|
|
|
return globalScale;
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-07-08 04:35:52 +00:00
|
|
|
|
2014-10-31 02:00:43 +00:00
|
|
|
this.canvas = params.canvas;
|
|
|
|
this.columns = {};
|
2014-10-31 06:25:59 +00:00
|
|
|
this.data = {};
|
|
|
|
this.range = new Range(-1.0, 1.0);
|
2014-10-31 06:58:25 +00:00
|
|
|
this.steps = params.steps || 20;
|
2014-10-31 02:00:43 +00:00
|
|
|
this.useLocalScale = params.useLocalScale || true;
|
|
|
|
this.useRelativeScale = params.useRelativeScale || true;
|
2014-09-28 07:58:56 +00:00
|
|
|
};
|
2014-09-27 01:10:45 +00:00
|
|
|
}(window.grapher = window.grapher || {}));
|