Added histograms to range slider facets.
git-svn-id: http://google-refine.googlecode.com/svn/trunk@25 7d457c2a-affb-35e4-300a-418c747d4874
This commit is contained in:
parent
00696a96fc
commit
ed5eae83af
@ -0,0 +1,88 @@
|
||||
package com.metaweb.gridlock.browsing.facets;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.metaweb.gridlock.expr.Evaluable;
|
||||
import com.metaweb.gridlock.model.Cell;
|
||||
import com.metaweb.gridlock.model.Project;
|
||||
import com.metaweb.gridlock.model.Row;
|
||||
|
||||
public class NumericBinIndex {
|
||||
public double min;
|
||||
public double max;
|
||||
public double step;
|
||||
public int[] bins;
|
||||
|
||||
public NumericBinIndex(Project project, int cellIndex, Evaluable eval) {
|
||||
Properties bindings = new Properties();
|
||||
|
||||
min = Double.POSITIVE_INFINITY;
|
||||
max = Double.NEGATIVE_INFINITY;
|
||||
|
||||
List<Double> allValues = new ArrayList<Double>();
|
||||
for (int i = 0; i < project.rows.size(); i++) {
|
||||
Row row = project.rows.get(i);
|
||||
|
||||
if (cellIndex < row.cells.size()) {
|
||||
Cell cell = row.cells.get(cellIndex);
|
||||
if (cell != null) {
|
||||
bindings.put("project", project);
|
||||
bindings.put("cell", cell);
|
||||
bindings.put("value", cell.value);
|
||||
|
||||
Object value = eval.evaluate(bindings);
|
||||
if (value != null) {
|
||||
if (value.getClass().isArray()) {
|
||||
Object[] a = (Object[]) value;
|
||||
for (Object v : a) {
|
||||
if (v instanceof Number) {
|
||||
processValue(((Number) v).doubleValue(), allValues);
|
||||
}
|
||||
}
|
||||
} else if (value instanceof Number) {
|
||||
processValue(((Number) value).doubleValue(), allValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (min >= max) {
|
||||
step = 0;
|
||||
bins = new int[0];
|
||||
return;
|
||||
}
|
||||
|
||||
double diff = max - min;
|
||||
if (diff > 10) {
|
||||
step = 1;
|
||||
while (step * 100 < diff) {
|
||||
step *= 10;
|
||||
}
|
||||
} else {
|
||||
step = 1;
|
||||
while (step * 100 > diff) {
|
||||
step /= 10;
|
||||
}
|
||||
}
|
||||
|
||||
min = Math.floor(min / step) * step;
|
||||
max = Math.ceil(max / step) * step;
|
||||
|
||||
int binCount = 1 + (int) Math.ceil((max - min) / step);
|
||||
|
||||
bins = new int[binCount];
|
||||
for (double d : allValues) {
|
||||
int bin = (int) Math.round((d - min) / step);
|
||||
bins[bin]++;
|
||||
}
|
||||
}
|
||||
|
||||
protected void processValue(double v, List<Double> allValues) {
|
||||
min = Math.min(min, v);
|
||||
max = Math.max(max, v);
|
||||
allValues.add(v);
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ import com.metaweb.gridlock.browsing.filters.ExpressionNumberComparisonRowFilter
|
||||
import com.metaweb.gridlock.browsing.filters.RowFilter;
|
||||
import com.metaweb.gridlock.expr.Evaluable;
|
||||
import com.metaweb.gridlock.expr.Parser;
|
||||
import com.metaweb.gridlock.model.Column;
|
||||
import com.metaweb.gridlock.model.Project;
|
||||
|
||||
public class RangeFacet implements Facet {
|
||||
@ -22,6 +23,11 @@ public class RangeFacet implements Facet {
|
||||
protected String _mode;
|
||||
protected double _min;
|
||||
protected double _max;
|
||||
protected double _step;
|
||||
protected int[] _bins;
|
||||
|
||||
protected double _from;
|
||||
protected double _to;
|
||||
|
||||
public RangeFacet() {
|
||||
}
|
||||
@ -34,15 +40,24 @@ public class RangeFacet implements Facet {
|
||||
writer.key("name"); writer.value(_name);
|
||||
writer.key("expression"); writer.value(_expression);
|
||||
writer.key("cellIndex"); writer.value(_cellIndex);
|
||||
writer.key("min"); writer.value(_min);
|
||||
writer.key("max"); writer.value(_max);
|
||||
writer.key("step"); writer.value(_step);
|
||||
|
||||
writer.key("bins"); writer.array();
|
||||
for (int b : _bins) {
|
||||
writer.value(b);
|
||||
}
|
||||
writer.endArray();
|
||||
|
||||
writer.key("mode"); writer.value(_mode);
|
||||
if ("min".equals(_mode)) {
|
||||
writer.key("min"); writer.value(_min);
|
||||
writer.key("from"); writer.value(_from);
|
||||
} else if ("max".equals(_mode)) {
|
||||
writer.key("max"); writer.value(_max);
|
||||
writer.key("to"); writer.value(_to);
|
||||
} else {
|
||||
writer.key("min"); writer.value(_min);
|
||||
writer.key("max"); writer.value(_max);
|
||||
writer.key("from"); writer.value(_from);
|
||||
writer.key("to"); writer.value(_to);
|
||||
}
|
||||
writer.endObject();
|
||||
}
|
||||
@ -57,12 +72,12 @@ public class RangeFacet implements Facet {
|
||||
|
||||
_mode = o.getString("mode");
|
||||
if ("min".equals(_mode)) {
|
||||
_min = o.getDouble("min");
|
||||
_from = o.getDouble("from");
|
||||
} else if ("max".equals(_mode)) {
|
||||
_max = o.getDouble("max");
|
||||
_to = o.getDouble("to");
|
||||
} else {
|
||||
_min = o.getDouble("min");
|
||||
_max = o.getDouble("max");
|
||||
_from = o.getDouble("from");
|
||||
_to = o.getDouble("to");
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,19 +86,19 @@ public class RangeFacet implements Facet {
|
||||
if ("min".equals(_mode)) {
|
||||
return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) {
|
||||
protected boolean checkValue(double d) {
|
||||
return d >= _min;
|
||||
return d >= _from;
|
||||
};
|
||||
};
|
||||
} else if ("max".equals(_mode)) {
|
||||
return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) {
|
||||
protected boolean checkValue(double d) {
|
||||
return d <= _max;
|
||||
return d <= _to;
|
||||
};
|
||||
};
|
||||
} else {
|
||||
return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) {
|
||||
protected boolean checkValue(double d) {
|
||||
return d >= _min && d <= _max;
|
||||
return d >= _from && d <= _to;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -91,6 +106,18 @@ public class RangeFacet implements Facet {
|
||||
|
||||
@Override
|
||||
public void computeChoices(Project project, FilteredRows filteredRows) {
|
||||
// nothing to do
|
||||
Column column = project.columnModel.getColumnByCellIndex(_cellIndex);
|
||||
|
||||
String key = "numeric-bin:" + _expression;
|
||||
NumericBinIndex index = (NumericBinIndex) column.getPrecompute(key);
|
||||
if (index == null) {
|
||||
index = new NumericBinIndex(project, _cellIndex, _eval);
|
||||
column.setPrecompute(key, index);
|
||||
}
|
||||
|
||||
_min = index.min;
|
||||
_max = index.max;
|
||||
_step = index.step;
|
||||
_bins = index.bins;
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ public class ApproveNewReconcileCommand extends Command {
|
||||
}
|
||||
}.init(cellIndex, cellChanges));
|
||||
|
||||
MassCellChange massCellChange = new MassCellChange(cellChanges);
|
||||
MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex);
|
||||
HistoryEntry historyEntry = new HistoryEntry(
|
||||
project, "Approve new topics for " + columnName, massCellChange);
|
||||
|
||||
|
@ -71,7 +71,7 @@ public class ApproveReconcileCommand extends Command {
|
||||
}
|
||||
}.init(cellIndex, cellChanges));
|
||||
|
||||
MassCellChange massCellChange = new MassCellChange(cellChanges);
|
||||
MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex);
|
||||
HistoryEntry historyEntry = new HistoryEntry(
|
||||
project, "Approve best recon candidates for " + columnName, massCellChange);
|
||||
|
||||
|
@ -67,7 +67,7 @@ public class DiscardReconcileCommand extends Command {
|
||||
}
|
||||
}.init(cellIndex, cellChanges));
|
||||
|
||||
MassCellChange massCellChange = new MassCellChange(cellChanges);
|
||||
MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex);
|
||||
HistoryEntry historyEntry = new HistoryEntry(
|
||||
project, "Discard recon results for " + columnName, massCellChange);
|
||||
|
||||
|
@ -85,7 +85,7 @@ public class DoTextTransformCommand extends Command {
|
||||
}
|
||||
}.init(cellIndex, bindings, cellChanges, eval));
|
||||
|
||||
MassCellChange massCellChange = new MassCellChange(cellChanges);
|
||||
MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex);
|
||||
HistoryEntry historyEntry = new HistoryEntry(
|
||||
project, "Text transform on " + columnName + ": " + expression, massCellChange);
|
||||
|
||||
|
@ -8,10 +8,12 @@ import com.metaweb.gridlock.model.Row;
|
||||
public class MassCellChange implements Change {
|
||||
private static final long serialVersionUID = -933571199802688027L;
|
||||
|
||||
protected CellChange[] _cellChanges;
|
||||
protected CellChange[] _cellChanges;
|
||||
protected int _commonCellIndex;
|
||||
|
||||
public MassCellChange(List<CellChange> cellChanges) {
|
||||
public MassCellChange(List<CellChange> cellChanges, int commonCellIndex) {
|
||||
_cellChanges = new CellChange[cellChanges.size()];
|
||||
_commonCellIndex = commonCellIndex;
|
||||
cellChanges.toArray(_cellChanges);
|
||||
}
|
||||
|
||||
@ -23,6 +25,10 @@ public class MassCellChange implements Change {
|
||||
for (CellChange cellChange : _cellChanges) {
|
||||
rows.get(cellChange.row).cells.set(cellChange.column, cellChange.newCell);
|
||||
}
|
||||
|
||||
if (_commonCellIndex >= 0) {
|
||||
project.columnModel.getColumnByCellIndex(_commonCellIndex).clearPrecomputes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +40,10 @@ public class MassCellChange implements Change {
|
||||
for (CellChange cellChange : _cellChanges) {
|
||||
rows.get(cellChange.row).cells.set(cellChange.column, cellChange.oldCell);
|
||||
}
|
||||
|
||||
if (_commonCellIndex >= 0) {
|
||||
project.columnModel.getColumnByCellIndex(_commonCellIndex).clearPrecomputes();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.metaweb.gridlock.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.json.JSONException;
|
||||
@ -15,6 +17,8 @@ public class Column implements Serializable, Jsonizable {
|
||||
public String headerLabel;
|
||||
public Class valueType;
|
||||
|
||||
transient protected Map<String, Object> _precomputes;
|
||||
|
||||
@Override
|
||||
public void write(JSONWriter writer, Properties options)
|
||||
throws JSONException {
|
||||
@ -25,4 +29,24 @@ public class Column implements Serializable, Jsonizable {
|
||||
writer.key("valueType"); writer.value(valueType == null ? null : valueType.getSimpleName());
|
||||
writer.endObject();
|
||||
}
|
||||
|
||||
public void clearPrecomputes() {
|
||||
if (_precomputes != null) {
|
||||
_precomputes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public Object getPrecompute(String key) {
|
||||
if (_precomputes != null) {
|
||||
return _precomputes.get(key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setPrecompute(String key, Object value) {
|
||||
if (_precomputes == null) {
|
||||
_precomputes = new HashMap<String, Object>();
|
||||
}
|
||||
_precomputes.put(key, value);
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ public class ReconProcess extends LongRunningProcess implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
MassCellChange massCellChange = new MassCellChange(cellChanges);
|
||||
MassCellChange massCellChange = new MassCellChange(cellChanges, _cellIndex);
|
||||
HistoryEntry historyEntry = new HistoryEntry(_project, _description, massCellChange);
|
||||
|
||||
_project.history.addEntry(historyEntry);
|
||||
|
@ -240,6 +240,7 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
|
||||
MenuSystem.createAndShowStandardMenu([
|
||||
{
|
||||
label: "Filter",
|
||||
tooltip: "Filter rows by this column's cell content or characteristics",
|
||||
submenu: [
|
||||
{ "heading" : "On Cell Content" },
|
||||
{
|
||||
@ -303,6 +304,9 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
|
||||
"name" : column.headerLabel + ": judgment",
|
||||
"cellIndex" : column.cellIndex,
|
||||
"expression" : "cell.recon.judgment"
|
||||
},
|
||||
{
|
||||
"scroll" : false
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -415,6 +419,7 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
|
||||
},
|
||||
{
|
||||
label: "Collapse/Expand",
|
||||
tooltip: "Collapse/expand columns to make viewing the data more convenient",
|
||||
submenu: [
|
||||
{
|
||||
label: "Collapse This Column",
|
||||
@ -475,6 +480,7 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
|
||||
},
|
||||
{
|
||||
label: "Reconcile",
|
||||
tooltip: "Match this column's cells to topics on Freebase",
|
||||
submenu: [
|
||||
{
|
||||
label: "Start Reconciling ...",
|
||||
|
@ -12,14 +12,14 @@ function RangeFacet(div, config, options) {
|
||||
RangeFacet.prototype._setDefaults = function() {
|
||||
switch (this._config.mode) {
|
||||
case "min":
|
||||
this._min = this._config.min;
|
||||
this._from = this._config.min;
|
||||
break;
|
||||
case "max":
|
||||
this._max = this._config.max;
|
||||
this._to = this._config.max;
|
||||
break;
|
||||
default:
|
||||
this._min = this._config.min;
|
||||
this._max = this._config.max;
|
||||
this._from = this._config.min;
|
||||
this._to = this._config.max;
|
||||
}
|
||||
};
|
||||
|
||||
@ -29,18 +29,18 @@ RangeFacet.prototype.getJSON = function() {
|
||||
|
||||
switch (this._config.mode) {
|
||||
case "min":
|
||||
o.min = this._min;
|
||||
o.from = this._from;
|
||||
break;
|
||||
case "max":
|
||||
o.max = this._max;
|
||||
o.to = this._to;
|
||||
break;
|
||||
default:
|
||||
o.min = this._min;
|
||||
if (this._max == this._config.max) {
|
||||
o.from = this._from;
|
||||
if (this._to == this._config.max) {
|
||||
// pretend to be open-ended
|
||||
o.mode = "min";
|
||||
} else {
|
||||
o.max = this._max;
|
||||
o.to = this._to;
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,12 +50,12 @@ RangeFacet.prototype.getJSON = function() {
|
||||
RangeFacet.prototype.hasSelection = function() {
|
||||
switch (this._config.mode) {
|
||||
case "min":
|
||||
return this._min > this._config.min;
|
||||
return this._from > this._config.min;
|
||||
case "max":
|
||||
return this._max < this._config.max;
|
||||
return this._to < this._config.max;
|
||||
default:
|
||||
return this._min > this._config.min ||
|
||||
this._max < this._config.max;
|
||||
return this._from > this._config.min ||
|
||||
this._to < this._config.max;
|
||||
}
|
||||
};
|
||||
|
||||
@ -71,21 +71,22 @@ RangeFacet.prototype._initializeUI = function() {
|
||||
}).prependTo(headerDiv);
|
||||
|
||||
var bodyDiv = $('<div></div>').addClass("facet-range-body").appendTo(container);
|
||||
|
||||
|
||||
this._histogramDiv = $('<div></div>').addClass("facet-range-histogram").appendTo(bodyDiv);
|
||||
this._sliderDiv = $('<div></div>').addClass("facet-range-slider").appendTo(bodyDiv);
|
||||
this._statusDiv = $('<div></div>').addClass("facet-range-status").appendTo(bodyDiv);
|
||||
|
||||
var onSlide = function(event, ui) {
|
||||
switch (self._config.mode) {
|
||||
case "min":
|
||||
self._min = ui.value;
|
||||
self._from = ui.value;
|
||||
break;
|
||||
case "max":
|
||||
self._max = ui.value;
|
||||
self._to = ui.value;
|
||||
break;
|
||||
default:
|
||||
self._min = ui.values[0];
|
||||
self._max = ui.values[1];
|
||||
self._from = ui.values[0];
|
||||
self._to = ui.values[1];
|
||||
}
|
||||
self._setRangeIndicators();
|
||||
self._scheduleUpdate();
|
||||
@ -104,15 +105,15 @@ RangeFacet.prototype._initializeUI = function() {
|
||||
switch (this._config.mode) {
|
||||
case "min":
|
||||
sliderConfig.range = "max";
|
||||
sliderConfig.value = this._min;
|
||||
sliderConfig.value = this._from;
|
||||
break;
|
||||
case "max":
|
||||
sliderConfig.range = "min";
|
||||
sliderConfig.value = this._max;
|
||||
sliderConfig.value = this._to;
|
||||
break;
|
||||
default:
|
||||
sliderConfig.range = true;
|
||||
sliderConfig.values = [ this._min, this._max ];
|
||||
sliderConfig.values = [ this._from, this._to ];
|
||||
}
|
||||
this._sliderDiv.slider(sliderConfig);
|
||||
this._setRangeIndicators();
|
||||
@ -122,29 +123,36 @@ RangeFacet.prototype._setRangeIndicators = function() {
|
||||
var text;
|
||||
switch (this._config.mode) {
|
||||
case "min":
|
||||
text = "At least " + this._min;
|
||||
text = "At least " + this._from;
|
||||
break;
|
||||
case "max":
|
||||
text = "At most " + this._max;
|
||||
text = "At most " + this._to;
|
||||
break;
|
||||
default:
|
||||
text = this._min + " to " + this._max;
|
||||
text = this._from + " to " + this._to;
|
||||
}
|
||||
this._statusDiv.text(text);
|
||||
};
|
||||
|
||||
RangeFacet.prototype.updateState = function(data) {
|
||||
this._config.min = data.min;
|
||||
this._config.max = data.max;
|
||||
this._config.step = data.step;
|
||||
this._bins = data.bins;
|
||||
|
||||
switch (this._config.mode) {
|
||||
case "min":
|
||||
this._min = data.min;
|
||||
this._from = Math.max(data.from, this._config.min);
|
||||
break;
|
||||
case "max":
|
||||
this._max = data.max;
|
||||
this._to = Math.min(data.to, this._config.max);
|
||||
break;
|
||||
default:
|
||||
this._min = data.min;
|
||||
if ("max" in data) {
|
||||
this._max = data.max;
|
||||
this._from = Math.max(data.from, this._config.min);
|
||||
if ("to" in data) {
|
||||
this._to = Math.min(data.to, this._config.max);
|
||||
} else {
|
||||
this._to = data.max;
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,6 +160,30 @@ RangeFacet.prototype.updateState = function(data) {
|
||||
};
|
||||
|
||||
RangeFacet.prototype.render = function() {
|
||||
this._sliderDiv.slider("option", "min", this._config.min);
|
||||
this._sliderDiv.slider("option", "max", this._config.max);
|
||||
this._sliderDiv.slider("option", "step", this._config.step);
|
||||
|
||||
var max = 0;
|
||||
for (var i = 0; i < this._bins.length; i++) {
|
||||
max = Math.max(max, this._bins[i]);
|
||||
}
|
||||
if (max == 0) {
|
||||
this._histogramDiv.hide();
|
||||
} else {
|
||||
var a = [];
|
||||
for (var i = 0; i < this._bins.length; i++) {
|
||||
a.push(Math.round(100 * this._bins[i] / max));
|
||||
}
|
||||
|
||||
this._histogramDiv.empty().show();
|
||||
$('<img />').attr("src",
|
||||
"http://chart.apis.google.com/chart?cht=ls&chs=" +
|
||||
this._histogramDiv[0].offsetWidth +
|
||||
"x50&chd=t:" + a.join(",")
|
||||
).appendTo(this._histogramDiv);
|
||||
}
|
||||
|
||||
this._setRangeIndicators();
|
||||
};
|
||||
|
||||
|
@ -69,12 +69,15 @@ a.facet-choice-link:hover {
|
||||
|
||||
.facet-range-body {
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
.facet-range-histogram {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.facet-range-slider {
|
||||
}
|
||||
.facet-range-status {
|
||||
margin: 5px 0px;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user