Renaming directory
1
client/bootstrap
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../libs/bootstrap
|
1
client/closure-library
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../libs/closure-library
|
1
client/data.json
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../scrape/data.json
|
1
client/fabric.js
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../libs/fabric.js
|
558
client/grapher.js
Normal file
@ -0,0 +1,558 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
goog.require('goog.color');
|
||||||
|
goog.require('goog.math');
|
||||||
|
goog.require('goog.math.Coordinate');
|
||||||
|
goog.require('goog.math.Range');
|
||||||
|
goog.require('goog.math.Rect');
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Column
|
||||||
|
//
|
||||||
|
|
||||||
|
function Column(canvas, name, params, scale, range, bounds) {
|
||||||
|
this.clearShapes = function() {
|
||||||
|
_.each(this.shapes, function(shape) {
|
||||||
|
this.canvas.remove(shape);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.shapes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateShapes = function(final) {
|
||||||
|
this.labelBounds = this.getLabelBounds(this.bounds);
|
||||||
|
this.columnBounds = this.getColumnBounds(this.bounds);
|
||||||
|
this.hintBounds = this.getHintBounds(this.columnBounds);
|
||||||
|
this.fillBounds = this.getFillBounds(this.columnBounds);
|
||||||
|
this.handleBounds = this.getHandleBounds(this.columnBounds, this.fillBounds);
|
||||||
|
|
||||||
|
if (final) {
|
||||||
|
this.updateRect('boundsRect', {
|
||||||
|
left: this.bounds.left,
|
||||||
|
top: this.bounds.top,
|
||||||
|
width: this.bounds.width,
|
||||||
|
height: this.bounds.height,
|
||||||
|
stroke: this.strokeColor,
|
||||||
|
fill: this.emptyColor
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateRect('hintRect', {
|
||||||
|
left: this.hintBounds.left,
|
||||||
|
top: this.hintBounds.top,
|
||||||
|
width: this.hintBounds.width,
|
||||||
|
height: this.hintBounds.height,
|
||||||
|
stroke: this.strokeColor
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hintRect.setGradient('fill', {
|
||||||
|
x1: 0.0,
|
||||||
|
y1: -this.hintRect.height / 2,
|
||||||
|
x2: 0.0,
|
||||||
|
y2: this.hintRect.height / 2,
|
||||||
|
colorStops: this.decimateHints(this.steps, this.scale)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateRect('labelRect', {
|
||||||
|
left: this.labelBounds.left,
|
||||||
|
top: this.labelBounds.top,
|
||||||
|
width: this.labelBounds.width,
|
||||||
|
height: this.labelBounds.height,
|
||||||
|
fill: this.strokeColor
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateText('label', this.name, {
|
||||||
|
left: this.fillBounds.left + this.fillBounds.width / 2,
|
||||||
|
top: this.labelBounds.top + this.labelBounds.height / 2,
|
||||||
|
fontSize: this.labelFontSize,
|
||||||
|
originX: 'center',
|
||||||
|
originY: 'center',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateRect('fillRect', {
|
||||||
|
left: this.fillBounds.left,
|
||||||
|
top: this.fillBounds.top,
|
||||||
|
width: this.fillBounds.width,
|
||||||
|
height: this.fillBounds.height,
|
||||||
|
fill: this.fillColor
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateRect('handleRect', {
|
||||||
|
left: this.handleBounds.left,
|
||||||
|
top: this.handleBounds.top,
|
||||||
|
width: this.handleBounds.width,
|
||||||
|
height: this.handleBounds.height,
|
||||||
|
fill: this.handleColor
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canvas.renderAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateRect = function(name, args) {
|
||||||
|
if (name in this) {
|
||||||
|
this[name].set(args);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var rect = new fabric.Rect(args);
|
||||||
|
this.canvas.add(rect);
|
||||||
|
this.shapes.push(rect);
|
||||||
|
this[name] = rect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateText = function(name, text, args) {
|
||||||
|
if (name in this) {
|
||||||
|
this[name].set(args);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var text = new fabric.Text(text, args);
|
||||||
|
this.canvas.add(text);
|
||||||
|
this.shapes.push(text);
|
||||||
|
this[name] = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateColor = function(color) {
|
||||||
|
var fillColorRgb = goog.color.hexToRgb(goog.color.parse(color).hex);
|
||||||
|
var handleColorRgb = goog.color.darken(fillColorRgb, this.handleDarken);
|
||||||
|
|
||||||
|
this.fillColor = goog.color.rgbToHex.apply(this, fillColorRgb);
|
||||||
|
this.handleColor = goog.color.rgbToHex.apply(this, handleColorRgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.decimateHints = function(steps, scale) {
|
||||||
|
var groups = this.groupHints(steps);
|
||||||
|
|
||||||
|
var colorStops = {};
|
||||||
|
_.each(groups, function(count, index) {
|
||||||
|
var colorPercent = 0;
|
||||||
|
if (scale.getLength() > 0) {
|
||||||
|
colorPercent = Math.max(0, count - scale.start) / scale.getLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorByte = 0xff - Math.min(0xff, Math.round(0xff * colorPercent));
|
||||||
|
var colorStr = goog.color.rgbToHex(colorByte, colorByte, colorByte);
|
||||||
|
|
||||||
|
colorStops[index / steps] = colorStr;
|
||||||
|
});
|
||||||
|
|
||||||
|
return colorStops;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.groupHints = function(steps) {
|
||||||
|
var stepSize = this.range.getLength() / steps;
|
||||||
|
|
||||||
|
var hintGroups = [];
|
||||||
|
for (var i = 0; i < steps; ++i) {
|
||||||
|
var stepMax = this.range.end - stepSize * i;
|
||||||
|
var stepMin = stepMax - stepSize;
|
||||||
|
|
||||||
|
var hintCount = 0;
|
||||||
|
_.each(this.hints, function(hint) {
|
||||||
|
if (hint.sample > stepMin && hint.sample <= stepMax) {
|
||||||
|
hintCount += hint.count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
hintGroups.push(hintCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hintGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setClampedValue = function(value, final) {
|
||||||
|
this.value = goog.math.clamp(value, this.range.start, this.range.end);
|
||||||
|
this.updateShapes(final);
|
||||||
|
|
||||||
|
if (this.onValueChanged && final) {
|
||||||
|
this.onValueChanged(this.name, this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setHints = function(hints, scale) {
|
||||||
|
this.hints = hints;
|
||||||
|
this.scale = scale;
|
||||||
|
this.updateShapes(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getLabelBounds = function(bounds) {
|
||||||
|
return new goog.math.Rect(
|
||||||
|
bounds.left,
|
||||||
|
bounds.top + bounds.height - this.labelSize,
|
||||||
|
bounds.width,
|
||||||
|
this.labelSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getColumnBounds = function(bounds) {
|
||||||
|
return new goog.math.Rect(
|
||||||
|
bounds.left,
|
||||||
|
bounds.top,
|
||||||
|
bounds.width,
|
||||||
|
bounds.height - this.labelSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getHintBounds = function(bounds) {
|
||||||
|
return new goog.math.Rect(
|
||||||
|
bounds.left + bounds.width - this.hintSize,
|
||||||
|
bounds.top,
|
||||||
|
this.hintSize,
|
||||||
|
bounds.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getFillBounds = function(bounds) {
|
||||||
|
var fill = (this.value - this.range.start) / this.range.getLength();
|
||||||
|
var height = bounds.height * (1.0 - goog.math.clamp(fill, 0.0, 1.0));
|
||||||
|
return new goog.math.Rect(
|
||||||
|
bounds.left,
|
||||||
|
bounds.top + height,
|
||||||
|
bounds.width - this.hintSize,
|
||||||
|
bounds.height - height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getHandleBounds = function(bounds, fillBounds) {
|
||||||
|
var handleBounds = new goog.math.Rect(
|
||||||
|
fillBounds.left,
|
||||||
|
fillBounds.top,
|
||||||
|
fillBounds.width,
|
||||||
|
this.handleSize
|
||||||
|
);
|
||||||
|
handleBounds.intersection(bounds);
|
||||||
|
return handleBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseDown = function(position) {
|
||||||
|
if (this.isGrabbing(position)) {
|
||||||
|
this.stateTransition(this.State.DRAG, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseUp = function(position) {
|
||||||
|
this.stateTransition(
|
||||||
|
this.isHovering(position) ? this.State.HOVER : this.State.NORMAL,
|
||||||
|
position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseMove = function(position) {
|
||||||
|
switch (this.state) {
|
||||||
|
case this.State.NORMAL:
|
||||||
|
if (this.isHovering(position)) {
|
||||||
|
this.stateTransition(this.State.HOVER, position);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case this.State.HOVER:
|
||||||
|
if (!this.isHovering(position)) {
|
||||||
|
this.stateTransition(this.State.NORMAL, position);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stateUpdate(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseOut = function(position) {
|
||||||
|
this.mouseUp(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseDoubleClick = function(position) {
|
||||||
|
if (this.isContained(position)) {
|
||||||
|
this.setClampedValue(this.getValueFromPos(position), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getValueFromPos = function(position) {
|
||||||
|
var percent = 1.0 - (position.y - this.columnBounds.top) / this.columnBounds.height;
|
||||||
|
return this.range.start + this.range.getLength() * percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isHovering = function(position) {
|
||||||
|
return this.isGrabbing(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isGrabbing = function(position) {
|
||||||
|
return this.handleBounds.contains(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isContained = function(position) {
|
||||||
|
return this.columnBounds.contains(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stateUpdate = function(position) {
|
||||||
|
switch (this.state) {
|
||||||
|
case this.State.DRAG:
|
||||||
|
this.setClampedValue(this.getValueFromPos(position) + this.dragDelta, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stateTransition = function(state, position) {
|
||||||
|
if (state == this.state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.state) {
|
||||||
|
case this.State.DRAG:
|
||||||
|
this.setClampedValue(this.getValueFromPos(position) + this.dragDelta, true);
|
||||||
|
case this.State.HOVER:
|
||||||
|
if (state == this.State.NORMAL) {
|
||||||
|
this.canvas.contextContainer.canvas.style.cursor = 'default';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case this.State.DRAG:
|
||||||
|
this.dragDelta = this.value - this.getValueFromPos(position);
|
||||||
|
case this.State.HOVER:
|
||||||
|
this.canvas.contextContainer.canvas.style.cursor = 'ns-resize';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.State = {
|
||||||
|
NORMAL: 0,
|
||||||
|
HOVER: 1,
|
||||||
|
DRAG: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleSize = 10;
|
||||||
|
this.handleDarken = 0.25;
|
||||||
|
this.hintSize = 10;
|
||||||
|
this.labelFontSize = 15;
|
||||||
|
this.labelSize = 20;
|
||||||
|
this.emptyColor = '#eeeeec';
|
||||||
|
this.strokeColor = '#d3d7cf';
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.shapes = [];
|
||||||
|
this.name = name;
|
||||||
|
this.value = params.value;
|
||||||
|
this.hints = params.hints;
|
||||||
|
this.steps = params.steps;
|
||||||
|
this.scale = scale;
|
||||||
|
this.range = range;
|
||||||
|
this.bounds = bounds;
|
||||||
|
this.state = this.State.NORMAL;
|
||||||
|
|
||||||
|
this.updateColor(params.color);
|
||||||
|
this.updateShapes(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Grapher
|
||||||
|
//
|
||||||
|
|
||||||
|
function Grapher(canvas, range, useLocalScale, useRelativeScale) {
|
||||||
|
this.setColumns = function(columns) {
|
||||||
|
this.clearColumns();
|
||||||
|
|
||||||
|
var graphBounds = this.getGraphBounds(this.getCanvasBounds());
|
||||||
|
var columnCount = _.keys(columns).length;
|
||||||
|
|
||||||
|
var scale = 0;
|
||||||
|
if (!useLocalScale) {
|
||||||
|
var hintData = {};
|
||||||
|
_.each(columns, function(columnValue, columnName) {
|
||||||
|
hintData[columnName] = columnValue.hints || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
scale = this.getGlobalScale(hintData);
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
var that = this;
|
||||||
|
_.each(columns, function(columnValue, columnName) {
|
||||||
|
if (useLocalScale) {
|
||||||
|
scale = that.getLocalScale(columnValue.hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
var columnBounds = that.getColumnBounds(graphBounds, index, columnCount);
|
||||||
|
that.columns.push(new Column(that.canvas, columnName, columnValue, scale, that.range, columnBounds));
|
||||||
|
that.indexMap[columnName] = index++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearColumns = function() {
|
||||||
|
_.each(this.columns, function(column) {
|
||||||
|
column.clearShapes();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.columns = [];
|
||||||
|
this.indexMap = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setColumnHints = function(hintData) {
|
||||||
|
var scale = 0;
|
||||||
|
if (!this.useLocalScale) {
|
||||||
|
scale = this.getGlobalScale(hintData);
|
||||||
|
}
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
_.each(hintData, function(hints, name) {
|
||||||
|
var index = that.getColumnIndex(name);
|
||||||
|
console.assert(index >= 0);
|
||||||
|
|
||||||
|
if (that.useLocalScale) {
|
||||||
|
scale = that.getLocalScale(hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
that.columns[index].setHints(hints, scale);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setUseLocalScale = function(useLocalScale) {
|
||||||
|
if (useLocalScale != this.useLocalScale) {
|
||||||
|
this.useLocalScale = useLocalScale;
|
||||||
|
this.invalidateHints();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setUseRelativeScale = function(useRelativeScale) {
|
||||||
|
if (useRelativeScale != this.useRelativeScale) {
|
||||||
|
this.useRelativeScale = useRelativeScale;
|
||||||
|
this.invalidateHints();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.invalidateHints = function() {
|
||||||
|
var hintData = {};
|
||||||
|
_.each(this.columns, function(column) {
|
||||||
|
hintData[column.name] = column.hints;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setColumnHints(hintData);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setValueChangedListener = function(listener) {
|
||||||
|
_.each(this.columns, function(column) {
|
||||||
|
column.onValueChanged = listener;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getLocalScale = function(hints) {
|
||||||
|
var counts = _.pluck(hints, 'count');
|
||||||
|
var min = this.useRelativeScale ? _.min(counts) : 0;
|
||||||
|
return new goog.math.Range(min, _.max(counts));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getGlobalScale = function(hintData) {
|
||||||
|
var that = this;
|
||||||
|
var globalScale = null;
|
||||||
|
|
||||||
|
_.each(hintData, function(hints) {
|
||||||
|
var localScale = that.getLocalScale(hints);
|
||||||
|
if (globalScale) {
|
||||||
|
globalScale.includeRange(localScale);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
globalScale = localScale;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return globalScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getColumnCount = function() {
|
||||||
|
return this.columns.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getColumnIndex = function(name) {
|
||||||
|
console.assert(name in this.indexMap);
|
||||||
|
return this.indexMap[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getColumnName = function(index) {
|
||||||
|
return this.columns[index].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getColumnNames = function() {
|
||||||
|
return _.pluck(this.columns, 'name');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getCanvasBounds = function() {
|
||||||
|
return new goog.math.Rect(0, 0, this.canvas.width - 1, this.canvas.height - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getGraphBounds = function(bounds) {
|
||||||
|
return this.getCanvasBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getColumnBounds = function(bounds, index, count) {
|
||||||
|
var secWidth = bounds.width / count;
|
||||||
|
var columnWidth = secWidth - this.padding * 2;
|
||||||
|
|
||||||
|
return new goog.math.Rect(
|
||||||
|
bounds.left + secWidth * index + this.padding,
|
||||||
|
bounds.top,
|
||||||
|
columnWidth,
|
||||||
|
bounds.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getMousePos = function(e) {
|
||||||
|
var rect = e.target.getBoundingClientRect();
|
||||||
|
return new goog.math.Coordinate(
|
||||||
|
e.clientX - rect.left,
|
||||||
|
e.clientY - rect.top
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseDown = function(e) {
|
||||||
|
var position = this.grapher.getMousePos(e);
|
||||||
|
_.each(this.grapher.columns, function(column) {
|
||||||
|
column.mouseDown(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseUp = function(e) {
|
||||||
|
var position = this.grapher.getMousePos(e);
|
||||||
|
_.each(this.grapher.columns, function(column) {
|
||||||
|
column.mouseUp(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseMove = function(e) {
|
||||||
|
var position = this.grapher.getMousePos(e);
|
||||||
|
_.each(this.grapher.columns, function(column) {
|
||||||
|
column.mouseMove(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseOut = function(e) {
|
||||||
|
var position = this.grapher.getMousePos(e);
|
||||||
|
_.each(this.grapher.columns, function(column) {
|
||||||
|
column.mouseOut(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseDoubleClick = function(e) {
|
||||||
|
var position = this.grapher.getMousePos(e);
|
||||||
|
_.each(this.grapher.columns, function(column) {
|
||||||
|
column.mouseDoubleClick(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.useLocalScale = useLocalScale;
|
||||||
|
this.useRelativeScale = useRelativeScale;
|
||||||
|
this.canvas = new fabric.StaticCanvas(canvas);
|
||||||
|
this.range = range;
|
||||||
|
this.padding = 10;
|
||||||
|
this.indexMap = {};
|
||||||
|
this.columns = [];
|
||||||
|
|
||||||
|
var c = this.canvas.contextContainer.canvas;
|
||||||
|
c.addEventListener('mousedown', this.mouseDown, false);
|
||||||
|
c.addEventListener('mouseup', this.mouseUp, false);
|
||||||
|
c.addEventListener('mousemove', this.mouseMove, false);
|
||||||
|
c.addEventListener('mouseout', this.mouseOut, false);
|
||||||
|
c.addEventListener('dblclick', this.mouseDoubleClick, false);
|
||||||
|
c.grapher = this;
|
||||||
|
}
|
1
client/handlebars.js
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../libs/handlebars.js/
|
125
client/index.html
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Multidimensional Search Forecasting</title>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
|
<link rel="stylesheet" href="bootstrap/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="bootstrap/css/bootstrap-theme.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Multidimensional Search Forecasting</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="input">
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="query" class="col-md-2 control-label">Keyword</label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<select id="query" class="form-control" name="query"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="minScore" class="col-md-2 control-label">Minimum score</label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input type="number" class="form-control" value="1.0" id="minScore" name="minScore">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="hintSteps" class="col-md-2 control-label">Hint steps</label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input class="form-control" type="number" value="20" id="hintSteps" name="hintSteps">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="maxResults" class="col-md-2 control-label">Max results</label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input class="form-control" type="number" value="100" id="maxResults" name="maxResults">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-offset-2 col-md-10">
|
||||||
|
<button class="btn btn-primary" id="search" type="button">
|
||||||
|
<span class="glyphicon glyphicon-search"></span> Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="output" style="display: none;">
|
||||||
|
<form>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><big>Semantic tweaks to <span id="keyword" class="text-primary"></span></big></div>
|
||||||
|
<div class="row" style="padding: 10px;">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<canvas id="grapher" width="500" height="550"></canvas>
|
||||||
|
<label class="checkbox-inline">
|
||||||
|
<input type="checkbox" id="useLocalScale" name="useLocalScale"> Use local scale
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-inline">
|
||||||
|
<input type="checkbox" id="useRelativeScale" name="useRelativeScale"> Use relative scale
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="text-center">
|
||||||
|
<canvas id="plotter" width="350" height="350" style="border: 1px #d3d7cf solid;"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label" for="plotAxisX">Plotter X axis</label>
|
||||||
|
<select class="form-control plotAxes" name="plotAxisX" id="plotAxisX"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label" for="plotAxisY">Plotter Y axis</label>
|
||||||
|
<select class="form-control plotAxes" name="plotAxisY" id="plotAxisY"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><big>Query results (<span id="count" class="text-primary"></span>)</big></div>
|
||||||
|
<div style="padding: 10px;">
|
||||||
|
<script id="template" type="text/x-handlers-template">
|
||||||
|
{{#if results}}
|
||||||
|
<table class="table table-striped table-condensed">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Score</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{{#each results}}
|
||||||
|
<tr>
|
||||||
|
<td>{{@index}}</td>
|
||||||
|
<td><a href="{{url}}">{{name}}</a></td>
|
||||||
|
<td>{{score}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
{{/if}}
|
||||||
|
</script>
|
||||||
|
<div id="results"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="underscore/underscore-min.js"></script>
|
||||||
|
<script src="handlebars.js/handlebars.min.js"></script>
|
||||||
|
<script src="closure-library/closure/goog/base.js"></script>
|
||||||
|
<script src="jquery/jquery.min.js"></script>
|
||||||
|
<script src="bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
<script src="fabric.js/dist/fabric.min.js"></script>
|
||||||
|
|
||||||
|
<script src="grapher.js"></script>
|
||||||
|
<script src="plotter.js"></script>
|
||||||
|
<script src="projection.js"></script>
|
||||||
|
<script src="keywords.json"></script>
|
||||||
|
<script src="data.json"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
1
client/jquery
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../libs/jquery/
|
38
client/keywords.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
var DATA_KEYWORDS = {
|
||||||
|
"hole in the wall": {
|
||||||
|
"food": 0.82,
|
||||||
|
"service": 0.10,
|
||||||
|
"value": 0.22,
|
||||||
|
"atmosphere": -0.79
|
||||||
|
},
|
||||||
|
"high class": {
|
||||||
|
"food": 0.93,
|
||||||
|
"service": 0.80,
|
||||||
|
"value": -0.05,
|
||||||
|
"atmosphere": 0.95
|
||||||
|
},
|
||||||
|
"cheap grub": {
|
||||||
|
"food": -0.42,
|
||||||
|
"service": -0.64,
|
||||||
|
"value": 0.87,
|
||||||
|
"atmosphere": -0.91
|
||||||
|
},
|
||||||
|
"ripoff": {
|
||||||
|
"food": -0.82,
|
||||||
|
"service": -0.64,
|
||||||
|
"value": -0.98,
|
||||||
|
"atmosphere": -0.23
|
||||||
|
},
|
||||||
|
"uninspired": {
|
||||||
|
"food": -0.83,
|
||||||
|
"service": 0.0,
|
||||||
|
"value": 0.0,
|
||||||
|
"atmosphere": -0.68
|
||||||
|
},
|
||||||
|
"moody": {
|
||||||
|
"food": 0.32,
|
||||||
|
"service": -0.33,
|
||||||
|
"value": -0.42,
|
||||||
|
"atmosphere": 0.61
|
||||||
|
}
|
||||||
|
};
|
108
client/plotter.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
goog.require('goog.color');
|
||||||
|
goog.require('goog.math');
|
||||||
|
goog.require('goog.math.Coordinate');
|
||||||
|
goog.require('goog.math.Range');
|
||||||
|
|
||||||
|
function Plotter(canvas, useRelativeScale) {
|
||||||
|
this.setRange = function(rangeX, rangeY) {
|
||||||
|
this.rangeX = rangeX;
|
||||||
|
this.rangeY = rangeY;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setData = function(data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setPosition = function(position) {
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setUseRelativeScale = function(useRelativeScale) {
|
||||||
|
this.useRelativeScale = useRelativeScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateShapes = function() {
|
||||||
|
var counts = _.pluck(this.data, 'count');
|
||||||
|
var min = this.useRelativeScale ? _.min(counts) : 0.0;
|
||||||
|
var scale = new goog.math.Range(min, _.max(counts));
|
||||||
|
var index = 0;
|
||||||
|
|
||||||
|
for (var i = 0, count = this.data.length; i < count; ++i) {
|
||||||
|
var value = this.data[i];
|
||||||
|
|
||||||
|
var colorPercent = 0;
|
||||||
|
if (scale.getLength() > 0) {
|
||||||
|
colorPercent = Math.max(0, value.count - scale.start) / scale.getLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorPercent < 0.01) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorByte = 0xff - Math.min(0xff, Math.round(0xff * colorPercent));
|
||||||
|
var colorStr = goog.color.rgbToHex(colorByte, colorByte, colorByte);
|
||||||
|
|
||||||
|
var position = new goog.math.Coordinate(value.sampleX, value.sampleY);
|
||||||
|
var marker = null;
|
||||||
|
|
||||||
|
if (this.dataMarkers.length <= index) {
|
||||||
|
marker = this.addDataPoint(position, 10.0, colorStr);
|
||||||
|
this.dataMarkers.push(marker);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
marker = this.dataMarkers[index];
|
||||||
|
marker.set(this.convertPosition(position));
|
||||||
|
marker.set({ 'fill': colorStr });
|
||||||
|
}
|
||||||
|
|
||||||
|
++index;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = index; i < this.dataMarkers.length; ++i) {
|
||||||
|
this.canvas.remove(this.dataMarkers[i]);
|
||||||
|
}
|
||||||
|
this.dataMarkers.splice(index, this.dataMarkers.length);
|
||||||
|
|
||||||
|
this.positionMarker.set(this.convertPosition(this.position));
|
||||||
|
this.positionMarker.bringToFront();
|
||||||
|
|
||||||
|
this.canvas.renderAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addDataPoint = function(position, radius, color) {
|
||||||
|
var params = {
|
||||||
|
'originX': 'center',
|
||||||
|
'originY': 'center',
|
||||||
|
'fill': color,
|
||||||
|
'radius': radius
|
||||||
|
};
|
||||||
|
|
||||||
|
_.extend(params, this.convertPosition(position));
|
||||||
|
|
||||||
|
var shape = new fabric.Circle(params);
|
||||||
|
this.canvas.add(shape);
|
||||||
|
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.convertPosition = function(coordinate) {
|
||||||
|
var percentX = (coordinate.x - this.rangeX.start) / this.rangeX.getLength();
|
||||||
|
var percentY = (coordinate.y - this.rangeY.start) / this.rangeY.getLength();
|
||||||
|
|
||||||
|
return {
|
||||||
|
'left': percentX * this.canvas.width,
|
||||||
|
'top': (1 - percentY) * this.canvas.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setRange(new goog.math.Range(-1.0, 1.0), new goog.math.Range(-1.0, 1.0));
|
||||||
|
this.setData([]);
|
||||||
|
this.setPosition(new goog.math.Coordinate(0.0, 0.0));
|
||||||
|
this.setUseRelativeScale(true);
|
||||||
|
|
||||||
|
this.canvas = new fabric.StaticCanvas(canvas);
|
||||||
|
this.positionMarker = this.addDataPoint(this.position, 5.0, '#ef2929');
|
||||||
|
this.dataMarkers = [];
|
||||||
|
}
|
273
client/projection.js
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
function innerProduct(values1, values2) {
|
||||||
|
var result = 0;
|
||||||
|
for (var feature in values1) {
|
||||||
|
result += values1[feature] * (values2[feature] || 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchData(queryParams, minScore) {
|
||||||
|
var results = [];
|
||||||
|
|
||||||
|
for (var i = 0, count = DATA_RECORDS.length; i < count; ++i) {
|
||||||
|
var record = DATA_RECORDS[i];
|
||||||
|
var score = innerProduct(queryParams, record['rating']);
|
||||||
|
|
||||||
|
if (score >= minScore) {
|
||||||
|
results.push({
|
||||||
|
name: record['name'],
|
||||||
|
url: 'http://www.tripadvisor.com' + record['relativeUrl'],
|
||||||
|
score: score
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.sort(function(a, b) {
|
||||||
|
return b.score - a.score;
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchStepper(range, steps, callback) {
|
||||||
|
var stepSize = range.getLength() / steps;
|
||||||
|
|
||||||
|
for (var i = 0; i < steps; ++i) {
|
||||||
|
var stepMax = range.end - stepSize * i;
|
||||||
|
var stepMin = stepMax - stepSize;
|
||||||
|
var stepMid = (stepMin + stepMax) / 2;
|
||||||
|
|
||||||
|
callback(stepMid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchProjection(queryParams, minScore, feature, range, steps) {
|
||||||
|
var testParams = _.clone(queryParams);
|
||||||
|
var results = [];
|
||||||
|
|
||||||
|
searchStepper(range, steps, function(position) {
|
||||||
|
testParams[feature] = position;
|
||||||
|
results.push({
|
||||||
|
'sample': position,
|
||||||
|
'values': searchData(testParams, minScore)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchProjection2d(queryParams, minScore, feature1, feature2, range, steps) {
|
||||||
|
var testParams = _.clone(queryParams);
|
||||||
|
var results = [];
|
||||||
|
|
||||||
|
searchStepper(range, steps, function(sampleX) {
|
||||||
|
testParams[feature1] = sampleX;
|
||||||
|
searchStepper(range, steps, function(sampleY) {
|
||||||
|
testParams[feature2] = sampleY;
|
||||||
|
results.push({
|
||||||
|
'sampleX': sampleX,
|
||||||
|
'sampleY': sampleY,
|
||||||
|
'values': searchData(testParams, minScore)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchBuildHints(queryParams, minScore, feature, range, steps) {
|
||||||
|
var projection = searchProjection(
|
||||||
|
queryParams,
|
||||||
|
minScore,
|
||||||
|
feature,
|
||||||
|
range,
|
||||||
|
steps
|
||||||
|
);
|
||||||
|
|
||||||
|
var hints = [];
|
||||||
|
_.each(projection, function(result) {
|
||||||
|
hints.push({
|
||||||
|
'sample': result.sample,
|
||||||
|
'count': result.values.length
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return hints;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchBuildHints2d(queryParams, minScore, feature1, feature2, range, steps) {
|
||||||
|
var projection = searchProjection2d(
|
||||||
|
queryParams,
|
||||||
|
minScore,
|
||||||
|
feature1,
|
||||||
|
feature2,
|
||||||
|
range,
|
||||||
|
steps
|
||||||
|
);
|
||||||
|
|
||||||
|
var hints = [];
|
||||||
|
_.each(projection, function(result) {
|
||||||
|
hints.push({
|
||||||
|
'sampleX': result.sampleX,
|
||||||
|
'sampleY': result.sampleY,
|
||||||
|
'count': result.values.length
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return hints;
|
||||||
|
}
|
||||||
|
|
||||||
|
function outputResults(results, maxResults) {
|
||||||
|
$('#results').empty();
|
||||||
|
$('#count').text(results.length);
|
||||||
|
|
||||||
|
results = results.splice(0, maxResults);
|
||||||
|
|
||||||
|
var template = Handlebars.compile($('#template').html());
|
||||||
|
$('#results').append(template({'results': results}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAdjust(name, value) {
|
||||||
|
var wa = window.adjuster;
|
||||||
|
var wg = window.grapher;
|
||||||
|
var wp = window.plotter;
|
||||||
|
|
||||||
|
wa.queryParams[name] = value;
|
||||||
|
console.log(wa.queryParams);
|
||||||
|
|
||||||
|
var hintData = {};
|
||||||
|
_.each(wg.getColumnNames(), function(name) {
|
||||||
|
hintData[name] = searchBuildHints(wa.queryParams, wa.minScore, name, wa.searchRange, wa.hintSteps);
|
||||||
|
});
|
||||||
|
wg.setColumnHints(hintData);
|
||||||
|
|
||||||
|
var plotterAxisX = $('#plotAxisX').val();
|
||||||
|
var plotterAxisY = $('#plotAxisY').val();
|
||||||
|
var plotterData = searchBuildHints2d(wa.queryParams, wa.minScore, plotterAxisX, plotterAxisY, wa.searchRange, wa.hintSteps)
|
||||||
|
var plotterPosition = new goog.math.Coordinate(wa.queryParams[plotterAxisX], wa.queryParams[plotterAxisY]);
|
||||||
|
|
||||||
|
wp.setPosition(plotterPosition);
|
||||||
|
wp.setData(plotterData);
|
||||||
|
wp.updateShapes();
|
||||||
|
|
||||||
|
var results = searchData(wa.queryParams, wa.minScore);
|
||||||
|
outputResults(results, wa.maxResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onQuery() {
|
||||||
|
var query = $('#query').val();
|
||||||
|
var minScore = parseInt($('#minScore').val()) || 1.0;
|
||||||
|
var hintSteps = parseInt($('#hintSteps').val()) || 20;
|
||||||
|
var maxResults = parseInt($('#maxResults').val()) || 100;
|
||||||
|
var useLocalScale = true;
|
||||||
|
var useRelativeScale = true;
|
||||||
|
|
||||||
|
console.assert(query in DATA_KEYWORDS);
|
||||||
|
|
||||||
|
var queryParams = DATA_KEYWORDS[query];
|
||||||
|
var searchRange = new goog.math.Range(-1.0, 1.0);
|
||||||
|
var graphColumns = {};
|
||||||
|
|
||||||
|
for (var feature in queryParams) {
|
||||||
|
var hints = searchBuildHints(
|
||||||
|
queryParams,
|
||||||
|
minScore,
|
||||||
|
feature,
|
||||||
|
searchRange,
|
||||||
|
hintSteps
|
||||||
|
);
|
||||||
|
|
||||||
|
graphColumns[feature] = {
|
||||||
|
'color': '#607080',
|
||||||
|
'value': queryParams[feature],
|
||||||
|
'hints': hints,
|
||||||
|
'steps': hintSteps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.adjuster = {
|
||||||
|
queryParams: queryParams,
|
||||||
|
searchRange: searchRange,
|
||||||
|
hintSteps: hintSteps,
|
||||||
|
minScore: minScore,
|
||||||
|
maxResults: maxResults
|
||||||
|
};
|
||||||
|
|
||||||
|
window.grapher = new Grapher('grapher', searchRange, useLocalScale, useRelativeScale);
|
||||||
|
window.grapher.setColumns(graphColumns);
|
||||||
|
window.grapher.setValueChangedListener(onAdjust);
|
||||||
|
|
||||||
|
var plotterAxisX = $('#plotAxisX').val();
|
||||||
|
var plotterAxisY = $('#plotAxisY').val();
|
||||||
|
var plotterData = searchBuildHints2d(queryParams, minScore, plotterAxisX, plotterAxisY, searchRange, hintSteps)
|
||||||
|
var plotterPosition = new goog.math.Coordinate(queryParams[plotterAxisX], queryParams[plotterAxisY]);
|
||||||
|
|
||||||
|
window.plotter = new Plotter('plotter', useRelativeScale);
|
||||||
|
window.plotter.setUseRelativeScale(useRelativeScale);
|
||||||
|
window.plotter.setPosition(plotterPosition);
|
||||||
|
window.plotter.setData(plotterData);
|
||||||
|
window.plotter.updateShapes();
|
||||||
|
|
||||||
|
var results = searchData(queryParams, minScore);
|
||||||
|
outputResults(results, maxResults);
|
||||||
|
|
||||||
|
$('#keyword').text(query);
|
||||||
|
$('#useLocalScale').prop('checked', useLocalScale);
|
||||||
|
$('#useRelativeScale').prop('checked', useRelativeScale);
|
||||||
|
$('#useLocalScale').click(function() {
|
||||||
|
var useLocalScale = $('#useLocalScale').is(':checked');
|
||||||
|
window.grapher.setUseLocalScale(useLocalScale);
|
||||||
|
});
|
||||||
|
$('#useRelativeScale').click(function() {
|
||||||
|
var useRelativeScale = $('#useRelativeScale').is(':checked');
|
||||||
|
window.grapher.setUseRelativeScale(useRelativeScale);
|
||||||
|
window.plotter.setUseRelativeScale(useRelativeScale);
|
||||||
|
window.plotter.updateShapes();
|
||||||
|
});
|
||||||
|
$('.plotAxes').change(function() {
|
||||||
|
var wa = window.adjuster;
|
||||||
|
var wp = window.plotter;
|
||||||
|
|
||||||
|
var plotterAxisX = $('#plotAxisX').val();
|
||||||
|
var plotterAxisY = $('#plotAxisY').val();
|
||||||
|
var plotterData = searchBuildHints2d(wa.queryParams, wa.minScore, plotterAxisX, plotterAxisY, wa.searchRange, wa.hintSteps)
|
||||||
|
var plotterPosition = new goog.math.Coordinate(wa.queryParams[plotterAxisX], wa.queryParams[plotterAxisY]);
|
||||||
|
|
||||||
|
wp.setPosition(plotterPosition);
|
||||||
|
wp.setData(plotterData);
|
||||||
|
wp.updateShapes();
|
||||||
|
});
|
||||||
|
$('#input').fadeOut(function() {
|
||||||
|
$('#output').fadeIn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
for (var keyword in DATA_KEYWORDS) {
|
||||||
|
$('#query').append($('<option></option>', {
|
||||||
|
'value': keyword,
|
||||||
|
'text': keyword
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
var features = ['food', 'service', 'value', 'atmosphere'];
|
||||||
|
_.each(features, function(feature) {
|
||||||
|
$('#plotAxisX').append($('<option></option>', {
|
||||||
|
'value': feature,
|
||||||
|
'text': feature
|
||||||
|
}));
|
||||||
|
|
||||||
|
$('#plotAxisY').append($('<option></option>', {
|
||||||
|
'value': feature,
|
||||||
|
'text': feature
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#plotAxisX').val(features[0]);
|
||||||
|
$('#plotAxisY').val(features[1]);
|
||||||
|
|
||||||
|
$('#search').click(onQuery);
|
||||||
|
});
|
1
client/underscore
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../libs/underscore
|
1
prototype/closure-library
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../libs/closure-library/
|
1
prototype/database.json
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
sense/database.json
|
1
prototype/definitions.json
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
sense/definitions.json
|
1
prototype/fabric.js
vendored
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../libs/fabric.js/
|
465
prototype/grapher.js
vendored
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
goog.require('goog.color');
|
||||||
|
goog.require('goog.math');
|
||||||
|
goog.require('goog.math.Coordinate');
|
||||||
|
goog.require('goog.math.Range');
|
||||||
|
goog.require('goog.math.Rect');
|
||||||
|
|
||||||
|
function Column(canvas, name, params, range, saturation, bounds) {
|
||||||
|
this.clearShapes = function() {
|
||||||
|
for (var i = 0, count = this.shapes.length; i < count; ++i) {
|
||||||
|
this.canvas.remove(this.shapes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shapes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateShapes = function(final) {
|
||||||
|
this.labelBounds = this.getLabelBounds(this.bounds);
|
||||||
|
this.columnBounds = this.getColumnBounds(this.bounds);
|
||||||
|
this.hintBounds = this.getHintBounds(this.columnBounds);
|
||||||
|
this.fillBounds = this.getFillBounds(this.columnBounds);
|
||||||
|
this.handleBounds = this.getHandleBounds(this.columnBounds, this.fillBounds);
|
||||||
|
|
||||||
|
if (final) {
|
||||||
|
this.updateRect('boundsRect', {
|
||||||
|
left: this.bounds.left,
|
||||||
|
top: this.bounds.top,
|
||||||
|
width: this.bounds.width,
|
||||||
|
height: this.bounds.height,
|
||||||
|
stroke: this.strokeColor,
|
||||||
|
fill: this.emptyColor
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateRect('hintRect', {
|
||||||
|
left: this.hintBounds.left,
|
||||||
|
top: this.hintBounds.top,
|
||||||
|
width: this.hintBounds.width,
|
||||||
|
height: this.hintBounds.height,
|
||||||
|
stroke: this.strokeColor
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hintRect.setGradient('fill', {
|
||||||
|
x1: 0.0,
|
||||||
|
y1: -this.hintRect.height / 2,
|
||||||
|
x2: 0.0,
|
||||||
|
y2: this.hintRect.height / 2,
|
||||||
|
colorStops: this.decimateHints(this.hintSteps, this.hintSaturation)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateRect('labelRect', {
|
||||||
|
left: this.labelBounds.left,
|
||||||
|
top: this.labelBounds.top,
|
||||||
|
width: this.labelBounds.width,
|
||||||
|
height: this.labelBounds.height,
|
||||||
|
fill: this.strokeColor
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateText('label', this.name, {
|
||||||
|
left: this.fillBounds.left + this.fillBounds.width / 2,
|
||||||
|
top: this.labelBounds.top + this.labelBounds.height / 2,
|
||||||
|
fontSize: this.labelFontSize,
|
||||||
|
originX: 'center',
|
||||||
|
originY: 'center',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateRect('fillRect', {
|
||||||
|
left: this.fillBounds.left,
|
||||||
|
top: this.fillBounds.top,
|
||||||
|
width: this.fillBounds.width,
|
||||||
|
height: this.fillBounds.height,
|
||||||
|
fill: this.fillColor
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateRect('handleRect', {
|
||||||
|
left: this.handleBounds.left,
|
||||||
|
top: this.handleBounds.top,
|
||||||
|
width: this.handleBounds.width,
|
||||||
|
height: this.handleBounds.height,
|
||||||
|
fill: this.handleColor
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canvas.renderAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateRect = function(name, args) {
|
||||||
|
if (name in this) {
|
||||||
|
this[name].set(args);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var rect = new fabric.Rect(args);
|
||||||
|
this.canvas.add(rect);
|
||||||
|
this.shapes.push(rect);
|
||||||
|
this[name] = rect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateText = function(name, text, args) {
|
||||||
|
if (name in this) {
|
||||||
|
this[name].set(args);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var text = new fabric.Text(text, args);
|
||||||
|
this.canvas.add(text);
|
||||||
|
this.shapes.push(text);
|
||||||
|
this[name] = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateColor = function(color) {
|
||||||
|
var fillColorRgb = goog.color.hexToRgb(goog.color.parse(color).hex);
|
||||||
|
var handleColorRgb = goog.color.darken(fillColorRgb, this.handleDarken);
|
||||||
|
|
||||||
|
this.fillColor = goog.color.rgbToHex.apply(this, fillColorRgb);
|
||||||
|
this.handleColor = goog.color.rgbToHex.apply(this, handleColorRgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.decimateHints = function(steps, saturation) {
|
||||||
|
var groups = this.groupHints(steps);
|
||||||
|
|
||||||
|
var colorStops = {};
|
||||||
|
groups.forEach(function(count, index) {
|
||||||
|
var colorByte = 0xff - Math.min(0xff, Math.round(0xff * count / saturation));
|
||||||
|
var colorStr = goog.color.rgbToHex(colorByte, colorByte, colorByte);
|
||||||
|
colorStops[String(index / steps)] = colorStr;
|
||||||
|
});
|
||||||
|
|
||||||
|
return colorStops;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.groupHints = function(steps) {
|
||||||
|
var stepSize = this.range.getLength() / steps;
|
||||||
|
|
||||||
|
var hintGroups = [];
|
||||||
|
for (var i = 0; i < steps; ++i) {
|
||||||
|
var stepMax = this.range.end - stepSize * i;
|
||||||
|
var stepMin = stepMax - stepSize;
|
||||||
|
|
||||||
|
var hintCount = 0;
|
||||||
|
this.hints.forEach(function(hint) {
|
||||||
|
if (hint > stepMin && hint <= stepMax) {
|
||||||
|
++hintCount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
hintGroups.push(hintCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hintGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setClampedValue = function(value, final) {
|
||||||
|
this.value = goog.math.clamp(value, this.range.start, this.range.end);
|
||||||
|
this.updateShapes(final);
|
||||||
|
|
||||||
|
if (this.onValueChanged && final) {
|
||||||
|
this.onValueChanged.call(this.onValueChangedObj, this.name, this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setHints = function(hints) {
|
||||||
|
this.hints = hints;
|
||||||
|
this.updateShapes(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getLabelBounds = function(bounds) {
|
||||||
|
return new goog.math.Rect(
|
||||||
|
bounds.left,
|
||||||
|
bounds.top + bounds.height - this.labelSize,
|
||||||
|
bounds.width,
|
||||||
|
this.labelSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getColumnBounds = function(bounds) {
|
||||||
|
return new goog.math.Rect(
|
||||||
|
bounds.left,
|
||||||
|
bounds.top,
|
||||||
|
bounds.width,
|
||||||
|
bounds.height - this.labelSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getHintBounds = function(bounds) {
|
||||||
|
return new goog.math.Rect(
|
||||||
|
bounds.left + bounds.width - this.hintSize,
|
||||||
|
bounds.top,
|
||||||
|
this.hintSize,
|
||||||
|
bounds.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getFillBounds = function(bounds) {
|
||||||
|
var fill = (this.value - this.range.start) / this.range.getLength();
|
||||||
|
var height = bounds.height * (1.0 - goog.math.clamp(fill, 0.0, 1.0));
|
||||||
|
return new goog.math.Rect(
|
||||||
|
bounds.left,
|
||||||
|
bounds.top + height,
|
||||||
|
bounds.width - this.hintSize,
|
||||||
|
bounds.height - height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getHandleBounds = function(bounds, fillBounds) {
|
||||||
|
var handleBounds = new goog.math.Rect(
|
||||||
|
fillBounds.left,
|
||||||
|
fillBounds.top,
|
||||||
|
fillBounds.width,
|
||||||
|
this.handleSize
|
||||||
|
);
|
||||||
|
handleBounds.intersection(bounds);
|
||||||
|
return handleBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseDown = function(position) {
|
||||||
|
if (this.isGrabbing(position)) {
|
||||||
|
this.stateTransition(this.State.DRAG, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseUp = function(position) {
|
||||||
|
this.stateTransition(
|
||||||
|
this.isHovering(position) ? this.State.HOVER : this.State.NORMAL,
|
||||||
|
position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseMove = function(position) {
|
||||||
|
switch (this.state) {
|
||||||
|
case this.State.NORMAL:
|
||||||
|
if (this.isHovering(position)) {
|
||||||
|
this.stateTransition(this.State.HOVER, position);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case this.State.HOVER:
|
||||||
|
if (!this.isHovering(position)) {
|
||||||
|
this.stateTransition(this.State.NORMAL, position);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stateUpdate(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseOut = function(position) {
|
||||||
|
this.mouseUp(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseDoubleClick = function(position) {
|
||||||
|
if (this.isContained(position)) {
|
||||||
|
this.setClampedValue(this.getValueFromPos(position), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getValueFromPos = function(position) {
|
||||||
|
var percent = 1.0 - (position.y - this.columnBounds.top) / this.columnBounds.height;
|
||||||
|
return this.range.start + this.range.getLength() * percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isHovering = function(position) {
|
||||||
|
return this.isGrabbing(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isGrabbing = function(position) {
|
||||||
|
return this.handleBounds.contains(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isContained = function(position) {
|
||||||
|
return this.columnBounds.contains(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stateUpdate = function(position) {
|
||||||
|
switch (this.state) {
|
||||||
|
case this.State.DRAG:
|
||||||
|
this.setClampedValue(this.getValueFromPos(position) + this.dragDelta, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stateTransition = function(state, position) {
|
||||||
|
if (state == this.state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.state) {
|
||||||
|
case this.State.DRAG:
|
||||||
|
this.setClampedValue(this.getValueFromPos(position) + this.dragDelta, true);
|
||||||
|
case this.State.HOVER:
|
||||||
|
if (state == this.State.NORMAL) {
|
||||||
|
this.canvas.contextContainer.canvas.style.cursor = 'default';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case this.State.DRAG:
|
||||||
|
this.dragDelta = this.value - this.getValueFromPos(position);
|
||||||
|
case this.State.HOVER:
|
||||||
|
this.canvas.contextContainer.canvas.style.cursor = 'ns-resize';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.State = {
|
||||||
|
NORMAL: 0,
|
||||||
|
HOVER: 1,
|
||||||
|
DRAG: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleSize = 10;
|
||||||
|
this.handleDarken = 0.25;
|
||||||
|
this.hintSaturation = saturation;
|
||||||
|
this.hintSize = 5;
|
||||||
|
this.hintSteps = 10;
|
||||||
|
this.labelFontSize = 15;
|
||||||
|
this.labelSize = 20;
|
||||||
|
this.emptyColor = '#eeeeec';
|
||||||
|
this.strokeColor = '#d3d7cf';
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.shapes = [];
|
||||||
|
this.name = name;
|
||||||
|
this.value = params.value;
|
||||||
|
this.hints = params.hints;
|
||||||
|
this.range = range;
|
||||||
|
this.bounds = bounds;
|
||||||
|
this.state = this.State.NORMAL;
|
||||||
|
|
||||||
|
this.updateColor(params.color);
|
||||||
|
this.updateShapes(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Grapher(canvas) {
|
||||||
|
this.setColumns = function(columns) {
|
||||||
|
this.columns.forEach(function(c) { c.clearShapes(); });
|
||||||
|
this.columns = [];
|
||||||
|
this.indexMap = {};
|
||||||
|
|
||||||
|
var graphBounds = this.getGraphBounds(this.getCanvasBounds());
|
||||||
|
var columnCount = Object.keys(columns).length;
|
||||||
|
|
||||||
|
var saturation = 0;
|
||||||
|
for (var columnName in columns) {
|
||||||
|
saturation = Math.max(columns[columnName].hints.length, saturation);
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
for (var columnName in columns) {
|
||||||
|
this.indexMap[columnName] = index;
|
||||||
|
this.columns.push(new Column(
|
||||||
|
this.canvas,
|
||||||
|
columnName,
|
||||||
|
columns[columnName],
|
||||||
|
this.range,
|
||||||
|
saturation,
|
||||||
|
this.getColumnBounds(graphBounds, index++, columnCount)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setColumnHints = function(name, hints) {
|
||||||
|
var index = this.getColumnIndex(name);
|
||||||
|
if (index < this.columns.length) {
|
||||||
|
this.columns[index].setHints(hints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setValueChangedListener = function(listener, object) {
|
||||||
|
this.columns.forEach(function (c) {
|
||||||
|
c.onValueChanged = listener;
|
||||||
|
c.onValueChangedObj = object;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getColumnCount = function() {
|
||||||
|
return this.columns.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getColumnIndex = function(name) {
|
||||||
|
return this.indexMap[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getColumnName = function(index) {
|
||||||
|
return this.columns[index].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getCanvasBounds = function() {
|
||||||
|
return new goog.math.Rect(0, 0, this.canvas.width - 1, this.canvas.height - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getGraphBounds = function(bounds) {
|
||||||
|
return this.getCanvasBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getColumnBounds = function(bounds, index, count) {
|
||||||
|
var secWidth = bounds.width / count;
|
||||||
|
var columnWidth = secWidth - this.padding * 2;
|
||||||
|
return new goog.math.Rect(
|
||||||
|
bounds.left + secWidth * index + this.padding,
|
||||||
|
bounds.top,
|
||||||
|
columnWidth,
|
||||||
|
bounds.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getMousePos = function(e) {
|
||||||
|
var rect = e.target.getBoundingClientRect();
|
||||||
|
return new goog.math.Coordinate(
|
||||||
|
e.clientX - rect.left,
|
||||||
|
e.clientY - rect.top
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseDown = function(e) {
|
||||||
|
var position = this.grapher.getMousePos(e);
|
||||||
|
this.grapher.columns.forEach(function(g) {
|
||||||
|
g.mouseDown(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseUp = function(e) {
|
||||||
|
var position = this.grapher.getMousePos(e);
|
||||||
|
this.grapher.columns.forEach(function(g) {
|
||||||
|
g.mouseUp(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseMove = function(e) {
|
||||||
|
var position = this.grapher.getMousePos(e);
|
||||||
|
this.grapher.columns.forEach(function(g) {
|
||||||
|
g.mouseMove(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseOut = function(e) {
|
||||||
|
var position = this.grapher.getMousePos(e);
|
||||||
|
this.grapher.columns.forEach(function(g) {
|
||||||
|
g.mouseOut(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseDoubleClick = function(e) {
|
||||||
|
var position = this.grapher.getMousePos(e);
|
||||||
|
this.grapher.columns.forEach(function(g) {
|
||||||
|
g.mouseDoubleClick(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canvas = new fabric.StaticCanvas(canvas);
|
||||||
|
this.range = new goog.math.Range(0.0, 1.0);
|
||||||
|
this.padding = 5;
|
||||||
|
this.indexMap = {};
|
||||||
|
this.columns = [];
|
||||||
|
|
||||||
|
var c = this.canvas.contextContainer.canvas;
|
||||||
|
c.addEventListener('mousedown', this.mouseDown, false);
|
||||||
|
c.addEventListener('mouseup', this.mouseUp, false);
|
||||||
|
c.addEventListener('mousemove', this.mouseMove, false);
|
||||||
|
c.addEventListener('mouseout', this.mouseOut, false);
|
||||||
|
c.addEventListener('dblclick', this.mouseDoubleClick, false);
|
||||||
|
c.grapher = this;
|
||||||
|
}
|
BIN
prototype/images/033899c043534d46cc89f689cab4f891.jpg
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
prototype/images/06bf75ff291e33d73cbd4df5af73ca1c.jpg
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
prototype/images/07f102c5fb9cbe20d8b21efd0873b349.jpg
Normal file
After Width: | Height: | Size: 288 KiB |
BIN
prototype/images/09217a740cbb7a95cf9660c04575d2c4.jpg
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
prototype/images/0bf1a255b0a18b51916796d55bf0faf9.jpg
Normal file
After Width: | Height: | Size: 136 KiB |
BIN
prototype/images/0c52970c9a975ef5e0050fa33bb096cc.jpg
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
prototype/images/0d8ee5a1f5869ab11bac692f41f72596.jpg
Normal file
After Width: | Height: | Size: 208 KiB |
BIN
prototype/images/0e296337fa33d40b747c697c839998b1.jpg
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
prototype/images/0e801b7f95874c109ae1a1c13050bb63.jpg
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
prototype/images/1124672a5c2ef7386cf41b4d50421826.jpg
Normal file
After Width: | Height: | Size: 643 KiB |
BIN
prototype/images/11e23f9c9564afee4189fb85fd0112dd.jpg
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
prototype/images/183902a0d027d22b7423b1c3ce17333f.jpg
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
prototype/images/18b1849f5e45a16b03dd38347b207e6e.jpg
Normal file
After Width: | Height: | Size: 137 KiB |
BIN
prototype/images/1bc70a9478d3cb7aefae63fb02861afd.jpg
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
prototype/images/1c756e65a575ca2aff9fffa75470d571.jpg
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
prototype/images/1c94b3a0205d074903ac6a629c537793.jpg
Normal file
After Width: | Height: | Size: 123 KiB |
BIN
prototype/images/1eee814a561e2056e146d5cf8f965b85.jpg
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
prototype/images/210b9c58431bd7b2d7b054a57f38605a.jpg
Normal file
After Width: | Height: | Size: 163 KiB |
BIN
prototype/images/2bb8609b6ac26b6e8d718bfcc229184e.jpg
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
prototype/images/2eecbb39d5b0882e0669830713df5739.jpg
Normal file
After Width: | Height: | Size: 221 KiB |
BIN
prototype/images/2fcc43e8ab68bb4961021a5c9a36fc67.jpg
Normal file
After Width: | Height: | Size: 141 KiB |
BIN
prototype/images/32e1169aac4ca2ad6ee20a4a8ca949d1.jpg
Normal file
After Width: | Height: | Size: 128 KiB |
BIN
prototype/images/331aac68c947c1151a516f255e790cef.jpg
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
prototype/images/34411bb66f72838aa39b9d73842d4d5e.jpg
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
prototype/images/38a95fbfb8b63f18f95445f6e53faa02.jpg
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
prototype/images/3b3e9fb177d733c02df9bcd4c2d19607.jpg
Normal file
After Width: | Height: | Size: 259 KiB |
BIN
prototype/images/4001c88a2b477b0c7adeaf87c7372965.jpg
Normal file
After Width: | Height: | Size: 125 KiB |
BIN
prototype/images/403bb6784263fb52c9f5f8fd5218ee73.jpg
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
prototype/images/40bf8583650914ccbeaa935934a0ebcd.jpg
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
prototype/images/422b0043dfd692f26171180414a81aac.jpg
Normal file
After Width: | Height: | Size: 187 KiB |
BIN
prototype/images/440866257f1413979af4c737ff04343a.jpg
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
prototype/images/4906680901ad20aa9cd56ca24a3af810.jpg
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
prototype/images/4a14285bd5fbd0fff4d2b12efd391b12.jpg
Normal file
After Width: | Height: | Size: 230 KiB |
BIN
prototype/images/4a7db1f457c69b90bb42d9bc0e7e4a2b.jpg
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
prototype/images/4b792cdf44fa762a89bac7a92361bb9a.jpg
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
prototype/images/4d1c6f9290bb1cdab48f1fe553f6be66.jpg
Normal file
After Width: | Height: | Size: 166 KiB |
BIN
prototype/images/4d29d485908041776f1f1753badda65c.jpg
Normal file
After Width: | Height: | Size: 135 KiB |
BIN
prototype/images/4d502011df2991db940083a74fab1ceb.jpg
Normal file
After Width: | Height: | Size: 228 KiB |
BIN
prototype/images/516bc57c955f521378a8ca1358907583.jpg
Normal file
After Width: | Height: | Size: 200 KiB |
BIN
prototype/images/5238eddcb35b58ab3c5fed72dcd8ffcf.jpg
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
prototype/images/591e8ab52dedb9c639d4f5a708e71736.jpg
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
prototype/images/592f3a4714e319f5fd60f446569f13fd.jpg
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
prototype/images/5af3e704666a1cab2db1d5aa67bdbbc5.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
prototype/images/5b1bcf1f2559d98124a34707e078721c.jpg
Normal file
After Width: | Height: | Size: 318 KiB |
BIN
prototype/images/5bb1afd6d5a438a15fc920a6ec935801.jpg
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
prototype/images/5ee4656d35518922f9eeba9ecc8552e3.jpg
Normal file
After Width: | Height: | Size: 107 KiB |
BIN
prototype/images/6026972652d3e811b95b3e8c61c9db87.jpg
Normal file
After Width: | Height: | Size: 130 KiB |
BIN
prototype/images/6049e03d493edf495a679d51a0d84401.jpg
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
prototype/images/6b961f574630db635732c4711cfc2970.jpg
Normal file
After Width: | Height: | Size: 162 KiB |
BIN
prototype/images/70f14358988486364f2dc2cb005a54b2.jpg
Normal file
After Width: | Height: | Size: 221 KiB |
BIN
prototype/images/74edb484247813d461d6fc57e5de6af8.jpg
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
prototype/images/7721de0bd6615dfbc5069854922e60dc.jpg
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
prototype/images/796c66de99a2daf9a5eec7a2074a9c14.jpg
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
prototype/images/7a330e0263a24c056f570b63a90032cb.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
prototype/images/7ae18dc11f89703373c6bfbdb26c510b.jpg
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
prototype/images/7afc811961059115f1af7ae2ef3677bf.jpg
Normal file
After Width: | Height: | Size: 162 KiB |
BIN
prototype/images/7c3878bf5d84f81b51541f86bc447ac0.jpg
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
prototype/images/7d71404fb9fd25647a9643bad87d90ec.jpg
Normal file
After Width: | Height: | Size: 98 KiB |
BIN
prototype/images/80d7590fe2dd7c57e51a0f46240191fc.jpg
Normal file
After Width: | Height: | Size: 158 KiB |
BIN
prototype/images/82c2c880e5fbc7f6d8bf489b916b75ac.jpg
Normal file
After Width: | Height: | Size: 149 KiB |
BIN
prototype/images/8833aa5d1f8ffc8b0e34a6e9847e1d66.jpg
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
prototype/images/89ed6915eb87856170a0e171f0f7ecb8.jpg
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
prototype/images/8f1a9a445d1e3d0dc18930c4ef47ad40.jpg
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
prototype/images/94ae483f9edd79e28686569337dcefa3.jpg
Normal file
After Width: | Height: | Size: 265 KiB |
BIN
prototype/images/94b403ae2361f813f8f7667b50c8dcd0.jpg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
prototype/images/97153024f3111fb0012c048acdfbac85.jpg
Normal file
After Width: | Height: | Size: 172 KiB |
BIN
prototype/images/9872dfce41e792a20d6118b2a6fec9f2.jpg
Normal file
After Width: | Height: | Size: 189 KiB |
BIN
prototype/images/98e6f495e99a6d751105924a93b0b1c8.jpg
Normal file
After Width: | Height: | Size: 149 KiB |
BIN
prototype/images/99e85c4afefd9435f90b8727f5bbba48.jpg
Normal file
After Width: | Height: | Size: 129 KiB |
BIN
prototype/images/99fe1f77555fc6b175582a2e19f5fe6e.jpg
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
prototype/images/9c87d66504c3d10d10e67aff1064a78e.jpg
Normal file
After Width: | Height: | Size: 186 KiB |
BIN
prototype/images/9e1ab8ef0a2b9e34a76cd8f5e857faa0.jpg
Normal file
After Width: | Height: | Size: 146 KiB |
BIN
prototype/images/9ed7e5c3dcb9219e153b7b3aeb839d38.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
prototype/images/a2fb6c659ac0a04b57e919513136721b.jpg
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
prototype/images/a59a2a18a58f51b7248111da61f59e9a.jpg
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
prototype/images/aa22a7443f6ce952bea0e022016c5e59.jpg
Normal file
After Width: | Height: | Size: 171 KiB |
BIN
prototype/images/ab0a7c8497a62c61012217f2e3e0cbfe.jpg
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
prototype/images/ab5722851fba17be808317da988e133a.jpg
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
prototype/images/ab5ae859dd2c0ac4c40335c4eac76058.jpg
Normal file
After Width: | Height: | Size: 217 KiB |
BIN
prototype/images/ace17802eae655c2565c01f0ff241d62.jpg
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
prototype/images/afaa036fa8ff238315bda5d09ee90ca5.jpg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
prototype/images/b03a7936a8ef9248ec22e2901c0aa937.jpg
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
prototype/images/b2ae7c42ce78b6dee448f9fd899cde48.jpg
Normal file
After Width: | Height: | Size: 52 KiB |