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-grid-label'; const ITEM_CSS_CLASS = 'dc-grid-item'; const SECTION_CSS_CLASS = 'dc-grid-section dc-grid-group'; const GRID_CSS_CLASS = 'dc-grid-top'; /** * Data grid is a simple widget designed to list the filtered records, providing * a simple way to define how the items are displayed. * * Note: Formerly the data grid chart (and data table) used the {@link DataGrid#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 https://dc-js.github.io/dc.js/ep/ List of members of the european parliament} * @mixes BaseMixin */ export class DataGrid extends BaseMixin { /** * Create a Data Grid. * @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._section = null; this._size = 999; // shouldn't be needed, but you might this._html = function (d) { return `you need to provide an html() handling param: ${JSON.stringify(d)}`; }; this._sortBy = function (d) { return d; }; this._order = ascending; this._beginSlice = 0; this._endSlice = undefined; this._htmlSection = d => `<div class='${SECTION_CSS_CLASS}'><h1 class='${LABEL_CSS_CLASS}'>${ this.keyAccessor()(d)}</h1></div>`; this._mandatoryAttributes(['dimension', 'section']); this.anchor(parent, chartGroup); } _doRender () { this.selectAll(`div.${GRID_CSS_CLASS}`).remove(); this._renderItems(this._renderSections()); return this; } _renderSections () { const sections = this.root().selectAll(`div.${GRID_CSS_CLASS}`) .data(this._nestEntries(), d => this.keyAccessor()(d)); const itemSection = sections .enter() .append('div') .attr('class', GRID_CSS_CLASS); if (this._htmlSection) { itemSection .html(d => this._htmlSection(d)); } sections.exit().remove(); return itemSection; } _nestEntries () { let 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 }); } _renderItems (sections) { let items = sections.order() .selectAll(`div.${ITEM_CSS_CLASS}`) .data(d => d.values); items.exit().remove(); items = items .enter() .append('div') .attr('class', ITEM_CSS_CLASS) .html(d => this._html(d)) .merge(items); return items; } _doRedraw () { return this._doRender(); } /** * Get or set the section function for the data grid. 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. * * Do not pass in a crossfilter section as this will not work. * @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|DataGrid} */ section (section) { if (!arguments.length) { return this._section; } this._section = section; return this; } /** * Backward-compatible synonym for {@link DataGrid#section section}. * * @param {Function} section Function taking a row of data and returning the nest key. * @returns {Function|DataGrid} */ group (section) { logger.warnOnce('consider using dataGrid.section instead of dataGrid.group for clarity'); if (!arguments.length) { return this.section(); } return this.section(section); } /** * Get or set the index of the beginning slice which determines which entries get displayed by the widget. * Useful when implementing pagination. * @param {Number} [beginSlice=0] * @returns {Number|DataGrid} */ 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. * @param {Number} [endSlice] * @returns {Number|DataGrid} */ endSlice (endSlice) { if (!arguments.length) { return this._endSlice; } this._endSlice = endSlice; return this; } /** * Get or set the grid size which determines the number of items displayed by the widget. * @param {Number} [size=999] * @returns {Number|DataGrid} */ size (size) { if (!arguments.length) { return this._size; } this._size = size; return this; } /** * Get or set the function that formats an item. The data grid widget uses a * function to generate dynamic html. Use your favourite templating engine or * generate the string directly. * @example * chart.html(function (d) { return '<div class='item '+data.exampleCategory+''>'+data.exampleString+'</div>';}); * @param {Function} [html] * @returns {Function|DataGrid} */ html (html) { if (!arguments.length) { return this._html; } this._html = html; return this; } /** * Get or set the function that formats a section label. * @example * chart.htmlSection (function (d) { return '<h2>'.d.key . 'with ' . d.values.length .' items</h2>'}); * @param {Function} [htmlSection] * @returns {Function|DataGrid} */ htmlSection (htmlSection) { if (!arguments.length) { return this._htmlSection; } this._htmlSection = htmlSection; return this; } /** * Backward-compatible synonym for {@link DataGrid#htmlSection htmlSection}. * @param {Function} [htmlSection] * @returns {Function|DataGrid} */ htmlGroup (htmlSection) { logger.warnOnce('consider using dataGrid.htmlSection instead of dataGrid.htmlGroup for clarity'); if (!arguments.length) { return this.htmlSection(); } return this.htmlSection(htmlSection); } /** * Get or set sort-by function. This function works as a value accessor at the item * level and returns a particular field to be sorted. * @example * chart.sortBy(function(d) { * return d.date; * }); * @param {Function} [sortByFunction] * @returns {Function|DataGrid} */ sortBy (sortByFunction) { if (!arguments.length) { return this._sortBy; } this._sortBy = sortByFunction; return this; } /** * Get or set sort the order function. * @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|DataGrid} */ order (order) { if (!arguments.length) { return this._order; } this._order = order; return this; } } export const dataGrid = (parent, chartGroup) => new DataGrid(parent, chartGroup);