[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\n    [\n      \"@babel/preset-env\",\n      {\n        \"targets\": {\n          \"browsers\": [\"last 2 versions\", \"safari >= 7\"]\n        },\n        \"modules\": false\n      }\n    ]\n  ]\n}\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"env\": {\n    \"browser\": true,\n    \"es6\": true\n  },\n  \"extends\": \"eslint:recommended\",\n  \"parserOptions\": {\n    \"sourceType\": \"module\"\n  },\n  \"rules\": {\n    \"indent\": [\"error\", \"tab\"],\n    \"linebreak-style\": [\"error\", \"unix\"],\n    \"semi\": [\"error\", \"always\"],\n    \"no-console\": [\n      \"error\",\n      {\n        \"allow\": [\"warn\", \"error\"]\n      }\n    ]\n  },\n  \"globals\": {\n    \"ENV\": true\n  }\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "#### Expected Behaviour\n\n#### Actual Behaviour\n\n#### Steps to Reproduce:\n*\n\n\nNOTE: Add a GIF/Screenshot if required.\n\nFrappé Charts version:\nCodepen / Codesandbox:"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!-- Thank you so much for contributing! We're glad to have you onboard :) -->\n<!-- Please help us understand you contribution better with these details -->\n\n###### Explanation About What Code Achieves:\n<!-- Please explain why this code is necessary / what it does -->\n  - Explanation\n\n###### Screenshots/GIFs:\n<!-- As this is mainly a visual lib, please include a screenshot/gif if your contribution modifies on-screen components -->\n  - Screenshot\n\n###### Steps To Test:\n<!-- What would someone do to be able to see the effects of your code? -->\n  - Steps\n\n###### TODOs:\n<!-- Is there any tests or logic that isn't in the pr that you want the reviewer to know about? -->\n  - None\n"
  },
  {
    "path": ".github/workflows/npm-publish.yml",
    "content": "# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created\n# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages\n\nname: Node.js Package\n\non:\n  release:\n    types: [created]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 12\n      - run: npm ci\n      - run: npm build\n\n  publish-npm:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 12\n          registry-url: https://registry.npmjs.org/\n      - run: npm ci\n      - run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}}"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Typescript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# next.js build output\n.next\n\n# npm build output\ndist\ndocs\ndocs/assets/\n\n.DS_Store\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\n\nnode_js:\n  - \"6\"\n  - \"8\"\n\nbefore_install:\n  - make install\n\nscript:\n  - make test\n\nafter_success:\n  - make coveralls\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Prateeksha Singh\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "-include .env\n\nBASEDIR\t\t\t\t\t\t\t\t\t= $(realpath .)\n\nSRCDIR\t\t\t\t\t\t\t\t\t= $(BASEDIR)/src\nDISTDIR\t\t\t\t\t\t\t\t\t= $(BASEDIR)/dist\nDOCSDIR\t\t\t\t\t\t\t\t\t= $(BASEDIR)/docs\n\nPROJECT\t\t\t\t\t\t\t\t\t= frappe-charts\n\nNODEMOD\t\t\t\t\t\t\t\t\t= $(BASEDIR)/node_modules\nNODEBIN\t\t\t\t\t\t\t\t\t= $(NODEMOD)/.bin\n\nbuild: clean install\n\t$(NODEBIN)/rollup\t\t\t\t\t\t    \\\n\t\t--config $(BASEDIR)/rollup.config.js\t\\\n\t\t--watch=$(watch)\n\nclean:\n\trm -rf \t\t\t\t\t\t\t\t\t\t\\\n\t\t$(BASEDIR)/.nyc_output\t\t\t\t\t\\\n\t\t$(BASEDIR)/.yarn-error.log\n\n\tclear\n\ninstall.dep:\nifeq ($(shell command -v yarn),)\n\t@echo \"Installing yarn...\"\n\tnpm install -g yarn\nendif\n\ninstall: install.dep\n\tyarn --cwd $(BASEDIR)\n\ntest: clean\n\t$(NODEBIN)/cross-env\t\t\t\t\t\t\t\\\n\t\tNODE_ENV=test\t\t\t\t\t\t\t\t\\\n\t$(NODEBIN)/nyc\t\t\t\t\t\t\t\t\t\\\n\t$(NODEBIN)/mocha\t\t\t\t\t\t\t\t\\\n\t\t--require $(NODEMOD)/babel-register\t\t\t\\\n\t\t--recursive\t\t\t\t\t\t\t\t\t\\\n\t\t$(SRCDIR)/js/**/test/*.test.js\n\ncoveralls:\n\t$(NODEBIN)/nyc report --reporter text-lcov | $(NODEBIN)/coveralls\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\" markdown=\"1\">\n    \n<img width=\"80\" alt=\"charts-logo\" src=\"https://github.com/user-attachments/assets/37b7ffaf-8354-48f2-8b9c-fa04fae0135b\" />\n    \n# Frappe Charts\n**GitHub-inspired modern, intuitive and responsive charts with zero dependencies**\n\n<p align=\"center\">\n    <a href=\"https://bundlephobia.com/result?p=frappe-charts\">\n        <img src=\"https://img.shields.io/bundlephobia/minzip/frappe-charts\">\n    </a>\n</p>\n\n<img src=\".github/example.gif\">\n\n<div>\n\n[Explore Demos](https://frappe.io/charts) - [Edit at CodeSandbox](https://codesandbox.io/s/frappe-charts-demo-viqud) - [Documentation](https://frappe.io/charts/docs)  \n\n</div>\n\n</div>\n\n## Frappe Charts\nFrappe Charts is a simple charting library with a focus on a simple API. The design is inspired by various charts you see on GitHub.\n\n### Motivation\n\nERPNext needed a simple sales history graph for its user company master to help users track sales. While using c3.js for reports, the library didn’t align well with our product’s classic design. Existing JS libraries were either too complex or rigid in their structure and behavior. To address this, I decided to create a library for translating value pairs into relative shapes or positions, focusing on simplicity.\n\n### Key Features\n\n- **Variety of chart types**: Frappe Charts supports various chart types, including Axis Charts, Area and Trends, Bar, Line, Pie, Percentage, Mixed Axis, and Heatmap.\n- **Annotations and tooltips**: Charts can be annotated with x and y markers, regions, and tooltips for enhanced data context and clarity.\n- **Dynamic data handling**: Add, remove, or update individual data points in place, or refresh the entire dataset to reflect changes.\n- **Customizable configurations**: Flexible options like colors, animations, and custom titles allow for a highly personalized chart experience.\n\n## Usage\n\n```sh\nnpm install frappe-charts\n```\n\nImport in your project:\n```js\nimport { Chart } from 'frappe-charts'\n// or esm import\nimport { Chart } from 'frappe-charts/dist/frappe-charts.esm.js'\n// import css\nimport 'frappe-charts/dist/frappe-charts.min.css'\n```\n\nOr directly include script in your HTML\n\n```html\n<script src=\"https://unpkg.com/frappe-charts@1.6.1/dist/frappe-charts.min.umd.js\"></script>\n```\n\n\n```js\nconst data = {\n    labels: [\"12am-3am\", \"3am-6pm\", \"6am-9am\", \"9am-12am\",\n        \"12pm-3pm\", \"3pm-6pm\", \"6pm-9pm\", \"9am-12am\"\n    ],\n    datasets: [\n        {\n            name: \"Some Data\", chartType: \"bar\",\n            values: [25, 40, 30, 35, 8, 52, 17, -4]\n        },\n        {\n            name: \"Another Set\", chartType: \"line\",\n            values: [25, 50, -10, 15, 18, 32, 27, 14]\n        }\n    ]\n}\n\nconst chart = new frappe.Chart(\"#chart\", {  // or a DOM element,\n                                            // new Chart() in case of ES6 module with above usage\n    title: \"My Awesome Chart\",\n    data: data,\n    type: 'axis-mixed', // or 'bar', 'line', 'scatter', 'pie', 'percentage'\n    height: 250,\n    colors: ['#7cd6fd', '#743ee2']\n})\n```\n\n## Contributing\n\n1. Clone this repo.\n2. `cd` into project directory\n3. `npm install`\n4. `npm i npm-run-all -D` (*optional --> might be required for some developers*)\n5. `npm run dev`\n\n## Links\n\n- [Read the blog](https://medium.com/@pratu16x7/so-we-decided-to-create-our-own-charts-a95cb5032c97)\n\n\n<br>\n<br>\n<div align=\"center\">\n\t<a href=\"https://frappe.io\" target=\"_blank\">\n\t\t<picture>\n\t\t\t<source media=\"(prefers-color-scheme: dark)\" srcset=\"https://frappe.io/files/Frappe-white.png\">\n\t\t\t<img src=\"https://frappe.io/files/Frappe-black.png\" alt=\"Frappe Technologies\" height=\"28\"/>\n\t\t</picture>\n\t</a>\n</div>\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"frappe-charts\",\n\t\"version\": \"v1.6.3\",\n\t\"type\": \"module\",\n\t\"main\": \"dist/frappe-charts.esm.js\",\n\t\"module\": \"dist/frappe-charts.esm.js\",\n\t\"browser\": \"dist/frappe-charts.umd.js\",\n\t\"common\": \"dist/frappe-charts.cjs.js\",\n\t\"unnpkg\": \"dist/frappe-charts.umd.js\",\n\t\"description\": \"https://frappe.github.io/charts\",\n\t\"directories\": {\n\t\t\"doc\": \"docs\"\n\t},\n\t\"files\": [\n\t\t\"src\",\n\t\t\"dist\"\n\t],\n\t\"scripts\": {\n\t\t\"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n\t\t\"watch\": \"rollup -c --watch\",\n\t\t\"dev\": \"npm-run-all --parallel watch\",\n\t\t\"build\": \"rollup -c\"\n\t},\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git+https://github.com/frappe/charts.git\"\n\t},\n\t\"keywords\": [\n\t\t\"js\",\n\t\t\"charts\"\n\t],\n\t\"author\": \"Prateeksha Singh\",\n\t\"license\": \"MIT\",\n\t\"bugs\": {\n\t\t\"url\": \"https://github.com/frappe/charts/issues\"\n\t},\n\t\"homepage\": \"https://github.com/frappe/charts#readme\",\n\t\"devDependencies\": {\n\t\t\"@babel/core\": \"^7.10.5\",\n\t\t\"@babel/preset-env\": \"^7.10.4\",\n\t\t\"node-sass\": \"^8.0.0\",\n\t\t\"rollup\": \"^2.21.0\",\n\t\t\"rollup-plugin-babel\": \"^4.4.0\",\n\t\t\"rollup-plugin-bundle-size\": \"^1.0.3\",\n\t\t\"rollup-plugin-commonjs\": \"^10.1.0\",\n\t\t\"rollup-plugin-eslint\": \"^7.0.0\",\n\t\t\"rollup-plugin-postcss\": \"^3.1.3\",\n\t\t\"rollup-plugin-scss\": \"^2.5.0\",\n\t\t\"rollup-plugin-terser\": \"^6.1.0\"\n\t}\n}\n"
  },
  {
    "path": "rollup.config.js",
    "content": "import pkg from \"./package.json\";\n\nimport commonjs from \"rollup-plugin-commonjs\";\nimport babel from \"rollup-plugin-babel\";\nimport postcss from \"rollup-plugin-postcss\";\nimport scss from \"rollup-plugin-scss\";\nimport bundleSize from \"rollup-plugin-bundle-size\";\nimport { terser } from \"rollup-plugin-terser\";\n\nexport default [\n  // browser-friendly UMD build\n  {\n    input: \"src/js/index.js\",\n    output: {\n      sourcemap: true,\n      name: \"frappe\",\n      file: pkg.browser,\n      format: \"umd\",\n    },\n    plugins: [\n      commonjs(),\n      babel({\n        exclude: [\"node_modules/**\"],\n      }),\n      terser(),\n      scss({ output: \"dist/frappe-charts.min.css\" }),\n      bundleSize(),\n    ],\n  },\n\n  // CommonJS (for Node) and ES module (for bundlers) build.\n  {\n    input: \"src/js/chart.js\",\n    output: [\n      { file: pkg.common, format: \"cjs\", sourcemap: true },\n      { file: pkg.module, format: \"es\", sourcemap: true },\n    ],\n    plugins: [\n      babel({\n        exclude: [\"node_modules/**\"],\n      }),\n      terser(),\n      postcss(),\n      bundleSize(),\n    ],\n  },\n];\n"
  },
  {
    "path": "src/css/charts.scss",
    "content": ":root {\n  --charts-label-color: #313b44;\n  --charts-axis-line-color: #f4f5f6;\n\n  --charts-tooltip-title: var(--charts-label-color);\n  --charts-tooltip-label: var(--charts-label-color);\n  --charts-tooltip-value: #192734;\n  --charts-tooltip-bg: #ffffff;\n\n  --charts-stroke-width: 2px;\n  --charts-dataset-circle-stroke: #ffffff;\n  --charts-dataset-circle-stroke-width: var(--charts-stroke-width);\n\n  --charts-legend-label: var(--charts-label-color);\n  --charts-legend-value: var(--charts-label-color);\n}\n\n.chart-container {\n  position: relative;\n  /* for absolutely positioned tooltip */\n\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\",\n    \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n    sans-serif;\n\n  .axis,\n  .chart-label {\n    fill: var(--charts-label-color);\n\n    line {\n      stroke: var(--charts-axis-line-color);\n    }\n  }\n\n  .dataset-units {\n    circle {\n      stroke: var(--charts-dataset-circle-stroke);\n      stroke-width: var(--charts-dataset-circle-stroke-width);\n    }\n\n    path {\n      fill: none;\n      stroke-opacity: 1;\n      stroke-width: var(--charts-stroke-width);\n    }\n  }\n\n  .dataset-path {\n    stroke-width: var(--charts-stroke-width);\n  }\n\n  .path-group {\n    path {\n      fill: none;\n      stroke-opacity: 1;\n      stroke-width: var(--charts-stroke-width);\n    }\n  }\n\n  line.dashed {\n    stroke-dasharray: 5, 3;\n  }\n\n  .axis-line {\n    .specific-value {\n      text-anchor: start;\n    }\n\n    .y-line {\n      text-anchor: end;\n    }\n\n    .x-line {\n      text-anchor: middle;\n    }\n  }\n\n  .legend-dataset-label {\n    fill: var(--charts-legend-label);\n    font-weight: 600;\n  }\n\n  .legend-dataset-value {\n    fill: var(--charts-legend-value);\n  }\n}\n\n.graph-svg-tip {\n  position: absolute;\n  z-index: 99999;\n  padding: 10px;\n  font-size: 12px;\n  text-align: center;\n  background: var(--charts-tooltip-bg);\n  box-shadow: 0px 1px 4px rgba(17, 43, 66, 0.1),\n    0px 2px 6px rgba(17, 43, 66, 0.08),\n    0px 40px 30px -30px rgba(17, 43, 66, 0.1);\n  border-radius: 6px;\n\n  ul {\n    padding-left: 0;\n    display: flex;\n  }\n\n  ol {\n    padding-left: 0;\n    display: flex;\n  }\n\n  ul.data-point-list {\n    li {\n      min-width: 90px;\n      font-weight: 600;\n    }\n  }\n\n  .svg-pointer {\n    position: absolute;\n    height: 12px;\n    width: 12px;\n    border-radius: 2px;\n    background: var(--charts-tooltip-bg);\n    transform: rotate(45deg);\n    margin-top: -7px;\n    margin-left: -6px;\n  }\n\n  &.comparison {\n    text-align: left;\n    padding: 0px;\n    pointer-events: none;\n\n    .title {\n      display: block;\n      padding: 16px;\n      margin: 0;\n      color: var(--charts-tooltip-title);\n      font-weight: 600;\n      line-height: 1;\n      pointer-events: none;\n      text-transform: uppercase;\n\n      strong {\n        color: var(--charts-tooltip-value);\n      }\n    }\n\n    ul {\n      margin: 0;\n      white-space: nowrap;\n      list-style: none;\n\n      &.tooltip-grid {\n        display: grid;\n        grid-template-columns: repeat(4, minmax(0, 1fr));\n        gap: 5px;\n      }\n    }\n\n    li {\n      display: inline-block;\n      display: flex;\n      flex-direction: row;\n      font-weight: 600;\n      line-height: 1;\n\n      padding: 5px 15px 15px 15px;\n\n      .tooltip-legend {\n        height: 12px;\n        width: 12px;\n        margin-right: 8px;\n        border-radius: 2px;\n      }\n\n      .tooltip-label {\n        margin-top: 4px;\n        font-size: 11px;\n        max-width: 100px;\n\n        color: var(--fr-tooltip-label);\n        overflow: hidden;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n      }\n\n      .tooltip-value {\n        color: var(--charts-tooltip-value);\n      }\n    }\n  }\n}"
  },
  {
    "path": "src/css/chartsCss.js",
    "content": "export const CSSTEXT =\n  \".chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .legend-dataset-text{fill:#6c7680;font-weight:600}.graph-svg-tip{position:absolute;z-index:99999;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ul{padding-left:0;display:flex}.graph-svg-tip ol{padding-left:0;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;flex:1;font-weight:600}.graph-svg-tip strong{color:#dfe2e5;font-weight:600}.graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:' ';border:5px solid transparent;}.graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.graph-svg-tip.comparison li{display:inline-block;padding:5px 10px}\";\n"
  },
  {
    "path": "src/js/chart.js",
    "content": "import \"../css/charts.scss\";\n\nimport PercentageChart from \"./charts/PercentageChart\";\nimport PieChart from \"./charts/PieChart\";\nimport Heatmap from \"./charts/Heatmap\";\nimport AxisChart from \"./charts/AxisChart\";\nimport DonutChart from \"./charts/DonutChart\";\n\nconst chartTypes = {\n  bar: AxisChart,\n  line: AxisChart,\n  percentage: PercentageChart,\n  heatmap: Heatmap,\n  pie: PieChart,\n  donut: DonutChart,\n};\n\nfunction getChartByType(chartType = \"line\", parent, options) {\n  if (chartType === \"axis-mixed\") {\n    options.type = \"line\";\n    return new AxisChart(parent, options);\n  }\n\n  if (!chartTypes[chartType]) {\n    console.error(\"Undefined chart type: \" + chartType);\n    return;\n  }\n\n  return new chartTypes[chartType](parent, options);\n}\n\nclass Chart {\n  constructor(parent, options) {\n    return getChartByType(options.type, parent, options);\n  }\n}\n\nexport { Chart, PercentageChart, PieChart, Heatmap, AxisChart };\n"
  },
  {
    "path": "src/js/charts/AggregationChart.js",
    "content": "import BaseChart from \"./BaseChart\";\nimport { truncateString } from \"../utils/draw-utils\";\nimport { legendDot } from \"../utils/draw\";\nimport { round } from \"../utils/helpers\";\nimport { getExtraWidth } from \"../utils/constants\";\n\nexport default class AggregationChart extends BaseChart {\n  constructor(parent, args) {\n    super(parent, args);\n  }\n\n  configure(args) {\n    super.configure(args);\n\n    this.config.formatTooltipY = (args.tooltipOptions || {}).formatTooltipY;\n    this.config.maxSlices = args.maxSlices || 20;\n    this.config.maxLegendPoints = args.maxLegendPoints || 20;\n    this.config.legendRowHeight = 60;\n  }\n\n  calc() {\n    let s = this.state;\n    let maxSlices = this.config.maxSlices;\n    s.sliceTotals = [];\n\n    let allTotals = this.data.labels\n      .map((label, i) => {\n        let total = 0;\n        this.data.datasets.map((e) => {\n          total += e.values[i];\n        });\n        return [total, label];\n      })\n      .filter((d) => {\n        return d[0] >= 0;\n      }); // keep only positive results\n\n    let totals = allTotals;\n    if (allTotals.length > maxSlices) {\n      // Prune and keep a grey area for rest as per maxSlices\n      allTotals.sort((a, b) => {\n        return b[0] - a[0];\n      });\n\n      totals = allTotals.slice(0, maxSlices - 1);\n      let remaining = allTotals.slice(maxSlices - 1);\n\n      let sumOfRemaining = 0;\n      remaining.map((d) => {\n        sumOfRemaining += d[0];\n      });\n      totals.push([sumOfRemaining, \"Rest\"]);\n      this.colors[maxSlices - 1] = \"grey\";\n    }\n\n    s.labels = [];\n    totals.map((d) => {\n      s.sliceTotals.push(round(d[0]));\n      s.labels.push(d[1]);\n    });\n\n    s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0);\n\n    this.center = {\n      x: this.width / 2,\n      y: this.height / 2,\n    };\n  }\n\n  renderLegend() {\n    let s = this.state;\n    this.legendArea.textContent = \"\";\n    this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints);\n    super.renderLegend(this.legendTotals);\n  }\n\n  makeLegend(data, index, x_pos, y_pos) {\n    let formatted = this.config.formatTooltipY\n      ? this.config.formatTooltipY(data)\n      : data;\n\n    return legendDot(\n      x_pos,\n      y_pos,\n      12, // size\n      3, // dot radius\n      this.colors[index], // fill\n      this.state.labels[index], // label\n      formatted, // value\n      null, // base_font_size\n      this.config.truncateLegends // truncate_legends\n    );\n  }\n}\n"
  },
  {
    "path": "src/js/charts/AxisChart.js",
    "content": "import BaseChart from \"./BaseChart\";\nimport {\n\tdataPrep,\n\tzeroDataPrep,\n\tgetShortenedLabels,\n} from \"../utils/axis-chart-utils\";\nimport { getComponent } from \"../objects/ChartComponents\";\nimport { getOffset, fire } from \"../utils/dom\";\nimport {\n\tcalcChartIntervals,\n\tgetIntervalSize,\n\tgetValueRange,\n\tgetZeroIndex,\n\tscale,\n\tgetClosestInArray,\n} from \"../utils/intervals\";\nimport { floatTwo } from \"../utils/helpers\";\nimport { makeOverlay, updateOverlay, legendDot } from \"../utils/draw\";\nimport {\n\tgetTopOffset,\n\tgetLeftOffset,\n\tMIN_BAR_PERCENT_HEIGHT,\n\tBAR_CHART_SPACE_RATIO,\n\tLINE_CHART_DOT_SIZE,\n\tLEGEND_ITEM_WIDTH,\n} from \"../utils/constants\";\n\nexport default class AxisChart extends BaseChart {\n\tconstructor(parent, args) {\n\t\tsuper(parent, args);\n\n\t\tthis.barOptions = args.barOptions || {};\n\t\tthis.lineOptions = args.lineOptions || {};\n\n\t\tthis.type = args.type || \"line\";\n\t\tthis.init = 1;\n\n\t\tthis.setup();\n\t}\n\n\tsetMeasures() {\n\t\tif (this.data.datasets.length <= 1) {\n\t\t\tthis.config.showLegend = 0;\n\t\t\tthis.measures.paddings.bottom = 30;\n\t\t}\n\t}\n\n\tconfigure(options) {\n\t\tsuper.configure(options);\n\t\tconst { axisOptions = {} } = options;\n\t\tconst { xAxis, yAxis } = axisOptions || {};\n\n\t\toptions.tooltipOptions = options.tooltipOptions || {};\n\n\t\tthis.config.xAxisMode = xAxis\n\t\t\t? xAxis.xAxisMode\n\t\t\t: axisOptions.xAxisMode || \"span\";\n\n\t\t// this will pass an array\n\t\t// lets determine if we need two yAxis based on if there is length\n\t\t// to the yAxis array\n\t\tif (yAxis && yAxis.length) {\n\t\t\tthis.config.yAxisConfig = yAxis.map((item) => {\n\t\t\t\treturn {\n\t\t\t\t\tyAxisMode: item.yAxisMode,\n\t\t\t\t\tid: item.id,\n\t\t\t\t\tposition: item.position,\n\t\t\t\t\ttitle: item.title,\n\t\t\t\t};\n\t\t\t});\n\t\t} else {\n\t\t\tthis.config.yAxisMode = yAxis\n\t\t\t\t? yAxis.yAxisMode\n\t\t\t\t: axisOptions.yAxisMode || \"span\";\n\n\t\t\t// if we have yAxis config settings lets populate a yAxis config array.\n\t\t\tif (yAxis && yAxis.id && yAxis.position) {\n\t\t\t\tthis.config.yAxisConfig = [yAxis];\n\t\t\t}\n\t\t}\n\n\t\tthis.config.xIsSeries = axisOptions.xIsSeries || 0;\n\t\tthis.config.shortenYAxisNumbers = axisOptions.shortenYAxisNumbers || 0;\n\n\t\tthis.config.formatTooltipX = options.tooltipOptions.formatTooltipX;\n\t\tthis.config.formatTooltipY = options.tooltipOptions.formatTooltipY;\n\n\t\tthis.config.valuesOverPoints = options.valuesOverPoints;\n\t\tthis.config.legendRowHeight = 30;\n\t}\n\n\tprepareData(data = this.data, config = this.config) {\n\t\treturn dataPrep(data, this.type, config.continuous);\n\t}\n\n\tprepareFirstData(data = this.data) {\n\t\treturn zeroDataPrep(data);\n\t}\n\n\tcalc(onlyWidthChange = false) {\n\t\tthis.calcXPositions();\n\t\tif (!onlyWidthChange) {\n\t\t\tthis.calcYAxisParameters(\n\t\t\t\tthis.getAllYValues(),\n\t\t\t\tthis.type === \"line\"\n\t\t\t);\n\t\t}\n\t\tthis.makeDataByIndex();\n\t}\n\n\tcalcXPositions() {\n\t\tlet s = this.state;\n\t\tlet labels = this.data.labels;\n\t\ts.datasetLength = labels.length;\n\n\t\ts.unitWidth = this.width / s.datasetLength;\n\t\t// Default, as per bar, and mixed. Only line will be a special case\n\t\ts.xOffset = s.unitWidth / 2;\n\n\t\t// // For a pure Line Chart\n\t\t// s.unitWidth = this.width/(s.datasetLength - 1);\n\t\t// s.xOffset = 0;\n\n\t\ts.xAxis = {\n\t\t\tlabels: labels,\n\t\t\tpositions: labels.map((d, i) =>\n\t\t\t\tfloatTwo(s.xOffset + i * s.unitWidth)\n\t\t\t),\n\t\t};\n\t}\n\n\tcalcYAxisParameters(dataValues, withMinimum = \"false\") {\n\t\tlet yPts,\n\t\t\tscaleMultiplier,\n\t\t\tintervalHeight,\n\t\t\tzeroLine,\n\t\t\tpositions,\n\t\t\tyAxisConfigObject,\n\t\t\tyAxisAlignment,\n\t\t\tyKeys;\n\n\t\tyKeys = [];\n\t\tyAxisConfigObject = this.config.yAxisMode || {};\n\t\tyAxisAlignment = yAxisConfigObject.position\n\t\t\t? yAxisConfigObject.position\n\t\t\t: \"left\";\n\n\t\t// if we have an object we have multiple yAxisParameters.\n\t\tif (dataValues instanceof Array) {\n\t\t\tyPts = calcChartIntervals(dataValues, withMinimum, this.config.overrideCeiling, this.config.overrideFloor);\n\t\t\tscaleMultiplier = this.height / getValueRange(yPts);\n\t\t\tintervalHeight = getIntervalSize(yPts) * scaleMultiplier;\n\t\t\tzeroLine = this.height - getZeroIndex(yPts) * intervalHeight;\n\n\t\t\tthis.state.yAxis = {\n\t\t\t\tlabels: yPts,\n\t\t\t\tpositions: yPts.map((d) => zeroLine - d * scaleMultiplier),\n\t\t\t\ttitle: yAxisConfigObject.title || null,\n\t\t\t\tpos: yAxisAlignment,\n\t\t\t\tscaleMultiplier: scaleMultiplier,\n\t\t\t\tzeroLine: zeroLine,\n\t\t\t};\n\t\t} else {\n\t\t\tthis.state.yAxis = [];\n\t\t\tfor (let key in dataValues) {\n\t\t\t\tconst dataValue = dataValues[key];\n\t\t\t\tyAxisConfigObject =\n\t\t\t\t\tthis.config.yAxisConfig.find((item) => key === item.id) ||\n\t\t\t\t\t[];\n\t\t\t\tyAxisAlignment = yAxisConfigObject.position\n\t\t\t\t\t? yAxisConfigObject.position\n\t\t\t\t\t: \"left\";\n\t\t\t\tyPts = calcChartIntervals(dataValue, withMinimum, this.config.overrideCeiling, this.config.overrideFloor);\n\t\t\t\tscaleMultiplier = this.height / getValueRange(yPts);\n\t\t\t\tintervalHeight = getIntervalSize(yPts) * scaleMultiplier;\n\t\t\t\tzeroLine = this.height - getZeroIndex(yPts) * intervalHeight;\n\t\t\t\tpositions = yPts.map((d) => zeroLine - d * scaleMultiplier);\n\t\t\t\tyKeys.push(key);\n\n\t\t\t\tif (this.state.yAxis.length > 1) {\n\t\t\t\t\tconst yPtsArray = [];\n\t\t\t\t\tconst firstArr = this.state.yAxis[0];\n\n\t\t\t\t\t// we need to calculate the scaleMultiplier.\n\n\t\t\t\t\t// now that we have an accurate scaleMultiplier we can\n\t\t\t\t\t// we need to loop through original positions.\n\t\t\t\t\tscaleMultiplier = this.height / getValueRange(yPts);\n\t\t\t\t\tfirstArr.positions.forEach((pos) => {\n\t\t\t\t\t\tyPtsArray.push(Math.ceil(pos / scaleMultiplier));\n\t\t\t\t\t});\n\t\t\t\t\tyPts = yPtsArray.reverse();\n\t\t\t\t\tzeroLine =\n\t\t\t\t\t\tthis.height - getZeroIndex(yPts) * intervalHeight;\n\t\t\t\t\tpositions = firstArr.positions;\n\t\t\t\t}\n\n\t\t\t\tthis.state.yAxis.push({\n\t\t\t\t\taxisID: key || \"left-axis\",\n\t\t\t\t\tlabels: yPts,\n\t\t\t\t\ttitle: yAxisConfigObject.title,\n\t\t\t\t\tpos: yAxisAlignment,\n\t\t\t\t\tscaleMultiplier,\n\t\t\t\t\tzeroLine,\n\t\t\t\t\tpositions,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// the labels are not aligned in length between the two yAxis objects,\n\t\t\t// we need to run some new calculations.\n\t\t\tif (\n\t\t\t\tthis.state.yAxis[1] &&\n\t\t\t\tthis.state.yAxis[0].labels.length !==\n\t\t\t\t\tthis.state.yAxis[1].labels.length\n\t\t\t) {\n\t\t\t\tconst newYptsArr = [];\n\t\t\t\t// find the shorter array\n\t\t\t\tconst shortest = this.state.yAxis.reduce(\n\t\t\t\t\t(p, c) => {\n\t\t\t\t\t\treturn p.length > c.labels.length ? c : p;\n\t\t\t\t\t},\n\t\t\t\t\t{ length: Infinity }\n\t\t\t\t);\n\t\t\t\t// return the longest\n\t\t\t\tconst longest = this.state.yAxis.reduce(\n\t\t\t\t\t(p, c) => {\n\t\t\t\t\t\treturn p.length < c.labels.length ? p : c;\n\t\t\t\t\t},\n\t\t\t\t\t{ length: Infinity }\n\t\t\t\t);\n\n\t\t\t\t// we now need to populate the shortest obj with the new scale multiplier\n\t\t\t\t// with the positions of the longest obj.\n\t\t\t\tlongest.positions.forEach((pos) => {\n\t\t\t\t\t// calculate a new yPts\n\t\t\t\t\tnewYptsArr.push(Math.ceil(pos / shortest.scaleMultiplier));\n\t\t\t\t});\n\n\t\t\t\tshortest.labels = newYptsArr.reverse();\n\t\t\t\tshortest.positions = longest.positions;\n\t\t\t}\n\t\t}\n\n\t\t// Dependent if above changes\n\t\tthis.calcDatasetPoints();\n\t\tthis.calcYExtremes();\n\t\tthis.calcYRegions();\n\t}\n\n\tcalcDatasetPoints() {\n\t\tlet s = this.state;\n\t\tlet scaleAll = (values, id) => {\n\t\t\treturn values.map((val) => {\n\t\t\t\tlet { yAxis } = s;\n\n\t\t\t\tif (yAxis instanceof Array) {\n\t\t\t\t\tyAxis =\n\t\t\t\t\t\tyAxis.length > 1\n\t\t\t\t\t\t\t? yAxis.find((axis) => id === axis.axisID)\n\t\t\t\t\t\t\t: s.yAxis[0];\n\t\t\t\t}\n\n\t\t\t\treturn scale(val, yAxis);\n\t\t\t});\n\t\t};\n\n\t\ts.barChartIndex = 1;\n\t\ts.datasets = this.data.datasets.map((d, i) => {\n\t\t\tlet values = d.values;\n\t\t\tlet cumulativeYs = d.cumulativeYs || [];\n\n\t\t\treturn {\n\t\t\t\tname:\n\t\t\t\t\td.name &&\n\t\t\t\t\td.name.replace(/<|>|&/g, (char) =>\n\t\t\t\t\t\tchar == \"&\" ? \"&amp;\" : char == \"<\" ? \"&lt;\" : \"&gt;\"\n\t\t\t\t\t),\n\t\t\t\tindex: i,\n\t\t\t\tbarIndex:\n\t\t\t\t\td.chartType === \"bar\" ? s.barChartIndex++ : s.barChartIndex,\n\t\t\t\tchartType: d.chartType,\n\n\t\t\t\tvalues: values,\n\t\t\t\tyPositions: scaleAll(values, d.axisID),\n\t\t\t\tid: d.axisID,\n\n\t\t\t\tcumulativeYs: cumulativeYs,\n\t\t\t\tcumulativeYPos: scaleAll(cumulativeYs, d.axisID),\n\t\t\t};\n\t\t});\n\t}\n\n\tcalcYExtremes() {\n\t\tlet s = this.state;\n\t\tif (this.barOptions.stacked) {\n\t\t\ts.yExtremes = s.datasets[s.datasets.length - 1].cumulativeYPos;\n\t\t\treturn;\n\t\t}\n\t\ts.yExtremes = new Array(s.datasetLength).fill(9999);\n\t\ts.datasets.map((d) => {\n\t\t\td.yPositions.map((pos, j) => {\n\t\t\t\tif (pos < s.yExtremes[j]) {\n\t\t\t\t\ts.yExtremes[j] = pos;\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\tcalcYRegions() {\n\t\tlet s = this.state;\n\t\tif (this.data.yMarkers) {\n\t\t\tthis.state.yMarkers = this.data.yMarkers.map((d) => {\n\t\t\t\td.position = scale(d.value, s.yAxis);\n\t\t\t\tif (!d.options) d.options = {};\n\t\t\t\t// if(!d.label.includes(':')) {\n\t\t\t\t// \td.label += ': ' + d.value;\n\t\t\t\t// }\n\t\t\t\treturn d;\n\t\t\t});\n\t\t}\n\t\tif (this.data.yRegions) {\n\t\t\tthis.state.yRegions = this.data.yRegions.map((d) => {\n\t\t\t\td.startPos = scale(d.start, s.yAxis);\n\t\t\t\td.endPos = scale(d.end, s.yAxis);\n\t\t\t\tif (!d.options) d.options = {};\n\t\t\t\treturn d;\n\t\t\t});\n\t\t}\n\t}\n\n\tgetAllYValues() {\n\t\tlet key = \"values\";\n\t\tlet multiAxis = this.config.yAxisConfig ? true : false;\n\t\tlet allValueLists = multiAxis ? {} : [];\n\n\t\tlet groupBy = (arr, property) => {\n\t\t\treturn arr.reduce((acc, cur) => {\n\t\t\t\tacc[cur[property]] = [...(acc[cur[property]] || []), cur];\n\t\t\t\treturn acc;\n\t\t\t}, {});\n\t\t};\n\n\t\tlet generateCumulative = (arr) => {\n\t\t\tlet cumulative = new Array(this.state.datasetLength).fill(0);\n\t\t\tarr.forEach((d, i) => {\n\t\t\t\tlet values = arr[i].values;\n\t\t\t\td[key] = cumulative = cumulative.map((c, i) => {\n\t\t\t\t\treturn c + values[i];\n\t\t\t\t});\n\t\t\t});\n\t\t};\n\n\t\tif (this.barOptions.stacked) {\n\t\t\tkey = \"cumulativeYs\";\n\t\t\t// we need to filter out the different yAxis ID's here.\n\t\t\tif (multiAxis) {\n\t\t\t\tconst groupedDataSets = groupBy(this.data.datasets, \"axisID\");\n\t\t\t\t// const dataSetsByAxis = this.data.dd\n\t\t\t\tfor (var axisID in groupedDataSets) {\n\t\t\t\t\tgenerateCumulative(groupedDataSets[axisID]);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tgenerateCumulative(this.data.datasets);\n\t\t\t}\n\t\t}\n\n\t\t// this is the trouble maker, we don't want to merge all\n\t\t// datasets since we are trying to run two yAxis.\n\t\tif (multiAxis) {\n\t\t\tthis.data.datasets.forEach((d) => {\n\t\t\t\t// if the array exists already just push more data into it.\n\t\t\t\t// otherwise create a new array into the object.\n\t\t\t\tallValueLists[d.axisID || key]\n\t\t\t\t\t? allValueLists[d.axisID || key].push(...d[key])\n\t\t\t\t\t: (allValueLists[d.axisID || key] = [...d[key]]);\n\t\t\t});\n\t\t} else {\n\t\t\tallValueLists = this.data.datasets.map((d) => {\n\t\t\t\treturn d[key];\n\t\t\t});\n\t\t}\n\n\t\tif (this.data.yMarkers && !multiAxis) {\n\t\t\tallValueLists.push(this.data.yMarkers.map((d) => d.value));\n\t\t}\n\n\t\tif (this.data.yRegions && !multiAxis) {\n\t\t\tthis.data.yRegions.map((d) => {\n\t\t\t\tallValueLists.push([d.end, d.start]);\n\t\t\t});\n\t\t}\n\n\t\treturn multiAxis ? allValueLists : [].concat(...allValueLists); //return [].concat(...allValueLists); master\n\t}\n\n\tsetupComponents() {\n\t\tlet componentConfigs = [\n\t\t\t[\n\t\t\t\t\"xAxis\",\n\t\t\t\t{\n\t\t\t\t\tmode: this.config.xAxisMode,\n\t\t\t\t\theight: this.height,\n\t\t\t\t\t// pos: 'right'\n\t\t\t\t},\n\t\t\t\tfunction () {\n\t\t\t\t\tlet s = this.state;\n\t\t\t\t\ts.xAxis.calcLabels = getShortenedLabels(\n\t\t\t\t\t\tthis.width,\n\t\t\t\t\t\ts.xAxis.labels,\n\t\t\t\t\t\tthis.config.xIsSeries\n\t\t\t\t\t);\n\n\t\t\t\t\treturn s.xAxis;\n\t\t\t\t}.bind(this),\n\t\t\t],\n\n\t\t\t[\n\t\t\t\t\"yRegions\",\n\t\t\t\t{\n\t\t\t\t\twidth: this.width,\n\t\t\t\t\tpos: \"right\",\n\t\t\t\t},\n\t\t\t\tfunction () {\n\t\t\t\t\treturn this.state.yRegions;\n\t\t\t\t}.bind(this),\n\t\t\t],\n\t\t];\n\n\t\t// if we have multiple yAxisConfigs we need to update the yAxisDefault\n\t\t// components to multiple yAxis components.\n\t\tif (this.config.yAxisConfig && this.config.yAxisConfig.length) {\n\t\t\tthis.config.yAxisConfig.forEach((yAxis) => {\n\t\t\t\tcomponentConfigs.push([\n\t\t\t\t\t\"yAxis\",\n\t\t\t\t\t{\n\t\t\t\t\t\tmode: yAxis.yAxisMode || \"span\",\n\t\t\t\t\t\twidth: this.width,\n\t\t\t\t\t\theight: this.baseHeight,\n\t\t\t\t\t\tshortenNumbers: this.config.shortenYAxisNumbers,\n\t\t\t\t\t\tpos: yAxis.position || \"left\",\n\t\t\t\t\t},\n\t\t\t\t\tfunction () {\n\t\t\t\t\t\treturn this.state.yAxis;\n\t\t\t\t\t}.bind(this),\n\t\t\t\t]);\n\t\t\t});\n\t\t} else {\n\t\t\tcomponentConfigs.push([\n\t\t\t\t\"yAxis\",\n\t\t\t\t{\n\t\t\t\t\tmode: this.config.yAxisMode,\n\t\t\t\t\twidth: this.width,\n\t\t\t\t\theight: this.baseHeight,\n\t\t\t\t\tshortenNumbers: this.config.shortenYAxisNumbers,\n\t\t\t\t},\n\t\t\t\tfunction () {\n\t\t\t\t\treturn this.state.yAxis;\n\t\t\t\t}.bind(this),\n\t\t\t]);\n\t\t}\n\n\t\tlet barDatasets = this.state.datasets.filter(\n\t\t\t(d) => d.chartType === \"bar\"\n\t\t);\n\t\tlet lineDatasets = this.state.datasets.filter(\n\t\t\t(d) => d.chartType === \"line\"\n\t\t);\n\n\t\tlet barsConfigs = barDatasets.map((d) => {\n\t\t\tlet index = d.index;\n\t\t\tlet barIndex = d.barIndex || index;\n\t\t\treturn [\n\t\t\t\t\"barGraph\" + \"-\" + d.index,\n\t\t\t\t{\n\t\t\t\t\tindex: index,\n\t\t\t\t\tcolor: this.colors[index],\n\t\t\t\t\tstacked: this.barOptions.stacked,\n\n\t\t\t\t\t// same for all datasets\n\t\t\t\t\tvaluesOverPoints: this.config.valuesOverPoints,\n\t\t\t\t\tminHeight: this.height * MIN_BAR_PERCENT_HEIGHT,\n\t\t\t\t},\n\t\t\t\tfunction () {\n\t\t\t\t\tlet s = this.state;\n\t\t\t\t\tlet { yAxis } = s;\n\t\t\t\t\tlet d = s.datasets[index];\n\t\t\t\t\tlet { id = \"left-axis\" } = d;\n\t\t\t\t\tlet stacked = this.barOptions.stacked;\n\n\t\t\t\t\tlet spaceRatio =\n\t\t\t\t\t\tthis.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO;\n\t\t\t\t\tlet barsWidth = s.unitWidth * (1 - spaceRatio);\n\t\t\t\t\tlet barWidth =\n\t\t\t\t\t\tbarsWidth / (stacked ? 1 : barDatasets.length);\n\n\t\t\t\t\t// if there are multiple yAxis we need to return the yAxis with the\n\t\t\t\t\t// proper ID.\n\t\t\t\t\tif (yAxis instanceof Array) {\n\t\t\t\t\t\t// if the person only configured one yAxis in the array return the first.\n\t\t\t\t\t\tyAxis =\n\t\t\t\t\t\t\tyAxis.length > 1\n\t\t\t\t\t\t\t\t? yAxis.find((axis) => id === axis.axisID)\n\t\t\t\t\t\t\t\t: s.yAxis[0];\n\t\t\t\t\t}\n\n\t\t\t\t\tlet xPositions = s.xAxis.positions.map(\n\t\t\t\t\t\t(x) => x - barsWidth / 2\n\t\t\t\t\t);\n\n\t\t\t\t\tif (!stacked) {\n\t\t\t\t\t\txPositions = xPositions.map(\n\t\t\t\t\t\t\t(p) => p + barWidth * barIndex - barWidth\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\tlet labels = new Array(s.datasetLength).fill(\"\");\n\t\t\t\t\tif (this.config.valuesOverPoints) {\n\t\t\t\t\t\tif (stacked && d.index === s.datasets.length - 1) {\n\t\t\t\t\t\t\tlabels = d.cumulativeYs;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlabels = d.values;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tlet offsets = new Array(s.datasetLength).fill(0);\n\t\t\t\t\tif (stacked) {\n\t\t\t\t\t\toffsets = d.yPositions.map(\n\t\t\t\t\t\t\t(y, j) => y - d.cumulativeYPos[j]\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\txPositions: xPositions,\n\t\t\t\t\t\tyPositions: d.yPositions,\n\t\t\t\t\t\toffsets: offsets,\n\t\t\t\t\t\t// values: d.values,\n\t\t\t\t\t\tlabels: labels,\n\n\t\t\t\t\t\tzeroLine: yAxis.zeroLine,\n\t\t\t\t\t\tbarsWidth: barsWidth,\n\t\t\t\t\t\tbarWidth: barWidth,\n\t\t\t\t\t};\n\t\t\t\t}.bind(this),\n\t\t\t];\n\t\t});\n\n\t\tlet lineConfigs = lineDatasets.map((d) => {\n\t\t\tlet index = d.index;\n\t\t\treturn [\n\t\t\t\t\"lineGraph\" + \"-\" + d.index,\n\t\t\t\t{\n\t\t\t\t\tindex: index,\n\t\t\t\t\tcolor: this.colors[index],\n\t\t\t\t\tsvgDefs: this.svgDefs,\n\t\t\t\t\theatline: this.lineOptions.heatline,\n\t\t\t\t\tregionFill: this.lineOptions.regionFill,\n\t\t\t\t\tspline: this.lineOptions.spline,\n\t\t\t\t\thideDots: this.lineOptions.hideDots,\n\t\t\t\t\thideLine: this.lineOptions.hideLine,\n\n\t\t\t\t\t// same for all datasets\n\t\t\t\t\tvaluesOverPoints: this.config.valuesOverPoints,\n\t\t\t\t},\n\t\t\t\tfunction () {\n\t\t\t\t\tlet s = this.state;\n\t\t\t\t\tlet d = s.datasets[index];\n\n\t\t\t\t\t// if we have more than one yindex lets map the values\n\t\t\t\t\tconst yAxis = s.yAxis.length\n\t\t\t\t\t\t? s.yAxis.find((axis) => d.id === axis.axisID) ||\n\t\t\t\t\t\t  s.yAxis[0]\n\t\t\t\t\t\t: s.yAxis;\n\n\t\t\t\t\tlet minLine =\n\t\t\t\t\t\tyAxis.positions[0] < yAxis.zeroLine\n\t\t\t\t\t\t\t? yAxis.positions[0]\n\t\t\t\t\t\t\t: yAxis.zeroLine;\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\txPositions: s.xAxis.positions,\n\t\t\t\t\t\tyPositions: d.yPositions,\n\n\t\t\t\t\t\tvalues: d.values,\n\n\t\t\t\t\t\tzeroLine: minLine,\n\t\t\t\t\t\tradius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE,\n\t\t\t\t\t};\n\t\t\t\t}.bind(this),\n\t\t\t];\n\t\t});\n\n\t\tlet markerConfigs = [\n\t\t\t[\n\t\t\t\t\"yMarkers\",\n\t\t\t\t{\n\t\t\t\t\twidth: this.width,\n\t\t\t\t\tpos: \"right\",\n\t\t\t\t},\n\t\t\t\tfunction () {\n\t\t\t\t\treturn this.state.yMarkers;\n\t\t\t\t}.bind(this),\n\t\t\t],\n\t\t];\n\n\t\tcomponentConfigs = componentConfigs.concat(\n\t\t\tbarsConfigs,\n\t\t\tlineConfigs,\n\t\t\tmarkerConfigs\n\t\t);\n\n\t\tlet optionals = [\"yMarkers\", \"yRegions\"];\n\t\tthis.dataUnitComponents = [];\n\n\t\tthis.components = new Map(\n\t\t\tcomponentConfigs\n\t\t\t\t.filter(\n\t\t\t\t\t(args) =>\n\t\t\t\t\t\t!optionals.includes(args[0]) || this.state[args[0]]\n\t\t\t\t)\n\t\t\t\t.map((args) => {\n\t\t\t\t\tlet component = getComponent(...args);\n\t\t\t\t\tif (\n\t\t\t\t\t\targs[0].includes(\"lineGraph\") ||\n\t\t\t\t\t\targs[0].includes(\"barGraph\")\n\t\t\t\t\t) {\n\t\t\t\t\t\tthis.dataUnitComponents.push(component);\n\t\t\t\t\t}\n\t\t\t\t\treturn [args[0], component];\n\t\t\t\t})\n\t\t);\n\t}\n\n\tmakeDataByIndex() {\n\t\tthis.dataByIndex = {};\n\n\t\tlet s = this.state;\n\t\tlet formatX = this.config.formatTooltipX;\n\t\tlet formatY = this.config.formatTooltipY;\n\t\tlet titles = s.xAxis.labels;\n\n\t\ttitles.map((label, index) => {\n\t\t\tlet values = this.state.datasets.map((set, i) => {\n\t\t\t\tlet value = set.values[index];\n\t\t\t\treturn {\n\t\t\t\t\ttitle: set.name,\n\t\t\t\t\tvalue: value,\n\t\t\t\t\tyPos: set.yPositions[index],\n\t\t\t\t\tcolor: this.colors[i],\n\t\t\t\t\tformatted: formatY ? formatY(value) : value,\n\t\t\t\t};\n\t\t\t});\n\n\t\t\tthis.dataByIndex[index] = {\n\t\t\t\tlabel: label,\n\t\t\t\tformattedLabel: formatX ? formatX(label) : label,\n\t\t\t\txPos: s.xAxis.positions[index],\n\t\t\t\tvalues: values,\n\t\t\t\tyExtreme: s.yExtremes[index],\n\t\t\t};\n\t\t});\n\t}\n\n\tbindTooltip() {\n\t\t// NOTE: could be in tooltip itself, as it is a given functionality for its parent\n\t\tthis.container.addEventListener(\"mousemove\", (e) => {\n\t\t\tlet m = this.measures;\n\t\t\tlet o = getOffset(this.container);\n\t\t\tlet relX = e.pageX - o.left - getLeftOffset(m);\n\t\t\tlet relY = e.pageY - o.top;\n\n\t\t\tif (\n\t\t\t\trelY < this.height + getTopOffset(m) &&\n\t\t\t\trelY > getTopOffset(m)\n\t\t\t) {\n\t\t\t\tthis.mapTooltipXPosition(relX);\n\t\t\t} else {\n\t\t\t\tthis.tip.hideTip();\n\t\t\t}\n\t\t});\n\t}\n\n\tmapTooltipXPosition(relX) {\n\t\tlet s = this.state;\n\t\tif (!s.yExtremes) return;\n\n\t\tlet index = getClosestInArray(relX, s.xAxis.positions, true);\n\t\tif (index >= 0) {\n\t\t\tlet dbi = this.dataByIndex[index];\n\n\t\t\tthis.tip.setValues(\n\t\t\t\tdbi.xPos + this.tip.offset.x,\n\t\t\t\tdbi.yExtreme + this.tip.offset.y,\n\t\t\t\t{ name: dbi.formattedLabel, value: \"\" },\n\t\t\t\tdbi.values,\n\t\t\t\tindex\n\t\t\t);\n\n\t\t\tthis.tip.showTip();\n\t\t}\n\t}\n\n\trenderLegend() {\n\t\tlet s = this.data;\n\t\tif (s.datasets.length > 1) {\n\t\t\tsuper.renderLegend(s.datasets);\n\t\t}\n\t}\n\n\t// Legacy\n\t/* \trenderLegend() {\n\t\tlet s = this.data;\n\t\tif (s.datasets.length > 1) {\n\t\t\tthis.legendArea.textContent = \"\";\n\t\t\tconsole.log(s.datasets);\n\t\t\ts.datasets.map((d, i) => {\n\t\t\t\tlet barWidth = LEGEND_ITEM_WIDTH;\n\t\t\t\t// let rightEndPoint = this.baseWidth - this.measures.margins.left - this.measures.margins.right;\n\t\t\t\t// let multiplier = s.datasets.length - i;\n\t\t\t\tlet rect = legendBar(\n\t\t\t\t\t// rightEndPoint - multiplier * barWidth,\t// To right align\n\t\t\t\t\tbarWidth * i,\n\t\t\t\t\t\"0\",\n\t\t\t\t\tbarWidth,\n\t\t\t\t\tthis.colors[i],\n\t\t\t\t\td.name,\n\t\t\t\t\tthis.config.truncateLegends\n\t\t\t\t);\n\t\t\t\tthis.legendArea.appendChild(rect);\n\t\t\t});\n\t\t}\n\t} */\n\n\tmakeLegend(data, index, x_pos, y_pos) {\n\t\treturn legendDot(\n\t\t\tx_pos,\n\t\t\ty_pos + 5, // Extra offset\n\t\t\t12, // size\n\t\t\t3, // dot radius\n\t\t\tthis.colors[index], // fill\n\t\t\tdata.name, //label\n\t\t\tnull, // value\n\t\t\t8.75, // base_font_size\n\t\t\tthis.config.truncateLegends // truncate legends\n\t\t);\n\t}\n\n\t// Overlay\n\tmakeOverlay() {\n\t\tif (this.init) {\n\t\t\tthis.init = 0;\n\t\t\treturn;\n\t\t}\n\t\tif (this.overlayGuides) {\n\t\t\tthis.overlayGuides.forEach((g) => {\n\t\t\t\tlet o = g.overlay;\n\t\t\t\to.parentNode.removeChild(o);\n\t\t\t});\n\t\t}\n\n\t\tthis.overlayGuides = this.dataUnitComponents.map((c) => {\n\t\t\treturn {\n\t\t\t\ttype: c.unitType,\n\t\t\t\toverlay: undefined,\n\t\t\t\tunits: c.units,\n\t\t\t};\n\t\t});\n\n\t\tif (this.state.currentIndex === undefined) {\n\t\t\tthis.state.currentIndex = this.state.datasetLength - 1;\n\t\t}\n\n\t\t// Render overlays\n\t\tthis.overlayGuides.map((d) => {\n\t\t\tlet currentUnit = d.units[this.state.currentIndex];\n\n\t\t\td.overlay = makeOverlay[d.type](currentUnit);\n\t\t\tthis.drawArea.appendChild(d.overlay);\n\t\t});\n\t}\n\n\tupdateOverlayGuides() {\n\t\tif (this.overlayGuides) {\n\t\t\tthis.overlayGuides.forEach((g) => {\n\t\t\t\tlet o = g.overlay;\n\t\t\t\to.parentNode.removeChild(o);\n\t\t\t});\n\t\t}\n\t}\n\n\tbindOverlay() {\n\t\tthis.parent.addEventListener(\"data-select\", () => {\n\t\t\tthis.updateOverlay();\n\t\t});\n\t}\n\n\tbindUnits() {\n\t\tthis.dataUnitComponents.map((c) => {\n\t\t\tc.units.map((unit) => {\n\t\t\t\tunit.addEventListener(\"click\", () => {\n\t\t\t\t\tlet index = unit.getAttribute(\"data-point-index\");\n\t\t\t\t\tthis.setCurrentDataPoint(index);\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\n\t\t// Note: Doesn't work as tooltip is absolutely positioned\n\t\tthis.tip.container.addEventListener(\"click\", () => {\n\t\t\tlet index = this.tip.container.getAttribute(\"data-point-index\");\n\t\t\tthis.setCurrentDataPoint(index);\n\t\t});\n\t}\n\n\tupdateOverlay() {\n\t\tthis.overlayGuides.map((d) => {\n\t\t\tlet currentUnit = d.units[this.state.currentIndex];\n\t\t\tupdateOverlay[d.type](currentUnit, d.overlay);\n\t\t});\n\t}\n\n\tonLeftArrow() {\n\t\tthis.setCurrentDataPoint(this.state.currentIndex - 1);\n\t}\n\n\tonRightArrow() {\n\t\tthis.setCurrentDataPoint(this.state.currentIndex + 1);\n\t}\n\n\tgetDataPoint(index = this.state.currentIndex) {\n\t\tlet s = this.state;\n\t\tlet data_point = {\n\t\t\tindex: index,\n\t\t\tlabel: s.xAxis.labels[index],\n\t\t\tvalues: s.datasets.map((d) => d.values[index]),\n\t\t};\n\t\treturn data_point;\n\t}\n\n\tsetCurrentDataPoint(index) {\n\t\tlet s = this.state;\n\t\tindex = parseInt(index);\n\t\tif (index < 0) index = 0;\n\t\tif (index >= s.xAxis.labels.length) index = s.xAxis.labels.length - 1;\n\t\tif (index === s.currentIndex) return;\n\t\ts.currentIndex = index;\n\t\tfire(this.parent, \"data-select\", this.getDataPoint());\n\t}\n\n\t// API\n\taddDataPoint(label, datasetValues, index = this.state.datasetLength) {\n\t\tsuper.addDataPoint(label, datasetValues, index);\n\t\tthis.data.labels.splice(index, 0, label);\n\t\tthis.data.datasets.map((d, i) => {\n\t\t\td.values.splice(index, 0, datasetValues[i]);\n\t\t});\n\t\tthis.update(this.data);\n\t}\n\n\tremoveDataPoint(index = this.state.datasetLength - 1) {\n\t\tif (this.data.labels.length <= 1) {\n\t\t\treturn;\n\t\t}\n\t\tsuper.removeDataPoint(index);\n\t\tthis.data.labels.splice(index, 1);\n\t\tthis.data.datasets.map((d) => {\n\t\t\td.values.splice(index, 1);\n\t\t});\n\t\tthis.update(this.data);\n\t}\n\n\tupdateDataset(datasetValues, index = 0) {\n\t\tthis.data.datasets[index].values = datasetValues;\n\t\tthis.update(this.data);\n\t}\n\t// addDataset(dataset, index) {}\n\t// removeDataset(index = 0) {}\n\n\tupdateDatasets(datasets) {\n\t\tthis.data.datasets.map((d, i) => {\n\t\t\tif (datasets[i]) {\n\t\t\t\td.values = datasets[i];\n\t\t\t}\n\t\t});\n\t\tthis.update(this.data);\n\t}\n\n\t// updateDataPoint(dataPoint, index = 0) {}\n\t// addDataPoint(dataPoint, index = 0) {}\n\t// removeDataPoint(index = 0) {}\n}\n"
  },
  {
    "path": "src/js/charts/BaseChart.js",
    "content": "import SvgTip from \"../objects/SvgTip\";\nimport {\n\t$,\n\tisElementInViewport,\n\tgetElementContentWidth,\n\tisHidden,\n} from \"../utils/dom\";\nimport {\n\tmakeSVGContainer,\n\tmakeSVGDefs,\n\tmakeSVGGroup,\n\tmakeText,\n} from \"../utils/draw\";\nimport { LEGEND_ITEM_WIDTH } from \"../utils/constants\";\nimport {\n\tBASE_MEASURES,\n\tgetExtraHeight,\n\tgetExtraWidth,\n\tgetTopOffset,\n\tgetLeftOffset,\n\tINIT_CHART_UPDATE_TIMEOUT,\n\tCHART_POST_ANIMATE_TIMEOUT,\n\tDEFAULT_COLORS,\n} from \"../utils/constants\";\nimport { getColor, isValidColor } from \"../utils/colors\";\nimport { runSMILAnimation } from \"../utils/animation\";\nimport { downloadFile, prepareForExport } from \"../utils/export\";\nimport { deepClone } from \"../utils/helpers\";\n\nexport default class BaseChart {\n\tconstructor(parent, options) {\n\t\t// deepclone options to avoid making changes to orignal object\n\t\toptions = deepClone(options);\n\n\t\tthis.parent =\n\t\t\ttypeof parent === \"string\"\n\t\t\t\t? document.querySelector(parent)\n\t\t\t\t: parent;\n\n\t\tif (!(this.parent instanceof HTMLElement)) {\n\t\t\tthrow new Error(\"No `parent` element to render on was provided.\");\n\t\t}\n\n\t\tthis.rawChartArgs = options;\n\n\t\tthis.title = options.title || \"\";\n\t\tthis.type = options.type || \"\";\n\n\t\tthis.colors = this.validateColors(options.colors, this.type);\n\n\t\tthis.config = {\n\t\t\tshowTooltip: 1, // calculate\n\t\t\tshowLegend:\n\t\t\t\ttypeof options.showLegend !== \"undefined\"\n\t\t\t\t\t? options.showLegend\n\t\t\t\t\t: 1,\n\t\t\tisNavigable: options.isNavigable || 0,\n\t\t\tanimate: 0,\n\t\t\toverrideCeiling: options.overrideCeiling || false,\n\t\t\toverrideFloor: options.overrideFloor || false,\n\t\t\ttruncateLegends:\n\t\t\t\ttypeof options.truncateLegends !== \"undefined\"\n\t\t\t\t\t? options.truncateLegends\n\t\t\t\t\t: 1,\n\t\t\tcontinuous:\n\t\t\t\ttypeof options.continuous !== \"undefined\"\n\t\t\t\t\t? options.continuous\n\t\t\t\t\t: 1,\n\t\t};\n\n\t\tthis.measures = JSON.parse(JSON.stringify(BASE_MEASURES));\n\t\tlet m = this.measures;\n\n\t\tthis.realData = this.prepareData(options.data, this.config);\n\t\tthis.data = this.prepareFirstData(this.realData);\n\n\t\tthis.setMeasures(options);\n\t\tif (!this.title.length) {\n\t\t\tm.titleHeight = 0;\n\t\t}\n\t\tif (!this.config.showLegend) m.legendHeight = 0;\n\t\tthis.argHeight = options.height || m.baseHeight;\n\n\t\tthis.state = {};\n\t\tthis.options = {};\n\n\t\tthis.initTimeout = INIT_CHART_UPDATE_TIMEOUT;\n\n\t\tif (this.config.isNavigable) {\n\t\t\tthis.overlays = [];\n\t\t}\n\n\t\tthis.configure(options);\n\t}\n\n\tprepareData(data) {\n\t\treturn data;\n\t}\n\n\tprepareFirstData(data) {\n\t\treturn data;\n\t}\n\n\tvalidateColors(colors, type) {\n\t\tconst validColors = [];\n\t\tcolors = (colors || []).concat(DEFAULT_COLORS[type]);\n\t\tcolors.forEach((string) => {\n\t\t\tconst color = getColor(string);\n\t\t\tif (!isValidColor(color)) {\n\t\t\t\tconsole.warn('\"' + string + '\" is not a valid color.');\n\t\t\t} else {\n\t\t\t\tvalidColors.push(color);\n\t\t\t}\n\t\t});\n\t\treturn validColors;\n\t}\n\n\tsetMeasures() {\n\t\t// Override measures, including those for title and legend\n\t\t// set config for legend and title\n\t}\n\n\tconfigure() {\n\t\tlet height = this.argHeight;\n\t\tthis.baseHeight = height;\n\t\tthis.height = height - getExtraHeight(this.measures);\n\n\t\t// Bind window events\n\t\tthis.boundDrawFn = () => this.draw(true);\n\t\t// Look into improving responsiveness\n\t\t//if (ResizeObserver) {\n\t\t//\tthis.resizeObserver = new ResizeObserver(this.boundDrawFn);\n\t\t//\tthis.resizeObserver.observe(this.parent);\n\t\t//}\n\t\twindow.addEventListener(\"resize\", this.boundDrawFn);\n\t\twindow.addEventListener(\"orientationchange\", this.boundDrawFn);\n\t}\n\n\tdestroy() {\n\t\t//if (this.resizeObserver) this.resizeObserver.disconnect();\n\t\twindow.removeEventListener(\"resize\", this.boundDrawFn);\n\t\twindow.removeEventListener(\"orientationchange\", this.boundDrawFn);\n\t}\n\n\t// Has to be called manually\n\tsetup() {\n\t\tthis.makeContainer();\n\t\tthis.updateWidth();\n\t\tthis.makeTooltip();\n\n\t\tthis.draw(false, true);\n\t}\n\n\tmakeContainer() {\n\t\t// Chart needs a dedicated parent element\n\t\tthis.parent.innerHTML = \"\";\n\n\t\tlet args = {\n\t\t\tinside: this.parent,\n\t\t\tclassName: \"chart-container\",\n\t\t};\n\n\t\tif (this.independentWidth) {\n\t\t\targs.styles = { width: this.independentWidth + \"px\" };\n\t\t}\n\n\t\tthis.container = $.create(\"div\", args);\n\t}\n\n\tmakeTooltip() {\n\t\tthis.tip = new SvgTip({\n\t\t\tparent: this.container,\n\t\t\tcolors: this.colors,\n\t\t});\n\t\tthis.bindTooltip();\n\t}\n\n\tbindTooltip() {}\n\n\tdraw(onlyWidthChange = false, init = false) {\n\t\tif (onlyWidthChange && isHidden(this.parent)) {\n\t\t\t// Don't update anything if the chart is hidden\n\t\t\treturn;\n\t\t}\n\t\tthis.updateWidth();\n\n\t\tthis.calc(onlyWidthChange);\n\t\tthis.makeChartArea();\n\t\tthis.setupComponents();\n\n\t\tthis.components.forEach((c) => c.setup(this.drawArea));\n\t\t// this.components.forEach(c => c.make());\n\t\tthis.render(this.components, false);\n\n\t\tif (init) {\n\t\t\tthis.data = this.realData;\n\t\t\tthis.update(this.data, true);\n\t\t\t// Not needed anymore since animate defaults to 0 and might potentially be refactored or deprecated\n\t\t\t/* setTimeout(() => {\n\t\t\t\tthis.update(this.data, true);\n\t\t\t}, this.initTimeout); */\n\t\t}\n\n\t\tif (this.config.showLegend) {\n\t\t\tthis.renderLegend();\n\t\t}\n\n\t\tthis.setupNavigation(init);\n\t}\n\n\tcalc() {} // builds state\n\n\tupdateWidth() {\n\t\tthis.baseWidth = getElementContentWidth(this.parent);\n\t\tthis.width = this.baseWidth - getExtraWidth(this.measures);\n\t}\n\n\tmakeChartArea() {\n\t\tif (this.svg) {\n\t\t\tthis.container.removeChild(this.svg);\n\t\t}\n\t\tlet m = this.measures;\n\n\t\tthis.svg = makeSVGContainer(\n\t\t\tthis.container,\n\t\t\t\"frappe-chart chart\",\n\t\t\tthis.baseWidth,\n\t\t\tthis.baseHeight\n\t\t);\n\t\tthis.svgDefs = makeSVGDefs(this.svg);\n\n\t\tif (this.title.length) {\n\t\t\tthis.titleEL = makeText(\n\t\t\t\t\"title\",\n\t\t\t\tm.margins.left,\n\t\t\t\tm.margins.top,\n\t\t\t\tthis.title,\n\t\t\t\t{\n\t\t\t\t\tfontSize: m.titleFontSize,\n\t\t\t\t\tfill: \"#666666\",\n\t\t\t\t\tdy: m.titleFontSize,\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\n\t\tlet top = getTopOffset(m);\n\t\tthis.drawArea = makeSVGGroup(\n\t\t\tthis.type + \"-chart chart-draw-area\",\n\t\t\t`translate(${getLeftOffset(m)}, ${top})`\n\t\t);\n\n\t\tif (this.config.showLegend) {\n\t\t\ttop += this.height + m.paddings.bottom;\n\t\t\tthis.legendArea = makeSVGGroup(\n\t\t\t\t\"chart-legend\",\n\t\t\t\t`translate(${getLeftOffset(m)}, ${top})`\n\t\t\t);\n\t\t}\n\n\t\tif (this.title.length) {\n\t\t\tthis.svg.appendChild(this.titleEL);\n\t\t}\n\t\tthis.svg.appendChild(this.drawArea);\n\t\tif (this.config.showLegend) {\n\t\t\tthis.svg.appendChild(this.legendArea);\n\t\t}\n\n\t\tthis.updateTipOffset(getLeftOffset(m), getTopOffset(m));\n\t}\n\n\tupdateTipOffset(x, y) {\n\t\tthis.tip.offset = {\n\t\t\tx: x,\n\t\t\ty: y,\n\t\t};\n\t}\n\n\tsetupComponents() {\n\t\tthis.components = new Map();\n\t}\n\n\tupdate(data, drawing = false, config) {\n\t\tif (!data) console.error(\"No data to update.\");\n\t\tif (!drawing) data = deepClone(data);\n\t\tthis.data = this.prepareData(data, config);\n\t\tthis.calc(); // builds state\n\t\tthis.render(this.components, this.config.animate);\n\t}\n\n\trender(components = this.components, animate = true) {\n\t\tif (this.config.isNavigable) {\n\t\t\t// Remove all existing overlays\n\t\t\tthis.overlays.map((o) => o.parentNode.removeChild(o));\n\t\t\t// ref.parentNode.insertBefore(element, ref);\n\t\t}\n\t\tlet elementsToAnimate = [];\n\t\t// Can decouple to this.refreshComponents() first to save animation timeout\n\t\tcomponents.forEach((c) => {\n\t\t\telementsToAnimate = elementsToAnimate.concat(c.update(animate));\n\t\t});\n\t\tif (elementsToAnimate.length > 0) {\n\t\t\trunSMILAnimation(this.container, this.svg, elementsToAnimate);\n\t\t\tsetTimeout(() => {\n\t\t\t\tcomponents.forEach((c) => c.make());\n\t\t\t\tthis.updateNav();\n\t\t\t}, CHART_POST_ANIMATE_TIMEOUT);\n\t\t} else {\n\t\t\tcomponents.forEach((c) => c.make());\n\t\t\tthis.updateNav();\n\t\t}\n\t}\n\n\tupdateNav() {\n\t\tif (this.config.isNavigable) {\n\t\t\tthis.makeOverlay();\n\t\t\tthis.bindUnits();\n\t\t}\n\t}\n\n\trenderLegend(dataset) {\n\t\tthis.legendArea.textContent = \"\";\n\t\tlet count = 0;\n\t\tlet y = 0;\n\n\t\tdataset.map((data, index) => {\n\t\t\tlet divisor = Math.floor(this.width / LEGEND_ITEM_WIDTH);\n\t\t\tif (count > divisor) {\n\t\t\t\tcount = 0;\n\t\t\t\ty += this.config.legendRowHeight;\n\t\t\t}\n\t\t\tlet x = LEGEND_ITEM_WIDTH * count;\n\t\t\tlet dot = this.makeLegend(data, index, x, y);\n\t\t\tthis.legendArea.appendChild(dot);\n\t\t\tcount++;\n\t\t});\n\t}\n\n\tmakeLegend() {}\n\n\tsetupNavigation(init = false) {\n\t\tif (!this.config.isNavigable) return;\n\n\t\tif (init) {\n\t\t\tthis.bindOverlay();\n\n\t\t\tthis.keyActions = {\n\t\t\t\t13: this.onEnterKey.bind(this),\n\t\t\t\t37: this.onLeftArrow.bind(this),\n\t\t\t\t38: this.onUpArrow.bind(this),\n\t\t\t\t39: this.onRightArrow.bind(this),\n\t\t\t\t40: this.onDownArrow.bind(this),\n\t\t\t};\n\n\t\t\tdocument.addEventListener(\"keydown\", (e) => {\n\t\t\t\tif (isElementInViewport(this.container)) {\n\t\t\t\t\te = e || window.event;\n\t\t\t\t\tif (this.keyActions[e.keyCode]) {\n\t\t\t\t\t\tthis.keyActions[e.keyCode]();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tmakeOverlay() {}\n\tupdateOverlay() {}\n\tbindOverlay() {}\n\tbindUnits() {}\n\n\tonLeftArrow() {}\n\tonRightArrow() {}\n\tonUpArrow() {}\n\tonDownArrow() {}\n\tonEnterKey() {}\n\n\taddDataPoint() {}\n\tremoveDataPoint() {}\n\n\tgetDataPoint() {}\n\tsetCurrentDataPoint() {}\n\n\tupdateDataset() {}\n\n\texport() {\n\t\tlet chartSvg = prepareForExport(this.svg);\n\t\tdownloadFile(this.title || \"Chart\", [chartSvg]);\n\t}\n}\n"
  },
  {
    "path": "src/js/charts/DonutChart.js",
    "content": "import AggregationChart from \"./AggregationChart\";\nimport { getComponent } from \"../objects/ChartComponents\";\nimport { getOffset } from \"../utils/dom\";\nimport { getPositionByAngle } from \"../utils/helpers\";\nimport { makeArcStrokePathStr, makeStrokeCircleStr } from \"../utils/draw\";\nimport { lightenDarkenColor } from \"../utils/colors\";\nimport { transform } from \"../utils/animation\";\nimport { FULL_ANGLE } from \"../utils/constants\";\n\nexport default class DonutChart extends AggregationChart {\n  constructor(parent, args) {\n    super(parent, args);\n    this.type = \"donut\";\n    this.initTimeout = 0;\n    this.init = 1;\n\n    this.setup();\n  }\n\n  configure(args) {\n    super.configure(args);\n    this.mouseMove = this.mouseMove.bind(this);\n    this.mouseLeave = this.mouseLeave.bind(this);\n\n    this.hoverRadio = args.hoverRadio || 0.1;\n    this.config.startAngle = args.startAngle || 0;\n\n    this.clockWise = args.clockWise || false;\n    this.strokeWidth = args.strokeWidth || 30;\n  }\n\n  calc() {\n    super.calc();\n    let s = this.state;\n    this.radius =\n      this.height > this.width\n        ? this.center.x - this.strokeWidth / 2\n        : this.center.y - this.strokeWidth / 2;\n\n    const { radius, clockWise } = this;\n\n    const prevSlicesProperties = s.slicesProperties || [];\n    s.sliceStrings = [];\n    s.slicesProperties = [];\n    let curAngle = 180 - this.config.startAngle;\n\n    s.sliceTotals.map((total, i) => {\n      const startAngle = curAngle;\n      const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE;\n      const largeArc = originDiffAngle > 180 ? 1 : 0;\n      const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;\n      const endAngle = (curAngle = curAngle + diffAngle);\n      const startPosition = getPositionByAngle(startAngle, radius);\n      const endPosition = getPositionByAngle(endAngle, radius);\n\n      const prevProperty = this.init && prevSlicesProperties[i];\n\n      let curStart, curEnd;\n      if (this.init) {\n        curStart = prevProperty ? prevProperty.startPosition : startPosition;\n        curEnd = prevProperty ? prevProperty.endPosition : startPosition;\n      } else {\n        curStart = startPosition;\n        curEnd = endPosition;\n      }\n      const curPath =\n        originDiffAngle === 360\n          ? makeStrokeCircleStr(\n              curStart,\n              curEnd,\n              this.center,\n              this.radius,\n              this.clockWise,\n              largeArc\n            )\n          : makeArcStrokePathStr(\n              curStart,\n              curEnd,\n              this.center,\n              this.radius,\n              this.clockWise,\n              largeArc\n            );\n\n      s.sliceStrings.push(curPath);\n      s.slicesProperties.push({\n        startPosition,\n        endPosition,\n        value: total,\n        total: s.grandTotal,\n        startAngle,\n        endAngle,\n        angle: diffAngle,\n      });\n    });\n    this.init = 0;\n  }\n\n  setupComponents() {\n    let s = this.state;\n\n    let componentConfigs = [\n      [\n        \"donutSlices\",\n        {},\n        function () {\n          return {\n            sliceStrings: s.sliceStrings,\n            colors: this.colors,\n            strokeWidth: this.strokeWidth,\n          };\n        }.bind(this),\n      ],\n    ];\n\n    this.components = new Map(\n      componentConfigs.map((args) => {\n        let component = getComponent(...args);\n        return [args[0], component];\n      })\n    );\n  }\n\n  calTranslateByAngle(property) {\n    const { radius, hoverRadio } = this;\n    const position = getPositionByAngle(\n      property.startAngle + property.angle / 2,\n      radius\n    );\n    return `translate3d(${position.x * hoverRadio}px,${\n      position.y * hoverRadio\n    }px,0)`;\n  }\n\n  hoverSlice(path, i, flag, e) {\n    if (!path) return;\n    const color = this.colors[i];\n    if (flag) {\n      transform(path, this.calTranslateByAngle(this.state.slicesProperties[i]));\n      path.style.stroke = lightenDarkenColor(color, 50);\n      let g_off = getOffset(this.svg);\n      let x = e.pageX - g_off.left + 10;\n      let y = e.pageY - g_off.top - 10;\n      let title =\n        (this.formatted_labels && this.formatted_labels.length > 0\n          ? this.formatted_labels[i]\n          : this.state.labels[i]) + \": \";\n      let percent = (\n        (this.state.sliceTotals[i] * 100) /\n        this.state.grandTotal\n      ).toFixed(1);\n      this.tip.setValues(x, y, { name: title, value: percent + \"%\" });\n      this.tip.showTip();\n    } else {\n      transform(path, \"translate3d(0,0,0)\");\n      this.tip.hideTip();\n      path.style.stroke = color;\n    }\n  }\n\n  bindTooltip() {\n    this.container.addEventListener(\"mousemove\", this.mouseMove);\n    this.container.addEventListener(\"mouseleave\", this.mouseLeave);\n  }\n\n  mouseMove(e) {\n    const target = e.target;\n    let slices = this.components.get(\"donutSlices\").store;\n    let prevIndex = this.curActiveSliceIndex;\n    let prevAcitve = this.curActiveSlice;\n    if (slices.includes(target)) {\n      let i = slices.indexOf(target);\n      this.hoverSlice(prevAcitve, prevIndex, false);\n      this.curActiveSlice = target;\n      this.curActiveSliceIndex = i;\n      this.hoverSlice(target, i, true, e);\n    } else {\n      this.mouseLeave();\n    }\n  }\n\n  mouseLeave() {\n    this.hoverSlice(this.curActiveSlice, this.curActiveSliceIndex, false);\n  }\n}\n"
  },
  {
    "path": "src/js/charts/Heatmap.js",
    "content": "import BaseChart from \"./BaseChart\";\nimport { getComponent } from \"../objects/ChartComponents\";\nimport { makeText, heatSquare } from \"../utils/draw\";\nimport {\n  DAY_NAMES_SHORT,\n  toMidnightUTC,\n  addDays,\n  areInSameMonth,\n  getLastDateInMonth,\n  setDayToSunday,\n  getYyyyMmDd,\n  getWeeksBetween,\n  getMonthName,\n  clone,\n  NO_OF_MILLIS,\n  NO_OF_YEAR_MONTHS,\n  NO_OF_DAYS_IN_WEEK,\n} from \"../utils/date-utils\";\nimport { calcDistribution, getMaxCheckpoint } from \"../utils/intervals\";\nimport {\n  getExtraHeight,\n  getExtraWidth,\n  HEATMAP_DISTRIBUTION_SIZE,\n  HEATMAP_SQUARE_SIZE,\n  HEATMAP_GUTTER_SIZE,\n} from \"../utils/constants\";\n\nconst COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE;\nconst ROW_HEIGHT = COL_WIDTH;\n// const DAY_INCR = 1;\n\nexport default class Heatmap extends BaseChart {\n  constructor(parent, options) {\n    super(parent, options);\n    this.type = \"heatmap\";\n\n    this.countLabel = options.countLabel || \"\";\n\n    let validStarts = [\"Sunday\", \"Monday\"];\n    let startSubDomain = validStarts.includes(options.startSubDomain)\n      ? options.startSubDomain\n      : \"Sunday\";\n    this.startSubDomainIndex = validStarts.indexOf(startSubDomain);\n\n    this.setup();\n  }\n\n  setMeasures(options) {\n    let m = this.measures;\n    this.discreteDomains = options.discreteDomains === 0 ? 0 : 1;\n\n    m.paddings.top = ROW_HEIGHT * 3;\n    m.paddings.bottom = 0;\n    m.legendHeight = ROW_HEIGHT * 2;\n    m.baseHeight = ROW_HEIGHT * NO_OF_DAYS_IN_WEEK + getExtraHeight(m);\n\n    let d = this.data;\n    let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;\n    this.independentWidth =\n      (getWeeksBetween(d.start, d.end) + spacing) * COL_WIDTH +\n      getExtraWidth(m);\n  }\n\n  updateWidth() {\n    let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;\n    let noOfWeeks = this.state.noOfWeeks ? this.state.noOfWeeks : 52;\n    this.baseWidth =\n      (noOfWeeks + spacing) * COL_WIDTH + getExtraWidth(this.measures);\n  }\n\n  prepareData(data = this.data) {\n    if (data.start && data.end && data.start > data.end) {\n      throw new Error(\"Start date cannot be greater than end date.\");\n    }\n\n    if (!data.start) {\n      data.start = new Date();\n      data.start.setFullYear(data.start.getFullYear() - 1);\n    }\n    data.start = toMidnightUTC(data.start);\n\n    if (!data.end) {\n      data.end = new Date();\n    }\n    data.end = toMidnightUTC(data.end);\n\n    data.dataPoints = data.dataPoints || {};\n\n    if (parseInt(Object.keys(data.dataPoints)[0]) > 100000) {\n      let points = {};\n      Object.keys(data.dataPoints).forEach((timestampSec) => {\n        let date = new Date(timestampSec * NO_OF_MILLIS);\n        points[getYyyyMmDd(date)] = data.dataPoints[timestampSec];\n      });\n      data.dataPoints = points;\n    }\n\n    return data;\n  }\n\n  calc() {\n    let s = this.state;\n\n    s.start = clone(this.data.start);\n    s.end = clone(this.data.end);\n\n    s.firstWeekStart = clone(s.start);\n    s.noOfWeeks = getWeeksBetween(s.start, s.end);\n    s.distribution = calcDistribution(\n      Object.values(this.data.dataPoints),\n      HEATMAP_DISTRIBUTION_SIZE\n    );\n\n    s.domainConfigs = this.getDomains();\n  }\n\n  setupComponents() {\n    let s = this.state;\n    let lessCol = this.discreteDomains ? 0 : 1;\n\n    let componentConfigs = s.domainConfigs.map((config, i) => [\n      \"heatDomain\",\n      {\n        index: config.index,\n        colWidth: COL_WIDTH,\n        rowHeight: ROW_HEIGHT,\n        squareSize: HEATMAP_SQUARE_SIZE,\n        radius: this.rawChartArgs.radius || 0,\n        xTranslate:\n          s.domainConfigs\n            .filter((config, j) => j < i)\n            .map((config) => config.cols.length - lessCol)\n            .reduce((a, b) => a + b, 0) * COL_WIDTH,\n      },\n      function () {\n        return s.domainConfigs[i];\n      }.bind(this),\n    ]);\n\n    this.components = new Map(\n      componentConfigs.map((args, i) => {\n        let component = getComponent(...args);\n        return [args[0] + \"-\" + i, component];\n      })\n    );\n\n    let y = 0;\n    DAY_NAMES_SHORT.forEach((dayName, i) => {\n      if ([1, 3, 5].includes(i)) {\n        let dayText = makeText(\"subdomain-name\", -COL_WIDTH / 2, y, dayName, {\n          fontSize: HEATMAP_SQUARE_SIZE,\n          dy: 8,\n          textAnchor: \"end\",\n        });\n        this.drawArea.appendChild(dayText);\n      }\n      y += ROW_HEIGHT;\n    });\n  }\n\n  update(data) {\n    if (!data) {\n      console.error(\"No data to update.\");\n    }\n\n    this.data = this.prepareData(data);\n    this.draw();\n    this.bindTooltip();\n  }\n\n  bindTooltip() {\n    this.container.addEventListener(\"mousemove\", (e) => {\n      this.components.forEach((comp) => {\n        let daySquares = comp.store;\n        let daySquare = e.target;\n        if (daySquares.includes(daySquare)) {\n          let count = daySquare.getAttribute(\"data-value\");\n          let dateParts = daySquare.getAttribute(\"data-date\").split(\"-\");\n\n          let month = getMonthName(parseInt(dateParts[1]) - 1, true);\n\n          let gOff = this.container.getBoundingClientRect(),\n            pOff = daySquare.getBoundingClientRect();\n\n          let width = parseInt(e.target.getAttribute(\"width\"));\n          let x = pOff.left - gOff.left + width / 2;\n          let y = pOff.top - gOff.top;\n          let value = count + \" \" + this.countLabel;\n          let name = \" on \" + month + \" \" + dateParts[0] + \", \" + dateParts[2];\n\n          this.tip.setValues(\n            x,\n            y,\n            { name: name, value: value, valueFirst: 1 },\n            []\n          );\n          this.tip.showTip();\n        }\n      });\n    });\n  }\n\n  renderLegend() {\n    this.legendArea.textContent = \"\";\n    let x = 0;\n    let y = ROW_HEIGHT;\n    let radius = this.rawChartArgs.radius || 0;\n\n    let lessText = makeText(\"subdomain-name\", x, y, \"Less\", {\n      fontSize: HEATMAP_SQUARE_SIZE + 1,\n      dy: 9,\n    });\n    x = COL_WIDTH * 2 + COL_WIDTH / 2;\n    this.legendArea.appendChild(lessText);\n\n    this.colors.slice(0, HEATMAP_DISTRIBUTION_SIZE).map((color, i) => {\n      const square = heatSquare(\n        \"heatmap-legend-unit\",\n        x + (COL_WIDTH + 3) * i,\n        y,\n        HEATMAP_SQUARE_SIZE,\n        radius,\n        color\n      );\n      this.legendArea.appendChild(square);\n    });\n\n    let moreTextX =\n      x + HEATMAP_DISTRIBUTION_SIZE * (COL_WIDTH + 3) + COL_WIDTH / 4;\n    let moreText = makeText(\"subdomain-name\", moreTextX, y, \"More\", {\n      fontSize: HEATMAP_SQUARE_SIZE + 1,\n      dy: 9,\n    });\n    this.legendArea.appendChild(moreText);\n  }\n\n  getDomains() {\n    let s = this.state;\n    const [startMonth, startYear] = [s.start.getMonth(), s.start.getFullYear()];\n    const [endMonth, endYear] = [s.end.getMonth(), s.end.getFullYear()];\n\n    const noOfMonths = endMonth - startMonth + 1 + (endYear - startYear) * 12;\n\n    let domainConfigs = [];\n\n    let startOfMonth = clone(s.start);\n    for (var i = 0; i < noOfMonths; i++) {\n      let endDate = s.end;\n      if (!areInSameMonth(startOfMonth, s.end)) {\n        let [month, year] = [\n          startOfMonth.getMonth(),\n          startOfMonth.getFullYear(),\n        ];\n        endDate = getLastDateInMonth(month, year);\n      }\n      domainConfigs.push(this.getDomainConfig(startOfMonth, endDate));\n\n      addDays(endDate, 1);\n      startOfMonth = endDate;\n    }\n\n    return domainConfigs;\n  }\n\n  getDomainConfig(startDate, endDate = \"\") {\n    let [month, year] = [startDate.getMonth(), startDate.getFullYear()];\n    let startOfWeek = setDayToSunday(startDate); // TODO: Monday as well\n    endDate = endDate\n      ? clone(endDate)\n      : toMidnightUTC(getLastDateInMonth(month, year));\n\n    let domainConfig = {\n      index: month,\n      cols: [],\n    };\n\n    addDays(endDate, 1);\n    let noOfMonthWeeks = getWeeksBetween(startOfWeek, endDate);\n\n    let cols = [],\n      col;\n    for (var i = 0; i < noOfMonthWeeks; i++) {\n      col = this.getCol(startOfWeek, month);\n      cols.push(col);\n\n      startOfWeek = toMidnightUTC(\n        new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd)\n      );\n      addDays(startOfWeek, 1);\n    }\n\n    if (col[NO_OF_DAYS_IN_WEEK - 1].dataValue !== undefined) {\n      addDays(startOfWeek, 1);\n      cols.push(this.getCol(startOfWeek, month, true));\n    }\n\n    domainConfig.cols = cols;\n\n    return domainConfig;\n  }\n\n  getCol(startDate, month, empty = false) {\n    let s = this.state;\n\n    // startDate is the start of week\n    let currentDate = clone(startDate);\n    let col = [];\n\n    for (var i = 0; i < NO_OF_DAYS_IN_WEEK; i++, addDays(currentDate, 1)) {\n      let config = {};\n\n      // Non-generic adjustment for entire heatmap, needs state\n      let currentDateWithinData =\n        currentDate >= s.start && currentDate <= s.end;\n\n      if (empty || currentDate.getMonth() !== month || !currentDateWithinData) {\n        config.yyyyMmDd = getYyyyMmDd(currentDate);\n      } else {\n        config = this.getSubDomainConfig(currentDate);\n      }\n      col.push(config);\n    }\n\n    return col;\n  }\n\n  getSubDomainConfig(date) {\n    let yyyyMmDd = getYyyyMmDd(date);\n    let dataValue = this.data.dataPoints[yyyyMmDd];\n    let config = {\n      yyyyMmDd: yyyyMmDd,\n      dataValue: dataValue || 0,\n      fill: this.colors[getMaxCheckpoint(dataValue, this.state.distribution)],\n    };\n    return config;\n  }\n}\n"
  },
  {
    "path": "src/js/charts/PercentageChart.js",
    "content": "import AggregationChart from \"./AggregationChart\";\nimport { getOffset } from \"../utils/dom\";\nimport { getComponent } from \"../objects/ChartComponents\";\nimport { PERCENTAGE_BAR_DEFAULT_HEIGHT } from \"../utils/constants\";\n\nexport default class PercentageChart extends AggregationChart {\n  constructor(parent, args) {\n    super(parent, args);\n    this.type = \"percentage\";\n    this.setup();\n  }\n\n  setMeasures(options) {\n    let m = this.measures;\n    this.barOptions = options.barOptions || {};\n\n    let b = this.barOptions;\n    b.height = b.height || PERCENTAGE_BAR_DEFAULT_HEIGHT;\n\n    m.paddings.right = 30;\n    m.legendHeight = 60;\n    m.baseHeight = (b.height + b.depth * 0.5) * 8;\n  }\n\n  setupComponents() {\n    let s = this.state;\n\n    let componentConfigs = [\n      [\n        \"percentageBars\",\n        {\n          barHeight: this.barOptions.height,\n        },\n        function () {\n          return {\n            xPositions: s.xPositions,\n            widths: s.widths,\n            colors: this.colors,\n          };\n        }.bind(this),\n      ],\n    ];\n\n    this.components = new Map(\n      componentConfigs.map((args) => {\n        let component = getComponent(...args);\n        return [args[0], component];\n      })\n    );\n  }\n\n  calc() {\n    super.calc();\n    let s = this.state;\n\n    s.xPositions = [];\n    s.widths = [];\n\n    let xPos = 0;\n    s.sliceTotals.map((value) => {\n      let width = (this.width * value) / s.grandTotal;\n      s.widths.push(width);\n      s.xPositions.push(xPos);\n      xPos += width;\n    });\n  }\n\n  makeDataByIndex() {}\n\n  bindTooltip() {\n    let s = this.state;\n    this.container.addEventListener(\"mousemove\", (e) => {\n      let bars = this.components.get(\"percentageBars\").store;\n      let bar = e.target;\n      if (bars.includes(bar)) {\n        let i = bars.indexOf(bar);\n        let gOff = getOffset(this.container),\n          pOff = getOffset(bar);\n\n        let x = pOff.left - gOff.left + parseInt(bar.getAttribute(\"width\")) / 2;\n        let y = pOff.top - gOff.top;\n        let title =\n          (this.formattedLabels && this.formattedLabels.length > 0\n            ? this.formattedLabels[i]\n            : this.state.labels[i]) + \": \";\n        let fraction = s.sliceTotals[i] / s.grandTotal;\n\n        this.tip.setValues(x, y, {\n          name: title,\n          value: (fraction * 100).toFixed(1) + \"%\",\n        });\n        this.tip.showTip();\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "src/js/charts/PieChart.js",
    "content": "import AggregationChart from \"./AggregationChart\";\nimport { getComponent } from \"../objects/ChartComponents\";\nimport { getOffset, fire } from \"../utils/dom\";\nimport { getPositionByAngle } from \"../utils/helpers\";\nimport { makeArcPathStr, makeCircleStr } from \"../utils/draw\";\nimport { lightenDarkenColor } from \"../utils/colors\";\nimport { transform } from \"../utils/animation\";\nimport { FULL_ANGLE } from \"../utils/constants\";\n\nexport default class PieChart extends AggregationChart {\n  constructor(parent, args) {\n    super(parent, args);\n    this.type = \"pie\";\n    this.initTimeout = 0;\n    this.init = 1;\n\n    this.setup();\n  }\n\n  configure(args) {\n    super.configure(args);\n    this.mouseMove = this.mouseMove.bind(this);\n    this.mouseLeave = this.mouseLeave.bind(this);\n\n    this.hoverRadio = args.hoverRadio || 0.1;\n    this.config.startAngle = args.startAngle || 0;\n\n    this.clockWise = args.clockWise || false;\n  }\n\n  calc() {\n    super.calc();\n    let s = this.state;\n    this.radius = this.height > this.width ? this.center.x : this.center.y;\n\n    const { radius, clockWise } = this;\n\n    const prevSlicesProperties = s.slicesProperties || [];\n    s.sliceStrings = [];\n    s.slicesProperties = [];\n    let curAngle = 180 - this.config.startAngle;\n\n    s.sliceTotals.map((total, i) => {\n      const startAngle = curAngle;\n      const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE;\n      const largeArc = originDiffAngle > 180 ? 1 : 0;\n      const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;\n      const endAngle = (curAngle = curAngle + diffAngle);\n      const startPosition = getPositionByAngle(startAngle, radius);\n      const endPosition = getPositionByAngle(endAngle, radius);\n\n      const prevProperty = this.init && prevSlicesProperties[i];\n\n      let curStart, curEnd;\n      if (this.init) {\n        curStart = prevProperty ? prevProperty.startPosition : startPosition;\n        curEnd = prevProperty ? prevProperty.endPosition : startPosition;\n      } else {\n        curStart = startPosition;\n        curEnd = endPosition;\n      }\n      const curPath =\n        originDiffAngle === 360\n          ? makeCircleStr(\n              curStart,\n              curEnd,\n              this.center,\n              this.radius,\n              clockWise,\n              largeArc\n            )\n          : makeArcPathStr(\n              curStart,\n              curEnd,\n              this.center,\n              this.radius,\n              clockWise,\n              largeArc\n            );\n\n      s.sliceStrings.push(curPath);\n      s.slicesProperties.push({\n        startPosition,\n        endPosition,\n        value: total,\n        total: s.grandTotal,\n        startAngle,\n        endAngle,\n        angle: diffAngle,\n      });\n    });\n    this.init = 0;\n  }\n\n  setupComponents() {\n    let s = this.state;\n\n    let componentConfigs = [\n      [\n        \"pieSlices\",\n        {},\n        function () {\n          return {\n            sliceStrings: s.sliceStrings,\n            colors: this.colors,\n          };\n        }.bind(this),\n      ],\n    ];\n\n    this.components = new Map(\n      componentConfigs.map((args) => {\n        let component = getComponent(...args);\n        return [args[0], component];\n      })\n    );\n  }\n\n  calTranslateByAngle(property) {\n    const { radius, hoverRadio } = this;\n    const position = getPositionByAngle(\n      property.startAngle + property.angle / 2,\n      radius\n    );\n    return `translate3d(${position.x * hoverRadio}px,${\n      position.y * hoverRadio\n    }px,0)`;\n  }\n\n  hoverSlice(path, i, flag, e) {\n    if (!path) return;\n    const color = this.colors[i];\n    if (flag) {\n      transform(path, this.calTranslateByAngle(this.state.slicesProperties[i]));\n      path.style.fill = lightenDarkenColor(color, 50);\n      let g_off = getOffset(this.svg);\n      let x = e.pageX - g_off.left + 10;\n      let y = e.pageY - g_off.top - 10;\n      let title =\n        (this.formatted_labels && this.formatted_labels.length > 0\n          ? this.formatted_labels[i]\n          : this.state.labels[i]) + \": \";\n      let percent = (\n        (this.state.sliceTotals[i] * 100) /\n        this.state.grandTotal\n      ).toFixed(1);\n      this.tip.setValues(x, y, { name: title, value: percent + \"%\" });\n      this.tip.showTip();\n    } else {\n      transform(path, \"translate3d(0,0,0)\");\n      this.tip.hideTip();\n      path.style.fill = color;\n    }\n  }\n\n  bindTooltip() {\n    this.container.addEventListener(\"mousemove\", this.mouseMove);\n    this.container.addEventListener(\"mouseleave\", this.mouseLeave);\n  }\n  getDataPoint(index = this.state.currentIndex) {\n    let s = this.state;\n    let data_point = {\n      index: index,\n      label: s.labels[index],\n      values: s.sliceTotals[index],\n    };\n    return data_point;\n  }\n  setCurrentDataPoint(index) {\n    let s = this.state;\n    index = parseInt(index);\n    if (index < 0) index = 0;\n    if (index >= s.labels.length) index = s.labels.length - 1;\n    if (index === s.currentIndex) return;\n    s.currentIndex = index;\n    fire(this.parent, \"data-select\", this.getDataPoint());\n  }\n\n  bindUnits() {\n    const units = this.components.get(\"pieSlices\").store;\n    if (!units) return;\n    units.forEach((unit, index) => {\n      unit.addEventListener(\"click\", () => {\n        this.setCurrentDataPoint(index);\n      });\n    });\n  }\n  mouseMove(e) {\n    const target = e.target;\n    let slices = this.components.get(\"pieSlices\").store;\n    let prevIndex = this.curActiveSliceIndex;\n    let prevAcitve = this.curActiveSlice;\n    if (slices.includes(target)) {\n      let i = slices.indexOf(target);\n      this.hoverSlice(prevAcitve, prevIndex, false);\n      this.curActiveSlice = target;\n      this.curActiveSliceIndex = i;\n      this.hoverSlice(target, i, true, e);\n    } else {\n      this.mouseLeave();\n    }\n  }\n\n  mouseLeave() {\n    this.hoverSlice(this.curActiveSlice, this.curActiveSliceIndex, false);\n  }\n}\n"
  },
  {
    "path": "src/js/index.js",
    "content": "import * as Charts from \"./chart\";\n\nlet frappe = {};\n\nfrappe.NAME = \"Frappe Charts\";\nfrappe.VERSION = \"1.6.2\";\n\nfrappe = Object.assign({}, frappe, Charts);\n\nexport default frappe;\n"
  },
  {
    "path": "src/js/objects/ChartComponents.js",
    "content": "import { makeSVGGroup } from \"../utils/draw\";\nimport {\n\tmakeText,\n\tmakePath,\n\txLine,\n\tyLine,\n\tgenerateAxisLabel,\n\tyMarker,\n\tyRegion,\n\tdatasetBar,\n\tdatasetDot,\n\tpercentageBar,\n\tgetPaths,\n\theatSquare,\n} from \"../utils/draw\";\nimport { equilizeNoOfElements } from \"../utils/draw-utils\";\nimport {\n\ttranslateHoriLine,\n\ttranslateVertLine,\n\tanimateRegion,\n\tanimateBar,\n\tanimateDot,\n\tanimatePath,\n\tanimatePathStr,\n} from \"../utils/animate\";\nimport { getMonthName } from \"../utils/date-utils\";\n\nclass ChartComponent {\n\tconstructor({\n\t\tlayerClass = \"\",\n\t\tlayerTransform = \"\",\n\t\tconstants,\n\n\t\tgetData,\n\t\tmakeElements,\n\t\tanimateElements,\n\t}) {\n\t\tthis.layerTransform = layerTransform;\n\t\tthis.constants = constants;\n\n\t\tthis.makeElements = makeElements;\n\t\tthis.getData = getData;\n\n\t\tthis.animateElements = animateElements;\n\n\t\tthis.store = [];\n\t\tthis.labels = [];\n\n\t\tthis.layerClass = layerClass;\n\t\tthis.layerClass =\n\t\t\ttypeof this.layerClass === \"function\"\n\t\t\t\t? this.layerClass()\n\t\t\t\t: this.layerClass;\n\n\t\tthis.refresh();\n\t}\n\n\trefresh(data) {\n\t\tthis.data = data || this.getData();\n\t}\n\n\tsetup(parent) {\n\t\tthis.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent);\n\t}\n\n\tmake() {\n\t\tthis.render(this.data);\n\t\tthis.oldData = this.data;\n\t}\n\n\trender(data) {\n\t\tthis.store = this.makeElements(data);\n\n\t\tthis.layer.textContent = \"\";\n\t\tthis.store.forEach((element) => {\n\t\t\telement.length\n\t\t\t\t? element.forEach((el) => {\n\t\t\t\t\t\tthis.layer.appendChild(el);\n\t\t\t\t  })\n\t\t\t\t: this.layer.appendChild(element);\n\t\t});\n\t\tthis.labels.forEach((element) => {\n\t\t\tthis.layer.appendChild(element);\n\t\t});\n\t}\n\n\tupdate(animate = true) {\n\t\tthis.refresh();\n\t\tlet animateElements = [];\n\t\tif (animate) {\n\t\t\tanimateElements = this.animateElements(this.data) || [];\n\t\t}\n\t\treturn animateElements;\n\t}\n}\n\nlet componentConfigs = {\n\tdonutSlices: {\n\t\tlayerClass: \"donut-slices\",\n\t\tmakeElements(data) {\n\t\t\treturn data.sliceStrings.map((s, i) => {\n\t\t\t\tlet slice = makePath(\n\t\t\t\t\ts,\n\t\t\t\t\t\"donut-path\",\n\t\t\t\t\tdata.colors[i],\n\t\t\t\t\t\"none\",\n\t\t\t\t\tdata.strokeWidth\n\t\t\t\t);\n\t\t\t\tslice.style.transition = \"transform .3s;\";\n\t\t\t\treturn slice;\n\t\t\t});\n\t\t},\n\n\t\tanimateElements(newData) {\n\t\t\treturn this.store.map((slice, i) =>\n\t\t\t\tanimatePathStr(slice, newData.sliceStrings[i])\n\t\t\t);\n\t\t},\n\t},\n\tpieSlices: {\n\t\tlayerClass: \"pie-slices\",\n\t\tmakeElements(data) {\n\t\t\treturn data.sliceStrings.map((s, i) => {\n\t\t\t\tlet slice = makePath(s, \"pie-path\", \"none\", data.colors[i]);\n\t\t\t\tslice.style.transition = \"transform .3s;\";\n\t\t\t\treturn slice;\n\t\t\t});\n\t\t},\n\n\t\tanimateElements(newData) {\n\t\t\treturn this.store.map((slice, i) =>\n\t\t\t\tanimatePathStr(slice, newData.sliceStrings[i])\n\t\t\t);\n\t\t},\n\t},\n\tpercentageBars: {\n\t\tlayerClass: \"percentage-bars\",\n\t\tmakeElements(data) {\n\t\t\tconst numberOfPoints = data.xPositions.length;\n\t\t\treturn data.xPositions.map((x, i) => {\n\t\t\t\tlet y = 0;\n\n\t\t\t\tlet isLast = i == numberOfPoints - 1;\n\t\t\t\tlet isFirst = i == 0;\n\n\t\t\t\tlet bar = percentageBar(\n\t\t\t\t\tx,\n\t\t\t\t\ty,\n\t\t\t\t\tdata.widths[i],\n\t\t\t\t\tthis.constants.barHeight,\n\t\t\t\t\tisFirst,\n\t\t\t\t\tisLast,\n\t\t\t\t\tdata.colors[i]\n\t\t\t\t);\n\t\t\t\treturn bar;\n\t\t\t});\n\t\t},\n\n\t\tanimateElements(newData) {\n\t\t\tif (newData) return [];\n\t\t},\n\t},\n\tyAxis: {\n\t\tlayerClass: \"y axis\",\n\t\tmakeElements(data) {\n\t\t\tlet elements = [];\n\t\t\t// will loop through each yaxis dataset if it exists\n\t\t\tif (data.length) {\n\t\t\t\tdata.forEach((item, i) => {\n\t\t\t\t\titem.positions.map((position, i) => {\n\t\t\t\t\t\telements.push(\n\t\t\t\t\t\t\tyLine(\n\t\t\t\t\t\t\t\tposition,\n\t\t\t\t\t\t\t\titem.labels[i],\n\t\t\t\t\t\t\t\tthis.constants.width,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tmode: this.constants.mode,\n\t\t\t\t\t\t\t\t\tpos: item.pos || this.constants.pos,\n\t\t\t\t\t\t\t\t\tshortenNumbers:\n\t\t\t\t\t\t\t\t\t\tthis.constants.shortenNumbers,\n\t\t\t\t\t\t\t\t\ttitle: item.title,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t});\n\t\t\t\t\t// we need to make yAxis titles if they are defined\n\t\t\t\t\tif (item.title) {\n\t\t\t\t\t\telements.push(\n\t\t\t\t\t\t\tgenerateAxisLabel({\n\t\t\t\t\t\t\t\ttitle: item.title,\n\t\t\t\t\t\t\t\tposition: item.pos,\n\t\t\t\t\t\t\t\theight: this.constants.height || data.zeroLine,\n\t\t\t\t\t\t\t\twidth: this.constants.width,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\treturn elements;\n\t\t\t}\n\n\t\t\tdata.positions.forEach((position, i) => {\n\t\t\t\telements.push(\n\t\t\t\t\tyLine(position, data.labels[i], this.constants.width, {\n\t\t\t\t\t\tmode: this.constants.mode,\n\t\t\t\t\t\tpos: data.pos || this.constants.pos,\n\t\t\t\t\t\tshortenNumbers: this.constants.shortenNumbers,\n\t\t\t\t\t})\n\t\t\t\t);\n\t\t\t});\n\n\t\t\tif (data.title) {\n\t\t\t\telements.push(\n\t\t\t\t\tgenerateAxisLabel({\n\t\t\t\t\t\ttitle: data.title,\n\t\t\t\t\t\tposition: data.pos,\n\t\t\t\t\t\theight: this.constants.height || data.zeroLine,\n\t\t\t\t\t\twidth: this.constants.width,\n\t\t\t\t\t})\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn elements;\n\t\t},\n\n\t\tanimateElements(newData) {\n\t\t\tconst animateMultipleElements = (oldData, newData) => {\n\t\t\t\tlet newPos = newData.positions;\n\t\t\t\tlet newLabels = newData.labels;\n\t\t\t\tlet oldPos = oldData.positions;\n\t\t\t\tlet oldLabels = oldData.labels;\n\n\t\t\t\t[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);\n\t\t\t\t[oldLabels, newLabels] = equilizeNoOfElements(\n\t\t\t\t\toldLabels,\n\t\t\t\t\tnewLabels\n\t\t\t\t);\n\n\t\t\t\tthis.render({\n\t\t\t\t\tpositions: oldPos,\n\t\t\t\t\tlabels: newLabels,\n\t\t\t\t});\n\n\t\t\t\treturn this.store.map((line, i) => {\n\t\t\t\t\treturn translateHoriLine(line, newPos[i], oldPos[i]);\n\t\t\t\t});\n\t\t\t};\n\n\t\t\t// we will need to animate both axis if we have more than one.\n\t\t\t// so check if the oldData is an array of values.\n\t\t\tif (this.oldData instanceof Array) {\n\t\t\t\treturn this.oldData.forEach((old, i) => {\n\t\t\t\t\tanimateMultipleElements(old, newData[i]);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tlet newPos = newData.positions;\n\t\t\tlet newLabels = newData.labels;\n\t\t\tlet oldPos = this.oldData.positions;\n\t\t\tlet oldLabels = this.oldData.labels;\n\n\t\t\t[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);\n\t\t\t[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);\n\n\t\t\tthis.render({\n\t\t\t\tpositions: oldPos,\n\t\t\t\tlabels: newLabels,\n\t\t\t});\n\n\t\t\treturn this.store.map((line, i) => {\n\t\t\t\treturn translateHoriLine(line, newPos[i], oldPos[i]);\n\t\t\t});\n\t\t},\n\t},\n\n\txAxis: {\n\t\tlayerClass: \"x axis\",\n\t\tmakeElements(data) {\n\t\t\treturn data.positions.map((position, i) =>\n\t\t\t\txLine(position, data.calcLabels[i], this.constants.height, {\n\t\t\t\t\tmode: this.constants.mode,\n\t\t\t\t\tpos: this.constants.pos,\n\t\t\t\t})\n\t\t\t);\n\t\t},\n\n\t\tanimateElements(newData) {\n\t\t\tlet newPos = newData.positions;\n\t\t\tlet newLabels = newData.calcLabels;\n\t\t\tlet oldPos = this.oldData.positions;\n\t\t\tlet oldLabels = this.oldData.calcLabels;\n\n\t\t\t[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);\n\t\t\t[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);\n\n\t\t\tthis.render({\n\t\t\t\tpositions: oldPos,\n\t\t\t\tcalcLabels: newLabels,\n\t\t\t});\n\n\t\t\treturn this.store.map((line, i) => {\n\t\t\t\treturn translateVertLine(line, newPos[i], oldPos[i]);\n\t\t\t});\n\t\t},\n\t},\n\n\tyMarkers: {\n\t\tlayerClass: \"y-markers\",\n\t\tmakeElements(data) {\n\t\t\treturn data.map((m) =>\n\t\t\t\tyMarker(m.position, m.label, this.constants.width, {\n\t\t\t\t\tlabelPos: m.options.labelPos,\n\t\t\t\t\tstroke: m.options.stroke,\n\t\t\t\t\tmode: \"span\",\n\t\t\t\t\tlineType: m.options.lineType,\n\t\t\t\t})\n\t\t\t);\n\t\t},\n\t\tanimateElements(newData) {\n\t\t\t[this.oldData, newData] = equilizeNoOfElements(\n\t\t\t\tthis.oldData,\n\t\t\t\tnewData\n\t\t\t);\n\n\t\t\tlet newPos = newData.map((d) => d.position);\n\t\t\tlet newLabels = newData.map((d) => d.label);\n\t\t\tlet newOptions = newData.map((d) => d.options);\n\n\t\t\tlet oldPos = this.oldData.map((d) => d.position);\n\n\t\t\tthis.render(\n\t\t\t\toldPos.map((pos, i) => {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tposition: oldPos[i],\n\t\t\t\t\t\tlabel: newLabels[i],\n\t\t\t\t\t\toptions: newOptions[i],\n\t\t\t\t\t};\n\t\t\t\t})\n\t\t\t);\n\n\t\t\treturn this.store.map((line, i) => {\n\t\t\t\treturn translateHoriLine(line, newPos[i], oldPos[i]);\n\t\t\t});\n\t\t},\n\t},\n\n\tyRegions: {\n\t\tlayerClass: \"y-regions\",\n\t\tmakeElements(data) {\n\t\t\treturn data.map((r) =>\n\t\t\t\tyRegion(r.startPos, r.endPos, this.constants.width, r.label, {\n\t\t\t\t\tlabelPos: r.options.labelPos,\n\t\t\t\t})\n\t\t\t);\n\t\t},\n\t\tanimateElements(newData) {\n\t\t\t[this.oldData, newData] = equilizeNoOfElements(\n\t\t\t\tthis.oldData,\n\t\t\t\tnewData\n\t\t\t);\n\n\t\t\tlet newPos = newData.map((d) => d.endPos);\n\t\t\tlet newLabels = newData.map((d) => d.label);\n\t\t\tlet newStarts = newData.map((d) => d.startPos);\n\t\t\tlet newOptions = newData.map((d) => d.options);\n\n\t\t\tlet oldPos = this.oldData.map((d) => d.endPos);\n\t\t\tlet oldStarts = this.oldData.map((d) => d.startPos);\n\n\t\t\tthis.render(\n\t\t\t\toldPos.map((pos, i) => {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstartPos: oldStarts[i],\n\t\t\t\t\t\tendPos: oldPos[i],\n\t\t\t\t\t\tlabel: newLabels[i],\n\t\t\t\t\t\toptions: newOptions[i],\n\t\t\t\t\t};\n\t\t\t\t})\n\t\t\t);\n\n\t\t\tlet animateElements = [];\n\n\t\t\tthis.store.map((rectGroup, i) => {\n\t\t\t\tanimateElements = animateElements.concat(\n\t\t\t\t\tanimateRegion(rectGroup, newStarts[i], newPos[i], oldPos[i])\n\t\t\t\t);\n\t\t\t});\n\n\t\t\treturn animateElements;\n\t\t},\n\t},\n\n\theatDomain: {\n\t\tlayerClass: function () {\n\t\t\treturn \"heat-domain domain-\" + this.constants.index;\n\t\t},\n\t\tmakeElements(data) {\n\t\t\tlet { index, colWidth, rowHeight, squareSize, radius, xTranslate } =\n\t\t\t\tthis.constants;\n\t\t\tlet monthNameHeight = -12;\n\t\t\tlet x = xTranslate,\n\t\t\t\ty = 0;\n\n\t\t\tthis.serializedSubDomains = [];\n\n\t\t\tdata.cols.map((week, weekNo) => {\n\t\t\t\tif (weekNo === 1) {\n\t\t\t\t\tthis.labels.push(\n\t\t\t\t\t\tmakeText(\n\t\t\t\t\t\t\t\"domain-name\",\n\t\t\t\t\t\t\tx,\n\t\t\t\t\t\t\tmonthNameHeight,\n\t\t\t\t\t\t\tgetMonthName(index, true).toUpperCase(),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tfontSize: 9,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tweek.map((day, i) => {\n\t\t\t\t\tif (day.fill) {\n\t\t\t\t\t\tlet data = {\n\t\t\t\t\t\t\t\"data-date\": day.yyyyMmDd,\n\t\t\t\t\t\t\t\"data-value\": day.dataValue,\n\t\t\t\t\t\t\t\"data-day\": i,\n\t\t\t\t\t\t};\n\t\t\t\t\t\tlet square = heatSquare(\n\t\t\t\t\t\t\t\"day\",\n\t\t\t\t\t\t\tx,\n\t\t\t\t\t\t\ty,\n\t\t\t\t\t\t\tsquareSize,\n\t\t\t\t\t\t\tradius,\n\t\t\t\t\t\t\tday.fill,\n\t\t\t\t\t\t\tdata\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.serializedSubDomains.push(square);\n\t\t\t\t\t}\n\t\t\t\t\ty += rowHeight;\n\t\t\t\t});\n\t\t\t\ty = 0;\n\t\t\t\tx += colWidth;\n\t\t\t});\n\n\t\t\treturn this.serializedSubDomains;\n\t\t},\n\n\t\tanimateElements(newData) {\n\t\t\tif (newData) return [];\n\t\t},\n\t},\n\n\tbarGraph: {\n\t\tlayerClass: function () {\n\t\t\treturn \"dataset-units dataset-bars dataset-\" + this.constants.index;\n\t\t},\n\t\tmakeElements(data) {\n\t\t\tlet c = this.constants;\n\t\t\tthis.unitType = \"bar\";\n\t\t\tthis.units = data.yPositions.map((y, j) => {\n\t\t\t\treturn datasetBar(\n\t\t\t\t\tdata.xPositions[j],\n\t\t\t\t\ty,\n\t\t\t\t\tdata.barWidth,\n\t\t\t\t\tc.color,\n\t\t\t\t\tdata.labels[j],\n\t\t\t\t\tj,\n\t\t\t\t\tdata.offsets[j],\n\t\t\t\t\t{\n\t\t\t\t\t\tzeroLine: data.zeroLine,\n\t\t\t\t\t\tbarsWidth: data.barsWidth,\n\t\t\t\t\t\tminHeight: c.minHeight,\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t});\n\t\t\treturn this.units;\n\t\t},\n\t\tanimateElements(newData) {\n\t\t\tlet newXPos = newData.xPositions;\n\t\t\tlet newYPos = newData.yPositions;\n\t\t\tlet newOffsets = newData.offsets;\n\t\t\tlet newLabels = newData.labels;\n\n\t\t\tlet oldXPos = this.oldData.xPositions;\n\t\t\tlet oldYPos = this.oldData.yPositions;\n\t\t\tlet oldOffsets = this.oldData.offsets;\n\t\t\tlet oldLabels = this.oldData.labels;\n\n\t\t\t[oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos);\n\t\t\t[oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos);\n\t\t\t[oldOffsets, newOffsets] = equilizeNoOfElements(\n\t\t\t\toldOffsets,\n\t\t\t\tnewOffsets\n\t\t\t);\n\t\t\t[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);\n\n\t\t\tthis.render({\n\t\t\t\txPositions: oldXPos,\n\t\t\t\tyPositions: oldYPos,\n\t\t\t\toffsets: oldOffsets,\n\t\t\t\tlabels: newLabels,\n\n\t\t\t\tzeroLine: this.oldData.zeroLine,\n\t\t\t\tbarsWidth: this.oldData.barsWidth,\n\t\t\t\tbarWidth: this.oldData.barWidth,\n\t\t\t});\n\n\t\t\tlet animateElements = [];\n\n\t\t\tthis.store.map((bar, i) => {\n\t\t\t\tanimateElements = animateElements.concat(\n\t\t\t\t\tanimateBar(\n\t\t\t\t\t\tbar,\n\t\t\t\t\t\tnewXPos[i],\n\t\t\t\t\t\tnewYPos[i],\n\t\t\t\t\t\tnewData.barWidth,\n\t\t\t\t\t\tnewOffsets[i],\n\t\t\t\t\t\t{ zeroLine: newData.zeroLine }\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t});\n\n\t\t\treturn animateElements;\n\t\t},\n\t},\n\n\tlineGraph: {\n\t\tlayerClass: function () {\n\t\t\treturn \"dataset-units dataset-line dataset-\" + this.constants.index;\n\t\t},\n\t\tmakeElements(data) {\n\t\t\tlet c = this.constants;\n\t\t\tthis.unitType = \"dot\";\n\t\t\tthis.paths = {};\n\t\t\tif (!c.hideLine) {\n\t\t\t\tthis.paths = getPaths(\n\t\t\t\t\tdata.xPositions,\n\t\t\t\t\tdata.yPositions,\n\t\t\t\t\tc.color,\n\t\t\t\t\t{\n\t\t\t\t\t\theatline: c.heatline,\n\t\t\t\t\t\tregionFill: c.regionFill,\n\t\t\t\t\t\tspline: c.spline,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tsvgDefs: c.svgDefs,\n\t\t\t\t\t\tzeroLine: data.zeroLine,\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthis.units = [];\n\t\t\tif (!c.hideDots) {\n\t\t\t\tthis.units = data.yPositions.map((y, j) => {\n\t\t\t\t\treturn datasetDot(\n\t\t\t\t\t\tdata.xPositions[j],\n\t\t\t\t\t\ty,\n\t\t\t\t\t\tdata.radius,\n\t\t\t\t\t\tc.color,\n\t\t\t\t\t\tc.valuesOverPoints ? data.values[j] : \"\",\n\t\t\t\t\t\tj\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn Object.values(this.paths).concat(this.units);\n\t\t},\n\t\tanimateElements(newData) {\n\t\t\tlet newXPos = newData.xPositions;\n\t\t\tlet newYPos = newData.yPositions;\n\t\t\tlet newValues = newData.values;\n\n\t\t\tlet oldXPos = this.oldData.xPositions;\n\t\t\tlet oldYPos = this.oldData.yPositions;\n\t\t\tlet oldValues = this.oldData.values;\n\n\t\t\t[oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos);\n\t\t\t[oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos);\n\t\t\t[oldValues, newValues] = equilizeNoOfElements(oldValues, newValues);\n\n\t\t\tthis.render({\n\t\t\t\txPositions: oldXPos,\n\t\t\t\tyPositions: oldYPos,\n\t\t\t\tvalues: newValues,\n\n\t\t\t\tzeroLine: this.oldData.zeroLine,\n\t\t\t\tradius: this.oldData.radius,\n\t\t\t});\n\n\t\t\tlet animateElements = [];\n\n\t\t\tif (Object.keys(this.paths).length) {\n\t\t\t\tanimateElements = animateElements.concat(\n\t\t\t\t\tanimatePath(\n\t\t\t\t\t\tthis.paths,\n\t\t\t\t\t\tnewXPos,\n\t\t\t\t\t\tnewYPos,\n\t\t\t\t\t\tnewData.zeroLine,\n\t\t\t\t\t\tthis.constants.spline\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (this.units.length) {\n\t\t\t\tthis.units.map((dot, i) => {\n\t\t\t\t\tanimateElements = animateElements.concat(\n\t\t\t\t\t\tanimateDot(dot, newXPos[i], newYPos[i])\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn animateElements;\n\t\t},\n\t},\n};\n\nexport function getComponent(name, constants, getData) {\n\tlet keys = Object.keys(componentConfigs).filter((k) => name.includes(k));\n\tlet config = componentConfigs[keys[0]];\n\tObject.assign(config, {\n\t\tconstants: constants,\n\t\tgetData: getData,\n\t});\n\treturn new ChartComponent(config);\n}\n"
  },
  {
    "path": "src/js/objects/SvgTip.js",
    "content": "import { $ } from \"../utils/dom\";\nimport { TOOLTIP_POINTER_TRIANGLE_HEIGHT } from \"../utils/constants\";\n\nexport default class SvgTip {\n  constructor({ parent = null, colors = [] }) {\n    this.parent = parent;\n    this.colors = colors;\n    this.titleName = \"\";\n    this.titleValue = \"\";\n    this.listValues = [];\n    this.titleValueFirst = 0;\n\n    this.x = 0;\n    this.y = 0;\n\n    this.top = 0;\n    this.left = 0;\n\n    this.setup();\n  }\n\n  setup() {\n    this.makeTooltip();\n  }\n\n  refresh() {\n    this.fill();\n    this.calcPosition();\n  }\n\n  makeTooltip() {\n    this.container = $.create(\"div\", {\n      inside: this.parent,\n      className: \"graph-svg-tip comparison\",\n      innerHTML: `<span class=\"title\"></span>\n\t\t\t\t<ul class=\"data-point-list\"></ul>\n\t\t\t\t<div class=\"svg-pointer\"></div>`,\n    });\n    this.hideTip();\n\n    this.title = this.container.querySelector(\".title\");\n    this.list = this.container.querySelector(\".data-point-list\");\n    this.dataPointList = this.container.querySelector(\".data-point-list\");\n\n    this.parent.addEventListener(\"mouseleave\", () => {\n      this.hideTip();\n    });\n  }\n\n  fill() {\n    let title;\n    if (this.index) {\n      this.container.setAttribute(\"data-point-index\", this.index);\n    }\n    if (this.titleValueFirst) {\n      title = `<strong>${this.titleValue}</strong>${this.titleName}`;\n    } else {\n      title = `${this.titleName}<strong>${this.titleValue}</strong>`;\n    }\n\n    if (this.listValues.length > 4) {\n      this.list.classList.add(\"tooltip-grid\");\n    } else {\n      this.list.classList.remove(\"tooltip-grid\");\n    }\n\n    this.title.innerHTML = title;\n    this.dataPointList.innerHTML = \"\";\n\n    this.listValues.map((set, i) => {\n      const color = this.colors[i] || \"black\";\n      let value =\n        set.formatted === 0 || set.formatted ? set.formatted : set.value;\n      let li = $.create(\"li\", {\n        innerHTML: `<div class=\"tooltip-legend\" style=\"background: ${color};\"></div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"tooltip-value\">${value === 0 || value ? value : \"\"}</div>\n\t\t\t\t\t\t<div class=\"tooltip-label\">${set.title ? set.title : \"\"}</div>\n\t\t\t\t\t</div>`,\n      });\n\n      this.dataPointList.appendChild(li);\n    });\n  }\n\n  calcPosition() {\n    let width = this.container.offsetWidth;\n\n    this.top =\n      this.y - this.container.offsetHeight - TOOLTIP_POINTER_TRIANGLE_HEIGHT;\n    this.left = this.x - width / 2;\n    let maxLeft = this.parent.offsetWidth - width;\n\n    let pointer = this.container.querySelector(\".svg-pointer\");\n\n    if (this.left < 0) {\n      pointer.style.left = `calc(50% - ${-1 * this.left}px)`;\n      this.left = 0;\n    } else if (this.left > maxLeft) {\n      let delta = this.left - maxLeft;\n      let pointerOffset = `calc(50% + ${delta}px)`;\n      pointer.style.left = pointerOffset;\n\n      this.left = maxLeft;\n    } else {\n      pointer.style.left = `50%`;\n    }\n  }\n\n  setValues(x, y, title = {}, listValues = [], index = -1) {\n    this.titleName = title.name;\n    this.titleValue = title.value;\n    this.listValues = listValues;\n    this.x = x;\n    this.y = y;\n    this.titleValueFirst = title.valueFirst || 0;\n    this.index = index;\n    this.refresh();\n  }\n\n  hideTip() {\n    this.container.style.top = \"0px\";\n    this.container.style.left = \"0px\";\n    this.container.style.opacity = \"0\";\n  }\n\n  showTip() {\n    this.container.style.top = this.top + \"px\";\n    this.container.style.left = this.left + \"px\";\n    this.container.style.opacity = \"1\";\n  }\n}\n"
  },
  {
    "path": "src/js/utils/animate.js",
    "content": "import { getBarHeightAndYAttr, getSplineCurvePointsStr } from \"./draw-utils\";\n\nexport const UNIT_ANIM_DUR = 350;\nexport const PATH_ANIM_DUR = 350;\nexport const MARKER_LINE_ANIM_DUR = UNIT_ANIM_DUR;\nexport const REPLACE_ALL_NEW_DUR = 250;\n\nexport const STD_EASING = \"easein\";\n\nexport function translate(unit, oldCoord, newCoord, duration) {\n  let old = typeof oldCoord === \"string\" ? oldCoord : oldCoord.join(\", \");\n  return [\n    unit,\n    { transform: newCoord.join(\", \") },\n    duration,\n    STD_EASING,\n    \"translate\",\n    { transform: old },\n  ];\n}\n\nexport function translateVertLine(xLine, newX, oldX) {\n  return translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR);\n}\n\nexport function translateHoriLine(yLine, newY, oldY) {\n  return translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR);\n}\n\nexport function animateRegion(rectGroup, newY1, newY2, oldY2) {\n  let newHeight = newY1 - newY2;\n  let rect = rectGroup.childNodes[0];\n  let width = rect.getAttribute(\"width\");\n  let rectAnim = [\n    rect,\n    { height: newHeight, \"stroke-dasharray\": `${width}, ${newHeight}` },\n    MARKER_LINE_ANIM_DUR,\n    STD_EASING,\n  ];\n\n  let groupAnim = translate(\n    rectGroup,\n    [0, oldY2],\n    [0, newY2],\n    MARKER_LINE_ANIM_DUR\n  );\n  return [rectAnim, groupAnim];\n}\n\nexport function animateBar(bar, x, yTop, width, offset = 0, meta = {}) {\n  let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);\n  y -= offset;\n  if (bar.nodeName !== \"rect\") {\n    let rect = bar.childNodes[0];\n    let rectAnim = [\n      rect,\n      { width: width, height: height },\n      UNIT_ANIM_DUR,\n      STD_EASING,\n    ];\n\n    let oldCoordStr = bar.getAttribute(\"transform\").split(\"(\")[1].slice(0, -1);\n    let groupAnim = translate(bar, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);\n    return [rectAnim, groupAnim];\n  } else {\n    return [\n      [\n        bar,\n        { width: width, height: height, x: x, y: y },\n        UNIT_ANIM_DUR,\n        STD_EASING,\n      ],\n    ];\n  }\n  // bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein);\n}\n\nexport function animateDot(dot, x, y) {\n  if (dot.nodeName !== \"circle\") {\n    let oldCoordStr = dot.getAttribute(\"transform\").split(\"(\")[1].slice(0, -1);\n    let groupAnim = translate(dot, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);\n    return [groupAnim];\n  } else {\n    return [[dot, { cx: x, cy: y }, UNIT_ANIM_DUR, STD_EASING]];\n  }\n  // dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);\n}\n\nexport function animatePath(paths, newXList, newYList, zeroLine, spline) {\n  let pathComponents = [];\n  let pointsStr = newYList.map((y, i) => newXList[i] + \",\" + y).join(\"L\");\n\n  if (spline) pointsStr = getSplineCurvePointsStr(newXList, newYList);\n\n  const animPath = [\n    paths.path,\n    { d: \"M\" + pointsStr },\n    PATH_ANIM_DUR,\n    STD_EASING,\n  ];\n  pathComponents.push(animPath);\n\n  if (paths.region) {\n    let regStartPt = `${newXList[0]},${zeroLine}L`;\n    let regEndPt = `L${newXList.slice(-1)[0]}, ${zeroLine}`;\n\n    const animRegion = [\n      paths.region,\n      { d: \"M\" + regStartPt + pointsStr + regEndPt },\n      PATH_ANIM_DUR,\n      STD_EASING,\n    ];\n    pathComponents.push(animRegion);\n  }\n\n  return pathComponents;\n}\n\nexport function animatePathStr(oldPath, pathStr) {\n  return [oldPath, { d: pathStr }, UNIT_ANIM_DUR, STD_EASING];\n}\n"
  },
  {
    "path": "src/js/utils/animation.js",
    "content": "// Leveraging SMIL Animations\n\nimport { REPLACE_ALL_NEW_DUR } from \"./animate\";\n\nconst EASING = {\n  ease: \"0.25 0.1 0.25 1\",\n  linear: \"0 0 1 1\",\n  // easein: \"0.42 0 1 1\",\n  easein: \"0.1 0.8 0.2 1\",\n  easeout: \"0 0 0.58 1\",\n  easeinout: \"0.42 0 0.58 1\",\n};\n\nfunction animateSVGElement(\n  element,\n  props,\n  dur,\n  easingType = \"linear\",\n  type = undefined,\n  oldValues = {}\n) {\n  let animElement = element.cloneNode(true);\n  let newElement = element.cloneNode(true);\n\n  for (var attributeName in props) {\n    let animateElement;\n    if (attributeName === \"transform\") {\n      animateElement = document.createElementNS(\n        \"http://www.w3.org/2000/svg\",\n        \"animateTransform\"\n      );\n    } else {\n      animateElement = document.createElementNS(\n        \"http://www.w3.org/2000/svg\",\n        \"animate\"\n      );\n    }\n    let currentValue =\n      oldValues[attributeName] || element.getAttribute(attributeName);\n    let value = props[attributeName];\n\n    let animAttr = {\n      attributeName: attributeName,\n      from: currentValue,\n      to: value,\n      begin: \"0s\",\n      dur: dur / 1000 + \"s\",\n      values: currentValue + \";\" + value,\n      keySplines: EASING[easingType],\n      keyTimes: \"0;1\",\n      calcMode: \"spline\",\n      fill: \"freeze\",\n    };\n\n    if (type) {\n      animAttr[\"type\"] = type;\n    }\n\n    for (var i in animAttr) {\n      animateElement.setAttribute(i, animAttr[i]);\n    }\n\n    animElement.appendChild(animateElement);\n\n    if (type) {\n      newElement.setAttribute(attributeName, `translate(${value})`);\n    } else {\n      newElement.setAttribute(attributeName, value);\n    }\n  }\n\n  return [animElement, newElement];\n}\n\nexport function transform(element, style) {\n  // eslint-disable-line no-unused-vars\n  element.style.transform = style;\n  element.style.webkitTransform = style;\n  element.style.msTransform = style;\n  element.style.mozTransform = style;\n  element.style.oTransform = style;\n}\n\nfunction animateSVG(svgContainer, elements) {\n  let newElements = [];\n  let animElements = [];\n\n  elements.map((element) => {\n    let unit = element[0];\n    let parent = unit.parentNode;\n\n    let animElement, newElement;\n\n    element[0] = unit;\n    [animElement, newElement] = animateSVGElement(...element);\n\n    newElements.push(newElement);\n    animElements.push([animElement, parent]);\n\n    if (parent) {\n      parent.replaceChild(animElement, unit);\n    }\n  });\n\n  let animSvg = svgContainer.cloneNode(true);\n\n  animElements.map((animElement, i) => {\n    if (animElement[1]) {\n      animElement[1].replaceChild(newElements[i], animElement[0]);\n      elements[i][0] = newElements[i];\n    }\n  });\n\n  return animSvg;\n}\n\nexport function runSMILAnimation(parent, svgElement, elementsToAnimate) {\n  if (elementsToAnimate.length === 0) return;\n\n  let animSvgElement = animateSVG(svgElement, elementsToAnimate);\n  if (svgElement.parentNode == parent) {\n    parent.removeChild(svgElement);\n    parent.appendChild(animSvgElement);\n  }\n\n  // Replace the new svgElement (data has already been replaced)\n  setTimeout(() => {\n    if (animSvgElement.parentNode == parent) {\n      parent.removeChild(animSvgElement);\n      parent.appendChild(svgElement);\n    }\n  }, REPLACE_ALL_NEW_DUR);\n}\n"
  },
  {
    "path": "src/js/utils/axis-chart-utils.js",
    "content": "import { fillArray } from \"../utils/helpers\";\nimport {\n\tDEFAULT_AXIS_CHART_TYPE,\n\tAXIS_DATASET_CHART_TYPES,\n\tDEFAULT_CHAR_WIDTH,\n\tSERIES_LABEL_SPACE_RATIO,\n} from \"../utils/constants\";\n\nexport function dataPrep(data, type, config) {\n\tdata.labels = data.labels || [];\n\n\tlet datasetLength = data.labels.length;\n\n\t// Datasets\n\tlet datasets = data.datasets;\n\tlet zeroArray = new Array(datasetLength).fill(0);\n\tif (!datasets) {\n\t\t// default\n\t\tdatasets = [\n\t\t\t{\n\t\t\t\tvalues: zeroArray,\n\t\t\t},\n\t\t];\n\t}\n\n\tdatasets.map((d) => {\n\t\t// Set values\n\t\tif (!d.values) {\n\t\t\td.values = zeroArray;\n\t\t} else {\n\t\t\t// Check for non values\n\t\t\tlet vals = d.values;\n\t\t\tvals = vals.map((val) => (!isNaN(val) ? val : 0));\n\n\t\t\t// Trim or extend\n\t\t\tif (vals.length > datasetLength) {\n\t\t\t\tvals = vals.slice(0, datasetLength);\n\t\t\t}\n\t\t\tif (config) {\n\t\t\t\tvals = fillArray(vals, datasetLength - vals.length, null);\n\t\t\t} else {\n\t\t\t\tvals = fillArray(vals, datasetLength - vals.length, 0);\n\t\t\t}\n\t\t\td.values = vals;\n\t\t}\n\n\t\t// Set type\n\t\tif (!d.chartType) {\n\t\t\tif (!AXIS_DATASET_CHART_TYPES.includes(type))\n\t\t\t\ttype = DEFAULT_AXIS_CHART_TYPE;\n\t\t\td.chartType = type;\n\t\t}\n\t});\n\n\t// Markers\n\n\t// Regions\n\t// data.yRegions = data.yRegions || [];\n\tif (data.yRegions) {\n\t\tdata.yRegions.map((d) => {\n\t\t\tif (d.end < d.start) {\n\t\t\t\t[d.start, d.end] = [d.end, d.start];\n\t\t\t}\n\t\t});\n\t}\n\n\treturn data;\n}\n\nexport function zeroDataPrep(realData) {\n\tlet datasetLength = realData.labels.length;\n\tlet zeroArray = new Array(datasetLength).fill(0);\n\n\tlet zeroData = {\n\t\tlabels: realData.labels.slice(0, -1),\n\t\tdatasets: realData.datasets.map((d) => {\n\t\t\tconst { axisID } = d;\n\t\t\treturn {\n\t\t\t\taxisID,\n\t\t\t\tname: \"\",\n\t\t\t\tvalues: zeroArray.slice(0, -1),\n\t\t\t\tchartType: d.chartType,\n\t\t\t};\n\t\t}),\n\t};\n\n\tif (realData.yMarkers) {\n\t\tzeroData.yMarkers = [\n\t\t\t{\n\t\t\t\tvalue: 0,\n\t\t\t\tlabel: \"\",\n\t\t\t},\n\t\t];\n\t}\n\n\tif (realData.yRegions) {\n\t\tzeroData.yRegions = [\n\t\t\t{\n\t\t\t\tstart: 0,\n\t\t\t\tend: 0,\n\t\t\t\tlabel: \"\",\n\t\t\t},\n\t\t];\n\t}\n\n\treturn zeroData;\n}\n\nexport function getShortenedLabels(chartWidth, labels = [], isSeries = true) {\n\tlet allowedSpace = (chartWidth / labels.length) * SERIES_LABEL_SPACE_RATIO;\n\tif (allowedSpace <= 0) allowedSpace = 1;\n\tlet allowedLetters = allowedSpace / DEFAULT_CHAR_WIDTH;\n\n\tlet seriesMultiple;\n\tif (isSeries) {\n\t\t// Find the maximum label length for spacing calculations\n\t\tlet maxLabelLength = Math.max(...labels.map((label) => label.length));\n\t\tseriesMultiple = Math.ceil(maxLabelLength / allowedLetters);\n\t}\n\n\tlet calcLabels = labels.map((label, i) => {\n\t\tlabel += \"\";\n\t\tif (label.length > allowedLetters) {\n\t\t\tif (!isSeries) {\n\t\t\t\tif (allowedLetters - 3 > 0) {\n\t\t\t\t\tlabel = label.slice(0, allowedLetters - 3) + \" ...\";\n\t\t\t\t} else {\n\t\t\t\t\tlabel = label.slice(0, allowedLetters) + \"..\";\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (i % seriesMultiple !== 0 && i !== labels.length - 1) {\n\t\t\t\t\tlabel = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn label;\n\t});\n\n\treturn calcLabels;\n}\n"
  },
  {
    "path": "src/js/utils/colors.js",
    "content": "const PRESET_COLOR_MAP = {\n  pink: \"#F683AE\",\n  blue: \"#318AD8\",\n  green: \"#48BB74\",\n  grey: \"#A6B1B9\",\n  red: \"#F56B6B\",\n  yellow: \"#FACF7A\",\n  purple: \"#44427B\",\n  teal: \"#5FD8C4\",\n  cyan: \"#15CCEF\",\n  orange: \"#F8814F\",\n  \"light-pink\": \"#FED7E5\",\n  \"light-blue\": \"#BFDDF7\",\n  \"light-green\": \"#48BB74\",\n  \"light-grey\": \"#F4F5F6\",\n  \"light-red\": \"#F6DFDF\",\n  \"light-yellow\": \"#FEE9BF\",\n  \"light-purple\": \"#E8E8F7\",\n  \"light-teal\": \"#D3FDF6\",\n  \"light-cyan\": \"#DDF8FD\",\n  \"light-orange\": \"#FECDB8\",\n};\n\nfunction limitColor(r) {\n  if (r > 255) return 255;\n  else if (r < 0) return 0;\n  return r;\n}\n\nexport function lightenDarkenColor(color, amt) {\n  let col = getColor(color);\n  let usePound = false;\n  if (col[0] == \"#\") {\n    col = col.slice(1);\n    usePound = true;\n  }\n  let num = parseInt(col, 16);\n  let r = limitColor((num >> 16) + amt);\n  let b = limitColor(((num >> 8) & 0x00ff) + amt);\n  let g = limitColor((num & 0x0000ff) + amt);\n  return (usePound ? \"#\" : \"\") + (g | (b << 8) | (r << 16)).toString(16);\n}\n\nexport function isValidColor(string) {\n  // https://stackoverflow.com/a/32685393\n  let HEX_RE = /(^\\s*)(#)((?:[A-Fa-f0-9]{3}){1,2})$/i;\n  let RGB_RE =\n    /(^\\s*)(rgb|hsl)(a?)[(]\\s*([\\d.]+\\s*%?)\\s*,\\s*([\\d.]+\\s*%?)\\s*,\\s*([\\d.]+\\s*%?)\\s*(?:,\\s*([\\d.]+)\\s*)?[)]$/i;\n  return HEX_RE.test(string) || RGB_RE.test(string);\n}\n\nexport const getColor = (color) => {\n  // When RGB color, convert to hexadecimal (alpha value is omitted)\n  if (/rgb[a]{0,1}\\([\\d, ]+\\)/gim.test(color)) {\n    return /\\D+(\\d*)\\D+(\\d*)\\D+(\\d*)/gim\n      .exec(color)\n      .map((x, i) => (i !== 0 ? Number(x).toString(16) : \"#\"))\n      .reduce((c, ch) => `${c}${ch}`);\n  }\n  return PRESET_COLOR_MAP[color] || color;\n};\n"
  },
  {
    "path": "src/js/utils/constants.js",
    "content": "export const ALL_CHART_TYPES = [\n  \"line\",\n  \"scatter\",\n  \"bar\",\n  \"percentage\",\n  \"heatmap\",\n  \"pie\",\n];\n\nexport const COMPATIBLE_CHARTS = {\n  bar: [\"line\", \"scatter\", \"percentage\", \"pie\"],\n  line: [\"scatter\", \"bar\", \"percentage\", \"pie\"],\n  pie: [\"line\", \"scatter\", \"percentage\", \"bar\"],\n  percentage: [\"bar\", \"line\", \"scatter\", \"pie\"],\n  heatmap: [],\n};\n\nexport const DATA_COLOR_DIVISIONS = {\n  bar: \"datasets\",\n  line: \"datasets\",\n  pie: \"labels\",\n  percentage: \"labels\",\n  heatmap: HEATMAP_DISTRIBUTION_SIZE,\n};\n\nexport const BASE_MEASURES = {\n  margins: {\n    top: 10,\n    bottom: 10,\n    left: 20,\n    right: 20,\n  },\n  paddings: {\n    top: 20,\n    bottom: 40,\n    left: 30,\n    right: 10,\n  },\n\n  baseHeight: 240,\n  titleHeight: 20,\n  legendHeight: 30,\n\n  titleFontSize: 12,\n};\n\nexport function getTopOffset(m) {\n  return m.titleHeight + m.margins.top + m.paddings.top;\n}\n\nexport function getLeftOffset(m) {\n  return m.margins.left + m.paddings.left;\n}\n\nexport function getExtraHeight(m) {\n  let totalExtraHeight =\n    m.margins.top +\n    m.margins.bottom +\n    m.paddings.top +\n    m.paddings.bottom +\n    m.titleHeight +\n    m.legendHeight;\n  return totalExtraHeight;\n}\n\nexport function getExtraWidth(m) {\n  let totalExtraWidth =\n    m.margins.left + m.margins.right + m.paddings.left + m.paddings.right;\n\n  return totalExtraWidth;\n}\n\nexport const INIT_CHART_UPDATE_TIMEOUT = 700;\nexport const CHART_POST_ANIMATE_TIMEOUT = 400;\n\nexport const DEFAULT_AXIS_CHART_TYPE = \"line\";\nexport const AXIS_DATASET_CHART_TYPES = [\"line\", \"bar\"];\n\nexport const LEGEND_ITEM_WIDTH = 150;\nexport const SERIES_LABEL_SPACE_RATIO = 0.6;\n\nexport const BAR_CHART_SPACE_RATIO = 0.5;\nexport const MIN_BAR_PERCENT_HEIGHT = 0.0;\n\nexport const LINE_CHART_DOT_SIZE = 4;\nexport const DOT_OVERLAY_SIZE_INCR = 4;\n\nexport const PERCENTAGE_BAR_DEFAULT_HEIGHT = 16;\n\n// Fixed 5-color theme,\n// More colors are difficult to parse visually\nexport const HEATMAP_DISTRIBUTION_SIZE = 5;\n\nexport const HEATMAP_SQUARE_SIZE = 10;\nexport const HEATMAP_GUTTER_SIZE = 2;\n\nexport const DEFAULT_CHAR_WIDTH = 7;\n\nexport const TOOLTIP_POINTER_TRIANGLE_HEIGHT = 7.48;\nconst DEFAULT_CHART_COLORS = [\n  \"pink\",\n  \"blue\",\n  \"green\",\n  \"grey\",\n  \"red\",\n  \"yellow\",\n  \"purple\",\n  \"teal\",\n  \"cyan\",\n  \"orange\",\n];\nconst HEATMAP_COLORS_GREEN = [\n  \"#ebedf0\",\n  \"#c6e48b\",\n  \"#7bc96f\",\n  \"#239a3b\",\n  \"#196127\",\n];\nexport const HEATMAP_COLORS_BLUE = [\n  \"#ebedf0\",\n  \"#c0ddf9\",\n  \"#73b3f3\",\n  \"#3886e1\",\n  \"#17459e\",\n];\nexport const HEATMAP_COLORS_YELLOW = [\n  \"#ebedf0\",\n  \"#fdf436\",\n  \"#ffc700\",\n  \"#ff9100\",\n  \"#06001c\",\n];\n\nexport const DEFAULT_COLORS = {\n  bar: DEFAULT_CHART_COLORS,\n  line: DEFAULT_CHART_COLORS,\n  pie: DEFAULT_CHART_COLORS,\n  percentage: DEFAULT_CHART_COLORS,\n  heatmap: HEATMAP_COLORS_GREEN,\n  donut: DEFAULT_CHART_COLORS,\n};\n\n// Universal constants\nexport const ANGLE_RATIO = Math.PI / 180;\nexport const FULL_ANGLE = 360;\n"
  },
  {
    "path": "src/js/utils/date-utils.js",
    "content": "// Playing around with dates\n\nexport const NO_OF_YEAR_MONTHS = 12;\nexport const NO_OF_DAYS_IN_WEEK = 7;\nexport const DAYS_IN_YEAR = 375;\nexport const NO_OF_MILLIS = 1000;\nexport const SEC_IN_DAY = 86400;\n\nexport const MONTH_NAMES = [\n  \"January\",\n  \"February\",\n  \"March\",\n  \"April\",\n  \"May\",\n  \"June\",\n  \"July\",\n  \"August\",\n  \"September\",\n  \"October\",\n  \"November\",\n  \"December\",\n];\nexport const MONTH_NAMES_SHORT = [\n  \"Jan\",\n  \"Feb\",\n  \"Mar\",\n  \"Apr\",\n  \"May\",\n  \"Jun\",\n  \"Jul\",\n  \"Aug\",\n  \"Sep\",\n  \"Oct\",\n  \"Nov\",\n  \"Dec\",\n];\n\nexport const DAY_NAMES_SHORT = [\n  \"Sun\",\n  \"Mon\",\n  \"Tue\",\n  \"Wed\",\n  \"Thu\",\n  \"Fri\",\n  \"Sat\",\n];\nexport const DAY_NAMES = [\n  \"Sunday\",\n  \"Monday\",\n  \"Tuesday\",\n  \"Wednesday\",\n  \"Thursday\",\n  \"Friday\",\n  \"Saturday\",\n];\n\n// https://stackoverflow.com/a/11252167/6495043\nfunction treatAsUtc(date) {\n  let result = new Date(date);\n  result.setMinutes(result.getMinutes() - result.getTimezoneOffset());\n  return result;\n}\n\nexport function toMidnightUTC(date) {\n  let result = new Date(date);\n  result.setUTCHours(0, result.getTimezoneOffset(), 0, 0);\n  return result;\n}\n\nexport function getYyyyMmDd(date) {\n  let dd = date.getDate();\n  let mm = date.getMonth() + 1; // getMonth() is zero-based\n  return [\n    date.getFullYear(),\n    (mm > 9 ? \"\" : \"0\") + mm,\n    (dd > 9 ? \"\" : \"0\") + dd,\n  ].join(\"-\");\n}\n\nexport function clone(date) {\n  return new Date(date.getTime());\n}\n\nexport function timestampSec(date) {\n  return date.getTime() / NO_OF_MILLIS;\n}\n\nexport function timestampToMidnight(timestamp, roundAhead = false) {\n  let midnightTs = Math.floor(timestamp - (timestamp % SEC_IN_DAY));\n  if (roundAhead) {\n    return midnightTs + SEC_IN_DAY;\n  }\n  return midnightTs;\n}\n\n// export function getMonthsBetween(startDate, endDate) {}\n\nexport function getWeeksBetween(startDate, endDate) {\n  let weekStartDate = setDayToSunday(startDate);\n  return Math.ceil(getDaysBetween(weekStartDate, endDate) / NO_OF_DAYS_IN_WEEK);\n}\n\nexport function getDaysBetween(startDate, endDate) {\n  let millisecondsPerDay = SEC_IN_DAY * NO_OF_MILLIS;\n  return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay;\n}\n\nexport function areInSameMonth(startDate, endDate) {\n  return (\n    startDate.getMonth() === endDate.getMonth() &&\n    startDate.getFullYear() === endDate.getFullYear()\n  );\n}\n\nexport function getMonthName(i, short = false) {\n  let monthName = MONTH_NAMES[i];\n  return short ? monthName.slice(0, 3) : monthName;\n}\n\nexport function getLastDateInMonth(month, year) {\n  return new Date(year, month + 1, 0); // 0: last day in previous month\n}\n\n// mutates\nexport function setDayToSunday(date) {\n  let newDate = clone(date);\n  const day = newDate.getDay();\n  if (day !== 0) {\n    addDays(newDate, -1 * day);\n  }\n  return newDate;\n}\n\n// mutates\nexport function addDays(date, numberOfDays) {\n  date.setDate(date.getDate() + numberOfDays);\n}\n"
  },
  {
    "path": "src/js/utils/dom.js",
    "content": "export function $(expr, con) {\n  return typeof expr === \"string\"\n    ? (con || document).querySelector(expr)\n    : expr || null;\n}\n\nexport function findNodeIndex(node) {\n  var i = 0;\n  while (node.previousSibling) {\n    node = node.previousSibling;\n    i++;\n  }\n  return i;\n}\n\n$.create = (tag, o) => {\n  var element = document.createElement(tag);\n\n  for (var i in o) {\n    var val = o[i];\n\n    if (i === \"inside\") {\n      $(val).appendChild(element);\n    } else if (i === \"around\") {\n      var ref = $(val);\n      ref.parentNode.insertBefore(element, ref);\n      element.appendChild(ref);\n    } else if (i === \"styles\") {\n      if (typeof val === \"object\") {\n        Object.keys(val).map((prop) => {\n          element.style[prop] = val[prop];\n        });\n      }\n    } else if (i in element) {\n      element[i] = val;\n    } else {\n      element.setAttribute(i, val);\n    }\n  }\n\n  return element;\n};\n\nexport function getOffset(element) {\n  let rect = element.getBoundingClientRect();\n  return {\n    // https://stackoverflow.com/a/7436602/6495043\n    // rect.top varies with scroll, so we add whatever has been\n    // scrolled to it to get absolute distance from actual page top\n    top:\n      rect.top +\n      (document.documentElement.scrollTop || document.body.scrollTop),\n    left:\n      rect.left +\n      (document.documentElement.scrollLeft || document.body.scrollLeft),\n  };\n}\n\n// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent\n// an element's offsetParent property will return null whenever it, or any of its parents,\n// is hidden via the display style property.\nexport function isHidden(el) {\n  return el.offsetParent === null;\n}\n\nexport function isElementInViewport(el) {\n  // Although straightforward: https://stackoverflow.com/a/7557433/6495043\n  var rect = el.getBoundingClientRect();\n\n  return (\n    rect.top >= 0 &&\n    rect.left >= 0 &&\n    rect.bottom <=\n      (window.innerHeight ||\n        document.documentElement.clientHeight) /*or $(window).height() */ &&\n    rect.right <=\n      (window.innerWidth ||\n        document.documentElement.clientWidth) /*or $(window).width() */\n  );\n}\n\nexport function getElementContentWidth(element) {\n  var styles = window.getComputedStyle(element);\n  var padding =\n    parseFloat(styles.paddingLeft) + parseFloat(styles.paddingRight);\n\n  return element.clientWidth - padding;\n}\n\nexport function bind(element, o) {\n  if (element) {\n    for (var event in o) {\n      var callback = o[event];\n\n      event.split(/\\s+/).forEach(function (event) {\n        element.addEventListener(event, callback);\n      });\n    }\n  }\n}\n\nexport function unbind(element, o) {\n  if (element) {\n    for (var event in o) {\n      var callback = o[event];\n\n      event.split(/\\s+/).forEach(function (event) {\n        element.removeEventListener(event, callback);\n      });\n    }\n  }\n}\n\nexport function fire(target, type, properties) {\n  var evt = document.createEvent(\"HTMLEvents\");\n\n  evt.initEvent(type, true, true);\n\n  for (var j in properties) {\n    evt[j] = properties[j];\n  }\n\n  return target.dispatchEvent(evt);\n}\n\n// https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/\nexport function forEachNode(nodeList, callback, scope) {\n  if (!nodeList) return;\n  for (var i = 0; i < nodeList.length; i++) {\n    callback.call(scope, nodeList[i], i);\n  }\n}\n\nexport function activate(\n  $parent,\n  $child,\n  commonClass,\n  activeClass = \"active\",\n  index = -1\n) {\n  let $children = $parent.querySelectorAll(`.${commonClass}.${activeClass}`);\n\n  forEachNode($children, (node, i) => {\n    if (index >= 0 && i <= index) return;\n    node.classList.remove(activeClass);\n  });\n\n  $child.classList.add(activeClass);\n}\n"
  },
  {
    "path": "src/js/utils/draw-utils.js",
    "content": "import { fillArray } from \"./helpers\";\n\nexport function getBarHeightAndYAttr(yTop, zeroLine) {\n  let height, y;\n  if (yTop <= zeroLine) {\n    height = zeroLine - yTop;\n    y = yTop;\n  } else {\n    height = yTop - zeroLine;\n    y = zeroLine;\n  }\n\n  return [height, y];\n}\n\nexport function equilizeNoOfElements(\n  array1,\n  array2,\n  extraCount = array2.length - array1.length\n) {\n  // Doesn't work if either has zero elements.\n  if (extraCount > 0) {\n    array1 = fillArray(array1, extraCount);\n  } else {\n    array2 = fillArray(array2, extraCount);\n  }\n  return [array1, array2];\n}\n\nexport function truncateString(txt, len) {\n  if (!txt) {\n    return;\n  }\n  if (txt.length > len) {\n    return txt.slice(0, len - 3) + \"...\";\n  } else {\n    return txt;\n  }\n}\n\nexport function shortenLargeNumber(label) {\n  let number;\n  if (typeof label === \"number\") number = label;\n  else if (typeof label === \"string\") {\n    number = Number(label);\n    if (Number.isNaN(number)) return label;\n  }\n\n  // Using absolute since log wont work for negative numbers\n  let p = Math.floor(Math.log10(Math.abs(number)));\n  if (p <= 2) return number; // Return as is for a 3 digit number of less\n  let l = Math.floor(p / 3);\n  let shortened =\n    Math.pow(10, p - l * 3) * +(number / Math.pow(10, p)).toFixed(1);\n\n  // Correct for floating point error upto 2 decimal places\n  return Math.round(shortened * 100) / 100 + \" \" + [\"\", \"K\", \"M\", \"B\", \"T\"][l];\n}\n\n// cubic bezier curve calculation (from example by François Romain)\nexport function getSplineCurvePointsStr(xList, yList) {\n  let points = [];\n  for (let i = 0; i < xList.length; i++) {\n    points.push([xList[i], yList[i]]);\n  }\n\n  let smoothing = 0.2;\n  let line = (pointA, pointB) => {\n    let lengthX = pointB[0] - pointA[0];\n    let lengthY = pointB[1] - pointA[1];\n    return {\n      length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),\n      angle: Math.atan2(lengthY, lengthX),\n    };\n  };\n\n  let controlPoint = (current, previous, next, reverse) => {\n    let p = previous || current;\n    let n = next || current;\n    let o = line(p, n);\n    let angle = o.angle + (reverse ? Math.PI : 0);\n    let length = o.length * smoothing;\n    let x = current[0] + Math.cos(angle) * length;\n    let y = current[1] + Math.sin(angle) * length;\n    return [x, y];\n  };\n\n  let bezierCommand = (point, i, a) => {\n    let cps = controlPoint(a[i - 1], a[i - 2], point);\n    let cpe = controlPoint(point, a[i - 1], a[i + 1], true);\n    return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`;\n  };\n\n  let pointStr = (points, command) => {\n    return points.reduce(\n      (acc, point, i, a) =>\n        i === 0 ? `${point[0]},${point[1]}` : `${acc} ${command(point, i, a)}`,\n      \"\"\n    );\n  };\n\n  return pointStr(points, bezierCommand);\n}\n"
  },
  {
    "path": "src/js/utils/draw.js",
    "content": "import {\n\tgetBarHeightAndYAttr,\n\ttruncateString,\n\tshortenLargeNumber,\n\tgetSplineCurvePointsStr,\n} from \"./draw-utils\";\nimport { getStringWidth, isValidNumber, round } from \"./helpers\";\n\nimport {\n\tDOT_OVERLAY_SIZE_INCR,\n} from \"./constants\";\n\nexport const AXIS_TICK_LENGTH = 6;\nconst LABEL_MARGIN = 4;\nconst LABEL_WIDTH = 25;\nconst TOTAL_PADDING = 120;\nconst LABEL_MAX_CHARS = 18;\nexport const FONT_SIZE = 10;\nconst BASE_LINE_COLOR = \"#E2E6E9\";\n\nfunction $(expr, con) {\n\treturn typeof expr === \"string\"\n\t\t? (con || document).querySelector(expr)\n\t\t: expr || null;\n}\n\nexport function createSVG(tag, o) {\n\tvar element = document.createElementNS(\"http://www.w3.org/2000/svg\", tag);\n\n\tfor (var i in o) {\n\t\tvar val = o[i];\n\n\t\tif (i === \"inside\") {\n\t\t\t$(val).appendChild(element);\n\t\t} else if (i === \"around\") {\n\t\t\tvar ref = $(val);\n\t\t\tref.parentNode.insertBefore(element, ref);\n\t\t\telement.appendChild(ref);\n\t\t} else if (i === \"styles\") {\n\t\t\tif (typeof val === \"object\") {\n\t\t\t\tObject.keys(val).map((prop) => {\n\t\t\t\t\telement.style[prop] = val[prop];\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\tif (i === \"className\") {\n\t\t\t\ti = \"class\";\n\t\t\t}\n\t\t\tif (i === \"innerHTML\") {\n\t\t\t\telement[\"textContent\"] = val;\n\t\t\t} else {\n\t\t\t\telement.setAttribute(i, val);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn element;\n}\n\nfunction renderVerticalGradient(svgDefElem, gradientId) {\n\treturn createSVG(\"linearGradient\", {\n\t\tinside: svgDefElem,\n\t\tid: gradientId,\n\t\tx1: 0,\n\t\tx2: 0,\n\t\ty1: 0,\n\t\ty2: 1,\n\t});\n}\n\nfunction setGradientStop(gradElem, offset, color, opacity) {\n\treturn createSVG(\"stop\", {\n\t\tinside: gradElem,\n\t\tstyle: `stop-color: ${color}`,\n\t\toffset: offset,\n\t\t\"stop-opacity\": opacity,\n\t});\n}\n\nexport function makeSVGContainer(parent, className, width, height) {\n\treturn createSVG(\"svg\", {\n\t\tclassName: className,\n\t\tinside: parent,\n\t\twidth: width,\n\t\theight: height,\n\t});\n}\n\nexport function makeSVGDefs(svgContainer) {\n\treturn createSVG(\"defs\", {\n\t\tinside: svgContainer,\n\t});\n}\n\nexport function makeSVGGroup(className, transform = \"\", parent = undefined) {\n\tlet args = {\n\t\tclassName: className,\n\t\ttransform: transform,\n\t};\n\tif (parent) args.inside = parent;\n\treturn createSVG(\"g\", args);\n}\n\nexport function wrapInSVGGroup(elements, className = \"\") {\n\tlet g = createSVG(\"g\", {\n\t\tclassName: className,\n\t});\n\telements.forEach((e) => g.appendChild(e));\n\treturn g;\n}\n\nexport function makePath(\n\tpathStr,\n\tclassName = \"\",\n\tstroke = \"none\",\n\tfill = \"none\",\n\tstrokeWidth = 2\n) {\n\treturn createSVG(\"path\", {\n\t\tclassName: className,\n\t\td: pathStr,\n\t\tstyles: {\n\t\t\tstroke: stroke,\n\t\t\tfill: fill,\n\t\t\t\"stroke-width\": strokeWidth,\n\t\t},\n\t});\n}\n\nexport function makeArcPathStr(\n\tstartPosition,\n\tendPosition,\n\tcenter,\n\tradius,\n\tclockWise = 1,\n\tlargeArc = 0\n) {\n\tlet [arcStartX, arcStartY] = [\n\t\tcenter.x + startPosition.x,\n\t\tcenter.y + startPosition.y,\n\t];\n\tlet [arcEndX, arcEndY] = [\n\t\tcenter.x + endPosition.x,\n\t\tcenter.y + endPosition.y,\n\t];\n\treturn `M${center.x} ${center.y}\n\t\tL${arcStartX} ${arcStartY}\n\t\tA ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}\n\t\t${arcEndX} ${arcEndY} z`;\n}\n\nexport function makeCircleStr(\n\tstartPosition,\n\tendPosition,\n\tcenter,\n\tradius,\n\tclockWise = 1,\n\tlargeArc = 0\n) {\n\tlet [arcStartX, arcStartY] = [\n\t\tcenter.x + startPosition.x,\n\t\tcenter.y + startPosition.y,\n\t];\n\tlet [arcEndX, midArc, arcEndY] = [\n\t\tcenter.x + endPosition.x,\n\t\tcenter.y * 2,\n\t\tcenter.y + endPosition.y,\n\t];\n\treturn `M${center.x} ${center.y}\n\t\tL${arcStartX} ${arcStartY}\n\t\tA ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}\n\t\t${arcEndX} ${midArc} z\n\t\tL${arcStartX} ${midArc}\n\t\tA ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}\n\t\t${arcEndX} ${arcEndY} z`;\n}\n\nexport function makeArcStrokePathStr(\n\tstartPosition,\n\tendPosition,\n\tcenter,\n\tradius,\n\tclockWise = 1,\n\tlargeArc = 0\n) {\n\tlet [arcStartX, arcStartY] = [\n\t\tcenter.x + startPosition.x,\n\t\tcenter.y + startPosition.y,\n\t];\n\tlet [arcEndX, arcEndY] = [\n\t\tcenter.x + endPosition.x,\n\t\tcenter.y + endPosition.y,\n\t];\n\n\treturn `M${arcStartX} ${arcStartY}\n\t\tA ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}\n\t\t${arcEndX} ${arcEndY}`;\n}\n\nexport function makeStrokeCircleStr(\n\tstartPosition,\n\tendPosition,\n\tcenter,\n\tradius,\n\tclockWise = 1,\n\tlargeArc = 0\n) {\n\tlet [arcStartX, arcStartY] = [\n\t\tcenter.x + startPosition.x,\n\t\tcenter.y + startPosition.y,\n\t];\n\tlet [arcEndX, midArc, arcEndY] = [\n\t\tcenter.x + endPosition.x,\n\t\tradius * 2 + arcStartY,\n\t\tcenter.y + startPosition.y,\n\t];\n\n\treturn `M${arcStartX} ${arcStartY}\n\t\tA ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}\n\t\t${arcEndX} ${midArc}\n\t\tM${arcStartX} ${midArc}\n\t\tA ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}\n\t\t${arcEndX} ${arcEndY}`;\n}\n\nexport function makeGradient(svgDefElem, color, lighter = false) {\n\tlet gradientId =\n\t\t\"path-fill-gradient\" +\n\t\t\"-\" +\n\t\tcolor +\n\t\t\"-\" +\n\t\t(lighter ? \"lighter\" : \"default\");\n\tlet gradientDef = renderVerticalGradient(svgDefElem, gradientId);\n\tlet opacities = [1, 0.6, 0.2];\n\tif (lighter) {\n\t\topacities = [0.4, 0.2, 0];\n\t}\n\n\tsetGradientStop(gradientDef, \"0%\", color, opacities[0]);\n\tsetGradientStop(gradientDef, \"50%\", color, opacities[1]);\n\tsetGradientStop(gradientDef, \"100%\", color, opacities[2]);\n\n\treturn gradientId;\n}\n\nexport function rightRoundedBar(x, width, height) {\n\t// https://medium.com/@dennismphil/one-side-rounded-rectangle-using-svg-fb31cf318d90\n\tlet radius = height / 2;\n\tlet xOffset = width - radius;\n\n\treturn `M${x},0 h${xOffset} q${radius},0 ${radius},${radius} q0,${radius} -${radius},${radius} h-${xOffset} v${height}z`;\n}\n\nexport function leftRoundedBar(x, width, height) {\n\tlet radius = height / 2;\n\tlet xOffset = width - radius;\n\n\treturn `M${\n\t\tx + radius\n\t},0 h${xOffset} v${height} h-${xOffset} q-${radius}, 0 -${radius},-${radius} q0,-${radius} ${radius},-${radius}z`;\n}\n\nexport function percentageBar(\n\tx,\n\ty,\n\twidth,\n\theight,\n\tisFirst,\n\tisLast,\n\tfill = \"none\"\n) {\n\tif (isLast) {\n\t\tlet pathStr = rightRoundedBar(x, width, height);\n\t\treturn makePath(pathStr, \"percentage-bar\", null, fill);\n\t}\n\n\tif (isFirst) {\n\t\tlet pathStr = leftRoundedBar(x, width, height);\n\t\treturn makePath(pathStr, \"percentage-bar\", null, fill);\n\t}\n\n\tlet args = {\n\t\tclassName: \"percentage-bar\",\n\t\tx: x,\n\t\ty: y,\n\t\twidth: width,\n\t\theight: height,\n\t\tfill: fill,\n\t};\n\n\treturn createSVG(\"rect\", args);\n}\n\nexport function heatSquare(\n\tclassName,\n\tx,\n\ty,\n\tsize,\n\tradius,\n\tfill = \"none\",\n\tdata = {}\n) {\n\tlet args = {\n\t\tclassName: className,\n\t\tx: x,\n\t\ty: y,\n\t\twidth: size,\n\t\theight: size,\n\t\trx: radius,\n\t\tfill: fill,\n\t};\n\n\tObject.keys(data).map((key) => {\n\t\targs[key] = data[key];\n\t});\n\n\treturn createSVG(\"rect\", args);\n}\n\nexport function legendDot(\n\tx,\n\ty,\n\tsize,\n\tradius,\n\tfill = \"none\",\n\tlabel,\n\tvalue,\n\tfont_size = null,\n\ttruncate = false\n) {\n\tlabel = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;\n\tif (!font_size) font_size = FONT_SIZE;\n\n\tlet args = {\n\t\tclassName: \"legend-dot\",\n\t\tx: 0,\n\t\ty: 4 - size,\n\t\theight: size,\n\t\twidth: size,\n\t\trx: radius,\n\t\tfill: fill,\n\t};\n\n\tlet textLabel = createSVG(\"text\", {\n\t\tclassName: \"legend-dataset-label\",\n\t\tx: size,\n\t\ty: 0,\n\t\tdx: font_size + \"px\",\n\t\tdy: font_size / 3 + \"px\",\n\t\t\"font-size\": font_size * 1.6 + \"px\",\n\t\t\"text-anchor\": \"start\",\n\t\tinnerHTML: label,\n\t});\n\n\tlet textValue = null;\n\tif (value) {\n\t\ttextValue = createSVG(\"text\", {\n\t\t\tclassName: \"legend-dataset-value\",\n\t\t\tx: size,\n\t\t\ty: FONT_SIZE + 10,\n\t\t\tdx: FONT_SIZE + \"px\",\n\t\t\tdy: FONT_SIZE / 3 + \"px\",\n\t\t\t\"font-size\": FONT_SIZE * 1.2 + \"px\",\n\t\t\t\"text-anchor\": \"start\",\n\t\t\tinnerHTML: value,\n\t\t});\n\t}\n\n\tlet group = createSVG(\"g\", {\n\t\ttransform: `translate(${x}, ${y})`,\n\t});\n\tgroup.appendChild(createSVG(\"rect\", args));\n\tgroup.appendChild(textLabel);\n\n\tif (value && textValue) {\n\t\tgroup.appendChild(textValue);\n\t}\n\n\treturn group;\n}\n\nexport function makeText(className, x, y, content, options = {}) {\n\tlet fontSize = options.fontSize || FONT_SIZE;\n\tlet dy = options.dy !== undefined ? options.dy : fontSize / 2;\n\t//let fill = options.fill || \"var(--charts-label-color)\";\n\tlet fill = options.fill || \"var(--charts-label-color)\";\n\tlet textAnchor = options.textAnchor || \"start\";\n\treturn createSVG(\"text\", {\n\t\tclassName: className,\n\t\tx: x,\n\t\ty: y,\n\t\tdy: dy + \"px\",\n\t\t\"font-size\": fontSize + \"px\",\n\t\tfill: fill,\n\t\t\"text-anchor\": textAnchor,\n\t\tinnerHTML: content,\n\t});\n}\n\nfunction makeVertLine(x, label, y1, y2, options = {}) {\n\tif (!options.stroke) options.stroke = BASE_LINE_COLOR;\n\tlet l = createSVG(\"line\", {\n\t\tclassName: \"line-vertical \" + options.className,\n\t\tx1: 0,\n\t\tx2: 0,\n\t\ty1: y1,\n\t\ty2: y2,\n\t\tstyles: {\n\t\t\tstroke: options.stroke,\n\t\t},\n\t});\n\n\tlet text = createSVG(\"text\", {\n\t\tx: 0,\n\t\ty: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE,\n\t\tdy: FONT_SIZE + \"px\",\n\t\t\"font-size\": FONT_SIZE + \"px\",\n\t\t\"text-anchor\": \"middle\",\n\t\tinnerHTML: label + \"\",\n\t});\n\n\tlet line = createSVG(\"g\", {\n\t\ttransform: `translate(${x}, 0)`,\n\t});\n\n\tline.appendChild(l);\n\tline.appendChild(text);\n\n\treturn line;\n}\n\nfunction makeHoriLine(y, label, x1, x2, options = {}) {\n\tif (!options.stroke) options.stroke = BASE_LINE_COLOR;\n\tif (!options.lineType) options.lineType = \"\";\n\tif (!options.alignment) options.alignment = \"left\";\n\tif (options.shortenNumbers) label = shortenLargeNumber(label);\n\n\tlet className =\n\t\t\"line-horizontal \" +\n\t\toptions.className +\n\t\t(options.lineType === \"dashed\" ? \"dashed\" : \"\");\n\n\tconst textXPos =\n\t\toptions.alignment === \"left\"\n\t\t\t? options.title\n\t\t\t\t? x1 - LABEL_MARGIN + LABEL_WIDTH\n\t\t\t\t: x1 - LABEL_MARGIN\n\t\t\t: options.title\n\t\t\t? x2 + LABEL_MARGIN * 4 - LABEL_WIDTH\n\t\t\t: x2 + LABEL_MARGIN * 4;\n\tconst lineX1Post = options.title ? x1 + LABEL_WIDTH : x1;\n\tconst lineX2Post = options.title ? x2 - LABEL_WIDTH : x2;\n\n\tlet l = createSVG(\"line\", {\n\t\tclassName: className,\n\t\tx1: lineX1Post,\n\t\tx2: lineX2Post,\n\t\ty1: 0,\n\t\ty2: 0,\n\t\tstyles: {\n\t\t\tstroke: options.stroke,\n\t\t},\n\t});\n\n\tlet text = createSVG(\"text\", {\n\t\tx: textXPos,\n\t\ty: 0,\n\t\tdy: FONT_SIZE / 2 - 2 + \"px\",\n\t\t\"font-size\": FONT_SIZE + \"px\",\n\t\t\"text-anchor\": x1 < x2 ? \"end\" : \"start\",\n\t\tinnerHTML: label + \"\",\n\t});\n\n\tlet line = createSVG(\"g\", {\n\t\ttransform: `translate(0, ${y})`,\n\t\t\"stroke-opacity\": 1,\n\t});\n\n\tif (text === 0 || text === \"0\") {\n\t\tline.style.stroke = \"rgba(27, 31, 35, 0.6)\";\n\t}\n\n\tline.appendChild(l);\n\tline.appendChild(text);\n\n\treturn line;\n}\n\nexport function generateAxisLabel(options) {\n\tif (!options.title) return;\n\n\tconst y =\n\t\toptions.position === \"left\"\n\t\t\t? (options.height - TOTAL_PADDING) / 2 +\n\t\t\t  getStringWidth(options.title, 5) / 2\n\t\t\t: (options.height - TOTAL_PADDING) / 2 -\n\t\t\t  getStringWidth(options.title, 5) / 2;\n\tconst x = options.position === \"left\" ? 0 : options.width;\n\tconst y2 =\n\t\toptions.position === \"left\"\n\t\t\t? FONT_SIZE - LABEL_WIDTH\n\t\t\t: FONT_SIZE + LABEL_WIDTH * -1;\n\n\tconst rotation =\n\t\toptions.position === \"right\" ? `rotate(90)` : `rotate(270)`;\n\n\tconst labelSvg = createSVG(\"text\", {\n\t\tclassName: \"chart-label\",\n\t\tx: 0, // getStringWidth(options.title, 5) / 2,\n\t\ty: 0, // y,\n\t\tdy: `${y2}px`,\n\t\t\"font-size\": `${FONT_SIZE}px`,\n\t\t\"text-anchor\": \"start\",\n\t\tinnerHTML: `${options.title} `,\n\t});\n\n\tlet wrapper = createSVG(\"g\", {\n\t\tx: 0,\n\t\ty: 0,\n\t\ttransformBox: \"fill-box\",\n\t\ttransform: `translate(${x}, ${y}) ${rotation}`,\n\t\tclassName: `test-${options.position}`,\n\t});\n\n\twrapper.appendChild(labelSvg);\n\n\treturn wrapper;\n}\n\nexport function yLine(y, label, width, options = {}) {\n\tif (!isValidNumber(y)) y = 0;\n\n\tif (!options.pos) options.pos = \"left\";\n\tif (!options.offset) options.offset = 0;\n\tif (!options.mode) options.mode = \"span\";\n\tif (!options.stroke) options.stroke = BASE_LINE_COLOR;\n\tif (!options.className) options.className = \"\";\n\n\tlet x1 = -1 * AXIS_TICK_LENGTH;\n\tlet x2 = options.mode === \"span\" ? width + AXIS_TICK_LENGTH : 0;\n\n\tif (options.mode === \"tick\" && options.pos === \"right\") {\n\t\tx1 = width + AXIS_TICK_LENGTH;\n\t\tx2 = width;\n\t}\n\n\tlet offset = options.pos === \"left\" ? -1 * options.offset : options.offset;\n\n\t// pr_366\n\t//x1 += offset;\n\t//x2 += offset;\n\tx1 += options.offset;\n\tx2 += options.offset;\n\n\tif (typeof label === \"number\") label = round(label);\n\n\treturn makeHoriLine(y, label, x1, x2, {\n\t\tstroke: options.stroke,\n\t\tclassName: options.className,\n\t\tlineType: options.lineType,\n\t\talignment: options.pos,\n\t\ttitle: options.title,\n\t\tshortenNumbers: options.shortenNumbers,\n\t});\n}\n\nexport function xLine(x, label, height, options = {}) {\n\tif (!isValidNumber(x)) x = 0;\n\n\tif (!options.pos) options.pos = \"bottom\";\n\tif (!options.offset) options.offset = 0;\n\tif (!options.mode) options.mode = \"span\";\n\tif (!options.stroke) options.stroke = BASE_LINE_COLOR;\n\tif (!options.className) options.className = \"\";\n\n\t// Draw X axis line in span/tick mode with optional label\n\t//                        \ty2(span)\n\t// \t\t\t\t\t\t|\n\t// \t\t\t\t\t\t|\n\t//\t\t\t\tx line\t|\n\t//\t\t\t\t\t\t|\n\t// \t\t\t\t\t   \t|\n\t// ---------------------+-- y2(tick)\n\t//\t\t\t\t\t\t|\n\t//\t\t\t\t\t\t\ty1\n\n\tlet y1 = height + AXIS_TICK_LENGTH;\n\tlet y2 = options.mode === \"span\" ? -1 * AXIS_TICK_LENGTH : height;\n\n\tif (options.mode === \"tick\" && options.pos === \"top\") {\n\t\t// top axis ticks\n\t\ty1 = -1 * AXIS_TICK_LENGTH;\n\t\ty2 = 0;\n\t}\n\n\treturn makeVertLine(x, label, y1, y2, {\n\t\tstroke: options.stroke,\n\t\tclassName: options.className,\n\t\tlineType: options.lineType,\n\t});\n}\n\nexport function yMarker(y, label, width, options = {}) {\n\tif (!isValidNumber(y)) y = 0;\n\n\tif (!options.labelPos) options.labelPos = \"right\";\n\tif (!options.lineType) options.lineType = \"dashed\";\n\tlet x =\n\t\toptions.labelPos === \"left\"\n\t\t\t? LABEL_MARGIN\n\t\t\t: width - getStringWidth(label, 5) - LABEL_MARGIN;\n\n\tlet labelSvg = createSVG(\"text\", {\n\t\tclassName: \"chart-label\",\n\t\tx: x,\n\t\ty: 0,\n\t\tdy: FONT_SIZE / -2 + \"px\",\n\t\t\"font-size\": FONT_SIZE + \"px\",\n\t\t\"text-anchor\": \"start\",\n\t\tinnerHTML: label + \"\",\n\t});\n\n\tlet line = makeHoriLine(y, \"\", 0, width, {\n\t\tstroke: options.stroke || BASE_LINE_COLOR,\n\t\tclassName: options.className || \"\",\n\t\tlineType: options.lineType,\n\t});\n\n\tline.appendChild(labelSvg);\n\n\treturn line;\n}\n\nexport function yRegion(y1, y2, width, label, options = {}) {\n\t// return a group\n\tlet height = y1 - y2;\n\n\tlet rect = createSVG(\"rect\", {\n\t\tclassName: `bar mini`, // remove class\n\t\tstyles: {\n\t\t\tfill: options.fill || `rgba(228, 234, 239, 0.49)`,\n\t\t\tstroke: options.stroke || BASE_LINE_COLOR,\n\t\t\t\"stroke-dasharray\": `${width}, ${height}`,\n\t\t},\n\t\t// 'data-point-index': index,\n\t\tx: 0,\n\t\ty: 0,\n\t\twidth: width,\n\t\theight: height,\n\t});\n\n\tif (!options.labelPos) options.labelPos = \"right\";\n\tlet x =\n\t\toptions.labelPos === \"left\"\n\t\t\t? LABEL_MARGIN\n\t\t\t: width - getStringWidth(label + \"\", 4.5) - LABEL_MARGIN;\n\n\tlet labelSvg = createSVG(\"text\", {\n\t\tclassName: \"chart-label\",\n\t\tx: x,\n\t\ty: 0,\n\t\tdy: FONT_SIZE / -2 + \"px\",\n\t\t\"font-size\": FONT_SIZE + \"px\",\n\t\t\"text-anchor\": \"start\",\n\t\tinnerHTML: label + \"\",\n\t});\n\n\tlet region = createSVG(\"g\", {\n\t\ttransform: `translate(0, ${y2})`,\n\t});\n\n\tregion.appendChild(rect);\n\tregion.appendChild(labelSvg);\n\n\treturn region;\n}\n\nexport function datasetBar(\n\tx,\n\tyTop,\n\twidth,\n\tcolor,\n\tlabel = \"\",\n\tindex = 0,\n\toffset = 0,\n\tmeta = {}\n) {\n\tlet [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);\n\ty -= offset;\n\n\tif (height === 0) {\n\t\theight = meta.minHeight;\n\t\ty -= meta.minHeight;\n\t}\n\n\t// Preprocess numbers to avoid svg building errors\n\tif (!isValidNumber(x)) x = 0;\n\tif (!isValidNumber(y)) y = 0;\n\tif (!isValidNumber(height, true)) height = 0;\n\tif (!isValidNumber(width, true)) width = 0;\n\n\t// x y h w\n\n\t// M{x},{y+r}\n\t// q0,-{r} {r},-{r}\n\t// q{r},0 {r},{r}\n\t// v{h-r}\n\t// h-{w}z\n\n\t// let radius = width/2;\n\t// let pathStr = `M${x},${y+radius} q0,-${radius} ${radius},-${radius} q${radius},0 ${radius},${radius} v${height-radius} h-${width}z`\n\n\t// let rect = createSVG('path', {\n\t// \tclassName: 'bar mini',\n\t// \td: pathStr,\n\t// \tstyles: { fill: color },\n\t// \tx: x,\n\t// \ty: y,\n\t// \t'data-point-index': index,\n\t// });\n\n\tlet rect = createSVG(\"rect\", {\n\t\tclassName: `bar mini`,\n\t\tstyle: `fill: ${color}`,\n\t\t\"data-point-index\": index,\n\t\tx: x,\n\t\ty: y,\n\t\twidth: width,\n\t\theight: height,\n\t});\n\n\tlabel += \"\";\n\n\tif (!label && !label.length) {\n\t\treturn rect;\n\t} else {\n\t\trect.setAttribute(\"y\", 0);\n\t\trect.setAttribute(\"x\", 0);\n\t\tlet text = createSVG(\"text\", {\n\t\t\tclassName: \"data-point-value\",\n\t\t\tx: width / 2,\n\t\t\ty: 0,\n\t\t\tdy: (FONT_SIZE / 2) * -1 + \"px\",\n\t\t\t\"font-size\": FONT_SIZE + \"px\",\n\t\t\t\"text-anchor\": \"middle\",\n\t\t\tinnerHTML: label,\n\t\t});\n\n\t\tlet group = createSVG(\"g\", {\n\t\t\t\"data-point-index\": index,\n\t\t\ttransform: `translate(${x}, ${y})`,\n\t\t});\n\t\tgroup.appendChild(rect);\n\t\tgroup.appendChild(text);\n\n\t\treturn group;\n\t}\n}\n\nexport function datasetDot(x, y, radius, color, label = \"\", index = 0) {\n\tlet dot = createSVG(\"circle\", {\n\t\tstyle: `fill: ${color}`,\n\t\t\"data-point-index\": index,\n\t\tcx: x,\n\t\tcy: y,\n\t\tr: radius,\n\t});\n\n\tlabel += \"\";\n\n\tif (!label && !label.length) {\n\t\treturn dot;\n\t} else {\n\t\tdot.setAttribute(\"cy\", 0);\n\t\tdot.setAttribute(\"cx\", 0);\n\n\t\tlet text = createSVG(\"text\", {\n\t\t\tclassName: \"data-point-value\",\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\tdy: (FONT_SIZE / 2) * -1 - radius + \"px\",\n\t\t\t\"font-size\": FONT_SIZE + \"px\",\n\t\t\t\"text-anchor\": \"middle\",\n\t\t\tinnerHTML: label,\n\t\t});\n\n\t\tlet group = createSVG(\"g\", {\n\t\t\t\"data-point-index\": index,\n\t\t\ttransform: `translate(${x}, ${y})`,\n\t\t});\n\t\tgroup.appendChild(dot);\n\t\tgroup.appendChild(text);\n\n\t\treturn group;\n\t}\n}\n\nexport function getPaths(xList, yList, color, options = {}, meta = {}) {\n\tlet pointsList = yList.map((y, i) => xList[i] + \",\" + y);\n\tlet pointsStr = pointsList.join(\"L\");\n\n\t// Spline\n\tif (options.spline) pointsStr = getSplineCurvePointsStr(xList, yList);\n\n\tlet path = makePath(\"M\" + pointsStr, \"line-graph-path\", color);\n\n\t// HeatLine\n\tif (options.heatline) {\n\t\tlet gradient_id = makeGradient(meta.svgDefs, color);\n\t\tpath.style.stroke = `url(#${gradient_id})`;\n\t}\n\n\tlet paths = {\n\t\tpath: path,\n\t};\n\n\t// Region\n\tif (options.regionFill) {\n\t\tlet gradient_id_region = makeGradient(meta.svgDefs, color, true);\n\n\t\tlet pathStr =\n\t\t\t\"M\" +\n\t\t\t`${xList[0]},${meta.zeroLine}L` +\n\t\t\tpointsStr +\n\t\t\t`L${xList.slice(-1)[0]},${meta.zeroLine}`;\n\t\tpaths.region = makePath(\n\t\t\tpathStr,\n\t\t\t`region-fill`,\n\t\t\t\"none\",\n\t\t\t`url(#${gradient_id_region})`\n\t\t);\n\t}\n\n\treturn paths;\n}\n\nexport let makeOverlay = {\n\tbar: (unit) => {\n\t\tlet transformValue;\n\t\tif (unit.nodeName !== \"rect\") {\n\t\t\ttransformValue = unit.getAttribute(\"transform\");\n\t\t\tunit = unit.childNodes[0];\n\t\t}\n\t\tlet overlay = unit.cloneNode();\n\t\toverlay.style.fill = \"#000000\";\n\t\toverlay.style.opacity = \"0.4\";\n\n\t\tif (transformValue) {\n\t\t\toverlay.setAttribute(\"transform\", transformValue);\n\t\t}\n\t\treturn overlay;\n\t},\n\n\tdot: (unit) => {\n\t\tlet transformValue;\n\t\tif (unit.nodeName !== \"circle\") {\n\t\t\ttransformValue = unit.getAttribute(\"transform\");\n\t\t\tunit = unit.childNodes[0];\n\t\t}\n\t\tlet overlay = unit.cloneNode();\n\t\tlet radius = unit.getAttribute(\"r\");\n\t\tlet fill = unit.getAttribute(\"fill\");\n\t\toverlay.setAttribute(\"r\", parseInt(radius) + DOT_OVERLAY_SIZE_INCR);\n\t\toverlay.setAttribute(\"fill\", fill);\n\t\toverlay.style.opacity = \"0.6\";\n\n\t\tif (transformValue) {\n\t\t\toverlay.setAttribute(\"transform\", transformValue);\n\t\t}\n\t\treturn overlay;\n\t},\n\n\theat_square: (unit) => {\n\t\tlet transformValue;\n\t\tif (unit.nodeName !== \"circle\") {\n\t\t\ttransformValue = unit.getAttribute(\"transform\");\n\t\t\tunit = unit.childNodes[0];\n\t\t}\n\t\tlet overlay = unit.cloneNode();\n\t\tlet radius = unit.getAttribute(\"r\");\n\t\tlet fill = unit.getAttribute(\"fill\");\n\t\toverlay.setAttribute(\"r\", parseInt(radius) + DOT_OVERLAY_SIZE_INCR);\n\t\toverlay.setAttribute(\"fill\", fill);\n\t\toverlay.style.opacity = \"0.6\";\n\n\t\tif (transformValue) {\n\t\t\toverlay.setAttribute(\"transform\", transformValue);\n\t\t}\n\t\treturn overlay;\n\t},\n};\n\nexport let updateOverlay = {\n\tbar: (unit, overlay) => {\n\t\tlet transformValue;\n\t\tif (unit.nodeName !== \"rect\") {\n\t\t\ttransformValue = unit.getAttribute(\"transform\");\n\t\t\tunit = unit.childNodes[0];\n\t\t}\n\t\tlet attributes = [\"x\", \"y\", \"width\", \"height\"];\n\t\tObject.values(unit.attributes)\n\t\t\t.filter((attr) => attributes.includes(attr.name) && attr.specified)\n\t\t\t.map((attr) => {\n\t\t\t\toverlay.setAttribute(attr.name, attr.nodeValue);\n\t\t\t});\n\n\t\tif (transformValue) {\n\t\t\toverlay.setAttribute(\"transform\", transformValue);\n\t\t}\n\t},\n\n\tdot: (unit, overlay) => {\n\t\tlet transformValue;\n\t\tif (unit.nodeName !== \"circle\") {\n\t\t\ttransformValue = unit.getAttribute(\"transform\");\n\t\t\tunit = unit.childNodes[0];\n\t\t}\n\t\tlet attributes = [\"cx\", \"cy\"];\n\t\tObject.values(unit.attributes)\n\t\t\t.filter((attr) => attributes.includes(attr.name) && attr.specified)\n\t\t\t.map((attr) => {\n\t\t\t\toverlay.setAttribute(attr.name, attr.nodeValue);\n\t\t\t});\n\n\t\tif (transformValue) {\n\t\t\toverlay.setAttribute(\"transform\", transformValue);\n\t\t}\n\t},\n\n\theat_square: (unit, overlay) => {\n\t\tlet transformValue;\n\t\tif (unit.nodeName !== \"circle\") {\n\t\t\ttransformValue = unit.getAttribute(\"transform\");\n\t\t\tunit = unit.childNodes[0];\n\t\t}\n\t\tlet attributes = [\"cx\", \"cy\"];\n\t\tObject.values(unit.attributes)\n\t\t\t.filter((attr) => attributes.includes(attr.name) && attr.specified)\n\t\t\t.map((attr) => {\n\t\t\t\toverlay.setAttribute(attr.name, attr.nodeValue);\n\t\t\t});\n\n\t\tif (transformValue) {\n\t\t\toverlay.setAttribute(\"transform\", transformValue);\n\t\t}\n\t},\n};\n"
  },
  {
    "path": "src/js/utils/export.js",
    "content": "import { $ } from \"../utils/dom\";\nimport { CSSTEXT } from \"../../css/chartsCss\";\n\nexport function downloadFile(filename, data) {\n  var a = document.createElement(\"a\");\n  a.style = \"display: none\";\n  var blob = new Blob(data, { type: \"image/svg+xml; charset=utf-8\" });\n  var url = window.URL.createObjectURL(blob);\n  a.href = url;\n  a.download = filename;\n  document.body.appendChild(a);\n  a.click();\n  setTimeout(function () {\n    document.body.removeChild(a);\n    window.URL.revokeObjectURL(url);\n  }, 300);\n}\n\nexport function prepareForExport(svg) {\n  let clone = svg.cloneNode(true);\n  clone.classList.add(\"chart-container\");\n  clone.setAttribute(\"xmlns\", \"http://www.w3.org/2000/svg\");\n  clone.setAttribute(\"xmlns:xlink\", \"http://www.w3.org/1999/xlink\");\n  let styleEl = $.create(\"style\", {\n    innerHTML: CSSTEXT,\n  });\n  clone.insertBefore(styleEl, clone.firstChild);\n\n  let container = $.create(\"div\");\n  container.appendChild(clone);\n\n  return container.innerHTML;\n}\n"
  },
  {
    "path": "src/js/utils/helpers.js",
    "content": "import { ANGLE_RATIO } from \"./constants\";\n\n/**\n * Returns the value of a number upto 2 decimal places.\n * @param {Number} d Any number\n */\nexport function floatTwo(d) {\n\treturn parseFloat(d.toFixed(2));\n}\n\n/**\n * Returns whether or not two given arrays are equal.\n * @param {Array} arr1 First array\n * @param {Array} arr2 Second array\n */\nexport function arraysEqual(arr1, arr2) {\n\tif (arr1.length !== arr2.length) return false;\n\tlet areEqual = true;\n\tarr1.map((d, i) => {\n\t\tif (arr2[i] !== d) areEqual = false;\n\t});\n\treturn areEqual;\n}\n\n/**\n * Shuffles array in place. ES6 version\n * @param {Array} array An array containing the items.\n */\nexport function shuffle(array) {\n\t// Awesomeness: https://bost.ocks.org/mike/shuffle/\n\t// https://stackoverflow.com/a/2450976/6495043\n\t// https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array?noredirect=1&lq=1\n\n\tfor (let i = array.length - 1; i > 0; i--) {\n\t\tlet j = Math.floor(Math.random() * (i + 1));\n\t\t[array[i], array[j]] = [array[j], array[i]];\n\t}\n\n\treturn array;\n}\n\n/**\n * Fill an array with extra points\n * @param {Array} array Array\n * @param {Number} count number of filler elements\n * @param {Object} element element to fill with\n * @param {Boolean} start fill at start?\n */\nexport function fillArray(array, count, element, start = false) {\n\tif (element == undefined) {\n\t\telement = start ? array[0] : array[array.length - 1];\n\t}\n\tlet fillerArray = new Array(Math.abs(count)).fill(element);\n\tarray = start ? fillerArray.concat(array) : array.concat(fillerArray);\n\treturn array;\n}\n\n/**\n * Returns pixel width of string.\n * @param {String} string\n * @param {Number} charWidth Width of single char in pixels\n */\nexport function getStringWidth(string, charWidth) {\n\treturn (string + \"\").length * charWidth;\n}\n\nexport function bindChange(obj, getFn, setFn) {\n\treturn new Proxy(obj, {\n\t\tset: function (target, prop, value) {\n\t\t\tsetFn();\n\t\t\treturn Reflect.set(target, prop, value);\n\t\t},\n\t\tget: function (target, prop) {\n\t\t\tgetFn();\n\t\t\treturn Reflect.get(target, prop);\n\t\t},\n\t});\n}\n\n// https://stackoverflow.com/a/29325222\nexport function getRandomBias(min, max, bias, influence) {\n\tconst range = max - min;\n\tconst biasValue = range * bias + min;\n\tvar rnd = Math.random() * range + min, // random in range\n\t\tmix = Math.random() * influence; // random mixer\n\treturn rnd * (1 - mix) + biasValue * mix; // mix full range and bias\n}\n\nexport function getPositionByAngle(angle, radius) {\n\treturn {\n\t\tx: Math.sin(angle * ANGLE_RATIO) * radius,\n\t\ty: Math.cos(angle * ANGLE_RATIO) * radius,\n\t};\n}\n\n/**\n * Check if a number is valid for svg attributes\n * @param {object} candidate Candidate to test\n * @param {Boolean} nonNegative flag to treat negative number as invalid\n */\nexport function isValidNumber(candidate, nonNegative = false) {\n\tif (Number.isNaN(candidate)) return false;\n\telse if (candidate === undefined) return false;\n\telse if (!Number.isFinite(candidate)) return false;\n\telse if (nonNegative && candidate < 0) return false;\n\telse return true;\n}\n\n/**\n * Round a number to the closes precision, max max precision 4\n * @param {Number} d Any Number\n */\nexport function round(d) {\n\t// https://floating-point-gui.de/\n\t// https://www.jacklmoore.com/notes/rounding-in-javascript/\n\treturn Number(Math.round(d + \"e4\") + \"e-4\");\n}\n\n/**\n * Creates a deep clone of an object\n * @param {Object} candidate Any Object\n */\nexport function deepClone(candidate) {\n\tlet cloned, value, key;\n\n\tif (candidate instanceof Date) {\n\t\treturn new Date(candidate.getTime());\n\t}\n\n\tif (typeof candidate !== \"object\" || candidate === null) {\n\t\treturn candidate;\n\t}\n\n\tcloned = Array.isArray(candidate) ? [] : {};\n\n\tfor (key in candidate) {\n\t\tvalue = candidate[key];\n\n\t\tcloned[key] = deepClone(value);\n\t}\n\n\treturn cloned;\n}\n"
  },
  {
    "path": "src/js/utils/intervals.js",
    "content": "import { floatTwo } from \"./helpers\";\n\nfunction normalize(x) {\n  // Calculates mantissa and exponent of a number\n  // Returns normalized number and exponent\n  // https://stackoverflow.com/q/9383593/6495043\n\n  if (x === 0) {\n    return [0, 0];\n  }\n  if (isNaN(x)) {\n    return { mantissa: -6755399441055744, exponent: 972 };\n  }\n  var sig = x > 0 ? 1 : -1;\n  if (!isFinite(x)) {\n    return { mantissa: sig * 4503599627370496, exponent: 972 };\n  }\n\n  x = Math.abs(x);\n  var exp = Math.floor(Math.log10(x));\n  var man = x / Math.pow(10, exp);\n\n  return [sig * man, exp];\n}\n\nfunction getChartRangeIntervals(max, min = 0) {\n  let upperBound = Math.ceil(max);\n  let lowerBound = Math.floor(min);\n  let range = upperBound - lowerBound;\n\n  let noOfParts = range;\n  let partSize = 1;\n\n  // To avoid too many partitions\n  if (range > 5) {\n    if (range % 2 !== 0) {\n      upperBound++;\n      // Recalc range\n      range = upperBound - lowerBound;\n    }\n    noOfParts = range / 2;\n    partSize = 2;\n  }\n\n  // Special case: 1 and 2\n  if (range <= 2) {\n    noOfParts = 4;\n    partSize = range / noOfParts;\n  }\n\n  // Special case: 0\n  if (range === 0) {\n    noOfParts = 5;\n    partSize = 1;\n  }\n\n  let intervals = [];\n  for (var i = 0; i <= noOfParts; i++) {\n    intervals.push(lowerBound + partSize * i);\n  }\n  return intervals;\n}\n\nfunction getChartIntervals(maxValue, minValue = 0) {\n  let [normalMaxValue, exponent] = normalize(maxValue);\n  let normalMinValue = minValue ? minValue / Math.pow(10, exponent) : 0;\n\n  // Allow only 7 significant digits\n  normalMaxValue = normalMaxValue.toFixed(6);\n\n  let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue);\n  intervals = intervals.map((value) => {\n    // For negative exponents we want to divide by 10^-exponent to avoid\n    // floating point arithmetic bugs. For instance, in javascript\n    // 6 * 10^-1 == 0.6000000000000001, we instead want 6 / 10^1 == 0.6\n    if (exponent < 0) {\n      return value / Math.pow(10, -exponent);\n    }\n    return value * Math.pow(10, exponent);\n  });\n  return intervals;\n}\n\nexport function calcChartIntervals(values, withMinimum = true, overrideCeiling=false, overrideFloor=false) {\n  //*** Where the magic happens ***\n\n  // Calculates best-fit y intervals from given values\n  // and returns the interval array\n\n  let maxValue = Math.max(...values);\n  let minValue = Math.min(...values);\n\n  if (overrideCeiling) {\n    maxValue = overrideCeiling\n  } \n\n  if (overrideFloor) {\n    minValue = overrideFloor\n  }\n\n  // Exponent to be used for pretty print\n  let exponent = 0,\n    intervals = []; // eslint-disable-line no-unused-vars\n\n  function getPositiveFirstIntervals(maxValue, absMinValue) {\n    let intervals = getChartIntervals(maxValue);\n\n    let intervalSize = intervals[1] - intervals[0];\n\n    // Then unshift the negative values\n    let value = 0;\n    for (var i = 1; value < absMinValue; i++) {\n      value += intervalSize;\n      intervals.unshift(-1 * value);\n    }\n    return intervals;\n  }\n\n  // CASE I: Both non-negative\n\n  if (maxValue >= 0 && minValue >= 0) {\n    exponent = normalize(maxValue)[1];\n    if (!withMinimum) {\n      intervals = getChartIntervals(maxValue);\n    } else {\n      intervals = getChartIntervals(maxValue, minValue);\n    }\n  }\n\n  // CASE II: Only minValue negative\n  else if (maxValue > 0 && minValue < 0) {\n    // `withMinimum` irrelevant in this case,\n    // We'll be handling both sides of zero separately\n    // (both starting from zero)\n    // Because ceil() and floor() behave differently\n    // in those two regions\n\n    let absMinValue = Math.abs(minValue);\n\n    if (maxValue >= absMinValue) {\n      exponent = normalize(maxValue)[1];\n      intervals = getPositiveFirstIntervals(maxValue, absMinValue);\n    } else {\n      // Mirror: maxValue => absMinValue, then change sign\n      exponent = normalize(absMinValue)[1];\n      let posIntervals = getPositiveFirstIntervals(absMinValue, maxValue);\n      intervals = posIntervals.reverse().map((d) => d * -1);\n    }\n  }\n\n  // CASE III: Both non-positive\n  else if (maxValue <= 0 && minValue <= 0) {\n    // Mirrored Case I:\n    // Work with positives, then reverse the sign and array\n\n    let pseudoMaxValue = Math.abs(minValue);\n    let pseudoMinValue = Math.abs(maxValue);\n\n    exponent = normalize(pseudoMaxValue)[1];\n    if (!withMinimum) {\n      intervals = getChartIntervals(pseudoMaxValue);\n    } else {\n      intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue);\n    }\n\n    intervals = intervals.reverse().map((d) => d * -1);\n  }\n\n  return intervals.sort((a, b) => a - b);\n}\n\nexport function getZeroIndex(yPts) {\n  let zeroIndex;\n  let interval = getIntervalSize(yPts);\n  if (yPts.indexOf(0) >= 0) {\n    // the range has a given zero\n    // zero-line on the chart\n    zeroIndex = yPts.indexOf(0);\n  } else if (yPts[0] > 0) {\n    // Minimum value is positive\n    // zero-line is off the chart: below\n    let min = yPts[0];\n    zeroIndex = (-1 * min) / interval;\n  } else {\n    // Maximum value is negative\n    // zero-line is off the chart: above\n    let max = yPts[yPts.length - 1];\n    zeroIndex = (-1 * max) / interval + (yPts.length - 1);\n  }\n  return zeroIndex;\n}\n\nexport function getRealIntervals(max, noOfIntervals, min = 0, asc = 1) {\n  let range = max - min;\n  let part = (range * 1.0) / noOfIntervals;\n  let intervals = [];\n\n  for (var i = 0; i <= noOfIntervals; i++) {\n    intervals.push(min + part * i);\n  }\n\n  return asc ? intervals : intervals.reverse();\n}\n\nexport function getIntervalSize(orderedArray) {\n  return orderedArray[1] - orderedArray[0];\n}\n\nexport function getValueRange(orderedArray) {\n  return orderedArray[orderedArray.length - 1] - orderedArray[0];\n}\n\nexport function scale(val, yAxis) {\n  return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier);\n}\n\nexport function isInRange(val, min, max) {\n  return val > min && val < max;\n}\n\nexport function isInRange2D(coord, minCoord, maxCoord) {\n  return (\n    isInRange(coord[0], minCoord[0], maxCoord[0]) &&\n    isInRange(coord[1], minCoord[1], maxCoord[1])\n  );\n}\n\nexport function getClosestInArray(goal, arr, index = false) {\n  let closest = arr.reduce(function (prev, curr) {\n    return Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev;\n  }, []);\n\n  return index ? arr.indexOf(closest) : closest;\n}\n\nexport function calcDistribution(values, distributionSize) {\n  // Assume non-negative values,\n  // implying distribution minimum at zero\n\n  let dataMaxValue = Math.max(...values);\n\n  let distributionStep = 1 / (distributionSize - 1);\n  let distribution = [];\n\n  for (var i = 0; i < distributionSize; i++) {\n    let checkpoint = dataMaxValue * (distributionStep * i);\n    distribution.push(checkpoint);\n  }\n\n  return distribution;\n}\n\nexport function getMaxCheckpoint(value, distribution) {\n  return distribution.filter((d) => d < value).length;\n}\n"
  },
  {
    "path": "src/js/utils/test/colors.test.js",
    "content": "const assert = require(\"assert\");\nconst colors = require(\"../colors\");\n\ndescribe(\"utils.colors\", () => {\n  it(\"should return #aaabac for RGB()\", () => {\n    assert.equal(colors.getColor(\"rgb(170, 171, 172)\"), \"#aaabac\");\n  });\n  it(\"should return #ff5858 for the named color red\", () => {\n    assert.equal(colors.getColor(\"red\"), \"#ff5858d\");\n  });\n  it(\"should return #1a5c29 for the hex color #1a5c29\", () => {\n    assert.equal(colors.getColor(\"#1a5c29\"), \"#1a5c29\");\n  });\n});\n"
  },
  {
    "path": "src/js/utils/test/helpers.test.js",
    "content": "const assert = require(\"assert\");\nconst helpers = require(\"../helpers\");\n\ndescribe(\"utils.helpers\", () => {\n  it(\"should return a value fixed upto 2 decimals\", () => {\n    assert.equal(helpers.floatTwo(1.234), 1.23);\n    assert.equal(helpers.floatTwo(1.456), 1.46);\n    assert.equal(helpers.floatTwo(1), 1.0);\n  });\n});\n"
  }
]