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:
David Huynh 2010-02-03 00:20:42 +00:00
parent 00696a96fc
commit ed5eae83af
12 changed files with 240 additions and 50 deletions

View File

@ -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);
}
}

View File

@ -11,6 +11,7 @@ import com.metaweb.gridlock.browsing.filters.ExpressionNumberComparisonRowFilter
import com.metaweb.gridlock.browsing.filters.RowFilter; import com.metaweb.gridlock.browsing.filters.RowFilter;
import com.metaweb.gridlock.expr.Evaluable; import com.metaweb.gridlock.expr.Evaluable;
import com.metaweb.gridlock.expr.Parser; import com.metaweb.gridlock.expr.Parser;
import com.metaweb.gridlock.model.Column;
import com.metaweb.gridlock.model.Project; import com.metaweb.gridlock.model.Project;
public class RangeFacet implements Facet { public class RangeFacet implements Facet {
@ -22,6 +23,11 @@ public class RangeFacet implements Facet {
protected String _mode; protected String _mode;
protected double _min; protected double _min;
protected double _max; protected double _max;
protected double _step;
protected int[] _bins;
protected double _from;
protected double _to;
public RangeFacet() { public RangeFacet() {
} }
@ -34,15 +40,24 @@ public class RangeFacet implements Facet {
writer.key("name"); writer.value(_name); writer.key("name"); writer.value(_name);
writer.key("expression"); writer.value(_expression); writer.key("expression"); writer.value(_expression);
writer.key("cellIndex"); writer.value(_cellIndex); 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); writer.key("mode"); writer.value(_mode);
if ("min".equals(_mode)) { if ("min".equals(_mode)) {
writer.key("min"); writer.value(_min); writer.key("from"); writer.value(_from);
} else if ("max".equals(_mode)) { } else if ("max".equals(_mode)) {
writer.key("max"); writer.value(_max); writer.key("to"); writer.value(_to);
} else { } else {
writer.key("min"); writer.value(_min); writer.key("from"); writer.value(_from);
writer.key("max"); writer.value(_max); writer.key("to"); writer.value(_to);
} }
writer.endObject(); writer.endObject();
} }
@ -57,12 +72,12 @@ public class RangeFacet implements Facet {
_mode = o.getString("mode"); _mode = o.getString("mode");
if ("min".equals(_mode)) { if ("min".equals(_mode)) {
_min = o.getDouble("min"); _from = o.getDouble("from");
} else if ("max".equals(_mode)) { } else if ("max".equals(_mode)) {
_max = o.getDouble("max"); _to = o.getDouble("to");
} else { } else {
_min = o.getDouble("min"); _from = o.getDouble("from");
_max = o.getDouble("max"); _to = o.getDouble("to");
} }
} }
@ -71,19 +86,19 @@ public class RangeFacet implements Facet {
if ("min".equals(_mode)) { if ("min".equals(_mode)) {
return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) { return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) {
protected boolean checkValue(double d) { protected boolean checkValue(double d) {
return d >= _min; return d >= _from;
}; };
}; };
} else if ("max".equals(_mode)) { } else if ("max".equals(_mode)) {
return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) { return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) {
protected boolean checkValue(double d) { protected boolean checkValue(double d) {
return d <= _max; return d <= _to;
}; };
}; };
} else { } else {
return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) { return new ExpressionNumberComparisonRowFilter(_eval, _cellIndex) {
protected boolean checkValue(double d) { 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 @Override
public void computeChoices(Project project, FilteredRows filteredRows) { 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;
} }
} }

View File

@ -71,7 +71,7 @@ public class ApproveNewReconcileCommand extends Command {
} }
}.init(cellIndex, cellChanges)); }.init(cellIndex, cellChanges));
MassCellChange massCellChange = new MassCellChange(cellChanges); MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex);
HistoryEntry historyEntry = new HistoryEntry( HistoryEntry historyEntry = new HistoryEntry(
project, "Approve new topics for " + columnName, massCellChange); project, "Approve new topics for " + columnName, massCellChange);

View File

@ -71,7 +71,7 @@ public class ApproveReconcileCommand extends Command {
} }
}.init(cellIndex, cellChanges)); }.init(cellIndex, cellChanges));
MassCellChange massCellChange = new MassCellChange(cellChanges); MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex);
HistoryEntry historyEntry = new HistoryEntry( HistoryEntry historyEntry = new HistoryEntry(
project, "Approve best recon candidates for " + columnName, massCellChange); project, "Approve best recon candidates for " + columnName, massCellChange);

View File

@ -67,7 +67,7 @@ public class DiscardReconcileCommand extends Command {
} }
}.init(cellIndex, cellChanges)); }.init(cellIndex, cellChanges));
MassCellChange massCellChange = new MassCellChange(cellChanges); MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex);
HistoryEntry historyEntry = new HistoryEntry( HistoryEntry historyEntry = new HistoryEntry(
project, "Discard recon results for " + columnName, massCellChange); project, "Discard recon results for " + columnName, massCellChange);

View File

@ -85,7 +85,7 @@ public class DoTextTransformCommand extends Command {
} }
}.init(cellIndex, bindings, cellChanges, eval)); }.init(cellIndex, bindings, cellChanges, eval));
MassCellChange massCellChange = new MassCellChange(cellChanges); MassCellChange massCellChange = new MassCellChange(cellChanges, cellIndex);
HistoryEntry historyEntry = new HistoryEntry( HistoryEntry historyEntry = new HistoryEntry(
project, "Text transform on " + columnName + ": " + expression, massCellChange); project, "Text transform on " + columnName + ": " + expression, massCellChange);

View File

@ -8,10 +8,12 @@ import com.metaweb.gridlock.model.Row;
public class MassCellChange implements Change { public class MassCellChange implements Change {
private static final long serialVersionUID = -933571199802688027L; 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()]; _cellChanges = new CellChange[cellChanges.size()];
_commonCellIndex = commonCellIndex;
cellChanges.toArray(_cellChanges); cellChanges.toArray(_cellChanges);
} }
@ -23,6 +25,10 @@ public class MassCellChange implements Change {
for (CellChange cellChange : _cellChanges) { for (CellChange cellChange : _cellChanges) {
rows.get(cellChange.row).cells.set(cellChange.column, cellChange.newCell); 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) { for (CellChange cellChange : _cellChanges) {
rows.get(cellChange.row).cells.set(cellChange.column, cellChange.oldCell); rows.get(cellChange.row).cells.set(cellChange.column, cellChange.oldCell);
} }
if (_commonCellIndex >= 0) {
project.columnModel.getColumnByCellIndex(_commonCellIndex).clearPrecomputes();
}
} }
} }
} }

View File

@ -1,6 +1,8 @@
package com.metaweb.gridlock.model; package com.metaweb.gridlock.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties; import java.util.Properties;
import org.json.JSONException; import org.json.JSONException;
@ -15,6 +17,8 @@ public class Column implements Serializable, Jsonizable {
public String headerLabel; public String headerLabel;
public Class valueType; public Class valueType;
transient protected Map<String, Object> _precomputes;
@Override @Override
public void write(JSONWriter writer, Properties options) public void write(JSONWriter writer, Properties options)
throws JSONException { throws JSONException {
@ -25,4 +29,24 @@ public class Column implements Serializable, Jsonizable {
writer.key("valueType"); writer.value(valueType == null ? null : valueType.getSimpleName()); writer.key("valueType"); writer.value(valueType == null ? null : valueType.getSimpleName());
writer.endObject(); 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);
}
} }

View File

@ -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); HistoryEntry historyEntry = new HistoryEntry(_project, _description, massCellChange);
_project.history.addEntry(historyEntry); _project.history.addEntry(historyEntry);

View File

@ -240,6 +240,7 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
MenuSystem.createAndShowStandardMenu([ MenuSystem.createAndShowStandardMenu([
{ {
label: "Filter", label: "Filter",
tooltip: "Filter rows by this column's cell content or characteristics",
submenu: [ submenu: [
{ "heading" : "On Cell Content" }, { "heading" : "On Cell Content" },
{ {
@ -303,6 +304,9 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
"name" : column.headerLabel + ": judgment", "name" : column.headerLabel + ": judgment",
"cellIndex" : column.cellIndex, "cellIndex" : column.cellIndex,
"expression" : "cell.recon.judgment" "expression" : "cell.recon.judgment"
},
{
"scroll" : false
} }
); );
} }
@ -415,6 +419,7 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
}, },
{ {
label: "Collapse/Expand", label: "Collapse/Expand",
tooltip: "Collapse/expand columns to make viewing the data more convenient",
submenu: [ submenu: [
{ {
label: "Collapse This Column", label: "Collapse This Column",
@ -475,6 +480,7 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
}, },
{ {
label: "Reconcile", label: "Reconcile",
tooltip: "Match this column's cells to topics on Freebase",
submenu: [ submenu: [
{ {
label: "Start Reconciling ...", label: "Start Reconciling ...",

View File

@ -12,14 +12,14 @@ function RangeFacet(div, config, options) {
RangeFacet.prototype._setDefaults = function() { RangeFacet.prototype._setDefaults = function() {
switch (this._config.mode) { switch (this._config.mode) {
case "min": case "min":
this._min = this._config.min; this._from = this._config.min;
break; break;
case "max": case "max":
this._max = this._config.max; this._to = this._config.max;
break; break;
default: default:
this._min = this._config.min; this._from = this._config.min;
this._max = this._config.max; this._to = this._config.max;
} }
}; };
@ -29,18 +29,18 @@ RangeFacet.prototype.getJSON = function() {
switch (this._config.mode) { switch (this._config.mode) {
case "min": case "min":
o.min = this._min; o.from = this._from;
break; break;
case "max": case "max":
o.max = this._max; o.to = this._to;
break; break;
default: default:
o.min = this._min; o.from = this._from;
if (this._max == this._config.max) { if (this._to == this._config.max) {
// pretend to be open-ended // pretend to be open-ended
o.mode = "min"; o.mode = "min";
} else { } else {
o.max = this._max; o.to = this._to;
} }
} }
@ -50,12 +50,12 @@ RangeFacet.prototype.getJSON = function() {
RangeFacet.prototype.hasSelection = function() { RangeFacet.prototype.hasSelection = function() {
switch (this._config.mode) { switch (this._config.mode) {
case "min": case "min":
return this._min > this._config.min; return this._from > this._config.min;
case "max": case "max":
return this._max < this._config.max; return this._to < this._config.max;
default: default:
return this._min > this._config.min || return this._from > this._config.min ||
this._max < this._config.max; this._to < this._config.max;
} }
}; };
@ -71,21 +71,22 @@ RangeFacet.prototype._initializeUI = function() {
}).prependTo(headerDiv); }).prependTo(headerDiv);
var bodyDiv = $('<div></div>').addClass("facet-range-body").appendTo(container); 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._sliderDiv = $('<div></div>').addClass("facet-range-slider").appendTo(bodyDiv);
this._statusDiv = $('<div></div>').addClass("facet-range-status").appendTo(bodyDiv); this._statusDiv = $('<div></div>').addClass("facet-range-status").appendTo(bodyDiv);
var onSlide = function(event, ui) { var onSlide = function(event, ui) {
switch (self._config.mode) { switch (self._config.mode) {
case "min": case "min":
self._min = ui.value; self._from = ui.value;
break; break;
case "max": case "max":
self._max = ui.value; self._to = ui.value;
break; break;
default: default:
self._min = ui.values[0]; self._from = ui.values[0];
self._max = ui.values[1]; self._to = ui.values[1];
} }
self._setRangeIndicators(); self._setRangeIndicators();
self._scheduleUpdate(); self._scheduleUpdate();
@ -104,15 +105,15 @@ RangeFacet.prototype._initializeUI = function() {
switch (this._config.mode) { switch (this._config.mode) {
case "min": case "min":
sliderConfig.range = "max"; sliderConfig.range = "max";
sliderConfig.value = this._min; sliderConfig.value = this._from;
break; break;
case "max": case "max":
sliderConfig.range = "min"; sliderConfig.range = "min";
sliderConfig.value = this._max; sliderConfig.value = this._to;
break; break;
default: default:
sliderConfig.range = true; sliderConfig.range = true;
sliderConfig.values = [ this._min, this._max ]; sliderConfig.values = [ this._from, this._to ];
} }
this._sliderDiv.slider(sliderConfig); this._sliderDiv.slider(sliderConfig);
this._setRangeIndicators(); this._setRangeIndicators();
@ -122,29 +123,36 @@ RangeFacet.prototype._setRangeIndicators = function() {
var text; var text;
switch (this._config.mode) { switch (this._config.mode) {
case "min": case "min":
text = "At least " + this._min; text = "At least " + this._from;
break; break;
case "max": case "max":
text = "At most " + this._max; text = "At most " + this._to;
break; break;
default: default:
text = this._min + " to " + this._max; text = this._from + " to " + this._to;
} }
this._statusDiv.text(text); this._statusDiv.text(text);
}; };
RangeFacet.prototype.updateState = function(data) { 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) { switch (this._config.mode) {
case "min": case "min":
this._min = data.min; this._from = Math.max(data.from, this._config.min);
break; break;
case "max": case "max":
this._max = data.max; this._to = Math.min(data.to, this._config.max);
break; break;
default: default:
this._min = data.min; this._from = Math.max(data.from, this._config.min);
if ("max" in data) { if ("to" in data) {
this._max = data.max; 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() { 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(); this._setRangeIndicators();
}; };

View File

@ -69,12 +69,15 @@ a.facet-choice-link:hover {
.facet-range-body { .facet-range-body {
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 5px; padding: 15px;
}
.facet-range-histogram {
margin-bottom: 10px;
} }
.facet-range-slider { .facet-range-slider {
} }
.facet-range-status { .facet-range-status {
margin: 5px 0px; margin-top: 5px;
text-align: center; text-align: center;
} }