added timeline facet (like the numeric binning facet but working on dates instead of numbers and with date-specific binning logic)
git-svn-id: http://google-refine.googlecode.com/svn/trunk@1234 7d457c2a-affb-35e4-300a-418c747d4874
This commit is contained in:
parent
37099b4c9f
commit
5d788c9260
@ -15,6 +15,7 @@ import com.google.gridworks.browsing.facets.ListFacet;
|
||||
import com.google.gridworks.browsing.facets.RangeFacet;
|
||||
import com.google.gridworks.browsing.facets.ScatterplotFacet;
|
||||
import com.google.gridworks.browsing.facets.TextSearchFacet;
|
||||
import com.google.gridworks.browsing.facets.TimeRangeFacet;
|
||||
import com.google.gridworks.browsing.util.ConjunctiveFilteredRecords;
|
||||
import com.google.gridworks.browsing.util.ConjunctiveFilteredRows;
|
||||
import com.google.gridworks.browsing.util.FilteredRecordsAsFilteredRows;
|
||||
@ -154,6 +155,8 @@ public class Engine implements Jsonizable {
|
||||
facet = new ListFacet();
|
||||
} else if ("range".equals(type)) {
|
||||
facet = new RangeFacet();
|
||||
} else if ("timerange".equals(type)) {
|
||||
facet = new TimeRangeFacet();
|
||||
} else if ("scatterplot".equals(type)) {
|
||||
facet = new ScatterplotFacet();
|
||||
} else if ("text".equals(type)) {
|
||||
|
@ -72,10 +72,10 @@ public class RangeFacet implements Facet {
|
||||
public RangeFacet() {
|
||||
}
|
||||
|
||||
private static final String MIN = "min";
|
||||
private static final String MAX = "max";
|
||||
private static final String TO = "to";
|
||||
private static final String FROM = "from";
|
||||
protected static final String MIN = "min";
|
||||
protected static final String MAX = "max";
|
||||
protected static final String TO = "to";
|
||||
protected static final String FROM = "from";
|
||||
|
||||
public void write(JSONWriter writer, Properties options)
|
||||
throws JSONException {
|
||||
|
@ -0,0 +1,201 @@
|
||||
package com.google.gridworks.browsing.facets;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONWriter;
|
||||
|
||||
import com.google.gridworks.browsing.FilteredRecords;
|
||||
import com.google.gridworks.browsing.FilteredRows;
|
||||
import com.google.gridworks.browsing.RowFilter;
|
||||
import com.google.gridworks.browsing.filters.ExpressionTimeComparisonRowFilter;
|
||||
import com.google.gridworks.browsing.util.ExpressionTimeValueBinner;
|
||||
import com.google.gridworks.browsing.util.RowEvaluable;
|
||||
import com.google.gridworks.browsing.util.TimeBinIndex;
|
||||
import com.google.gridworks.browsing.util.TimeBinRecordIndex;
|
||||
import com.google.gridworks.browsing.util.TimeBinRowIndex;
|
||||
import com.google.gridworks.expr.MetaParser;
|
||||
import com.google.gridworks.expr.ParsingException;
|
||||
import com.google.gridworks.model.Column;
|
||||
import com.google.gridworks.model.Project;
|
||||
import com.google.gridworks.util.JSONUtilities;
|
||||
|
||||
public class TimeRangeFacet extends RangeFacet {
|
||||
|
||||
protected boolean _selectTime; // whether the time selection applies, default true
|
||||
protected boolean _selectNonTime;
|
||||
|
||||
protected int _baseTimeCount;
|
||||
protected int _baseNonTimeCount;
|
||||
|
||||
protected int _timeCount;
|
||||
protected int _nonTimeCount;
|
||||
|
||||
public void write(JSONWriter writer, Properties options) throws JSONException {
|
||||
|
||||
writer.object();
|
||||
writer.key("name"); writer.value(_name);
|
||||
writer.key("expression"); writer.value(_expression);
|
||||
writer.key("columnName"); writer.value(_columnName);
|
||||
|
||||
if (_errorMessage != null) {
|
||||
writer.key("error"); writer.value(_errorMessage);
|
||||
} else {
|
||||
if (!Double.isInfinite(_min) && !Double.isInfinite(_max)) {
|
||||
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("baseBins"); writer.array();
|
||||
for (int b : _baseBins) {
|
||||
writer.value(b);
|
||||
}
|
||||
writer.endArray();
|
||||
|
||||
writer.key(FROM); writer.value(_from);
|
||||
writer.key(TO); writer.value(_to);
|
||||
}
|
||||
|
||||
writer.key("baseTimeCount"); writer.value(_baseTimeCount);
|
||||
writer.key("baseNonTimeCount"); writer.value(_baseNonTimeCount);
|
||||
writer.key("baseBlankCount"); writer.value(_baseBlankCount);
|
||||
writer.key("baseErrorCount"); writer.value(_baseErrorCount);
|
||||
|
||||
writer.key("timeCount"); writer.value(_timeCount);
|
||||
writer.key("nonTimeCount"); writer.value(_nonTimeCount);
|
||||
writer.key("blankCount"); writer.value(_blankCount);
|
||||
writer.key("errorCount"); writer.value(_errorCount);
|
||||
}
|
||||
writer.endObject();
|
||||
}
|
||||
|
||||
public void initializeFromJSON(Project project, JSONObject o) throws Exception {
|
||||
_name = o.getString("name");
|
||||
_expression = o.getString("expression");
|
||||
_columnName = o.getString("columnName");
|
||||
|
||||
if (_columnName.length() > 0) {
|
||||
Column column = project.columnModel.getColumnByName(_columnName);
|
||||
if (column != null) {
|
||||
_cellIndex = column.getCellIndex();
|
||||
} else {
|
||||
_errorMessage = "No column named " + _columnName;
|
||||
}
|
||||
} else {
|
||||
_cellIndex = -1;
|
||||
}
|
||||
|
||||
try {
|
||||
_eval = MetaParser.parse(_expression);
|
||||
} catch (ParsingException e) {
|
||||
_errorMessage = e.getMessage();
|
||||
}
|
||||
|
||||
if (o.has(FROM) || o.has(TO)) {
|
||||
_from = o.has(FROM) ? o.getDouble(FROM) : _min;
|
||||
_to = o.has(TO) ? o.getDouble(TO) : _max;
|
||||
_selected = true;
|
||||
}
|
||||
|
||||
_selectTime = JSONUtilities.getBoolean(o, "selectTime", true);
|
||||
_selectNonTime = JSONUtilities.getBoolean(o, "selectNonTime", true);
|
||||
_selectBlank = JSONUtilities.getBoolean(o, "selectBlank", true);
|
||||
_selectError = JSONUtilities.getBoolean(o, "selectError", true);
|
||||
|
||||
if (!_selectTime || !_selectNonTime || !_selectBlank || !_selectError) {
|
||||
_selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
public RowFilter getRowFilter(Project project) {
|
||||
if (_eval != null && _errorMessage == null && _selected) {
|
||||
return new ExpressionTimeComparisonRowFilter(
|
||||
getRowEvaluable(project), _selectTime, _selectNonTime, _selectBlank, _selectError) {
|
||||
|
||||
protected boolean checkValue(long t) {
|
||||
return t >= _from && t < _to;
|
||||
};
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void computeChoices(Project project, FilteredRows filteredRows) {
|
||||
if (_eval != null && _errorMessage == null) {
|
||||
RowEvaluable rowEvaluable = getRowEvaluable(project);
|
||||
|
||||
Column column = project.columnModel.getColumnByCellIndex(_cellIndex);
|
||||
String key = "time-bin:row-based:" + _expression;
|
||||
TimeBinIndex index = (TimeBinIndex) column.getPrecompute(key);
|
||||
if (index == null) {
|
||||
index = new TimeBinRowIndex(project, rowEvaluable);
|
||||
column.setPrecompute(key, index);
|
||||
}
|
||||
|
||||
retrieveDataFromBaseBinIndex(index);
|
||||
|
||||
ExpressionTimeValueBinner binner = new ExpressionTimeValueBinner(rowEvaluable, index);
|
||||
|
||||
filteredRows.accept(project, binner);
|
||||
retrieveDataFromBinner(binner);
|
||||
}
|
||||
}
|
||||
|
||||
public void computeChoices(Project project, FilteredRecords filteredRecords) {
|
||||
if (_eval != null && _errorMessage == null) {
|
||||
RowEvaluable rowEvaluable = getRowEvaluable(project);
|
||||
|
||||
Column column = project.columnModel.getColumnByCellIndex(_cellIndex);
|
||||
String key = "time-bin:record-based:" + _expression;
|
||||
TimeBinIndex index = (TimeBinIndex) column.getPrecompute(key);
|
||||
if (index == null) {
|
||||
index = new TimeBinRecordIndex(project, rowEvaluable);
|
||||
column.setPrecompute(key, index);
|
||||
}
|
||||
|
||||
retrieveDataFromBaseBinIndex(index);
|
||||
|
||||
ExpressionTimeValueBinner binner = new ExpressionTimeValueBinner(rowEvaluable, index);
|
||||
|
||||
filteredRecords.accept(project, binner);
|
||||
|
||||
retrieveDataFromBinner(binner);
|
||||
}
|
||||
}
|
||||
|
||||
protected void retrieveDataFromBaseBinIndex(TimeBinIndex index) {
|
||||
_min = index.getMin();
|
||||
_max = index.getMax();
|
||||
_step = index.getStep();
|
||||
_baseBins = index.getBins();
|
||||
|
||||
_baseTimeCount = index.getTimeRowCount();
|
||||
_baseNonTimeCount = index.getNonTimeRowCount();
|
||||
_baseBlankCount = index.getBlankRowCount();
|
||||
_baseErrorCount = index.getErrorRowCount();
|
||||
|
||||
if (_selected) {
|
||||
_from = Math.max(_from, _min);
|
||||
_to = Math.min(_to, _max);
|
||||
} else {
|
||||
_from = _min;
|
||||
_to = _max;
|
||||
}
|
||||
}
|
||||
|
||||
protected void retrieveDataFromBinner(ExpressionTimeValueBinner binner) {
|
||||
_bins = binner.bins;
|
||||
_timeCount = binner.timeCount;
|
||||
_nonTimeCount = binner.nonTimeCount;
|
||||
_blankCount = binner.blankCount;
|
||||
_errorCount = binner.errorCount;
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package com.google.gridworks.browsing.filters;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.google.gridworks.browsing.util.RowEvaluable;
|
||||
import com.google.gridworks.expr.ExpressionUtils;
|
||||
|
||||
/**
|
||||
* Judge if a row matches by evaluating a given expression on the row, based on a particular
|
||||
* column, and checking the result. It's a match if the result satisfies some time comparisons,
|
||||
* or if the result is not a time or blank or error and we want non-time or blank or error
|
||||
* values.
|
||||
*/
|
||||
abstract public class ExpressionTimeComparisonRowFilter extends ExpressionNumberComparisonRowFilter {
|
||||
|
||||
final protected boolean _selectTime;
|
||||
final protected boolean _selectNonTime;
|
||||
|
||||
public ExpressionTimeComparisonRowFilter(
|
||||
RowEvaluable rowEvaluable,
|
||||
boolean selectTime,
|
||||
boolean selectNonTime,
|
||||
boolean selectBlank,
|
||||
boolean selectError
|
||||
) {
|
||||
super(rowEvaluable, selectTime, selectNonTime, selectBlank, selectError);
|
||||
_selectTime = selectTime;
|
||||
_selectNonTime = selectNonTime;
|
||||
}
|
||||
|
||||
protected boolean checkValue(Object v) {
|
||||
if (ExpressionUtils.isError(v)) {
|
||||
return _selectError;
|
||||
} else if (ExpressionUtils.isNonBlankData(v)) {
|
||||
if (v instanceof Date) {
|
||||
long time = ((Date) v).getTime();
|
||||
return _selectTime && checkValue(time);
|
||||
} else {
|
||||
return _selectNonTime;
|
||||
}
|
||||
} else {
|
||||
return _selectBlank;
|
||||
}
|
||||
}
|
||||
|
||||
// not really needed for operation, just to make extending the abstract class possible
|
||||
protected boolean checkValue(double d) {
|
||||
return false;
|
||||
}
|
||||
|
||||
abstract protected boolean checkValue(long d);
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
package com.google.gridworks.browsing.util;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.google.gridworks.browsing.RecordVisitor;
|
||||
import com.google.gridworks.browsing.RowVisitor;
|
||||
import com.google.gridworks.expr.ExpressionUtils;
|
||||
import com.google.gridworks.model.Project;
|
||||
import com.google.gridworks.model.Record;
|
||||
import com.google.gridworks.model.Row;
|
||||
|
||||
/**
|
||||
* Visit matched rows or records and slot them into bins based on the date computed
|
||||
* from a given expression.
|
||||
*/
|
||||
public class ExpressionTimeValueBinner implements RowVisitor, RecordVisitor {
|
||||
|
||||
/*
|
||||
* Configuration
|
||||
*/
|
||||
final protected RowEvaluable _rowEvaluable;
|
||||
final protected TimeBinIndex _index; // base bins
|
||||
|
||||
/*
|
||||
* Computed results
|
||||
*/
|
||||
final public int[] bins;
|
||||
public int timeCount;
|
||||
public int nonTimeCount;
|
||||
public int blankCount;
|
||||
public int errorCount;
|
||||
|
||||
/*
|
||||
* Scratchpad variables
|
||||
*/
|
||||
protected boolean hasError;
|
||||
protected boolean hasBlank;
|
||||
protected boolean hasTime;
|
||||
protected boolean hasNonTime;
|
||||
|
||||
public ExpressionTimeValueBinner(RowEvaluable rowEvaluable, TimeBinIndex index) {
|
||||
_rowEvaluable = rowEvaluable;
|
||||
_index = index;
|
||||
bins = new int[_index.getBins().length];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Project project) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end(Project project) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(Project project, int rowIndex, Row row) {
|
||||
resetFlags();
|
||||
|
||||
Properties bindings = ExpressionUtils.createBindings(project);
|
||||
processRow(project, rowIndex, row, bindings);
|
||||
|
||||
updateCounts();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(Project project, Record record) {
|
||||
resetFlags();
|
||||
|
||||
Properties bindings = ExpressionUtils.createBindings(project);
|
||||
for (int r = record.fromRowIndex; r < record.toRowIndex; r++) {
|
||||
processRow(project, r, project.rows.get(r), bindings);
|
||||
}
|
||||
|
||||
updateCounts();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void resetFlags() {
|
||||
hasError = false;
|
||||
hasBlank = false;
|
||||
hasTime = false;
|
||||
hasNonTime = false;
|
||||
}
|
||||
|
||||
protected void updateCounts() {
|
||||
if (hasError) {
|
||||
errorCount++;
|
||||
}
|
||||
if (hasBlank) {
|
||||
blankCount++;
|
||||
}
|
||||
if (hasTime) {
|
||||
timeCount++;
|
||||
}
|
||||
if (hasNonTime) {
|
||||
nonTimeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
protected void processRow(Project project, int rowIndex, Row row, Properties bindings) {
|
||||
Object value = _rowEvaluable.eval(project, rowIndex, row, bindings);
|
||||
if (value != null) {
|
||||
if (value.getClass().isArray()) {
|
||||
Object[] a = (Object[]) value;
|
||||
for (Object v : a) {
|
||||
processValue(v);
|
||||
}
|
||||
return;
|
||||
} else if (value instanceof Collection<?>) {
|
||||
for (Object v : ExpressionUtils.toObjectCollection(value)) {
|
||||
processValue(v);
|
||||
}
|
||||
return;
|
||||
} // else, fall through
|
||||
}
|
||||
|
||||
processValue(value);
|
||||
}
|
||||
|
||||
protected void processValue(Object value) {
|
||||
if (ExpressionUtils.isError(value)) {
|
||||
hasError = true;
|
||||
} else if (ExpressionUtils.isNonBlankData(value)) {
|
||||
if (value instanceof Date) {
|
||||
long t = ((Date) value).getTime();
|
||||
hasTime = true;
|
||||
|
||||
int bin = (int) Math.floor((t - _index.getMin()) / _index.getStep());
|
||||
if (bin >= 0 && bin < bins.length) { // as a precaution
|
||||
bins[bin]++;
|
||||
}
|
||||
} else {
|
||||
hasNonTime = true;
|
||||
}
|
||||
} else {
|
||||
hasBlank = true;
|
||||
}
|
||||
}
|
||||
}
|
218
main/src/com/google/gridworks/browsing/util/TimeBinIndex.java
Normal file
218
main/src/com/google/gridworks/browsing/util/TimeBinIndex.java
Normal file
@ -0,0 +1,218 @@
|
||||
package com.google.gridworks.browsing.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.google.gridworks.expr.ExpressionUtils;
|
||||
import com.google.gridworks.model.Project;
|
||||
import com.google.gridworks.model.Row;
|
||||
|
||||
/**
|
||||
* A utility class for computing the base bins that form the base histograms of
|
||||
* temporal range facets. It evaluates an expression on all the rows of a project to
|
||||
* get temporal values, determines how many bins to distribute those values in, and
|
||||
* bins the rows accordingly.
|
||||
*
|
||||
* This class processes all rows rather than just the filtered rows because it
|
||||
* needs to compute the base bins of a temporal range facet, which remain unchanged
|
||||
* as the user interacts with the facet.
|
||||
*/
|
||||
abstract public class TimeBinIndex {
|
||||
|
||||
protected int _totalValueCount;
|
||||
protected int _timeValueCount;
|
||||
protected long _min;
|
||||
protected long _max;
|
||||
protected long _step;
|
||||
protected int[] _bins;
|
||||
|
||||
protected int _timeRowCount;
|
||||
protected int _nonTimeRowCount;
|
||||
protected int _blankRowCount;
|
||||
protected int _errorRowCount;
|
||||
|
||||
protected boolean _hasError = false;
|
||||
protected boolean _hasNonTime = false;
|
||||
protected boolean _hasTime = false;
|
||||
protected boolean _hasBlank = false;
|
||||
|
||||
protected long[] steps = {
|
||||
1, // msec
|
||||
1000, // sec
|
||||
1000*60, // min
|
||||
1000*60*60, // hour
|
||||
1000*60*60*24, // day
|
||||
1000*60*60*24*7, // week
|
||||
1000*2629746, // month (average Gregorian year / 12)
|
||||
1000*31556952, // year (average Gregorian year)
|
||||
1000*31556952*10, // decade
|
||||
1000*31556952*100, // century
|
||||
1000*31556952*1000, // millennium
|
||||
};
|
||||
|
||||
abstract protected void iterate(Project project, RowEvaluable rowEvaluable, List<Long> allValues);
|
||||
|
||||
public TimeBinIndex(Project project, RowEvaluable rowEvaluable) {
|
||||
_min = Long.MAX_VALUE;
|
||||
_max = Long.MIN_VALUE;
|
||||
|
||||
List<Long> allValues = new ArrayList<Long>();
|
||||
|
||||
iterate(project, rowEvaluable, allValues);
|
||||
|
||||
_timeValueCount = allValues.size();
|
||||
|
||||
if (_min >= _max) {
|
||||
_step = 1;
|
||||
_min = Math.min(_min, _max);
|
||||
_max = _step;
|
||||
_bins = new int[1];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
long diff = _max - _min;
|
||||
|
||||
for (int i = 0; i < steps.length; i++) {
|
||||
_step = steps[i];
|
||||
if (diff / _step <= 100) break;
|
||||
}
|
||||
|
||||
_bins = new int[(int) (diff / _step) + 1];
|
||||
for (long d : allValues) {
|
||||
int bin = (int) Math.max((d - _min) / _step,0);
|
||||
_bins[bin]++;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTemporal() {
|
||||
return _timeValueCount > _totalValueCount / 2;
|
||||
}
|
||||
|
||||
public long getMin() {
|
||||
return _min;
|
||||
}
|
||||
|
||||
public long getMax() {
|
||||
return _max;
|
||||
}
|
||||
|
||||
public long getStep() {
|
||||
return _step;
|
||||
}
|
||||
|
||||
public int[] getBins() {
|
||||
return _bins;
|
||||
}
|
||||
|
||||
public int getTimeRowCount() {
|
||||
return _timeRowCount;
|
||||
}
|
||||
|
||||
public int getNonTimeRowCount() {
|
||||
return _nonTimeRowCount;
|
||||
}
|
||||
|
||||
public int getBlankRowCount() {
|
||||
return _blankRowCount;
|
||||
}
|
||||
|
||||
public int getErrorRowCount() {
|
||||
return _errorRowCount;
|
||||
}
|
||||
|
||||
protected void processRow(
|
||||
Project project,
|
||||
RowEvaluable rowEvaluable,
|
||||
List<Long> allValues,
|
||||
int rowIndex,
|
||||
Row row,
|
||||
Properties bindings
|
||||
) {
|
||||
Object value = rowEvaluable.eval(project, rowIndex, row, bindings);
|
||||
|
||||
if (ExpressionUtils.isError(value)) {
|
||||
_hasError = true;
|
||||
} else if (ExpressionUtils.isNonBlankData(value)) {
|
||||
if (value.getClass().isArray()) {
|
||||
Object[] a = (Object[]) value;
|
||||
for (Object v : a) {
|
||||
_totalValueCount++;
|
||||
|
||||
if (ExpressionUtils.isError(v)) {
|
||||
_hasError = true;
|
||||
} else if (ExpressionUtils.isNonBlankData(v)) {
|
||||
if (v instanceof Date) {
|
||||
_hasTime = true;
|
||||
processValue(((Date) v).getTime(), allValues);
|
||||
} else {
|
||||
_hasNonTime = true;
|
||||
}
|
||||
} else {
|
||||
_hasBlank = true;
|
||||
}
|
||||
}
|
||||
} else if (value instanceof Collection<?>) {
|
||||
for (Object v : ExpressionUtils.toObjectCollection(value)) {
|
||||
_totalValueCount++;
|
||||
|
||||
if (ExpressionUtils.isError(v)) {
|
||||
_hasError = true;
|
||||
} else if (ExpressionUtils.isNonBlankData(v)) {
|
||||
if (v instanceof Date) {
|
||||
_hasTime = true;
|
||||
processValue(((Date) v).getTime(), allValues);
|
||||
} else {
|
||||
_hasNonTime = true;
|
||||
}
|
||||
} else {
|
||||
_hasBlank = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_totalValueCount++;
|
||||
|
||||
if (value instanceof Date) {
|
||||
_hasTime = true;
|
||||
processValue(((Date) value).getTime(), allValues);
|
||||
} else {
|
||||
_hasNonTime = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_hasBlank = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected void preprocessing() {
|
||||
_hasBlank = false;
|
||||
_hasError = false;
|
||||
_hasNonTime = false;
|
||||
_hasTime = false;
|
||||
}
|
||||
|
||||
protected void postprocessing() {
|
||||
if (_hasError) {
|
||||
_errorRowCount++;
|
||||
}
|
||||
if (_hasBlank) {
|
||||
_blankRowCount++;
|
||||
}
|
||||
if (_hasTime) {
|
||||
_timeRowCount++;
|
||||
}
|
||||
if (_hasNonTime) {
|
||||
_nonTimeRowCount++;
|
||||
}
|
||||
}
|
||||
|
||||
protected void processValue(long v, List<Long> allValues) {
|
||||
_min = Math.min(_min, v);
|
||||
_max = Math.max(_max, v);
|
||||
allValues.add(v);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package com.google.gridworks.browsing.util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.google.gridworks.expr.ExpressionUtils;
|
||||
import com.google.gridworks.model.Project;
|
||||
import com.google.gridworks.model.Record;
|
||||
import com.google.gridworks.model.Row;
|
||||
|
||||
public class TimeBinRecordIndex extends TimeBinIndex {
|
||||
|
||||
public TimeBinRecordIndex(Project project, RowEvaluable rowEvaluable) {
|
||||
super(project, rowEvaluable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void iterate(Project project, RowEvaluable rowEvaluable, List<Long> allValues) {
|
||||
|
||||
Properties bindings = ExpressionUtils.createBindings(project);
|
||||
int count = project.recordModel.getRecordCount();
|
||||
|
||||
for (int r = 0; r < count; r++) {
|
||||
Record record = project.recordModel.getRecord(r);
|
||||
|
||||
preprocessing();
|
||||
|
||||
for (int i = record.fromRowIndex; i < record.toRowIndex; i++) {
|
||||
Row row = project.rows.get(i);
|
||||
|
||||
processRow(project, rowEvaluable, allValues, i, row, bindings);
|
||||
}
|
||||
|
||||
postprocessing();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.google.gridworks.browsing.util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.google.gridworks.expr.ExpressionUtils;
|
||||
import com.google.gridworks.model.Project;
|
||||
import com.google.gridworks.model.Row;
|
||||
|
||||
public class TimeBinRowIndex extends TimeBinIndex {
|
||||
|
||||
public TimeBinRowIndex(Project project, RowEvaluable rowEvaluable) {
|
||||
super(project, rowEvaluable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void iterate(Project project, RowEvaluable rowEvaluable, List<Long> allValues) {
|
||||
|
||||
Properties bindings = ExpressionUtils.createBindings(project);
|
||||
|
||||
for (int i = 0; i < project.rows.size(); i++) {
|
||||
Row row = project.rows.get(i);
|
||||
|
||||
preprocessing();
|
||||
|
||||
processRow(project, rowEvaluable, allValues, i, row, bindings);
|
||||
|
||||
postprocessing();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -208,6 +208,7 @@ function init() {
|
||||
|
||||
"scripts/facets/list-facet.js",
|
||||
"scripts/facets/range-facet.js",
|
||||
"scripts/facets/timerange-facet.js",
|
||||
"scripts/facets/scatterplot-facet.js",
|
||||
"scripts/facets/text-search-facet.js",
|
||||
|
||||
|
392
main/webapp/modules/core/scripts/facets/timerange-facet.js
Normal file
392
main/webapp/modules/core/scripts/facets/timerange-facet.js
Normal file
@ -0,0 +1,392 @@
|
||||
function TimeRangeFacet(div, config, options) {
|
||||
this._div = div;
|
||||
this._config = config;
|
||||
this._options = options;
|
||||
|
||||
this._from = ("from" in this._config) ? this._config.from : null;
|
||||
this._to = ("to" in this._config) ? this._config.to : null;
|
||||
this._step = ("step" in this._config) ? this._config.step : null;
|
||||
|
||||
this._selectTime = ("selectTime" in this._config) ? this._config.selectTime : true;
|
||||
this._selectNonTime = ("selectNonTime" in this._config) ? this._config.selectNonTime : true;
|
||||
this._selectBlank = ("selectBlank" in this._config) ? this._config.selectBlank : true;
|
||||
this._selectError = ("selectError" in this._config) ? this._config.selectError : true;
|
||||
|
||||
this._baseTimeCount = 0;
|
||||
this._baseNonTimeCount = 0;
|
||||
this._baseBlankCount = 0;
|
||||
this._baseErrorCount = 0;
|
||||
|
||||
this._TimeCount = 0;
|
||||
this._nonTimeCount = 0;
|
||||
this._blankCount = 0;
|
||||
this._errorCount = 0;
|
||||
|
||||
this._error = false;
|
||||
this._initializedUI = false;
|
||||
}
|
||||
|
||||
TimeRangeFacet.prototype.reset = function() {
|
||||
this._from = this._config.min;
|
||||
this._to = this._config.max;
|
||||
this._sliderWidget.update(
|
||||
this._config.min,
|
||||
this._config.max,
|
||||
this._config.step,
|
||||
this._from,
|
||||
this._to
|
||||
);
|
||||
|
||||
this._selectTime = true;
|
||||
this._selectNonTime = true;
|
||||
this._selectBlank = true;
|
||||
this._selectError = true;
|
||||
|
||||
this._setRangeIndicators();
|
||||
};
|
||||
|
||||
TimeRangeFacet.reconstruct = function(div, uiState) {
|
||||
return new TimeRangeFacet(div, uiState.c, uiState.o);
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype.dispose = function() {
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype.getUIState = function() {
|
||||
var json = {
|
||||
c: this.getJSON(),
|
||||
o: this._options
|
||||
};
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype.getJSON = function() {
|
||||
var o = {
|
||||
type: "timerange",
|
||||
name: this._config.name,
|
||||
expression: this._config.expression,
|
||||
columnName: this._config.columnName,
|
||||
selectTime: this._selectTime,
|
||||
selectNonTime: this._selectNonTime,
|
||||
selectBlank: this._selectBlank,
|
||||
selectError: this._selectError
|
||||
};
|
||||
|
||||
if (this._from !== null) {
|
||||
o.from = this._from;
|
||||
}
|
||||
if (this._to !== null) {
|
||||
o.to = this._to;
|
||||
}
|
||||
|
||||
return o;
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype.hasSelection = function() {
|
||||
if (!this._selectTime || !this._selectNonTime || !this._selectBlank || !this._selectError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (this._from !== null && (!this._initializedUI || this._from > this._config.min)) ||
|
||||
(this._to !== null && (!this._initializedUI || this._to < this._config.max));
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype._initializeUI = function() {
|
||||
var self = this;
|
||||
this._div
|
||||
.empty()
|
||||
.show()
|
||||
.html(
|
||||
'<div class="facet-title" bind="headerDiv">' +
|
||||
'<div class="grid-layout layout-tightest layout-full"><table><tr>' +
|
||||
'<td width="1%"><a href="javascript:{}" title="Remove this facet" class="facet-title-remove" bind="removeButton"> </a></td>' +
|
||||
'<td>' +
|
||||
'<a href="javascript:{}" class="facet-choice-link" bind="resetButton">reset</a>' +
|
||||
'<a href="javascript:{}" class="facet-choice-link" bind="changeButton">change</a>' +
|
||||
'<span bind="facetTitle"></span>' +
|
||||
'</td>' +
|
||||
'</tr></table></div>' +
|
||||
'</div>' +
|
||||
'<div class="facet-expression" bind="expressionDiv"></div>' +
|
||||
'<div class="facet-range-body">' +
|
||||
'<div class="facet-range-message" bind="messageDiv">Loading...</div>' +
|
||||
'<div class="facet-range-slider" bind="sliderWidgetDiv">' +
|
||||
'<div class="facet-range-histogram" bind="histogramDiv"></div>' +
|
||||
'</div>' +
|
||||
'<div class="facet-range-status" bind="statusDiv"></div>' +
|
||||
'<div class="facet-range-other-choices" bind="otherChoicesDiv"></div>' +
|
||||
'</div>'
|
||||
);
|
||||
this._elmts = DOM.bind(this._div);
|
||||
|
||||
this._elmts.facetTitle.text(this._config.name);
|
||||
this._elmts.changeButton.attr("title","Current Expression: " + this._config.expression).click(function() {
|
||||
self._elmts.expressionDiv.slideToggle(100);
|
||||
});
|
||||
this._elmts.expressionDiv.text(this._config.expression).click(function() {
|
||||
self._editExpression();
|
||||
}).hide();
|
||||
|
||||
this._elmts.resetButton.click(function() {
|
||||
self.reset();
|
||||
self._updateRest();
|
||||
});
|
||||
this._elmts.removeButton.click(function() {
|
||||
self._remove();
|
||||
});
|
||||
|
||||
this._histogram = new HistogramWidget(this._elmts.histogramDiv, { binColors: [ "#ccccff", "#6666ff" ] });
|
||||
this._sliderWidget = new SliderWidget(this._elmts.sliderWidgetDiv);
|
||||
|
||||
this._elmts.sliderWidgetDiv.bind("slide", function(evt, data) {
|
||||
self._from = data.from;
|
||||
self._to = data.to;
|
||||
self._setRangeIndicators();
|
||||
}).bind("stop", function(evt, data) {
|
||||
self._from = data.from;
|
||||
self._to = data.to;
|
||||
self._selectTime = true;
|
||||
self._updateRest();
|
||||
});
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype._renderOtherChoices = function() {
|
||||
var self = this;
|
||||
var container = this._elmts.otherChoicesDiv.empty();
|
||||
|
||||
if (this._baseNonTimeCount === 0 && this._baseBlankCount === 0 && this._baseErrorCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var facet_id = this._div.attr("id");
|
||||
|
||||
var choices = $('<div>').addClass("facet-range-choices");
|
||||
|
||||
// ----------------- time -----------------
|
||||
|
||||
var timeCheck = $('<input type="checkbox" />').attr("id",facet_id + "-time").appendTo(choices).change(function() {
|
||||
self._selectTime = !self._selectTime;
|
||||
self._updateRest();
|
||||
});
|
||||
if (this._selectTime) timeCheck.attr("checked","checked");
|
||||
|
||||
var timeLabel = $('<label>').attr("for", facet_id + "-time").appendTo(choices);
|
||||
$('<span>').text("Time ").addClass("facet-choice-label").appendTo(timeLabel);
|
||||
$('<br>').appendTo(timeLabel);
|
||||
$('<span>').text(this._timeCount).addClass("facet-choice-count").appendTo(timeLabel);
|
||||
|
||||
// ----------------- non-Time -----------------
|
||||
|
||||
var nonTimeCheck = $('<input type="checkbox" />').attr("id",facet_id + "-non-time").appendTo(choices).change(function() {
|
||||
self._selectNonTime = !self._selectNonTime;
|
||||
self._updateRest();
|
||||
});
|
||||
if (this._selectNonTime) nonTimeCheck.attr("checked","checked");
|
||||
|
||||
var nonTimeLabel = $('<label>').attr("for", facet_id + "-non-time").appendTo(choices);
|
||||
$('<span>').text("Non-Time ").addClass("facet-choice-label").appendTo(nonTimeLabel);
|
||||
$('<br>').appendTo(nonTimeLabel);
|
||||
$('<span>').text(this._nonTimeCount).addClass("facet-choice-count").appendTo(nonTimeLabel);
|
||||
|
||||
if (this._baseNonTimeCount === 0) nonTimeCheck.removeAttr("checked");
|
||||
|
||||
// ----------------- blank -----------------
|
||||
|
||||
var blankCheck = $('<input type="checkbox" />').attr("id",facet_id + "-blank").appendTo(choices).change(function() {
|
||||
self._selectBlank = !self._selectBlank;
|
||||
self._updateRest();
|
||||
});
|
||||
if (this._selectBlank) blankCheck.attr("checked","checked");
|
||||
|
||||
var blankLabel = $('<label>').attr("for", facet_id + "-blank").appendTo(choices);
|
||||
$('<span>').text("Blank ").addClass("facet-choice-label").appendTo(blankLabel);
|
||||
$('<br>').appendTo(blankLabel);
|
||||
$('<span>').text(this._blankCount).addClass("facet-choice-count").appendTo(blankLabel);
|
||||
|
||||
if (this._baseBlankCount === 0) blankCheck.removeAttr("checked");
|
||||
|
||||
// ----------------- error -----------------
|
||||
|
||||
var errorCheck = $('<input type="checkbox" />').attr("id",facet_id + "-error").appendTo(choices).change(function() {
|
||||
self._selectError = !self._selectError;
|
||||
self._updateRest();
|
||||
});
|
||||
if (this._selectError) errorCheck.attr("checked","checked");
|
||||
|
||||
var errorLabel = $('<label>').attr("for", facet_id + "-error").appendTo(choices);
|
||||
$('<span>').text("Error ").addClass("facet-choice-label").appendTo(errorLabel);
|
||||
$('<br>').appendTo(errorLabel);
|
||||
$('<span>').text(this._errorCount).addClass("facet-choice-count").appendTo(errorLabel);
|
||||
|
||||
if (this._baseErrorCount === 0) errorCheck.removeAttr("checked");
|
||||
|
||||
// --------------------------
|
||||
|
||||
choices.buttonset().appendTo(container);
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype.steps = [
|
||||
1, // msec
|
||||
1000, // sec
|
||||
1000*60, // min
|
||||
1000*60*60, // hour
|
||||
1000*60*60*24, // day
|
||||
1000*60*60*24*7, // week
|
||||
1000*2629746, // month (average Gregorian year / 12)
|
||||
1000*31556952, // year (average Gregorian year)
|
||||
1000*31556952*10, // decade
|
||||
1000*31556952*100, // century
|
||||
1000*31556952*1000, // millennium
|
||||
];
|
||||
|
||||
TimeRangeFacet.prototype._setRangeIndicators = function() {
|
||||
var fromDate = new Date(this._from);
|
||||
var toDate = new Date(this._to);
|
||||
|
||||
if (this._step > 2629746000) { // > month
|
||||
var format = "yyyy";
|
||||
this._elmts.statusDiv.html(fromDate.toString(format) + " — " + toDate.toString(format));
|
||||
} else if (this.step > 3600000) { // > hour
|
||||
var format = "yyyy-MM-dd";
|
||||
this._elmts.statusDiv.html(fromDate.toString(format) + " — " + toDate.toString(format));
|
||||
} else {
|
||||
var format = "HH:mm:ss";
|
||||
this._elmts.statusDiv.html("<b style='margin-right: 4em'>" + fromDate.toString("yyyy-MM-dd") + "</b> " + fromDate.toString(format) + " — " + toDate.toString(format));
|
||||
}
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype._addCommas = function(nStr) {
|
||||
nStr += '';
|
||||
x = nStr.split('.');
|
||||
x1 = x[0];
|
||||
x2 = x.length > 1 ? '.' + x[1] : '';
|
||||
var rgx = /(\d+)(\d{3})/;
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, '$1' + ',' + '$2');
|
||||
}
|
||||
return x1 + x2;
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype.updateState = function(data) {
|
||||
if ("min" in data && "max" in data) {
|
||||
this._error = false;
|
||||
|
||||
this._config.min = data.min;
|
||||
this._config.max = data.max;
|
||||
this._config.step = data.step;
|
||||
this._baseBins = data.baseBins;
|
||||
this._bins = data.bins;
|
||||
|
||||
switch (this._config.mode) {
|
||||
case "min":
|
||||
this._from = Math.max(data.from, this._config.min);
|
||||
break;
|
||||
case "max":
|
||||
this._to = Math.min(data.to, this._config.max);
|
||||
break;
|
||||
default:
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
this._baseTimeCount = data.baseTimeCount;
|
||||
this._baseNonTimeCount = data.baseNonTimeCount;
|
||||
this._baseBlankCount = data.baseBlankCount;
|
||||
this._baseErrorCount = data.baseErrorCount;
|
||||
|
||||
this._timeCount = data.timeCount;
|
||||
this._nonTimeCount = data.nonTimeCount;
|
||||
this._blankCount = data.blankCount;
|
||||
this._errorCount = data.errorCount;
|
||||
} else {
|
||||
this._error = true;
|
||||
this._errorMessage = "error" in data ? data.error : "Unknown error.";
|
||||
}
|
||||
|
||||
this.render();
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype.render = function() {
|
||||
if (!this._initializedUI) {
|
||||
this._initializeUI();
|
||||
this._initializedUI = true;
|
||||
}
|
||||
|
||||
if (this._error) {
|
||||
this._elmts.messageDiv.text(this._errorMessage).show();
|
||||
this._elmts.sliderWidgetDiv.hide();
|
||||
this._elmts.histogramDiv.hide();
|
||||
this._elmts.statusDiv.hide();
|
||||
this._elmts.otherChoicesDiv.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this._elmts.messageDiv.hide();
|
||||
this._elmts.sliderWidgetDiv.show();
|
||||
this._elmts.histogramDiv.show();
|
||||
this._elmts.statusDiv.show();
|
||||
this._elmts.otherChoicesDiv.show();
|
||||
|
||||
this._sliderWidget.update(
|
||||
this._config.min,
|
||||
this._config.max,
|
||||
this._config.step,
|
||||
this._from,
|
||||
this._to
|
||||
);
|
||||
this._histogram.update(
|
||||
this._config.min,
|
||||
this._config.max,
|
||||
this._config.step,
|
||||
[ this._baseBins, this._bins ]
|
||||
);
|
||||
|
||||
this._setRangeIndicators();
|
||||
this._renderOtherChoices();
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype._remove = function() {
|
||||
ui.browsingEngine.removeFacet(this);
|
||||
|
||||
this._div = null;
|
||||
this._config = null;
|
||||
this._data = null;
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype._updateRest = function() {
|
||||
Gridworks.update({ engineChanged: true });
|
||||
};
|
||||
|
||||
TimeRangeFacet.prototype._editExpression = function() {
|
||||
var self = this;
|
||||
var title = (this._config.columnName) ?
|
||||
("Edit Facet's Expression based on Column " + this._config.columnName) :
|
||||
"Edit Facet's Expression";
|
||||
|
||||
var column = Gridworks.columnNameToColumn(this._config.columnName);
|
||||
var o = DataTableView.sampleVisibleRows(column);
|
||||
|
||||
new ExpressionPreviewDialog(
|
||||
title,
|
||||
column ? column.cellIndex : -1,
|
||||
o.rowIndices,
|
||||
o.values,
|
||||
this._config.expression,
|
||||
function(expr) {
|
||||
if (expr != self._config.expression) {
|
||||
self._config.expression = expr;
|
||||
self._elmts.expressionDiv.text(self._config.expression);
|
||||
|
||||
self.reset();
|
||||
self._from = null;
|
||||
self._to = null;
|
||||
self._updateRest();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
@ -15,6 +15,9 @@ function BrowsingEngine(div, facetConfigs) {
|
||||
case "range":
|
||||
facet = RangeFacet.reconstruct(elmt, facetConfig);
|
||||
break;
|
||||
case "timerange":
|
||||
facet = TimeRangeFacet.reconstruct(elmt, facetConfig);
|
||||
break;
|
||||
case "scatterplot":
|
||||
facet = ScatterplotFacet.reconstruct(elmt, facetConfig);
|
||||
break;
|
||||
@ -152,6 +155,9 @@ BrowsingEngine.prototype.addFacet = function(type, config, options) {
|
||||
case "range":
|
||||
facet = new RangeFacet(elmt, config, options);
|
||||
break;
|
||||
case "timerange":
|
||||
facet = new TimeRangeFacet(elmt, config, options);
|
||||
break;
|
||||
case "scatterplot":
|
||||
facet = new ScatterplotFacet(elmt, config, options);
|
||||
break;
|
||||
|
@ -49,6 +49,21 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "core/time-facet",
|
||||
label: "Timeline Facet",
|
||||
click: function() {
|
||||
ui.browsingEngine.addFacet(
|
||||
"timerange",
|
||||
{
|
||||
"name": column.name,
|
||||
"columnName": column.name,
|
||||
"expression": "value",
|
||||
"mode": "range"
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "core/scatterplot-facet",
|
||||
label: "Scatterplot Facet",
|
||||
|
Loading…
Reference in New Issue
Block a user