dc
Version:
A multi-dimensional charting library built to work natively with crossfilter and rendered using d3.js
429 lines (390 loc) • 15.3 kB
JavaScript
/**
* The data table is a simple widget designed to list crossfilter focused data set (rows being
* filtered) in a good old tabular fashion.
*
* Note: Unlike other charts, the data table (and data grid chart) use the {@link dc.dataTable#group group} attribute as a
* keying function for {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#nest nesting} the data
* together in groups. Do not pass in a crossfilter group as this will not work.
*
* Another interesting feature of the data table is that you can pass a crossfilter group to the `dimension`, as
* long as you specify the {@link dc.dataTable#order order} as `d3.descending`, since the data
* table will use `dimension.top()` to fetch the data in that case, and the method is equally
* supported on the crossfilter group as the crossfilter dimension.
*
* Examples:
* - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
* - {@link http://dc-js.github.io/dc.js/examples/table-on-aggregated-data.html dataTable on a crossfilter group}
* ({@link https://github.com/dc-js/dc.js/blob/develop/web/examples/table-on-aggregated-data.html source})
* @class dataTable
* @memberof dc
* @mixes dc.baseMixin
* @param {String|node|d3.selection} parent - Any valid
* {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying
* a dom block element such as a div; or a dom element or d3 selection.
* @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
* Interaction with a chart will only trigger events and redraws within the chart's group.
* @returns {dc.dataTable}
*/
dc.dataTable = function (parent, chartGroup) {
var LABEL_CSS_CLASS = 'dc-table-label';
var ROW_CSS_CLASS = 'dc-table-row';
var COLUMN_CSS_CLASS = 'dc-table-column';
var GROUP_CSS_CLASS = 'dc-table-group';
var HEAD_CSS_CLASS = 'dc-table-head';
var _chart = dc.baseMixin({});
var _size = 25;
var _columns = [];
var _sortBy = function (d) {
return d;
};
var _order = d3.ascending;
var _beginSlice = 0;
var _endSlice;
var _showGroups = true;
_chart._doRender = function () {
_chart.selectAll('tbody').remove();
renderRows(renderGroups());
return _chart;
};
_chart._doColumnValueFormat = function (v, d) {
return ((typeof v === 'function') ?
v(d) : // v as function
((typeof v === 'string') ?
d[v] : // v is field name string
v.format(d) // v is Object, use fn (element 2)
)
);
};
_chart._doColumnHeaderFormat = function (d) {
// if 'function', convert to string representation
// show a string capitalized
// if an object then display its label string as-is.
return (typeof d === 'function') ?
_chart._doColumnHeaderFnToString(d) :
((typeof d === 'string') ?
_chart._doColumnHeaderCapitalize(d) : String(d.label));
};
_chart._doColumnHeaderCapitalize = function (s) {
// capitalize
return s.charAt(0).toUpperCase() + s.slice(1);
};
_chart._doColumnHeaderFnToString = function (f) {
// columnString(f) {
var s = String(f);
var i1 = s.indexOf('return ');
if (i1 >= 0) {
var i2 = s.lastIndexOf(';');
if (i2 >= 0) {
s = s.substring(i1 + 7, i2);
var i3 = s.indexOf('numberFormat');
if (i3 >= 0) {
s = s.replace('numberFormat', '');
}
}
}
return s;
};
function renderGroups () {
// The 'original' example uses all 'functions'.
// If all 'functions' are used, then don't remove/add a header, and leave
// the html alone. This preserves the functionality of earlier releases.
// A 2nd option is a string representing a field in the data.
// A third option is to supply an Object such as an array of 'information', and
// supply your own _doColumnHeaderFormat and _doColumnValueFormat functions to
// create what you need.
var bAllFunctions = true;
_columns.forEach(function (f) {
bAllFunctions = bAllFunctions & (typeof f === 'function');
});
if (!bAllFunctions) {
// ensure one thead
var thead = _chart.selectAll('thead').data([0]);
thead.enter().append('thead');
thead.exit().remove();
// with one tr
var headrow = thead.selectAll('tr').data([0]);
headrow.enter().append('tr');
headrow.exit().remove();
// with a th for each column
var headcols = headrow.selectAll('th')
.data(_columns);
headcols.enter().append('th');
headcols.exit().remove();
headcols
.attr('class', HEAD_CSS_CLASS)
.html(function (d) {
return (_chart._doColumnHeaderFormat(d));
});
}
var groups = _chart.root().selectAll('tbody')
.data(nestEntries(), function (d) {
return _chart.keyAccessor()(d);
});
var rowGroup = groups
.enter()
.append('tbody');
if (_showGroups === true) {
rowGroup
.append('tr')
.attr('class', GROUP_CSS_CLASS)
.append('td')
.attr('class', LABEL_CSS_CLASS)
.attr('colspan', _columns.length)
.html(function (d) {
return _chart.keyAccessor()(d);
});
}
groups.exit().remove();
return rowGroup;
}
function nestEntries () {
var entries;
if (_order === d3.ascending) {
entries = _chart.dimension().bottom(_size);
} else {
entries = _chart.dimension().top(_size);
}
return d3.nest()
.key(_chart.group())
.sortKeys(_order)
.entries(entries.sort(function (a, b) {
return _order(_sortBy(a), _sortBy(b));
}).slice(_beginSlice, _endSlice));
}
function renderRows (groups) {
var rows = groups.order()
.selectAll('tr.' + ROW_CSS_CLASS)
.data(function (d) {
return d.values;
});
var rowEnter = rows.enter()
.append('tr')
.attr('class', ROW_CSS_CLASS);
_columns.forEach(function (v, i) {
rowEnter.append('td')
.attr('class', COLUMN_CSS_CLASS + ' _' + i)
.html(function (d) {
return _chart._doColumnValueFormat(v, d);
});
});
rows.exit().remove();
return rows;
}
_chart._doRedraw = function () {
return _chart._doRender();
};
/**
* Get or set the group function for the data table. The group function takes a data row and
* returns the key to specify to {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_nest d3.nest}
* to split rows into groups.
*
* Do not pass in a crossfilter group as this will not work.
* @method group
* @memberof dc.dataTable
* @instance
* @example
* // group rows by the value of their field
* chart
* .group(function(d) { return d.field; })
* @param {Function} groupFunction Function taking a row of data and returning the nest key.
* @returns {Function|dc.dataTable}
*/
/**
* Get or set the table size which determines the number of rows displayed by the widget.
* @method size
* @memberof dc.dataTable
* @instance
* @param {Number} [size=25]
* @returns {Number|dc.dataTable}
*/
_chart.size = function (size) {
if (!arguments.length) {
return _size;
}
_size = size;
return _chart;
};
/**
* Get or set the index of the beginning slice which determines which entries get displayed
* by the widget. Useful when implementing pagination.
*
* Note: the sortBy function will determine how the rows are ordered for pagination purposes.
* See the {@link http://dc-js.github.io/dc.js/examples/table-pagination.html table pagination example}
* to see how to implement the pagination user interface using `beginSlice` and `endSlice`.
* @method beginSlice
* @memberof dc.dataTable
* @instance
* @param {Number} [beginSlice=0]
* @returns {Number|dc.dataTable}
*/
_chart.beginSlice = function (beginSlice) {
if (!arguments.length) {
return _beginSlice;
}
_beginSlice = beginSlice;
return _chart;
};
/**
* Get or set the index of the end slice which determines which entries get displayed by the
* widget. Useful when implementing pagination. See {@link dc.dataTable#beginSlice `beginSlice`} for more information.
* @method endSlice
* @memberof dc.dataTable
* @instance
* @param {Number|undefined} [endSlice=undefined]
* @returns {Number|dc.dataTable}
*/
_chart.endSlice = function (endSlice) {
if (!arguments.length) {
return _endSlice;
}
_endSlice = endSlice;
return _chart;
};
/**
* Get or set column functions. The data table widget supports several methods of specifying the
* columns to display.
*
* The original method uses an array of functions to generate dynamic columns. Column functions
* are simple javascript functions with only one input argument `d` which represents a row in
* the data set. The return value of these functions will be used to generate the content for
* each cell. However, this method requires the HTML for the table to have a fixed set of column
* headers.
*
* <pre><code>chart.columns([
* function(d) { return d.date; },
* function(d) { return d.open; },
* function(d) { return d.close; },
* function(d) { return numberFormat(d.close - d.open); },
* function(d) { return d.volume; }
* ]);
* </code></pre>
*
* In the second method, you can list the columns to read from the data without specifying it as
* a function, except where necessary (ie, computed columns). Note the data element name is
* capitalized when displayed in the table header. You can also mix in functions as necessary,
* using the third `{label, format}` form, as shown below.
*
* <pre><code>chart.columns([
* "date", // d["date"], ie, a field accessor; capitalized automatically
* "open", // ...
* "close", // ...
* {
* label: "Change",
* format: function (d) {
* return numberFormat(d.close - d.open);
* }
* },
* "volume" // d["volume"], ie, a field accessor; capitalized automatically
* ]);
* </code></pre>
*
* In the third example, we specify all fields using the `{label, format}` method:
* <pre><code>chart.columns([
* {
* label: "Date",
* format: function (d) { return d.date; }
* },
* {
* label: "Open",
* format: function (d) { return numberFormat(d.open); }
* },
* {
* label: "Close",
* format: function (d) { return numberFormat(d.close); }
* },
* {
* label: "Change",
* format: function (d) { return numberFormat(d.close - d.open); }
* },
* {
* label: "Volume",
* format: function (d) { return d.volume; }
* }
* ]);
* </code></pre>
*
* You may wish to override the dataTable functions `_doColumnHeaderCapitalize` and
* `_doColumnHeaderFnToString`, which are used internally to translate the column information or
* function into a displayed header. The first one is used on the "string" column specifier; the
* second is used to transform a stringified function into something displayable. For the Stock
* example, the function for Change becomes the table header **d.close - d.open**.
*
* Finally, you can even specify a completely different form of column definition. To do this,
* override `_chart._doColumnHeaderFormat` and `_chart._doColumnValueFormat` Be aware that
* fields without numberFormat specification will be displayed just as they are stored in the
* data, unformatted.
* @method columns
* @memberof dc.dataTable
* @instance
* @param {Array<Function>} [columns=[]]
* @returns {Array<Function>}|dc.dataTable}
*/
_chart.columns = function (columns) {
if (!arguments.length) {
return _columns;
}
_columns = columns;
return _chart;
};
/**
* Get or set sort-by function. This function works as a value accessor at row level and returns a
* particular field to be sorted by.
* @method sortBy
* @memberof dc.dataTable
* @instance
* @example
* chart.sortBy(function(d) {
* return d.date;
* });
* @param {Function} [sortBy=identity function]
* @returns {Function|dc.dataTable}
*/
_chart.sortBy = function (sortBy) {
if (!arguments.length) {
return _sortBy;
}
_sortBy = sortBy;
return _chart;
};
/**
* Get or set sort order. If the order is `d3.ascending`, the data table will use
* `dimension().bottom()` to fetch the data; otherwise it will use `dimension().top()`
* @method order
* @memberof dc.dataTable
* @instance
* @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_ascending d3.ascending}
* @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_descending d3.descending}
* @example
* chart.order(d3.descending);
* @param {Function} [order=d3.ascending]
* @returns {Function|dc.dataTable}
*/
_chart.order = function (order) {
if (!arguments.length) {
return _order;
}
_order = order;
return _chart;
};
/**
* Get or set if group rows will be shown. The dataTable {@link dc.dataTable#group group}
* function must be specified even if groups are not shown.
* @method showGroups
* @memberof dc.dataTable
* @instance
* @example
* chart
* .group([value], [name])
* .showGroups(true|false);
* @param {Boolean} [showGroups=true]
* @returns {Boolean|dc.dataTable}
*/
_chart.showGroups = function (showGroups) {
if (!arguments.length) {
return _showGroups;
}
_showGroups = showGroups;
return _chart;
};
return _chart.anchor(parent, chartGroup);
};