import {ascending} from 'd3-array';
import {logger} from '../core/logger';
import {BaseMixin} from '../base/base-mixin';
import {d3compat} from '../core/config';
const LABEL_CSS_CLASS = 'dc-table-label';
const ROW_CSS_CLASS = 'dc-table-row';
const COLUMN_CSS_CLASS = 'dc-table-column';
const SECTION_CSS_CLASS = 'dc-table-section dc-table-group';
const HEAD_CSS_CLASS = 'dc-table-head';
/**
* The data table is a simple widget designed to list crossfilter focused data set (rows being
* filtered) in a good old tabular fashion.
*
* An interesting feature of the data table is that you can pass a crossfilter group to the
* `dimension`, if you want to show aggregated data instead of raw data rows. This requires no
* special code as long as you specify the {@link 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.
*
* If you want to display aggregated data in ascending order, you will need to wrap the group
* in a [fake dimension](https://github.com/dc-js/dc.js/wiki/FAQ#fake-dimensions) to support the
* `.bottom()` method. See the example linked below for more details.
*
* Note: Formerly the data table (and data grid chart) used the {@link DataTable#group group} attribute as a
* keying function for {@link https://github.com/d3/d3-collection/blob/master/README.md#nest nesting} the data
* together in sections. This was confusing so it has been renamed to `section`, although `group` still works.
* 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/master/web-src/examples/table-on-aggregated-data.html source})
*
* @mixes BaseMixin
*/
export class DataTable extends BaseMixin {
/**
* Create a Data Table.
*
* @param {String|node|d3.selection} parent - Any valid
* {@link https://github.com/d3/d3-selection/blob/master/README.md#select 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.
*/
constructor (parent, chartGroup) {
super();
this._size = 25;
this._columns = [];
this._sortBy = d => d;
this._order = ascending;
this._beginSlice = 0;
this._endSlice = undefined;
this._showSections = true;
this._section = () => ''; // all in one section
this._mandatoryAttributes(['dimension']);
this.anchor(parent, chartGroup);
}
_doRender () {
this.selectAll('tbody').remove();
this._renderRows(this._renderSections());
return this;
}
_doColumnValueFormat (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)
}
_doColumnHeaderFormat (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') ? this._doColumnHeaderFnToString(d) :
(typeof d === 'string') ? this._doColumnHeaderCapitalize(d) :
String(d.label);
}
_doColumnHeaderCapitalize (s) {
// capitalize
return s.charAt(0).toUpperCase() + s.slice(1);
}
_doColumnHeaderFnToString (f) {
// columnString(f) {
let s = String(f);
const i1 = s.indexOf('return ');
if (i1 >= 0) {
const i2 = s.lastIndexOf(';');
if (i2 >= 0) {
s = s.substring(i1 + 7, i2);
const i3 = s.indexOf('numberFormat');
if (i3 >= 0) {
s = s.replace('numberFormat', '');
}
}
}
return s;
}
_renderSections () {
// 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.
let bAllFunctions = true;
this._columns.forEach(f => {
bAllFunctions = bAllFunctions & (typeof f === 'function');
});
if (!bAllFunctions) {
// ensure one thead
let thead = this.selectAll('thead').data([0]);
thead.exit().remove();
thead = thead.enter()
.append('thead')
.merge(thead);
// with one tr
let headrow = thead.selectAll('tr').data([0]);
headrow.exit().remove();
headrow = headrow.enter()
.append('tr')
.merge(headrow);
// with a th for each column
const headcols = headrow.selectAll('th')
.data(this._columns);
headcols.exit().remove();
headcols.enter().append('th')
.merge(headcols)
.attr('class', HEAD_CSS_CLASS)
.html(d => (this._doColumnHeaderFormat(d)));
}
const sections = this.root().selectAll('tbody')
.data(this._nestEntries(), d => this.keyAccessor()(d));
const rowSection = sections
.enter()
.append('tbody');
if (this._showSections === true) {
rowSection
.append('tr')
.attr('class', SECTION_CSS_CLASS)
.append('td')
.attr('class', LABEL_CSS_CLASS)
.attr('colspan', this._columns.length)
.html(d => this.keyAccessor()(d));
}
sections.exit().remove();
return rowSection;
}
_nestEntries () {
let entries;
if (this._order === ascending) {
entries = this.dimension().bottom(this._size);
} else {
entries = this.dimension().top(this._size);
}
entries = entries.sort((a, b) => this._order(this._sortBy(a), this._sortBy(b))).slice(this._beginSlice, this._endSlice)
return d3compat.nester({
key: this.section(),
sortKeys: this._order,
entries
});
}
_renderRows (sections) {
const rows = sections.order()
.selectAll(`tr.${ROW_CSS_CLASS}`)
.data(d => d.values);
const rowEnter = rows.enter()
.append('tr')
.attr('class', ROW_CSS_CLASS);
this._columns.forEach((v, i) => {
rowEnter.append('td')
.attr('class', `${COLUMN_CSS_CLASS} _${i}`)
.html(d => this._doColumnValueFormat(v, d));
});
rows.exit().remove();
return rows;
}
_doRedraw () {
return this._doRender();
}
/**
* Get or set the section function for the data table. The section function takes a data row and
* returns the key to specify to {@link https://github.com/d3/d3-collection/blob/master/README.md#nest d3.nest}
* to split rows into sections. By default there will be only one section with no name.
*
* Set {@link DataTable#showSections showSections} to false to hide the section headers
*
* @example
* // section rows by the value of their field
* chart
* .section(function(d) { return d.field; })
* @param {Function} section Function taking a row of data and returning the nest key.
* @returns {Function|DataTable}
*/
section (section) {
if (!arguments.length) {
return this._section;
}
this._section = section;
return this;
}
/**
* Backward-compatible synonym for {@link DataTable#section section}.
*
* @param {Function} section Function taking a row of data and returning the nest key.
* @returns {Function|DataTable}
*/
group (section) {
logger.warnOnce('consider using dataTable.section instead of dataTable.group for clarity');
if (!arguments.length) {
return this.section();
}
return this.section(section);
}
/**
* Get or set the table size which determines the number of rows displayed by the widget.
* @param {Number} [size=25]
* @returns {Number|DataTable}
*/
size (size) {
if (!arguments.length) {
return this._size;
}
this._size = size;
return this;
}
/**
* 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`.
* @param {Number} [beginSlice=0]
* @returns {Number|DataTable}
*/
beginSlice (beginSlice) {
if (!arguments.length) {
return this._beginSlice;
}
this._beginSlice = beginSlice;
return this;
}
/**
* Get or set the index of the end slice which determines which entries get displayed by the
* widget. Useful when implementing pagination. See {@link DataTable#beginSlice `beginSlice`} for more information.
* @param {Number|undefined} [endSlice=undefined]
* @returns {Number|DataTable}
*/
endSlice (endSlice) {
if (!arguments.length) {
return this._endSlice;
}
this._endSlice = endSlice;
return this;
}
/**
* 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.
* @param {Array<Function>} [columns=[]]
* @returns {Array<Function>}|DataTable}
*/
columns (columns) {
if (!arguments.length) {
return this._columns;
}
this._columns = columns;
return this;
}
/**
* 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.
* @example
* chart.sortBy(function(d) {
* return d.date;
* });
* @param {Function} [sortBy=identity function]
* @returns {Function|DataTable}
*/
sortBy (sortBy) {
if (!arguments.length) {
return this._sortBy;
}
this._sortBy = sortBy;
return this;
}
/**
* 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()`
* @see {@link https://github.com/d3/d3-array/blob/master/README.md#ascending d3.ascending}
* @see {@link https://github.com/d3/d3-array/blob/master/README.md#descending d3.descending}
* @example
* chart.order(d3.descending);
* @param {Function} [order=d3.ascending]
* @returns {Function|DataTable}
*/
order (order) {
if (!arguments.length) {
return this._order;
}
this._order = order;
return this;
}
/**
* Get or set if section header rows will be shown.
* @example
* chart
* .section([value], [name])
* .showSections(true|false);
* @param {Boolean} [showSections=true]
* @returns {Boolean|DataTable}
*/
showSections (showSections) {
if (!arguments.length) {
return this._showSections;
}
this._showSections = showSections;
return this;
}
/**
* Backward-compatible synonym for {@link DataTable#showSections showSections}.
* @param {Boolean} [showSections=true]
* @returns {Boolean|DataTable}
*/
showGroups (showSections) {
logger.warnOnce('consider using dataTable.showSections instead of dataTable.showGroups for clarity');
if (!arguments.length) {
return this.showSections();
}
return this.showSections(showSections);
}
}
export const dataTable = (parent, chartGroup) => new DataTable(parent, chartGroup);