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.