Source: charts/data-grid.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
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);