[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\n    [\"@babel/preset-env\", { \"modules\": false }]\n  ]\n}\n"
  },
  {
    "path": ".browserslistrc",
    "content": "defaults\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[**.js]\nindent_size = 4\n"
  },
  {
    "path": ".gitattributes",
    "content": "d3* linguist-vendored=false\nexamples/* linguist-vendored=true\ntest/* linguist-vendored=true\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: npm\n  directory: \"/\"\n  schedule:\n    interval: daily\n    time: \"10:00\"\n  open-pull-requests-limit: 10\n  ignore:\n  - dependency-name: eslint-config-takiyon\n    versions:\n    - 1.0.0\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Build\non: push\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node: [20, 22, 24]\n    steps:\n      -\n        uses: actions/checkout@v3\n      -\n        name: Use Node.js ${{ matrix.node }}\n        uses: actions/setup-node@v3\n        with:\n          node-version: ${{ matrix.node }}\n      -\n        name: Install dependencies\n        run: yarn install\n      -\n        name: Install Playwright\n        run: npx playwright install --with-deps firefox\n      -\n        name: Run tests\n        run: npm run test && npm run build\n"
  },
  {
    "path": ".gitignore",
    "content": "/dist\r\n/examples/dist\r\n/node_modules\r\n/test/compiled\r\npackage-lock.json\r\nyarn.lock\r\n"
  },
  {
    "path": ".mocharc.json",
    "content": "{\n  \"require\": [\n    \"@babel/register\",\n    \"global-jsdom/register\"\n  ],\n  \"reporter\": \"spec\"\n}\n"
  },
  {
    "path": ".stylelintrc.yml",
    "content": "extends:\n  - stylelint-config-takiyon\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## [v2.1.3](https://github.com/jakezatecky/d3-funnel/compare/v2.1.2...v2.1.3) (2025-09-11)\n\n### Bug Fixes\n\n* Fix issue with default export/import in ESM modules (#267)\n\n## [v2.1.2](https://github.com/jakezatecky/d3-funnel/compare/v2.1.0...v2.1.2) (2025-09-08)\n\n* Release with modern optimizations\n\n## [v2.1.0](https://github.com/jakezatecky/d3-funnel/compare/v2.0.0...v2.1.0) (2022-02-25)\n\n### Other\n\n* [#156]: Add TypeScript index file\n\n## [v2.0.0](https://github.com/jakezatecky/d3-funnel/compare/v1.2.2...v2.0.0) (2021-06-04)\n\n### Bug Fixes\n\n* [#138]: Fix an issue with tooltip alignment in newer versions of Chrome\n\n### Dependencies\n\n* **Breaking**: Upgrade to using D3 v6 (changes `events.click.block(d)` to `events.click.block(event, d)`)\n\n## [v1.2.2](https://github.com/jakezatecky/d3-funnel/compare/v1.2.1...v1.2.2) (2019-01-26)\n\n### Performance\n\n* [#97]: Significantly reduce package size to around 27% of its original size\n\n## [v1.2.1](https://github.com/jakezatecky/d3-funnel/compare/v1.2.0...v1.2.1) (2018-10-13)\n\n### Build Process\n\n* [#93]: Fix issue where `dist/d3-funnel.js` was being minified alongside `dist/d3-funnel.min.js`\n\n## [v1.2.0](https://github.com/jakezatecky/d3-funnel/compare/v1.1.1...v1.2.0) (2018-06-25)\n\n### Dependencies\n\n* [#87]: Add official support for D3 v5 (while continuing support for D3 v4)\n\n### Bug Fixes\n\n* [#86]: Fix issue where heights were being calculated incorrectly when the sum of values was zero\n\n## [v1.1.1](https://github.com/jakezatecky/d3-funnel/compare/v1.1.0...v1.1.1) (2017-07-31)\n\nThis is a patch for the npm release, which was shipped without the updated `/dist` directory.\n\n## [v1.1.0](https://github.com/jakezatecky/d3-funnel/compare/v1.0.1...v1.1.0) (2017-07-31)\n\nRelease **v1.1.0** adds a variety of new functionality to the funnel, and introduces a new data structure that allows for more flexibility on a row level than previously capable:\n\n``` javascript\nfunnel.draw([{\n    label: 'Prospects',\n    value: 5000,\n    backgroundColor: '#d33',\n}]);\n```\n\nThe old structure of an array-of-arrays has been deprecated and will be removed in the **v2.0** release. Please update to the newest data structure as soon as possible. Refer to the README for the list of available options, which includes all of the capabilities previously held in the data array.\n\n### Deprecations\n\n* [#73]: The old array-of-arrays data structure has been deprecated in favor of a data objects\n\n### New Features\n\n* [#45]: Add support for tooltips via `tooltip.enabled` and `tooltip.format`\n* [#71]: Add `hideLabel` option to the data object\n* [#74]: Add `label.enabled` chart option\n* [#79]: Add support for `HTMLElement` in the D3Funnel constructor\n\n### Bug Fixes\n\n* [#77]: Fix an issue where containers with zero width and/or height would not inherit from the default dimensions\n\n## [v1.0.1](https://github.com/jakezatecky/d3-funnel/compare/v1.0.0...v1.0.1) (2017-01-30)\n\n### Bug Fixes\n\n* [#67]: Add missing `cursor: pointer` style to blocks when clickable\n* [#70]: Fix NaN and Infinity values in block paths when height is zero and `dynamicHeight: true`\n\n## [v1.0.0](https://github.com/jakezatecky/d3-funnel/compare/v0.8.0...v1.0.0) (2016-08-02)\n\nThis release breaks major backwards compatibility by upgrading D3 3.x to\nD3 4.x. Refer to D3's [changes documentation](d3-changes) for more info.\n\n### Behavior Changes\n\n* [#62]: Upgrade D3 3.x to 4.x\n\n[d3-changes]: https://github.com/d3/d3/blob/master/CHANGES.md\n\n## [v0.8.0](https://github.com/jakezatecky/d3-funnel/compare/v0.7.7...v0.8.0) (2016-07-21)\n\n### New Features\n\n* [#19]: Add support for percentages in `chart.width` and `chart.height` (e.g. `'75%'`)\n* [#38]: Split line break characters found in `label.format` into multiple lines\n\n### Bug Fixes\n\n* [#49]: Fix issue where gradient definitions could conflict with existing definitions\n\n## [v0.7.7](https://github.com/jakezatecky/d3-funnel/compare/v0.7.6...v0.7.7) (2016-07-15)\n\n### New Features\n\n* [#50]: Add `block.barOverlay` option to display bar charts proportional to block value\n* [#52]: Add `chart.totalCount` option to override total counts used in ratio calculations\n\n### Other\n\n* Simplify and clean up examples\n\n## [v0.7.6](https://github.com/jakezatecky/d3-funnel/compare/v0.7.5...v0.7.6) (2016-07-12)\n\n### New Features\n\n* [#53]: Add `label.fontSize` option\n* [#57]: Add `block.dynamicSlope` option to make the funnel width proportional to its value\n\n### Bug Fixes\n\n* [#59]: Fix issue where formatted array values were not being passed to the label formatter\n\n## [v0.7.5](https://github.com/jakezatecky/d3-funnel/compare/v0.7.4...v0.7.5) (2015-12-19)\n\n### New Features\n\n* [#44]: Pass DOM node to event data\n\n## [v0.7.4](https://github.com/jakezatecky/d3-funnel/compare/v0.7.3...v0.7.4) (2015-12-11)\n\n### Build Changes\n\n* [#42]: Use ES6 imports and exports in source files\n* [#43]: Require D3.js for CommonJS environments\n\n## [v0.7.3](https://github.com/jakezatecky/d3-funnel/compare/v0.7.2...v0.7.3) (skipped)\n\nD3Funnel v0.7.3 is an NPM-only hotfix that adds in missing compiled files.\n\n## [v0.7.2](https://github.com/jakezatecky/d3-funnel/compare/v0.7.1...v0.7.2) (2015-11-18)\n\n### Bug Fixes\n\n* [#41]: Fix issue where `events.click.block` would error on `null`\n\n## [v0.7.1](https://github.com/jakezatecky/d3-funnel/compare/v0.7.0...v0.7.1) (2015-10-28)\n\n### Behavior Changes\n\n* Errors thrown on data validation are now more descriptive and context-aware\n\n### Bug Fixes\n\n* [#35]: Fix issue where gradient background would not persist after mouse out\n* [#36]: Fix issue where non-SVG entities were not being removed from container\n\n## [v0.7.0](https://github.com/jakezatecky/d3-funnel/compare/v0.6.13...v0.7.0) (2015-10-04)\n\nD3Funnel v0.7 is a **backwards-incompatible** release that resolves some\noutstanding bugs, standardizes several option names and formats, and introduces\na few new features.\n\nNo new features will be added to the v0.6 series, but minor patches will be\navailable for a few months.\n\n### Behavior Changes\n\n* [#29]: Dynamic block heights are no longer determined by their weighted area, but by their weighted height\n\t* Heights determined by weighted area: http://jsfiddle.net/zq4L82kv/2/ (legacy v0.6.x)\n\t* Heights determined by weighted height: http://jsfiddle.net/bawv6m0j/3/ (v0.7+)\n\n### New Features\n\n* [#9]: Block can now have their color scale specified in addition to data points\n* [#34]: Default options are now statically available and overridable\n\n### Bug Fixes\n\n* [#25]: Fix issues with `isInverted` and `dynamicArea` producing odd pyramids\n* [#32]: Fix issue where pinched blocks were not having the same width as `bottomWidth`\n\n### Upgrading from v0.6.x\n\nSeveral options have been renamed for standardization. Please refer to the table\nbelow for the new equivalent option:\n\n| Old option     | New option            | Notes           |\n| -------------- | --------------------- | --------------- |\n| `animation`    | `chart.animate`       |                 |\n| `bottomPinch`  | `chart.bottomPinch`   |                 |\n| `bottomWidth`  | `chart.bottomWidth`   |                 |\n| `curveHeight`  | `chart.curve.height`  |                 |\n| `dynamicArea`  | `block.dynamicHeight` | See change #29. |\n| `fillType`     | `block.fill.type`     |                 |\n| `height`       | `chart.height`        |                 |\n| `hoverEffects` | `block.hightlight`    |                 |\n| `isCurved`     | `chart.curve.enabled` |                 |\n| `isInverted`   | `chart.inverted`      |                 |\n| `onItemClick`  | `events.click.block`  |                 |\n| `minHeight`    | `block.minHeight`     |                 |\n| `width`        | `chart.width`         |                 |\n\nIn addition, please refer to change #29.\n\n## [v0.6.13](https://github.com/jakezatecky/d3-funnel/compare/v0.6.12...v0.6.13) (2015-10-02)\n\n### Bug Fixes\n\n* [#33]: Fix issue where `package.json` pointed to the incorrect main file\n\n## [v0.6.12](https://github.com/jakezatecky/d3-funnel/compare/0.6.11...v0.6.12) (2015-09-25)\n\n### New Features\n\n* [#16]: Add support for formatted labels\n\n### Bug Fixes\n\n* [#26]: Fix issues with closed range intervals in `bottomWidth`\n* [#28]: Fix issue where short hex colors did not translate properly in color manipulations\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017 Jake Zatecky\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# d3-funnel\n\n[![npm](https://img.shields.io/npm/v/d3-funnel.svg?style=flat-square)](https://www.npmjs.com/package/d3-funnel)\n[![Build Status](https://img.shields.io/github/actions/workflow/status/jakezatecky/d3-funnel/main.yml?branch=master&style=flat-square)](https://github.com/jakezatecky/d3-funnel/actions/workflows/main.yml)\n[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/jakezatecky/d3-funnel/master/LICENSE.txt)\n\n**d3-funnel** is an extensible, open-source JavaScript library for rendering\nfunnel charts using the [D3.js][d3] library.\n\nd3-funnel is focused on providing practical and visually appealing funnels\nthrough a variety of customization options. Check out the [examples page][examples]\nto get a showcasing of the several possible options.\n\n# Installation\n\nInstall this library via npm, yarn, pnpm, or your preferred package manager:\n\n```\nnpm install d3-funnel --save\n```\n\nYou can then load this library into your app using `import`:\n\n``` javascript\nimport D3Funnel from 'd3-funnel';\n```\n\n# Usage\n\nTo use this library, you must create a container element and instantiate a new\nfunnel chart. By default, the chart will assume the width and height of the\nparent container:\n\n``` html\n<div id=\"funnel\"></div>\n\n<script>\n    const data = [\n        { label: 'Inquiries', value: 5000 },\n        { label: 'Applicants', value: 2500 },\n        { label: 'Admits', value: 500 },\n        { label: 'Deposits', value: 200 },\n    ];\n    const options = {\n        block: {\n            dynamicHeight: true,\n            minHeight: 15,\n        },\n    };\n\n    const chart = new D3Funnel('#funnel');\n    chart.draw(data, options);\n</script>\n```\n\n## Options\n\n| Option                 | Description                                                               | Type     | Default                 |\n| ---------------------- | ------------------------------------------------------------------------- | -------- | ----------------------- |\n| `chart.width`          | The width of the chart in pixels or a percentage.                         | mixed    | Container's width       |\n| `chart.height`         | The height of the chart in pixels or a percentage.                        | mixed    | Container's height      |\n| `chart.bottomWidth`    | The percent of total width the bottom should be.                          | number   | `1 / 3`                 |\n| `chart.bottomPinch`    | How many blocks to pinch on the bottom to create a funnel \"neck\".         | number   | `0`                     |\n| `chart.inverted`       | Whether the funnel direction is inverted (like a pyramid).                | bool     | `false`                 |\n| `chart.animate`        | The load animation speed in milliseconds.                                 | number   | `0` (disabled)          |\n| `chart.curve.enabled`  | Whether the funnel is curved.                                             | bool     | `false`                 |\n| `chart.curve.height`   | The curvature amount.                                                     | number   | `20`                    |\n| `chart.totalCount`     | Override the total count used in ratio calculations.                      | number   | `null`                  |\n| `block.dynamicHeight`  | Whether the block heights are proportional to their weight.               | bool     | `false`                 |\n| `block.dynamicSlope`   | Whether the block widths are proportional to their value decrease.        | bool     | `false`                 |\n| `block.barOverlay`     | Whether the blocks have bar chart overlays proportional to its weight.    | bool     | `false`                 |\n| `block.fill.scale`     | The background color scale as an array or function.                       | mixed    | `d3.schemeCategory10`   |\n| `block.fill.type`      | Either `'solid'` or `'gradient'`.                                         | string   | `'solid'`               |\n| `block.minHeight`      | The minimum pixel height of a block.                                      | number   | `0`                     |\n| `block.highlight`      | Whether the blocks are highlighted on hover.                              | bool     | `false`                 |\n| `label.enabled`        | Whether the block labels should be displayed.                             | bool     | `true`                  |\n| `label.fontFamily`     | Any valid font family for the labels.                                     | string   | `null`                  |\n| `label.fontSize`       | Any valid font size for the labels.                                       | string   | `'14px'`                |\n| `label.fill`           | Any valid hex color for the label color.                                  | string   | `'#fff'`                |\n| `label.format`         | Either `function(label, value)` or a format string. See below.            | mixed    | `'{l}: {f}'`            |\n| `tooltip.enabled`      | Whether tooltips should be enabled on hover.                              | bool     | `false`                 |\n| `tooltip.format`       | Either `function(label, value)` or a format string. See below.            | mixed    | `'{l}: {f}'`            |\n| `events.click.block`   | Callback `function(data)` for when a block is clicked.                    | function | `null`                  |\n\n### Label/Tooltip Format\n\nThe option `label.format` can either be a function or a string. The following\nkeys will be substituted by the string formatter:\n\n| Key     | Description                  |\n| ------- | ---------------------------- |\n| `'{l}'` | The block's supplied label.  |\n| `'{v}'` | The block's raw value.       |\n| `'{f}'` | The block's formatted value. |\n\n### Event Data\n\nBlock-based events are passed a `data` object containing the following elements:\n\n| Key             | Type   | Description                           |\n| --------------- | ------ | ------------------------------------- |\n| index           | number | The index of the block.               |\n| node            | object | The DOM node of the block.            |\n| value           | number | The numerical value.                  |\n| fill            | string | The background color.                 |\n| label.raw       | string | The unformatted label.                |\n| label.formatted | string | The result of `options.label.format`. |\n| label.color     | string | The label color.                      |\n\nExample:\n\n``` javascript\n{\n    index: 0,\n    node: { ... },\n    value: 150,\n    fill: '#c33',\n    label: {\n        raw: 'Visitors',\n        formatted: 'Visitors: 150',\n        color: '#fff',\n    },\n},\n```\n\n### Overriding Defaults\n\nYou may wish to override the default chart options. For example, you may wish\nfor every funnel to have proportional heights. To do this, simply modify the\n`D3Funnel.defaults` property:\n\n``` javascript\nD3Funnel.defaults.block.dynamicHeight = true;\n```\n\nShould you wish to override multiple properties at a time, you may consider\nusing [lodash's][lodash-merge] `_.merge` or [jQuery's][jquery-extend] `$.extend`:\n\n``` javascript\nD3Funnel.defaults = _.merge(D3Funnel.defaults, {\n    block: {\n        dynamicHeight: true,\n        fill: {\n            type: 'gradient',\n        },\n    },\n    label: {\n        format: '{l}: ${f}',\n    },\n});\n```\n\n## Advanced Data\n\nIn the examples above, both `label` and `value` were just to describe a block\nwithin the funnel. A complete listing of the available options is included\nbelow:\n\n| Option          | Type   | Description                                                     | Example       |\n| --------------- | ------ | --------------------------------------------------------------- | ------------- |\n| label           | mixed  | **Required.** The label to associate with the block.            | `'Students'`  |\n| value           | number | **Required.** The value (or count) to associate with the block. | `500`         |\n| backgroundColor | string | A row-level override for `block.fill.scale`. Hex only.          | `'#008080'`   |\n| formattedValue  | mixed  | A row-level override for `label.format`.                        | `'USD: $150'` |\n| hideLabel       | bool   | Whether to hide the formatted label for this block.             | `true`        |\n| labelColor      | string | A row-level override for `label.fill`. Hex only.                | `'#333'`      |\n\n## API\n\nAdditional methods beyond `draw()` are accessible after instantiating the chart:\n\n| Method      | Description                                     |\n| ----------- | ----------------------------------------------- |\n| `destroy()` | Removes the funnel and its events from the DOM. |\n\n# License\n\nMIT license.\n\n[d3]: http://d3js.org/\n[examples]: http://jakezatecky.github.io/d3-funnel/\n[jQuery-extend]: https://api.jquery.com/jquery.extend/\n[lodash-merge]: https://lodash.com/docs#merge\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import takiyonConfig from 'eslint-config-takiyon';\nimport globals from 'globals';\n\nimport webpackConfig from './webpack.config.test.js';\n\n// Resolve issue with HTML Webpack Bundler causing circular references\n// https://github.com/webdiscus/html-bundler-webpack-plugin/issues/186\ndelete webpackConfig.plugins;\n\nexport default [\n    ...takiyonConfig,\n    {\n        files: [\n            '**/*.{js,jsx}',\n        ],\n        ignores: ['./node_modules/**/*'],\n        settings: {\n            // Account for webpack.resolve.module imports\n            'import/resolver': {\n                webpack: {\n                    config: webpackConfig,\n                },\n            },\n        },\n    },\n    {\n        // Front-end files\n        files: [\n            'examples/**/*.js',\n            'src/**/*.js',\n        ],\n        languageOptions: {\n            globals: globals.browser,\n        },\n    },\n    {\n        // Test files\n        files: ['test/**/*.js'],\n        languageOptions: {\n            globals: {\n                ...globals.browser,\n                ...globals.mocha,\n            },\n        },\n    },\n    {\n        // Build files\n        files: ['*.js'],\n        languageOptions: {\n            globals: globals.node,\n        },\n    },\n];\n"
  },
  {
    "path": "examples/src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en-US\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <title>d3-funnel</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"description\" content=\"d3-funnel: A JavaScript SVG library for rendering funnel, pipeline, and pyramid charts using the D3.js framework.\">\n    <meta name=\"theme-color\" content=\"#3498db\">\n    <link rel=\"stylesheet\" href=\"//cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css\">\n    <link rel=\"stylesheet\" href=\"//fonts.googleapis.com/css?family=Open+Sans:400,700\">\n    <link rel=\"stylesheet\" href=\"//fonts.googleapis.com/css?family=Reem+Kufi\">\n    <link rel=\"stylesheet\" href=\"./scss/style.scss\">\n\n    <!-- Google tag (gtag.js) -->\n    <script async src=\"https://www.googletagmanager.com/gtag/js?id=G-RMNRQBDL6G\"></script>\n    <script>\n        window.dataLayer = window.dataLayer || [];\n        function gtag(){dataLayer.push(arguments);}\n        gtag('js', new Date());\n\n        gtag('config', 'G-RMNRQBDL6G');\n    </script>\n  </head>\n  <body>\n    <section class=\"page-header\">\n      <h1 class=\"project-name\">d3-funnel</h1>\n      <h2 class=\"project-tagline\">A JavaScript library for rendering funnel charts using the D3.js framework.</h2>\n      <a href=\"https://github.com/jakezatecky/d3-funnel\" class=\"btn\">View on GitHub</a>\n      <a href=\"https://github.com/jakezatecky/d3-funnel/zipball/master\" class=\"btn\">Download .zip</a>\n      <a href=\"https://github.com/jakezatecky/d3-funnel/tarball/master\" class=\"btn\">Download .tar.gz</a>\n    </section>\n\n    <section class=\"main-content\">\n      <p>\n        <strong>d3-funnel</strong> is an extensible, open-source JavaScript library for rendering\n        funnel charts using the <a href=\"https://d3js.org/\">D3.js</a> library.\n      </p>\n      <p>\n        d3-funnel is focused on providing practical and visually appealing funnels through a variety\n        of customization options. Check out the example below to get a showcasing of the several\n        possible options.\n      </p>\n      <div class=\"demo\">\n        <div class=\"demo-funnel\" id=\"funnel\"></div>\n        <div class=\"demo-options\">\n          <form>\n            <label>\n              <input type=\"checkbox\" value=\"curved\">\n              Curved\n            </label>\n            <label>\n              <input type=\"checkbox\" value=\"pinched\" checked>\n              Pinched\n            </label>\n            <label>\n              <input type=\"checkbox\" value=\"gradient\">\n              Gradient\n            </label>\n            <label>\n              <input type=\"checkbox\" value=\"inverted\">\n              Inverted\n            </label>\n            <label>\n              <input type=\"checkbox\" value=\"hover\">\n              Highlight on Hover\n            </label>\n            <label>\n              <input type=\"checkbox\" value=\"tooltip\">\n              Tooltips\n            </label>\n            <label>\n              <input type=\"checkbox\" value=\"click\">\n              Click Event\n            </label>\n            <label>\n              <input type=\"checkbox\" value=\"dynamicHeight\" checked>\n              Dynamic Height\n            </label>\n            <label>\n              <input type=\"checkbox\" value=\"barOverlay\">\n              Bar Overlay\n            </label>\n            <label>\n              <input type=\"checkbox\" value=\"animation\">\n              Load Animation\n            </label>\n            <label>\n              <input type=\"checkbox\" value=\"label\">\n              Style Labels\n            </label>\n            <label>\n              <input type=\"checkbox\" value=\"color\">\n              Custom Color\n            </label>\n          </form>\n        </div>\n      </div>\n\n      <footer class=\"site-footer\">\n        <span class=\"site-footer-owner\">\n          <a href=\"https://github.com/jakezatecky/d3-funnel\">d3-funnel</a> is maintained by <a href=\"https://github.com/jakezatecky\">jakezatecky</a>.\n        </span>\n        <span class=\"site-footer-credits\">\n          This page is hosted by <a href=\"https://pages.github.com\">GitHub Pages</a>.\n        </span>\n      </footer>\n    </section>\n\n    <script src=\"./js/index.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/src/js/index.js",
    "content": "import { merge } from 'lodash';\nimport D3Funnel from 'd3-funnel';\n\nconst settings = {\n    curved: {\n        chart: {\n            curve: {\n                enabled: true,\n            },\n        },\n    },\n    pinched: {\n        chart: {\n            bottomPinch: 1,\n        },\n    },\n    gradient: {\n        block: {\n            fill: {\n                type: 'gradient',\n            },\n        },\n    },\n    inverted: {\n        chart: {\n            inverted: true,\n        },\n    },\n    hover: {\n        block: {\n            highlight: true,\n        },\n    },\n    tooltip: {\n        tooltip: {\n            enabled: true,\n        },\n    },\n    click: {\n        events: {\n            click: {\n                block(event, d) {\n                    // eslint-disable-next-line no-alert\n                    alert(d.label.raw);\n                },\n            },\n        },\n    },\n    dynamicHeight: {\n        block: {\n            dynamicHeight: true,\n        },\n    },\n    barOverlay: {\n        block: {\n            barOverlay: true,\n        },\n    },\n    animation: {\n        chart: {\n            animate: 200,\n        },\n    },\n    label: {\n        label: {\n            fontFamily: '\"Reem Kufi\", sans-serif',\n            fontSize: '16px',\n        },\n    },\n};\n\nconst chart = new D3Funnel('#funnel');\nconst checkboxes = [...document.querySelectorAll('input')];\nconst color = document.querySelector('[value=\"color\"]');\n\nfunction onChange() {\n    let data = !color.checked ?\n        [\n            { label: 'Applicants', value: 12000 },\n            { label: 'Pre-screened', value: 4000 },\n            { label: 'Interviewed', value: 2500 },\n            { label: 'Hired', value: 1500 },\n        ] :\n        [\n            { label: 'Teal', value: 12000, backgroundColor: '#008080' },\n            { label: 'Byzantium', value: 4000, backgroundColor: '#702963' },\n            { label: 'Persimmon', value: 2500, backgroundColor: '#ff634d' },\n            { label: 'Azure', value: 1500, backgroundColor: '#007fff' },\n        ];\n\n    let options = {\n        chart: {\n            bottomWidth: 3 / 8,\n        },\n        block: {\n            minHeight: 25,\n        },\n        label: {\n            format: '{l}\\n{f}',\n        },\n    };\n\n    checkboxes.forEach((checkbox) => {\n        if (checkbox.checked) {\n            options = merge(options, settings[checkbox.value]);\n        }\n    });\n\n    // Reverse data for inversion\n    if (options.chart.inverted) {\n        options.chart.bottomWidth = 1 / 3;\n        data = data.reverse();\n    }\n\n    chart.draw(data, options);\n}\n\n// Bind event listeners\ncheckboxes.forEach((checkbox) => {\n    checkbox.addEventListener('change', onChange);\n});\n\n// Trigger change event for initial render\ncheckboxes[0].dispatchEvent(new CustomEvent('change'));\n"
  },
  {
    "path": "examples/src/scss/_cayman.scss",
    "content": "// Breakpoints\n$large-breakpoint: 64em;\n$medium-breakpoint: 42em;\n\n// Headers\n$header-heading-color: #fff !default;\n$header-bg-color: #159957 !default;\n$header-bg-color-secondary: #155799 !default;\n\n// Text\n$font-family: // stylelint-disable-line scss/dollar-variable-colon-space-after\n  \"Open Sans\", \"Helvetica Neue\", \"Helvetica\", \"Arial\", sans-serif !default;\n$section-headings-color: #159957 !default;\n$body-text-color: #606c71 !default;\n$body-link-color: #1e6bb8 !default;\n$blockquote-text-color: #819198 !default;\n\n// Code\n$font-family-code: // stylelint-disable-line scss/dollar-variable-colon-space-after\n  \"Consolas\", \"Liberation Mono\", \"Menlo\", \"Courier\", monospace !default;\n$code-bg-color: #f3f6fa !default;\n$code-text-color: #567482 !default;\n\n// Borders\n$border-color: #dce6f0 !default;\n$table-border-color: #e9ebec !default;\n$hr-border-color: #eff0f1 !default;\n\n@mixin large {\n  @media screen and (min-width: #{$large-breakpoint}) {\n    @content;\n  }\n}\n\n@mixin medium {\n  @media screen and (min-width: #{$medium-breakpoint}) and (max-width: #{$large-breakpoint}) {\n    @content;\n  }\n}\n\n@mixin small {\n  @media screen and (max-width: #{$medium-breakpoint}) {\n    @content;\n  }\n}\n\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  padding: 0;\n  margin: 0;\n  font-family: $font-family;\n  font-size: 16px;\n  line-height: 1.5;\n  color: $body-text-color;\n}\n\na {\n  color: $body-link-color;\n  text-decoration: none;\n\n  &:hover {\n    text-decoration: underline;\n  }\n}\n\n.btn {\n  display: inline-block;\n  margin-bottom: 1rem;\n  color: rgba(255, 255, 255, 0.7);\n  background-color: rgba(255, 255, 255, 0.08);\n  border-color: rgba(255, 255, 255, 0.2);\n  border-style: solid;\n  border-width: 1px;\n  border-radius: 0.3rem;\n  transition:\n    color 0.2s,\n    background-color 0.2s,\n    border-color 0.2s;\n\n  @include large {\n    padding: 0.75rem 1rem;\n\n    + .btn {\n      margin-left: 1rem;\n    }\n  }\n\n  @include medium {\n    padding: 0.6rem 0.9rem;\n    font-size: 0.9rem;\n\n    + .btn {\n      margin-left: 1rem;\n    }\n  }\n\n  @include small {\n    display: block;\n    width: 100%;\n    padding: 0.75rem;\n    font-size: 0.9rem;\n\n    + .btn {\n      margin-top: 1rem;\n      margin-left: 0;\n    }\n  }\n\n  &:hover {\n    color: rgba(255, 255, 255, 0.8);\n    text-decoration: none;\n    background-color: rgba(255, 255, 255, 0.2);\n    border-color: rgba(255, 255, 255, 0.3);\n  }\n}\n\n.page-header {\n  color: $header-heading-color;\n  text-align: center;\n  background-color: $header-bg-color;\n  background-image: linear-gradient(\n    120deg,\n    $header-bg-color-secondary,\n    $header-bg-color\n  );\n\n  @include large {\n    padding: 3rem 4rem;\n  }\n\n  @include medium {\n    padding: 3rem 4rem;\n  }\n\n  @include small {\n    padding: 2rem 1rem;\n  }\n}\n\n.project-name {\n  margin-top: 0;\n  margin-bottom: 0.1rem;\n\n  @include large {\n    font-size: 3.25rem;\n  }\n\n  @include medium {\n    font-size: 2.25rem;\n  }\n\n  @include small {\n    font-size: 1.75rem;\n  }\n}\n\n.project-tagline {\n  margin-bottom: 2rem;\n  font-weight: normal;\n  opacity: 0.7;\n\n  @include large {\n    font-size: 1.25rem;\n  }\n\n  @include medium {\n    font-size: 1.15rem;\n  }\n\n  @include small {\n    font-size: 1rem;\n  }\n}\n\n.main-content {\n  word-wrap: break-word;\n\n  @include large {\n    max-width: 64rem;\n    padding: 2rem 6rem;\n    margin: 0 auto;\n    font-size: 1.1rem;\n  }\n\n  @include medium {\n    padding: 2rem 4rem;\n    font-size: 1.1rem;\n  }\n\n  @include small {\n    padding: 2rem 1rem;\n    font-size: 1rem;\n  }\n\n  :first-child {\n    margin-top: 0;\n  }\n\n  img {\n    max-width: 100%;\n  }\n\n  h1,\n  h2,\n  h3,\n  h4,\n  h5,\n  h6 {\n    margin-top: 2rem;\n    margin-bottom: 1rem;\n    font-weight: normal;\n    color: $section-headings-color;\n  }\n\n  p {\n    margin-bottom: 1em;\n  }\n\n  code {\n    padding: 2px 4px;\n    font-family: $font-family-code;\n    font-size: 0.9rem;\n    color: $code-text-color;\n    background-color: $code-bg-color;\n    border-radius: 0.3rem;\n  }\n\n  pre {\n    padding: 0.8rem;\n    margin-top: 0;\n    margin-bottom: 1rem;\n    font: 1rem $font-family-code;\n    color: $code-text-color;\n    word-wrap: normal;\n    background-color: $code-bg-color;\n    border: solid 1px $border-color;\n    border-radius: 0.3rem;\n\n    > code {\n      padding: 0;\n      margin: 0;\n      font-size: 0.9rem;\n      color: $code-text-color;\n      word-break: normal;\n      white-space: pre;\n      background: transparent;\n      border: 0;\n    }\n  }\n\n  .highlight {\n    margin-bottom: 1rem;\n\n    pre {\n      margin-bottom: 0;\n      word-break: normal;\n    }\n  }\n\n  .highlight pre,\n  pre {\n    padding: 0.8rem;\n    overflow: auto;\n    font-size: 0.9rem;\n    line-height: 1.45;\n    border-radius: 0.3rem;\n    -webkit-overflow-scrolling: touch;\n  }\n\n  pre code,\n  pre tt {\n    display: inline;\n    max-width: initial;\n    padding: 0;\n    margin: 0;\n    overflow: initial;\n    line-height: inherit;\n    word-wrap: normal;\n    background-color: transparent;\n    border: 0;\n\n    &::before,\n    &::after {\n      content: normal;\n    }\n  }\n\n  ul,\n  ol {\n    margin-top: 0;\n  }\n\n  blockquote {\n    padding: 0 1rem;\n    margin-left: 0;\n    color: $blockquote-text-color;\n    border-left: 0.3rem solid $border-color;\n\n    > :first-child {\n      margin-top: 0;\n    }\n\n    > :last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  table {\n    display: block;\n    width: 100%;\n    overflow: auto;\n    word-break: normal;\n\n    th {\n      font-weight: bold;\n    }\n\n    th,\n    td {\n      padding: 0.5rem 1rem;\n      border: 1px solid $table-border-color;\n    }\n  }\n\n  dl {\n    padding: 0;\n\n    dt {\n      padding: 0;\n      margin-top: 1rem;\n      font-size: 1rem;\n      font-weight: bold;\n    }\n\n    dd {\n      padding: 0;\n      margin-bottom: 1rem;\n    }\n  }\n\n  hr {\n    height: 2px;\n    padding: 0;\n    margin: 1rem 0;\n    background-color: $hr-border-color;\n    border: 0;\n  }\n}\n\n.site-footer {\n  padding-top: 2rem;\n  margin-top: 2rem;\n  border-top: solid 1px $hr-border-color;\n\n  @include large {\n    font-size: 1rem;\n  }\n\n  @include medium {\n    font-size: 1rem;\n  }\n\n  @include small {\n    font-size: 0.9rem;\n  }\n}\n\n.site-footer-owner {\n  display: block;\n  font-weight: bold;\n}\n\n.site-footer-credits {\n  color: $blockquote-text-color;\n}\n"
  },
  {
    "path": "examples/src/scss/style.scss",
    "content": "@use \"cayman\" with (\n  $body-text-color: #444,\n  $code-text-color: #465e6a,\n  $header-bg-color: #3498db,\n  $header-bg-color-secondary: #2c3e50,\n  $section-headings-color: #2079b5,\n  $blockquote-text-color: #576266\n);\n\n.main-content {\n  h1,\n  h2,\n  h3 {\n    font-weight: bold;\n  }\n}\n\n.demo {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 10px 30px;\n\n  form {\n    label {\n      display: flex;\n      font-weight: normal;\n    }\n\n    input {\n      margin-right: 0.5rem;\n    }\n  }\n}\n\n.demo-funnel {\n  width: 320px;\n  height: 400px;\n  margin-right: 50px;\n}\n"
  },
  {
    "path": "gh-deploy.sh",
    "content": "#!/bin/bash\nVERSION=\"$(cat ./package.json | python -c \"import sys, json; print(json.load(sys.stdin)['version'])\")\"\ngit diff --exit-code\n\nif [[ $? == 0 ]]\nthen\n    sed -i '\\:/examples/dist:d' ./.gitignore\n    git add .\n    git commit -m \"Publish v${VERSION} examples\"\n    git push origin `git subtree split --prefix examples/dist master`:gh-pages --force\n    git reset HEAD~\n    git checkout .gitignore\nelse\n    echo \"Need clean working directory to publish\"\nfi\n"
  },
  {
    "path": "index.d.ts",
    "content": "declare module 'd3-funnel'\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"d3-funnel\",\n  \"version\": \"2.1.3\",\n  \"description\": \"A library for rendering SVG funnel charts using D3.js\",\n  \"author\": \"Jake Zatecky\",\n  \"license\": \"MIT\",\n  \"keywords\": [\n    \"d3\",\n    \"funnel\",\n    \"pyramid\",\n    \"svg\",\n    \"chart\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/jakezatecky/d3-funnel\"\n  },\n  \"bugs\": \"https://github.com/jakezatecky/d3-funnel/issues\",\n  \"type\": \"module\",\n  \"main\": \"dist/index.cjs.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"imports\": {\n    \"#js/*\": \"./src/d3-funnel/*\"\n  },\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/index.esm.js\",\n      \"require\": \"./dist/index.cjs.js\"\n    },\n    \"./*\": \"./*\"\n  },\n  \"browser\": \"dist/d3-funnel.min.js\",\n  \"scripts\": {\n    \"build\": \"npm run build:browser && npm run build:esm && npm run build:umd\",\n    \"build:browser\": \"webpack --env=target=browser\",\n    \"build:esm\": \"webpack --env=target=esm\",\n    \"build:umd\": \"webpack --env=target=umd\",\n    \"build:test\": \"webpack --config=webpack.config.test.js\",\n    \"examples\": \"webpack serve --config=webpack.config.examples.js\",\n    \"format:style\": \"prettier --write examples/src/scss/**/*.scss\",\n    \"gh-build\": \"webpack --config=webpack.config.examples.js --mode=production\",\n    \"gh-deploy\": \"npm run gh-build && bash ./gh-deploy.sh\",\n    \"lint\": \"npm run lint:script && npm run lint:style\",\n    \"lint:script\": \"eslint src/**/*.js examples/src/**/*.js test/*.js ./test/d3-funnel/**/*.js *.js\",\n    \"lint:style\": \"stylelint examples/src/scss/**/*.scss\",\n    \"prepublishOnly\": \"npm run release\",\n    \"release\": \"npm run test && npm run build\",\n    \"test\": \"npm run lint && npm run test:script && npm run test:style-format\",\n    \"test:script\": \"npm run build:test && node test/test.js\",\n    \"test:style-format\": \"prettier --check examples/src/scss/**/*.scss\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.0.0\",\n    \"@babel/eslint-parser\": \"^7.13.14\",\n    \"@babel/preset-env\": \"^7.0.0\",\n    \"@babel/register\": \"^7.0.0\",\n    \"babel-loader\": \"^10.0.0\",\n    \"browser-sync\": \"^3.0.3\",\n    \"chai\": \"^6.0.1\",\n    \"css-loader\": \"^7.0.0\",\n    \"d3\": \"^7.0.0\",\n    \"eslint\": \"^10.1.0\",\n    \"eslint-config-takiyon\": \"^4.0.0\",\n    \"eslint-import-resolver-webpack\": \"^0.13.0\",\n    \"eslint-plugin-import\": \"^2.7.0\",\n    \"global-jsdom\": \"^27.0.0\",\n    \"globals\": \"^16.3.0\",\n    \"html-bundler-webpack-plugin\": \"^4.21.1\",\n    \"jsdom\": \"^27.0.0\",\n    \"lodash\": \"^4.17.21\",\n    \"mocha\": \"^11.7.2\",\n    \"playwright\": \"^1.5.2\",\n    \"prettier\": \"^3.1.1\",\n    \"process\": \"^0.11.10\",\n    \"sass\": \"^1.69.5\",\n    \"sass-loader\": \"^16.0.5\",\n    \"sinon\": \"^21.0.0\",\n    \"stylelint\": \"^16.0.2\",\n    \"stylelint-config-takiyon\": \"^6.0.0\",\n    \"util\": \"^0.12.4\",\n    \"webpack\": \"^5.3.2\",\n    \"webpack-cli\": \"^6.0.1\",\n    \"webpack-dev-server\": \"^5.0.0\"\n  },\n  \"dependencies\": {\n    \"d3-array\": \"^3.0.1\",\n    \"d3-ease\": \"^3.0.1\",\n    \"d3-scale\": \"^4.0.0\",\n    \"d3-scale-chromatic\": \"^3.0.0\",\n    \"d3-selection\": \"^3.0.0\",\n    \"d3-transition\": \"^3.0.0\",\n    \"nanoid\": \"^5.0.2\"\n  }\n}\n"
  },
  {
    "path": "src/d3-funnel/Colorizer.js",
    "content": "class Colorizer {\n    /**\n     * @return {void}\n     */\n    constructor() {\n        this.hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;\n        this.instanceId = null;\n        this.labelFill = null;\n        this.scale = null;\n    }\n\n    /**\n     * @param {string} instanceId\n     *\n     * @return {void}\n     */\n    setInstanceId(instanceId) {\n        this.instanceId = instanceId;\n    }\n\n    /**\n     * @param {string} fill\n     *\n     * @return {void}\n     */\n    setLabelFill(fill) {\n        this.labelFill = fill;\n    }\n\n    /**\n     * @param {function|Array} scale\n     *\n     * @return {void}\n     */\n    setScale(scale) {\n        this.scale = scale;\n    }\n\n    /**\n     * Given a raw data block, return an appropriate color for the block.\n     *\n     * @param {string} fill\n     * @param {Number} index\n     * @param {string} fillType\n     *\n     * @return {Object}\n     */\n    getBlockFill(fill, index, fillType) {\n        const raw = this.getBlockRawFill(fill, index);\n\n        return {\n            raw,\n            actual: this.getBlockActualFill(raw, index, fillType),\n        };\n    }\n\n    /**\n     * Return the raw hex color for the block.\n     *\n     * @param {string} fill\n     * @param {Number} index\n     *\n     * @return {string}\n     */\n    getBlockRawFill(fill, index) {\n        // Use the block's color, if set and valid\n        if (this.hexExpression.test(fill)) {\n            return fill;\n        }\n\n        // Otherwise, attempt to use the array scale\n        if (Array.isArray(this.scale)) {\n            return this.scale[index];\n        }\n\n        // Finally, use a functional scale\n        return this.scale(index);\n    }\n\n    /**\n     * Return the actual background for the block.\n     *\n     * @param {string} raw\n     * @param {Number} index\n     * @param {string} fillType\n     *\n     * @return {string}\n     */\n    getBlockActualFill(raw, index, fillType) {\n        if (fillType === 'solid') {\n            return raw;\n        }\n\n        return `url(#${this.getGradientId(index)})`;\n    }\n\n    /**\n     * Return the gradient ID for the given index.\n     *\n     * @param {Number} index\n     *\n     * @return {string}\n     */\n    getGradientId(index) {\n        return `${this.instanceId}-gradient-${index}`;\n    }\n\n    /**\n     * Given a raw data block, return an appropriate label color.\n     *\n     * @param {string} labelFill\n     *\n     * @return {string}\n     */\n    getLabelColor(labelFill) {\n        return this.hexExpression.test(labelFill) ? labelFill : this.labelFill;\n    }\n\n    /**\n     * Shade a color to the given percentage.\n     *\n     * @param {string} color A hex color.\n     * @param {number} shade The shade adjustment. Can be positive or negative.\n     *\n     * @return {string}\n     */\n    shade(color, shade) {\n        const { R, G, B } = this.hexToRgb(color);\n        const t = shade < 0 ? 0 : 255;\n        const p = shade < 0 ? shade * -1 : shade;\n\n        const converted = 0x1000000 +\n            ((Math.round((t - R) * p) + R) * 0x10000) +\n            ((Math.round((t - G) * p) + G) * 0x100) +\n            (Math.round((t - B) * p) + B);\n\n        return `#${converted.toString(16).slice(1)}`;\n    }\n\n    /**\n     * Convert a hex color to an RGB object.\n     *\n     * @param {string} color\n     *\n     * @returns {{R: Number, G: number, B: number}}\n     */\n    hexToRgb(color) {\n        let hex = color.slice(1);\n\n        if (hex.length === 3) {\n            hex = this.expandHex(hex);\n        }\n\n        const f = parseInt(hex, 16);\n\n        /* eslint-disable no-bitwise */\n        const R = f >> 16;\n        const G = (f >> 8) & 0x00FF;\n        const B = f & 0x0000FF;\n        /* eslint-enable */\n\n        return { R, G, B };\n    }\n\n    /**\n     * Expands a three character hex code to six characters.\n     *\n     * @param {string} hex\n     *\n     * @return {string}\n     */\n    expandHex(hex) {\n        return hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];\n    }\n}\n\nexport default Colorizer;\n"
  },
  {
    "path": "src/d3-funnel/D3Funnel.js",
    "content": "import { easeLinear } from 'd3-ease';\nimport { range } from 'd3-array';\nimport { scaleOrdinal } from 'd3-scale';\nimport { schemeCategory10 } from 'd3-scale-chromatic';\nimport { select } from 'd3-selection';\nimport 'd3-transition';\nimport { nanoid } from 'nanoid';\n\nimport Colorizer from '#js/Colorizer.js';\nimport Formatter from '#js/Formatter.js';\nimport Navigator from '#js/Navigator.js';\nimport Utils from '#js/Utils.js';\n\nclass D3Funnel {\n    static defaults = {\n        chart: {\n            width: 350,\n            height: 400,\n            bottomWidth: 1 / 3,\n            bottomPinch: 0,\n            inverted: false,\n            horizontal: false,\n            animate: 0,\n            curve: {\n                enabled: false,\n                height: 20,\n                shade: -0.4,\n            },\n            totalCount: null,\n        },\n        block: {\n            dynamicHeight: false,\n            dynamicSlope: false,\n            barOverlay: false,\n            fill: {\n                scale: scaleOrdinal(schemeCategory10).domain(range(0, 10)),\n                type: 'solid',\n            },\n            minHeight: 0,\n            highlight: false,\n        },\n        label: {\n            enabled: true,\n            fontFamily: null,\n            fontSize: '14px',\n            fill: '#fff',\n            format: '{l}: {f}',\n        },\n        tooltip: {\n            enabled: false,\n            format: '{l}: {f}',\n        },\n        events: {\n            click: {\n                block: null,\n            },\n        },\n    };\n\n    /**\n     * @param {string|HTMLElement} selector A selector for the container element.\n     *\n     * @return {void}\n     */\n    constructor(selector) {\n        this.container = select(selector).node();\n\n        this.colorizer = new Colorizer();\n        this.formatter = new Formatter();\n        this.navigator = new Navigator();\n\n        this.id = null;\n\n        // Bind event handlers\n        this.onMouseOver = this.onMouseOver.bind(this);\n        this.onMouseOut = this.onMouseOut.bind(this);\n    }\n\n    /**\n     * Remove the funnel and its events from the DOM.\n     *\n     * @return {void}\n     */\n    destroy() {\n        const container = select(this.container);\n\n        // D3's remove method appears to be sufficient for removing the events\n        container.selectAll('svg').remove();\n\n        // Remove other elements from container\n        container.selectAll('*').remove();\n\n        // Remove inner text from container\n        container.text('');\n    }\n\n    /**\n     * Draw the chart inside the container with the data and configuration\n     * specified. This will remove any previous SVG elements in the container\n     * and draw a new funnel chart on top of it.\n     *\n     * @param {Array}  data    A list of rows containing a category, a count,\n     *                         and optionally a color (in hex).\n     * @param {Object} options An optional configuration object to override\n     *                         defaults. See the docs.\n     *\n     * @return {void}\n     */\n    draw(data, options = {}) {\n        this.destroy();\n\n        this.initialize(data, options);\n\n        this.drawOntoDom();\n    }\n\n    /**\n     * Initialize and calculate important variables for drawing the chart.\n     *\n     * @param {Array}  data\n     * @param {Object} options\n     *\n     * @return {void}\n     */\n    initialize(data, options) {\n        this.validateData(data);\n\n        const settings = this.getSettings(options);\n\n        this.id = `d3-funnel-${nanoid()}`;\n\n        // Set labels\n        this.labelFormatter = this.formatter.getFormatter(settings.label.format);\n        this.tooltipFormatter = this.formatter.getFormatter(settings.tooltip.format);\n\n        // Set color scales\n        this.colorizer.setInstanceId(this.id);\n        this.colorizer.setLabelFill(settings.label.fill);\n        this.colorizer.setScale(settings.block.fill.scale);\n\n        // Initialize funnel chart settings\n        this.settings = {\n            width: settings.chart.width,\n            height: settings.chart.height,\n            bottomWidth: settings.chart.width * settings.chart.bottomWidth,\n            bottomPinch: settings.chart.bottomPinch,\n            isInverted: settings.chart.inverted,\n            isCurved: settings.chart.curve.enabled,\n            curveHeight: settings.chart.curve.height,\n            curveShade: settings.chart.curve.shade,\n            addValueOverlay: settings.block.barOverlay,\n            animation: settings.chart.animate,\n            totalCount: settings.chart.totalCount,\n            fillType: settings.block.fill.type,\n            hoverEffects: settings.block.highlight,\n            dynamicHeight: settings.block.dynamicHeight,\n            dynamicSlope: settings.block.dynamicSlope,\n            minHeight: settings.block.minHeight,\n            label: settings.label,\n            tooltip: settings.tooltip,\n            onBlockClick: settings.events.click.block,\n        };\n\n        this.setBlocks(data);\n    }\n\n    /**\n     * @param {Array} data\n     *\n     * @return void\n     */\n    validateData(data) {\n        if (Array.isArray(data) === false) {\n            throw new Error('Data must be an array.');\n        }\n\n        if (data.length === 0) {\n            throw new Error('Data array must contain at least one element.');\n        }\n\n        if (typeof data[0] !== 'object') {\n            throw new Error('Data array elements must be an object.');\n        }\n\n        if (\n            (Array.isArray(data[0]) && data[0].length < 2) ||\n            (Array.isArray(data[0]) === false && (\n                data[0].label === undefined || data[0].value === undefined\n            ))\n        ) {\n            throw new Error('Data array elements must contain a label and value.');\n        }\n    }\n\n    /**\n     * @param {Object} options\n     *\n     * @return {Object}\n     */\n    getSettings(options) {\n        const containerDimensions = this.getContainerDimensions();\n        const defaults = this.getDefaultSettings(containerDimensions);\n\n        // Prepare the configuration settings based on the defaults\n        let settings = Utils.extend({}, defaults);\n\n        // Override default settings with user options\n        settings = Utils.extend(settings, options);\n\n        // Account for any percentage-based dimensions\n        settings.chart = {\n            ...settings.chart,\n            ...this.castDimensions(settings, containerDimensions),\n        };\n\n        return settings;\n    }\n\n    /**\n     * Return default settings.\n     *\n     * @param {Object} containerDimensions\n     *\n     * @return {Object}\n     */\n    getDefaultSettings(containerDimensions) {\n        const settings = D3Funnel.defaults;\n\n        // Set the default width and height based on the container\n        settings.chart = {\n            ...settings.chart,\n            ...containerDimensions,\n        };\n\n        return settings;\n    }\n\n    /**\n     * Get the width/height dimensions of the container.\n     *\n     * @return {{width: Number, height: Number}}\n     */\n    getContainerDimensions() {\n        const dimensions = {\n            width: parseFloat(select(this.container).style('width')),\n            height: parseFloat(select(this.container).style('height')),\n        };\n\n        // Remove container dimensions that resolve to zero\n        ['width', 'height'].forEach((direction) => {\n            if (dimensions[direction] === 0) {\n                delete dimensions[direction];\n            }\n        });\n\n        return dimensions;\n    }\n\n    /**\n     * Cast dimensions into tangible or meaningful numbers.\n     *\n     * @param {Object} chart\n     * @param {Object} containerDimensions\n     *\n     * @return {{width: Number, height: Number}}\n     */\n    castDimensions({ chart }, containerDimensions) {\n        const dimensions = {};\n\n        Object.keys(containerDimensions).forEach((direction) => {\n            const chartDimension = chart[direction];\n            const containerDimension = containerDimensions[direction];\n\n            if (/%$/.test(String(chartDimension))) {\n                // Convert string into a percentage of the container\n                dimensions[direction] = (parseFloat(chartDimension) / 100) * containerDimension;\n            } else if (chartDimension <= 0) {\n                // If case of non-positive number, set to a usable number\n                dimensions[direction] = D3Funnel.defaults.chart[direction];\n            } else {\n                dimensions[direction] = chartDimension;\n            }\n        });\n\n        return dimensions;\n    }\n\n    /**\n     * Register the raw data into a standard block format and pre-calculate\n     * some values.\n     *\n     * @param {Array} data\n     *\n     * @return void\n     */\n    setBlocks(data) {\n        const totalCount = this.getTotalCount(data);\n\n        this.blocks = this.standardizeData(data, totalCount);\n    }\n\n    /**\n     * Return the total count of all blocks.\n     *\n     * @param {Array} data\n     *\n     * @return {Number}\n     */\n    getTotalCount(data) {\n        if (this.settings.totalCount !== null) {\n            return this.settings.totalCount || 0;\n        }\n\n        return data.reduce((a, b) => a + Utils.getRawBlockCount(b), 0);\n    }\n\n    /**\n     * Convert the raw data into a standardized format.\n     *\n     * @param {Array}  data\n     * @param {Number} totalCount\n     *\n     * @return {Array}\n     */\n    standardizeData(data, totalCount) {\n        return data.map((rawBlock, index) => {\n            const block = Array.isArray(rawBlock) ? Utils.convertLegacyBlock(rawBlock) : rawBlock;\n            const ratio = totalCount > 0 ? (block.value / totalCount || 0) : 1 / data.length;\n\n            return {\n                index,\n                ratio,\n                value: block.value,\n                height: this.settings.height * ratio,\n                fill: this.colorizer.getBlockFill(\n                    block.backgroundColor,\n                    index,\n                    this.settings.fillType,\n                ),\n                label: {\n                    enabled: !block.hideLabel,\n                    raw: block.label,\n                    formatted: this.formatter.format(block, this.labelFormatter),\n                    color: this.colorizer.getLabelColor(block.labelColor),\n                },\n                tooltip: {\n                    enabled: block.enabled,\n                    formatted: this.formatter.format(block, this.tooltipFormatter),\n                },\n            };\n        });\n    }\n\n    /**\n     * Draw the chart onto the DOM.\n     *\n     * @return {void}\n     */\n    drawOntoDom() {\n        // Add the SVG\n        this.svg = select(this.container)\n            .append('svg')\n            .attr('id', this.id)\n            .attr('width', this.settings.width)\n            .attr('height', this.settings.height);\n\n        [this.blockPaths, this.overlayPaths] = this.makePaths();\n\n        // Define color gradients\n        if (this.settings.fillType === 'gradient') {\n            this.defineColorGradients(this.svg);\n        }\n\n        // Add top oval if curved\n        if (this.settings.isCurved) {\n            this.drawTopOval(this.svg, this.blockPaths);\n        }\n\n        // Add each block\n        this.drawBlock(0);\n    }\n\n    /**\n     * Create the paths to be used to define the discrete funnel blocks and\n     * returns the results in an array.\n     *\n     * @return {Array, Array}\n     */\n    makePaths() {\n        // Calculate the important fixed positions\n        const bottomLeftX = (this.settings.width - this.settings.bottomWidth) / 2;\n        const centerX = this.settings.width / 2;\n\n        let paths = [];\n        let overlayPaths = [];\n\n        // Calculate change in x, y direction\n        this.dx = this.getDx(bottomLeftX);\n        this.dy = this.getDy();\n\n        // Initialize velocity\n        let { dx, dy } = this;\n\n        // Initialize starting positions\n        let prevLeftX = 0;\n        let prevRightX = this.settings.width;\n        let prevHeight = 0;\n\n        // Start from the bottom for inverted\n        if (this.settings.isInverted) {\n            prevLeftX = bottomLeftX;\n            prevRightX = this.settings.width - bottomLeftX;\n        }\n\n        // Initialize next positions\n        let nextLeftX = 0;\n        let nextRightX = 0;\n        let nextHeight = 0;\n\n        // Move down if there is an initial curve\n        if (this.settings.isCurved) {\n            prevHeight = this.settings.curveHeight / 2;\n        }\n\n        let totalHeight = this.settings.height;\n\n        // This is greedy in that the block will have a guaranteed height\n        // and the remaining is shared among the ratio, instead of being\n        // shared according to the remaining minus the guaranteed\n        if (this.settings.minHeight !== 0) {\n            totalHeight = this.settings.height - (this.settings.minHeight * this.blocks.length);\n        }\n\n        let slopeHeight = this.settings.height;\n\n        // Correct slope height if there are blocks being pinched (and thus\n        // requiring a sharper curve)\n        if (this.settings.bottomPinch > 0) {\n            this.blocks.forEach((block, i) => {\n                let height = (totalHeight * block.ratio);\n\n                // Add greedy minimum height\n                if (this.settings.minHeight !== 0) {\n                    height += this.settings.minHeight;\n                }\n\n                // Account for any curvature\n                if (this.settings.isCurved) {\n                    height += this.settings.curveHeight / this.blocks.length;\n                }\n\n                if (this.settings.isInverted) {\n                    if (i < this.settings.bottomPinch) {\n                        slopeHeight -= height;\n                    }\n                } else if (i >= this.blocks.length - this.settings.bottomPinch) {\n                    slopeHeight -= height;\n                }\n            });\n        }\n\n        // The slope will determine the x points on each block iteration\n        // Given: slope = (y1 - y2) / (x1 - x2)\n        // (x1, y1) = (bottomLeftX, height)\n        // (x2, y2) = (0, 0)\n        const slope = slopeHeight / bottomLeftX;\n\n        // Create the path definition for each funnel block\n        // Remember to loop back to the beginning point for a closed path\n        this.blocks.forEach((block, i) => {\n            // Make heights proportional to block weight\n            if (this.settings.dynamicHeight) {\n                // Slice off the height proportional to this block\n                dy = totalHeight * block.ratio;\n\n                // Add greedy minimum height\n                if (this.settings.minHeight !== 0) {\n                    dy += this.settings.minHeight;\n                }\n\n                // Account for any curvature\n                if (this.settings.isCurved) {\n                    dy -= this.settings.curveHeight / this.blocks.length;\n                }\n\n                // Given: y = mx + b\n                // Given: b = 0 (when funnel), b = this.settings.height (when pyramid)\n                // For funnel, x_i = y_i / slope\n                nextLeftX = (prevHeight + dy) / slope;\n\n                // For pyramid, x_i = y_i - this.settings.height / -slope\n                if (this.settings.isInverted) {\n                    nextLeftX = ((prevHeight + dy) - this.settings.height) / (-1 * slope);\n                }\n\n                // If bottomWidth is 0, adjust last x position (to circumvent\n                // errors associated with rounding)\n                if (this.settings.bottomWidth === 0 && i === this.blocks.length - 1) {\n                    // For funnel, last position is the center\n                    nextLeftX = this.settings.width / 2;\n\n                    // For pyramid, last position is the origin\n                    if (this.settings.isInverted) {\n                        nextLeftX = 0;\n                    }\n                }\n\n                // If bottomWidth is same as width, stop x velocity\n                if (this.settings.bottomWidth === this.settings.width) {\n                    nextLeftX = prevLeftX;\n                }\n\n                // Prevent NaN or Infinite values (caused by zero heights)\n                if (Number.isNaN(nextLeftX) || !Number.isFinite(nextLeftX)) {\n                    nextLeftX = 0;\n                }\n\n                // Calculate the shift necessary for both x points\n                dx = nextLeftX - prevLeftX;\n\n                if (this.settings.isInverted) {\n                    dx = prevLeftX - nextLeftX;\n                }\n            }\n\n            // Make slope width proportional to change in block value\n            if (this.settings.dynamicSlope && !this.settings.isInverted) {\n                const nextBlockValue = this.blocks[i + 1] ?\n                    this.blocks[i + 1].value :\n                    block.value;\n\n                const widthRatio = nextBlockValue / block.value;\n                dx = (1 - widthRatio) * (centerX - prevLeftX);\n            }\n\n            // Stop velocity for pinched blocks\n            if (this.settings.bottomPinch > 0) {\n                // Check if we've reached the bottom of the pinch\n                // If so, stop changing on x\n                if (!this.settings.isInverted) {\n                    if (i >= this.blocks.length - this.settings.bottomPinch) {\n                        dx = 0;\n                    }\n                    // Pinch at the first blocks relating to the bottom pinch\n                    // Revert back to normal velocity after pinch\n                } else {\n                    // Revert velocity back to the initial if we are using\n                    // static heights (prevents zero velocity if isInverted\n                    // and bottomPinch are non-trivial and dynamicHeight is\n                    // false)\n                    if (!this.settings.dynamicHeight) {\n                        ({ dx } = this);\n                    }\n\n                    dx = i < this.settings.bottomPinch ? 0 : dx;\n                }\n            }\n\n            // Calculate the position of next block\n            nextLeftX = prevLeftX + dx;\n            nextRightX = prevRightX - dx;\n            nextHeight = prevHeight + dy;\n\n            this.blocks[i].height = dy;\n\n            // Expand outward if inverted\n            if (this.settings.isInverted) {\n                nextLeftX = prevLeftX - dx;\n                nextRightX = prevRightX + dx;\n            }\n\n            const dimensions = {\n                centerX,\n                prevLeftX,\n                prevRightX,\n                prevHeight,\n                nextLeftX,\n                nextRightX,\n                nextHeight,\n                curveHeight: this.settings.curveHeight,\n                ratio: block.ratio,\n            };\n\n            if (this.settings.isCurved) {\n                paths = [...paths, this.navigator.makeCurvedPaths(dimensions)];\n\n                if (this.settings.addValueOverlay) {\n                    overlayPaths = [\n                        ...overlayPaths,\n                        this.navigator.makeCurvedPaths(dimensions, true),\n                    ];\n                }\n            } else {\n                paths = [...paths, this.navigator.makeStraightPaths(dimensions)];\n\n                if (this.settings.addValueOverlay) {\n                    overlayPaths = [\n                        ...overlayPaths,\n                        this.navigator.makeStraightPaths(dimensions, true),\n                    ];\n                }\n            }\n\n            // Set the next block's previous position\n            prevLeftX = nextLeftX;\n            prevRightX = nextRightX;\n            prevHeight = nextHeight;\n        });\n\n        return [paths, overlayPaths];\n    }\n\n    /**\n     * @param {Number} bottomLeftX\n     *\n     * @return {Number}\n     */\n    getDx(bottomLeftX) {\n        // Will be sharper if there is a pinch\n        if (this.settings.bottomPinch > 0) {\n            return bottomLeftX / (this.blocks.length - this.settings.bottomPinch);\n        }\n\n        return bottomLeftX / this.blocks.length;\n    }\n\n    /**\n     * @return {Number}\n     */\n    getDy() {\n        // Curved chart needs reserved pixels to account for curvature\n        if (this.settings.isCurved) {\n            return (this.settings.height - this.settings.curveHeight) / this.blocks.length;\n        }\n\n        return this.settings.height / this.blocks.length;\n    }\n\n    /**\n     * Define the linear color gradients.\n     *\n     * @param {Object} svg\n     *\n     * @return {void}\n     */\n    defineColorGradients(svg) {\n        const defs = svg.append('defs');\n\n        // Create a gradient for each block\n        this.blocks.forEach((block, index) => {\n            const color = block.fill.raw;\n            const shade = this.colorizer.shade(color, -0.2);\n\n            // Create linear gradient\n            const gradient = defs.append('linearGradient')\n                .attr('id', this.colorizer.getGradientId(index));\n\n            // Define the gradient stops\n            const stops = [\n                [0, shade],\n                [40, color],\n                [60, color],\n                [100, shade],\n            ];\n\n            // Add the gradient stops\n            stops.forEach((stop) => {\n                gradient.append('stop')\n                    .attr('offset', `${stop[0]}%`)\n                    .attr('style', `stop-color: ${stop[1]}`);\n            });\n        });\n    }\n\n    /**\n     * Draw the top oval of a curved funnel.\n     *\n     * @param {Object} svg\n     * @param {Array}  blockPaths\n     *\n     * @return {void}\n     */\n    drawTopOval(svg, blockPaths) {\n        const centerX = this.settings.width / 2;\n\n        // Create path from top-most block\n        const paths = blockPaths[0];\n        const topCurve = paths[1][1] + (this.settings.curveHeight / 2);\n\n        const path = this.navigator.plot([\n            ['M', paths[0][0], paths[0][1]],\n            ['Q', centerX, topCurve],\n            [' ', paths[2][0], paths[2][1]],\n            ['M', paths[2][0], this.settings.curveHeight / 2],\n            ['Q', centerX, 0],\n            [' ', paths[0][0], this.settings.curveHeight / 2],\n        ]);\n\n        // Draw top oval\n        svg.append('path')\n            .attr('fill', this.colorizer.shade(this.blocks[0].fill.raw, this.settings.curveShade))\n            .attr('d', path);\n    }\n\n    /**\n     * Draw the next block in the iteration.\n     *\n     * @param {int} index\n     *\n     * @return {void}\n     */\n    drawBlock(index) {\n        if (index === this.blocks.length) {\n            return;\n        }\n\n        // Create a group just for this block\n        const group = this.svg.append('g');\n        const block = this.blocks[index];\n\n        // Fetch path element\n        const path = this.getBlockPath(group, index);\n\n        // Attach data to the element\n        this.attachData(path, block);\n\n        let overlayPath = null;\n        let pathColor = block.fill.actual;\n\n        if (this.settings.addValueOverlay) {\n            overlayPath = this.getOverlayPath(group, index);\n            this.attachData(overlayPath, block);\n\n            // Add data attribute to distinguish between paths\n            path.node().setAttribute('pathType', 'background');\n            overlayPath.node().setAttribute('pathType', 'foreground');\n\n            // Default path becomes background of lighter shade\n            pathColor = this.colorizer.shade(block.fill.raw, 0.3);\n        }\n\n        // Add animation components\n        if (this.settings.animation !== 0) {\n            path.transition()\n                .duration(this.settings.animation)\n                .ease(easeLinear)\n                .attr('fill', pathColor)\n                .attr('d', this.getPathDefinition(index))\n                .on('end', () => {\n                    this.drawBlock(index + 1);\n                });\n        } else {\n            path.attr('fill', pathColor)\n                .attr('d', this.getPathDefinition(index));\n            this.drawBlock(index + 1);\n        }\n\n        // Add path overlay\n        if (this.settings.addValueOverlay) {\n            path.attr('stroke', this.blocks[index].fill.raw);\n\n            if (this.settings.animation !== 0) {\n                overlayPath.transition()\n                    .duration(this.settings.animation)\n                    .ease(easeLinear)\n                    .attr('fill', block.fill.actual)\n                    .attr('d', this.getOverlayPathDefinition(index));\n            } else {\n                overlayPath.attr('fill', block.fill.actual)\n                    .attr('d', this.getOverlayPathDefinition(index));\n            }\n        }\n\n        // Add the hover events\n        if (this.settings.hoverEffects) {\n            [path, overlayPath].forEach((target) => {\n                if (!target) {\n                    return;\n                }\n\n                target\n                    .on('mouseover', this.onMouseOver)\n                    .on('mouseout', this.onMouseOut);\n            });\n        }\n\n        // Add block click event\n        if (this.settings.onBlockClick !== null) {\n            [path, overlayPath].forEach((target) => {\n                if (!target) {\n                    return;\n                }\n\n                target.style('cursor', 'pointer')\n                    .on('click', this.settings.onBlockClick);\n            });\n        }\n\n        // Add tooltips\n        if (this.settings.tooltip.enabled) {\n            [path, overlayPath].forEach((target) => {\n                if (!target) {\n                    return;\n                }\n\n                target.node().addEventListener('mouseout', () => {\n                    if (this.tooltip) {\n                        this.container.removeChild(this.tooltip);\n                        this.tooltip = null;\n                    }\n                });\n                target.node().addEventListener('mousemove', (e) => {\n                    if (!this.tooltip) {\n                        this.tooltip = document.createElement('div');\n                        this.tooltip.setAttribute('class', 'd3-funnel-tooltip');\n                        this.container.appendChild(this.tooltip);\n                    }\n\n                    this.tooltip.innerText = block.tooltip.formatted;\n\n                    const width = this.tooltip.offsetWidth;\n                    const height = this.tooltip.offsetHeight;\n                    const rect = this.container.getBoundingClientRect();\n                    const heightOffset = height + 5;\n                    const containerY = rect.y + window.scrollY;\n                    const isAbove = e.pageY - heightOffset < containerY;\n                    const top = isAbove ? e.pageY + 5 : e.pageY - heightOffset;\n\n                    const styles = [\n                        'display: inline-block',\n                        'position: absolute',\n                        `left: ${e.pageX - (width / 2)}px`,\n                        `top: ${top}px`,\n                        `border: 1px solid ${block.fill.raw}`,\n                        'background: rgb(255,255,255,0.75)',\n                        'padding: 5px 15px',\n                        'color: #000',\n                        'font-size: 14px',\n                        'font-weight: bold',\n                        'text-align: center',\n                        'cursor: default',\n                        'pointer-events: none',\n                    ];\n                    this.tooltip.setAttribute('style', styles.join(';'));\n                });\n            });\n        }\n\n        if (this.settings.label.enabled && block.label.enabled) {\n            this.addBlockLabel(group, index);\n        }\n    }\n\n    /**\n     * @param {Object} group\n     * @param {int}    index\n     *\n     * @return {Object}\n     */\n    getBlockPath(group, index) {\n        const path = group.append('path');\n\n        if (this.settings.animation !== 0) {\n            this.addBeforeTransition(path, index, false);\n        }\n\n        return path;\n    }\n\n    /**\n     * @param {Object} group\n     * @param {int}    index\n     *\n     * @return {Object}\n     */\n    getOverlayPath(group, index) {\n        const path = group.append('path');\n\n        if (this.settings.animation !== 0) {\n            this.addBeforeTransition(path, index, true);\n        }\n\n        return path;\n    }\n\n    /**\n     * Set the attributes of a path element before its animation.\n     *\n     * @param {Object}  path\n     * @param {int}     index\n     * @param {boolean} isOverlay\n     *\n     * @return {void}\n     */\n    addBeforeTransition(path, index, isOverlay) {\n        const paths = isOverlay ? this.overlayPaths[index] : this.blockPaths[index];\n\n        let beforePath;\n        let beforeFill;\n\n        // Construct the top of the trapezoid and leave the other elements\n        // hovering around to expand downward on animation\n        if (!this.settings.isCurved) {\n            beforePath = this.navigator.plot([\n                ['M', paths[0][0], paths[0][1]],\n                ['L', paths[1][0], paths[1][1]],\n                ['L', paths[1][0], paths[1][1]],\n                ['L', paths[0][0], paths[0][1]],\n            ]);\n        } else {\n            beforePath = this.navigator.plot([\n                ['M', paths[0][0], paths[0][1]],\n                ['Q', paths[1][0], paths[1][1]],\n                [' ', paths[2][0], paths[2][1]],\n                ['L', paths[2][0], paths[2][1]],\n                ['M', paths[2][0], paths[2][1]],\n                ['Q', paths[1][0], paths[1][1]],\n                [' ', paths[0][0], paths[0][1]],\n            ]);\n        }\n\n        if (this.settings.fillType === 'solid' && index > 0) {\n            // Use previous fill color, if available\n            beforeFill = this.blocks[index - 1].fill.actual;\n        } else {\n            // Otherwise use current background\n            beforeFill = this.blocks[index].fill.actual;\n        }\n\n        path.attr('d', beforePath)\n            .attr('fill', beforeFill);\n    }\n\n    /**\n     * Attach data to the target element. Also attach the current node to the\n     * data object.\n     *\n     * @param {Object} element\n     * @param {Object} data\n     *\n     * @return {void}\n     */\n    attachData(element, data) {\n        const nodeData = {\n            ...data,\n            node: element.node(),\n        };\n\n        element.data([nodeData]);\n    }\n\n    /**\n     * @param {int} index\n     *\n     * @return {string}\n     */\n    getPathDefinition(index) {\n        const commands = [];\n\n        this.blockPaths[index].forEach((command) => {\n            commands.push([command[2], command[0], command[1]]);\n        });\n\n        return this.navigator.plot(commands);\n    }\n\n    /**\n     * @param {int} index\n     *\n     * @return {string}\n     */\n    getOverlayPathDefinition(index) {\n        const commands = [];\n\n        this.overlayPaths[index].forEach((command) => {\n            commands.push([command[2], command[0], command[1]]);\n        });\n\n        return this.navigator.plot(commands);\n    }\n\n    /**\n     * @param {Object} event\n     * @param {Object} data\n     *\n     * @return {void}\n     */\n    onMouseOver(event, data) {\n        const children = event.target.parentElement.childNodes;\n\n        // Highlight all paths within one block\n        [...children].forEach((node) => {\n            if (node.nodeName.toLowerCase() === 'path') {\n                const type = node.getAttribute('pathType') || '';\n\n                if (type === 'foreground') {\n                    select(node).attr('fill', this.colorizer.shade(data.fill.raw, -0.5));\n                } else {\n                    select(node).attr('fill', this.colorizer.shade(data.fill.raw, -0.2));\n                }\n            }\n        });\n    }\n\n    /**\n     * @param {Object} event\n     * @param {Object} data\n     *\n     * @return {void}\n     */\n    onMouseOut(event, data) {\n        const children = event.target.parentElement.childNodes;\n\n        // Restore original color for all paths of a block\n        [...children].forEach((node) => {\n            if (node.nodeName.toLowerCase() === 'path') {\n                const type = node.getAttribute('pathType') || '';\n\n                if (type === 'background') {\n                    const backgroundColor = this.colorizer.shade(data.fill.raw, 0.3);\n                    select(node).attr('fill', backgroundColor);\n                } else {\n                    select(node).attr('fill', data.fill.actual);\n                }\n            }\n        });\n    }\n\n    /**\n     * @param {Object} group\n     * @param {int}    index\n     *\n     * @return {void}\n     */\n    addBlockLabel(group, index) {\n        const paths = this.blockPaths[index];\n\n        const formattedLabel = this.blocks[index].label.formatted;\n        const fill = this.blocks[index].label.color;\n\n        // Center the text\n        const x = this.settings.width / 2;\n        const y = this.getTextY(paths);\n\n        const text = group.append('text')\n            .attr('x', x)\n            .attr('y', y)\n            .attr('fill', fill)\n            .attr('font-size', this.settings.label.fontSize)\n            .attr('text-anchor', 'middle')\n            .attr('dominant-baseline', 'middle')\n            .attr('pointer-events', 'none');\n\n        // Add font-family, if exists\n        if (this.settings.label.fontFamily !== null) {\n            text.attr('font-family', this.settings.label.fontFamily);\n        }\n\n        this.addLabelLines(text, formattedLabel, x);\n    }\n\n    /**\n     * Add <tspan> elements for each line of the formatted label.\n     *\n     * @param {Object} text\n     * @param {String} formattedLabel\n     * @param {Number} x\n     *\n     * @return {void}\n     */\n    addLabelLines(text, formattedLabel, x) {\n        const lines = formattedLabel.split('\\n');\n        const lineHeight = 20;\n\n        // dy will signify the change from the initial height y\n        // We need to initially start the first line at the very top, factoring\n        // in the other number of lines\n        const initialDy = (-1 * lineHeight * (lines.length - 1)) / 2;\n\n        lines.forEach((line, i) => {\n            const dy = i === 0 ? initialDy : lineHeight;\n\n            text.append('tspan').attr('x', x).attr('dy', dy).text(line);\n        });\n    }\n\n    /**\n     * Returns the y position of the given label's text. This is determined by\n     * taking the mean of the bases.\n     *\n     * @param {Array} paths\n     *\n     * @return {Number}\n     */\n    getTextY(paths) {\n        const { isCurved, curveHeight } = this.settings;\n\n        if (isCurved) {\n            return ((paths[2][1] + paths[3][1]) / 2) + ((1.5 * curveHeight) / this.blocks.length);\n        }\n\n        return (paths[1][1] + paths[2][1]) / 2;\n    }\n}\n\nexport default D3Funnel;\n"
  },
  {
    "path": "src/d3-funnel/Formatter.js",
    "content": "class Formatter {\n    /**\n     * Register the format function.\n     *\n     * @param {string|function} format\n     *\n     * @return {function}\n     */\n    getFormatter(format) {\n        if (typeof format === 'function') {\n            return format;\n        }\n\n        return (label, value, formattedValue) => (\n            this.stringFormatter(label, value, formattedValue, format)\n        );\n    }\n\n    /**\n     * Format the given value according to the data point or the format.\n     *\n     * @param {string}   label\n     * @param {number}   value\n     * @param {*}        formattedValue\n     * @param {function} formatter\n     *\n     * @return string\n     */\n    format({ label, value, formattedValue = null }, formatter) {\n        return formatter(label, value, formattedValue);\n    }\n\n    /**\n     * Format the string according to a simple expression.\n     *\n     * {l}: label\n     * {v}: raw value\n     * {f}: formatted value\n     *\n     * @param {string} label\n     * @param {number} value\n     * @param {*}      formattedValue\n     * @param {string} expression\n     *\n     * @return {string}\n     */\n    stringFormatter(label, value, formattedValue, expression) {\n        let formatted = formattedValue;\n\n        // Attempt to use supplied formatted value\n        // Otherwise, use the default\n        if (formattedValue === null) {\n            formatted = this.getDefaultFormattedValue(value);\n        }\n\n        return expression\n            .split('{l}')\n            .join(label)\n            .split('{v}')\n            .join(value)\n            .split('{f}')\n            .join(formatted);\n    }\n\n    /**\n     * @param {number} value\n     *\n     * @return {string}\n     */\n    getDefaultFormattedValue(value) {\n        return value.toLocaleString();\n    }\n}\n\nexport default Formatter;\n"
  },
  {
    "path": "src/d3-funnel/Navigator.js",
    "content": "class Navigator {\n    /**\n     * Given a list of path commands, returns the compiled description.\n     *\n     * @param {Array} commands\n     *\n     * @return {string}\n     */\n    plot(commands) {\n        let path = '';\n\n        commands.forEach((command) => {\n            path += `${command[0]}${command[1]},${command[2]} `;\n        });\n\n        return path.replace(/ +/g, ' ').trim();\n    }\n\n    /**\n     * @param {Object}  dimensions\n     * @param {boolean} isValueOverlay\n     *\n     * @return {Array}\n     */\n    makeCurvedPaths(dimensions, isValueOverlay = false) {\n        const points = this.makeBezierPoints(dimensions);\n\n        if (isValueOverlay) {\n            return this.makeBezierPath(points, dimensions.ratio);\n        }\n\n        return this.makeBezierPath(points);\n    }\n\n    /**\n     * @param {Number} centerX\n     * @param {Number} prevLeftX\n     * @param {Number} prevRightX\n     * @param {Number} prevHeight\n     * @param {Number} nextLeftX\n     * @param {Number} nextRightX\n     * @param {Number} nextHeight\n     * @param {Number} curveHeight\n     *\n     * @return {Object}\n     */\n    makeBezierPoints({\n        centerX,\n        prevLeftX,\n        prevRightX,\n        prevHeight,\n        nextLeftX,\n        nextRightX,\n        nextHeight,\n        curveHeight,\n    }) {\n        return {\n            p00: {\n                x: prevLeftX,\n                y: prevHeight,\n            },\n            p01: {\n                x: centerX,\n                y: prevHeight + (curveHeight / 2),\n            },\n            p02: {\n                x: prevRightX,\n                y: prevHeight,\n            },\n\n            p10: {\n                x: nextLeftX,\n                y: nextHeight,\n            },\n            p11: {\n                x: centerX,\n                y: nextHeight + curveHeight,\n            },\n            p12: {\n                x: nextRightX,\n                y: nextHeight,\n            },\n        };\n    }\n\n    /**\n     * @param {Object} p00\n     * @param {Object} p01\n     * @param {Object} p02\n     * @param {Object} p10\n     * @param {Object} p11\n     * @param {Object} p12\n     * @param {Number} ratio\n     *\n     * @return {Array}\n     */\n    makeBezierPath({\n        p00,\n        p01,\n        p02,\n        p10,\n        p11,\n        p12,\n    }, ratio = 1) {\n        const curve0 = this.getQuadraticBezierCurve(p00, p01, p02, ratio);\n        const curve1 = this.getQuadraticBezierCurve(p10, p11, p12, ratio);\n\n        return [\n            // Top Bezier curve\n            [curve0.p0.x, curve0.p0.y, 'M'],\n            [curve0.p1.x, curve0.p1.y, 'Q'],\n            [curve0.p2.x, curve0.p2.y, ''],\n            // Right line\n            [curve1.p2.x, curve1.p2.y, 'L'],\n            // Bottom Bezier curve\n            [curve1.p2.x, curve1.p2.y, 'M'],\n            [curve1.p1.x, curve1.p1.y, 'Q'],\n            [curve1.p0.x, curve1.p0.y, ''],\n            // Left line\n            [curve0.p0.x, curve0.p0.y, 'L'],\n        ];\n    }\n\n    /**\n     * @param {Object} p0\n     * @param {Object} p1\n     * @param {Object} p2\n     * @param {Number} t\n     *\n     * @return {Object}\n     */\n    getQuadraticBezierCurve(p0, p1, p2, t = 1) {\n        // Quadratic Bezier curve syntax: M(P0) Q(P1) P2\n        // Where P0, P2 are the curve endpoints and P1 is the control point\n\n        // More generally, at 0 <= t <= 1, we have the following:\n        // Q0(t), which varies linearly from P0 to P1\n        // Q1(t), which varies linearly from P1 to P2\n        // B(t), which is interpolated linearly between Q0(t) and Q1(t)\n\n        // For an intermediate curve at 0 <= t <= 1:\n        // P1(t) = Q0(t)\n        // P2(t) = B(t)\n\n        return {\n            p0,\n            p1: {\n                x: this.getLinearInterpolation(p0, p1, t, 'x'),\n                y: this.getLinearInterpolation(p0, p1, t, 'y'),\n            },\n            p2: {\n                x: this.getQuadraticInterpolation(p0, p1, p2, t, 'x'),\n                y: this.getQuadraticInterpolation(p0, p1, p2, t, 'y'),\n            },\n        };\n    }\n\n    /**\n     * @param {Object} p0\n     * @param {Object} p1\n     * @param {Number} t\n     * @param {string} axis\n     *\n     * @return {Number}\n     */\n    getLinearInterpolation(p0, p1, t, axis) {\n        return p0[axis] + (t * (p1[axis] - p0[axis]));\n    }\n\n    /**\n     * @param {Object} p0\n     * @param {Object} p1\n     * @param {Object} p2\n     * @param {Number} t\n     * @param {string} axis\n     *\n     * @return {Number}\n     */\n    getQuadraticInterpolation(p0, p1, p2, t, axis) {\n        return (((1 - t) ** 2) * p0[axis]) +\n            (2 * (1 - t) * t * p1[axis]) +\n            ((t ** 2) * p2[axis]);\n    }\n\n    /**\n     * @param {Number}  prevLeftX\n     * @param {Number}  prevRightX\n     * @param {Number}  prevHeight\n     * @param {Number}  nextLeftX\n     * @param {Number}  nextRightX\n     * @param {Number}  nextHeight\n     * @param {Number}  ratio\n     * @param {boolean} isValueOverlay\n     *\n     * @return {Object}\n     */\n    makeStraightPaths({\n        prevLeftX,\n        prevRightX,\n        prevHeight,\n        nextLeftX,\n        nextRightX,\n        nextHeight,\n        ratio,\n    }, isValueOverlay = false) {\n        if (isValueOverlay) {\n            const lengthTop = (prevRightX - prevLeftX);\n            const lengthBtm = (nextRightX - nextLeftX);\n            let rightSideTop = (lengthTop * (ratio || 0)) + prevLeftX;\n            let rightSideBtm = (lengthBtm * (ratio || 0)) + nextLeftX;\n\n            // Overlay should not be longer than the max length of the path\n            rightSideTop = Math.min(rightSideTop, lengthTop);\n            rightSideBtm = Math.min(rightSideBtm, lengthBtm);\n\n            return [\n                // Start position\n                [prevLeftX, prevHeight, 'M'],\n                // Move to right\n                [rightSideTop, prevHeight, 'L'],\n                // Move down\n                [rightSideBtm, nextHeight, 'L'],\n                // Move to left\n                [nextLeftX, nextHeight, 'L'],\n                // Wrap back to top\n                [prevLeftX, prevHeight, 'L'],\n            ];\n        }\n\n        return [\n            // Start position\n            [prevLeftX, prevHeight, 'M'],\n            // Move to right\n            [prevRightX, prevHeight, 'L'],\n            // Move down\n            [nextRightX, nextHeight, 'L'],\n            // Move to left\n            [nextLeftX, nextHeight, 'L'],\n            // Wrap back to top\n            [prevLeftX, prevHeight, 'L'],\n        ];\n    }\n}\n\nexport default Navigator;\n"
  },
  {
    "path": "src/d3-funnel/Utils.js",
    "content": "class Utils {\n    /**\n     * Determine whether the given parameter is an extendable object.\n     *\n     * @param {*} a\n     *\n     * @return {boolean}\n     */\n    static isExtendableObject(a) {\n        return typeof a === 'object' && a !== null && !Array.isArray(a);\n    }\n\n    /**\n     * Extends an object with the members of another.\n     *\n     * @param {Object} a The object to be extended.\n     * @param {Object} b The object to clone from.\n     *\n     * @return {Object}\n     */\n    static extend(a, b) {\n        let result = {};\n\n        // If a is non-trivial, extend the result with it\n        if (Object.keys(a).length > 0) {\n            result = Utils.extend({}, a);\n        }\n\n        // Copy over the properties in b into a\n        Object.keys(b).forEach((prop) => {\n            if (Utils.isExtendableObject(b[prop])) {\n                if (Utils.isExtendableObject(a[prop])) {\n                    result[prop] = Utils.extend(a[prop], b[prop]);\n                } else {\n                    result[prop] = Utils.extend({}, b[prop]);\n                }\n            } else {\n                result[prop] = b[prop];\n            }\n        });\n\n        return result;\n    }\n\n    /**\n     * Convert the legacy block array to a block object.\n     *\n     * @param {Array} block\n     *\n     * @returns {Object}\n     */\n    static convertLegacyBlock(block) {\n        return {\n            label: block[0],\n            value: Utils.getRawBlockCount(block),\n            formattedValue: Array.isArray(block[1]) ? block[1][1] : null,\n            backgroundColor: block[2],\n            labelColor: block[3],\n        };\n    }\n\n    /**\n     * Given a raw data block, return its count.\n     *\n     * @param {Array} block\n     *\n     * @return {Number}\n     */\n    static getRawBlockCount(block) {\n        if (Array.isArray(block)) {\n            return Array.isArray(block[1]) ? block[1][0] : block[1];\n        }\n\n        return block.value;\n    }\n}\n\nexport default Utils;\n"
  },
  {
    "path": "src/index.js",
    "content": "import D3Funnel from '#js/D3Funnel.js';\n\nexport default D3Funnel;\n"
  },
  {
    "path": "test/d3-funnel/Colorizer.js",
    "content": "import { assert } from 'chai';\n\nimport Colorizer from '../../src/d3-funnel/Colorizer.js';\n\ndescribe('Colorizer', () => {\n    describe('expandHex', () => {\n        it('should expand a three character hex code to six characters', () => {\n            const hex = 'd33';\n\n            assert.equal('dd3333', (new Colorizer()).expandHex(hex));\n        });\n    });\n\n    describe('shade', () => {\n        it('should brighten a color by the given positive percentage', () => {\n            const color = '#000000';\n\n            assert.equal('#222222', (new Colorizer()).shade(color, 2 / 15));\n        });\n\n        it('should shade a color by the given negative percentage', () => {\n            const color = '#ffffff';\n\n            assert.equal('#dddddd', (new Colorizer()).shade(color, -2 / 15));\n        });\n\n        it('should expand a three-character hex', () => {\n            const color = '#fff';\n\n            assert.equal('#ffffff', (new Colorizer()).shade(color, 0));\n        });\n    });\n\n    describe('hexToRg', () => {\n        it('should convert a hex value to its RGB value', () => {\n            const color = '#007fff';\n\n            assert.deepEqual({ R: 0, G: 127, B: 255 }, (new Colorizer()).hexToRgb(color));\n        });\n\n        it('should expand a three-character hex', () => {\n            const color = '#d33';\n\n            assert.deepEqual({ R: 221, G: 51, B: 51 }, (new Colorizer()).hexToRgb(color));\n        });\n    });\n});\n"
  },
  {
    "path": "test/d3-funnel/D3Funnel.js",
    "content": "import { cloneDeep } from 'lodash';\nimport {\n    range,\n    select,\n    selectAll,\n    scaleOrdinal,\n    schemeCategory10,\n} from 'd3';\nimport { assert } from 'chai';\nimport sinon from 'sinon';\n\nimport D3Funnel from '../../src/d3-funnel/D3Funnel.js';\n\nfunction getFunnel() {\n    return new D3Funnel('#funnel');\n}\n\nfunction getSvg() {\n    return select('#funnel').selectAll('svg');\n}\n\nfunction getSvgId() {\n    return document.querySelector('#funnel svg').id;\n}\n\nfunction getBasicData() {\n    return [{ label: 'Node', value: 1000 }];\n}\n\nfunction isLetter(str) {\n    return str.length === 1 && str.match(/[a-z]/i);\n}\n\nfunction getCommandPoint(command) {\n    const points = command.split(',');\n    const y = points[1];\n\n    let x = points[0];\n\n    // Strip any letter in front of number\n    if (isLetter(x[0])) {\n        x = x.substr(1);\n    }\n\n    return {\n        x: parseFloat(x),\n        y: parseFloat(y),\n    };\n}\n\nfunction getPathTopWidth(path) {\n    const commands = path.attr('d').split(' ');\n\n    return getCommandPoint(commands[1]).x - getCommandPoint(commands[0]).x;\n}\n\nfunction getPathBottomWidth(path) {\n    const commands = path.attr('d').split(' ');\n\n    return getCommandPoint(commands[2]).x - getCommandPoint(commands[3]).x;\n}\n\nfunction getPathHeight(path) {\n    const commands = path.attr('d').split(' ');\n\n    return getCommandPoint(commands[2]).y - getCommandPoint(commands[0]).y;\n}\n\nconst defaults = cloneDeep(D3Funnel.defaults);\n\ndescribe('D3Funnel', () => {\n    beforeEach((done) => {\n        // Reset any styles\n        select('#funnel').attr('style', null);\n\n        // Reset defaults\n        D3Funnel.defaults = cloneDeep(defaults);\n\n        // Clear out sandbox\n        document.getElementById('sandbox').innerHTML = '';\n\n        done();\n    });\n\n    describe('constructor', () => {\n        it('should instantiate without error when a query string is provided', () => {\n            new D3Funnel('#funnel'); // eslint-disable-line no-new\n        });\n\n        it('should instantiate without error when a DOM node is provided', () => {\n            new D3Funnel(document.querySelector('#funnel')); // eslint-disable-line no-new\n        });\n    });\n\n    describe('methods', () => {\n        describe('draw', () => {\n            it('should draw a chart on the identified target', () => {\n                getFunnel().draw(getBasicData());\n\n                assert.equal(1, getSvg().nodes().length);\n            });\n\n            it('should draw when no options are specified', () => {\n                getFunnel().draw(getBasicData());\n\n                assert.equal(1, getSvg().nodes().length);\n            });\n\n            it('should throw an error when the data is not an array', () => {\n                const funnel = getFunnel();\n\n                assert.throws(() => {\n                    funnel.draw('Not array');\n                }, Error, 'Data must be an array.');\n            });\n\n            it('should throw an error when the data array does not have an element', () => {\n                const funnel = getFunnel();\n\n                assert.throws(() => {\n                    funnel.draw([]);\n                }, Error, 'Data array must contain at least one element.');\n            });\n\n            it('should throw an error when the first data array element is not an object', () => {\n                const funnel = getFunnel();\n\n                assert.throws(() => {\n                    funnel.draw(['Not array']);\n                }, Error, 'Data array elements must be an object.');\n            });\n\n            it('should throw an error when the first data array element does not have a value', () => {\n                const funnel = getFunnel();\n\n                assert.throws(() => {\n                    funnel.draw([{ label: 'Only Label' }]);\n                }, Error, 'Data array elements must contain a label and value.');\n            });\n\n            it('should draw as many blocks as there are elements', () => {\n                getFunnel().draw([\n                    { label: 'Node A', value: 1 },\n                    { label: 'Node B', value: 2 },\n                    { label: 'Node C', value: 3 },\n                    { label: 'Node D', value: 4 },\n                ]);\n\n                assert.equal(4, getSvg().selectAll('path').nodes().length);\n            });\n\n            it('should pass any row-specified formatted values to the label formatter', () => {\n                getFunnel().draw([\n                    { label: 'Node A', value: 1, formattedValue: 'One' },\n                    { label: 'Node B', value: 2 },\n                    { label: 'Node C', value: 1, formattedValue: 'Three' },\n                ]);\n\n                const texts = getSvg().selectAll('text').nodes();\n\n                assert.equal('Node A: One', select(texts[0]).text());\n                assert.equal('Node B: 2', select(texts[1]).text());\n                assert.equal('Node C: Three', select(texts[2]).text());\n            });\n\n            it('should hide the labels of any row specified', () => {\n                getFunnel().draw([\n                    { label: 'Node A', value: 1, hideLabel: true },\n                    { label: 'Node B', value: 2 },\n                    { label: 'Node C', value: 3, hideLabel: true },\n                ]);\n\n                const texts = getSvg().selectAll('text').nodes();\n\n                assert.equal('Node B: 2', select(texts[0]).text());\n                assert.equal(undefined, texts[1]);\n            });\n\n            it('should use colors assigned to a data element', () => {\n                getFunnel().draw([\n                    { label: 'Node A', value: 1, backgroundColor: '#111' },\n                    { label: 'Node B', value: 2, backgroundColor: '#222' },\n                    { label: 'Node C', value: 3 },\n                    { label: 'Node D', value: 4, backgroundColor: '#444' },\n                ]);\n\n                const paths = getSvg().selectAll('path').nodes();\n                const colorScale = scaleOrdinal(schemeCategory10).domain(range(0, 10));\n\n                assert.equal('#111', select(paths[0]).attr('fill'));\n                assert.equal('#222', select(paths[1]).attr('fill'));\n                assert.equal(colorScale(2), select(paths[2]).attr('fill'));\n                assert.equal('#444', select(paths[3]).attr('fill'));\n            });\n\n            it('should use label colors assigned to a data element', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 1, labelColor: '#111' },\n                    { label: 'B', value: 2, labelColor: '#222' },\n                    { label: 'C', value: 3 },\n                    { label: 'D', value: 4, labelColor: '#444' },\n                ]);\n\n                const texts = getSvg().selectAll('text').nodes();\n\n                assert.equal('#111', select(texts[0]).attr('fill'));\n                assert.equal('#222', select(texts[1]).attr('fill'));\n                assert.equal('#fff', select(texts[2]).attr('fill'));\n                assert.equal('#444', select(texts[3]).attr('fill'));\n            });\n\n            it('should remove other elements from container', () => {\n                const container = select('#funnel');\n                const funnel = getFunnel();\n\n                // Make sure the container has no children\n                container.selectAll('*').remove();\n\n                container.append('p');\n                funnel.draw(getBasicData());\n\n                // Expect funnel children count plus funnel itself\n                const expected = getSvg().selectAll('*').size() + 1;\n                const actual = container.selectAll('*').size();\n\n                assert.equal(expected, actual);\n            });\n\n            it('should remove inner text from container', () => {\n                const container = select('#funnel');\n                const funnel = getFunnel();\n\n                // Make sure the container has no text\n                container.text();\n\n                container.text('to be removed');\n                funnel.draw(getBasicData());\n\n                // Make sure the only text in container comes from the funnel\n                assert.equal(getSvg().text(), container.text());\n            });\n\n            it('should assign a unique ID upon draw', () => {\n                getFunnel().draw(getBasicData());\n\n                const id = getSvgId();\n\n                assert.isTrue(document.querySelectorAll(`#${id}`).length === 1);\n            });\n        });\n\n        describe('destroy', () => {\n            it('should remove a drawn SVG element', () => {\n                const funnel = getFunnel();\n\n                funnel.draw(getBasicData());\n                funnel.destroy();\n\n                assert.equal(0, getSvg().nodes().length);\n            });\n        });\n    });\n\n    describe('defaults', () => {\n        it('should affect all default options', () => {\n            D3Funnel.defaults.label.fill = '#777';\n\n            getFunnel().draw(getBasicData());\n\n            assert.isTrue(select('#funnel text').attr('fill').indexOf('#777') > -1);\n        });\n    });\n\n    describe('options', () => {\n        describe('chart.width/height', () => {\n            it('should default to the container\\'s dimensions', () => {\n                ['width', 'height'].forEach((direction) => {\n                    select('#funnel').style(direction, '250px');\n\n                    getFunnel().draw(getBasicData());\n\n                    assert.equal(250, getSvg().node().getBBox()[direction]);\n                });\n            });\n\n            it('should default to the library defaults if the container dimensions are zero', () => {\n                document.querySelector('#funnel').style.width = '0px';\n                document.querySelector('#funnel').style.height = '0px';\n\n                getFunnel().draw(getBasicData());\n\n                assert.equal(350, getSvg().node().getBBox().width);\n                assert.equal(400, getSvg().node().getBBox().height);\n            });\n\n            it('should set the funnel\\'s width/height to the specified amount', () => {\n                ['width', 'height'].forEach((direction) => {\n                    getFunnel().draw(getBasicData(), {\n                        chart: {\n                            [direction]: 200,\n                        },\n                    });\n\n                    assert.equal(200, getSvg().node().getBBox()[direction]);\n                });\n            });\n\n            it('should set the funnel\\'s percent width/height to the specified amount', () => {\n                ['width', 'height'].forEach((direction) => {\n                    select('#funnel').style(direction, '200px');\n\n                    getFunnel().draw(getBasicData(), {\n                        chart: {\n                            [direction]: '75%',\n                        },\n                    });\n\n                    assert.equal(150, getSvg().node().getBBox()[direction]);\n                });\n            });\n        });\n\n        describe('chart.height', () => {\n            it('should default to the container\\'s height', () => {\n                select('#funnel').style('height', '250px');\n\n                getFunnel().draw(getBasicData());\n\n                assert.equal(250, getSvg().node().getBBox().height);\n            });\n\n            it('should set the funnel\\'s height to the specified amount', () => {\n                getFunnel().draw(getBasicData(), {\n                    chart: {\n                        height: 200,\n                    },\n                });\n\n                assert.equal(200, getSvg().node().getBBox().height);\n            });\n\n            it('should set the funnel\\'s percentage height to the specified amount', () => {\n                select('#funnel').style('height', '300px');\n\n                getFunnel().draw(getBasicData(), {\n                    chart: {\n                        height: '50%',\n                    },\n                });\n\n                assert.equal(150, getSvg().node().getBBox().height);\n            });\n        });\n\n        describe('chart.bottomWidth', () => {\n            it('should set the bottom tip width to the specified percentage', () => {\n                getFunnel().draw(getBasicData(), {\n                    chart: {\n                        width: 200,\n                        bottomWidth: 1 / 2,\n                    },\n                });\n\n                assert.equal(100, getPathBottomWidth(select('path')));\n            });\n        });\n\n        describe('chart.bottomPinch', () => {\n            it('should set the last n number of blocks to have the width of chart.bottomWidth', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 1 },\n                    { label: 'B', value: 2 },\n                    { label: 'C', value: 3 },\n                ], {\n                    chart: {\n                        width: 450,\n                        bottomWidth: 1 / 3,\n                        bottomPinch: 2,\n                    },\n                });\n\n                const paths = selectAll('path').nodes();\n\n                assert.equal(150, paths[1].getBBox().width);\n                assert.equal(150, paths[2].getBBox().width);\n            });\n\n            it('should maintain chart.bottomWidth when combined with block.minHeight', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 1 },\n                    { label: 'B', value: 2 },\n                    { label: 'C', value: 3 },\n                ], {\n                    chart: {\n                        width: 450,\n                        height: 100,\n                        bottomWidth: 1 / 3,\n                        bottomPinch: 1,\n                    },\n                    block: {\n                        dynamicHeight: true,\n                        minHeight: 20,\n                    },\n                });\n\n                const paths = selectAll('path').nodes();\n\n                assert.equal(150, paths[2].getBBox().width);\n            });\n\n            it('should maintain chart.bottomWidth when combined with block.dynamicHeight and curve.enabled', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 1 },\n                    { label: 'B', value: 2 },\n                    { label: 'C', value: 3 },\n                    { label: 'D', value: 4 },\n                ], {\n                    chart: {\n                        width: 320,\n                        height: 400,\n                        bottomWidth: 3 / 8,\n                        bottomPinch: 1,\n                        curve: {\n                            enabled: true,\n                        },\n                    },\n                    block: {\n                        dynamicHeight: true,\n                    },\n                });\n\n                const paths = selectAll('path').nodes();\n\n                assert.equal(120, paths[4].getBBox().width);\n            });\n        });\n\n        describe('chart.inverted', () => {\n            it('should draw the chart in a top-to-bottom arrangement by default', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 1 },\n                    { label: 'B', value: 2 },\n                ], {\n                    chart: {\n                        width: 200,\n                        bottomWidth: 1 / 2,\n                    },\n                });\n\n                const paths = selectAll('path').nodes();\n\n                assert.equal(200, getPathTopWidth(select(paths[0])));\n                assert.equal(100, getPathBottomWidth(select(paths[1])));\n            });\n\n            it('should draw the chart in a bottom-to-top arrangement when true', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 1 },\n                    { label: 'B', value: 2 },\n                ], {\n                    chart: {\n                        width: 200,\n                        bottomWidth: 1 / 2,\n                        inverted: true,\n                    },\n                });\n\n                const paths = selectAll('path').nodes();\n\n                assert.equal(100, getPathTopWidth(select(paths[0])));\n                assert.equal(200, getPathBottomWidth(select(paths[1])));\n            });\n        });\n\n        describe('chart.curve.enabled', () => {\n            it('should create an additional path on top of the trapezoids', () => {\n                getFunnel().draw(getBasicData(), {\n                    chart: {\n                        curve: {\n                            enabled: true,\n                        },\n                    },\n                });\n\n                assert.equal(2, selectAll('#funnel path').nodes().length);\n            });\n\n            it('should create a quadratic Bezier curve on each path', () => {\n                getFunnel().draw(getBasicData(), {\n                    chart: {\n                        curve: {\n                            enabled: true,\n                        },\n                    },\n                });\n\n                const paths = selectAll('#funnel path').nodes();\n                const quadraticPaths = paths.filter((path) => select(path).attr('d').indexOf('Q') > -1);\n\n                assert.equal(paths.length, quadraticPaths.length);\n            });\n        });\n\n        describe('block.dynamicHeight', () => {\n            it('should use equal heights when false', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 1 },\n                    { label: 'B', value: 2 },\n                ], {\n                    chart: {\n                        height: 300,\n                    },\n                });\n\n                const paths = selectAll('#funnel path').nodes();\n\n                assert.equal(150, getPathHeight(select(paths[0])));\n                assert.equal(150, getPathHeight(select(paths[1])));\n            });\n\n            it('should use proportional heights when true', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 1 },\n                    { label: 'B', value: 2 },\n                ], {\n                    chart: {\n                        height: 300,\n                    },\n                    block: {\n                        dynamicHeight: true,\n                    },\n                });\n\n                const paths = selectAll('#funnel path').nodes();\n\n                assert.equal(100, parseInt(getPathHeight(select(paths[0])), 10));\n                assert.equal(200, parseInt(getPathHeight(select(paths[1])), 10));\n            });\n\n            it('should not have NaN in the last path when bottomWidth is equal to 0%', () => {\n                // A very specific cooked-up example that could trigger NaN\n                getFunnel().draw([\n                    { label: 'A', value: 120 },\n                    { label: 'B', value: 40 },\n                    { label: 'C', value: 20 },\n                    { label: 'D', value: 15 },\n                ], {\n                    chart: {\n                        height: 300,\n                        bottomWidth: 0,\n                    },\n                    block: {\n                        dynamicHeight: true,\n                    },\n                });\n\n                const paths = selectAll('#funnel path').nodes();\n\n                assert.equal(-1, select(paths[3]).attr('d').indexOf('NaN'));\n            });\n\n            it('should not error when bottomWidth is equal to 100%', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 1 },\n                    { label: 'B', value: 2 },\n                ], {\n                    chart: {\n                        height: 300,\n                        bottomWidth: 1,\n                    },\n                    block: {\n                        dynamicHeight: true,\n                    },\n                });\n            });\n\n            it('should not generate NaN or Infinite values when zero', () => {\n                getFunnel().draw(getBasicData(), {\n                    chart: {\n                        height: 0,\n                    },\n                    block: {\n                        dynamicHeight: true,\n                    },\n                });\n\n                selectAll('path').nodes().forEach((node) => {\n                    const definition = String(select(node).attr('d'));\n\n                    assert.equal(false, definition.indexOf('NaN') > -1 || definition.indexOf('Infinity') > -1);\n                });\n            });\n\n            it('should give all blocks equal height if the sum of values is zero', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 0 },\n                    { label: 'B', value: 0 },\n                ], {\n                    chart: {\n                        height: 300,\n                    },\n                    block: {\n                        dynamicHeight: true,\n                    },\n                });\n\n                const paths = selectAll('#funnel path').nodes();\n\n                assert.equal(150, getPathHeight(select(paths[0])));\n                assert.equal(150, getPathHeight(select(paths[1])));\n            });\n        });\n\n        describe('block.dynamicSlope', () => {\n            it('should give each block top width relative to its value', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 100 },\n                    { label: 'B', value: 55 },\n                    { label: 'C', value: 42 },\n                    { label: 'D', value: 74 },\n                ], {\n                    chart: {\n                        width: 100,\n                    },\n                    block: {\n                        dynamicSlope: true,\n                    },\n                });\n\n                const paths = selectAll('#funnel path').nodes();\n\n                assert.equal(parseFloat(getPathTopWidth(select(paths[0]))), 100);\n                assert.equal(parseFloat(getPathTopWidth(select(paths[1]))), 55);\n                assert.equal(parseFloat(getPathTopWidth(select(paths[2]))), 42);\n                assert.equal(parseFloat(getPathTopWidth(select(paths[3]))), 74);\n            });\n\n            it('should make the last block top width equal to bottom width', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 100 },\n                    { label: 'B', value: 52 },\n                    { label: 'C', value: 42 },\n                    { label: 'D', value: 74 },\n                ], {\n                    chart: {\n                        width: 100,\n                    },\n                    block: {\n                        dynamicSlope: true,\n                    },\n                });\n\n                const paths = selectAll('#funnel path').nodes();\n\n                assert.equal(parseFloat(getPathTopWidth(select(paths[3]))), 74);\n                assert.equal(parseFloat(getPathBottomWidth(select(paths[3]))), 74);\n            });\n\n            it('should use bottomWidth value when false', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 100 },\n                    { label: 'B', value: 90 },\n                ], {\n                    chart: {\n                        width: 100,\n                        bottomWidth: 0.4,\n                    },\n                });\n\n                const paths = selectAll('#funnel path').nodes();\n\n                assert.equal(parseFloat(getPathTopWidth(select(paths[0]))), 100);\n                assert.equal(parseFloat(getPathBottomWidth(select(paths[1]))), 40);\n            });\n        });\n\n        describe('block.barOverlay', () => {\n            it('should draw value overlay within each path', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 10 },\n                    { label: 'B', value: 20 },\n                ], {\n                    block: {\n                        barOverlay: true,\n                    },\n                });\n\n                // draw 2 path for each data point\n                assert.equal(4, selectAll('#funnel path').nodes().length);\n            });\n\n            it('should draw value overlay with overridden total count', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 10 },\n                    { label: 'B', value: 20 },\n                ], {\n                    chart: {\n                        totalCount: 100,\n                    },\n                    block: {\n                        barOverlay: true,\n                    },\n                });\n\n                const paths = selectAll('path').nodes();\n\n                const APathFullWidth = getPathTopWidth(select(paths[0]));\n                const APathOverlayWidth = getPathTopWidth(select(paths[1]));\n                const BPathFullWidth = getPathTopWidth(select(paths[2]));\n                const BPathOverlayWidth = getPathTopWidth(select(paths[3]));\n\n                assert.equal(10, Math.round((APathOverlayWidth / APathFullWidth) * 100));\n                assert.equal(20, Math.round((BPathOverlayWidth / BPathFullWidth) * 100));\n            });\n        });\n\n        describe('block.fill.scale', () => {\n            it('should use a function\\'s return value', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 1 },\n                    { label: 'B', value: 2 },\n                ], {\n                    block: {\n                        fill: {\n                            scale: (index) => {\n                                if (index === 0) {\n                                    return '#111';\n                                }\n\n                                return '#222';\n                            },\n                        },\n                    },\n                });\n\n                const paths = getSvg().selectAll('path').nodes();\n\n                assert.equal('#111', select(paths[0]).attr('fill'));\n                assert.equal('#222', select(paths[1]).attr('fill'));\n            });\n\n            it('should use an array\\'s return value', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 1 },\n                    { label: 'B', value: 2 },\n                ], {\n                    block: {\n                        fill: {\n                            scale: ['#111', '#222'],\n                        },\n                    },\n                });\n\n                const paths = getSvg().selectAll('path').nodes();\n\n                assert.equal('#111', select(paths[0]).attr('fill'));\n                assert.equal('#222', select(paths[1]).attr('fill'));\n            });\n        });\n\n        describe('block.fill.type', () => {\n            it('should create gradients when set to \\'gradient\\'', () => {\n                getFunnel().draw(getBasicData(), {\n                    block: {\n                        fill: {\n                            type: 'gradient',\n                        },\n                    },\n                });\n\n                const id = getSvgId();\n\n                // Cannot try to re-select the camelCased linearGradient element\n                // due to a Webkit bug in the current PhantomJS; workaround is\n                // to select the known ID of the linearGradient element\n                // https://bugs.webkit.org/show_bug.cgi?id=83438\n                assert.equal(1, selectAll(`#funnel defs #${id}-gradient-0`).nodes().length);\n\n                assert.equal(`url(#${id}-gradient-0)`, select('#funnel path').attr('fill'));\n            });\n\n            it('should use solid fill when not set to \\'gradient\\'', () => {\n                getFunnel().draw(getBasicData());\n\n                // Check for valid hex string\n                assert.isTrue(/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(\n                    select('#funnel path').attr('fill'),\n                ));\n            });\n        });\n\n        describe('block.minHeight', () => {\n            it('should give each block the minimum height specified', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 299 },\n                    { label: 'B', value: 1 },\n                ], {\n                    chart: {\n                        height: 300,\n                    },\n                    block: {\n                        dynamicHeight: true,\n                        minHeight: 10,\n                    },\n                });\n\n                const paths = selectAll('#funnel path').nodes();\n\n                assert.isAbove(parseFloat(getPathHeight(select(paths[0]))), 10);\n                assert.isAbove(parseFloat(getPathHeight(select(paths[1]))), 10);\n            });\n\n            it('should decrease the height of blocks above the minimum', () => {\n                getFunnel().draw([\n                    { label: 'A', value: 299 },\n                    { label: 'B', value: 1 },\n                ], {\n                    chart: {\n                        height: 300,\n                    },\n                    block: {\n                        dynamicHeight: true,\n                        minHeight: 10,\n                    },\n                });\n\n                const paths = selectAll('#funnel path').nodes();\n\n                assert.isBelow(parseFloat(getPathHeight(select(paths[0]))), 290);\n            });\n        });\n\n        describe('block.highlight', () => {\n            it('should change block color on hover', () => {\n                const event = document.createEvent('CustomEvent');\n                event.initCustomEvent('mouseover', false, false, null);\n\n                getFunnel().draw([\n                    { label: 'A', value: 1, backgroundColor: '#fff' },\n                ], {\n                    block: {\n                        highlight: true,\n                    },\n                });\n\n                select('#funnel path').node().dispatchEvent(event);\n\n                // #fff * -1/5 => #cccccc\n                assert.equal('#cccccc', select('#funnel path').attr('fill'));\n            });\n        });\n\n        describe('label.enabled', () => {\n            it('should render block labels when set to true', () => {\n                getFunnel().draw(getBasicData(), {\n                    label: { enabled: true },\n                });\n\n                assert.equal(1, selectAll('#funnel text').size());\n            });\n\n            it('should not render block labels when set to false', () => {\n                getFunnel().draw(getBasicData(), {\n                    label: { enabled: false },\n                });\n\n                assert.equal(0, selectAll('#funnel text').size());\n            });\n        });\n\n        describe('label.fontFamily', () => {\n            it('should set the label\\'s font size to the specified amount', () => {\n                getFunnel().draw(getBasicData(), {\n                    label: {\n                        fontFamily: 'Open Sans',\n                    },\n                });\n\n                assert.equal('Open Sans', select('#funnel text').attr('font-family'));\n            });\n        });\n\n        describe('label.fontSize', () => {\n            it('should set the label\\'s font size to the specified amount', () => {\n                getFunnel().draw(getBasicData(), {\n                    label: {\n                        fontSize: '16px',\n                    },\n                });\n\n                assert.equal('16px', select('#funnel text').attr('font-size'));\n            });\n        });\n\n        describe('label.fill', () => {\n            it('should set the label\\'s fill color to the specified color', () => {\n                getFunnel().draw(getBasicData(), {\n                    label: {\n                        fill: '#777',\n                    },\n                });\n\n                assert.isTrue(select('#funnel text').attr('fill').indexOf('#777') > -1);\n            });\n        });\n\n        describe('label.format', () => {\n            it('should parse a string template', () => {\n                getFunnel().draw(getBasicData(), {\n                    label: {\n                        format: '{l} {v} {f}',\n                    },\n                });\n\n                assert.equal('Node 1000 1,000', select('#funnel text').text());\n            });\n\n            it('should create split multiple lines into multiple tspans', () => {\n                getFunnel().draw(getBasicData(), {\n                    label: {\n                        format: '{l}\\n{v}',\n                    },\n                });\n\n                const tspans = selectAll('#funnel text tspan').nodes();\n\n                assert.equal('Node', select(tspans[0]).text());\n                assert.equal('1000', select(tspans[1]).text());\n            });\n\n            it('should create position multiple lines in a vertically-centered manner', () => {\n                getFunnel().draw(getBasicData(), {\n                    chart: {\n                        height: 200,\n                    },\n                    label: {\n                        format: '{l}\\n{v}\\n{f}',\n                    },\n                });\n\n                const tspans = selectAll('#funnel text tspan').nodes();\n\n                assert.equal(-20, select(tspans[0]).attr('dy'));\n                assert.equal(20, select(tspans[1]).attr('dy'));\n                assert.equal(20, select(tspans[2]).attr('dy'));\n            });\n\n            it('should pass values to a supplied function', () => {\n                getFunnel().draw(getBasicData(), {\n                    label: {\n                        format: (label, value, formattedValue) => `${label}/${value}/${formattedValue}`,\n                    },\n                });\n\n                assert.equal('Node/1000/null', select('#funnel text').text());\n            });\n        });\n\n        describe('tooltip.enabled', () => {\n            it('should render a simple tooltip box when hovering over a block', () => {\n                const event = document.createEvent('CustomEvent');\n                event.initCustomEvent('mousemove', false, false, null);\n\n                getFunnel().draw(getBasicData(), {\n                    tooltip: {\n                        enabled: true,\n                    },\n                });\n\n                select('#funnel path').node().dispatchEvent(event);\n\n                assert.notEqual(null, select('#funnel .d3-funnel-tooltip').node());\n            });\n\n            it('should hide the tooltip on mouseout', () => {\n                const mouseMove = document.createEvent('CustomEvent');\n                const mouseOut = document.createEvent('CustomEvent');\n                mouseMove.initCustomEvent('mousemove', false, false, null);\n                mouseOut.initCustomEvent('mouseout', false, false, null);\n\n                getFunnel().draw(getBasicData(), {\n                    tooltip: {\n                        enabled: true,\n                    },\n                });\n\n                select('#funnel path').node().dispatchEvent(mouseMove);\n                select('#funnel path').node().dispatchEvent(mouseOut);\n\n                assert.equal(null, select('#funnel .d3-funnel-tooltip').node());\n            });\n        });\n\n        describe('tooltip.format', () => {\n            it('should render tooltips according to the format provided', () => {\n                const event = document.createEvent('CustomEvent');\n                event.initCustomEvent('mousemove', false, false, null);\n\n                getFunnel().draw(getBasicData(), {\n                    tooltip: {\n                        enabled: true,\n                        format: '{l} - {v}',\n                    },\n                });\n\n                select('#funnel path').node().dispatchEvent(event);\n\n                assert.equal('Node - 1000', select('#funnel .d3-funnel-tooltip').text());\n            });\n        });\n\n        describe('events.click.block', () => {\n            it('should invoke the callback function with the correct data', () => {\n                const event = document.createEvent('CustomEvent');\n                event.initCustomEvent('click', false, false, null);\n\n                const proxy = sinon.fake();\n\n                getFunnel().draw(getBasicData(), {\n                    events: {\n                        click: {\n                            block: (e, d) => {\n                                proxy({\n                                    index: d.index,\n                                    node: d.node,\n                                    label: d.label.raw,\n                                    value: d.value,\n                                });\n                            },\n                        },\n                    },\n                });\n\n                select('#funnel path').node().dispatchEvent(event);\n\n                assert.isTrue(proxy.calledWith({\n                    index: 0,\n                    node: select('#funnel path').node(),\n                    label: 'Node',\n                    value: 1000,\n                }));\n            });\n\n            it('should not trigger errors when null', () => {\n                const event = document.createEvent('CustomEvent');\n                event.initCustomEvent('click', false, false, null);\n\n                getFunnel().draw(getBasicData(), {\n                    events: {\n                        click: {\n                            block: null,\n                        },\n                    },\n                });\n\n                select('#funnel path').node().dispatchEvent(event);\n            });\n\n            it('should set the block style to `cursor: pointer` when non-null', () => {\n                getFunnel().draw(getBasicData(), {\n                    events: {\n                        click: {\n                            block: () => {\n                            },\n                        },\n                    },\n                });\n\n                assert.equal('pointer', select('#funnel path').style('cursor'));\n            });\n        });\n    });\n});\n"
  },
  {
    "path": "test/d3-funnel/Navigator.js",
    "content": "import { assert } from 'chai';\n\nimport Navigator from '../../src/d3-funnel/Navigator.js';\n\ndescribe('Navigator', () => {\n    describe('plot', () => {\n        it('should concatenate a list of path commands together', () => {\n            const commands = [\n                ['M', 0, 15],\n                ['', 5, 25],\n            ];\n\n            assert.equal('M0,15 5,25', (new Navigator()).plot(commands));\n        });\n    });\n});\n"
  },
  {
    "path": "test/d3-funnel/Utils.js",
    "content": "import { assert } from 'chai';\n\nimport Utils from '../../src/d3-funnel/Utils.js';\n\ndescribe('Utils', () => {\n    describe('extend', () => {\n        it('should override object a with the properties of object b', () => {\n            const a = {\n                name: 'Fluoride',\n            };\n\n            const b = {\n                name: 'Argon',\n                atomicNumber: 18,\n            };\n\n            assert.deepEqual(b, Utils.extend(a, b));\n        });\n\n        it('should add properties of object b to object a', () => {\n            const a = {\n                name: 'Alpha Centauri',\n            };\n\n            const b = {\n                distanceFromSol: 4.37,\n                stars: [{\n                    name: 'Alpha Centauri A',\n                }, {\n                    name: 'Alpha Centauri B',\n                }, {\n                    name: 'Proxima Centauri',\n                }],\n            };\n\n            const merged = {\n                name: 'Alpha Centauri',\n                distanceFromSol: 4.37,\n                stars: [{\n                    name: 'Alpha Centauri A',\n                }, {\n                    name: 'Alpha Centauri B',\n                }, {\n                    name: 'Proxima Centauri',\n                }],\n            };\n\n            assert.deepEqual(merged, Utils.extend(a, b));\n        });\n    });\n\n    describe('convertLegacyBlock', () => {\n        it('should translate a standard legacy block array into an object', () => {\n            const block = ['Terran', 200];\n            const { label, value } = Utils.convertLegacyBlock(block);\n\n            assert.deepEqual({ label: 'Terran', value: 200 }, { label, value });\n        });\n\n        it('should translate a formatted value', () => {\n            const block = ['Terran', [200, 'Two Hundred']];\n            const { formattedValue } = Utils.convertLegacyBlock(block);\n\n            assert.equal('Two Hundred', formattedValue);\n        });\n\n        it('should translate a background color', () => {\n            const block = ['Terran', 200, '#e5b81f'];\n            const { backgroundColor } = Utils.convertLegacyBlock(block);\n\n            assert.equal('#e5b81f', backgroundColor);\n        });\n\n        it('should translate a label color', () => {\n            const block = ['Terran', 200, null, '#e5b81f'];\n            const { labelColor } = Utils.convertLegacyBlock(block);\n\n            assert.equal('#e5b81f', labelColor);\n        });\n    });\n});\n"
  },
  {
    "path": "test/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <title>Mocha</title>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  </head>\n  <body>\n    <div id=\"funnel\"></div>\n    <div id=\"sandbox\"></div>\n    <div id=\"mocha\"></div>\n\n    <script src=\"../node_modules/mocha/mocha.js\"></script>\n    <script>\n        mocha.ui('bdd');\n        mocha.reporter('spec');\n        mocha.color(true);\n    </script>\n    <script src=\"./index.js\"></script>\n    <script>\n        mocha.run();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "test/index.js",
    "content": "// Because JSDom does not support SVGs properly this must run in a browser\n// https://github.com/jsdom/jsdom/issues/918\n\nimport './d3-funnel/Colorizer.js';\nimport './d3-funnel/D3Funnel.js';\nimport './d3-funnel/Navigator.js';\nimport './d3-funnel/Utils.js';\n"
  },
  {
    "path": "test/test.js",
    "content": "import path from 'node:path';\n// Use Firefox because it has the most consumable console pass through\nimport { firefox } from 'playwright';\n\nconst { dirname } = import.meta;\n\nfunction outputStream(out, stream) {\n    stream.forEach((message) => {\n        const formatted = `${message}\\n`;\n        out.write(formatted);\n    });\n}\n\nconst stream = [];\n\n(async () => {\n    const browser = await firefox.launch();\n    const page = await browser.newPage();\n    let hasError = false;\n\n    // Capture Mocha spec reporter messages\n    page.on('console', (message) => {\n        const type = message.type();\n        const text = message.text();\n\n        // Pass message as-is to output stream\n        stream.push(text);\n\n        // Identify failures\n        if (type === 'error' || text.includes('failing')) {\n            hasError = true;\n        }\n    });\n\n    // Identity hard runtime errors\n    page.on('pageerror', (error) => {\n        outputStream(process.stderr, [error]);\n        process.exit(1);\n    });\n\n    // Visit the page for any errors\n    await page.goto(`file:${path.join(dirname, 'compiled/index.html')}`, { waitUntil: 'networkidle' });\n    await browser.close();\n\n    // Output log stream;\n    // If the exit is non-zero, all output must go to stderr to be seen in Gulp\n    if (hasError) {\n        outputStream(process.stderr, stream);\n        process.exitCode = 1;\n    } else {\n        outputStream(process.stdout, stream);\n    }\n})();\n"
  },
  {
    "path": "webpack.config.examples.js",
    "content": "import HtmlBundlerPlugin from 'html-bundler-webpack-plugin';\nimport path from 'node:path';\n\nconst { dirname } = import.meta;\n\nexport default {\n    mode: 'development',\n    entry: {\n        index: path.join(dirname, 'examples/src/index.js'),\n        style: path.join(dirname, 'examples/src/scss/style.scss'),\n    },\n    output: {\n        path: path.join(dirname, 'examples/dist'),\n        library: {\n            name: 'D3Funnel',\n            type: 'umd',\n        },\n    },\n    resolve: {\n        extensions: ['.js'],\n        alias: {\n            'd3-funnel': path.resolve(dirname, 'src/index.js'),\n        },\n    },\n    module: {\n        rules: [\n            {\n                test: /\\.jsx?$/,\n                exclude: /(node_modules)/,\n                loader: 'babel-loader',\n            },\n            {\n                test: /\\.s[ac]ss$/i,\n                use: [\n                    'css-loader',\n                    'sass-loader',\n                ],\n            },\n        ],\n    },\n    devServer: {\n        open: true,\n        static: {\n            directory: path.join(dirname, 'examples/dist'),\n        },\n        watchFiles: ['src/**/*', 'examples/src/**/*'],\n    },\n    plugins: [\n        new HtmlBundlerPlugin({\n            entry: {\n                index: 'examples/src/index.html',\n            },\n            js: {\n                filename: '[name].[contenthash:8].js',\n            },\n            css: {\n                filename: '[name].[contenthash:8].css',\n            },\n        }),\n    ],\n};\n"
  },
  {
    "path": "webpack.config.js",
    "content": "import path from 'node:path';\nimport webpack from 'webpack';\nimport { readFile } from 'node:fs/promises';\n\nconst { dirname } = import.meta;\nconst json = await readFile(new URL('./package.json', import.meta.url));\nconst pkg = JSON.parse(json.toString());\nconst banner = `\n${pkg.name} - v${pkg.version}\nCopyright (c) ${pkg.author}\nLicensed under the ${pkg.license} License.\n`;\n\nconst commonConfig = {\n    target: 'web',\n    entry: path.join(dirname, 'src/index.js'),\n    resolve: {\n        extensions: ['.js'],\n    },\n    module: {\n        rules: [\n            {\n                test: /\\.js?$/,\n                exclude: /(node_modules)/,\n                loader: 'babel-loader',\n            },\n        ],\n    },\n    externals: {\n        // Do not compile d3 with the output\n        // In the browser, this allows window.d3 to be used\n        // In Node, this will use the included d3 package\n        d3: 'd3',\n    },\n    plugins: [\n        new webpack.BannerPlugin(banner.trim()),\n    ],\n};\n\nconst configMap = {\n    esm: {\n        ...commonConfig,\n        mode: 'none',\n        output: {\n            path: path.join(dirname, '/dist'),\n            filename: 'index.esm.js',\n            library: {\n                type: 'module',\n            },\n        },\n        experiments: {\n            outputModule: true,\n        },\n    },\n    umd: {\n        ...commonConfig,\n        mode: 'none',\n        output: {\n            path: path.join(dirname, '/dist'),\n            filename: 'index.cjs.js',\n            library: {\n                name: 'D3Funnel',\n                type: 'umd',\n                umdNamedDefine: true,\n            },\n        },\n    },\n    browser: {\n        ...commonConfig,\n        mode: 'production',\n        output: {\n            path: path.join(dirname, '/dist'),\n            filename: 'd3-funnel.min.js',\n            library: {\n                name: 'D3Funnel',\n                type: 'umd',\n                umdNamedDefine: true,\n            },\n        },\n    },\n};\n\nfunction makeConfig({ target }) {\n    return configMap[target];\n}\n\nexport default makeConfig;\n"
  },
  {
    "path": "webpack.config.test.js",
    "content": "import HtmlBundlerPlugin from 'html-bundler-webpack-plugin';\nimport path from 'node:path';\n\nconst { dirname } = import.meta;\n\nexport default {\n    mode: 'development',\n    entry: {\n        index: path.join(dirname, 'test/index.js'),\n    },\n    output: {\n        path: path.join(dirname, 'test/compiled'),\n        library: {\n            name: 'D3Funnel',\n            type: 'umd',\n        },\n    },\n    resolve: {\n        extensions: ['.js'],\n        alias: {\n            'd3-funnel': path.resolve(dirname, 'src/index.js'),\n        },\n    },\n    module: {\n        rules: [\n            {\n                test: /\\.jsx?$/,\n                exclude: /(node_modules)/,\n                loader: 'babel-loader',\n            },\n            {\n                test: /\\.s[ac]ss$/i,\n                use: [\n                    'css-loader',\n                    'sass-loader',\n                ],\n            },\n        ],\n    },\n    plugins: [\n        new HtmlBundlerPlugin({\n            entry: {\n                index: 'test/index.html',\n            },\n            js: {\n                filename: '[name].[contenthash:8].js',\n            },\n            css: {\n                filename: '[name].[contenthash:8].css',\n            },\n        }),\n    ],\n};\n"
  }
]