PageSourceSearch

https://trainfo.ca/wp-content/plugins/relish-freemium/js/vendor/dc.js?ver=6.5.12

js trainfo.ca collected 2026-10-03 21:01:26 UTC 487,373 bytes, 13,517 lines download raw bytes

1/*!
2 *  dc 4.2.7
3 *  http://dc-js.github.io/dc.js/
4 *  Copyright 2012-2021 Nick Zhu & the dc.js Developers
5 *  https://github.com/dc-js/dc.js/blob/master/AUTHORS
6 *
7 *  Licensed under the Apache License, Version 2.0 (the "License");
8 *  you may not use this file except in compliance with the License.
9 *  You may obtain a copy of the License at
10 *
11 *      http://www.apache.org/licenses/LICENSE-2.0
12 *
13 *  Unless required by applicable law or agreed to in writing, software
14 *  distributed under the License is distributed on an "AS IS" BASIS,
15 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 *  See the License for the specific language governing permissions and
17 *  limitations under the License.
18 */
19
20(function (global, factory) {
21  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3')) :
22  typeof define === 'function' && define.amd ? define(['exports', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3'], factory) :
23  (global = global || self, factory(global.dc = {}, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3));
24}(this, (function (exports, d3TimeFormat, d3Time, d3Format, d3Selection, d3Dispatch, d3Array, d3Scale, d3Interpolate, d3ScaleChromatic, d3Axis, d3Zoom, d3Brush, d3Timer, d3Shape, d3Geo, d3Ease, d3Hierarchy, d3, d3Collection) { 'use strict';
25
26  const version = "4.2.7";
27
28  class BadArgumentException extends Error { }
29
30  const constants = {
31      CHART_CLASS: 'dc-chart',
32      DEBUG_GROUP_CLASS: 'debug',
33      STACK_CLASS: 'stack',
34      DESELECTED_CLASS: 'deselected',
35      SELECTED_CLASS: 'selected',
36      NODE_INDEX_NAME: '__index__',
37      GROUP_INDEX_NAME: '__group_index__',
38      DEFAULT_CHART_GROUP: '__default_chart_group__',
39      EVENT_DELAY: 40,
40      NEGLIGIBLE_NUMBER: 1e-10
41  };
42
43  /**
44   * Provides basis logging and deprecation utilities
45   */
46  class Logger {
47
48      constructor () {
49          /**
50           * Enable debug level logging. Set to `false` by default.
51           * @name enableDebugLog
52           * @memberof Logger
53           * @instance
54           */
55          this.enableDebugLog = false;
56
57          this._alreadyWarned = {};
58      }
59
60      /**
61       * Put a warning message to console
62       * @example
63       * logger.warn('Invalid use of .tension on CurveLinear');
64       * @param {String} [msg]
65       * @returns {Logger}
66       */
67      warn (msg) {
68          if (console) {
69              if (console.warn) {
70                  console.warn(msg);
71              } else if (console.log) {
72                  console.log(msg);
73              }
74          }
75
76          return this;
77      }
78
79      /**
80       * Put a warning message to console. It will warn only on unique messages.
81       * @example
82       * logger.warnOnce('Invalid use of .tension on CurveLinear');
83       * @param {String} [msg]
84       * @returns {Logger}
85       */
86      warnOnce (msg) {
87          if (!this._alreadyWarned[msg]) {
88              this._alreadyWarned[msg] = true;
89
90              logger.warn(msg);
91          }
92
93          return this;
94      }
95
96      /**
97       * Put a debug message to console. It is controlled by `logger.enableDebugLog`
98       * @example
99       * logger.debug('Total number of slices: ' + numSlices);
100       * @param {String} [msg]
101       * @returns {Logger}
102       */
103      debug (msg) {
104          if (this.enableDebugLog && console) {
105              if (console.debug) {
106                  console.debug(msg);
107              } else if (console.log) {
108                  console.log(msg);
109              }
110          }
111
112          return this;
113      }
114  }
115
116  const logger = new Logger();
117
118  /**
119   * General configuration
120   */
121  class Config {
122      constructor () {
123          this._defaultColors = Config._schemeCategory20c;
124
125          /**
126           * The default date format for dc.js
127           * @type {Function}
128           * @default d3.timeFormat('%m/%d/%Y')
129           */
130          this.dateFormat = d3TimeFormat.timeFormat('%m/%d/%Y');
131
132          this._renderlet = null;
133
134          /**
135           * If this boolean is set truthy, all transitions will be disabled, and changes to the charts will happen
136           * immediately.
137           * @type {Boolean}
138           * @default false
139           */
140          this.disableTransitions = false;
141      }
142
143      /**
144       * Set the default color scheme for ordinal charts. Changing it will impact all ordinal charts.
145       *
146       * By default it is set to a copy of
147       * `d3.schemeCategory20c` for backward compatibility. This color scheme has been
148       * [removed from D3v5](https://github.com/d3/d3/blob/master/CHANGES.md#changes-in-d3-50).
149       * In DC 3.1 release it will change to a more appropriate default.
150       *
151       * @example
152       * config.defaultColors(d3.schemeSet1)
153       * @param {Array} [colors]
154       * @returns {Array|config}
155       */
156      defaultColors (colors) {
157          if (!arguments.length) {
158              // Issue warning if it uses _schemeCategory20c
159              if (this._defaultColors === Config._schemeCategory20c) {
160                  logger.warnOnce('You are using d3.schemeCategory20c, which has been removed in D3v5. ' +
161                      'See the explanation at https://github.com/d3/d3/blob/master/CHANGES.md#changes-in-d3-50. ' +
162                      'DC is using it for backward compatibility, however it will be changed in DCv3.1. ' +
163                      'You can change it by calling dc.config.defaultColors(newScheme). ' +
164                      'See https://github.com/d3/d3-scale-chromatic for some alternatives.');
165              }
166              return this._defaultColors;
167          }
168          this._defaultColors = colors;
169          return this;
170      }
171
172  }
173
174  // D3v5 has removed schemeCategory20c, copied here for backward compatibility
175  Config._schemeCategory20c = [
176      '#3182bd', '#6baed6', '#9ecae1', '#c6dbef', '#e6550d',
177      '#fd8d3c', '#fdae6b', '#fdd0a2', '#31a354', '#74c476',
178      '#a1d99b', '#c7e9c0', '#756bb1', '#9e9ac8', '#bcbddc',
179      '#dadaeb', '#636363', '#969696', '#bdbdbd', '#d9d9d9'];
180
181  /**
182   * General configuration object; see {@link Config} for members.
183   */
184  const config = new Config();
185
186  /**
187   * d3.js compatiblity layer
188   */
189  const d3compat = {
190      eventHandler: handler => function eventHandler (a, b) {
191          console.warn('No d3.js compatbility event handler registered, defaulting to v6 behavior.');
192          handler.call(this, b, a);
193      },
194      nester: ({key, sortKeys, sortValues, entries}) => {
195          throw new Error('No d3.js compatbility nester registered, load v5 or v6 compability layer.');
196      },
197      pointer: () => { throw new Error('No d3.js compatbility pointer registered, load v5 or v6 compability layer.'); }
198  };
199
200  /**
201   * The ChartRegistry maintains sets of all instantiated dc.js charts under named groups
202   * and the default group. There is a single global ChartRegistry object named `chartRegistry`
203   *
204   * A chart group often corresponds to a crossfilter instance. It specifies
205   * the set of charts which should be updated when a filter changes on one of the charts or when the
206   * global functions {@link filterAll filterAll}, {@link refocusAll refocusAll},
207   * {@link renderAll renderAll}, {@link redrawAll redrawAll}, or chart functions
208   * {@link baseMixin#renderGroup baseMixin.renderGroup},
209   * {@link baseMixin#redrawGroup baseMixin.redrawGroup} are called.
210   */
211  class ChartRegistry {
212      constructor () {
213          // chartGroup:string => charts:array
214          this._chartMap = {};
215      }
216
217      _initializeChartGroup (group) {
218          if (!group) {
219              group = constants.DEFAULT_CHART_GROUP;
220          }
221
222          if (!(this._chartMap)[group]) {
223              (this._chartMap)[group] = [];
224          }
225
226          return group;
227      }
228
229      /**
230       * Determine if a given chart instance resides in any group in the registry.
231       * @param {Object} chart dc.js chart instance
232       * @returns {Boolean}
233       */
234      has (chart) {
235          for (const e in this._chartMap) {
236              if ((this._chartMap)[e].indexOf(chart) >= 0) {
237                  return true;
238              }
239          }
240          return false;
241      }
242
243      /**
244       * Add given chart instance to the given group, creating the group if necessary.
245       * If no group is provided, the default group `constants.DEFAULT_CHART_GROUP` will be used.
246       * @param {Object} chart dc.js chart instance
247       * @param {String} [group] Group name
248       * @return {undefined}
249       */
250      register (chart, group) {
251          const _chartMap = this._chartMap;
252          group = this._initializeChartGroup(group);
253          _chartMap[group].push(chart);
254      }
255
256      /**
257       * Remove given chart instance from the given group, creating the group if necessary.
258       * If no group is provided, the default group `constants.DEFAULT_CHART_GROUP` will be used.
259       * @param {Object} chart dc.js chart instance
260       * @param {String} [group] Group name
261       * @return {undefined}
262       */
263      deregister (chart, group) {
264          group = this._initializeChartGroup(group);
265          for (let i = 0; i < (this._chartMap)[group].length; i++) {
266              if ((this._chartMap)[group][i].anchorName() === chart.anchorName()) {
267                  (this._chartMap)[group].splice(i, 1);
268                  break;
269              }
270          }
271      }
272
273      /**
274       * Clear given group if one is provided, otherwise clears all groups.
275       * @param {String} group Group name
276       * @return {undefined}
277       */
278      clear (group) {
279          if (group) {
280              delete (this._chartMap)[group];
281          } else {
282              this._chartMap = {};
283          }
284      }
285
286      /**
287       * Get an array of each chart instance in the given group.
288       * If no group is provided, the charts in the default group are returned.
289       * @param {String} [group] Group name
290       * @returns {Array<Object>}
291       */
292      list (group) {
293          group = this._initializeChartGroup(group);
294          return (this._chartMap)[group];
295      }
296  }
297
298  /**
299   * The chartRegistry object maintains sets of all instantiated dc.js charts under named groups
300   * and the default group. See {@link ChartRegistry ChartRegistry} for its methods.
301   */
302  const chartRegistry = new ChartRegistry();
303
304  /**
305   * Add given chart instance to the given group, creating the group if necessary.
306   * If no group is provided, the default group `constants.DEFAULT_CHART_GROUP` will be used.
307   * @function registerChart
308   * @param {Object} chart dc.js chart instance
309   * @param {String} [group] Group name
310   * @return {undefined}
311   */
312  const registerChart = function (chart, group) {
313      chartRegistry.register(chart, group);
314  };
315
316  /**
317   * Remove given chart instance from the given group, creating the group if necessary.
318   * If no group is provided, the default group `constants.DEFAULT_CHART_GROUP` will be used.
319   * @function deregisterChart
320   * @param {Object} chart dc.js chart instance
321   * @param {String} [group] Group name
322   * @return {undefined}
323   */
324  const deregisterChart = function (chart, group) {
325      chartRegistry.deregister(chart, group);
326  };
327
328  /**
329   * Determine if a given chart instance resides in any group in the registry.
330   * @function hasChart
331   * @param {Object} chart dc.js chart instance
332   * @returns {Boolean}
333   */
334  const hasChart = function (chart) {
335      return chartRegistry.has(chart);
336  };
337
338  /**
339   * Clear given group if one is provided, otherwise clears all groups.
340   * @function deregisterAllCharts
341   * @param {String} group Group name
342   * @return {undefined}
343   */
344  const deregisterAllCharts = function (group) {
345      chartRegistry.clear(group);
346  };
347
348  /**
349   * Clear all filters on all charts within the given chart group. If the chart group is not given then
350   * only charts that belong to the default chart group will be reset.
351   * @function filterAll
352   * @param {String} [group]
353   * @return {undefined}
354   */
355  const filterAll = function (group) {
356      const charts = chartRegistry.list(group);
357      for (let i = 0; i < charts.length; ++i) {
358          charts[i].filterAll();
359      }
360  };
361
362  /**
363   * Reset zoom level / focus on all charts that belong to the given chart group. If the chart group is
364   * not given then only charts that belong to the default chart group will be reset.
365   * @function refocusAll
366   * @param {String} [group]
367   * @return {undefined}
368   */
369  const refocusAll = function (group) {
370      const charts = chartRegistry.list(group);
371      for (let i = 0; i < charts.length; ++i) {
372          if (charts[i].focus) {
373              charts[i].focus();
374          }
375      }
376  };
377
378  /**
379   * Re-render all charts belong to the given chart group. If the chart group is not given then only
380   * charts that belong to the default chart group will be re-rendered.
381   * @function renderAll
382   * @param {String} [group]
383   * @return {undefined}
384   */
385  const renderAll = function (group) {
386      const charts = chartRegistry.list(group);
387      for (let i = 0; i < charts.length; ++i) {
388          charts[i].render();
389      }
390
391      if (config._renderlet !== null) {
392          config._renderlet(group);
393      }
394  };
395
396  /**
397   * Redraw all charts belong to the given chart group. If the chart group is not given then only charts
398   * that belong to the default chart group will be re-drawn. Redraw is different from re-render since
399   * when redrawing dc tries to update the graphic incrementally, using transitions, instead of starting
400   * from scratch.
401   * @function redrawAll
402   * @param {String} [group]
403   * @return {undefined}
404   */
405  const redrawAll = function (group) {
406      const charts = chartRegistry.list(group);
407      for (let i = 0; i < charts.length; ++i) {
408          charts[i].redraw();
409      }
410
411      if (config._renderlet !== null) {
412          config._renderlet(group);
413      }
414  };
415
416  /**
417   * Start a transition on a selection if transitions are globally enabled
418   * ({@link disableTransitions} is false) and the duration is greater than zero; otherwise return
419   * the selection. Since most operations are the same on a d3 selection and a d3 transition, this
420   * allows a common code path for both cases.
421   * @function transition
422   * @param {d3.selection} selection - the selection to be transitioned
423   * @param {Number|Function} [duration=250] - the duration of the transition in milliseconds, a
424   * function returning the duration, or 0 for no transition
425   * @param {Number|Function} [delay] - the delay of the transition in milliseconds, or a function
426   * returning the delay, or 0 for no delay
427   * @param {String} [name] - the name of the transition (if concurrent transitions on the same
428   * elements are needed)
429   * @returns {d3.transition|d3.selection}
430   */
431  const transition = function (selection, duration, delay, name) {
432      if (config.disableTransitions || duration <= 0) {
433          return selection;
434      }
435
436      let s = selection.transition(name);
437
438      if (duration >= 0 || duration !== undefined) {
439          s = s.duration(duration);
440      }
441      if (delay >= 0 || delay !== undefined) {
442          s = s.delay(delay);
443      }
444
445      return s;
446  };
447
448  /* somewhat silly, but to avoid duplicating logic */
449  const optionalTransition = function (enable, duration, delay, name) {
450      if (enable) {
451          return function (selection) {
452              return transition(selection, duration, delay, name);
453          };
454      } else {
455          return function (selection) {
456              return selection;
457          };
458      }
459  };
460
461  // See http://stackoverflow.com/a/20773846
462  const afterTransition = function (_transition, callback) {
463      if (_transition.empty() || !_transition.duration) {
464          callback.call(_transition);
465      } else {
466          let n = 0;
467          _transition
468              .each(() => { ++n; })
469              .on('end', () => {
470                  if (!--n) {
471                      callback.call(_transition);
472                  }
473              });
474      }
475  };
476
477  const renderlet = function (_) {
478      if (!arguments.length) {
479          return config._renderlet;
480      }
481      config._renderlet = _;
482      return null;
483  };
484
485  const instanceOfChart = function (o) {
486      return o instanceof Object && o.__dcFlag__ && true;
487  };
488
489  const events = {
490      current: null
491  };
492
493  /**
494   * This function triggers a throttled event function with a specified delay (in milli-seconds).  Events
495   * that are triggered repetitively due to user interaction such brush dragging might flood the library
496   * and invoke more renders than can be executed in time. Using this function to wrap your event
497   * function allows the library to smooth out the rendering by throttling events and only responding to
498   * the most recent event.
499   * @name events.trigger
500   * @example
501   * chart.on('renderlet', function(chart) {
502   *     // smooth the rendering through event throttling
503   *     events.trigger(function(){
504   *         // focus some other chart to the range selected by user on this chart
505   *         someOtherChart.focus(chart.filter());
506   *     });
507   * })
508   * @param {Function} closure
509   * @param {Number} [delay]
510   * @return {undefined}
511   */
512  events.trigger = function (closure, delay) {
513      if (!delay) {
514          closure();
515          return;
516      }
517
518      events.current = closure;
519
520      setTimeout(() => {
521          if (closure === events.current) {
522              closure();
523          }
524      }, delay);
525  };
526
527  /**
528   * The dc.js filters are functions which are passed into crossfilter to chose which records will be
529   * accumulated to produce values for the charts.  In the crossfilter model, any filters applied on one
530   * dimension will affect all the other dimensions but not that one.  dc always applies a filter
531   * function to the dimension; the function combines multiple filters and if any of them accept a
532   * record, it is filtered in.
533   *
534   * These filter constructors are used as appropriate by the various charts to implement brushing.  We
535   * mention below which chart uses which filter.  In some cases, many instances of a filter will be added.
536   *
537   * Each of the dc.js filters is an object with the following properties:
538   * * `isFiltered` - a function that returns true if a value is within the filter
539   * * `filterType` - a string identifying the filter, here the name of the constructor
540   *
541   * Currently these filter objects are also arrays, but this is not a requirement. Custom filters
542   * can be used as long as they have the properties above.
543   * @namespace filters
544   * @type {{}}
545   */
546  const filters = {};
547
548  /**
549   * RangedFilter is a filter which accepts keys between `low` and `high`.  It is used to implement X
550   * axis brushing for the {@link CoordinateGridMixin coordinate grid charts}.
551   *
552   * Its `filterType` is 'RangedFilter'
553   * @name RangedFilter
554   * @memberof filters
555   * @param {Number} low
556   * @param {Number} high
557   * @returns {Array<Number>}
558   * @constructor
559   */
560  filters.RangedFilter = function (low, high) {
561      const range = new Array(low, high);
562      range.isFiltered = function (value) {
563          return value >= this[0] && value < this[1];
564      };
565      range.filterType = 'RangedFilter';
566
567      return range;
568  };
569
570  /**
571   * TwoDimensionalFilter is a filter which accepts a single two-dimensional value.  It is used by the
572   * {@link HeatMap heat map chart} to include particular cells as they are clicked.  (Rows and columns are
573   * filtered by filtering all the cells in the row or column.)
574   *
575   * Its `filterType` is 'TwoDimensionalFilter'
576   * @name TwoDimensionalFilter
577   * @memberof filters
578   * @param {Array<Number>} filter
579   * @returns {Array<Number>}
580   * @constructor
581   */
582  filters.TwoDimensionalFilter = function (filter) {
583      if (filter === null) { return null; }
584
585      const f = filter;
586      f.isFiltered = function (value) {
587          return value.length && value.length === f.length &&
588                 value[0] === f[0] && value[1] === f[1];
589      };
590      f.filterType = 'TwoDimensionalFilter';
591
592      return f;
593  };
594
595  /**
596   * The RangedTwoDimensionalFilter allows filtering all values which fit within a rectangular
597   * region. It is used by the {@link ScatterPlot scatter plot} to implement rectangular brushing.
598   *
599   * It takes two two-dimensional points in the form `[[x1,y1],[x2,y2]]`, and n
599ormalizes them so that
600   * `x1 <= x2` and `y1 <= y2`. It then returns a filter which accepts any points which are in the
601   * rectangular range including the lower values but excluding the higher values.
602   *
603   * If an array of two values are given to the RangedTwoDimensionalFilter, it interprets the values as
604   * two x coordinates `x1` and `x2` and returns a filter which accepts any points for which `x1 <= x <
605   * x2`.
606   *
607   * Its `filterType` is 'RangedTwoDimensionalFilter'
608   * @name RangedTwoDimensionalFilter
609   * @memberof filters
610   * @param {Array<Array<Number>>} filter
611   * @returns {Array<Array<Number>>}
612   * @constructor
613   */
614  filters.RangedTwoDimensionalFilter = function (filter) {
615      if (filter === null) { return null; }
616
617      const f = filter;
618      let fromBottomLeft;
619
620      if (f[0] instanceof Array) {
621          fromBottomLeft = [
622              [Math.min(filter[0][0], filter[1][0]), Math.min(filter[0][1], filter[1][1])],
623              [Math.max(filter[0][0], filter[1][0]), Math.max(filter[0][1], filter[1][1])]
624          ];
625      } else {
626          fromBottomLeft = [[filter[0], -Infinity], [filter[1], Infinity]];
627      }
628
629      f.isFiltered = function (value) {
630          let x, y;
631
632          if (value instanceof Array) {
633              x = value[0];
634              y = value[1];
635          } else {
636              x = value;
637              y = fromBottomLeft[0][1];
638          }
639
640          return x >= fromBottomLeft[0][0] && x < fromBottomLeft[1][0] &&
641                 y >= fromBottomLeft[0][1] && y < fromBottomLeft[1][1];
642      };
643      f.filterType = 'RangedTwoDimensionalFilter';
644
645      return f;
646  };
647
648  // ******** Sunburst Chart ********
649
650  /**
651   * HierarchyFilter is a filter which accepts a key path as an array. It matches any node at, or
652   * child of, the given path. It is used by the {@link SunburstChart sunburst chart} to include particular cells and all
653   * their children as they are clicked.
654   *
655   * @name HierarchyFilter
656   * @memberof filters
657   * @param {String} path
658   * @returns {Array<String>}
659   * @constructor
660   */
661  filters.HierarchyFilter = function (path) {
662      if (path === null) {
663          return null;
664      }
665
666      const filter = path.slice(0);
667      filter.isFiltered = function (value) {
668          if (!(filter.length && value && value.length && value.length >= filter.length)) {
669              return false;
670          }
671
672          for (let i = 0; i < filter.length; i++) {
673              if (value[i] !== filter[i]) {
674                  return false;
675              }
676          }
677
678          return true;
679      };
680      return filter;
681  };
682
683  class InvalidStateException extends Error { }
684
685  /**
686   * Returns a function that given a string property name, can be used to pluck the property off an object.  A function
687   * can be passed as the second argument to also alter the data being returned.
688   *
689   * This can be a useful shorthand method to create accessor functions.
690   * @example
691   * var xPluck = pluck('x');
692   * var objA = {x: 1};
693   * xPluck(objA) // 1
694   * @example
695   * var xPosition = pluck('x', function (x, i) {
696   *     // `this` is the original datum,
697   *     // `x` is the x property of the datum,
698   *     // `i` is the position in the array
699   *     return this.radius + x;
700   * });
701   * selectAll('.circle').data(...).x(xPosition);
702   * @function pluck
703   * @param {String} n
704   * @param {Function} [f]
705   * @returns {Function}
706   */
707  const pluck = function (n, f) {
708      if (!f) {
709          return function (d) { return d[n]; };
710      }
711      return function (d, i) { return f.call(d, d[n], i); };
712  };
713
714  /**
715   * @namespace utils
716   * @type {{}}
717   */
718  const utils = {};
719
720  /**
721   * Print a single value filter.
722   * @method printSingleValue
723   * @memberof utils
724   * @param {any} filter
725   * @returns {String}
726   */
727  utils.printSingleValue = function (filter) {
728      let s = `${filter}`;
729
730      if (filter instanceof Date) {
731          s = config.dateFormat(filter);
732      } else if (typeof (filter) === 'string') {
733          s = filter;
734      } else if (utils.isFloat(filter)) {
735          s = utils.printSingleValue.fformat(filter);
736      } else if (utils.isInteger(filter)) {
737          s = Math.round(filter);
738      }
739
740      return s;
741  };
742  utils.printSingleValue.fformat = d3Format.format('.2f');
743
744  // convert 'day' to d3.timeDay and similar
745  utils._toTimeFunc = function (t) {
746      const mappings = {
747          'second': d3Time.timeSecond,
748          'minute': d3Time.timeMinute,
749          'hour': d3Time.timeHour,
750          'day': d3Time.timeDay,
751          'week': d3Time.timeWeek,
752          'month': d3Time.timeMonth,
753          'year': d3Time.timeYear
754      };
755      return mappings[t];
756  };
757
758  /**
759   * Arbitrary add one value to another.
760   *
761   * If the value l is of type Date, adds r units to it. t becomes the unit.
762   * For example utils.add(dt, 3, 'week') will add 3 (r = 3) weeks (t= 'week') to dt.
763   *
764   * If l is of type numeric, t is ignored. In this case if r is of type string,
765   * it is assumed to be percentage (whether or not it includes %). For example
766   * utils.add(30, 10) will give 40 and utils.add(30, '10') will give 33.
767   *
768   * They also generate strange results if l is a string.
769   * @method add
770   * @memberof utils
771   * @param {Date|Number} l the value to modify
772   * @param {String|Number} r the amount by which to modify the value
773   * @param {Function|String} [t=d3.timeDay] if `l` is a `Date`, then this should be a
774   * [d3 time interval](https://github.com/d3/d3-time/blob/master/README.md#_interval).
775   * For backward compatibility with dc.js 2.0, it can also be the name of an interval, i.e.
776   * 'millis', 'second', 'minute', 'hour', 'day', 'week', 'month', or 'year'
777   * @returns {Date|Number}
778   */
779  utils.add = function (l, r, t) {
780      if (typeof r === 'string') {
781          r = r.replace('%', '');
782      }
783
784      if (l instanceof Date) {
785          if (typeof r === 'string') {
786              r = +r;
787          }
788          if (t === 'millis') {
789              return new Date(l.getTime() + r);
790          }
791          t = t || d3Time.timeDay;
792          if (typeof t !== 'function') {
793              t = utils._toTimeFunc(t);
794          }
795          return t.offset(l, r);
796      } else if (typeof r === 'string') {
797          const percentage = (+r / 100);
798          return l > 0 ? l * (1 + percentage) : l * (1 - percentage);
799      } else {
800          return l + r;
801      }
802  };
803
804  /**
805   * Arbitrary subtract one value from another.
806   *
807   * If the value l is of type Date, subtracts r units from it. t becomes the unit.
808   * For example utils.subtract(dt, 3, 'week') will subtract 3 (r = 3) weeks (t= 'week') from dt.
809   *
810   * If l is of type numeric, t is ignored. In this case if r is of type string,
811   * it is assumed to be percentage (whether or not it includes %). For example
812   * utils.subtract(30, 10) will give 20 and utils.subtract(30, '10') 
812will give 27.
813   *
814   * They also generate strange results if l is a string.
815   * @method subtract
816   * @memberof utils
817   * @param {Date|Number} l the value to modify
818   * @param {String|Number} r the amount by which to modify the value
819   * @param {Function|String} [t=d3.timeDay] if `l` is a `Date`, then this should be a
820   * [d3 time interval](https://github.com/d3/d3-time/blob/master/README.md#_interval).
821   * For backward compatibility with dc.js 2.0, it can also be the name of an interval, i.e.
822   * 'millis', 'second', 'minute', 'hour', 'day', 'week', 'month', or 'year'
823   * @returns {Date|Number}
824   */
825  utils.subtract = function (l, r, t) {
826      if (typeof r === 'string') {
827          r = r.replace('%', '');
828      }
829
830      if (l instanceof Date) {
831          if (typeof r === 'string') {
832              r = +r;
833          }
834          if (t === 'millis') {
835              return new Date(l.getTime() - r);
836          }
837          t = t || d3Time.timeDay;
838          if (typeof t !== 'function') {
839              t = utils._toTimeFunc(t);
840          }
841          return t.offset(l, -r);
842      } else if (typeof r === 'string') {
843          const percentage = (+r / 100);
844          return l < 0 ? l * (1 + percentage) : l * (1 - percentage);
845      } else {
846          return l - r;
847      }
848  };
849
850  /**
851   * Is the value a number?
852   * @method isNumber
853   * @memberof utils
854   * @param {any} n
855   * @returns {Boolean}
856   */
857  utils.isNumber = function (n) {
858      return n === +n;
859  };
860
861  /**
862   * Is the value a float?
863   * @method isFloat
864   * @memberof utils
865   * @param {any} n
866   * @returns {Boolean}
867   */
868  utils.isFloat = function (n) {
869      return n === +n && n !== (n | 0);
870  };
871
872  /**
873   * Is the value an integer?
874   * @method isInteger
875   * @memberof utils
876   * @param {any} n
877   * @returns {Boolean}
878   */
879  utils.isInteger = function (n) {
880      return n === +n && n === (n | 0);
881  };
882
883  /**
884   * Is the value very close to zero?
885   * @method isNegligible
886   * @memberof utils
887   * @param {any} n
888   * @returns {Boolean}
889   */
890  utils.isNegligible = function (n) {
891      return !utils.isNumber(n) || (n < constants.NEGLIGIBLE_NUMBER && n > -constants.NEGLIGIBLE_NUMBER);
892  };
893
894  /**
895   * Ensure the value is no greater or less than the min/max values.  If it is return the boundary value.
896   * @method clamp
897   * @memberof utils
898   * @param {any} val
899   * @param {any} min
900   * @param {any} max
901   * @returns {any}
902   */
903  utils.clamp = function (val, min, max) {
904      return val < min ? min : (val > max ? max : val);
905  };
906
907  /**
908   * Given `x`, return a function that always returns `x`.
909   *
910   * {@link https://github.com/d3/d3/blob/master/CHANGES.md#internals `d3.functor` was removed in d3 version 4}.
911   * This function helps to implement the replacement,
912   * `typeof x === "function" ? x : utils.constant(x)`
913   * @method constant
914   * @memberof utils
915   * @param {any} x
916   * @returns {Function}
917   */
918  utils.constant = function (x) {
919      return function () {
920          return x;
921      };
922  };
923
924  /**
925   * Using a simple static counter, provide a unique integer id.
926   * @method uniqueId
927   * @memberof utils
928   * @returns {Number}
929   */
930  let _idCounter = 0;
931  utils.uniqueId = function () {
932      return ++_idCounter;
933  };
934
935  /**
936   * Convert a name to an ID.
937   * @method nameToId
938   * @memberof utils
939   * @param {String} name
940   * @returns {String}
941   */
942  utils.nameToId = function (name) {
943      return name.toLowerCase().replace(/[\s]/g, '_').replace(/[\.']/g, '');
944  };
945
946  /**
947   * Append or select an item on a parent element.
948   * @method appendOrSelect
949   * @memberof utils
950   * @param {d3.selection} parent
951   * @param {String} selector
952   * @param {String} tag
953   * @returns {d3.selection}
954   */
955  utils.appendOrSelect = function (parent, selector, tag) {
956      tag = tag || selector;
957      let element = parent.select(selector);
958      if (element.empty()) {
959          element = parent.append(tag);
960      }
961      return element;
962  };
963
964  /**
965   * Return the number if the value is a number; else 0.
966   * @method safeNumber
967   * @memberof utils
968   * @param {Number|any} n
969   * @returns {Number}
970   */
971  utils.safeNumber = function (n) { return utils.isNumber(+n) ? +n : 0;};
972
973  /**
974   * Return true if both arrays are equal, if both array are null these are considered equal
975   * @method arraysEqual
976   * @memberof utils
977   * @param {Array|null} a1
978   * @param {Array|null} a2
979   * @returns {Boolean}
980   */
981  utils.arraysEqual = function (a1, a2) {
982      if (!a1 && !a2) {
983          return true;
984      }
985
986      if (!a1 || !a2) {
987          return false;
988      }
989
990      return a1.length === a2.length &&
991          // If elements are not integers/strings, we hope that it will match because of toString
992          // Test cases cover dates as well.
993          a1.every((elem, i) => elem.valueOf() === a2[i].valueOf());
994  };
995
996  // ******** Sunburst Chart ********
997  utils.allChildren = function (node) {
998      let paths = [];
999      paths.push(node.path);
1000      console.log('currentNode', node);
1001      if (node.children) {
1002          for (let i = 0; i < node.children.length; i++) {
1003              paths = paths.concat(utils.allChildren(node.children[i]));
1004          }
1005      }
1006      return paths;
1007  };
1008
1009  // builds a d3 Hierarchy from a collection
1010  // TODO: turn this monster method something better.
1011  utils.toHierarchy = function (list, accessor) {
1012      const root = {'key': 'root', 'children': []};
1013      for (let i = 0; i < list.length; i++) {
1014          const data = list[i];
1015          const parts = data.key;
1016          const value = accessor(data);
1017          let currentNode = root;
1018          for (let j = 0; j < parts.length; j++) {
1019              const currentPath = parts.slice(0, j + 1);
1020              const children = currentNode.children;
1021              const nodeName = parts[j];
1022              let childNode;
1023              if (j + 1 < parts.length) {
1024                  // Not yet at the end of the sequence; move down the tree.
1025                  childNode = findChild(children, nodeName);
1026
1027                  // If we don't already have a child node for this branch, create it.
1028                  if (childNode === void 0) {
1029                      childNode = {'key': nodeName, 'children': [], 'path': currentPath};
1030                      children.push(childNode);
1031                  }
1032                  currentNode = childNode;
1033              } else {
1034                  // Reached the end of the sequence; create a leaf node.
1035                  childNode = {'key': nodeName, 'value': value, 'data': data, 'path': currentPath};
1036                  children.push(childNode);
1037              }
1038          }
1039      }
1040      return root;
1041  };
1042
1043  function findChild (children, nodeName) {
1044      for (let k = 0; k < children.length; k++) {
1045          if (children[k].key === nodeName) {
1046              return children[k];
1047          }
1048      }
1049  }
1050
1051  utils.getAncestors = function (node) {
1052      const path = [];
1053      let current = node;
1054      while (current.parent) {
1055          path.unshift(current.name);
1056          current = current.parent;
1057      }
1058      return path;
1059  };
1060
1061  utils.arraysIdentical = function (a, b) {
1062      let i = a.length;
1063      if (i !== b.length) {
1064          return false;
1065      }
1066      while (i--) {
1067          if (a[i] !== b[i]) {
1068              return false;
1069          }
1070      }
1071      return true;
1072  };
1073
1074  /**
1075   * @namespace printers
1076   * @type {{}}
1077   */
1078  const printers = {};
1079
1080  /**
1081   * Converts a list of filters into a readable string.
1082   * @method filters
1083   * @memberof printers
1084   * @param {Array<filters>} filters
1085   * @returns {String}
1086   */
1087  printers.filters = function (filters) {
1088      let s = '';
1089
1090      for (let i = 0; i < filters.length; ++i) {
1091          if (i > 0) {
1092              s += ', ';
1093          }
1094          s += printers.filter(filters[i]);
1095      }
1096
1097      return s;
1098  };
1099
1100  /**
1101   * Converts a filter into a readable string.
1102   * @method filter
1103   * @memberof printers
1104   * @param {filters|any|Array<any>} filter
1105   * @returns {String}
1106   */
1107  printers.filter = function (filter) {
1108      let s = '';
1109
1110      if (typeof filter !== 'undefined' && filter !== null) {
1111          if (filter instanceof Array) {
1112              if (filter.length >= 2) {
1113                  s = `[${filter.map(e => utils.printSingleValue(e)).join(' -> ')}]`;
1114              } else if (filter.length >= 1) {
1115                  s = utils.printSingleValue(filter[0]);
1116              }
1117          } else {
1118              s = utils.printSingleValue(filter);
1119          }
1120      }
1121
1122      return s;
1123  };
1124
1125  /**
1126   * @namespace units
1127   * @type {{}}
1128   */
1129  const units = {};
1130
1131  /**
1132   * The default value for {@link CoordinateGridMixin#xUnits .xUnits} for the
1133   * {@link CoordinateGridMixin Coordinate Grid Chart} and should
1134   * be used when the x values are a sequence of integers.
1135   * It is a function that counts the number of integers in the range supplied in its start and end parameters.
1136   * @method integers
1137   * @memberof units
1138   * @see {@link CoordinateGridMixin#xUnits coordinateGridMixin.xUnits}
1139   * @example
1140   * chart.xUnits(units.integers) // already the default
1141   * @param {Number} start
1142   * @param {Number} end
1143   * @returns {Number}
1144   */
1145  units.integers = function (start, end) {
1146      return Math.abs(end - start);
1147  };
1148
1149  /**
1150   * This argument can be passed to the {@link CoordinateGridMixin#xUnits .xUnits} function of a
1151   * coordinate grid chart to specify ordinal units for the x axis. Usually this parameter is used in
1152   * combination with passing
1153   * {@link https://github.com/d3/d3-scale/blob/master/README.md#ordinal-scales d3.scaleOrdinal}
1154   * to {@link CoordinateGridMixin#x .x}.
1155   *
1156   * As of dc.js 3.0, this is purely a placeholder or magic value which causes the chart to go into ordinal mode; the
1157   * function is not called.
1158   * @method ordinal
1159   * @memberof units
1160   * @return {uncallable}
1161   * @see {@link https://github.com/d3/d3-scale/blob/master/README.md#ordinal-scales d3.scaleOrdinal}
1162   * @see {@link CoordinateGridMixin#xUnits coordinateGridMixin.xUnits}
1163   * @see {@link CoordinateGridMixin#x coordinateGridMixin.x}
1164   * @example
1165   * chart.xUnits(units.ordinal)
1166   *      .x(d3.scaleOrdinal())
1167   */
1168  units.ordinal = function () {
1169      throw new Error('dc.units.ordinal should not be called - it is a placeholder');
1170  };
1171
1172  /**
1173   * @namespace fp
1174   * @memberof units
1175   * @type {{}}
1176   */
1177  units.fp = {};
1178  /**
1179   * This function generates an argument for the {@link CoordinateGridMixin Coordinate Grid Chart}
1180   * {@link CoordinateGridMixin#xUnits .xUnits} function specifying that the x values are floating-point
1181   * numbers with the given precision.
1182   * The returned function determines how many values at the given precision will fit into the range
1183   * supplied in its start and end parameters.
1184   * @method precision
1185   * @memberof units.fp
1186   * @see {@link CoordinateGridMixin#xUnits coordinateGridMixin.xUnits}
1187   * @example
1188   * // specify values (and ticks) every 0.1 units
1189   * chart.xUnits(units.fp.precision(0.1)
1190   * // there are 500 units between 0.5 and 1 if the precision is 0.001
1191   * var thousandths = units.fp.precision(0.001);
1192   * thousandths(0.5, 1.0) // returns 500
1193   * @param {Number} precision
1194   * @returns {Function} start-end unit function
1195   */
1196  units.fp.precision = function (precision) {
1197      const _f = function (s, e) {
1198          const d = Math.abs((e - s) / _f.resolution);
1199          if (utils.isNegligible(d - Math.floor(d))) {
1200              return Math.floor(d);
1201          } else {
1202              return Math.ceil(d);
1203          }
1204      };
1205      _f.resolution = precision;
1206      return _f;
1207  };
1208
1209  const _defaultFilterHandler = (dimension, filters) => {
1210      if (filters.length === 0) {
1211          dimension.filter(null);
1212      } else if (filters.length === 1 && !filters[0].isFiltered) {
1213          // single value and not a function-based filter
1214          dimension.filterExact(filters[0]);
1215      } else if (filters.length === 1 && filters[0].filterType === 'RangedFilter') {
1216          // single range-based filter
1217          dimension.filterRange(filters[0]);
1218      } else {
1219          dimension.filterFunction(d => {
1220              for (let i = 0; i < filters.length; i++) {
1221                  const filter = filters[i];
1222                  if (filter.isFiltered) {
1223                      if(filter.isFiltered(d)) {
1224                          return true;
1225                      }
1226                  } else if (filter <= d && filter >= d) {
1227                      return true;
1228                  }
1229              }
1230              return false;
1231          });
1232      }
1233      return filters;
1234  };
1235
1236  const _defaultHasFilterHandler = (filters, filter) => {
1237      if (filter === null || typeof (filter) === 'undefined') {
1238          return filters.length > 0;
1239      }
1240      return filters.some(f => filter <= f && filter >= f);
1241  };
1242
1243  const _defaultRemoveFilterHandler = (filters, filter) => {
1244      for (let i = 0; i < filters.length; i++) {
1245          if (filters[i] <= filter && filters[i] >= filter) {
1246              filters.splice(i, 1);
1247              break;
1248          }
1249      }
1250      return filters;
1251  };
1252
1253  const _defaultAddFilterHandler = (filters, filter) => {
1254      filters.push(filter);
1255      return filters;
1256  };
1257
1258  const _defaultResetFilterHandler = filters => [];
1259
1260  /**
1261   * `BaseMixin` is an abstract functional object representing a basic `dc` chart object
1262   * for all chart and widget implementations. Methods from the {@link #BaseMixin BaseMixin} are inherited
1263   * and available on all chart implementations in the `dc` library.
1264   * @mixin BaseMixin
1265   */
1266  class BaseMixin {
1267      constructor () {
1268          this.__dcFlag__ = utils.uniqueId();
1269          this._svgDescription = null;
1270          this._keyboardAccessible = false;
1271
1272          this._dimension = undefined;
1273          this._group = undefined;
1274
1275          this._anchor = undefined;
1276          this._root = undefined;
1277          this._svg = undefined;
1278          this._isChild = undefined;
1279
1280          this._minWidth = 200;
1281          this._defaultWidthCalc = element => {
1282              const width = element && element.getBoundingClientRect && element.getBoundingClientRect().width;
1283              return (width && width > this._minWidth) ? width : this._minWidth;
1284          };
1285          this._widthCalc = this._defaultWidthCalc;
1286
1287          this._minHeight = 200;
1288          this._defaultHeightCalc = element => {
1289              const height = element && element.getBoundingClientRect && element.getBoundingClientRect().height;
1290              return (height && height > this._minHeight) ? height : this._minHeight;
1291          };
1292          this._heightCalc = this._defaultHeightCalc;
1293          this._width = undefined;
1294          this._height = undefined;
1295          this._useViewBoxResizing = false;
1296
1297          this._keyAccessor = pluck('key');
1298          this._valueAccessor = pluck('value');
1299          this._label = pluck('key');
1300
1301          this._ordering = pluck('key');
1302
1303          this._renderLabel = false;
1304
1305          this._title = d => `${this.keyAccessor()(d)}: ${this.valueAccessor()(d)}`;
1306          this._renderTitle = true;
1307          this._controlsUseVisibility = false;
1308
1309          this._transitionDuration = 750;
1310
1311          this._transitionDelay = 0;
1312
1313          this._filterPrinter = printers.filters;
1314
1315          this._mandatoryAttributesList = ['dimension', 'group'];
1316
1317          this._chartGroup = constants.DEFAULT_CHART_GROUP;
1318
1319          this._listeners = d3Dispatch.dispatch(
1320              'preRender',
1321              'postRender',
1322              'preRedraw',
1323              'postRedraw',
1324              'filtered',
1325              'zoomed',
1326              'renderlet',
1327              'pretransition');
1328
1329          this._legend = undefined;
1330          this._commitHandler = undefined;
1331
1332          this._defaultData = group => group.all();
1333          this._data = this._defaultData;
1334
1335          this._filters = [];
1336
1337          this._filterHandler = _defaultFilterHandler;
1338          this._hasFilterHandler = _defaultHasFilterHandler;
1339          this._removeFilterHandler = _defaultRemoveFilterHandler;
1340          this._addFilterHandler = _defaultAddFilterHandler;
1341          this._resetFilterHandler = _defaultResetFilterHandler;
1342      }
1343
1344      /**
1345       * Set or get the height attribute of a chart. The height is applied to the SVGElement generated by
1346       * the chart when rendered (or re-rendered). If a value is given, then it will be used to calculate
1347       * the new height and the chart returned for method chaining.  The value can either be a numeric, a
1348       * function, or falsy. If no value is specified then the value of the current height attribute will
1349       * be returned.
1350       *
1351       * By default, without an explicit height being given, the chart will select the width of its
1352       * anchor element. If that isn't possible it defaults to 200 (provided by the
1353       * {@link BaseMixin#minHeight minHeight} property). Setting the value falsy will return
1354       * the chart to the default behavior.
1355       * @see {@link BaseMixin#minHeight minHeight}
1356       * @example
1357       * // Default height
1358       * chart.height(function (element) {
1359       *     var height = element && element.getBoundingClientRect && element.getBoundingClientRect().height;
1360       *     return (height && height > chart.minHeight()) ? height : chart.minHeight();
1361       * });
1362       *
1363       * chart.height(250);
1363 // Set the chart's height to 250px;
1364       * chart.height(function(anchor) { return doSomethingWith(anchor); }); // set the chart's height with a function
1365       * chart.height(null); // reset the height to the default auto calculation
1366       * @param {Number|Function} [height]
1367       * @returns {Number|BaseMixin}
1368       */
1369      height (height) {
1370          if (!arguments.length) {
1371              if (!utils.isNumber(this._height)) {
1372                  // only calculate once
1373                  this._height = this._heightCalc(this._root.node());
1374              }
1375              return this._height;
1376          }
1377          this._heightCalc = height ? (typeof height === 'function' ? height : utils.constant(height)) : this._defaultHeightCalc;
1378          this._height = undefined;
1379          return this;
1380      }
1381
1382      /**
1383       * Set or get the width attribute of a chart.
1384       * @see {@link BaseMixin#height height}
1385       * @see {@link BaseMixin#minWidth minWidth}
1386       * @example
1387       * // Default width
1388       * chart.width(function (element) {
1389       *     var width = element && element.getBoundingClientRect && element.getBoundingClientRect().width;
1390       *     return (width && width > chart.minWidth()) ? width : chart.minWidth();
1391       * });
1392       * @param {Number|Function} [width]
1393       * @returns {Number|BaseMixin}
1394       */
1395      width (width) {
1396          if (!arguments.length) {
1397              if (!utils.isNumber(this._width)) {
1398                  // only calculate once
1399                  this._width = this._widthCalc(this._root.node());
1400              }
1401              return this._width;
1402          }
1403          this._widthCalc = width ? (typeof width === 'function' ? width : utils.constant(width)) : this._defaultWidthCalc;
1404          this._width = undefined;
1405          return this;
1406      }
1407
1408      /**
1409       * Set or get the minimum width attribute of a chart. This only has effect when used with the default
1410       * {@link BaseMixin#width width} function.
1411       * @see {@link BaseMixin#width width}
1412       * @param {Number} [minWidth=200]
1413       * @returns {Number|BaseMixin}
1414       */
1415      minWidth (minWidth) {
1416          if (!arguments.length) {
1417              return this._minWidth;
1418          }
1419          this._minWidth = minWidth;
1420          return this;
1421      }
1422
1423      /**
1424       * Set or get the minimum height attribute of a chart. This only has effect when used with the default
1425       * {@link BaseMixin#height height} function.
1426       * @see {@link BaseMixin#height height}
1427       * @param {Number} [minHeight=200]
1428       * @returns {Number|BaseMixin}
1429       */
1430      minHeight (minHeight) {
1431          if (!arguments.length) {
1432              return this._minHeight;
1433          }
1434          this._minHeight = minHeight;
1435          return this;
1436      }
1437
1438      /**
1439       * Turn on/off using the SVG
1440       * {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox `viewBox` attribute}.
1441       * When enabled, `viewBox` will be set on the svg root element instead of `width` and `height`.
1442       * Requires that the chart aspect ratio be defined using chart.width(w) and chart.height(h).
1443       *
1444       * This will maintain the aspect ratio while enabling the chart to resize responsively to the
1445       * space given to the chart using CSS. For example, the chart can use `width: 100%; height:
1446       * 100%` or absolute positioning to resize to its parent div.
1447       *
1448       * Since the text will be sized as if the chart is drawn according to the width and height, and
1449       * will be resized if the chart is any other size, you need to set the chart width and height so
1450       * that the text looks good. In practice, 600x400 seems to work pretty well for most charts.
1451       *
1452       * You can see examples of this resizing strategy in the [Chart Resizing
1453       * Examples](http://dc-js.github.io/dc.js/resizing/); just add `?resize=viewbox` to any of the
1454       * one-chart examples to enable `useViewBoxResizing`.
1455       * @param {Boolean} [useViewBoxResizing=false]
1456       * @returns {Boolean|BaseMixin}
1457       */
1458      useViewBoxResizing (useViewBoxResizing) {
1459          if (!arguments.length) {
1460              return this._useViewBoxResizing;
1461          }
1462          this._useViewBoxResizing = useViewBoxResizing;
1463          return this;
1464      }
1465
1466      /**
1467       * **mandatory**
1468       *
1469       * Set or get the dimension attribute of a chart. In `dc`, a dimension can be any valid
1470       * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#dimension crossfilter dimension}
1471       *
1472       * If a value is given, then it will be used as the new dimension. If no value is specified then
1473       * the current dimension will be returned.
1474       * @see {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#dimension crossfilter.dimension}
1475       * @example
1476       * var index = crossfilter([]);
1477       * var dimension = index.dimension(pluck('key'));
1478       * chart.dimension(dimension);
1479       * @param {crossfilter.dimension} [dimension]
1480       * @returns {crossfilter.dimension|BaseMixin}
1481       */
1482      dimension (dimension) {
1483          if (!arguments.length) {
1484              return this._dimension;
1485          }
1486          this._dimension = dimension;
1487          this.expireCache();
1488          return this;
1489      }
1490
1491      /**
1492       * Set the data callback or retrieve the chart's data set. The data callback is passed the chart's
1493       * group and by default will return
1494       * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_all group.all}.
1495       * This behavior may be modified to, for instance, return only the top 5 groups.
1496       * @example
1497       * // Default data function
1498       * chart.data(function (group) { return group.all(); });
1499       *
1500       * chart.data(function (group) { return group.top(5); });
1501       * @param {Function} [callback]
1502       * @returns {*|BaseMixin}
1503       */
1504      data (callback) {
1505          if (!arguments.length) {
1506              return this._data(this._group);
1507          }
1508          this._data = typeof callback === 'function' ? callback : utils.constant(callback);
1509          this.expireCache();
1510          return this;
1511      }
1512
1513      /**
1514       * **mandatory**
1515       *
1516       * Set or get the group attribute of a chart. In `dc` a group is a
1517       * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group-map-reduce crossfilter group}.
1518       * Usually the group should be created from the particular dimension associated with the same chart. If a value is
1519       * given, then it will be used as the new group.
1520       *
1521       * If no value specified then the current group will be returned.
1522       * If `name` is specified then it will be used to generate legend label.
1523       * @see {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group-map-reduce crossfilter.group}
1524       * @example
1525       * var index = crossfilter([]);
1526       * var dimension = index.dimension(pluck('key'));
1527       * chart.dimension(dimension);
1528       * chart.group(dimension.group().reduceSum());
1529       * @param {crossfilter.group} [group]
1530       * @param {String} [name]
1531       * @returns {crossfilter.group|BaseMixin}
1532       */
1533      group (group, name) {
1534          if (!arguments.length) {
1535              return this._group;
1536          }
1537          this._group = group;
1538          this._groupName = name;
1539          this.expireCache();
1540          return this;
1541      }
1542
1543      /**
1544       * Get or set an accessor to order ordinal dimensions.  The chart uses
1545       * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort Array.sort}
1546       * to sort elements; this accessor returns the value to order on.
1547       * @example
1548       * // Default ordering accessor
1549       * _chart.ordering(pluck('key'));
1550       * @param {Function} [orderFunction]
1551       * @returns {Function|BaseMixin}
1552       */
1553      ordering (orderFunction) {
1554          if (!arguments.length) {
1555              return this._ordering;
1556          }
1557          this._ordering = orderFunction;
1558          this.expireCache();
1559          return this;
1560      }
1561
1562      _computeOrderedGroups (data) {
1563          // clone the array before sorting, otherwise Array.sort sorts in-place
1564          return Array.from(data).sort((a, b) => d3Array.ascending(this._ordering(a), this._ordering(b)));
1565      }
1566
1567      /**
1568       * Clear all filters associated with this chart. The same effect can be achieved by calling
1569       * {@link BaseMixin#filter chart.filter(null)}.
1570       * @returns {BaseMixin}
1571       */
1572      filterAll () {
1573          return this.filter(null);
1574      }
1575
1576      /**
1577       * Execute d3 single selection in the chart's scope using the given selector and return the d3
1578       * selection.
1579       *
1580       * This function is **not chainable** since it does not return a chart instance; however the d3
1581       * selection result can be chained to d3 function calls.
1582       * @see {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3.select}
1583       * @example
1584       * // Has the same effect as d3.select('#chart-id').select(selector)
1585       * chart.select(selector)
1586       * @param {String} sel CSS selector string
1587       * @returns {d3.selection}
1588       */
1589      select (sel) {
1590          return this._root.select(sel);
1591      }
1592
1593      /**
1594       * Execute in scope d3 selectAll using the given selector and return d3 selection result.
1595       *
1596       * This function is **not chainable** since it does not return a chart instance; however the d3
1597       * selection result can be chained to d3 function calls.
1598       * @see {@link https://github.com/d3/d3-selection/blob/master/README.md#selectAll d3.selectAll}
1599       * @example
1600       * // Has the same effect as d3.select('#chart-id').selectAll(selector)
1601       * chart.selectAll(selector)
1602       * @param {String} sel CSS selector string
1603       * @returns {d3.selection}
1604       */
1605      selectAll (sel) {
1606          return this._root ? this._root.selectAll(sel) : null;
1607      }
1608
1609      /**
1610       * Set the root SVGElement to either be an existing chart's root; or any valid [d3 single
1611       * selector](https://github.com/d3/d3-selection/blob/master/README.md#selecting-elements) specifying a dom
1612       * block element such as a div; or a dom element or d3 selection. Optionally registers the chart
1613       * within the chartGroup. This class is called internally on chart initialization, but be called
1614       * again to relocate the chart. However, it will orphan any previously created SVGElements.
1615       * @param {anchorChart|anchorSelector|anchorNode} [parent]
1616       * @param {String} [chartGroup]
1617       * @returns {String|node|d3.selection|BaseMixin}
1618       */
1619      anchor (parent, chartGroup) {
1620          if (!arguments.length) {
1621              return this._anchor;
1622          }
1623          if (instanceOfChart(parent)) {
1624              this._anchor = parent.anchor();
1625              if (this._anchor.children) { // is _anchor a div?
1626                  this._anchor = `#${parent.anchorName()}`;
1627              }
1628              this._root = parent.root();
1629              this._isChild = true;
1630          } else if (parent) {
1631              if (parent.select && parent.classed) { // detect d3 selection
1632                  this._anchor = parent.node();
1633              } else {
1634                  this._anchor = parent;
1635              }
1636              this._root = d3Selection.select(this._anchor);
1637              this._root.classed(constants.CHART_CLASS, true);
1638              registerChart(this, chartGroup);
1639              this._isChild = false;
1640          } else {
1641              throw new BadArgumentException('parent must be defined');
1642          }
1643          this._chartGroup = chartGroup;
1644          return this;
1645      }
1646
1647      /**
1648       * Returns the DOM id for the chart's anchored location.
1649       * @returns {String}
1650       */
1651      anchorName () {
1652          const a = this.anchor();
1653          if (a && a.id) {
1654              return a.id;
1655          }
1656          if (a && a.replace) {
1657              return a.replace('#', '');
1658          }
1659          return `dc-chart${this.chartID()}`;
1660      }
1661
1662      /**
1663       * Returns the root element where a chart resides. Usually it will be the parent div element where
1664       * the SVGElement was created. You can also pass in a new root element however this is usually handled by
1665       * dc internally. Resetting the root element on a chart outside of dc internals may have
1666       * unexpected consequences.
1667       * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement HTMLElement}
1668       * @param {HTMLElement} [rootElement]
1669       * @returns {HTMLElement|BaseMixin}
1670       */
1671      root (rootElement) {
1672          if (!arguments.length) {
1673              return this._root;
1674          }
1675          this._root = rootElement;
1676          return this;
1677      }
1678
1679      /**
1680       * Returns the top SVGElement for this specific chart. You can also pass in a new SVGElement,
1681       * however this is usually handled by dc internally. Resetting the SVGElement on a chart outside
1682       * of dc internals may have unexpected consequences.
1683       * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SVGElement SVGElement}
1684       * @param {SVGElement|d3.selection} [svgElement]
1685       * @returns {SVGElement|d3.selection|BaseMixin}
1686       */
1687      svg (svgElement) {
1688          if (!arguments.length) {
1689              return this._svg;
1690          }
1691          this._svg = svgElement;
1692          return this;
1693      }
1694
1695      /**
1696       * Remove the chart's SVGElements from the dom and recreate the container SVGElement.
1697       * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SVGElement SVGElement}
1698       * @returns {SVGElement}
1699       */
1700      resetSvg () {
1701          this.select('svg').remove();
1702          return this.generateSvg();
1703      }
1704
1705      sizeSvg () {
1706          if (this._svg) {
1707              if (!this._useViewBoxResizing) {
1708                  this._svg
1709                      .attr('width', this.width())
1710                      .attr('height', this.height());
1711              } else if (!this._svg.attr('viewBox')) {
1712                  this._svg
1713                      .attr('viewBox', `0 0 ${this.width()} ${this.height()}`);
1714              }
1715          }
1716      }
1717
1718      generateSvg () {
1719          this._svg = this.root().append('svg');
1720      
1721          if (this._svgDescription || this._keyboardAccessible) {
1722
1723              this._svg.append('desc')
1724                  .attr('id', `desc-id-${this.__dcFlag__}`)
1725                  .html(`${this.svgDescription()}`);
1726
1727              this._svg
1728                  .attr('tabindex', '0')
1729                  .attr('role', 'img')
1730                  .attr('aria-labelledby', `desc-id-${this.__dcFlag__}`);
1731          }
1732
1733          this.sizeSvg();
1734          return this._svg;
1735      }
1736
1737      /**
1738       * Set or get description text for the entire SVG graphic. If set, will create a `<desc>` element as the first
1739       * child of the SVG with the description text and also make the SVG focusable from keyboard.
1740       * @param {String} [description]
1741       * @returns {String|BaseMixin}
1742       */
1743      svgDescription (description) {
1744          if (!arguments.length) {
1745              return this._svgDescription || this.constructor.name;
1746          }
1747
1748          this._svgDescription = description;
1749          return this;
1750      }
1751
1752      /**
1753       * If set, interactive chart elements like individual bars in a bar chart or symbols in a scatter plot
1754       * will be focusable from keyboard and on pressing Enter or Space will behave as if clicked on.
1755       * 
1756       * If `svgDescription` has not been explicitly set, will also set SVG description text to the class
1757       * constructor name, like BarChart or HeatMap, and make the entire SVG focusable.
1758       * @param {Boolean} [keyboardAccessible=false]
1759       * @returns {Boolean|BarChart}
1760       */
1761      keyboardAccessible (keyboardAccessible) {
1762          if (!arguments.length) {
1763              return this._keyboardAccessible;
1764          }
1765          this._keyboardAccessible = keyboardAccessible;
1766          return this;
1767      }
1768
1769      /**
1770       * Set or get the filter printer function. The filter printer function is used to generate human
1771       * friendly text for filter value(s) associated with the chart instance. The text will get shown
1772       * in the `.filter element; see {@link BaseMixin#turnOnControls turnOnControls}.
1773       *
1774       * By default dc charts use a default filter printer {@link printers.filters printers.filters}
1775       * that provides simple printing support for both single value and ranged filters.
1776       * @example
1777       * // for a chart with an ordinal brush, print the filters in upper case
1778       * chart.filterPrinter(function(filters) {
1779       *   return filters.map(function(f) { return f.toUpperCase(); }).join(', ');
1780       * });
1781       * // for a chart with a range brush, print the filter as start and extent
1782       * chart.filterPrinter(function(filters) {
1783       *   return 'start ' + utils.printSingleValue(filters[0][0]) +
1784       *     ' extent ' + utils.printSingleValue(filters[0][1] - filters[0][0]);
1785       * });
1786       * @param {Function} [filterPrinterFunction=printers.filters]
1787       * @returns {Function|BaseMixin}
1788       */
1789      filterPrinter (filterPrinterFunction) {
1790          if (!arguments.length) {
1791              return this._filterPrinter;
1792          }
1793          this._filterPrinter = filterPrinterFunction;
1794          return this;
1795      }
1796
1797      /**
1798       * If set, use the `visibility` attribute instead of the `display` attribute for showing/hiding
1799       * chart reset and filter controls, for less disruption to the layout.
1800       * @param {Boolean} [controlsUseVisibility=false]
1801       * @returns {Boolean|BaseMixin}
1802       */
1803      controlsUseVisibility (controlsUseVisibility) {
1804          if (!arguments.length) {
1805              return this._controlsUseVisibility;
1806          }
1807          this._controlsUseVisibility = controlsUseVisibility;
1808          return this;
1809      }
1810
1811      /**
1812       * Turn on optional control elements within the root element. dc currently supports the
1813       * following html control elements.
1814       * * root.selectAll('.reset') - elements are turned on if the chart has an active filter. This type
1815       * of control element is usually used to store a reset link to allow user to reset filter on a
1816       * certain chart. This element will be turned off automatically if the filter is cleared.
1817       * * root.selectAll('.filter') elements are turned on if the chart has an active filter. The text
1818       * content of this element is then replaced with the current filter value using the filter printer
1819       * function. This type of element will be turned off automatically if the filter is cleared.
1820       * @returns {BaseMixin}
1821       */
1822      turnOnControls () {
1823          if (this._root) {
1824              const attribute = this.controlsUseVisibility() ? 'visibility' : 'display';
1825              this.selectAll('.reset').style(attribute, null);
1826              this.selectAll('.filter').text(this._filterPrinter(this.filters())).style(attribute, null);
1827          }
1828          return this;
1829      }
1830
1831      /**
1832       * Turn off optional control elements within the root element.
1833       * @see {@link BaseMixin#turnOnControls turnOnControls}
1834       * @returns {BaseMixin}
1835       */
1836      turnOffControls () {
1837          if (this._root) {
1838              const attribute = this.controlsUseVisibility() ? 'visibility' : 'display';
1839              const value = this.controlsUseVisibility() ? 'hidden' : 'none';
1840              this.selectAll('.reset').style(attribute, value);
1841              this.selectAll('.filter').style(attribute, value).text(this.filter());
1842          }
1843          return this;
1844      }
1845
1846      /**
1847       * Set or get the animation transition duration (in milliseconds) for this chart instance.
1848       * @param {Number} [duration=750]
1849       * @returns {Number|BaseMixin}
1850       */
1851      transitionDuration (duration) {
1852          if (!arguments.length) {
1853              return this._transitionDuration;
1854          }
1855          this._transitionDuration = duration;
1856          return this;
1857      }
1858
1859      /**
1860       * Set or get the animation transition delay (in milliseconds) for this chart instance.
1861       * @param {Number} [delay=0]
1862       * @returns {Number|BaseMixin}
1863       */
1864      transitionDelay (delay) {
1865          if (!arguments.length) {
1866              return this._transitionDelay;
1867          }
1868          this._transitionDelay = delay;
1869          return this;
1870      }
1871
1872      _mandatoryAttributes (_) {
1873          if (!arguments.length) {
1874              return this._mandatoryAttributesList;
1875          }
1876          this._mandatoryAttributesList = _;
1877          return this;
1878      }
1879
1880      checkForMandatoryAttributes (a) {
1881          if (!this[a] || !this[a]()) {
1882              throw new InvalidStateException(`Mandatory attribute chart.${a} is missing on chart[#${this.anchorName()}]`);
1883          }
1884      }
1885
1886      /**
1887       * Invoking this method will force the chart to re-render everything from scratch. Generally it
1888       * should only be used to render the chart for the first time on the page or if you want to make
1889       * sure everything is redrawn from scratch instead of relying on the default incremental redrawing
1890       * behaviour.
1891       * @returns {BaseMixin}
1892       */
1893      render () {
1894          this._height = this._width = undefined; // force recalculate
1895          this._listeners.call('preRender', this, this);
1896
1897          if (this._mandatoryAttributesList) {
1898              this._mandatoryAttributesList.forEach(e => this.checkForMandatoryAttributes(e));
1899          }
1900
1901          const result = this._doRender();
1902
1903          if (this._legend) {
1904              this._legend.render();
1905          }
1906
1907          this._activateRenderlets('postRender');
1908
1909          return result;
1910      }
1911
1912      _makeKeyboardAccessible (onClickFunction, ...onClickArgs) {
1913          // called from each chart module's render and redraw methods
1914          const tabElements = this._svg
1915              .selectAll('.dc-tabbable')
1916              .attr('tabindex', 0);
1917                  
1918          if (onClickFunction) {
1919              tabElements.on('keydown', d3compat.eventHandler((d, event) => {
1920                  // trigger only if d is an object undestood by KeyAccessor()
1921                  if (event.keyCode === 13 && typeof d === 'object') {
1922                      onClickFunction.call(this, d, ...onClickArgs);
1923                  } 
1924                  // special case for space key press - prevent scrolling
1925                  if (event.keyCode === 32 && typeof d === 'object') {
1926                      onClickFunction.call(this, d, ...onClickArgs);
1927                      event.preventDefault();                
1928                  }
1929              
1930              }));
1931          }
1932      }
1933
1934      _activateRenderlets (event) {
1935          this._listeners.call('pretransition', this, this);
1936          if (this.transitionDuration() > 0 && this._svg) {
1937              this._svg.transition().duration(this.transitionDuration()).delay(this.transitionDelay())
1938                  .on('end', () => {
1939                      this._listeners.call('renderlet', this, this);
1940                      if (event) {
1941                          this._listeners.call(event, this, this);
1942                      }
1943                  });
1944          } else {
1945              this._listeners.call('renderlet', this, this);
1946              if (event) {
1947                  this._listeners.call(event, this, this);
1948              }
1949          }
1950      }
1951
1952      /**
1953       * Calling redraw will cause the chart to re-render data changes incrementally. If there is no
1954       * change in the underlying data dimension then calling this method will have no effect on the
1955       * chart. Most chart interaction in dc will automatically trigger this method through internal
1956       * events (in particular {@link redrawAll redrawAll}); therefore, you only need to
1957       * manually invoke this function if data is manipulated outside of dc's control (for example if
1958       * data is loaded in the background using
1959       * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#crossfilter_add crossfilter.add}).
1960       * @returns {BaseMixin}
1961       */
1962      redraw () {
1963          this.sizeSvg();
1964          this._listeners.call('preRedraw', this, this);
1965
1966          const result = this._doRedraw();
1967
1968          if (this._legend) {
1969              this._legend.render();
1970          }
1971
1972          this._activateRenderlets('postRedraw');
1973
1974          return result;
1975      }
1976
1977      /**
1978       * Gets/sets the commit handler. If the chart has a commit handler, the handler will be called when
1979       * the chart's filters have changed, in order to send the filter data asynchronously to a server.
1980       *
1981       * Unlike other functions in dc.js, the commit handler is asynchronous. It takes two arguments:
1982       * a flag indicating whether this is a render (true) or a redraw (false), and a callback to be
1983       * triggered once the commit is done. The callback has the standard node.js continuation signature
1984       * with error first and result second.
1985       * @param {Function} commitHandler
1986       * @returns {BaseMixin}
1987       */
1988      commitHandler (commitHandler) {
1989          if (!arguments.length) {
1990              return this._commitHandler;
1991          }
1992          this._commitHandler = commitHandler;
1993          return this;
1994      }
1995
1996      /**
1997       * Redraws all charts in the same group as this chart, typically in reaction to a filter
1998       * change. If the chart has a {@link BaseMixin.commitFilter commitHandler}, it will
1999       * be executed and waited for.
2000       * @returns {BaseMixin}
2001       */
2002      redrawGroup () {
2003          if (this._commitHandler) {
2004              this._commitHandler(false, (error, result) => {
2005                  if (error) {
2006                      console.log(error);
2007                  } else {
2008                      redrawAll(this.chartGroup());
2009                  }
2010              });
2011          } else {
2012              redrawAll(this.chartGroup());
2013          }
2014          return this;
2015      }
2016
2017      /**
2018       * Renders all charts in the same group as this chart. If the chart has a
2019       * {@link BaseMixin.commitFilter commitHandler}, it will be executed and waited for
2020       * @returns {BaseMixin}
2021       */
2022      renderGroup () {
2023          if (this._commitHandler) {
2024              this._commitHandler(false, (error, result) => {
2025                  if (error) {
2026                      console.log(error);
2027                  } else {
2028                      renderAll(this.chartGroup());
2029                  }
2030              });
2031          } else {
2032              renderAll(this.chartGroup());
2033          }
2034          return this;
2035      }
2036
2037      _invokeFilteredListener (f) {
2038          if (f !== undefined) {
2039              this._listeners.call('filtered', this, this, f);
2040          }
2041      }
2042
2043      _invokeZoomedListener () {
2044          this._listeners.call('zoomed', this, this);
2045      }
2046
2047      /**
2048       * Set or get the has-filter handler. The has-filter handler is a function that checks to see if
2049       * the chart's current filters (first argument) include a specific filter (second argument).  Using a custom has-filter handler allows
2050       * you to change the way filters are checked for and replaced.
2051       * @example
2052       * // default has-filter handler
2053       * chart.hasFilterHandler(function (filters, filter) {
2054       *     if (filter === null || typeof(filter) === 'undefined') {
2055       *         return filters.length > 0;
2056       *     }
2057       *     return filters.some(function (f) {
2058       *         return filter <= f && filter >= f;
2059       *     });
2060       * });
2061       *
2062       * // custom filter handler (no-op)
2063       * chart.hasFilterHandler(function(filters, filter) {
2064       *     return false;
2065       * });
2066       * @param {Function} [hasFilterHandler]
2067       * @returns {Function|BaseMixin}
2068       */
2069      hasFilterHandler (hasFilterHandler) {
2070          if (!arguments.length) {
2071              return this._hasFilterHandler;
2072          }
2073          this._hasFilterHandler = hasFilterHandler;
2074          return this;
2075      }
2076
2077      /**
2078       * Check whether any active filter or a specific filter is associated with particular chart instance.
2079       * This function is **not chainable**.
2080       * @see {@link BaseMixin#hasFilterHandler hasFilterHandler}
2081       * @param {*} [filter]
2082       * @returns {Boolean}
2083       */
2084      hasFilter (filter) {
2085          return this._hasFilterHandler(this._filters, filter);
2086      }
2087
2088      /**
2089       * Set or get the remove filter handler. The remove filter handler is a function that removes a
2090       * filter from the chart's current filters. Using a custom remove filter handler allows you to
2091       * change how filters are removed or perform additional work when removing a filter, e.g. when
2092       * using a filter server other than crossfilter.
2093       *
2094       * The handler should return a new or modified array as the result.
2095       * @example
2096       * // default remove filter handler
2097       * chart.removeFilterHandler(function (filters, filter) {
2098       *     for (var i = 0; i < filters.length; i++) {
2099       *         if (filters[i] <= filter && filters[i] >= filter) {
2100       *             filters.splice(i, 1);
2101       *             break;
2102       *         }
2103       *     }
2104       *     return filters;
2105       * });
2106       *
2107       * // custom filter handler (no-op)
2108       * chart.removeFilterHandler(function(filters, filter) {
2109       *     return filters;
2110       * });
2111       * @param {Function} [removeFilterHandler]
2112       * @returns {Function|BaseMixin}
2113       */
2114      removeFilterHandler (removeFilterHandler) {
2115          if (!arguments.length) {
2116              return this._removeFilterHandler;
2117          }
2118          this._removeFilterHandler = removeFilterHandler;
2119          return this;
2120      }
2121
2122      /**
2123       * Set or get the add filter handler. The add filter handler is a function that adds a filter to
2124       * the chart's filter list. Using a custom add filter handler allows you to change the way filters
2125       * are added or perform additional work when adding a filter, e.g. when using a filter server other
2126       * than crossfilter.
2127       *
2128       * The handler should return a new or modified array as the result.
2129       * @example
2130       * // default add filter handler
2131       * chart.addFilterHandler(function (filters, filter) {
2132       *     filters.push(filter);
2133       *     return filters;
2134       * });
2135       *
2136       * // custom filter handler (no-op)
2137       * chart.addFilterHandler(function(filters, filter) {
2138       *     return filters;
2139       * });
2140       * @param {Function} [addFilterHandler]
2141       * @returns {Function|BaseMixin}
2142       */
2143      addFilterHandler (addFilterHandler) {
2144          if (!arguments.length) {
2145              return this._addFilterHandler;
2146          }
2147          this._addFilterHandler = addFilterHandler;
2148          return this;
2149      }
2150
2151      /**
2152       * Set or get the reset filter handler. The reset filter handler is a function that resets the
2153       * chart's filter list by returning a new list. Using a custom reset filter handler allows you to
2154       * change the way filters are reset, or perform additional work when resetting the filters,
2155       * e.g. when using a filter server other than crossfilter.
2156       *
2157       * The handler should return a new or modified array as the result.
2158       * @example
2159       * // default remove filter handler
2160       * function (filters) {
2161       *     return [];
2162       * }
2163       *
2164       * // custom filter handler (no-op)
2165       * chart.resetFilterHandler(function(filters) {
2166       *     return filters;
2167       * });
2168       * @param {Function} [resetFilterHandler]
2169       * @returns {BaseMixin}
2170       */
2171      resetFilterHandler (resetFilterHandler) {
2172          if (!arguments.length) {
2173              return this._resetFilterHandler;
2174          }
2175          this._resetFilterHandler = resetFilterHandler;
2176          return this;
2177      }
2178
2179      applyFilters (filters) {
2180          if (this.dimension() && this.dimension().filter) {
2181              const fs = this._filterHandler(this.dimension(), filters);
2182              if (fs) {
2183                  filters = fs;
2184              }
2185          }
2186          return filters;
2187      }
2188
2189      /**
2190       * Replace the chart filter. This is equivalent to calling `chart.filter(null).filter(filter)`
2191       * but more efficient because the filter is only applied once.
2192       *
2193       * @param {*} [filter]
2194       * @returns {BaseMixin}
2195       */
2196      replaceFilter (filter) {
2197          this._filters = this._resetFilterHandler(this._filters);
2198          this.filter(filter);
2199          return this;
2200      }
2201
2202      /**
2203       * Filter the chart by the given parameter, or return the current filter if no input parameter
2204       * is given.
2205       *
2206       * The filter parameter can take one of these forms:
2207       * * A single value: the value will be toggled (added if it is not present in the current
2208       * filters, removed if it is present)
2209       * * An array containing a single array of values (`[[value,value,value]]`): each value is
2210       * toggled
2211       * * When appropriate for the chart, a {@link filters dc filter object} such as
2212       *   * {@link filters.RangedFilter `filters.RangedFilter`} for the
2213       * {@link CoordinateGridMixin CoordinateGridMixin} charts
2214       *   * {@link filters.TwoDimensionalFilter `filters.TwoDimensionalFilter`} for the
2215       * {@link HeatMap heat map}
2216       *   * {@link filters.RangedTwoDimensionalFilter `filters.RangedTwoDimensionalFilter`}
2217       * for the {@link ScatterPlot scatter plot}
2218       * * `null`: the filter will be reset using the
2219       * {@link BaseMixin#resetFilterHandler resetFilterHandler}
2220       *
2221       * Note that this is always a toggle (even when it doesn't make sense for the filter type). If
2222       * you wish to replace the current filter, either call `chart.filter(null)` first - or it's more
2223       * efficient to call {@link BaseMixin#replaceFilter `chart.replaceFilter(filter)`} instead.
2224       *
2225       * Each toggle is executed by checking if the value is already present using the
2226       * {@link BaseMixin#hasFilterHandler hasFilterHandler}; if it is not present, it is added
2227       * using the {@link BaseMixin#addFilterHandler addFilterHandler}; if it is already present,
2228       * it is removed using the {@link BaseMixin#removeFilterHandler removeFilterHandler}.
2229       *
2230       * Once the filters array has been updated, the filters are applied to the
2231       * crossfilter dimension, using the {@link BaseMixin#filterHandler filterHandler}.
2232       *
2233       * Once you have set the filters, call {@link BaseMixin#redrawGroup `chart.redrawGroup()`}
2234       * (or {@link redrawAll `redrawAll()`}
2234) to redraw the chart's group.
2235       * @see {@link BaseMixin#addFilterHandler addFilterHandler}
2236       * @see {@link BaseMixin#removeFilterHandler removeFilterHandler}
2237       * @see {@link BaseMixin#resetFilterHandler resetFilterHandler}
2238       * @see {@link BaseMixin#filterHandler filterHandler}
2239       * @example
2240       * // filter by a single string
2241       * chart.filter('Sunday');
2242       * // filter by a single age
2243       * chart.filter(18);
2244       * // filter by a set of states
2245       * chart.filter([['MA', 'TX', 'ND', 'WA']]);
2246       * // filter by range -- note the use of filters.RangedFilter, which is different
2247       * // from the syntax for filtering a crossfilter dimension directly, dimension.filter([15,20])
2248       * chart.filter(filters.RangedFilter(15,20));
2249       * @param {*} [filter]
2250       * @returns {BaseMixin}
2251       */
2252      filter (filter) {
2253          if (!arguments.length) {
2254              return this._filters.length > 0 ? this._filters[0] : null;
2255          }
2256          let filters = this._filters;
2257          if (filter instanceof Array && filter[0] instanceof Array && !filter.isFiltered) {
2258              // toggle each filter
2259              filter[0].forEach(f => {
2260                  if (this._hasFilterHandler(filters, f)) {
2261                      filters = this._removeFilterHandler(filters, f);
2262                  } else {
2263                      filters = this._addFilterHandler(filters, f);
2264                  }
2265              });
2266          } else if (filter === null) {
2267              filters = this._resetFilterHandler(filters);
2268          } else {
2269              if (this._hasFilterHandler(filters, filter)) {
2270                  filters = this._removeFilterHandler(filters, filter);
2271              } else {
2272                  filters = this._addFilterHandler(filters, filter);
2273              }
2274          }
2275          this._filters = this.applyFilters(filters);
2276          this._invokeFilteredListener(filter);
2277
2278          if (this._root !== null && this.hasFilter()) {
2279              this.turnOnControls();
2280          } else {
2281              this.turnOffControls();
2282          }
2283
2284          return this;
2285      }
2286
2287      /**
2288       * Returns all current filters. This method does not perform defensive cloning of the internal
2289       * filter array before returning, therefore any modification of the returned array will effect the
2290       * chart's internal filter storage.
2291       * @returns {Array<*>}
2292       */
2293      filters () {
2294          return this._filters;
2295      }
2296
2297      highlightSelected (e) {
2298          d3Selection.select(e).classed(constants.SELECTED_CLASS, true);
2299          d3Selection.select(e).classed(constants.DESELECTED_CLASS, false);
2300      }
2301
2302      fadeDeselected (e) {
2303          d3Selection.select(e).classed(constants.SELECTED_CLASS, false);
2304          d3Selection.select(e).classed(constants.DESELECTED_CLASS, true);
2305      }
2306
2307      resetHighlight (e) {
2308          d3Selection.select(e).classed(constants.SELECTED_CLASS, false);
2309          d3Selection.select(e).classed(constants.DESELECTED_CLASS, false);
2310      }
2311
2312      /**
2313       * This function is passed to d3 as the onClick handler for each chart. The default behavior is to
2314       * filter on the clicked datum (passed to the callback) and redraw the chart group.
2315       *
2316       * This function can be replaced in order to change the click behavior (but first look at
2317       * @example
2318       * var oldHandler = chart.onClick;
2319       * chart.onClick = function(datum) {
2320       *   // use datum.
2321       * @param {*} datum
2322       * @return {undefined}
2323       */
2324      onClick (datum) {
2325          const filter = this.keyAccessor()(datum);
2326          events.trigger(() => {
2327              this.filter(filter);
2328              this.redrawGroup();
2329          });
2330      }
2331
2332      /**
2333       * Set or get the filter handler. The filter handler is a function that performs the filter action
2334       * on a specific dimension. Using a custom filter handler allows you to perform additional logic
2335       * before or after filtering.
2336       * @see {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#dimension_filter crossfilter.dimension.filter}
2337       * @example
2338       * // the default filter handler handles all possible cases for the charts in dc.js
2339       * // you can replace it with something more specialized for your own chart
2340       * chart.filterHandler(function (dimension, filters) {
2341       *     if (filters.length === 0) {
2342       *         // the empty case (no filtering)
2343       *         dimension.filter(null);
2344       *     } else if (filters.length === 1 && !filters[0].isFiltered) {
2345       *         // single value and not a function-based filter
2346       *         dimension.filterExact(filters[0]);
2347       *     } else if (filters.length === 1 && filters[0].filterType === 'RangedFilter') {
2348       *         // single range-based filter
2349       *         dimension.filterRange(filters[0]);
2350       *     } else {
2351       *         // an array of values, or an array of filter objects
2352       *         dimension.filterFunction(function (d) {
2353       *             for (var i = 0; i < filters.length; i++) {
2354       *                 var filter = filters[i];
2355       *                 if (filter.isFiltered && filter.isFiltered(d)) {
2356       *                     return true;
2357       *                 } else if (filter <= d && filter >= d) {
2358       *                     return true;
2359       *                 }
2360       *             }
2361       *             return false;
2362       *         });
2363       *     }
2364       *     return filters;
2365       * });
2366       *
2367       * // custom filter handler
2368       * chart.filterHandler(function(dimension, filter){
2369       *     var newFilter = filter + 10;
2370       *     dimension.filter(newFilter);
2371       *     return newFilter; // set the actual filter value to the new value
2372       * });
2373       * @param {Function} [filterHandler]
2374       * @returns {Function|BaseMixin}
2375       */
2376      filterHandler (filterHandler) {
2377          if (!arguments.length) {
2378              return this._filterHandler;
2379          }
2380          this._filterHandler = filterHandler;
2381          return this;
2382      }
2383
2384      // abstract function stub
2385      _doRender () {
2386          // do nothing in base, should be overridden by sub-function
2387          return this;
2388      }
2389
2390      _doRedraw () {
2391          // do nothing in base, should be overridden by sub-function
2392          return this;
2393      }
2394
2395      legendables () {
2396          // do nothing in base, should be overridden by sub-function
2397          return [];
2398      }
2399
2400      legendHighlight () {
2401          // do nothing in base, should be overridden by sub-function
2402      }
2403
2404      legendReset () {
2405          // do nothing in base, should be overridden by sub-function
2406      }
2407
2408      legendToggle () {
2409          // do nothing in base, should be overriden by sub-function
2410      }
2411
2412      isLegendableHidden () {
2413          // do nothing in base, should be overridden by sub-function
2414          return false;
2415      }
2416
2417      /**
2418       * Set or get the key accessor function. The key accessor function is used to retrieve the key
2419       * value from the crossfilter group. Key values are used differently in different charts, for
2420       * example keys correspond to slices in a pie chart and x axis positions in a grid coordinate chart.
2421       * @example
2422       * // default key accessor
2423       * chart.keyAccessor(function(d) { return d.key; });
2424       * // custom key accessor for a multi-value crossfilter reduction
2425       * chart.keyAccessor(function(p) { return p.value.absGain; });
2426       * @param {Function} [keyAccessor]
2427       * @returns {Function|BaseMixin}
2428       */
2429      keyAccessor (keyAccessor) {
2430          if (!arguments.length) {
2431              return this._keyAccessor;
2432          }
2433          this._keyAccessor = keyAccessor;
2434          return this;
2435      }
2436
2437      /**
2438       * Set or get the value accessor function. The value accessor function is used to retrieve the
2439       * value from the crossfilter group. Group values are used differently in different charts, for
2440       * example values correspond to slice sizes in a pie chart and y axis positions in a grid
2441       * coordinate chart.
2442       * @example
2443       * // default value accessor
2444       * chart.valueAccessor(function(d) { return d.value; });
2445       * // custom value accessor for a multi-value crossfilter reduction
2446       * chart.valueAccessor(function(p) { return p.value.percentageGain; });
2447       * @param {Function} [valueAccessor]
2448       * @returns {Function|BaseMixin}
2449       */
2450      valueAccessor (valueAccessor) {
2451          if (!arguments.length) {
2452              return this._valueAccessor;
2453          }
2454          this._valueAccessor = valueAccessor;
2455          return this;
2456      }
2457
2458      /**
2459       * Set or get the label function. The chart class will use this function to render labels for each
2460       * child element in the chart, e.g. slices in a pie chart or bubbles in a bubble chart. Not every
2461       * chart supports the label function, for example line chart does not use this function
2462       * at all. By default, enables labels; pass false for the second parameter if this is not desired.
2463       * @example
2464       * // default label function just return the key
2465       * chart.label(function(d) { return d.key; });
2466       * // label function has access to the standard d3 data binding and can get quite complicated
2467       * chart.label(function(d) { return d.data.key + '(' + Math.floor(d.data.value / all.value() * 100) + '%)'; });
2468       * @param {Function} [labelFunction]
2469       * @param {Boolean} [enableLabels=true]
2470       * @returns {Function|BaseMixin}
2471       */
2472      label (labelFunction, enableLabels) {
2473          if (!arguments.length) {
2474              return this._label;
2475          }
2476          this._label = labelFunction;
2477          if ((enableLabels === undefined) || enableLabels) {
2478              this._renderLabel = true;
2479          }
2480          return this;
2481      }
2482
2483      /**
2484       * Turn on/off label rendering
2485       * @param {Boolean} [renderLabel=false]
2486       * @returns {Boolean|BaseMixin}
2487       */
2488      renderLabel (renderLabel) {
2489          if (!arguments.length) {
2490              return this._renderLabel;
2491          }
2492          this._renderLabel = renderLabel;
2493          return this;
2494      }
2495
2496      /**
2497       * Set or get the title function. The chart class will use this function to render the SVGElement title
2498       * (usually interpreted by browser as tooltips) for each child element in the chart, e.g. a slice
2499       * in a pie chart or a bubble in a bubble chart. Almost every chart supports the title function;
2500       * however in grid coordinate charts you need to turn off the brush in order to see titles, because
2501       * otherwise the brush layer will block tooltip triggering.
2502       * @example
2503       * // default title function shows "key: value"
2504       * chart.title(function(d) { return d.key + ': ' + d.value; });
2505       * // title function has access to the standard d3 data binding and can get quite complicated
2506       * chart.title(function(p) {
2507       *    return p.key.getFullYear()
2508       *        + '\n'
2509       *        + 'Index Gain: ' + numberFormat(p.value.absGain) + '\n'
2510       *        + 'Index Gain in Percentage: ' + numberFormat(p.value.percentageGain) + '%\n'
2511       *        + 'Fluctuation / Index Ratio: ' + numberFormat(p.value.fluctuationPercentage) + '%';
2512       * });
2513       * @param {Function} [titleFunction]
2514       * @returns {Function|BaseMixin}
2515       */
2516      title (titleFunction) {
2517          if (!arguments.length) {
2518              return this._title;
2519          }
2520          this._title = titleFunction;
2521          return this;
2522      }
2523
2524      /**
2525       * Turn on/off title rendering, or return the state of the render title flag if no arguments are
2526       * given.
2527       * @param {Boolean} [renderTitle=true]
2528       * @returns {Boolean|BaseMixin}
2529       */
2530      renderTitle (renderTitle) {
2531          if (!arguments.length) {
2532              return this._renderTitle;
2533          }
2534          this._renderTitle = renderTitle;
2535          return this;
2536      }
2537
2538      /**
2539       * Get or set the chart group to which this chart belongs. Chart groups are rendered or redrawn
2540       * together since it is expected they share the same underlying crossfilter data set.
2541       * @param {String} [chartGroup]
2542       * @returns {String|BaseMixin}
2543       */
2544      chartGroup (chartGroup) {
2545          if (!arguments.length) {
2546              return this._chartGroup;
2547          }
2548          if (!this._isChild) {
2549              deregisterChart(this, this._chartGroup);
2550          }
2551          this._chartGroup = chartGroup;
2552          if (!this._isChild) {
2553              registerChart(this, this._chartGroup);
2554          }
2555          return this;
2556      }
2557
2558      /**
2559       * Expire the internal chart cache. dc charts cache some data internally on a per chart basis to
2560       * speed up rendering and avoid unnecessary calculation; however it might be useful to clear the
2561       * cache if you have changed state which will affect rendering.  For example, if you invoke
2562       * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#crossfilter_add crossfilter.add}
2563       * function or reset group or dimension after rendering, it is a good idea to
2564       * clear the cache to make sure charts are rendered properly.
2565       * @returns {BaseMixin}
2566       */
2567      expireCache () {
2568          // do nothing in base, should be overridden by sub-function
2569          return this;
2570      }
2571
2572      /**
2573       * Attach a Legend widget to this chart. The legend widget will automatically draw legend labels
2574       * based on the color setting and names associated with each group.
2575       * @example
2576       * chart.legend(new Legend().x(400).y(10).itemHeight(13).gap(5))
2577       * @param {Legend} [legend]
2578       * @returns {Legend|BaseMixin}
2579       */
2580      legend (legend) {
2581          if (!arguments.length) {
2582              return this._legend;
2583          }
2584          this._legend = legend;
2585          this._legend.parent(this);
2586          return this;
2587      }
2588
2589      /**
2590       * Returns the internal numeric ID of the chart.
2591       * @returns {String}
2592       */
2593      chartID () {
2594          return this.__dcFlag__;
2595      }
2596
2597      /**
2598       * Set chart options using a configuration object. Each key in the object will cause the method of
2599       * the same name to be called with the value to set that attribute for the chart.
2600       * @example
2601       * chart.options({dimension: myDimension, group: myGroup});
2602       * @param {{}} opts
2603       * @returns {BaseMixin}
2604       */
2605      options (opts) {
2606          const applyOptions = [
2607              'anchor',
2608              'group',
2609              'xAxisLabel',
2610              'yAxisLabel',
2611              'stack',
2612              'title',
2613              'point',
2614              'getColor',
2615              'overlayGeoJson'
2616          ];
2617
2618          for (const o in opts) {
2619              if (typeof (this[o]) === 'function') {
2620                  if (opts[o] instanceof Array && applyOptions.indexOf(o) !== -1) {
2621                      this[o].apply(this, opts[o]);
2622                  } else {
2623                      this[o].call(this, opts[o]);
2624                  }
2625              } else {
2626                  logger.debug(`Not a valid option setter name: ${o}`);
2627              }
2628          }
2629          return this;
2630      }
2631
2632      /**
2633       * All dc chart instance supports the following listeners.
2634       * Supports the following events:
2635       * * `renderlet` - This listener function will be invoked after transitions after redraw and render. Replaces the
2636       * deprecated {@link BaseMixin#renderlet renderlet} method.
2637       * * `pretransition` - Like `.on('renderlet', ...)` but the event is fired before transitions start.
2638       * * `preRender` - This listener function will be invoked before chart rendering.
2639       * * `postRender` - This listener function will be invoked after chart finish rendering including
2640       * all renderlets' logic.
2641       * * `preRedraw` - This listener function will be invoked before chart redrawing.
2642       * * `postRedraw` - This listener function will be invoked after chart finish redrawing
2643       * including all renderlets' logic.
2644       * * `filtered` - This listener function will be invoked after a filter is applied, added or removed.
2645       * * `zoomed` - This listener function will be invoked after a zoom is triggered.
2646       * @see {@link https://github.com/d3/d3-dispatch/blob/master/README.md#dispatch_on d3.dispatch.on}
2647       * @example
2648       * .on('renderlet', function(chart, filter){...})
2649       * .on('pretransition', function(chart, filter){...})
2650       * .on('preRender', function(chart){...})
2651       * .on('postRender', function(chart){...})
2652       * .on('preRedraw', function(chart){...})
2653       * .on('postRedraw', function(chart){...})
2654       * .on('filtered', function(chart, filter){...})
2655       * .on('zoomed', function(chart, filter){...})
2656       * @param {String} event
2657       * @param {Function} listener
2658       * @returns {BaseMixin}
2659       */
2660      on (event, listener) {
2661          this._listeners.on(event, listener);
2662          return this;
2663      }
2664
2665      /**
2666       * A renderlet is similar to an event listener on rendering event. Multiple renderlets can be added
2667       * to an individual chart.  Each time a chart is rerendered or redrawn the renderlets are invoked
2668       * right after the chart finishes its transitions, giving you a way to modify the SVGElements.
2669       * Renderlet functions take the chart instance as the only input parameter and you can
2670       * use the dc API or use raw d3 to achieve pretty much any effect.
2671       *
2672       * Use {@link BaseMixin#on on} with a 'renderlet' prefix.
2673       * Generates a random key for the renderlet, which makes it hard to remove.
2674       * @deprecated chart.renderlet has been deprecated. Please use chart.on("renderlet.<renderletKey>", renderletFunction)
2675       * @example
2676       * // do this instead of .renderlet(function(chart) { ... })
2677       * chart.on("renderlet", function(chart){
2678       *     // mix of dc API and d3 manipulation
2679       *     chart.select('g.y').style('display', 'none');
2680       *     // its a closure so you can also access other chart variable available in the closure scope
2681       *     moveChart.filter(chart.filter());
2682       * });
2683       * @param {Function} renderletFunction
2684       * @returns {BaseMixin}
2685       */
2686      renderlet (renderletFunction) {
2687          logger.warnOnce('chart.renderlet has been deprecated. Please use chart.on("renderlet.<renderletKey>", renderletFunction)');
2688          this.on(`renderlet.${utils.uniqueId()}`, renderletFunction);
2689          return this;
2690      }
2691  }
2692
2693  const baseMixin = () => new BaseMixin();
2694
2695  /**
2696   * The Color Mixin is an abstract chart functional class providing universal coloring support
2697   * as a mix-in for any concrete chart implementation.
2698   * @mixin ColorMixin
2699   * @param {Object} Base
2700   * @returns {ColorMixin}
2701   */
2702  const ColorMixin = Base => class extends Base {
2703      constructor () {
2704          super();
2705
2706          this._colors = d3Scale.scaleOrdinal(config.defaultColors());
2707
2708          this._colorAccessor = d => this.keyAccessor()(d);
2709          this._colorCalculator = undefined;
2710
2711          {
2712              const chart = this;
2713              // ES6: this method is called very differently from stack-mixin and derived charts
2714              // Removing and placing it as a member method is tricky
2715
2716              /**
2717                   * Get the color for the datum d and counter i. This is used internally by charts to retrieve a color.
2718                   * @method getColor
2719                   * @memberof ColorMixin
2720                   * @instance
2721                   * @param {*} d
2722                   * @param {Number} [i]
2723                   * @returns {String}
2724                   */
2725              chart.getColor = function (d, i) {
2726                  return chart._colorCalculator ?
2727                      chart._colorCalculator.call(this, d, i) :
2728                      chart._colors(chart._colorAccessor.call(this, d, i));
2729              };
2730          }
2731      }
2732
2733      /**
2734           * Set the domain by determining the min and max values as retrieved by
2735           * {@link ColorMixin#colorAccessor .colorAccessor} over the chart's dataset.
2736           * @memberof ColorMixin
2737           * @instance
2738           * @returns {ColorMixin}
2739           */
2740      calculateColorDomain () {
2741          const newDomain = [d3Array.min(this.data(), this.colorAccessor()),
2742                             d3Array.max(this.data(), this.colorAccessor())];
2743          this._colors.domain(newDomain);
2744          return this;
2745      }
2746
2747      /**
2748           * Retrieve current color scale or set a new color scale. This methods accepts any function that
2749           * operates like a d3 scale.
2750           * @memberof ColorMixin
2751           * @instance
2752           * @see {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale}
2753           * @example
2754           * // alternate categorical scale
2755           * chart.colors(d3.scale.category20b());
2756           * // ordinal scale
2757           * chart.colors(d3.scaleOrdinal().range(['red','green','blue']));
2758           * // convenience method, the same as above
2759           * chart.ordinalColors(['red','green','blue']);
2760           * // set a linear scale
2761           * chart.linearColors(["#4575b4", "#ffffbf", "#a50026"]);
2762           * @param {d3.scale} [colorScale=d3.scaleOrdinal(d3.schemeCategory20c)]
2763           * @returns {d3.scale|ColorMixin}
2764           */
2765      colors (colorScale) {
2766          if (!arguments.length) {
2767              return this._colors;
2768          }
2769          if (colorScale instanceof Array) {
2770              this._colors = d3Scale.scaleQuantize().range(colorScale); // deprecated legacy support, note: this fails for ordinal domains
2771          } else {
2772              this._colors = typeof colorScale === 'function' ? colorScale : utils.constant(colorScale);
2773          }
2774          return this;
2775      }
2776
2777      /**
2778           * Convenience method to set the color scale to
2779           * {@link https://github.com/d3/d3-scale/blob/master/README.md#ordinal-scales d3.scaleOrdinal} with
2780           * range `r`.
2781           * @memberof ColorMixin
2782           * @instance
2783           * @param {Array<String>} r
2784           * @returns {ColorMixin}
2785           */
2786      ordinalColors (r) {
2787          return this.colors(d3Scale.scaleOrdinal().range(r));
2788      }
2789
2790      /**
2791           * Convenience method to set the color scale to an Hcl interpolated linear scale with range `r`.
2792           * @memberof ColorMixin
2793           * @instance
2794           * @param {Array<Number>} r
2795           * @returns {ColorMixin}
2796           */
2797      linearColors (r) {
2798          return this.colors(d3Scale.scaleLinear()
2799                  .range(r)
2800                  .interpolate(d3Interpolate.interpolateHcl));
2801      }
2802
2803      /**
2804           * Set or the get color accessor function. This function will be used to map a data point in a
2805           * crossfilter group to a color value on the color scale. The default function uses the key
2806           * accessor.
2807           * @memberof ColorMixin
2808           * @instance
2809           * @example
2810           * // default index based color accessor
2811           * .colorAccessor(function (d, i){return i;})
2812           * // color accessor for a multi-value crossfilter reduction
2813           * .colorAccessor(function (d){return d.value.absGain;})
2814           * @param {Function} [colorAccessor]
2815           * @returns {Function|ColorMixin}
2816           */
2817      colorAccessor (colorAccessor) {
2818          if (!arguments.length) {
2819              return this._colorAccessor;
2820          }
2821          this._colorAccessor = colorAccessor;
2822          return this;
2823      }
2824
2825      /**
2826           * Set or get the current domain for the color mapping function. The domain must be supplied as an
2827           * array.
2828           *
2829           * Note: previously this method accepted a callback function. Instead you may use a custom scale
2830           * set by {@link ColorMixin#colors .colors}.
2831           * @memberof ColorMixin
2832           * @instance
2833           * @param {Array<String>} [domain]
2834           * @returns {Array<String>|ColorMixin}
2835           */
2836      colorDomain (domain) {
2837          if (!arguments.length) {
2838              return this._colors.domain();
2839          }
2840          this._colors.domain(domain);
2841          return this;
2842      }
2843
2844      /**
2845           * Overrides the color selection algorithm, replacing it with a simple function.
2846           *
2847           * Normally colors will be determined by calling the `colorAccessor` to get a value, and then passing that
2848           * value through the `colorScale`.
2849           *
2850           * But sometimes it is difficult to get a color scale to produce the desired effect. The `colorCalculator`
2851           * takes the datum and index and returns a color directly.
2852           * @memberof ColorMixin
2853           * @instance
2854           * @param {*} [colorCalculator]
2855           * @returns {Function|ColorMixin}
2856           */
2857      colorCalculator (colorCalculator) {
2858          if (!arguments.length) {
2859              return this._colorCalculator || this.getColor;
2860          }
2861          this._colorCalculator = colorCalculator;
2862          return this;
2863      }
2864  };
2865
2866  /**
2867   * This Mixin provides reusable functionalities for any chart that needs to visualize data using bubbles.
2868   * @mixin BubbleMixin
2869   * @mixes ColorMixin
2870   * @param {Object} Base
2871   * @returns {BubbleMixin}
2872   */
2873  const BubbleMixin = Base => class extends ColorMixin(Base) {
2874      constructor () {
2875          super();
2876
2877          this._maxBubbleRelativeSize = 0.3;
2878          this._minRadiusWithLabel = 10;
2879          this._sortBubbleSize = false;
2880          this._elasticRadius = false;
2881          this._excludeElasticZero = true;
2882
2883          // These cane be used by derived classes as well, so member status
2884          this.BUBBLE_NODE_CLASS = 'node';
2885          this.BUBBLE_CLASS = 'bubble';
2886          this.MIN_RADIUS = 10;
2887
2888          this.renderLabel(true);
2889
2890          this.data(group => {
2891              const data = group.all();
2892
2893              if (this._keyboardAccessible) {
2894                  // sort based on the x value (key)
2895                  data.sort((a, b) => d3Array.ascending(this.keyAccessor()(a), this.keyAccessor()(b)));
2896              }
2897
2898              if (this._sortBubbleSize) {
2899                  // sort descending so smaller bubbles are on top
2900                  const radiusAccessor = this.radiusValueAccessor();
2901                  data.sort((a, b) => d3Array.descending(radiusAccessor(a), radiusAccessor(b)));
2902              }
2903              return data;
2904          });
2905
2906          this._r = d3Scale.scaleLinear().domain([0, 100]);
2907      }
2908
2909      _rValueAccessor (d) {
2910          return d.r;
2911      }
2912
2913      /**
2914           * Get or set the bubble radius scale. By default the bubble chart uses
2915           * {@link https://github.com/d3/d3-scale/blob/master/README.md#scaleLinear d3.scaleLinear().domain([0, 100])}
2916           * as its radius scale.
2917           * @memberof BubbleMixin
2918           * @instance
2919           * @see {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale}
2920           * @param {d3.scale} [bubbleRadiusScale=d3.scaleLinear().domain([0, 100])]
2921           * @returns {d3.scale|BubbleMixin}
2922           */
2923      r (bubbleRadiusScale) {
2924          if (!arguments.length) {
2925              return this._r;
2926          }
2927          this._r = bubbleRadiusScale;
2928          return this;
2929      }
2930
2931      /**
2932           * Turn on or off the elastic bubble radius feature, or return the value of the flag. If this
2933           * feature is turned on, then bubble radii will be automatically rescaled to fit the chart better.
2934           * @memberof BubbleMixin
2935           * @instance
2936           * @param {Boolean} [elasticRadius=false]
2937           * @returns {Boolean|BubbleChart}
2938           */
2939      elasticRadius (elasticRadius) {
2940          if (!arguments.length) {
2941              return this._elasticRadius;
2942          }
2943          this._elasticRadius = elasticRadius;
2944          return this;
2945      }
2946
2947      calculateRadiusDomain () {
2948          if (this._elasticRadius) {
2949              this.r().domain([this.rMin(), this.rMax()]);
2950          }
2951      }
2952
2953      /**
2954           * Get or set the radius value accessor function. If set, the radius value accessor function will
2955           * be used to retrieve a data value for each bubble. The data retrieved then will be mapped using
2956           * the r scale to the actual bubble radius. This allows you to encode a data dimension using bubble
2957           * size.
2958           * @memberof BubbleMixin
2959           * @instance
2960           * @param {Function} [radiusValueAccessor]
2961           * @returns {Function|BubbleMixin}
2962           */
2963      radiusValueAccessor (radiusValueAccessor) {
2964          if (!arguments.length) {
2965              return this._rValueAccessor;
2966          }
2967          this._rValueAccessor = radiusValueAccessor;
2968          return this;
2969      }
2970
2971      rMin () {
2972          let values = this.data().map(this.radiusValueAccessor());
2973          if(this._excludeElasticZero) {
2974              values = values.filter(value => value > 0);
2975          }
2976          return d3Array.min(values);
2977      }
2978
2979      rMax () {
2980          return d3Array.max(this.data(), e => this.radiusValueAccessor()(e));
2981      }
2982
2983      bubbleR (d) {
2984          const value = this.radiusValueAccessor()(d);
2985          let r = this.r()(value);
2986          if (isNaN(r) || value <= 0) {
2987              r = 0;
2988          }
2989          return r;
2990      }
2991
2992      _labelFunction (d) {
2993          return this.label()(d);
2994      }
2995
2996      _shouldLabel (d) {
2997          return (this.bubbleR(d) > this._minRadiusWithLabel);
2998      }
2999
3000      _labelOpacity (d) {
3001          return this._shouldLabel(d) ? 1 : 0;
3002      }
3003
3004      _labelPointerEvent (d) {
3005          return this._shouldLabel(d) ? 'all' : 'none';
3006      }
3007
3008      _doRenderLabel (bubbleGEnter) {
3009          if (this.renderLabel()) {
3010              let label = bubbleGEnter.select('text');
3011
3012              if (label.empty()) {
3013                  label = bubbleGEnter.append('text')
3014                          .attr('text-anchor', 'middle')
3015                          .attr('dy', '.3em')
3016                          .on('click', d3compat.eventHandler(d => this.onClick(d)));
3017              }
3018
3019              label
3020                      .attr('opacity', 0)
3021                      .attr('pointer-events', d => this._labelPointerEvent(d))
3022                      .text(d => this._labelFunction(d));
3023              transition(label, this.transitionDuration(), this.transitionDelay())
3024                      .attr('opacity', d => this._labelOpacity(d));
3025          }
3026      }
3027
3028      doUpdateLabels (bubbleGEnter) {
3029          if (this.renderLabel()) {
3030              const labels = bubbleGEnter.select('text')
3031                      .attr('pointer-events', d => this._labelPointerEvent(d))
3032                      .text(d => this._labelFunction(d));
3033              transition(labels, this.transitionDuration(), this.transitionDelay())
3034                      .attr('opacity', d => this._labelOpacity(d));
3035          }
3036      }
3037
3038      _titleFunction (d) {
3039          return this.title()(d);
3040      }
3041
3042      _doRenderTitles (g) {
3043          if (this.renderTitle()) {
3044              const title = g.select('title');
3045
3046              if (title.empty()) {
3047                  g.append('title').text(d => this._titleFunction(d));
3048              }
3049          }
3050      }
3051
3052      doUpdateTitles (g) {
3053          if (this.renderTitle()) {
3054              g.select('title').text(d => this._titleFunction(d));
3055          }
3056      }
3057
3058      /**
3059           * Turn on or off the bubble sorting feature, or return the value of the flag. If enabled,
3060           * bubbles will be sorted by their radius, with smaller bubbles in front.
3061           * @memberof BubbleChart
3062           * @instance
3063           * @param {Boolean} [sortBubbleSize=false]
3064           * @returns {Boolean|BubbleChart}
3065           */
3066      sortBubbleSize (sortBubbleSize) {
3067          if (!arguments.length) {
3068              return this._sortBubbleSize;
3069          }
3070          this._sortBubbleSize = sortBubbleSize;
3071          return this;
3072      }
3073
3074      /**
3075           * Get or set the minimum radius. This will be used to initialize the radius scale's range.
3076           * @memberof BubbleMixin
3077           * @instance
3078           * @param {Number} [radius=10]
3079           * @returns {Number|BubbleMixin}
3080           */
3081      minRadius (radius) {
3082          if (!arguments.length) {
3083              return this.MIN_RADIUS;
3084          }
3085          this.MIN_RADIUS = radius;
3086          return this;
3087      }
3088
3089      /**
3090           * Get or set the minimum radius for label rendering. If a bubble's radius is less than this value
3091           * then no label will be rendered.
3092           * @memberof BubbleMixin
3093           * @instance
3094           * @param {Number} [radius=10]
3095           * @returns {Number|BubbleMixin}
3096           */
3097
3098      minRadiusWithLabel (radius) {
3099          if (!arguments.length) {
3100              return this._minRadiusWithLabel;
3101          }
3102          this._minRadiusWithLabel = radius;
3103          return this;
3104      }
3105
3106      /**
3107           * Get or set the maximum relative size of a bubble to the length of x axis. This value is useful
3108           * when the difference in radius between bubbles is too great.
3109           * @memberof BubbleMixin
3110           * @instance
3111           * @param {Number} [relativeSize=0.3]
3112           * @returns {Number|BubbleMixin}
3113           */
3114      maxBubbleRelativeSize (relativeSize) {
3115          if (!arguments.length) {
3116              return this._maxBubbleRelativeSize;
3117          }
3118          this._maxBubbleRelativeSize = relativeSize;
3119          return this;
3120      }
3121
3122      /**
3123       * Should the chart exclude zero when calculating elastic bubble radius?
3124       * @memberof BubbleMixin
3125       * @instance
3126       * @param  {Boolean} [excludeZero=true]
3127       * @returns {Boolean|BubbleMixin}
3128       */
3129      excludeElasticZero (excludeZero) {
3130          if (!arguments.length) {
3131              return this._excludeElasticZero;
3132          }
3133          this._excludeElasticZero = excludeZero;
3134          return this;
3135      }
3136
3137      fadeDeselectedArea (selection) {
3138          if (this.hasFilter()) {
3139              const chart = this;
3140              this.selectAll(`g.${chart.BUBBLE_NODE_CLASS}`).each(function (d) {
3141                  if (chart.isSelectedNode(d)) {
3142                      chart.highlightSelected(this);
3143                  } else {
3144                      chart.fadeDeselected(this);
3145                  }
3146              });
3147          } else {
3148              const chart = this;
3149              this.selectAll(`g.${chart.BUBBLE_NODE_CLASS}`).each(function () {
3150                  chart.resetHighlight(this);
3151              });
3152          }
3153      }
3154
3155      isSelectedNode (d) {
3156          return this.hasFilter(d.key);
3157      }
3158
3159      onClick (d) {
3160          const filter = d.key;
3161          events.trigger(() => {
3162              this.filter(filter);
3163              this.redrawGroup();
3164          });
3165      }
3166  };
3167
3168  /**
3169   * Cap is a mixin that groups small data elements below a _cap_ into an *others* grouping for both the
3170   * Row and Pie Charts.
3171   *
3172   * The top ordered elements in the group up to the cap amount will be kept in the chart, and the rest
3173   * will be replaced with an *others* element, with value equal to the sum of the replaced values. The
3174   * keys of the elements below the cap limit are recorded in order to filter by those keys when the
3175   * others* element is clicked.
3176   * @mixin CapMixin
3177   * @param {Object} Base
3178   * @returns {CapMixin}
3179   */
3180  const CapMixin = Base => class extends Base {
3181      constructor () {
3182          super();
3183
3184          this._cap = Infinity;
3185          this._takeFront = true;
3186          this._othersLabel = 'Others';
3187
3188          this._othersGrouper = (topItems, restItems) => {
3189              const restItemsSum = d3Array.sum(restItems, this.valueAccessor()),
3190                  restKeys = restItems.map(this.keyAccessor());
3191              if (restItemsSum > 0) {
3192                  return topItems.concat([{
3193                      others: restKeys,
3194                      key: this.othersLabel(),
3195                      value: restItemsSum
3196                  }]);
3197              }
3198              return topItems;
3199          };
3200
3201          // emulate old group.top(N) ordering
3202          this.ordering(kv => -kv.value);
3203
3204          // return N "top" groups, where N is the cap, sorted by baseMixin.ordering
3205          // whether top means front or back depends on takeFront
3206          this.data(group => {
3207              if (this._cap === Infinity) {
3208                  return this._computeOrderedGroups(group.all());
3209              } else {
3210                  let items = group.all(), rest;
3211                  items = this._computeOrderedGroups(items); // sort by baseMixin.ordering
3212
3213                  if (this._cap) {
3214                      if (this._takeFront) {
3215                          rest = items.slice(this._cap);
3216                          items = items.slice(0, this._cap);
3217                      } else {
3218                          const start = Math.max(0, items.length - this._cap);
3219                          rest = items.slice(0, start);
3220                          items = items.slice(start);
3221                      }
3222                  }
3223
3224                  if (this._othersGrouper) {
3225                      return this._othersGrouper(items, rest);
3226                  }
3227                  return items;
3228              }
3229          });
3230      }
3231
3232      cappedKeyAccessor (d, i) {
3233          if (d.others) {
3234              return d.key;
3235          }
3236          return this.keyAccessor()(d, i);
3237      }
3238
3239      cappedValueAccessor (d, i) {
3240          if (d.others) {
3241              return d.value;
3242          }
3243          return this.valueAccessor()(d, i);
3244      }
3245
3246      /**
3247           * Get or set the count of elements to that will be included in the cap. If there is an
3248           * {@link CapMixin#othersGrouper othersGrouper}, any further elements will be combined in an
3249           * extra element with its name determined by {@link CapMixin#othersLabel othersLabel}.
3250           *
3251           * As of dc.js 2.1 and onward, the capped charts use
3252           * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_all group.all()}
3253           * and {@link BaseMixin#ordering BaseMixin.ordering()} to determine the order of
3254           * elements. Then `cap` and {@link CapMixin#takeFront takeFront} determine how many elements
3255           * to keep, from which end of the resulting array.
3256           *
3257           * **Migration note:** Up through dc.js 2.0.*, capping used
3258           * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_top group.top(N)},
3259           * which selects the largest items according to
3260           * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_order group.order()}.
3261           * The chart then sorted the items according to {@link BaseMixin#ordering baseMixin.ordering()}.
3262           * So the two values essentially had to agree, but if the `group.order()` was incorrect (it's
3263           * easy to forget about), the wrong rows or slices would be displayed, in the correct order.
3264           *
3265           * If your chart previously relied on `group.order()`, use `chart.ordering()` instead. As of
3266           * 2.1.5, the ordering defaults to sorting from greatest to least like `group.top(N)` did.
3267           *
3268           * If you want to cap by one ordering but sort by another, you can still do this by
3269           * specifying your own {@link BaseMixin#data `.data()`} callback. For details, see the example
3270           * {@link https://dc-js.github.io/dc.js/examples/cap-and-sort-differently.html Cap and Sort Differently}.
3271           * @memberof CapMixin
3272           * @instance
3273           * @param {Number} [count=Infinity]
3274           * @returns {Number|CapMixin}
3275           */
3276      cap (count) {
3277          if (!arguments.length) {
3278              return this._cap;
3279          }
3280          this._cap = count;
3281          return this;
3282      }
3283
3284      /**
3285           * Get or set the direction of capping. If set, the chart takes the first
3286           * {@link CapMixin#cap cap} elements from the sorted array of elements; otherwise
3287           * it takes the last `cap` elements.
3288           * @memberof CapMixin
3289           * @instance
3290           * @param {Boolean} [takeFront=true]
3291           * @returns {Boolean|CapMixin}
3292           */
3293      takeFront (takeFront) {
3294          if (!arguments.length) {
3295              return this._takeFront;
3296          }
3297          this._takeFront = takeFront;
3298          return this;
3299      }
3300
3301      /**
3302           * Get or set the label for *Others* slice when slices cap is specified.
3303           * @memberof CapMixin
3304           * @instance
3305           * @param {String} [label="Others"]
3306           * @returns {String|CapMixin}
3307           */
3308      othersLabel (label) {
3309          if (!arguments.length) {
3310              return this._othersLabel;
3311          }
3312          this._othersLabel = label;
3313          return this;
3314      }
3315
3316      /**
3317           * Get or set the grouper function that will perform the insertion of data for the *Others* slice
3318           * if the slices cap is specified. If set to a falsy value, no others will be added.
3319           *
3320           * The grouper function takes an array of included ("top") items, and an array of the rest of
3321           * the items. By default the grouper function computes the sum of the rest.
3322           * @memberof CapMixin
3323           * @instance
3324           * @example
3325           * // Do not show others
3326           * chart.othersGrouper(null);
3327           * // Default others grouper
3328           * chart.othersGrouper(function (topItems, restItems) {
3329           *     var restItemsSum = d3.sum(restItems, _chart.valueAccessor()),
3330           *         restKeys = restItems.map(_chart.keyAccessor());
3331           *     if (restItemsSum > 0) {
3332           *         return topItems.concat([{
3333           *             others: restKeys,
3334           *             key: _chart.othersLabel(),
3335           *             value: restItemsSum
3336           *         }]);
3337           *     }
3338           *     return topItems;
3339           * });
3340           * @param {Function} [grouperFunction]
3341           * @returns {Function|CapMixin}
3342           */
3343      othersGrouper (grouperFunction) {
3344          if (!arguments.length) {
3345              return this._othersGrouper;
3346          }
3347          this._othersGrouper = grouperFunction;
3348          return this;
3349      }
3350
3351      onClick (d) {
3352          if (d.others) {
3353              this.filter([d.others]);
3354          }
3355          super.onClick(d);
3356      }
3357  };
3358
3359  /**
3360   * Margin is a mixin that provides margin utility functions for both the Row Chart and Coordinate Grid
3361   * Charts.
3362   * @mixin MarginMixin
3363   * @param {Object} Base
3364   * @returns {MarginMixin}
3365   */
3366  class MarginMixin extends BaseMixin {
3367      constructor () {
3368          super();
3369
3370          this._margin = {top: 10, right: 50, bottom: 30, left: 30};
3371      }
3372
3373      /**
3374       * Get or set the margins for a particular coordinate grid chart instance. The margins is stored as
3375       * an associative Javascript array.
3376       * @memberof MarginMixin
3377       * @instance
3378       * @example
3379       * var leftMargin = chart.margins().left; // 30 by default
3380       * chart.margins().left = 50;
3381       * leftMargin = chart.margins().left; // now 50
3382       * @param {{top: Number, right: Number, left: Number, bottom: Number}} [margins={top: 10, right: 50, bottom: 30, left: 30}]
3383       * @returns {{top: Number, right: Number, left: Number, bottom: Number}|MarginMixin}
3384       */
3385      margins (margins) {
3386          if (!arguments.length) {
3387              return this._margin;
3388          }
3389          this._margin = margins;
3390          return this;
3391      }
3392
3393      /**
3394       * Effective width of the chart excluding margins (in pixels).
3395       *
3396       * @returns {number}
3397       */
3398      effectiveWidth () {
3399          return this.width() - this.margins().left - this.margins().right;
3400      }
3401
3402      /**
3403       * Effective height of the chart excluding margins (in pixels).
3404       *
3405       * @returns {number}
3406       */
3407      effectiveHeight () {
3408          return this.height() - this.margins().top - this.margins().bottom;
3409      }
3410  }
3411
3412  const GRID_LINE_CLASS = 'grid-line';
3413  const HORIZONTAL_CLASS = 'horizontal';
3414  const VERTICAL_CLASS = 'vertical';
3415  const Y_AXIS_LABEL_CLASS = 'y-axis-label';
3416  const X_AXIS_LABEL_CLASS = 'x-axis-label';
3417  const CUSTOM_BRUSH_HANDLE_CLASS = 'custom-brush-handle';
3418  const DEFAULT_AXIS_LABEL_PADDING = 12;
3419
3420  /**
3421   * Coordinate Grid is an abstract base chart designed to support a number of coordinate grid based
3422   * concrete chart types, e.g. bar chart, line chart, and bubble chart.
3423   * @mixin CoordinateGridMixin
3424   * @mixes ColorMixin
3425   * @mixes MarginMixin
3426   */
3427  class CoordinateGridMixin extends ColorMixin(MarginMixin) {
3428      constructor () {
3429          super();
3430
3431          this.colors(d3Scale.scaleOrdinal(d3ScaleChromatic.schemeCategory10));
3432          this._mandatoryAttributes().push('x');
3433          this._parent = undefined;
3434          this._g = undefined;
3435          this._chartBodyG = undefined;
3436
3437          this._x = undefined;
3438          this._origX = undefined; // Will hold original scale in case of zoom
3439          this._xOriginalDomain = undefined;
3440          this._xAxis = null;
3441          this._xUnits = units.integers;
3442          this._xAxisPadding = 0;
3443          this._xAxisPaddingUnit = d3Time.timeDay;
3444          this._xElasticity = false;
3445          this._xAxisLabel = undefined;
3446          this._xAxisLabelPadding = 0;
3447          this._lastXDomain = undefined;
3448
3449          this._y = undefined;
3450          this._yAxis = null;
3451          this._yAxisPadding = 0;
3452          this._yElasticity = false;
3453          this._yAxisLabel = undefined;
3454          this._yAxisLabelPadding = 0;
3455
3456          this._brush = d3Brush.brushX();
3457
3458          this._gBrush = undefined;
3459          this._brushOn = true;
3460          this._parentBrushOn = false;
3461          this._round = undefined;
3462          this._ignoreBrushEvents = false; // ignore when carrying out programmatic brush operations
3463
3464          this._renderHorizontalGridLine = false;
3465          this._renderVerticalGridLine = false;
3466
3467          this._resizing = false;
3468          this._unitCount = undefined;
3469
3470          this._zoomScale = [1, Infinity];
3471          this._zoomOutRestrict = true;
3472
3473          this._zoom = d3Zoom.zoom().on('zoom', d3compat.eventHandler((d, evt) => this._onZoom(evt)));
3474          this._nullZoom = d3Zoom.zoom().on('zoom', null);
3475          this._hasBeenMouseZoomable = false;
3476          this._ignoreZoomEvents = false; // ignore when carrying out programmatic zoom operations
3477
3478          this._rangeChart = undefined;
3479          this._focusChart = undefined;
3480
3481          this._mouseZoomable = false;
3482          this._clipPadding = 0;
3483
3484          this._fOuterRangeBandPadding = 0.5;
3485          this._fRangeBandPadding = 0;
3486
3487          this._useRightYAxis = false;
3488          this._useTopXAxis = false;
3489      }
3490
3491      /**
3492       * When changing the domain of the x or y scale, it is necessary to tell the chart to recalculate
3493       * and redraw the axes. (`.rescale()` is called automatically when the x or y scale is replaced
3494       * with {@link CoordinateGridMixin+x .x()} or {@link CoordinateGridMixin#y .y()}, and has
3495       * no effect on elastic scales.)
3496       * @returns {CoordinateGridMixin}
3497       */
3498      rescale () {
3499          this._unitCount = undefined;
3500          this._resizing = true;
3501          return this;
3502      }
3503
3504      resizing (resizing) {
3505          if (!arguments.length) {
3506              return this._resizing;
3507          }
3508          this._resizing = resizing;
3509          return this;
3510      }
3511
3512      /**
3513       * Get or set the range selection chart associated with this instance. Setting the range selection
3514       * chart using this function will automatically update its selection brush when the current chart
3515       * zooms in. In return the given range chart will also automatically attach this chart as its focus
3516       * chart hence zoom in when range brush updates.
3517       *
3518       * Usually the range and focus charts will share a dimension. The range chart will set the zoom
3519       * boundaries for the focus chart, so its dimension values must be compatible with the domain of
3520       * the focus chart.
3521       *
3522       * See the [Nasdaq 100 Index](http://dc-js.github.com/dc.js/) example for this effect in action.
3523       * @param {CoordinateGridMixin} [rangeChart]
3524       * @returns {CoordinateGridMixin}
3525       */
3526      rangeChart (rangeChart) {
3527          if (!arguments.length) {
3528              return this._rangeChart;
3529          }
3530          this._rangeChart = rangeChart;
3531          this._rangeChart.focusChart(this);
3532          return this;
3533      }
3534
3535      /**
3536       * Get or set the scale extent for mouse zooms.
3537       * @param {Array<Number|Date>} [extent=[1, Infinity]]
3538       * @returns {Array<Number|Date>|CoordinateGridMixin}
3539       */
3540      zoomScale (extent) {
3541          if (!arguments.length) {
3542              return this._zoomScale;
3543          }
3544          this._zoomScale = extent;
3545          return this;
3546      }
3547
3548      /**
3549       * Get or set the zoom restriction for the chart. If true limits the zoom to origional domain of the chart.
3550       * @param {Boolean} [zoomOutRestrict=true]
3551       * @returns {Boolean|CoordinateGridMixin}
3552       */
3553      zoomOutRestrict (zoomOutRestrict) {
3554          if (!arguments.length) {
3555              return this._zoomOutRestrict;
3556          }
3557          this._zoomOutRestrict = zoomOutRestrict;
3558          return this;
3559      }
3560
3561      _generateG (parent) {
3562          if (parent === undefined) {
3563              this._parent = this.svg();
3564          } else {
3565              this._parent = parent;
3566          }
3567
3568          const href = window.location.href.split('#')[0];
3569
3570          this._g = this._parent.append('g');
3571
3572          this._chartBodyG = this._g.append('g').attr('class', 'chart-body')
3573              .attr('transform', `translate(${this.margins().left}, ${this.margins().top})`)
3574              .attr('clip-path', `url(${href}#${this._getClipPathId()})`);
3575
3576          return this._g;
3577      }
3578
3579      /**
3580       * Get or set the root g element. This method is usually used to retrieve the g element in order to
3581       * overlay custom svg drawing programatically. **Caution**: The root g element is usually generated
3582       * by dc.js internals, and resetting it might produce unpredictable result.
3583       * @param {SVGElement} [gElement]
3584       * @returns {SVGElement|CoordinateGridMixin}
3585       */
3586      g (gElement) {
3587          if (!arguments.length) {
3588              return this._g;
3589          }
3590          this._g = gElement;
3591          return this;
3592      }
3593
3594      /**
3595       * Set or get mouse zoom capability flag (default: false). When turned on the chart will be
3596       * zoomable using the mouse wheel. If the range selector chart is attached zooming will also update
3597       * the range selection brush on the associated range selector chart.
3598       * @param {Boolean} [mouseZoomable=false]
3599       * @returns {Boolean|CoordinateGridMixin}
3600       */
3601      mouseZoomable (mouseZoomable) {
3602          if (!arguments.length) {
3603              return this._mouseZoomable;
3604          }
3605          this._mouseZoomable = mouseZoomable;
3606          return this;
3607      }
3608
3609      /**
3610       * Retrieve the svg group for the chart body.
3611       * @param {SVGElement} [chartBodyG]
3612       * @returns {SVGElement}
3613       */
3614      chartBodyG (chartBodyG) {
3615          if (!arguments.length) {
3616              return this._chartBodyG;
3617          }
3618          this._chartBodyG = chartBodyG;
3619          return this;
3620      }
3621
3622      /**
3623       * **mandatory**
3624       *
3625       * Get or set the x scale. The x scale can be any d3
3626       * {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale} or
3627       * {@link https://github.com/d3/d3-scale/blob/master/README.md#ordinal-scales ordinal scale}
3628       * @see {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale}
3629       * @example
3630       * // set x to a linear scale
3631       * chart.x(d3.scaleLinear().domain([-2500, 2500]))
3632       * // set x to a time scale to generate histogram
3633       * chart.x(d3.scaleTime().domain([new Date(1985, 0, 1), new Date(2012, 11, 31)]))
3634       * @param {d3.scale} [xScale]
3635       * @returns {d3.scale|CoordinateGridMixin}
3636       */
3637      x (xScale) {
3638          if (!arguments.length) {
3639              return this._x;
3640          }
3641          this._x = xScale;
3642          this._xOriginalDomain = this._x.domain();
3643          this.rescale();
3644          return this;
3645      }
3646
3647      xOriginalDomain () {
3648          return this._xOriginalDomain;
3649      }
3650
3651      /**
3652       * Set or get the xUnits function. The coordinate grid chart uses the xUnits function to calculate
3653       * the number of data projections on the x axis such as the number of bars for a bar chart or the
3654       * number of dots for a line chart.
3655       *
3656       * This function is expected to return a Javascript array of all data points on the x axis, or
3657       * the number of points on the axis. d3 time range functions [d3.timeDays, d3.timeMonths, and
3658       * d3.timeYears](https://github.com/d3/d3-time/blob/master/README.md#intervals) are all valid
3659       * xUnits functions.
3660       *
3661       * dc.js also provides a few units function, see the {@link units Units Namespace} for
3662       * a list of built-in units functions.
3663       *
3664       * Note that as of dc.js 3.0, `units.ordinal` is not a real function, because it is not
3665       * possible to define this function compliant with the d3 range functions. It was already a
3666       * magic value which caused charts to behave differently, and now it is completely so.
3667       * @example
3668       * // set x units to count days
3669       * chart.xUnits(d3.timeDays);
3670       * // set x units to count months
3671       * chart.xUnits(d3.timeMonths);
3672       *
3673       * // A custom xUnits function can be used as long as it follows the following interface:
3674       * // units in integer
3675       * function(start, end) {
3676       *      // simply calculates how many integers in the domain
3677       *      return Math.abs(end - start);
3678       * }
3679       *
3680       * // fixed units
3681       * function(start, end) {
3682       *      // be aware using fixed units will disable the focus/zoom ability on the chart
3683       *      return 1000;
3684       * }
3685       * @param {Function} [xUnits=units.integers]
3686       * @returns {Function|CoordinateGridMixin}
3687       */
3688      xUnits (xUnits) {
3689          if (!arguments.length) {
3690              return this._xUnits;
3691          }
3692          this._xUnits = xUnits;
3693          return this;
3694      }
3695
3696      /**
3697       * Set or get the x axis used by a particular coordinate grid chart instance. This function is most
3698       * useful when x axis customization is required. The x axis in dc.js is an instance of a
3699       * {@link https://github.com/d3/d3-axis/blob/master/README.md#axisBottom d3 bottom axis object};
3700       * therefore it supports any valid d3 axisBottom manipulation.
3701       *
3702       * **Caution**: The x axis is usually generated internally by dc; resetting it may cause
3703       * unexpected results. Note also that when used as a getter, this function is not chainable:
3704       * it returns the axis, not the chart,
3705       * {@link https://github.com/dc-js/dc.js/wiki/FAQ#why-does-everything-break-after-a-call-to-xaxis-or-yaxis
3706           * so attempting to call chart functions after calling `.xAxis()` will fail}.
3707       * @see {@link https://github.com/d3/d3-axis/blob/master/README.md#axisBottom d3.axisBottom}
3708       * @example
3709       * // customize x axis tick format
3710       * chart.xAxis().tickFormat(function(v) {return v + '%';});
3711       * // customize x axis tick values
3712       * chart.xAxis().tickValues([0, 100, 200, 300]);
3713       * @param {d3.axis} [xAxis=d3.axisBottom()]
3714       * @returns {d3.axis|CoordinateGridMixin}
3715       */
3716      xAxis (xAxis) {
3717          if (!arguments.length) {
3718              if (!this._xAxis) {
3719                  this._xAxis = this._createXAxis();
3720              }
3721              return this._xAxis;
3722          }
3723          this._xAxis = xAxis;
3724          return this;
3725      }
3726
3727      /**
3728       * Turn on/off elastic x axis behavior. If x axis elasticity is turned on, then the grid chart will
3729       * attempt to recalculate the x axis range whenever a redraw event is triggered.
3730       * @param {Boolean} [elasticX=false]
3731       * @returns {Boolean|CoordinateGridMixin}
3732       */
3733      elasticX (elasticX) {
3734          if (!arguments.length) {
3735              return this._xElasticity;
3736          }
3737          this._xElasticity = elasticX;
3738          return this;
3739      }
3740
3741      /**
3742       * Set or get x axis padding for the elastic x axis. The padding will be added to both end of the x
3743       * axis if elasticX is turned on; otherwise it is ignored.
3744       *
3745       * Padding can be an integer or percentage in string (e.g. '10%'). Padding can be applied to
3746       * number or date x axes.  When padding a date axis, an integer represents number of units being padded
3747       * and a percentage string will be treated the same as an integer. The unit will be determined by the
3748       * xAxisPaddingUnit variable.
3749       * @param {Number|String} [padding=0]
3750       * @returns {Number|String|CoordinateGridMixin}
3751       */
3752      xAxisPadding (padding) {
3753          if (!arguments.length) {
3754              return this._xAxisPadding;
3755          }
3756          this._xAxisPadding = padding;
3757          return this;
3758      }
3759
3760      /**
3761       * Set or get x axis padding unit for the elastic x axis. The padding unit will determine which unit to
3762       * use when applying xAxis padding if elasticX is turned on and if x-axis uses a time dimension;
3763       * otherwise it is ignored.
3764       *
3765       * The padding unit should be a
3766       * [d3 time interval](https://github.com/d3/d3-time/blob/master/README.md#self._interval).
3767       * For backward compatibility with dc.js 2.0, it can also be the name of a d3 time interval
3768       * ('day', 'hour', etc). Available arguments are the
3769       * [d3 time intervals](https://github.com/d3/d3-time/blob/master/README.md#intervals d3.timeInterval).
3770       * @param {String} [unit=d3.timeDay]
3771       * @returns {String|CoordinateGridMixin}
3772       */
3773      xAxisPaddingUnit (unit) {
3774          if (!arguments.length) {
3775              return this._xAxisPaddingUnit;
3776          }
3777          this._xAxisPaddingUnit = unit;
3778          return this;
3779      }
3780
3781      /**
3782       * Returns the number of units displayed on the x axis. If the x axis is ordinal (`xUnits` is
3783       * `units.ordinal`), this is the number of items in the domain of the x scale. Otherwise, the
3784       * x unit count is calculated using the {@link CoordinateGridMixin#xUnits xUnits} function.
3785       * @returns {Number}
3786       */
3787      xUnitCount () {
3788          if (this._unitCount === undefined) {
3789              if (this.isOrdinal()) {
3790                  // In this case it number of items in domain
3791                  this._unitCount = this.x().domain().length;
3792              } else {
3793                  this._unitCount = this.xUnits()(this.x().domain()[0], this.x().domain()[1]);
3794
3795                  // Sometimes xUnits() may return an array while sometimes directly the count
3796                  if (this._unitCount instanceof Array) {
3797                      this._unitCount = this._unitCount.length;
3798                  }
3799              }
3800          }
3801
3802          return this._unitCount;
3803      }
3804
3805      /**
3806       * Gets or sets whether the chart should be drawn with a right axis instead of a left axis. When
3807       * used with a chart in a composite chart, allows both left and right Y axes to be shown on a
3808       * chart.
3809       * @param {Boolean} [useRightYAxis=false]
3810       * @returns {Boolean|CoordinateGridMixin}
3811       */
3812      useRightYAxis (useRightYAxis) {
3813          if (!arguments.length) {
3814              return this._useRightYAxis;
3815          }
3816
3817          // We need to warn if value is changing after self._yAxis was created
3818          if (this._useRightYAxis !== useRightYAxis && this._yAxis) {
3819              logger.warn('Value of useRightYAxis has been altered, after yAxis was created. ' +
3820                  'You might get unexpected yAxis behavior. ' +
3821                  'Make calls to useRightYAxis sooner in your chart creation process.');
3822          }
3823
3824          this._useRightYAxis = useRightYAxis;
3825          return this;
3826      }
3827
3828      /**
3829       * Gets or sets whether the chart should be drawn with a top axis instead of a bottom axis. When
3830       * used with a chart in a composite chart, allows both top and bottom X axes to be shown on a
3831       * chart.
3832       * @param {Boolean} [useTopXAxis=false]
3833       * @returns {Boolean|CoordinateGridMixin}
3834       */
3835      useTopXAxis (useTopXAxis) {
3836          if (!arguments.length) {
3837              return this._useTopXAxis;
3838          }
3839
3840          // We need to warn if value is changing after self._yAxis was created
3841          if (this._useTopXAxis !== useTopXAxis && this._xAxis) {
3842              logger.warn('Value of useTopXAxis has been altered, after xAxis was created. ' +
3843                  'You might get unexpected yAxis behavior. ' +
3844                  'Make calls to useTopXAxis sooner in your chart creation process.');
3845          }
3846
3847          this._useTopXAxis = useTopXAxis;
3848          return this;
3849      }
3850
3851      /**
3852       * Returns true if the chart is using ordinal xUnits ({@link units.ordinal units.ordinal}, or false
3853       * otherwise. Most charts behave differently with ordinal data and use the result of this method to
3854       * trigger the appropriate logic.
3855       * @returns {Boolean}
3856       */
3857      isOrdinal () {
3858          return this.xUnits() === units.ordinal;
3859      }
3860
3861      _useOuterPadding () {
3862          return true;
3863      }
3864
3865      _ordinalXDomain () {
3866          const groups = this._computeOrderedGroups(this.data());
3867          return groups.map(this.keyAccessor());
3868      }
3869
3870      _createXAxis () {
3871          return this._useTopXAxis ? d3Axis.axisTop() : d3Axis.axisBottom();
3872      }
3873
3874      // eslint-disable-next-line complexity
3875      _prepareXAxis (g, render) {
3876          if (!this.isOrdinal()) {
3877              if (this.elasticX()) {
3878                  this._x.domain([this.xAxisMin(), this.xAxisMax()]);
3879              }
3880          } else { // self._chart.isOrdinal()
3881              // D3v4 - Ordinal charts would need scaleBand
3882              // bandwidth is a method in scaleBand
3883              // (https://github.com/d3/d3-scale/blob/master/README.md#scaleBand)
3884              if (!this._x.bandwidth) {
3885                  // If self._x is not a scaleBand create a new scale and
3886                  // copy the original domain to the new scale
3887                  logger.warn('For compatibility with d3v4+, dc.js d3.0 ordinal bar/line/bubble charts need ' +
3888                      'd3.scaleBand() for the x scale, instead of d3.scaleOrdinal(). ' +
3889                      'Replacing .x() with a d3.scaleBand with the same domain - ' +
3890                      'make the same change in your code to avoid this warning!');
3891                  this._x = d3Scale.scaleBand().domain(this._x.domain());
3892              }
3893
3894              if (this.elasticX() || this._x.domain().length === 0) {
3895                  this._x.domain(this._ordinalXDomain());
3896              }
3897          }
3898
3899          // has the domain changed?
3900          const xdom = this._x.domain();
3901          if (render || !utils.arraysEqual(this._lastXDomain, xdom)) {
3902              this.rescale();
3903          }
3904          this._lastXDomain = xdom;
3905
3906          // please can't we always use rangeBands for bar charts?
3907          if (this.isOrdinal()) {
3908              this._x.range([0, this.xAxisLength()])
3909                  .paddingInner(this._fRangeBandPadding)
3910                  .paddingOuter(this._useOuterPadding() ? this._fOuterRangeBandPadding : 0);
3911          } else {
3912              this._x.range([0, this.xAxisLength()]);
3913          }
3914
3915          if (!this._xAxis) {
3916              this._xAxis = this._createXAxis();
3917          }
3918
3919          this._xAxis = this._xAxis.scale(this.x());
3920
3921          this._renderVerticalGridLines(g);
3922      }
3923
3924      renderXAxis (g) {
3925          let axisXG = g.select('g.x');
3926
3927          if (axisXG.empty()) {
3928              axisXG = g.append('g')
3929                  .attr('class', 'axis x')
3930                  .attr('transform', `translate(${this.margins().left},${this._xAxisY()})`);
3931          }
3932
3933          let axisXLab = g.select(`text.${X_AXIS_LABEL_CLASS}`);
3934          const axisXLabY = this._useTopXAxis ? this._xAxisLabelPadding : (this.height() - this._xAxisLabelPadding);
3935          if (axisXLab.empty() && this.xAxisLabel()) {
3936              axisXLab = g.append('text')
3937                  .attr('class', X_AXIS_LABEL_CLASS)
3938                  .attr('transform', `translate(${this.margins().left + this.xAxisLength() / 2},${axisXLabY})`)
3939                  .attr('text-anchor', 'middle');
3940          }
3941          if (this.xAxisLabel() && axisXLab.text() !== this.xAxisLabel()) {
3942              axisXLab.text(this.xAxisLabel());
3943          }
3944
3945          transition(axisXG, this.transitionDuration(), this.transitionDelay())
3946              .attr('transform', `translate(${this.margins().left},${this._xAxisY()})`)
3947              .call(this._xAxis);
3948          transition(axisXLab, this.transitionDuration(), this.transitionDelay())
3949              .attr('transform', `translate(${this.margins().left + this.xAxisLength() / 2},${axisXLabY})`);
3950      }
3951
3952      _renderVerticalGridLines (g) {
3953          let gridLineG = g.select(`g.${VERTICAL_CLASS}`);
3954
3955          if (this._renderVerticalGridLine) {
3956              if (gridLineG.empty()) {
3957                  gridLineG = g.insert('g', ':first-child')
3958                      .attr('class', `${GRID_LINE_CLASS} ${VERTICAL_CLASS}`)
3959                      .attr('transform', `translate(${this.margins().left},${this.margins().top})`);
3960              }
3961
3962              const ticks = this._xAxis.tickValues() ? this._xAxis.tickValues() :
3963                  (typeof this._x.ticks === 'function' ? this._x.ticks.apply(this._x, this._xAxis.tickArguments()) : this._x.domain());
3964
3965              const lines = gridLineG.selectAll('line')
3966                  .data(ticks);
3967
3968              // enter
3969              const linesGEnter = lines.enter()
3970                  .append('line')
3971                  .attr('x1', d => this._x(d))
3972                  .attr('y1', this._xAxisY() - this.margins().top)
3973                  .attr('x2', d => this._x(d))
3974                  .attr('y2', 0)
3975                  .attr('opacity', 0);
3976              transition(linesGEnter, this.transitionDuration(), this.transitionDelay())
3977                  .attr('opacity', 0.5);
3978
3979              // update
3980              transition(lines, this.transitionDuration(), this.transitionDelay())
3981                  .attr('x1', d => this._x(d))
3982                  .attr('y1', this._xAxisY() - this.margins().top)
3983                  .attr('x2', d => this._x(d))
3984                  .attr('y2', 0);
3985
3986              // exit
3987              lines.exit().remove();
3988          } else {
3989              gridLineG.selectAll('line').remove();
3990          }
3991      }
3992
3993      _xAxisY () {
3994          return this._useTopXAxis ? this.margins().top : this.height() - this.margins().bottom;
3995      }
3996
3997      xAxisLength () {
3998          return this.effectiveWidth();
3999      }
4000
4001      /**
4002       * Set or get the x axis label. If setting the label, you may optionally include additional padding to
4003       * the margin to make room for the label. By default the padded is set to 12 to accomodate the text height.
4004       * @param {String} [labelText]
4005       * @param {Number} [padding=12]
4006       * @returns {String}
4007       */
4008      xAxisLabel (labelText, padding) {
4009          if (!arguments.length) {
4010              return this._xAxisLabel;
4011          }
4012          this._xAxisLabel = labelText;
4013          this.margins().bottom -= this._xAxisLabelPadding;
4014          this._xAxisLabelPadding = (padding === undefined) ? DEFAULT_AXIS_LABEL_PADDING : padding;
4015          this.margins().bottom += this._xAxisLabelPadding;
4016          return this;
4017      }
4018
4019      _createYAxis () {
4020          return this._useRightYAxis ? d3Axis.axisRight() : d3Axis.axisLeft();
4021      }
4022
4023      _prepareYAxis (g) {
4024          if (this._y === undefined || this.elasticY()) {
4025              if (this._y === undefined) {
4026                  this._y = d3Scale.scaleLinear();
4027              }
4028              const _min = this.yAxisMin() || 0;
4029              const _max = this.yAxisMax() || 0;
4030              this._y.domain([_min, _max]).rangeRound([this.yAxisHeight(), 0]);
4031          }
4032
4033          this._y.range([this.yAxisHeight(), 0]);
4034
4035          if (!this._yAxis) {
4036              this._yAxis = this._createYAxis();
4037          }
4038
4039          this._yAxis.scale(this._y);
4040
4041          this._renderHorizontalGridLinesForAxis(g, this._y, this._yAxis);
4042      }
4043
4044      renderYAxisLabel (axisClass, text, rotation, labelXPosition) {
4045          labelXPosition = labelXPosition || this._yAxisLabelPadding;
4046
4047          let axisYLab = this.g().select(`text.${Y_AXIS_LABEL_CLASS}.${axisClass}-label`);
4048          const labelYPosition = (this.margins().top + this.yAxisHeight() / 2);
4049          if (axisYLab.empty() && text) {
4050              axisYLab = this.g().append('text')
4051                  .attr('transform', `translate(${labelXPosition},${labelYPosition}),rotate(${rotation})`)
4052                  .attr('class', `${Y_AXIS_LABEL_CLASS} ${axisClass}-label`)
4053                  .attr('text-anchor', 'middle')
4054                  .text(text);
4055          }
4056          if (text && axisYLab.text() !== text) {
4057              axisYLab.text(text);
4058          }
4059          transition(axisYLab, this.transitionDuration(), this.transitionDelay())
4060              .attr('transform', `translate(${labelXPosition},${labelYPosition}),rotate(${rotation})`);
4061      }
4062
4063      renderYAxisAt (axisClass, axis, position) {
4064          let axisYG = this.g().select(`g.${axisClass}`);
4065          if (axisYG.empty()) {
4066              axisYG = this.g().append('g')
4067                  .attr('class', `axis ${axisClass}`)
4068                  .attr('transform', `translate(${position},${this.margins().top})`);
4069          }
4070
4071          transition(axisYG, this.transitionDuration(), this.transitionDelay())
4072              .attr('transform', `translate(${position},${this.margins().top})`)
4073              .call(axis);
4074      }
4075
4076      renderYAxis () {
4077          const axisPosition = this._useRightYAxis ? (this.width() - this.margins().right) : this._yAxisX();
4078          this.renderYAxisAt('y', this._yAxis, axisPosition);
4079          const labelPosition = this._useRightYAxis ? (this.width() - this._yAxisLabelPadding) : this._yAxisLabelPadding;
4080          const rotation = this._useRightYAxis ? 90 : -90;
4081          this.renderYAxisLabel('y', this.yAxisLabel(), rotation, labelPosition);
4082      }
4083
4084      _renderHorizontalGridLinesForAxis (g, scale, axis) {
4085          let gridLineG = g.select(`g.${HORIZONTAL_CLASS}`);
4086
4087          if (this._renderHorizontalGridLine) {
4088              // see https://github.com/d3/d3-axis/blob/master/src/axis.js#L48
4089              const ticks = axis.tickValues() ? axis.tickValues() :
4090                  (scale.ticks ? scale.ticks.apply(scale, axis.tickArguments()) : scale.domain());
4091
4092              if (gridLineG.empty()) {
4093                  gridLineG = g.insert('g', ':first-child')
4094                      .attr('class', `${GRID_LINE_CLASS} ${HORIZONTAL_CLASS}`)
4095                      .attr('transform', `translate(${this.margins().left},${this.margins().top})`);
4096              }
4097
4098              const lines = gridLineG.selectAll('line')
4099                  .data(ticks);
4100
4101              // enter
4102              const linesGEnter = lines.enter()
4103                  .append('line')
4104                  .attr('x1', 1)
4105                  .attr('y1', d => scale(d))
4106                  .attr('x2', this.xAxisLength())
4107                  .attr('y2', d => scale(d))
4108                  .attr('opacity', 0);
4109              transition(linesGEnter, this.transitionDuration(), this.transitionDelay())
4110                  .attr('opacity', 0.5);
4111
4112              // update
4113              transition(lines, this.transitionDuration(), this.transitionDelay())
4114                  .attr('x1', 1)
4115                  .attr('y1', d => scale(d))
4116                  .attr('x2', this.xAxisLength())
4117                  .attr('y2', d => scale(d));
4118
4119              // exit
4120              lines.exit().remove();
4121          } else {
4122              gridLineG.selectAll('line').remove();
4123          }
4124      }
4125
4126      _yAxisX () {
4127          return this.useRightYAxis() ? this.width() - this.margins().right : this.margins().left;
4128      }
4129
4130      /**
4131       * Set or get the y axis label. If setting the label, you may optionally include additional padding
4132       * to the margin to make room for the label. By default the padding is set to 12 to accommodate the
4133       * text height.
4134       * @param {String} [labelText]
4135       * @param {Number} [padding=12]
4136       * @returns {String|CoordinateGridMixin}
4137       */
4138      yAxisLabel (labelText, padding) {
4139          if (!arguments.length) {
4140              return this._yAxisLabel;
4141          }
4142          this._yAxisLabel = labelText;
4143          this.margins().left -= this._yAxisLabelPadding;
4144          this._yAxisLabelPadding = (padding === undefined) ? DEFAULT_AXIS_LABEL_PADDING : padding;
4145          this.margins().left += this._yAxisLabelPadding;
4146          return this;
4147      }
4148
4149      /**
4150       * Get or set the y scale. The y scale is typically automatically determined by the chart implementation.
4151       * @see {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale}
4152       * @param {d3.scale} [yScale]
4153       * @returns {d3.scale|CoordinateGridMixin}
4154       */
4155      y (yScale) {
4156          if (!arguments.length) {
4157              return this._y;
4158          }
4159          this._y = yScale;
4160          this.rescale();
4161          return this;
4162      }
4163
4164      /**
4165       * Set or get the y axis used by the coordinate grid chart instance. This function is most useful
4166       * when y axis customization is required. Depending on `useRightYAxis` the y axis in dc.js is an instance of
4167       * either [d3.axisLeft](https://github.com/d3/d3-axis/blob/master/README.md#axisLeft) or
4168       * [d3.axisRight](https://github.com/d3/d3-axis/blob/master/README.md#axisRight); therefore it supports any
4169       * valid d3 axis manipulation.
4170       *
4171       * **Caution**: The y axis is usually generated internally by dc; resetting it may cause
4172       * unexpected results.  Note also that when used as a getter, this function is not chainable: it
4173       * returns the axis, not the chart,
4174       * {@link https://github.com/dc-js/dc.js/wiki/FAQ#why-does-everything-break-after-a-call-to-xaxis-or-yaxis
4175           * so attempting to call chart functions after calling `.yAxis()` will fail}.
4176       * In addition, depending on whether you are going to use the axis on left or right
4177       * you need to appropriately pass [d3.axisLeft](https://github.com/d3/d3-axis/blob/master/README.md#axisLeft)
4178       * or [d3.axisRight](https://github.com/d3/d3-axis/blob/master/README.md#axisRight)
4179       * @see {@link https://github.com/d3/d3-axis/blob/master/README.md d3.axis}
4180       * @example
4181       * // customize y axis tick format
4182       * chart.yAxis().tickFormat(function(v) {return v + '%';});
4183       * // customize y axis tick values
4184       * chart.yAxis().tickValues([0, 100, 200, 300]);
4185       * @param {d3.axisLeft|d3.axisRight} [yAxis]
4186       * @returns {d3.axisLeft|d3.axisRight|CoordinateGridMixin}
4187       */
4188      yAxis (yAxis) {
4189          if (!arguments.length) {
4190              if (!this._yAxis) {
4191                  this._yAxis = this._createYAxis();
4192              }
4193              return this._yAxis;
4194          }
4195          this._yAxis = yAxis;
4196          return this;
4197      }
4198
4199      /**
4200       * Turn on/off elastic y axis behavior. If y axis elasticity is turned on, then the grid chart will
4201       * attempt to recalculate the y axis range whenever a redraw event is triggered.
4202       * @param {Boolean} [elasticY=false]
4203       * @returns {Boolean|CoordinateGridMixin}
4204       */
4205      elasticY (elasticY) {
4206          if (!arguments.length) {
4207              return this._yElasticity;
4208          }
4209          this._yElasticity = elasticY;
4210          return this;
4211      }
4212
4213      /**
4214       * Turn on/off horizontal grid lines.
4215       * @param {Boolean} [renderHorizontalGridLines=false]
4216       * @returns {Boolean|CoordinateGridMixin}
4217       */
4218      renderHorizontalGridLines (renderHorizontalGridLines) {
4219          if (!arguments.length) {
4220              return this._renderHorizontalGridLine;
4221          }
4222          this._renderHorizontalGridLine = renderHorizontalGridLines;
4223          return this;
4224      }
4225
4226      /**
4227       * Turn on/off vertical grid lines.
4228       * @param {Boolean} [renderVerticalGridLines=false]
4229       * @returns {Boolean|CoordinateGridMixin}
4230       */
4231      renderVerticalGridLines (renderVerticalGridLines) {
4232          if (!arguments.length) {
4233              return this._renderVerticalGridLine;
4234          }
4235          this._renderVerticalGridLine = renderVerticalGridLines;
4236          return this;
4237      }
4238
4239      /**
4240       * Calculates the minimum x value to display in the chart. Includes xAxisPadding if set.
4241       * @returns {*}
4242       */
4243      xAxisMin () {
4244          const m = d3Array.min(this.data(), e => this.keyAccessor()(e));
4245          return utils.subtract(m, this._xAxisPadding, this._xAxisPaddingUnit);
4246      }
4247
4248      /**
4249       * Calculates the maximum x value to display in the chart. Includes xAxisPadding if set.
4250       * @returns {*}
4251       */
4252      xAxisMax () {
4253          const m = d3Array.max(this.data(), e => this.keyAccessor()(e));
4254          return utils.add(m, this._xAxisPadding, this._xAxisPaddingUnit);
4255      }
4256
4257      /**
4258       * Calculates the minimum y value to display in the chart. Includes yAxisPadding if set.
4259       * @returns {*}
4260       */
4261      yAxisMin () {
4262          const m = d3Array.min(this.data(), e => this.valueAccessor()(e));
4263          return utils.subtract(m, this._yAxisPadding);
4264      }
4265
4266      /**
4267       * Calculates the maximum y value to display in the chart. Includes yAxisPadding if set.
4268       * @returns {*}
4269       */
4270      yAxisMax () {
4271          const m = d3Array.max(this.data(), e => this.valueAccessor()(e));
4272          return utils.add(m, this._yAxisPadding);
4273      }
4274
4275      /**
4276       * Set or get y axis padding for the elastic y axis. The padding will be added to the top and
4277       * bottom of the y axis if elasticY is turned on; otherwise it is ignored.
4278       *
4279       * Padding can be an integer or percentage in string (e.g. '10%'). Padding can be applied to
4280       * number or date axes. When padding a date axis, an integer represents number of days being padded
4281       * and a percentage string will be treated the same as an integer.
4282       * @param {Number|String} [padding=0]
4283       * @returns {Number|CoordinateGridMixin}
4284       */
4285      yAxisPadding (padding) {
4286          if (!arguments.length) {
4287              return this._yAxisPadding;
4288          }
4289          this._yAxisPadding = padding;
4290          return this;
4291      }
4292
4293      yAxisHeight () {
4294          return this.effectiveHeight();
4295      }
4296
4297      /**
4298       * Set or get the rounding function used to quantize the selection when brushing is enabled.
4299       * @example
4300       * // set x unit round to by month, this will make sure range selection brush will
4301       * // select whole months
4302       * chart.round(d3.timeMonth.round);
4303       * @param {Function} [round]
4304       * @returns {Function|CoordinateGridMixin}
4305       */
4306      round (round) {
4307          if (!arguments.length) {
4308              return this._round;
4309          }
4310          this._round = round;
4311          return this;
4312      }
4313
4314      _rangeBandPadding (_) {
4315          if (!arguments.length) {
4316              return this._fRangeBandPadding;
4317          }
4318          this._fRangeBandPadding = _;
4319          return this;
4320      }
4321
4322      _outerRangeBandPadding (_) {
4323          if (!arguments.length) {
4324              return this._fOuterRangeBandPadding;
4325          }
4326          this._fOuterRangeBandPadding = _;
4327          return this;
4328      }
4329
4330      filter (_) {
4331          if (!arguments.length) {
4332              return super.filter();
4333          }
4334
4335          super.filter(_);
4336
4337          this.redrawBrush(_, false);
4338
4339          return this;
4340      }
4341
4342      /**
4343       * Get or set the brush. Brush must be an instance of d3 brushes
4344       * https://github.com/d3/d3-brush/blob/master/README.md
4345       * You will use this only if you are writing a new chart type that supports brushing.
4346       *
4347       * **Caution**: dc creates and manages brushes internally. Go through and understand the source code
4348       * if you want to pass a new brush object. Even if you are only using the getter,
4349       * the brush object may not behave the way you expect.
4350       *
4351       * @param {d3.brush} [_]
4352       * @returns {d3.brush|CoordinateGridMixin}
4353       */
4354      brush (_) {
4355          if (!arguments.length) {
4356              return this._brush;
4357          }
4358          this._brush = _;
4359          return this;
4360      }
4361
4362      renderBrush (g, doTransition) {
4363          if (this._brushOn) {
4364              this._brush.on('start brush end', d3compat.eventHandler((d, evt) => this._brushing(evt)));
4365
4366              // To retrieve selection we need self._gBrush
4367              this._gBrush = g.append('g')
4368                  .attr('class', 'brush')
4369                  .attr('transform', `translate(${this.margins().left},${this.margins().top})`);
4370
4371              this.setBrushExtents();
4372
4373              this.createBrushHandlePaths(this._gBrush, doTransition);
4374
4375              this.redrawBrush(this.filter(), doTransition);
4376          }
4377      }
4378
4379      createBrushHandlePaths (gBrush) {
4380          let brushHandles = gBrush.selectAll(`path.${CUSTOM_BRUSH_HANDLE_CLASS}`).data([{type: 'w'}, {type: 'e'}]);
4381
4382          brushHandles = brushHandles
4383              .enter()
4384              .append('path')
4385              .attr('class', CUSTOM_BRUSH_HANDLE_CLASS)
4386              .merge(brushHandles);
4387
4388          brushHandles
4389              .attr('d', d => this.resizeHandlePath(d));
4390      }
4391
4392      extendBrush (brushSelection) {
4393          if (brushSelection && this.round()) {
4394              brushSelection[0] = this.round()(brushSelection[0]);
4395              brushSelection[1] = this.round()(brushSelection[1]);
4396          }
4397          return brushSelection;
4398      }
4399
4400      brushIsEmpty (brushSelection) {
4401          return !brushSelection || brushSelection[1] <= brushSelection[0];
4402      }
4403
4404      _brushing (evt) {
4405          if (this._ignoreBrushEvents) {
4406              return;
4407          }
4408
4409          let brushSelection = evt.selection;
4410          if (brushSelection) {
4411              brushSelection = brushSelection.map(this.x().invert);
4412          }
4413
4414          brushSelection = this.extendBrush(brushSelection);
4415
4416          this.redrawBrush(brushSelection, false);
4417
4418          const rangedFilter = this.brushIsEmpty(brushSelection) ? null : filters.RangedFilter(brushSelection[0], brushSelection[1]);
4419
4420          events.trigger(() => {
4421              this.applyBrushSelection(rangedFilter);
4422          }, constants.EVENT_DELAY);
4423      }
4424
4425      // This can be overridden in a derived chart. For example Composite chart overrides it
4426      applyBrushSelection (rangedFilter) {
4427          this.replaceFilter(rangedFilter);
4428          this.redrawGroup();
4429      }
4430
4431      _withoutBrushEvents (closure) {
4432          const oldValue = this._ignoreBrushEvents;
4433          this._ignoreBrushEvents = true;
4434
4435          try {
4436              closure();
4437          } finally {
4438              this._ignoreBrushEvents = oldValue;
4439          }
4440      }
4441
4442      setBrushExtents (doTransition) {
4443          this._withoutBrushEvents(() => {
4444              // Set boundaries of the brush, must set it before applying to self._gBrush
4445              this._brush.extent([[0, 0], [this.effectiveWidth(), this.effectiveHeight()]]);
4446          });
4447
4448          this._gBrush
4449              .call(this._brush);
4450      }
4451
4452      redrawBrush (brushSelection, doTransition) {
4453          if (this._brushOn && this._gBrush) {
4454              if (this._resizing) {
4455                  this.setBrushExtents(doTransition);
4456              }
4457
4458              if (!brushSelection) {
4459                  this._withoutBrushEvents(() => {
4460                      this._gBrush
4461                          .call(this._brush.move, null);
4462                  });
4463
4464                  this._gBrush.selectAll(`path.${CUSTOM_BRUSH_HANDLE_CLASS}`)
4465                      .attr('display', 'none');
4466              } else {
4467                  const scaledSelection = [this._x(brushSelection[0]), this._x(brushSelection[1])];
4468
4469                  const gBrush =
4470                      optionalTransition(doTransition, this.transitionDuration(), this.transitionDelay())(this._gBrush);
4471
4472                  this._withoutBrushEvents(() => {
4473                      gBrush
4474                          .call(this._brush.move, scaledSelection);
4475                  });
4476
4477                  gBrush.selectAll(`path.${CUSTOM_BRUSH_HANDLE_CLASS}`)
4478                      .attr('display', null)
4479                      .attr('transform', (d, i) => `translate(${this._x(brushSelection[i])}, 0)`)
4480                      .attr('d', d => this.resizeHandlePath(d));
4481              }
4482          }
4483          this.fadeDeselectedArea(brushSelection);
4484      }
4485
4486      fadeDeselectedArea (brushSelection) {
4487          // do nothing, sub-chart should override this function
4488      }
4489
4490      // borrowed from Crossfilter example
4491      resizeHandlePath (d) {
4492          d = d.type;
4493          const e = +(d === 'e'), x = e ? 1 : -1, y = this.effectiveHeight() / 3;
4494          return `M${0.5 * x},${y 
4495        }A6,6 0 0 ${e} ${6.5 * x},${y + 6 
4496        }V${2 * y - 6 
4497        }A6,6 0 0 ${e} ${0.5 * x},${2 * y 
4498        }Z` +
4499              `M${2.5 * x},${y + 8 
4500            }V${2 * y - 8 
4501            }M${4.5 * x},${y + 8 
4502            }V${2 * y - 8}`;
4503      }
4504
4505      _getClipPathId () {
4506          return `${this.anchorName().replace(/[ .#=\[\]"]/g, '-')}-clip`;
4507      }
4508
4509      /**
4510       * Get or set the padding in pixels for the clip path. Once set padding will be applied evenly to
4511       * the top, left, right, and bottom when the clip path is generated. If set to zero, the clip area
4512       * will be exactly the chart body area minus the margins.
4513       * @param {Number} [padding=5]
4514       * @returns {Number|CoordinateGridMixin}
4515       */
4516      clipPadding (padding) {
4517          if (!arguments.length) {
4518              return this._clipPadding;
4519          }
4520          this._clipPadding = padding;
4521          return this;
4522      }
4523
4524      _generateClipPath () {
4525          const defs = utils.appendOrSelect(this._parent, 'defs');
4526          // cannot select <clippath> elements; bug in WebKit, must select by id
4527          // https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I
4528          const id = this._getClipPathId();
4529          const chartBodyClip = utils.appendOrSelect(defs, `#${id}`, 'clipPath').attr('id', id);
4530
4531          const padding = this._clipPadding * 2;
4532
4533          utils.appendOrSelect(chartBodyClip, 'rect')
4534              .attr('width', this.xAxisLength() + padding)
4535              .attr('height', this.yAxisHeight() + padding)
4536              .attr('transform', `translate(-${this._clipPadding}, -${this._clipPadding})`);
4537      }
4538
4539      _preprocessData () {
4540      }
4541
4542      _doRender () {
4543          this.resetSvg();
4544
4545          this._preprocessData();
4546
4547          this._generateG();
4548          this._generateClipPath();
4549
4550          this._drawChart(true);
4551
4552          this._configureMouseZoom();
4553
4554          return this;
4555      }
4556
4557      _doRedraw () {
4558          this._preprocessData();
4559
4560          this._drawChart(false);
4561          this._generateClipPath();
4562
4563          return this;
4564      }
4565
4566      _drawChart (render) {
4567          if (this.isOrdinal()) {
4568              this._brushOn = false;
4569          }
4570
4571          this._prepareXAxis(this.g(), render);
4572          this._prepareYAxis(this.g());
4573
4574          this.plotData();
4575
4576          if (this.elasticX() || this._resizing || render) {
4577              this.renderXAxis(this.g());
4578          }
4579
4580          if (this.elasticY() || this._resizing || render) {
4581              this.renderYAxis(this.g());
4582          }
4583
4584          if (render) {
4585              this.renderBrush(this.g(), false);
4586          } else {
4587              // Animate the brush only while resizing
4588              this.redrawBrush(this.filter(), this._resizing);
4589          }
4590          this.fadeDeselectedArea(this.filter());
4591          this.resizing(false);
4592      }
4593
4594      _configureMouseZoom () {
4595          // Save a copy of original x scale
4596          this._origX = this._x.copy();
4597
4598          if (this._mouseZoomable) {
4599              this._enableMouseZoom();
4600          } else if (this._hasBeenMouseZoomable) {
4601              this._disableMouseZoom();
4602          }
4603      }
4604
4605      _enableMouseZoom () {
4606          this._hasBeenMouseZoomable = true;
4607
4608          const extent = [[0, 0], [this.effectiveWidth(), this.effectiveHeight()]];
4609
4610          this._zoom
4611              .scaleExtent(this._zoomScale)
4612              .extent(extent)
4613              .duration(this.transitionDuration());
4614
4615          if (this._zoomOutRestrict) {
4616              // Ensure minimum zoomScale is at least 1
4617              const zoomScaleMin = Math.max(this._zoomScale[0], 1);
4618              this._zoom
4619                  .translateExtent(extent)
4620                  .scaleExtent([zoomScaleMin, this._zoomScale[1]]);
4621          }
4622
4623          this.root().call(this._zoom);
4624
4625          // Tell D3 zoom our current zoom/pan status
4626          this._updateD3zoomTransform();
4627      }
4628
4629      _disableMouseZoom () {
4630          this.root().call(this._nullZoom);
4631      }
4632
4633      _zoomHandler (newDomain, noRaiseEvents) {
4634          let domFilter;
4635
4636          if (this._hasRangeSelected(newDomain)) {
4637              this.x().domain(newDomain);
4638              domFilter = filters.RangedFilter(newDomain[0], newDomain[1]);
4639          } else {
4640              this.x().domain(this._xOriginalDomain);
4641              domFilter = null;
4642          }
4643
4644          this.replaceFilter(domFilter);
4645          this.rescale();
4646          this.redraw();
4647
4648          if (!noRaiseEvents) {
4649              if (this._rangeChart && !utils.arraysEqual(this.filter(), this._rangeChart.filter())) {
4650                  events.trigger(() => {
4651                      this._rangeChart.replaceFilter(domFilter);
4652                      this._rangeChart.redraw();
4653                  });
4654              }
4655
4656              this._invokeZoomedListener();
4657              events.trigger(() => {
4658                  this.redrawGroup();
4659              }, constants.EVENT_DELAY);
4660          }
4661      }
4662
4663      // event.transform.rescaleX(self._origX).domain() should give back newDomain
4664      _domainToZoomTransform (newDomain, origDomain, xScale) {
4665          const k = (origDomain[1] - origDomain[0]) / (newDomain[1] - newDomain[0]);
4666          const xt = -1 * xScale(newDomain[0]);
4667
4668          return d3Zoom.zoomIdentity.scale(k).translate(xt, 0);
4669      }
4670
4671      // If we changing zoom status (for example by calling focus), tell D3 zoom about it
4672      _updateD3zoomTransform () {
4673          if (this._zoom) {
4674              this._withoutZoomEvents(() => {
4675                  this._zoom.transform(this.root(), this._domainToZoomTransform(this.x().domain(), this._xOriginalDomain, this._origX));
4676              });
4677          }
4678      }
4679
4680      _withoutZoomEvents (closure) {
4681          const oldValue = this._ignoreZoomEvents;
4682          this._ignoreZoomEvents = true;
4683
4684          try {
4685              closure();
4686          } finally {
4687              this._ignoreZoomEvents = oldValue;
4688          }
4689      }
4690
4691      _onZoom (evt) {
4692          // ignore zoom events if it was caused by a programmatic change
4693          if (this._ignoreZoomEvents) {
4694              return;
4695          }
4696
4697          const newDomain = evt.transform.rescaleX(this._origX).domain();
4698          this.focus(newDomain, false);
4699      }
4700
4701      _checkExtents (ext, outerLimits) {
4702          if (!ext || ext.length !== 2 || !outerLimits || outerLimits.length !== 2) {
4703              return ext;
4704          }
4705
4706          if (ext[0] > outerLimits[1] || ext[1] < outerLimits[0]) {
4707              console.warn('Could not intersect extents, will reset');
4708          }
4709          // Math.max does not work (as the values may be dates as well)
4710          return [ext[0] > outerLimits[0] ? ext[0] : outerLimits[0], ext[1] < outerLimits[1] ? ext[1] : outerLimits[1]];
4711      }
4712
4713      /**
4714       * Zoom this chart to focus on the given range. The given range should be an array containing only
4715       * 2 elements (`[start, end]`) defining a range in the x domain. If the range is not given or set
4716       * to null, then the zoom will be reset. _For focus to work elasticX has to be turned off;
4717       * otherwise focus will be ignored.
4718       *
4719       * To avoid ping-pong volley of events between a pair of range and focus charts please set
4720       * `noRaiseEvents` to `true`. In that case it will update this chart but will not fire `zoom` event
4721       * and not try to update back the associated range chart.
4722       * If you are calling it manually - typically you will leave it to `false` (the default).
4723       *
4724       * @example
4725       * chart.on('renderlet', function(chart) {
4726       *     // smooth the rendering through event throttling
4727       *     events.trigger(function(){
4728       *          // focus some other chart to the range selected by user on this chart
4729       *          someOtherChart.focus(chart.filter());
4730       *     });
4731       * })
4732       * @param {Array<Number>} [range]
4733       * @param {Boolean} [noRaiseEvents = false]
4734       * @return {undefined}
4735       */
4736      focus (range, noRaiseEvents) {
4737          if (this._zoomOutRestrict) {
4738              // ensure range is within self._xOriginalDomain
4739              range = this._checkExtents(range, this._xOriginalDomain);
4740
4741              // If it has an associated range chart ensure range is within domain of that rangeChart
4742              if (this._rangeChart) {
4743                  range = this._checkExtents(range, this._rangeChart.x().domain());
4744              }
4745          }
4746
4747          this._zoomHandler(range, noRaiseEvents);
4748          this._updateD3zoomTransform();
4749      }
4750
4751      refocused () {
4752          return !utils.arraysEqual(this.x().domain(), this._xOriginalDomain);
4753      }
4754
4755      focusChart (c) {
4756          if (!arguments.length) {
4757              return this._focusChart;
4758          }
4759          this._focusChart = c;
4760          this.on('filtered.dcjs-range-chart', chart => {
4761              if (!chart.filter()) {
4762                  events.trigger(() => {
4763                      this._focusChart.x().domain(this._focusChart.xOriginalDomain(), true);
4764                  });
4765              } else if (!utils.arraysEqual(chart.filter(), this._focusChart.filter())) {
4766                  events.trigger(() => {
4767                      this._focusChart.focus(chart.filter(), true);
4768                  });
4769              }
4770          });
4771          return this;
4772      }
4773
4774      /**
4775       * Turn on/off the brush-based range filter. When brushing is on then user can drag the mouse
4776       * across a chart with a quantitative scale to perform range filtering based on the extent of the
4777       * brush, or click on the bars of an ordinal bar chart or slices of a pie chart to filter and
4778       * un-filter them. However turning on the brush filter will disable other interactive elements on
4779       * the chart such as highlighting, tool tips, and reference lines. Zooming will still be possible
4780       * if enabled, but only via scrolling (panning will be disabled.)
4781       * @param {Boolean} [brushOn=true]
4782       * @returns {Boolean|CoordinateGridMixin}
4783       */
4784      brushOn (brushOn) {
4785          if (!arguments.length) {
4786              return this._brushOn;
4787          }
4788          this._brushOn = brushOn;
4789          return this;
4790      }
4791
4792      /**
4793       * This will be internally used by composite chart onto children. Please go not invoke directly.
4794       *
4795       * @protected
4796       * @param {Boolean} [brushOn=false]
4797       * @returns {Boolean|CoordinateGridMixin}
4798       */
4799      parentBrushOn (brushOn) {
4800          if (!arguments.length) {
4801              return this._parentBrushOn;
4802          }
4803          this._parentBrushOn = brushOn;
4804          return this;
4805      }
4806
4807      // Get the SVG rendered brush
4808      gBrush () {
4809          return this._gBrush;
4810      }
4811
4812      _hasRangeSelected (range) {
4813          return range instanceof Array && range.length > 1;
4814      }
4815  }
4816
4817  // https://github.com/d3/d3-plugins/blob/master/box/box.js
4818
4819  const d3Box = function () {
4820      let width = 1;
4821      let height = 1;
4822      let duration = 0;
4823      const delay = 0;
4824      let domain = null;
4825      let value = Number;
4826      let whiskers = boxWhiskers;
4827      let quartiles = boxQuartiles;
4828      let tickFormat = null;
4829
4830      // Enhanced attributes
4831      let renderDataPoints = false;
4832      const dataRadius = 3;
4833      let dataOpacity = 0.3;
4834      let dataWidthPortion = 0.8;
4835      let renderTitle = false;
4836      let showOutliers = true;
4837      let boldOutlier = false;
4838
4839
4840      // For each small multiple…
4841      function box (g) {
4842          g.each(function (data, index) {
4843              data = data.map(value).sort(d3Array.ascending);
4844              const _g = d3Selection.select(this);
4845              const n = data.length;
4846              let min;
4847              let max;
4848
4849              // Leave if there are no items.
4850              if (n === 0) {return;}
4851
4852              // Compute quartiles. Must return exactly 3 elements.
4853              const quartileData = data.quartiles = quartiles(data);
4854
4855              // Compute whiskers. Must return exactly 2 elements, or null.
4856              const whiskerIndices = whiskers && whiskers.call(this, data, index),
4857                  whiskerData = whiskerIndices && whiskerIndices.map(_i => data[_i]);
4858
4859              // Compute outliers. If no whiskers are specified, all data are 'outliers'.
4860              // We compute the outliers as indices, so that we can join across transitions!
4861              const outlierIndices = whiskerIndices ?
4862                  d3Array.range(0, whiskerIndices[0]).concat(d3Array.range(whiskerIndices[1] + 1, n)) : d3Array.range(n);
4863
4864              // Determine the maximum value based on if outliers are shown
4865              if (showOutliers) {
4866                  min = data[0];
4867                  max = data[n - 1];
4868              } else {
4869                  min = data[whiskerIndices[0]];
4870                  max = data[whiskerIndices[1]];
4871              }
4872              const pointIndices = d3Array.range(whiskerIndices[0], whiskerIndices[1] + 1);
4873
4874              // Compute the new x-scale.
4875              const x1 = d3Scale.scaleLinear()
4876                  .domain(domain && domain.call(this, data, index) || [min, max])
4877                  .range([height, 0]);
4878
4879              // Retrieve the old x-scale, if this is an update.
4880              const x0 = this.__chart__ || d3Scale.scaleLinear()
4881                  .domain([0, Infinity])
4882                  .range(x1.range());
4883
4884              // Stash the new scale.
4885              this.__chart__ = x1;
4886
4887              // Note: the box, median, and box tick elements are fixed in number,
4888              // so we only have to handle enter and update. In contrast, the outliers
4889              // and other elements are variable, so we need to exit them! Variable
4890              // elements also fade in and out.
4891
4892              // Update center line: the vertical line spanning the whiskers.
4893              const center = _g.selectAll('line.center')
4894                  .data(whiskerData ? [whiskerData] : []);
4895
4896              center.enter().insert('line', 'rect')
4897                  .attr('class', 'center')
4898                  .attr('x1', width / 2)
4899                  .attr('y1', d => x0(d[0]))
4900                  .attr('x2', width / 2)
4901                  .attr('y2', d => x0(d[1]))
4902                  .style('opacity', 1e-6)
4903                  .transition()
4904                  .duration(duration)
4905                  .delay(delay)
4906                  .style('opacity', 1)
4907                  .attr('y1', d => x1(d[0]))
4908                  .attr('y2', d => x1(d[1]));
4909
4910              center.transition()
4911                  .duration(duration)
4912                  .delay(delay)
4913                  .style('opacity', 1)
4914                  .attr('x1', width / 2)
4915                  .attr('x2', width / 2)
4916                  .attr('y1', d => x1(d[0]))
4917                  .attr('y2', d => x1(d[1]));
4918
4919              center.exit().transition()
4920                  .duration(duration)
4921                  .delay(delay)
4922                  .style('opacity', 1e-6)
4923                  .attr('y1', d => x1(d[0]))
4924                  .attr('y2', d => x1(d[1]))
4925                  .remove();
4926
4927              // Update innerquartile box.
4928              const _box = _g.selectAll('rect.box')
4929                  .data([quartileData]);
4930
4931              _box.enter().append('rect')
4932                  .attr('class', 'box')
4933                  .attr('x', 0)
4934                  .attr('y', d => x0(d[2]))
4935                  .attr('width', width)
4936                  .attr('height', d => x0(d[0]) - x0(d[2]))
4937                  .style('fill-opacity', (renderDataPoints) ? 0.1 : 1)
4938                  .transition()
4939                  .duration(duration)
4940                  .delay(delay)
4941                  .attr('y', d => x1(d[2]))
4942                  .attr('height', d => x1(d[0]) - x1(d[2]));
4943
4944              _box.transition()
4945                  .duration(duration)
4946                  .delay(delay)
4947                  .attr('width', width)
4948                  .attr('y', d => x1(d[2]))
4949                  .attr('height', d => x1(d[0]) - x1(d[2]));
4950
4951              // Update median line.
4952              const medianLine = _g.selectAll('line.median')
4953                  .data([quartileData[1]]);
4954
4955              medianLine.enter().append('line')
4956                  .attr('class', 'median')
4957                  .attr('x1', 0)
4958                  .attr('y1', x0)
4959                  .attr('x2', width)
4960                  .attr('y2', x0)
4961                  .transition()
4962                  .duration(duration)
4963                  .delay(delay)
4964                  .attr('y1', x1)
4965                  .attr('y2', x1);
4966
4967              medianLine.transition()
4968                  .duration(duration)
4969                  .delay(delay)
4970                  .attr('x1', 0)
4971                  .attr('x2', width)
4972                  .attr('y1', x1)
4973                  .attr('y2', x1);
4974
4975              // Update whiskers.
4976              const whisker = _g.selectAll('line.whisker')
4977                  .data(whiskerData || []);
4978
4979              whisker.enter().insert('line', 'circle, text')
4980                  .attr('class', 'whisker')
4981                  .attr('x1', 0)
4982                  .attr('y1', x0)
4983                  .attr('x2', width)
4984                  .attr('y2', x0)
4985                  .style('opacity', 1e-6)
4986                  .transition()
4987                  .duration(duration)
4988                  .delay(delay)
4989                  .attr('y1', x1)
4990                  .attr('y2', x1)
4991                  .style('opacity', 1);
4992
4993              whisker.transition()
4994                  .duration(duration)
4995                  .delay(delay)
4996                  .attr('x1', 0)
4997                  .attr('x2', width)
4998                  .attr('y1', x1)
4999                  .attr('y2', x1)
5000                  .style('opacity', 1);
5001
5002              whisker.exit().transition()
5003                  .duration(duration)
5004                  .delay(delay)
5005                  .attr('y1', x1)
5006                  .attr('y2', x1)
5007                  .style('opacity', 1e-6)
5008                  .remove();
5009
5010              // Update outliers.
5011              if (showOutliers) {
5012                  const outlierClass = boldOutlier ? 'outlierBold' : 'outlier';
5013                  const outlierSize = boldOutlier ? 3 : 5;
5014                  const outlierX = boldOutlier ?
5015                      function () {
5016                          return Math.floor(Math.random() *
5017                              (width * dataWidthPortion) +
5018                              1 + ((width - (width * dataWidthPortion)) / 2));
5019                      } :
5020                      function () {
5021                          return width / 2;
5022                      };
5023
5024                  const outlier = _g.selectAll(`circle.${outlierClass}`)
5025                      .data(outlierIndices, Number);
5026
5027                  outlier.enter().insert('circle', 'text')
5028                      .attr('class', outlierClass)
5029                      .attr('r', outlierSize)
5030                      .attr('cx', outlierX)
5031                      .attr('cy', i => x0(data[i]))
5032                      .style('opacity', 1e-6)
5033                      .transition()
5034                      .duration(duration)
5035                      .delay(delay)
5036                      .attr('cy', i => x1(data[i]))
5037                      .style('opacity', 0.6);
5038
5039                  if (renderTitle) {
5040                      outlier.selectAll('title').remove();
5041                      outlier.append('title').text(i => data[i]);
5042                  }
5043
5044                  outlier.transition()
5045                      .duration(duration)
5046                      .delay(delay)
5047                      .attr('cx', outlierX)
5048                      .attr('cy', i => x1(data[i]))
5049                      .style('opacity', 0.6);
5050
5051                  outlier.exit().transition()
5052                      .duration(duration)
5053                      .delay(delay)
5054                      .attr('cy', 0) //function (i) { return x1(d[i]); })
5055                      .style('opacity', 1e-6)
5056                      .remove();
5057              }
5058
5059              // Update Values
5060              if (renderDataPoints) {
5061                  const point = _g.selectAll('circle.data')
5062                      .data(pointIndices);
5063
5064                  point.enter().insert('circle', 'text')
5065                      .attr('class', 'data')
5066                      .attr('r', dataRadius)
5067                      .attr('cx', () => Math.floor(Math.random() *
5068                              (width * dataWidthPortion) +
5069                              1 + ((width - (width * dataWidthPortion)) / 2)))
5070                      .attr('cy', i => x0(data[i]))
5071                      .style('opacity', 1e-6)
5072                      .transition()
5073                      .duration(duration)
5074                      .delay(delay)
5075                      .attr('cy', i => x1(data[i]))
5076                      .style('opacity', dataOpacity);
5077
5078                  if (renderTitle) {
5079                      point.selectAll('title').remove();
5080                      point.append('title').text(i => data[i]);
5081                  }
5082
5083                  point.transition()
5084                      .duration(duration)
5085                      .delay(delay)
5086                      .attr('cx', () => Math.floor(Math.random() *
5087                              (width * dataWidthPortion) +
5088                              1 + ((width - (width * dataWidthPortion)) / 2)))
5089                      .attr('cy', i => x1(data[i]))
5090                      .style('opacity', dataOpacity);
5091
5092                  point.exit().transition()
5093                      .duration(duration)
5094                      .delay(delay)
5095                      .attr('cy', 0)
5096                      .style('opacity', 1e-6)
5097                      .remove();
5098              }
5099
5100              // Compute the tick format.
5101              const format = tickFormat || x1.tickFormat(8);
5102
5103              // Update box ticks.
5104              const boxTick = _g.selectAll('text.box')
5105                  .data(quartileData);
5106
5107              boxTick.enter().append('text')
5108                  .attr('class', 'box')
5109                  .attr('dy', '.3em')
5110                  .attr('dx', (d, i) => i & 1 ? 6 : -6)
5111                  .attr('x', (d, i) => i & 1 ? width : 0)
5112                  .attr('y', x0)
5113                  .attr('text-anchor', (d, i) => i & 1 ? 'start' : 'end')
5114                  .text(format)
5115                  .transition()
5116                  .duration(duration)
5117                  .delay(delay)
5118                  .attr('y', x1);
5119
5120              boxTick.transition()
5121                  .duration(duration)
5122                  .delay(delay)
5123                  .text(format)
5124                  .attr('x', (d, i) => i & 1 ? width : 0)
5125                  .attr('y', x1);
5126
5127              // Update whisker ticks. These are handled separately from the box
5128              // ticks because they may or may not exist, and we want don't want
5129              // to join box ticks pre-transition with whisker ticks post-.
5130              const whiskerTick = _g.selectAll('text.whisker')
5131                  .data(whiskerData || []);
5132
5133              whiskerTick.enter().append('text')
5134                  .attr('class', 'whisker')
5135                  .attr('dy', '.3em')
5136                  .attr('dx', 6)
5137                  .attr('x', width)
5138                  .attr('y', x0)
5139                  .text(format)
5140                  .style('opacity', 1e-6)
5141                  .transition()
5142                  .duration(duration)
5143                  .delay(delay)
5144                  .attr('y', x1)
5145                  .style('opacity', 1);
5146
5147              whiskerTick.transition()
5148                  .duration(duration)
5149                  .delay(delay)
5150                  .text(format)
5151                  .attr('x', width)
5152                  .attr('y', x1)
5153                  .style('opacity', 1);
5154
5155              whiskerTick.exit().transition()
5156                  .duration(duration)
5157                  .delay(delay)
5158                  .attr('y', x1)
5159                  .style('opacity', 1e-6)
5160                  .remove();
5161
5162              // Remove temporary quartiles element from within data array.
5163              delete data.quartiles;
5164          });
5165          d3Timer.timerFlush();
5166      }
5167
5168      box.width = function (x) {
5169          if (!arguments.length) {
5170              return width;
5171          }
5172          width = x;
5173          return box;
5174      };
5175
5176      box.height = function (x) {
5177          if (!arguments.length) {
5178              return height;
5179          }
5180          height = x;
5181          return box;
5182      };
5183
5184      box.tickFormat = function (x) {
5185          if (!arguments.length) {
5186              return tickFormat;
5187          }
5188          tickFormat = x;
5189          return box;
5190      };
5191
5192      box.showOutliers = function (x) {
5193          if (!arguments.length) {
5194              return showOutliers;
5195          }
5196          showOutliers = x;
5197          return box;
5198      };
5199
5200      box.boldOutlier = function (x) {
5201          if (!arguments.length) {
5202              return boldOutlier;
5203          }
5204          boldOutlier = x;
5205          return box;
5206      };
5207
5208      box.renderDataPoints = function (x) {
5209          if (!arguments.length) {
5210              return renderDataPoints;
5211          }
5212          renderDataPoints = x;
5213          return box;
5214      };
5215
5216      box.renderTitle = function (x) {
5217          if (!arguments.length) {
5218              return renderTitle;
5219          }
5220          renderTitle = x;
5221          return box;
5222      };
5223
5224      box.dataOpacity = function (x) {
5225          if (!arguments.length) {
5226              return dataOpacity;
5227          }
5228          dataOpacity = x;
5229          return box;
5230      };
5231
5232      box.dataWidthPortion = function (x) {
5233          if (!arguments.length) {
5234              return dataWidthPortion;
5235          }
5236          dataWidthPortion = x;
5237          return box;
5238      };
5239
5240      box.duration = function (x) {
5241          if (!arguments.length) {
5242              return duration;
5243          }
5244          duration = x;
5245          return box;
5246      };
5247
5248      box.domain = function (x) {
5249          if (!arguments.length) {
5250              return domain;
5251          }
5252          domain = x === null ? x : typeof x === 'function' ? x : utils.constant(x);
5253          return box;
5254      };
5255
5256      box.value = function (x) {
5257          if (!arguments.length) {
5258              return value;
5259          }
5260          value = x;
5261          return box;
5262      };
5263
5264      box.whiskers = function (x) {
5265          if (!arguments.length) {
5266              return whiskers;
5267          }
5268          whiskers = x;
5269          return box;
5270      };
5271
5272      box.quartiles = function (x) {
5273          if (!arguments.length) {
5274              return quartiles;
5275          }
5276          quartiles = x;
5277          return box;
5278      };
5279
5280      return box;
5281  };
5282
5283  function boxWhiskers (d) {
5284      return [0, d.length - 1];
5285  }
5286
5287  function boxQuartiles (d) {
5288      return [
5289          d3Array.quantile(d, 0.25),
5290          d3Array.quantile(d, 0.5),
5291          d3Array.quantile(d, 0.75)
5292      ];
5293  }
5294
5295  /**
5296   * Stack Mixin is an mixin that provides cross-chart support of stackability using d3.stack.
5297   * @mixin StackMixin
5298   * @mixes CoordinateGridMixin
5299   */
5300  class StackMixin extends CoordinateGridMixin {
5301      constructor () {
5302          super();
5303
5304          this._stackLayout = d3Shape.stack();
5305
5306          this._stack = [];
5307          this._titles = {};
5308
5309          this._hidableStacks = false;
5310          this._evadeDomainFilter = false;
5311
5312          this.data(() => {
5313              const layers = this._stack.filter(this._visibility);
5314              if (!layers.length) {
5315                  return [];
5316              }
5317              layers.forEach((l, i) => this._prepareValues(l, i));
5318              const v4data = layers[0].values.map((v, i) => {
5319                  const col = {x: v.x};
5320                  layers.forEach(layer => {
5321                      col[layer.name] = layer.values[i].y;
5322                  });
5323                  return col;
5324              });
5325              const keys = layers.map(layer => layer.name);
5326              const v4result = this.stackLayout().keys(keys)(v4data);
5327              v4result.forEach((series, i) => {
5328                  series.forEach((ys, j) => {
5329                      layers[i].values[j].y0 = ys[0];
5330                      layers[i].values[j].y1 = ys[1];
5331                  });
5332              });
5333              return layers;
5334          });
5335
5336          this.colorAccessor(function (d) {
5337              return this.layer || this.name || d.name || d.layer;
5338          });
5339      }
5340
5341      _prepareValues (layer, layerIdx) {
5342          const valAccessor = layer.accessor || this.valueAccessor();
5343          layer.name = String(layer.name || layerIdx);
5344          const allValues = layer.group.all().map((d, i) => ({
5345              x: this.keyAccessor()(d, i),
5346              y: layer.hidden ? null : valAccessor(d, i),
5347              data: d,
5348              layer: layer.name,
5349              hidden: layer.hidden
5350          }));
5351
5352          layer.domainValues = allValues.filter(l => this._domainFilter()(l));
5353          layer.values = this.evadeDomainFilter() ? allValues : layer.domainValues;
5354      }
5355
5356      _domainFilter () {
5357          if (!this.x()) {
5358              return utils.constant(true);
5359          }
5360          const xDomain = this.x().domain();
5361          if (this.isOrdinal()) {
5362              // TODO #416
5363              //var domainSet = d3.set(xDomain);
5364              return () => true //domainSet.has(p.x);
5365              ;
5366          }
5367          if (this.elasticX()) {
5368              return () => true;
5369          }
5370          return p => p.x >= xDomain[0] && p.x <= xDomain[xDomain.length - 1];
5371      }
5372
5373      /**
5374       * Stack a new crossfilter group onto this chart with an optional custom value accessor. All stacks
5375       * in the same chart will share the same key accessor and therefore the same set of keys.
5376       *
5377       * For example, in a stacked bar chart, the bars of each stack will be positioned using the same set
5378       * of keys on the x axis, while stacked vertically. If name is specified then it will be used to
5379       * generate the legend label.
5380       * @see {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group-map-reduce crossfilter.group}
5381       * @example
5382       * // stack group using default accessor
5383       * chart.stack(valueSumGroup)
5384       * // stack group using custom accessor
5385       * .stack(avgByDayGroup, function(d){return d.value.avgByDay;});
5386       * @param {crossfilter.group} group
5387       * @param {String} [name]
5388       * @param {Function} [accessor]
5389       * @returns {Array<{group: crossfilter.group, name: String, accessor: Function}>|StackMixin}
5390       */
5391      stack (group, name, accessor) {
5392          if (!arguments.length) {
5393              return this._stack;
5394          }
5395
5396          if (arguments.length <= 2) {
5397              accessor = name;
5398          }
5399
5400          const layer = {group: group};
5401          if (typeof name === 'string') {
5402              layer.name = name;
5403          }
5404          if (typeof accessor === 'function') {
5405              layer.accessor = accessor;
5406          }
5407          this._stack.push(layer);
5408
5409          return this;
5410      }
5411
5412      group (g, n, f) {
5413          if (!arguments.length) {
5414              return super.group();
5415          }
5416          this._stack = [];
5417          this._titles = {};
5418          this.stack(g, n);
5419          if (f) {
5420              this.valueAccessor(f);
5421          }
5422          return super.group(g, n);
5423      }
5424
5425      /**
5426       * Allow named stacks to be hidden or shown by clicking on legend items.
5427       * This does not affect the behavior of hideStack or showStack.
5428       * @param {Boolean} [hidableStacks=false]
5429       * @returns {Boolean|StackMixin}
5430       */
5431      hidableStacks (hidableStacks) {
5432          if (!arguments.length) {
5433              return this._hidableStacks;
5434          }
5435          this._hidableStacks = hidableStacks;
5436          return this;
5437      }
5438
5439      _findLayerByName (n) {
5440          const i = this._stack.map(pluck('name')).indexOf(n);
5441          return this._stack[i];
5442      }
5443
5444      /**
5445       * Hide all stacks on the chart with the given name.
5446       * The chart must be re-rendered for this change to appear.
5447       * @param {String} stackName
5448       * @returns {StackMixin}
5449       */
5450      hideStack (stackName) {
5451          const layer = this._findLayerByName(stackName);
5452          if (layer) {
5453              layer.hidden = true;
5454          }
5455          return this;
5456      }
5457
5458      /**
5459       * Show all stacks on the chart with the given name.
5460       * The chart must be re-rendered for this change to appear.
5461       * @param {String} stackName
5462       * @returns {StackMixin}
5463       */
5464      showStack (stackName) {
5465          const layer = this._findLayerByName(stackName);
5466          if (layer) {
5467              layer.hidden = false;
5468          }
5469          return this;
5470      }
5471
5472      getValueAccessorByIndex (index) {
5473          return this._stack[index].accessor || this.valueAccessor();
5474      }
5475
5476      yAxisMin () {
5477          const m = d3Array.min(this._flattenStack(), p => (p.y < 0) ? (p.y + p.y0) : p.y0);
5478          return utils.subtract(m, this.yAxisPadding());
5479      }
5480
5481      yAxisMax () {
5482          const m = d3Array.max(this._flattenStack(), p => (p.y > 0) ? (p.y + p.y0) : p.y0);
5483          return utils.add(m, this.yAxisPadding());
5484      }
5485
5486      _flattenStack () {
5487          // A round about way to achieve flatMap
5488          // When target browsers support flatMap, just replace map -> flatMap, no concat needed
5489          const values = this.data().map(layer => layer.domainValues);
5490          return [].concat(...values);
5491      }
5492
5493      xAxisMin () {
5494          const m = d3Array.min(this._flattenStack(), pluck('x'));
5495          return utils.subtract(m, this.xAxisPadding(), this.xAxisPaddingUnit());
5496      }
5497
5498      xAxisMax () {
5499          const m = d3Array.max(this._flattenStack(), pluck('x'));
5500          return utils.add(m, this.xAxisPadding(), this.xAxisPaddingUnit());
5501      }
5502
5503      /**
5504       * Set or get the title function. Chart class will use this function to render svg title (usually interpreted by
5505       * browser as tooltips) for each child element in the chart, i.e. a slice in a pie chart or a bubble in a bubble chart.
5506       * Almost every chart supports title function however in grid coordinate chart you need to turn off brush in order to
5507       * use title otherwise the brush layer will block tooltip trigger.
5508       *
5509       * If the first argument is a stack name, the title function will get or set the title for that stack. If stackName
5510       * is not provided, the first stack is implied.
5511       * @example
5512       * // set a title function on 'first stack'
5513       * chart.title('first stack', function(d) { return d.key + ': ' + d.value; });
5514       * // get a title function from 'second stack'
5515       * var secondTitleFunction = chart.title('second stack');
5516       * @param {String} [stackName]
5517       * @param {Function} [titleAccessor]
5518       * @returns {String|StackMixin}
5519       */
5520      title (stackName, titleAccessor) {
5521          if (!stackName) {
5522              return super.title();
5523          }
5524
5525          if (typeof stackName === 'function') {
5526              return super.title(stackName);
5527          }
5528          if (stackName === this._groupName && typeof titleAccessor === 'function') {
5529              return super.title(titleAccessor);
5530          }
5531
5532          if (typeof titleAccessor !== 'function') {
5533              return this._titles[stackName] || super.title();
5534          }
5535
5536          this._titles[stackName] = titleAccessor;
5537
5538          return this;
5539      }
5540
5541      /**
5542       * Gets or sets the stack layout algorithm, which computes a baseline for each stack and
5543       * propagates it to the next.
5544       * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Stack-Layout.md d3.stackD3v3}
5545       * @param {Function} [_stack=d3.stackD3v3]
5546       * @returns {Function|StackMixin}
5547       */
5548      stackLayout (_stack) {
5549          if (!arguments.length) {
5550              return this._stackLayout;
5551          }
5552          this._stackLayout = _stack;
5553          return this;
5554      }
5555
5556      /**
5557       * Since dc.js 2.0, there has been {@link https://github.com/dc-js/dc.js/issues/949 an issue}
5558       * where points are filtered to the current domain. While this is a useful optimization, it is
5559       * incorrectly implemented: the next point outside the domain is required in order to draw lines
5560       * that are clipped to the bounds, as well as bars that are partly clipped.
5561       *
5562       * A fix will be included in dc.js 2.1.x, but a workaround is needed for dc.js 2.0 and until
5563       * that fix is published, so set this flag to skip any filtering of points.
5564       *
5565       * Once the bug is fixed, this flag will have no effect, and it will be deprecated.
5566       * @param {Boolean} [evadeDomainFilter=false]
5567       * @returns {Boolean|StackMixin}
5568       */
5569      evadeDomainFilter (evadeDomainFilter) {
5570          if (!arguments.length) {
5571              return this._evadeDomainFilter;
5572          }
5573          this._evadeDomainFilter = evadeDomainFilter;
5574          return this;
5575      }
5576
5577      _visibility (l) {
5578          return !l.hidden;
5579      }
5580
5581      _ordinalXDomain () {
5582          const flat = this._flattenStack().map(pluck('data'));
5583          const ordered = this._computeOrderedGroups(flat);
5584          return ordered.map(this.keyAccessor());
5585      }
5586
5587      legendables () {
5588          return this._stack.map((layer, i) => ({
5589              chart: this,
5590              name: layer.name,
5591              hidden: layer.hidden || false,
5592              color: this.getColor.call(layer, layer.values, i)
5593          }));
5594      }
5595
5596      isLegendableHidden (d) {
5597          const layer = this._findLayerByName(d.name);
5598          return layer ? layer.hidden : false;
5599      }
5600
5601      legendToggle (d) {
5602          if (this._hidableStacks) {
5603              if (this.isLegendableHidden(d)) {
5604                  this.showStack(d.name);
5605              } else {
5606                  this.hideStack(d.name);
5607              }
5608              //_chart.redraw();
5609              this.renderGroup();
5610          }
5611      }
5612  }
5613
5614  const MIN_BAR_WIDTH = 1;
5615  const DEFAULT_GAP_BETWEEN_BARS = 2;
5616  const LABEL_PADDING = 3;
5617
5618  /**
5619   * Concrete bar chart/histogram implementation.
5620   *
5621   * Examples:
5622   * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
5623   * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats}
5624   * @mixes StackMixin
5625   */
5626  class BarChart extends StackMixin {
5627      /**
5628       * Create a Bar Chart
5629       * @example
5630       * // create a bar chart under #chart-container1 element using the default global chart group
5631       * var chart1 = new BarChart('#chart-container1');
5632       * // create a bar chart under #chart-container2 element using chart group A
5633       * var chart2 = new BarChart('#chart-container2', 'chartGroupA');
5634       * // create a sub-chart under a composite parent chart
5635       * var chart3 = new BarChart(compositeChart);
5636       * @param {String|node|d3.selection|CompositeChart} parent - Any valid
5637       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector}
5638       * specifying a dom block element such as a div; or a dom element or d3 selection.  If the bar
5639       * chart is a sub-chart in a {@link CompositeChart Composite Chart} then pass in the parent
5640       * composite chart instance instead.
5641       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
5642       * Interaction with a chart will only trigger events and redraws within the chart's group.
5643       */
5644      constructor (parent, chartGroup) {
5645          super();
5646
5647          this._gap = DEFAULT_GAP_BETWEEN_BARS;
5648          this._centerBar = false;
5649          this._alwaysUseRounding = false;
5650
5651          this._barWidth = undefined;
5652
5653          this.label(d => utils.printSingleValue(d.y0 + d.y), false);
5654
5655          this.anchor(parent, chartGroup);
5656      }
5657
5658      /**
5659       * Get or set the outer padding on an ordinal bar chart. This setting has no effect on non-ordinal charts.
5660       * Will pad the width by `padding * barWidth` on each side of the chart.
5661       * @param {Number} [padding=0.5]
5662       * @returns {Number|BarChart}
5663       */
5664      outerPadding (padding) {
5665          if (!arguments.length) {
5666              return this._outerRangeBandPadding();
5667          }
5668          return this._outerRangeBandPadding(padding);
5669      }
5670
5671      rescale () {
5672          super.rescale();
5673          this._barWidth = undefined;
5674          return this;
5675      }
5676
5677      render () {
5678          if (this.round() && this._centerBar && !this._alwaysUseRounding) {
5679              logger.warn('By default, brush rounding is disabled if bars are centered. ' +
5680                  'See dc.js bar chart API documentation for details.');
5681          }
5682
5683          return super.render();
5684      }
5685
5686      plotData () {
5687          let layers = this.chartBodyG().selectAll('g.stack')
5688              .data(this.data());
5689
5690          this._calculateBarWidth();
5691
5692          layers = layers
5693              .enter()
5694              .append('g')
5695              .attr('class', (d, i) => `stack _${i}`)
5696              .merge(layers);
5697
5698          const last = layers.size() - 1;
5699          {
5700              const chart = this;
5701              layers.each(function (d, i) {
5702                  const layer = d3Selection.select(this);
5703
5704                  chart._renderBars(layer, i, d);
5705
5706                  if (chart.renderLabel() && last === i) {
5707                      chart._renderLabels(layer, i, d);
5708                  }
5709              });
5710          }
5711      }
5712
5713      _barHeight (d) {
5714          return utils.safeNumber(Math.abs(this.y()(d.y + d.y0) - this.y()(d.y0)));
5715      }
5716
5717      _labelXPos (d) {
5718          let x = this.x()(d.x);
5719          if (!this._centerBar) {
5720              x += this._barWidth / 2;
5721          }
5722          if (this.isOrdinal() && this._gap !== undefined) {
5723              x += this._gap / 2;
5724          }
5725          return utils.safeNumber(x);
5726      }
5727
5728      _labelYPos (d) {
5729          let y = this.y()(d.y + d.y0);
5730
5731          if (d.y < 0) {
5732              y -= this._barHeight(d);
5733          }
5734
5735          return utils.safeNumber(y - LABEL_PADDING);
5736      }
5737
5738      _renderLabels (layer, layerIndex, data) {
5739          const labels = layer.selectAll('text.barLabel')
5740              .data(data.values, pluck('x'));
5741
5742          const labelsEnterUpdate = labels
5743              .enter()
5744              .append('text')
5745              .attr('class', 'barLabel')
5746              .attr('text-anchor', 'middle')
5747              .attr('x', d => this._labelXPos(d))
5748              .attr('y', d => this._labelYPos(d))
5749              .merge(labels);
5750
5751          if (this.isOrdinal()) {
5752              labelsEnterUpdate.on('click', d3compat.eventHandler(d => this.onClick(d)));
5753              labelsEnterUpdate.attr('cursor', 'pointer');
5754          }
5755
5756          transition(labelsEnterUpdate, this.transitionDuration(), this.transitionDelay())
5757              .attr('x', d => this._labelXPos(d))
5758              .attr('y', d => this._labelYPos(d))
5759              .text(d =>
5759 this.label()(d));
5760
5761          transition(labels.exit(), this.transitionDuration(), this.transitionDelay())
5762              .attr('height', 0)
5763              .remove();
5764      }
5765
5766      _barXPos (d) {
5767          let x = this.x()(d.x);
5768          if (this._centerBar) {
5769              x -= this._barWidth / 2;
5770          }
5771          if (this.isOrdinal() && this._gap !== undefined) {
5772              x += this._gap / 2;
5773          }
5774          return utils.safeNumber(x);
5775      }
5776
5777      _renderBars (layer, layerIndex, data) {
5778          const bars = layer.selectAll('rect.bar')
5779              .data(data.values, pluck('x'));
5780
5781          const enter = bars.enter()
5782              .append('rect')
5783              .attr('class', 'bar')
5784              .classed('dc-tabbable', this._keyboardAccessible)
5785              .attr('fill', pluck('data', this.getColor))
5786              .attr('x', d => this._barXPos(d))
5787              .attr('y', this.yAxisHeight())
5788              .attr('height', 0);
5789
5790          const barsEnterUpdate = enter.merge(bars);
5791
5792          if (this.renderTitle()) {
5793              enter.append('title').text(pluck('data', this.title(data.name)));
5794          }
5795
5796          if (this.isOrdinal()) {
5797              barsEnterUpdate.on('click', d3compat.eventHandler(d => this.onClick(d)));
5798          }
5799
5800          if (this._keyboardAccessible) {
5801              this._makeKeyboardAccessible(this.onClick);
5802          }
5803
5804          transition(barsEnterUpdate, this.transitionDuration(), this.transitionDelay())
5805              .attr('x', d => this._barXPos(d))
5806              .attr('y', d => {
5807                  let y = this.y()(d.y + d.y0);
5808
5809                  if (d.y < 0) {
5810                      y -= this._barHeight(d);
5811                  }
5812
5813                  return utils.safeNumber(y);
5814              })
5815              .attr('width', this._barWidth)
5816              .attr('height', d => this._barHeight(d))
5817              .attr('fill', pluck('data', this.getColor))
5818              .select('title').text(pluck('data', this.title(data.name)));
5819
5820          transition(bars.exit(), this.transitionDuration(), this.transitionDelay())
5821              .attr('x', d => this.x()(d.x))
5822              .attr('width', this._barWidth * 0.9)
5823              .remove();
5824      }
5825
5826      _calculateBarWidth () {
5827          if (this._barWidth === undefined) {
5828              const numberOfBars = this.xUnitCount();
5829
5830              // please can't we always use rangeBands for bar charts?
5831              if (this.isOrdinal() && this._gap === undefined) {
5832                  this._barWidth = Math.floor(this.x().bandwidth());
5833              } else if (this._gap) {
5834                  this._barWidth = Math.floor((this.xAxisLength() - (numberOfBars - 1) * this._gap) / numberOfBars);
5835              } else {
5836                  this._barWidth = Math.floor(this.xAxisLength() / (1 + this.barPadding()) / numberOfBars);
5837              }
5838
5839              if (this._barWidth === Infinity || isNaN(this._barWidth) || this._barWidth < MIN_BAR_WIDTH) {
5840                  this._barWidth = MIN_BAR_WIDTH;
5841              }
5842          }
5843      }
5844
5845      fadeDeselectedArea (brushSelection) {
5846          const bars = this.chartBodyG().selectAll('rect.bar');
5847
5848          if (this.isOrdinal()) {
5849              if (this.hasFilter()) {
5850                  bars.classed(constants.SELECTED_CLASS, d => this.hasFilter(d.x));
5851                  bars.classed(constants.DESELECTED_CLASS, d => !this.hasFilter(d.x));
5852              } else {
5853                  bars.classed(constants.SELECTED_CLASS, false);
5854                  bars.classed(constants.DESELECTED_CLASS, false);
5855              }
5856          } else if (this.brushOn() || this.parentBrushOn()) {
5857              if (!this.brushIsEmpty(brushSelection)) {
5858                  const start = brushSelection[0];
5859                  const end = brushSelection[1];
5860
5861                  bars.classed(constants.DESELECTED_CLASS, d => d.x < start || d.x >= end);
5862              } else {
5863                  bars.classed(constants.DESELECTED_CLASS, false);
5864              }
5865          }
5866      }
5867
5868      /**
5869       * Whether the bar chart will render each bar centered around the data position on the x-axis.
5870       * @param {Boolean} [centerBar=false]
5871       * @returns {Boolean|BarChart}
5872       */
5873      centerBar (centerBar) {
5874          if (!arguments.length) {
5875              return this._centerBar;
5876          }
5877          this._centerBar = centerBar;
5878          return this;
5879      }
5880
5881      onClick (d) {
5882          super.onClick(d.data);
5883      }
5884
5885      /**
5886       * Get or set the spacing between bars as a fraction of bar size. Valid values are between 0-1.
5887       * Setting this value will also remove any previously set {@link BarChart#gap gap}. See the
5888       * {@link https://github.com/d3/d3-scale/blob/master/README.md#scaleBand d3 docs}
5889       * for a visual description of how the padding is applied.
5890       * @param {Number} [barPadding=0]
5891       * @returns {Number|BarChart}
5892       */
5893      barPadding (barPadding) {
5894          if (!arguments.length) {
5895              return this._rangeBandPadding();
5896          }
5897          this._rangeBandPadding(barPadding);
5898          this._gap = undefined;
5899          return this;
5900      }
5901
5902      _useOuterPadding () {
5903          return this._gap === undefined;
5904      }
5905
5906      /**
5907       * Manually set fixed gap (in px) between bars instead of relying on the default auto-generated
5908       * gap.  By default the bar chart implementation will calculate and set the gap automatically
5909       * based on the number of data points and the length of the x axis.
5910       * @param {Number} [gap=2]
5911       * @returns {Number|BarChart}
5912       */
5913      gap (gap) {
5914          if (!arguments.length) {
5915              return this._gap;
5916          }
5917          this._gap = gap;
5918          return this;
5919      }
5920
5921      extendBrush (brushSelection) {
5922          if (brushSelection && this.round() && (!this._centerBar || this._alwaysUseRounding)) {
5923              brushSelection[0] = this.round()(brushSelection[0]);
5924              brushSelection[1] = this.round()(brushSelection[1]);
5925          }
5926          return brushSelection;
5927      }
5928
5929      /**
5930       * Set or get whether rounding is enabled when bars are centered. If false, using
5931       * rounding with centered bars will result in a warning and rounding will be ignored.  This flag
5932       * has no effect if bars are not {@link BarChart#centerBar centered}.
5933       * When using standard d3.js rounding methods, the brush often doesn't align correctly with
5934       * centered bars since the bars are offset.  The rounding function must add an offset to
5935       * compensate, such as in the following example.
5936       * @example
5937       * chart.round(function(n) { return Math.floor(n) + 0.5; });
5938       * @param {Boolean} [alwaysUseRounding=false]
5939       * @returns {Boolean|BarChart}
5940       */
5941      alwaysUseRounding (alwaysUseRounding) {
5942          if (!arguments.length) {
5943              return this._alwaysUseRounding;
5944          }
5945          this._alwaysUseRounding = alwaysUseRounding;
5946          return this;
5947      }
5948
5949      legendHighlight (d) {
5950          const colorFilter = (color, inv) => function () {
5951              const item = d3Selection.select(this);
5952              const match = item.attr('fill') === color;
5953              return inv ? !match : match;
5954          };
5955
5956          if (!this.isLegendableHidden(d)) {
5957              this.g().selectAll('rect.bar')
5958                  .classed('highlight', colorFilter(d.color))
5959                  .classed('fadeout', colorFilter(d.color, true));
5960          }
5961      }
5962
5963      legendReset () {
5964          this.g().selectAll('rect.bar')
5965              .classed('highlight', false)
5966              .classed('fadeout', false);
5967      }
5968
5969      xAxisMax () {
5970          let max = super.xAxisMax();
5971          if ('resolution' in this.xUnits()) {
5972              const res = this.xUnits().resolution;
5973              max += res;
5974          }
5975          return max;
5976      }
5977  }
5978
5979  const barChart = (parent, chartGroup) => new BarChart(parent, chartGroup);
5980
5981  // Returns a function to compute the interquartile range.
5982  function defaultWhiskersIQR (k) {
5983      return d => {
5984          const q1 = d.quartiles[0];
5985          const q3 = d.quartiles[2];
5986          const iqr = (q3 - q1) * k;
5987
5988          let i = -1;
5989          let j = d.length;
5990
5991          do {
5992              ++i;
5993          } while (d[i] < q1 - iqr);
5994
5995          do {
5996              --j;
5997          } while (d[j] > q3 + iqr);
5998
5999          return [i, j];
6000      };
6001  }
6002
6003  /**
6004   * A box plot is a chart that depicts numerical data via their quartile ranges.
6005   *
6006   * Examples:
6007   * - {@link http://dc-js.github.io/dc.js/examples/boxplot-basic.html Boxplot Basic example}
6008   * - {@link http://dc-js.github.io/dc.js/examples/boxplot-enhanced.html Boxplot Enhanced example}
6009   * - {@link http://dc-js.github.io/dc.js/examples/boxplot-render-data.html Boxplot Render Data example}
6010   * - {@link http://dc-js.github.io/dc.js/examples/boxplot-time.html Boxplot time example}
6011   * @mixes CoordinateGridMixin
6012   */
6013  class BoxPlot extends CoordinateGridMixin {
6014      /**
6015       * Create a Box Plot.
6016       *
6017       * @example
6018       * // create a box plot under #chart-container1 element using the default global chart group
6019       * var boxPlot1 = new BoxPlot('#chart-container1');
6020       * // create a box plot under #chart-container2 element using chart group A
6021       * var boxPlot2 = new BoxPlot('#chart-container2', 'chartGroupA');
6022       * @param {String|node|d3.selection} parent - Any valid
6023       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
6024       * a dom block element such as a div; or a dom element or d3 selection.
6025       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
6026       * Interaction with a chart will only trigger events and redraws within the chart's group.
6027       */
6028      constructor (parent, chartGroup) {
6029          super();
6030
6031          this._whiskerIqrFactor = 1.5;
6032          this._whiskersIqr = defaultWhiskersIQR;
6033          this._whiskers = this._whiskersIqr(this._whiskerIqrFactor);
6034
6035          this._box = d3Box();
6036          this._tickFormat = null;
6037          this._renderDataPoints = false;
6038          this._dataOpacity = 0.3;
6039          this._dataWidthPortion = 0.8;
6040          this._showOutliers = true;
6041          this._boldOutlier = false;
6042
6043          // Used in yAxisMin and yAxisMax to add padding in pixel coordinates
6044          // so the min and max data points/whiskers are within the chart
6045          this._yRangePadding = 8;
6046
6047          this._boxWidth = (innerChartWidth, xUnits) => {
6048              if (this.isOrdinal()) {
6049                  return this.x().bandwidth();
6050              } else {
6051                  return innerChartWidth / (1 + this.boxPadding()) / xUnits;
6052              }
6053          };
6054
6055          // default to ordinal
6056          this.x(d3Scale.scaleBand());
6057          this.xUnits(units.ordinal);
6058
6059          // valueAccessor should return an array of values that can be coerced into numbers
6060          // or if data is overloaded for a static array of arrays, it should be `Number`.
6061          // Empty arrays are not included.
6062          this.data(group => group.all().map(d => {
6063              d.map = accessor => accessor.call(d, d);
6064              return d;
6065          }).filter(d => {
6066              const values = this.valueAccessor()(d);
6067              return values.length !== 0;
6068          }));
6069
6070          this.boxPadding(0.8);
6071          this.outerPadding(0.5);
6072
6073          this.anchor(parent, chartGroup);
6074      }
6075
6076      /**
6077       * Get or set the spacing between boxes as a fraction of box size. Valid values are within 0-1.
6078       * See the {@link https://github.com/d3/d3-scale/blob/master/README.md#scaleBand d3 docs}
6079       * for a visual description of how the padding is applied.
6080       * @see {@link https://github.com/d3/d3-scale/blob/master/README.md#scaleBand d3.scaleBand}
6081       * @param {Number} [padding=0.8]
6082       * @returns {Number|BoxPlot}
6083       */
6084      boxPadding (padding) {
6085          if (!arguments.length) {
6086              return this._rangeBandPadding();
6087          }
6088          return this._rangeBandPadding(padding);
6089      }
6090
6091      /**
6092       * Get or set the outer padding on an ordinal box chart. This setting has no effect on non-ordinal charts
6093       * or on charts with a custom {@link BoxPlot#boxWidth .boxWidth}. Will pad the width by
6094       * `padding * barWidth` on each side of the chart.
6095       * @param {Number} [padding=0.5]
6096       * @returns {Number|BoxPlot}
6097       */
6098      outerPadding (padding) {
6099          if (!arguments.length) {
6100              return this._outerRangeBandPadding();
6101          }
6102          return this._outerRangeBandPadding(padding);
6103      }
6104
6105      /**
6106       * Get or set the numerical width of the boxplot box. The width may also be a function taking as
6107       * parameters the chart width excluding the right and left margins, as well as the number of x
6108       * units.
6109       * @example
6110       * // Using numerical parameter
6111       * chart.boxWidth(10);
6112       * // Using function
6113       * chart.boxWidth((innerChartWidth, xUnits) { ... });
6114       * @param {Number|Function} [boxWidth=0.5]
6115       * @returns {Number|Function|BoxPlot}
6116       */
6117      boxWidth (boxWidth) {
6118          if (!arguments.length) {
6119              return this._boxWidth;
6120          }
6121          this._boxWidth = typeof boxWidth === 'function' ? boxWidth : utils.constant(boxWidth);
6122          return this;
6123      }
6124
6125      _boxTransform (d, i) {
6126          const xOffset = this.x()(this.keyAccessor()(d, i));
6127          return `translate(${xOffset}, 0)`;
6128      }
6129
6130      _preprocessData () {
6131          if (this.elasticX()) {
6132              this.x().domain([]);
6133          }
6134      }
6135
6136      plotData () {
6137          this._calculatedBoxWidth = this._boxWidth(this.effectiveWidth(), this.xUnitCount());
6138
6139          this._box.whiskers(this._whiskers)
6140              .width(this._calculatedBoxWidth)
6141              .height(this.effectiveHeight())
6142              .value(this.valueAccessor())
6143              .domain(this.y().domain())
6144              .duration(this.transitionDuration())
6145              .tickFormat(this._tickFormat)
6146              .renderDataPoints(this._renderDataPoints)
6147              .dataOpacity(this._dataOpacity)
6148              .dataWidthPortion(this._dataWidthPortion)
6149              .renderTitle(this.renderTitle())
6150              .showOutliers(this._showOutliers)
6151              .boldOutlier(this._boldOutlier);
6152
6153          const boxesG = this.chartBodyG().selectAll('g.box').data(this.data(), this.keyAccessor());
6154
6155          const boxesGEnterUpdate = this._renderBoxes(boxesG);
6156          this._updateBoxes(boxesGEnterUpdate);
6157          this._removeBoxes(boxesG);
6158
6159          this.fadeDeselectedArea(this.filter());
6160      }
6161
6162      _renderBoxes (boxesG) {
6163          const boxesGEnter = boxesG.enter().append('g');
6164
6165          boxesGEnter
6166              .attr('class', 'box')
6167              .classed('dc-tabbable', this._keyboardAccessible)
6168              .attr('transform', (d, i) => this._boxTransform(d, i))
6169              .call(this._box)
6170              .on('click', d3compat.eventHandler(d => {
6171                  this.filter(this.keyAccessor()(d));
6172                  this.redrawGroup();
6173              }))
6174              .selectAll('circle')
6175              .classed('dc-tabbable', this._keyboardAccessible);
6176
6177          if (this._keyboardAccessible) {
6178              this._makeKeyboardAccessible(this.onClick);
6179          }
6180
6181          return boxesGEnter.merge(boxesG);
6182      }
6183
6184      _updateBoxes (boxesG) {
6185          const chart = this;
6186          transition(boxesG, this.transitionDuration(), this.transitionDelay())
6187              .attr('transform', (d, i) => this._boxTransform(d, i))
6188              .call(this._box)
6189              .each(function (d) {
6190                  const color = chart.getColor(d, 0);
6191                  d3Selection.select(this).select('rect.box').attr('fill', color);
6192                  d3Selection.select(this).selectAll('circle.data').attr('fill', color);
6193              });
6194      }
6195
6196      _removeBoxes (boxesG) {
6197          boxesG.exit().remove().call(this._box);
6198      }
6199
6200      _minDataValue () {
6201          return d3Array.min(this.data(), e => d3Array.min(this.valueAccessor()(e)));
6202      }
6203
6204      _maxDataValue () {
6205          return d3Array.max(this.data(), e => d3Array.max(this.valueAccessor()(e)));
6206      }
6207
6208      _yAxisRangeRatio () {
6209          return ((this._maxDataValue() - this._minDataValue()) / this.effectiveHeight());
6210      }
6211
6212      onClick (d) {
6213          this.filter(this.keyAccessor()(d));
6214          this.redrawGroup();
6215      }
6216
6217      fadeDeselectedArea (brushSelection) {
6218          const chart = this;
6219          if (this.hasFilter()) {
6220              if (this.isOrdinal()) {
6221                  this.g().selectAll('g.box').each(function (d) {
6222                      if (chart.isSelectedNode(d)) {
6223                          chart.highlightSelected(this);
6224                      } else {
6225                          chart.fadeDeselected(this);
6226                      }
6227                  });
6228              } else {
6229                  if (!(this.brushOn() || this.parentBrushOn())) {
6230                      return;
6231                  }
6232                  const start = brushSelection[0];
6233                  const end = brushSelection[1];
6234                  this.g().selectAll('g.box').each(function (d) {
6235                      const key = chart.keyAccessor()(d);
6236                      if (key < start || key >= end) {
6237                          chart.fadeDeselected(this);
6238                      } else {
6239                          chart.highlightSelected(this);
6240                      }
6241                  });
6242              }
6243          } else {
6244              this.g().selectAll('g.box').each(function () {
6245                  chart.resetHighlight(this);
6246              });
6247          }
6248      }
6249
6250      isSelectedNode (d) {
6251          return this.hasFilter(this.keyAccessor()(d));
6252      }
6253
6254      yAxisMin () {
6255          const padding = this._yRangePadding * this._yAxisRangeRatio();
6256          return utils.subtract(this._minDataValue() - padding, this.yAxisPadding());
6257      }
6258
6259      yAxisMax () {
6260          const padding = this._yRangePadding * this._yAxisRangeRatio();
6261          return utils.add(this._maxDataValue() + padding, this.yAxisPadding());
6262      }
6263
6264      /**
6265       * Get or set the numerical format of the boxplot median, whiskers and quartile labels. Defaults
6266       * to integer formatting.
6267       * @example
6268       * // format ticks to 2 decimal places
6269       * chart.tickFormat(d3.format('.2f'));
6270       * @param {Function} [tickFormat]
6271       * @returns {Number|Function|BoxPlot}
6272       */
6273      tickFormat (tickFormat) {
6274          if (!arguments.length) {
6275              return this._tickFormat;
6276          }
6277          this._tickFormat = tickFormat;
6278          return this;
6279      }
6280
6281      /**
6282       * Get or set the amount of padding to add, in pixel coordinates, to the top and
6283       * bottom of the chart to accommodate box/whisker labels.
6284       * @example
6285       * // allow more space for a bigger whisker font
6286       * chart.yRangePadding(12);
6287       * @param {Function} [yRangePadding = 8]
6288       * @returns {Number|Function|BoxPlot}
6289       */
6290      yRangePadding (yRangePadding) {
6291          if (!arguments.length) {
6292              return this._yRangePadding;
6293          }
6294          this._yRangePadding = yRangePadding;
6295          return this;
6296      }
6297
6298      /**
6299       * Get or set whether individual data points will be rendered.
6300       * @example
6301       * // Enable rendering of individual data points
6302       * chart.renderDataPoints(true);
6303       * @param {Boolean} [show=false]
6304       * @returns {Boolean|BoxPlot}
6305       */
6306      renderDataPoints (show) {
6307          if (!arguments.length) {
6308              return this._renderDataPoints;
6309          }
6310          this._renderDataPoints = show;
6311          return this;
6312      }
6313
6314      /**
6315       * Get or set the opacity when rendering data.
6316       * @example
6317       * // If individual data points are rendered increase the opacity.
6318       * chart.dataOpacity(0.7);
6319       * @param {Number} [opacity=0.3]
6320       * @returns {Number|BoxPlot}
6321       */
6322      dataOpacity (opacity) {
6323          if (!arguments.length) {
6324              return this._dataOpacity;
6325          }
6326          this._dataOpacity = opacity;
6327          return this;
6328      }
6329
6330      /**
6331       * Get or set the portion of the width of the box to show data points.
6332       * @example
6333       * // If individual data points are rendered increase the data box.
6334       * chart.dataWidthPortion(0.9);
6335       * @param {Number} [percentage=0.8]
6336       * @returns {Number|BoxPlot}
6337       */
6338      dataWidthPortion (percentage) {
6339          if (!arguments.length) {
6340              return this._dataWidthPortion;
6341          }
6342          this._dataWidthPortion = percentage;
6343          return this;
6344      }
6345
6346      /**
6347       * Get or set whether outliers will be rendered.
6348       * @example
6349       * // Disable rendering of outliers
6350       * chart.showOutliers(false);
6351       * @param {Boolean} [show=true]
6352       * @returns {Boolean|BoxPlot}
6353       */
6354      showOutliers (show) {
6355          if (!arguments.length) {
6356              return this._showOutliers;
6357          }
6358          this._showOutliers = show;
6359          return this;
6360      }
6361
6362      /**
6363       * Get or set whether outliers will be drawn bold.
6364       * @example
6365       * // If outliers are rendered display as bold
6366       * chart.boldOutlier(true);
6367       * @param {Boolean} [show=false]
6368       * @returns {Boolean|BoxPlot}
6369       */
6370      boldOutlier (show) {
6371          if (!arguments.length) {
6372              return this._boldOutlier;
6373          }
6374          this._boldOutlier = show;
6375          return this;
6376      }
6377  }
6378
6379  const boxPlot = (parent, chartGroup) => new BoxPlot(parent, chartGroup);
6380
6381  /**
6382   * A concrete implementation of a general purpose bubble chart that allows data visualization using the
6383   * following dimensions:
6384   * - x axis position
6385   * - y axis position
6386   * - bubble radius
6387   * - color
6388   *
6389   * Examples:
6390   * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
6391   * - {@link http://dc-js.github.com/dc.js/vc/index.html US Venture Capital Landscape 2011}
6392   * @mixes BubbleMixin
6393   * @mixes CoordinateGridMixin
6394   */
6395  class BubbleChart extends BubbleMixin(CoordinateGridMixin) {
6396      /**
6397       * Create a Bubble Chart.
6398       *
6399       * @example
6400       * // create a bubble chart under #chart-container1 element using the default global chart group
6401       * var bubbleChart1 = new BubbleChart('#chart-container1');
6402       * // create a bubble chart under #chart-container2 element using chart group A
6403       * var bubbleChart2 = new BubbleChart('#chart-container2', 'chartGroupA');
6404       * @param {String|node|d3.selection} parent - Any valid
6405       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
6406       * a dom block element such as a div; or a dom element or d3 selection.
6407       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
6408       * Interaction with a chart will only trigger events and redraws within the chart'
6408s group.
6409       */
6410      constructor (parent, chartGroup) {
6411          super();
6412
6413          this.transitionDuration(750);
6414
6415          this.transitionDelay(0);
6416
6417          this.anchor(parent, chartGroup);
6418      }
6419
6420      _bubbleLocator (d) {
6421          return `translate(${this._bubbleX(d)},${this._bubbleY(d)})`;
6422      }
6423
6424      plotData () {
6425          this.calculateRadiusDomain();
6426          this.r().range([this.MIN_RADIUS, this.xAxisLength() * this.maxBubbleRelativeSize()]);
6427
6428          const data = this.data();
6429          let bubbleG = this.chartBodyG().selectAll(`g.${this.BUBBLE_NODE_CLASS}`)
6430              .data(data, d => d.key);
6431          if (this.sortBubbleSize() || this.keyboardAccessible()) {
6432              // update dom order based on sort
6433              bubbleG.order();
6434          }
6435
6436          this._removeNodes(bubbleG);
6437
6438          bubbleG = this._renderNodes(bubbleG);
6439
6440          this._updateNodes(bubbleG);
6441
6442          this.fadeDeselectedArea(this.filter());
6443      }
6444
6445      _renderNodes (bubbleG) {
6446          const bubbleGEnter = bubbleG.enter().append('g');
6447
6448          bubbleGEnter
6449              .attr('class', this.BUBBLE_NODE_CLASS)
6450              .attr('transform', d => this._bubbleLocator(d))
6451              .append('circle').attr('class', (d, i) => `${this.BUBBLE_CLASS} _${i}`)
6452              .on('click', d3compat.eventHandler(d => this.onClick(d)))
6453              .classed('dc-tabbable', this._keyboardAccessible)
6454              .attr('fill', this.getColor)
6455              .attr('r', 0);
6456
6457          bubbleG = bubbleGEnter.merge(bubbleG);
6458
6459          transition(bubbleG, this.transitionDuration(), this.transitionDelay())
6460              .select(`circle.${this.BUBBLE_CLASS}`)
6461              .attr('r', d => this.bubbleR(d))
6462              .attr('opacity', d => (this.bubbleR(d) > 0) ? 1 : 0);
6463
6464          if (this._keyboardAccessible) {
6465              this._makeKeyboardAccessible(this.onClick);
6466          }
6467
6468          this._doRenderLabel(bubbleGEnter);
6469
6470          this._doRenderTitles(bubbleGEnter);
6471
6472          return bubbleG;
6473      }
6474
6475      _updateNodes (bubbleG) {
6476          transition(bubbleG, this.transitionDuration(), this.transitionDelay())
6477              .attr('transform', d => this._bubbleLocator(d))
6478              .select(`circle.${this.BUBBLE_CLASS}`)
6479              .attr('fill', this.getColor)
6480              .attr('r', d => this.bubbleR(d))
6481              .attr('opacity', d => (this.bubbleR(d) > 0) ? 1 : 0);
6482
6483          this.doUpdateLabels(bubbleG);
6484          this.doUpdateTitles(bubbleG);
6485      }
6486
6487      _removeNodes (bubbleG) {
6488          bubbleG.exit().remove();
6489      }
6490
6491      _bubbleX (d) {
6492          let x = this.x()(this.keyAccessor()(d));
6493          if (isNaN(x) || !isFinite(x)) {
6494              x = 0;
6495          }
6496          return x;
6497      }
6498
6499      _bubbleY (d) {
6500          let y = this.y()(this.valueAccessor()(d));
6501          if (isNaN(y) || !isFinite(y)) {
6502              y = 0;
6503          }
6504          return y;
6505      }
6506
6507      renderBrush () {
6508          // override default x axis brush from parent chart
6509      }
6510
6511      redrawBrush (brushSelection, doTransition) {
6512          // override default x axis brush from parent chart
6513          this.fadeDeselectedArea(brushSelection);
6514      }
6515  }
6516
6517  const bubbleChart = (parent, chartGroup) => new BubbleChart(parent, chartGroup);
6518
6519  const BUBBLE_OVERLAY_CLASS = 'bubble-overlay';
6520  const BUBBLE_NODE_CLASS = 'node';
6521  const BUBBLE_CLASS = 'bubble';
6522
6523  /**
6524   * The bubble overlay chart is quite different from the typical bubble chart. With the bubble overlay
6525   * chart you can arbitrarily place bubbles on an existing svg or bitmap image, thus changing the
6526   * typical x and y positioning while retaining the capability to visualize data using bubble radius
6527   * and coloring.
6528   *
6529   * Examples:
6530   * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats}
6531   * @mixes BubbleMixin
6532   * @mixes BaseMixin
6533   */
6534  class BubbleOverlay extends BubbleMixin(BaseMixin) {
6535      /**
6536       * Create a Bubble Overlay.
6537       *
6538       * @example
6539       * // create a bubble overlay chart on top of the '#chart-container1 svg' element using the default global chart group
6540       * var bubbleChart1 = BubbleOverlayChart('#chart-container1').svg(d3.select('#chart-container1 svg'));
6541       * // create a bubble overlay chart on top of the '#chart-container2 svg' element using chart group A
6542       * var bubbleChart2 = new CompositeChart('#chart-container2', 'chartGroupA').svg(d3.select('#chart-container2 svg'));
6543       * @param {String|node|d3.selection} parent - Any valid
6544       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
6545       * a dom block element such as a div; or a dom element or d3 selection.
6546       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
6547       * Interaction with a chart will only trigger events and redraws within the chart's group.
6548       */
6549      constructor (parent, chartGroup) {
6550          super();
6551
6552          /**
6553           * **mandatory**
6554           *
6555           * Set the underlying svg image element. Unlike other dc charts this chart will not generate a svg
6556           * element; therefore the bubble overlay chart will not work if this function is not invoked. If the
6557           * underlying image is a bitmap, then an empty svg will need to be created on top of the image.
6558           * @example
6559           * // set up underlying svg element
6560           * chart.svg(d3.select('#chart svg'));
6561           * @param {SVGElement|d3.selection} [imageElement]
6562           * @returns {BubbleOverlay}
6563           */
6564          this._g = undefined;
6565          this._points = [];
6566          this._keyboardAccessible = false;
6567
6568          this.transitionDuration(750);
6569
6570          this.transitionDelay(0);
6571
6572          this.radiusValueAccessor(d => d.value);
6573
6574          this.anchor(parent, chartGroup);
6575      }
6576
6577      /**
6578       * **mandatory**
6579       *
6580       * Set up a data point on the overlay. The name of a data point should match a specific 'key' among
6581       * data groups generated using keyAccessor.  If a match is found (point name <-> data group key)
6582       * then a bubble will be generated at the position specified by the function. x and y
6583       * value specified here are relative to the underlying svg.
6584       * @param {String} name
6585       * @param {Number} x
6586       * @param {Number} y
6587       * @returns {BubbleOverlay}
6588       */
6589      point (name, x, y) {
6590          this._points.push({name: name, x: x, y: y});
6591          return this;
6592      }
6593
6594      _doRender () {
6595          this._g = this._initOverlayG();
6596
6597          this.r().range([this.MIN_RADIUS, this.width() * this.maxBubbleRelativeSize()]);
6598
6599          this._initializeBubbles();
6600
6601          this.fadeDeselectedArea(this.filter());
6602
6603          return this;
6604      }
6605
6606      _initOverlayG () {
6607          this._g = this.select(`g.${BUBBLE_OVERLAY_CLASS}`);
6608          if (this._g.empty()) {
6609              this._g = this.svg().append('g').attr('class', BUBBLE_OVERLAY_CLASS);
6610          }
6611          return this._g;
6612      }
6613
6614      _initializeBubbles () {
6615          const data = this._mapData();
6616          this.calculateRadiusDomain();
6617
6618          this._points.forEach(point => {
6619              const nodeG = this._getNodeG(point, data);
6620
6621              let circle = nodeG.select(`circle.${BUBBLE_CLASS}`);
6622
6623              if (circle.empty()) {
6624                  circle = nodeG.append('circle')
6625                      .attr('class', BUBBLE_CLASS)
6626                      .classed('dc-tabbable', this._keyboardAccessible)
6627                      .attr('r', 0)
6628                      .attr('fill', this.getColor)
6629                      .on('click', d3compat.eventHandler(d => this.onClick(d)));
6630              }
6631
6632              if (this._keyboardAccessible) {
6633                  this._makeKeyboardAccessible(this.onClick);
6634              }
6635
6636              transition(circle, this.transitionDuration(), this.transitionDelay())
6637                  .attr('r', d => this.bubbleR(d));
6638
6639              this._doRenderLabel(nodeG);
6640
6641              this._doRenderTitles(nodeG);
6642          });
6643      }
6644
6645      _mapData () {
6646          const data = {};
6647          this.data().forEach(datum => {
6648              data[this.keyAccessor()(datum)] = datum;
6649          });
6650          return data;
6651      }
6652
6653      _getNodeG (point, data) {
6654          const bubbleNodeClass = `${BUBBLE_NODE_CLASS} ${utils.nameToId(point.name)}`;
6655
6656          let nodeG = this._g.select(`g.${utils.nameToId(point.name)}`);
6657
6658          if (nodeG.empty()) {
6659              nodeG = this._g.append('g')
6660                  .attr('class', bubbleNodeClass)
6661                  .attr('transform', `translate(${point.x},${point.y})`);
6662          }
6663
6664          nodeG.datum(data[point.name]);
6665
6666          return nodeG;
6667      }
6668
6669      _doRedraw () {
6670          this._updateBubbles();
6671
6672          this.fadeDeselectedArea(this.filter());
6673
6674          return this;
6675      }
6676
6677      _updateBubbles () {
6678          const data = this._mapData();
6679          this.calculateRadiusDomain();
6680
6681          this._points.forEach(point => {
6682              const nodeG = this._getNodeG(point, data);
6683
6684              const circle = nodeG.select(`circle.${BUBBLE_CLASS}`);
6685
6686              transition(circle, this.transitionDuration(), this.transitionDelay())
6687                  .attr('r', d => this.bubbleR(d))
6688                  .attr('fill', this.getColor);
6689
6690              this.doUpdateLabels(nodeG);
6691
6692              this.doUpdateTitles(nodeG);
6693          });
6694      }
6695
6696      debug (flag) {
6697          if (flag) {
6698              let debugG = this.select(`g.${constants.DEBUG_GROUP_CLASS}`);
6699
6700              if (debugG.empty()) {
6701                  debugG = this.svg()
6702                      .append('g')
6703                      .attr('class', constants.DEBUG_GROUP_CLASS);
6704              }
6705
6706              const debugText = debugG.append('text')
6707                  .attr('x', 10)
6708                  .attr('y', 20);
6709
6710              debugG
6711                  .append('rect')
6712                  .attr('width', this.width())
6713                  .attr('height', this.height())
6714                  .on('mousemove', d3compat.eventHandler((d, evt) => {
6715                      const position = d3compat.pointer(evt, debugG.node());
6716                      const msg = `${position[0]}, ${position[1]}`;
6717                      debugText.text(msg);
6718                  }));
6719          } else {
6720              this.selectAll('.debug').remove();
6721          }
6722
6723          return this;
6724      }
6725
6726  }
6727
6728  const bubbleOverlay = (parent, chartGroup) => new BubbleOverlay(parent, chartGroup);
6729
6730  const GROUP_CSS_CLASS = 'dc-cbox-group';
6731  const ITEM_CSS_CLASS = 'dc-cbox-item';
6732
6733  /**
6734   * The CboxMenu is a simple widget designed to filter a dimension by
6735   * selecting option(s) from a set of HTML `<input />` elements. The menu can be
6736   * made into a set of radio buttons (single select) or checkboxes (multiple).
6737   * @mixes BaseMixin
6738   */
6739  class CboxMenu extends BaseMixin {
6740      /**
6741       * Create a Cbox Menu.
6742       *
6743       * @example
6744       * // create a cboxMenu under #cbox-container using the default global chart group
6745       * var cbox = new CboxMenu('#cbox-container')
6746       *                .dimension(states)
6747       *                .group(stateGroup);
6748       * // the option text can be set via the title() function
6749       * // by default the option text is '`key`: `value`'
6750       * cbox.title(function (d){
6751       *     return 'STATE: ' + d.key;
6752       * })
6753       * @param {String|node|d3.selection|CompositeChart} parent - Any valid
6754       * [d3 single selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements) specifying
6755       * a dom block element such as a div; or a dom element or d3 selection.
6756       * @param {String} [chartGroup] - The name of the chart group this widget should be placed in.
6757       * Interaction with the widget will only trigger events and redraws within its group.
6758       */
6759      constructor (parent, chartGroup) {
6760          super();
6761
6762          this._cbox = undefined;
6763          this._promptText = 'Select all';
6764          this._multiple = false;
6765          this._inputType = 'radio';
6766          this._promptValue = null;
6767
6768          this._uniqueId = utils.uniqueId();
6769
6770          this.data(group => group.all().filter(this._filterDisplayed));
6771
6772          // There is an accessor for this attribute, initialized with default value
6773          this._filterDisplayed = d => this.valueAccessor()(d) > 0;
6774
6775          this._order = (a, b) => {
6776              if (this.keyAccessor()(a) > this.keyAccessor()(b)) {
6777                  return 1;
6778              }
6779              if (this.keyAccessor()(a) < this.keyAccessor()(b)) {
6780                  return -1;
6781              }
6782              return 0;
6783          };
6784
6785          this.anchor(parent, chartGroup);
6786      }
6787
6788      _doRender () {
6789          return this._doRedraw();
6790      }
6791
6792      _doRedraw () {
6793          this.select('ul').remove();
6794          this._cbox = this.root()
6795              .append('ul')
6796              .classed(GROUP_CSS_CLASS, true);
6797          this._renderOptions();
6798
6799          if (this.hasFilter() && this._multiple) {
6800              this._cbox.selectAll('input')
6801              // adding `false` avoids failing test cases in phantomjs
6802                  .property('checked', d =>
6802 d && this.filters().indexOf(String(this.keyAccessor()(d))) >= 0 || false);
6803          } else if (this.hasFilter()) {
6804              this._cbox.selectAll('input')
6805                  .property('checked', d => {
6806                      if (!d) {
6807                          return false;
6808                      }
6809                      return this.keyAccessor()(d) === this.filter();
6810                  });
6811          }
6812          return this;
6813      }
6814
6815      _renderOptions () {
6816          let options = this._cbox
6817              .selectAll(`li.${ITEM_CSS_CLASS}`)
6818              .data(this.data(), d => this.keyAccessor()(d));
6819
6820          options.exit().remove();
6821
6822          options = options.enter()
6823              .append('li')
6824              .classed(ITEM_CSS_CLASS, true)
6825              .merge(options);
6826
6827          options
6828              .append('input')
6829              .attr('type', this._inputType)
6830              .attr('value', d => this.keyAccessor()(d))
6831              .attr('name', `domain_${this._uniqueId}`)
6832              .attr('id', (d, i) => `input_${this._uniqueId}_${i}`);
6833          options
6834              .append('label')
6835              .attr('for', (d, i) => `input_${this._uniqueId}_${i}`)
6836              .text(this.title());
6837
6838          const chart = this;
6839          // 'all' option
6840          if (this._multiple) {
6841              this._cbox
6842                  .append('li')
6843                  .append('input')
6844                  .attr('type', 'reset')
6845                  .text(this._promptText)
6846                  .on('click', d3compat.eventHandler(function (d, evt) {
6847                      return chart._onChange(d, evt, this);
6848                  }));
6849          } else {
6850              const li = this._cbox.append('li');
6851              li.append('input')
6852                  .attr('type', this._inputType)
6853                  .attr('value', this._promptValue)
6854                  .attr('name', `domain_${this._uniqueId}`)
6855                  .attr('id', (d, i) => `input_${this._uniqueId}_all`)
6856                  .property('checked', true);
6857              li.append('label')
6858                  .attr('for', (d, i) => `input_${this._uniqueId}_all`)
6859                  .text(this._promptText);
6860          }
6861
6862          this._cbox
6863              .selectAll(`li.${ITEM_CSS_CLASS}`)
6864              .sort(this._order);
6865
6866          this._cbox.on('change', d3compat.eventHandler(function (d, evt) {
6867              return chart._onChange(d, evt, this);
6868          }));
6869          return options;
6870      }
6871
6872      _onChange (d, evt, element) {
6873          let values;
6874
6875          const target = d3Selection.select(evt.target);
6876          let options;
6877
6878          if (!target.datum()) {
6879              values = this._promptValue || null;
6880          } else {
6881              options = d3Selection.select(element).selectAll('input')
6882                  .filter(function (o) {
6883                      if (o) {
6884                          return this.checked;
6885                      }
6886                  });
6887              values = options.nodes().map(option => option.value);
6888              // check if only prompt option is selected
6889              if (!this._multiple && values.length === 1) {
6890                  values = values[0];
6891              }
6892          }
6893          this.onChange(values);
6894      }
6895
6896      onChange (val) {
6897          if (val && this._multiple) {
6898              this.replaceFilter([val]);
6899          } else if (val) {
6900              this.replaceFilter(val);
6901          } else {
6902              this.filterAll();
6903          }
6904          events.trigger(() => {
6905              this.redrawGroup();
6906          });
6907      }
6908
6909      /**
6910       * Get or set the function that controls the ordering of option tags in the
6911       * cbox menu. By default options are ordered by the group key in ascending
6912       * order.
6913       * @param {Function} [order]
6914       * @returns {Function|CboxMenu}
6915       * @example
6916       * // order by the group's value
6917       * chart.order(function (a,b) {
6918       *     return a.value > b.value ? 1 : b.value > a.value ? -1 : 0;
6919       * });
6920       */
6921      order (order) {
6922          if (!arguments.length) {
6923              return this._order;
6924          }
6925          this._order = order;
6926          return this;
6927      }
6928
6929      /**
6930       * Get or set the text displayed in the options used to prompt selection.
6931       * @param {String} [promptText='Select all']
6932       * @returns {String|CboxMenu}
6933       * @example
6934       * chart.promptText('All states');
6935       */
6936      promptText (promptText) {
6937          if (!arguments.length) {
6938              return this._promptText;
6939          }
6940          this._promptText = promptText;
6941          return this;
6942      }
6943
6944      /**
6945       * Get or set the function that filters options prior to display. By default options
6946       * with a value of < 1 are not displayed.
6947       * @param {function} [filterDisplayed]
6948       * @returns {Function|CboxMenu}
6949       * @example
6950       * // display all options override the `filterDisplayed` function:
6951       * chart.filterDisplayed(function () {
6952       *     return true;
6953       * });
6954       */
6955      filterDisplayed (filterDisplayed) {
6956          if (!arguments.length) {
6957              return this._filterDisplayed;
6958          }
6959          this._filterDisplayed = filterDisplayed;
6960          return this;
6961      }
6962
6963      /**
6964       * Controls the type of input element. Setting it to true converts
6965       * the HTML `input` tags from radio buttons to checkboxes.
6966       * @param {boolean} [multiple=false]
6967       * @returns {Boolean|CboxMenu}
6968       * @example
6969       * chart.multiple(true);
6970       */
6971      multiple (multiple) {
6972          if (!arguments.length) {
6973              return this._multiple;
6974          }
6975          this._multiple = multiple;
6976          if (this._multiple) {
6977              this._inputType = 'checkbox';
6978          } else {
6979              this._inputType = 'radio';
6980          }
6981          return this;
6982      }
6983
6984      /**
6985       * Controls the default value to be used for
6986       * [dimension.filter](https://github.com/crossfilter/crossfilter/wiki/API-Reference#dimension_filter)
6987       * when only the prompt value is selected. If `null` (the default), no filtering will occur when
6988       * just the prompt is selected.
6989       * @param {?*} [promptValue=null]
6990       * @returns {*|CboxMenu}
6991       */
6992      promptValue (promptValue) {
6993          if (!arguments.length) {
6994              return this._promptValue;
6995          }
6996          this._promptValue = promptValue;
6997
6998          return this;
6999      }
7000  }
7001
7002  const cboxMenu = (parent, chartGroup) => new CboxMenu(parent, chartGroup);
7003
7004  const SUB_CHART_CLASS = 'sub';
7005  const DEFAULT_RIGHT_Y_AXIS_LABEL_PADDING = 12;
7006
7007  /**
7008   * Composite charts are a special kind of chart that render multiple charts on the same Coordinate
7009   * Grid. You can overlay (compose) different bar/line/area charts in a single composite chart to
7010   * achieve some quite flexible charting effects.
7011   * @mixes CoordinateGridMixin
7012   */
7013  class CompositeChart extends CoordinateGridMixin {
7014      /**
7015       * Create a Composite Chart.
7016       * @example
7017       * // create a composite chart under #chart-container1 element using the default global chart group
7018       * var compositeChart1 = new CompositeChart('#chart-container1');
7019       * // create a composite chart under #chart-container2 element using chart group A
7020       * var compositeChart2 = new CompositeChart('#chart-container2', 'chartGroupA');
7021       * @param {String|node|d3.selection} parent - Any valid
7022       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
7023       * a dom block element such as a div; or a dom element or d3 selection.
7024       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
7025       * Interaction with a chart will only trigger events and redraws within the chart's group.
7026       */
7027      constructor (parent, chartGroup) {
7028          super();
7029
7030          this._children = [];
7031
7032          this._childOptions = {};
7033
7034          this._shareColors = false;
7035          this._shareTitle = true;
7036          this._alignYAxes = false;
7037
7038          this._rightYAxis = d3Axis.axisRight();
7039          this._rightYAxisLabel = 0;
7040          this._rightYAxisLabelPadding = DEFAULT_RIGHT_Y_AXIS_LABEL_PADDING;
7041          this._rightY = undefined;
7042          this._rightAxisGridLines = false;
7043
7044          this._mandatoryAttributes([]);
7045          this.transitionDuration(500);
7046          this.transitionDelay(0);
7047
7048          this.on('filtered.dcjs-composite-chart', chart => {
7049              // Propagate the filters onto the children
7050              // Notice that on children the call is .replaceFilter and not .filter
7051              //   the reason is that _chart.filter() returns the entire current set of filters not just the last added one
7052              for (let i = 0; i < this._children.length; ++i) {
7053                  this._children[i].replaceFilter(this.filter());
7054              }
7055          });
7056
7057          this.anchor(parent, chartGroup);
7058      }
7059
7060      _generateG () {
7061          const g = super._generateG();
7062
7063          for (let i = 0; i < this._children.length; ++i) {
7064              const child = this._children[i];
7065
7066              this._generateChildG(child, i);
7067
7068              if (!child.dimension()) {
7069                  child.dimension(this.dimension());
7070              }
7071              if (!child.group()) {
7072                  child.group(this.group());
7073              }
7074
7075              child.chartGroup(this.chartGroup());
7076              child.svg(this.svg());
7077              child.xUnits(this.xUnits());
7078              child.transitionDuration(this.transitionDuration(), this.transitionDelay());
7079              child.parentBrushOn(this.brushOn());
7080              child.brushOn(false);
7081              child.renderTitle(this.renderTitle());
7082              child.elasticX(this.elasticX());
7083          }
7084
7085          return g;
7086      }
7087
7088      rescale () {
7089          super.rescale();
7090
7091          this._children.forEach(child => {
7092              child.rescale();
7093          });
7094
7095          return this;
7096      }
7097
7098      resizing (resizing) {
7099          if (!arguments.length) {
7100              return super.resizing();
7101          }
7102          super.resizing(resizing);
7103
7104          this._children.forEach(child => {
7105              child.resizing(resizing);
7106          });
7107
7108          return this;
7109      }
7110
7111      _prepareYAxis () {
7112          const left = (this._leftYAxisChildren().length !== 0);
7113          const right = (this._rightYAxisChildren().length !== 0);
7114          const ranges = this._calculateYAxisRanges(left, right);
7115
7116          if (left) {
7117              this._prepareLeftYAxis(ranges);
7118          }
7119          if (right) {
7120              this._prepareRightYAxis(ranges);
7121          }
7122
7123          if (this._leftYAxisChildren().length > 0 && !this._rightAxisGridLines) {
7124              this._renderHorizontalGridLinesForAxis(this.g(), this.y(), this.yAxis());
7125          } else if (this._rightYAxisChildren().length > 0) {
7126              this._renderHorizontalGridLinesForAxis(this.g(), this._rightY, this._rightYAxis);
7127          }
7128      }
7129
7130      renderYAxis () {
7131          if (this._leftYAxisChildren().length !== 0) {
7132              this.renderYAxisAt('y', this.yAxis(), this.margins().left);
7133              this.renderYAxisLabel('y', this.yAxisLabel(), -90);
7134          }
7135
7136          if (this._rightYAxisChildren().length !== 0) {
7137              this.renderYAxisAt('yr', this.rightYAxis(), this.width() - this.margins().right);
7138              this.renderYAxisLabel('yr', this.rightYAxisLabel(), 90, this.width() - this._rightYAxisLabelPadding);
7139          }
7140      }
7141
7142      _calculateYAxisRanges (left, right) {
7143          let lyAxisMin, lyAxisMax, ryAxisMin, ryAxisMax;
7144          let ranges;
7145
7146          if (left) {
7147              lyAxisMin = this._yAxisMin();
7148              lyAxisMax = this._yAxisMax();
7149          }
7150
7151          if (right) {
7152              ryAxisMin = this._rightYAxisMin();
7153              ryAxisMax = this._rightYAxisMax();
7154          }
7155
7156          if (this.alignYAxes() && left && right) {
7157              ranges = this._alignYAxisRanges(lyAxisMin, lyAxisMax, ryAxisMin, ryAxisMax);
7158          }
7159
7160          return ranges || {
7161              lyAxisMin: lyAxisMin,
7162              lyAxisMax: lyAxisMax,
7163              ryAxisMin: ryAxisMin,
7164              ryAxisMax: ryAxisMax
7165          };
7166      }
7167
7168      _alignYAxisRanges (lyAxisMin, lyAxisMax, ryAxisMin, ryAxisMax) {
7169          // since the two series will share a zero, each Y is just a multiple
7170          // of the other. and the ratio should be the ratio of the ranges of the
7171          // input data, so that they come out the same height. so we just min/max
7172
7173          // note: both ranges already include zero due to the stack mixin (#667)
7174          // if #667 changes, we can reconsider whether we want data height or
7175          // height from zero to be equal. and it will be possible for the axes
7176          // to be aligned but not visible.
7177          const extentRatio = (ryAxisMax - ryAxisMin) / (lyAxisMax - lyAxisMin);
7178
7179          return {
7180              lyAxisMin: Math.min(lyAxisMin, ryAxisMin / extentRatio),
7181              lyAxisMax: Math.max(lyAxisMax, ryAxisMax / extentRatio),
7182              ryAxisMin: Math.min(ryAxisMin, lyAxisMin * extentRatio),
7183              ryAxisMax: Math.max(ryAxisMax, lyAxisMax * extentRatio)
7184          };
7185      }
7186
7187      _prepareRightYAxis (ranges) {
7188          const needDomain = this.rightY() === undefined || this.elasticY(),
7189              needRange = needDomain || this.resizing();
7190          if (this.rightY() === undefined) {
7191              this.rightY(d3Scale.scaleLinear());
7192          }
7193          if (needDomain) {
7194              this.rightY().domain([ranges.ryAxisMin, ranges.ryAxisMax]);
7195          }
7196          if (needRange) {
7197              this.rightY().rangeRound([this.yAxisHeight(), 0]);
7198          }
7199
7200          this.rightY().range([this.yAxisHeight(), 0]);
7201          this.rightYAxis(this.rightYAxis().scale(this.rightY()));
7202
7203          // In D3v4 create a RightAxis
7204          // _chart.rightYAxis().orient('right');
7205      }
7206
7207      _prepareLeftYAxis (ranges) {
7208          const needDomain = this.y() === undefined || this.elasticY(),
7209              needRange = needDomain || this.resizing();
7210          if (this.y() === undefined) {
7211              this.y(d3Scale.scaleLinear());
7212          }
7213          if (needDomain) {
7214              this.y().domain([ranges.lyAxisMin, ranges.lyAxisMax]);
7215          }
7216          if (needRange) {
7217              this.y().rangeRound([this.yAxisHeight(), 0]);
7218          }
7219
7220          this.y().range([this.yAxisHeight(), 0]);
7221          this.yAxis(this.yAxis().scale(this.y()));
7222
7223          // In D3v4 create a LeftAxis
7224          // _chart.yAxis().orient('left');
7225      }
7226
7227      _generateChildG (child, i) {
7228          child._generateG(this.g());
7229          child.g().attr('class', `${SUB_CHART_CLASS} _${i}`);
7230      }
7231
7232      plotData () {
7233          for (let i = 0; i < this._children.length; ++i) {
7234              const child = this._children[i];
7235
7236              if (!child.g()) {
7237                  this._generateChildG(child, i);
7238              }
7239
7240              if (this._shareColors) {
7241                  child.colors(this.colors());
7242              }
7243
7244              child.x(this.x());
7245
7246              child.xAxis(this.xAxis());
7247
7248              if (child.useRightYAxis()) {
7249                  child.y(this.rightY());
7250                  child.yAxis(this.rightYAxis());
7251              } else {
7252                  child.y(this.y());
7253                  child.yAxis(this.yAxis());
7254              }
7255
7256              child.plotData();
7257
7258              child._activateRenderlets();
7259          }
7260      }
7261
7262      /**
7263       * Get or set whether to draw gridlines from the right y axis.  Drawing from the left y axis is the
7264       * default behavior. This option is only respected when subcharts with both left and right y-axes
7265       * are present.
7266       * @param {Boolean} [useRightAxisGridLines=false]
7267       * @returns {Boolean|CompositeChart}
7268       */
7269      useRightAxisGridLines (useRightAxisGridLines) {
7270          if (!arguments) {
7271              return this._rightAxisGridLines;
7272          }
7273
7274          this._rightAxisGridLines = useRightAxisGridLines;
7275          return this;
7276      }
7277
7278      /**
7279       * Get or set chart-specific options for all child charts. This is equivalent to calling
7280       * {@link BaseMixin#options .options} on each child chart.
7281       * @param {Object} [childOptions]
7282       * @returns {Object|CompositeChart}
7283       */
7284      childOptions (childOptions) {
7285          if (!arguments.length) {
7286              return this._childOptions;
7287          }
7288          this._childOptions = childOptions;
7289          this._children.forEach(child => {
7290              child.options(this._childOptions);
7291          });
7292          return this;
7293      }
7294
7295      fadeDeselectedArea (brushSelection) {
7296          if (this.brushOn()) {
7297              for (let i = 0; i < this._children.length; ++i) {
7298                  const child = this._children[i];
7299                  child.fadeDeselectedArea(brushSelection);
7300              }
7301          }
7302      }
7303
7304      /**
7305       * Set or get the right y axis label.
7306       * @param {String} [rightYAxisLabel]
7307       * @param {Number} [padding]
7308       * @returns {String|CompositeChart}
7309       */
7310      rightYAxisLabel (rightYAxisLabel, padding) {
7311          if (!arguments.length) {
7312              return this._rightYAxisLabel;
7313          }
7314          this._rightYAxisLabel = rightYAxisLabel;
7315          this.margins().right -= this._rightYAxisLabelPadding;
7316          this._rightYAxisLabelPadding = (padding === undefined) ? DEFAULT_RIGHT_Y_AXIS_LABEL_PADDING : padding;
7317          this.margins().right += this._rightYAxisLabelPadding;
7318          return this;
7319      }
7320
7321      /**
7322       * Combine the given charts into one single composite coordinate grid chart.
7323       * @example
7324       * moveChart.compose([
7325       *     // when creating sub-chart you need to pass in the parent chart
7326       *     new LineChart(moveChart)
7327       *         .group(indexAvgByMonthGroup) // if group is missing then parent's group will be used
7328       *         .valueAccessor(function (d){return d.value.avg;})
7329       *         // most of the normal functions will continue to work in a composed chart
7330       *         .renderArea(true)
7331       *         .stack(monthlyMoveGroup, function (d){return d.value;})
7332       *         .title(function (d){
7333       *             var value = d.value.avg?d.value.avg:d.value;
7334       *             if(isNaN(value)) value = 0;
7335       *             return dateFormat(d.key) + '\n' + numberFormat(value);
7336       *         }),
7337       *     new BarChart(moveChart)
7338       *         .group(volumeByMonthGroup)
7339       *         .centerBar(true)
7340       * ]);
7341       * @param {Array<Chart>} [subChartArray]
7342       * @returns {CompositeChart}
7343       */
7344      compose (subChartArray) {
7345          this._children = subChartArray;
7346          this._children.forEach(child => {
7347              child.height(this.height());
7348              child.width(this.width());
7349              child.margins(this.margins());
7350
7351              if (this._shareTitle) {
7352                  child.title(this.title());
7353              }
7354
7355              child.options(this._childOptions);
7356          });
7357          this.rescale();
7358          return this;
7359      }
7360
7361      _setChildrenProperty (prop, value) {
7362          this._children.forEach(child => {
7363              child[prop](value);
7364          });
7365      }
7366
7367      // properties passed through in compose()
7368      height (height) {
7369          if(!arguments.length) {
7370              return super.height();
7371          }
7372          super.height(height);
7373          this._setChildrenProperty('height', height);
7374          return this;
7375      }
7376
7377      width (width) {
7378          if(!arguments.length) {
7379              return super.width();
7380          }
7381          super.width(width);
7382          this._setChildrenProperty('width', width);
7383          return this;
7384      }
7385
7386      margins (margins) {
7387          if(!arguments.length) {
7388              return super.margins();
7389          }
7390          super.margins(margins);
7391          this._setChildrenProperty('margins', margins);
7392          return this;
7393      }
7394
7395      /**
7396       * Returns the child charts which are composed into the composite chart.
7397       * @returns {Array<BaseMixin>}
7398       */
7399      children () {
7400          return this._children;
7401      }
7402
7403      /**
7404       * Get or set color sharing for the chart. If set, the {@link ColorMixin#colors .colors()} value from this chart
7405       * will be shared with composed children. Additionally if the child chart implements
7406       * Stackable and has not set a custom .colorAccessor, then it will generate a color
7407       * specific to its order in the composition.
7408       * @param {Boolean} [shareColors=false]
7409       * @returns {Boolean|CompositeChart}
7410       */
7411      shareColors (shareColors) {
7412          if (!arguments.length) {
7413              return this._shareColors;
7414          }
7415          this._shareColors = shareColors;
7416          return this;
7417      }
7418
7419      /**
7420       * Get or set title sharing for the chart. If set, the {@link BaseMixin#title .title()} value from
7421       * this chart will be shared with composed children.
7422       *
7423       * Note: currently you must call this before `compose` or the child will still get the parent's
7424       * `title` function!
7425       * @param {Boolean} [shareTitle=true]
7426       * @returns {Boolean|CompositeChart}
7427       */
7428      shareTitle (shareTitle) {
7429          if (!arguments.length) {
7430              return this._shareTitle;
7431          }
7432          this._shareTitle = shareTitle;
7433          return this;
7434      }
7435
7436      /**
7437       * Get or set the y scale for the right axis. The right y scale is typically automatically
7438       * generated by the chart implementation.
7439       * @see {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale}
7440       * @param {d3.scale} [yScale]
7441       * @returns {d3.scale|CompositeChart}
7442       */
7443      rightY (yScale) {
7444          if (!arguments.length) {
7445              return this._rightY;
7446          }
7447          this._rightY = yScale;
7448          this.rescale();
7449          return this;
7450      }
7451
7452      /**
7453       * Get or set alignment between left and right y axes. A line connecting '0' on both y axis
7454       * will be parallel to x axis. This only has effect when {@link CoordinateGridMixin#elasticY elasticY} is true.
7455       * @param {Boolean} [alignYAxes=false]
7456       * @returns {Chart}
7457       */
7458      alignYAxes (alignYAxes) {
7459          if (!arguments.length) {
7460              return this._alignYAxes;
7461          }
7462          this._alignYAxes = alignYAxes;
7463          this.rescale();
7464          return this;
7465      }
7466
7467      _leftYAxisChildren () {
7468          return this._children.filter(child => !child.useRightYAxis());
7469      }
7470
7471      _rightYAxisChildren () {
7472          return this._children.filter(child => child.useRightYAxis());
7473      }
7474
7475      _getYAxisMin (charts) {
7476          return charts.map(c => c.yAxisMin());
7477      }
7478
7479      _yAxisMin () {
7480          return d3Array.min(this._getYAxisMin(this._leftYAxisChildren()));
7481      }
7482
7483      _rightYAxisMin () {
7484          return d3Array.min(this._getYAxisMin(this._rightYAxisChildren()));
7485      }
7486
7487      _getYAxisMax (charts) {
7488          return charts.map(c => c.yAxisMax());
7489      }
7490
7491      _yAxisMax () {
7492          return utils.add(d3Array.max(this._getYAxisMax(this._leftYAxisChildren())), this.yAxisPadding());
7493      }
7494
7495      _rightYAxisMax () {
7496          return utils.add(d3Array.max(this._getYAxisMax(this._rightYAxisChildren())), this.yAxisPadding());
7497      }
7498
7499      _getAllXAxisMinFromChildCharts () {
7500          return this._children.map(c => c.xAxisMin());
7501      }
7502
7503      xAxisMin () {
7504          return utils.subtract(d3Array.min(this._getAllXAxisMinFromChildCharts()), this.xAxisPadding(), this.xAxisPaddingUnit());
7505      }
7506
7507      _getAllXAxisMaxFromChildCharts () {
7508          return this._children.map(c => c.xAxisMax());
7509      }
7510
7511      xAxisMax () {
7512          return utils.add(d3Array.max(this._getAllXAxisMaxFromChildCharts()), this.xAxisPadding(), this.xAxisPaddingUnit());
7513      }
7514
7515      legendables () {
7516          return this._children.reduce((items, child) => {
7517              if (this._shareColors) {
7518                  child.colors(this.colors());
7519              }
7520              items.push.apply(items, child.legendables());
7521              return items;
7522          }, []);
7523      }
7524
7525      legendHighlight (d) {
7526          for (let j = 0; j < this._children.length; ++j) {
7527              const child = this._children[j];
7528              child.legendHighlight(d);
7529          }
7530      }
7531
7532      legendReset (d) {
7533          for (let j = 0; j < this._children.length; ++j) {
7534              const child = this._children[j];
7535              child.legendReset(d);
7536          }
7537      }
7538
7539      legendToggle () {
7540          console.log('composite should not be getting legendToggle itself');
7541      }
7542
7543      /**
7544       * Set or get the right y axis used by the composite chart. This function is most useful when y
7545       * axis customization is required. The y axis in dc.js is an instance of a
7546       * [d3.axisRight](https://github.com/d3/d3-axis/blob/master/README.md#axisRight) therefore it supports any valid
7547       * d3 axis manipulation.
7548       *
7549       * **Caution**: The right y axis is usually generated internally by dc; resetting it may cause
7550       * unexpected results.  Note also that when used as a getter, this function is not chainable: it
7551       * returns the axis, not the chart,
7552       * {@link https://github.com/dc-js/dc.js/wiki/FAQ#why-does-everything-break-after-a-call-to-xaxis-or-yaxis
7553       * so attempting to call chart functions after calling `.yAxis()` will fail}.
7554       * @see {@link https://github.com/d3/d3-axis/blob/master/README.md#axisRight}
7555       * @example
7556       * // customize y axis tick format
7557       * chart.rightYAxis().tickFormat(function (v) {return v + '%';});
7558       * // customize y axis tick values
7559       * chart.rightYAxis().tickValues([0, 100, 200, 300]);
7560       * @param {d3.axisRight} [rightYAxis]
7561       * @returns {d3.axisRight|CompositeChart}
7562       */
7563      rightYAxis (rightYAxis) {
7564          if (!arguments.length) {
7565              return this._rightYAxis;
7566          }
7567          this._rightYAxis = rightYAxis;
7568          return this;
7569      }
7570
7571      yAxisMin () {
7572          throw new Error('Not supported for this chart type');
7573      }
7574
7575      yAxisMax () {
7576          throw new Error('Not supported for this chart type');
7577      }
7578  }
7579
7580  const compositeChart = (parent, chartGroup) => new CompositeChart(parent, chartGroup);
7581
7582  /**
7583   * The data count widget is a simple widget designed to display the number of records selected by the
7584   * current filters out of the total number of records in the data set. Once created the data count widget
7585   * will automatically update the text content of child elements with the following classes:
7586   *
7587   * * `.total-count` - total number of records
7588   * * `.filter-count` - number of records matched by the current filters
7589   *
7590   * Note: this widget works best for the specific case of showing the number of records out of a
7591   * total. If you want a more general-purpose numeric display, please use the
7592   * {@link NumberDisplay} widget instead.
7593   *
7594   * Examples:
7595   * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
7596   * @mixes BaseMixin
7597   */
7598  class DataCount extends BaseMixin {
7599      /**
7600       * Create a Data Count widget.
7601       * @example
7602       * var ndx = crossfilter(data);
7603       * var all = ndx.groupAll();
7604       *
7605       * new DataCount('.dc-data-count')
7606       *     .crossfilter(ndx)
7607       *     .groupAll(all);
7608       * @param {String|node|d3.selection} parent - Any valid
7609       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
7610       * a dom block element such as a div; or a dom element or d3 selection.
7611       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
7612       * Interaction with a chart will only trigger events and redraws within the chart's group.
7613       */
7614      constructor (parent, chartGroup) {
7615          super();
7616
7617          this._formatNumber = d3Format.format(',d');
7618          this._crossfilter = null;
7619          this._groupAll = null;
7620          this._html = {some: '', all: ''};
7621
7622          this._mandatoryAttributes(['crossfilter', 'groupAll']);
7623
7624          this.anchor(parent, chartGroup);
7625      }
7626
7627      /**
7628       * Gets or sets an optional object specifying HTML templates to use depending how many items are
7629       * selected. The text `%total-count` will replaced with the total number of records, and the text
7630       * `%filter-count` will be replaced with the number of selected records.
7631       * - all: HTML template to use if all items are selected
7632       * - some: HTML template to use if not all items are selected
7633       * @example
7634       * counter.html({
7635       *      some: '%filter-count out of %total-count records selected',
7636       *      all: 'All records selected. Click on charts to apply filters'
7637       * })
7638       * @param {{some:String, all: String}} [options]
7639       * @returns {{some:String, all: String}|DataCount}
7640       */
7641      html (options) {
7642          if (!arguments.length) {
7643              return this._html;
7644          }
7645          if (options.all) {
7646              this._html.all = options.all;
7647          }
7648          if (options.some) {
7649              this._html.some = options.some;
7650          }
7651          return this;
7652      }
7653
7654      /**
7655       * Gets or sets an optional function to format the filter count and total count.
7656       * @see {@link https://github.com/d3/d3-format/blob/master/README.md#format d3.format}
7657       * @example
7658       * counter.formatNumber(d3.format('.2g'))
7659       * @param {Function} [formatter=d3.format('.2g')]
7660       * @returns {Function|DataCount}
7661       */
7662      formatNumber (formatter) {
7663          if (!arguments.length) {
7664              return this._formatNumber;
7665          }
7666          this._formatNumber = formatter;
7667          return this;
7668      }
7669
7670      _doRender () {
7671          const tot = this.crossfilter().size(),
7672              val = this.groupAll().value();
7673          const all = this._formatNumber(tot);
7674          const selected = this._formatNumber(val);
7675
7676          if ((tot === val) && (this._html.all !== '')) {
7677              this.root().html(this._html.all.replace('%total-count', all).replace('%filter-count', selected));
7678          } else if (this._html.some !== '') {
7679              this.root().html(this._html.some.replace('%total-count', all).replace('%filter-count', selected));
7680          } else {
7681              this.selectAll('.total-count').text(all);
7682              this.selectAll('.filter-count').text(selected);
7683          }
7684          return this;
7685      }
7686
7687      _doRedraw () {
7688          return this._doRender();
7689      }
7690
7691      crossfilter (cf) {
7692          if (!arguments.length) {
7693              return this._crossfilter;
7694          }
7695          this._crossfilter = cf;
7696          return this;
7697      }
7698
7699      dimension (cf) {
7700          logger.warnOnce('consider using dataCount.crossfilter instead of dataCount.dimension for clarity');
7701          if (!arguments.length) {
7702              return this.crossfilter();
7703          }
7704          return this.crossfilter(cf);
7705      }
7706
7707      groupAll (groupAll) {
7708          if (!arguments.length) {
7709              return this._groupAll;
7710          }
7711          this._groupAll = groupAll;
7712          return this;
7713      }
7714
7715      group (groupAll) {
7716          logger.warnOnce('consider using dataCount.groupAll instead of dataCount.group for clarity');
7717          if (!arguments.length) {
7718              return this.groupAll();
7719          }
7720          return this.groupAll(groupAll);
7721      }
7722  }
7723
7724  const dataCount = (parent, chartGroup) => new DataCount(parent, chartGroup);
7725
7726  const LABEL_CSS_CLASS = 'dc-grid-label';
7727  const ITEM_CSS_CLASS$1 = 'dc-grid-item';
7728  const SECTION_CSS_CLASS = 'dc-grid-section dc-grid-group';
7729  const GRID_CSS_CLASS = 'dc-grid-top';
7730
7731  /**
7732   * Data grid is a simple widget designed to list the filtered records, providing
7733   * a simple way to define how the items are displayed.
7734   *
7735   * Note: Formerly the data grid chart (and data table) used the {@link DataGrid#group group} attribute as a
7736   * keying function for {@link https://github.com/d3/d3-collection/blob/master/README.md#nest nesting} the data
7737   * together in sections.  This was confusing so it has been renamed to `section`, although `group` still works.
7738   *
7739   * Examples:
7740   * - {@link https://dc-js.github.io/dc.js/ep/ List of members of the european parliament}
7741   * @mixes BaseMixin
7742   */
7743  class DataGrid extends BaseMixin {
7744      /**
7745       * Create a Data Grid.
7746       * @param {String|node|d3.selection} parent - Any valid
7747       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
7748       * a dom block element such as a div; or a dom element or d3 selection.
7749       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
7750       * Interaction with a chart will only trigger events and redraws within the chart's group.
7751       */
7752      constructor (parent, chartGroup) {
7753          super();
7754
7755          this._section = null;
7756          this._size = 999; // shouldn't be needed, but you might
7757          this._html = function (d) {
7758              return `you need to provide an html() handling param:  ${JSON.stringify(d)}`;
7759          };
7760          this._sortBy = function (d) {
7761              return d;
7762          };
7763          this._order = d3Array.ascending;
7764          this._beginSlice = 0;
7765          this._endSlice = undefined;
7766
7767          this._htmlSection = d => `<div class='${SECTION_CSS_CLASS}'><h1 class='${LABEL_CSS_CLASS}'>${ 
7768            this.keyAccessor()(d)}</h1></div>`;
7769
7770          this._mandatoryAttributes(['dimension', 'section']);
7771
7772          this.anchor(parent, chartGroup);
7773      }
7774
7775      _doRender () {
7776          this.selectAll(`div.${GRID_CSS_CLASS}`).remove();
7777
7778          this._renderItems(this._renderSections());
7779
7780          return this;
7781      }
7782
7783      _renderSections () {
7784          const sections = this.root().selectAll(`div.${GRID_CSS_CLASS}`)
7785              .data(this._nestEntries(), d =>
7785 this.keyAccessor()(d));
7786
7787          const itemSection = sections
7788              .enter()
7789              .append('div')
7790              .attr('class', GRID_CSS_CLASS);
7791
7792          if (this._htmlSection) {
7793              itemSection
7794                  .html(d => this._htmlSection(d));
7795          }
7796
7797          sections.exit().remove();
7798          return itemSection;
7799      }
7800
7801      _nestEntries () {
7802          let entries = this.dimension().top(this._size);
7803
7804          entries = entries
7805              .sort((a, b) => this._order(this._sortBy(a), this._sortBy(b)))
7806              .slice(this._beginSlice, this._endSlice);
7807
7808          return d3compat.nester({
7809              key: this.section(),
7810              sortKeys: this._order,
7811              entries
7812          });
7813      }
7814
7815      _renderItems (sections) {
7816          let items = sections.order()
7817              .selectAll(`div.${ITEM_CSS_CLASS$1}`)
7818              .data(d => d.values);
7819
7820          items.exit().remove();
7821
7822          items = items
7823              .enter()
7824              .append('div')
7825              .attr('class', ITEM_CSS_CLASS$1)
7826              .html(d => this._html(d))
7827              .merge(items);
7828
7829          return items;
7830      }
7831
7832      _doRedraw () {
7833          return this._doRender();
7834      }
7835
7836      /**
7837       * Get or set the section function for the data grid. The section function takes a data row and
7838       * returns the key to specify to {@link https://github.com/d3/d3-collection/blob/master/README.md#nest d3.nest}
7839       * to split rows into sections.
7840       *
7841       * Do not pass in a crossfilter section as this will not work.
7842       * @example
7843       * // section rows by the value of their field
7844       * chart
7845       *     .section(function(d) { return d.field; })
7846       * @param {Function} section Function taking a row of data and returning the nest key.
7847       * @returns {Function|DataGrid}
7848       */
7849      section (section) {
7850          if (!arguments.length) {
7851              return this._section;
7852          }
7853          this._section = section;
7854          return this;
7855      }
7856
7857      /**
7858       * Backward-compatible synonym for {@link DataGrid#section section}.
7859       *
7860       * @param {Function} section Function taking a row of data and returning the nest key.
7861       * @returns {Function|DataGrid}
7862       */
7863      group (section) {
7864          logger.warnOnce('consider using dataGrid.section instead of dataGrid.group for clarity');
7865          if (!arguments.length) {
7866              return this.section();
7867          }
7868          return this.section(section);
7869      }
7870
7871      /**
7872       * Get or set the index of the beginning slice which determines which entries get displayed by the widget.
7873       * Useful when implementing pagination.
7874       * @param {Number} [beginSlice=0]
7875       * @returns {Number|DataGrid}
7876       */
7877      beginSlice (beginSlice) {
7878          if (!arguments.length) {
7879              return this._beginSlice;
7880          }
7881          this._beginSlice = beginSlice;
7882          return this;
7883      }
7884
7885      /**
7886       * Get or set the index of the end slice which determines which entries get displayed by the widget.
7887       * Useful when implementing pagination.
7888       * @param {Number} [endSlice]
7889       * @returns {Number|DataGrid}
7890       */
7891      endSlice (endSlice) {
7892          if (!arguments.length) {
7893              return this._endSlice;
7894          }
7895          this._endSlice = endSlice;
7896          return this;
7897      }
7898
7899      /**
7900       * Get or set the grid size which determines the number of items displayed by the widget.
7901       * @param {Number} [size=999]
7902       * @returns {Number|DataGrid}
7903       */
7904      size (size) {
7905          if (!arguments.length) {
7906              return this._size;
7907          }
7908          this._size = size;
7909          return this;
7910      }
7911
7912      /**
7913       * Get or set the function that formats an item. The data grid widget uses a
7914       * function to generate dynamic html. Use your favourite templating engine or
7915       * generate the string directly.
7916       * @example
7917       * chart.html(function (d) { return '<div class='item '+data.exampleCategory+''>'+data.exampleString+'</div>';});
7918       * @param {Function} [html]
7919       * @returns {Function|DataGrid}
7920       */
7921      html (html) {
7922          if (!arguments.length) {
7923              return this._html;
7924          }
7925          this._html = html;
7926          return this;
7927      }
7928
7929      /**
7930       * Get or set the function that formats a section label.
7931       * @example
7932       * chart.htmlSection (function (d) { return '<h2>'.d.key . 'with ' . d.values.length .' items</h2>'});
7933       * @param {Function} [htmlSection]
7934       * @returns {Function|DataGrid}
7935       */
7936      htmlSection (htmlSection) {
7937          if (!arguments.length) {
7938              return this._htmlSection;
7939          }
7940          this._htmlSection = htmlSection;
7941          return this;
7942      }
7943
7944      /**
7945       * Backward-compatible synonym for {@link DataGrid#htmlSection htmlSection}.
7946       * @param {Function} [htmlSection]
7947       * @returns {Function|DataGrid}
7948       */
7949      htmlGroup (htmlSection) {
7950          logger.warnOnce('consider using dataGrid.htmlSection instead of dataGrid.htmlGroup for clarity');
7951          if (!arguments.length) {
7952              return this.htmlSection();
7953          }
7954          return this.htmlSection(htmlSection);
7955      }
7956
7957      /**
7958       * Get or set sort-by function. This function works as a value accessor at the item
7959       * level and returns a particular field to be sorted.
7960       * @example
7961       * chart.sortBy(function(d) {
7962       *     return d.date;
7963       * });
7964       * @param {Function} [sortByFunction]
7965       * @returns {Function|DataGrid}
7966       */
7967      sortBy (sortByFunction) {
7968          if (!arguments.length) {
7969              return this._sortBy;
7970          }
7971          this._sortBy = sortByFunction;
7972          return this;
7973      }
7974
7975      /**
7976       * Get or set sort the order function.
7977       * @see {@link https://github.com/d3/d3-array/blob/master/README.md#ascending d3.ascending}
7978       * @see {@link https://github.com/d3/d3-array/blob/master/README.md#descending d3.desce
7978nding}
7979       * @example
7980       * chart.order(d3.descending);
7981       * @param {Function} [order=d3.ascending]
7982       * @returns {Function|DataGrid}
7983       */
7984      order (order) {
7985          if (!arguments.length) {
7986              return this._order;
7987          }
7988          this._order = order;
7989          return this;
7990      }
7991  }
7992
7993  const dataGrid = (parent, chartGroup) => new DataGrid(parent, chartGroup);
7994
7995  const LABEL_CSS_CLASS$1 = 'dc-table-label';
7996  const ROW_CSS_CLASS = 'dc-table-row';
7997  const COLUMN_CSS_CLASS = 'dc-table-column';
7998  const SECTION_CSS_CLASS$1 = 'dc-table-section dc-table-group';
7999  const HEAD_CSS_CLASS = 'dc-table-head';
8000
8001  /**
8002   * The data table is a simple widget designed to list crossfilter focused data set (rows being
8003   * filtered) in a good old tabular fashion.
8004   *
8005   * An interesting feature of the data table is that you can pass a crossfilter group to the
8006   * `dimension`, if you want to show aggregated data instead of raw data rows. This requires no
8007   * special code as long as you specify the {@link DataTable#order order}
8007 as `d3.descending`,
8008   * since the data table will use `dimension.top()` to fetch the data in that case, and the method is
8009   * equally supported on the crossfilter group as the crossfilter dimension.
8010   *
8011   * If you want to display aggregated data in ascending order, you will need to wrap the group
8012   * in a [fake dimension](https://github.com/dc-js/dc.js/wiki/FAQ#fake-dimensions) to support the
8013   * `.bottom()` method. See the example linked below for more details.
8014   *
8015   * Note: Formerly the data table (and data grid chart) used the {@link DataTable#group group} attribute as a
8016   * keying function for {@link https://github.com/d3/d3-collection/blob/master/README.md#nest nesting} the data
8017   * together in sections.  This was confusing so it has been renamed to `section`, although `group` still works.
8018   * Examples:
8019   * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
8020   * - {@link http://dc-js.github.io/dc.js/examples/table-on-aggregated-data.html dataTable on a crossfilter group}
8021   * ({@link https://github.com/dc-js/dc.js/blob/master/web-src/examples/table-on-aggregated-data.html source})
8022   *
8023   * @mixes BaseMixin
8024   */
8025  class DataTable extends BaseMixin {
8026      /**
8027       * Create a Data Table.
8028       *
8029       * @param {String|node|d3.selection} parent - Any valid
8030       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
8031       * a dom block element such as a div; or a dom element or d3 selection.
8032       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
8033       * Interaction with a chart will only trigger events and redraws within the chart's group.
8034       */
8035      constructor (parent, chartGroup) {
8036          super();
8037
8038          this._size = 25;
8039          this._columns = [];
8040          this._sortBy = d => d;
8041          this._order = d3Array.ascending;
8042          this._beginSlice = 0;
8043          this._endSlice = undefined;
8044          this._showSections = true;
8045          this._section = () => ''; // all in one section
8046
8047          this._mandatoryAttributes(['dimension']);
8048
8049          this.anchor(parent, chartGroup);
8050      }
8051
8052      _doRender () {
8053          this.selectAll('tbody').remove();
8054
8055          this._renderRows(this._renderSections());
8056
8057          return this;
8058      }
8059
8060      _doColumnValueFormat (v, d) {
8061          return (typeof v === 'function') ? v(d) :  // v as function
8062              (typeof v === 'string') ? d[v] :       // v is field name string
8063              v.format(d);                           // v is Object, use fn (element 2)
8064      }
8065
8066      _doColumnHeaderFormat (d) {
8067          // if 'function', convert to string representation
8068          // show a string capitalized
8069          // if an object then display its label string as-is.
8070          return (typeof d === 'function') ? this._doColumnHeaderFnToString(d) :
8071              (typeof d === 'string') ? this._doColumnHeaderCapitalize(d) :
8072              String(d.label);
8073      }
8074
8075      _doColumnHeaderCapitalize (s) {
8076          // capitalize
8077          return s.charAt(0).toUpperCase() + s.slice(1);
8078      }
8079
8080      _doColumnHeaderFnToString (f) {
8081          // columnString(f) {
8082          let s = String(f);
8083          const i1 = s.indexOf('return ');
8084          if (i1 >= 0) {
8085              const i2 = s.lastIndexOf(';');
8086              if (i2 >= 0) {
8087                  s = s.substring(i1 + 7, i2);
8088                  const i3 = s.indexOf('numberFormat');
8089                  if (i3 >= 0) {
8090                      s = s.replace('numberFormat', '');
8091                  }
8092              }
8093          }
8094          return s;
8095      }
8096
8097      _renderSections () {
8098          // The 'original' example uses all 'functions'.
8099          // If all 'functions' are used, then don't remove/add a header, and leave
8100          // the html alone. This preserves the functionality of earlier releases.
8101          // A 2nd option is a string representing a field in the data.
8102          // A third option is to supply an Object such as an array of 'information', and
8103          // supply your own _doColumnHeaderFormat and _doColumnValueFormat functions to
8104          // create what you need.
8105          let bAllFunctions = true;
8106          this._columns.forEach(f => {
8107              bAllFunctions = bAllFunctions & (typeof f === 'function');
8108          });
8109
8110          if (!bAllFunctions) {
8111              // ensure one thead
8112              let thead = this.selectAll('thead').data([0]);
8113              thead.exit().remove();
8114              thead = thead.enter()
8115                  .append('thead')
8116                  .merge(thead);
8117
8118              // with one tr
8119              let headrow = thead.selectAll('tr').data([0]);
8120              headrow.exit().remove();
8121              headrow = headrow.enter()
8122                  .append('tr')
8123                  .merge(headrow);
8124
8125              // with a th for each column
8126              const headcols = headrow.selectAll('th')
8127                  .data(this._columns);
8128              headcols.exit().remove();
8129              headcols.enter().append('th')
8130                  .merge(headcols)
8131                  .attr('class', HEAD_CSS_CLASS)
8132                  .html(d => (this._doColumnHeaderFormat(d)));
8133          }
8134
8135          const sections = this.root().selectAll('tbody')
8136              .data(this._nestEntries(), d => this.keyAccessor()(d));
8137
8138          const rowSection = sections
8139              .enter()
8140              .append('tbody');
8141
8142          if (this._showSections === true) {
8143              rowSection
8144                  .append('tr')
8145                  .attr('class', SECTION_CSS_CLASS$1)
8146                  .append('td')
8147                  .attr('class', LABEL_CSS_CLASS$1)
8148                  .attr('colspan', this._columns.length)
8149                  .html(d => this.keyAccessor()(d));
8150          }
8151
8152          sections.exit().remove();
8153
8154          return rowSection;
8155      }
8156
8157      _nestEntries () {
8158          let entries;
8159          if (this._order === d3Array.ascending) {
8160              entries = this.dimension().bottom(this._size);
8161          } else {
8162              entries = this.dimension().top(this._size);
8163          }
8164
8165          entries = entries.sort((a, b) => this._order(this._sortBy(a), this._sortBy(b))).slice(this._beginSlice, this._endSlice);
8166
8167          return d3compat.nester({
8168              key: this.section(),
8169              sortKeys: this._order,
8170              entries
8171          });
8172      }
8173
8174      _renderRows (sections) {
8175          const rows = sections.order()
8176              .selectAll(`tr.${ROW_CSS_CLASS}`)
8177              .data(d => d.values);
8178
8179          const rowEnter = rows.enter()
8180              .append('tr')
8181              .attr('class', ROW_CSS_CLASS);
8182
8183          this._columns.forEach((v, i) => {
8184              rowEnter.append('td')
8185                  .attr('class', `${COLUMN_CSS_CLASS} _${i}`)
8186                  .html(d => this._doColumnValueFormat(v, d));
8187          });
8188
8189          rows.exit().remove();
8190
8191          return rows;
8192      }
8193
8194      _doRedraw () {
8195          return this._doRender();
8196      }
8197
8198      /**
8199       * Get or set the section function for the data table. The section function takes a data row and
8200       * returns the key to specify to {@link https://github.com/d3/d3-collection/blob/master/README.md#nest d3.nest}
8201       * to split rows into sections. By default there will be only one section with no name.
8202       *
8203       * Set {@link DataTable#showSections showSections} to false to hide the section headers
8204       *
8205       * @example
8206       * // section rows by the value of their field
8207       * chart
8208       *     .section(function(d) { return d.field; })
8209       * @param {Function} section Function taking a row of data and returning the nest key.
8210       * @returns {Function|DataTable}
8211       */
8212      section (section) {
8213          if (!arguments.length) {
8214              return this._section;
8215          }
8216          this._section = section;
8217          return this;
8218      }
8219
8220      /**
8221       * Backward-compatible synonym for {@link DataTable#section section}.
8222       *
8223       * @param {Function} section Function taking a row of data and returning the nest key.
8224       * @returns {Function|DataTable}
8225       */
8226      group (section) {
8227          logger.warnOnce('consider using dataTable.section instead of dataTable.group for clarity');
8228          if (!arguments.length) {
8229              return this.section();
8230          }
8231          return this.section(section);
8232      }
8233
8234      /**
8235       * Get or set the table size which determines the number of rows displayed by the widget.
8236       * @param {Number} [size=25]
8237       * @returns {Number|DataTable}
8238       */
8239      size (size) {
8240          if (!arguments.length) {
8241              return this._size;
8242          }
8243          this._size = size;
8244          return this;
8245      }
8246
8247      /**
8248       * Get or set the index of the beginning slice which determines which entries get displayed
8249       * by the widget. Useful when implementing pagination.
8250       *
8251       * Note: the sortBy function will determine how the rows are ordered for pagination purposes.
8252
8253       * See the {@link http://dc-js.github.io/dc.js/examples/table-pagination.html table pagination example}
8254       * to see how to implement the pagination user interface using `beginSlice` and `endSlice`.
8255       * @param {Number} [beginSlice=0]
8256       * @returns {Number|DataTable}
8257       */
8258      beginSlice (beginSlice) {
8259          if (!arguments.length) {
8260              return this._beginSlice;
8261          }
8262          this._beginSlice = beginSlice;
8263          return this;
8264      }
8265
8266      /**
8267       * Get or set the index of the end slice which determines which entries get displayed by the
8268       * widget. Useful when implementing pagination. See {@link DataTable#beginSlice `beginSlice`} for more information.
8269       * @param {Number|undefined} [endSlice=undefined]
8270       * @returns {Number|DataTable}
8271       */
8272      endSlice (endSlice) {
8273          if (!arguments.length) {
8274              return this._endSlice;
8275          }
8276          this._endSlice = endSlice;
8277          return this;
8278      }
8279
8280      /**
8281       * Get or set column functions. The data table widget supports several methods of specifying the
8282       * columns to display.
8283       *
8284       * The original method uses an array of functions to generate dynamic columns. Column functions
8285       * are simple javascript functions with only one input argument `d` which represents a row in
8286       * the data set. The return value of these functions will be used to generate the content for
8287       * each cell. However, this method requires the HTML for the table to have a fixed set of column
8288       * headers.
8289       *
8290       * <pre><code>chart.columns([
8291       *     function(d) { return d.date; },
8292       *     function(d) { return d.open; },
8293       *     function(d) { return d.close; },
8294       *     function(d) { return numberFormat(d.close - d.open); },
8295       *     function(d) { return d.volume; }
8296       * ]);
8297       * </code></pre>
8298       *
8299       * In the second method, you can list the columns to read from the data without specifying it as
8300       * a function, except where necessary (ie, computed columns).  Note the data element name is
8301       * capitalized when displayed in the table header. You can also mix in functions as 
8301necessary,
8302       * using the third `{label, format}` form, as shown below.
8303       *
8304       * <pre><code>chart.columns([
8305       *     "date",    // d["date"], ie, a field accessor; capitalized automatically
8306       *     "open",    // ...
8307       *     "close",   // ...
8308       *     {
8309       *         label: "Change",
8310       *         format: function (d) {
8311       *             return numberFormat(d.close - d.open);
8312       *         }
8313       *     },
8314       *     "volume"   // d["volume"], ie, a field accessor; capitalized automatically
8315       * ]);
8316       * </code></pre>
8317       *
8318       * In the third example, we specify all fields using the `{label, format}` method:
8319       * <pre><code>chart.columns([
8320       *     {
8321       *         label: "Date",
8322       *         format: function (d) { return d.date; }
8323       *     },
8324       *     {
8325       *         label: "Open",
8326       *         format: function (d) { return numberFormat(d.open); }
8327       *     },
8328       *     {
8329       *         label: "Close",
8330       *         format: function (d) { return numberFormat(d.close); }
8331       *     },
8332       *     {
8333       *         label: "Change",
8334       *         format: function (d) { return numberFormat(d.close - d.open); }
8335       *     },
8336       *     {
8337       *         label: "Volume",
8338       *         format: function (d) { return d.volume; }
8339       *     }
8340       * ]);
8341       * </code></pre>
8342       *
8343       * You may wish to override the dataTable functions `_doColumnHeaderCapitalize` and
8344       * `_doColumnHeaderFnToString`, which are used internally to translate the column information or
8345       * function into a displayed header. The first one is used on the "string" column specifier; the
8346       * second is used to transform a stringified function into something displayable. For the Stock
8347       * example, the function for Change becomes the table header **d.close - d.open**.
8348       *
8349       * Finally, you can even specify a completely different form of column definition. To do this,
8350       * override `_chart._doColumnHeaderFormat` and `_chart._doColumnValueFormat` Be aware that
8351       * fields without numberFormat specification will be displayed just as they are stored in the
8352       * data, unformatted.
8353       * @param {Array<Function>} [columns=[]]
8354       * @returns {Array<Function>}|DataTable}
8355       */
8356      columns (columns) {
8357          if (!arguments.length) {
8358              return this._columns;
8359          }
8360          this._columns = columns;
8361          return this;
8362      }
8363
8364      /**
8365       * Get or set sort-by function. This function works as a value accessor at row level and returns a
8366       * particular field to be sorted by.
8367       * @example
8368       * chart.sortBy(function(d) {
8369       *     return d.date;
8370       * });
8371       * @param {Function} [sortBy=identity function]
8372       * @returns {Function|DataTable}
8373       */
8374      sortBy (sortBy) {
8375          if (!arguments.length) {
8376              return this._sortBy;
8377          }
8378          this._sortBy = sortBy;
8379          return this;
8380      }
8381
8382      /**
8383       * Get or set sort order. If the order is `d3.ascending`, the data table will use
8384       * `dimension().bottom()` to fetch the data; otherwise it will use `dimension().top()`
8385       * @see {@link https://github.com/d3/d3-array/blob/master/README.md#ascending d3.ascending}
8386       * @see {@link https://github.com/d3/d3-array/blob/master/README.md#descending d3.descending}
8387       * @example
8388       * chart.order(d3.descending);
8389       * @param {Function} [order=d3.ascending]
8390       * @returns {Function|DataTable}
8391       */
8392      order (order) {
8393          if (!arguments.length) {
8394              return this._order;
8395          }
8396          this._order = order;
8397          return this;
8398      }
8399
8400      /**
8401       * Get or set if section header rows will be shown.
8402       * @example
8403       * chart
8404       *     .section([value], [name])
8405       *     .showSections(true|false);
8406       * @param {Boolean} [showSections=true]
8407       * @returns {Boolean|DataTable}
8408       */
8409      showSections (showSections) {
8410          if (!arguments.length) {
8411              return this._showSections;
8412          }
8413          this._showSections = showSections;
8414          return this;
8415      }
8416
8417      /**
8418       * Backward-compatible synonym for {@link DataTable#showSections showSections}.
8419       * @param {Boolean} [showSections=true]
8420       * @returns {Boolean|DataTable}
8421       */
8422      showGroups (showSections) {
8423          logger.warnOnce('consider using dataTable.showSections instead of dataTable.showGroups for clarity');
8424          if (!arguments.length) {
8425              return this.showSections();
8426          }
8427          return this.showSections(showSections);
8428      }
8429  }
8430
8431  const dataTable = (parent, chartGroup) => new DataTable(parent, chartGroup);
8432
8433  /**
8434   * The geo choropleth chart is designed as an easy way to create a crossfilter driven choropleth map
8435   * from GeoJson data. This chart implementation was inspired by
8436   * {@link http://bl.ocks.org/4060606 the great d3 choropleth example}.
8437   *
8438   * Examples:
8439   * - {@link http://dc-js.github.com/dc.js/vc/index.html US Venture Capital Landscape 2011}
8440   * @mixes ColorMixin
8441   * @mixes BaseMixin
8442   */
8443  class GeoChoroplethChart extends ColorMixin(BaseMixin) {
8444      /**
8445       * Create a Geo Choropleth Chart.
8446       * @example
8447       * // create a choropleth chart under '#us-chart' element using the default global chart group
8448       * var chart1 = new GeoChoroplethChart('#us-chart');
8449       * // create a choropleth chart under '#us-chart2' element using chart group A
8450       * var chart2 = new CompositeChart('#us-chart2', 'chartGroupA');
8451       * @param {String|node|d3.selection} parent - Any valid
8452       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
8453       * a dom block element such as a div; or a dom element or d3 selection.
8454       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
8455       * Interaction with a chart will only trigger events and redraws within the chart's group.
8456       */
8457      constructor (parent, chartGroup) {
8458          super();
8459
8460          this.colorAccessor(d => d || 0);
8461
8462          this._geoPath = d3Geo.geoPath();
8463          this._projectionFlag = undefined;
8464          this._projection = undefined;
8465
8466          this._geoJsons = [];
8467
8468          this.anchor(parent, chartGroup);
8469      }
8470
8471      _doRender () {
8472          this.resetSvg();
8473          for (let layerIndex = 0; layerIndex < this._geoJsons.length; ++layerIndex) {
8474              const states = this.svg().append('g')
8475                  .attr('class', `layer${layerIndex}`);
8476
8477              let regionG = states.selectAll(`g.${this._geoJson(layerIndex).name}`)
8478                  .data(this._geoJson(layerIndex).data);
8479
8480              regionG = regionG.enter()
8481                  .append('g')
8482                  .attr('class', this._geoJson(layerIndex).name)
8483                  .merge(regionG);
8484
8485              regionG
8486                  .append('path')
8487                  .classed('dc-tabbable', this._keyboardAccessible)
8488                  .attr('fill', 'white')
8489                  .attr('d', this._getGeoPath());
8490
8491              regionG.append('title');
8492
8493              this._plotData(layerIndex);
8494          }
8495          this._projectionFlag = false;
8496      }
8497
8498      _plotData (layerIndex) {
8499          const data = this._generateLayeredData();
8500
8501          if (this._isDataLayer(layerIndex)) {
8502              const regionG = this._renderRegionG(layerIndex);
8503
8504              this._renderPaths(regionG, layerIndex, data);
8505
8506              this._renderTitles(regionG, layerIndex, data);
8507          }
8508      }
8509
8510      _generateLayeredData () {
8511          const data = {};
8512          const groupAll = this.data();
8513          for (let i = 0; i < groupAll.length; ++i) {
8514              data[this.keyAccessor()(groupAll[i])] = this.valueAccessor()(groupAll[i]);
8515          }
8516          return data;
8517      }
8518
8519      _isDataLayer (layerIndex) {
8520          return this._geoJson(layerIndex).keyAccessor;
8521      }
8522
8523      _renderRegionG (layerIndex) {
8524          const regionG = this.svg()
8525              .selectAll(this._layerSelector(layerIndex))
8526              .classed('selected', d => this._isSelected(layerIndex, d))
8527              .classed('deselected', d => this._isDeselected(layerIndex, d))
8528              .attr('class', d => {
8529                  const layerNameClass = this._geoJson(layerIndex).name;
8530                  const regionClass = utils.nameToId(this._geoJson(layerIndex).keyAccessor(d));
8531                  let baseClasses = `${layerNameClass} ${regionClass}`;
8532                  if (this._isSelected(layerIndex, d)) {
8533                      baseClasses += ' selected';
8534                  }
8535                  if (this._isDeselected(layerIndex, d)) {
8536                      baseClasses += ' deselected';
8537                  }
8538                  return baseClasses;
8539              });
8540          return regionG;
8541      }
8542
8543      _layerSelector (layerIndex) {
8544          return `g.layer${layerIndex} g.${this._geoJson(layerIndex).name}`;
8545      }
8546
8547      _isSelected (layerIndex, d) {
8548          return this.hasFilter() && this.hasFilter(this._getKey(layerIndex, d));
8549      }
8550
8551      _isDeselected (layerIndex, d) {
8552          return this.hasFilter() && !this.hasFilter(this._getKey(layerIndex, d));
8553      }
8554
8555      _getKey (layerIndex, d) {
8556          return this._geoJson(layerIndex).keyAccessor(d);
8557      }
8558
8559      _geoJson (index) {
8560          return this._geoJsons[index];
8561      }
8562
8563      _renderPaths (regionG, layerIndex, data) {
8564          const paths = regionG
8565              .select('path')
8566              .attr('fill', function () {
8567                  const currentFill = d3Selection.select(this).attr('fill');
8568                  if (currentFill) {
8569                      return currentFill;
8570                  }
8571                  return 'none';
8572              })
8573              .on('click', d3compat.eventHandler(d => this.onClick(d, layerIndex)));
8574
8575          if (this._keyboardAccessible) {
8576              this._makeKeyboardAccessible(this.onClick, layerIndex);
8577          }
8578
8579          transition(paths, this.transitionDuration(),
8580                     this.transitionDelay()).attr('fill', (d, i) => this.getColor(data[this._geoJson(layerIndex).keyAccessor(d)], i));
8581      }
8582
8583      onClick (d, layerIndex) {
8584          const selectedRegion = this._geoJson(layerIndex).keyAccessor(d);
8585          events.trigger(() => {
8586              this.filter(selectedRegion);
8587              this.redrawGroup();
8588          });
8589      }
8590
8591      _renderTitles (regionG, layerIndex, data) {
8592          if (this.renderTitle()) {
8593              regionG.selectAll('title').text(d => {
8594                  const key = this._getKey(layerIndex, d);
8595                  const value = data[key];
8596                  return this.title()({key: key, value: value});
8597              });
8598          }
8599      }
8600
8601      _doRedraw () {
8602          for (let layerIndex = 0; layerIndex < this._geoJsons.length; ++layerIndex) {
8603              this._plotData(layerIndex);
8604              if (this._projectionFlag) {
8605                  this.svg().selectAll(`g.${this._geoJson(layerIndex).name} path`).attr('d', this._getGeoPath());
8606              }
8607          }
8608          this._projectionFlag = false;
8609      }
8610
8611      /**
8612       * **mandatory**
8613       *
8614       * Use this function to insert a new GeoJson map layer. This function can be invoked multiple times
8615       * if you have multiple GeoJson data layers to render on top of each other. If you overlay multiple
8616       * layers with the same name the new overlay will override the existing one.
8617       * @see {@link http://geojson.org/ GeoJSON}
8618       * @see {@link https://github.com/topojson/topojson/wiki TopoJSON}
8619       * @see {@link https://github.com/topojson/topojson-1.x-api-reference/blob/master/API-Reference.md#wiki-feature topojson.feature}
8620       * @example
8621       * // insert a layer for rendering US states
8622       * chart.overlayGeoJson(statesJson.features, 'state', function(d) {
8623       *      return d.properties.name;
8624       * });
8625       * @param {_geoJson} json - a geojson feed
8626       * @param {String} name - name of the layer
8627       * @param {Function} keyAccessor - accessor function used to extract 'key' from the GeoJson data. The key extracted by
8628       * this function should match the keys returned by the crossfilter groups.
8629       * @returns {GeoChoroplethChart}
8630       */
8631      overlayGeoJson (json, name, keyAccessor) {
8632          for (let i = 0; i < this._geoJsons.length; ++i) {
8633              if (this._geoJsons[i].name === name) {
8634                  this._geoJsons[i].data = json;
8635                  this._geoJsons[i].keyAccessor = keyAccessor;
8636                  return this;
8637              }
8638          }
8639          this._geoJsons.push({name: name, data: json, keyAccessor: keyAccessor});
8640          return this;
8641      }
8642
8643      /**
8644       * Gets or sets a custom geo projection function. See the available
8645       * {@link https://github.com/d3/d3-geo/blob/master/README.md#projections d3 geo projection functions}.
8646       *
8647       * Starting version 3.0 it has been deprecated to rely on the default projection being
8648       * {@link https://github.com/d3/d3-geo/blob/master/README.md#geoAlbersUsa d3.geoAlbersUsa()}. Please
8649       * set it explicitly. {@link https://bl.ocks.org/mbostock/5557726
8650       * Considering that `null` is also a valid value for projection}, if you need
8651       * projection to be `null` please set it explicitly to `null`.
8652       * @see {@link https://github.com/d3/d3-geo/blob/master/README.md#projections d3.projection}
8653       * @see {@link https://github.com/d3/d3-geo-projection d3-geo-projection}
8654       * @param {d3.projection} [projection=d3.geoAlbersUsa()]
8655       * @returns {d3.projection|GeoChoroplethChart}
8656       */
8657      projection (projection) {
8658          if (!arguments.length) {
8659              return this._projection;
8660          }
8661
8662          this._projection = projection;
8663          this._projectionFlag = true;
8664          return this;
8665      }
8666
8667      _getGeoPath () {
8668          if (this._projection === undefined) {
8669              logger.warn('choropleth projection default of geoAlbers is deprecated,' +
8670                  ' in next version projection will need to be set explicitly');
8671              return this._geoPath.projection(d3Geo.geoAlbersUsa());
8672          }
8673
8674          return this._geoPath.projection(this._projection);
8675      }
8676
8677      /**
8678       * Returns all GeoJson layers currently registered with this chart. The returned array is a
8679       * reference to this chart's internal data structure, so any modification to this array will also
8680       * modify this chart's internal registration.
8681       * @returns {Array<{name:String, data: Object, accessor: Function}>}
8682       */
8683      geoJsons () {
8684          return this._geoJsons;
8685      }
8686
8687      /**
8688       * Returns the {@link https://github.com/d3/d3-geo/blob/master/README.md#paths d3.geoPath} object used to
8689       * render the projection and features.  Can be useful for figuring out the bounding box of the
8690       * feature set and thus a way to calculate scale and translation for the projection.
8691       * @see {@link https://github.com/d3/d3-geo/blob/master/README.md#paths d3.geoPath}
8692       * @returns {d3.geoPath}
8693       */
8694      geoPath () {
8695          return this._geoPath;
8696      }
8697
8698      /**
8699       * Remove a GeoJson layer from this chart by name
8700       * @param {String} name
8701       * @returns {GeoChoroplethChart}
8702       */
8703      removeGeoJson (name) {
8704          const geoJsons = [];
8705
8706          for (let i = 0; i < this._geoJsons.length; ++i) {
8707              const layer = this._geoJsons[i];
8708              if (layer.name !== name) {
8709                  geoJsons.push(layer);
8710              }
8711          }
8712
8713          this._geoJsons = geoJsons;
8714
8715          return this;
8716      }
8717  }
8718
8719  const geoChoroplethChart = (parent, chartGroup) =>
8719 new GeoChoroplethChart(parent, chartGroup);
8720
8721  const DEFAULT_BORDER_RADIUS = 6.75;
8722
8723  /**
8724   * A heat map is matrix that represents the values of two dimensions of data using colors.
8725   * @mixes ColorMixin
8726   * @mixes MarginMixin
8727   * @mixes BaseMixin
8728   */
8729  class HeatMap extends ColorMixin(MarginMixin) {
8730      /**
8731       * Create a Heat Map
8732       * @example
8733       * // create a heat map under #chart-container1 element using the default global chart group
8734       * var heatMap1 = new HeatMap('#chart-container1');
8735       * // create a heat map under #chart-container2 element using chart group A
8736       * var heatMap2 = new HeatMap('#chart-container2', 'chartGroupA');
8737       * @param {String|node|d3.selection} parent - Any valid
8738       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
8739       * a dom block element such as a div; or a dom element or d3 selection.
8740       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
8741       * Interaction with a chart will only trigger events and redraws within the chart's group.
8742       */
8743      constructor (parent, chartGroup) {
8744          super();
8745
8746          this._chartBody = undefined;
8747
8748          this._cols = undefined;
8749          this._rows = undefined;
8750          this._colOrdering = d3Array.ascending;
8751          this._rowOrdering = d3Array.ascending;
8752          this._colScale = d3Scale.scaleBand();
8753          this._rowScale = d3Scale.scaleBand();
8754
8755          this._xBorderRadius = DEFAULT_BORDER_RADIUS;
8756          this._yBorderRadius = DEFAULT_BORDER_RADIUS;
8757
8758          this._mandatoryAttributes(['group']);
8759          this.title(this.colorAccessor());
8760
8761          this._colsLabel = d => d;
8762          this._rowsLabel = d => d;
8763
8764          this._xAxisOnClick = d => {
8765              this._filterAxis(0, d);
8766          };
8767          this._yAxisOnClick = d => {
8768              this._filterAxis(1, d);
8769          };
8770          this._boxOnClick = d => {
8771              const filter = d.key;
8772              events.trigger(() => {
8773                  this.filter(filters.TwoDimensionalFilter(filter));
8774                  this.redrawGroup();
8775              });
8776          };
8777
8778          this.anchor(parent, chartGroup);
8779      }
8780
8781      /**
8782       * Set or get the column label function. The chart class uses this function to render
8783       * column labels on the X axis. It is passed the column name.
8784       * @example
8785       * // the default label function just returns the name
8786       * chart.colsLabel(function(d) { return d; });
8787       * @param  {Function} [labelFunction=function(d) { return d; }]
8788       * @returns {Function|HeatMap}
8789       */
8790      colsLabel (labelFunction) {
8791          if (!arguments.length) {
8792              return this._colsLabel;
8793          }
8794          this._colsLabel = labelFunction;
8795          return this;
8796      }
8797
8798      /**
8799       * Set or get the row label function. The chart class uses this function to render
8800       * row labels on the Y axis. It is passed the row name.
8801       * @example
8802       * // the default label function just returns the name
8803       * chart.rowsLabel(function(d) { return d; });
8804       * @param  {Function} [labelFunction=function(d) { return d; }]
8805       * @returns {Function|HeatMap}
8806       */
8807      rowsLabel (labelFunction) {
8808          if (!arguments.length) {
8809              return this._rowsLabel;
8810          }
8811          this._rowsLabel = labelFunction;
8812          return this;
8813      }
8814
8815      _filterAxis (axis, value) {
8816          const cellsOnAxis = this.selectAll('.box-group').filter(d => d.key[axis] === value);
8817          const unfilteredCellsOnAxis = cellsOnAxis.filter(d => !this.hasFilter(d.key));
8818          events.trigger(() => {
8819              const selection = unfilteredCellsOnAxis.empty() ? cellsOnAxis : unfilteredCellsOnAxis;
8820              const filtersList = selection.data().map(kv => filters.TwoDimensionalFilter(kv.key));
8821              this.filter([filtersList]);
8822              this.redrawGroup();
8823          });
8824      }
8825
8826      filter (filter) {
8827          const nonstandardFilter = f => {
8828              logger.warnOnce('heatmap.filter taking a coordinate is deprecated - please pass dc.filters.TwoDimensionalFilter instead');
8829              return this._filter(filters.TwoDimensionalFilter(f));
8830          };
8831
8832          if (!arguments.length) {
8833              return super.filter();
8834          }
8835          if (filter !== null && filter.filterType !== 'TwoDimensionalFilter' &&
8836              !(Array.isArray(filter) && Array.isArray(filter[0]) && filter[0][0].filterType === 'TwoDimensionalFilter')) {
8837              return nonstandardFilter(filter);
8838          }
8839          return super.filter(filter);
8840      }
8841
8842      /**
8843       * Gets or sets the values used to create the rows of the heatmap, as an array. By default, all
8844       * the values will be fetched from the data using the value accessor.
8845       * @param  {Array<String|Number>} [rows]
8846       * @returns {Array<String|Number>|HeatMap}
8847       */
8848
8849      rows (rows) {
8850          if (!arguments.length) {
8851              return this._rows;
8852          }
8853          this._rows = rows;
8854          return this;
8855      }
8856
8857      /**
8858       * Get or set a comparator to order the rows.
8859       * Default is {@link https://github.com/d3/d3-array#ascending d3.ascending}.
8860       * @param  {Function} [rowOrdering]
8861       * @returns {Function|HeatMap}
8862       */
8863      rowOrdering (rowOrdering) {
8864          if (!arguments.length) {
8865              return this._rowOrdering;
8866          }
8867          this._rowOrdering = rowOrdering;
8868          return this;
8869      }
8870
8871      /**
8872       * Gets or sets the keys used to create the columns of the heatmap, as an array. By default, all
8873       * the values will be fetched from the data using the key accessor.
8874       * @param  {Array<String|Number>} [cols]
8875       * @returns {Array<String|Number>|HeatMap}
8876       */
8877      cols (cols) {
8878          if (!arguments.length) {
8879              return this._cols;
8880          }
8881          this._cols = cols;
8882          return this;
8883      }
8884
8885      /**
8886       * Get or set a comparator to order the columns.
8887       * Default is  {@link https://github.com/d3/d3-array#ascending d3.ascending}.
8888       * @param  {Function} [colOrdering]
8889       * @returns {Function|HeatMap}
8890       */
8891      colOrdering (colOrdering) {
8892          if (!arguments.length) {
8893              return this._colOrdering;
8894          }
8895          this._colOrdering = colOrdering;
8896          return this;
8897      }
8898
8899      _doRender () {
8900          this.resetSvg();
8901
8902          this._chartBody = this.svg()
8903              .append('g')
8904              .attr('class', 'heatmap')
8905              .attr('transform', `translate(${this.margins().left},${this.margins().top})`);
8906
8907          return this._doRedraw();
8908      }
8909
8910      _doRedraw () {
8911          const data = this.data();
8912          let rows = this.rows() || data.map(this.valueAccessor()),
8913              cols = this.cols() || data.map(this.keyAccessor());
8914          if (this._rowOrdering) {
8915              rows = rows.sort(this._rowOrdering);
8916          }
8917          if (this._colOrdering) {
8918              cols = cols.sort(this._colOrdering);
8919          }
8920          rows = this._rowScale.domain(rows);
8921          cols = this._colScale.domain(cols);
8922
8923          const rowCount = rows.domain().length,
8924              colCount = cols.domain().length,
8925              boxWidth = Math.floor(this.effectiveWidth() / colCount),
8926              boxHeight = Math.floor(this.effectiveHeight() / rowCount);
8927
8928          cols.rangeRound([0, this.effectiveWidth()]);
8929          rows.rangeRound([this.effectiveHeight(), 0]);
8930
8931          let boxes = this._chartBody.selectAll('g.box-group').data(this.data(),
8932                                                                    (d, i) => `${this.keyAccessor()(d, i)}\0${this.valueAccessor()(d, i)}`);
8933
8934          boxes.exit().remove();
8935
8936          const gEnter = boxes.enter().append('g')
8937              .attr('class', 'box-group');
8938
8939          gEnter.append('rect')
8940              .attr('class', 'heat-box')
8941              .classed('dc-tabbable', this._keyboardAccessible)
8942              .attr('fill', 'white')
8943              .attr('x', (d, i) => cols(this.keyAccessor()(d, i)))
8944              .attr('y', (d, i) => rows(this.valueAccessor()(d, i)))
8945              .on('click', d3compat.eventHandler(this.boxOnClick()));
8946
8947          if (this._keyboardAccessible) {
8948              this._makeKeyboardAccessible(this.boxOnClick);
8949          }
8950
8951          boxes = gEnter.merge(boxes);
8952
8953          if (this.renderTitle()) {
8954              gEnter.append('title');
8955              boxes.select('title').text(this.title());
8956          }
8957
8958          transition(boxes.select('rect'), this.transitionDuration(), this.transitionDelay())
8959              .attr('x', (d, i) => cols(this.keyAccessor()(d, i)))
8960              .attr('y', (d, i) => rows(this.valueAccessor()(d, i)))
8961              .attr('rx', this._xBorderRadius)
8962              .attr('ry', this._yBorderRadius)
8963              .attr('fill', this.getColor)
8964              .attr('width', boxWidth)
8965              .attr('height', boxHeight);
8966
8967          let gCols = this._chartBody.select('g.cols');
8968          if (gCols.empty()) {
8969              gCols = this._chartBody.append('g').attr('class', 'cols axis');
8970          }
8971          let gColsText = gCols.selectAll('text').data(cols.domain());
8972
8973          gColsText.exit().remove();
8974
8975          gColsText = gColsText
8976              .enter()
8977              .append('text')
8978              .attr('x', d => cols(d) + boxWidth / 2)
8979              .style('text-anchor', 'middle')
8980              .attr('y', this.effectiveHeight())
8981              .attr('dy', 12)
8982              .on('click', d3compat.eventHandler(this.xAxisOnClick()))
8983              .text(this.colsLabel())
8984              .merge(gColsText);
8985
8986          transition(gColsText, this.transitionDuration(), this.transitionDelay())
8987              .text(this.colsLabel())
8988              .attr('x', d => cols(d) + boxWidth / 2)
8989              .attr('y', this.effectiveHeight());
8990
8991          let gRows = this._chartBody.select('g.rows');
8992          if (gRows.empty()) {
8993              gRows = this._chartBody.append('g').attr('class', 'rows axis');
8994          }
8995
8996          let gRowsText = gRows.selectAll('text').data(rows.domain());
8997
8998          gRowsText.exit().remove();
8999
9000          gRowsText = gRowsText
9001              .enter()
9002              .append('text')
9003              .style('text-anchor', 'end')
9004              .attr('x', 0)
9005              .attr('dx', -2)
9006              .attr('y', d => rows(d) + boxHeight / 2)
9007              .attr('dy', 6)
9008              .on('click', d3compat.eventHandler(this.yAxisOnClick()))
9009              .text(this.rowsLabel())
9010              .merge(gRowsText);
9011
9012          transition(gRowsText, this.transitionDuration(), this.transitionDelay())
9013              .text(this.rowsLabel())
9014              .attr('y', d => rows(d) + boxHeight / 2);
9015
9016          if (this.hasFilter()) {
9017              const chart = this;
9018              this.selectAll('g.box-group').each(function (d) {
9019                  if (chart.isSelectedNode(d)) {
9020                      chart.highlightSelected(this);
9021                  } else {
9022                      chart.fadeDeselected(this);
9023                  }
9024              });
9025          } else {
9026              const chart = this;
9027              this.selectAll('g.box-group').each(function () {
9028                  chart.resetHighlight(this);
9029              });
9030          }
9031          return this;
9032      }
9033
9034      /**
9035       * Gets or sets the handler that fires when an individual cell is clicked in the heatmap.
9036       * By default, filtering of the cell will be toggled.
9037       * @example
9038       * // default box on click handler
9039       * chart.boxOnClick(function (d) {
9040       *     var filter = d.key;
9041       *     events.trigger(function () {
9042       *         _chart.filter(filter);
9043       *         _chart.redrawGroup();
9044       *     });
9045       * });
9046       * @param  {Function} [handler]
9047       * @returns {Function|HeatMap}
9048       */
9049      boxOnClick (handler) {
9050          if (!arguments.length) {
9051              return this._boxOnClick;
9052          }
9053          this._boxOnClick = handler;
9054          return this;
9055      }
9056
9057      /**
9058       * Gets or sets the handler that fires when a column tick is clicked in the x axis.
9059       * By default, if any cells in the column are unselected, the whole column will be selected,
9060       * otherwise the whole column will be unselected.
9061       * @param  {Function} [handler]
9062       * @returns {Function|HeatMap}
9063       */
9064      xAxisOnClick (handler) {
9065          if (!arguments.length) {
9066              return this._xAxisOnClick;
9067          }
9068          this._xAxisOnClick = handler;
9069          return this;
9070      }
9071
9072      /**
9073       * Gets or sets the handler that fires when a row tick is clicked in the y axis.
9074       * By default, if any cells in the row are unselected, the whole row will be selected,
9075       * otherwise the whole row will be unselected.
9076       * @param  {Function} [handler]
9077       * @returns {Function|HeatMap}
9078       */
9079      yAxisOnClick (handler) {
9080          if (!arguments.length) {
9081              return this._yAxisOnClick;
9082          }
9083          this._yAxisOnClick = handler;
9084          return this;
9085      }
9086
9087      /**
9088       * Gets or sets the X border radius.  Set to 0 to get full rectangles.
9089       * @param  {Number} [xBorderRadius=6.75]
9090       * @returns {Number|HeatMap}
9091       */
9092      xBorderRadius (xBorderRadius) {
9093          if (!arguments.length) {
9094              return this._xBorderRadius;
9095          }
9096          this._xBorderRadius = xBorderRadius;
9097          return this;
9098      }
9099
9100      /**
9101       * Gets or sets the Y border radius.  Set to 0 to get full rectangles.
9102       * @param  {Number} [yBorderRadius=6.75]
9103       * @returns {Number|HeatMap}
9104       */
9105      yBorderRadius (yBorderRadius) {
9106          if (!arguments.length) {
9107              return this._yBorderRadius;
9108          }
9109          this._yBorderRadius = yBorderRadius;
9110          return this;
9111      }
9112
9113      isSelectedNode (d) {
9114          return this.hasFilter(d.key);
9115      }
9116  }
9117
9118  const heatMap = (parent, chartGroup) => new HeatMap(parent, chartGroup);
9119
9120  /**
9121   * htmlLegend is a attachable widget that can be added to other dc charts to render horizontal/vertical legend
9122   * labels.
9123   * @example
9124   * chart.legend(HtmlLegend().container(legendContainerElement).horizontal(false))
9125   * @returns {HtmlLegend}
9126   */
9127  class HtmlLegend {
9128      constructor () {
9129          this._htmlLegendDivCssClass = 'dc-html-legend';
9130          this._legendItemCssClassHorizontal = 'dc-legend-item-horizontal';
9131          this._legendItemCssClassVertical = 'dc-legend-item-vertical';
9132          this._parent = undefined;
9133          this._container = undefined;
9134          this._legendText = pluck('name');
9135          this._maxItems = undefined;
9136          this._horizontal = false;
9137          this._legendItemClass = undefined;
9138          this._highlightSelected = false;
9139          this._keyboardAccessible = false;
9140      }
9141
9142      parent (p) {
9143          if (!arguments.length) {
9144              return this._parent;
9145          }
9146          this._parent = p;
9147          return this;
9148      }
9149
9150      render () {
9151          this._defaultLegendItemCssClass = this._horizontal ? this._legendItemCssClassHorizontal : this._legendItemCssClassVertical;
9152          this._container.select(`div.${this._htmlLegendDivCssClass}`).remove();
9153
9154          const container = this._container.append('div').attr('class', this._htmlLegendDivCssClass);
9155          container.attr('style', `max-width:${this._container.nodes()[0].style.width}`);
9156
9157          let legendables = this._parent.legendables();
9158          const filters = this._parent.filters();
9159
9160          if (this._maxItems !== undefined) {
9161              legendables = legendables.slice(0, this._maxItems);
9162          }
9163
9164          const legendItemClassName = this._legendItemClass ? this._legendItemClass : this._defaultLegendItemCssClass;
9165
9166          const itemEnter = container.selectAll(`div.${legendItemClassName}`)
9167              .data(legendables).enter()
9168              .append('div')
9169              .classed(legendItemClassName, true)
9170              .on('mouseover', d3compat.eventHandler(d => this._parent.legendHighlight(d)))
9171              .on('mouseout', d3compat.eventHandler(d => this._parent.legendReset(d)))
9172              .on('click', d3compat.eventHandler(d => this._parent.legendToggle(d)));
9173
9174          if (this._highlightSelected) {
9175              itemEnter.classed(constants.SELECTED_CLASS, d => filters.indexOf(d.name) !== -1);
9176          }
9177
9178          itemEnter.append('span')
9179              .attr('class', 'dc-legend-item-color')
9180              .style('background-color', pluck('color'));
9181
9182          itemEnter.append('span')
9183              .attr('class', 'dc-legend-item-label')
9184              .classed('dc-tabbable', this._keyboardAccessible)
9185              .attr('title', this._legendText)
9186              .text(this._legendText);
9187
9188          if (this._keyboardAccessible) {
9189              this._makeLegendKeyboardAccessible();
9190          }
9191      }
9192
9193      /**
9194       * Set the container selector for the legend widget. Required.
9195       * @param {String} [container]
9196       * @return {String|HtmlLegend}
9197       */
9198      container (container) {
9199          if (!arguments.length) {
9200              return this._container;
9201          }
9202          this._container = d3Selection.select(container);
9203          return this;
9204      }
9205
9206      /**
9207       * This can be optionally used to override class for legenditem and just use this class style.
9208       * This is helpful for overriding the style of a particular chart rather than overriding
9209       * the style for all charts.
9210       *
9211       * Setting this will disable the highlighting of selected items also.
9212       * @param {String} [legendItemClass]
9213       * @return {String|HtmlLegend}
9214       */
9215      legendItemClass (legendItemClass) {
9216          if (!arguments.length) {
9217              return this._legendItemClass;
9218          }
9219          this._legendItemClass = legendItemClass;
9220          return this;
9221      }
9222
9223      /**
9224       * This can be optionally used to enable highlighting legends for the selections/filters for the
9225       * chart.
9226       * @param {String} [highlightSelected]
9227       * @return {String|HtmlLegend}
9228       */
9229      highlightSelected (highlightSelected) {
9230          if (!arguments.length) {
9231              return this._highlightSelected;
9232          }
9233          this._highlightSelected = highlightSelected;
9234          return this;
9235      }
9236
9237      /**
9238       * Display the legend horizontally instead of vertically
9239       * @param {String} [horizontal]
9240       * @return {String|HtmlLegend}
9241       */
9242      horizontal (horizontal) {
9243          if (!arguments.length) {
9244              return this._horizontal;
9245          }
9246          this._horizontal = horizontal;
9247          return this;
9248      }
9249
9250      /**
9251       * Set or get the legend text function. The legend widget uses this function to render the legend
9252       * text for each item. If no function is specified the legend widget will display the names
9253       * associated with each group.
9254       * @param  {Function} [legendText]
9255       * @returns {Function|HtmlLegend}
9256       * @example
9257       * // default legendText
9258       * legend.legendText(pluck('name'))
9259       *
9260       * // create numbered legend items
9261       * chart.legend(new HtmlLegend().legendText(function(d, i) { return i + '. ' + d.name; }))
9262       *
9263       * // create legend displaying group counts
9264       * chart.legend(new HtmlLegend().legendText(function(d) { return d.name + ': ' d.data; }))
9265       */
9266      legendText (legendText) {
9267          if (!arguments.length) {
9268              return this._legendText;
9269          }
9270          this._legendText = legendText;
9271          return this;
9272      }
9273
9274      /**
9275       * Maximum number of legend items to display
9276       * @param  {Number} [maxItems]
9277       * @return {HtmlLegend}
9278       */
9279      maxItems (maxItems) {
9280          if (!arguments.length) {
9281              return this._maxItems;
9282          }
9283          this._maxItems = utils.isNumber(maxItems) ? maxItems : undefined;
9284          return this;
9285      }
9286
9287      /**
9288       * If set, individual legend items will be focusable from keyboard and on pressing Enter or Space
9289       * will behave as if clicked on.
9290       * 
9291       * If `svgDescription` on the parent chart has not been explicitly set, will also set the default 
9292       * SVG description text to the class constructor name, like BarChart or HeatMap, and make the entire
9293       * SVG focusable.
9294       * @param {Boolean} [keyboardAccessible=false]
9295       * @returns {Boolean|HtmlLegend}
9296       */
9297      keyboardAccessible (keyboardAccessible) {
9298          if (!arguments.length) {
9299              return this._keyboardAccessible;
9300          }
9301          this._keyboardAccessible = keyboardAccessible;
9302          return this;
9303      }
9304
9305      _makeLegendKeyboardAccessible () {
9306
9307          if (!this._parent._svgDescription) {
9308
9309              this._parent.svg().append('desc')
9310                  .attr('id', `desc-id-${this._parent.__dcFlag__}`)
9311                  .html(`${this._parent.svgDescription()}`);
9312
9313              this._parent.svg()
9314                  .attr('tabindex', '0')
9315                  .attr('role', 'img')
9316                  .attr('aria-labelledby', `desc-id-${this._parent.__dcFlag__}`);
9317          }
9318
9319          const tabElements = this.container()
9320              .selectAll('.dc-legend-item-label.dc-tabbable')
9321              .attr('tabindex', 0);
9322
9323          tabElements
9324              .on('keydown', d3compat.eventHandler((d, event) => {
9325                  // trigger only if d is an object
9326                  if (event.keyCode === 13 && typeof d === 'object') {
9327                      d.chart.legendToggle(d);
9328                  } 
9329                  // special case for space key press - prevent scrolling
9330                  if (event.keyCode === 32 && typeof d === 'object') {
9331                      d.chart.legendToggle(d);
9332                      event.preventDefault();            
9333                  }
9334              }))
9335              .on('focus', d3compat.eventHandler(d => {
9336                  this._parent.legendHighlight(d);
9337              }))
9338              .on('blur', d3compat.eventHandler(d => {
9339                  this._parent.legendReset(d);
9340              }));
9341      }
9342  }
9343
9344  const htmlLegend = () => new HtmlLegend();
9345
9346  const LABEL_GAP = 2;
9347
9348  /**
9349   * Legend is a attachable widget that can be added to other dc charts to render horizontal legend
9350   * labels.
9351   *
9352   * Examples:
9353   * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
9354   * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats}
9355   * @example
9356   * chart.legend(new Legend().x(400).y(10).itemHeight(13).gap(5))
9357   * @returns {Legend}
9358   */
9359  class Legend {
9360      constructor () {
9361          this._parent = undefined;
9362          this._x = 0;
9363          this._y = 0;
9364          this._itemHeight = 12;
9365          this._gap = 5;
9366          this._horizontal = false;
9367          this._legendWidth = 560;
9368          this._itemWidth = 70;
9369          this._autoItemWidth = false;
9370          this._legendText = pluck('name');
9371          this._maxItems = undefined;
9372          this._highlightSelected = false;
9373          this._keyboardAccessible = false;
9374
9375          this._g = undefined;
9376      }
9377
9378      parent (p) {
9379          if (!arguments.length) {
9380              return this._parent;
9381          }
9382          this._parent = p;
9383          return this;
9384      }
9385
9386      /**
9387       * Set or get x coordinate for legend widget.
9388       * @param  {Number} [x=0]
9389       * @returns {Number|Legend}
9390       */
9391      x (x) {
9392          if (!arguments.length) {
9393              return this._x;
9394          }
9395          this._x = x;
9396          return this;
9397      }
9398
9399      /**
9400       * Set or get y coordinate for legend widget.
9401       * @param  {Number} [y=0]
9402       * @returns {Number|Legend}
9403       */
9404      y (y) {
9405          if (!arguments.length) {
9406              return this._y;
9407          }
9408          this._y = y;
9409          return this;
9410      }
9411
9412      /**
9413       * Set or get gap between legend items.
9414       * @param  {Number} [gap=5]
9415       * @returns {Number|Legend}
9416       */
9417      gap (gap) {
9418          if (!arguments.length) {
9419              return this._gap;
9420          }
9421          this._gap = gap;
9422          return this;
9423      }
9424
9425      /**
9426       * This can be optionally used to enable highlighting legends for the selections/filters for the
9427       * chart.
9428       * @param {String} [highlightSelected]
9429       * @return {String|dc.legend}
9430       **/
9431      highlightSelected (highlightSelected) {
9432          if (!arguments.length) {
9433              return this._highlightSelected;
9434          }
9435          this._highlightSelected = highlightSelected;
9436          return this;
9437      }
9438
9439      /**
9440       * Set or get legend item height.
9441       * @param  {Number} [itemHeight=12]
9442       * @returns {Number|Legend}
9443       */
9444      itemHeight (itemHeight) {
9445          if (!arguments.length) {
9446              return this._itemHeight;
9447          }
9448          this._itemHeight = itemHeight;
9449          return this;
9450      }
9451
9452      /**
9453       * Position legend horizontally instead of vertically.
9454       * @param  {Boolean} [horizontal=false]
9455       * @returns {Boolean|Legend}
9456       */
9457      horizontal (horizontal) {
9458          if (!arguments.length) {
9459              return this._horizontal;
9460          }
9461          this._horizontal = horizontal;
9462          return this;
9463      }
9464
9465      /**
9466       * Maximum width for horizontal legend.
9467       * @param  {Number} [legendWidth=500]
9468       * @returns {Number|Legend}
9469       */
9470      legendWidth (legendWidth) {
9471          if (!arguments.length) {
9472              return this._legendWidth;
9473          }
9474          this._legendWidth = legendWidth;
9475          return this;
9476      }
9477
9478      /**
9479       * Legend item width for horizontal legend.
9480       * @param  {Number} [itemWidth=70]
9481       * @returns {Number|Legend}
9482       */
9483      itemWidth (itemWidth) {
9484          if (!arguments.length) {
9485              return this._itemWidth;
9486          }
9487          this._itemWidth = itemWidth;
9488          return this;
9489      }
9490
9491      /**
9492       * Turn automatic width for legend items on or off. If true, {@link Legend#itemWidth itemWidth} is ignored.
9493       * This setting takes into account the {@link Legend#gap gap}.
9494       * @param  {Boolean} [autoItemWidth=false]
9495       * @returns {Boolean|Legend}
9496       */
9497      autoItemWidth (autoItemWidth) {
9498          if (!arguments.length) {
9499              return this._autoItemWidth;
9500          }
9501          this._autoItemWidth = autoItemWidth;
9502          return this;
9503      }
9504
9505      /**
9506       * Set or get the legend text function. The legend widget uses this function to render the legend
9507       * text for each item. If no function is specified the legend widget will display the names
9508       * associated with each group.
9509       * @param  {Function} [legendText]
9510       * @returns {Function|Legend}
9511       * @example
9512       * // default legendText
9513       * legend.legendText(pluck('name'))
9514       *
9515       * // create numbered legend items
9516       * chart.legend(new Legend().legendText(function(d, i) { return i + '. ' + d.name; }))
9517       *
9518       * // create legend displaying group counts
9519       * chart.legend(new Legend().legendText(function(d) { return d.name + ': ' d.data; }))
9520       */
9521      legendText (legendText) {
9522          if (!arguments.length) {
9523              return this._legendText;
9524          }
9525          this._legendText = legendText;
9526          return this;
9527      }
9528
9529      /**
9530       * Maximum number of legend items to display
9531       * @param  {Number} [maxItems]
9532       * @return {Legend}
9533       */
9534      maxItems (maxItems) {
9535          if (!arguments.length) {
9536              return this._maxItems;
9537          }
9538          this._maxItems = utils.isNumber(maxItems) ? maxItems : undefined;
9539          return this;
9540      }
9541
9542      /**
9543       * If set, individual legend items will be focusable from keyboard and on pressing Enter or Space
9544       * will behave as if clicked on.
9545       * 
9546       * If `svgDescription` on the parent chart has not been explicitly set, will also set the default 
9547       * SVG description text to the class constructor name, like BarChart or HeatMap, and make the entire
9548       * SVG focusable.
9549       * @param {Boolean} [keyboardAccessible=false]
9550       * @returns {Boolean|Legend}
9551       */
9552      keyboardAccessible (keyboardAccessible) {
9553          if (!arguments.length) {
9554              return this._keyboardAccessible;
9555          }
9556          this._keyboardAccessible = keyboardAccessible;
9557          return this;
9558      }
9559
9560      // Implementation methods
9561
9562      _legendItemHeight () {
9563          return this._gap + this._itemHeight;
9564      }
9565
9566      _makeLegendKeyboardAccessible () {
9567
9568          if (!this._parent._svgDescription) {
9569
9570              this._parent.svg().append('desc')
9571                  .attr('id', `desc-id-${this._parent.__dcFlag__}`)
9572                  .html(`${this._parent.svgDescription()}`);
9573
9574              this._parent.svg()
9575                  .attr('tabindex', '0')
9576                  .attr('role', 'img')
9577                  .attr('aria-labelledby', `desc-id-${this._parent.__dcFlag__}`);
9578          }
9579
9580          const tabElements = this._parent.svg()
9581              .selectAll('.dc-legend .dc-tabbable')
9582              .attr('tabindex', 0);
9583
9584          tabElements
9585              .on('keydown', d3compat.eventHandler((d, event) => {
9586                  // trigger only if d is an object
9587                  if (event.keyCode === 13 && typeof d === 'object') {
9588                      d.chart.legendToggle(d);
9589                  } 
9590                  // special case for space key press - prevent scrolling
9591                  if (event.keyCode === 32 && typeof d === 'object') {
9592                      d.chart.legendToggle(d);
9593                      event.preventDefault();            
9594                  }
9595              }))
9596              .on('focus', d3compat.eventHandler(d => {
9597                  this._parent.legendHighlight(d);
9598              }))
9599              .on('blur', d3compat.eventHandler(d => {
9600                  this._parent.legendReset(d);
9601              }));
9602      }
9603
9604      render () {
9605          this._parent.svg().select('g.dc-legend').remove();
9606          this._g = this._parent.svg().append('g')
9607              .attr('class', 'dc-legend')
9608              .attr('transform', `translate(${this._x},${this._y})`);
9609          let legendables = this._parent.legendables();
9610          const filters = this._parent.filters();
9611
9612          if (this._maxItems !== undefined) {
9613              legendables = legendables.slice(0, this._maxItems);
9614          }
9615
9616          const itemEnter = this._g.selectAll('g.dc-legend-item')
9617              .data(legendables)
9618              .enter()
9619              .append('g')
9620              .attr('class', 'dc-legend-item')
9621              .on('mouseover', d3compat.eventHandler(d => {
9622                  this._parent.legendHighlight(d);
9623              }))
9624              .on('mouseout', d3compat.eventHandler(d => {
9625                  this._parent.legendReset(d);
9626              }))
9627              .on('click', d3compat.eventHandler(d => {
9628                  d.chart.legendToggle(d);
9629              }));
9630
9631          if (this._highlightSelected) {
9632              itemEnter.classed(constants.SELECTED_CLASS,
9633                                d => filters.indexOf(d.name) !== -1);
9634          }
9635
9636
9637          this._g.selectAll('g.dc-legend-item')
9638              .classed('fadeout', d => d.chart.isLegendableHidden(d));
9639
9640          if (legendables.some(pluck('dashstyle'))) {
9641              itemEnter
9642                  .append('line')
9643                  .attr('x1', 0)
9644                  .attr('y1', this._itemHeight / 2)
9645                  .attr('x2', this._itemHeight)
9646                  .attr('y2', this._itemHeight / 2)
9647                  .attr('stroke-width', 2)
9648                  .attr('stroke-dasharray', pluck('dashstyle'))
9649                  .attr('stroke', pluck('color'));
9650          } else {
9651              itemEnter
9652                  .append('rect')
9653                  .attr('width', this._itemHeight)
9654                  .attr('height', this._itemHeight)
9655                  .attr('fill', d => d ? d.color : 'blue');
9656          }
9657
9658          {
9659              const self = this;
9660
9661              itemEnter.append('text')
9662                  .text(self._legendText)
9663                  .classed('dc-tabbable', this._keyboardAccessible)
9664                  .attr('x', self._itemHeight + LABEL_GAP)
9665                  .attr('y', function () {
9666                      return self._itemHeight / 2 + (this.clientHeight ? this.clientHeight : 13) / 2 - 2;
9667                  });
9668
9669              if (this._keyboardAccessible) {
9670                  this._makeLegendKeyboardAccessible();
9671              }
9672          }
9673
9674          let cumulativeLegendTextWidth = 0;
9675          let row = 0;
9676
9677          {
9678              const self = this;
9679
9680              itemEnter.attr('transform', function (d, i) {
9681                  if (self._horizontal) {
9682                      const itemWidth = self._autoItemWidth === true ? this.getBBox().width + self._gap : self._itemWidth;
9683                      if ((cumulativeLegendTextWidth + itemWidth) > self._legendWidth && cumulativeLegendTextWidth > 0) {
9684                          ++row;
9685                          cumulativeLegendTextWidth = 0;
9686                      }
9687                      const translateBy = `translate(${cumulativeLegendTextWidth},${row * self._legendItemHeight()})`;
9688                      cumulativeLegendTextWidth += itemWidth;
9689                      return translateBy;
9690                  } else {
9691                      return `translate(0,${i * self._legendItemHeight()})`;
9692                  }
9693              });
9694          }
9695      }
9696
9697  }
9698
9699  const legend = () => new Legend();
9700
9701  const DEFAULT_DOT_RADIUS = 5;
9702  const TOOLTIP_G_CLASS = 'dc-tooltip';
9703  const DOT_CIRCLE_CLASS = 'dot';
9704  const Y_AXIS_REF_LINE_CLASS = 'yRef';
9705  const X_AXIS_REF_LINE_CLASS = 'xRef';
9706  const DEFAULT_DOT_OPACITY = 1e-6;
9707  const LABEL_PADDING$1 = 3;
9708
9709  /**
9710   * Concrete line/area chart implementation.
9711   *
9712   * Examples:
9713   * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
9714   * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats}
9715   * @mixes StackMixin
9716   * @mixes CoordinateGridMixin
9717   */
9718  class LineChart extends StackMixin {
9719      /**
9720       * Create a Line Chart.
9721       * @example
9722       * // create a line chart under #chart-container1 element using the default global chart group
9723       * var chart1 = new LineChart('#chart-container1');
9724       * // create a line chart under #chart-container2 element using chart group A
9725       * var chart2 = new LineChart('#chart-container2', 'chartGroupA');
9726       * // create a sub-chart under a composite parent chart
9727       * var chart3 = new LineChart(compositeChart);
9728       * @param {String|node|d3.selection|CompositeChart} parent - Any valid
9729       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector}
9730       * specifying a dom block element such as a div; or a dom element or d3 selection.  If the line
9731       * chart is a sub-chart in a {@link CompositeChart Composite Chart} then pass in the parent
9732       * composite chart instance instead.
9733       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
9734       * Interaction with a chart will only trigger events and redraws within the chart's group.
9735       */
9736      constructor (parent, chartGroup) {
9737          super();
9738
9739          this._renderArea = false;
9740          this._dotRadius = DEFAULT_DOT_RADIUS;
9741          this._dataPointRadius = null;
9742          this._dataPointFillOpacity = DEFAULT_DOT_OPACITY;
9743          this._dataPointStrokeOpacity = DEFAULT_DOT_OPACITY;
9744          this._curve = null;
9745          this._interpolate = null; // d3.curveLinear;  // deprecated in 3.0
9746          this._tension = null;  // deprecated in 3.0
9747          this._defined = undefined;
9748          this._dashStyle = undefined;
9749          this._xyTipsOn = true;
9750
9751          this.transitionDuration(500);
9752          this.transitionDelay(0);
9753          this._rangeBandPadding(1);
9754
9755          this.label(d => utils.printSingleValue(d.y0 + d.y), false);
9756
9757          this.anchor(parent, chartGroup);
9758      }
9759
9760      plotData () {
9761          const chartBody = this.chartBodyG();
9762          let layersList = chartBody.select('g.stack-list');
9763
9764          if (layersList.empty()) {
9765              layersList = chartBody.append('g').attr('class', 'stack-list');
9766          }
9767
9768          let layers = layersList.selectAll('g.stack').data(this.data());
9769
9770          const layersEnter = layers
9771              .enter()
9772              .append('g')
9773              .attr('class', (d, i) => `stack _${i}`);
9774
9775          layers = layersEnter.merge(layers);
9776
9777          this._drawLine(layersEnter, layers);
9778
9779          this._drawArea(layersEnter, layers);
9780
9781          this._drawDots(chartBody, layers);
9782
9783          if (this.renderLabel()) {
9784              this._drawLabels(layers);
9785          }
9786      }
9787
9788      /**
9789       * Gets or sets the curve factory to use for lines and areas drawn, allowing e.g. step
9790       * functions, splines, and cubic interpolation. Typically you would use one of the interpolator functions
9791       * provided by {@link https://github.com/d3/d3-shape/blob/master/README.md#curves d3 curves}.
9792       *
9793       * Replaces the use of {@link LineChart#interpolate} and {@link LineChart#tension}
9794       * in dc.js < 3.0
9795       *
9796       * This is passed to
9797       * {@link https://github.com/d3/d3-shape/blob/master/README.md#line_curve line.curve} and
9798       * {@link https://github.com/d3/d3-shape/blob/master/README.md#area_curve area.curve}.
9799       * @example
9800       * // default
9801       * chart
9802       *     .curve(d3.curveLinear);
9803       * // Add tension to curves that support it
9804       * chart
9805       *     .curve(d3.curveCardinal.tension(0.5));
9806       * // You can use some specialized variation like
9807       * // https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline
9808       * chart
9809       *     .curve(d3.curveCatmullRom.alpha(0.5));
9810       * @see {@link https://github.com/d3/d3-shape/blob/master/README.md#line_curve line.curve}
9811       * @see {@link https://github.com/d3/d3-shape/blob/master/README.md#area_curve area.curve}
9812       * @param  {d3.curve} [curve=d3.curveLinear]
9813       * @returns {d3.curve|LineChart}
9814       */
9815      curve (curve) {
9816          if (!arguments.length) {
9817              return this._curve;
9818          }
9819          this._curve = curve;
9820          return this;
9821      }
9822
9823      /**
9824       * Gets or sets the interpolator to use for lines drawn, by string name, allowing e.g. step
9825       * functions, splines, and cubic interpolation.
9826       *
9827       * Possible values are: 'linear', 'linear-closed', 'step', 'step-before', 'step-after', 'basis',
9828       * 'basis-open', 'basis-closed', 'bundle', 'cardinal', 'cardinal-open', 'cardinal-closed', and
9829       * 'monotone'.
9830       *
9831       * This function exists for backward compatibility. Use {@link LineChart#curve}
9832       * which is generic and provides more options.
9833       * Value set through `.curve` takes precedence over `.interpolate` and `.tension`.
9834       * @deprecated since version 3.0 use {@link LineChart#curve} instead
9835       * @see {@link LineChart#curve}
9836       * @param  {d3.curve} [interpolate=d3.curveLinear]
9837       * @returns {d3.curve|LineChart}
9838       */
9839      interpolate (interpolate) {
9840          logger.warnOnce('dc.lineChart.interpolate has been deprecated since version 3.0 use dc.lineChart.curve instead');
9841          if (!arguments.length) {
9842              return this._interpolate;
9843          }
9844          this._interpolate = interpolate;
9845          return this;
9846      }
9847
9848      /**
9849       * Gets or sets the tension to use for lines drawn, in the range 0 to 1.
9850       *
9851       * Passed to the {@link https://github.com/d3/d3-shape/blob/master/README.md#curves d3 curve function}
9852       * if it provides a `.tension` function. Example:
9853       * {@link https://github.com/d3/d3-shape/blob/master/README.md#curveCardinal_tension curveCardinal.tension}.
9854       *
9855       * This function exists for backward compatibility. Use {@link LineChart#curve}
9856       * which is generic and provides more options.
9857       * Value set through `.curve` takes precedence over `.interpolate` and `.tension`.
9858       * @deprecated since version 3.0 use {@link LineChart#curve} instead
9859       * @see {@link LineChart#curve}
9860       * @param  {Number} [tension=0]
9861       * @returns {Number|LineChart}
9862       */
9863      tension (tension) {
9864          logger.warnOnce('dc.lineChart.tension has been deprecated since version 3.0 use dc.lineChart.curve instead');
9865          if (!arguments.length) {
9866              return this._tension;
9867          }
9868          this._tension = tension;
9869          return this;
9870      }
9871
9872      /**
9873       * Gets or sets a function that will determine discontinuities in the line which should be
9874       * skipped: the path will be broken into separate subpaths if some points are undefined.
9875       * This function is passed to
9876       * {@link https://github.com/d3/d3-shape/blob/master/README.md#line_defined line.defined}
9877       *
9878       * Note: crossfilter will sometimes coerce nulls to 0, so you may 
9878need to carefully write
9879       * custom reduce functions to get this to work, depending on your data. See
9880       * {@link https://github.com/dc-js/dc.js/issues/615#issuecomment-49089248 this GitHub comment}
9881       * for more details and an example.
9882       * @see {@link https://github.com/d3/d3-shape/blob/master/README.md#line_defined line.defined}
9883       * @param  {Function} [defined]
9884       * @returns {Function|LineChart}
9885       */
9886      defined (defined) {
9887          if (!arguments.length) {
9888              return this._defined;
9889          }
9890          this._defined = defined;
9891          return this;
9892      }
9893
9894      /**
9895       * Set the line's d3 dashstyle. This value becomes the 'stroke-dasharray' of line. Defaults to empty
9896       * array (solid line).
9897       * @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray stroke-dasharray}
9898       * @example
9899       * // create a Dash Dot Dot Dot
9900       * chart.dashStyle([3,1,1,1]);
9901       * @param  {Array<Number>} [dashStyle=[]]
9902       * @returns {Array<Number>|LineChart}
9903       */
9904      dashStyle (dashStyle) {
9905          if (!arguments.length) {
9906              return this._dashStyle;
9907          }
9908          this._dashStyle = dashStyle;
9909          return this;
9910      }
9911
9912      /**
9913       * Get or set render area flag. If the flag is set to true then the chart will render the area
9914       * beneath each line and the line chart effectively becomes an area chart.
9915       * @param  {Boolean} [renderArea=false]
9916       * @returns {Boolean|LineChart}
9917       */
9918      renderArea (renderArea) {
9919          if (!arguments.length) {
9920              return this._renderArea;
9921          }
9922          this._renderArea = renderArea;
9923          return this;
9924      }
9925
9926      _getColor (d, i) {
9927          return this.getColor.call(d, d.values, i);
9928      }
9929
9930      // To keep it backward compatible, this covers multiple cases
9931      // See https://github.com/dc-js/dc.js/issues/1376
9932      // It will be removed when interpolate and tension are removed.
9933      _getCurveFactory () {
9934          let curve = null;
9935
9936          // _curve takes precedence
9937          if (this._curve) {
9938              return this._curve;
9939          }
9940
9941          // Approximate the D3v3 behavior
9942          if (typeof this._interpolate === 'function') {
9943              curve = this._interpolate;
9944          } else {
9945              // If _interpolate is string
9946              const mapping = {
9947                  'linear': d3Shape.curveLinear,
9948                  'linear-closed': d3Shape.curveLinearClosed,
9949                  'step': d3Shape.curveStep,
9950                  'step-before': d3Shape.curveStepBefore,
9951                  'step-after': d3Shape.curveStepAfter,
9952                  'basis': d3Shape.curveBasis,
9953                  'basis-open': d3Shape.curveBasisOpen,
9954                  'basis-closed': d3Shape.curveBasisClosed,
9955                  'bundle': d3Shape.curveBundle,
9956                  'cardinal': d3Shape.curveCardinal,
9957                  'cardinal-open': d3Shape.curveCardinalOpen,
9958                  'cardinal-closed': d3Shape.curveCardinalClosed,
9959                  'monotone': d3Shape.curveMonotoneX
9960              };
9961              curve = mapping[this._interpolate];
9962          }
9963
9964          // Default value
9965          if (!curve) {
9966              curve = d3Shape.curveLinear;
9967          }
9968
9969          if (this._tension !== null) {
9970              if (typeof curve.tension !== 'function') {
9971                  logger.warn('tension was specified but the curve/interpolate does not support it.');
9972              } else {
9973                  curve = curve.tension(this._tension);
9974              }
9975          }
9976          return curve;
9977      }
9978
9979      _drawLine (layersEnter, layers) {
9980          const _line = d3Shape.line()
9981              .x(d => this.x()(d.x))
9982              .y(d => this.y()(d.y + d.y0))
9983              .curve(this._getCurveFactory());
9984          if (this._defined) {
9985              _line.defined(this._defined);
9986          }
9987
9988          const path = layersEnter.append('path')
9989              .attr('class', 'line')
9990              .attr('stroke', (d, i) => this._getColor(d, i));
9991          if (this._dashStyle) {
9992              path.attr('stroke-dasharray', this._dashStyle);
9993          }
9994
9995          transition(layers.select('path.line'), this.transitionDuration(), this.transitionDelay())
9996          //.ease('linear')
9997              .attr('stroke', (d, i) => this._getColor(d, i))
9998              .attr('d', d => this._safeD(_line(d.values)));
9999      }
10000
10001      _drawArea (layersEnter, layers) {
10002          if (this._renderArea) {
10003              const _area = d3Shape.area()
10004                  .x(d => this.x()(d.x))
10005                  .y1(d => this.y()(d.y + d.y0))
10006                  .y0(d => this.y()(d.y0))
10007                  .curve(this._getCurveFactory());
10008              if (this._defined) {
10009                  _area.defined(this._defined);
10010              }
10011
10012              layersEnter.append('path')
10013                  .attr('class', 'area')
10014                  .attr('fill', (d, i) => this._getColor(d, i))
10015                  .attr('d', d => this._safeD(_area(d.values)));
10016
10017              transition(layers.select('path.area'), this.transitionDuration(), this.transitionDelay())
10018              //.ease('linear')
10019                  .attr('fill', (d, i) => this._getColor(d, i))
10020                  .attr('d', d => this._safeD(_area(d.values)));
10021          }
10022      }
10023
10024      _safeD (d) {
10025          return (!d || d.indexOf('NaN') >= 0) ? 'M0,0' : d;
10026      }
10027
10028      _drawDots (chartBody, layers) {
10029          if (this.xyTipsOn() === 'always' || (!(this.brushOn() || this.parentBrushOn()) && this.xyTipsOn())) {
10030              const tooltipListClass = `${TOOLTIP_G_CLASS}-list`;
10031              let tooltips = chartBody.select(`g.${tooltipListClass}`);
10032
10033              if (tooltips.empty()) {
10034                  tooltips = chartBody.append('g').attr('class', tooltipListClass);
10035              }
10036
10037              layers.each((data, layerIndex) => {
10038                  let points = data.values;
10039                  if (this._defined) {
10040                      points = points.filter(this._defined);
10041                  }
10042
10043                  let g = tooltips.select(`g.${TOOLTIP_G_CLASS}._${layerIndex}`);
10044                  if (g.empty()) {
10045                      g = tooltips.append('g').attr('class', `${TOOLTIP_G_CLASS} _${layerIndex}`);
10046                  }
10047
10048                  this._createRefLines(g);
10049
10050                  const dots = g.selectAll(`circle.${DOT_CIRCLE_CLASS}`)
10051                      .data(points, pluck('x'));
10052
10053                  const chart = this;
10054                  const dotsEnterModify = dots
10055                      .enter()
10056                      .append('circle')
10057                      .attr('class', DOT_CIRCLE_CLASS)
10058                      .classed('dc-tabbable', this._keyboardAccessible)
10059                      .attr('cx', d => utils.safeNumber(this.x()(d.x)))
10060                      .attr('cy', d => utils.safeNumber(this.y()(d.y + d.y0)))
10061                      .attr('r', this._getDotRadius())
10062                      .style('fill-opacity', this._dataPointFillOpacity)
10063                      .style('stroke-opacity', this._dataPointStrokeOpacity)
10064                      .attr('fill', this.getColor)
10065                      .attr('stroke', this.getColor)
10066                      .on('mousemove', function () {
10067                          const dot = d3Selection.select(this);
10068                          chart._showDot(dot);
10069                          chart._showRefLines(dot, g);
10070                      })
10071                      .on('mouseout', function () {
10072                          const dot = d3Selection.select(this);
10073                          chart._hideDot(dot);
10074                          chart._hideRefLines(g);
10075                      })
10076                      .merge(dots);
10077
10078                  // special case for on-focus for line chart and its dots
10079                  if (this._keyboardAccessible) {
10080
10081                      this._svg.selectAll('.dc-tabbable')
10082                          .attr('tabindex', 0)
10083                          .on('focus', function () {
10084                              const dot = d3Selection.select(this);
10085                              chart._showDot(dot);
10086                              chart._showRefLines(dot, g);
10087                          })
10088                          .on('blur', function () {
10089                              const dot = d3Selection.select(this);
10090                              chart._hideDot(dot);
10091                              chart._hideRefLines(g);
10092                          });
10093                  }
10094
10095                  dotsEnterModify.call(dot => this._doRenderTitle(dot, data));
10096
10097                  transition(dotsEnterModify, this.transitionDuration())
10098                      .attr('cx', d => utils.safeNumber(this.x()(d.x)))
10099                      .attr('cy', d => utils.safeNumber(this.y()(d.y + d.y0)))
10100                      .attr('fill', this.getColor);
10101
10102                  dots.exit().remove();
10103              });
10104          }
10105      }
10106
10107      _drawLabels (layers) {
10108          const chart = this;
10109          layers.each(function (data, layerIndex) {
10110              const layer = d3Selection.select(this);
10111              const labels = layer.selectAll('text.lineLabel')
10112                  .data(data.values, pluck('x'));
10113
10114              const labelsEnterModify = labels
10115                  .enter()
10116                  .append('text')
10117                  .attr('class', 'lineLabel')
10118                  .attr('text-anchor', 'middle')
10119                  .merge(labels);
10120
10121              transition(labelsEnterModify, chart.transitionDuration())
10122                  .attr('x', d => utils.safeNumber(chart.x()(d.x)))
10123                  .attr('y', d => {
10124                      const y = chart.y()(d.y + d.y0) - LABEL_PADDING$1;
10125                      return utils.safeNumber(y);
10126                  })
10127                  .text(d =>
10127 chart.label()(d));
10128
10129              transition(labels.exit(), chart.transitionDuration())
10130                  .attr('height', 0)
10131                  .remove();
10132          });
10133      }
10134
10135      _createRefLines (g) {
10136          const yRefLine = g.select(`path.${Y_AXIS_REF_LINE_CLASS}`).empty() ?
10137              g.append('path').attr('class', Y_AXIS_REF_LINE_CLASS) : g.select(`path.${Y_AXIS_REF_LINE_CLASS}`);
10138          yRefLine.style('display', 'none').attr('stroke-dasharray', '5,5');
10139
10140          const xRefLine = g.select(`path.${X_AXIS_REF_LINE_CLASS}`).empty() ?
10141              g.append('path').attr('class', X_AXIS_REF_LINE_CLASS) : g.select(`path.${X_AXIS_REF_LINE_CLASS}`);
10142          xRefLine.style('display', 'none').attr('stroke-dasharray', '5,5');
10143      }
10144
10145      _showDot (dot) {
10146          dot.style('fill-opacity', 0.8);
10147          dot.style('stroke-opacity', 0.8);
10148          dot.attr('r', this._dotRadius);
10149          return dot;
10150      }
10151
10152      _showRefLines (dot, g) {
10153          const x = dot.attr('cx');
10154          const y = dot.attr('cy');
10155          const yAxisX = (this._yAxisX() - this.margins().left);
10156          const yAxisRefPathD = `M${yAxisX} ${y}L${x} ${y}`;
10157          const xAxisRefPathD = `M${x} ${this.yAxisHeight()}L${x} ${y}`;
10158          g.select(`path.${Y_AXIS_REF_LINE_CLASS}`).style('display', '').attr('d', yAxisRefPathD);
10159          g.select(`path.${X_AXIS_REF_LINE_CLASS}`).style('display', '').attr('d', xAxisRefPathD);
10160      }
10161
10162      _getDotRadius () {
10163          return this._dataPointRadius || this._dotRadius;
10164      }
10165
10166      _hideDot (dot) {
10167          dot.style('fill-opacity', this._dataPointFillOpacity)
10168              .style('stroke-opacity', this._dataPointStrokeOpacity)
10169              .attr('r', this._getDotRadius());
10170      }
10171
10172      _hideRefLines (g) {
10173          g.select(`path.${Y_AXIS_REF_LINE_CLASS}`).style('display', 'none');
10174          g.select(`path.${X_AXIS_REF_LINE_CLASS}`).style('display', 'none');
10175      }
10176
10177      _doRenderTitle (dot, d) {
10178          if (this.renderTitle()) {
10179              dot.select('title').remove();
10180              dot.append('title').text(pluck('data', this.title(d.name)));
10181          }
10182      }
10183
10184      /**
10185       * Turn on/off the mouseover behavior of an individual data point which renders a circle and x/y axis
10186       * dashed lines back to each respective axis.  This is ignored if the chart
10187       * {@link CoordinateGridMixin#brushOn brush} is on
10188       * @param  {Boolean} [xyTipsOn=false]
10189       * @returns {Boolean|LineChart}
10190       */
10191      xyTipsOn (xyTipsOn) {
10192          if (!arguments.length) {
10193              return this._xyTipsOn;
10194          }
10195          this._xyTipsOn = xyTipsOn;
10196          return this;
10197      }
10198
10199      /**
10200       * Get or set the radius (in px) for dots displayed on the data points.
10201       * @param  {Number} [dotRadius=5]
10202       * @returns {Number|LineChart}
10203       */
10204      dotRadius (dotRadius) {
10205          if (!arguments.length) {
10206              return this._dotRadius;
10207          }
10208          this._dotRadius = dotRadius;
10209          return this;
10210      }
10211
10212      /**
10213       * Always show individual dots for each datapoint.
10214       *
10215       * If `options` is falsy, it disables data point rendering. If no `options` are provided, the
10216       * current `options` values are instead returned.
10217       * @example
10218       * chart.renderDataPoints({radius: 2, fillOpacity: 0.8, strokeOpacity: 0.0})
10219       * @param  {{fillOpacity: Number, strokeOpacity: Number, radius: Number}} [options={fillOpacity: 0.8, strokeOpacity: 0.0, radius: 2}]
10220       * @returns {{fillOpacity: Number, strokeOpacity: Number, radius: Number}|LineChart}
10221       */
10222      renderDataPoints (options) {
10223          if (!arguments.length) {
10224              return {
10225                  fillOpacity: this._dataPointFillOpacity,
10226                  strokeOpacity: this._dataPointStrokeOpacity,
10227                  radius: this._dataPointRadius
10228              };
10229          } else if (!options) {
10230              this._dataPointFillOpacity = DEFAULT_DOT_OPACITY;
10231              this._dataPointStrokeOpacity = DEFAULT_DOT_OPACITY;
10232              this._dataPointRadius = null;
10233          } else {
10234              this._dataPointFillOpacity = options.fillOpacity || 0.8;
10235              this._dataPointStrokeOpacity = options.strokeOpacity || 0.0;
10236              this._dataPointRadius = options.radius || 2;
10237          }
10238          return this;
10239      }
10240
10241      _colorFilter (color, dashstyle, inv) {
10242          return function () {
10243              const item = d3Selection.select(this);
10244              const match = (item.attr('stroke') === color &&
10245                  item.attr('stroke-dasharray') === ((dashstyle instanceof Array) ?
10246                      dashstyle.join(',') : null)) || item.attr('fill') === color;
10247              return inv ? !match : match;
10248          };
10249      }
10250
10251      legendHighlight (d) {
10252          if (!this.isLegendableHidden(d)) {
10253              this.g().selectAll('path.line, path.area')
10254                  .classed('highlight', this._colorFilter(d.color, d.dashstyle))
10255                  .classed('fadeout', this._colorFilter(d.color, d.dashstyle, true));
10256          }
10257      }
10258
10259      legendReset () {
10260          this.g().selectAll('path.line, path.area')
10261              .classed('highlight', false)
10262              .classed('fadeout', false);
10263      }
10264
10265      legendables () {
10266          const legendables = super.legendables();
10267          if (!this._dashStyle) {
10268              return legendables;
10269          }
10270          return legendables.map(l => {
10271              l.dashstyle = this._dashStyle;
10272              return l;
10273          });
10274      }
10275  }
10276
10277  const lineChart = (parent, chartGroup) => new LineChart(parent, chartGroup);
10278
10279  const SPAN_CLASS = 'number-display';
10280
10281  /**
10282   * A display of a single numeric value.
10283   *
10284   * Unlike other charts, you do not need to set a dimension. Instead a group object must be provided and
10285   * a valueAccessor that returns a single value.
10286   *
10287   * If the group is a {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#crossfilter_groupAll groupAll}
10288   * then its `.value()` will be displayed. This is the recommended usage.
10289   *
10290   * However, if it is given an ordinary group, the `numberDisplay` will show the last bin's value, after
10291   * sorting with the {@link https://dc-js.github.io/dc.js/docs/html/dc.baseMixin.html#ordering__anchor ordering}
10292   * function. `numberDisplay` defaults the `ordering` function to sorting by value, so this will display
10293   * the largest value if the values are numeric.
10294   * @mixes BaseMixin
10295   */
10296  class NumberDisplay extends BaseMixin {
10297      /**
10298       * Create a Number Display widget.
10299       *
10300       * @example
10301       * // create a number display under #chart-container1 element using the default global chart group
10302       * var display1 = new NumberDisplay('#chart-container1');
10303       * @param {String|node|d3.selection} parent - Any valid
10304       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
10305       * a dom block element such as a div; or a dom element or d3 selection.
10306       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
10307       * Interaction with a chart will only trigger events and redraws within the chart's group.
10308       */
10309      constructor (parent, chartGroup) {
10310          super();
10311
10312          this._formatNumber = d3Format.format('.2s');
10313          this._html = {one: '', some: '', none: ''};
10314          this._lastValue = undefined;
10315          this._ariaLiveRegion = false;
10316
10317          // dimension not required
10318          this._mandatoryAttributes(['group']);
10319
10320          // default to ordering by value, to emulate old group.top(1) behavior when multiple groups
10321          this.ordering(kv => kv.value);
10322
10323          this.data(group => {
10324              const valObj = group.value ? group.value() : this._maxBin(group.all());
10325              return this.valueAccessor()(valObj);
10326          });
10327
10328          this.transitionDuration(250); // good default
10329          this.transitionDelay(0);
10330
10331          this.anchor(parent, chartGroup);
10332      }
10333
10334      /**
10335       * Gets or sets an optional object specifying HTML templates to use depending on the number
10336       * displayed.  The text `%number` will be replaced with the current value.
10337       * - one: HTML template to use if the number is 1
10338       * - zero: HTML template to use if the number is 0
10339       * - some: HTML template to use otherwise
10340       * @example
10341       * numberWidget.html({
10342       *      one:'%number record',
10343       *      some:'%number records',
10344       *      none:'no records'})
10345       * @param {{one:String, some:String, none:String}} [html={one: '', some: '', none: ''}]
10346       * @returns {{one:String, some:String, none:String}|NumberDisplay}
10347       */
10348      html (html) {
10349          if (!arguments.length) {
10350              return this._html;
10351          }
10352          if (html.none) {
10353              this._html.none = html.none;//if none available
10354          } else if (html.one) {
10355              this._html.none = html.one;//if none not available use one
10356          } else if (html.some) {
10357              this._html.none = html.some;//if none and one not available use some
10358          }
10359          if (html.one) {
10360              this._html.one = html.one;//if one available
10361          } else if (html.some) {
10362              this._html.one = html.some;//if one not available use some
10363          }
10364          if (html.some) {
10365              this._html.some = html.some;//if some available
10366          } else if (html.one) {
10367              this._html.some = html.one;//if some not available use one
10368          }
10369          return this;
10370      }
10371
10372      /**
10373       * Calculate and return the underlying value of the display.
10374       * @returns {Number}
10375       */
10376      value () {
10377          return this.data();
10378      }
10379
10380      _maxBin (all) {
10381          if (!all.length) {
10382              return null;
10383          }
10384          const sorted = this._computeOrderedGroups(all);
10385          return sorted[sorted.length - 1];
10386      }
10387
10388      _doRender () {
10389          const newValue = this.value();
10390          let span = this.selectAll(`.${SPAN_CLASS}`);
10391
10392          if (span.empty()) {
10393              span = span.data([0])
10394                  .enter()
10395                  .append('span')
10396                  .attr('class', SPAN_CLASS)
10397                  .classed('dc-tabbable', this._keyboardAccessible)
10398                  .merge(span);
10399
10400              if (this._keyboardAccessible) {
10401                  span.attr('tabindex', '0');
10402              }
10403
10404              if (this._ariaLiveRegion) {
10405                  this.transitionDuration(0);
10406                  span.attr('aria-live', 'polite');
10407              }
10408          }
10409
10410          {
10411              const chart = this;
10412              span.transition()
10413                  .duration(chart.transitionDuration())
10414                  .delay(chart.transitionDelay())
10415                  .ease(d3Ease.easeQuad)
10416                  .tween('text', function () {
10417                      // [XA] don't try and interpolate from Infinity, else this breaks.
10418                      const interpStart = isFinite(chart._lastValue) ? chart._lastValue : 0;
10419                      const interp = d3Interpolate.interpolateNumber(interpStart || 0, newValue);
10420                      chart._lastValue = newValue;
10421
10422                      // need to save it in D3v4
10423                      const node = this;
10424                      return t => {
10425                          let html = null;
10426                          const num = chart.formatNumber()(interp(t));
10427                          if (newValue === 0 && (chart._html.none !== '')) {
10428                              html = chart._html.none;
10429                          } else if (newValue === 1 && (chart._html.one !== '')) {
10430                              html = chart._html.one;
10431                          } else if (chart._html.some !== '') {
10432                              html = chart._html.some;
10433                          }
10434                          node.innerHTML = html ? html.replace('%number', num) : num;
10435                      };
10436                  });
10437          }
10438      }
10439
10440      _doRedraw () {
10441          return this._doRender();
10442      }
10443
10444      /**
10445       * Get or set a function to format the value for the display.
10446       * @see {@link https://github.com/d3/d3-format/blob/master/README.md#format d3.format}
10447       * @param {Function} [formatter=d3.format('.2s')]
10448       * @returns {Function|NumberDisplay}
10449       */
10450      formatNumber (formatter) {
10451          if (!arguments.length) {
10452              return this._formatNumber;
10453          }
10454          this._formatNumber = formatter;
10455          return this;
10456      }
10457
10458      /**
10459       * If set, the Number Display widget will have its aria-live attribute set to 'polite' which will
10460       * notify screen readers when the widget changes its value. Note that setting this method will also
10461       * disable the default transition between the old and the new values. This is to avoid change
10462       * notifications spoken out before the new value finishes re-drawing. It is also advisable to check
10463       * if the widget has appropriately set accessibility description or label. 
10464       * @param {Boolean} [ariaLiveRegion=false]
10465       * @returns {Boolean|NumberDisplay}
10466       */
10467      ariaLiveRegion (ariaLiveRegion) {
10468          if (!arguments.length) {
10469              return this._ariaLiveRegion;
10470          }
10471          this._ariaLiveRegion = ariaLiveRegion;
10472          return this;
10473      }
10474
10475  }
10476
10477  const numberDisplay = (parent, chartGroup) => new NumberDisplay(parent, chartGroup);
10478
10479  const DEFAULT_MIN_ANGLE_FOR_LABEL = 0.5;
10480
10481  /**
10482   * The pie chart implementation is usually used to visualize a small categorical distribution.  The pie
10483   * chart uses keyAccessor to determine the slices, and valueAccessor to calculate the size of each
10484   * slice relative to the sum of all values. Slices are ordered by {@link BaseMixin#ordering ordering}
10485   * which defaults to sorting by key.
10486   *
10487   * Examples:
10488   * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
10489   * @mixes CapMixin
10490   * @mixes ColorMixin
10491   * @mixes BaseMixin
10492   */
10493  class PieChart extends CapMixin(ColorMixin(BaseMixin)) {
10494      /**
10495       * Create a Pie Chart
10496       *
10497       * @example
10498       * // create a pie chart under #chart-container1 element using the default global chart group
10499       * var chart1 = new PieChart('#chart-container1');
10500       * // create a pie chart under #chart-container2 element using chart group A
10501       * var chart2 = new PieChart('#chart-container2', 'chartGroupA');
10502       * @param {String|node|d3.selection} parent - Any valid
10503       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
10504       * a dom block element such as a div; or a dom element or d3 selection.
10505       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
10506       * Interaction with a chart will only trigger events and redraws within the chart'
10506s group.
10507       */
10508      constructor (parent, chartGroup) {
10509          super();
10510
10511          this._sliceCssClass = 'pie-slice';
10512          this._labelCssClass = 'pie-label';
10513          this._sliceGroupCssClass = 'pie-slice-group';
10514          this._labelGroupCssClass = 'pie-label-group';
10515          this._emptyCssClass = 'empty-chart';
10516          this._emptyTitle = 'empty';
10517
10518          this._radius = undefined;
10519          this._givenRadius = undefined; // specified radius, if any
10520          this._innerRadius = 0;
10521          this._externalRadiusPadding = 0;
10522
10523
10524          this._g = undefined;
10525          this._cx = undefined;
10526          this._cy = undefined;
10527          this._minAngleForLabel = DEFAULT_MIN_ANGLE_FOR_LABEL;
10528          this._externalLabelRadius = undefined;
10529          this._drawPaths = false;
10530
10531          this.colorAccessor(d => this.cappedKeyAccessor(d));
10532
10533          this.title(d => `${this.cappedKeyAccessor(d)}: ${this.cappedValueAccessor(d)}`);
10534
10535          this.label(d => this.cappedKeyAccessor(d));
10536          this.renderLabel(true);
10537
10538          this.transitionDuration(350);
10539          this.transitionDelay(0);
10540
10541          this.anchor(parent, chartGroup);
10542      }
10543
10544      /**
10545       * Get or set the maximum number of slices the pie chart will generate. The top slices are determined by
10546       * value from high to low. Other slices exceeding the cap will be rolled up into one single *Others* slice.
10547       * @param {Number} [cap]
10548       * @returns {Number|PieChart}
10549       */
10550      slicesCap (cap) {
10551          return this.cap(cap)
10552      }
10553
10554      _doRender () {
10555          this.resetSvg();
10556
10557          this._g = this.svg()
10558              .append('g')
10559              .attr('transform', `translate(${this.cx()},${this.cy()})`);
10560
10561          this._g.append('g').attr('class', this._sliceGroupCssClass);
10562          this._g.append('g').attr('class', this._labelGroupCssClass);
10563
10564          this._drawChart();
10565
10566          return this;
10567      }
10568
10569      _drawChart () {
10570          // set radius from chart size if none given, or if given radius is too large
10571          const maxRadius = d3Array.min([this.width(), this.height()]) / 2;
10572          this._radius = this._givenRadius && this._givenRadius < maxRadius ? this._givenRadius : maxRadius;
10573
10574          const arcs = this._buildArcs();
10575
10576          const pieLayout = this._pieLayout();
10577          let pieData;
10578          // if we have data...
10579          if (d3Array.sum(this.data(), d => this.cappedValueAccessor(d))) {
10580              pieData = pieLayout(this.data());
10581              this._g.classed(this._emptyCssClass, false);
10582          } else {
10583              // otherwise we'd be getting NaNs, so override
10584              // note: abuse others for its ignoring the value accessor
10585              pieData = pieLayout([{key: this._emptyTitle, value: 1, others: [this._emptyTitle]}]);
10586              this._g.classed(this._emptyCssClass, true);
10587          }
10588
10589          if (this._g) {
10590              const slices = this._g.select(`g.${this._sliceGroupCssClass}`)
10591                  .selectAll(`g.${this._sliceCssClass}`)
10592                  .data(pieData);
10593
10594              const labels = this._g.select(`g.${this._labelGroupCssClass}`)
10595                  .selectAll(`text.${this._labelCssClass}`)
10596                  .data(pieData);
10597
10598              this._removeElements(slices, labels);
10599
10600              this._createElements(slices, labels, arcs, pieData);
10601
10602              this._updateElements(pieData, arcs);
10603
10604              this._highlightFilter();
10605
10606              transition(this._g, this.transitionDuration(), this.transitionDelay())
10607                  .attr('transform', `translate(${this.cx()},${this.cy()})`);
10608          }
10609      }
10610
10611      _createElements (slices, labels, arcs, pieData) {
10612          const slicesEnter = this._createSliceNodes(slices);
10613
10614          this._createSlicePath(slicesEnter, arcs);
10615
10616          this._createTitles(slicesEnter);
10617
10618          this._createLabels(labels, pieData, arcs);
10619      }
10620
10621      _createSliceNodes (slices) {
10622          return slices
10623              .enter()
10624              .append('g')
10625              .attr('class', (d, i) => `${this._sliceCssClass} _${i}`)
10626              .classed('dc-tabbable', this._keyboardAccessible);
10627      }
10628
10629      _createSlicePath (slicesEnter, arcs) {
10630          const slicePath = slicesEnter.append('path')
10631              .attr('fill', (d, i) => this._fill(d, i))
10632              .on('click', d3compat.eventHandler(d => this._onClick(d)))
10633              .attr('d', (d, i) => this._safeArc(d, i, arcs));
10634
10635          if (this._keyboardAccessible) {
10636              this._makeKeyboardAccessible(this._onClick);
10637          }
10638
10639          const tranNodes = transition(slicePath, this.transitionDuration(), this.transitionDelay());
10640          if (tranNodes.attrTween) {
10641              const chart = this;
10642              tranNodes.attrTween('d', function (d) {
10643                  return chart._tweenPie(d, this);
10644              });
10645          }
10646      }
10647
10648      _createTitles (slicesEnter) {
10649          if (this.renderTitle()) {
10650              slicesEnter.append('title').text(d => this.title()(d.data));
10651          }
10652      }
10653
10654      _applyLabelText (labels) {
10655          labels
10656              .text(d => {
10657                  const data = d.data;
10658                  if ((this._sliceHasNoData(data) || this._sliceTooSmall(d)) && !this._isSelectedSlice(d)) {
10659                      return '';
10660                  }
10661                  return this.label()(d.data);
10662              });
10663      }
10664
10665      _positionLabels (labels, arcs) {
10666          this._applyLabelText(labels);
10667          transition(labels, this.transitionDuration(), this.transitionDelay())
10668              .attr('transform', d => this._labelPosition(d, arcs))
10669              .attr('text-anchor', 'middle');
10670      }
10671
10672      _highlightSlice (i, whether) {
10673          this.select(`g.pie-slice._${i}`)
10674              .classed('highlight', whether);
10675      }
10676
10677      _createLabels (labels, pieData, arcs) {
10678          if (this.renderLabel()) {
10679              const labelsEnter = labels
10680                  .enter()
10681                  .append('text')
10682                  .attr('class', (d, i) => {
10683                      let classes = `${this._sliceCssClass} ${this._labelCssClass} _${i}`;
10684                      if (this._externalLabelRadius) {
10685                          classes += ' external';
10686                      }
10687                      return classes;
10688                  })
10689                  .on('click', d3compat.eventHandler(d => this._onClick(d)))
10690                  .on('mouseover', d3compat.eventHandler(d => {
10691                      this._highlightSlice(d.index, true);
10692                  }))
10693                  .on('mouseout', d3compat.eventHandler(d => {
10694                      this._highlightSlice(d.index, false);
10695                  }));
10696              this._positionLabels(labelsEnter, arcs);
10697              if (this._externalLabelRadius && this._drawPaths) {
10698                  this._updateLabelPaths(pieData, arcs);
10699              }
10700          }
10701      }
10702
10703      _updateLabelPaths (pieData, arcs) {
10704          let polyline = this._g.selectAll(`polyline.${this._sliceCssClass}`)
10705              .data(pieData);
10706
10707          polyline.exit().remove();
10708
10709          polyline = polyline
10710              .enter()
10711              .append('polyline')
10712              .attr('class', (d, i) => `pie-path _${i} ${this._sliceCssClass}`)
10713              .on('click', d3compat.eventHandler(d => this._onClick(d)))
10714              .on('mouseover', d3compat.eventHandler(d => {
10715                  this._highlightSlice(d.index, true);
10716              }))
10717              .on('mouseout', d3compat.eventHandler(d => {
10718                  this._highlightSlice(d.index, false);
10719              }))
10720              .merge(polyline);
10721
10722          const arc2 = d3Shape.arc()
10723              .outerRadius(this._radius - this._externalRadiusPadding + this._externalLabelRadius)
10724              .innerRadius(this._radius - this._externalRadiusPadding);
10725          const tranNodes = transition(polyline, this.transitionDuration(), this.transitionDelay());
10726          // this is one rare case where d3.selection differs from d3.transition
10727          if (tranNodes.attrTween) {
10728              tranNodes
10729                  .attrTween('points', function (d) {
10730                      let current = this._current || d;
10731                      current = {startAngle: current.startAngle, endAngle: current.endAngle};
10732                      const _interpolate = d3Interpolate.interpolate(current, d);
10733                      this._current = _interpolate(0);
10734                      return t => {
10735                          const d2 = _interpolate(t);
10736                          return [arcs.centroid(d2), arc2.centroid(d2)];
10737                      };
10738                  });
10739          } else {
10740              tranNodes.attr('points', d => [arcs.centroid(d), arc2.centroid(d)]);
10741          }
10742          tranNodes.style('visibility', d => d.endAngle - d.startAngle < 0.0001 ? 'hidden' : 'visible');
10743
10744      }
10745
10746      _updateElements (pieData, arcs) {
10747          this._updateSlicePaths(pieData, arcs);
10748          this._updateLabels(pieData, arcs);
10749          this._updateTitles(pieData);
10750      }
10751
10752      _updateSlicePaths (pieData, arcs) {
10753          const slicePaths = this._g.selectAll(`g.${this._sliceCssClass}`)
10754              .data(pieData)
10755              .select('path')
10756              .attr('d', (d, i) => this._safeArc(d, i, arcs));
10757          const tranNodes = transition(slicePaths, this.transitionDuration(), this.transitionDelay());
10758          if (tranNodes.attrTween) {
10759              const chart = this;
10760              tranNodes.attrTween('d', function (d) {
10761                  return chart._tweenPie(d, this);
10762              });
10763          }
10764          tranNodes.attr('fill', (d, i) => this._fill(d, i));
10765      }
10766
10767      _updateLabels (pieData, arcs) {
10768          if (this.renderLabel()) {
10769              const labels = this._g.selectAll(`text.${this._labelCssClass}`)
10770                  .data(pieData);
10771              this._positionLabels(labels, arcs);
10772              if (this._externalLabelRadius && this._drawPaths) {
10773                  this._updateLabelPaths(pieData, arcs);
10774              }
10775          }
10776      }
10777
10778      _updateTitles (pieData) {
10779          if (this.renderTitle()) {
10780              this._g.selectAll(`g.${this._sliceCssClass}`)
10781                  .data(pieData)
10782                  .select('title')
10783                  .text(d => this.title()(d.data));
10784          }
10785      }
10786
10787      _removeElements (slices, labels) {
10788          slices.exit().remove();
10789          labels.exit().remove();
10790      }
10791
10792      _highlightFilter () {
10793          const chart = this;
10794          if (this.hasFilter()) {
10795              this.selectAll(`g.${this._sliceCssClass}`).each(function (d) {
10796                  if (chart._isSelectedSlice(d)) {
10797                      chart.highlightSelected(this);
10798                  } else {
10799                      chart.fadeDeselected(this);
10800                  }
10801              });
10802          } else {
10803              this.selectAll(`g.${this._sliceCssClass}`).each(function () {
10804                  chart.resetHighlight(this);
10805              });
10806          }
10807      }
10808
10809      /**
10810       * Get or set the external radius padding of the pie chart. This will force the radius of the
10811       * pie chart to become smaller or larger depending on the value.
10812       * @param {Number} [externalRadiusPadding=0]
10813       * @returns {Number|PieChart}
10814       */
10815      externalRadiusPadding (externalRadiusPadding) {
10816          if (!arguments.length) {
10817              return this._externalRadiusPadding;
10818          }
10819          this._externalRadiusPadding = externalRadiusPadding;
10820          return this;
10821      }
10822
10823      /**
10824       * Get or set the inner radius of the pie chart. If the inner radius is greater than 0px then the
10825       * pie chart will be rendered as a doughnut chart.
10826       * @param {Number} [innerRadius=0]
10827       * @returns {Number|PieChart}
10828       */
10829      innerRadius (innerRadius) {
10830          if (!arguments.length) {
10831              return this._innerRadius;
10832          }
10833          this._innerRadius = innerRadius;
10834          return this;
10835      }
10836
10837      /**
10838       * Get or set the outer radius. If the radius is not set, it will be half of the minimum of the
10839       * chart width and height.
10840       * @param {Number} [radius]
10841       * @returns {Number|PieChart}
10842       */
10843      radius (radius) {
10844          if (!arguments.length) {
10845              return this._givenRadius;
10846          }
10847          this._givenRadius = radius;
10848          return this;
10849      }
10850
10851      /**
10852       * Get or set center x coordinate position. Default is center of svg.
10853       * @param {Number} [cx]
10854       * @returns {Number|PieChart}
10855       */
10856      cx (cx) {
10857          if (!arguments.length) {
10858              return (this._cx || this.width() / 2);
10859          }
10860          this._cx = cx;
10861          return this;
10862      }
10863
10864      /**
10865       * Get or set center y coordinate position. Default is center of svg.
10866       * @param {Number} [cy]
10867       * @returns {Number|PieChart}
10868       */
10869      cy (cy) {
10870          if (!arguments.length) {
10871              return (this._cy || this.height() / 2);
10872          }
10873          this._cy = cy;
10874          return this;
10875      }
10876
10877      _buildArcs () {
10878          return d3Shape.arc()
10879              .outerRadius(this._radius - this._externalRadiusPadding)
10880              .innerRadius(this._innerRadius);
10881      }
10882
10883      _isSelectedSlice (d) {
10884          return this.hasFilter(this.cappedKeyAccessor(d.data));
10885      }
10886
10887      _doRedraw () {
10888          this._drawChart();
10889          return this;
10890      }
10891
10892      /**
10893       * Get or set the minimal slice angle for label rendering. Any slice with a smaller angle will not
10894       * display a slice label.
10895       * @param {Number} [minAngleForLabel=0.5]
10896       * @returns {Number|PieChart}
10897       */
10898      minAngleForLabel (minAngleForLabel) {
10899          if (!arguments.length) {
10900              return this._minAngleForLabel;
10901          }
10902          this._minAngleForLabel = minAngleForLabel;
10903          return this;
10904      }
10905
10906      _pieLayout () {
10907          return d3Shape.pie().sort(null).value(d => this.cappedValueAccessor(d));
10908      }
10909
10910      _sliceTooSmall (d) {
10911          const angle = (d.endAngle - d.startAngle);
10912          return isNaN(angle) || angle < this._minAngleForLabel;
10913      }
10914
10915      _sliceHasNoData (d) {
10916          return this.cappedValueAccessor(d) === 0;
10917      }
10918
10919      _isOffCanvas (current) {
10920          return !current || isNaN(current.startAngle) || isNaN(current.endAngle);
10921      }
10922
10923      _fill (d, i) {
10924          return this.getColor(d.data, i);
10925      }
10926
10927      _onClick (d) {
10928          if (this._g.attr('class') !== this._emptyCssClass) {
10929              this.onClick(d.data);
10930          }
10931      }
10932
10933      _safeArc (d, i, _arc) {
10934          let path = _arc(d, i);
10935          if (path.indexOf('NaN') >= 0) {
10936              path = 'M0,0';
10937          }
10938          return path;
10939      }
10940
10941      /**
10942       * Title to use for the only slice when there is no data.
10943       * @param {String} [title]
10944       * @returns {String|PieChart}
10945       */
10946      emptyTitle (title) {
10947          if (arguments.length === 0) {
10948              return this._emptyTitle;
10949          }
10950          this._emptyTitle = title;
10951          return this;
10952      }
10953
10954      /**
10955       * Position slice labels offset from the outer edge of the chart.
10956       *
10957       * The argument specifies the extra radius to be added for slice labels.
10958       * @param {Number} [externalLabelRadius]
10959       * @returns {Number|PieChart}
10960       */
10961      externalLabels (externalLabelRadius) {
10962          if (arguments.length === 0) {
10963              return this._externalLabelRadius;
10964          } else if (externalLabelRadius) {
10965              this._externalLabelRadius = externalLabelRadius;
10966          } else {
10967              this._externalLabelRadius = undefined;
10968          }
10969
10970          return this;
10971      }
10972
10973      /**
10974       * Get or set whether to draw lines from pie slices to their labels.
10975       *
10976       * @param {Boolean} [drawPaths]
10977       * @returns {Boolean|PieChart}
10978       */
10979      drawPaths (drawPaths) {
10980          if (arguments.length === 0) {
10981              return this._drawPaths;
10982          }
10983          this._drawPaths = drawPaths;
10984          return this;
10985      }
10986
10987      _labelPosition (d, _arc) {
10988          let centroid;
10989          if (this._externalLabelRadius) {
10990              centroid = d3Shape.arc()
10991                  .outerRadius(this._radius - this._externalRadiusPadding + this._externalLabelRadius)
10992                  .innerRadius(this._radius - this._externalRadiusPadding + this._externalLabelRadius)
10993                  .centroid(d);
10994          } else {
10995              centroid = _arc.centroid(d);
10996          }
10997          if (isNaN(centroid[0]) || isNaN(centroid[1])) {
10998              return 'translate(0,0)';
10999          } else {
11000              return `translate(${centroid})`;
11001          }
11002      }
11003
11004      legendables () {
11005          return this.data().map((d, i) => {
11006              const legendable = {name: d.key, data: d.value, others: d.others, chart: this};
11007              legendable.color = this.getColor(d, i);
11008              return legendable;
11009          });
11010      }
11011
11012      legendHighlight (d) {
11013          this._highlightSliceFromLegendable(d, true);
11014      }
11015
11016      legendReset (d) {
11017          this._highlightSliceFromLegendable(d, false);
11018      }
11019
11020      legendToggle (d) {
11021          this.onClick({key: d.name, others: d.others});
11022      }
11023
11024      _highlightSliceFromLegendable (legendable, highlighted) {
11025          this.selectAll('g.pie-slice').each(function (d) {
11026              if (legendable.name === d.data.key) {
11027                  d3Selection.select(this).classed('highlight', highlighted);
11028              }
11029          });
11030      }
11031
11032      _tweenPie (b, element) {
11033          b.innerRadius = this._innerRadius;
11034          let current = element._current;
11035          if (this._isOffCanvas(current)) {
11036              current = {startAngle: 0, endAngle: 0};
11037          } else {
11038              // only interpolate startAngle & endAngle, not the whole data object
11039              current = {startAngle: current.startAngle, endAngle: current.endAngle};
11040          }
11041          const i = d3Interpolate.interpolate(current, b);
11042          element._current = i(0);
11043          return t => this._safeArc(i(t), 0, this._buildArcs());
11044      }
11045
11046
11047  }
11048
11049  const pieChart = (parent, chartGroup) => new PieChart(parent, chartGroup);
11050
11051  /**
11052   * Concrete row chart implementation.
11053   *
11054   * Examples:
11055   * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
11056   * @mixes CapMixin
11057   * @mixes MarginMixin
11058   * @mixes ColorMixin
11059   * @mixes BaseMixin
11060   */
11061  class RowChart extends CapMixin(ColorMixin(MarginMixin)) {
11062      /**
11063       * Create a Row Chart.
11064       * @example
11065       * // create a row chart under #chart-container1 element using the default global chart group
11066       * var chart1 = new RowChart('#chart-container1');
11067       * // create a row chart under #chart-container2 element using chart group A
11068       * var chart2 = new RowChart('#chart-container2', 'chartGroupA');
11069       * @param {String|node|d3.selection} parent - Any valid
11070       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
11071       * a dom block element such as a div; or a dom element or d3 selection.
11072       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
11073       * Interaction with a chart will only trigger events and redraws within the chart's group.
11074       */
11075      constructor (parent, chartGroup) {
11076          super();
11077
11078          this._g = undefined;
11079
11080          this._labelOffsetX = 10;
11081          this._labelOffsetY = 15;
11082          this._hasLabelOffsetY = false;
11083          this._dyOffset = '0.35em'; // this helps center labels https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#svg_text
11084          this._titleLabelOffsetX = 2;
11085
11086          this._gap = 5;
11087
11088          this._fixedBarHeight = false;
11089          this._rowCssClass = 'row';
11090          this._titleRowCssClass = 'titlerow';
11091          this._renderTitleLabel = false;
11092
11093          this._x = undefined;
11094
11095          this._elasticX = undefined;
11096
11097          this._xAxis = d3Axis.axisBottom();
11098
11099          this._rowData = undefined;
11100
11101          this.rowsCap = this.cap;
11102
11103          this.title(d => `${this.cappedKeyAccessor(d)}: ${this.cappedValueAccessor(d)}`);
11104
11105          this.label(d => this.cappedKeyAccessor(d));
11106
11107          this.anchor(parent, chartGroup);
11108      }
11109
11110      _calculateAxisScale () {
11111          if (!this._x || this._elasticX) {
11112              const _extent = d3Array.extent(this._rowData, d => this.cappedValueAccessor(d));
11113              if (_extent[0] > 0) {
11114                  _extent[0] = 0;
11115              }
11116              if (_extent[1] < 0) {
11117                  _extent[1] = 0;
11118              }
11119              this._x = d3Scale.scaleLinear().domain(_extent)
11120                  .range([0, this.effectiveWidth()]);
11121          }
11122          this._xAxis.scale(this._x);
11123      }
11124
11125      _drawAxis () {
11126          let axisG = this._g.select('g.axis');
11127
11128          this._calculateAxisScale();
11129
11130          if (axisG.empty()) {
11131              axisG = this._g.append('g').attr('class', 'axis');
11132          }
11133          axisG.attr('transform', `translate(0, ${this.effectiveHeight()})`);
11134
11135          transition(axisG, this.transitionDuration(), this.transitionDelay())
11136              .call(this._xAxis);
11137      }
11138
11139      _doRender () {
11140          this.resetSvg();
11141
11142          this._g = this.svg()
11143              .append('g')
11144              .attr('transform', `translate(${this.margins().left},${this.margins().top})`);
11145
11146          this._drawChart();
11147
11148          return this;
11149      }
11150
11151      /**
11152       * Gets or sets the x scale. The x scale can be any d3
11153       * {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale}.
11154       * @see {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale}
11155       * @param {d3.scale} [scale]
11156       * @returns {d3.scale|RowChart}
11157       */
11158      x (scale) {
11159          if (!arguments.length) {
11160              return this._x;
11161          }
11162          this._x = scale;
11163          return this;
11164      }
11165
11166      _drawGridLines () {
11167          this._g.selectAll('g.tick')
11168              .select('line.grid-line')
11169              .remove();
11170
11171          this._g.selectAll('g.tick')
11172              .append('line')
11173              .attr('class', 'grid-line')
11174              .attr('x1', 0)
11175              .attr('y1', 0)
11176              .attr('x2', 0)
11177              .attr('y2', () => -this.effectiveHeight());
11178      }
11179
11180      _drawChart () {
11181          this._rowData = this.data();
11182
11183          this._drawAxis();
11184          this._drawGridLines();
11185
11186          let rows = this._g.selectAll(`g.${this._rowCssClass}`)
11187              .data(this._rowData);
11188
11189          this._removeElements(rows);
11190          rows = this._createElements(rows)
11191              .merge(rows);
11192          this._updateElements(rows);
11193      }
11194
11195      _createElements (rows) {
11196          const rowEnter = rows.enter()
11197              .append('g')
11198              .attr('class', (d, i) => `${this._rowCssClass} _${i}`);
11199
11200          rowEnter.append('rect').attr('width', 0);
11201
11202          this._createLabels(rowEnter);
11203
11204          return rowEnter;
11205      }
11206
11207      _removeElements (rows) {
11208          rows.exit().remove();
11209      }
11210
11211      _rootValue () {
11212          const root = this._x(0);
11213          return (root === -Infinity || root !== root) ? this._x(1) : root;
11214      }
11215
11216      _updateElements (rows) {
11217          const n = this._rowData.length;
11218
11219          let height;
11220          if (!this._fixedBarHeight) {
11221              height = (this.effectiveHeight() - (n + 1) * this._gap) / n;
11222          } else {
11223              height = this._fixedBarHeight;
11224          }
11225
11226          // vertically align label in center unless they override the value via property setter
11227          if (!this._hasLabelOffsetY) {
11228              this._labelOffsetY = height / 2;
11229          }
11230
11231          const rect = rows.attr('transform', (d, i) => `translate(0,${(i + 1) * this._gap + i * height})`).select('rect')
11232              .attr('height', height)
11233              .attr('fill', this.getColor)
11234              .on('click', d3compat.eventHandler(d => this._onClick(d)))
11235              .classed('dc-tabbable', this._keyboardAccessible)
11236              .classed('deselected', d => (this.hasFilter()) ? !this._isSelectedRow(d) : false)
11237              .classed('selected', d => (this.hasFilter()) ? this._isSelectedRow(d) : false);
11238
11239          if (this._keyboardAccessible) {
11240              this._makeKeyboardAccessible(d => this._onClick(d));
11241          }
11242
11243          transition(rect, this.transitionDuration(), this.transitionDelay())
11244              .attr('width', d => Math.abs(this._rootValue() - this._x(this.cappedValueAccessor(d))))
11245              .attr('transform', d => this._translateX(d));
11246
11247          this._createTitles(rows);
11248          this._updateLabels(rows);
11249      }
11250
11251      _createTitles (rows) {
11252          if (this.renderTitle()) {
11253              rows.select('title').remove();
11254              rows.append('title').text(this.title());
11255          }
11256      }
11257
11258      _createLabels (rowEnter) {
11259          if (this.renderLabel()) {
11260              rowEnter.append('text')
11261                  .on('click', d3compat.eventHandler(d => this._onClick(d)));
11262          }
11263          if (this.renderTitleLabel()) {
11264              rowEnter.append('text')
11265                  .attr('class', this._titleRowCssClass)
11266                  .on('click', d3compat.eventHandler(d => this._onClick(d)));
11267          }
11268      }
11269
11270      _updateLabels (rows) {
11271          if (this.renderLabel()) {
11272              const lab = rows.select('text')
11273                  .attr('x', this._labelOffsetX)
11274                  .attr('y', this._labelOffsetY)
11275                  .attr('dy', this._dyOffset)
11276                  .on('click', d3compat.eventHandler(d => this._onClick(d)))
11277                  .attr('class', (d, i) => `${this._rowCssClass} _${i}`)
11278                  .text(d =>
11278 this.label()(d));
11279              transition(lab, this.transitionDuration(), this.transitionDelay())
11280                  .attr('transform', d => this._translateX(d));
11281          }
11282          if (this.renderTitleLabel()) {
11283              const titlelab = rows.select(`.${this._titleRowCssClass}`)
11284                  .attr('x', this.effectiveWidth() - this._titleLabelOffsetX)
11285                  .attr('y', this._labelOffsetY)
11286                  .attr('dy', this._dyOffset)
11287                  .attr('text-anchor', 'end')
11288                  .on('click', d3compat.eventHandler(d => this._onClick(d)))
11289                  .attr('class', (d, i) => `${this._titleRowCssClass} _${i}`)
11290                  .text(d => this.title()(d));
11291              transition(titlelab, this.transitionDuration(), this.transitionDelay())
11292                  .attr('transform', d => this._translateX(d));
11293          }
11294      }
11295
11296      /**
11297       * Turn on/off Title label rendering (values) using SVG style of text-anchor 'end'.
11298       * @param {Boolean} [renderTitleLabel=false]
11299       * @returns {Boolean|RowChart}
11300       */
11301      renderTitleLabel (renderTitleLabel) {
11302          if (!arguments.length) {
11303              return this._renderTitleLabel;
11304          }
11305          this._renderTitleLabel = renderTitleLabel;
11306          return this;
11307      }
11308
11309      _onClick (d) {
11310          this.onClick(d);
11311      }
11312
11313      _translateX (d) {
11314          const x = this._x(this.cappedValueAccessor(d)),
11315              x0 = this._rootValue(),
11316              s = x > x0 ? x0 : x;
11317          return `translate(${s},0)`;
11318      }
11319
11320      _doRedraw () {
11321          this._drawChart();
11322          return this;
11323      }
11324
11325      /**
11326       * Get or sets the x axis for the row chart instance.
11327       * See the {@link https://github.com/d3/d3-axis/blob/master/README.md d3.axis}
11328       * documention for more information.
11329       * @param {d3.axis} [xAxis]
11330       * @example
11331       * // customize x axis tick format
11332       * chart.xAxis().tickFormat(function (v) {return v + '%';});
11333       * // customize x axis tick values
11334       * chart.xAxis().tickValues([0, 100, 200, 300]);
11335       * // use a top-oriented axis. Note: position of the axis and grid lines will need to
11336       * // be set manually, see https://dc-js.github.io/dc.js/examples/row-top-axis.html
11337       * chart.xAxis(d3.axisTop())
11338       * @returns {d3.axis|RowChart}
11339       */
11340      xAxis (xAxis) {
11341          if (!arguments.length) {
11342              return this._xAxis;
11343          }
11344          this._xAxis = xAxis;
11345          return this;
11346      }
11347
11348      /**
11349       * Get or set the fixed bar height. Default is [false] which will auto-scale bars.
11350       * For example, if you want to fix the height for a specific number of bars (useful in TopN charts)
11351       * you could fix height as follows (where count = total number of bars in your TopN and gap is
11352       * your vertical gap space).
11353       * @example
11354       * chart.fixedBarHeight( chartheight - (count + 1) * gap / count);
11355       * @param {Boolean|Number} [fixedBarHeight=false]
11356       * @returns {Boolean|Number|RowChart}
11357       */
11358      fixedBarHeight (fixedBarHeight) {
11359          if (!arguments.length) {
11360              return this._fixedBarHeight;
11361          }
11362          this._fixedBarHeight = fixedBarHeight;
11363          return this;
11364      }
11365
11366      /**
11367       * Get or set the vertical gap space between rows on a particular row chart instance.
11368       * @param {Number} [gap=5]
11369       * @returns {Number|RowChart}
11370       */
11371      gap (gap) {
11372          if (!arguments.length) {
11373              return this._gap;
11374          }
11375          this._gap = gap;
11376          return this;
11377      }
11378
11379      /**
11380       * Get or set the elasticity on x axis. If this attribute is set to true, then the x axis will rescale to auto-fit the
11381       * data range when filtered.
11382       * @param {Boolean} [elasticX]
11383       * @returns {Boolean|RowChart}
11384       */
11385      elasticX (elasticX) {
11386          if (!arguments.length) {
11387              return this._elasticX;
11388          }
11389          this._elasticX = elasticX;
11390          return this;
11391      }
11392
11393      /**
11394       * Get or set the x offset (horizontal space to the top left corner of a row) for labels on a particular row chart.
11395       * @param {Number} [labelOffsetX=10]
11396       * @returns {Number|RowChart}
11397       */
11398      labelOffsetX (labelOffsetX) {
11399          if (!arguments.length) {
11400              return this._labelOffsetX;
11401          }
11402          this._labelOffsetX = labelOffsetX;
11403          return this;
11404      }
11405
11406      /**
11407       * Get or set the y offset (vertical space to the top left corner of a row) for labels on a particular row chart.
11408       * @param {Number} [labelOffsety=15]
11409       * @returns {Number|RowChart}
11410       */
11411      labelOffsetY (labelOffsety) {
11412          if (!arguments.length) {
11413              return this._labelOffsetY;
11414          }
11415          this._labelOffsetY = labelOffsety;
11416          this._hasLabelOffsetY = true;
11417          return this;
11418      }
11419
11420      /**
11421       * Get of set the x offset (horizontal space between right edge of row and right edge or text.
11422       * @param {Number} [titleLabelOffsetX=2]
11423       * @returns {Number|RowChart}
11424       */
11425      titleLabelOffsetX (titleLabelOffsetX) {
11426          if (!arguments.length) {
11427              return this._titleLabelOffsetX;
11428          }
11429          this._titleLabelOffsetX = titleLabelOffsetX;
11430          return this;
11431      }
11432
11433      _isSelectedRow (d) {
11434          return this.hasFilter(this.cappedKeyAccessor(d));
11435      }
11436  }
11437
11438  const rowChart = (parent, chartGroup) => new RowChart(parent, chartGroup);
11439
11440  /**
11441   * A scatter plot chart
11442   *
11443   * Examples:
11444   * - {@link http://dc-js.github.io/dc.js/examples/scatter.html Scatter Chart}
11445   * - {@link http://dc-js.github.io/dc.js/examples/multi-scatter.html Multi-Scatter Chart}
11446   * @mixes CoordinateGridMixin
11447   */
11448  class ScatterPlot extends CoordinateGridMixin {
11449      /**
11450       * Create a Scatter Plot.
11451       * @example
11452       * // create a scatter plot under #chart-container1 element using the default global chart group
11453       * var chart1 = new ScatterPlot('#chart-container1');
11454       * // create a scatter plot under #chart-container2 element using chart group A
11455       * var chart2 = new ScatterPlot('#chart-container2', 'chartGroupA');
11456       * // create a sub-chart under a composite parent chart
11457       * var chart3 = new ScatterPlot(compositeChart);
11458       * @param {String|node|d3.selection} parent - Any valid
11459       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
11460       * a dom block element such as a div; or a dom element or d3 selection.
11461       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
11462       * Interaction with a chart will only trigger events and redraws within the chart's group.
11463       */
11464      constructor (parent, chartGroup) {
11465          super();
11466
11467          this._symbol = d3Shape.symbol();
11468
11469          this._existenceAccessor = d => d.value;
11470
11471          const originalKeyAccessor = this.keyAccessor();
11472          this.keyAccessor(d => originalKeyAccessor(d)[0]);
11473          this.valueAccessor(d => originalKeyAccessor(d)[1]);
11474          this.colorAccessor(() => this._groupName);
11475
11476          // this basically just counteracts the setting of its own key/value accessors
11477          // see https://github.com/dc-js/dc.js/issues/702
11478          this.title(d => `${this.keyAccessor()(d)},${this.valueAccessor()(d)}: ${this.existenceAccessor()(d)}`);
11479
11480          this._highlightedSize = 7;
11481          this._symbolSize = 5;
11482          this._excludedSize = 3;
11483          this._excludedColor = null;
11484          this._excludedOpacity = 1.0;
11485          this._emptySize = 0;
11486          this._emptyOpacity = 0;
11487          this._nonemptyOpacity = 1;
11488          this._emptyColor = null;
11489          this._filtered = [];
11490          this._canvas = null;
11491          this._context = null;
11492          this._useCanvas = false;
11493
11494
11495          // Use a 2 dimensional brush
11496          this.brush(d3Brush.brush());
11497
11498          this._symbol.size((d, i) => this._elementSize(d, i));
11499
11500          this.anchor(parent, chartGroup);
11501      }
11502
11503      // Calculates element radius for canvas plot to be comparable to D3 area based symbol sizes
11504      _canvasElementSize (d, isFiltered) {
11505          if (!this._existenceAccessor(d)) {
11506              return this._emptySize / Math.sqrt(Math.PI);
11507          } else if (isFiltered) {
11508              return this._symbolSize / Math.sqrt(Math.PI);
11509          } else {
11510              return this._excludedSize / Math.sqrt(Math.PI);
11511          }
11512      }
11513
11514      _elementSize (d, i) {
11515          if (!this._existenceAccessor(d)) {
11516              return Math.pow(this._emptySize, 2);
11517          } else if (this._filtered[i]) {
11518              return Math.pow(this._symbolSize, 2);
11519          } else {
11520              return Math.pow(this._excludedSize, 2);
11521          }
11522      }
11523
11524      _locator (d) {
11525          return `translate(${this.x()(this.keyAccessor()(d))},${ 
11526            this.y()(this.valueAccessor()(d))})`;
11527      }
11528
11529      filter (filter) {
11530          if (!arguments.length) {
11531              return super.filter();
11532          }
11533
11534          return super.filter(filters.RangedTwoDimensionalFilter(filter));
11535      }
11536
11537      /**
11538       * Method that replaces original resetSvg and appropriately inserts canvas
11539       * element along with svg element and sets their CSS properties appropriately
11540       * so they are overlapped on top of each other.
11541       * Remove the chart's SVGElements from the dom and recreate the container SVGElement.
11542       * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SVGElement SVGElement}
11543       * @returns {SVGElement}
11544       */
11545      resetSvg () {
11546          if (!this._useCanvas) {
11547              return super.resetSvg();
11548          } else {
11549              super.resetSvg(); // Perform original svgReset inherited from baseMixin
11550              this.select('canvas').remove(); // remove old canvas
11551
11552              const svgSel = this.svg();
11553              const rootSel = this.root();
11554
11555              // Set root node to relative positioning and svg to absolute
11556              rootSel.style('position', 'relative');
11557              svgSel.style('position', 'relative');
11558
11559              // Check if SVG element already has any extra top/left CSS offsets
11560              const svgLeft = isNaN(parseInt(svgSel.style('left'), 10)) ? 0 : parseInt(svgSel.style('left'), 10);
11561              const svgTop = isNaN(parseInt(svgSel.style('top'), 10)) ? 0 : parseInt(svgSel.style('top'), 10);
11562              const width = this.effectiveWidth();
11563              const height = this.effectiveHeight();
11564              const margins = this.margins(); // {top: 10, right: 130, bottom: 42, left: 42}
11565
11566              // Add the canvas element such that it perfectly overlaps the plot area of the scatter plot SVG
11567              const devicePixelRatio = window.devicePixelRatio || 1;
11568              this._canvas = this.root().append('canvas')
11569                  .attr('x', 0)
11570                  .attr('y', 0)
11571                  .attr('width', (width) * devicePixelRatio)
11572                  .attr('height', (height) * devicePixelRatio)
11573                  .style('width', `${width}px`)
11574                  .style('height', `${height}px`)
11575                  .style('position', 'absolute')
11576                  .style('top', `${margins.top + svgTop}px`)
11577                  .style('left', `${margins.left + svgLeft}px`)
11578                  .style('z-index', -1) // Place behind SVG
11579                  .style('pointer-events', 'none'); // Disable pointer events on canvas so SVG can capture brushing
11580
11581              // Define canvas context and set clipping path
11582              this._context = this._canvas.node().getContext('2d');
11583              this._context.scale(devicePixelRatio, devicePixelRatio);
11584              this._context.rect(0, 0, width, height);
11585              this._context.clip(); // Setup clipping path
11586              this._context.imageSmoothingQuality = 'high';
11587
11588              return this.svg(); // Respect original return param for this.resetSvg;
11589          }
11590      }
11591
11592      _resizeCanvas () {
11593          const width = this.effectiveWidth();
11594          const height = this.effectiveHeight();
11595
11596          const devicePixelRatio = window.devicePixelRatio || 1;
11597          this._canvas
11598              .attr('width', (width) * devicePixelRatio)
11599              .attr('height', (height) * devicePixelRatio)
11600              .style('width', `${width}px`)
11601              .style('height', `${height}px`);
11602          this._context.scale(devicePixelRatio, devicePixelRatio);
11603      }
11604
11605
11606      /**
11607       * Set or get whether to use canvas backend for plotting scatterPlot. Note that the
11608       * canvas backend does not currently support
11609       * {@link ScatterPlot#customSymbol customSymbol} or
11610       * {@link ScatterPlot#symbol symbol} methods and is limited to always plotting
11611       * with filled circles. Symbols are drawn with
11612       * {@link ScatterPlot#symbolSize symbolSize} radius. By default, the SVG backend
11613       * is used when `useCanvas` is set to `false`.
11614       * @param {Boolean} [useCanvas=false]
11615       * @return {Boolean|d3.selection}
11616       */
11617      useCanvas (useCanvas) {
11618          if (!arguments.length) {
11619              return this._useCanvas;
11620          }
11621          this._useCanvas = useCanvas;
11622          return this;
11623      }
11624
11625      /**
11626       * Set or get canvas element. You should usually only ever use the get method as
11627       * dc.js will handle canvas element generation.  Provides valid canvas only when
11628       * {@link ScatterPlot#useCanvas useCanvas} is set to `true`
11629       * @param {CanvasElement|d3.selection} [canvasElement]
11630       * @return {CanvasElement|d3.selection}
11631       */
11632      canvas (canvasElement) {
11633          if (!arguments.length) {
11634              return this._canvas;
11635          }
11636          this._canvas = canvasElement;
11637          return this;
11638      }
11639
11640      /**
11641       * Get canvas 2D context. Provides valid context only when
11642       * {@link ScatterPlot#useCanvas useCanvas} is set to `true`
11643       * @return {CanvasContext}
11644       */
11645      context () {
11646          return this._context;
11647      }
11648
11649      /*eslint complexity: [2,11] */
11650      // Plots data on canvas element. If argument provided, assumes legend is
11651      // currently being highlighted and modifies opacity/size of symbols accordingly
11652      // @param {Object} [legendHighlightDatum] - Datum provided to legendHighlight method
11653      _plotOnCanvas (legendHighlightDatum) {
11654          this._resizeCanvas();
11655          const context = this.context();
11656          context.clearRect(0, 0, (context.canvas.width + 2) * 1, (context.canvas.height + 2) * 1);
11657          const data = this.data();
11658
11659          // Draw the data on canvas
11660          data.forEach((d, i) => {
11661              const isFiltered = !this.filter() || this.filter().isFiltered([d.key[0], d.key[1]]);
11662              // Calculate opacity for current data point
11663              let cOpacity = 1;
11664              if (!this._existenceAccessor(d)) {
11665                  cOpacity = this._emptyOpacity;
11666              } else if (isFiltered) {
11667                  cOpacity = this._nonemptyOpacity;
11668              } else {
11669                  cOpacity = this.excludedOpacity();
11670              }
11671              // Calculate color for current data point
11672              let cColor = null;
11673              if (this._emptyColor && !this._existenceAccessor(d)) {
11674                  cColor = this._emptyColor;
11675              } else if (this.excludedColor() && !isFiltered) {
11676                  cColor = this.excludedColor();
11677              } else {
11678                  cColor = this.getColor(d);
11679              }
11680              let cSize = this._canvasElementSize(d, isFiltered);
11681
11682              // Adjust params for data points if legend is highlighted
11683              if (legendHighlightDatum) {
11684                  const isHighlighted = (cColor === legendHighlightDatum.color);
11685                  // Calculate opacity for current data point
11686                  const fadeOutOpacity = 0.1; // TODO: Make this programmatically setable
11687                  if (!isHighlighted) { // Fade out non-highlighted colors + highlighted colors outside filter
11688                      cOpacity = fadeOutOpacity;
11689                  }
11690                  if (isHighlighted) { // Set size for highlighted color data points
11691                      cSize = this._highlightedSize / Math.sqrt(Math.PI);
11692                  }
11693              }
11694
11695              // Draw point on canvas
11696              context.save();
11697              context.globalAlpha = cOpacity;
11698              context.beginPath();
11699              context.arc(this.x()(this.keyAccessor()(d)), this.y()(this.valueAccessor()(d)), cSize, 0, 2 * Math.PI, true);
11700              context.fillStyle = cColor;
11701              context.fill();
11702              // context.lineWidth = 0.5; // Commented out code to add stroke around scatter points if desired
11703              // context.strokeStyle = '#333';
11704              // context.stroke();
11705              context.restore();
11706          });
11707      }
11708
11709      _plotOnSVG () {
11710
11711          const data = this.data();
11712
11713          if (this._keyboardAccessible) {
11714              // sort based on the x value (key)
11715              data.sort((a, b) => d3Array.ascending(this.keyAccessor()(a), this.keyAccessor()(b)));
11716          }
11717
11718          let symbols = this.chartBodyG().selectAll('path.symbol')
11719              .data(data);
11720
11721          transition(symbols.exit(), this.transitionDuration(), this.transitionDelay())
11722              .attr('opacity', 0).remove();
11723
11724          symbols = symbols
11725              .enter()
11726              .append('path')
11727              .attr('class', 'symbol')
11728              .classed('dc-tabbable', this._keyboardAccessible)
11729              .attr('opacity', 0)
11730              .attr('fill', this.getColor)
11731              .attr('transform', d => this._locator(d))
11732              .merge(symbols);
11733
11734          // no click handler - just tabindex for reading out of tooltips
11735          if (this._keyboardAccessible) {
11736              this._makeKeyboardAccessible();
11737              symbols.order();
11738          }
11739
11740          symbols.call(s => this._renderTitles(s, data));
11741
11742          symbols.each((d, i) => {
11743              this._filtered[i] = !this.filter() || this.filter().isFiltered([this.keyAccessor()(d), this.valueAccessor()(d)]);
11744          });
11745
11746          transition(symbols, this.transitionDuration(), this.transitionDelay())
11747              .attr('opacity', (d, i) => {
11748                  if (!this._existenceAccessor(d)) {
11749                      return this._emptyOpacity;
11750                  } else if (this._filtered[i]) {
11751                      return this._nonemptyOpacity;
11752                  } else {
11753                      return this.excludedOpacity();
11754                  }
11755              })
11756              .attr('fill', (d, i) => {
11757                  if (this._emptyColor && !this._existenceAccessor(d)) {
11758                      return this._emptyColor;
11759                  } else if (this.excludedColor() && !this._filtered[i]) {
11760                      return this.excludedColor();
11761                  } else {
11762                      return this.getColor(d);
11763                  }
11764              })
11765              .attr('transform', d => this._locator(d))
11766              .attr('d', this._symbol);
11767      }
11768
11769      plotData () {
11770          if (this._useCanvas) {
11771              this._plotOnCanvas();
11772          } else {
11773              this._plotOnSVG();
11774          }
11775      }
11776
11777      _renderTitles (_symbol, _d) {
11778          if (this.renderTitle()) {
11779              _symbol.selectAll('title').remove();
11780              _symbol.append('title').text(d => this.title()(d));
11781          }
11782      }
11783
11784      /**
11785       * Get or set the existence accessor.  If a point exists, it is drawn with
11786       * {@link ScatterPlot#symbolSize symbolSize} radius and
11787       * opacity 1; if it does not exist, it is drawn with
11788       * {@link ScatterPlot#emptySize emptySize} radius and opacity 0. By default,
11789       * the existence accessor checks if the reduced value is truthy.
11790       * @see {@link ScatterPlot#symbolSize symbolSize}
11791       * @see {@link ScatterPlot#emptySize emptySize}
11792       * @example
11793       * // default accessor
11794       * chart.existenceAccessor(function (d) { return d.value; });
11795       * @param {Function} [accessor]
11796       * @returns {Function|ScatterPlot}
11797       */
11798      existenceAccessor (accessor) {
11799          if (!arguments.length) {
11800              return this._existenceAccessor;
11801          }
11802          this._existenceAccessor = accessor;
11803          return this;
11804      }
11805
11806      /**
11807       * Get or set the symbol type used for each point. By default the symbol is a circle (d3.symbolCircle).
11808       * Type can be a constant or an accessor.
11809       * @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_type symbol.type}
11810       * @example
11811       * // Circle type
11812       * chart.symbol(d3.symbolCircle);
11813       * // Square type
11814       * chart.symbol(d3.symbolSquare);
11815       * @param {Function} [type=d3.symbolCircle]
11816       * @returns {Function|ScatterPlot}
11817       */
11818      symbol (type) {
11819          if (!arguments.length) {
11820              return this._symbol.type();
11821          }
11822          this._symbol.type(type);
11823          return this;
11824      }
11825
11826      /**
11827       * Get or set the symbol generator. By default `ScatterPlot` will use
11828       * {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol d3.symbol()}
11829       * to generate symbols. `ScatterPlot` will set the
11830       * {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_size symbol size accessor}
11831       * on the symbol generator.
11832       * @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol d3.symbol}
11833       * @see {@link https://stackoverflow.com/questions/25332120/create-additional-d3-js-symbols Create additional D3.js symbols}
11834       * @param {String|Function} [customSymbol=d3.symbol()]
11835       * @returns {String|Function|ScatterPlot}
11836       */
11837      customSymbol (customSymbol) {
11838          if (!arguments.length) {
11839              return this._symbol;
11840          }
11841          this._symbol = customSymbol;
11842          this._symbol.size((d, i) => this._elementSize(d, i));
11843          return this;
11844      }
11845
11846      /**
11847       * Set or get radius for symbols.
11848       * @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_size d3.symbol.size}
11849       * @param {Number} [symbolSize=3]
11850       * @returns {Number|ScatterPlot}
11851       */
11852      symbolSize (symbolSize) {
11853          if (!arguments.length) {
11854              return this._symbolSize;
11855          }
11856          this._symbolSize = symbolSize;
11857          return this;
11858      }
11859
11860      /**
11861       * Set or get radius for highlighted symbols.
11862       * @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_size d3.symbol.size}
11863       * @param {Number} [highlightedSize=5]
11864       * @returns {Number|ScatterPlot}
11865       */
11866      highlightedSize (highlightedSize) {
11867          if (!arguments.length) {
11868              return this._highlightedSize;
11869          }
11870          this._highlightedSize = highlightedSize;
11871          return this;
11872      }
11873
11874      /**
11875       * Set or get size for symbols excluded from this chart's filter. If null, no
11876       * special size is applied for symbols based on their filter status.
11877       * @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_size d3.symbol.size}
11878       * @param {Number} [excludedSize=null]
11879       * @returns {Number|ScatterPlot}
11880       */
11881      excludedSize (excludedSize) {
11882          if (!arguments.length) {
11883              return this._excludedSize;
11884          }
11885          this._excludedSize = excludedSize;
11886          return this;
11887      }
11888
11889      /**
11890       * Set or get color for symbols excluded from this chart's filter. If null, no
11891       * special color is applied for symbols based on their filter status.
11892       * @param {Number} [excludedColor=null]
11893       * @returns {Number|ScatterPlot}
11894       */
11895      excludedColor (excludedColor) {
11896          if (!arguments.length) {
11897              return this._excludedColor;
11898          }
11899          this._excludedColor = excludedColor;
11900          return this;
11901      }
11902
11903      /**
11904       * Set or get opacity for symbols excluded from this chart's filter.
11905       * @param {Number} [excludedOpacity=1.0]
11906       * @returns {Number|ScatterPlot}
11907       */
11908      excludedOpacity (excludedOpacity) {
11909          if (!arguments.length) {
11910              return this._excludedOpacity;
11911          }
11912          this._excludedOpacity = excludedOpacity;
11913          return this;
11914      }
11915
11916      /**
11917       * Set or get radius for symbols when the group is empty.
11918       * @see {@link https://github.com/d3/d3-shape/blob/master/README.md#symbol_size d3.symbol.size}
11919       * @param {Number} [emptySize=0]
11920       * @returns {Number|ScatterPlot}
11921       */
11922      emptySize (emptySize) {
11923          if (!arguments.length) {
11924              return this._emptySize;
11925          }
11926          this._emptySize = emptySize;
11927          return this;
11928      }
11929
11930      hiddenSize (emptySize) {
11931          if (!arguments.length) {
11932              return this.emptySize();
11933          }
11934          return this.emptySize(emptySize);
11935      }
11936
11937      /**
11938       * Set or get color for symbols when the group is empty. If null, just use the
11939       * {@link ColorMixin#colors colorMixin.colors} color scale zero value.
11940       * @param {String} [emptyColor=null]
11941       * @return {String}
11942       * @return {ScatterPlot}/
11943       */
11944      emptyColor (emptyColor) {
11945          if (!arguments.length) {
11946              return this._emptyColor;
11947          }
11948          this._emptyColor = emptyColor;
11949          return this;
11950      }
11951
11952      /**
11953       * Set or get opacity for symbols when the group is empty.
11954       * @param {Number} [emptyOpacity=0]
11955       * @return {Number}
11956       * @return {ScatterPlot}
11957       */
11958      emptyOpacity (emptyOpacity) {
11959          if (!arguments.length) {
11960              return this._emptyOpacity;
11961          }
11962          this._emptyOpacity = emptyOpacity;
11963          return this;
11964      }
11965
11966      /**
11967       * Set or get opacity for symbols when the group is not empty.
11968       * @param {Number} [nonemptyOpacity=1]
11969       * @return {Number}
11970       * @return {ScatterPlot}
11971       */
11972      nonemptyOpacity (nonemptyOpacity) {
11973          if (!arguments.length) {
11974              return this._emptyOpacity;
11975          }
11976          this._nonemptyOpacity = nonemptyOpacity;
11977          return this;
11978      }
11979
11980      legendables () {
11981          return [{chart: this, name: this._groupName, color: this.getColor()}];
11982      }
11983
11984      legendHighlight (d) {
11985          if (this._useCanvas) {
11986              this._plotOnCanvas(d); // Supply legend datum to plotOnCanvas
11987          } else {
11988              this._resizeSymbolsWhere(s => s.attr('fill') === d.color, this._highlightedSize);
11989              this.chartBodyG().selectAll('.chart-body path.symbol').filter(function () {
11990                  return d3Selection.select(this).attr('fill') !== d.color;
11991              }).classed('fadeout', true);
11992          }
11993      }
11994
11995      legendReset (d) {
11996          if (this._useCanvas) {
11997              this._plotOnCanvas(d); // Supply legend datum to plotOnCanvas
11998          } else {
11999              this._resizeSymbolsWhere(s => s.attr('fill') === d.color, this._symbolSize);
12000              this.chartBodyG().selectAll('.chart-body path.symbol').filter(function () {
12001                  return d3Selection.select(this).attr('fill') !== d.color;
12002              }).classed('fadeout', false);
12003          }
12004      }
12005
12006      _resizeSymbolsWhere (condition, size) {
12007          const symbols = this.chartBodyG().selectAll('.chart-body path.symbol').filter(function () {
12008              return condition(d3Selection.select(this));
12009          });
12010          const oldSize = this._symbol.size();
12011          this._symbol.size(Math.pow(size, 2));
12012          transition(symbols, this.transitionDuration(), this.transitionDelay()).attr('d', this._symbol);
12013          this._symbol.size(oldSize);
12014      }
12015      createBrushHandlePaths () {
12016          // no handle paths for poly-brushes
12017      }
12018
12019      extendBrush (brushSelection) {
12020          if (this.round()) {
12021              brushSelection[0] = brushSelection[0].map(this.round());
12022              brushSelection[1] = brushSelection[1].map(this.round());
12023          }
12024          return brushSelection;
12025      }
12026
12027      brushIsEmpty (brushSelection) {
12028          return !brushSelection || brushSelection[0][0] >= brushSelection[1][0] || brushSelection[0][1] >= brushSelection[1][1];
12029      }
12030
12031      _brushing (evt) {
12032          if (this._ignoreBrushEvents) {
12033              return;
12034          }
12035
12036          let brushSelection = evt.selection;
12037
12038          // Testing with pixels is more reliable
12039          let brushIsEmpty = this.brushIsEmpty(brushSelection);
12040
12041          if (brushSelection) {
12042              brushSelection = brushSelection.map(point => point.map((coord, i) => {
12043                  const scale = i === 0 ? this.x() : this.y();
12044                  return scale.invert(coord);
12045              }));
12046
12047              brushSelection = this.extendBrush(brushSelection);
12048
12049              // The rounding process might have made brushSelection empty, so we need to recheck
12050              brushIsEmpty = brushIsEmpty && this.brushIsEmpty(brushSelection);
12051          }
12052
12053          this.redrawBrush(brushSelection, false);
12054
12055          const ranged2DFilter = brushIsEmpty ? null : filters.RangedTwoDimensionalFilter(brushSelection);
12056
12057          events.trigger(() => {
12058              this.replaceFilter(ranged2DFilter);
12059              this.redrawGroup();
12060          }, constants.EVENT_DELAY);
12061      }
12062
12063      redrawBrush (brushSelection, doTransition) {
12064          // override default x axis brush from parent chart
12065          this._gBrush = this.gBrush();
12066
12067          if (this.brushOn() && this._gBrush) {
12068              if (this.resizing()) {
12069                  this.setBrushExtents(doTransition);
12070              }
12071
12072              if (!brushSelection) {
12073                  this._withoutBrushEvents(() => {
12074                      this._gBrush
12075                          .call(this.brush().move, brushSelection);
12076                  });
12077              } else {
12078                  brushSelection = brushSelection.map(point => point.map((coord, i) => {
12079                      const scale = i === 0 ? this.x() : this.y();
12080                      return scale(coord);
12081                  }));
12082
12083                  const gBrush =
12084                      optionalTransition(doTransition, this.transitionDuration(), this.transitionDelay())(this._gBrush);
12085
12086                  this._withoutBrushEvents(() => {
12087                      gBrush
12088                          .call(this.brush().move, brushSelection);
12089                  });
12090              }
12091          }
12092
12093          this.fadeDeselectedArea(brushSelection);
12094      }
12095  }
12096
12097  const scatterPlot = (parent, chartGroup) => new ScatterPlot(parent, chartGroup);
12098
12099  const SELECT_CSS_CLASS = 'dc-select-menu';
12100  const OPTION_CSS_CLASS = 'dc-select-option';
12101
12102  /**
12103   * The select menu is a simple widget designed to filter a dimension by selecting an option from
12104   * an HTML `<select/>` menu. The menu can be optionally turned into a multiselect.
12105   * @mixes BaseMixin
12106   */
12107  class SelectMenu extends BaseMixin {
12108      /**
12109       * Create a Select Menu.
12110       * @example
12111       * // create a select menu under #select-container using the default global chart group
12112       * var select = new SelectMenu('#select-container')
12113       *                .dimension(states)
12114       *                .group(stateGroup);
12115       * // the option text can be set via the title() function
12116       * // by default the option text is '`key`: `value`'
12117       * select.title(function (d){
12118       *     return 'STATE: ' + d.key;
12119       * })
12120       * @param {String|node|d3.selection|CompositeChart} parent - Any valid
12121       * [d3 single selector](https://github.com/mbostock/d3/wiki/Selections#selecting-elements) specifying
12122       * a dom block element such as a div; or a dom element or d3 selection.
12123       * @param {String} [chartGroup] - The name of the chart group this widget should be placed in.
12124       * Interaction with the widget will only trigger events and redraws within its group.
12125       */
12126      constructor (parent, chartGroup) {
12127          super();
12128
12129          this._select = undefined;
12130          this._promptText = 'Select all';
12131          this._multiple = false;
12132          this._promptValue = null;
12133          this._numberVisible = null;
12134
12135          this.data(group => group.all().filter(this._filterDisplayed));
12136
12137          this._filterDisplayed = d => this.valueAccessor()(d) > 0;
12138
12139          this._order = (a, b) => {
12140              if (this.keyAccessor()(a) > this.keyAccessor()(b)) {
12141                  return 1;
12142              }
12143              if (this.keyAccessor()(a) < this.keyAccessor()(b)) {
12144                  return -1;
12145              }
12146              return 0;
12147          };
12148
12149          this.anchor(parent, chartGroup);
12150      }
12151
12152      _doRender () {
12153          this.select('select').remove();
12154          this._select = this.root().append('select')
12155              .classed(SELECT_CSS_CLASS, true);
12156          this._select.append('option').text(this._promptText).attr('value', '');
12157
12158          this._doRedraw();
12159          return this;
12160      }
12161
12162      _doRedraw () {
12163          this._setAttributes();
12164          this._renderOptions();
12165          // select the option(s) corresponding to current filter(s)
12166          if (this.hasFilter() && this._multiple) {
12167              this._select.selectAll('option')
12168                  .property('selected', d => typeof d !== 'undefined' && this.filters().indexOf(String(this.keyAccessor()(d))) >= 0);
12169          } else if (this.hasFilter()) {
12170              this._select.property('value', this.filter());
12171          } else {
12172              this._select.property('value', '');
12173          }
12174          return this;
12175      }
12176
12177      _renderOptions () {
12178          const options = this._select.selectAll(`option.${OPTION_CSS_CLASS}`)
12179              .data(this.data(), d =>
12179 this.keyAccessor()(d));
12180
12181          options.exit().remove();
12182
12183          options.enter()
12184              .append('option')
12185              .classed(OPTION_CSS_CLASS, true)
12186              .attr('value', d => this.keyAccessor()(d))
12187              .merge(options)
12188              .text(this.title());
12189
12190          this._select.selectAll(`option.${OPTION_CSS_CLASS}`).sort(this._order);
12191
12192          this._select.on('change', d3compat.eventHandler((d, evt) => this._onChange(d, evt)));
12193      }
12194
12195      _onChange (_d, evt) {
12196          let values;
12197
12198          const target = evt.target;
12199
12200          if (target.selectedOptions) {
12201              const selectedOptions = Array.prototype.slice.call(target.selectedOptions);
12202              values = selectedOptions.map(d => d.value);
12203          } else { // IE and other browsers do not support selectedOptions
12204              // adapted from this polyfill: https://gist.github.com/brettz9/4212217
12205              const options = [].slice.call(evt.target.options);
12206              values = options.filter(option => option.selected).map(option => option.value);
12207          }
12208          // console.log(values);
12209          // check if only prompt option is selected
12210          if (values.length === 1 && values[0] === '') {
12211              values = this._promptValue || null;
12212          } else if (!this._multiple && values.length === 1) {
12213              values = values[0];
12214          }
12215          this.onChange(values);
12216      }
12217
12218      onChange (val) {
12219          if (val && this._multiple) {
12220              this.replaceFilter([val]);
12221          } else if (val) {
12222              this.replaceFilter(val);
12223          } else {
12224              this.filterAll();
12225          }
12226          events.trigger(() => {
12227              this.redrawGroup();
12228          });
12229      }
12230
12231      _setAttributes () {
12232          if (this._multiple) {
12233              this._select.attr('multiple', true);
12234          } else {
12235              this._select.attr('multiple', null);
12236          }
12237          if (this._numberVisible !== null) {
12238              this._select.attr('size', this._numberVisible);
12239          } else {
12240              this._select.attr('size', null);
12241          }
12242      }
12243
12244      /**
12245       * Get or set the function that controls the ordering of option tags in the
12246       * select menu. By default options are ordered by the group key in ascending
12247       * order.
12248       * @param {Function} [order]
12249       * @returns {Function|SelectMenu}
12250       * @example
12251       * // order by the group's value
12252       * chart.order(function (a,b) {
12253       *     return a.value > b.value ? 1 : b.value > a.value ? -1 : 0;
12254       * });
12255       */
12256      order (order) {
12257          if (!arguments.length) {
12258              return this._order;
12259          }
12260          this._order = order;
12261          return this;
12262      }
12263
12264      /**
12265       * Get or set the text displayed in the options used to prompt selection.
12266       * @param {String} [promptText='Select all']
12267       * @returns {String|SelectMenu}
12268       * @example
12269       * chart.promptText('All states');
12270       */
12271      promptText (promptText) {
12272          if (!arguments.length) {
12273              return this._promptText;
12274          }
12275          this._promptText = promptText;
12276          return this;
12277      }
12278
12279      /**
12280       * Get or set the function that filters option tags prior to display. By default options
12281       * with a value of < 1 are not displayed.
12282       * @param {function} [filterDisplayed]
12283       * @returns {Function|SelectMenu}
12284       * @example
12285       * // display all options override the `filterDisplayed` function:
12286       * chart.filterDisplayed(function () {
12287       *     return true;
12288       * });
12289       */
12290      filterDisplayed (filterDisplayed) {
12291          if (!arguments.length) {
12292              return this._filterDisplayed;
12293          }
12294          this._filterDisplayed = filterDisplayed;
12295          return this;
12296      }
12297
12298      /**
12299       * Controls the type of select menu. Setting it to true converts the underlying
12300       * HTML tag into a multiple select.
12301       * @param {boolean} [multiple=false]
12302       * @returns {boolean|SelectMenu}
12303       * @example
12304       * chart.multiple(true);
12305       */
12306      multiple (multiple) {
12307          if (!arguments.length) {
12308              return this._multiple;
12309          }
12310          this._multiple = multiple;
12311
12312          return this;
12313      }
12314
12315      /**
12316       * Controls the default value to be used for
12317       * [dimension.filter](https://github.com/crossfilter/crossfilter/wiki/API-Reference#dimension_filter)
12318       * when only the prompt value is selected. If `null` (the default), no filtering will occur when
12319       * just the prompt is selected.
12320       * @param {?*} [promptValue=null]
12321       * @returns {*|SelectMenu}
12322       */
12323      promptValue (promptValue) {
12324          if (!arguments.length) {
12325              return this._promptValue;
12326          }
12327          this._promptValue = promptValue;
12328
12329          return this;
12330      }
12331
12332      /**
12333       * Controls the number of items to show in the select menu, when `.multiple()` is true. This
12334       * controls the [`size` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#Attributes) of
12335       * the `select` element. If `null` (the default), uses the browser's default height.
12336       * @param {?number} [numberVisible=null]
12337       * @returns {number|SelectMenu}
12338       * @example
12339       * chart.numberVisible(10);
12340       */
12341      numberVisible (numberVisible) {
12342          if (!arguments.length) {
12343              return this._numberVisible;
12344          }
12345          this._numberVisible = numberVisible;
12346
12347          return this;
12348      }
12349
12350      size (numberVisible) {
12351          logger.warnOnce('selectMenu.size is ambiguous - use selectMenu.numberVisible instead');
12352          if (!arguments.length) {
12353              return this.numberVisible();
12354          }
12355          return this.numberVisible(numberVisible);
12356      }
12357  }
12358
12359  const selectMenu = (parent, chartGroup) => new SelectMenu(parent, chartGroup);
12360
12361  /**
12362   * A series chart is a chart that shows multiple series of data overlaid on one chart, where the
12363   * series is specified in the data. It is a specialization of Composite Chart and inherits all
12364   * composite features other than recomposing the chart.
12365   *
12366   * Examples:
12367   * - {@link http://dc-js.github.io/dc.js/examples/series.html Series Chart}
12368   * @mixes CompositeChart
12369   */
12370  class SeriesChart extends CompositeChart {
12371      /**
12372       * Create a Series Chart.
12373       * @example
12374       * // create a series chart under #chart-container1 element using the default global chart group
12375       * var seriesChart1 = new SeriesChart("#chart-container1");
12376       * // create a series chart under #chart-container2 element using chart group A
12377       * var seriesChart2 = new SeriesChart("#chart-container2", "chartGroupA");
12378       * @param {String|node|d3.selection} parent - Any valid
12379       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
12380       * a dom block element such as a div; or a dom element or d3 selection.
12381       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
12382       * Interaction with a chart will only trigger events and redraws within the chart's group.
12383       */
12384      constructor (parent, chartGroup) {
12385          super(parent, chartGroup);
12386
12387          this._keySort = (a, b) => d3Array.ascending(this.keyAccessor()(a), this.keyAccessor()(b));
12388
12389          this._charts = {};
12390          this._chartFunction = lineChart;
12391          this._chartGroup = chartGroup;
12392          this._seriesAccessor = undefined;
12393          this._seriesSort = d3Array.ascending;
12394          this._valueSort = this._keySort;
12395
12396          this._mandatoryAttributes().push('seriesAccessor', 'chart');
12397          this.shareColors(true);
12398      }
12399
12400      _compose (subChartArray) {
12401          super.compose(subChartArray);
12402      }
12403
12404      compose (subChartArray) {
12405          throw new Error('Not supported for this chart type');
12406      }
12407
12408      _preprocessData () {
12409          const keep = [];
12410          let childrenChanged;
12411
12412          const nesting = d3compat.nester({
12413              key: this._seriesAccessor,
12414              sortKeys: this._seriesSort,
12415              sortValues: this._valueSort,
12416              entries: this.data()
12417          });
12418
12419          const children =
12420              nesting.map((sub, i) => {
12421                  const subChart = this._charts[sub.key] || this._chartFunction(this, this._chartGroup , sub.key, i);
12422                  if (!this._charts[sub.key]) {
12423                      childrenChanged = true;
12424                  }
12425                  this._charts[sub.key] = subChart;
12426                  keep.push(sub.key);
12427                  return subChart
12428                      .dimension(this.dimension())
12429                      .group({
12430                          all: typeof sub.values === 'function' ? sub.values : utils.constant(sub.values)
12431                      }, sub.key)
12432                      .keyAccessor(this.keyAccessor())
12433                      .valueAccessor(this.valueAccessor())
12434                      .brushOn(false);
12435              });
12436          // this works around the fact compositeChart doesn't really
12437          // have a removal interface
12438          Object.keys(this._charts)
12439              .filter(c => keep.indexOf(c) === -1)
12440              .forEach(c => {
12441                  this._clearChart(c);
12442                  childrenChanged = true;
12443              });
12444          this._compose(children);
12445          if (childrenChanged && this.legend()) {
12446              this.legend().render();
12447          }
12448      }
12449
12450      _clearChart (c) {
12451          if (this._charts[c].g()) {
12452              this._charts[c].g().remove();
12453          }
12454          delete this._charts[c];
12455      }
12456
12457      _resetChildren () {
12458          Object.keys(this._charts).map(this._clearChart.bind(this));
12459          this._charts = {};
12460      }
12461
12462      /**
12463       * Get or set the chart function, which generates the child charts.
12464       * @example
12465       * // put curve on the line charts used for the series
12466       * chart.chart(function(c) { return new LineChart(c).curve(d3.curveBasis); })
12467       * // do a scatter series chart
12468       * chart.chart(anchor => new ScatterPlot(anchor))
12469       * @param {Function} [chartFunction= (anchor) =>  new LineChart(anchor)]
12470       * @returns {Function|SeriesChart}
12471       */
12472      chart (chartFunction) {
12473          if (!arguments.length) {
12474              return this._chartFunction;
12475          }
12476          this._chartFunction = chartFunction;
12477          this._resetChildren();
12478          return this;
12479      }
12480
12481      /**
12482       * **mandatory**
12483       *
12484       * Get or set accessor function for the displayed series. Given a datum, this function
12485       * should return the series that datum belongs to.
12486       * @example
12487       * // simple series accessor
12488       * chart.seriesAccessor(function(d) { return "Expt: " + d.key[0]; })
12489       * @param {Function} [accessor]
12490       * @returns {Function|SeriesChart}
12491       */
12492      seriesAccessor (accessor) {
12493          if (!arguments.length) {
12494              return this._seriesAccessor;
12495          }
12496          this._seriesAccessor = accessor;
12497          this._resetChildren();
12498          return this;
12499      }
12500
12501      /**
12502       * Get or set a function to sort the list of series by, given series values.
12503       * @see {@link https://github.com/d3/d3-array/blob/master/README.md#ascending d3.ascending}
12504       * @see {@link https://github.com/d3/d3-array/blob/master/README.md#descending d3.descending}
12505       * @example
12506       * chart.seriesSort(d3.descending);
12507       * @param {Function} [sortFunction=d3.ascending]
12508       * @returns {Function|SeriesChart}
12509       */
12510      seriesSort (sortFunction) {
12511          if (!arguments.length) {
12512              return this._seriesSort;
12513          }
12514          this._seriesSort = sortFunction;
12515          this._resetChildren();
12516          return this;
12517      }
12518
12519      /**
12520       * Get or set a function to sort each series values by. By default this is the key accessor which,
12521       * for example, will ensure a lineChart series connects its points in increasing key/x order,
12522       * rather than haphazardly.
12523       * @see {@link https://github.com/d3/d3-array/blob/master/README.md#ascending d3.ascending}
12524       * @see {@link https://github.com/d3/d3-array/blob/master/README.md#descending d3.descending}
12525       * @example
12526       * // Default value sort
12527       * _chart.valueSort(function keySort (a, b) {
12528       *     return d3.ascending(_chart.keyAccessor()(a), _chart.keyAccessor()(b));
12529       * });
12530       * @param {Function} [sortFunction]
12531       * @returns {Function|SeriesChart}
12532       */
12533      valueSort (sortFunction) {
12534          if (!arguments.length) {
12535              return this._valueSort;
12536          }
12537          this._valueSort = sortFunction;
12538          this._resetChildren();
12539          return this;
12540      }
12541
12542  }
12543
12544  const seriesChart = (parent, chartGroup) => new SeriesChart(parent, chartGroup);
12545
12546  const DEFAULT_MIN_ANGLE_FOR_LABEL$1 = 0.5;
12547
12548  /**
12549   * The sunburst chart implementation is usually used to visualize a small tree distribution.  The sunburst
12550   * chart uses keyAccessor to determine the slices, and valueAccessor to calculate the size of each
12551   * slice relative to the sum of all values. Slices are ordered by {@link BaseMixin#ordering ordering} which defaults to sorting
12552   * by key.
12553   *
12554   * The keys used in the sunburst chart should be arrays, representing paths in the tree.
12555   *
12556   * When filtering, the sunburst chart creates instances of {@link Filters.HierarchyFilter HierarchyFilter}.
12557   *
12558   * @mixes CapMixin
12559   * @mixes ColorMixin
12560   * @mixes BaseMixin
12561   */
12562  class SunburstChart extends ColorMixin(BaseMixin) {
12563      /**
12564       * Create a Sunburst Chart
12565       * @example
12566       * // create a sunburst chart under #chart-container1 element using the default global chart group
12567       * var chart1 = new SunburstChart('#chart-container1');
12568       * // create a sunburst chart under #chart-container2 element using chart group A
12569       * var chart2 = new SunburstChart('#chart-container2', 'chartGroupA');
12570       *
12571       * @param {String|node|d3.selection} parent - Any valid
12572       * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying
12573       * a dom block element such as a div; or a dom element or d3 selection.
12574       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
12575       * Interaction with a chart will only trigger events and redraws within the chart's group.
12576       */
12577      constructor (parent, chartGroup) {
12578          super();
12579
12580          this._sliceCssClass = 'pie-slice';
12581          this._emptyCssClass = 'empty-chart';
12582          this._emptyTitle = 'empty';
12583
12584          this._radius = undefined;
12585          this._givenRadius = undefined; // given radius, if any
12586          this._innerRadius = 0;
12587          this._ringSizes = null;
12588
12589          this._g = undefined;
12590          this._cx = undefined;
12591          this._cy = undefined;
12592          this._minAngleForLabel = DEFAULT_MIN_ANGLE_FOR_LABEL$1;
12593          this._externalLabelRadius = undefined;
12594
12595          this.colorAccessor(d =>
12595 this.keyAccessor()(d));
12596
12597          // override cap mixin
12598          this.ordering(pluck('key'));
12599
12600          this.title(d => `${this.keyAccessor()(d)}: ${this._extendedValueAccessor(d)}`);
12601
12602          this.label(d => this.keyAccessor()(d));
12603          this.renderLabel(true);
12604
12605          this.transitionDuration(350);
12606
12607          this.anchor(parent, chartGroup);
12608      }
12609
12610      // Handle cases if value corresponds to generated parent nodes
12611      _extendedValueAccessor (d) {
12612          if (d.path) {
12613              return d.value;
12614          }
12615          return this.valueAccessor()(d);
12616      }
12617
12618      _scaleRadius (ringIndex, y) {
12619          if (ringIndex === 0) {
12620              return this._innerRadius;
12621          } else {
12622              const customRelativeRadius = d3Array.sum(this.ringSizes().relativeRingSizes.slice(0, ringIndex));
12623              const scaleFactor = (ringIndex * (1 / this.ringSizes().relativeRingSizes.length)) /
12624                    customRelativeRadius;
12625              const standardRadius = (y - this.ringSizes().rootOffset) /
12626                    (1 - this.ringSizes().rootOffset) * (this._radius - this._innerRadius);
12627              return this._innerRadius + standardRadius / scaleFactor;
12628          }
12629      }
12630
12631      _doRender () {
12632          this.resetSvg();
12633
12634          this._g = this.svg()
12635              .append('g')
12636              .attr('transform', `translate(${this.cx()},${this.cy()})`);
12637
12638          this._drawChart();
12639
12640          return this;
12641      }
12642
12643      _drawChart () {
12644          // set radius from chart size if none given, or if given radius is too large
12645          const maxRadius = d3Array.min([this.width(), this.height()]) / 2;
12646          this._radius = this._givenRadius && this._givenRadius < maxRadius ? this._givenRadius : maxRadius;
12647
12648          const arcs = this._buildArcs();
12649
12650          let partitionedNodes, cdata;
12651          // if we have data...
12652          if (d3Array.sum(this.data(), this.valueAccessor())) {
12653              cdata = utils.toHierarchy(this.data(), this.valueAccessor());
12654              partitionedNodes = this._partitionNodes(cdata);
12655              // First one is the root, which is not needed
12656              partitionedNodes.nodes.shift();
12657              this._g.classed(this._emptyCssClass, false);
12658          } else {
12659              // otherwise we'd be getting NaNs, so override
12660              // note: abuse others for its ignoring the value accessor
12661              cdata = utils.toHierarchy([], d => d.value);
12662              partitionedNodes = this._partitionNodes(cdata);
12663              this._g.classed(this._emptyCssClass, true);
12664          }
12665          this.ringSizes().rootOffset = partitionedNodes.rootOffset;
12666          this.ringSizes().relativeRingSizes = partitionedNodes.relativeRingSizes;
12667
12668          if (this._g) {
12669              const slices = this._g.selectAll(`g.${this._sliceCssClass}`)
12670                  .data(partitionedNodes.nodes);
12671              this._createElements(slices, arcs, partitionedNodes.nodes);
12672
12673              this._updateElements(partitionedNodes.nodes, arcs);
12674
12675              this._removeElements(slices);
12676
12677              this._highlightFilter();
12678
12679              transition(this._g, this.transitionDuration(), this.transitionDelay())
12680                  .attr('transform', `translate(${this.cx()},${this.cy()})`);
12681          }
12682      }
12683
12684      _createElements (slices, arcs, sunburstData) {
12685          const slicesEnter = this._createSliceNodes(slices);
12686
12687          this._createSlicePath(slicesEnter, arcs);
12688          this._createTitles(slicesEnter);
12689          this._createLabels(sunburstData, arcs);
12690      }
12691
12692      _createSliceNodes (slices) {
12693          return slices
12694              .enter()
12695              .append('g')
12696              .attr('class', (d, i) => `${this._sliceCssClass
12697            } _${i} ${
12698                this._sliceCssClass}-level-${d.depth}`);
12699      }
12700
12701      _createSlicePath (slicesEnter, arcs) {
12702          const slicePath = slicesEnter.append('path')
12703              .attr('fill', (d, i) => this._fill(d, i))
12704              .on('click', d3compat.eventHandler(d => this.onClick(d)))
12705              .classed('dc-tabbable', this._keyboardAccessible)
12706              .attr('d', d => this._safeArc(arcs, d));
12707
12708          if (this._keyboardAccessible) {
12709              this._makeKeyboardAccessible(this.onClick);
12710          }
12711
12712          const tranNodes = transition(slicePath, this.transitionDuration());
12713          if (tranNodes.attrTween) {
12714              const chart = this;
12715              tranNodes.attrTween('d', function (d) {
12716                  return chart._tweenSlice(d, this);
12717              });
12718          }
12719      }
12720
12721      _createTitles (slicesEnter) {
12722          if (this.renderTitle()) {
12723              slicesEnter.append('title').text(d =>
12723 this.title()(d));
12724          }
12725      }
12726
12727      _positionLabels (labelsEnter, arcs) {
12728          transition(labelsEnter, this.transitionDuration())
12729              .attr('transform', d => this._labelPosition(d, arcs))
12730              .attr('text-anchor', 'middle')
12731              .text(d => {
12732                  // position label...
12733                  if (this._sliceHasNoData(d) || this._sliceTooSmall(d)) {
12734                      return '';
12735                  }
12736                  return this.label()(d);
12737              });
12738      }
12739
12740      _createLabels (sunburstData, arcs) {
12741          if (this.renderLabel()) {
12742              const labels = this._g.selectAll(`text.${this._sliceCssClass}`)
12743                  .data(sunburstData);
12744
12745              labels.exit().remove();
12746
12747              const labelsEnter = labels
12748                  .enter()
12749                  .append('text')
12750                  .attr('class', (d, i) => {
12751                      let classes = `${this._sliceCssClass} _${i}`;
12752                      if (this._externalLabelRadius) {
12753                          classes += ' external';
12754                      }
12755                      return classes;
12756                  })
12757                  .on('click', d3compat.eventHandler(d => this.onClick(d)));
12758              this._positionLabels(labelsEnter, arcs);
12759          }
12760      }
12761
12762      _updateElements (sunburstData, arcs) {
12763          this._updateSlicePaths(sunburstData, arcs);
12764          this._updateLabels(sunburstData, arcs);
12765          this._updateTitles(sunburstData);
12766      }
12767
12768      _updateSlicePaths (sunburstData, arcs) {
12769          const slicePaths = this._g.selectAll(`g.${this._sliceCssClass}`)
12770              .data(sunburstData)
12771              .select('path')
12772              .attr('d', (d, i) => this._safeArc(arcs, d));
12773          const tranNodes = transition(slicePaths, this.transitionDuration());
12774          if (tranNodes.attrTween) {
12775              const chart = this;
12776              tranNodes.attrTween('d', function (d) {
12777                  return chart._tweenSlice(d, this);
12778              });
12779          }
12780          tranNodes.attr('fill', (d, i) => this._fill(d, i));
12781      }
12782
12783      _updateLabels (sunburstData, arcs) {
12784          if (this.renderLabel()) {
12785              const labels = this._g.selectAll(`text.${this._sliceCssClass}`)
12786                  .data(sunburstData);
12787              this._positionLabels(labels, arcs);
12788          }
12789      }
12790
12791      _updateTitles (sunburstData) {
12792          if (this.renderTitle()) {
12793              this._g.selectAll(`g.${this._sliceCssClass}`)
12794                  .data(sunburstData)
12795                  .select('title')
12796                  .text(d => this.title()(d));
12797          }
12798      }
12799
12800      _removeElements (slices) {
12801          slices.exit().remove();
12802      }
12803
12804      _highlightFilter () {
12805          const chart = this;
12806          if (chart.hasFilter()) {
12807              chart.selectAll(`g.${chart._sliceCssClass}`).each(function (d) {
12808                  if (chart._isSelectedSlice(d)) {
12809                      chart.highlightSelected(this);
12810                  } else {
12811                      chart.fadeDeselected(this);
12812                  }
12813              });
12814          } else {
12815              chart.selectAll(`g.${chart._sliceCssClass}`).each(function (d) {
12816                  chart.resetHighlight(this);
12817              });
12818          }
12819      }
12820
12821      /**
12822       * Get or set the inner radius of the sunburst chart. If the inner radius is greater than 0px then the
12823       * sunburst chart will be rendered as a doughnut chart. Default inner radius is 0px.
12824       * @param {Number} [innerRadius=0]
12825       * @returns {Number|SunburstChart}
12826       */
12827      innerRadius (innerRadius) {
12828          if (!arguments.length) {
12829              return this._innerRadius;
12830          }
12831          this._innerRadius = innerRadius;
12832          return this;
12833      }
12834
12835      /**
12836       * Get or set the outer radius. If the radius is not set, it will be half of the minimum of the
12837       * chart width and height.
12838       * @param {Number} [radius]
12839       * @returns {Number|SunburstChart}
12840       */
12841      radius (radius) {
12842          if (!arguments.length) {
12843              return this._givenRadius;
12844          }
12845          this._givenRadius = radius;
12846          return this;
12847      }
12848
12849      /**
12850       * Get or set center x coordinate position. Default is center of svg.
12851       * @param {Number} [cx]
12852       * @returns {Number|SunburstChart}
12853       */
12854      cx (cx) {
12855          if (!arguments.length) {
12856              return (this._cx || this.width() / 2);
12857          }
12858          this._cx = cx;
12859          return this;
12860      }
12861
12862      /**
12863       * Get or set center y coordinate position. Default is center of svg.
12864       * @param {Number} [cy]
12865       * @returns {Number|SunburstChart}
12866       */
12867      cy (cy) {
12868          if (!arguments.length) {
12869              return (this._cy || this.height() / 2);
12870          }
12871          this._cy = cy;
12872          return this;
12873      }
12874
12875      /**
12876       * Get or set the minimal slice angle for label rendering. Any slice with a smaller angle will not
12877       * display a slice label.
12878       * @param {Number} [minAngleForLabel=0.5]
12879       * @returns {Number|SunburstChart}
12880       */
12881      minAngleForLabel (minAngleForLabel) {
12882          if (!arguments.length) {
12883              return this._minAngleForLabel;
12884          }
12885          this._minAngleForLabel = minAngleForLabel;
12886          return this;
12887      }
12888
12889      /**
12890       * Title to use for the only slice when there is no data.
12891       * @param {String} [title]
12892       * @returns {String|SunburstChart}
12893       */
12894      emptyTitle (title) {
12895          if (arguments.length === 0) {
12896              return this._emptyTitle;
12897          }
12898          this._emptyTitle = title;
12899          return this;
12900      }
12901
12902      /**
12903       * Position slice labels offset from the outer edge of the chart.
12904       *
12905       * The argument specifies the extra radius to be added for slice labels.
12906       * @param {Number} [externalLabelRadius]
12907       * @returns {Number|SunburstChart}
12908       */
12909      externalLabels (externalLabelRadius) {
12910          if (arguments.length === 0) {
12911              return this._externalLabelRadius;
12912          } else if (externalLabelRadius) {
12913              this._externalLabelRadius = externalLabelRadius;
12914          } else {
12915              this._externalLabelRadius = undefined;
12916          }
12917
12918          return this;
12919      }
12920
12921      /**
12922       * Constructs the default RingSizes parameter for {@link SunburstChart#ringSizes ringSizes()},
12923       * which makes the rings narrower as they get farther away from the center.
12924       *
12925       * Can be used as a parameter to ringSizes() to reset the default behavior, or modified for custom ring sizes.
12926       *
12927       * @example
12928       *   var chart = new dc.SunburstChart(...);
12929       *   chart.ringSizes(chart.defaultRingSizes())
12930       * @returns {RingSizes}
12931       */
12932      defaultRingSizes () {
12933          return {
12934              partitionDy: () => this._radius * this._radius,
12935              scaleInnerRadius: d => d.data.path && d.data.path.length === 1 ?
12936                  this._innerRadius :
12937                  Math.sqrt(d.y0),
12938              scaleOuterRadius: d => Math.sqrt(d.y1),
12939              relativeRingSizesFunction: () => []
12940          };
12941      }
12942
12943      /**
12944       * Constructs a RingSizes parameter for {@link SunburstChart#ringSizes ringSizes()}
12945       * that will make the chart rings equally wide.
12946       *
12947       * @example
12948       *   var chart = new dc.SunburstChart(...);
12949       *   chart.ringSizes(chart.equalRingSizes())
12950       * @returns {RingSizes}
12951       */
12952      equalRingSizes () {
12953          return this.relativeRingSizes(
12954              ringCount => {
12955                  const result = [];
12956                  for (let i = 0; i < ringCount; i++) {
12957                      result.push(1 / ringCount);
12958                  }
12959                  return result;
12960              }
12961          );
12962      }
12963
12964      /**
12965       * Constructs a RingSizes parameter for {@link SunburstChart#ringSizes ringSizes()} using the given function
12966       * to determine each rings width.
12967       *
12968       * * The function must return an array containing portion values for each ring/level of the chart.
12969       * * The length of the array must match the number of rings of the chart at runtime, which is provided as the only
12970       *   argument.
12971       * * The sum of all portions from the array must be 1 (100%).
12972       *
12973       * @example
12974       * // specific relative portions (the number of rings (3) is known in this case)
12975       * chart.ringSizes(chart.relativeRingSizes(function (ringCount) {
12976       *     return [.1, .3, .6];
12977       * });
12978       * @param {Function} [relativeRingSizesFunction]
12979       * @returns {RingSizes}
12980       */
12981      relativeRingSizes (relativeRingSizesFunction) {
12982          function assertPortionsArray (relativeSizes, numberOfRings) {
12983              if (!Array.isArray(relativeSizes)) {
12984                  throw new BadArgumentException('relativeRingSizes function must return an array');
12985              }
12986
12987              const portionsSum = d3Array.sum(relativeSizes);
12988              if (Math.abs(portionsSum - 1) > constants.NEGLIGIBLE_NUMBER) {
12989                  throw new BadArgumentException(
12990                      `relativeRingSizes : portions must add up to 1, but sum was ${portionsSum}`);
12991              }
12992
12993              if (relativeSizes.length !== numberOfRings) {
12994                  throw new BadArgumentException(
12995                      `relativeRingSizes : number of values must match number of rings (${
12996                        numberOfRings}) but was ${relativeSizes.length}`);
12997              }
12998          }
12999          return {
13000              partitionDy: () => 1,
13001              scaleInnerRadius: d => this._scaleRadius(d.data.path.length - 1, d.y0),
13002              scaleOuterRadius: d => this._scaleRadius(d.data.path.length, d.y1),
13003              relativeRingSizesFunction: ringCount => {
13004                  const result = relativeRingSizesFunction(ringCount);
13005                  assertPortionsArray(result, ringCount);
13006                  return result;
13007              }
13008          };
13009      }
13010
13011      /**
13012       * Get or set the strategy to use for sizing the charts rings.
13013       *
13014       * There are three strategies available
13015       * * {@link SunburstChart#defaultRingSizes `defaultRingSizes`}: the rings get narrower farther away from the center
13016       * * {@link SunburstChart#relativeRingSizes `relativeRingSizes`}: set the ring sizes as portions of 1
13017       * * {@link SunburstChart#equalRingSizes `equalRingSizes`}: the rings are equally wide
13018       *
13019       * You can modify the returned strategy, or create your own, for custom ring sizing.
13020       *
13021       * RingSizes is a duck-typed interface that must support the following methods:
13022       * * `partitionDy()`: used for
13023       *   {@link https://github.com/d3/d3-hierarchy/blob/v1.1.9/README.md#partition_size `d3.partition.size`}
13024       * * `scaleInnerRadius(d)`: takes datum and returns radius for
13025       *    {@link https://github.com/d3/d3-shape/blob/v1.3.7/README.md#arc_innerRadius `d3.arc.innerRadius`}
13026       * * `scaleOuterRadius(d)`: takes datum and returns radius for
13027       *    {@link https://github.com/d3/d3-shape/blob/v1.3.7/README.md#arc_outerRadius `d3.arc.outerRadius`}
13028       * * `relativeRingSizesFunction(ringCount)`: takes ring count and returns an array of portions that
13029       *   must add up to 1
13030       *
13031       * @example
13032       * // make rings equally wide
13033       * chart.ringSizes(chart.equalRingSizes())
13034       * // reset to default behavior
13035       * chart.ringSizes(chart.defaultRingSizes()))
13036       * @param {RingSizes} ringSizes
13037       * @returns {Object|SunburstChart}
13038       */
13039      ringSizes (ringSizes) {
13040          if (!arguments.length) {
13041              if (!this._ringSizes) {
13042                  this._ringSizes = this.defaultRingSizes();
13043              }
13044              return this._ringSizes;
13045          }
13046          this._ringSizes = ringSizes;
13047          return this;
13048      }
13049
13050      _buildArcs () {
13051          return d3Shape.arc()
13052              .startAngle(d => d.x0)
13053              .endAngle(d => d.x1)
13054              .innerRadius(d => this.ringSizes().scaleInnerRadius(d))
13055              .outerRadius(d =>
13055 this.ringSizes().scaleOuterRadius(d));
13056      }
13057
13058      _isSelectedSlice (d) {
13059          return this._isPathFiltered(d.path);
13060      }
13061
13062      _isPathFiltered (path) {
13063          for (let i = 0; i < this.filters().length; i++) {
13064              const currentFilter = this.filters()[i];
13065              if (currentFilter.isFiltered(path)) {
13066                  return true;
13067              }
13068          }
13069          return false;
13070      }
13071
13072      // returns all filters that are a parent or child of the path
13073      _filtersForPath (path) {
13074          const pathFilter = filters.HierarchyFilter(path);
13075          const filtersList = [];
13076          for (let i = 0; i < this.filters().length; i++) {
13077              const currentFilter = this.filters()[i];
13078              if (currentFilter.isFiltered(path) || pathFilter.isFiltered(currentFilter)) {
13079                  filtersList.push(currentFilter);
13080              }
13081          }
13082          return filtersList;
13083      }
13084
13085      _doRedraw () {
13086          this._drawChart();
13087          return this;
13088      }
13089
13090      _partitionNodes (data) {
13091          const getSortable = function (d) {
13092              return {'key': d.data.key, 'value': d.value};
13093          };
13094          const _hierarchy = d3Hierarchy.hierarchy(data)
13095              .sum(d => d.children ? 0 : this._extendedValueAccessor(d))
13096              .sort((a, b) => d3Array.ascending(this.ordering()(getSortable(a)), this.ordering()(getSortable(b))));
13097
13098          const _partition = d3Hierarchy.partition()
13099                .size([2 * Math.PI, this.ringSizes().partitionDy()]);
13100
13101          _partition(_hierarchy);
13102
13103          // In D3v4 the returned data is slightly different, change it enough to suit our purposes.
13104          const nodes = _hierarchy.descendants().map(d => {
13105              d.key = d.data.key;
13106              d.path = d.data.path;
13107              return d;
13108          });
13109
13110          const relativeSizes = this.ringSizes().relativeRingSizesFunction(_hierarchy.height);
13111
13112          return {
13113              nodes,
13114              rootOffset: _hierarchy.y1,
13115              relativeRingSizes: relativeSizes
13116          };
13117      }
13118
13119      _sliceTooSmall (d) {
13120          const angle = d.x1 - d.x0;
13121          return isNaN(angle) || angle < this._minAngleForLabel;
13122      }
13123
13124      _sliceHasNoData (d) {
13125          return this._extendedValueAccessor(d) === 0;
13126      }
13127
13128      _isOffCanvas (d) {
13129          return !d || isNaN(d.x0) || isNaN(d.y0);
13130      }
13131
13132      _fill (d, i) {
13133          return this.getColor(d.data, i);
13134      }
13135
13136      onClick (d) {
13137          if (this._g.attr('class') === this._emptyCssClass) {
13138              return;
13139          }
13140
13141          // Must be better way to handle this, in legends we need to access `d.key`
13142          const path = d.path || d.key;
13143          const filter = filters.HierarchyFilter(path);
13144
13145          // filters are equal to parents or children of the path.
13146          const filtersList = this._filtersForPath(path);
13147          let exactMatch = false;
13148          // clear out any filters that cover the path filtered.
13149          for (let j = filtersList.length - 1; j >= 0; j--) {
13150              const currentFilter = filtersList[j];
13151              if (utils.arraysIdentical(currentFilter, path)) {
13152                  exactMatch = true;
13153              }
13154              this.filter(filtersList[j]);
13155          }
13156          events.trigger(() => {
13157              // if it is a new filter - put it in.
13158              if (!exactMatch) {
13159                  this.filter(filter);
13160              }
13161              this.redrawGroup();
13162          });
13163      }
13164
13165      _safeArc (_arc, d) {
13166          let path = _arc(d);
13167          if (path.indexOf('NaN') >= 0) {
13168              path = 'M0,0';
13169          }
13170          return path;
13171      }
13172
13173      _labelPosition (d, _arc) {
13174          let centroid;
13175          if (this._externalLabelRadius) {
13176              centroid = d3Shape.arc()
13177                  .outerRadius(this._radius + this._externalLabelRadius)
13178                  .innerRadius(this._radius + this._externalLabelRadius)
13179                  .centroid(d);
13180          } else {
13181              centroid = _arc.centroid(d);
13182          }
13183          if (isNaN(centroid[0]) || isNaN(centroid[1])) {
13184              return 'translate(0,0)';
13185          } else {
13186              return `translate(${centroid})`;
13187          }
13188      }
13189
13190      legendables () {
13191          return this.data().map((d, i) => {
13192              const legendable = {name: d.key, data: d.value, others: d.others, chart: this};
13193              legendable.color = this.getColor(d, i);
13194              return legendable;
13195          });
13196      }
13197
13198      legendHighlight (d) {
13199          this._highlightSliceFromLegendable(d, true);
13200      }
13201
13202      legendReset (d) {
13203          this._highlightSliceFromLegendable(d, false);
13204      }
13205
13206      legendToggle (d) {
13207          this.onClick({key: d.name, others: d.others});
13208      }
13209
13210      _highlightSliceFromLegendable (legendable, highlighted) {
13211          this.selectAll('g.pie-slice').each(function (d) {
13212              if (legendable.name === d.key) {
13213                  d3Selection.select(this).classed('highlight', highlighted);
13214              }
13215          });
13216      }
13217
13218      _tweenSlice (d, element) {
13219          let current = element._current;
13220          if (this._isOffCanvas(current)) {
13221              current = {x0: 0, x1: 0, y0: 0, y1: 0};
13222          }
13223          const tweenTarget = {
13224              x0: d.x0,
13225              x1: d.x1,
13226              y0: d.y0,
13227              y1: d.y1
13228          };
13229          const i = d3Interpolate.interpolate(current, tweenTarget);
13230          element._current = i(0);
13231          return t => this._safeArc(this._buildArcs(), Object.assign({}, d, i(t)));
13232      }
13233  }
13234
13235  const sunburstChart = (parent, chartGroup) => new SunburstChart(parent, chartGroup);
13236
13237  const INPUT_CSS_CLASS = 'dc-text-filter-input';
13238
13239  /**
13240   * Text Filter Widget
13241   *
13242   * The text filter widget is a simple widget designed to display an input field allowing to filter
13243   * data that matches the text typed.
13244   * As opposed to the other charts, this doesn't display any result and doesn't update its display,
13245   * it's just to input an filter other charts.
13246   *
13247   * @mixes BaseMixin
13248   */
13249  class TextFilterWidget extends BaseMixin {
13250      /**
13251       * Create Text Filter widget
13252       * @example
13253       *
13254       * var data = [{"firstName":"John","lastName":"Coltrane"}{"firstName":"Miles",lastName:"Davis"}]
13255       * var ndx = crossfilter(data);
13256       * var dimension = ndx.dimension(function(d) {
13257       *     return d.lastName.toLowerCase() + ' ' + d.firstName.toLowerCase();
13258       * });
13259       *
13260       * new TextFilterWidget('#search')
13261       *     .dimension(dimension);
13262       *     // you don't need the group() function
13263       *
13264       * @param {String|node|d3.selection|CompositeChart} parent - Any valid
13265       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector}
13266       * specifying a dom block element such as a div; or a dom element or d3 selection.
13267       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
13268       * Interaction with a chart will only trigger events and redraws within the chart'
13268s group.
13269       */
13270      constructor (parent, chartGroup) {
13271          super();
13272
13273          this._normalize = s => s.toLowerCase();
13274
13275          this._filterFunctionFactory = query => {
13276              query = this._normalize(query);
13277              return d => this._normalize(d).indexOf(query) !== -1;
13278          };
13279
13280          this._placeHolder = 'search';
13281
13282          this.group(() => {
13283              throw 'the group function on textFilterWidget should never be called, please report the issue';
13284          });
13285
13286          this.anchor(parent, chartGroup);
13287      }
13288
13289      _doRender () {
13290          this.select('input').remove();
13291
13292          this._input = this.root().append('input')
13293              .classed(INPUT_CSS_CLASS, true);
13294
13295          const chart = this;
13296          this._input.on('input', function () {
13297              chart.dimension().filterFunction(chart._filterFunctionFactory(this.value));
13298              events.trigger(() => {
13299                  chart.redrawGroup();
13300              }, constants.EVENT_DELAY);
13301          });
13302
13303          this._doRedraw();
13304
13305          return this;
13306      }
13307
13308      _doRedraw () {
13309          this.root().selectAll('input')
13310              .attr('placeholder', this._placeHolder);
13311
13312          return this;
13313      }
13314
13315      /**
13316       * This function will be called on values before calling the filter function.
13317       * @example
13318       * // This is the default
13319       * chart.normalize(function (s) {
13320       *   return s.toLowerCase();
13321       * });
13322       * @param {function} [normalize]
13323       * @returns {TextFilterWidget|function}
13324       */
13325      normalize (normalize) {
13326          if (!arguments.length) {
13327              return this._normalize;
13328          }
13329          this._normalize = normalize;
13330          return this;
13331      }
13332
13333      /**
13334       * Placeholder text in the search box.
13335       * @example
13336       * // This is the default
13337       * chart.placeHolder('type to filter');
13338       * @param {function} [placeHolder='search']
13339       * @returns {TextFilterWidget|string}
13340       */
13341      placeHolder (placeHolder) {
13342          if (!arguments.length) {
13343              return this._placeHolder;
13344          }
13345          this._placeHolder = placeHolder;
13346          return this;
13347      }
13348
13349      /**
13350       * This function will be called with the search text, it needs to return a function that will be used to
13351       * filter the data. The default function checks presence of the search text.
13352       * @example
13353       * // This is the default
13354       * function (query) {
13355       *     query = _normalize(query);
13356       *     return function (d) {
13357       *         return _normalize(d).indexOf(query) !== -1;
13358       *     };
13359       * };
13360       * @param {function} [filterFunctionFactory]
13361       * @returns {TextFilterWidget|function}
13362       */
13363      filterFunctionFactory (filterFunctionFactory) {
13364          if (!arguments.length) {
13365              return this._filterFunctionFactory;
13366          }
13367          this._filterFunctionFactory = filterFunctionFactory;
13368          return this;
13369      }
13370  }
13371
13372  const textFilterWidget = (parent, chartGroup) => new TextFilterWidget(parent, chartGroup);
13373
13374  const majorVer = +d3.version[0];
13375
13376  if (majorVer < 6) {
13377      Object.assign(d3compat, {
13378          eventHandler: handler => function eventHandler (d, _) {
13379              handler.call(this, d, d3Selection.event);
13380          },
13381          // manual firing of event, usu for tests
13382          callHandler: function callHandler (handler, that, _, d) {
13383              // note: dropping event as well as any extra args
13384              // d3@6 does not pass extra args anymore, so we can't use them and remain compatible
13385              handler.call(that, d);
13386          },
13387          nester: ({key, sortKeys, sortValues, entries}) => {
13388              const nester = d3Collection.nest().key(key);
13389              if (sortKeys) {
13390                  nester.sortKeys(sortKeys);
13391              }
13392              if (sortValues) {
13393                  nester.sortValues(sortValues);
13394              }
13395              return nester.entries(entries);
13396          },
13397          pointer: (evt, elem) => d3Selection.mouse(elem)
13398      });
13399  }
13400
13401  const majorVer$1 = +d3.version[0];
13402
13403  if (majorVer$1 > 5) {
13404      Object.assign(d3compat, {
13405          eventHandler: handler => function eventHandler (event, d) {
13406              handler.call(this, d, event);
13407          },
13408          // manual firing of event, usu for tests
13409          callHandler: function callHandler (handler, that, event, d) {
13410              handler.call(that, event, d);
13411          },
13412          nester: ({key, sortKeys, sortValues, entries}) => {
13413              if (sortValues) {
13414                  entries = [...entries].sort(sortValues);
13415              }
13416              let out = d3Array.groups(entries, key);
13417              if (sortKeys) {
13418                  out = out.sort(sortKeys);
13419              }
13420
13421              // remap to d3@v5 structure
13422              return out.map(e => ({
13423                  key: `${e[0]}`, // d3@v5 always returns key as string
13424                  values: e[1]
13425              }));
13426          },
13427          pointer: d3Selection.pointer
13428      });
13429  }
13430
13431  exports.BadArgumentException = BadArgumentException;
13432  exports.BarChart = BarChart;
13433  exports.BaseMixin = BaseMixin;
13434  exports.BoxPlot = BoxPlot;
13435  exports.BubbleChart = BubbleChart;
13436  exports.BubbleMixin = BubbleMixin;
13437  exports.BubbleOverlay = BubbleOverlay;
13438  exports.CapMixin = CapMixin;
13439  exports.CboxMenu = CboxMenu;
13440  exports.ColorMixin = ColorMixin;
13441  exports.CompositeChart = CompositeChart;
13442  exports.Config = Config;
13443  exports.CoordinateGridMixin = CoordinateGridMixin;
13444  exports.DataCount = DataCount;
13445  exports.DataGrid = DataGrid;
13446  exports.DataTable = DataTable;
13447  exports.GeoChoroplethChart = GeoChoroplethChart;
13448  exports.HeatMap = HeatMap;
13449  exports.HtmlLegend = HtmlLegend;
13450  exports.InvalidStateException = InvalidStateException;
13451  exports.Legend = Legend;
13452  exports.LineChart = LineChart;
13453  exports.Logger = Logger;
13454  exports.MarginMixin = MarginMixin;
13455  exports.NumberDisplay = NumberDisplay;
13456  exports.PieChart = PieChart;
13457  exports.RowChart = RowChart;
13458  exports.ScatterPlot = ScatterPlot;
13459  exports.SelectMenu = SelectMenu;
13460  exports.SeriesChart = SeriesChart;
13461  exports.StackMixin = StackMixin;
13462  exports.SunburstChart = SunburstChart;
13463  exports.TextFilterWidget = TextFilterWidget;
13464  exports.afterTransition = afterTransition;
13465  exports.barChart = barChart;
13466  exports.baseMixin = baseMixin;
13467  exports.boxPlot = boxPlot;
13468  exports.bubbleChart = bubbleChart;
13469  exports.bubbleOverlay = bubbleOverlay;
13470  exports.cboxMenu = cboxMenu;
13471  exports.chartRegistry = chartRegistry;
13472  exports.compositeChart = compositeChart;
13473  exports.config = config;
13474  exports.constants = constants;
13475  exports.d3Box = d3Box;
13476  exports.d3compat = d3compat;
13477  exports.dataCount = dataCount;
13478  exports.dataGrid = dataGrid;
13479  exports.dataTable = dataTable;
13480  exports.deregisterAllCharts = deregisterAllCharts;
13481  exports.deregisterChart = deregisterChart;
13482  exports.events = events;
13483  exports.filterAll = filterAll;
13484  exports.filters = filters;
13485  exports.geoChoroplethChart = geoChoroplethChart;
13486  exports.hasChart = hasChart;
13487  exports.heatMap = heatMap;
13488  exports.htmlLegend = htmlLegend;
13489  exports.instanceOfChart = instanceOfChart;
13490  exports.legend = legend;
13491  exports.lineChart = lineChart;
13492  exports.logger = logger;
13493  exports.numberDisplay = numberDisplay;
13494  exports.optionalTransition = optionalTransition;
13495  exports.pieChart = pieChart;
13496  exports.pluck = pluck;
13497  exports.printers = printers;
13498  exports.redrawAll = redrawAll;
13499  exports.refocusAll = refocusAll;
13500  exports.registerChart = registerChart;
13501  exports.renderAll = renderAll;
13502  exports.renderlet = renderlet;
13503  exports.rowChart = rowChart;
13504  exports.scatterPlot = scatterPlot;
13505  exports.selectMenu = selectMenu;
13506  exports.seriesChart = seriesChart;
13507  exports.sunburstChart = sunburstChart;
13508  exports.textFilterWidget = textFilterWidget;
13509  exports.transition = transition;
13510  exports.units = units;
13511  exports.utils = utils;
13512  exports.version = version;
13513
13514  Object.defineProperty(exports, '__esModule', { value: true });
13515
13516})));
13517//# sourceMappingURL=dc.js.map

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.