From 0816e2b4c14b7ff5514fed5f553f03766f81e719 Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Tue, 7 Sep 2021 09:14:59 +1200 Subject: [PATCH] Update live for next release (#513) --- .gitattributes | 8 + .gitignore | 3 + .phpcs.xml.dist | 9 +- .travis.yml | 2 - CHANGELOG.md | 7 + LEGALNOTICE | 7 +- assets/chart.js | 52 + assets/js/blocks/matomo_opt_out.js | 19 + classes/WpMatomo.php | 95 +- classes/WpMatomo/API.php | 62 +- classes/WpMatomo/Access.php | 9 +- classes/WpMatomo/Admin/AccessSettings.php | 24 +- classes/WpMatomo/Admin/Admin.php | 7 +- classes/WpMatomo/Admin/AdminSettings.php | 70 +- classes/WpMatomo/Admin/AdvancedSettings.php | 77 +- classes/WpMatomo/Admin/Chart.php | 20 + classes/WpMatomo/Admin/CookieConsent.php | 20 +- classes/WpMatomo/Admin/Dashboard.php | 134 +- classes/WpMatomo/Admin/ExclusionSettings.php | 50 +- .../WpMatomo/Admin/GeolocationSettings.php | 34 +- classes/WpMatomo/Admin/GetStarted.php | 26 +- classes/WpMatomo/Admin/Info.php | 44 +- classes/WpMatomo/Admin/Marketplace.php | 1 - classes/WpMatomo/Admin/Menu.php | 126 +- classes/WpMatomo/Admin/PrivacySettings.php | 16 +- classes/WpMatomo/Admin/SafeModeMenu.php | 12 +- classes/WpMatomo/Admin/Summary.php | 81 +- classes/WpMatomo/Admin/SystemReport.php | 1127 +- classes/WpMatomo/Admin/TrackingSettings.php | 146 +- .../WpMatomo/Admin/TrackingSettings/Forms.php | 28 +- classes/WpMatomo/Admin/views/access.php | 58 +- .../Admin/views/advanced_settings.php | 91 +- .../Admin/views/exclusion_settings.php | 226 +- .../Admin/views/geolocation_settings.php | 66 +- classes/WpMatomo/Admin/views/get_started.php | 31 +- classes/WpMatomo/Admin/views/info.php | 47 +- .../WpMatomo/Admin/views/info_bug_report.php | 28 +- classes/WpMatomo/Admin/views/info_help.php | 16 +- .../Admin/views/info_high_traffic.php | 8 +- .../WpMatomo/Admin/views/info_multisite.php | 41 +- .../WpMatomo/Admin/views/info_newsletter.php | 30 +- classes/WpMatomo/Admin/views/info_shared.php | 4 +- classes/WpMatomo/Admin/views/marketplace.php | 383 +- classes/WpMatomo/Admin/views/privacy_gdpr.php | 92 +- classes/WpMatomo/Admin/views/settings.php | 37 +- .../WpMatomo/Admin/views/settings_errors.php | 10 +- classes/WpMatomo/Admin/views/summary.php | 141 +- classes/WpMatomo/Admin/views/systemreport.php | 157 +- classes/WpMatomo/Admin/views/tracking.php | 104 +- classes/WpMatomo/Annotations.php | 14 +- classes/WpMatomo/Bootstrap.php | 14 +- classes/WpMatomo/Capabilities.php | 18 +- classes/WpMatomo/Commands/MatomoCommands.php | 21 +- classes/WpMatomo/Compatibility.php | 17 +- classes/WpMatomo/Db/Settings.php | 45 +- classes/WpMatomo/Db/WordPress.php | 12 +- classes/WpMatomo/Db/WordPressDbStatement.php | 17 +- classes/WpMatomo/Db/WordPressTracker.php | 8 +- classes/WpMatomo/Ecommerce/Base.php | 36 +- .../Ecommerce/EasyDigitalDownloads.php | 46 +- .../Ecommerce/MatomoTestEcommerce.php | 37 + classes/WpMatomo/Ecommerce/MemberPress.php | 47 +- classes/WpMatomo/Ecommerce/Woocommerce.php | 103 +- classes/WpMatomo/Email.php | 191 +- classes/WpMatomo/Installer.php | 133 +- classes/WpMatomo/Logger.php | 29 +- classes/WpMatomo/OptOut.php | 70 +- classes/WpMatomo/Paths.php | 16 +- classes/WpMatomo/PrivacyBadge.php | 8 +- classes/WpMatomo/RedirectOnActivation.php | 14 +- classes/WpMatomo/Referral.php | 19 +- classes/WpMatomo/Report/Data.php | 7 +- classes/WpMatomo/Report/Dates.php | 8 +- classes/WpMatomo/Report/Metadata.php | 65 +- classes/WpMatomo/Report/Renderer.php | 59 +- classes/WpMatomo/Report/views/table.php | 2 +- .../Report/views/table_map_no_dimension.php | 16 +- .../Report/views/table_no_dimension.php | 2 +- classes/WpMatomo/Roles.php | 26 +- classes/WpMatomo/ScheduledTasks.php | 164 +- classes/WpMatomo/Settings.php | 78 +- classes/WpMatomo/Site.php | 1 - classes/WpMatomo/Site/Sync.php | 173 +- classes/WpMatomo/Site/Sync/SyncConfig.php | 215 +- classes/WpMatomo/TrackingCode.php | 38 +- .../TrackingCode/TrackingCodeGenerator.php | 65 +- classes/WpMatomo/Uninstaller.php | 13 +- classes/WpMatomo/Updater.php | 77 +- .../Updater/UpdateInProgressException.php | 10 +- classes/WpMatomo/User.php | 3 - classes/WpMatomo/User/Sync.php | 177 +- classes/WpMatomo/views/referral.php | 15 +- composer.json | 5 +- config/config.php | 1 + matomo.php | 108 +- node_modules/chart.js/LICENSE.md | 9 + node_modules/chart.js/README.md | 36 + node_modules/chart.js/auto/auto.esm.d.ts | 4 + node_modules/chart.js/auto/auto.esm.js | 5 + node_modules/chart.js/auto/auto.js | 1 + node_modules/chart.js/auto/package.json | 8 + node_modules/chart.js/dist/chart.esm.js | 10452 +++++++++++++ node_modules/chart.js/dist/chart.js | 13050 ++++++++++++++++ node_modules/chart.js/dist/chart.min.js | 13 + .../chart.js/dist/chunks/helpers.segment.js | 2464 +++ node_modules/chart.js/dist/helpers.esm.js | 7 + .../chart.js/helpers/helpers.esm.d.ts | 1 + node_modules/chart.js/helpers/helpers.esm.js | 1 + node_modules/chart.js/helpers/helpers.js | 1 + node_modules/chart.js/helpers/package.json | 8 + node_modules/chart.js/package.json | 134 + node_modules/chart.js/types/adapters.d.ts | 63 + node_modules/chart.js/types/animation.d.ts | 32 + node_modules/chart.js/types/basic.d.ts | 3 + node_modules/chart.js/types/color.d.ts | 1 + node_modules/chart.js/types/element.d.ts | 30 + node_modules/chart.js/types/geometric.d.ts | 13 + .../types/helpers/helpers.canvas.d.ts | 99 + .../types/helpers/helpers.collection.d.ts | 20 + .../chart.js/types/helpers/helpers.color.d.ts | 33 + .../chart.js/types/helpers/helpers.core.d.ts | 140 + .../chart.js/types/helpers/helpers.curve.d.ts | 34 + .../chart.js/types/helpers/helpers.dom.d.ts | 17 + .../types/helpers/helpers.easing.d.ts | 5 + .../types/helpers/helpers.extras.d.ts | 23 + .../types/helpers/helpers.interpolation.d.ts | 1 + .../chart.js/types/helpers/helpers.intl.d.ts | 7 + .../chart.js/types/helpers/helpers.math.d.ts | 16 + .../types/helpers/helpers.options.d.ts | 50 + .../chart.js/types/helpers/helpers.rtl.d.ts | 12 + .../types/helpers/helpers.segment.d.ts | 1 + .../chart.js/types/helpers/index.d.ts | 15 + node_modules/chart.js/types/index.esm.d.ts | 3421 ++++ node_modules/chart.js/types/layout.d.ts | 65 + node_modules/chart.js/types/utils.d.ts | 18 + package-lock.json | 11 + plugins/WordPress/Menu.php | 1 - plugins/WordPress/WordPress.php | 9 +- plugins/WordPress/stylesheets/user.css | 3 + readme.txt | 2 +- scripts/deploy_marketplace_only.sh | 8 +- tests/phpunit/bootstrap.php | 26 +- tests/phpunit/framework/test-case.php | 2 +- .../phpunit/framework/test-local-tracker.php | 13 +- .../framework/test-matomo-test-case.php | 33 +- .../phpunit/wpmatomo/admin/test-dashboard.php | 194 +- tests/phpunit/wpmatomo/admin/test-install.php | 91 +- tests/phpunit/wpmatomo/admin/test-summary.php | 94 +- .../wpmatomo/admin/test-systemreport.php | 23 +- tests/phpunit/wpmatomo/db/test-wordpress.php | 66 +- .../wpmatomo/db/test-wordpresstracker.php | 45 +- .../phpunit/wpmatomo/ecommerce/test-base.php | 51 + .../phpunit/wpmatomo/report/test-renderer.php | 18 +- .../wpmatomo/site/sync/test-syncconfig.php | 92 +- tests/phpunit/wpmatomo/test-api.php | 4 +- tests/phpunit/wpmatomo/test-capabilities.php | 7 + tests/phpunit/wpmatomo/test-install.php | 10 +- tests/phpunit/wpmatomo/test-matomo.php | 1 - tests/phpunit/wpmatomo/test-optout.php | 18 +- tests/phpunit/wpmatomo/test-referral.php | 6 +- tests/phpunit/wpmatomo/test-release.php | 2 +- .../phpunit/wpmatomo/test-scheduled-tasks.php | 23 +- tests/phpunit/wpmatomo/test-settings.php | 2 +- tests/phpunit/wpmatomo/test-trackingcode.php | 15 +- tests/phpunit/wpmatomo/test-updater.php | 37 +- .../test-trackingcodegenerator.php | 20 +- tests/phpunit/wpmatomo/user/test-sync.php | 22 +- 167 files changed, 34249 insertions(+), 3142 deletions(-) create mode 100644 assets/chart.js create mode 100644 assets/js/blocks/matomo_opt_out.js create mode 100644 classes/WpMatomo/Admin/Chart.php create mode 100644 classes/WpMatomo/Ecommerce/MatomoTestEcommerce.php create mode 100644 node_modules/chart.js/LICENSE.md create mode 100644 node_modules/chart.js/README.md create mode 100644 node_modules/chart.js/auto/auto.esm.d.ts create mode 100644 node_modules/chart.js/auto/auto.esm.js create mode 100644 node_modules/chart.js/auto/auto.js create mode 100644 node_modules/chart.js/auto/package.json create mode 100644 node_modules/chart.js/dist/chart.esm.js create mode 100644 node_modules/chart.js/dist/chart.js create mode 100644 node_modules/chart.js/dist/chart.min.js create mode 100644 node_modules/chart.js/dist/chunks/helpers.segment.js create mode 100644 node_modules/chart.js/dist/helpers.esm.js create mode 100644 node_modules/chart.js/helpers/helpers.esm.d.ts create mode 100644 node_modules/chart.js/helpers/helpers.esm.js create mode 100644 node_modules/chart.js/helpers/helpers.js create mode 100644 node_modules/chart.js/helpers/package.json create mode 100644 node_modules/chart.js/package.json create mode 100644 node_modules/chart.js/types/adapters.d.ts create mode 100644 node_modules/chart.js/types/animation.d.ts create mode 100644 node_modules/chart.js/types/basic.d.ts create mode 100644 node_modules/chart.js/types/color.d.ts create mode 100644 node_modules/chart.js/types/element.d.ts create mode 100644 node_modules/chart.js/types/geometric.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.canvas.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.collection.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.color.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.core.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.curve.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.dom.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.easing.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.extras.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.interpolation.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.intl.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.math.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.options.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.rtl.d.ts create mode 100644 node_modules/chart.js/types/helpers/helpers.segment.d.ts create mode 100644 node_modules/chart.js/types/helpers/index.d.ts create mode 100644 node_modules/chart.js/types/index.esm.d.ts create mode 100644 node_modules/chart.js/types/layout.d.ts create mode 100644 node_modules/chart.js/types/utils.d.ts create mode 100644 package-lock.json create mode 100644 plugins/WordPress/stylesheets/user.css create mode 100644 tests/phpunit/wpmatomo/ecommerce/test-base.php diff --git a/.gitattributes b/.gitattributes index 29ec5395d..f5a6049a0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,13 @@ /scripts/ export-ignore /vendor/ export-ignore /bin/ export-ignore +/node_modules/chart.js/auto export-ignore +/node_modules/chart.js/helpers export-ignore +/node_modules/chart.js/types export-ignore +/node_modules/chart.js/dist/chunks export-ignore +/node_modules/chart.js/dist/chart.esm.js export-ignore +/node_modules/chart.js/dist/chart.js export-ignore +/node_modules/chart.js/dist/helpers.esm.js export-ignore /README.md export-ignore /.wordpress-org export-ignore /.github export-ignore @@ -9,6 +16,7 @@ .gitignore export-ignore .gitattributes export-ignore .travis.yml export-ignore +package-lock.json bower.json export-ignore composer.json export-ignore composer.lock export-ignore diff --git a/.gitignore b/.gitignore index 4193f9890..369334ed7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .DS_Store /vendor/ +.php-cs-fixer.cache +.*~ +*~ \ No newline at end of file diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 93781ee23..8c25f1e2f 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -8,12 +8,8 @@ ./matomo.php ./shared.php ./uninstall.php - /classes/WpMatomo/Db/* - /classes/WpMatomo/AjaxTracker.php - /tests/phpunit/bootstrap.php - tests/phpunit/framework/test-matomo-test-case.php - tests/phpunit/framework/test-local-tracker.php - + /classes/WpMatomo/Db/*.php + /classes/WpMatomo/AjaxTracker.php @@ -58,6 +54,7 @@ + diff --git a/.travis.yml b/.travis.yml index 38d73f30b..a4c97dc4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,8 +35,6 @@ matrix: env: WP_VERSION=trunk - php: 7.2 env: WP_TRAVISCI=phpcs - allow_failures: - - env: WP_TRAVISCI=phpcs before_install: - if [[ "$WP_TRAVISCI" == "phpcs" ]]; then export PHPCS_DIR=/tmp/phpcs; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d1a39e1..0ee7b73c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ == Changelog == += 4.4.2 = +* Allow users to add opt out using a Gutenberg block +* Add a visual graph to the summary page +* Enable feature to select default report date +* Internal change: Improve coding style consistency +* Improve installation process + = 4.4.1 = * Update core to 4.4.1 * Fix CORS settings could not be saved in the Matomo admin diff --git a/LEGALNOTICE b/LEGALNOTICE index 615ec6a76..b90097917 100644 --- a/LEGALNOTICE +++ b/LEGALNOTICE @@ -42,4 +42,9 @@ See a list of all components/libraries and its licenses in Matomo in `app/LEGALN THIRD-PARTY CONTENT -See the list in `app/LEGALNOTICE` + Name: chart.js + Link: https://www.chartjs.org/ + License: The MIT License (MIT) see node_modules/chart.js/LICENSE.md + + For more third party content see the list in `app/LEGALNOTICE`. + diff --git a/assets/chart.js b/assets/chart.js new file mode 100644 index 000000000..627e1b73d --- /dev/null +++ b/assets/chart.js @@ -0,0 +1,52 @@ +jQuery(document).ready(function(){ + jQuery('.matomo-table[data-chart]').each(function() { + let $this = jQuery(this); + let $postbox = $this.parents('div.postbox'); + let $table = $postbox.find('table'); + $table.hide(); + let $canvas = jQuery('',{'id':$this.attr('data-chart')}); + $canvas.insertAfter($table); + let data = []; + let labels = []; + let title = $postbox.find('h2').text(); + let $row; + let value; + $table.find('tr').each(function() { + $row = jQuery(this); + value = $row.find('td:nth-child(2)').text(); + if ( '-' === value ) { + value = 0; + } + data.push(value); + labels.push($row.find('td:nth-child(1)').text()); + }); + + var myChart = new Chart($canvas, { + type: 'line', + data: { + labels: labels.reverse(), + datasets: [{ + label: title, + data: data.reverse(), + borderColor: "#55bae7", + pointBackgroundColor: "#55bae7", + pointBorderColor: "#55bae7", + pointHoverBackgroundColor: "#55bae7", + pointHoverBorderColor: "#55bae7", + }] + }, + options: { + plugins: { + legend: { + display: false + } + }, + scales: { + y: { + beginAtZero: true + } + } + } + }); + }); +}); diff --git a/assets/js/blocks/matomo_opt_out.js b/assets/js/blocks/matomo_opt_out.js new file mode 100644 index 000000000..a459d970d --- /dev/null +++ b/assets/js/blocks/matomo_opt_out.js @@ -0,0 +1,19 @@ +(function (blocks, i18n, element) { + var el = element.createElement; + var __ = i18n.__; + + const matomo_icon = el('img', {src: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgd2lkdGg9IjI0LjM0NTg3NW1tIgogICBoZWlnaHQ9IjEzLjg0NzIwOG1tIgogICB2aWV3Qm94PSIwIDAgMjQuMzQ1ODc1IDEzLjg0NzIwOCIKICAgdmVyc2lvbj0iMS4xIgogICBpZD0ic3ZnMzk3MiIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC45Mi41ICgyMDYwZWMxZjlmLCAyMDIwLTA0LTA4KSIKICAgc29kaXBvZGk6ZG9jbmFtZT0ibG9nby5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnMzOTY2Ij4KICAgIDxjbGlwUGF0aAogICAgICAgaWQ9IlNWR0lEXzJfIj4KICAgICAgPHVzZQogICAgICAgICBpZD0idXNlMzgzMyIKICAgICAgICAgc3R5bGU9Im92ZXJmbG93OnZpc2libGUiCiAgICAgICAgIHhsaW5rOmhyZWY9IiNTVkdJRF8xXyIKICAgICAgICAgeD0iMCIKICAgICAgICAgeT0iMCIKICAgICAgICAgd2lkdGg9IjEwMCUiCiAgICAgICAgIGhlaWdodD0iMTAwJSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8cGF0aAogICAgICAgZD0ibSAxNDEuNzEsMTE1LjUyIDAuMDEsLTAuMDEgLTAuMjQsLTAuMzYgYyAtMC4wNCwtMC4wNiAtMC4wNywtMC4xMSAtMC4xMSwtMC4xNyBsIC0xNi4yNCwtMjQuNzQgLTAuMDEsMC4wMSBjIC0yLjMyLC0zLjc2IC02LjQ1LC02LjI3IC0xMS4xOSwtNi4yNyAtNy4yNiwwIC0xMy4xNSw1Ljg5IC0xMy4xNSwxMy4xNSAwLDMuMzcgMS4yOCw2LjQzIDMuMzYsOC43NSBsIC0wLjAxLDAuMDEgMTUuMzYsMjMuNjQgYyAwLjA3LDAuMSAwLjEzLDAuMiAwLjIsMC4zIGwgMC4wOCwwLjEzIDAuMDEsLTAuMDEgYyAyLjM4LDMuMzggNi4zLDUuNTkgMTAuNzUsNS41OSA3LjI2LDAgMTMuMTUsLTUuODkgMTMuMTUsLTEzLjE1IC0wLjAyLC0yLjUxIC0wLjc0LC00Ljg2IC0xLjk3LC02Ljg3IHoiCiAgICAgICBpZD0iU1ZHSURfMV8iCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogIDwvZGVmcz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9ImJhc2UiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6em9vbT0iOS4yNjcxNTc5IgogICAgIGlua3NjYXBlOmN4PSI0Ni4wMDc5NDYiCiAgICAgaW5rc2NhcGU6Y3k9IjI2LjE2Nzk0NSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEyMDgiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNjU2IgogICAgIGlua3NjYXBlOndpbmRvdy14PSI3MiIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMjciCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMC4xIgogICAgIGZpdC1tYXJnaW4tbGVmdD0iMC4xIgogICAgIGZpdC1tYXJnaW4tcmlnaHQ9IjAuMSIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMC4xIiAvPgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTM5NjkiPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgICAgPGRjOnRpdGxlPjwvZGM6dGl0bGU+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIgogICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjguNDM0MiwtMTcwLjYzNTkyKSI+CiAgICA8ZwogICAgICAgaWQ9ImczODg4IgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC4yNjQ1ODMzMywwLDAsMC4yNjQ1ODMzMywxMTQuNjY3MzksMTQ4LjUxNjIxKSI+CiAgICAgIDxnCiAgICAgICAgIGlkPSJnMzg2MCI+CiAgICAgICAgPHBhdGgKICAgICAgICAgICBpZD0icGF0aDM4MjYiCiAgICAgICAgICAgZD0ibSAxNDEuNzEsMTE1LjUyIDAuMDEsLTAuMDEgLTAuMjQsLTAuMzYgYyAtMC4wNCwtMC4wNiAtMC4wNywtMC4xMSAtMC4xMSwtMC4xNyBsIC0xNi4yNCwtMjQuNzQgLTIxLjAxLDE1LjY1IDE1LjM2LDIzLjY0IGMgMC4wNywwLjEgMC4xMywwLjIgMC4yLDAuMyBsIDAuMDgsMC4xMyAwLjAxLC0wLjAxIGMgMi4zOCwzLjM4IDYuMyw1LjU5IDEwLjc1LDUuNTkgNy4yNiwwIDEzLjE1LC01Ljg5IDEzLjE1LC0xMy4xNSAtMC4wMSwtMi41MSAtMC43MywtNC44NiAtMS45NiwtNi44NyB6IgogICAgICAgICAgIGNsYXNzPSJzdDEiCiAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICBzdHlsZT0iZmlsbDojOTVjNzQ4IiAvPgogICAgICAgIDxjaXJjbGUKICAgICAgICAgICBpZD0iY2lyY2xlMzgyOCIKICAgICAgICAgICByPSIxMy4xNSIKICAgICAgICAgICBjeT0iMTIyLjQiCiAgICAgICAgICAgY3g9IjY1LjU1OTk5OCIKICAgICAgICAgICBjbGFzcz0ic3QyIgogICAgICAgICAgIHN0eWxlPSJmaWxsOiMzNWJmYzAiIC8+CiAgICAgICAgPGcKICAgICAgICAgICBpZD0iZzM4NDgiPgogICAgICAgICAgPGRlZnMKICAgICAgICAgICAgIGlkPSJkZWZzMzgzMSI+CiAgICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgIGQ9Im0gMTQxLjcxLDExNS41MiAwLjAxLC0wLjAxIC0wLjI0LC0wLjM2IGMgLTAuMDQsLTAuMDYgLTAuMDcsLTAuMTEgLTAuMTEsLTAuMTcgbCAtMTYuMjQsLTI0Ljc0IC0wLjAxLDAuMDEgYyAtMi4zMiwtMy43NiAtNi40NSwtNi4yNyAtMTEuMTksLTYuMjcgLTcuMjYsMCAtMTMuMTUsNS44OSAtMTMuMTUsMTMuMTUgMCwzLjM3IDEuMjgsNi40MyAzLjM2LDguNzUgbCAtMC4wMSwwLjAxIDE1LjM2LDIzLjY0IGMgMC4wNywwLjEgMC4xMywwLjIgMC4yLDAuMyBsIDAuMDgsMC4xMyAwLjAxLC0wLjAxIGMgMi4zOCwzLjM4IDYuMyw1LjU5IDEwLjc1LDUuNTkgNy4yNiwwIDEzLjE1LC01Ljg5IDEzLjE1LC0xMy4xNSAtMC4wMiwtMi41MSAtMC43NCwtNC44NiAtMS45NywtNi44NyB6IgogICAgICAgICAgICAgICBpZD0icGF0aDQwMDUiCiAgICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICAgICAgICA8L2RlZnM+CiAgICAgICAgICA8Y2xpcFBhdGgKICAgICAgICAgICAgIGlkPSJjbGlwUGF0aDM5NDgiPgogICAgICAgICAgICA8dXNlCiAgICAgICAgICAgICAgIGlkPSJ1c2UzOTQ2IgogICAgICAgICAgICAgICBzdHlsZT0ib3ZlcmZsb3c6dmlzaWJsZSIKICAgICAgICAgICAgICAgeGxpbms6aHJlZj0iI1NWR0lEXzFfIgogICAgICAgICAgICAgICB4PSIwIgogICAgICAgICAgICAgICB5PSIwIgogICAgICAgICAgICAgICB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgaGVpZ2h0PSIxMDAlIiAvPgogICAgICAgICAgPC9jbGlwUGF0aD4KICAgICAgICAgIDxnCiAgICAgICAgICAgICBpZD0iZzM4NDYiCiAgICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjU1ZHSURfMl8pIgogICAgICAgICAgICAgY2xhc3M9InN0MyI+CiAgICAgICAgICAgIDxpbWFnZQogICAgICAgICAgICAgICBpZD0iaW1hZ2UzODM2IgogICAgICAgICAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0OC43ODE1LDc4LjIzNTcpIgogICAgICAgICAgICAgICB4bGluazpocmVmPSJkYXRhOmltYWdlL3BuZztiYXNlNjQsaVZCT1J3MEtHZ29BQUFBTlNVaEVVZ0FBQUZjQUFBQkNDQVlBQUFBaTAwcEVBQUFBQ1hCSVdYTUFBQXNTQUFBTEVnSFMzWDc4QUFBQSBHWFJGV0hSVGIyWjBkMkZ5WlFCQlpHOWlaU0JKYldGblpWSmxZV1I1Y2NsbFBBQUFFYzVKUkVGVWVOcnNYRmw3MnppeVJRRWt0ZHVTIDdlenA2ZW4welBOOXVQLy9KOXp2NjNucmg4azJTWng0azdXTEcxRDNGRURac3VNa1hpUmJtUm5tWTBSVEZBRWNGazZkS2dCVTZyL2IgMmpiYWtQS1g2OEhmT1A3cHlxY0hBcFIyZDNmcDFhdFhOQndPS2M5emNzN0plWTdqV0RVYURSZEZFZi94eHgrODFFRCsyY3FuKzdUUSBuWjBkL2VUSkU1Mk5SanBoTnE1ZU4xU1dwb2dpSTQzRHBneVJNMlZwMlpoU3oyWTJhelpkclZaemYvNzVwN3REUXgray9Qc0FsN3JkIHJrYkREQ3pFb0tLeHNpcWh3aVdzYlkzSUpFd2NzZE5hRWJNbXR1eW9VR3d6cmVMTTFWVEJ6SG1hcGlVc3lyNSsvWHJSU043MDh0Y0ogTGoyVDd0ZHE2ZUhPRG5wYm5MQzFkYTFVazBrM3RlTW1ybWt4S2ZsTWlNbWd0bWdkRitSVWlvcE44ZDBNcloyUnRUT3I5UnlXbFpiVCBhZkg0OE5EK24xTHVCMWIwME9XdkRWemEyOXVqNTl2YmtadWtjUmJYR3NyWURtcXpoZSsyRmV1ZS95VDVtN2RRdlFiRGhGQVpHQkhuIE9KN2kreUhPRDNITkVPMFk0THNoQUJoUm9hZE5ubVZwdDFzdWRWWGVzUExYQnE1djJMTnVOeXJtOHhyNHJJMysxbFdhOXBqVkkzYnEgTVVwOUJCdnA0Yk9MNnp2WTYwSjNZam40bCtQdldkVzRnU0k2d2ZFaGFYV0VoaC9DWHZwT3VWSEQybm5aNlJSWE5QQ2h5MThmdUdpWSBsb1k1YVpqV0hVdTBSOW84UmRFdlVmTVhMTDJWK0JHeGIxZ2JleE9WU0ZBekxSWEVjWW5qRE1jVDFHNkU0ejdPSHBEaWZYVExqN2prIEU3cjNZVTJwZ1hadWRxbUJhZ0hzUFpkdnJ3STNXaVd3ditGaDFiUE1ETk0waVV6Y1FRTWVvU0V2NEJCK3hWZS9vZlJmaVBneHFyRlQgTmF5T3ZZYnpFWjgvYUtsb2dUM0ZkVE9jM01NOWRwZ0ozUmw4Q2JWRVFDdGpyWnl4YkFjRC9oOWMvdy84NERIMjNRY28vMzhCYk1YQiBGd0EycTZRRGFyZE4xTzRsRWNWdGJkUWV1dHBMVk80VnZ2b2JxdjQ3S2ZvTHJudUt2M2NDMzZtVzc1S0thdDZwK0oxaWFiQnZPRW5qIHFZSGpScmlHSTgxZU1jRWdWWW51bnBPTnk3SVJ1Ny9QWnFyV2JPcFpweE5IcUltT0RDeFd2VmhiK1Jyc3kzQisxcGJqWnRPT3B0T3YgTEhkbDRQWjZQYlBWNjhYb1hFMkRKdzJvbjZNTHdwalYzN0QvanBML2dncUtjVWwzYkMwYVZQVWVxWWRlMnVWY0ZCcktjYmlPcE92RyBhREF3MEdKb0Jhd3daMlVoc0xSTjIyMHU4SEFwaWhwRzZ4NnM2em02czFqcjM5ZFF2a1FjQlk0eXJYWE90VnFKOXNPSUI3eHlXdmdGIHBYWFF4Q0xMYWdodE9zcVlQWngrZ2YydjFTNFdERWZDSFhRYzZZcm9ocXkvd2ZsbklXbDFqZmFOSjVHZy9qdDBQeTV4SC9BaXB4VHAgUENaakVSUUkyZ1FlaEx6U08zQll6OURTWC9HVFgvRWpzV0NoaUswN2xpK25MVDdFNmMyVnFBcWlXV1J0TmltS1VsMlNaNnV3WE5KQyBCNzFlb3VLNGpjSWVrZFl2NFpsL2w2Nkk3OEYzNm9tWFBvSGpvcXJDUDNLbXRMVHJKZXZ5djBQdkxORTl4ZkdrT0puRHlUaDh4bzRCIElHbnArcUhYVUNpZlF2bU51NVl2WFFiRzZ3QjhqbTltYU9zTXNkMGNuem1jcVQwOVBlV1ZXUzY2Zzk3YTJrS0U0eHB3SE5zd25rY3MgbnBub0Y5UUtubG50UXNKMCtHYkFMbSs2K3BRdUxJSWZHaFZjcTNndWVwVEZnb1Q3akpHR2l6TVM3UXBhVUZLUFBXaFRrVnpvTWJ5SyA4bHVlRGhTTG94dmduc2V5bzMwRGFMblpaRDdQMVpKamkxWklCMjA4UGFHRDUrRXJmb0ZuRE0vTTIzd3ppL2xSQTlFN1ZLQUZWbk4wIFZVUlRsTU5zZ1RkbktCTmRuNlJNY0NzSjBDMWNWOE0xMzZPQ2E1VmZjVEFVQTVTRDRqMzhMUTlRbk9PaDZHSkVnWE44bGxVWmJGWkogQjhyVEFiM0M4ZThlWUdoTTdCM3NTZFd0N3FxcnRmUktXZ2FKbERpWEVnMjFPRVlaMUNGNXFFRVpQTUhsUFpLUVY1elIrVU82NnliVyBtWHVwcGhHNUVZa1ZUMkxtZEd0M3QxZzR0bHRicm1TWXVoRHJFcThyejNNQVY1d1kwVXN2ZDRqRXFiUXJhMXNGc05YdldTS3BXc1hoIEM4Y2kxQ0RXM0ZlU2hHR3hicXJEVWhOWWRzU3JBZFZ6YjlVRGFpRXZRVUkzV3pqWnhIRmlvOGhvK05SS0s5OGFYQ3FLUWtNRXhWRFUgTFNkQUtucUtoa0FWOEhNUHRBQit6ck9yaWdRWHprVWVtRmhzRjQzS3dQV3A2TjZnV1pWNGM0UzBMTWZSSmNkMDUvS0ZXbkFqUEdCUSBCSGdjSVYwZDVTZVFaTkRBckdkbHVhZ24zd3BjU2VIdGRqb1JmbHgzV20vNUtFYnhNM0NkT0REUHN3QTQ4Q3pmbXVkK3lMOHNRUUN6IFJGdkN2NkFGbG1nckZja0Zla0pZQzQwYUxHbWx1Wk9xSnhqUWdVZzBBMnNPa3MwNXhCU1dicTBXdkJNckNsTTRsOWc0N3VDcDdhSUIgRXJ2RFl0VVQ5dDRjWGhWUGMwVjA4QzBMUnNOOElOQUd3SHZDdlNMMHE2U0xKR0p3M2tkZGhsZGJCNllnQjlCMHJrU3Q1SUZKWWplRyBCZDg2aUtCeEc1Rmx0eHRyWTVxUVgxMG9CSWw2bmxWaEpUd29peVdqMFpJZjViV2xOTDJsc085NVFqMWJ2cVVBVzNLd3dyTWtrc3p6IDdwbEtXVkhCTUJzOFNCUW5JWGpCa2tYVFZPSUVoTFlXaFhBN25YdEIwenJuTlMwYUNPbEZMMEtnd0JKYU5vSm5YamtkZkk5LzJZc0kgUEZRQW0xV1cybHdLR2xaVkQvWTlSSnluQkM0S3dRUHgzT2VBY2M0NFo1TjZuVytzYzcvU3RDSEVGV0Jmc253UzdWYnFvTFpHT3JpYSBIa0taRWtRSURSUjByb2xqYjkwQitGWFVSenFJS0lHTVBQM3dCTGNkU2FRR1k4dHpJbnUwdjM5ank3MUFCeUR1SHA3YUUzU0o1NmczIHJKWWZoOHorMnB6WWowWlNUUFdIcVVEMTU4VFJvQXV2cWk0c1doci9GV2hmaXJaUGZKU20xQWltUEFXNDJSemViREtaM014eXZ3cHggdFY1b1dvbkUxcUZwYnd1d1dSNlI0SEM0S3F1VmZJSU5DU09mU0JkZ1Q0bWRCQkV6T1BmY0JIMTdmY3U5Um9pN0xrMTdVNERYV2E1RCBPMjJRZko0S0FLemtGZFN4Z0F5RGsrUk5mbnA2Nm00QzdrVTZZTzVCYnp4bXAxNDhNQjNjNThZZVhPYWlpZ2FIT0Q0Q1JSeWdieHdwIEdlNEJ1RG1pcXRsc2RuMXdyd3B4dVFweE9hVHhlampmckJMTHhGZVBndjdzWUxzcW5FMkZYMzBtVEtuUE1xWUdtampDMzJPcmRWcXYgMSsxU1B2ZUg0SjZGdUlqdG1qRDdIdkI3QXFuOC9BellFRzVLS0NoOFZGNEtOU1ZMUlpjODljOEd0RFJBa3ZPUVhXcUs2dmR4L0FVdCArb2kyZlFZZG5DaXRKMWxaNW5Cazl0b0RsQkxpZGhEaXNnaHhyVHVBYjRjbHhhWjRsOExnbmpnUDhaNlNOQkZwUW53NXVVd1VYUnBHICtkbXMyUnNOVjFZTGF6a0dCcDhGWVB4OUpCUmgwblRlR28zS2o1Y280WnZnbm9XNFJaRndrclJnZkYxSGFnZFBxa3NpekFPRUVnbVYga0NSNmFkQmU3QlNSazc5djdXeWdMK2pQK0ZLMFJCdFBCOEdKNVFnU3h2ZzhRYnRndGJ5UDR3TTA0aFJjTzdYRzVPUFo3TnBENjk2SiB4YTFXYkxSdUVPazJITmkySko1eFl4a0J4Wk9rQVlBZEI3WERZSXd6cnFIcW5vbk1ZdkVqQUFvU2pVa1MzTTBGMEJJYVZ4SGNwb0s4IDdNUm1LdVJyRHpsdzdSZllzNHcraktaNW5nMm1VenY1eG95YjZDb25KaU81enRvRWlEVk5pTnNCRWtuR0NRS2FqbUdlNkJJUTBvcEYgOHhVK1VSMFNHS1JDVWxyVVF3ZXc3YWlRcWQ5bG1mV2lXQjVTQ3hXczhUbGRiS29USzgvVkFTaEEwVDZNNjVOWUxZQWV5Tnl4cU5FbyBKZ2NIVGwwVFhPL0U4anhQWW1NYUVoZ0F0WFpJeENnWkVPeFhDV3FoaEFrZUxmaVdFUFZ4NkJaQ0VheGlnQ2ZqL1RKTlNQSzhqME5TIGg1L2k5NC94b1BZazMwbytCeUhPYnVQazI3bW1sV0NCejNqMm95YTE3eHlmSVBRWEo1YU54Mk9ydmpNUkw3cENlaGtBNEJQQjJCdncgaGpFQWN6SUpUUXBrNG9sRGw4QkZZeFNXc3FhQ2JDbVRXMzJ0Y0wwaDUrUmh5RmhUbDJXZWxmQ1ZIOUNUZUp4S0g2TDcwWm9Md05JRyAwc0dwOHZYblQ2Q0JULzdZMnFISjgyODZzVytCNnhPOXNGeURHOFd4MWdsdUhsZVpvSmtmU21ZYUs0UjdPRDlCMkRkMUdzV1VoVFVWIFZrNmtGMEo2U0xjSU42czVCNjRpZllyZnlXekJDUWRKNHkrcTBvVm1pYWMza0E1OGtQQVI0SDVBM2IrdzQ3NkQ5RUtGdituRXZndXUgZVBNb2lqVExzZFl5RndBeWhHWk9yRTdMeERROTBscmpXT2UxUnFNOC9QQ0JqVEVCWE9ka3lqdHQ3ZTdxMldnMHQyVTVUMHcwaFpTVCB5dWJPa3pKSDFUQjdqY0xETTFWdTlxSHA0U282Z0RMZ0Q2alVQZ3ZReG94Z2ZHbC9QQzVIMTVqOGZNRmlndDZIYlRKYmhMc2lRYVlzIEV4N1lsUUJvU3BxbVJWN01BR0ErR0F6S0Zrb0U3L0RsZXd6SFl4cUJqeUt0eSsxMlc4YjVyZWF6OGhvVXBtMzZlVnFLZVFGdy9JRDAgOEQwNjJKZWhjMURkRU5wMEhvTU9QdjJBRHE0Q0Y1STJjUUN1Vk5ibUxpdkFqNUVWZTdKa0M4Y3VSUkFvWVY3UjcvZnR5Y21KT3pvNiArcW9BZ0s2cW9XV1d0UVFtanJuWmFDaG5aV0NQWWppL3BrelpZMUVOck9TNGhnWXNSbWlqcXRMMEFIU3dIT0lLSFh3Q3VCL1IyVDdEIDJrNXVRZ2RxT1E5NmxxTGprTkJ2eExIU1JlRktFNVZhNmRTVVVRcDFtclZhclFLZ0NyRFhXaE1BbWxGbENhTUhiVVFBV1F1OHdnemEgUjI0MUN2UlFQMHRzbjArRXUwOXdmWjZXQWgyZ3Q1Tk04UGdYYXZBYW4yOVpISmt4L2R6YTJlbDRYUFN6N05xTFhTN29UQUNoc2l4VCBNWUJvZFRxMkpDNlRXbElZYllyandiRlBUSHorL1BrbWl6MDh3SWoybEl4a3NnNXpPZ2lLUWpxS1RPZXR3RjFNNFl4VUdFbWxld1JZIHJMYmdNTERaOS95cTZDMXM3QTMrL29ENkhrVlpOcWtOaC9sK3lCL2NEbHhwRUp3U2w5WWlER05PMDlSbWVlWnkvRHM4UEhURDRmQlcgYThFU0FFeTFtdEtOQmp1MW1EWURnRm1GdWJCVWhjcmV3ZkY5aHNqQmlSRkpia1RXUFh4Qm5kNlRXQzJwOTZqRWx3Z0JBK3FjanZyOSBjbnJESlZyUkZjU3U1dk01WTZmbGMzZlpKSlUweXpLM2xXVkZNMjZFN0ZJRUhSM3l3R0dTTWZzOXZrZDV0cElROXlhV3U3Wk5LR2U3IEtMZ0pqNVpGRWN2YUdRcnl3aXNGOWhUQmRaSzVDSkpOQytmWFNROExUUnZVQVN2SXJqTTZlTytCMW5wTWNaeWgxOXJiR05sOWluZEcgV0szbUxXZmpScEl4cEEwYUlITzVZTDNVOWt1V0Z1c1UxaS9QVmhiaWJvVGxLaFU4UmdHbkpoSk5acVk0Q2VuWTV4ZkVVaE94M3BEUyBKRkVTOGFVOE1LMlVEa0tPWklLeWpvUHNVbTgwc3lpRWovREN4OUMwazJRd3lEL2QwSWs5R0xpeUljSlJZc0VBbDZNb1pycC9lWGFKIERoZ1dxOTZnRHEraGFkL0RqeDg0aEw2eVd2STJUdXhCd1Yzdzd3UEpzNFU2U0xGTHNQQUZ2ZVU5enY4VE4zNTNGMDI3TWVBK2tEejcgSGgyOFVSVG9JQUlkM0ViVGJoUzRNcjg5azZVYlVhUmluYkRHUDZYOWdLWk13SThxY092VnpNWG9qdnk3QUxha0MrcEF2VjJpZ3krciBvb01IQi9lYThxeEdZUmIzQW1COUM0RDVRaXBSMXZSS3NBQzVoUzllQTh5M1VDNzdxNlNEalFCWHRwclFRNk91NHEyMjUwUUtTUTVkIE9iTmFOUW9TbkJ0ZFdJeDNIWURQZ1EzTHFtU2cwZWRveFlscHhXOFc2bUNWZExBeDRDN0xNMk5BdWRhR3hDNDhuSkozSUpCazB2ekkgY1V4K1lQTnNaYzFsQjNjNW9xem1ISGlMbFp5MExKSStramtIUG5lZzZEVUpIVGgzYUZkTUJ4c0Q3ckk4azVuWk1jVk95Mmk5RHF2cCBLdEFNaFpVNmxXb2dQd2Y1SEZWYXpuQnhsVDRVYTgwbDBWOWx1NDdJanlxb3QwRjZxZmZRMlB0c3pXbXV5cFhTd1VhQit4WC94bDcvIEl1N0g3a0VNODAxOG9qY000MysxeXdJL0NxRG1hckhLUjFTQmpOMlJPb1MxZmdyQWVpcDRwMWp2VytKK21lZVQ3bmlZZjVoTW5GcnggMjZBMlplenFMRHpPMm13YmlVbFZYZ3pCRTlvSnRsckwxRTFaTlRsakdTZ2xQK3RIMXZHMktrMGMwL243RW16MXZvUjVHTGRUb2d3TyAvZnNTb0dseHdiK1VkWitOY3NlUkxTZmNhUlFuaC9zckIzYlR3RlhIa2oxTFUvZTAzUzVhY1NRekI1V05JZ2NtTG1TNENkWTNKSm0yIHlUSmN6ekpYYlpHUFNBSnR5TEJ5OWE0RVA2SWdhVVQvcG84RGRJZ3ZBRjZpc1VONHpYNXN5ekUwZHZaNU1DaVAxL1Qrc2syYzdVTE4gWnBPZTd1M3BwakZ4a2RzNjY2ak4yc3BzZHV4cUYrRHVLbGwrR2w2aklrdFFHMkVXejlrN2FpYXFlbzBLR3RqSHlST0FmNnBsRWFEViBJNlB5YVZTdjV4N1k0Mk5XLzBIZ25nSDh2RjdYVGFYTnBOV3VBVHBaVE5lRzViYjl4RURtRHF5eXpZcXJUSnFzQ2ZNcm1EemZ3bG1OIFpZNEZUb3lKM1pobERFeVdrK1kyM1o0TWl2ZTFtbDBuc0pzTXJ0LzJKRHhMRXJLOW5tbTMyM0ZaRkhGa2pDekRxbE1wczNwY0E1NnYgRGc2T1ZmVmVNQ1d6ZjVneUFEb0gzaWtDNXhRNk5pMWgwWEVjRi8yREE5dUZsbjI3L2xjWi9oUlRPYjBWQTF3b1g2MjN0N2NqbTJiRyBsQzR1RGNYbzZwR0x0SEV1T0RRakM2MnRLMlZoU0dTNUtPcXh4Yy9LbzZNaksxcjY0T0JncmRhNmtWTHNCenFZcDlPcG4zd2lHblkyIGxzbmNxbWozdWptenl4MENBQWRMaFh5YlEyRE1ramhLbzFxU25wNGNGZk95RkFkblpReHdlc1Y3YVA3VExmZktlbmU3WFpuYlJtbWEga294WWh4bFZZWGxvbzlId24rL2V2YnMzSy8xM0F2ZDc3V0QxMyszZmYvdC9BUVlBNjZnMU1DdTRUUjhBQUFBQVNVVk9SSzVDWUlJPSIKICAgICAgICAgICAgICAgaGVpZ2h0PSI2NSIKICAgICAgICAgICAgICAgd2lkdGg9Ijg2IgogICAgICAgICAgICAgICBzdHlsZT0ib3ZlcmZsb3c6dmlzaWJsZTtvcGFjaXR5OjAuNSIgLz4KICAgICAgICAgICAgPGcKICAgICAgICAgICAgICAgaWQ9ImczODQ0Ij4KICAgICAgICAgICAgICA8ZwogICAgICAgICAgICAgICAgIGlkPSJnMzg0MiI+CiAgICAgICAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgICAgICAgaWQ9InBhdGgzODM4IgogICAgICAgICAgICAgICAgICAgZD0ibSAxMjcuMTQsOTcuMTUgYyAwLC03LjI2IC01Ljg5LC0xMy4xNSAtMTMuMTUsLTEzLjE1IC03LjI2LDAgLTEzLjE1LDUuODkgLTEzLjE1LDEzLjE1IDAsMi42NyAwLjgsNS4xNSAyLjE3LDcuMjIgMCwwIDAsMCAwLDAgSCAxMDMgTCA5My4wMiw5MC4yNiBIIDkzIEMgOTAuNjgsODYuNTEgODYuNTUsODQgODEuODEsODQgYyAtNC43NCwwIC04Ljg3LDIuNTEgLTExLjE4LDYuMjYgaCAtMC4wMSBsIC0xNS45LDI0Ljc4IGMgMi4zNiwtMy40OSA2LjM2LC01Ljc5IDEwLjksLTUuNzkgNC43NywwIDguOTQsMi41NSAxMS4yNCw2LjM2IGggMC4wMiBsIDEwLjM5LDE0LjU5IGggMC4wMiBjIDIuMzksMy4yNCA2LjIzLDUuMzYgMTAuNTcsNS4zNiA0LjM0LDAgOC4xOCwtMi4xMSAxMC41NywtNS4zNiBoIDAuMDIgbCAwLjExLC0wLjE3IGMgMC4yNywtMC4zOCAwLjUyLC0wLjc3IDAuNzYsLTEuMTggbCAxNS41NywtMjQuMzMgYyAwLDAgMCwwIDAsMCAxLjQxLC0yLjEgMi4yNSwtNC42NCAyLjI1LC03LjM3IHogTSAxMDQsMTA1LjY5IGMgLTAuMDEsLTAuMDIgLTAuMDMsLTAuMDMgLTAuMDUsLTAuMDUgMC4wMiwwLjAxIDAuMDMsMC4wMyAwLjA1LDAuMDUgeiBtIDAuOTcsMS4wMSBjIDAuMDgsMC4wNyAwLjE2LDAuMTQgMC4yMywwLjIxIC0wLjA3LC0wLjA2IC0wLjE1LC0wLjEzIC0wLjIzLC0wLjIxIHogbSAxLjE3LDAuOTggYyAwLjA5LDAuMDYgMC4xNywwLjEzIDAuMjYsMC4xOSAtMC4wOSwtMC4wNiAtMC4xNywtMC4xMiAtMC4yNiwtMC4xOSB6IG0gMS40OSwwLjk4IGMgMC4wMSwwIDAuMDIsMC4wMSAwLjAzLDAuMDIgLTAuMDEsLTAuMDEgLTAuMDIsLTAuMDIgLTAuMDMsLTAuMDIgeiBtIDEuNCwwLjY2IGMgMC4xMSwwLjA0IDAuMjIsMC4wOCAwLjM0LDAuMTIgLTAuMTIsLTAuMDQgLTAuMjMsLTAuMDcgLTAuMzQsLTAuMTIgeiBtIDEuNDEsMC40OCBjIDAuMTYsMC4wNCAwLjMxLDAuMDggMC40NywwLjEyIC0wLjE1LC0wLjA0IC0wLjMxLC0wLjA4IC0wLjQ3LC0wLjEyIHogbSAxLjU0LDAuMzMgYyAwLjE1LDAuMDIgMC4zLDAuMDUgMC40NiwwLjA3IC0wLjE2LC0wLjAyIC0wLjMxLC0wLjA1IC0wLjQ2LC0wLjA3IHogbSAxMC42NSwtMy4wOSBjIDAuMTIsLTAuMSAwLjIzLC0wLjIxIDAuMzUsLTAuMzEgLTAuMTEsMC4xIC0wLjIzLDAuMiAtMC4zNSwwLjMxIHogbSAtNy4xMSwzLjE2IGMgMC4xNiwtMC4wMiAwLjMxLC0wLjA1IDAuNDYsLTAuMDcgLTAuMTUsMC4wMiAtMC4zLDAuMDUgLTAuNDYsMC4wNyB6IG0gMS40OSwtMC4yNyBjIDAuMTcsLTAuMDQgMC4zMywtMC4wOCAwLjUsLTAuMTMgLTAuMTYsMC4wNSAtMC4zMywwLjA5IC0wLjUsMC4xMyB6IG0gMS40OSwtMC40NSBjIDAuMTQsLTAuMDUgMC4yOCwtMC4xIDAuNDIsLTAuMTUgLTAuMTQsMC4wNiAtMC4yOCwwLjEgLTAuNDIsMC4xNSB6IG0gMS43NCwtMC43NiBjIDAsMCAwLDAgMCwwIDAsMCAwLDAgMCwwIHogbSAxLjI0LC0wLjc4IGMgMC4xMSwtMC4wOCAwLjIyLC0wLjE2IDAuMzMsLTAuMjQgLTAuMTEsMC4wOCAtMC4yMSwwLjE2IC0wLjMzLDAuMjQgeiBtIDIuMjMsLTEuOTcgYyAwLjA5LC0wLjEgMC4xOSwtMC4yIDAuMjgsLTAuMzEgLTAuMDksMC4xMSAtMC4xOCwwLjIxIC0wLjI4LDAuMzEgeiIKICAgICAgICAgICAgICAgICAgIGNsYXNzPSJzdDQiCiAgICAgICAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzMyNTJhMCIgLz4KICAgICAgICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgICAgICBpZD0icGF0aDM4NDAiCiAgICAgICAgICAgICAgICAgICBkPSJtIDU0LjQsMTE1LjU2IGMgMC4xLC0wLjE2IDAuMTksLTAuMzIgMC4yOSwtMC40OCBsIC0wLjMxLDAuNDggeiIKICAgICAgICAgICAgICAgICAgIGNsYXNzPSJzdDQiCiAgICAgICAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzMyNTJhMCIgLz4KICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDwvZz4KICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICAgICAgPGNpcmNsZQogICAgICAgICAgIGlkPSJjaXJjbGUzODUwIgogICAgICAgICAgIHI9IjEzLjE4IgogICAgICAgICAgIGN5PSI5Ny4xNjk5OTgiCiAgICAgICAgICAgY3g9IjExMy45NiIKICAgICAgICAgICBjbGFzcz0ic3Q1IgogICAgICAgICAgIHN0eWxlPSJmaWxsOiNmMzgzMzQiIC8+CiAgICAgICAgPGcKICAgICAgICAgICBpZD0iZzM4NTgiPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgIGlkPSJwYXRoMzg1MiIKICAgICAgICAgICAgIGQ9Im0gMTEzLjkzLDExMC4zIGMgLTQuNTksMCAtOC42MywtMi4zNiAtMTAuOTgsLTUuOTMgaCAtMC4wMSBMIDkyLjk2LDkwLjI2IEggOTIuOTUgQyA5MC42Myw4Ni41MSA4Ni40OSw4NCA4MS43Niw4NCBjIC00LjczLDAgLTguODcsMi41MSAtMTEuMTgsNi4yNiBoIC0wLjAxIGwgLTE1LjksMjQuNzggYyAyLjM2LC0zLjQ5IDYuMzYsLTUuNzkgMTAuOSwtNS43OSA0Ljc3LDAgOC45NCwyLjU1IDExLjI0LDYuMzYgaCAwLjAyIGwgMTAuMzksMTQuNTkgaCAwLjAyIGMgMi4zOSwzLjI0IDYuMjMsNS4zNiAxMC41Nyw1LjM2IDQuMzQsMCA4LjE4LC0yLjExIDEwLjU3LC01LjM2IGggMC4wMiBsIDAuMTEsLTAuMTcgYyAwLjI3LC0wLjM4IDAuNTIsLTAuNzcgMC43NiwtMS4xOCBsIDE1LjYyLC0yNC4zMyBjIC0yLjM4LDMuNDkgLTYuNDMsNS43OCAtMTAuOTYsNS43OCB6IgogICAgICAgICAgICAgY2xhc3M9InN0NiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojMzE1MmEwIiAvPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgIGlkPSJwYXRoMzg1NCIKICAgICAgICAgICAgIGQ9Im0gNTQuMzMsMTE1LjU2IGggMC4wMiBjIDAuMSwtMC4xNiAwLjE5LC0wLjMyIDAuMjksLTAuNDggeiIKICAgICAgICAgICAgIGNsYXNzPSJzdDYiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzMxNTJhMCIgLz4KICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICBpZD0icGF0aDM4NTYiCiAgICAgICAgICAgICBkPSJNIDEyNC44OCwxMDQuNDMiCiAgICAgICAgICAgICBjbGFzcz0ic3Q2IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTUyYTAiIC8+CiAgICAgICAgPC9nPgogICAgICA8L2c+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K'}); + + blocks.registerBlockType('matomo/matomo-opt-out', { + title: __('Matomo opt out', 'matomo'), + icon: matomo_icon, + category: 'text', + example: {}, + edit: function () { + return __('Matomo opt out', 'matomo'); + }, + save: function () { + return '[matomo_opt_out]'; + }, + }); +})(window.wp.blocks, window.wp.i18n, window.wp.element); \ No newline at end of file diff --git a/classes/WpMatomo.php b/classes/WpMatomo.php index 7da2e0d85..e999fc526 100644 --- a/classes/WpMatomo.php +++ b/classes/WpMatomo.php @@ -11,32 +11,32 @@ exit; // if accessed directly } -use WpMatomo\Admin\Menu; +use WpMatomo\Admin\Admin; +use WpMatomo\Admin\Chart; use WpMatomo\Admin\Dashboard; +use WpMatomo\Admin\Menu; +use WpMatomo\AjaxTracker; +use WpMatomo\Annotations; +use WpMatomo\API; +use WpMatomo\Capabilities; use WpMatomo\Commands\MatomoCommands; use WpMatomo\Ecommerce\EasyDigitalDownloads; use WpMatomo\Ecommerce\MemberPress; +use WpMatomo\Ecommerce\Woocommerce; +use WpMatomo\Installer; use WpMatomo\OptOut; use WpMatomo\Paths; -use WpMatomo\ScheduledTasks; -use \WpMatomo\Site\Sync as SiteSync; -use WpMatomo\AjaxTracker; -use \WpMatomo\User\Sync as UserSync; -use \WpMatomo\Installer; -use \WpMatomo\Updater; -use \WpMatomo\Roles; -use \WpMatomo\Annotations; -use \WpMatomo\TrackingCode; -use \WpMatomo\Settings; -use \WpMatomo\Capabilities; -use \WpMatomo\Ecommerce\Woocommerce; -use \WpMatomo\Report\Renderer; -use WpMatomo\API; -use \WpMatomo\Admin\Admin; use WpMatomo\RedirectOnActivation; +use WpMatomo\Report\Renderer; +use WpMatomo\Roles; +use WpMatomo\ScheduledTasks; +use WpMatomo\Settings; +use WpMatomo\Site\Sync as SiteSync; +use WpMatomo\TrackingCode; +use WpMatomo\Updater; +use WpMatomo\User\Sync as UserSync; class WpMatomo { - /** * @var Settings */ @@ -58,7 +58,7 @@ public function __construct() { return; } - add_action( 'init', array( $this, 'init_plugin' ) ); + add_action( 'init', [ $this, 'init_plugin' ] ); $capabilities = new Capabilities( self::$settings ); $capabilities->register_hooks(); @@ -97,10 +97,13 @@ public function __construct() { $referral->register_hooks(); } + $chart = new Chart(); + $chart->register_hooks(); + /* * @see https://github.com/matomo-org/matomo-for-wordpress/issues/434 */ - $redirect = new RedirectOnActivation($this); + $redirect = new RedirectOnActivation( $this ); $redirect->register_hooks(); } @@ -115,10 +118,10 @@ public function __construct() { add_filter( 'plugin_action_links_' . plugin_basename( MATOMO_ANALYTICS_FILE ), - array( + [ $this, 'add_settings_link', - ) + ] ); } @@ -134,7 +137,7 @@ private function check_compatibility() { $upload_path = $paths->get_upload_base_dir(); if ( $upload_path - && ! is_writable( dirname( $upload_path ) ) ) { + && ! is_writable( dirname( $upload_path ) ) ) { add_action( 'init', function () use ( $upload_path ) { @@ -142,7 +145,7 @@ function () use ( $upload_path ) { add_action( 'admin_notices', function () use ( $upload_path ) { - echo '

' . sprintf( __( 'Matomo Analytics requires the uploads directory %s to be writable. Please make the directory writable for it to work.', 'matomo' ), '(' . esc_html( dirname( $upload_path ) ) . ')' ) . '

'; + echo '

' . sprintf( esc_html__( 'Matomo Analytics requires the uploads directory %s to be writable. Please make the directory writable for it to work.', 'matomo' ), '(' . esc_html( dirname( $upload_path ) ) . ')' ) . '

'; } ); } @@ -157,24 +160,13 @@ function () use ( $upload_path ) { public static function is_admin_user() { if ( ! function_exists( 'is_multisite' ) - || ! is_multisite() ) { + || ! is_multisite() ) { return current_user_can( 'administrator' ); } return is_super_admin(); } - private static function get_active_plugins() { - $plugins = []; - if ( function_exists( 'is_multisite' ) && is_multisite() ) { - $muplugins = get_site_option( 'active_sitewide_plugins' ); - $plugins = array_keys( $muplugins ); - } - $plugins = array_merge( (array) get_option( 'active_plugins', array() ), $plugins ); - - return $plugins; - } - public static function is_safe_mode() { if ( defined( 'MATOMO_SAFE_MODE' ) ) { return MATOMO_SAFE_MODE; @@ -183,14 +175,29 @@ public static function is_safe_mode() { // we are not using is_plugin_active() for performance reasons $active_plugins = self::get_active_plugins(); - if ( in_array( 'wp-rss-aggregator/wp-rss-aggregator.php', $active_plugins ) - || in_array( 'wp-defender/wp-defender.php', $active_plugins ) ) { + if ( in_array( 'wp-rss-aggregator/wp-rss-aggregator.php', $active_plugins, true ) + || in_array( 'wp-defender/wp-defender.php', $active_plugins, true ) ) { return true; } return false; } + private static function get_active_plugins() { + $plugins = []; + if ( function_exists( 'is_multisite' ) && is_multisite() ) { + $muplugins = get_site_option( 'active_sitewide_plugins' ); + $plugins = array_keys( $muplugins ); + } + $plugins = array_merge( (array) get_option( 'active_plugins', [] ), $plugins ); + + return $plugins; + } + + public static function should_disable_addhandler() { + return defined( 'MATOMO_DISABLE_ADDHANDLER' ) && MATOMO_DISABLE_ADDHANDLER; + } + public function add_settings_link( $links ) { $get_started = new \WpMatomo\Admin\GetStarted( self::$settings ); @@ -204,13 +211,11 @@ public function add_settings_link( $links ) { } public function init_plugin() { - if ( ( is_admin() || matomo_is_app_request() ) - && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) { + if ( ( is_admin() || matomo_is_app_request() ) && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) { $installer = new Installer( self::$settings ); $installer->register_hooks(); if ( $installer->looks_like_it_is_installed() ) { - if ( is_admin() - && ( ! defined( 'MATOMO_ENABLE_AUTO_UPGRADE' ) || MATOMO_ENABLE_AUTO_UPGRADE ) ) { + if ( is_admin() && ( ! defined( 'MATOMO_ENABLE_AUTO_UPGRADE' ) || MATOMO_ENABLE_AUTO_UPGRADE ) ) { $updater = new Updater( self::$settings ); $updater->update_if_needed(); } @@ -228,8 +233,8 @@ public function init_plugin() { } $tracking_code = new TrackingCode( self::$settings ); if ( self::$settings->is_tracking_enabled() - && self::$settings->get_global_option( 'track_ecommerce' ) - && ! $tracking_code->is_hidden_user() ) { + && self::$settings->get_global_option( 'track_ecommerce' ) + && ! $tracking_code->is_hidden_user() ) { $tracker = new AjaxTracker( self::$settings ); $woocommerce = new Woocommerce( $tracker ); @@ -244,8 +249,4 @@ public function init_plugin() { do_action( 'matomo_ecommerce_init', $tracker ); } } - - public static function should_disable_addhandler() { - return defined( 'MATOMO_DISABLE_ADDHANDLER' ) && MATOMO_DISABLE_ADDHANDLER; - } } diff --git a/classes/WpMatomo/API.php b/classes/WpMatomo/API.php index a0ab000a3..33d3d170f 100644 --- a/classes/WpMatomo/API.php +++ b/classes/WpMatomo/API.php @@ -9,31 +9,36 @@ namespace WpMatomo; +use Exception; use Piwik\API\Request; use Piwik\Common; +use WP_Error; +use WP_REST_Request; if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } - +/** + * phpcs:disable WordPress.Security.NonceVerification.Missing + */ class API { const VERSION = 'matomo/v1'; const ROUTE_HIT = 'hit'; public function register_hooks() { - add_action( 'rest_api_init', array( $this, 'register_routes' ) ); + add_action( 'rest_api_init', [ $this, 'register_routes' ] ); } public function register_routes() { register_rest_route( self::VERSION, '/' . self::ROUTE_HIT . '/', - array( - 'methods' => array( 'GET', 'POST' ), + [ + 'methods' => [ 'GET', 'POST' ], 'permission_callback' => '__return_true', - 'callback' => array( $this, 'hit' ), - ) + 'callback' => [ $this, 'hit' ], + ] ); $this->register_route( 'API', 'getProcessedReport' ); $this->register_route( 'API', 'getReportMetadata' ); @@ -88,18 +93,20 @@ public function hit() { if ( empty( $_GET ) && empty( $_POST ) && empty( $_POST['idsite'] ) && empty( $_GET['idsite'] ) ) { // todo if uploads dir is not writable, we may want to generate the matomo.js here and save it as an // option... then we could also save it compressed - $paths = new Paths(); - $path = $paths->get_matomo_js_upload_path(); + $paths = new Paths(); + $path = $paths->get_matomo_js_upload_path(); + $wp_filesystem = $paths->get_file_system(); header( 'Content-Type: application/javascript' ); header( 'Content-Length: ' . ( filesize( $path ) ) ); - readfile( $paths->get_upload_base_dir() . '/matomo.js' ); // Reading the file into the output buffer + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $wp_filesystem->get_contents( $paths->get_upload_base_dir() . '/matomo.js' ); // Reading the file into the output buffer exit; } include_once plugin_dir_path( MATOMO_ANALYTICS_FILE ) . 'app/piwik.php'; exit; } - public function execute_api_method( \WP_REST_Request $request ) { + public function execute_api_method( WP_REST_Request $request ) { $attributes = $request->get_attributes(); $method = $attributes['matomoModule'] . '.' . $attributes['matomoMethod']; @@ -135,7 +142,7 @@ public function to_snake_case( $method ) { * @api */ public function register_route( $api_module, $api_method ) { - $methods = array( + $methods = [ 'get' => 'GET', 'edit' => 'PUT', 'update' => 'PUT', @@ -147,8 +154,8 @@ public function register_route( $api_module, $api_method ) { 'send' => 'POST', 'delete' => 'DELETE', 'remove' => 'DELETE', - ); - $starts_with_keep_prefix = array( 'anonymize', 'invalidate', 'run', 'send' ); + ]; + $starts_with_keep_prefix = [ 'anonymize', 'invalidate', 'run', 'send' ]; $method = 'GET'; $wp_api_module = $this->to_snake_case( $api_module ); @@ -170,13 +177,13 @@ public function register_route( $api_module, $api_method ) { register_rest_route( self::VERSION, '/' . $wp_api_module . '/' . $wp_api_action . '/', - array( - 'methods' => $method, - 'callback' => array( $this, 'execute_api_method' ), + [ + 'methods' => $method, + 'callback' => [ $this, 'execute_api_method' ], 'permission_callback' => '__return_true', // permissions are checked in the method itself - 'matomoModule' => $api_module, - 'matomoMethod' => $api_method, - ) + 'matomoModule' => $api_module, + 'matomoMethod' => $api_method, + ] ); } @@ -186,7 +193,7 @@ private function execute_request( $api_method, $with_idsite, $params ) { $idsite = $site->get_current_matomo_site_id(); if ( ! $idsite ) { - return new \WP_Error( 'Site not found. Make sure it is synced' ); + return new WP_Error( 'Site not found. Make sure it is synced' ); } $params['idSite'] = $idsite; @@ -203,19 +210,18 @@ private function execute_request( $api_method, $with_idsite, $params ) { // refs https://github.com/matomo-org/wp-matomo/issues/370 ensuring segment will be used from default request when // creating new request object and not the encoded segment - if (isset($params['segment'])) { - if (isset($_GET['segment']) || isset($_POST['segment'])) { - unset($params['segment']); // matomo will read the segment from default request - } elseif (!empty($params['segment']) && is_string($params['segment'])) { + if ( isset( $params['segment'] ) ) { + if ( isset( $_GET['segment'] ) || isset( $_POST['segment'] ) ) { + unset( $params['segment'] ); // matomo will read the segment from default request + } elseif ( ! empty( $params['segment'] ) && is_string( $params['segment'] ) ) { // manually unsanitize this value - $params['segment'] = Common::unsanitizeInputValue($params['segment']); + $params['segment'] = Common::unsanitizeInputValue( $params['segment'] ); } } - try { $result = Request::processRequest( $api_method, $params ); - } catch ( \Exception $e ) { + } catch ( Exception $e ) { $code = 'matomo_error'; if ( $e->getCode() ) { $code .= '_' . $code; @@ -224,7 +230,7 @@ private function execute_request( $api_method, $with_idsite, $params ) { $code = str_replace( 'piwik', 'matomo', $this->to_snake_case( get_class( $e ) ) ); } - return new \WP_Error( $code, $e->getMessage() ); + return new WP_Error( $code, $e->getMessage() ); } return $result; diff --git a/classes/WpMatomo/Access.php b/classes/WpMatomo/Access.php index c5b9f0e1a..ea05a2c93 100644 --- a/classes/WpMatomo/Access.php +++ b/classes/WpMatomo/Access.php @@ -16,12 +16,12 @@ } class Access { - public static $matomo_permissions = array( + public static $matomo_permissions = [ Capabilities::KEY_NONE => 'None', Capabilities::KEY_VIEW => 'View', Capabilities::KEY_WRITE => 'Write', Capabilities::KEY_ADMIN => 'Admin', - ); + ]; /** * @var Settings @@ -47,7 +47,7 @@ public function save( $values ) { $roles = new Roles( $this->settings ); $available_roles = $roles->get_available_roles_for_configuration(); - $caps_to_store = array(); + $caps_to_store = []; foreach ( $values as $role => $matomo_permission ) { if ( isset( $available_roles[ $role ] ) && $wp_roles->is_role( $role ) @@ -58,7 +58,7 @@ public function save( $values ) { // we can't add the capabilities to the role directly using say $wp_roles->add_role cause it would not be // synced across sites when the plugin is network activated - $this->settings->apply_changes( array( Settings::OPTION_KEY_CAPS_ACCESS => $caps_to_store ) ); + $this->settings->apply_changes( [ Settings::OPTION_KEY_CAPS_ACCESS => $caps_to_store ] ); $sync = new Sync(); $sync->sync_current_users(); @@ -70,5 +70,4 @@ public function save( $values ) { wp_schedule_single_event( time() + 10, ScheduledTasks::EVENT_SYNC ); } } - } diff --git a/classes/WpMatomo/Admin/AccessSettings.php b/classes/WpMatomo/Admin/AccessSettings.php index 6f75f38f5..82135e03b 100644 --- a/classes/WpMatomo/Admin/AccessSettings.php +++ b/classes/WpMatomo/Admin/AccessSettings.php @@ -11,8 +11,8 @@ use WpMatomo\Access; use WpMatomo\Capabilities; -use WpMatomo\Settings; use WpMatomo\Roles; +use WpMatomo\Settings; if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly @@ -41,27 +41,27 @@ public function get_title() { return esc_html__( 'Access', 'matomo' ); } + public function show_settings() { + $this->update_if_submitted(); + + $access = $this->access; + $roles = new Roles( $this->settings ); + $capabilites = new Capabilities( $this->settings ); + include dirname( __FILE__ ) . '/views/access.php'; + } + private function update_if_submitted() { if ( isset( $_POST ) && ! empty( $_POST[ self::FORM_NAME ] ) && is_admin() && check_admin_referer( self::NONCE_NAME ) && current_user_can( Capabilities::KEY_SUPERUSER ) ) { - $this->access->save( $_POST[ self::FORM_NAME ] ); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $this->access->save( wp_unslash( $_POST[ self::FORM_NAME ] ) ); return true; } return false; } - - public function show_settings() { - $this->update_if_submitted(); - - $access = $this->access; - $roles = new Roles( $this->settings ); - $capabilites = new Capabilities( $this->settings ); - include dirname( __FILE__ ) . '/views/access.php'; - } - } diff --git a/classes/WpMatomo/Admin/Admin.php b/classes/WpMatomo/Admin/Admin.php index 5134cbf41..f9042670e 100644 --- a/classes/WpMatomo/Admin/Admin.php +++ b/classes/WpMatomo/Admin/Admin.php @@ -20,16 +20,15 @@ class Admin { * @param Settings $settings */ public function __construct( $settings, $init_menu = true ) { - if ($init_menu) { + if ( $init_menu ) { new Menu( $settings ); } - add_action( 'admin_enqueue_scripts', array( $this, 'load_scripts' ) ); + add_action( 'admin_enqueue_scripts', [ $this, 'load_scripts' ] ); } public function load_scripts() { wp_enqueue_style( 'matomo_admin_css', plugins_url( 'assets/css/admin-style.css', MATOMO_ANALYTICS_FILE ), false, '1.0.0' ); - wp_enqueue_script( 'matomo_admin_js', plugins_url( 'assets/js/admin.js', MATOMO_ANALYTICS_FILE ), array( 'jquery' ), '1.0', true ); + wp_enqueue_script( 'matomo_admin_js', plugins_url( 'assets/js/admin.js', MATOMO_ANALYTICS_FILE ), [ 'jquery' ], '1.0', true ); } - } diff --git a/classes/WpMatomo/Admin/AdminSettings.php b/classes/WpMatomo/Admin/AdminSettings.php index fda3dcefe..ce2f9af4f 100644 --- a/classes/WpMatomo/Admin/AdminSettings.php +++ b/classes/WpMatomo/Admin/AdminSettings.php @@ -9,9 +9,6 @@ namespace WpMatomo\Admin; -use Piwik\Cache; -use Piwik\Option; -use Piwik\Plugins\SitesManager\API; use WpMatomo\Access; use WpMatomo\Settings; @@ -20,12 +17,12 @@ } class AdminSettings { - const TAB_TRACKING = 'tracking'; - const TAB_ACCESS = 'access'; + const TAB_TRACKING = 'tracking'; + const TAB_ACCESS = 'access'; const TAB_EXCLUSIONS = 'exlusions'; - const TAB_PRIVACY = 'privacy'; + const TAB_PRIVACY = 'privacy'; const TAB_GEOLOCATION = 'geolocation'; - const TAB_ADVANCED = 'advanced'; + const TAB_ADVANCED = 'advanced'; /** * @var Settings @@ -40,10 +37,10 @@ public static function make_url( $tab ) { global $_parent_pages; $menu_slug = Menu::SLUG_SETTINGS; - if (is_multisite() && is_network_admin()) { - if ( isset( $_parent_pages[$menu_slug] ) ) { - $parent_slug = $_parent_pages[$menu_slug]; - if ( $parent_slug && ! isset( $_parent_pages[$parent_slug] ) ) { + if ( is_multisite() && is_network_admin() ) { + if ( isset( $_parent_pages[ $menu_slug ] ) ) { + $parent_slug = $_parent_pages[ $menu_slug ]; + if ( $parent_slug && ! isset( $_parent_pages[ $parent_slug ] ) ) { $url = network_admin_url( add_query_arg( 'page', $menu_slug, $parent_slug ) ); } else { $url = network_admin_url( 'admin.php?page=' . $menu_slug ); @@ -51,51 +48,52 @@ public static function make_url( $tab ) { } else { $url = ''; } - - $url = esc_url( $url ); } else { $url = menu_page_url( $menu_slug, false ); } - return add_query_arg( array( 'tab' => $tab ), $url ); + + return add_query_arg( [ 'tab' => $tab ], $url ); } public function show() { - $access = new Access( $this->settings ); + $access = new Access( $this->settings ); $access_settings = new AccessSettings( $access, $this->settings ); - $tracking = new TrackingSettings( $this->settings ); - $exclusions = new ExclusionSettings( $this->settings ); - $geolocation = new GeolocationSettings( $this->settings ); - $privacy = new PrivacySettings( $this->settings ); - $advanced = new AdvancedSettings( $this->settings ); - $setting_tabs = array( - self::TAB_TRACKING => $tracking, - self::TAB_ACCESS => $access_settings, - self::TAB_PRIVACY => $privacy, - self::TAB_EXCLUSIONS => $exclusions, + $tracking = new TrackingSettings( $this->settings ); + $exclusions = new ExclusionSettings( $this->settings ); + $geolocation = new GeolocationSettings( $this->settings ); + $privacy = new PrivacySettings( $this->settings ); + $advanced = new AdvancedSettings( $this->settings ); + $setting_tabs = [ + self::TAB_TRACKING => $tracking, + self::TAB_ACCESS => $access_settings, + self::TAB_PRIVACY => $privacy, + self::TAB_EXCLUSIONS => $exclusions, self::TAB_GEOLOCATION => $geolocation, - self::TAB_ADVANCED => $advanced, - ); + self::TAB_ADVANCED => $advanced, + ]; $active_tab = self::TAB_TRACKING; - if ($this->settings->is_network_enabled() && !is_network_admin()){ - $active_tab = self::TAB_EXCLUSIONS; - $setting_tabs = array( + if ( $this->settings->is_network_enabled() && ! is_network_admin() ) { + $active_tab = self::TAB_EXCLUSIONS; + $setting_tabs = [ self::TAB_EXCLUSIONS => $exclusions, - self::TAB_PRIVACY => $privacy, - ); + self::TAB_PRIVACY => $privacy, + ]; } $setting_tabs = apply_filters( 'matomo_setting_tabs', $setting_tabs, $this->settings ); - if ( ! empty( $_GET['tab'] ) && isset( $setting_tabs[ $_GET['tab'] ] ) ) { - $active_tab = $_GET['tab']; + if ( ! empty( $_GET['tab'] ) ) { + $tab = sanitize_text_field( wp_unslash( $_GET['tab'] ) ); + if ( isset( $setting_tabs[ $tab ] ) ) { + $active_tab = $tab; + } } - $content_tab = $setting_tabs[ $active_tab ]; + $content_tab = $setting_tabs[ $active_tab ]; $matomo_settings = $this->settings; include dirname( __FILE__ ) . '/views/settings.php'; } - } diff --git a/classes/WpMatomo/Admin/AdvancedSettings.php b/classes/WpMatomo/Admin/AdvancedSettings.php index 4d7f95d6b..7f9c9e35f 100644 --- a/classes/WpMatomo/Admin/AdvancedSettings.php +++ b/classes/WpMatomo/Admin/AdvancedSettings.php @@ -9,7 +9,6 @@ namespace WpMatomo\Admin; -use Piwik\Config; use Piwik\IP; use WpMatomo\Bootstrap; use WpMatomo\Capabilities; @@ -19,12 +18,14 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } - +/** + * phpcs:disable WordPress.Security.NonceVerification.Missing + */ class AdvancedSettings implements AdminSettingsInterface { - const FORM_NAME = 'matomo'; - const NONCE_NAME = 'matomo_advanced'; + const FORM_NAME = 'matomo'; + const NONCE_NAME = 'matomo_advanced'; - public static $valid_host_headers = array( + public static $valid_host_headers = [ 'HTTP_CLIENT_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR', @@ -34,7 +35,7 @@ class AdvancedSettings implements AdminSettingsInterface { 'HTTP_CF_CONNECTING_IP', 'HTTP_TRUE_CLIENT_IP', 'HTTP_X_CLUSTER_CLIENT_IP', - ); + ]; /** * @var Settings @@ -50,7 +51,7 @@ class AdvancedSettings implements AdminSettingsInterface { * @param Settings $settings */ public function __construct( $settings ) { - $this->settings = $settings; + $this->settings = $settings; $this->site_config_sync = new SiteConfigSync( $settings ); } @@ -58,13 +59,27 @@ public function get_title() { return esc_html__( 'Advanced', 'matomo' ); } + public function show_settings() { + $was_updated = $this->update_if_submitted(); + + $matomo_client_headers = $this->site_config_sync->get_config_value( 'General', 'proxy_client_headers' ); + if ( empty( $matomo_client_headers ) ) { + $matomo_client_headers = []; + } + + Bootstrap::do_bootstrap(); + $matomo_detected_ip = IP::getIpFromHeader(); + $matomo_delete_all_data = $this->settings->should_delete_all_data_on_uninstall(); + + include dirname( __FILE__ ) . '/views/advanced_settings.php'; + } + private function update_if_submitted() { if ( isset( $_POST ) && ! empty( $_POST[ self::FORM_NAME ] ) && is_admin() && check_admin_referer( self::NONCE_NAME ) && $this->can_user_manage() ) { - $this->apply_settings(); return true; @@ -78,40 +93,24 @@ public function can_user_manage() { } private function apply_settings() { - if (!defined('MATOMO_REMOVE_ALL_DATA')) { - $this->settings->apply_changes(array( - Settings::DELETE_ALL_DATA_ON_UNINSTALL => !empty($_POST['matomo']['delete_all_data']) - )); - } - - $client_headers = []; - if (!empty($_POST[ self::FORM_NAME ]['proxy_client_header'])) { - $client_header = $_POST[ self::FORM_NAME ]['proxy_client_header']; - if (in_array($client_header, self::$valid_host_headers, true)) { - $client_headers[] = $client_header; - } - } - - $this->site_config_sync->set_config_value('General', 'proxy_client_headers', $client_headers); - - return true; - } - - public function show_settings() { - $was_updated = $this->update_if_submitted(); + if ( ! defined( 'MATOMO_REMOVE_ALL_DATA' ) ) { + $this->settings->apply_changes( + [ + Settings::DELETE_ALL_DATA_ON_UNINSTALL => ! empty( $_POST['matomo']['delete_all_data'] ), + ] + ); + } - $matomo_client_headers = $this->site_config_sync->get_config_value('General', 'proxy_client_headers'); - if (empty($matomo_client_headers)) { - $matomo_client_headers = array(); + $client_headers = []; + if ( ! empty( $_POST[ self::FORM_NAME ]['proxy_client_header'] ) ) { + $client_header = sanitize_text_field( wp_unslash( $_POST[ self::FORM_NAME ]['proxy_client_header'] ) ); + if ( in_array( $client_header, self::$valid_host_headers, true ) ) { + $client_headers[] = $client_header; + } } - Bootstrap::do_bootstrap(); - $matomo_detected_ip = IP::getIpFromHeader(); - $matomo_delete_all_data = $this->settings->should_delete_all_data_on_uninstall(); + $this->site_config_sync->set_config_value( 'General', 'proxy_client_headers', $client_headers ); - include dirname( __FILE__ ) . '/views/advanced_settings.php'; + return true; } - - - } diff --git a/classes/WpMatomo/Admin/Chart.php b/classes/WpMatomo/Admin/Chart.php new file mode 100644 index 000000000..1303f26c0 --- /dev/null +++ b/classes/WpMatomo/Admin/Chart.php @@ -0,0 +1,20 @@ + __( 'None', 'matomo' ), - self::REQUIRE_COOKIE_CONSENT => __('Require cookie consent', 'matomo'), - self::REQUIRE_TRACKING_CONSENT => __('Require tracking consent', 'matomo') + self::REQUIRE_NONE => __( 'None', 'matomo' ), + self::REQUIRE_COOKIE_CONSENT => __( 'Require cookie consent', 'matomo' ), + self::REQUIRE_TRACKING_CONSENT => __( 'Require tracking consent', 'matomo' ), ]; - } + /** * @param string $tracking_mode - * @see CookieConsent::REQUIRE_COOKIE_CONSENT + * + * @return string * @see CookieConsent::REQUIRE_NONE * @see CookieConsent::REQUIRE_TRACKING_CONSENT - * @return string + * @see CookieConsent::REQUIRE_COOKIE_CONSENT */ public function get_tracking_consent_option( $tracking_mode ) { - switch( $tracking_mode ) { + switch ( $tracking_mode ) { case self::REQUIRE_TRACKING_CONSENT: $tracking_code = <<get_widgets(); - if (!empty($widgets) && is_array($widgets) && current_user_can(Capabilities::KEY_VIEW)) { - foreach ($widgets as $widget) { - - try { - - $widget_meta = $this->is_valid_widget($widget['unique_id'], $widget['date']); - if (!empty($widget_meta['report']['name'])) { - $id = 'matomo_dashboard_widget_' . $widget['unique_id'] . '_' . $widget['date']; - - $title = $widget_meta['report']['name'] . ' - ' . $widget_meta['date'] . ' - Matomo'; - wp_add_dashboard_widget( $id, esc_html($title), function () use ($widget) { - $renderer = new Renderer(); - echo $renderer->show_report(array( - 'unique_id' => $widget['unique_id'], - 'report_date' => $widget['date'], - 'limit' => 10, - )); - }); - } - } catch (\Exception $e) { - // dont want to break dashboard if there is any issue with matomo ... eg in case bootstrap fails - // or is reinstalled but matomo not yet fully installed etc - $logger = new Logger(); - $logger->log(sprintf('Failed to add Matomo widget %s to dashboard: %s', wp_json_encode($widget), $e->getMessage())); - } + if ( ! empty( $widgets ) && is_array( $widgets ) && current_user_can( Capabilities::KEY_VIEW ) ) { + do_action( 'matomo_load_chartjs' ); + foreach ( $widgets as $widget ) { + try { + $widget_meta = $this->is_valid_widget( $widget['unique_id'], $widget['date'] ); + if ( ! empty( $widget_meta['report']['name'] ) ) { + $id = 'matomo_dashboard_widget_' . $widget['unique_id'] . '_' . $widget['date']; + + $title = $widget_meta['report']['name'] . ' - ' . $widget_meta['date'] . ' - Matomo'; + + wp_add_dashboard_widget( + $id, + esc_html( $title ), + function () use ( $widget ) { + $renderer = new Renderer(); + // do not escape the content, we want the HTML + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $renderer->show_report( + [ + 'unique_id' => $widget['unique_id'], + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + 'report_date' => $widget['date'], + 'limit' => 10, + ] + ); + } + ); + } + } catch ( Exception $e ) { + // dont want to break dashboard if there is any issue with matomo ... eg in case bootstrap fails + // or is reinstalled but matomo not yet fully installed etc + $logger = new Logger(); + $logger->log( sprintf( 'Failed to add Matomo widget %s to dashboard: %s', wp_json_encode( $widget ), $e->getMessage() ) ); + } } } } - public function is_valid_widget( $unique_id, $date ) - { - if (empty($unique_id) || empty($date)) { + public function get_widgets() { + $meta = get_user_meta( get_current_user_id(), self::DASHBOARD_USER_OPTION, true ); + if ( empty( $meta ) ) { + $meta = []; + } + + return $meta; + } + + public function is_valid_widget( $unique_id, $date ) { + if ( empty( $unique_id ) || empty( $date ) ) { return false; } $metadata = new Metadata(); - $report = $metadata->find_report_by_unique_id( $unique_id ); + $report = $metadata->find_report_by_unique_id( $unique_id ); - if (empty($report)) { + if ( empty( $report ) ) { return false; } $report_dates_obj = new Dates(); $report_dates = $report_dates_obj->get_supported_dates(); - if (empty($report_dates[$date])) { + if ( empty( $report_dates[ $date ] ) ) { return false; } - return array('report' => $report, 'date' => $report_dates[$date]); + return [ + 'report' => $report, + 'date' => $report_dates[ $date ], + ]; } - public function has_widget($report_unique_id, $report_date) - { + public function has_widget( $report_unique_id, $report_date ) { $widgets = $this->get_widgets(); - foreach ($widgets as $index => $widget) { - if ($widget['unique_id'] === $report_unique_id && $widget['date'] === $report_date) { + foreach ( $widgets as $index => $widget ) { + if ( $widget['unique_id'] === $report_unique_id && $widget['date'] === $report_date ) { return true; } } + return false; } - public function toggle_widget($report_unique_id, $report_date) - { + public function toggle_widget( $report_unique_id, $report_date ) { $widgets = $this->get_widgets(); - foreach ($widgets as $index => $widget) { - if ($widget['unique_id'] === $report_unique_id && $widget['date'] === $report_date) { - unset($widgets[$index]); - $this->set_widgets(array_values($widgets)); + foreach ( $widgets as $index => $widget ) { + if ( $widget['unique_id'] === $report_unique_id && $widget['date'] === $report_date ) { + unset( $widgets[ $index ] ); + $this->set_widgets( array_values( $widgets ) ); + return; } } - $widgets[] = array('unique_id' => $report_unique_id, 'date' => $report_date); - - $this->set_widgets($widgets); - } + $widgets[] = [ + 'unique_id' => $report_unique_id, + 'date' => $report_date, + ]; - public function get_widgets() - { - $meta = get_user_meta(get_current_user_id(), self::DASHBOARD_USER_OPTION, true); - if (empty($meta)) { - $meta = array(); - } - return $meta; + $this->set_widgets( $widgets ); } - private function set_widgets($widgets) - { - update_user_meta(get_current_user_id(),self::DASHBOARD_USER_OPTION, $widgets); + private function set_widgets( $widgets ) { + update_user_meta( get_current_user_id(), self::DASHBOARD_USER_OPTION, $widgets ); } public function uninstall() { - Uninstaller::uninstall_user_meta(self::DASHBOARD_USER_OPTION); + Uninstaller::uninstall_user_meta( self::DASHBOARD_USER_OPTION ); } } diff --git a/classes/WpMatomo/Admin/ExclusionSettings.php b/classes/WpMatomo/Admin/ExclusionSettings.php index 7af126e3d..bc8d76d60 100644 --- a/classes/WpMatomo/Admin/ExclusionSettings.php +++ b/classes/WpMatomo/Admin/ExclusionSettings.php @@ -36,6 +36,24 @@ public function get_title() { return esc_html__( 'Exclusions', 'matomo' ); } + public function show_settings() { + global $wp_roles; + + $was_updated = $this->update_if_submitted(); + + Bootstrap::do_bootstrap(); + + $api = API::getInstance(); + $excluded_ips = $this->from_comma_list( $api->getExcludedIpsGlobal() ); + $excluded_query_params = $this->from_comma_list( $api->getExcludedQueryParametersGlobal() ); + $excluded_user_agents = $this->from_comma_list( $api->getExcludedUserAgentsGlobal() ); + $keep_url_fragments = $api->getKeepURLFragmentsGlobal(); + $current_ip = $this->get_current_ip(); + $settings = $this->settings; + + include dirname( __FILE__ ) . '/views/exclusion_settings.php'; + } + private function update_if_submitted() { if ( isset( $_POST ) && ! empty( $_POST[ self::FORM_NAME ] ) @@ -43,8 +61,8 @@ private function update_if_submitted() { && check_admin_referer( self::NONCE_NAME ) && current_user_can( Capabilities::KEY_SUPERUSER ) ) { Bootstrap::do_bootstrap(); - - $post = $_POST[ self::FORM_NAME ]; + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $post = wp_unslash( $_POST[ self::FORM_NAME ] ); $api = API::getInstance(); if ( isset( $post['excluded_ips'] ) ) { @@ -69,11 +87,12 @@ private function update_if_submitted() { } $keep_fragments = ! empty( $post['keep_url_fragments'] ); + // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison if ( $keep_fragments != $api->getKeepURLFragmentsGlobal() ) { $api->setKeepURLFragmentsGlobal( $keep_fragments ); } - $setting_values = array( Settings::OPTION_KEY_STEALTH => array() ); + $setting_values = [ Settings::OPTION_KEY_STEALTH => [] ]; if ( ! empty( $post[ Settings::OPTION_KEY_STEALTH ] ) ) { $setting_values[ Settings::OPTION_KEY_STEALTH ] = $post[ Settings::OPTION_KEY_STEALTH ]; } @@ -104,24 +123,12 @@ private function from_comma_list( $value ) { return implode( "\n", array_filter( explode( ',', $value ) ) ); } - public function show_settings() { - global $wp_roles; - - $was_updated = $this->update_if_submitted(); - - Bootstrap::do_bootstrap(); - - $api = API::getInstance(); - $excluded_ips = $this->from_comma_list( $api->getExcludedIpsGlobal() ); - $excluded_query_params = $this->from_comma_list( $api->getExcludedQueryParametersGlobal() ); - $excluded_user_agents = $this->from_comma_list( $api->getExcludedUserAgentsGlobal() ); - $keep_url_fragments = $api->getKeepURLFragmentsGlobal(); - $current_ip = $this->get_current_ip(); - $settings = $this->settings; - - include dirname( __FILE__ ) . '/views/exclusion_settings.php'; - } - + /** + * do not sanitize $_SERVER variables + * phpcs:disable WordPress.Security.ValidatedSanitizedInput + * + * @return mixed|string + */ private function get_current_ip() { if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) { $ip = $_SERVER['HTTP_CLIENT_IP']; @@ -133,5 +140,4 @@ private function get_current_ip() { return $ip; } - } diff --git a/classes/WpMatomo/Admin/GeolocationSettings.php b/classes/WpMatomo/Admin/GeolocationSettings.php index da5079766..bce750c02 100644 --- a/classes/WpMatomo/Admin/GeolocationSettings.php +++ b/classes/WpMatomo/Admin/GeolocationSettings.php @@ -19,7 +19,7 @@ class GeolocationSettings implements AdminSettingsInterface { const NONCE_NAME = 'matomo_geolocation'; - const FORM_NAME = 'matomo_maxmind_license'; + const FORM_NAME = 'matomo_maxmind_license'; /** * @var Settings @@ -34,24 +34,33 @@ public function get_title() { return esc_html__( 'Geolocation', 'matomo' ); } + public function show_settings() { + $invalid_format = $this->update_if_submitted() === false; + + $current_maxmind_license = $this->settings->get_global_option( 'maxmind_license_key' ); + + include dirname( __FILE__ ) . '/views/geolocation_settings.php'; + } + private function update_if_submitted() { if ( isset( $_POST ) && isset( $_POST[ self::FORM_NAME ] ) && is_admin() && check_admin_referer( self::NONCE_NAME ) && current_user_can( Capabilities::KEY_SUPERUSER ) ) { + $maxmind_license = trim( stripslashes( sanitize_text_field( wp_unslash( $_POST[ self::FORM_NAME ] ) ) ) ); - $maxmind_license = trim(stripslashes($_POST[ self::FORM_NAME ])); - - if (empty($maxmind_license)) { + if ( empty( $maxmind_license ) ) { $maxmind_license = ''; - } elseif (strlen($maxmind_license) > 20 || strlen($maxmind_license) < 7 || !ctype_graph($maxmind_license)) { + } elseif ( strlen( $maxmind_license ) > 20 || strlen( $maxmind_license ) < 7 || ! ctype_graph( $maxmind_license ) ) { return false; } - $this->settings->apply_changes(array( - 'maxmind_license_key' => $maxmind_license - )); + $this->settings->apply_changes( + [ + 'maxmind_license_key' => $maxmind_license, + ] + ); // update geoip in the backgronud wp_schedule_single_event( time() + 10, ScheduledTasks::EVENT_GEOIP ); @@ -59,13 +68,4 @@ private function update_if_submitted() { return true; } } - - public function show_settings() { - $invalid_format = $this->update_if_submitted() === false; - - $current_maxmind_license = $this->settings->get_global_option('maxmind_license_key'); - - include dirname( __FILE__ ) . '/views/geolocation_settings.php'; - } - } diff --git a/classes/WpMatomo/Admin/GetStarted.php b/classes/WpMatomo/Admin/GetStarted.php index 68786103a..980c8260d 100644 --- a/classes/WpMatomo/Admin/GetStarted.php +++ b/classes/WpMatomo/Admin/GetStarted.php @@ -31,6 +31,15 @@ public function __construct( $settings ) { $this->settings = $settings; } + public function show() { + $was_updated = $this->update_if_submitted(); + $settings = $this->settings; + $can_user_edit = $this->can_user_manage(); + $show_this_page = $this->settings->get_global_option( Settings::SHOW_GET_STARTED_PAGE ); + + include dirname( __FILE__ ) . '/views/get_started.php'; + } + private function update_if_submitted() { if ( isset( $_POST ) && ! empty( $_POST[ self::FORM_NAME ] ) @@ -40,16 +49,16 @@ private function update_if_submitted() { if ( ! empty( $_POST[ self::FORM_NAME ][ Settings::SHOW_GET_STARTED_PAGE ] ) && 'no' === $_POST[ self::FORM_NAME ][ Settings::SHOW_GET_STARTED_PAGE ] ) { $this->settings->apply_changes( - array( + [ Settings::SHOW_GET_STARTED_PAGE => 0, - ) + ] ); return true; } if ( ! empty( $_POST[ self::FORM_NAME ]['track_mode'] ) && TrackingSettings::TRACK_MODE_DEFAULT === $_POST[ self::FORM_NAME ]['track_mode'] ) { - $this->settings->apply_tracking_related_changes( array( 'track_mode' => TrackingSettings::TRACK_MODE_DEFAULT ) ); + $this->settings->apply_tracking_related_changes( [ 'track_mode' => TrackingSettings::TRACK_MODE_DEFAULT ] ); return true; } @@ -63,15 +72,4 @@ public function can_user_manage() { return $tracking_settings->can_user_manage(); } - - public function show() { - $was_updated = $this->update_if_submitted(); - $settings = $this->settings; - $can_user_edit = $this->can_user_manage(); - $show_this_page = $this->settings->get_global_option( Settings::SHOW_GET_STARTED_PAGE ); - - include dirname( __FILE__ ) . '/views/get_started.php'; - } - - } diff --git a/classes/WpMatomo/Admin/Info.php b/classes/WpMatomo/Admin/Info.php index 9af0ca208..b9804dfee 100644 --- a/classes/WpMatomo/Admin/Info.php +++ b/classes/WpMatomo/Admin/Info.php @@ -21,48 +21,50 @@ class Info { private function update_if_submitted() { if ( isset( $_POST ) - && !empty( $_POST[ self::FORM_NAME ] ) - && is_admin() - && check_admin_referer( self::NONCE_NAME ) - && $this->show_newsletter_signup() - && current_user_can( Capabilities::KEY_VIEW ) ) { - - $user = wp_get_current_user(); - $locale = explode('_', get_user_locale($user->ID)); - wp_remote_get('https://api.matomo.org/1.0/subscribeNewsletter/?' . http_build_query(array( - 'email' => $user->user_email, - 'wordpress' => 1, - 'language' => $locale[0], - ))); - update_user_meta($user->ID, self::FORM_NAME, '1'); + && ! empty( $_POST[ self::FORM_NAME ] ) + && is_admin() + && check_admin_referer( self::NONCE_NAME ) + && $this->show_newsletter_signup() + && current_user_can( Capabilities::KEY_VIEW ) ) { + $user = wp_get_current_user(); + $locale = explode( '_', get_user_locale( $user->ID ) ); + wp_remote_get( + 'https://api.matomo.org/1.0/subscribeNewsletter/?' . http_build_query( + [ + 'email' => $user->user_email, + 'wordpress' => 1, + 'language' => $locale[0], + ] + ) + ); + update_user_meta( $user->ID, self::FORM_NAME, '1' ); return true; } } private function show_newsletter_signup() { - if (!is_user_logged_in()) { + if ( ! is_user_logged_in() ) { return false; } $user = wp_get_current_user(); - return !get_user_meta($user->ID, self::FORM_NAME, true); + + return ! get_user_meta( $user->ID, self::FORM_NAME, true ); } public function show() { - $this->render('info'); + $this->render( 'info' ); } public function show_multisite() { - $this->render('info_multisite'); + $this->render( 'info_multisite' ); } - private function render($template) { + private function render( $template ) { $signedup_newsletter = $this->update_if_submitted(); $show_newsletter = $this->show_newsletter_signup(); include dirname( __FILE__ ) . '/views/' . $template . '.php'; } - - } diff --git a/classes/WpMatomo/Admin/Marketplace.php b/classes/WpMatomo/Admin/Marketplace.php index 56ca5ce44..3542e2d26 100644 --- a/classes/WpMatomo/Admin/Marketplace.php +++ b/classes/WpMatomo/Admin/Marketplace.php @@ -31,5 +31,4 @@ public function show() { include dirname( __FILE__ ) . '/views/marketplace.php'; } - } diff --git a/classes/WpMatomo/Admin/Menu.php b/classes/WpMatomo/Admin/Menu.php index 5dab0ab79..6d46f3811 100644 --- a/classes/WpMatomo/Admin/Menu.php +++ b/classes/WpMatomo/Admin/Menu.php @@ -52,15 +52,15 @@ class Menu { public function __construct( $settings ) { $this->settings = $settings; // Hook for adding admin menus - add_action( 'admin_menu', array( $this, 'add_menu' ) ); - add_action( 'network_admin_menu', array( $this, 'add_menu' ) ); - add_action( 'admin_head', array( $this, 'menu_external_icons' ) ); + add_action( 'admin_menu', [ $this, 'add_menu' ] ); + add_action( 'network_admin_menu', [ $this, 'add_menu' ] ); + add_action( 'admin_head', [ $this, 'menu_external_icons' ] ); // as we are redirecting we need to perform the redirect as soon as possible before WP has eg echoed the header - add_action( 'load-matomo-analytics_page_' . self::SLUG_REPORTING, array( $this, 'reporting' ) ); - add_action( 'load-' . self::$parent_slug . '_page_' . self::SLUG_REPORTING, array( $this, 'reporting' ) ); - add_action( 'load-matomo-analytics_page_' . self::SLUG_TAGMANAGER, array( $this, 'tagmanager' ) ); - add_action( 'load-' . self::$parent_slug . '_page_' . self::SLUG_TAGMANAGER, array( $this, 'tagmanager' ) ); + add_action( 'load-matomo-analytics_page_' . self::SLUG_REPORTING, [ $this, 'reporting' ] ); + add_action( 'load-' . self::$parent_slug . '_page_' . self::SLUG_REPORTING, [ $this, 'reporting' ] ); + add_action( 'load-matomo-analytics_page_' . self::SLUG_TAGMANAGER, [ $this, 'tagmanager' ] ); + add_action( 'load-' . self::$parent_slug . '_page_' . self::SLUG_TAGMANAGER, [ $this, 'tagmanager' ] ); } public function add_menu() { @@ -75,19 +75,19 @@ public function add_menu() { add_menu_page( 'Matomo Analytics', 'Matomo Analytics', self::CAP_NOT_EXISTS, 'matomo', null, 'dashicons-analytics' ); if ( $this->settings->get_global_option( Settings::SHOW_GET_STARTED_PAGE ) && $get_started->can_user_manage() ) { - if (!is_multisite() || !is_network_admin()) { - add_submenu_page( - self::$parent_slug, - __( 'Get Started', 'matomo' ), - __( 'Get Started', 'matomo' ), - Capabilities::KEY_SUPERUSER, - self::SLUG_GET_STARTED, - array( - $get_started, - 'show', - ) - ); - } + if ( ! is_multisite() || ! is_network_admin() ) { + add_submenu_page( + self::$parent_slug, + __( 'Get Started', 'matomo' ), + __( 'Get Started', 'matomo' ), + Capabilities::KEY_SUPERUSER, + self::SLUG_GET_STARTED, + [ + $get_started, + 'show', + ] + ); + } } if ( is_network_admin() ) { @@ -97,10 +97,10 @@ public function add_menu() { __( 'Multi Site', 'matomo' ), Capabilities::KEY_SUPERUSER, 'matomo-multisite', - array( + [ $info, 'show_multisite', - ) + ] ); } else { add_submenu_page( @@ -109,10 +109,10 @@ public function add_menu() { __( 'Summary', 'matomo' ), Capabilities::KEY_VIEW, self::SLUG_REPORT_SUMMARY, - array( + [ $summary, 'show', - ) + ] ); // the network itself is not a blog @@ -122,30 +122,29 @@ public function add_menu() { __( 'Reporting', 'matomo' ), Capabilities::KEY_VIEW, self::SLUG_REPORTING, - array( + [ $this, 'reporting', - ) + ] ); // the network itself is not a blog - if ( matomo_has_tag_manager() ) { - add_submenu_page( - self::$parent_slug, - __( 'Tag Manager', 'matomo' ), - __( 'Tag Manager', 'matomo' ), - Capabilities::KEY_WRITE, - self::SLUG_TAGMANAGER, - array( - $this, - 'tagmanager', - ) - ); - } - + if ( matomo_has_tag_manager() ) { + add_submenu_page( + self::$parent_slug, + __( 'Tag Manager', 'matomo' ), + __( 'Tag Manager', 'matomo' ), + Capabilities::KEY_WRITE, + self::SLUG_TAGMANAGER, + [ + $this, + 'tagmanager', + ] + ); + } } - // we always show settings except when multi site is used, plugin is not network enabled, and we are in network admin - $can_matomo_be_managed = ( !is_multisite() || $this->settings->is_network_enabled() || !is_network_admin() ); + // we always show settings except when multi site is used, plugin is not network enabled, and we are in network admin + $can_matomo_be_managed = ( ! is_multisite() || $this->settings->is_network_enabled() || ! is_network_admin() ); if ( $can_matomo_be_managed ) { add_submenu_page( @@ -154,10 +153,10 @@ public function add_menu() { __( 'Settings', 'matomo' ), Capabilities::KEY_SUPERUSER, self::SLUG_SETTINGS, - array( + [ $admin_settings, 'show', - ) + ] ); } @@ -168,10 +167,10 @@ public function add_menu() { __( 'Marketplace', 'matomo' ), Capabilities::KEY_VIEW, self::SLUG_MARKETPLACE, - array( + [ $marketplace, 'show', - ) + ] ); } @@ -182,10 +181,10 @@ public function add_menu() { __( 'Diagnostics', 'matomo' ), Capabilities::KEY_SUPERUSER, self::SLUG_SYSTEM_REPORT, - array( + [ $system_report, 'show', - ) + ] ); } @@ -195,10 +194,10 @@ public function add_menu() { __( 'About', 'matomo' ), Capabilities::KEY_VIEW, self::SLUG_ABOUT, - array( + [ $info, 'show', - ) + ] ); } @@ -210,6 +209,8 @@ public function menu_external_icons() { $tagmanager = __( 'Tag Manager', 'matomo' ); foreach ( $submenu[ self::$parent_slug ] as $key => $menu_item ) { if ( 0 === strpos( $menu_item[0], $reporting ) || 0 === strpos( $menu_item[0], $tagmanager ) ) { + // No other choice + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $submenu[ self::$parent_slug ][ $key ][0] .= ' '; } } @@ -217,7 +218,7 @@ public function menu_external_icons() { } public static function get_matomo_goto_url( $goto ) { - return add_query_arg( array( 'goto' => $goto ), menu_page_url( self::SLUG_REPORTING, false ) ); + return add_query_arg( [ 'goto' => $goto ], menu_page_url( self::SLUG_REPORTING, false ) ); } public static function get_reporting_url() { @@ -233,7 +234,7 @@ public function tagmanager() { public function reporting() { if ( ! empty( $_GET['goto'] ) ) { - switch ( $_GET['goto'] ) { + switch ( sanitize_text_field( wp_unslash( $_GET['goto'] ) ) ) { case self::REPORTING_GOTO_ADMIN: $this->go_to_matomo_page( 'CoreAdminHome', 'home', Capabilities::KEY_SUPERUSER ); break; @@ -264,26 +265,26 @@ public function reporting() { $idsite = $site->get_current_matomo_site_id(); if ( $idsite ) { - $url = add_query_arg( array( 'idSite' => (int) $idsite ), $url ); + $url = add_query_arg( [ 'idSite' => (int) $idsite ], $url ); } if ( ! empty( $_GET['report_date'] ) ) { - $url = add_query_arg( - array( + $report_date = sanitize_text_field( wp_unslash( $_GET['report_date'] ) ); + $url = add_query_arg( + [ 'module' => 'CoreHome', 'action' => 'index', - ), + ], $url ); - $date = new Dates(); - list( $period, $date ) = $date->detect_period_and_date( $_GET['report_date'] ); + list( $period, $date ) = $date->detect_period_and_date( $report_date ); $url = add_query_arg( - array( + [ 'period' => $period, 'date' => $date, - ), + ], $url ); } @@ -295,7 +296,7 @@ public function reporting() { /** * @api */ - public static function get_matomo_reporting_url( $category, $subcategory, $params = array() ) { + public static function get_matomo_reporting_url( $category, $subcategory, $params = [] ) { $site = new Site(); $idsite = $site->get_current_matomo_site_id(); @@ -330,7 +331,7 @@ private static function make_matomo_app_base_url() { /** * @api */ - public static function get_matomo_action_url( $module, $action, $params = array() ) { + public static function get_matomo_action_url( $module, $action, $params = [] ) { $site = new Site(); $idsite = $site->get_current_matomo_site_id(); @@ -372,5 +373,4 @@ public function go_to_matomo_page( $module, $action, $cap ) { wp_safe_redirect( $url ); exit; } - } diff --git a/classes/WpMatomo/Admin/PrivacySettings.php b/classes/WpMatomo/Admin/PrivacySettings.php index ce7073080..e25720dd5 100644 --- a/classes/WpMatomo/Admin/PrivacySettings.php +++ b/classes/WpMatomo/Admin/PrivacySettings.php @@ -19,21 +19,21 @@ class PrivacySettings implements AdminSettingsInterface { const EXAMPLE_MINIMAL = '[matomo_opt_out]'; const EXAMPLE_FULL = '[matomo_opt_out language=de]'; - /** - * @var Settings - */ - private $settings; + /** + * @var Settings + */ + private $settings; - public function __construct( Settings $settings ) { - $this->settings = $settings; - } + public function __construct( Settings $settings ) { + $this->settings = $settings; + } public function get_title() { return esc_html__( 'Privacy & GDPR', 'matomo' ); } public function show_settings() { - $matomo_settings = $this->settings; + $matomo_settings = $this->settings; include dirname( __FILE__ ) . '/views/privacy_gdpr.php'; } diff --git a/classes/WpMatomo/Admin/SafeModeMenu.php b/classes/WpMatomo/Admin/SafeModeMenu.php index e3f56c704..b03eaa831 100644 --- a/classes/WpMatomo/Admin/SafeModeMenu.php +++ b/classes/WpMatomo/Admin/SafeModeMenu.php @@ -9,6 +9,7 @@ namespace WpMatomo\Admin; +use WpMatomo; use WpMatomo\Settings; if ( ! defined( 'ABSPATH' ) ) { @@ -28,12 +29,12 @@ class SafeModeMenu { */ public function __construct( $settings ) { $this->settings = $settings; - add_action( 'admin_menu', array( $this, 'add_menu' ) ); - add_action( 'network_admin_menu', array( $this, 'add_menu' ) ); + add_action( 'admin_menu', [ $this, 'add_menu' ] ); + add_action( 'network_admin_menu', [ $this, 'add_menu' ] ); } public function add_menu() { - if ( ! \WpMatomo::is_admin_user() ) { + if ( ! WpMatomo::is_admin_user() ) { return; } @@ -47,11 +48,10 @@ public function add_menu() { __( 'System Report', 'matomo' ), 'administrator', Menu::SLUG_SYSTEM_REPORT, - array( + [ $system_report, 'show', - ) + ] ); } - } diff --git a/classes/WpMatomo/Admin/Summary.php b/classes/WpMatomo/Admin/Summary.php index 9e031ffb8..499f7246d 100644 --- a/classes/WpMatomo/Admin/Summary.php +++ b/classes/WpMatomo/Admin/Summary.php @@ -14,13 +14,13 @@ use WpMatomo\Report\Metadata; use WpMatomo\Report\Renderer; use WpMatomo\Settings; +use Piwik\Plugins\UsersManager\UserPreferences; if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } class Summary { - const NONCE_DASHBOARD = 'matomo_pin_dashboard'; /** @@ -36,20 +36,19 @@ public function __construct( $settings ) { } private function pin_if_submitted() { - if ( ! empty( $_GET[ 'pin' ] ) - && ! empty( $_GET[ 'report_uniqueid' ] ) - && ! empty( $_GET[ 'report_date' ] ) - && is_admin() - && check_admin_referer( self::NONCE_DASHBOARD ) - && is_user_logged_in() - && current_user_can( Capabilities::KEY_VIEW ) ) { - $unique_id = $_GET[ 'report_uniqueid' ]; - $date = $_GET[ 'report_date' ]; - + if ( ! empty( $_GET['pin'] ) + && ! empty( $_GET['report_uniqueid'] ) + && ! empty( $_GET['report_date'] ) + && is_admin() + && check_admin_referer( self::NONCE_DASHBOARD ) + && is_user_logged_in() + && current_user_can( Capabilities::KEY_VIEW ) ) { + $unique_id = sanitize_text_field( wp_unslash( $_GET['report_uniqueid'] ) ); + $date = sanitize_text_field( wp_unslash( $_GET['report_date'] ) ); $dashobard = new Dashboard(); - if ($dashobard->is_valid_widget($unique_id, $date)) { + if ( $dashobard->is_valid_widget( $unique_id, $date ) ) { $dashobard->toggle_widget( $unique_id, $date ); - return true; + return true; } } @@ -57,6 +56,8 @@ private function pin_if_submitted() { } public function show() { + do_action( 'matomo_load_chartjs' ); + $matomo_pinned = $this->pin_if_submitted(); $settings = $this->settings; @@ -67,9 +68,42 @@ public function show() { $report_dates_obj = new Dates(); $report_dates = $report_dates_obj->get_supported_dates(); - $report_date = Dates::YESTERDAY; + $user_preference = new UserPreferences(); + $default_date = $user_preference->getDefaultDate(); + $report_period = $user_preference->getDefaultPeriod(); + switch ( $report_period ) { + case 'day': + $report_date = $default_date; + break; + case 'year': + case 'month': + case 'week': + switch ( $default_date ) { + case 'yesterday': + $report_date = 'last' . $report_period; + break; + case 'today': + $report_date = 'this' . $report_period; + break; + } + break; + case 'range': + switch ( $default_date ) { + case 'previous30': + $report_date = 'lastmonth'; + break; + case 'previous7': + $report_date = 'lastweek'; + break; + case 'last30': + $report_date = 'thismonth'; + break; + case 'last7': + $report_date = 'thisweek'; + } + } if ( isset( $_GET['report_date'] ) && isset( $report_dates[ $_GET['report_date'] ] ) ) { - $report_date = $_GET['report_date']; + $report_date = sanitize_text_field( wp_unslash( $_GET['report_date'] ) ); } list( $report_period_selected, $report_date_selected ) = $report_dates_obj->detect_period_and_date( $report_date ); @@ -78,22 +112,24 @@ public function show() { $matomo_dashboard = new Dashboard(); - $wp_version = get_bloginfo( 'version' ); - $matomo_is_version_pre55 = empty($wp_version) || version_compare($wp_version, '5.5.0') === -1; + $wp_version = get_bloginfo( 'version' ); + $matomo_is_version_pre55 = empty( $wp_version ) || version_compare( $wp_version, '5.5.0' ) === - 1; include dirname( __FILE__ ) . '/views/summary.php'; } private function get_reports_to_show() { - $reports_to_show = array( + $reports_to_show = [ + Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, 'VisitsSummary_get', 'UserCountry_getCountry', + 'Actions_get', 'DevicesDetection_getType', + 'Goals_get', 'Resolution_getResolution', 'DevicesDetection_getOsFamilies', 'DevicesDetection_getBrowsers', 'VisitTime_getVisitInformationPerServerTime', - 'Actions_get', 'Actions_getPageTitles', 'Actions_getEntryPageTitles', 'Actions_getExitPageTitles', @@ -102,18 +138,16 @@ private function get_reports_to_show() { 'Referrers_getAll', 'Referrers_getSocials', 'Referrers_getCampaigns', - 'Goals_get', - ); + ]; if ( $this->settings->get_global_option( 'track_ecommerce' ) ) { $reports_to_show[] = 'Goals_get_idGoal--ecommerceOrder'; $reports_to_show[] = 'Goals_getItemsName'; } - $reports_to_show[] = Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME; $reports_to_show = apply_filters( 'matomo_report_summary_report_ids', $reports_to_show ); - $report_metadata = array(); + $report_metadata = []; $metadata = new Metadata(); foreach ( $reports_to_show as $report_unique_id ) { $report = $metadata->find_report_by_unique_id( $report_unique_id ); @@ -128,5 +162,4 @@ private function get_reports_to_show() { return $report_metadata; } - } diff --git a/classes/WpMatomo/Admin/SystemReport.php b/classes/WpMatomo/Admin/SystemReport.php index 4ea0b21cc..cdfd52900 100644 --- a/classes/WpMatomo/Admin/SystemReport.php +++ b/classes/WpMatomo/Admin/SystemReport.php @@ -9,6 +9,8 @@ namespace WpMatomo\Admin; +use Exception; +use ITSEC_Modules; use Piwik\CliMulti; use Piwik\Common; use Piwik\Config; @@ -23,6 +25,7 @@ use Piwik\SettingsPiwik; use Piwik\Tracker\Failures; use Piwik\Version; +use WpMatomo; use WpMatomo\Bootstrap; use WpMatomo\Capabilities; use WpMatomo\Installer; @@ -39,6 +42,20 @@ exit; // if accessed directly } +/** + * error_reporting is required for this page + * phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting + * + * We want a real data, not something coming from cache + * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + * + * This is a report error, so silent the possible errors + * phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged + * + * We cannot use parameters of statements as this is the table names we build + * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + * phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + */ class SystemReport { const NONCE_NAME = 'matomo_troubleshooting'; const TROUBLESHOOT_SYNC_USERS = 'matomo_troubleshooting_action_site_users'; @@ -51,21 +68,32 @@ class SystemReport { const TROUBLESHOOT_CLEAR_LOGS = 'matomo_troubleshooting_action_clear_logs'; const TROUBLESHOOT_RUN_UPDATER = 'matomo_troubleshooting_action_run_updater'; - private $not_compatible_plugins = array( - 'background-manager', // Uses an old version of Twig and plugin is no longer maintained. - 'all-in-one-event-calendar', // Uses an old version of Twig - 'data-tables-generator-by-supsystic', // uses an old version of twig causing some styles to go funny in the reporting and admin - 'tweet-old-post-pro', // uses a newer version of monolog - 'wp-rss-aggregator', // see https://wordpress.org/support/topic/critical-error-after-upgrade/ conflict re php-di version - 'wp-defender', // see https://wordpress.org/support/topic/critical-error-after-upgrade/ conflict re php-di version - 'age-verification-for-woocommerce', // see https://github.com/matomo-org/wp-matomo/issues/428 - 'minify-html-markup', // see https://wordpress.org/support/topic/graphs-are-not-displayed-in-the-visits-overview-widget/#post-14298068 - 'bigbuy-wc-dropshipping-connector', // see https://wordpress.org/support/topic/20-total-errors-during-this-script-execution/ - 'google-listings-and-ads', // see https://wordpress.org/support/topic/20-total-errors-during-this-script-execution/ - 'accelerated-mobile-pages' // see https://wordpress.org/support/topic/receiving-errors-from-my-plesk-server/ - ); - - private $valid_tabs = array( 'troubleshooting' ); + private $not_compatible_plugins = [ + 'background-manager', + // Uses an old version of Twig and plugin is no longer maintained. + 'all-in-one-event-calendar', + // Uses an old version of Twig + 'data-tables-generator-by-supsystic', + // uses an old version of twig causing some styles to go funny in the reporting and admin + 'tweet-old-post-pro', + // uses a newer version of monolog + 'wp-rss-aggregator', + // see https://wordpress.org/support/topic/critical-error-after-upgrade/ conflict re php-di version + 'wp-defender', + // see https://wordpress.org/support/topic/critical-error-after-upgrade/ conflict re php-di version + 'age-verification-for-woocommerce', + // see https://github.com/matomo-org/wp-matomo/issues/428 + 'minify-html-markup', + // see https://wordpress.org/support/topic/graphs-are-not-displayed-in-the-visits-overview-widget/#post-14298068 + 'bigbuy-wc-dropshipping-connector', + // see https://wordpress.org/support/topic/20-total-errors-during-this-script-execution/ + 'google-listings-and-ads', + // see https://wordpress.org/support/topic/20-total-errors-during-this-script-execution/ + 'accelerated-mobile-pages', + // see https://wordpress.org/support/topic/receiving-errors-from-my-plesk-server/ + ]; + + private $valid_tabs = [ 'troubleshooting' ]; /** * @var Settings @@ -81,12 +109,12 @@ class SystemReport { /** * @var \WpMatomo\Db\Settings */ - public $dbSettings; + public $db_settings; public function __construct( Settings $settings ) { - $this->settings = $settings; - $this->logger = new Logger(); - $this->dbSettings = new \WpMatomo\Db\Settings(); + $this->settings = $settings; + $this->logger = new Logger(); + $this->db_settings = new \WpMatomo\Db\Settings(); } public function get_not_compatible_plugins() { @@ -102,36 +130,38 @@ private function execute_troubleshoot_if_needed() { Bootstrap::do_bootstrap(); $scheduled_tasks = new ScheduledTasks( $this->settings ); - if (!defined('PIWIK_ARCHIVE_NO_TRUNCATE')) { - define('PIWIK_ARCHIVE_NO_TRUNCATE', 1); // when triggering it manually, we prefer the full error message + if ( ! defined( 'PIWIK_ARCHIVE_NO_TRUNCATE' ) ) { + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound + define( 'PIWIK_ARCHIVE_NO_TRUNCATE', 1 ); // when triggering it manually, we prefer the full error message } try { // force invalidation of archive to ensure it actually will rearchive the data - $site = new Site(); + $site = new Site(); $idsite = $site->get_current_matomo_site_id(); - if ($idsite) { - $timezone = \Piwik\Site::getTimezoneFor($idsite); - $now_string = \Piwik\Date::factory('now', $timezone)->toString(); - foreach (array('day') as $period) { - API::getInstance()->invalidateArchivedReports($idsite, $now_string, $period, false, false); + if ( $idsite ) { + $timezone = \Piwik\Site::getTimezoneFor( $idsite ); + $now_string = \Piwik\Date::factory( 'now', $timezone )->toString(); + foreach ( [ 'day' ] as $period ) { + API::getInstance()->invalidateArchivedReports( $idsite, $now_string, $period, false, false ); } } - } catch (\Exception $e) { - $this->logger->log_exception('archive_invalidate', $e); + } catch ( Exception $e ) { + $this->logger->log_exception( 'archive_invalidate', $e ); } try { - $errors = $scheduled_tasks->archive( $force = true, $throw_exception = false ); - } catch (\Exception $e) { - echo '

' . esc_html__('Matomo Archive Error', 'matomo') . ': '. esc_html(matomo_anonymize_value($e->getMessage() . ' =>' . $this->logger->get_readable_trace($e))) . '

'; + $errors = $scheduled_tasks->archive( true, false ); + } catch ( Exception $e ) { + echo '

' . esc_html__( 'Matomo Archive Error', 'matomo' ) . ': ' . esc_html( matomo_anonymize_value( $e->getMessage() . ' =>' . $this->logger->get_readable_trace( $e ) ) ) . '

'; throw $e; } if ( ! empty( $errors ) ) { echo '

Matomo Archive Warnings: '; - foreach ($errors as $error) { - echo nl2br(esc_html(matomo_anonymize_value(var_export($error, 1)))); + foreach ( $errors as $error ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export + echo nl2br( esc_html( matomo_anonymize_value( var_export( $error, 1 ) ) ) ); echo '
'; } echo '

'; @@ -191,47 +221,52 @@ public function show() { $settings = $this->settings; $matomo_active_tab = ''; - if ( isset( $_GET['tab'] ) && in_array( $_GET['tab'], $this->valid_tabs, true ) ) { - $matomo_active_tab = $_GET['tab']; + + if ( isset( $_GET['tab'] ) ) { + $tab = sanitize_text_field( wp_unslash( $_GET['tab'] ) ); + if ( in_array( $tab, $this->valid_tabs, true ) ) { + $matomo_active_tab = $tab; + } } - $matomo_tables = array(); + $matomo_tables = []; if ( empty( $matomo_active_tab ) ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting $this->initial_error_reporting = @error_reporting(); - $matomo_tables = array( - array( + $matomo_tables = [ + [ 'title' => 'Matomo', 'rows' => $this->get_matomo_info(), 'has_comments' => true, - ), - array( - 'title' => 'WordPress', - 'rows' => $this->get_wordpress_info(), + ], + [ + 'title' => 'WordPress', + 'rows' => $this->get_wordpress_info(), 'has_comments' => true, - ), - array( + ], + [ 'title' => 'WordPress Plugins', 'rows' => $this->get_plugins_info(), 'has_comments' => true, - ), - array( + ], + [ 'title' => 'Server', 'rows' => $this->get_server_info(), 'has_comments' => true, - ), - array( + ], + [ 'title' => 'Database', 'rows' => $this->get_db_info(), 'has_comments' => true, - ), - array( + ], + [ 'title' => 'Browser', 'rows' => $this->get_browser_info(), 'has_comments' => true, - ), - ); + ], + ]; } - $matomo_tables = apply_filters('matomo_systemreport_tables', $matomo_tables); + $matomo_tables = apply_filters( 'matomo_systemreport_tables', $matomo_tables ); $matomo_tables = $this->add_errors_first( $matomo_tables ); $matomo_has_warning_and_no_errors = $this->has_only_warnings_no_error( $matomo_tables ); @@ -258,11 +293,11 @@ private function has_only_warnings_no_error( $report_tables ) { } private function add_errors_first( $report_tables ) { - $errors = array( + $errors = [ 'title' => 'Errors', - 'rows' => array(), + 'rows' => [], 'has_comments' => true, - ); + ]; foreach ( $report_tables as $report_table ) { foreach ( $report_table['rows'] as $row ) { if ( ! empty( $row['is_error'] ) ) { @@ -293,28 +328,28 @@ private function check_file_exists_and_writable( $rows, $path_to_check, $title, $comment .= sprintf( esc_html__( '%s is not writable. ', 'matomo' ), $title ); } - $rows[] = array( + $rows[] = [ 'name' => sprintf( esc_html__( '%s exists and is writable.', 'matomo' ), $title ), 'value' => $file_exists && $file_readable && $file_writable ? esc_html__( 'Yes', 'matomo' ) : esc_html__( 'No', 'matomo' ), 'comment' => $comment, 'is_error' => $required && ( ! $file_exists || ! $file_readable ), 'is_warning' => ! $required && ( ! $file_exists || ! $file_readable ), - ); + ]; return $rows; } private function get_matomo_info() { - $rows = array(); + $rows = []; $plugin_data = get_plugin_data( MATOMO_ANALYTICS_FILE, $markup = false, $translate = false ); - $install_time = get_option(Installer::OPTION_NAME_INSTALL_DATE); + $install_time = get_option( Installer::OPTION_NAME_INSTALL_DATE ); - $rows[] = array( + $rows[] = [ 'name' => esc_html__( 'Matomo Plugin Version', 'matomo' ), 'value' => $plugin_data['Version'], 'comment' => '', - ); + ]; $paths = new Paths(); $path_config_file = $paths->get_config_ini_path(); @@ -323,21 +358,23 @@ private function get_matomo_info() { $path_tracker_file = $paths->get_matomo_js_upload_path(); $rows = $this->check_file_exists_and_writable( $rows, $path_tracker_file, 'JS Tracker', false ); - $rows[] = array( + $rows[] = [ 'name' => esc_html__( 'Plugin directories', 'matomo' ), 'value' => ! empty( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) ? 'Yes' : 'No', 'comment' => ! empty( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) ? wp_json_encode( $GLOBALS['MATOMO_PLUGIN_DIRS'] ) : '', - ); + ]; $tmp_dir = $paths->get_tmp_dir(); - $rows[] = array( + $rows[] = [ 'name' => esc_html__( 'Tmp directory writable', 'matomo' ), 'value' => is_writable( $tmp_dir ), 'comment' => $tmp_dir, - ); + ]; if ( ! empty( $_SERVER['MATOMO_WP_ROOT_PATH'] ) ) { + // we can have / in this value + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $custom_path = rtrim( $_SERVER['MATOMO_WP_ROOT_PATH'], '/' ) . '/wp-load.php'; $path_exists = file_exists( $custom_path ); $comment = ''; @@ -345,147 +382,146 @@ private function get_matomo_info() { $comment = 'It seems the path does not point to the WP root directory.'; } - $rows[] = array( + $rows[] = [ 'name' => 'Custom MATOMO_WP_ROOT_PATH', 'value' => $path_exists, 'is_error' => ! $path_exists, 'comment' => $comment, - ); + ]; } $report = null; - if ( ! \WpMatomo::is_safe_mode() ) { + if ( ! WpMatomo::is_safe_mode() ) { try { Bootstrap::do_bootstrap(); /** @var DiagnosticService $service */ $service = StaticContainer::get( DiagnosticService::class ); $report = $service->runDiagnostics(); - $rows[] = array( + $rows[] = [ 'name' => esc_html__( 'Matomo Version', 'matomo' ), 'value' => \Piwik\Version::VERSION, 'comment' => '', - ); - } catch ( \Exception $e ) { - $rows[] = array( + ]; + } catch ( Exception $e ) { + $rows[] = [ 'name' => esc_html__( 'Matomo System Check', 'matomo' ), 'value' => 'Failed to run Matomo system check.', 'comment' => $e->getMessage(), - ); + ]; } } $site = new Site(); $idsite = $site->get_current_matomo_site_id(); - $rows[] = array( + $rows[] = [ 'name' => esc_html__( 'Matomo Blog idSite', 'matomo' ), 'value' => $idsite, 'comment' => '', - ); + ]; $install_date = ''; - if (!empty($install_time)) { - $install_date = 'Install date: '. $this->convert_time_to_date($install_time, true, false); + if ( ! empty( $install_time ) ) { + $install_date = 'Install date: ' . $this->convert_time_to_date( $install_time, true, false ); } - $rows[] = array( + $rows[] = [ 'name' => esc_html__( 'Matomo Install Version', 'matomo' ), - 'value' => get_option(Installer::OPTION_NAME_INSTALL_VERSION), + 'value' => get_option( Installer::OPTION_NAME_INSTALL_VERSION ), 'comment' => $install_date, - ); - - $wpmatomo_updater = new \WpMatomo\Updater($this->settings); - if (!\WpMatomo::is_safe_mode()) { + ]; + $wpmatomo_updater = new \WpMatomo\Updater( $this->settings ); + if ( ! WpMatomo::is_safe_mode() ) { $outstanding_updates = $wpmatomo_updater->get_plugins_requiring_update(); $upgrade_in_progress = $wpmatomo_updater->is_upgrade_in_progress(); - $rows[] = array( - 'name' => 'Upgrades outstanding', - 'value' => !empty($outstanding_updates), - 'comment' => !empty($outstanding_updates) ? json_encode($outstanding_updates) : '', - ); - $rows[] = array( - 'name' => 'Upgrade in progress', - 'value' => $upgrade_in_progress, - 'comment' => '', - ); + $rows[] = [ + 'name' => 'Upgrades outstanding', + 'value' => ! empty( $outstanding_updates ), + 'comment' => ! empty( $outstanding_updates ) ? wp_json_encode( $outstanding_updates ) : '', + ]; + $rows[] = [ + 'name' => 'Upgrade in progress', + 'value' => $upgrade_in_progress, + 'comment' => '', + ]; } - if (!$wpmatomo_updater->load_plugin_functions()) { + if ( ! $wpmatomo_updater->load_plugin_functions() ) { // this should actually never happen... - $rows[] = array( - 'name' => 'Matomo Upgrade Plugin Functions', - 'is_warning' => true, - 'value' => false, - 'comment' => 'Function "get_plugin_data" not available. There may be an issue with upgrades not being executed. Please reach out to us.', - ); + $rows[] = [ + 'name' => 'Matomo Upgrade Plugin Functions', + 'is_warning' => true, + 'value' => false, + 'comment' => 'Function "get_plugin_data" not available. There may be an issue with upgrades not being executed. Please reach out to us.', + ]; } - $rows[] = array( + $rows[] = [ 'section' => 'Endpoints', - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'Matomo JavaScript Tracker URL', 'value' => '', 'comment' => $paths->get_js_tracker_url_in_matomo_dir(), - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'Matomo JavaScript Tracker - WP Rest API', 'value' => '', 'comment' => $paths->get_js_tracker_rest_api_endpoint(), - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'Matomo HTTP Tracking API', 'value' => '', 'comment' => $paths->get_tracker_api_url_in_matomo_dir(), - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'Matomo HTTP Tracking API - WP Rest API', 'value' => '', 'comment' => $paths->get_tracker_api_rest_api_endpoint(), - ); + ]; - $matomo_plugin_dir_name = basename(dirname(MATOMO_ANALYTICS_FILE)); - if ($matomo_plugin_dir_name !== 'matomo') { - $rows[] = array( - 'name' => 'Matomo Plugin Name is correct', - 'value' => false, + $matomo_plugin_dir_name = basename( dirname( MATOMO_ANALYTICS_FILE ) ); + if ( 'matomo' !== $matomo_plugin_dir_name ) { + $rows[] = [ + 'name' => 'Matomo Plugin Name is correct', + 'value' => false, 'is_error' => true, - 'comment' => 'The plugin name should be "matomo" but seems to be "' . $matomo_plugin_dir_name . '". As a result, admin pages and other features might not work. You might need to rename the directory name of this plugin and reactive the plugin.', - ); - } elseif (!is_plugin_active('matomo/matomo.php')) { - $rows[] = array( - 'name' => 'Matomo Plugin not active', - 'value' => false, + 'comment' => 'The plugin name should be "matomo" but seems to be "' . $matomo_plugin_dir_name . '". As a result, admin pages and other features might not work. You might need to rename the directory name of this plugin and reactive the plugin.', + ]; + } elseif ( ! is_plugin_active( 'matomo/matomo.php' ) ) { + $rows[] = [ + 'name' => 'Matomo Plugin not active', + 'value' => false, 'is_error' => true, - 'comment' => 'It seems WordPress thinks that `matomo/matomo.php` is not active. As a result Matomo reporting and admin pages may not work. You may be able to fix this by deactivating and activating the Matomo Analytics plugin. One of the reasons this could happen is that you used to have Matomo installed in the wrong folder.', - ); + 'comment' => 'It seems WordPress thinks that `matomo/matomo.php` is not active. As a result Matomo reporting and admin pages may not work. You may be able to fix this by deactivating and activating the Matomo Analytics plugin. One of the reasons this could happen is that you used to have Matomo installed in the wrong folder.', + ]; } - $rows[] = array( + $rows[] = [ 'section' => 'Crons', - ); + ]; $scheduled_tasks = new ScheduledTasks( $this->settings ); $all_events = $scheduled_tasks->get_all_events(); - $rows[] = array( + $rows[] = [ 'name' => esc_html__( 'Server time', 'matomo' ), 'value' => $this->convert_time_to_date( time(), false ), 'comment' => '', - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => esc_html__( 'Blog time', 'matomo' ), 'value' => $this->convert_time_to_date( time(), true ), 'comment' => esc_html__( 'Below dates are shown in blog timezone', 'matomo' ), - ); + ]; foreach ( $all_events as $event_name => $event_config ) { $last_run_before = $scheduled_tasks->get_last_time_before_cron( $event_name ); @@ -497,133 +533,131 @@ private function get_matomo_info() { $comment .= ' Last ended: ' . $this->convert_time_to_date( $last_run_after, true, true ) . '.'; $comment .= ' Interval: ' . $event_config['interval']; - $rows[] = array( + $rows[] = [ 'name' => $event_config['name'], 'value' => 'Next run: ' . $this->convert_time_to_date( $next_scheduled, true, true ), 'comment' => $comment, - ); + ]; } $suports_async = false; - if ( ! \WpMatomo::is_safe_mode() && $report ) { - $rows[] = array( + if ( ! WpMatomo::is_safe_mode() && $report ) { + $rows[] = [ 'section' => esc_html__( 'Mandatory checks', 'matomo' ), - ); + ]; $rows = $this->add_diagnostic_results( $rows, $report->getMandatoryDiagnosticResults() ); - $rows[] = array( + $rows[] = [ 'section' => esc_html__( 'Optional checks', 'matomo' ), - ); + ]; $rows = $this->add_diagnostic_results( $rows, $report->getOptionalDiagnosticResults() ); - $cli_multi = new CliMulti(); + $cli_multi = new CliMulti(); $suports_async = $cli_multi->supportsAsync(); - $rows[] = array( + $rows[] = [ 'name' => 'Supports Async Archiving', 'value' => $suports_async, 'comment' => '', - ); + ]; $location_provider = LocationProvider::getCurrentProvider(); - if ($location_provider) { - $rows[] = array( + if ( $location_provider ) { + $rows[] = [ 'name' => 'Location provider ID', 'value' => $location_provider->getId(), 'comment' => '', - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'Location provider available', 'value' => $location_provider->isAvailable(), 'comment' => '', - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'Location provider working', 'value' => $location_provider->isWorking(), 'comment' => '', - ); + ]; } - if ( ! \WpMatomo::is_safe_mode() ) { + if ( ! WpMatomo::is_safe_mode() ) { Bootstrap::do_bootstrap(); $general = Config::getInstance()->General; - - if (empty($general['proxy_client_headers'])) { - foreach (AdvancedSettings::$valid_host_headers as $header) { - if (!empty($_SERVER[$header])) { - $rows[] = array( - 'name' => 'Proxy header', - 'value' => $header, + + if ( empty( $general['proxy_client_headers'] ) ) { + foreach ( AdvancedSettings::$valid_host_headers as $header ) { + if ( ! empty( $_SERVER[ $header ] ) ) { + $rows[] = [ + 'name' => 'Proxy header', + 'value' => $header, 'is_warning' => true, - 'comment' => 'A proxy header is set which means you maybe need to configure a proxy header in the Advanced settings to make location reporting work. If the location in your reports is detected correctly, you can ignore this warning. Learn more: https://matomo.org/faq/wordpress/how-do-i-fix-the-proxy-header-warning-in-the-matomo-for-wordpress-system-report/', - ); + 'comment' => 'A proxy header is set which means you maybe need to configure a proxy header in the Advanced settings to make location reporting work. If the location in your reports is detected correctly, you can ignore this warning. Learn more: https://matomo.org/faq/wordpress/how-do-i-fix-the-proxy-header-warning-in-the-matomo-for-wordpress-system-report/', + ]; } } } - $incompatible_plugins = Plugin\Manager::getInstance()->getIncompatiblePlugins(Version::VERSION); - if (!empty($incompatible_plugins)) { - $rows[] = array( - 'section' => esc_html__( 'Incompatible Matomo plugins', 'matomo' ), - ); - foreach ($incompatible_plugins as $plugin) { - $rows[] = array( - 'name' => 'Plugin has missing dependencies', - 'value' => $plugin->getPluginName(), - 'is_error' => true, - 'comment' => $plugin->getMissingDependenciesAsString(Version::VERSION) . ' If the plugin requires a different Matomo version you may need to update it. If you no longer use it consider uninstalling it.', - ); - } - - } + $incompatible_plugins = Plugin\Manager::getInstance()->getIncompatiblePlugins( Version::VERSION ); + if ( ! empty( $incompatible_plugins ) ) { + $rows[] = [ + 'section' => esc_html__( 'Incompatible Matomo plugins', 'matomo' ), + ]; + foreach ( $incompatible_plugins as $plugin ) { + $rows[] = [ + 'name' => 'Plugin has missing dependencies', + 'value' => $plugin->getPluginName(), + 'is_error' => true, + 'comment' => $plugin->getMissingDependenciesAsString( Version::VERSION ) . ' If the plugin requires a different Matomo version you may need to update it. If you no longer use it consider uninstalling it.', + ]; + } + } } $num_days_check_visits = 5; - $had_visits = $this->had_visits_in_last_days($num_days_check_visits); - if ($had_visits === false || $had_visits === true) { + $had_visits = $this->had_visits_in_last_days( $num_days_check_visits ); + if ( false === $had_visits || true === $had_visits ) { // do not show info if we could not detect it (had_visits === null) $comment = ''; - if (!$had_visits) { + if ( ! $had_visits ) { $comment = 'It looks like there were no visits in the last ' . $num_days_check_visits . ' days. This may be expected if tracking is disabled, you have not added the tracking code, or your website does not have many visitors in general and you exclude your own visits.'; } - $rows[] = array( - 'name' => 'Had visit in last ' . $num_days_check_visits . ' days', - 'value' => $had_visits, - 'is_warning' => !$had_visits && $this->settings->is_tracking_enabled(), - 'comment' => $comment, - ); + $rows[] = [ + 'name' => 'Had visit in last ' . $num_days_check_visits . ' days', + 'value' => $had_visits, + 'is_warning' => ! $had_visits && $this->settings->is_tracking_enabled(), + 'comment' => $comment, + ]; } - if ( ! \WpMatomo::is_safe_mode() ) { + if ( ! WpMatomo::is_safe_mode() ) { Bootstrap::do_bootstrap(); $matomo_url = SettingsPiwik::getPiwikUrl(); - $rows[] = array( + $rows[] = [ 'name' => 'Matomo URL', 'comment' => $matomo_url, 'value' => ! empty( $matomo_url ), - ); + ]; } - } - $rows[] = array( + $rows[] = [ 'section' => 'Matomo Settings', - ); + ]; // always show these settings - $global_settings_always_show = array( + $global_settings_always_show = [ 'track_mode', 'track_codeposition', 'track_api_endpoint', 'track_js_endpoint', - ); + ]; foreach ( $global_settings_always_show as $key ) { - $rows[] = array( + $rows[] = [ 'name' => ucfirst( str_replace( '_', ' ', $key ) ), 'value' => $this->settings->get_global_option( $key ), 'comment' => '', - ); + ]; } // otherwise show only few customised settings @@ -635,132 +669,130 @@ private function get_matomo_info() { $val = implode( ', ', $val ); } - $rows[] = array( + $rows[] = [ 'name' => ucfirst( str_replace( '_', ' ', $key ) ), 'value' => $val, 'comment' => '', - ); + ]; } } - $rows[] = array( + $rows[] = [ 'section' => 'Logs', - ); + ]; $error_log_entries = $this->logger->get_last_logged_entries(); - - if ( ! empty( $error_log_entries ) ) { + if ( ! empty( $error_log_entries ) ) { foreach ( $error_log_entries as $error ) { - if (!empty($install_time) - && is_numeric($install_time) - && !empty($error['name']) - && !empty($error['value']) - && is_numeric($error['value']) - && $error['name'] === 'cron_sync' - && $error['value'] < ($install_time + 300)) { + if ( ! empty( $install_time ) + && is_numeric( $install_time ) + && ! empty( $error['name'] ) + && ! empty( $error['value'] ) + && is_numeric( $error['value'] ) + && 'cron_sync' === $error['name'] + && $error['value'] < ( $install_time + 300 ) ) { // the first sync might right after the installation continue; } // we only consider plugin_updates as errors only if there are still outstanding updates - $is_plugin_update_error = !empty($error['name']) && $error['name'] === 'plugin_update' - && !empty($outstanding_updates); + $is_plugin_update_error = ! empty( $error['name'] ) && 'plugin_update' === $error['name'] + && ! empty( $outstanding_updates ); - $skip_plugin_update = !empty($error['name']) && $error['name'] === 'plugin_update' - && empty($outstanding_updates); + $skip_plugin_update = ! empty( $error['name'] ) && 'plugin_update' === $error['name'] + && empty( $outstanding_updates ); - if (empty($error['comment']) && $error['comment'] !== '0') { + if ( empty( $error['comment'] ) && '0' !== $error['comment'] ) { $error['comment'] = ''; } - $error['value'] = $this->convert_time_to_date( $error['value'], true, false ); - $error['is_warning'] = !empty($error['name']) && stripos($error['name'], 'archiv') !== false && $error['name'] !== 'archive_boot'; - $error['is_error'] = $is_plugin_update_error; - if ($is_plugin_update_error) { + $error['value'] = $this->convert_time_to_date( $error['value'], true, false ); + $error['is_warning'] = ! empty( $error['name'] ) && stripos( $error['name'], 'archiv' ) !== false && 'archive_boot' !== $error['name']; + $error['is_error'] = $is_plugin_update_error; + if ( $is_plugin_update_error ) { $error['comment'] = 'Please reach out to us and include the copied system report (see https://matomo.org/faq/wordpress/how-do-i-troubleshoot-a-failed-database-upgrade-in-matomo-for-wordpress/ for more info)

You can also retry the update manually by clicking in the top on the "Troubleshooting" tab and then clicking on the "Run updater" button.' . $error['comment']; - } elseif ($skip_plugin_update) { + } elseif ( $skip_plugin_update ) { $error['comment'] = 'As there are no outstanding plugin updates it looks like this log can be ignored.

' . $error['comment']; } - $error['comment'] = matomo_anonymize_value($error['comment']); - $rows[] = $error; + $error['comment'] = matomo_anonymize_value( $error['comment'] ); + $rows[] = $error; } foreach ( $error_log_entries as $error ) { - if ($suports_async - && !empty($error['value']) && is_string($error['value']) - && strpos($error['value'], __( 'Your PHP installation appears to be missing the MySQL extension which is required by WordPress.' )) > 0) { - - $rows[] = array( - 'name' => 'Cli has no MySQL', - 'value' => true, - 'comment' => 'It looks like MySQL is not available on CLI. Please read our FAQ on how to fix this issue: https://matomo.org/faq/wordpress/how-do-i-fix-the-error-your-php-installation-appears-to-be-missing-the-mysql-extension-which-is-required-by-wordpress-in-matomo-system-report/ ', - 'is_error' => true - ); + if ( $suports_async + && ! empty( $error['value'] ) && is_string( $error['value'] ) + && strpos( $error['value'], __( 'Your PHP installation appears to be missing the MySQL extension which is required by WordPress.', 'matomo' ) ) > 0 ) { + $rows[] = [ + 'name' => 'Cli has no MySQL', + 'value' => true, + 'comment' => 'It looks like MySQL is not available on CLI. Please read our FAQ on how to fix this issue: https://matomo.org/faq/wordpress/how-do-i-fix-the-error-your-php-installation-appears-to-be-missing-the-mysql-extension-which-is-required-by-wordpress-in-matomo-system-report/ ', + 'is_error' => true, + ]; } } } else { - $rows[] = array( - 'name' => __('None', 'matomo'), + $rows[] = [ + 'name' => __( 'None', 'matomo' ), 'value' => '', 'comment' => '', - ); + ]; } - - if ( ! \WpMatomo::is_safe_mode() ) { + if ( ! WpMatomo::is_safe_mode() ) { Bootstrap::do_bootstrap(); $trackfailures = []; try { $tracking_failures = new Failures(); - $trackfailures = $tracking_failures->getAllFailures(); - } catch (\Exception $e) { + $trackfailures = $tracking_failures->getAllFailures(); + // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + } catch ( Exception $e ) { // ignored in case not set up yet etc. } - if (!empty($trackfailures)) { - $rows[] = array( + if ( ! empty( $trackfailures ) ) { + $rows[] = [ 'section' => 'Tracking failures', - ); - foreach ($trackfailures as $failure) { - $comment = sprintf('Solution: %s
More info: %s
Date: %s
Request URL: %s', - $failure['solution'], $failure['solution_url'], - $failure['pretty_date_first_occurred'], $failure['request_url']); - $rows[] = array( - 'name' => $failure['problem'], - 'is_warning' => true, - 'value' => '', - 'comment' => $comment, + ]; + foreach ( $trackfailures as $failure ) { + $comment = sprintf( + 'Solution: %s
More info: %s
Date: %s
Request URL: %s', + $failure['solution'], + $failure['solution_url'], + $failure['pretty_date_first_occurred'], + $failure['request_url'] ); + $rows[] = [ + 'name' => $failure['problem'], + 'is_warning' => true, + 'value' => '', + 'comment' => $comment, + ]; } - } - } - return $rows; } - private function had_visits_in_last_days($numDays) - { + private function had_visits_in_last_days( $num_days ) { global $wpdb; - if (\WpMatomo::is_safe_mode()) { + if ( WpMatomo::is_safe_mode() ) { return null; } - $days_in_seconds = $numDays * 86400; + $days_in_seconds = $num_days * 86400; - $prefix_table = $this->dbSettings->prefix_table_name('log_visit'); + $prefix_table = $this->db_settings->prefix_table_name( 'log_visit' ); $suppress_errors = $wpdb->suppress_errors; $wpdb->suppress_errors( true );// prevent any of this showing in logs just in case try { $time = gmdate( 'Y-m-d H:i:s', time() - $days_in_seconds ); - $sql = $wpdb->prepare('SELECT idsite from ' . $prefix_table . ' where visit_last_action_time > %s LIMIT 1', $time ); - $row = $wpdb->get_var( $sql ); - } catch ( \Exception $e ) { + $sql = $wpdb->prepare( 'SELECT idsite from ' . $prefix_table . ' where visit_last_action_time > %s LIMIT 1', $time ); + $row = $wpdb->get_var( $sql ); + } catch ( Exception $e ) { $row = null; } @@ -769,8 +801,8 @@ private function had_visits_in_last_days($numDays) // 0 === had no visit // 1 === had visit // null === sum error... eg table was not correctly installed - if ($row !== null) { - $row = !empty($row); + if ( null !== $row ) { + $row = ! empty( $row ); } return $row; @@ -781,7 +813,7 @@ private function convert_time_to_date( $time, $in_blog_timezone, $print_diff = f return esc_html__( 'Unknown', 'matomo' ); } - $date = gmdate( 'Y-m-d H:i:s', (int)$time ); + $date = gmdate( 'Y-m-d H:i:s', (int) $time ); if ( $in_blog_timezone ) { $date = get_date_from_gmt( $date, 'Y-m-d H:i:s' ); @@ -789,7 +821,7 @@ private function convert_time_to_date( $time, $in_blog_timezone, $print_diff = f if ( $print_diff && class_exists( '\Piwik\Metrics\Formatter' ) ) { $formatter = new \Piwik\Metrics\Formatter(); - $date .= ' (' . $formatter->getPrettyTimeFromSeconds( $time - time(), true, false ) . ')'; + $date .= ' (' . $formatter->getPrettyTimeFromSeconds( $time - time(), true, false ) . ')'; } return $date; @@ -818,13 +850,13 @@ private function add_diagnostic_results( $rows, $results ) { } } - $rows[] = array( + $rows[] = [ 'name' => $result->getLabel(), 'value' => $result->getStatus() . ' ' . $result->getLongErrorMessage(), 'comment' => $comment, 'is_warning' => $result->getStatus() === DiagnosticResult::STATUS_WARNING, 'is_error' => $result->getStatus() === DiagnosticResult::STATUS_ERROR, - ); + ]; } return $rows; @@ -842,245 +874,272 @@ private function get_wordpress_info() { $is_network_enabled = $settings->is_network_enabled(); } - $rows = array(); - $rows[] = array( + $rows = []; + $rows[] = [ 'name' => 'Home URL', 'value' => home_url(), - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'Site URL', 'value' => site_url(), - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'WordPress Version', 'value' => get_bloginfo( 'version' ), - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'Number of blogs', 'value' => $num_blogs, - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'Multisite Enabled', 'value' => $is_multi_site, - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'Network Enabled', 'value' => $is_network_enabled, - ); - $consts = array('WP_DEBUG', 'WP_DEBUG_DISPLAY', 'WP_DEBUG_LOG', 'DISABLE_WP_CRON', 'FORCE_SSL_ADMIN', 'WP_CACHE', - 'CONCATENATE_SCRIPTS', 'COMPRESS_SCRIPTS', 'COMPRESS_CSS', 'ENFORCE_GZIP', 'WP_LOCAL_DEV', - 'WP_CONTENT_URL', 'WP_CONTENT_DIR', 'UPLOADS', 'BLOGUPLOADDIR', - 'DIEONDBERROR', 'WPLANG', 'ALTERNATE_WP_CRON', 'WP_CRON_LOCK_TIMEOUT', 'WP_DISABLE_FATAL_ERROR_HANDLER', - 'MATOMO_SUPPORT_ASYNC_ARCHIVING', 'MATOMO_TRIGGER_BROWSER_ARCHIVING', 'MATOMO_ENABLE_TAG_MANAGER', 'MATOMO_SUPPRESS_DB_ERRORS', 'MATOMO_ENABLE_AUTO_UPGRADE', - 'MATOMO_DEBUG', 'MATOMO_SAFE_MODE', 'MATOMO_GLOBAL_UPLOAD_DIR', 'MATOMO_LOGIN_REDIRECT'); - foreach ($consts as $const) { - $rows[] = array( + ]; + $consts = [ + 'WP_DEBUG', + 'WP_DEBUG_DISPLAY', + 'WP_DEBUG_LOG', + 'DISABLE_WP_CRON', + 'FORCE_SSL_ADMIN', + 'WP_CACHE', + 'CONCATENATE_SCRIPTS', + 'COMPRESS_SCRIPTS', + 'COMPRESS_CSS', + 'ENFORCE_GZIP', + 'WP_LOCAL_DEV', + 'WP_CONTENT_URL', + 'WP_CONTENT_DIR', + 'UPLOADS', + 'BLOGUPLOADDIR', + 'DIEONDBERROR', + 'WPLANG', + 'ALTERNATE_WP_CRON', + 'WP_CRON_LOCK_TIMEOUT', + 'WP_DISABLE_FATAL_ERROR_HANDLER', + 'MATOMO_SUPPORT_ASYNC_ARCHIVING', + 'MATOMO_TRIGGER_BROWSER_ARCHIVING', + 'MATOMO_ENABLE_TAG_MANAGER', + 'MATOMO_SUPPRESS_DB_ERRORS', + 'MATOMO_ENABLE_AUTO_UPGRADE', + 'MATOMO_DEBUG', + 'MATOMO_SAFE_MODE', + 'MATOMO_GLOBAL_UPLOAD_DIR', + 'MATOMO_LOGIN_REDIRECT', + ]; + foreach ( $consts as $const ) { + $rows[] = [ 'name' => $const, - 'value' => defined( $const ) ? constant( $const) : '-', - ); + 'value' => defined( $const ) ? constant( $const ) : '-', + ]; } - $rows[] = array( + $rows[] = [ 'name' => 'Permalink Structure', 'value' => get_option( 'permalink_structure' ) ? get_option( 'permalink_structure' ) : 'Default', - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'Possibly uses symlink', 'value' => strpos( __DIR__, ABSPATH ) === false && strpos( __DIR__, WP_CONTENT_DIR ) === false, - ); + ]; $upload_dir = wp_upload_dir(); - $rows[] = array( + $rows[] = [ 'name' => 'Upload base url', 'value' => $upload_dir['baseurl'], - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'Upload base dir', 'value' => $upload_dir['basedir'], - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'Upload url', 'value' => $upload_dir['url'], - ); + ]; - foreach (['upload_path', 'upload_url_path'] as $option_read) { - $rows[] = array( + foreach ( [ 'upload_path', 'upload_url_path' ] as $option_read ) { + $rows[] = [ 'name' => 'Custom ' . $option_read, 'value' => get_option( $option_read ), - ); + ]; } - if (is_plugin_active('wp-piwik/wp-piwik.php')) { - $rows[] = array( - 'name' => 'WP-Matomo (WP-Piwik) activated', - 'value' => true, + if ( is_plugin_active( 'wp-piwik/wp-piwik.php' ) ) { + $rows[] = [ + 'name' => 'WP-Matomo (WP-Piwik) activated', + 'value' => true, 'is_warning' => true, - 'comment' => 'It is usually not recommended or needed to run Matomo for WordPress and WP-Matomo at the same time. To learn more about the differences between the two plugins view this URL: https://matomo.org/faq/wordpress/why-are-there-two-different-matomo-for-wordpress-plugins-what-is-the-difference-to-wp-matomo-integration-plugin/' - ); + 'comment' => 'It is usually not recommended or needed to run Matomo for WordPress and WP-Matomo at the same time. To learn more about the differences between the two plugins view this URL: https://matomo.org/faq/wordpress/why-are-there-two-different-matomo-for-wordpress-plugins-what-is-the-difference-to-wp-matomo-integration-plugin/', + ]; - $mode = get_option ( 'wp-piwik_global-piwik_mode' ); - if (function_exists('get_site_option') && is_plugin_active_for_network ( 'wp-piwik/wp-piwik.php' )) { - $mode = get_site_option ( 'wp-piwik_global-piwik_mode'); + $mode = get_option( 'wp-piwik_global-piwik_mode' ); + if ( function_exists( 'get_site_option' ) && is_plugin_active_for_network( 'wp-piwik/wp-piwik.php' ) ) { + $mode = get_site_option( 'wp-piwik_global-piwik_mode' ); } - if (!empty($mode)) { - $rows[] = array( - 'name' => 'WP-Matomo mode', - 'value' => $mode, - 'is_warning' => $mode === 'php' || $mode === 'PHP', - 'comment' => 'WP-Matomo is configured in "PHP mode". This is known to cause issues with Matomo for WordPress. We recommend you either deactivate WP-Matomo or you go "Settings => WP-Matomo" and change the "Matomo Mode" in the "Connect to Matomo" section to "Self-hosted HTTP API".' - ); + if ( ! empty( $mode ) ) { + $rows[] = [ + 'name' => 'WP-Matomo mode', + 'value' => $mode, + 'is_warning' => 'php' === $mode || 'PHP' === $mode, + 'comment' => 'WP-Matomo is configured in "PHP mode". This is known to cause issues with Matomo for WordPress. We recommend you either deactivate WP-Matomo or you go "Settings => WP-Matomo" and change the "Matomo Mode" in the "Connect to Matomo" section to "Self-hosted HTTP API".', + ]; } } $compatible_content_dir = matomo_has_compatible_content_dir(); - if ($compatible_content_dir === true) { - $rows[] = array( + if ( true === $compatible_content_dir ) { + $rows[] = [ 'name' => 'Compatible content directory', 'value' => true, - ); + ]; } else { - $rows[] = array( - 'name' => 'Compatible content directory', - 'value' => $compatible_content_dir, + $rows[] = [ + 'name' => 'Compatible content directory', + 'value' => $compatible_content_dir, 'is_warning' => true, - 'comment' => __( 'It looks like you are maybe using a custom WordPress content directory. The Matomo reporting/admin pages might not work. You may be able to workaround this.', 'matomo' ) . ' ' . __( 'Learn more', 'matomo' ) . ': https://matomo.org/faq/wordpress/how-do-i-make-matomo-for-wordpress-work-when-i-have-a-custom-content-directory/' - ); + 'comment' => __( 'It looks like you are maybe using a custom WordPress content directory. The Matomo reporting/admin pages might not work. You may be able to workaround this.', 'matomo' ) . ' ' . __( 'Learn more', 'matomo' ) . ': https://matomo.org/faq/wordpress/how-do-i-make-matomo-for-wordpress-work-when-i-have-a-custom-content-directory/', + ]; } return $rows; } private function get_server_info() { - $rows = array(); + $rows = []; if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) { - $rows[] = array( + $rows[] = [ 'name' => 'Server Info', - 'value' => $_SERVER['SERVER_SOFTWARE'], - ); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash + 'value' => sanitize_text_field( $_SERVER['SERVER_SOFTWARE'] ), + ]; } if ( PHP_OS ) { - $rows[] = array( + $rows[] = [ 'name' => 'PHP OS', 'value' => PHP_OS, - ); + ]; } - $rows[] = array( + $rows[] = [ 'name' => 'PHP Version', 'value' => phpversion(), - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'PHP SAPI', 'value' => php_sapi_name(), - ); - if (defined('PHP_BINARY') && PHP_BINARY) { - $rows[] = array( + ]; + if ( defined( 'PHP_BINARY' ) && PHP_BINARY ) { + $rows[] = [ 'name' => 'PHP Binary Name', - 'value' => @basename(PHP_BINARY), - ); + 'value' => @basename( PHP_BINARY ), + ]; } // we report error reporting before matomo bootstraped and after to see if Matomo changed it successfully etc - $rows[] = array( + $rows[] = [ 'name' => 'PHP Error Reporting', - 'value' => $this->initial_error_reporting . ' After bootstrap: ' . @error_reporting() - ); - if (!\WpMatomo::is_safe_mode()) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting + 'value' => $this->initial_error_reporting . ' After bootstrap: ' . @error_reporting(), + ]; + if ( ! WpMatomo::is_safe_mode() ) { Bootstrap::do_bootstrap(); - $cliPhp = new CliMulti\CliPhp(); - $binary = $cliPhp->findPhpBinary(); - if (!empty($binary)) { - $binary = basename($binary); - $rows[] = array( + $cli_php = new CliMulti\CliPhp(); + $binary = $cli_php->findPhpBinary(); + if ( ! empty( $binary ) ) { + $binary = basename( $binary ); + $rows[] = [ 'name' => 'PHP Found Binary', 'value' => $binary, - ); + ]; } } - $rows[] = array( + $rows[] = [ 'name' => 'Timezone', 'value' => date_default_timezone_get(), - ); - if (function_exists('wp_timezone_string')) { - $rows[] = array( + ]; + if ( function_exists( 'wp_timezone_string' ) ) { + $rows[] = [ 'name' => 'WP timezone', 'value' => wp_timezone_string(), - ); + ]; } - $rows[] = array( + $rows[] = [ 'name' => 'Locale', 'value' => get_locale(), - ); - if (function_exists('get_user_locale')) { - $rows[] = array( + ]; + if ( function_exists( 'get_user_locale' ) ) { + $rows[] = [ 'name' => 'User Locale', 'value' => get_user_locale(), - ); + ]; } - $rows[] = array( + $rows[] = [ 'name' => 'Memory Limit', 'value' => @ini_get( 'memory_limit' ), 'comment' => 'At least 128MB recommended. Depending on your traffic 256MB or more may be needed.', - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'WP Memory Limit', 'value' => defined( 'WP_MEMORY_LIMIT' ) ? WP_MEMORY_LIMIT : '', 'comment' => '', - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'WP Max Memory Limit', 'value' => defined( 'WP_MAX_MEMORY_LIMIT' ) ? WP_MAX_MEMORY_LIMIT : '', 'comment' => '', - ); - - if (function_exists('timezone_version_get')) { - $rows[] = array( + ]; + + if ( function_exists( 'timezone_version_get' ) ) { + $rows[] = [ 'name' => 'Timezone version', 'value' => timezone_version_get(), - ); + ]; } - - $rows[] = array( + + $rows[] = [ 'name' => 'Time', 'value' => time(), - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'Max Execution Time', 'value' => ini_get( 'max_execution_time' ), - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'Max Post Size', 'value' => ini_get( 'post_max_size' ), - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'Max Upload Size', 'value' => wp_max_upload_size(), - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'Max Input Vars', 'value' => ini_get( 'max_input_vars' ), - ); + ]; - $disabled_functions = ini_get('disable_functions'); - $rows[] = array( - 'name' => 'Disabled PHP functions', - 'value' => !empty($disabled_functions), - 'comment' => !empty($disabled_functions) ? $disabled_functions : '' - ); + $disabled_functions = ini_get( 'disable_functions' ); + $rows[] = [ + 'name' => 'Disabled PHP functions', + 'value' => ! empty( $disabled_functions ), + 'comment' => ! empty( $disabled_functions ) ? $disabled_functions : '', + ]; $zlib_compression = ini_get( 'zlib.output_compression' ); - $row = array( + $row = [ 'name' => 'zlib.output_compression is off', - 'value' => $zlib_compression !== '1', - ); + 'value' => '1' !== $zlib_compression, + ]; - if ( $zlib_compression === '1' ) { + if ( '1' === $zlib_compression ) { $row['is_error'] = true; $row['comment'] = 'You need to set "zlib.output_compression" in your php.ini to "Off".'; } @@ -1089,108 +1148,108 @@ private function get_server_info() { if ( function_exists( 'curl_version' ) ) { $curl_version = curl_version(); $curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version']; - $rows[] = array( + $rows[] = [ 'name' => 'Curl Version', 'value' => $curl_version, - ); + ]; } $suhosin_installed = ( extension_loaded( 'suhosin' ) || ( defined( 'SUHOSIN_PATCH' ) && constant( 'SUHOSIN_PATCH' ) ) ); - $rows[] = array( - 'name' => 'Suhosin installed', - 'value' => !empty($suhosin_installed), - 'comment' => '' - ); + $rows[] = [ + 'name' => 'Suhosin installed', + 'value' => ! empty( $suhosin_installed ), + 'comment' => '', + ]; return $rows; } private function get_browser_info() { - $rows = array(); + $rows = []; - if (!empty($_SERVER['HTTP_USER_AGENT'])) { - $rows[] = array( + if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) { + $rows[] = [ 'name' => 'Browser', 'value' => '', - 'comment' => $_SERVER['HTTP_USER_AGENT'] - ); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash + 'comment' => sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ), + ]; } - if (!\WpMatomo::is_safe_mode()) { + if ( ! WpMatomo::is_safe_mode() ) { Bootstrap::do_bootstrap(); try { - if (!empty($_SERVER['HTTP_USER_AGENT'])) { - $detector = StaticContainer::get(DeviceDetectorFactory::class)->makeInstance($_SERVER['HTTP_USER_AGENT']); - $client = $detector->getClient(); - if (!empty($client['name']) && $client['name'] === 'Microsoft Edge' && (int) $client['version'] >= 85) { - $rows[] = array( - 'name' => 'Browser Compatibility', + if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $detector = StaticContainer::get( DeviceDetectorFactory::class )->makeInstance( sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) ); + $client = $detector->getClient(); + if ( ! empty( $client['name'] ) && 'Microsoft Edge' === $client['name'] && (int) $client['version'] >= 85 ) { + $rows[] = [ + 'name' => 'Browser Compatibility', 'is_warning' => true, - 'value' => 'Yes', - 'comment' => 'Because you are using MS Edge browser, you may see a warning like "This site has been reported as unsafe" from "Microsoft Defender SmartScreen" when you view the Matomo Reporting, Admin or Tag Manager page. This is a false alert and you can safely ignore this warning by clicking on the icon next to the URL (in the address bar) and choosing either "Report as safe" (preferred) or "Show unsafe content". We are hoping to get this false warning removed in the future.' - ); + 'value' => 'Yes', + 'comment' => 'Because you are using MS Edge browser, you may see a warning like "This site has been reported as unsafe" from "Microsoft Defender SmartScreen" when you view the Matomo Reporting, Admin or Tag Manager page. This is a false alert and you can safely ignore this warning by clicking on the icon next to the URL (in the address bar) and choosing either "Report as safe" (preferred) or "Show unsafe content". We are hoping to get this false warning removed in the future.', + ]; } } - - } catch (\Exception $e) { - + } catch ( Exception $e ) { + $this->logger->log( $e->getMessage() ); } - $rows[] = array( + $rows[] = [ 'name' => 'Language', 'value' => Common::getBrowserLanguage(), - 'comment' => '' - ); + 'comment' => '', + ]; } - return $rows; } private function get_db_info() { global $wpdb; - $rows = array(); + $rows = []; - $rows[] = array( + $rows[] = [ 'name' => 'MySQL Version', 'value' => ! empty( $wpdb->is_mysql ) ? $wpdb->db_version() : '', 'comment' => '', - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'Mysqli Connect', 'value' => function_exists( 'mysqli_connect' ), 'comment' => '', - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'Force MySQL over Mysqli', 'value' => defined( 'WP_USE_EXT_MYSQL' ) && WP_USE_EXT_MYSQL, 'comment' => '', - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'DB Prefix', 'value' => $wpdb->prefix, - ); + ]; - $rows[] = array( + $rows[] = [ 'name' => 'DB CHARSET', - 'value' => defined('DB_CHARSET') ? DB_CHARSET : '', - ); + 'value' => defined( 'DB_CHARSET' ) ? DB_CHARSET : '', + ]; - $rows[] = array( + $rows[] = [ 'name' => 'DB COLLATE', - 'value' => defined('DB_COLLATE') ? DB_COLLATE : '', - ); + 'value' => defined( 'DB_COLLATE' ) ? DB_COLLATE : '', + ]; - $rows[] = array( + $rows[] = [ 'name' => 'SHOW ERRORS', - 'value' => !empty($wpdb->show_errors), - ); + 'value' => ! empty( $wpdb->show_errors ), + ]; - $rows[] = array( + $rows[] = [ 'name' => 'SUPPRESS ERRORS', - 'value' => !empty($wpdb->suppress_errors), - ); + 'value' => ! empty( $wpdb->suppress_errors ), + ]; if ( method_exists( $wpdb, 'parse_db_host' ) ) { $host_data = $wpdb->parse_db_host( DB_HOST ); @@ -1198,41 +1257,52 @@ private function get_db_info() { list( $host, $port, $socket, $is_ipv6 ) = $host_data; } - $rows[] = array( + $rows[] = [ 'name' => 'Uses Socket', 'value' => ! empty( $socket ), - ); - $rows[] = array( + ]; + $rows[] = [ 'name' => 'Uses IPv6', 'value' => ! empty( $is_ipv6 ), - ); + ]; } - $rows[] = array( + $rows[] = [ 'name' => 'Matomo tables found', 'value' => $this->get_num_matomo_tables(), - ); - - $missing_tables = $this->get_missing_tables(); - $has_missing_tables = ( count($missing_tables) > 0 ); - $rows[] = array( - 'name' => 'DB tables exist', - 'value' => ( ! $has_missing_tables ) , - 'comment' => $has_missing_tables ? sprintf( __('Some tables may be missing: %s', 'matomo'), implode(', ', $missing_tables ) ) : '', - 'is_error' => $has_missing_tables - ); - - foreach (['user', 'site'] as $table) { - $rows[] = array( - 'name' => 'Matomo '.$table.'s found', - 'value' => $this->get_num_entries_in_table($table), - ); + ]; + + $missing_tables = $this->get_missing_tables(); + $has_missing_tables = ( count( $missing_tables ) > 0 ); + $rows[] = [ + 'name' => 'DB tables exist', + 'value' => ( ! $has_missing_tables ), + 'comment' => $has_missing_tables ? sprintf( __( 'Some tables may be missing: %s', 'matomo' ), implode( ', ', $missing_tables ) ) : '', + 'is_error' => $has_missing_tables, + ]; + + foreach ( [ 'user', 'site' ] as $table ) { + $rows[] = [ + 'name' => 'Matomo ' . $table . 's found', + 'value' => $this->get_num_entries_in_table( $table ), + ]; } $grants = $this->get_db_grants(); // we only show these grants for security reasons as only they are needed and we don't need to know any other ones - $needed_grants = array( 'SELECT', 'INSERT', 'UPDATE', 'INDEX', 'DELETE', 'CREATE', 'DROP', 'ALTER', 'CREATE TEMPORARY TABLES', 'LOCK TABLES' ); + $needed_grants = [ + 'SELECT', + 'INSERT', + 'UPDATE', + 'INDEX', + 'DELETE', + 'CREATE', + 'DROP', + 'ALTER', + 'CREATE TEMPORARY TABLES', + 'LOCK TABLES', + ]; if ( in_array( 'ALL PRIVILEGES', $grants, true ) ) { // ALL PRIVILEGES may be used pre MySQL 8.0 $grants = $needed_grants; @@ -1243,27 +1313,27 @@ private function get_db_info() { if ( empty( $grants ) || ! is_array( $grants ) || count( $grants_missing ) === count( $needed_grants ) ) { - $rows[] = array( + $rows[] = [ 'name' => esc_html__( 'Required permissions', 'matomo' ), 'value' => esc_html__( 'Failed to detect granted permissions', 'matomo' ), 'comment' => esc_html__( 'Please check your MySQL user has these permissions (grants):', 'matomo' ) . '
' . implode( ', ', $needed_grants ), 'is_warning' => false, - ); + ]; } else { if ( ! empty( $grants_missing ) ) { - $rows[] = array( + $rows[] = [ 'name' => esc_html__( 'Required permissions', 'matomo' ), 'value' => esc_html__( 'Error', 'matomo' ), 'comment' => esc_html__( 'Missing permissions', 'matomo' ) . ': ' . implode( ', ', $grants_missing ) . '. ' . __( 'Please check if any of these MySQL permission (grants) are missing and add them if needed.', 'matomo' ) . ' ' . __( 'Learn more', 'matomo' ) . ': https://matomo.org/faq/troubleshooting/how-do-i-check-if-my-mysql-user-has-all-required-grants/', 'is_warning' => true, - ); + ]; } else { - $rows[] = array( + $rows[] = [ 'name' => esc_html__( 'Required permissions', 'matomo' ), 'value' => esc_html__( 'OK', 'matomo' ), 'comment' => '', 'is_warning' => false, - ); + ]; } } @@ -1276,31 +1346,33 @@ private function get_db_info() { public function get_missing_tables() { global $wpdb; - $required_matomo_tables = $this->dbSettings->get_matomo_tables(); - $required_matomo_tables = array_map( array( $this->dbSettings, 'prefix_table_name' ), $required_matomo_tables ); + $required_matomo_tables = $this->db_settings->get_matomo_tables(); + $required_matomo_tables = array_map( [ $this->db_settings, 'prefix_table_name' ], $required_matomo_tables ); - $existing_tables = array(); + $existing_tables = []; try { - $prefix = $this->dbSettings->prefix_table_name(''); + $prefix = $this->db_settings->prefix_table_name( '' ); $existing_tables = $wpdb->get_col( 'SHOW TABLES LIKE "' . $prefix . '%"' ); - } catch (\Exception $e) { + } catch ( Exception $e ) { $this->logger->log( 'no show tables: ' . $e->getMessage() ); } + return array_diff( $required_matomo_tables, $existing_tables ); } - private function get_num_entries_in_table($table) { + private function get_num_entries_in_table( $table ) { global $wpdb; - $prefix = $this->dbSettings->prefix_table_name($table); + $prefix = $this->db_settings->prefix_table_name( $table ); $results = null; try { - $results = $wpdb->get_var('select count(*) from '.$prefix); - } catch (\Exception $e) { + $results = $wpdb->get_var( 'select count(*) from ' . $prefix ); + } catch ( Exception $e ) { + $this->logger->log( 'no count(*): ' . $e->getMessage() ); } - if (isset($results) && is_numeric($results)) { + if ( isset( $results ) && is_numeric( $results ) ) { return $results; } @@ -1310,17 +1382,17 @@ private function get_num_entries_in_table($table) { private function get_num_matomo_tables() { global $wpdb; - $prefix = $this->dbSettings->prefix_table_name(''); + $prefix = $this->db_settings->prefix_table_name( '' ); $results = null; try { - $results = $wpdb->get_results('show tables like "'.$prefix.'%"'); - } catch (\Exception $e) { - $this->logger->log('no show tables: ' . $e->getMessage()); + $results = $wpdb->get_results( 'show tables like "' . $prefix . '%"' ); + } catch ( Exception $e ) { + $this->logger->log( 'no show tables: ' . $e->getMessage() ); } - if (is_array($results)) { - return count($results); + if ( is_array( $results ) ) { + return count( $results ); } return 'show tables not working'; @@ -1334,24 +1406,24 @@ private function get_db_grants() { try { $values = $wpdb->get_results( 'SHOW GRANTS', ARRAY_N ); - } catch ( \Exception $e ) { + } catch ( Exception $e ) { // We ignore any possible error in case of permission or not supported etc. - $values = array(); + $values = []; } $wpdb->suppress_errors( $suppress_errors ); - $grants = array(); + $grants = []; foreach ( $values as $index => $value ) { if ( empty( $value[0] ) || ! is_string( $value[0] ) ) { continue; } if ( stripos( $value[0], 'ALL PRIVILEGES' ) !== false ) { - return array( 'ALL PRIVILEGES' ); // the split on empty string wouldn't work otherwise + return [ 'ALL PRIVILEGES' ]; // the split on empty string wouldn't work otherwise } - foreach ( array( ' ON ', ' TO ', ' IDENTIFIED ', ' BY ' ) as $keyword ) { + foreach ( [ ' ON ', ' TO ', ' IDENTIFIED ', ' BY ' ] as $keyword ) { if ( stripos( $values[ $index ][0], $keyword ) !== false ) { // make sure to never show by any accident a db user or password by cutting anything after on/to $values[ $index ][0] = substr( $value[0], 0, stripos( $value[0], $keyword ) ); @@ -1362,40 +1434,48 @@ private function get_db_grants() { } } // make sure to never show by any accident a db user or password - $values[ $index ][0] = str_replace( array( DB_USER, DB_PASSWORD ), array( 'DB_USER', 'DB_PASS' ), $values[ $index ][0] ); + $values[ $index ][0] = str_replace( + [ DB_USER, DB_PASSWORD ], + [ + 'DB_USER', + 'DB_PASS', + ], + $values[ $index ][0] + ); $grants = array_merge( $grants, explode( ',', $values[ $index ][0] ) ); } $grants = array_map( 'trim', $grants ); $grants = array_map( 'strtoupper', $grants ); $grants = array_unique( $grants ); + return $grants; } private function get_plugins_info() { - $rows = array(); + $rows = []; $mu_plugins = get_mu_plugins(); if ( ! empty( $mu_plugins ) ) { - $rows[] = array( + $rows[] = [ 'section' => 'MU Plugins', - ); + ]; foreach ( $mu_plugins as $mu_pin ) { $comment = ''; if ( ! empty( $plugin['Network'] ) ) { $comment = 'Network enabled'; } - $rows[] = array( + $rows[] = [ 'name' => $mu_pin['Name'], 'value' => $mu_pin['Version'], 'comment' => $comment, - ); + ]; } - $rows[] = array( + $rows[] = [ 'section' => 'Plugins', - ); + ]; } $plugins = get_plugins(); @@ -1405,79 +1485,76 @@ private function get_plugins_info() { if ( ! empty( $plugin['Network'] ) ) { $comment = 'Network enabled'; } - $rows[] = array( + $rows[] = [ 'name' => $plugin['Name'], 'value' => $plugin['Version'], 'comment' => $comment, - ); + ]; } - $active_plugins = get_option( 'active_plugins', array() ); + $active_plugins = get_option( 'active_plugins', [] ); if ( ! empty( $active_plugins ) && is_array( $active_plugins ) ) { $active_plugins = array_map( function ( $active_plugin ) { $parts = explode( '/', trim( $active_plugin ) ); + return trim( $parts[0] ); }, $active_plugins ); - $rows[] = array( + $rows[] = [ 'name' => 'Active Plugins', 'value' => count( $active_plugins ), 'comment' => implode( ' ', $active_plugins ), - ); + ]; $used_not_compatible = array_intersect( $active_plugins, $this->not_compatible_plugins ); if ( ! empty( $used_not_compatible ) ) { - $additional_comment = ''; - if (in_array('tweet-old-post-pro', $used_not_compatible)) { + if ( in_array( 'tweet-old-post-pro', $used_not_compatible, true ) ) { $additional_comment .= '

A workaround for Revive Old Posts Pro may be to add the following line to your "wp-config.php".
define( \'MATOMO_SUPPORT_ASYNC_ARCHIVING\', false );.'; } - if (in_array('secupress', $used_not_compatible)) { + if ( in_array( 'secupress', $used_not_compatible, true ) ) { $additional_comment .= '

If reports aren\'t being generated then you may need to disable the feature "Firewall -> Block Bad Request Methods" in SecuPress (if it is enabled) or add the following line to your "wp-config.php":
define( \'MATOMO_SUPPORT_ASYNC_ARCHIVING\', false );.'; } $is_warning = true; - $is_error = false; - if (in_array('cookiebot', $used_not_compatible)) { + $is_error = false; + if ( in_array( 'cookiebot', $used_not_compatible, true ) ) { $is_warning = false; - $is_error = true; + $is_error = true; } - $rows[] = array( - 'name' => __( 'Not compatible plugins', 'matomo' ), - 'value' => count( $used_not_compatible ), - 'comment' => implode( ', ', $used_not_compatible ) . '

Matomo may work fine when using these plugins but there may be some issues. For more information see
https://matomo.org/faq/wordpress/which-plugins-is-matomo-for-wordpress-known-to-be-not-compatible-with/ ' . $additional_comment, + $rows[] = [ + 'name' => __( 'Not compatible plugins', 'matomo' ), + 'value' => count( $used_not_compatible ), + 'comment' => implode( ', ', $used_not_compatible ) . '

Matomo may work fine when using these plugins but there may be some issues. For more information see
https://matomo.org/faq/wordpress/which-plugins-is-matomo-for-wordpress-known-to-be-not-compatible-with/ ' . $additional_comment, 'is_warning' => $is_warning, - 'is_error' => $is_error, - ); + 'is_error' => $is_error, + ]; } } - $rows[] = array( - 'name' => 'Theme', - 'value' => function_exists('get_template') ? get_template() : '', - 'comment' => get_option('stylesheet') - ); - + $rows[] = [ + 'name' => 'Theme', + 'value' => function_exists( 'get_template' ) ? get_template() : '', + 'comment' => get_option( 'stylesheet' ), + ]; - if ( is_plugin_active('better-wp-security/better-wp-security.php')) { - if (method_exists('\ITSEC_Modules', 'get_setting') - && \ITSEC_Modules::get_setting( 'system-tweaks', 'long_url_strings' ) ) { - $rows[] = array( + if ( is_plugin_active( 'better-wp-security/better-wp-security.php' ) ) { + if ( method_exists( '\ITSEC_Modules', 'get_setting' ) + && ITSEC_Modules::get_setting( 'system-tweaks', 'long_url_strings' ) ) { + $rows[] = [ 'name' => 'iThemes Security Long URLs Enabled', 'value' => true, 'comment' => 'Tracking might not work because it looks like you have Long URLs disabled in iThemes Security. To fix this please go to "Security -> Settings -> System Tweaks" and disable the setting "Long URL Strings".', 'is_error' => true, - ); + ]; } } return $rows; } - - } diff --git a/classes/WpMatomo/Admin/TrackingSettings.php b/classes/WpMatomo/Admin/TrackingSettings.php index a93ea284b..260cc1feb 100644 --- a/classes/WpMatomo/Admin/TrackingSettings.php +++ b/classes/WpMatomo/Admin/TrackingSettings.php @@ -9,6 +9,7 @@ namespace WpMatomo\Admin; +use Exception; use WpMatomo\Capabilities; use WpMatomo\Settings; use WpMatomo\Site; @@ -18,7 +19,10 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } - +/** + * @todo set up the nonce verification + * phpcs:disable WordPress.Security.NonceVerification.Missing + */ class TrackingSettings implements AdminSettingsInterface { const FORM_NAME = 'matomo'; const NONCE_NAME = 'matomo_settings'; @@ -59,7 +63,7 @@ public function can_user_manage() { } private function apply_settings() { - $keys_to_keep = array( + $keys_to_keep = [ 'track_mode', 'track_across', 'track_across_alias', @@ -96,46 +100,45 @@ private function apply_settings() { 'track_js_endpoint', 'track_jserrors', 'track_api_endpoint', - Settings::SITE_CURRENCY - ); + Settings::SITE_CURRENCY, + ]; if ( matomo_has_tag_manager() ) { $keys_to_keep[] = 'tagmanger_container_ids'; } - $values = array(); + $values = []; // default value in case no role/ post type is selected to make sure we unset it if no role /post type is selected - $values['add_post_annotations'] = array(); - $values['tagmanger_container_ids'] = array(); + $values['add_post_annotations'] = []; + $values['tagmanger_container_ids'] = []; $valid_currencies = $this->get_supported_currencies(); - if ( !empty( $_POST[ self::FORM_NAME ]['tracker_debug'] ) ) { + if ( ! empty( $_POST[ self::FORM_NAME ]['tracker_debug'] ) ) { $site_config_sync = new SiteConfigSync( $this->settings ); - switch ($_POST[ self::FORM_NAME ]['tracker_debug']) { + switch ( $_POST[ self::FORM_NAME ]['tracker_debug'] ) { case 'always': - $site_config_sync->set_config_value('Tracker', 'debug', 1); - $site_config_sync->set_config_value('Tracker', 'debug_on_demand', 0); + $site_config_sync->set_config_value( 'Tracker', 'debug', 1 ); + $site_config_sync->set_config_value( 'Tracker', 'debug_on_demand', 0 ); break; case 'on_demand': - $site_config_sync->set_config_value('Tracker', 'debug', 0); - $site_config_sync->set_config_value('Tracker', 'debug_on_demand', 1); + $site_config_sync->set_config_value( 'Tracker', 'debug', 0 ); + $site_config_sync->set_config_value( 'Tracker', 'debug_on_demand', 1 ); break; default: - $site_config_sync->set_config_value('Tracker', 'debug', 0); - $site_config_sync->set_config_value('Tracker', 'debug_on_demand', 0); + $site_config_sync->set_config_value( 'Tracker', 'debug', 0 ); + $site_config_sync->set_config_value( 'Tracker', 'debug_on_demand', 0 ); } } - if ( empty( $_POST[ self::FORM_NAME ][Settings::SITE_CURRENCY] ) - || !array_key_exists( $_POST[ self::FORM_NAME ][Settings::SITE_CURRENCY], $valid_currencies ) ) { - $_POST[ self::FORM_NAME ][Settings::SITE_CURRENCY] = 'USD'; + if ( empty( $_POST[ self::FORM_NAME ][ Settings::SITE_CURRENCY ] ) + || ! array_key_exists( sanitize_text_field( wp_unslash( $_POST[ self::FORM_NAME ][ Settings::SITE_CURRENCY ] ) ), $valid_currencies ) ) { + $_POST[ self::FORM_NAME ][ Settings::SITE_CURRENCY ] = 'USD'; } if ( ! empty( $_POST[ self::FORM_NAME ]['track_mode'] ) ) { - $track_mode = $_POST[ self::FORM_NAME ]['track_mode']; - + $track_mode = $this->get_track_mode(); if ( self::TRACK_MODE_TAGMANAGER === $track_mode ) { // no noscript mode in this case $_POST[ self::FORM_NAME ]['track_noscript'] = ''; @@ -146,12 +149,18 @@ private function apply_settings() { if ( $this->must_update_tracker() === true ) { // We want to keep the tracking code when user switches between disabled and manually or disabled to disabled. if ( ! empty( $_POST[ self::FORM_NAME ]['tracking_code'] ) ) { + // don't process, this is a script + // phpcs:disable WordPress.Security.ValidatedSanitizedInput $_POST[ self::FORM_NAME ]['tracking_code'] = stripslashes( $_POST[ self::FORM_NAME ]['tracking_code'] ); + // phpcs:enable WordPress.Security.ValidatedSanitizedInput } else { $_POST[ self::FORM_NAME ]['tracking_code'] = ''; } if ( ! empty( $_POST[ self::FORM_NAME ]['noscript_code'] ) ) { + // don't process, this is a script + // phpcs:disable WordPress.Security.ValidatedSanitizedInput $_POST[ self::FORM_NAME ]['noscript_code'] = stripslashes( $_POST[ self::FORM_NAME ]['noscript_code'] ); + // phpcs:enable WordPress.Security.ValidatedSanitizedInput } else { $_POST[ self::FORM_NAME ]['noscript_code'] = ''; } @@ -160,42 +169,50 @@ private function apply_settings() { $_POST[ self::FORM_NAME ]['tracking_code'] = ''; } } - + // phpcs:disable WordPress.Security.ValidatedSanitizedInput foreach ( $_POST[ self::FORM_NAME ] as $name => $value ) { if ( in_array( $name, $keys_to_keep, true ) ) { $values[ $name ] = $value; } } - + // phpcs:enable WordPress.Security.ValidatedSanitizedInput $this->settings->apply_tracking_related_changes( $values ); return true; } + private function get_track_mode() { + if ( ! empty( $_POST[ self::FORM_NAME ]['track_mode'] ) ) { + return sanitize_text_field( wp_unslash( $_POST[ self::FORM_NAME ]['track_mode'] ) ); + } + return ''; + } /** * Reauires form to be posted + * * @return bool */ - private function must_update_tracker () { - $track_mode = $_POST[ self::FORM_NAME ]['track_mode']; + private function must_update_tracker() { + $track_mode = $this->get_track_mode(); $previus_track_mode = $this->settings->get_global_option( 'track_mode' ); $must_update = false; if ( self::TRACK_MODE_MANUALLY === $track_mode - || (self::TRACK_MODE_DISABLED === $track_mode && - in_array( $previus_track_mode, array( self::TRACK_MODE_DISABLED, self::TRACK_MODE_MANUALLY ) )) ) { + || ( self::TRACK_MODE_DISABLED === $track_mode && + in_array( $previus_track_mode, [ self::TRACK_MODE_DISABLED, self::TRACK_MODE_MANUALLY ], true ) ) ) { // We want to keep the tracking code when user switches between disabled and manually or disabled to disabled. $must_update = true; } + return $must_update; } /** * @return bool */ - private function form_submitted () { - return isset( $_POST ) && ! empty( $_POST[ self::FORM_NAME ] ) - && is_admin() - && $this->can_user_manage(); + private function form_submitted() { + return isset( $_POST ) && ! empty( $_POST[ self::FORM_NAME ] ) + && is_admin() + && $this->can_user_manage(); } /** @@ -203,50 +220,55 @@ private function form_submitted () { * * @return bool */ - private function has_valid_html_comments ($field) { + private function has_valid_html_comments( $field ) { $valid = true; if ( $this->form_submitted() === true ) { if ( $this->must_update_tracker() === true ) { - if ( ! empty( $_POST[ self::FORM_NAME ][$field] ) ) { - $valid = $this->validate_html_comments( $_POST[ self::FORM_NAME ][$field] ); + if ( ! empty( $_POST[ self::FORM_NAME ][ $field ] ) ) { + // phpcs:disable WordPress.Security.ValidatedSanitizedInput + $valid = $this->validate_html_comments( $_POST[ self::FORM_NAME ][ $field ] ); + // phpcs:enable WordPress.Security.ValidatedSanitizedInput } } } + return $valid; } /** * @param string $html html content to validate + * * @returns boolean */ public function validate_html_comments( $html ) { - $opening = substr_count( $html, '' ); - return ( $opening === $closing ); + $opening = substr_count( $html, '' ); + + return ( $opening === $closing ); } public function show_settings() { - $was_updated = false; - $errors = []; + $was_updated = false; + $settings_errors = []; if ( $this->has_valid_html_comments( 'tracking_code' ) !== true ) { - $errors[] = __( 'Settings have not been saved. There is an issue with the HTML comments in the field "Tracking code". Make sure all opened comments () correctly.', 'matomo' ); + $settings_errors[] = __( 'Settings have not been saved. There is an issue with the HTML comments in the field "Tracking code". Make sure all opened comments () correctly.', 'matomo' ); } if ( $this->has_valid_html_comments( 'noscript_code' ) !== true ) { - $errors[] = __( 'Settings have not been saved. There is an issue with the HTML comments in the field "Noscript code". Make sure all opened comments () correctly.', 'matomo' ); + $settings_errors[] = __( 'Settings have not been saved. There is an issue with the HTML comments in the field "Noscript code". Make sure all opened comments () correctly.', 'matomo' ); } - if ( count($errors) === 0 ) { + if ( count( $settings_errors ) === 0 ) { $was_updated = $this->update_if_submitted(); } - $settings = $this->settings; + $settings = $this->settings; $containers = $this->get_active_containers(); - $track_modes = array( + $track_modes = [ self::TRACK_MODE_DISABLED => esc_html__( 'Disabled', 'matomo' ), self::TRACK_MODE_DEFAULT => esc_html__( 'Default tracking', 'matomo' ), self::TRACK_MODE_MANUALLY => esc_html__( 'Enter manually', 'matomo' ), - ); + ]; if ( ! empty( $containers ) ) { $track_modes[ self::TRACK_MODE_TAGMANAGER ] = esc_html__( 'Tag Manager', 'matomo' ); @@ -268,46 +290,46 @@ public function show_settings() { /** * @return string[] */ - private function get_cookie_consent_modes() - { + private function get_cookie_consent_modes() { $modes = []; - foreach(CookieConsent::get_available_options() as $option => $description) { - $modes[$option] = $description; + foreach ( CookieConsent::get_available_options() as $option => $description ) { + $modes[ $option ] = $description; } + return $modes; } - private function get_supported_currencies() - { - $all = include dirname( MATOMO_ANALYTICS_FILE ) . '/app/core/Intl/Data/Resources/currencies.php'; - $currencies = array(); - foreach ($all as $key => $single) { - $currencies[$key] = $single[0] . ' ' . $single[1]; + private function get_supported_currencies() { + $all = include dirname( MATOMO_ANALYTICS_FILE ) . '/app/core/Intl/Data/Resources/currencies.php'; + $currencies = []; + foreach ( $all as $key => $single ) { + $currencies[ $key ] = $single[0] . ' ' . $single[1]; } + return $currencies; } public function get_active_containers() { // we don't use Matomo API here to avoid needing to bootstrap Matomo which is slow and could break things - $containers = array(); + $containers = []; if ( matomo_has_tag_manager() ) { global $wpdb; - $dbsettings = new \WpMatomo\Db\Settings(); - $container_table = $dbsettings->prefix_table_name( 'tagmanager_container' ); + $db_settings = new \WpMatomo\Db\Settings(); + $container_table = $db_settings->prefix_table_name( 'tagmanager_container' ); try { + // phpcs:disable WordPress.DB $containers = $wpdb->get_results( sprintf( 'SELECT `idcontainer`, `name` FROM %s where `status` = "active"', $container_table ) ); - } catch ( \Exception $e ) { + // phpcs:enable WordPress.DB + } catch ( Exception $e ) { // table may not exist yet etc - $containers = array(); + $containers = []; } } - $by_id = array(); + $by_id = []; foreach ( $containers as $container ) { $by_id[ $container->idcontainer ] = $container->name; } return $by_id; } - - } diff --git a/classes/WpMatomo/Admin/TrackingSettings/Forms.php b/classes/WpMatomo/Admin/TrackingSettings/Forms.php index 8bb031eed..50c71c210 100644 --- a/classes/WpMatomo/Admin/TrackingSettings/Forms.php +++ b/classes/WpMatomo/Admin/TrackingSettings/Forms.php @@ -14,6 +14,7 @@ namespace WpMatomo\Admin\TrackingSettings; use Piwik\Config; +use WpMatomo; use WpMatomo\Admin\TrackingSettings; use WpMatomo\Bootstrap; use WpMatomo\Settings; @@ -21,7 +22,10 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } - +/** + * we deal with HTML + * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped + */ class Forms { /** * @var Settings @@ -59,7 +63,7 @@ public function get_description( $id, $description, $hide_description = true ) { * @param string $on_change javascript for onchange event (default: empty) */ public function show_checkbox( $id, $name, $description, $is_hidden = false, $group_name = '', $hide_description = true, $on_change = '' ) { - printf( ':settings->get_global_option( $id ) ? ' checked="checked"' : '' ) . ' onchange="jQuery(\'#%s\').val(this.checked?1:0);%s" /> %s', esc_html( $name ), $id, $on_change, $this->get_description( $id, $description, $hide_description ) ); + printf( ':settings->get_global_option( $id ) ? ' checked="checked"' : '' ) . ' onchange="jQuery(\'#%s\').val(this.checked?1:0);%s" /> %s', esc_html( $name ), esc_attr( $id ), $on_change, $this->get_description( $id, $description, $hide_description ) ); } /** @@ -92,8 +96,8 @@ public function show_textarea( $id, $name, $rows, $description, $is_hidden, $gro * * @param string $text Text to show */ - public function show_text( $text , $group_name = '' ) { - printf( '

%s

', $group_name, esc_html( $text ) ); + public function show_text( $text, $group_name = '' ) { + printf( '

%s

', esc_attr( $group_name ), esc_html( $text ) ); } /** @@ -101,8 +105,8 @@ public function show_text( $text , $group_name = '' ) { * * @param string $text Text to show */ - public function show_headline( $text , $group_name = '') { - printf( '

%s

', $group_name, esc_html( $text ) ); + public function show_headline( $text, $group_name = '' ) { + printf( '

%s

', esc_attr( $group_name ), esc_html( $text ) ); } /** @@ -134,23 +138,24 @@ public function show_input( $id, $name, $description, $is_hidden = false, $group * @param boolean $hide_description $hideDescription set to false to show description initially (default: true) * @param boolean $global set to false if the textarea shows a site-specific option (default: true) */ - public function show_select( $id, $name, $options = array(), $description = '', $on_change = '', $is_hidden = false, $group_name = '', $hide_description = true, $global = true ) { + public function show_select( $id, $name, $options = [], $description = '', $on_change = '', $is_hidden = false, $group_name = '', $hide_description = true, $global = true ) { $options_list = ''; - if ($id === 'tracker_debug' && !\WpMatomo::is_safe_mode() && !$this->settings->is_network_enabled()) { + if ( 'tracker_debug' === $id && ! WpMatomo::is_safe_mode() && ! $this->settings->is_network_enabled() ) { Bootstrap::do_bootstrap(); - if (Config::getInstance()->Tracker['debug']) { + if ( Config::getInstance()->Tracker['debug'] ) { $default = 'always'; - } elseif (Config::getInstance()->Tracker['debug_on_demand']) { + } elseif ( Config::getInstance()->Tracker['debug_on_demand'] ) { $default = 'on_demand'; } else { $default = 'disabled'; } } else { - $default = $global ? $this->settings->get_global_option( $id ) : $this->settings->get_option( $id ); + $default = $global ? $this->settings->get_global_option( $id ) : $this->settings->get_option( $id ); } if ( is_array( $options ) ) { foreach ( $options as $key => $value ) { + // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison $options_list .= sprintf( '', esc_attr( $key ), esc_html( $value ) ); } } @@ -172,5 +177,4 @@ public function show_select( $id, $name, $options = array(), $description = '', public function show_box( $type, $icon, $content ) { printf( '

%s

', esc_attr( $type ), esc_attr( $icon ), esc_html( $content ) ); } - } diff --git a/classes/WpMatomo/Admin/views/access.php b/classes/WpMatomo/Admin/views/access.php index 7073f6b53..71080f36d 100644 --- a/classes/WpMatomo/Admin/views/access.php +++ b/classes/WpMatomo/Admin/views/access.php @@ -12,10 +12,12 @@ use WpMatomo\Access; use WpMatomo\Admin\AccessSettings; +use WpMatomo\Capabilities; +use WpMatomo\Roles; /** @var Access $access */ -/** @var \WpMatomo\Roles $roles */ -/** @var \WpMatomo\Capabilities $capabilites */ +/** @var Roles $roles */ +/** @var Capabilities $capabilites */ ?>

@@ -35,7 +37,7 @@ foreach ( $roles->get_available_roles_for_configuration() as $matomo_role_id => $matomo_role_name ) { echo ''; echo esc_html( $matomo_role_name ) . ''; - echo ""; $matomo_value = $access->get_permission_for_role( $matomo_role_id ); foreach ( Access::$matomo_permissions as $matomo_permission => $matomo_display_name ) { echo "'; @@ -52,30 +54,34 @@

- + - , + , , - , + ,
- +

- + 'matomo' + ) + ?>

    get_matomo_roles() as $matomo_role_config ) { ?> @@ -85,18 +91,20 @@

    - + 'matomo' + ) + ?>

      get_all_capabilities_sorted_by_highest_permission() as $matomo_cap_name ) { ?>
    • - -
    \ No newline at end of file + +
diff --git a/classes/WpMatomo/Admin/views/advanced_settings.php b/classes/WpMatomo/Admin/views/advanced_settings.php index 5cf3b5f99..a8089e583 100644 --- a/classes/WpMatomo/Admin/views/advanced_settings.php +++ b/classes/WpMatomo/Admin/views/advanced_settings.php @@ -10,7 +10,10 @@ * https://github.com/braekling/WP-Matomo * */ - +/** + * phpcs consider all our variables as global and want them prefixed with matomo + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + */ use WpMatomo\Admin\AdvancedSettings; if ( ! defined( 'ABSPATH' ) ) { @@ -29,45 +32,55 @@
-

- - - - - + + + + + + + + + + + + + +
- +

+ + + + + - - - - - - - - - - - - - -
+ REMOTE_ADDR ' . ( ! empty( $_SERVER[ 'REMOTE_ADDR' ] ) ? esc_html( $_SERVER[ 'REMOTE_ADDR' ] ) : esc_html__( 'No value found', 'matomo' ) ) . ' (' . esc_html__( 'Default', 'matomo' ) .')'; + echo ' REMOTE_ADDR ' . ( ! empty( $_SERVER['REMOTE_ADDR'] ) ? esc_html( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) : esc_html__( 'No value found', 'matomo' ) ) . ' (' . esc_html__( 'Default', 'matomo' ) . ')'; foreach ( AdvancedSettings::$valid_host_headers as $host_header ) { - echo ' ' . $host_header . ' ' . ( ! empty( $_SERVER[ $host_header ] ) ? (''. esc_html( $_SERVER[ $host_header ] ) . '') : esc_html__( 'No value found', 'matomo' ) ) . '   '; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo ' ' . esc_html( $host_header ) . ' ' . ( ! empty( $_SERVER[ $host_header ] ) ? ( '' . esc_html( sanitize_text_field( wp_unslash( $_SERVER[ $host_header ] ) ) ) . '' ) : esc_html__( 'No value found', 'matomo' ) ) . '   '; } ?> - - -
- ', '') ?>

- -
- - '.esc_html__( 'Yes', 'matomo' ).''; - ?> - - By default, when you uninstall the Matomo plugin, all data is deleted and cannot be restored unless you have backups. When you disable this feature, the tracked data in the database will be kept. This can be useful to prevent accidental deletion of all your historical analytics data when you uninstall the plugin. Learn more -

- \ No newline at end of file +
+ +
+ ', '' ); ?> +

+ +
+ + ' . esc_html__( 'Yes', 'matomo' ) . ''; + ?> + + By default, when you uninstall the Matomo plugin, all data is deleted and cannot be restored unless + you have backups. When you disable this feature, the tracked data in the database will be kept. This + can be useful to prevent accidental deletion of all your historical analytics data when you + uninstall the plugin. Learn more +

+ diff --git a/classes/WpMatomo/Admin/views/exclusion_settings.php b/classes/WpMatomo/Admin/views/exclusion_settings.php index 687ac6dda..6d738b299 100644 --- a/classes/WpMatomo/Admin/views/exclusion_settings.php +++ b/classes/WpMatomo/Admin/views/exclusion_settings.php @@ -7,13 +7,13 @@ * @package matomo * Code Based on * @author André Bräkling - * @package WP_Matomo * https://github.com/braekling/matomo * */ use Piwik\Piwik; use WpMatomo\Admin\ExclusionSettings; +use WpMatomo\Settings; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -25,7 +25,7 @@ /** @var string $excluded_user_agents */ /** @var string $excluded_query_params */ /** @var bool|string|int $keep_url_fragments */ -/** @var \WpMatomo\Settings $settings */ +/** @var Settings $settings */ ?> @@ -34,120 +34,132 @@ include 'update_notice_clear_cache.php'; } ?> -is_network_enabled() && is_network_admin()) { ?> -

Exclusion settings

-

- Exclusion settings have to be configured on a per blog basis. - Should you wish to change any setting, please go to the Matomo exclusion settings within each blog. - We are hoping to improve this in the future. -

+is_network_enabled() && is_network_admin() ) { ?> +

Exclusion settings

+

+ Exclusion settings have to be configured on a per blog basis. + Should you wish to change any setting, please go to the Matomo exclusion settings within each blog. + We are hoping to improve this in the future. +

-
- + + -

- - +

+
+ - - - - - - - - - + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - + + + + + - - - - + + + + + - -
: - - get_global_option( $matomo_tracking_caps ); - foreach ( $wp_roles->role_names as $matomo_key => $matomo_name ) { - echo ' ' . esc_html( $matomo_name ) . '  
'; - } - ?> -
- ', '' ); ?> - is_network_enabled()) { ?> -

This setting will be applied to all blogs. Changing it here also changes it for other blogs.

- -
: - - %2$s', 'excluded_ips', esc_html( $excluded_ips ) ); ?> - - + : + + get_global_option( $matomo_tracking_caps ); + foreach ( $wp_roles->role_names as $matomo_key => $matomo_name ) { + echo ' ' . esc_html( $matomo_name ) . '  
'; + } + ?> +
+ ', '' ); ?> + is_network_enabled() ) { ?> +

This setting will be applied to all blogs. Changing it here also changes it for + other blogs.

+ +
+ : + + %2$s', 'excluded_ips', esc_html( $excluded_ips ) ); ?> + + -
- -
: - - %2$s', 'excluded_query_parameters', esc_html( $excluded_query_params ) ); ?> - - - -
: - - %2$s', 'excluded_user_agents', esc_html( $excluded_user_agents ) ); ?> - + ?> +
+ +
+ : + + %2$s', 'excluded_query_parameters', esc_html( $excluded_query_params ) ); ?> + + + +
+ : + + %2$s', 'excluded_user_agents', esc_html( $excluded_user_agents ) ); ?> + - -
- - + +
+ + -
: - - ', 'keep_url_fragments', $keep_url_fragments ? ' checked="checked"' : '' ); ?> - +
+ : + + ', 'keep_url_fragments', $keep_url_fragments ? ' checked="checked"' : '' ); + ?> + - #', - 'example.org/index.html#first_section', - 'example.org/index.html', + #', + 'example.org/index.html#first_section', + 'example.org/index.html', + ] + ) ) - ) - ?> -
- + ?> +
+ -
-

-
+

+
-
+ + + - \ No newline at end of file + diff --git a/classes/WpMatomo/Admin/views/geolocation_settings.php b/classes/WpMatomo/Admin/views/geolocation_settings.php index c90b128e1..600b9dd10 100644 --- a/classes/WpMatomo/Admin/views/geolocation_settings.php +++ b/classes/WpMatomo/Admin/views/geolocation_settings.php @@ -7,9 +7,7 @@ * @package matomo * Code Based on * @author André Bräkling - * @package WP_Matomo * https://github.com/braekling/matomo - * */ use WpMatomo\Admin\GeolocationSettings; @@ -21,11 +19,11 @@ /** @var bool $invalid_format */ /** @var string $current_maxmind_license */ -if ($invalid_format) { ?> -
-

-
- +
+

+
+ @@ -33,39 +31,43 @@

- -

-

- -

- ', '' - ); - ?> -

+ +

+

+ +

+ ', + '' + ); + ?> +

- + -
- : + + : + id="" + name="" + value=""> + + +

+

+ +

+ + +

- -

- -

- - -

-
diff --git a/classes/WpMatomo/Admin/views/get_started.php b/classes/WpMatomo/Admin/views/get_started.php index 46b9fcfd8..82e27f1e8 100644 --- a/classes/WpMatomo/Admin/views/get_started.php +++ b/classes/WpMatomo/Admin/views/get_started.php @@ -6,17 +6,21 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @package matomo */ - +/** + * phpcs considers all of our variables as global and want them prefixed with matomo + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + */ use WpMatomo\Admin\AdminSettings; +use WpMatomo\Admin\GetStarted; use WpMatomo\Admin\Menu; use WpMatomo\Admin\TrackingSettings; -use WpMatomo\Admin\GetStarted; +use WpMatomo\Settings; if ( ! defined( 'ABSPATH' ) ) { exit; } -/** @var \WpMatomo\Settings $settings */ +/** @var Settings $settings */ /** @var bool $can_user_edit */ /** @var bool $was_updated */ /** @var bool $show_this_page */ @@ -38,15 +42,18 @@ ?> is_tracking_enabled() ) { ?> -

1.

-

+

1.

+

+ +

1.

- +
@@ -54,17 +61,18 @@

2.

- [matomo_opt_out]', '', '' ); ?> + [matomo_opt_out]', '', '' ); ?> - ', '', '', ''); ?> + ', '', '', '' ); ?>

3.

- +


@@ -73,5 +81,6 @@ + require 'info_help.php'; + ?> diff --git a/classes/WpMatomo/Admin/views/info.php b/classes/WpMatomo/Admin/views/info.php index b9a6f5f97..47246a456 100644 --- a/classes/WpMatomo/Admin/views/info.php +++ b/classes/WpMatomo/Admin/views/info.php @@ -6,8 +6,11 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @package matomo */ - -use \WpMatomo\Admin\Menu; +/** + * phpcs considers all of our variables as global and want them prefixed with matomo + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + */ +use WpMatomo\Admin\Menu; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -26,36 +29,36 @@

- ', - '', - '', - '' - ); - ?> + 'matomo' + ), + '', + '', + '', + '' + ); + ?>

- Matomo will always cost you nothing to use, but that doesn't mean it costs us nothing to make. - Matomo needs your continued support to grow and thrive. - ', + '', '', '', '' ); ?> - Every penny will help. + Every penny will help.

@@ -69,18 +72,18 @@
  • + class="dashicons-before dashicons-email">
  • + class="dashicons-before dashicons-facebook"> Facebook
  • + class="dashicons-before dashicons-twitter"> Twitter
  • diff --git a/classes/WpMatomo/Admin/views/info_bug_report.php b/classes/WpMatomo/Admin/views/info_bug_report.php index 67e81a080..9fec63815 100644 --- a/classes/WpMatomo/Admin/views/info_bug_report.php +++ b/classes/WpMatomo/Admin/views/info_bug_report.php @@ -13,17 +13,17 @@ ?>

    -', - '', - '', - '', - '', - '', - '', - '' -); -?> -

    + ', + '', + '', + '', + '', + '', + '', + '' + ); + ?> +

    diff --git a/classes/WpMatomo/Admin/views/info_help.php b/classes/WpMatomo/Admin/views/info_help.php index 69f652f73..737a2793a 100644 --- a/classes/WpMatomo/Admin/views/info_help.php +++ b/classes/WpMatomo/Admin/views/info_help.php @@ -7,6 +7,8 @@ * @package matomo */ +use WpMatomo\Admin\Menu; + if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } @@ -22,10 +24,12 @@ href="https://matomo.org/docs/"> -
  • -
  • +
  • -
  • -
  • +
  • -
  • -
  • - - -
  • +
  • + - +
diff --git a/classes/WpMatomo/Admin/views/info_high_traffic.php b/classes/WpMatomo/Admin/views/info_high_traffic.php index a91538870..164ef22c0 100644 --- a/classes/WpMatomo/Admin/views/info_high_traffic.php +++ b/classes/WpMatomo/Admin/views/info_high_traffic.php @@ -12,17 +12,17 @@ } ?>

If your website gets a lot of traffic we recommend installing Matomo + target="_blank" rel="noreferrer noopener">Matomo On-Premise separately (it's free as well) in combination with the WP-Matomo WordPress + target="_blank" + rel="noreferrer noopener">WP-Matomo WordPress plugin. It's free to install and has the same requirements as WordPress. Your Matomo will then run a lot faster and it allows you to put your Matomo installation on a separate server if needed.

Don't want all the hassle of maintaining a Matomo? Sign up + rel="noreferrer noopener" target="_blank">Sign up for a free Matomo Cloud trial. We can migrate all your data onto our Cloud for free. 100% data ownership guaranteed.

diff --git a/classes/WpMatomo/Admin/views/info_multisite.php b/classes/WpMatomo/Admin/views/info_multisite.php index 721181b0f..03534a0f1 100644 --- a/classes/WpMatomo/Admin/views/info_multisite.php +++ b/classes/WpMatomo/Admin/views/info_multisite.php @@ -6,11 +6,14 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @package matomo */ + +use WpMatomo\Settings; + if ( ! defined( 'ABSPATH' ) ) { exit; } -/** @var \WpMatomo\Settings $settings */ +/** @var Settings $settings */ ?>
@@ -29,25 +32,25 @@

- ', - '', - '', - '', - '', - '' - ); - ?> + 'matomo' + ), + '', + '', + '', + '', + '', + '' + ); + ?>

. + href="http://matomo.org/start-free-analytics-trial/" rel="noreferrer noopener" + target="_blank">.

@@ -57,8 +60,8 @@ foreach ( get_sites() as $matomo_site ) { /** @var WP_Site $matomo_site */ switch_to_blog( $matomo_site->blog_id ); - if (function_exists('is_plugin_active') && is_plugin_active('matomo/matomo.php')) { - echo '
  • ' . esc_html($matomo_site->blogname) . ' (Site ID: ' . esc_html($matomo_site->blog_id) . ')
  • '; + if ( function_exists( 'is_plugin_active' ) && is_plugin_active( 'matomo/matomo.php' ) ) { + echo '
  • ' . esc_html( $matomo_site->blogname ) . ' (Site ID: ' . esc_html( $matomo_site->blog_id ) . ')
  • '; } restore_current_blog(); } diff --git a/classes/WpMatomo/Admin/views/info_newsletter.php b/classes/WpMatomo/Admin/views/info_newsletter.php index 572a4cea9..65c290ce3 100644 --- a/classes/WpMatomo/Admin/views/info_newsletter.php +++ b/classes/WpMatomo/Admin/views/info_newsletter.php @@ -7,7 +7,7 @@ * @package matomo */ -use \WpMatomo\Admin\Info; +use WpMatomo\Admin\Info; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -15,15 +15,15 @@ /** @var bool $signedup_newsletter */ /** @var bool $show_newsletter */ -if ($signedup_newsletter) { -?> -
    -

    -
    - +
    +

    +
    + @@ -33,14 +33,14 @@

    - -

    -
    \ No newline at end of file + diff --git a/classes/WpMatomo/Admin/views/info_shared.php b/classes/WpMatomo/Admin/views/info_shared.php index e50eb3f1d..82a7cea5f 100644 --- a/classes/WpMatomo/Admin/views/info_shared.php +++ b/classes/WpMatomo/Admin/views/info_shared.php @@ -11,12 +11,12 @@ exit; // if accessed directly } ?> -

    +

    PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION, 'matomo' => $settings->get_global_option( 'core_version' ), 'wp_version' => ! empty( $GLOBALS['wp_version'] ) ? $GLOBALS['wp_version'] : '', - ) + ] ); ?>

    @@ -30,256 +30,287 @@
    -

    +

    -
    -

    ', '' ); ?> -

    -

    +

    +

    ', '' ); ?> +

    +

    - -

    -
    + +

    +
    ' . esc_html( $matomo_feature_section['title'] ) . ''; - echo '
    '; + echo '

    ' . esc_html( $matomo_feature_section['title'] ) . '

    '; + echo '
    '; - foreach ( $matomo_feature_section['features'] as $matomo_index => $matomo_feature ) { - $matomo_style = ''; - $matomo_is_3_columns = $matomo_num_features_in_block === 3; - if ( $matomo_is_3_columns ) { - $matomo_style = 'width: calc(33% - 8px);min-width:282px;max-width:350px;'; - if ( $matomo_index % 3 === 2 ) { - $matomo_style .= 'clear: inherit;margin-right: 0;margin-left: 16px;'; - } - } - ?> -
    - - + foreach ( $matomo_feature_section['features'] as $matomo_index => $matomo_feature ) { + $matomo_style = ''; + $matomo_is_3_columns = 3 === $matomo_num_features_in_block; + if ( $matomo_is_3_columns ) { + $matomo_style = 'width: calc(33% - 8px);min-width:282px;max-width:350px;'; + if ( 2 === $matomo_index % 3 ) { + $matomo_style .= 'clear: inherit;margin-right: 0;margin-left: 16px;'; + } + } + ?> +
    + + + -
    -
    +
    -

    - - - - - -

    -
    -
    +

    + + + + + + +

    +
    +
    -

    - '. esc_html__( 'Learn more', 'matomo' ).''; - } elseif (!empty($matomo_feature['url'])) { - echo ' '. esc_html__( 'Learn more', 'matomo' ).''; - } ?>

    -

    - - -

    -
    -
    -
    -
    '; - if (!empty($matomo_feature_section['more_url'])) { - echo ''. esc_html($matomo_feature_section['more_text']).''; - } - echo '
    '; - } - } + if ( ! $matomo_is_3_columns ) { + ?> + desc column-description + + " + style="margin-right: 0; + + "> +

    + ' . esc_html__( 'Learn more', 'matomo' ) . ''; + } elseif ( ! empty( $matomo_feature['url'] ) ) { + echo ' ' . esc_html__( 'Learn more', 'matomo' ) . ''; + } + ?> +

    + +

    + + +

    + +
    +
    +
    +
    '; + if ( ! empty( $matomo_feature_section['more_url'] ) ) { + echo '' . esc_html( $matomo_feature_section['more_text'] ) . ''; + } + echo '
    '; + } + } - $matomo_feature_sections = array( - array( - 'title' => 'Top free plugins', - 'more_url' => 'https://plugins.matomo.org/free?wp=1', + $matomo_feature_sections = [ + [ + 'title' => 'Top free plugins', + 'more_url' => 'https://plugins.matomo.org/free?wp=1', 'more_text' => 'Browse all free plugins', - 'features' => - array( - array( + 'features' => + [ + [ 'name' => 'Marketing Campaigns Reporting', 'description' => 'Measure the effectiveness of your marketing campaigns. Track up to five channels instead of two: campaign, source, medium, keyword, content.', 'price' => 'free', 'download_url' => 'https://plugins.matomo.org/api/2.0/plugins/MarketingCampaignsReporting/download/latest?wp=1' . $matomo_extra_url_params, 'url' => 'https://plugins.matomo.org/MarketingCampaignsReporting?wp=1&pk_campaign=WP&pk_source=Plugin', 'image' => '', - ), - array( + ], + [ 'name' => 'Custom Alerts', 'description' => 'Create custom Alerts to be notified of important changes on your website or app!', 'price' => 'free', 'download_url' => 'https://plugins.matomo.org/api/2.0/plugins/CustomAlerts/download/latest?wp=1' . $matomo_extra_url_params, 'url' => 'https://plugins.matomo.org/CustomAlerts?wp=1&pk_campaign=WP&pk_source=Plugin', 'image' => '', - ), - ), - ), - ); + ], + ], + ], + ]; - matomo_show_tables($matomo_feature_sections); + matomo_show_tables( $matomo_feature_sections ); - echo '
    '; + echo '
    '; - $matomo_feature_sections = array( - array( - 'title' => 'Most popular premium features', - 'features' => - array( - array( - 'name' => 'Heatmap & Session Recording', - 'description' => 'Truly understand your visitors by seeing where they click, hover, type and scroll. Replay their actions in a video and ultimately increase conversions.', - 'price' => '99EUR / 119USD', - 'url' => 'https://plugins.matomo.org/HeatmapSessionRecording?wp=1', - 'image' => '', - ), - array( - 'name' => 'Custom Reports', - 'description' => 'Pull out the information you need in order to be successful. Develop your custom strategy to meet your individualized goals while saving money & time.', - 'price' => '99EUR / 119USD', - 'url' => 'https://plugins.matomo.org/CustomReports?wp=1', - 'image' => '', - ), + $matomo_feature_sections = [ + [ + 'title' => 'Most popular premium features', + 'features' => + [ + [ + 'name' => 'Heatmap & Session Recording', + 'description' => 'Truly understand your visitors by seeing where they click, hover, type and scroll. Replay their actions in a video and ultimately increase conversions.', + 'price' => '99EUR / 119USD', + 'url' => 'https://plugins.matomo.org/HeatmapSessionRecording?wp=1', + 'image' => '', + ], + [ + 'name' => 'Custom Reports', + 'description' => 'Pull out the information you need in order to be successful. Develop your custom strategy to meet your individualized goals while saving money & time.', + 'price' => '99EUR / 119USD', + 'url' => 'https://plugins.matomo.org/CustomReports?wp=1', + 'image' => '', + ], - array( - 'name' => 'Premium Bundle', - 'description' => 'All premium features in one bundle, make the most out of your Matomo for WordPress and enjoy discounts of over 25%!', - 'price' => '499EUR / 579USD', - 'url' => 'https://plugins.matomo.org/WpPremiumBundle?wp=1', - 'image' => '', - ) - ), - ), - array( + [ + 'name' => 'Premium Bundle', + 'description' => 'All premium features in one bundle, make the most out of your Matomo for WordPress and enjoy discounts of over 25%!', + 'price' => '499EUR / 579USD', + 'url' => 'https://plugins.matomo.org/WpPremiumBundle?wp=1', + 'image' => '', + ], + ], + ], + [ 'title' => 'Most popular content engagement', 'features' => - array( - array( + [ + [ 'name' => 'Form Analytics', 'description' => 'Increase conversions on your online forms and lose less visitors by learning everything about your users behavior and their pain points on your forms.', 'price' => '79EUR / 89USD', 'url' => 'https://plugins.matomo.org/FormAnalytics?wp=1', 'image' => '', - ), - array( + ], + [ 'name' => 'Video & Audio Analytics', 'description' => 'Grow your business with advanced video & audio analytics. Get powerful insights into how your audience watches your videos and listens to your audio.', 'price' => '79EUR / 89USD', 'url' => 'https://plugins.matomo.org/MediaAnalytics?wp=1', 'image' => '', - ), - array( + ], + [ 'name' => 'Users Flow', 'description' => 'Users Flow is a visual representation of the most popular paths your users take through your website & app which lets you understand your users needs.', 'price' => '39EUR / 39USD', 'url' => 'https://plugins.matomo.org/UsersFlow?wp=1', 'image' => '', - ), - ), - ), - array( + ], + ], + ], + [ 'title' => 'Most popular acquisition & SEO features', 'features' => - array( - array( + [ + [ 'name' => 'Search Engine Keywords Performance', 'description' => 'All keywords searched by your users on search engines are now visible into your Referrers reports! The ultimate solution to \'Keyword not defined\'.', 'price' => '69EUR / 79USD', 'url' => 'https://plugins.matomo.org/SearchEngineKeywordsPerformance?wp=1', 'image' => '', - ), - array( + ], + [ 'name' => 'Advertising Conversion Export', 'description' => 'Provides an export of attributed goal conversions for usage in ad networks like Google Ads so you no longer need a conversion pixel.', 'price' => '79EUR / 89USD', 'url' => 'https://plugins.matomo.org/AdvertisingConversionExport?wp=1', 'image' => '', - ), - array( + ], + [ 'name' => 'Multi Attribution', 'description' => 'Get a clear understanding of how much credit each of your marketing channel is actually responsible for to shift your marketing efforts wisely.', 'price' => '39EUR / 39USD', 'url' => 'https://plugins.matomo.org/MultiChannelConversionAttribution?wp=1', 'image' => '', - ), - /* - array( - 'name' => 'Activity Log', - 'description' => 'Truly understand your visitors by seeing where they click, hover, type and scroll. Replay their actions in a video and ultimately increase conversions', - 'price' => '19EUR / 19USD', - 'url' => 'https://plugins.matomo.org/ActivityLog?wp=1', - 'image' => '', - ),*/ - ), - ), - array( + ], + ], + ], + [ 'title' => 'Other premium features', 'features' => - array( - array( + [ + [ 'name' => 'Funnels', 'description' => 'Identify and understand where your visitors drop off to increase your conversions, sales and revenue with your existing traffic.', 'price' => '89EUR / 99USD', 'url' => 'https://plugins.matomo.org/Funnels?wp=1', 'image' => '', - ), - array( + ], + [ 'name' => 'Cohorts', 'description' => 'Track your retention efforts over time and keep your visitors engaged and coming back for more.', 'price' => '49EUR / 59USD', 'url' => 'https://plugins.matomo.org/Cohorts?wp=1', 'image' => '', - ), - ), - ), - ); + ], + ], + ], + ]; - matomo_show_tables($matomo_feature_sections); + matomo_show_tables( $matomo_feature_sections ); ?> diff --git a/classes/WpMatomo/Admin/views/privacy_gdpr.php b/classes/WpMatomo/Admin/views/privacy_gdpr.php index 8229e214e..bab3e5b94 100644 --- a/classes/WpMatomo/Admin/views/privacy_gdpr.php +++ b/classes/WpMatomo/Admin/views/privacy_gdpr.php @@ -11,81 +11,89 @@ use WpMatomo\Admin\Menu; use WpMatomo\Admin\PrivacySettings; +use WpMatomo\Settings; if ( ! defined( 'ABSPATH' ) ) { exit; } -/** @var \WpMatomo\Settings $matomo_settings */ +/** @var Settings $matomo_settings */ ?>

    + class="matomo-blockquote">

    . ', '' ); ?>

    -is_network_enabled() && is_network_admin()) { ?> -

    Configure privacy settings

    -

    - Currently, privacy settings have to be configured on a per blog basis. - IP addresses are anonmyised by default. Should you wish to change any privacy setting, please go to the Matomo privacy settings within each blog. - We are hoping to improve this in the future. -

    +is_network_enabled() && is_network_admin() ) { ?> +

    Configure privacy settings

    +

    + Currently, privacy settings have to be configured on a per blog basis. + IP addresses are anonmyised by default. Should you wish to change any privacy setting, please go to the Matomo + privacy settings within each blog. + We are hoping to improve this in the future. +

    -

    - -

    -

    -

    +

    + +

    +

    +

    -
      -
    • - -
    • -
    • - -
    • -
    • - - () -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    +
      +
    • + +
    • +
    • + +
    • +
    • + + () +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +

    -

    +

    +
      +
    • ' . esc_html( PrivacySettings::EXAMPLE_MINIMAL ) . '' ); ?> -
      +
      -

        -
      • language - eg de or en.
      • +
      • language - eg de or + en.
      • +
      + +: +
    • +
    -

    :

    diff --git a/classes/WpMatomo/Admin/views/settings.php b/classes/WpMatomo/Admin/views/settings.php index 29a4c1f61..bea7ead96 100644 --- a/classes/WpMatomo/Admin/views/settings.php +++ b/classes/WpMatomo/Admin/views/settings.php @@ -11,6 +11,7 @@ use WpMatomo\Admin\AdminSettingsInterface; use WpMatomo\Admin\Menu; use WpMatomo\Capabilities; +use WpMatomo\Settings; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -18,37 +19,39 @@ /** @var AdminSettingsInterface[] $setting_tabs */ /** @var AdminSettingsInterface $content_tab */ /** @var string $active_tab */ -/** @var \WpMatomo\Settings $matomo_settings */ +/** @var Settings $matomo_settings */ ?>
    -

    - is_network_enabled() && is_network_admin() ) { - echo '

    You are running Matomo in network mode. This means below settings will be applied to all blogs in your network.

    '; - } elseif ($matomo_settings->is_network_enabled() && !is_network_admin()) { - echo '

    '; - esc_html_e('You are running Matomo in network mode.', 'matomo'); - echo ' '; - echo 'Below settings aren\'t applied for all blogs but have to be configured for each blog separately. We are hoping to improve this in the future. Any setting within the Matomo admin is configured on a per blog basis as well. Only you as a Matomo super user can see these settings.

    '; - } - ?> +

    + is_network_enabled() && is_network_admin() ) { + echo '

    You are running Matomo in network mode. This means below settings will be applied to all blogs in your network.

    '; + } elseif ( $matomo_settings->is_network_enabled() && ! is_network_admin() ) { + echo '

    '; + esc_html_e( 'You are running Matomo in network mode.', 'matomo' ); + echo ' '; + echo 'Below settings aren\'t applied for all blogs but have to be configured for each blog separately. We are hoping to improve this in the future. Any setting within the Matomo admin is configured on a per blog basis as well. Only you as a Matomo super user can see these settings.

    '; + } + ?> - show_settings(); ?> + show_settings(); ?>
    diff --git a/classes/WpMatomo/Admin/views/settings_errors.php b/classes/WpMatomo/Admin/views/settings_errors.php index f873e4a9f..0e2892068 100644 --- a/classes/WpMatomo/Admin/views/settings_errors.php +++ b/classes/WpMatomo/Admin/views/settings_errors.php @@ -6,13 +6,17 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @package matomo */ -/** @var string[] $errors */ +/** + * phpcs considers all of our variables as global and want them prefixed with matomo + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + */ +/** @var string[] $settings_errors */ if ( ! defined( 'ABSPATH' ) ) { exit; } ?>
    - -

    + +

    diff --git a/classes/WpMatomo/Admin/views/summary.php b/classes/WpMatomo/Admin/views/summary.php index f52597b9b..c90ee5f47 100644 --- a/classes/WpMatomo/Admin/views/summary.php +++ b/classes/WpMatomo/Admin/views/summary.php @@ -6,8 +6,13 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @package matomo */ - +/** + * phpcs considers all of our variables as global and want them prefixed with matomo + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + */ +use WpMatomo\Admin\Dashboard; use WpMatomo\Admin\Menu; +use WpMatomo\Admin\Summary; use WpMatomo\Report\Dates; if ( ! defined( 'ABSPATH' ) ) { @@ -23,32 +28,34 @@ /** @var bool $matomo_pinned */ /** @var bool $is_tracking */ /** @var bool $matomo_is_version_pre55 */ -/** @var \WpMatomo\Admin\Dashboard $matomo_dashboard */ +/** @var Dashboard $matomo_dashboard */ global $wp; -$matomo_dashboard_nonce = wp_create_nonce(\WpMatomo\Admin\Summary::NONCE_DASHBOARD); +$matomo_dashboard_nonce = wp_create_nonce( Summary::NONCE_DASHBOARD ); ?>

    ' . esc_html__( 'Dashboard updated.', 'matomo' ) . '

    '; - } - if ($matomo_is_version_pre55) { - echo ''; - } +if ( $matomo_pinned ) { + echo '

    ' . esc_html__( 'Dashboard updated.', 'matomo' ) . '

    '; +} +if ( $matomo_is_version_pre55 ) { + echo ''; +} ?> -

    +
    +

    +
    -

    +

    ' . esc_html__( 'Reports for today are only refreshed approximately every hour through the WordPress cronjob.', 'matomo' ) . '
    '; } ?>

    -

    @@ -58,16 +65,16 @@ if ( $report_date === $matomo_report_date_key ) { $matomo_button_class = 'button-primary'; } - echo '' . esc_html( $matomo_report_name ) . ' '; + echo '' . esc_html( $matomo_report_name ) . ' '; } ?>

    $matomo_column_modulo ) { ?> -
    +
    $matomo_report_meta ) { @@ -77,58 +84,84 @@ $shortcode = sprintf( '[matomo_report unique_id=%s report_date=%s limit=10]', $matomo_report_meta['uniqueId'], $report_date ); ?>
    -
    -

    -

    -
    - - - + " style="color: inherit;text-decoration: none;" target="_blank" + rel="noreferrer noopener" + class="dashicons-before dashicons-external" aria-hidden="true"> + + - has_widget($matomo_report_meta['uniqueId'], $report_date); - ?> - + " style="color: inherit;text-decoration: none; + + " + class="dashicons-before dashicons-admin-post" aria-hidden="true"> + -
    +
    +
    - +
    - +

    diff --git a/classes/WpMatomo/Admin/views/systemreport.php b/classes/WpMatomo/Admin/views/systemreport.php index a4ee18526..bdbf929ef 100644 --- a/classes/WpMatomo/Admin/views/systemreport.php +++ b/classes/WpMatomo/Admin/views/systemreport.php @@ -6,7 +6,10 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @package matomo */ - +/** + * phpcs considers all of our variables as global and want them prefixed with matomo + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + */ if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -25,11 +28,11 @@ if ( ! function_exists( 'matomo_format_value_text' ) ) { function matomo_format_value_text( $value ) { if ( is_string( $value ) && ! empty( $value ) ) { - $matomo_format = array( + $matomo_format = [ '
    ' => ' ', '
    ' => ' ', '
    ' => ' ', - ); + ]; foreach ( $matomo_format as $search => $replace ) { $value = str_replace( $search, $replace, $value ); } @@ -44,9 +47,9 @@ function matomo_format_value_text( $value ) { -

    -

    -
    +
    +

    +
    @@ -56,12 +59,12 @@ function matomo_format_value_text( $value ) {
    -

    +

    @@ -78,7 +81,7 @@ class='button-primary'> ' . esc_html( $matomo_table['title'] ) . ""; foreach ( $matomo_table['rows'] as $matomo_row ) { if ( ! empty( $matomo_row['section'] ) ) { @@ -136,7 +139,7 @@ class='button-primary'>" . esc_html( $matomo_row['name'] ) . ''; echo "'; if ( ! empty( $matomo_table['has_comments'] ) ) { - $matomo_replaced_elements = array( + $matomo_replaced_elements = [ '' => '__#CODEBACKUP#__', '' => '__##CODEBACKUP##__', '
    ' => '__#PREBACKUP#__',
    @@ -144,10 +147,11 @@ class='button-primary'>'   => '__#BRBACKUP#__',
     						'
    ' => '__#BRBACKUP#__', '
    ' => '__#BRBACKUP#__', - ); + ]; $matomo_comment = isset( $matomo_row['comment'] ) ? $matomo_row['comment'] : ''; $matomo_replaced = str_replace( array_keys( $matomo_replaced_elements ), array_values( $matomo_replaced_elements ), $matomo_comment ); $matomo_escaped = esc_html( $matomo_replaced ); + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo "
    '; } @@ -163,51 +167,54 @@ class='button-primary'> - -

    - -

    - - -

    + +

    + +

    + + +

    - -

    - + +

    + is_network_enabled() || ! is_network_admin() ) { ?> - -

    - -

    - + +

    + +

    + is_network_enabled() ) { ?> - -

    + +

    @@ -219,11 +226,25 @@ class='button-primary' ?>

      -
    • -
    • -
    • -
    • -
    • +
    • + +
    • +
    • +
    • +
    • +
    • +
    • +
    • +

    @@ -235,15 +256,15 @@ class='button-primary'

    - DISABLE_WP_CRON', - 'wp-config.php', - '', - '' - ); - ?> + DISABLE_WP_CRON', + 'wp-config.php', + '', + '' + ); + ?>

    diff --git a/classes/WpMatomo/Admin/views/tracking.php b/classes/WpMatomo/Admin/views/tracking.php index 1c8fab281..263b7dab7 100644 --- a/classes/WpMatomo/Admin/views/tracking.php +++ b/classes/WpMatomo/Admin/views/tracking.php @@ -10,7 +10,9 @@ * https://github.com/braekling/WP-Matomo * */ - +/** + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + */ use WpMatomo\Admin\TrackingSettings; use WpMatomo\Paths; @@ -23,7 +25,7 @@ /** @var array $containers */ /** @var array $track_modes */ /** @var array $matomo_currencies */ -/** @var string[] $errors */ +/** @var string[] $settings_errors */ /** @var array $cookie_consent_modes */ $matomo_form = new \WpMatomo\Admin\TrackingSettings\Forms( $settings ); @@ -34,18 +36,18 @@ if ( $was_updated ) { include 'update_notice_clear_cache.php'; } -if ( count( $errors ) ) { - include 'settings_errors.php'; +if ( count( $settings_errors ) ) { + include 'settings_errors.php'; } ?>

    - - - -

    + + + +

    " . esc_html( $matomo_value ) . '" . str_replace( array_values( $matomo_replaced_elements ), array_keys( $matomo_replaced_elements ), $matomo_escaped ) . '
    @@ -56,22 +58,22 @@ $matomo_is_not_generated_tracking = $matomo_is_not_tracking || $settings->get_global_option( 'track_mode' ) === TrackingSettings::TRACK_MODE_MANUALLY; $matomo_full_generated_tracking_group = 'matomo-track-option matomo-track-option-default '; - $matomo_description = sprintf( '%s
    %s: %s
    %s: %s
    %s: %s
    %s: %s', esc_html__( 'You can choose between four tracking code modes:', 'matomo' ), esc_html__( 'Disabled', 'matomo' ), esc_html__( 'matomo will not add the tracking code. Use this, if you want to add the tracking code to your template files or you use another plugin to add the tracking code.', 'matomo' ), esc_html__( 'Default tracking', 'matomo' ), esc_html__( 'matomo will use Matomo\'s standard tracking code.', 'matomo' ) . ' ' .esc_html__('This mode is recommended for most use cases.', 'matomo'), esc_html__( 'Enter manually', 'matomo' ), esc_html__( 'Enter your own tracking code manually. You can choose one of the prior options, pre-configure your tracking code and switch to manually editing at last.', 'matomo' ) . ( $settings->is_network_enabled() ? ' ' . esc_html__( 'Use the placeholder {ID} to add the Matomo site ID.', 'matomo' ) : '' ), esc_html__( 'Tag Manager', 'matomo' ), esc_html__( 'If you have created containers in the Tag Manager, you can select one of them and it will embed the code for the container automatically.', 'matomo' ) ); + $matomo_description = sprintf( '%s
    %s: %s
    %s: %s
    %s: %s
    %s: %s', esc_html__( 'You can choose between four tracking code modes:', 'matomo' ), esc_html__( 'Disabled', 'matomo' ), esc_html__( 'matomo will not add the tracking code. Use this, if you want to add the tracking code to your template files or you use another plugin to add the tracking code.', 'matomo' ), esc_html__( 'Default tracking', 'matomo' ), esc_html__( 'matomo will use Matomo\'s standard tracking code.', 'matomo' ) . ' ' . esc_html__( 'This mode is recommended for most use cases.', 'matomo' ), esc_html__( 'Enter manually', 'matomo' ), esc_html__( 'Enter your own tracking code manually. You can choose one of the prior options, pre-configure your tracking code and switch to manually editing at last.', 'matomo' ) . ( $settings->is_network_enabled() ? ' ' . esc_html__( 'Use the placeholder {ID} to add the Matomo site ID.', 'matomo' ) : '' ), esc_html__( 'Tag Manager', 'matomo' ), esc_html__( 'If you have created containers in the Tag Manager, you can select one of them and it will embed the code for the container automatically.', 'matomo' ) ); $matomo_form->show_select( 'track_mode', esc_html__( 'Add tracking code', 'matomo' ), $track_modes, $matomo_description, 'jQuery(\'tr.matomo-track-option\').addClass(\'hidden\'); jQuery(\'tr.matomo-track-option-\' + jQuery(\'#track_mode\').val()).removeClass(\'hidden\'); jQuery(\'#tracking_code, #noscript_code\').prop(\'readonly\', jQuery(\'#track_mode\').val() != \'manually\');' ); - $matomo_manually_network = ''; - if ( $settings->is_network_enabled() ) { - $matomo_manually_network = ' ' . sprintf( esc_html__( 'You can use these variables: %1$s. %2$sLearn more%3$s', 'matomo' ), '{MATOMO_IDSITE}, {MATOMO_API_ENDPOINT}, {MATOMO_JS_ENDPOINT}', '', '' ); - } + $matomo_manually_network = ''; + if ( $settings->is_network_enabled() ) { + $matomo_manually_network = ' ' . sprintf( esc_html__( 'You can use these variables: %1$s. %2$sLearn more%3$s', 'matomo' ), '{MATOMO_IDSITE}, {MATOMO_API_ENDPOINT}, {MATOMO_JS_ENDPOINT}', '', '' ); + } if ( ! empty( $containers ) ) { echo ''; echo ''; } @@ -79,9 +81,9 @@ $matomo_form->show_textarea( 'tracking_code', esc_html__( 'Tracking code', 'matomo' ), 15, 'This is a preview of your current tracking code based on your configuration below. You don\'t need to do anything with it and this is purely for your information. If you choose to enter your tracking code manually, you can change it here. The tracking code is a piece of code that will be automatically embedded into your site and it is repsonsible for tracking your visitors. Have a look at the system report to get a list of all available JS tracker and tracking API endpoints. You don\'t need to embed this tracking code into your website, our plugin does this automatically.' . $matomo_manually_network, $matomo_is_not_tracking, 'matomo-track-option matomo-track-option-default matomo-track-option-tagmanager matomo-track-option-manually', ! $settings->is_network_enabled(), '', ( $settings->get_global_option( 'track_mode' ) !== 'manually' ), false ); - $matomo_form->show_select( \WpMatomo\Settings::SITE_CURRENCY, esc_html__( 'Currency', 'matomo' ), $matomo_currencies, esc_html__('Choose the currency which will be used in reports. The currency will be used if you have an ecommerce store or if you are using the Matomo goals feature and assign a monetary value to a goal.', 'matomo'), '' ); + $matomo_form->show_select( \WpMatomo\Settings::SITE_CURRENCY, esc_html__( 'Currency', 'matomo' ), $matomo_currencies, esc_html__( 'Choose the currency which will be used in reports. The currency will be used if you have an ecommerce store or if you are using the Matomo goals feature and assign a monetary value to a goal.', 'matomo' ), '' ); - $matomo_form->show_headline(esc_html__('Customise tracking (optional)', 'matomo'), 'matomo-track-option matomo-track-option-default matomo-track-option-manually matomo-track-option-tagmanager'); + $matomo_form->show_headline( esc_html__( 'Customise tracking (optional)', 'matomo' ), 'matomo-track-option matomo-track-option-default matomo-track-option-manually matomo-track-option-tagmanager' ); $matomo_form->show_checkbox( 'disable_cookies', esc_html__( 'Disable cookies', 'matomo' ), esc_html__( 'Disable all tracking cookies for a visitor.', 'matomo' ), $matomo_is_not_generated_tracking, $matomo_full_generated_tracking_group ); @@ -93,22 +95,22 @@ $matomo_form->show_checkbox( 'track_jserrors', esc_html__( 'Track JS errors', 'matomo' ), esc_html__( 'Enable to track JavaScript errors that occur on your website as Matomo events.', 'matomo' ) . ' ' . sprintf( esc_html__( 'See %1$sMatomo FAQ%2$s.', 'matomo' ), '', '' ), $matomo_is_not_tracking, $matomo_full_generated_tracking_group ); - echo ''; + echo ''; echo ''; $matomo_form->show_select( 'track_content', __( 'Enable content tracking', 'matomo' ), - array( + [ 'disabled' => esc_html__( 'Disabled', 'matomo' ), 'all' => esc_html__( 'Track all content blocks', 'matomo' ), 'visible' => esc_html__( 'Track only visible content blocks', 'matomo' ), - ), + ], __( 'Content tracking allows you to track interaction with the content of a web page or application.', 'matomo' ) . ' ' . sprintf( esc_html__( 'See %1$sMatomo documentation%2$s.', 'matomo' ), '', '' ), '', $matomo_is_not_tracking, @@ -146,13 +148,13 @@ $matomo_form->show_select( 'track_user_id', __( 'User ID Tracking', 'matomo' ), - array( + [ 'disabled' => esc_html__( 'Disabled', 'matomo' ), 'uid' => esc_html__( 'WP User ID', 'matomo' ), 'email' => esc_html__( 'Email Address', 'matomo' ), 'username' => esc_html__( 'Username', 'matomo' ), 'displayname' => esc_html__( 'Display Name (Not Recommended!)', 'matomo' ), - ), + ], __( 'When a user is logged in to WordPress, track their "User ID". You can select which field from the User\'s profile is tracked as the "User ID". When enabled, Tracking based on Email Address is recommended.', 'matomo' ), '', $matomo_is_not_tracking, @@ -176,10 +178,10 @@ $matomo_form->show_select( 'force_protocol', __( 'Force Matomo to use a specific protocol', 'matomo' ), - array( + [ 'disabled' => esc_html__( 'Disabled (default)', 'matomo' ), 'https' => esc_html__( 'https (SSL)', 'matomo' ), - ), + ], __( 'Choose if you want to explicitly want to force Matomo to use HTTP or HTTPS. Does not work with a CDN URL.', 'matomo' ), '', $matomo_is_not_tracking, @@ -188,10 +190,10 @@ $matomo_form->show_select( 'track_codeposition', __( 'JavaScript code position', 'matomo' ), - array( + [ 'footer' => esc_html__( 'Footer', 'matomo' ), 'header' => esc_html__( 'Header', 'matomo' ), - ), + ], __( 'Choose whether the JavaScript code is added to the footer or the header.', 'matomo' ), '', $matomo_is_not_tracking, @@ -200,11 +202,11 @@ $matomo_form->show_select( 'track_api_endpoint', __( 'Endpoint for HTTP Tracking API', 'matomo' ), - array( + [ 'default' => esc_html__( 'Default', 'matomo' ), 'restapi' => esc_html__( 'Through WordPress Rest API', 'matomo' ), - ), - __( 'By default the HTTP Tracking API points to your Matomo plugin directory "' . esc_html( $matomo_paths->get_tracker_api_url_in_matomo_dir() ) . '". You can choose to use the WP Rest API (' . esc_html( $matomo_paths->get_tracker_api_rest_api_endpoint() ) . ') instead for example to hide matomo.php or if the other URL doesn\'t work for you. Note: If the tracking mode "Tag Manager" is selected, then this URL currently only applies to the feed tracking.', 'matomo' ), + ], + sprintf( __( 'By default the HTTP Tracking API points to your Matomo plugin directory "%1$s". You can choose to use the WP Rest API (%2$s) instead for example to hide matomo.php or if the other URL doesn\'t work for you. Note: If the tracking mode "Tag Manager" is selected, then this URL currently only applies to the feed tracking.', 'matomo' ), esc_html( $matomo_paths->get_tracker_api_url_in_matomo_dir() ), esc_html( $matomo_paths->get_tracker_api_rest_api_endpoint() ) ), '', $matomo_is_not_tracking, $matomo_full_generated_tracking_group . ' matomo-track-option-manually matomo-track-option-tagmanager' @@ -213,35 +215,35 @@ $matomo_form->show_select( 'track_js_endpoint', __( 'Endpoint for JavaScript tracker', 'matomo' ), - array( + [ 'default' => esc_html__( 'Default', 'matomo' ), 'restapi' => esc_html__( 'Through WordPress Rest API (slower)', 'matomo' ), - 'plugin' => esc_html__( 'Plugin (an alternative JS file if the default is blocked by the webserver)', 'matomo' ), - ), - __( 'By default the JS tracking code will be loaded from "' . esc_html( $matomo_paths->get_js_tracker_url_in_matomo_dir() ) . '". You can choose to serve the JS file through the WP Rest API (' . esc_html( $matomo_paths->get_js_tracker_rest_api_endpoint() ) . ') for example to hide matomo.js. Please note that this means every request to the JavaScript file will launch WordPress PHP and therefore will be slower compared to your webserver serving the JS file directly. Using the "Plugin" method will cause issues with our paid Heatmap and Session Recording, Form Analytics, and Media Analyics plugin.', 'matomo' ), + 'plugin' => esc_html__( 'Plugin (an alternative JS file if the default is blocked by the webserver)', 'matomo' ), + ], + sprintf( __( 'By default the JS tracking code will be loaded from "%1$s". You can choose to serve the JS file through the WP Rest API (%2$s) for example to hide matomo.js. Please note that this means every request to the JavaScript file will launch WordPress PHP and therefore will be slower compared to your webserver serving the JS file directly. Using the "Plugin" method will cause issues with our paid Heatmap and Session Recording, Form Analytics, and Media Analyics plugin.', 'matomo' ), esc_html( $matomo_paths->get_js_tracker_url_in_matomo_dir() ), esc_html( $matomo_paths->get_js_tracker_rest_api_endpoint() ) ), '', $matomo_is_not_tracking, $matomo_full_generated_tracking_group ); - $matomo_form->show_select( 'cookie_consent', esc_html__( 'Custom consent screen', 'matomo' ), $cookie_consent_modes, sprintf(esc_html__( 'Activates a specific Matomo consent mode. Only configure a consent mode if you are implementing a consent screen yourself. This requires a custom consent implementation. For more information please read this %1$sFAQ%2$s (this option will take care of step 1 for you). By default no consent mode is applied.', 'matomo' ), '', ''), '', $matomo_is_not_generated_tracking, $matomo_full_generated_tracking_group ); + $matomo_form->show_select( 'cookie_consent', esc_html__( 'Custom consent screen', 'matomo' ), $cookie_consent_modes, sprintf( esc_html__( 'Activates a specific Matomo consent mode. Only configure a consent mode if you are implementing a consent screen yourself. This requires a custom consent implementation. For more information please read this %1$sFAQ%2$s (this option will take care of step 1 for you). By default no consent mode is applied.', 'matomo' ), '', '' ), '', $matomo_is_not_generated_tracking, $matomo_full_generated_tracking_group ); - $matomo_form->show_headline(esc_html__('For Developers', 'matomo'), 'matomo-track-option matomo-track-option-default matomo-track-option-disabled matomo-track-option-manually matomo-track-option-tagmanager'); + $matomo_form->show_headline( esc_html__( 'For Developers', 'matomo' ), 'matomo-track-option matomo-track-option-default matomo-track-option-disabled matomo-track-option-manually matomo-track-option-tagmanager' ); $matomo_form->show_select( 'tracker_debug', __( 'Tracker Debug Mode', 'matomo' ), - array( - 'disabled' => esc_html__( 'Disabled (recommended)', 'matomo' ), + [ + 'disabled' => esc_html__( 'Disabled (recommended)', 'matomo' ), 'always' => esc_html__( 'Always enabled', 'matomo' ), - 'on_demand' => esc_html__( 'Enabled on demand', 'matomo' ), - ), + 'on_demand' => esc_html__( 'Enabled on demand', 'matomo' ), + ], __( 'For security and privacy reasons you should only enable this setting for as short time of a time as possible.', 'matomo' ), '', $matomo_is_not_tracking, $matomo_full_generated_tracking_group . ' matomo-track-option-disabled matomo-track-option-manually matomo-track-option-tagmanager' ); - + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $matomo_submit_button; ?> @@ -250,13 +252,13 @@ is_network_enabled() ) { // Can't show it for multisite as idsite and url is always different. ?> -
    -

    -

    - ', '' ); ?> -

    - '; ?> -

    - '; ?> -
    +
    +

    +

    + ', '' ); ?> +

    + '; ?> +

    + '; ?> +
    diff --git a/classes/WpMatomo/Annotations.php b/classes/WpMatomo/Annotations.php index 8abb727f5..f7fe91f3a 100644 --- a/classes/WpMatomo/Annotations.php +++ b/classes/WpMatomo/Annotations.php @@ -9,6 +9,8 @@ namespace WpMatomo; +use Exception; + if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } @@ -33,7 +35,7 @@ public function __construct( $settings ) { } public function register_hooks() { - add_action( 'transition_post_status', array( $this, 'add_annotation' ), 10, 3 ); + add_action( 'transition_post_status', [ $this, 'add_annotation' ], 10, 3 ); } /** @@ -70,13 +72,13 @@ public function add_annotation( $new_status, $old_status, $post ) { $logger = $this->logger; \Piwik\Access::doAsSuperUser( function () use ( $post, $logger, $idsite ) { - $note = esc_html__( 'Published:', 'matomo' ) . ' ' . $post->post_title . ' - URL: ' . get_permalink( $post->ID ); - \Piwik\Plugins\Annotations\API::unsetInstance();// make sure latest instance will be loaded with all up to date dependencies... mainly needed for tests - $id = \Piwik\Plugins\Annotations\API::getInstance()->add( $idsite, gmdate( 'Y-m-d' ), $note ); - $logger->log( 'Add post annotation. ' . $note . ' - ' . wp_json_encode( $id ) ); + $note = esc_html__( 'Published:', 'matomo' ) . ' ' . $post->post_title . ' - URL: ' . get_permalink( $post->ID ); + \Piwik\Plugins\Annotations\API::unsetInstance();// make sure latest instance will be loaded with all up to date dependencies... mainly needed for tests + $id = \Piwik\Plugins\Annotations\API::getInstance()->add( $idsite, gmdate( 'Y-m-d' ), $note ); + $logger->log( 'Add post annotation. ' . $note . ' - ' . wp_json_encode( $id ) ); } ); - } catch ( \Exception $e ) { + } catch ( Exception $e ) { $this->logger->log( 'Add post annotation failed: ' . $e->getMessage() ); return; diff --git a/classes/WpMatomo/Bootstrap.php b/classes/WpMatomo/Bootstrap.php index f421bd7c1..d5f9a11ff 100644 --- a/classes/WpMatomo/Bootstrap.php +++ b/classes/WpMatomo/Bootstrap.php @@ -9,10 +9,16 @@ namespace WpMatomo; +use Piwik\Application\Environment; +use Piwik\FrontController; + if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } - +/** + * piwik constants + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound + */ class Bootstrap { /** * Tests only @@ -70,11 +76,11 @@ public function bootstrap() { include_once 'Db/WordPress.php'; - $environment = new \Piwik\Application\Environment( null ); + $environment = new Environment( null ); $environment->init(); - \Piwik\FrontController::unsetInstance(); - $controller = \Piwik\FrontController::getInstance(); + FrontController::unsetInstance(); + $controller = FrontController::getInstance(); $controller->init(); add_action( diff --git a/classes/WpMatomo/Capabilities.php b/classes/WpMatomo/Capabilities.php index a07fc4d58..b994c9307 100644 --- a/classes/WpMatomo/Capabilities.php +++ b/classes/WpMatomo/Capabilities.php @@ -17,6 +17,7 @@ } class Capabilities { + const KEY_NONE = 'none_matomo'; /** @@ -50,9 +51,9 @@ public function __construct( $settings ) { } public function register_hooks() { - add_action( 'wp_roles_init', array( $this, 'add_capabilities_to_roles' ) ); - add_filter( 'user_has_cap', array( $this, 'add_capabilities_to_user' ), 10, 4 ); - add_filter( 'map_meta_cap', array( $this, 'map_meta_cap' ), 10, 4 ); + add_action( 'wp_roles_init', [ $this, 'add_capabilities_to_roles' ] ); + add_filter( 'user_has_cap', [ $this, 'add_capabilities_to_user' ], 10, 4 ); + add_filter( 'map_meta_cap', [ $this, 'map_meta_cap' ], 10, 4 ); } /** @@ -61,9 +62,9 @@ public function register_hooks() { * @internal */ public function remove_hooks() { - remove_action( 'wp_roles_init', array( $this, 'add_capabilities_to_roles' ) ); - remove_filter( 'user_has_cap', array( $this, 'add_capabilities_to_user' ), 10 ); - remove_filter( 'map_meta_cap', array( $this, 'map_meta_cap' ), 10 ); + remove_action( 'wp_roles_init', [ $this, 'add_capabilities_to_roles' ] ); + remove_filter( 'user_has_cap', [ $this, 'add_capabilities_to_user' ], 10 ); + remove_filter( 'map_meta_cap', [ $this, 'map_meta_cap' ], 10 ); } public function map_meta_cap( $caps, $cap, $user_id, $args ) { @@ -156,12 +157,12 @@ public function add_capabilities_to_roles( $roles ) { } public function get_all_capabilities_sorted_by_highest_permission() { - return array( + return [ self::KEY_SUPERUSER, self::KEY_ADMIN, self::KEY_WRITE, self::KEY_VIEW, - ); + ]; } protected function has_any_higher_permission( $cap_to_find, $allcaps ) { @@ -182,5 +183,4 @@ protected function has_any_higher_permission( $cap_to_find, $allcaps ) { return false; } - } diff --git a/classes/WpMatomo/Commands/MatomoCommands.php b/classes/WpMatomo/Commands/MatomoCommands.php index 0efd57c68..68871e75f 100644 --- a/classes/WpMatomo/Commands/MatomoCommands.php +++ b/classes/WpMatomo/Commands/MatomoCommands.php @@ -9,11 +9,12 @@ namespace WpMatomo\Commands; +use WP_CLI; +use WP_CLI_Command; +use WP_Site; use WpMatomo\Installer; use WpMatomo\Settings; use WpMatomo\Uninstaller; -use WP_CLI; -use WP_CLI_Command; use WpMatomo\Updater; if ( ! defined( 'ABSPATH' ) ) { @@ -52,6 +53,7 @@ public function uninstall( $args, $assoc_args ) { WP_CLI::success( 'Uninstalled Matomo Analytics' ); } + /** * Updates Matomo. * @@ -67,17 +69,17 @@ public function uninstall( $args, $assoc_args ) { * @when after_wp_load */ public function update( $args, $assoc_args ) { - if ( function_exists('is_multisite') && is_multisite() && function_exists( 'get_sites' ) ) { + if ( function_exists( 'is_multisite' ) && is_multisite() && function_exists( 'get_sites' ) ) { foreach ( get_sites() as $site ) { - /** @var \WP_Site $site */ + /** @var WP_Site $site */ switch_to_blog( $site->blog_id ); // this way we make sure all blogs get updated eventually WP_CLI::log( 'Blog ID' . $site->blog_id ); - $this->_doUpdate( ! empty( $assoc_args['force'] ) ); + $this->do_update( ! empty( $assoc_args['force'] ) ); restore_current_blog(); } } else { - $this->_doUpdate( ! empty( $assoc_args['force'] ) ); + $this->do_update( ! empty( $assoc_args['force'] ) ); } WP_CLI::success( 'Matomo Analytics Updater finished' ); @@ -86,12 +88,13 @@ public function update( $args, $assoc_args ) { /** * @param $assoc_args */ - public function _doUpdate( $force ) { + private function do_update( $force ) { $settings = new Settings(); $installer = new Installer( $settings ); if ( ! $installer->looks_like_it_is_installed() ) { WP_CLI::log( 'Skipping as looks like Matomo is not yet installed' ); + return; } @@ -109,7 +112,7 @@ public function _doUpdate( $force ) { WP_CLI::add_command( 'matomo', '\WpMatomo\Commands\MatomoCommands', - array( + [ 'shortdesc' => 'Manage your Matomo Analytics. Commands are recommended only to be used in development mode', - ) + ] ); diff --git a/classes/WpMatomo/Compatibility.php b/classes/WpMatomo/Compatibility.php index 7d57743e4..3c03e708e 100644 --- a/classes/WpMatomo/Compatibility.php +++ b/classes/WpMatomo/Compatibility.php @@ -14,7 +14,6 @@ } class Compatibility { - public function register_hooks() { $this->ithemes_security(); } @@ -35,13 +34,13 @@ function ( $rules ) { // todo ideally we would make the plugins path relative and match the specific path... // like preg_quote(relative_wp_content_dir)... $is_wp_content_dir_compatible = defined( 'WP_CONTENT_DIR' ) - && ABSPATH . 'wp-content' === rtrim( WP_CONTENT_DIR, '/' ); + && ABSPATH . 'wp-content' === rtrim( WP_CONTENT_DIR, '/' ); if ( $rules - && $is_wp_content_dir_compatible - && is_string( $rules ) - && strpos( $rules, 'RewriteEngine On' ) > 0 - && strpos( $rules, 'content' ) > 0 - && strpos( $rules, 'plugins' ) > 0 ) { + && $is_wp_content_dir_compatible + && is_string( $rules ) + && strpos( $rules, 'RewriteEngine On' ) > 0 + && strpos( $rules, 'content' ) > 0 + && strpos( $rules, 'plugins' ) > 0 ) { $rules = ' RewriteEngine On @@ -51,11 +50,11 @@ function ( $rules ) { ' . $rules; } + return $rules; }, 9999999991, - $acceptedArgs = 1 + $accepted_args = 1 ); } - } diff --git a/classes/WpMatomo/Db/Settings.php b/classes/WpMatomo/Db/Settings.php index baec09fc3..f9c1edb00 100644 --- a/classes/WpMatomo/Db/Settings.php +++ b/classes/WpMatomo/Db/Settings.php @@ -12,7 +12,17 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } - +/** + * We want a real data, not something coming from cache + * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + * + * This is a report error, so silent the possible errors + * phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged + * + * We cannot use parameters of statements as this is the table names we build + * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + * phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + */ class Settings { /** @@ -23,7 +33,7 @@ class Settings { * @return string * @api */ - public function prefix_table_name( $table_name_to_prefix = '') { + public function prefix_table_name( $table_name_to_prefix = '' ) { global $wpdb; return $wpdb->prefix . MATOMO_DATABASE_PREFIX . $table_name_to_prefix; @@ -35,7 +45,7 @@ public function prefix_table_name( $table_name_to_prefix = '') { public function get_matomo_tables() { // we need to hard code them unfortunately for tests cause there are temporary tables used and we can't find a // list of existing temp tables - $tables = array( + $tables = [ 'access', 'archive_invalidations', 'brute_force_log', @@ -65,24 +75,30 @@ public function get_matomo_tables() { 'user_dashboard', 'user_language', 'user_token_auth', - ); - if ( !is_multisite() ) { - $tables = array_merge($tables, ['tagmanager_container', - 'tagmanager_container_release', - 'tagmanager_container_version', - 'tagmanager_tag', - 'tagmanager_trigger', - 'tagmanager_variable'] ); + ]; + if ( ! is_multisite() ) { + $tables = array_merge( + $tables, + [ + 'tagmanager_container', + 'tagmanager_container_release', + 'tagmanager_container_version', + 'tagmanager_tag', + 'tagmanager_trigger', + 'tagmanager_variable', + ] + ); } + return $tables; } public function get_installed_matomo_tables() { global $wpdb; - $table_names = array(); + $table_names = []; - $tables = $wpdb->get_results( 'SHOW TABLES LIKE "' . $this->prefix_table_name() . '%"', ARRAY_N ); + $tables = $wpdb->get_results( 'SHOW TABLES LIKE "' . $this->prefix_table_name() . '%"', ARRAY_N ); foreach ( $tables as $table_name_to_look_for ) { $table_names[] = array_shift( $table_name_to_look_for ); } @@ -98,7 +114,7 @@ public function get_installed_matomo_tables() { $table_names_to_look_for = apply_filters( 'matomo_install_tables', $table_names_to_look_for ); foreach ( $table_names_to_look_for as $table_name_to_look_for ) { - $table_name_to_test = $this->prefix_table_name($table_name_to_look_for); + $table_name_to_test = $this->prefix_table_name( $table_name_to_look_for ); if ( ! in_array( $table_name_to_test, $table_names, true ) ) { $table_names[] = $table_name_to_test; } @@ -106,5 +122,4 @@ public function get_installed_matomo_tables() { return $table_names; } - } diff --git a/classes/WpMatomo/Db/WordPress.php b/classes/WpMatomo/Db/WordPress.php index a8eeb838c..8971e06a9 100644 --- a/classes/WpMatomo/Db/WordPress.php +++ b/classes/WpMatomo/Db/WordPress.php @@ -347,10 +347,10 @@ private function before_execute_query( $wpdb, $sql ) { } if ( defined( 'WP_DEBUG' ) - && WP_DEBUG - && defined( 'WP_DEBUG_DISPLAY' ) - && WP_DEBUG_DISPLAY - && ! is_admin() ) { + && WP_DEBUG + && defined( 'WP_DEBUG_DISPLAY' ) + && WP_DEBUG_DISPLAY + && ! is_admin() ) { // prevent showing some notices in frontend eg if cronjob runs there $is_likely_dedicated_cron = defined( 'DOING_CRON' ) && DOING_CRON && defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON; @@ -363,8 +363,8 @@ private function before_execute_query( $wpdb, $sql ) { } if ( ( stripos( $sql, '/* WP IGNORE ERROR */' ) !== false ) - || stripos( $sql, 'SELECT @@TX_ISOLATION' ) !== false - || stripos( $sql, 'SELECT @@transaction_isolation' ) !== false ) { + || stripos( $sql, 'SELECT @@TX_ISOLATION' ) !== false + || stripos( $sql, 'SELECT @@transaction_isolation' ) !== false ) { // prevent notices for queries that are expected to fail // SELECT 1 FROM wp_matomo_logtmpsegment1cc77bce7a13181081e44ea6ffc0a9fd LIMIT 1 => runs to detect if temp table exists or not and regularly the query fails which is expected // SELECT @@TX_ISOLATION => not available in all mysql versions diff --git a/classes/WpMatomo/Db/WordPressDbStatement.php b/classes/WpMatomo/Db/WordPressDbStatement.php index 784501142..a52091aa2 100644 --- a/classes/WpMatomo/Db/WordPressDbStatement.php +++ b/classes/WpMatomo/Db/WordPressDbStatement.php @@ -9,12 +9,23 @@ namespace Piwik\Db\Adapter; +use Zend_Db_Statement; + if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } - -class WordPressDbStatement extends \Zend_Db_Statement { - +/** + * We want a real data, not something coming from cache + * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + * + * This is a report error, so silent the possible errors + * phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged + * + * We cannot use parameters of statements as this is the table names we build + * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + * phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + */ +class WordPressDbStatement extends Zend_Db_Statement { private $result; private $sql; diff --git a/classes/WpMatomo/Db/WordPressTracker.php b/classes/WpMatomo/Db/WordPressTracker.php index 8bf6d6ebb..f80edbcd0 100644 --- a/classes/WpMatomo/Db/WordPressTracker.php +++ b/classes/WpMatomo/Db/WordPressTracker.php @@ -76,10 +76,10 @@ private function after_execute_query( $wpdb ) { */ private function before_execute_query( $wpdb, $sql ) { if ( ! $wpdb->suppress_errors - && defined( 'WP_DEBUG' ) - && WP_DEBUG - && defined( 'WP_DEBUG_DISPLAY' ) - && WP_DEBUG_DISPLAY ) { + && defined( 'WP_DEBUG' ) + && WP_DEBUG + && defined( 'WP_DEBUG_DISPLAY' ) + && WP_DEBUG_DISPLAY ) { // we want to prevent showing these notices if ( defined( 'MATOMO_SUPPRESS_DB_ERRORS' ) ) { if ( MATOMO_SUPPRESS_DB_ERRORS === true ) { diff --git a/classes/WpMatomo/Ecommerce/Base.php b/classes/WpMatomo/Ecommerce/Base.php index 11cd58894..30a2b4ccb 100644 --- a/classes/WpMatomo/Ecommerce/Base.php +++ b/classes/WpMatomo/Ecommerce/Base.php @@ -9,10 +9,12 @@ namespace WpMatomo\Ecommerce; +use Exception; +use WpMatomo; use WpMatomo\Admin\TrackingSettings; +use WpMatomo\AjaxTracker; use WpMatomo\Logger; use WpMatomo\Settings; -use WpMatomo\AjaxTracker; if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly @@ -40,7 +42,7 @@ class Base { */ protected $cart_update_queue = ''; - private $ajax_tracker_calls = array(); + private $ajax_tracker_calls = []; public function __construct( AjaxTracker $tracker ) { $this->logger = new Logger(); @@ -52,18 +54,20 @@ public function __construct( AjaxTracker $tracker ) { public function register_hooks() { if ( ! is_admin() ) { - add_action( 'wp_footer', array( $this, 'on_print_queues' ), 99999, 0 ); + add_action( 'wp_footer', [ $this, 'on_print_queues' ], 99999, 0 ); } } public function on_print_queues() { // we need to queue in case there are multiple cart updates within one page load if ( ! empty( $this->cart_update_queue ) ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->cart_update_queue; } } protected function has_order_been_tracked_already( $order_id ) { + // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison return get_post_meta( $order_id, $this->key_order_tracked, true ) == 1; } @@ -72,8 +76,8 @@ protected function set_order_been_tracked( $order_id ) { } protected function should_track_background() { - return (defined( 'DOING_AJAX' ) && DOING_AJAX) - || \WpMatomo::$settings->get_global_option('track_mode') === TrackingSettings::TRACK_MODE_TAGMANAGER; + return ( defined( 'DOING_AJAX' ) && DOING_AJAX ) + || WpMatomo::$settings->get_global_option( 'track_mode' ) === TrackingSettings::TRACK_MODE_TAGMANAGER; } protected function make_matomo_js_tracker_call( $params ) { @@ -87,22 +91,22 @@ protected function make_matomo_js_tracker_call( $params ) { protected function wrap_script( $script ) { if ( $this->should_track_background() ) { foreach ( $this->ajax_tracker_calls as $call ) { - $methods = array( + $methods = [ 'addEcommerceItem' => 'addEcommerceItem', 'trackEcommerceOrder' => 'doTrackEcommerceOrder', 'trackEcommerceCartUpdate' => 'doTrackEcommerceCartUpdate', - ); + ]; if ( ! empty( $call[0] ) && ! empty( $methods[ $call[0] ] ) ) { try { $tracker_method = $methods[ $call[0] ]; array_shift( $call ); - call_user_func_array( array( $this->tracker, $tracker_method ), $call ); - } catch (\Exception $e) { - $this->logger->log_exception($call[0], $e); + call_user_func_array( [ $this->tracker, $tracker_method ], $call ); + } catch ( Exception $e ) { + $this->logger->log_exception( $call[0], $e ); } } } - $this->ajax_tracker_calls = array(); + $this->ajax_tracker_calls = []; return ''; } @@ -111,7 +115,13 @@ protected function wrap_script( $script ) { return ''; } - return ''; - } + if ( function_exists( 'wp_get_inline_script_tag' ) ) { + $script = wp_get_inline_script_tag( $script ); + } else { + // line feed is required to match the wp_get_inline_script_tag output + $script = '' . PHP_EOL; + } + return $script; + } } diff --git a/classes/WpMatomo/Ecommerce/EasyDigitalDownloads.php b/classes/WpMatomo/Ecommerce/EasyDigitalDownloads.php index 1b76d9e51..463d850df 100644 --- a/classes/WpMatomo/Ecommerce/EasyDigitalDownloads.php +++ b/classes/WpMatomo/Ecommerce/EasyDigitalDownloads.php @@ -9,26 +9,27 @@ namespace WpMatomo\Ecommerce; +use EDD_Download; + if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } class EasyDigitalDownloads extends Base { - public function register_hooks() { if ( ! is_admin() ) { - add_action( 'template_redirect', array( $this, 'on_product_view' ), 99999, 0 ); + add_action( 'template_redirect', [ $this, 'on_product_view' ], 99999, 0 ); } parent::register_hooks(); // these actions may be triggered in admin when ajax is used - add_action( 'edd_payment_receipt_after_table', array( $this, 'on_order' ), 99999, 2 ); - add_action( 'edd_post_remove_from_cart', array( $this, 'on_cart_update' ), 99999, 0 ); - add_action( 'edd_post_add_to_cart', array( $this, 'on_cart_update' ), 99999, 0 ); - add_action( 'edd_cart_discounts_removed', array( $this, 'on_cart_update' ), 99999, 0 ); - add_action( 'edd_after_set_cart_item_quantity', array( $this, 'on_cart_update' ), 99999, 0 ); - add_action( 'edd_cart_discount_set', array( $this, 'on_cart_update' ), 99999, 0 ); + add_action( 'edd_payment_receipt_after_table', [ $this, 'on_order' ], 99999, 2 ); + add_action( 'edd_post_remove_from_cart', [ $this, 'on_cart_update' ], 99999, 0 ); + add_action( 'edd_post_add_to_cart', [ $this, 'on_cart_update' ], 99999, 0 ); + add_action( 'edd_cart_discounts_removed', [ $this, 'on_cart_update' ], 99999, 0 ); + add_action( 'edd_after_set_cart_item_quantity', [ $this, 'on_cart_update' ], 99999, 0 ); + add_action( 'edd_cart_discount_set', [ $this, 'on_cart_update' ], 99999, 0 ); } public function on_cart_update() { @@ -43,7 +44,7 @@ public function on_cart_update() { $tracking_code = ''; foreach ( $contents as $key => $item ) { - $download = new \EDD_Download( $item['id'] ); + $download = new EDD_Download( $item['id'] ); // If the item is not a download or it's status has changed since it was added to the cart. if ( empty( $download->ID ) || ! $download->can_purchase() ) { @@ -64,7 +65,7 @@ public function on_cart_update() { $categories = $this->get_product_categories( $download->ID ); $quantity = isset( $item['quantity'] ) ? $item['quantity'] : 0; - $params = array( 'addEcommerceItem', $sku, $name, $categories, $price, $quantity ); + $params = [ 'addEcommerceItem', $sku, $name, $categories, $price, $quantity ]; $tracking_code .= $this->make_matomo_js_tracker_call( $params ); } @@ -75,7 +76,7 @@ public function on_cart_update() { $total = $cart->get_total(); } - $tracking_code .= $this->make_matomo_js_tracker_call( array( 'trackEcommerceCartUpdate', $total ) ); + $tracking_code .= $this->make_matomo_js_tracker_call( [ 'trackEcommerceCartUpdate', $total ] ); // we can't echo directly as we wouldn't know where in the template rendering stage we are and whether // we're supposed to print or not etc @@ -90,7 +91,7 @@ private function get_product_categories( $download_id ) { } /** - * @param \EDD_Download $download + * @param EDD_Download $download * * @return mixed */ @@ -118,18 +119,18 @@ public function on_product_view() { return; } - $download = new \EDD_Download( $download_id ); + $download = new EDD_Download( $download_id ); $sku = $this->get_sku( $download, $download_id ); - $params = array( + $params = [ 'setEcommerceView', $sku, $download->get_name(), $this->get_product_categories( $download_id ), $download->get_price(), - ); - + ]; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->wrap_script( $this->make_matomo_js_tracker_call( $params ) ); } @@ -175,7 +176,7 @@ public function on_order( $payment, $edd_receipt_args ) { $name .= ' - ' . edd_get_price_option_name( $item['id'], $price_id ); } - $download = new \EDD_Download( $item['id'] ); + $download = new EDD_Download( $item['id'] ); $sku = $this->get_sku( $download, $item['id'] ); $price = 0; @@ -183,14 +184,14 @@ public function on_order( $payment, $edd_receipt_args ) { $price = $item['item_price']; } - $params = array( + $params = [ 'addEcommerceItem', $sku, $name, $this->get_product_categories( $item['id'] ), $price, $item['quantity'], - ); + ]; $tracking_code .= $this->make_matomo_js_tracker_call( $params ); } } @@ -208,7 +209,7 @@ public function on_order( $payment, $edd_receipt_args ) { $discount = reset( $discount ); } - $params = array( + $params = [ 'trackEcommerceOrder', '' . $order_id_to_track, $grand_total ? $grand_total : 0, @@ -216,11 +217,10 @@ public function on_order( $payment, $edd_receipt_args ) { edd_use_taxes() ? edd_get_payment_tax( $payment->ID, $payment_meta ) : '0', $shipping = 0, $discount, - ); + ]; $tracking_code .= $this->make_matomo_js_tracker_call( $params ); - + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->wrap_script( $tracking_code ); } } - } diff --git a/classes/WpMatomo/Ecommerce/MatomoTestEcommerce.php b/classes/WpMatomo/Ecommerce/MatomoTestEcommerce.php new file mode 100644 index 000000000..ffadf2fda --- /dev/null +++ b/classes/WpMatomo/Ecommerce/MatomoTestEcommerce.php @@ -0,0 +1,37 @@ +id; $product = $transaction->product(); - $params = array( + $params = [ 'addEcommerceItem', $sku, $product->post_title, - $categories = array(), + $categories = [], $transaction->amount, 1, - ); + ]; $tracking_code .= $this->make_matomo_js_tracker_call( $params ); $total = $transaction->total; - $tracking_code .= $this->make_matomo_js_tracker_call( array( 'trackEcommerceCartUpdate', $total ) ); + $tracking_code .= $this->make_matomo_js_tracker_call( [ 'trackEcommerceCartUpdate', $total ] ); // we can't echo directly as we wouldn't know where in the template rendering stage we are and whether // we're supposed to print or not etc @@ -66,18 +68,18 @@ public function on_product_view() { return; } - $product = new \MeprProduct( $product_id ); + $product = new MeprProduct( $product_id ); $sku = $product_id; - $params = array( + $params = [ 'setEcommerceView', '' . $sku, $product->post_title, - $categories = array(), + $categories = [], $product->price, - ); - + ]; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->wrap_script( $this->make_matomo_js_tracker_call( $params ) ); } @@ -85,13 +87,13 @@ public function on_order() { if ( isset( $_GET['membership'] ) && isset( $_GET['trans_num'] ) && class_exists( '\MeprTransaction' ) ) { - $txn = \MeprTransaction::get_one_by_trans_num($_GET['trans_num'] ); + $txn = MeprTransaction::get_one_by_trans_num( sanitize_text_field( wp_unslash( $_GET['trans_num'] ) ) ); if ( isset( $txn->id ) && $txn->id > 0 ) { if ( $this->has_order_been_tracked_already( $txn->id ) ) { return; } $this->set_order_been_tracked( $txn->id ); - $transaction = new \MeprTransaction( $txn->id ); + $transaction = new MeprTransaction( $txn->id ); $order_id_to_track = $txn->trans_num; $product = $transaction->product(); @@ -101,16 +103,16 @@ public function on_order() { $discount = $product->price - $txn->amount; } $tracking_code = ''; - $params = array( + $params = [ 'addEcommerceItem', '' . $product->ID, $product->post_title, - array(), + [], $txn->amount, 1, - ); + ]; $tracking_code .= $this->make_matomo_js_tracker_call( $params ); - $params = array( + $params = [ 'trackEcommerceOrder', '' . $order_id_to_track, $txn->total, @@ -118,12 +120,11 @@ public function on_order() { $txn->tax_amount, $shipping = 0, $discount, - ); + ]; $tracking_code .= $this->make_matomo_js_tracker_call( $params ); - + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->wrap_script( $tracking_code ); } } } - } diff --git a/classes/WpMatomo/Ecommerce/Woocommerce.php b/classes/WpMatomo/Ecommerce/Woocommerce.php index f35933c88..f4b50248f 100644 --- a/classes/WpMatomo/Ecommerce/Woocommerce.php +++ b/classes/WpMatomo/Ecommerce/Woocommerce.php @@ -9,12 +9,15 @@ namespace WpMatomo\Ecommerce; +use WC_Order; +use WC_Product; + if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } class Woocommerce extends Base { - private $order_status_ignore = array( 'cancelled', 'failed', 'refunded' ); + private $order_status_ignore = [ 'cancelled', 'failed', 'refunded' ]; public function register_hooks() { if ( is_admin() ) { @@ -23,30 +26,38 @@ public function register_hooks() { parent::register_hooks(); - add_action( 'wp_head', array( $this, 'maybe_track_order_complete' ), 99999 ); - add_action( 'woocommerce_after_single_product', array( $this, 'on_product_view' ), 99999, $args = 0 ); - add_action( 'woocommerce_add_to_cart', array( $this, 'on_cart_updated_safe' ), 99999, 0 ); - add_action( 'woocommerce_cart_item_removed', array( $this, 'on_cart_updated_safe' ), 99999, 0 ); - add_action( 'woocommerce_cart_item_restored', array( $this, 'on_cart_updated_safe' ), 99999, 0 ); - add_action( 'woocommerce_cart_item_set_quantity', array( $this, 'on_cart_updated_safe' ), 99999, 0 ); - add_action('woocommerce_thankyou', array($this, 'anonymise_orderid_in_url'), 1, 1); + add_action( 'wp_head', [ $this, 'maybe_track_order_complete' ], 99999 ); + add_action( 'woocommerce_after_single_product', [ $this, 'on_product_view' ], 99999, $args = 0 ); + add_action( 'woocommerce_add_to_cart', [ $this, 'on_cart_updated_safe' ], 99999, 0 ); + add_action( 'woocommerce_cart_item_removed', [ $this, 'on_cart_updated_safe' ], 99999, 0 ); + add_action( 'woocommerce_cart_item_restored', [ $this, 'on_cart_updated_safe' ], 99999, 0 ); + add_action( 'woocommerce_cart_item_set_quantity', [ $this, 'on_cart_updated_safe' ], 99999, 0 ); + add_action( 'woocommerce_thankyou', [ $this, 'anonymise_orderid_in_url' ], 1, 1 ); - if (!$this->should_track_background()) { + if ( ! $this->should_track_background() ) { // prevent possibly executing same event twice where eg first a PHP Matomo tracker request is created // because of woocommerce_applied_coupon and then also because of woocommerce_update_cart_action_cart_updated itself // causing two tracking requests to be issues from the server. refs #215 // when not ajax mode the later event will simply overwrite the first and it should be fine. - add_filter( 'woocommerce_update_cart_action_cart_updated', array( $this, 'on_cart_updated_safe' ), 99999, 1 ); + add_filter( + 'woocommerce_update_cart_action_cart_updated', + [ + $this, + 'on_cart_updated_safe', + ], + 99999, + 1 + ); } - add_action( 'woocommerce_applied_coupon', array( $this, 'on_coupon_updated_safe' ), 99999, 0 ); - add_action( 'woocommerce_removed_coupon', array( $this, 'on_coupon_updated_safe' ), 99999, 0 ); + add_action( 'woocommerce_applied_coupon', [ $this, 'on_coupon_updated_safe' ], 99999, 0 ); + add_action( 'woocommerce_removed_coupon', [ $this, 'on_coupon_updated_safe' ], 99999, 0 ); } - public function anonymise_orderid_in_url($order_id) - { - if ( !empty($order_id) && is_numeric($order_id)) { + public function anonymise_orderid_in_url( $order_id ) { + if ( ! empty( $order_id ) && is_numeric( $order_id ) ) { $order_id = (int) $order_id; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo "\n"; } - $script = ''.$script.''; + $script = '' . $script . ''; $no_script = ''; $script = apply_filters( 'matomo_tracking_code_script', $script, $idsite ); $script = apply_filters( 'matomo_tracking_code_noscript', $script, $idsite ); - $this->logger->log( 'Finished tracking code: ' . $script, $logLevel ); - $this->logger->log( 'Finished noscript code: ' . $no_script, $logLevel); + $this->logger->log( 'Finished tracking code: ' . $script, $log_level ); + $this->logger->log( 'Finished noscript code: ' . $no_script, $log_level ); - return array( + return [ 'script' => $script, 'noscript' => $no_script, - ); + ]; } private function apply_404_changes( $tracking_code ) { @@ -348,7 +350,7 @@ private function apply_404_changes( $tracking_code ) { private function apply_search_changes( $tracking_code ) { $this->logger->log( 'Apply search tracking changes. Blog ID: ' . get_current_blog_id() ); - $obj_search = new \WP_Query( 's=' . get_search_query() . '&showposts=-1' ); + $obj_search = new WP_Query( 's=' . get_search_query() . '&showposts=-1' ); $int_result_count = $obj_search->post_count; $code = "window._paq = window._paq || []; window._paq.push(['trackSiteSearch','" . get_search_query() . "', false, " . $int_result_count . "]);\n"; @@ -360,7 +362,7 @@ private function apply_search_changes( $tracking_code ) { private function apply_user_tracking( $tracking_code ) { $user_id_to_track = null; - if ( \is_user_logged_in() ) { + if ( is_user_logged_in() ) { // Get the User ID Admin option, and the current user's data $uid_from = $this->settings->get_global_option( 'track_user_id' ); $current_user = wp_get_current_user(); // current user @@ -385,5 +387,4 @@ private function apply_user_tracking( $tracking_code ) { return $tracking_code; } - } diff --git a/classes/WpMatomo/Uninstaller.php b/classes/WpMatomo/Uninstaller.php index 12200b062..aeda62a67 100644 --- a/classes/WpMatomo/Uninstaller.php +++ b/classes/WpMatomo/Uninstaller.php @@ -14,7 +14,15 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } - +/** + * We need to access db not cache + * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + * + * Table names management + * phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + * phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + */ class Uninstaller { /** @@ -135,7 +143,7 @@ public function uninstall_multisite( $should_remove_all_data ) { private function drop_tables() { global $wpdb; - $db_settings = new \WpMatomo\Db\Settings(); + $db_settings = new \WpMatomo\Db\Settings(); $installed_tables = $db_settings->get_installed_matomo_tables(); $this->logger->log( sprintf( 'Matomo will now drop %s matomo tables', count( $installed_tables ) ) ); @@ -143,6 +151,7 @@ private function drop_tables() { // temporary table are used in tests and just making sure they are being removed // $wpdb->query( "DROP TEMPORARY TABLE IF EXISTS `$tableName`" ); // two spaces between drop and table so it won't be replaced in WP tests + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->query( "DROP TABLE IF EXISTS `$table_name`" ); } } diff --git a/classes/WpMatomo/Updater.php b/classes/WpMatomo/Updater.php index fa0e70b38..858db8a40 100644 --- a/classes/WpMatomo/Updater.php +++ b/classes/WpMatomo/Updater.php @@ -9,12 +9,15 @@ namespace WpMatomo; +use Exception; use Piwik\Cache as PiwikCache; use Piwik\Filesystem; use Piwik\Option; use Piwik\Plugins\Installation\ServerFilesGenerator; use Piwik\SettingsServer; use Piwik\Version; +use WP_Upgrader; +use WpMatomo\Paths; use WpMatomo\Updater\UpdateInProgressException; if ( ! defined( 'ABSPATH' ) ) { @@ -47,13 +50,12 @@ public function load_plugin_functions() { return function_exists( 'get_plugin_data' ); } - public function get_plugins_requiring_update() - { + public function get_plugins_requiring_update() { if ( ! $this->load_plugin_functions() ) { return []; } - $keys = []; + $keys = []; $plugin_files = $GLOBALS['MATOMO_PLUGIN_FILES']; if ( ! in_array( MATOMO_ANALYTICS_FILE, $plugin_files, true ) ) { $plugin_files[] = MATOMO_ANALYTICS_FILE; @@ -70,7 +72,7 @@ public function get_plugins_requiring_update() if ( ! Installer::is_intalled() ) { return []; } - $keys[$key] = $plugin_data['Version']; + $keys[ $key ] = $plugin_data['Version']; } } @@ -78,16 +80,17 @@ public function get_plugins_requiring_update() } public function update_if_needed() { - $executed_updates = array(); + $executed_updates = []; $plugins_requiring_update = $this->get_plugins_requiring_update(); - foreach ($plugins_requiring_update as $key => $plugin_version) { + foreach ( $plugins_requiring_update as $key => $plugin_version ) { try { $this->update(); } catch ( UpdateInProgressException $e ) { - $this->logger->log( 'Matomo update is already in progress'); + $this->logger->log( 'Matomo update is already in progress' ); + return; // we also don't execute any further update as they should be executed in another process - }catch ( \Exception $e ) { + } catch ( Exception $e ) { $this->logger->log_exception( 'plugin_update', $e ); continue; } @@ -117,11 +120,11 @@ public function update() { $history = $this->settings->get_global_option( 'version_history' ); if ( empty( $history ) || ! is_array( $history ) ) { - $history = array(); + $history = []; } if ( ! empty( $plugin_data['Version'] ) - && ! in_array( $plugin_data['Version'], $history, true ) ) { + && ! in_array( $plugin_data['Version'], $history, true ) ) { // this allows us to see which versions of matomo the user was using before this update so we better understand // which version maybe regressed something array_unshift( $history, $plugin_data['Version'] ); @@ -147,11 +150,13 @@ function () { ); $upload_dir = $paths->get_upload_base_dir(); + + $wp_filesystem = $paths->get_file_system(); if ( is_dir( $upload_dir ) && is_writable( $upload_dir ) ) { - @file_put_contents( $upload_dir . '/index.php', '//hello' ); - @file_put_contents( $upload_dir . '/index.html', '//hello' ); - @file_put_contents( $upload_dir . '/index.htm', '//hello' ); - @file_put_contents( + $wp_filesystem->put_contents( $upload_dir . '/index.php', '//hello' ); + $wp_filesystem->put_contents( $upload_dir . '/index.html', '//hello' ); + $wp_filesystem->put_contents( $upload_dir . '/index.htm', '//hello' ); + $wp_filesystem->put_contents( $upload_dir . '/.htaccess', ' ' . ServerFilesGenerator::getDenyHtaccessContent() . ' @@ -163,12 +168,12 @@ function () { } $config_dir = $paths->get_config_ini_path(); if ( is_dir( $config_dir ) && is_writable( $config_dir ) ) { - @file_put_contents( $config_dir . '/index.php', '//hello' ); - @file_put_contents( $config_dir . '/index.html', '//hello' ); - @file_put_contents( $config_dir . '/index.htm', '//hello' ); + $wp_filesystem->put_contents( $config_dir . '/index.php', '//hello' ); + $wp_filesystem->put_contents( $config_dir . '/index.html', '//hello' ); + $wp_filesystem->put_contents( $config_dir . '/index.htm', '//hello' ); } - if ($this->settings->should_disable_addhandler()) { + if ( $this->settings->should_disable_addhandler() ) { wp_schedule_single_event( time() + 10, ScheduledTasks::EVENT_DISABLE_ADDHANDLER ); } } @@ -178,9 +183,10 @@ public function is_upgrade_in_progress() { return 'no upgrader'; } - if (self::lock()) { + if ( self::lock() ) { // we can get the lock meaning no update is in progress self::unlock(); + return false; } @@ -188,56 +194,57 @@ public function is_upgrade_in_progress() { } private static function load_upgrader() { - if (!class_exists('\WP_Upgrader', false)) { + if ( ! class_exists( '\WP_Upgrader', false ) ) { + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; } - return class_exists('\WP_Upgrader', false); + + return class_exists( '\WP_Upgrader', false ); } - public static function lock() - { + public static function lock() { // prevent the upgrade from being started several times at once // we lock for 4 minutes. In case of major Matomo upgrades the upgrade may take much longer but it should be // safe in this case to run the upgrade several times // important: we always need to use the same timeout otherwise if something did use `create_lock(2)` then // even though another job locked it for 4 minutes, the other job that locks it only for 2 seconds would release // the lock basically since WP does not remember the initialy set release timeout - return self::load_upgrader() && \WP_Upgrader::create_lock(self::LOCK_NAME, 60*4); + return self::load_upgrader() && WP_Upgrader::create_lock( self::LOCK_NAME, 60 * 4 ); } - public static function unlock() - { - return self::load_upgrader() && \WP_Upgrader::release_lock(self::LOCK_NAME); + public static function unlock() { + return self::load_upgrader() && WP_Upgrader::release_lock( self::LOCK_NAME ); } private static function update_components() { $updater = new \Piwik\Updater(); - $components_with_update_file = $updater->getComponentUpdates( ); + $components_with_update_file = $updater->getComponentUpdates(); if ( empty( $components_with_update_file ) ) { return false; } - if (!self::lock()) { + if ( ! self::lock() ) { throw new UpdateInProgressException(); } try { - SettingsServer::setMaxExecutionTime(0); + SettingsServer::setMaxExecutionTime( 0 ); - if (function_exists('ignore_user_abort')) { - @ignore_user_abort(true); + if ( function_exists( 'ignore_user_abort' ) ) { + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + @ignore_user_abort( true ); } $result = $updater->updateComponents( $components_with_update_file ); - } catch (\Exception $e) { + } catch ( Exception $e ) { self::unlock(); throw $e; } self::unlock(); - if (!empty($result['errors'])) { - throw new \Exception('Error while updating components: ' . implode(', ' , $result['errors'])); + if ( ! empty( $result['errors'] ) ) { + throw new Exception( 'Error while updating components: ' . implode( ', ', $result['errors'] ) ); } \Piwik\Updater::recordComponentSuccessfullyUpdated( 'core', Version::VERSION ); diff --git a/classes/WpMatomo/Updater/UpdateInProgressException.php b/classes/WpMatomo/Updater/UpdateInProgressException.php index 61edba595..990fae688 100644 --- a/classes/WpMatomo/Updater/UpdateInProgressException.php +++ b/classes/WpMatomo/Updater/UpdateInProgressException.php @@ -9,13 +9,17 @@ namespace WpMatomo\Updater; +use Exception; + if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly } -class UpdateInProgressException extends \Exception { - - public function __construct( $message = "Matomo upgrade is already in progress", $code = 0, $previous = null ) { +/** + * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found + */ +class UpdateInProgressException extends Exception { + public function __construct( $message = 'Matomo upgrade is already in progress', $code = 0, $previous = null ) { parent::__construct( $message, $code, $previous ); } } diff --git a/classes/WpMatomo/User.php b/classes/WpMatomo/User.php index bdc73b585..2ca70798c 100644 --- a/classes/WpMatomo/User.php +++ b/classes/WpMatomo/User.php @@ -14,7 +14,6 @@ } class User { - const USER_MAPPING_PREFIX = 'matomo-user-login-'; /** @@ -41,6 +40,4 @@ public static function map_matomo_user_login( $wp_user_id, $matomo_user_login ) public function uninstall() { Uninstaller::uninstall_options( self::USER_MAPPING_PREFIX ); } - - } diff --git a/classes/WpMatomo/User/Sync.php b/classes/WpMatomo/User/Sync.php index 6e249ad57..9569d6c30 100644 --- a/classes/WpMatomo/User/Sync.php +++ b/classes/WpMatomo/User/Sync.php @@ -9,6 +9,7 @@ namespace WpMatomo\User; +use Exception; use Piwik\Access; use Piwik\Access\Role\Admin; use Piwik\Access\Role\View; @@ -18,8 +19,9 @@ use Piwik\Date; use Piwik\Plugin; use Piwik\Plugins\LanguagesManager\API; -use Piwik\Plugins\UsersManager\Model; use Piwik\Plugins\UsersManager; +use Piwik\Plugins\UsersManager\Model; +use WP_User; use WpMatomo\Bootstrap; use WpMatomo\Capabilities; use WpMatomo\Logger; @@ -32,6 +34,7 @@ } class Sync { + /** * actually allowed is 100 characters... * but we do -5 to have some room to append `wp_`.$login.XYZ if needed @@ -48,21 +51,20 @@ public function __construct() { } public function register_hooks() { - add_action( 'add_user_role', array( $this, 'sync_current_users_1000' ), $prio = 10, $args = 0 ); - add_action( 'remove_user_role', array( $this, 'sync_current_users_1000' ), $prio = 10, $args = 0 ); - add_action( 'add_user_to_blog', array( $this, 'sync_current_users_1000' ), $prio = 10, $args = 0 ); - add_action( 'remove_user_from_blog', array( $this, 'sync_current_users_1000' ), $prio = 10, $args = 0 ); - add_action( 'user_register', array( $this, 'sync_current_users_1000' ), $prio = 10, $args = 0 ); - add_action( 'profile_update', array( $this, 'sync_maybe_background' ), $prio = 10, $args = 0 ); + add_action( 'add_user_role', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 ); + add_action( 'remove_user_role', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 ); + add_action( 'add_user_to_blog', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 ); + add_action( 'remove_user_from_blog', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 ); + add_action( 'user_register', [ $this, 'sync_current_users_1000' ], $prio = 10, $args = 0 ); + add_action( 'profile_update', [ $this, 'sync_maybe_background' ], $prio = 10, $args = 0 ); } - public function sync_maybe_background() - { + public function sync_maybe_background() { global $pagenow; - if ( is_admin() && $pagenow == 'users.php' ) { + if ( is_admin() && 'users.php' === $pagenow ) { // eg for profile update we don't want to sync directly see #365 as it could cause issues with other plugins // if they eg alter `get_users` option - wp_schedule_single_event(time() + 5, ScheduledTasks::EVENT_SYNC); + wp_schedule_single_event( time() + 5, ScheduledTasks::EVENT_SYNC ); } else { $this->sync_current_users_1000(); } @@ -77,10 +79,10 @@ public function sync_all() { try { if ( $idsite ) { - $users = $this->get_users( array('blog_id' => $site->blog_id ) ); + $users = $this->get_users( [ 'blog_id' => $site->blog_id ] ); $this->sync_users( $users, $idsite ); } - } catch ( \Exception $e ) { + } catch ( Exception $e ) { // we don't want to rethrow exception otherwise some other blogs might never sync $this->logger->log_exception( 'user_sync ', $e ); } @@ -92,51 +94,51 @@ public function sync_all() { } } - private function get_users($options = array()) - { - /** @var \WP_User[] $users */ - $users = get_users( $options ); - - $current_user = wp_get_current_user(); - if (!empty($current_user) && !empty($current_user->user_login)) { - // refs https://github.com/matomo-org/wp-matomo/issues/365 - // some other plugins may under circumstances overwrite the get_users query and not return all users - // as a result we would delete some users in the matomo users table. this way we make sure at least the current - // user will be added and not deleted even if the list of users is not complete - $found = false; - foreach ($users as $user) { - if ($user->user_login === $current_user->user_login) { - $found = true; - break; - } - } - if (!$found) { - $users[] = $current_user; - } - } - - if (is_multisite()) { - $super_admins = get_super_admins(); - if (!empty($super_admins)) { - foreach ($super_admins as $super_admin) { - $found = false; - foreach ($users as $user) { - if ($user->user_login === $super_admin) { - $found = true; - break; - } - } - if (!$found) { - $user = get_user_by('login', $super_admin); - if (!empty($user)) { - $users[] = $user; - } - } - } - } - } - return $users; - } + private function get_users( $options = [] ) { + /** @var WP_User[] $users */ + $users = get_users( $options ); + + $current_user = wp_get_current_user(); + if ( ! empty( $current_user ) && ! empty( $current_user->user_login ) ) { + // refs https://github.com/matomo-org/wp-matomo/issues/365 + // some other plugins may under circumstances overwrite the get_users query and not return all users + // as a result we would delete some users in the matomo users table. this way we make sure at least the current + // user will be added and not deleted even if the list of users is not complete + $found = false; + foreach ( $users as $user ) { + if ( $user->user_login === $current_user->user_login ) { + $found = true; + break; + } + } + if ( ! $found ) { + $users[] = $current_user; + } + } + + if ( is_multisite() ) { + $super_admins = get_super_admins(); + if ( ! empty( $super_admins ) ) { + foreach ( $super_admins as $super_admin ) { + $found = false; + foreach ( $users as $user ) { + if ( $user->user_login === $super_admin ) { + $found = true; + break; + } + } + if ( ! $found ) { + $user = get_user_by( 'login', $super_admin ); + if ( ! empty( $user ) ) { + $users[] = $user; + } + } + } + } + } + + return $users; + } public function sync_current_users() { $idsite = Site::get_matomo_site_id( get_current_blog_id() ); @@ -149,9 +151,10 @@ public function sync_current_users() { /** * similar method to sync_current_users which synchronise on the fly only if we have less than 1000 users. * Otherwise it will be done by a background task - * @see Sync::sync_current_users() - * @see https://github.com/matomo-org/matomo-for-wordpress/issues/460 + * * @return void + * @see https://github.com/matomo-org/matomo-for-wordpress/issues/460 + * @see Sync::sync_current_users() */ public function sync_current_users_1000() { $idsite = Site::get_matomo_site_id( get_current_blog_id() ); @@ -162,11 +165,12 @@ public function sync_current_users_1000() { } } } + /** * Sync all users. Make sure to always pass all sites that exist within a given site... you cannot just sync an individual * user... we would delete all other users * - * @param \WP_User[] $users + * @param WP_User[] $users * @param $idsite */ protected function sync_users( $users, $idsite ) { @@ -174,8 +178,8 @@ protected function sync_users( $users, $idsite ) { $this->logger->log( 'Matomo will now sync ' . count( $users ) . ' users' ); - $super_users = array(); - $logins_with_some_view_access = array( 'anonmyous' ); // may or may not exist... we don't want to delete this user though + $super_users = []; + $logins_with_some_view_access = [ 'anonmyous' ]; // may or may not exist... we don't want to delete this user though $user_model = new Model(); // need to make sure we recreate new instance later with latest dependencies in case they changed @@ -197,34 +201,34 @@ protected function sync_users( $users, $idsite ) { $logins_with_some_view_access[] = $matomo_login; } elseif ( user_can( $user, Capabilities::KEY_ADMIN ) ) { $matomo_login = $this->ensure_user_exists( $user ); - $user_model->deleteUserAccess( $mapped_matomo_login, array( $idsite ) ); - $user_model->addUserAccess( $matomo_login, Admin::ID, array( $idsite ) ); + $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] ); + $user_model->addUserAccess( $matomo_login, Admin::ID, [ $idsite ] ); $user_model->setSuperUserAccess( $matomo_login, false ); $logins_with_some_view_access[] = $matomo_login; } elseif ( user_can( $user, Capabilities::KEY_WRITE ) ) { $matomo_login = $this->ensure_user_exists( $user ); - $user_model->deleteUserAccess( $mapped_matomo_login, array( $idsite ) ); - $user_model->addUserAccess( $matomo_login, Write::ID, array( $idsite ) ); + $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] ); + $user_model->addUserAccess( $matomo_login, Write::ID, [ $idsite ] ); $user_model->setSuperUserAccess( $matomo_login, false ); $logins_with_some_view_access[] = $matomo_login; } elseif ( user_can( $user, Capabilities::KEY_VIEW ) ) { $matomo_login = $this->ensure_user_exists( $user ); - $user_model->deleteUserAccess( $mapped_matomo_login, array( $idsite ) ); - $user_model->addUserAccess( $matomo_login, View::ID, array( $idsite ) ); + $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] ); + $user_model->addUserAccess( $matomo_login, View::ID, [ $idsite ] ); $user_model->setSuperUserAccess( $matomo_login, false ); $logins_with_some_view_access[] = $matomo_login; - } elseif ($mapped_matomo_login) { - $user_model->deleteUserAccess( $mapped_matomo_login, array( $idsite ) ); + } elseif ( $mapped_matomo_login ) { + $user_model->deleteUserAccess( $mapped_matomo_login, [ $idsite ] ); } if ( $matomo_login ) { - $locale = get_user_locale( $user->ID ); - $locale_dash = Common::mb_strtolower(str_replace('_', '-', $locale)); - $parts = []; - if ($locale && in_array($locale_dash, ['zh-cn', 'zh-tw', 'pt-br', 'es-ar'], true)) { - $parts = [$locale_dash]; - } elseif (!empty($locale) && is_string($locale)) { - $parts = explode( '_', $locale ); + $locale = get_user_locale( $user->ID ); + $locale_dash = Common::mb_strtolower( str_replace( '_', '-', $locale ) ); + $parts = []; + if ( $locale && in_array( $locale_dash, [ 'zh-cn', 'zh-tw', 'pt-br', 'es-ar' ], true ) ) { + $parts = [ $locale_dash ]; + } elseif ( ! empty( $locale ) && is_string( $locale ) ) { + $parts = explode( '_', $locale ); } if ( ! empty( $parts[0] ) ) { @@ -237,8 +241,8 @@ protected function sync_users( $users, $idsite ) { } } } - - if ($idsite != 1) { + // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + if ( 1 != $idsite ) { // only needed if the actual site is not the default site... makes sure when they click in Matomo // UI on "Dashboard" that the correct site is being opened by default // eg if the linked site is actually idSite=2. @@ -254,10 +258,10 @@ function () use ( $matomo_login, &$idsite ) { UsersManager\API::PREFERENCE_DEFAULT_REPORT, $idsite ); - } catch (\Exception $e) { + //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + } catch ( Exception $e ) { // ignore any error for now } - } ); } @@ -268,11 +272,10 @@ function () use ( $matomo_login, &$idsite ) { } $logins_with_some_view_access = array_unique( $logins_with_some_view_access ); - $all_users = $user_model->getUsers( array() ); + $all_users = $user_model->getUsers( [] ); foreach ( $all_users as $all_user ) { if ( ! in_array( $all_user['login'], $logins_with_some_view_access, true ) && ! empty( $all_user['login'] ) ) { - Access::doAsSuperUser( function () use ( $user_model, $all_user ) { $user_model->deleteUserOnly( $all_user['login'] ); @@ -285,7 +288,7 @@ function () use ( $user_model, $all_user ) { } /** - * @param \WP_User $wp_user + * @param WP_User $wp_user */ protected function ensure_user_exists( $wp_user ) { $user_model = new Model(); @@ -299,7 +302,7 @@ protected function ensure_user_exists( $wp_user ) { $user_in_matomo = $user_model->getUser( $matomo_user_login ); } else { // wp usernames may include whitespace etc - $login = preg_replace('/[^A-Za-zÄäÖöÜüß0-9_.@+-]+/D', '_', $login); + $login = preg_replace( '/[^A-Za-zÄäÖöÜüß0-9_.@+-]+/D', '_', $login ); $login = substr( $login, 0, self::MAX_USER_NAME_LENGTH ); if ( ! $user_model->getUser( $login ) ) { @@ -334,7 +337,7 @@ protected function ensure_user_exists( $wp_user ) { User::map_matomo_user_login( $user_id, $matomo_user_login ); } elseif ( $user_in_matomo['email'] !== $wp_user->user_email ) { $this->logger->log( 'Matomo is now updating the email for wpUserID ' . $user_id . ' matomo login ' . $matomo_user_login ); - $user_model->updateUserFields( $matomo_user_login, array( 'email' => $wp_user->user_email ) ); + $user_model->updateUserFields( $matomo_user_login, [ 'email' => $wp_user->user_email ] ); } return $matomo_user_login; diff --git a/classes/WpMatomo/views/referral.php b/classes/WpMatomo/views/referral.php index edd59c489..5f8ec80e3 100644 --- a/classes/WpMatomo/views/referral.php +++ b/classes/WpMatomo/views/referral.php @@ -12,11 +12,12 @@ } ?>
    -

    - +

    + - -

    -
    -
    \ No newline at end of file + +

    +
    + diff --git a/composer.json b/composer.json index 7ca279641..b2529141e 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "dealerdirect/phpcodesniffer-composer-installer": "^v0.7.1", "phpcompatibility/phpcompatibility-wp": "^2.1.1", "squizlabs/php_codesniffer": "^3.5.8", - "wp-coding-standards/wpcs": "^2.3.0" + "wp-coding-standards/wpcs": "^2.3.0", + "friendsofphp/php-cs-fixer": "^3.0" } -} \ No newline at end of file +} diff --git a/config/config.php b/config/config.php index bfe08f704..c9aef4d76 100644 --- a/config/config.php +++ b/config/config.php @@ -122,6 +122,7 @@ $class_name = get_class($check); if ($class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\ForceSSLCheck' || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\LoadDataInfileCheck' + || $class_name === 'Piwik\Plugins\CustomJsTracker\Diagnostic\TrackerJsCheck' || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\RequiredPrivateDirectories' // it doesn't resolve config path correctly as it is outside matomo dir etc || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\CronArchivingCheck' || $class_name === 'Piwik\Plugins\Diagnostics\Diagnostic\FileIntegrityCheck') { diff --git a/matomo.php b/matomo.php index 8ee86d9f0..b98f1a9eb 100644 --- a/matomo.php +++ b/matomo.php @@ -4,7 +4,7 @@ * Description: The #1 Google Analytics alternative that gives you full control over your data and protects the privacy for your users. Free, secure and open. * Author: Matomo * Author URI: https://matomo.org - * Version: 4.4.1 + * Version: 4.4.2 * Domain Path: /languages * WC requires at least: 2.4.0 * WC tested up to: 5.5.0 @@ -14,6 +14,9 @@ * @link https://matomo.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @package matomo + * phpcs:disable WordPress.Security.ValidatedSanitizedInput + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + * phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged */ if ( ! defined( 'ABSPATH' ) ) { exit; // if accessed directly @@ -25,7 +28,7 @@ define( 'MATOMO_ANALYTICS_FILE', __FILE__ ); } -if ( ! defined('MATOMO_MARKETPLACE_PLUGIN_NAME' )) { +if ( ! defined( 'MATOMO_MARKETPLACE_PLUGIN_NAME' ) ) { define( 'MATOMO_MARKETPLACE_PLUGIN_NAME', 'matomo-marketplace-for-wordpress/matomo-marketplace-for-wordpress.php' ); } @@ -35,8 +38,8 @@ $GLOBALS['MATOMO_PLUGIN_FILES'] = array( MATOMO_ANALYTICS_FILE ); function matomo_has_compatible_content_dir() { - if ( !empty( $_SERVER['MATOMO_WP_ROOT_PATH'] ) - && file_exists( rtrim($_SERVER['MATOMO_WP_ROOT_PATH'], '/') . '/wp-load.php' ) ) { + if ( ! empty( $_SERVER['MATOMO_WP_ROOT_PATH'] ) + && file_exists( rtrim( $_SERVER['MATOMO_WP_ROOT_PATH'], '/' ) . '/wp-load.php' ) ) { return true; } @@ -44,49 +47,50 @@ function matomo_has_compatible_content_dir() { return false; } - $contentDir = rtrim(rtrim( WP_CONTENT_DIR, '/' ), DIRECTORY_SEPARATOR ); - $contentDir = wp_normalize_path($contentDir); - $absPath = wp_normalize_path(ABSPATH); + $content_dir = rtrim( rtrim( WP_CONTENT_DIR, '/' ), DIRECTORY_SEPARATOR ); + $content_dir = wp_normalize_path( $content_dir ); + $abs_path = wp_normalize_path( ABSPATH ); - $absPaths = array( - $absPath . 'wp-content', - $absPath . '/wp-content', - $absPath . DIRECTORY_SEPARATOR . 'wp-content' + $abs_paths = array( + $abs_path . 'wp-content', + $abs_path . '/wp-content', + $abs_path . DIRECTORY_SEPARATOR . 'wp-content', ); - if (in_array($contentDir, $absPaths, true)) { + if ( in_array( $content_dir, $abs_paths, true ) ) { return true; } $wpload_base = '../../../wp-load.php'; $wpload_full = dirname( __FILE__ ) . '/' . $wpload_base; - if ( file_exists($wpload_full ) && is_readable( $wpload_full ) ) { + if ( file_exists( $wpload_full ) && is_readable( $wpload_full ) ) { return true; - } elseif (realpath( $wpload_full ) && file_exists(realpath( $wpload_full )) && is_readable(realpath( $wpload_full ))) { + } elseif ( realpath( $wpload_full ) && file_exists( realpath( $wpload_full ) ) && is_readable( realpath( $wpload_full ) ) ) { return true; - } elseif (!empty($_SERVER['SCRIPT_FILENAME']) && file_exists($_SERVER['SCRIPT_FILENAME'])) { + } elseif ( ! empty( $_SERVER['SCRIPT_FILENAME'] ) && file_exists( $_SERVER['SCRIPT_FILENAME'] ) ) { // seems symlinked... eg the wp-content dir or wp-content/plugins dir is symlinked from some very much other place... - $wpload_full = dirname($_SERVER['SCRIPT_FILENAME']) . '/' . $wpload_base; - if ( file_exists($wpload_full ) ) { + $wpload_full = dirname( $_SERVER['SCRIPT_FILENAME'] ) . '/' . $wpload_base; + if ( file_exists( $wpload_full ) ) { return true; - } elseif (realpath( $wpload_full ) && file_exists(realpath( $wpload_full ))) { + } elseif ( realpath( $wpload_full ) && file_exists( realpath( $wpload_full ) ) ) { return true; - } elseif (file_exists(dirname( $_SERVER['SCRIPT_FILENAME'] )) . '/wp-load.php') { + } elseif ( file_exists( dirname( $_SERVER['SCRIPT_FILENAME'] ) ) . '/wp-load.php' ) { return true; } } // look in plugins directory if there is a config file for us - $wpload_config = dirname(__FILE__) . '/../matomo.wpload_dir.php'; - if (file_exists( $wpload_config) && is_readable($wpload_config)) { - $content = @file_get_contents($wpload_config); // we do not include that file for security reasons - if (!empty($content)) { - $content = str_replace(array(''; + echo ''; } function matomo_is_app_request() { @@ -109,7 +113,7 @@ function matomo_is_app_request() { function matomo_has_tag_manager() { if ( defined( 'MATOMO_ENABLE_TAG_MANAGER' ) ) { - return !empty(MATOMO_ENABLE_TAG_MANAGER); + return ! empty( MATOMO_ENABLE_TAG_MANAGER ); } $is_multisite = function_exists( 'is_multisite' ) && is_multisite(); @@ -123,34 +127,34 @@ function matomo_has_tag_manager() { function matomo_anonymize_value( $value ) { if ( is_string( $value ) && ! empty( $value ) ) { $values_to_anonymize = array( - ABSPATH => '$ABSPATH/', - str_replace( '/', '\/', ABSPATH ) => '$ABSPATH\/', - str_replace( '/', '\\', ABSPATH ) => '$ABSPATH\/', - WP_CONTENT_DIR => '$WP_CONTENT_DIR/', + ABSPATH => '$abs_path/', + str_replace( '/', '\/', ABSPATH ) => '$abs_path\/', + str_replace( '/', '\\', ABSPATH ) => '$abs_path\/', + WP_CONTENT_DIR => '$WP_CONTENT_DIR/', str_replace( '/', '\\', WP_CONTENT_DIR ) => '$WP_CONTENT_DIR\\', - home_url() => '$home_url', - site_url() => '$site_url', - DB_PASSWORD => '$DB_PASSWORD', - DB_USER => '$DB_USER', - DB_HOST => '$DB_HOST', - DB_NAME => '$DB_NAME', + home_url() => '$home_url', + site_url() => '$site_url', + DB_PASSWORD => '$DB_PASSWORD', + DB_USER => '$DB_USER', + DB_HOST => '$DB_HOST', + DB_NAME => '$DB_NAME', ); - $keys = array('AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'AUTH_SALT', 'NONCE_KEY', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT'); - foreach ($keys as $key) { - if (defined($key)) { - $const_value = constant($key); - if (!empty($const_value) && is_string($const_value) && strlen($key) > 3) { - $values_to_anonymize[$const_value] = '$' . $key; + $keys = array( 'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'AUTH_SALT', 'NONCE_KEY', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT' ); + foreach ( $keys as $key ) { + if ( defined( $key ) ) { + $const_value = constant( $key ); + if ( ! empty( $const_value ) && is_string( $const_value ) && strlen( $key ) > 3 ) { + $values_to_anonymize[ $const_value ] = '$' . $key; } } } foreach ( $values_to_anonymize as $search => $replace ) { - if ($search) { + if ( $search ) { $value = str_replace( $search, $replace, $value ); } } // replace anything like token_auth etc or md5 or sha1 ... - $value = preg_replace('/[[:xdigit:]]{31,80}/', 'TOKEN_REPLACED', $value); + $value = preg_replace( '/[[:xdigit:]]{31,80}/', 'TOKEN_REPLACED', $value ); } return $value; @@ -195,11 +199,11 @@ function matomo_add_plugin( $plugins_directory, $wp_plugin_file, $is_marketplace ); } -if (matomo_is_app_request() || !empty($GLOBALS['MATOMO_LOADED_DIRECTLY'])) { +if ( matomo_is_app_request() || ! empty( $GLOBALS['MATOMO_LOADED_DIRECTLY'] ) ) { // prevent layout being broken when thegem theme is used. their lazy items class causes the reporting UI to not appear // because it creates a JS error because of escaping " too often. only breaks when " Activate image loading optimization (for desktops)" // is enabled in the general theme settings - add_filter('thegem_lazy_items_need_process_content', '__return_false', 99999999, $args = 0); + add_filter( 'thegem_lazy_items_need_process_content', '__return_false', 99999999, $args = 0 ); } require_once __DIR__ . DIRECTORY_SEPARATOR . 'classes' . DIRECTORY_SEPARATOR . 'WpMatomo.php'; diff --git a/node_modules/chart.js/LICENSE.md b/node_modules/chart.js/LICENSE.md new file mode 100644 index 000000000..5060fabc1 --- /dev/null +++ b/node_modules/chart.js/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2014-2021 Chart.js Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/chart.js/README.md b/node_modules/chart.js/README.md new file mode 100644 index 000000000..57a242aa0 --- /dev/null +++ b/node_modules/chart.js/README.md @@ -0,0 +1,36 @@ +

    +
    + Simple yet flexible JavaScript charting for designers & developers +

    + +

    + Downloads + GitHub Workflow Status + Coverage + Awesome + Slack +

    + +## Documentation + +All the links point to the new version 3 of the lib. + +* [Introduction](https://www.chartjs.org/docs/latest/) +* [Getting Started](https://www.chartjs.org/docs/latest/getting-started/index) +* [General](https://www.chartjs.org/docs/latest/general/data-structures) +* [Configuration](https://www.chartjs.org/docs/latest/configuration/index) +* [Charts](https://www.chartjs.org/docs/latest/charts/line) +* [Axes](https://www.chartjs.org/docs/latest/axes/index) +* [Developers](https://www.chartjs.org/docs/latest/developers/index) +* [Popular Extensions](https://github.com/chartjs/awesome) +* [Samples](https://www.chartjs.org/samples/) + +In case you are looking for the docs of version 2, you will have to specify the specific version in the url like this: [https://www.chartjs.org/docs/2.9.4/](https://www.chartjs.org/docs/2.9.4/) + +## Contributing + +Instructions on building and testing Chart.js can be found in [the documentation](https://www.chartjs.org/docs/master/developers/contributing.html#building-and-testing). Before submitting an issue or a pull request, please take a moment to look over the [contributing guidelines](https://www.chartjs.org/docs/master/developers/contributing) first. For support, please post questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/chartjs) with the `chartjs` tag. + +## License + +Chart.js is available under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/node_modules/chart.js/auto/auto.esm.d.ts b/node_modules/chart.js/auto/auto.esm.d.ts new file mode 100644 index 000000000..f0bc38054 --- /dev/null +++ b/node_modules/chart.js/auto/auto.esm.d.ts @@ -0,0 +1,4 @@ +import { Chart } from '../types/index.esm'; + +export * from '../types/index.esm'; +export default Chart; diff --git a/node_modules/chart.js/auto/auto.esm.js b/node_modules/chart.js/auto/auto.esm.js new file mode 100644 index 000000000..42626764a --- /dev/null +++ b/node_modules/chart.js/auto/auto.esm.js @@ -0,0 +1,5 @@ +import {Chart, registerables} from '../dist/chart.esm'; + +Chart.register(...registerables); + +export default Chart; diff --git a/node_modules/chart.js/auto/auto.js b/node_modules/chart.js/auto/auto.js new file mode 100644 index 000000000..235580fef --- /dev/null +++ b/node_modules/chart.js/auto/auto.js @@ -0,0 +1 @@ +module.exports = require('../dist/chart'); diff --git a/node_modules/chart.js/auto/package.json b/node_modules/chart.js/auto/package.json new file mode 100644 index 000000000..c2ad5b2ca --- /dev/null +++ b/node_modules/chart.js/auto/package.json @@ -0,0 +1,8 @@ +{ + "name": "chart.js-auto", + "private": true, + "description": "auto registering package", + "main": "auto.js", + "module": "auto.esm.js", + "types": "auto.esm.d.ts" +} diff --git a/node_modules/chart.js/dist/chart.esm.js b/node_modules/chart.js/dist/chart.esm.js new file mode 100644 index 000000000..7c26884b2 --- /dev/null +++ b/node_modules/chart.js/dist/chart.esm.js @@ -0,0 +1,10452 @@ +/*! + * Chart.js v3.4.1 + * https://www.chartjs.org + * (c) 2021 Chart.js Contributors + * Released under the MIT License + */ +import { r as requestAnimFrame, a as resolve, e as effects, c as color, i as isObject, b as isArray, d as defaults, v as valueOrDefault, u as unlistenArrayEvents, l as listenArrayEvents, f as resolveObjectKey, g as isNumberFinite, h as defined, s as sign, j as isNullOrUndef, _ as _arrayUnique, t as toRadians, k as toPercentage, m as toDimension, T as TAU, n as formatNumber, o as _angleBetween, H as HALF_PI, P as PI, p as isNumber, q as _limitValue, w as _lookupByKey, x as getRelativePosition$1, y as _isPointInArea, z as _rlookupByKey, A as toPadding, B as each, C as getMaximumSize, D as _getParentNode, E as readUsedSize, F as throttled, G as supportsEventListenerOptions, I as log10, J as _factorize, K as finiteOrDefault, L as callback, M as _addGrace, N as toDegrees, O as _measureText, Q as _int16Range, R as _alignPixel, S as clipArea, U as renderText, V as unclipArea, W as toFont, X as _toLeftRightCenter, Y as _alignStartEnd, Z as overrides, $ as merge, a0 as _capitalize, a1 as descriptors, a2 as isFunction, a3 as _attachContext, a4 as _createResolver, a5 as _descriptors, a6 as mergeIf, a7 as uid, a8 as debounce, a9 as retinaScale, aa as clearCanvas, ab as setsEqual, ac as _elementsEqual, ad as getAngleFromPoint, ae as _readValueToProps, af as _updateBezierControlPoints, ag as _computeSegments, ah as _boundSegments, ai as _steppedInterpolation, aj as _bezierInterpolation, ak as _pointInLine, al as _steppedLineTo, am as _bezierCurveTo, an as drawPoint, ao as addRoundedRectPath, ap as toTRBL, aq as toTRBLCorners, ar as _boundSegment, as as _normalizeAngle, at as getRtlAdapter, au as overrideTextDirection, av as _textX, aw as restoreTextDirection, ax as noop, ay as distanceBetweenPoints, az as _setMinAndMaxByKey, aA as niceNum, aB as almostWhole, aC as almostEquals, aD as _decimalPlaces, aE as _longestText, aF as _filterBetween, aG as _lookup } from './chunks/helpers.segment.js'; +export { d as defaults } from './chunks/helpers.segment.js'; + +class Animator { + constructor() { + this._request = null; + this._charts = new Map(); + this._running = false; + this._lastDate = undefined; + } + _notify(chart, anims, date, type) { + const callbacks = anims.listeners[type]; + const numSteps = anims.duration; + callbacks.forEach(fn => fn({ + chart, + initial: anims.initial, + numSteps, + currentStep: Math.min(date - anims.start, numSteps) + })); + } + _refresh() { + const me = this; + if (me._request) { + return; + } + me._running = true; + me._request = requestAnimFrame.call(window, () => { + me._update(); + me._request = null; + if (me._running) { + me._refresh(); + } + }); + } + _update(date = Date.now()) { + const me = this; + let remaining = 0; + me._charts.forEach((anims, chart) => { + if (!anims.running || !anims.items.length) { + return; + } + const items = anims.items; + let i = items.length - 1; + let draw = false; + let item; + for (; i >= 0; --i) { + item = items[i]; + if (item._active) { + if (item._total > anims.duration) { + anims.duration = item._total; + } + item.tick(date); + draw = true; + } else { + items[i] = items[items.length - 1]; + items.pop(); + } + } + if (draw) { + chart.draw(); + me._notify(chart, anims, date, 'progress'); + } + if (!items.length) { + anims.running = false; + me._notify(chart, anims, date, 'complete'); + anims.initial = false; + } + remaining += items.length; + }); + me._lastDate = date; + if (remaining === 0) { + me._running = false; + } + } + _getAnims(chart) { + const charts = this._charts; + let anims = charts.get(chart); + if (!anims) { + anims = { + running: false, + initial: true, + items: [], + listeners: { + complete: [], + progress: [] + } + }; + charts.set(chart, anims); + } + return anims; + } + listen(chart, event, cb) { + this._getAnims(chart).listeners[event].push(cb); + } + add(chart, items) { + if (!items || !items.length) { + return; + } + this._getAnims(chart).items.push(...items); + } + has(chart) { + return this._getAnims(chart).items.length > 0; + } + start(chart) { + const anims = this._charts.get(chart); + if (!anims) { + return; + } + anims.running = true; + anims.start = Date.now(); + anims.duration = anims.items.reduce((acc, cur) => Math.max(acc, cur._duration), 0); + this._refresh(); + } + running(chart) { + if (!this._running) { + return false; + } + const anims = this._charts.get(chart); + if (!anims || !anims.running || !anims.items.length) { + return false; + } + return true; + } + stop(chart) { + const anims = this._charts.get(chart); + if (!anims || !anims.items.length) { + return; + } + const items = anims.items; + let i = items.length - 1; + for (; i >= 0; --i) { + items[i].cancel(); + } + anims.items = []; + this._notify(chart, anims, Date.now(), 'complete'); + } + remove(chart) { + return this._charts.delete(chart); + } +} +var animator = new Animator(); + +const transparent = 'transparent'; +const interpolators = { + boolean(from, to, factor) { + return factor > 0.5 ? to : from; + }, + color(from, to, factor) { + const c0 = color(from || transparent); + const c1 = c0.valid && color(to || transparent); + return c1 && c1.valid + ? c1.mix(c0, factor).hexString() + : to; + }, + number(from, to, factor) { + return from + (to - from) * factor; + } +}; +class Animation { + constructor(cfg, target, prop, to) { + const currentValue = target[prop]; + to = resolve([cfg.to, to, currentValue, cfg.from]); + const from = resolve([cfg.from, currentValue, to]); + this._active = true; + this._fn = cfg.fn || interpolators[cfg.type || typeof from]; + this._easing = effects[cfg.easing] || effects.linear; + this._start = Math.floor(Date.now() + (cfg.delay || 0)); + this._duration = this._total = Math.floor(cfg.duration); + this._loop = !!cfg.loop; + this._target = target; + this._prop = prop; + this._from = from; + this._to = to; + this._promises = undefined; + } + active() { + return this._active; + } + update(cfg, to, date) { + const me = this; + if (me._active) { + me._notify(false); + const currentValue = me._target[me._prop]; + const elapsed = date - me._start; + const remain = me._duration - elapsed; + me._start = date; + me._duration = Math.floor(Math.max(remain, cfg.duration)); + me._total += elapsed; + me._loop = !!cfg.loop; + me._to = resolve([cfg.to, to, currentValue, cfg.from]); + me._from = resolve([cfg.from, currentValue, to]); + } + } + cancel() { + const me = this; + if (me._active) { + me.tick(Date.now()); + me._active = false; + me._notify(false); + } + } + tick(date) { + const me = this; + const elapsed = date - me._start; + const duration = me._duration; + const prop = me._prop; + const from = me._from; + const loop = me._loop; + const to = me._to; + let factor; + me._active = from !== to && (loop || (elapsed < duration)); + if (!me._active) { + me._target[prop] = to; + me._notify(true); + return; + } + if (elapsed < 0) { + me._target[prop] = from; + return; + } + factor = (elapsed / duration) % 2; + factor = loop && factor > 1 ? 2 - factor : factor; + factor = me._easing(Math.min(1, Math.max(0, factor))); + me._target[prop] = me._fn(from, to, factor); + } + wait() { + const promises = this._promises || (this._promises = []); + return new Promise((res, rej) => { + promises.push({res, rej}); + }); + } + _notify(resolved) { + const method = resolved ? 'res' : 'rej'; + const promises = this._promises || []; + for (let i = 0; i < promises.length; i++) { + promises[i][method](); + } + } +} + +const numbers = ['x', 'y', 'borderWidth', 'radius', 'tension']; +const colors = ['color', 'borderColor', 'backgroundColor']; +defaults.set('animation', { + delay: undefined, + duration: 1000, + easing: 'easeOutQuart', + fn: undefined, + from: undefined, + loop: undefined, + to: undefined, + type: undefined, +}); +const animationOptions = Object.keys(defaults.animation); +defaults.describe('animation', { + _fallback: false, + _indexable: false, + _scriptable: (name) => name !== 'onProgress' && name !== 'onComplete' && name !== 'fn', +}); +defaults.set('animations', { + colors: { + type: 'color', + properties: colors + }, + numbers: { + type: 'number', + properties: numbers + }, +}); +defaults.describe('animations', { + _fallback: 'animation', +}); +defaults.set('transitions', { + active: { + animation: { + duration: 400 + } + }, + resize: { + animation: { + duration: 0 + } + }, + show: { + animations: { + colors: { + from: 'transparent' + }, + visible: { + type: 'boolean', + duration: 0 + }, + } + }, + hide: { + animations: { + colors: { + to: 'transparent' + }, + visible: { + type: 'boolean', + easing: 'linear', + fn: v => v | 0 + }, + } + } +}); +class Animations { + constructor(chart, config) { + this._chart = chart; + this._properties = new Map(); + this.configure(config); + } + configure(config) { + if (!isObject(config)) { + return; + } + const animatedProps = this._properties; + Object.getOwnPropertyNames(config).forEach(key => { + const cfg = config[key]; + if (!isObject(cfg)) { + return; + } + const resolved = {}; + for (const option of animationOptions) { + resolved[option] = cfg[option]; + } + (isArray(cfg.properties) && cfg.properties || [key]).forEach((prop) => { + if (prop === key || !animatedProps.has(prop)) { + animatedProps.set(prop, resolved); + } + }); + }); + } + _animateOptions(target, values) { + const newOptions = values.options; + const options = resolveTargetOptions(target, newOptions); + if (!options) { + return []; + } + const animations = this._createAnimations(options, newOptions); + if (newOptions.$shared) { + awaitAll(target.options.$animations, newOptions).then(() => { + target.options = newOptions; + }, () => { + }); + } + return animations; + } + _createAnimations(target, values) { + const animatedProps = this._properties; + const animations = []; + const running = target.$animations || (target.$animations = {}); + const props = Object.keys(values); + const date = Date.now(); + let i; + for (i = props.length - 1; i >= 0; --i) { + const prop = props[i]; + if (prop.charAt(0) === '$') { + continue; + } + if (prop === 'options') { + animations.push(...this._animateOptions(target, values)); + continue; + } + const value = values[prop]; + let animation = running[prop]; + const cfg = animatedProps.get(prop); + if (animation) { + if (cfg && animation.active()) { + animation.update(cfg, value, date); + continue; + } else { + animation.cancel(); + } + } + if (!cfg || !cfg.duration) { + target[prop] = value; + continue; + } + running[prop] = animation = new Animation(cfg, target, prop, value); + animations.push(animation); + } + return animations; + } + update(target, values) { + if (this._properties.size === 0) { + Object.assign(target, values); + return; + } + const animations = this._createAnimations(target, values); + if (animations.length) { + animator.add(this._chart, animations); + return true; + } + } +} +function awaitAll(animations, properties) { + const running = []; + const keys = Object.keys(properties); + for (let i = 0; i < keys.length; i++) { + const anim = animations[keys[i]]; + if (anim && anim.active()) { + running.push(anim.wait()); + } + } + return Promise.all(running); +} +function resolveTargetOptions(target, newOptions) { + if (!newOptions) { + return; + } + let options = target.options; + if (!options) { + target.options = newOptions; + return; + } + if (options.$shared) { + target.options = options = Object.assign({}, options, {$shared: false, $animations: {}}); + } + return options; +} + +function scaleClip(scale, allowedOverflow) { + const opts = scale && scale.options || {}; + const reverse = opts.reverse; + const min = opts.min === undefined ? allowedOverflow : 0; + const max = opts.max === undefined ? allowedOverflow : 0; + return { + start: reverse ? max : min, + end: reverse ? min : max + }; +} +function defaultClip(xScale, yScale, allowedOverflow) { + if (allowedOverflow === false) { + return false; + } + const x = scaleClip(xScale, allowedOverflow); + const y = scaleClip(yScale, allowedOverflow); + return { + top: y.end, + right: x.end, + bottom: y.start, + left: x.start + }; +} +function toClip(value) { + let t, r, b, l; + if (isObject(value)) { + t = value.top; + r = value.right; + b = value.bottom; + l = value.left; + } else { + t = r = b = l = value; + } + return { + top: t, + right: r, + bottom: b, + left: l, + disabled: value === false + }; +} +function getSortedDatasetIndices(chart, filterVisible) { + const keys = []; + const metasets = chart._getSortedDatasetMetas(filterVisible); + let i, ilen; + for (i = 0, ilen = metasets.length; i < ilen; ++i) { + keys.push(metasets[i].index); + } + return keys; +} +function applyStack(stack, value, dsIndex, options) { + const keys = stack.keys; + const singleMode = options.mode === 'single'; + let i, ilen, datasetIndex, otherValue; + if (value === null) { + return; + } + for (i = 0, ilen = keys.length; i < ilen; ++i) { + datasetIndex = +keys[i]; + if (datasetIndex === dsIndex) { + if (options.all) { + continue; + } + break; + } + otherValue = stack.values[datasetIndex]; + if (isNumberFinite(otherValue) && (singleMode || (value === 0 || sign(value) === sign(otherValue)))) { + value += otherValue; + } + } + return value; +} +function convertObjectDataToArray(data) { + const keys = Object.keys(data); + const adata = new Array(keys.length); + let i, ilen, key; + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + adata[i] = { + x: key, + y: data[key] + }; + } + return adata; +} +function isStacked(scale, meta) { + const stacked = scale && scale.options.stacked; + return stacked || (stacked === undefined && meta.stack !== undefined); +} +function getStackKey(indexScale, valueScale, meta) { + return `${indexScale.id}.${valueScale.id}.${meta.stack || meta.type}`; +} +function getUserBounds(scale) { + const {min, max, minDefined, maxDefined} = scale.getUserBounds(); + return { + min: minDefined ? min : Number.NEGATIVE_INFINITY, + max: maxDefined ? max : Number.POSITIVE_INFINITY + }; +} +function getOrCreateStack(stacks, stackKey, indexValue) { + const subStack = stacks[stackKey] || (stacks[stackKey] = {}); + return subStack[indexValue] || (subStack[indexValue] = {}); +} +function getLastIndexInStack(stack, vScale, positive) { + for (const meta of vScale.getMatchingVisibleMetas('bar').reverse()) { + const value = stack[meta.index]; + if ((positive && value > 0) || (!positive && value < 0)) { + return meta.index; + } + } + return null; +} +function updateStacks(controller, parsed) { + const {chart, _cachedMeta: meta} = controller; + const stacks = chart._stacks || (chart._stacks = {}); + const {iScale, vScale, index: datasetIndex} = meta; + const iAxis = iScale.axis; + const vAxis = vScale.axis; + const key = getStackKey(iScale, vScale, meta); + const ilen = parsed.length; + let stack; + for (let i = 0; i < ilen; ++i) { + const item = parsed[i]; + const {[iAxis]: index, [vAxis]: value} = item; + const itemStacks = item._stacks || (item._stacks = {}); + stack = itemStacks[vAxis] = getOrCreateStack(stacks, key, index); + stack[datasetIndex] = value; + stack._top = getLastIndexInStack(stack, vScale, true); + stack._bottom = getLastIndexInStack(stack, vScale, false); + } +} +function getFirstScaleId(chart, axis) { + const scales = chart.scales; + return Object.keys(scales).filter(key => scales[key].axis === axis).shift(); +} +function createDatasetContext(parent, index) { + return Object.assign(Object.create(parent), + { + active: false, + dataset: undefined, + datasetIndex: index, + index, + mode: 'default', + type: 'dataset' + } + ); +} +function createDataContext(parent, index, element) { + return Object.assign(Object.create(parent), { + active: false, + dataIndex: index, + parsed: undefined, + raw: undefined, + element, + index, + mode: 'default', + type: 'data' + }); +} +function clearStacks(meta, items) { + const axis = meta.vScale && meta.vScale.axis; + if (!axis) { + return; + } + items = items || meta._parsed; + for (const parsed of items) { + const stacks = parsed._stacks; + if (!stacks || stacks[axis] === undefined || stacks[axis][meta.index] === undefined) { + return; + } + delete stacks[axis][meta.index]; + } +} +const isDirectUpdateMode = (mode) => mode === 'reset' || mode === 'none'; +const cloneIfNotShared = (cached, shared) => shared ? cached : Object.assign({}, cached); +class DatasetController { + constructor(chart, datasetIndex) { + this.chart = chart; + this._ctx = chart.ctx; + this.index = datasetIndex; + this._cachedDataOpts = {}; + this._cachedMeta = this.getMeta(); + this._type = this._cachedMeta.type; + this.options = undefined; + this._parsing = false; + this._data = undefined; + this._objectData = undefined; + this._sharedOptions = undefined; + this._drawStart = undefined; + this._drawCount = undefined; + this.enableOptionSharing = false; + this.$context = undefined; + this._syncList = []; + this.initialize(); + } + initialize() { + const me = this; + const meta = me._cachedMeta; + me.configure(); + me.linkScales(); + meta._stacked = isStacked(meta.vScale, meta); + me.addElements(); + } + updateIndex(datasetIndex) { + if (this.index !== datasetIndex) { + clearStacks(this._cachedMeta); + } + this.index = datasetIndex; + } + linkScales() { + const me = this; + const chart = me.chart; + const meta = me._cachedMeta; + const dataset = me.getDataset(); + const chooseId = (axis, x, y, r) => axis === 'x' ? x : axis === 'r' ? r : y; + const xid = meta.xAxisID = valueOrDefault(dataset.xAxisID, getFirstScaleId(chart, 'x')); + const yid = meta.yAxisID = valueOrDefault(dataset.yAxisID, getFirstScaleId(chart, 'y')); + const rid = meta.rAxisID = valueOrDefault(dataset.rAxisID, getFirstScaleId(chart, 'r')); + const indexAxis = meta.indexAxis; + const iid = meta.iAxisID = chooseId(indexAxis, xid, yid, rid); + const vid = meta.vAxisID = chooseId(indexAxis, yid, xid, rid); + meta.xScale = me.getScaleForId(xid); + meta.yScale = me.getScaleForId(yid); + meta.rScale = me.getScaleForId(rid); + meta.iScale = me.getScaleForId(iid); + meta.vScale = me.getScaleForId(vid); + } + getDataset() { + return this.chart.data.datasets[this.index]; + } + getMeta() { + return this.chart.getDatasetMeta(this.index); + } + getScaleForId(scaleID) { + return this.chart.scales[scaleID]; + } + _getOtherScale(scale) { + const meta = this._cachedMeta; + return scale === meta.iScale + ? meta.vScale + : meta.iScale; + } + reset() { + this._update('reset'); + } + _destroy() { + const meta = this._cachedMeta; + if (this._data) { + unlistenArrayEvents(this._data, this); + } + if (meta._stacked) { + clearStacks(meta); + } + } + _dataCheck() { + const me = this; + const dataset = me.getDataset(); + const data = dataset.data || (dataset.data = []); + const _data = me._data; + if (isObject(data)) { + me._data = convertObjectDataToArray(data); + } else if (_data !== data) { + if (_data) { + unlistenArrayEvents(_data, me); + const meta = me._cachedMeta; + clearStacks(meta); + meta._parsed = []; + } + if (data && Object.isExtensible(data)) { + listenArrayEvents(data, me); + } + me._syncList = []; + me._data = data; + } + } + addElements() { + const me = this; + const meta = me._cachedMeta; + me._dataCheck(); + if (me.datasetElementType) { + meta.dataset = new me.datasetElementType(); + } + } + buildOrUpdateElements(resetNewElements) { + const me = this; + const meta = me._cachedMeta; + const dataset = me.getDataset(); + let stackChanged = false; + me._dataCheck(); + const oldStacked = meta._stacked; + meta._stacked = isStacked(meta.vScale, meta); + if (meta.stack !== dataset.stack) { + stackChanged = true; + clearStacks(meta); + meta.stack = dataset.stack; + } + me._resyncElements(resetNewElements); + if (stackChanged || oldStacked !== meta._stacked) { + updateStacks(me, meta._parsed); + } + } + configure() { + const me = this; + const config = me.chart.config; + const scopeKeys = config.datasetScopeKeys(me._type); + const scopes = config.getOptionScopes(me.getDataset(), scopeKeys, true); + me.options = config.createResolver(scopes, me.getContext()); + me._parsing = me.options.parsing; + } + parse(start, count) { + const me = this; + const {_cachedMeta: meta, _data: data} = me; + const {iScale, _stacked} = meta; + const iAxis = iScale.axis; + let sorted = start === 0 && count === data.length ? true : meta._sorted; + let prev = start > 0 && meta._parsed[start - 1]; + let i, cur, parsed; + if (me._parsing === false) { + meta._parsed = data; + meta._sorted = true; + parsed = data; + } else { + if (isArray(data[start])) { + parsed = me.parseArrayData(meta, data, start, count); + } else if (isObject(data[start])) { + parsed = me.parseObjectData(meta, data, start, count); + } else { + parsed = me.parsePrimitiveData(meta, data, start, count); + } + const isNotInOrderComparedToPrev = () => cur[iAxis] === null || (prev && cur[iAxis] < prev[iAxis]); + for (i = 0; i < count; ++i) { + meta._parsed[i + start] = cur = parsed[i]; + if (sorted) { + if (isNotInOrderComparedToPrev()) { + sorted = false; + } + prev = cur; + } + } + meta._sorted = sorted; + } + if (_stacked) { + updateStacks(me, parsed); + } + } + parsePrimitiveData(meta, data, start, count) { + const {iScale, vScale} = meta; + const iAxis = iScale.axis; + const vAxis = vScale.axis; + const labels = iScale.getLabels(); + const singleScale = iScale === vScale; + const parsed = new Array(count); + let i, ilen, index; + for (i = 0, ilen = count; i < ilen; ++i) { + index = i + start; + parsed[i] = { + [iAxis]: singleScale || iScale.parse(labels[index], index), + [vAxis]: vScale.parse(data[index], index) + }; + } + return parsed; + } + parseArrayData(meta, data, start, count) { + const {xScale, yScale} = meta; + const parsed = new Array(count); + let i, ilen, index, item; + for (i = 0, ilen = count; i < ilen; ++i) { + index = i + start; + item = data[index]; + parsed[i] = { + x: xScale.parse(item[0], index), + y: yScale.parse(item[1], index) + }; + } + return parsed; + } + parseObjectData(meta, data, start, count) { + const {xScale, yScale} = meta; + const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; + const parsed = new Array(count); + let i, ilen, index, item; + for (i = 0, ilen = count; i < ilen; ++i) { + index = i + start; + item = data[index]; + parsed[i] = { + x: xScale.parse(resolveObjectKey(item, xAxisKey), index), + y: yScale.parse(resolveObjectKey(item, yAxisKey), index) + }; + } + return parsed; + } + getParsed(index) { + return this._cachedMeta._parsed[index]; + } + getDataElement(index) { + return this._cachedMeta.data[index]; + } + applyStack(scale, parsed, mode) { + const chart = this.chart; + const meta = this._cachedMeta; + const value = parsed[scale.axis]; + const stack = { + keys: getSortedDatasetIndices(chart, true), + values: parsed._stacks[scale.axis] + }; + return applyStack(stack, value, meta.index, {mode}); + } + updateRangeFromParsed(range, scale, parsed, stack) { + const parsedValue = parsed[scale.axis]; + let value = parsedValue === null ? NaN : parsedValue; + const values = stack && parsed._stacks[scale.axis]; + if (stack && values) { + stack.values = values; + range.min = Math.min(range.min, value); + range.max = Math.max(range.max, value); + value = applyStack(stack, parsedValue, this._cachedMeta.index, {all: true}); + } + range.min = Math.min(range.min, value); + range.max = Math.max(range.max, value); + } + getMinMax(scale, canStack) { + const me = this; + const meta = me._cachedMeta; + const _parsed = meta._parsed; + const sorted = meta._sorted && scale === meta.iScale; + const ilen = _parsed.length; + const otherScale = me._getOtherScale(scale); + const stack = canStack && meta._stacked && {keys: getSortedDatasetIndices(me.chart, true), values: null}; + const range = {min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY}; + const {min: otherMin, max: otherMax} = getUserBounds(otherScale); + let i, value, parsed, otherValue; + function _skip() { + parsed = _parsed[i]; + value = parsed[scale.axis]; + otherValue = parsed[otherScale.axis]; + return !isNumberFinite(value) || otherMin > otherValue || otherMax < otherValue; + } + for (i = 0; i < ilen; ++i) { + if (_skip()) { + continue; + } + me.updateRangeFromParsed(range, scale, parsed, stack); + if (sorted) { + break; + } + } + if (sorted) { + for (i = ilen - 1; i >= 0; --i) { + if (_skip()) { + continue; + } + me.updateRangeFromParsed(range, scale, parsed, stack); + break; + } + } + return range; + } + getAllParsedValues(scale) { + const parsed = this._cachedMeta._parsed; + const values = []; + let i, ilen, value; + for (i = 0, ilen = parsed.length; i < ilen; ++i) { + value = parsed[i][scale.axis]; + if (isNumberFinite(value)) { + values.push(value); + } + } + return values; + } + getMaxOverflow() { + return false; + } + getLabelAndValue(index) { + const me = this; + const meta = me._cachedMeta; + const iScale = meta.iScale; + const vScale = meta.vScale; + const parsed = me.getParsed(index); + return { + label: iScale ? '' + iScale.getLabelForValue(parsed[iScale.axis]) : '', + value: vScale ? '' + vScale.getLabelForValue(parsed[vScale.axis]) : '' + }; + } + _update(mode) { + const me = this; + const meta = me._cachedMeta; + me.configure(); + me._cachedDataOpts = {}; + me.update(mode || 'default'); + meta._clip = toClip(valueOrDefault(me.options.clip, defaultClip(meta.xScale, meta.yScale, me.getMaxOverflow()))); + } + update(mode) {} + draw() { + const me = this; + const ctx = me._ctx; + const chart = me.chart; + const meta = me._cachedMeta; + const elements = meta.data || []; + const area = chart.chartArea; + const active = []; + const start = me._drawStart || 0; + const count = me._drawCount || (elements.length - start); + let i; + if (meta.dataset) { + meta.dataset.draw(ctx, area, start, count); + } + for (i = start; i < start + count; ++i) { + const element = elements[i]; + if (element.active) { + active.push(element); + } else { + element.draw(ctx, area); + } + } + for (i = 0; i < active.length; ++i) { + active[i].draw(ctx, area); + } + } + getStyle(index, active) { + const mode = active ? 'active' : 'default'; + return index === undefined && this._cachedMeta.dataset + ? this.resolveDatasetElementOptions(mode) + : this.resolveDataElementOptions(index || 0, mode); + } + getContext(index, active, mode) { + const me = this; + const dataset = me.getDataset(); + let context; + if (index >= 0 && index < me._cachedMeta.data.length) { + const element = me._cachedMeta.data[index]; + context = element.$context || + (element.$context = createDataContext(me.getContext(), index, element)); + context.parsed = me.getParsed(index); + context.raw = dataset.data[index]; + context.index = context.dataIndex = index; + } else { + context = me.$context || + (me.$context = createDatasetContext(me.chart.getContext(), me.index)); + context.dataset = dataset; + context.index = context.datasetIndex = me.index; + } + context.active = !!active; + context.mode = mode; + return context; + } + resolveDatasetElementOptions(mode) { + return this._resolveElementOptions(this.datasetElementType.id, mode); + } + resolveDataElementOptions(index, mode) { + return this._resolveElementOptions(this.dataElementType.id, mode, index); + } + _resolveElementOptions(elementType, mode = 'default', index) { + const me = this; + const active = mode === 'active'; + const cache = me._cachedDataOpts; + const cacheKey = elementType + '-' + mode; + const cached = cache[cacheKey]; + const sharing = me.enableOptionSharing && defined(index); + if (cached) { + return cloneIfNotShared(cached, sharing); + } + const config = me.chart.config; + const scopeKeys = config.datasetElementScopeKeys(me._type, elementType); + const prefixes = active ? [`${elementType}Hover`, 'hover', elementType, ''] : [elementType, '']; + const scopes = config.getOptionScopes(me.getDataset(), scopeKeys); + const names = Object.keys(defaults.elements[elementType]); + const context = () => me.getContext(index, active); + const values = config.resolveNamedOptions(scopes, names, context, prefixes); + if (values.$shared) { + values.$shared = sharing; + cache[cacheKey] = Object.freeze(cloneIfNotShared(values, sharing)); + } + return values; + } + _resolveAnimations(index, transition, active) { + const me = this; + const chart = me.chart; + const cache = me._cachedDataOpts; + const cacheKey = `animation-${transition}`; + const cached = cache[cacheKey]; + if (cached) { + return cached; + } + let options; + if (chart.options.animation !== false) { + const config = me.chart.config; + const scopeKeys = config.datasetAnimationScopeKeys(me._type, transition); + const scopes = config.getOptionScopes(me.getDataset(), scopeKeys); + options = config.createResolver(scopes, me.getContext(index, active, transition)); + } + const animations = new Animations(chart, options && options.animations); + if (options && options._cacheable) { + cache[cacheKey] = Object.freeze(animations); + } + return animations; + } + getSharedOptions(options) { + if (!options.$shared) { + return; + } + return this._sharedOptions || (this._sharedOptions = Object.assign({}, options)); + } + includeOptions(mode, sharedOptions) { + return !sharedOptions || isDirectUpdateMode(mode) || this.chart._animationsDisabled; + } + updateElement(element, index, properties, mode) { + if (isDirectUpdateMode(mode)) { + Object.assign(element, properties); + } else { + this._resolveAnimations(index, mode).update(element, properties); + } + } + updateSharedOptions(sharedOptions, mode, newOptions) { + if (sharedOptions && !isDirectUpdateMode(mode)) { + this._resolveAnimations(undefined, mode).update(sharedOptions, newOptions); + } + } + _setStyle(element, index, mode, active) { + element.active = active; + const options = this.getStyle(index, active); + this._resolveAnimations(index, mode, active).update(element, { + options: (!active && this.getSharedOptions(options)) || options + }); + } + removeHoverStyle(element, datasetIndex, index) { + this._setStyle(element, index, 'active', false); + } + setHoverStyle(element, datasetIndex, index) { + this._setStyle(element, index, 'active', true); + } + _removeDatasetHoverStyle() { + const element = this._cachedMeta.dataset; + if (element) { + this._setStyle(element, undefined, 'active', false); + } + } + _setDatasetHoverStyle() { + const element = this._cachedMeta.dataset; + if (element) { + this._setStyle(element, undefined, 'active', true); + } + } + _resyncElements(resetNewElements) { + const me = this; + const data = me._data; + const elements = me._cachedMeta.data; + for (const [method, arg1, arg2] of me._syncList) { + me[method](arg1, arg2); + } + me._syncList = []; + const numMeta = elements.length; + const numData = data.length; + const count = Math.min(numData, numMeta); + if (count) { + me.parse(0, count); + } + if (numData > numMeta) { + me._insertElements(numMeta, numData - numMeta, resetNewElements); + } else if (numData < numMeta) { + me._removeElements(numData, numMeta - numData); + } + } + _insertElements(start, count, resetNewElements = true) { + const me = this; + const meta = me._cachedMeta; + const data = meta.data; + const end = start + count; + let i; + const move = (arr) => { + arr.length += count; + for (i = arr.length - 1; i >= end; i--) { + arr[i] = arr[i - count]; + } + }; + move(data); + for (i = start; i < end; ++i) { + data[i] = new me.dataElementType(); + } + if (me._parsing) { + move(meta._parsed); + } + me.parse(start, count); + if (resetNewElements) { + me.updateElements(data, start, count, 'reset'); + } + } + updateElements(element, start, count, mode) {} + _removeElements(start, count) { + const me = this; + const meta = me._cachedMeta; + if (me._parsing) { + const removed = meta._parsed.splice(start, count); + if (meta._stacked) { + clearStacks(meta, removed); + } + } + meta.data.splice(start, count); + } + _onDataPush() { + const count = arguments.length; + this._syncList.push(['_insertElements', this.getDataset().data.length - count, count]); + } + _onDataPop() { + this._syncList.push(['_removeElements', this._cachedMeta.data.length - 1, 1]); + } + _onDataShift() { + this._syncList.push(['_removeElements', 0, 1]); + } + _onDataSplice(start, count) { + this._syncList.push(['_removeElements', start, count]); + this._syncList.push(['_insertElements', start, arguments.length - 2]); + } + _onDataUnshift() { + this._syncList.push(['_insertElements', 0, arguments.length]); + } +} +DatasetController.defaults = {}; +DatasetController.prototype.datasetElementType = null; +DatasetController.prototype.dataElementType = null; + +function getAllScaleValues(scale) { + if (!scale._cache.$bar) { + const metas = scale.getMatchingVisibleMetas('bar'); + let values = []; + for (let i = 0, ilen = metas.length; i < ilen; i++) { + values = values.concat(metas[i].controller.getAllParsedValues(scale)); + } + scale._cache.$bar = _arrayUnique(values.sort((a, b) => a - b)); + } + return scale._cache.$bar; +} +function computeMinSampleSize(scale) { + const values = getAllScaleValues(scale); + let min = scale._length; + let i, ilen, curr, prev; + const updateMinAndPrev = () => { + if (curr === 32767 || curr === -32768) { + return; + } + if (defined(prev)) { + min = Math.min(min, Math.abs(curr - prev) || min); + } + prev = curr; + }; + for (i = 0, ilen = values.length; i < ilen; ++i) { + curr = scale.getPixelForValue(values[i]); + updateMinAndPrev(); + } + prev = undefined; + for (i = 0, ilen = scale.ticks.length; i < ilen; ++i) { + curr = scale.getPixelForTick(i); + updateMinAndPrev(); + } + return min; +} +function computeFitCategoryTraits(index, ruler, options, stackCount) { + const thickness = options.barThickness; + let size, ratio; + if (isNullOrUndef(thickness)) { + size = ruler.min * options.categoryPercentage; + ratio = options.barPercentage; + } else { + size = thickness * stackCount; + ratio = 1; + } + return { + chunk: size / stackCount, + ratio, + start: ruler.pixels[index] - (size / 2) + }; +} +function computeFlexCategoryTraits(index, ruler, options, stackCount) { + const pixels = ruler.pixels; + const curr = pixels[index]; + let prev = index > 0 ? pixels[index - 1] : null; + let next = index < pixels.length - 1 ? pixels[index + 1] : null; + const percent = options.categoryPercentage; + if (prev === null) { + prev = curr - (next === null ? ruler.end - ruler.start : next - curr); + } + if (next === null) { + next = curr + curr - prev; + } + const start = curr - (curr - Math.min(prev, next)) / 2 * percent; + const size = Math.abs(next - prev) / 2 * percent; + return { + chunk: size / stackCount, + ratio: options.barPercentage, + start + }; +} +function parseFloatBar(entry, item, vScale, i) { + const startValue = vScale.parse(entry[0], i); + const endValue = vScale.parse(entry[1], i); + const min = Math.min(startValue, endValue); + const max = Math.max(startValue, endValue); + let barStart = min; + let barEnd = max; + if (Math.abs(min) > Math.abs(max)) { + barStart = max; + barEnd = min; + } + item[vScale.axis] = barEnd; + item._custom = { + barStart, + barEnd, + start: startValue, + end: endValue, + min, + max + }; +} +function parseValue(entry, item, vScale, i) { + if (isArray(entry)) { + parseFloatBar(entry, item, vScale, i); + } else { + item[vScale.axis] = vScale.parse(entry, i); + } + return item; +} +function parseArrayOrPrimitive(meta, data, start, count) { + const iScale = meta.iScale; + const vScale = meta.vScale; + const labels = iScale.getLabels(); + const singleScale = iScale === vScale; + const parsed = []; + let i, ilen, item, entry; + for (i = start, ilen = start + count; i < ilen; ++i) { + entry = data[i]; + item = {}; + item[iScale.axis] = singleScale || iScale.parse(labels[i], i); + parsed.push(parseValue(entry, item, vScale, i)); + } + return parsed; +} +function isFloatBar(custom) { + return custom && custom.barStart !== undefined && custom.barEnd !== undefined; +} +class BarController extends DatasetController { + parsePrimitiveData(meta, data, start, count) { + return parseArrayOrPrimitive(meta, data, start, count); + } + parseArrayData(meta, data, start, count) { + return parseArrayOrPrimitive(meta, data, start, count); + } + parseObjectData(meta, data, start, count) { + const {iScale, vScale} = meta; + const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; + const iAxisKey = iScale.axis === 'x' ? xAxisKey : yAxisKey; + const vAxisKey = vScale.axis === 'x' ? xAxisKey : yAxisKey; + const parsed = []; + let i, ilen, item, obj; + for (i = start, ilen = start + count; i < ilen; ++i) { + obj = data[i]; + item = {}; + item[iScale.axis] = iScale.parse(resolveObjectKey(obj, iAxisKey), i); + parsed.push(parseValue(resolveObjectKey(obj, vAxisKey), item, vScale, i)); + } + return parsed; + } + updateRangeFromParsed(range, scale, parsed, stack) { + super.updateRangeFromParsed(range, scale, parsed, stack); + const custom = parsed._custom; + if (custom && scale === this._cachedMeta.vScale) { + range.min = Math.min(range.min, custom.min); + range.max = Math.max(range.max, custom.max); + } + } + getMaxOverflow() { + return 0; + } + getLabelAndValue(index) { + const me = this; + const meta = me._cachedMeta; + const {iScale, vScale} = meta; + const parsed = me.getParsed(index); + const custom = parsed._custom; + const value = isFloatBar(custom) + ? '[' + custom.start + ', ' + custom.end + ']' + : '' + vScale.getLabelForValue(parsed[vScale.axis]); + return { + label: '' + iScale.getLabelForValue(parsed[iScale.axis]), + value + }; + } + initialize() { + const me = this; + me.enableOptionSharing = true; + super.initialize(); + const meta = me._cachedMeta; + meta.stack = me.getDataset().stack; + } + update(mode) { + const me = this; + const meta = me._cachedMeta; + me.updateElements(meta.data, 0, meta.data.length, mode); + } + updateElements(bars, start, count, mode) { + const me = this; + const reset = mode === 'reset'; + const vScale = me._cachedMeta.vScale; + const base = vScale.getBasePixel(); + const horizontal = vScale.isHorizontal(); + const ruler = me._getRuler(); + const firstOpts = me.resolveDataElementOptions(start, mode); + const sharedOptions = me.getSharedOptions(firstOpts); + const includeOptions = me.includeOptions(mode, sharedOptions); + me.updateSharedOptions(sharedOptions, mode, firstOpts); + for (let i = start; i < start + count; i++) { + const parsed = me.getParsed(i); + const vpixels = reset || isNullOrUndef(parsed[vScale.axis]) ? {base, head: base} : me._calculateBarValuePixels(i); + const ipixels = me._calculateBarIndexPixels(i, ruler); + const stack = (parsed._stacks || {})[vScale.axis]; + const properties = { + horizontal, + base: vpixels.base, + enableBorderRadius: !stack || isFloatBar(parsed._custom) || (me.index === stack._top || me.index === stack._bottom), + x: horizontal ? vpixels.head : ipixels.center, + y: horizontal ? ipixels.center : vpixels.head, + height: horizontal ? ipixels.size : Math.abs(vpixels.size), + width: horizontal ? Math.abs(vpixels.size) : ipixels.size + }; + if (includeOptions) { + properties.options = sharedOptions || me.resolveDataElementOptions(i, bars[i].active ? 'active' : mode); + } + me.updateElement(bars[i], i, properties, mode); + } + } + _getStacks(last, dataIndex) { + const me = this; + const meta = me._cachedMeta; + const iScale = meta.iScale; + const metasets = iScale.getMatchingVisibleMetas(me._type); + const stacked = iScale.options.stacked; + const ilen = metasets.length; + const stacks = []; + let i, item; + for (i = 0; i < ilen; ++i) { + item = metasets[i]; + if (!item.controller.options.grouped) { + continue; + } + if (typeof dataIndex !== 'undefined') { + const val = item.controller.getParsed(dataIndex)[ + item.controller._cachedMeta.vScale.axis + ]; + if (isNullOrUndef(val) || isNaN(val)) { + continue; + } + } + if (stacked === false || stacks.indexOf(item.stack) === -1 || + (stacked === undefined && item.stack === undefined)) { + stacks.push(item.stack); + } + if (item.index === last) { + break; + } + } + if (!stacks.length) { + stacks.push(undefined); + } + return stacks; + } + _getStackCount(index) { + return this._getStacks(undefined, index).length; + } + _getStackIndex(datasetIndex, name, dataIndex) { + const stacks = this._getStacks(datasetIndex, dataIndex); + const index = (name !== undefined) + ? stacks.indexOf(name) + : -1; + return (index === -1) + ? stacks.length - 1 + : index; + } + _getRuler() { + const me = this; + const opts = me.options; + const meta = me._cachedMeta; + const iScale = meta.iScale; + const pixels = []; + let i, ilen; + for (i = 0, ilen = meta.data.length; i < ilen; ++i) { + pixels.push(iScale.getPixelForValue(me.getParsed(i)[iScale.axis], i)); + } + const barThickness = opts.barThickness; + const min = barThickness || computeMinSampleSize(iScale); + return { + min, + pixels, + start: iScale._startPixel, + end: iScale._endPixel, + stackCount: me._getStackCount(), + scale: iScale, + grouped: opts.grouped, + ratio: barThickness ? 1 : opts.categoryPercentage * opts.barPercentage + }; + } + _calculateBarValuePixels(index) { + const me = this; + const {vScale, _stacked} = me._cachedMeta; + const {base: baseValue, minBarLength} = me.options; + const parsed = me.getParsed(index); + const custom = parsed._custom; + const floating = isFloatBar(custom); + let value = parsed[vScale.axis]; + let start = 0; + let length = _stacked ? me.applyStack(vScale, parsed, _stacked) : value; + let head, size; + if (length !== value) { + start = length - value; + length = value; + } + if (floating) { + value = custom.barStart; + length = custom.barEnd - custom.barStart; + if (value !== 0 && sign(value) !== sign(custom.barEnd)) { + start = 0; + } + start += value; + } + const startValue = !isNullOrUndef(baseValue) && !floating ? baseValue : start; + let base = vScale.getPixelForValue(startValue); + if (this.chart.getDataVisibility(index)) { + head = vScale.getPixelForValue(start + length); + } else { + head = base; + } + size = head - base; + if (minBarLength !== undefined && Math.abs(size) < minBarLength) { + size = size < 0 ? -minBarLength : minBarLength; + if (value === 0) { + base -= size / 2; + } + head = base + size; + } + const actualBase = baseValue || 0; + if (base === vScale.getPixelForValue(actualBase)) { + const halfGrid = vScale.getLineWidthForValue(actualBase) / 2; + if (size > 0) { + base += halfGrid; + size -= halfGrid; + } else if (size < 0) { + base -= halfGrid; + size += halfGrid; + } + } + return { + size, + base, + head, + center: head + size / 2 + }; + } + _calculateBarIndexPixels(index, ruler) { + const me = this; + const scale = ruler.scale; + const options = me.options; + const skipNull = options.skipNull; + const maxBarThickness = valueOrDefault(options.maxBarThickness, Infinity); + let center, size; + if (ruler.grouped) { + const stackCount = skipNull ? me._getStackCount(index) : ruler.stackCount; + const range = options.barThickness === 'flex' + ? computeFlexCategoryTraits(index, ruler, options, stackCount) + : computeFitCategoryTraits(index, ruler, options, stackCount); + const stackIndex = me._getStackIndex(me.index, me._cachedMeta.stack, skipNull ? index : undefined); + center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + size = Math.min(maxBarThickness, range.chunk * range.ratio); + } else { + center = scale.getPixelForValue(me.getParsed(index)[scale.axis], index); + size = Math.min(maxBarThickness, ruler.min * ruler.ratio); + } + return { + base: center - size / 2, + head: center + size / 2, + center, + size + }; + } + draw() { + const me = this; + const meta = me._cachedMeta; + const vScale = meta.vScale; + const rects = meta.data; + const ilen = rects.length; + let i = 0; + for (; i < ilen; ++i) { + if (me.getParsed(i)[vScale.axis] !== null) { + rects[i].draw(me._ctx); + } + } + } +} +BarController.id = 'bar'; +BarController.defaults = { + datasetElementType: false, + dataElementType: 'bar', + categoryPercentage: 0.8, + barPercentage: 0.9, + grouped: true, + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'base', 'width', 'height'] + } + } +}; +BarController.overrides = { + interaction: { + mode: 'index' + }, + scales: { + _index_: { + type: 'category', + offset: true, + grid: { + offset: true + } + }, + _value_: { + type: 'linear', + beginAtZero: true, + } + } +}; + +class BubbleController extends DatasetController { + initialize() { + this.enableOptionSharing = true; + super.initialize(); + } + parseObjectData(meta, data, start, count) { + const {xScale, yScale} = meta; + const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; + const parsed = []; + let i, ilen, item; + for (i = start, ilen = start + count; i < ilen; ++i) { + item = data[i]; + parsed.push({ + x: xScale.parse(resolveObjectKey(item, xAxisKey), i), + y: yScale.parse(resolveObjectKey(item, yAxisKey), i), + _custom: item && item.r && +item.r + }); + } + return parsed; + } + getMaxOverflow() { + const {data, _parsed} = this._cachedMeta; + let max = 0; + for (let i = data.length - 1; i >= 0; --i) { + max = Math.max(max, data[i].size() / 2, _parsed[i]._custom); + } + return max > 0 && max; + } + getLabelAndValue(index) { + const me = this; + const meta = me._cachedMeta; + const {xScale, yScale} = meta; + const parsed = me.getParsed(index); + const x = xScale.getLabelForValue(parsed.x); + const y = yScale.getLabelForValue(parsed.y); + const r = parsed._custom; + return { + label: meta.label, + value: '(' + x + ', ' + y + (r ? ', ' + r : '') + ')' + }; + } + update(mode) { + const me = this; + const points = me._cachedMeta.data; + me.updateElements(points, 0, points.length, mode); + } + updateElements(points, start, count, mode) { + const me = this; + const reset = mode === 'reset'; + const {iScale, vScale} = me._cachedMeta; + const firstOpts = me.resolveDataElementOptions(start, mode); + const sharedOptions = me.getSharedOptions(firstOpts); + const includeOptions = me.includeOptions(mode, sharedOptions); + const iAxis = iScale.axis; + const vAxis = vScale.axis; + for (let i = start; i < start + count; i++) { + const point = points[i]; + const parsed = !reset && me.getParsed(i); + const properties = {}; + const iPixel = properties[iAxis] = reset ? iScale.getPixelForDecimal(0.5) : iScale.getPixelForValue(parsed[iAxis]); + const vPixel = properties[vAxis] = reset ? vScale.getBasePixel() : vScale.getPixelForValue(parsed[vAxis]); + properties.skip = isNaN(iPixel) || isNaN(vPixel); + if (includeOptions) { + properties.options = me.resolveDataElementOptions(i, point.active ? 'active' : mode); + if (reset) { + properties.options.radius = 0; + } + } + me.updateElement(point, i, properties, mode); + } + me.updateSharedOptions(sharedOptions, mode, firstOpts); + } + resolveDataElementOptions(index, mode) { + const parsed = this.getParsed(index); + let values = super.resolveDataElementOptions(index, mode); + if (values.$shared) { + values = Object.assign({}, values, {$shared: false}); + } + const radius = values.radius; + if (mode !== 'active') { + values.radius = 0; + } + values.radius += valueOrDefault(parsed && parsed._custom, radius); + return values; + } +} +BubbleController.id = 'bubble'; +BubbleController.defaults = { + datasetElementType: false, + dataElementType: 'point', + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'borderWidth', 'radius'] + } + } +}; +BubbleController.overrides = { + scales: { + x: { + type: 'linear' + }, + y: { + type: 'linear' + } + }, + plugins: { + tooltip: { + callbacks: { + title() { + return ''; + } + } + } + } +}; + +function getRatioAndOffset(rotation, circumference, cutout) { + let ratioX = 1; + let ratioY = 1; + let offsetX = 0; + let offsetY = 0; + if (circumference < TAU) { + const startAngle = rotation; + const endAngle = startAngle + circumference; + const startX = Math.cos(startAngle); + const startY = Math.sin(startAngle); + const endX = Math.cos(endAngle); + const endY = Math.sin(endAngle); + const calcMax = (angle, a, b) => _angleBetween(angle, startAngle, endAngle, true) ? 1 : Math.max(a, a * cutout, b, b * cutout); + const calcMin = (angle, a, b) => _angleBetween(angle, startAngle, endAngle, true) ? -1 : Math.min(a, a * cutout, b, b * cutout); + const maxX = calcMax(0, startX, endX); + const maxY = calcMax(HALF_PI, startY, endY); + const minX = calcMin(PI, startX, endX); + const minY = calcMin(PI + HALF_PI, startY, endY); + ratioX = (maxX - minX) / 2; + ratioY = (maxY - minY) / 2; + offsetX = -(maxX + minX) / 2; + offsetY = -(maxY + minY) / 2; + } + return {ratioX, ratioY, offsetX, offsetY}; +} +class DoughnutController extends DatasetController { + constructor(chart, datasetIndex) { + super(chart, datasetIndex); + this.enableOptionSharing = true; + this.innerRadius = undefined; + this.outerRadius = undefined; + this.offsetX = undefined; + this.offsetY = undefined; + } + linkScales() {} + parse(start, count) { + const data = this.getDataset().data; + const meta = this._cachedMeta; + let i, ilen; + for (i = start, ilen = start + count; i < ilen; ++i) { + meta._parsed[i] = +data[i]; + } + } + _getRotation() { + return toRadians(this.options.rotation - 90); + } + _getCircumference() { + return toRadians(this.options.circumference); + } + _getRotationExtents() { + let min = TAU; + let max = -TAU; + const me = this; + for (let i = 0; i < me.chart.data.datasets.length; ++i) { + if (me.chart.isDatasetVisible(i)) { + const controller = me.chart.getDatasetMeta(i).controller; + const rotation = controller._getRotation(); + const circumference = controller._getCircumference(); + min = Math.min(min, rotation); + max = Math.max(max, rotation + circumference); + } + } + return { + rotation: min, + circumference: max - min, + }; + } + update(mode) { + const me = this; + const chart = me.chart; + const {chartArea} = chart; + const meta = me._cachedMeta; + const arcs = meta.data; + const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs) + me.options.spacing; + const maxSize = Math.max((Math.min(chartArea.width, chartArea.height) - spacing) / 2, 0); + const cutout = Math.min(toPercentage(me.options.cutout, maxSize), 1); + const chartWeight = me._getRingWeight(me.index); + const {circumference, rotation} = me._getRotationExtents(); + const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(rotation, circumference, cutout); + const maxWidth = (chartArea.width - spacing) / ratioX; + const maxHeight = (chartArea.height - spacing) / ratioY; + const maxRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0); + const outerRadius = toDimension(me.options.radius, maxRadius); + const innerRadius = Math.max(outerRadius * cutout, 0); + const radiusLength = (outerRadius - innerRadius) / me._getVisibleDatasetWeightTotal(); + me.offsetX = offsetX * outerRadius; + me.offsetY = offsetY * outerRadius; + meta.total = me.calculateTotal(); + me.outerRadius = outerRadius - radiusLength * me._getRingWeightOffset(me.index); + me.innerRadius = Math.max(me.outerRadius - radiusLength * chartWeight, 0); + me.updateElements(arcs, 0, arcs.length, mode); + } + _circumference(i, reset) { + const me = this; + const opts = me.options; + const meta = me._cachedMeta; + const circumference = me._getCircumference(); + if ((reset && opts.animation.animateRotate) || !this.chart.getDataVisibility(i) || meta._parsed[i] === null) { + return 0; + } + return me.calculateCircumference(meta._parsed[i] * circumference / TAU); + } + updateElements(arcs, start, count, mode) { + const me = this; + const reset = mode === 'reset'; + const chart = me.chart; + const chartArea = chart.chartArea; + const opts = chart.options; + const animationOpts = opts.animation; + const centerX = (chartArea.left + chartArea.right) / 2; + const centerY = (chartArea.top + chartArea.bottom) / 2; + const animateScale = reset && animationOpts.animateScale; + const innerRadius = animateScale ? 0 : me.innerRadius; + const outerRadius = animateScale ? 0 : me.outerRadius; + const firstOpts = me.resolveDataElementOptions(start, mode); + const sharedOptions = me.getSharedOptions(firstOpts); + const includeOptions = me.includeOptions(mode, sharedOptions); + let startAngle = me._getRotation(); + let i; + for (i = 0; i < start; ++i) { + startAngle += me._circumference(i, reset); + } + for (i = start; i < start + count; ++i) { + const circumference = me._circumference(i, reset); + const arc = arcs[i]; + const properties = { + x: centerX + me.offsetX, + y: centerY + me.offsetY, + startAngle, + endAngle: startAngle + circumference, + circumference, + outerRadius, + innerRadius + }; + if (includeOptions) { + properties.options = sharedOptions || me.resolveDataElementOptions(i, arc.active ? 'active' : mode); + } + startAngle += circumference; + me.updateElement(arc, i, properties, mode); + } + me.updateSharedOptions(sharedOptions, mode, firstOpts); + } + calculateTotal() { + const meta = this._cachedMeta; + const metaData = meta.data; + let total = 0; + let i; + for (i = 0; i < metaData.length; i++) { + const value = meta._parsed[i]; + if (value !== null && !isNaN(value) && this.chart.getDataVisibility(i)) { + total += Math.abs(value); + } + } + return total; + } + calculateCircumference(value) { + const total = this._cachedMeta.total; + if (total > 0 && !isNaN(value)) { + return TAU * (Math.abs(value) / total); + } + return 0; + } + getLabelAndValue(index) { + const me = this; + const meta = me._cachedMeta; + const chart = me.chart; + const labels = chart.data.labels || []; + const value = formatNumber(meta._parsed[index], chart.options.locale); + return { + label: labels[index] || '', + value, + }; + } + getMaxBorderWidth(arcs) { + const me = this; + let max = 0; + const chart = me.chart; + let i, ilen, meta, controller, options; + if (!arcs) { + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + if (chart.isDatasetVisible(i)) { + meta = chart.getDatasetMeta(i); + arcs = meta.data; + controller = meta.controller; + if (controller !== me) { + controller.configure(); + } + break; + } + } + } + if (!arcs) { + return 0; + } + for (i = 0, ilen = arcs.length; i < ilen; ++i) { + options = controller.resolveDataElementOptions(i); + if (options.borderAlign !== 'inner') { + max = Math.max(max, options.borderWidth || 0, options.hoverBorderWidth || 0); + } + } + return max; + } + getMaxOffset(arcs) { + let max = 0; + for (let i = 0, ilen = arcs.length; i < ilen; ++i) { + const options = this.resolveDataElementOptions(i); + max = Math.max(max, options.offset || 0, options.hoverOffset || 0); + } + return max; + } + _getRingWeightOffset(datasetIndex) { + let ringWeightOffset = 0; + for (let i = 0; i < datasetIndex; ++i) { + if (this.chart.isDatasetVisible(i)) { + ringWeightOffset += this._getRingWeight(i); + } + } + return ringWeightOffset; + } + _getRingWeight(datasetIndex) { + return Math.max(valueOrDefault(this.chart.data.datasets[datasetIndex].weight, 1), 0); + } + _getVisibleDatasetWeightTotal() { + return this._getRingWeightOffset(this.chart.data.datasets.length) || 1; + } +} +DoughnutController.id = 'doughnut'; +DoughnutController.defaults = { + datasetElementType: false, + dataElementType: 'arc', + animation: { + animateRotate: true, + animateScale: false + }, + animations: { + numbers: { + type: 'number', + properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth', 'spacing'] + }, + }, + cutout: '50%', + rotation: 0, + circumference: 360, + radius: '100%', + spacing: 0, + indexAxis: 'r', +}; +DoughnutController.descriptors = { + _scriptable: (name) => name !== 'spacing', + _indexable: (name) => name !== 'spacing', +}; +DoughnutController.overrides = { + aspectRatio: 1, + plugins: { + legend: { + labels: { + generateLabels(chart) { + const data = chart.data; + if (data.labels.length && data.datasets.length) { + const {labels: {pointStyle}} = chart.legend.options; + return data.labels.map((label, i) => { + const meta = chart.getDatasetMeta(0); + const style = meta.controller.getStyle(i); + return { + text: label, + fillStyle: style.backgroundColor, + strokeStyle: style.borderColor, + lineWidth: style.borderWidth, + pointStyle: pointStyle, + hidden: !chart.getDataVisibility(i), + index: i + }; + }); + } + return []; + } + }, + onClick(e, legendItem, legend) { + legend.chart.toggleDataVisibility(legendItem.index); + legend.chart.update(); + } + }, + tooltip: { + callbacks: { + title() { + return ''; + }, + label(tooltipItem) { + let dataLabel = tooltipItem.label; + const value = ': ' + tooltipItem.formattedValue; + if (isArray(dataLabel)) { + dataLabel = dataLabel.slice(); + dataLabel[0] += value; + } else { + dataLabel += value; + } + return dataLabel; + } + } + } + } +}; + +class LineController extends DatasetController { + initialize() { + this.enableOptionSharing = true; + super.initialize(); + } + update(mode) { + const me = this; + const meta = me._cachedMeta; + const {dataset: line, data: points = [], _dataset} = meta; + const animationsDisabled = me.chart._animationsDisabled; + let {start, count} = getStartAndCountOfVisiblePoints(meta, points, animationsDisabled); + me._drawStart = start; + me._drawCount = count; + if (scaleRangesChanged(meta)) { + start = 0; + count = points.length; + } + line._decimated = !!_dataset._decimated; + line.points = points; + const options = me.resolveDatasetElementOptions(mode); + if (!me.options.showLine) { + options.borderWidth = 0; + } + options.segment = me.options.segment; + me.updateElement(line, undefined, { + animated: !animationsDisabled, + options + }, mode); + me.updateElements(points, start, count, mode); + } + updateElements(points, start, count, mode) { + const me = this; + const reset = mode === 'reset'; + const {iScale, vScale, _stacked} = me._cachedMeta; + const firstOpts = me.resolveDataElementOptions(start, mode); + const sharedOptions = me.getSharedOptions(firstOpts); + const includeOptions = me.includeOptions(mode, sharedOptions); + const iAxis = iScale.axis; + const vAxis = vScale.axis; + const spanGaps = me.options.spanGaps; + const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY; + const directUpdate = me.chart._animationsDisabled || reset || mode === 'none'; + let prevParsed = start > 0 && me.getParsed(start - 1); + for (let i = start; i < start + count; ++i) { + const point = points[i]; + const parsed = me.getParsed(i); + const properties = directUpdate ? point : {}; + const nullData = isNullOrUndef(parsed[vAxis]); + const iPixel = properties[iAxis] = iScale.getPixelForValue(parsed[iAxis], i); + const vPixel = properties[vAxis] = reset || nullData ? vScale.getBasePixel() : vScale.getPixelForValue(_stacked ? me.applyStack(vScale, parsed, _stacked) : parsed[vAxis], i); + properties.skip = isNaN(iPixel) || isNaN(vPixel) || nullData; + properties.stop = i > 0 && (parsed[iAxis] - prevParsed[iAxis]) > maxGapLength; + properties.parsed = parsed; + if (includeOptions) { + properties.options = sharedOptions || me.resolveDataElementOptions(i, point.active ? 'active' : mode); + } + if (!directUpdate) { + me.updateElement(point, i, properties, mode); + } + prevParsed = parsed; + } + me.updateSharedOptions(sharedOptions, mode, firstOpts); + } + getMaxOverflow() { + const me = this; + const meta = me._cachedMeta; + const dataset = meta.dataset; + const border = dataset.options && dataset.options.borderWidth || 0; + const data = meta.data || []; + if (!data.length) { + return border; + } + const firstPoint = data[0].size(me.resolveDataElementOptions(0)); + const lastPoint = data[data.length - 1].size(me.resolveDataElementOptions(data.length - 1)); + return Math.max(border, firstPoint, lastPoint) / 2; + } + draw() { + const meta = this._cachedMeta; + meta.dataset.updateControlPoints(this.chart.chartArea, meta.iScale.axis); + super.draw(); + } +} +LineController.id = 'line'; +LineController.defaults = { + datasetElementType: 'line', + dataElementType: 'point', + showLine: true, + spanGaps: false, +}; +LineController.overrides = { + scales: { + _index_: { + type: 'category', + }, + _value_: { + type: 'linear', + }, + } +}; +function getStartAndCountOfVisiblePoints(meta, points, animationsDisabled) { + const pointCount = points.length; + let start = 0; + let count = pointCount; + if (meta._sorted) { + const {iScale, _parsed} = meta; + const axis = iScale.axis; + const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); + if (minDefined) { + start = _limitValue(Math.min( + _lookupByKey(_parsed, iScale.axis, min).lo, + animationsDisabled ? pointCount : _lookupByKey(points, axis, iScale.getPixelForValue(min)).lo), + 0, pointCount - 1); + } + if (maxDefined) { + count = _limitValue(Math.max( + _lookupByKey(_parsed, iScale.axis, max).hi + 1, + animationsDisabled ? 0 : _lookupByKey(points, axis, iScale.getPixelForValue(max)).hi + 1), + start, pointCount) - start; + } else { + count = pointCount - start; + } + } + return {start, count}; +} +function scaleRangesChanged(meta) { + const {xScale, yScale, _scaleRanges} = meta; + const newRanges = { + xmin: xScale.min, + xmax: xScale.max, + ymin: yScale.min, + ymax: yScale.max + }; + if (!_scaleRanges) { + meta._scaleRanges = newRanges; + return true; + } + const changed = _scaleRanges.xmin !== xScale.min + || _scaleRanges.xmax !== xScale.max + || _scaleRanges.ymin !== yScale.min + || _scaleRanges.ymax !== yScale.max; + Object.assign(_scaleRanges, newRanges); + return changed; +} + +class PolarAreaController extends DatasetController { + constructor(chart, datasetIndex) { + super(chart, datasetIndex); + this.innerRadius = undefined; + this.outerRadius = undefined; + } + getLabelAndValue(index) { + const me = this; + const meta = me._cachedMeta; + const chart = me.chart; + const labels = chart.data.labels || []; + const value = formatNumber(meta._parsed[index].r, chart.options.locale); + return { + label: labels[index] || '', + value, + }; + } + update(mode) { + const arcs = this._cachedMeta.data; + this._updateRadius(); + this.updateElements(arcs, 0, arcs.length, mode); + } + _updateRadius() { + const me = this; + const chart = me.chart; + const chartArea = chart.chartArea; + const opts = chart.options; + const minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top); + const outerRadius = Math.max(minSize / 2, 0); + const innerRadius = Math.max(opts.cutoutPercentage ? (outerRadius / 100) * (opts.cutoutPercentage) : 1, 0); + const radiusLength = (outerRadius - innerRadius) / chart.getVisibleDatasetCount(); + me.outerRadius = outerRadius - (radiusLength * me.index); + me.innerRadius = me.outerRadius - radiusLength; + } + updateElements(arcs, start, count, mode) { + const me = this; + const reset = mode === 'reset'; + const chart = me.chart; + const dataset = me.getDataset(); + const opts = chart.options; + const animationOpts = opts.animation; + const scale = me._cachedMeta.rScale; + const centerX = scale.xCenter; + const centerY = scale.yCenter; + const datasetStartAngle = scale.getIndexAngle(0) - 0.5 * PI; + let angle = datasetStartAngle; + let i; + const defaultAngle = 360 / me.countVisibleElements(); + for (i = 0; i < start; ++i) { + angle += me._computeAngle(i, mode, defaultAngle); + } + for (i = start; i < start + count; i++) { + const arc = arcs[i]; + let startAngle = angle; + let endAngle = angle + me._computeAngle(i, mode, defaultAngle); + let outerRadius = chart.getDataVisibility(i) ? scale.getDistanceFromCenterForValue(dataset.data[i]) : 0; + angle = endAngle; + if (reset) { + if (animationOpts.animateScale) { + outerRadius = 0; + } + if (animationOpts.animateRotate) { + startAngle = endAngle = datasetStartAngle; + } + } + const properties = { + x: centerX, + y: centerY, + innerRadius: 0, + outerRadius, + startAngle, + endAngle, + options: me.resolveDataElementOptions(i, arc.active ? 'active' : mode) + }; + me.updateElement(arc, i, properties, mode); + } + } + countVisibleElements() { + const dataset = this.getDataset(); + const meta = this._cachedMeta; + let count = 0; + meta.data.forEach((element, index) => { + if (!isNaN(dataset.data[index]) && this.chart.getDataVisibility(index)) { + count++; + } + }); + return count; + } + _computeAngle(index, mode, defaultAngle) { + return this.chart.getDataVisibility(index) + ? toRadians(this.resolveDataElementOptions(index, mode).angle || defaultAngle) + : 0; + } +} +PolarAreaController.id = 'polarArea'; +PolarAreaController.defaults = { + dataElementType: 'arc', + animation: { + animateRotate: true, + animateScale: true + }, + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius'] + }, + }, + indexAxis: 'r', + startAngle: 0, +}; +PolarAreaController.overrides = { + aspectRatio: 1, + plugins: { + legend: { + labels: { + generateLabels(chart) { + const data = chart.data; + if (data.labels.length && data.datasets.length) { + const {labels: {pointStyle}} = chart.legend.options; + return data.labels.map((label, i) => { + const meta = chart.getDatasetMeta(0); + const style = meta.controller.getStyle(i); + return { + text: label, + fillStyle: style.backgroundColor, + strokeStyle: style.borderColor, + lineWidth: style.borderWidth, + pointStyle: pointStyle, + hidden: !chart.getDataVisibility(i), + index: i + }; + }); + } + return []; + } + }, + onClick(e, legendItem, legend) { + legend.chart.toggleDataVisibility(legendItem.index); + legend.chart.update(); + } + }, + tooltip: { + callbacks: { + title() { + return ''; + }, + label(context) { + return context.chart.data.labels[context.dataIndex] + ': ' + context.formattedValue; + } + } + } + }, + scales: { + r: { + type: 'radialLinear', + angleLines: { + display: false + }, + beginAtZero: true, + grid: { + circular: true + }, + pointLabels: { + display: false + }, + startAngle: 0 + } + } +}; + +class PieController extends DoughnutController { +} +PieController.id = 'pie'; +PieController.defaults = { + cutout: 0, + rotation: 0, + circumference: 360, + radius: '100%' +}; + +class RadarController extends DatasetController { + getLabelAndValue(index) { + const me = this; + const vScale = me._cachedMeta.vScale; + const parsed = me.getParsed(index); + return { + label: vScale.getLabels()[index], + value: '' + vScale.getLabelForValue(parsed[vScale.axis]) + }; + } + update(mode) { + const me = this; + const meta = me._cachedMeta; + const line = meta.dataset; + const points = meta.data || []; + const labels = meta.iScale.getLabels(); + line.points = points; + if (mode !== 'resize') { + const options = me.resolveDatasetElementOptions(mode); + if (!me.options.showLine) { + options.borderWidth = 0; + } + const properties = { + _loop: true, + _fullLoop: labels.length === points.length, + options + }; + me.updateElement(line, undefined, properties, mode); + } + me.updateElements(points, 0, points.length, mode); + } + updateElements(points, start, count, mode) { + const me = this; + const dataset = me.getDataset(); + const scale = me._cachedMeta.rScale; + const reset = mode === 'reset'; + for (let i = start; i < start + count; i++) { + const point = points[i]; + const options = me.resolveDataElementOptions(i, point.active ? 'active' : mode); + const pointPosition = scale.getPointPositionForValue(i, dataset.data[i]); + const x = reset ? scale.xCenter : pointPosition.x; + const y = reset ? scale.yCenter : pointPosition.y; + const properties = { + x, + y, + angle: pointPosition.angle, + skip: isNaN(x) || isNaN(y), + options + }; + me.updateElement(point, i, properties, mode); + } + } +} +RadarController.id = 'radar'; +RadarController.defaults = { + datasetElementType: 'line', + dataElementType: 'point', + indexAxis: 'r', + showLine: true, + elements: { + line: { + fill: 'start' + } + }, +}; +RadarController.overrides = { + aspectRatio: 1, + scales: { + r: { + type: 'radialLinear', + } + } +}; + +class ScatterController extends LineController { +} +ScatterController.id = 'scatter'; +ScatterController.defaults = { + showLine: false, + fill: false +}; +ScatterController.overrides = { + interaction: { + mode: 'point' + }, + plugins: { + tooltip: { + callbacks: { + title() { + return ''; + }, + label(item) { + return '(' + item.label + ', ' + item.formattedValue + ')'; + } + } + } + }, + scales: { + x: { + type: 'linear' + }, + y: { + type: 'linear' + } + } +}; + +var controllers = /*#__PURE__*/Object.freeze({ +__proto__: null, +BarController: BarController, +BubbleController: BubbleController, +DoughnutController: DoughnutController, +LineController: LineController, +PolarAreaController: PolarAreaController, +PieController: PieController, +RadarController: RadarController, +ScatterController: ScatterController +}); + +function abstract() { + throw new Error('This method is not implemented: Check that a complete date adapter is provided.'); +} +class DateAdapter { + constructor(options) { + this.options = options || {}; + } + formats() { + return abstract(); + } + parse(value, format) { + return abstract(); + } + format(timestamp, format) { + return abstract(); + } + add(timestamp, amount, unit) { + return abstract(); + } + diff(a, b, unit) { + return abstract(); + } + startOf(timestamp, unit, weekday) { + return abstract(); + } + endOf(timestamp, unit) { + return abstract(); + } +} +DateAdapter.override = function(members) { + Object.assign(DateAdapter.prototype, members); +}; +var adapters = { + _date: DateAdapter +}; + +function getRelativePosition(e, chart) { + if ('native' in e) { + return { + x: e.x, + y: e.y + }; + } + return getRelativePosition$1(e, chart); +} +function evaluateAllVisibleItems(chart, handler) { + const metasets = chart.getSortedVisibleDatasetMetas(); + let index, data, element; + for (let i = 0, ilen = metasets.length; i < ilen; ++i) { + ({index, data} = metasets[i]); + for (let j = 0, jlen = data.length; j < jlen; ++j) { + element = data[j]; + if (!element.skip) { + handler(element, index, j); + } + } + } +} +function binarySearch(metaset, axis, value, intersect) { + const {controller, data, _sorted} = metaset; + const iScale = controller._cachedMeta.iScale; + if (iScale && axis === iScale.axis && _sorted && data.length) { + const lookupMethod = iScale._reversePixels ? _rlookupByKey : _lookupByKey; + if (!intersect) { + return lookupMethod(data, axis, value); + } else if (controller._sharedOptions) { + const el = data[0]; + const range = typeof el.getRange === 'function' && el.getRange(axis); + if (range) { + const start = lookupMethod(data, axis, value - range); + const end = lookupMethod(data, axis, value + range); + return {lo: start.lo, hi: end.hi}; + } + } + } + return {lo: 0, hi: data.length - 1}; +} +function optimizedEvaluateItems(chart, axis, position, handler, intersect) { + const metasets = chart.getSortedVisibleDatasetMetas(); + const value = position[axis]; + for (let i = 0, ilen = metasets.length; i < ilen; ++i) { + const {index, data} = metasets[i]; + const {lo, hi} = binarySearch(metasets[i], axis, value, intersect); + for (let j = lo; j <= hi; ++j) { + const element = data[j]; + if (!element.skip) { + handler(element, index, j); + } + } + } +} +function getDistanceMetricForAxis(axis) { + const useX = axis.indexOf('x') !== -1; + const useY = axis.indexOf('y') !== -1; + return function(pt1, pt2) { + const deltaX = useX ? Math.abs(pt1.x - pt2.x) : 0; + const deltaY = useY ? Math.abs(pt1.y - pt2.y) : 0; + return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); + }; +} +function getIntersectItems(chart, position, axis, useFinalPosition) { + const items = []; + if (!_isPointInArea(position, chart.chartArea, chart._minPadding)) { + return items; + } + const evaluationFunc = function(element, datasetIndex, index) { + if (element.inRange(position.x, position.y, useFinalPosition)) { + items.push({element, datasetIndex, index}); + } + }; + optimizedEvaluateItems(chart, axis, position, evaluationFunc, true); + return items; +} +function getNearestItems(chart, position, axis, intersect, useFinalPosition) { + const distanceMetric = getDistanceMetricForAxis(axis); + let minDistance = Number.POSITIVE_INFINITY; + let items = []; + if (!_isPointInArea(position, chart.chartArea, chart._minPadding)) { + return items; + } + const evaluationFunc = function(element, datasetIndex, index) { + if (intersect && !element.inRange(position.x, position.y, useFinalPosition)) { + return; + } + const center = element.getCenterPoint(useFinalPosition); + if (!_isPointInArea(center, chart.chartArea, chart._minPadding)) { + return; + } + const distance = distanceMetric(position, center); + if (distance < minDistance) { + items = [{element, datasetIndex, index}]; + minDistance = distance; + } else if (distance === minDistance) { + items.push({element, datasetIndex, index}); + } + }; + optimizedEvaluateItems(chart, axis, position, evaluationFunc); + return items; +} +function getAxisItems(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const items = []; + const axis = options.axis; + const rangeMethod = axis === 'x' ? 'inXRange' : 'inYRange'; + let intersectsItem = false; + evaluateAllVisibleItems(chart, (element, datasetIndex, index) => { + if (element[rangeMethod](position[axis], useFinalPosition)) { + items.push({element, datasetIndex, index}); + } + if (element.inRange(position.x, position.y, useFinalPosition)) { + intersectsItem = true; + } + }); + if (options.intersect && !intersectsItem) { + return []; + } + return items; +} +var Interaction = { + modes: { + index(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const axis = options.axis || 'x'; + const items = options.intersect + ? getIntersectItems(chart, position, axis, useFinalPosition) + : getNearestItems(chart, position, axis, false, useFinalPosition); + const elements = []; + if (!items.length) { + return []; + } + chart.getSortedVisibleDatasetMetas().forEach((meta) => { + const index = items[0].index; + const element = meta.data[index]; + if (element && !element.skip) { + elements.push({element, datasetIndex: meta.index, index}); + } + }); + return elements; + }, + dataset(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const axis = options.axis || 'xy'; + let items = options.intersect + ? getIntersectItems(chart, position, axis, useFinalPosition) : + getNearestItems(chart, position, axis, false, useFinalPosition); + if (items.length > 0) { + const datasetIndex = items[0].datasetIndex; + const data = chart.getDatasetMeta(datasetIndex).data; + items = []; + for (let i = 0; i < data.length; ++i) { + items.push({element: data[i], datasetIndex, index: i}); + } + } + return items; + }, + point(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const axis = options.axis || 'xy'; + return getIntersectItems(chart, position, axis, useFinalPosition); + }, + nearest(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const axis = options.axis || 'xy'; + return getNearestItems(chart, position, axis, options.intersect, useFinalPosition); + }, + x(chart, e, options, useFinalPosition) { + options.axis = 'x'; + return getAxisItems(chart, e, options, useFinalPosition); + }, + y(chart, e, options, useFinalPosition) { + options.axis = 'y'; + return getAxisItems(chart, e, options, useFinalPosition); + } + } +}; + +const STATIC_POSITIONS = ['left', 'top', 'right', 'bottom']; +function filterByPosition(array, position) { + return array.filter(v => v.pos === position); +} +function filterDynamicPositionByAxis(array, axis) { + return array.filter(v => STATIC_POSITIONS.indexOf(v.pos) === -1 && v.box.axis === axis); +} +function sortByWeight(array, reverse) { + return array.sort((a, b) => { + const v0 = reverse ? b : a; + const v1 = reverse ? a : b; + return v0.weight === v1.weight ? + v0.index - v1.index : + v0.weight - v1.weight; + }); +} +function wrapBoxes(boxes) { + const layoutBoxes = []; + let i, ilen, box; + for (i = 0, ilen = (boxes || []).length; i < ilen; ++i) { + box = boxes[i]; + layoutBoxes.push({ + index: i, + box, + pos: box.position, + horizontal: box.isHorizontal(), + weight: box.weight + }); + } + return layoutBoxes; +} +function setLayoutDims(layouts, params) { + let i, ilen, layout; + for (i = 0, ilen = layouts.length; i < ilen; ++i) { + layout = layouts[i]; + if (layout.horizontal) { + layout.width = layout.box.fullSize && params.availableWidth; + layout.height = params.hBoxMaxHeight; + } else { + layout.width = params.vBoxMaxWidth; + layout.height = layout.box.fullSize && params.availableHeight; + } + } +} +function buildLayoutBoxes(boxes) { + const layoutBoxes = wrapBoxes(boxes); + const fullSize = sortByWeight(layoutBoxes.filter(wrap => wrap.box.fullSize), true); + const left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true); + const right = sortByWeight(filterByPosition(layoutBoxes, 'right')); + const top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true); + const bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom')); + const centerHorizontal = filterDynamicPositionByAxis(layoutBoxes, 'x'); + const centerVertical = filterDynamicPositionByAxis(layoutBoxes, 'y'); + return { + fullSize, + leftAndTop: left.concat(top), + rightAndBottom: right.concat(centerVertical).concat(bottom).concat(centerHorizontal), + chartArea: filterByPosition(layoutBoxes, 'chartArea'), + vertical: left.concat(right).concat(centerVertical), + horizontal: top.concat(bottom).concat(centerHorizontal) + }; +} +function getCombinedMax(maxPadding, chartArea, a, b) { + return Math.max(maxPadding[a], chartArea[a]) + Math.max(maxPadding[b], chartArea[b]); +} +function updateMaxPadding(maxPadding, boxPadding) { + maxPadding.top = Math.max(maxPadding.top, boxPadding.top); + maxPadding.left = Math.max(maxPadding.left, boxPadding.left); + maxPadding.bottom = Math.max(maxPadding.bottom, boxPadding.bottom); + maxPadding.right = Math.max(maxPadding.right, boxPadding.right); +} +function updateDims(chartArea, params, layout) { + const box = layout.box; + const maxPadding = chartArea.maxPadding; + if (!isObject(layout.pos)) { + if (layout.size) { + chartArea[layout.pos] -= layout.size; + } + layout.size = layout.horizontal ? box.height : box.width; + chartArea[layout.pos] += layout.size; + } + if (box.getPadding) { + updateMaxPadding(maxPadding, box.getPadding()); + } + const newWidth = Math.max(0, params.outerWidth - getCombinedMax(maxPadding, chartArea, 'left', 'right')); + const newHeight = Math.max(0, params.outerHeight - getCombinedMax(maxPadding, chartArea, 'top', 'bottom')); + const widthChanged = newWidth !== chartArea.w; + const heightChanged = newHeight !== chartArea.h; + chartArea.w = newWidth; + chartArea.h = newHeight; + return layout.horizontal + ? {same: widthChanged, other: heightChanged} + : {same: heightChanged, other: widthChanged}; +} +function handleMaxPadding(chartArea) { + const maxPadding = chartArea.maxPadding; + function updatePos(pos) { + const change = Math.max(maxPadding[pos] - chartArea[pos], 0); + chartArea[pos] += change; + return change; + } + chartArea.y += updatePos('top'); + chartArea.x += updatePos('left'); + updatePos('right'); + updatePos('bottom'); +} +function getMargins(horizontal, chartArea) { + const maxPadding = chartArea.maxPadding; + function marginForPositions(positions) { + const margin = {left: 0, top: 0, right: 0, bottom: 0}; + positions.forEach((pos) => { + margin[pos] = Math.max(chartArea[pos], maxPadding[pos]); + }); + return margin; + } + return horizontal + ? marginForPositions(['left', 'right']) + : marginForPositions(['top', 'bottom']); +} +function fitBoxes(boxes, chartArea, params) { + const refitBoxes = []; + let i, ilen, layout, box, refit, changed; + for (i = 0, ilen = boxes.length, refit = 0; i < ilen; ++i) { + layout = boxes[i]; + box = layout.box; + box.update( + layout.width || chartArea.w, + layout.height || chartArea.h, + getMargins(layout.horizontal, chartArea) + ); + const {same, other} = updateDims(chartArea, params, layout); + refit |= same && refitBoxes.length; + changed = changed || other; + if (!box.fullSize) { + refitBoxes.push(layout); + } + } + return refit && fitBoxes(refitBoxes, chartArea, params) || changed; +} +function placeBoxes(boxes, chartArea, params) { + const userPadding = params.padding; + let x = chartArea.x; + let y = chartArea.y; + let i, ilen, layout, box; + for (i = 0, ilen = boxes.length; i < ilen; ++i) { + layout = boxes[i]; + box = layout.box; + if (layout.horizontal) { + box.left = box.fullSize ? userPadding.left : chartArea.left; + box.right = box.fullSize ? params.outerWidth - userPadding.right : chartArea.left + chartArea.w; + box.top = y; + box.bottom = y + box.height; + box.width = box.right - box.left; + y = box.bottom; + } else { + box.left = x; + box.right = x + box.width; + box.top = box.fullSize ? userPadding.top : chartArea.top; + box.bottom = box.fullSize ? params.outerHeight - userPadding.bottom : chartArea.top + chartArea.h; + box.height = box.bottom - box.top; + x = box.right; + } + } + chartArea.x = x; + chartArea.y = y; +} +defaults.set('layout', { + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0 + } +}); +var layouts = { + addBox(chart, item) { + if (!chart.boxes) { + chart.boxes = []; + } + item.fullSize = item.fullSize || false; + item.position = item.position || 'top'; + item.weight = item.weight || 0; + item._layers = item._layers || function() { + return [{ + z: 0, + draw(chartArea) { + item.draw(chartArea); + } + }]; + }; + chart.boxes.push(item); + }, + removeBox(chart, layoutItem) { + const index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; + if (index !== -1) { + chart.boxes.splice(index, 1); + } + }, + configure(chart, item, options) { + item.fullSize = options.fullSize; + item.position = options.position; + item.weight = options.weight; + }, + update(chart, width, height, minPadding) { + if (!chart) { + return; + } + const padding = toPadding(chart.options.layout.padding); + const availableWidth = Math.max(width - padding.width, 0); + const availableHeight = Math.max(height - padding.height, 0); + const boxes = buildLayoutBoxes(chart.boxes); + const verticalBoxes = boxes.vertical; + const horizontalBoxes = boxes.horizontal; + each(chart.boxes, box => { + if (typeof box.beforeLayout === 'function') { + box.beforeLayout(); + } + }); + const visibleVerticalBoxCount = verticalBoxes.reduce((total, wrap) => + wrap.box.options && wrap.box.options.display === false ? total : total + 1, 0) || 1; + const params = Object.freeze({ + outerWidth: width, + outerHeight: height, + padding, + availableWidth, + availableHeight, + vBoxMaxWidth: availableWidth / 2 / visibleVerticalBoxCount, + hBoxMaxHeight: availableHeight / 2 + }); + const maxPadding = Object.assign({}, padding); + updateMaxPadding(maxPadding, toPadding(minPadding)); + const chartArea = Object.assign({ + maxPadding, + w: availableWidth, + h: availableHeight, + x: padding.left, + y: padding.top + }, padding); + setLayoutDims(verticalBoxes.concat(horizontalBoxes), params); + fitBoxes(boxes.fullSize, chartArea, params); + fitBoxes(verticalBoxes, chartArea, params); + if (fitBoxes(horizontalBoxes, chartArea, params)) { + fitBoxes(verticalBoxes, chartArea, params); + } + handleMaxPadding(chartArea); + placeBoxes(boxes.leftAndTop, chartArea, params); + chartArea.x += chartArea.w; + chartArea.y += chartArea.h; + placeBoxes(boxes.rightAndBottom, chartArea, params); + chart.chartArea = { + left: chartArea.left, + top: chartArea.top, + right: chartArea.left + chartArea.w, + bottom: chartArea.top + chartArea.h, + height: chartArea.h, + width: chartArea.w, + }; + each(boxes.chartArea, (layout) => { + const box = layout.box; + Object.assign(box, chart.chartArea); + box.update(chartArea.w, chartArea.h); + }); + } +}; + +class BasePlatform { + acquireContext(canvas, aspectRatio) {} + releaseContext(context) { + return false; + } + addEventListener(chart, type, listener) {} + removeEventListener(chart, type, listener) {} + getDevicePixelRatio() { + return 1; + } + getMaximumSize(element, width, height, aspectRatio) { + width = Math.max(0, width || element.width); + height = height || element.height; + return { + width, + height: Math.max(0, aspectRatio ? Math.floor(width / aspectRatio) : height) + }; + } + isAttached(canvas) { + return true; + } +} + +class BasicPlatform extends BasePlatform { + acquireContext(item) { + return item && item.getContext && item.getContext('2d') || null; + } +} + +const EXPANDO_KEY = '$chartjs'; +const EVENT_TYPES = { + touchstart: 'mousedown', + touchmove: 'mousemove', + touchend: 'mouseup', + pointerenter: 'mouseenter', + pointerdown: 'mousedown', + pointermove: 'mousemove', + pointerup: 'mouseup', + pointerleave: 'mouseout', + pointerout: 'mouseout' +}; +const isNullOrEmpty = value => value === null || value === ''; +function initCanvas(canvas, aspectRatio) { + const style = canvas.style; + const renderHeight = canvas.getAttribute('height'); + const renderWidth = canvas.getAttribute('width'); + canvas[EXPANDO_KEY] = { + initial: { + height: renderHeight, + width: renderWidth, + style: { + display: style.display, + height: style.height, + width: style.width + } + } + }; + style.display = style.display || 'block'; + style.boxSizing = style.boxSizing || 'border-box'; + if (isNullOrEmpty(renderWidth)) { + const displayWidth = readUsedSize(canvas, 'width'); + if (displayWidth !== undefined) { + canvas.width = displayWidth; + } + } + if (isNullOrEmpty(renderHeight)) { + if (canvas.style.height === '') { + canvas.height = canvas.width / (aspectRatio || 2); + } else { + const displayHeight = readUsedSize(canvas, 'height'); + if (displayHeight !== undefined) { + canvas.height = displayHeight; + } + } + } + return canvas; +} +const eventListenerOptions = supportsEventListenerOptions ? {passive: true} : false; +function addListener(node, type, listener) { + node.addEventListener(type, listener, eventListenerOptions); +} +function removeListener(chart, type, listener) { + chart.canvas.removeEventListener(type, listener, eventListenerOptions); +} +function fromNativeEvent(event, chart) { + const type = EVENT_TYPES[event.type] || event.type; + const {x, y} = getRelativePosition$1(event, chart); + return { + type, + chart, + native: event, + x: x !== undefined ? x : null, + y: y !== undefined ? y : null, + }; +} +function createAttachObserver(chart, type, listener) { + const canvas = chart.canvas; + const container = canvas && _getParentNode(canvas); + const element = container || canvas; + const observer = new MutationObserver(entries => { + const parent = _getParentNode(element); + entries.forEach(entry => { + for (let i = 0; i < entry.addedNodes.length; i++) { + const added = entry.addedNodes[i]; + if (added === element || added === parent) { + listener(entry.target); + } + } + }); + }); + observer.observe(document, {childList: true, subtree: true}); + return observer; +} +function createDetachObserver(chart, type, listener) { + const canvas = chart.canvas; + const container = canvas && _getParentNode(canvas); + if (!container) { + return; + } + const observer = new MutationObserver(entries => { + entries.forEach(entry => { + for (let i = 0; i < entry.removedNodes.length; i++) { + if (entry.removedNodes[i] === canvas) { + listener(); + break; + } + } + }); + }); + observer.observe(container, {childList: true}); + return observer; +} +const drpListeningCharts = new Map(); +let oldDevicePixelRatio = 0; +function onWindowResize() { + const dpr = window.devicePixelRatio; + if (dpr === oldDevicePixelRatio) { + return; + } + oldDevicePixelRatio = dpr; + drpListeningCharts.forEach((resize, chart) => { + if (chart.currentDevicePixelRatio !== dpr) { + resize(); + } + }); +} +function listenDevicePixelRatioChanges(chart, resize) { + if (!drpListeningCharts.size) { + window.addEventListener('resize', onWindowResize); + } + drpListeningCharts.set(chart, resize); +} +function unlistenDevicePixelRatioChanges(chart) { + drpListeningCharts.delete(chart); + if (!drpListeningCharts.size) { + window.removeEventListener('resize', onWindowResize); + } +} +function createResizeObserver(chart, type, listener) { + const canvas = chart.canvas; + const container = canvas && _getParentNode(canvas); + if (!container) { + return; + } + const resize = throttled((width, height) => { + const w = container.clientWidth; + listener(width, height); + if (w < container.clientWidth) { + listener(); + } + }, window); + const observer = new ResizeObserver(entries => { + const entry = entries[0]; + const width = entry.contentRect.width; + const height = entry.contentRect.height; + if (width === 0 && height === 0) { + return; + } + resize(width, height); + }); + observer.observe(container); + listenDevicePixelRatioChanges(chart, resize); + return observer; +} +function releaseObserver(chart, type, observer) { + if (observer) { + observer.disconnect(); + } + if (type === 'resize') { + unlistenDevicePixelRatioChanges(chart); + } +} +function createProxyAndListen(chart, type, listener) { + const canvas = chart.canvas; + const proxy = throttled((event) => { + if (chart.ctx !== null) { + listener(fromNativeEvent(event, chart)); + } + }, chart, (args) => { + const event = args[0]; + return [event, event.offsetX, event.offsetY]; + }); + addListener(canvas, type, proxy); + return proxy; +} +class DomPlatform extends BasePlatform { + acquireContext(canvas, aspectRatio) { + const context = canvas && canvas.getContext && canvas.getContext('2d'); + if (context && context.canvas === canvas) { + initCanvas(canvas, aspectRatio); + return context; + } + return null; + } + releaseContext(context) { + const canvas = context.canvas; + if (!canvas[EXPANDO_KEY]) { + return false; + } + const initial = canvas[EXPANDO_KEY].initial; + ['height', 'width'].forEach((prop) => { + const value = initial[prop]; + if (isNullOrUndef(value)) { + canvas.removeAttribute(prop); + } else { + canvas.setAttribute(prop, value); + } + }); + const style = initial.style || {}; + Object.keys(style).forEach((key) => { + canvas.style[key] = style[key]; + }); + canvas.width = canvas.width; + delete canvas[EXPANDO_KEY]; + return true; + } + addEventListener(chart, type, listener) { + this.removeEventListener(chart, type); + const proxies = chart.$proxies || (chart.$proxies = {}); + const handlers = { + attach: createAttachObserver, + detach: createDetachObserver, + resize: createResizeObserver + }; + const handler = handlers[type] || createProxyAndListen; + proxies[type] = handler(chart, type, listener); + } + removeEventListener(chart, type) { + const proxies = chart.$proxies || (chart.$proxies = {}); + const proxy = proxies[type]; + if (!proxy) { + return; + } + const handlers = { + attach: releaseObserver, + detach: releaseObserver, + resize: releaseObserver + }; + const handler = handlers[type] || removeListener; + handler(chart, type, proxy); + proxies[type] = undefined; + } + getDevicePixelRatio() { + return window.devicePixelRatio; + } + getMaximumSize(canvas, width, height, aspectRatio) { + return getMaximumSize(canvas, width, height, aspectRatio); + } + isAttached(canvas) { + const container = _getParentNode(canvas); + return !!(container && _getParentNode(container)); + } +} + +class Element { + constructor() { + this.x = undefined; + this.y = undefined; + this.active = false; + this.options = undefined; + this.$animations = undefined; + } + tooltipPosition(useFinalPosition) { + const {x, y} = this.getProps(['x', 'y'], useFinalPosition); + return {x, y}; + } + hasValue() { + return isNumber(this.x) && isNumber(this.y); + } + getProps(props, final) { + const me = this; + const anims = this.$animations; + if (!final || !anims) { + return me; + } + const ret = {}; + props.forEach(prop => { + ret[prop] = anims[prop] && anims[prop].active() ? anims[prop]._to : me[prop]; + }); + return ret; + } +} +Element.defaults = {}; +Element.defaultRoutes = undefined; + +const formatters = { + values(value) { + return isArray(value) ? value : '' + value; + }, + numeric(tickValue, index, ticks) { + if (tickValue === 0) { + return '0'; + } + const locale = this.chart.options.locale; + let notation; + let delta = tickValue; + if (ticks.length > 1) { + const maxTick = Math.max(Math.abs(ticks[0].value), Math.abs(ticks[ticks.length - 1].value)); + if (maxTick < 1e-4 || maxTick > 1e+15) { + notation = 'scientific'; + } + delta = calculateDelta(tickValue, ticks); + } + const logDelta = log10(Math.abs(delta)); + const numDecimal = Math.max(Math.min(-1 * Math.floor(logDelta), 20), 0); + const options = {notation, minimumFractionDigits: numDecimal, maximumFractionDigits: numDecimal}; + Object.assign(options, this.options.ticks.format); + return formatNumber(tickValue, locale, options); + }, + logarithmic(tickValue, index, ticks) { + if (tickValue === 0) { + return '0'; + } + const remain = tickValue / (Math.pow(10, Math.floor(log10(tickValue)))); + if (remain === 1 || remain === 2 || remain === 5) { + return formatters.numeric.call(this, tickValue, index, ticks); + } + return ''; + } +}; +function calculateDelta(tickValue, ticks) { + let delta = ticks.length > 3 ? ticks[2].value - ticks[1].value : ticks[1].value - ticks[0].value; + if (Math.abs(delta) >= 1 && tickValue !== Math.floor(tickValue)) { + delta = tickValue - Math.floor(tickValue); + } + return delta; +} +var Ticks = {formatters}; + +defaults.set('scale', { + display: true, + offset: false, + reverse: false, + beginAtZero: false, + bounds: 'ticks', + grace: 0, + grid: { + display: true, + lineWidth: 1, + drawBorder: true, + drawOnChartArea: true, + drawTicks: true, + tickLength: 8, + tickWidth: (_ctx, options) => options.lineWidth, + tickColor: (_ctx, options) => options.color, + offset: false, + borderDash: [], + borderDashOffset: 0.0, + borderWidth: 1 + }, + title: { + display: false, + text: '', + padding: { + top: 4, + bottom: 4 + } + }, + ticks: { + minRotation: 0, + maxRotation: 50, + mirror: false, + textStrokeWidth: 0, + textStrokeColor: '', + padding: 3, + display: true, + autoSkip: true, + autoSkipPadding: 3, + labelOffset: 0, + callback: Ticks.formatters.values, + minor: {}, + major: {}, + align: 'center', + crossAlign: 'near', + showLabelBackdrop: false, + backdropColor: 'rgba(255, 255, 255, 0.75)', + backdropPadding: 2, + } +}); +defaults.route('scale.ticks', 'color', '', 'color'); +defaults.route('scale.grid', 'color', '', 'borderColor'); +defaults.route('scale.grid', 'borderColor', '', 'borderColor'); +defaults.route('scale.title', 'color', '', 'color'); +defaults.describe('scale', { + _fallback: false, + _scriptable: (name) => !name.startsWith('before') && !name.startsWith('after') && name !== 'callback' && name !== 'parser', + _indexable: (name) => name !== 'borderDash' && name !== 'tickBorderDash', +}); +defaults.describe('scales', { + _fallback: 'scale', +}); +defaults.describe('scale.ticks', { + _scriptable: (name) => name !== 'backdropPadding' && name !== 'callback', + _indexable: (name) => name !== 'backdropPadding', +}); + +function autoSkip(scale, ticks) { + const tickOpts = scale.options.ticks; + const ticksLimit = tickOpts.maxTicksLimit || determineMaxTicks(scale); + const majorIndices = tickOpts.major.enabled ? getMajorIndices(ticks) : []; + const numMajorIndices = majorIndices.length; + const first = majorIndices[0]; + const last = majorIndices[numMajorIndices - 1]; + const newTicks = []; + if (numMajorIndices > ticksLimit) { + skipMajors(ticks, newTicks, majorIndices, numMajorIndices / ticksLimit); + return newTicks; + } + const spacing = calculateSpacing(majorIndices, ticks, ticksLimit); + if (numMajorIndices > 0) { + let i, ilen; + const avgMajorSpacing = numMajorIndices > 1 ? Math.round((last - first) / (numMajorIndices - 1)) : null; + skip(ticks, newTicks, spacing, isNullOrUndef(avgMajorSpacing) ? 0 : first - avgMajorSpacing, first); + for (i = 0, ilen = numMajorIndices - 1; i < ilen; i++) { + skip(ticks, newTicks, spacing, majorIndices[i], majorIndices[i + 1]); + } + skip(ticks, newTicks, spacing, last, isNullOrUndef(avgMajorSpacing) ? ticks.length : last + avgMajorSpacing); + return newTicks; + } + skip(ticks, newTicks, spacing); + return newTicks; +} +function determineMaxTicks(scale) { + const offset = scale.options.offset; + const tickLength = scale._tickSize(); + const maxScale = scale._length / tickLength + (offset ? 0 : 1); + const maxChart = scale._maxLength / tickLength; + return Math.floor(Math.min(maxScale, maxChart)); +} +function calculateSpacing(majorIndices, ticks, ticksLimit) { + const evenMajorSpacing = getEvenSpacing(majorIndices); + const spacing = ticks.length / ticksLimit; + if (!evenMajorSpacing) { + return Math.max(spacing, 1); + } + const factors = _factorize(evenMajorSpacing); + for (let i = 0, ilen = factors.length - 1; i < ilen; i++) { + const factor = factors[i]; + if (factor > spacing) { + return factor; + } + } + return Math.max(spacing, 1); +} +function getMajorIndices(ticks) { + const result = []; + let i, ilen; + for (i = 0, ilen = ticks.length; i < ilen; i++) { + if (ticks[i].major) { + result.push(i); + } + } + return result; +} +function skipMajors(ticks, newTicks, majorIndices, spacing) { + let count = 0; + let next = majorIndices[0]; + let i; + spacing = Math.ceil(spacing); + for (i = 0; i < ticks.length; i++) { + if (i === next) { + newTicks.push(ticks[i]); + count++; + next = majorIndices[count * spacing]; + } + } +} +function skip(ticks, newTicks, spacing, majorStart, majorEnd) { + const start = valueOrDefault(majorStart, 0); + const end = Math.min(valueOrDefault(majorEnd, ticks.length), ticks.length); + let count = 0; + let length, i, next; + spacing = Math.ceil(spacing); + if (majorEnd) { + length = majorEnd - majorStart; + spacing = length / Math.floor(length / spacing); + } + next = start; + while (next < 0) { + count++; + next = Math.round(start + count * spacing); + } + for (i = Math.max(start, 0); i < end; i++) { + if (i === next) { + newTicks.push(ticks[i]); + count++; + next = Math.round(start + count * spacing); + } + } +} +function getEvenSpacing(arr) { + const len = arr.length; + let i, diff; + if (len < 2) { + return false; + } + for (diff = arr[0], i = 1; i < len; ++i) { + if (arr[i] - arr[i - 1] !== diff) { + return false; + } + } + return diff; +} + +const reverseAlign = (align) => align === 'left' ? 'right' : align === 'right' ? 'left' : align; +const offsetFromEdge = (scale, edge, offset) => edge === 'top' || edge === 'left' ? scale[edge] + offset : scale[edge] - offset; +function sample(arr, numItems) { + const result = []; + const increment = arr.length / numItems; + const len = arr.length; + let i = 0; + for (; i < len; i += increment) { + result.push(arr[Math.floor(i)]); + } + return result; +} +function getPixelForGridLine(scale, index, offsetGridLines) { + const length = scale.ticks.length; + const validIndex = Math.min(index, length - 1); + const start = scale._startPixel; + const end = scale._endPixel; + const epsilon = 1e-6; + let lineValue = scale.getPixelForTick(validIndex); + let offset; + if (offsetGridLines) { + if (length === 1) { + offset = Math.max(lineValue - start, end - lineValue); + } else if (index === 0) { + offset = (scale.getPixelForTick(1) - lineValue) / 2; + } else { + offset = (lineValue - scale.getPixelForTick(validIndex - 1)) / 2; + } + lineValue += validIndex < index ? offset : -offset; + if (lineValue < start - epsilon || lineValue > end + epsilon) { + return; + } + } + return lineValue; +} +function garbageCollect(caches, length) { + each(caches, (cache) => { + const gc = cache.gc; + const gcLen = gc.length / 2; + let i; + if (gcLen > length) { + for (i = 0; i < gcLen; ++i) { + delete cache.data[gc[i]]; + } + gc.splice(0, gcLen); + } + }); +} +function getTickMarkLength(options) { + return options.drawTicks ? options.tickLength : 0; +} +function getTitleHeight(options, fallback) { + if (!options.display) { + return 0; + } + const font = toFont(options.font, fallback); + const padding = toPadding(options.padding); + const lines = isArray(options.text) ? options.text.length : 1; + return (lines * font.lineHeight) + padding.height; +} +function createScaleContext(parent, scale) { + return Object.assign(Object.create(parent), { + scale, + type: 'scale' + }); +} +function createTickContext(parent, index, tick) { + return Object.assign(Object.create(parent), { + tick, + index, + type: 'tick' + }); +} +function titleAlign(align, position, reverse) { + let ret = _toLeftRightCenter(align); + if ((reverse && position !== 'right') || (!reverse && position === 'right')) { + ret = reverseAlign(ret); + } + return ret; +} +function titleArgs(scale, offset, position, align) { + const {top, left, bottom, right} = scale; + let rotation = 0; + let maxWidth, titleX, titleY; + if (scale.isHorizontal()) { + titleX = _alignStartEnd(align, left, right); + titleY = offsetFromEdge(scale, position, offset); + maxWidth = right - left; + } else { + titleX = offsetFromEdge(scale, position, offset); + titleY = _alignStartEnd(align, bottom, top); + rotation = position === 'left' ? -HALF_PI : HALF_PI; + } + return {titleX, titleY, maxWidth, rotation}; +} +class Scale extends Element { + constructor(cfg) { + super(); + this.id = cfg.id; + this.type = cfg.type; + this.options = undefined; + this.ctx = cfg.ctx; + this.chart = cfg.chart; + this.top = undefined; + this.bottom = undefined; + this.left = undefined; + this.right = undefined; + this.width = undefined; + this.height = undefined; + this._margins = { + left: 0, + right: 0, + top: 0, + bottom: 0 + }; + this.maxWidth = undefined; + this.maxHeight = undefined; + this.paddingTop = undefined; + this.paddingBottom = undefined; + this.paddingLeft = undefined; + this.paddingRight = undefined; + this.axis = undefined; + this.labelRotation = undefined; + this.min = undefined; + this.max = undefined; + this._range = undefined; + this.ticks = []; + this._gridLineItems = null; + this._labelItems = null; + this._labelSizes = null; + this._length = 0; + this._maxLength = 0; + this._longestTextCache = {}; + this._startPixel = undefined; + this._endPixel = undefined; + this._reversePixels = false; + this._userMax = undefined; + this._userMin = undefined; + this._suggestedMax = undefined; + this._suggestedMin = undefined; + this._ticksLength = 0; + this._borderValue = 0; + this._cache = {}; + this._dataLimitsCached = false; + this.$context = undefined; + } + init(options) { + const me = this; + me.options = options.setContext(me.getContext()); + me.axis = options.axis; + me._userMin = me.parse(options.min); + me._userMax = me.parse(options.max); + me._suggestedMin = me.parse(options.suggestedMin); + me._suggestedMax = me.parse(options.suggestedMax); + } + parse(raw, index) { + return raw; + } + getUserBounds() { + let {_userMin, _userMax, _suggestedMin, _suggestedMax} = this; + _userMin = finiteOrDefault(_userMin, Number.POSITIVE_INFINITY); + _userMax = finiteOrDefault(_userMax, Number.NEGATIVE_INFINITY); + _suggestedMin = finiteOrDefault(_suggestedMin, Number.POSITIVE_INFINITY); + _suggestedMax = finiteOrDefault(_suggestedMax, Number.NEGATIVE_INFINITY); + return { + min: finiteOrDefault(_userMin, _suggestedMin), + max: finiteOrDefault(_userMax, _suggestedMax), + minDefined: isNumberFinite(_userMin), + maxDefined: isNumberFinite(_userMax) + }; + } + getMinMax(canStack) { + const me = this; + let {min, max, minDefined, maxDefined} = me.getUserBounds(); + let range; + if (minDefined && maxDefined) { + return {min, max}; + } + const metas = me.getMatchingVisibleMetas(); + for (let i = 0, ilen = metas.length; i < ilen; ++i) { + range = metas[i].controller.getMinMax(me, canStack); + if (!minDefined) { + min = Math.min(min, range.min); + } + if (!maxDefined) { + max = Math.max(max, range.max); + } + } + return { + min: finiteOrDefault(min, finiteOrDefault(max, min)), + max: finiteOrDefault(max, finiteOrDefault(min, max)) + }; + } + getPadding() { + const me = this; + return { + left: me.paddingLeft || 0, + top: me.paddingTop || 0, + right: me.paddingRight || 0, + bottom: me.paddingBottom || 0 + }; + } + getTicks() { + return this.ticks; + } + getLabels() { + const data = this.chart.data; + return this.options.labels || (this.isHorizontal() ? data.xLabels : data.yLabels) || data.labels || []; + } + beforeLayout() { + this._cache = {}; + this._dataLimitsCached = false; + } + beforeUpdate() { + callback(this.options.beforeUpdate, [this]); + } + update(maxWidth, maxHeight, margins) { + const me = this; + const tickOpts = me.options.ticks; + const sampleSize = tickOpts.sampleSize; + me.beforeUpdate(); + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me._margins = margins = Object.assign({ + left: 0, + right: 0, + top: 0, + bottom: 0 + }, margins); + me.ticks = null; + me._labelSizes = null; + me._gridLineItems = null; + me._labelItems = null; + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + me._maxLength = me.isHorizontal() + ? me.width + margins.left + margins.right + : me.height + margins.top + margins.bottom; + if (!me._dataLimitsCached) { + me.beforeDataLimits(); + me.determineDataLimits(); + me.afterDataLimits(); + me._range = _addGrace(me, me.options.grace); + me._dataLimitsCached = true; + } + me.beforeBuildTicks(); + me.ticks = me.buildTicks() || []; + me.afterBuildTicks(); + const samplingEnabled = sampleSize < me.ticks.length; + me._convertTicksToLabels(samplingEnabled ? sample(me.ticks, sampleSize) : me.ticks); + me.configure(); + me.beforeCalculateLabelRotation(); + me.calculateLabelRotation(); + me.afterCalculateLabelRotation(); + if (tickOpts.display && (tickOpts.autoSkip || tickOpts.source === 'auto')) { + me.ticks = autoSkip(me, me.ticks); + me._labelSizes = null; + } + if (samplingEnabled) { + me._convertTicksToLabels(me.ticks); + } + me.beforeFit(); + me.fit(); + me.afterFit(); + me.afterUpdate(); + } + configure() { + const me = this; + let reversePixels = me.options.reverse; + let startPixel, endPixel; + if (me.isHorizontal()) { + startPixel = me.left; + endPixel = me.right; + } else { + startPixel = me.top; + endPixel = me.bottom; + reversePixels = !reversePixels; + } + me._startPixel = startPixel; + me._endPixel = endPixel; + me._reversePixels = reversePixels; + me._length = endPixel - startPixel; + me._alignToPixels = me.options.alignToPixels; + } + afterUpdate() { + callback(this.options.afterUpdate, [this]); + } + beforeSetDimensions() { + callback(this.options.beforeSetDimensions, [this]); + } + setDimensions() { + const me = this; + if (me.isHorizontal()) { + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; + me.top = 0; + me.bottom = me.height; + } + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + } + afterSetDimensions() { + callback(this.options.afterSetDimensions, [this]); + } + _callHooks(name) { + const me = this; + me.chart.notifyPlugins(name, me.getContext()); + callback(me.options[name], [me]); + } + beforeDataLimits() { + this._callHooks('beforeDataLimits'); + } + determineDataLimits() {} + afterDataLimits() { + this._callHooks('afterDataLimits'); + } + beforeBuildTicks() { + this._callHooks('beforeBuildTicks'); + } + buildTicks() { + return []; + } + afterBuildTicks() { + this._callHooks('afterBuildTicks'); + } + beforeTickToLabelConversion() { + callback(this.options.beforeTickToLabelConversion, [this]); + } + generateTickLabels(ticks) { + const me = this; + const tickOpts = me.options.ticks; + let i, ilen, tick; + for (i = 0, ilen = ticks.length; i < ilen; i++) { + tick = ticks[i]; + tick.label = callback(tickOpts.callback, [tick.value, i, ticks], me); + } + } + afterTickToLabelConversion() { + callback(this.options.afterTickToLabelConversion, [this]); + } + beforeCalculateLabelRotation() { + callback(this.options.beforeCalculateLabelRotation, [this]); + } + calculateLabelRotation() { + const me = this; + const options = me.options; + const tickOpts = options.ticks; + const numTicks = me.ticks.length; + const minRotation = tickOpts.minRotation || 0; + const maxRotation = tickOpts.maxRotation; + let labelRotation = minRotation; + let tickWidth, maxHeight, maxLabelDiagonal; + if (!me._isVisible() || !tickOpts.display || minRotation >= maxRotation || numTicks <= 1 || !me.isHorizontal()) { + me.labelRotation = minRotation; + return; + } + const labelSizes = me._getLabelSizes(); + const maxLabelWidth = labelSizes.widest.width; + const maxLabelHeight = labelSizes.highest.height; + const maxWidth = _limitValue(me.chart.width - maxLabelWidth, 0, me.maxWidth); + tickWidth = options.offset ? me.maxWidth / numTicks : maxWidth / (numTicks - 1); + if (maxLabelWidth + 6 > tickWidth) { + tickWidth = maxWidth / (numTicks - (options.offset ? 0.5 : 1)); + maxHeight = me.maxHeight - getTickMarkLength(options.grid) + - tickOpts.padding - getTitleHeight(options.title, me.chart.options.font); + maxLabelDiagonal = Math.sqrt(maxLabelWidth * maxLabelWidth + maxLabelHeight * maxLabelHeight); + labelRotation = toDegrees(Math.min( + Math.asin(Math.min((labelSizes.highest.height + 6) / tickWidth, 1)), + Math.asin(Math.min(maxHeight / maxLabelDiagonal, 1)) - Math.asin(maxLabelHeight / maxLabelDiagonal) + )); + labelRotation = Math.max(minRotation, Math.min(maxRotation, labelRotation)); + } + me.labelRotation = labelRotation; + } + afterCalculateLabelRotation() { + callback(this.options.afterCalculateLabelRotation, [this]); + } + beforeFit() { + callback(this.options.beforeFit, [this]); + } + fit() { + const me = this; + const minSize = { + width: 0, + height: 0 + }; + const {chart, options: {ticks: tickOpts, title: titleOpts, grid: gridOpts}} = me; + const display = me._isVisible(); + const isHorizontal = me.isHorizontal(); + if (display) { + const titleHeight = getTitleHeight(titleOpts, chart.options.font); + if (isHorizontal) { + minSize.width = me.maxWidth; + minSize.height = getTickMarkLength(gridOpts) + titleHeight; + } else { + minSize.height = me.maxHeight; + minSize.width = getTickMarkLength(gridOpts) + titleHeight; + } + if (tickOpts.display && me.ticks.length) { + const {first, last, widest, highest} = me._getLabelSizes(); + const tickPadding = tickOpts.padding * 2; + const angleRadians = toRadians(me.labelRotation); + const cos = Math.cos(angleRadians); + const sin = Math.sin(angleRadians); + if (isHorizontal) { + const labelHeight = tickOpts.mirror ? 0 : sin * widest.width + cos * highest.height; + minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight + tickPadding); + } else { + const labelWidth = tickOpts.mirror ? 0 : cos * widest.width + sin * highest.height; + minSize.width = Math.min(me.maxWidth, minSize.width + labelWidth + tickPadding); + } + me._calculatePadding(first, last, sin, cos); + } + } + me._handleMargins(); + if (isHorizontal) { + me.width = me._length = chart.width - me._margins.left - me._margins.right; + me.height = minSize.height; + } else { + me.width = minSize.width; + me.height = me._length = chart.height - me._margins.top - me._margins.bottom; + } + } + _calculatePadding(first, last, sin, cos) { + const me = this; + const {ticks: {align, padding}, position} = me.options; + const isRotated = me.labelRotation !== 0; + const labelsBelowTicks = position !== 'top' && me.axis === 'x'; + if (me.isHorizontal()) { + const offsetLeft = me.getPixelForTick(0) - me.left; + const offsetRight = me.right - me.getPixelForTick(me.ticks.length - 1); + let paddingLeft = 0; + let paddingRight = 0; + if (isRotated) { + if (labelsBelowTicks) { + paddingLeft = cos * first.width; + paddingRight = sin * last.height; + } else { + paddingLeft = sin * first.height; + paddingRight = cos * last.width; + } + } else if (align === 'start') { + paddingRight = last.width; + } else if (align === 'end') { + paddingLeft = first.width; + } else { + paddingLeft = first.width / 2; + paddingRight = last.width / 2; + } + me.paddingLeft = Math.max((paddingLeft - offsetLeft + padding) * me.width / (me.width - offsetLeft), 0); + me.paddingRight = Math.max((paddingRight - offsetRight + padding) * me.width / (me.width - offsetRight), 0); + } else { + let paddingTop = last.height / 2; + let paddingBottom = first.height / 2; + if (align === 'start') { + paddingTop = 0; + paddingBottom = first.height; + } else if (align === 'end') { + paddingTop = last.height; + paddingBottom = 0; + } + me.paddingTop = paddingTop + padding; + me.paddingBottom = paddingBottom + padding; + } + } + _handleMargins() { + const me = this; + if (me._margins) { + me._margins.left = Math.max(me.paddingLeft, me._margins.left); + me._margins.top = Math.max(me.paddingTop, me._margins.top); + me._margins.right = Math.max(me.paddingRight, me._margins.right); + me._margins.bottom = Math.max(me.paddingBottom, me._margins.bottom); + } + } + afterFit() { + callback(this.options.afterFit, [this]); + } + isHorizontal() { + const {axis, position} = this.options; + return position === 'top' || position === 'bottom' || axis === 'x'; + } + isFullSize() { + return this.options.fullSize; + } + _convertTicksToLabels(ticks) { + const me = this; + me.beforeTickToLabelConversion(); + me.generateTickLabels(ticks); + let i, ilen; + for (i = 0, ilen = ticks.length; i < ilen; i++) { + if (isNullOrUndef(ticks[i].label)) { + ticks.splice(i, 1); + ilen--; + i--; + } + } + me.afterTickToLabelConversion(); + } + _getLabelSizes() { + const me = this; + let labelSizes = me._labelSizes; + if (!labelSizes) { + const sampleSize = me.options.ticks.sampleSize; + let ticks = me.ticks; + if (sampleSize < ticks.length) { + ticks = sample(ticks, sampleSize); + } + me._labelSizes = labelSizes = me._computeLabelSizes(ticks, ticks.length); + } + return labelSizes; + } + _computeLabelSizes(ticks, length) { + const {ctx, _longestTextCache: caches} = this; + const widths = []; + const heights = []; + let widestLabelSize = 0; + let highestLabelSize = 0; + let i, j, jlen, label, tickFont, fontString, cache, lineHeight, width, height, nestedLabel; + for (i = 0; i < length; ++i) { + label = ticks[i].label; + tickFont = this._resolveTickFontOptions(i); + ctx.font = fontString = tickFont.string; + cache = caches[fontString] = caches[fontString] || {data: {}, gc: []}; + lineHeight = tickFont.lineHeight; + width = height = 0; + if (!isNullOrUndef(label) && !isArray(label)) { + width = _measureText(ctx, cache.data, cache.gc, width, label); + height = lineHeight; + } else if (isArray(label)) { + for (j = 0, jlen = label.length; j < jlen; ++j) { + nestedLabel = label[j]; + if (!isNullOrUndef(nestedLabel) && !isArray(nestedLabel)) { + width = _measureText(ctx, cache.data, cache.gc, width, nestedLabel); + height += lineHeight; + } + } + } + widths.push(width); + heights.push(height); + widestLabelSize = Math.max(width, widestLabelSize); + highestLabelSize = Math.max(height, highestLabelSize); + } + garbageCollect(caches, length); + const widest = widths.indexOf(widestLabelSize); + const highest = heights.indexOf(highestLabelSize); + const valueAt = (idx) => ({width: widths[idx] || 0, height: heights[idx] || 0}); + return { + first: valueAt(0), + last: valueAt(length - 1), + widest: valueAt(widest), + highest: valueAt(highest), + widths, + heights, + }; + } + getLabelForValue(value) { + return value; + } + getPixelForValue(value, index) { + return NaN; + } + getValueForPixel(pixel) {} + getPixelForTick(index) { + const ticks = this.ticks; + if (index < 0 || index > ticks.length - 1) { + return null; + } + return this.getPixelForValue(ticks[index].value); + } + getPixelForDecimal(decimal) { + const me = this; + if (me._reversePixels) { + decimal = 1 - decimal; + } + const pixel = me._startPixel + decimal * me._length; + return _int16Range(me._alignToPixels ? _alignPixel(me.chart, pixel, 0) : pixel); + } + getDecimalForPixel(pixel) { + const decimal = (pixel - this._startPixel) / this._length; + return this._reversePixels ? 1 - decimal : decimal; + } + getBasePixel() { + return this.getPixelForValue(this.getBaseValue()); + } + getBaseValue() { + const {min, max} = this; + return min < 0 && max < 0 ? max : + min > 0 && max > 0 ? min : + 0; + } + getContext(index) { + const me = this; + const ticks = me.ticks || []; + if (index >= 0 && index < ticks.length) { + const tick = ticks[index]; + return tick.$context || + (tick.$context = createTickContext(me.getContext(), index, tick)); + } + return me.$context || + (me.$context = createScaleContext(me.chart.getContext(), me)); + } + _tickSize() { + const me = this; + const optionTicks = me.options.ticks; + const rot = toRadians(me.labelRotation); + const cos = Math.abs(Math.cos(rot)); + const sin = Math.abs(Math.sin(rot)); + const labelSizes = me._getLabelSizes(); + const padding = optionTicks.autoSkipPadding || 0; + const w = labelSizes ? labelSizes.widest.width + padding : 0; + const h = labelSizes ? labelSizes.highest.height + padding : 0; + return me.isHorizontal() + ? h * cos > w * sin ? w / cos : h / sin + : h * sin < w * cos ? h / cos : w / sin; + } + _isVisible() { + const display = this.options.display; + if (display !== 'auto') { + return !!display; + } + return this.getMatchingVisibleMetas().length > 0; + } + _computeGridLineItems(chartArea) { + const me = this; + const axis = me.axis; + const chart = me.chart; + const options = me.options; + const {grid, position} = options; + const offset = grid.offset; + const isHorizontal = me.isHorizontal(); + const ticks = me.ticks; + const ticksLength = ticks.length + (offset ? 1 : 0); + const tl = getTickMarkLength(grid); + const items = []; + const borderOpts = grid.setContext(me.getContext()); + const axisWidth = borderOpts.drawBorder ? borderOpts.borderWidth : 0; + const axisHalfWidth = axisWidth / 2; + const alignBorderValue = function(pixel) { + return _alignPixel(chart, pixel, axisWidth); + }; + let borderValue, i, lineValue, alignedLineValue; + let tx1, ty1, tx2, ty2, x1, y1, x2, y2; + if (position === 'top') { + borderValue = alignBorderValue(me.bottom); + ty1 = me.bottom - tl; + ty2 = borderValue - axisHalfWidth; + y1 = alignBorderValue(chartArea.top) + axisHalfWidth; + y2 = chartArea.bottom; + } else if (position === 'bottom') { + borderValue = alignBorderValue(me.top); + y1 = chartArea.top; + y2 = alignBorderValue(chartArea.bottom) - axisHalfWidth; + ty1 = borderValue + axisHalfWidth; + ty2 = me.top + tl; + } else if (position === 'left') { + borderValue = alignBorderValue(me.right); + tx1 = me.right - tl; + tx2 = borderValue - axisHalfWidth; + x1 = alignBorderValue(chartArea.left) + axisHalfWidth; + x2 = chartArea.right; + } else if (position === 'right') { + borderValue = alignBorderValue(me.left); + x1 = chartArea.left; + x2 = alignBorderValue(chartArea.right) - axisHalfWidth; + tx1 = borderValue + axisHalfWidth; + tx2 = me.left + tl; + } else if (axis === 'x') { + if (position === 'center') { + borderValue = alignBorderValue((chartArea.top + chartArea.bottom) / 2 + 0.5); + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + borderValue = alignBorderValue(me.chart.scales[positionAxisID].getPixelForValue(value)); + } + y1 = chartArea.top; + y2 = chartArea.bottom; + ty1 = borderValue + axisHalfWidth; + ty2 = ty1 + tl; + } else if (axis === 'y') { + if (position === 'center') { + borderValue = alignBorderValue((chartArea.left + chartArea.right) / 2); + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + borderValue = alignBorderValue(me.chart.scales[positionAxisID].getPixelForValue(value)); + } + tx1 = borderValue - axisHalfWidth; + tx2 = tx1 - tl; + x1 = chartArea.left; + x2 = chartArea.right; + } + const limit = valueOrDefault(options.ticks.maxTicksLimit, ticksLength); + const step = Math.max(1, Math.ceil(ticksLength / limit)); + for (i = 0; i < ticksLength; i += step) { + const optsAtIndex = grid.setContext(me.getContext(i)); + const lineWidth = optsAtIndex.lineWidth; + const lineColor = optsAtIndex.color; + const borderDash = grid.borderDash || []; + const borderDashOffset = optsAtIndex.borderDashOffset; + const tickWidth = optsAtIndex.tickWidth; + const tickColor = optsAtIndex.tickColor; + const tickBorderDash = optsAtIndex.tickBorderDash || []; + const tickBorderDashOffset = optsAtIndex.tickBorderDashOffset; + lineValue = getPixelForGridLine(me, i, offset); + if (lineValue === undefined) { + continue; + } + alignedLineValue = _alignPixel(chart, lineValue, lineWidth); + if (isHorizontal) { + tx1 = tx2 = x1 = x2 = alignedLineValue; + } else { + ty1 = ty2 = y1 = y2 = alignedLineValue; + } + items.push({ + tx1, + ty1, + tx2, + ty2, + x1, + y1, + x2, + y2, + width: lineWidth, + color: lineColor, + borderDash, + borderDashOffset, + tickWidth, + tickColor, + tickBorderDash, + tickBorderDashOffset, + }); + } + me._ticksLength = ticksLength; + me._borderValue = borderValue; + return items; + } + _computeLabelItems(chartArea) { + const me = this; + const axis = me.axis; + const options = me.options; + const {position, ticks: optionTicks} = options; + const isHorizontal = me.isHorizontal(); + const ticks = me.ticks; + const {align, crossAlign, padding, mirror} = optionTicks; + const tl = getTickMarkLength(options.grid); + const tickAndPadding = tl + padding; + const hTickAndPadding = mirror ? -padding : tickAndPadding; + const rotation = -toRadians(me.labelRotation); + const items = []; + let i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset; + let textBaseline = 'middle'; + if (position === 'top') { + y = me.bottom - hTickAndPadding; + textAlign = me._getXAxisLabelAlignment(); + } else if (position === 'bottom') { + y = me.top + hTickAndPadding; + textAlign = me._getXAxisLabelAlignment(); + } else if (position === 'left') { + const ret = me._getYAxisLabelAlignment(tl); + textAlign = ret.textAlign; + x = ret.x; + } else if (position === 'right') { + const ret = me._getYAxisLabelAlignment(tl); + textAlign = ret.textAlign; + x = ret.x; + } else if (axis === 'x') { + if (position === 'center') { + y = ((chartArea.top + chartArea.bottom) / 2) + tickAndPadding; + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + y = me.chart.scales[positionAxisID].getPixelForValue(value) + tickAndPadding; + } + textAlign = me._getXAxisLabelAlignment(); + } else if (axis === 'y') { + if (position === 'center') { + x = ((chartArea.left + chartArea.right) / 2) - tickAndPadding; + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + x = me.chart.scales[positionAxisID].getPixelForValue(value); + } + textAlign = me._getYAxisLabelAlignment(tl).textAlign; + } + if (axis === 'y') { + if (align === 'start') { + textBaseline = 'top'; + } else if (align === 'end') { + textBaseline = 'bottom'; + } + } + const labelSizes = me._getLabelSizes(); + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + tick = ticks[i]; + label = tick.label; + const optsAtIndex = optionTicks.setContext(me.getContext(i)); + pixel = me.getPixelForTick(i) + optionTicks.labelOffset; + font = me._resolveTickFontOptions(i); + lineHeight = font.lineHeight; + lineCount = isArray(label) ? label.length : 1; + const halfCount = lineCount / 2; + const color = optsAtIndex.color; + const strokeColor = optsAtIndex.textStrokeColor; + const strokeWidth = optsAtIndex.textStrokeWidth; + if (isHorizontal) { + x = pixel; + if (position === 'top') { + if (crossAlign === 'near' || rotation !== 0) { + textOffset = -lineCount * lineHeight + lineHeight / 2; + } else if (crossAlign === 'center') { + textOffset = -labelSizes.highest.height / 2 - halfCount * lineHeight + lineHeight; + } else { + textOffset = -labelSizes.highest.height + lineHeight / 2; + } + } else { + if (crossAlign === 'near' || rotation !== 0) { + textOffset = lineHeight / 2; + } else if (crossAlign === 'center') { + textOffset = labelSizes.highest.height / 2 - halfCount * lineHeight; + } else { + textOffset = labelSizes.highest.height - lineCount * lineHeight; + } + } + if (mirror) { + textOffset *= -1; + } + } else { + y = pixel; + textOffset = (1 - lineCount) * lineHeight / 2; + } + let backdrop; + if (optsAtIndex.showLabelBackdrop) { + const labelPadding = toPadding(optsAtIndex.backdropPadding); + const height = labelSizes.heights[i]; + const width = labelSizes.widths[i]; + let top = y + textOffset - labelPadding.top; + let left = x - labelPadding.left; + switch (textBaseline) { + case 'middle': + top -= height / 2; + break; + case 'bottom': + top -= height; + break; + } + switch (textAlign) { + case 'center': + left -= width / 2; + break; + case 'right': + left -= width; + break; + } + backdrop = { + left, + top, + width: width + labelPadding.width, + height: height + labelPadding.height, + color: optsAtIndex.backdropColor, + }; + } + items.push({ + rotation, + label, + font, + color, + strokeColor, + strokeWidth, + textOffset, + textAlign, + textBaseline, + translation: [x, y], + backdrop, + }); + } + return items; + } + _getXAxisLabelAlignment() { + const me = this; + const {position, ticks} = me.options; + const rotation = -toRadians(me.labelRotation); + if (rotation) { + return position === 'top' ? 'left' : 'right'; + } + let align = 'center'; + if (ticks.align === 'start') { + align = 'left'; + } else if (ticks.align === 'end') { + align = 'right'; + } + return align; + } + _getYAxisLabelAlignment(tl) { + const me = this; + const {position, ticks: {crossAlign, mirror, padding}} = me.options; + const labelSizes = me._getLabelSizes(); + const tickAndPadding = tl + padding; + const widest = labelSizes.widest.width; + let textAlign; + let x; + if (position === 'left') { + if (mirror) { + textAlign = 'left'; + x = me.right + padding; + } else { + x = me.right - tickAndPadding; + if (crossAlign === 'near') { + textAlign = 'right'; + } else if (crossAlign === 'center') { + textAlign = 'center'; + x -= (widest / 2); + } else { + textAlign = 'left'; + x = me.left; + } + } + } else if (position === 'right') { + if (mirror) { + textAlign = 'right'; + x = me.left + padding; + } else { + x = me.left + tickAndPadding; + if (crossAlign === 'near') { + textAlign = 'left'; + } else if (crossAlign === 'center') { + textAlign = 'center'; + x += widest / 2; + } else { + textAlign = 'right'; + x = me.right; + } + } + } else { + textAlign = 'right'; + } + return {textAlign, x}; + } + _computeLabelArea() { + const me = this; + if (me.options.ticks.mirror) { + return; + } + const chart = me.chart; + const position = me.options.position; + if (position === 'left' || position === 'right') { + return {top: 0, left: me.left, bottom: chart.height, right: me.right}; + } if (position === 'top' || position === 'bottom') { + return {top: me.top, left: 0, bottom: me.bottom, right: chart.width}; + } + } + drawBackground() { + const {ctx, options: {backgroundColor}, left, top, width, height} = this; + if (backgroundColor) { + ctx.save(); + ctx.fillStyle = backgroundColor; + ctx.fillRect(left, top, width, height); + ctx.restore(); + } + } + getLineWidthForValue(value) { + const me = this; + const grid = me.options.grid; + if (!me._isVisible() || !grid.display) { + return 0; + } + const ticks = me.ticks; + const index = ticks.findIndex(t => t.value === value); + if (index >= 0) { + const opts = grid.setContext(me.getContext(index)); + return opts.lineWidth; + } + return 0; + } + drawGrid(chartArea) { + const me = this; + const grid = me.options.grid; + const ctx = me.ctx; + const items = me._gridLineItems || (me._gridLineItems = me._computeGridLineItems(chartArea)); + let i, ilen; + const drawLine = (p1, p2, style) => { + if (!style.width || !style.color) { + return; + } + ctx.save(); + ctx.lineWidth = style.width; + ctx.strokeStyle = style.color; + ctx.setLineDash(style.borderDash || []); + ctx.lineDashOffset = style.borderDashOffset; + ctx.beginPath(); + ctx.moveTo(p1.x, p1.y); + ctx.lineTo(p2.x, p2.y); + ctx.stroke(); + ctx.restore(); + }; + if (grid.display) { + for (i = 0, ilen = items.length; i < ilen; ++i) { + const item = items[i]; + if (grid.drawOnChartArea) { + drawLine( + {x: item.x1, y: item.y1}, + {x: item.x2, y: item.y2}, + item + ); + } + if (grid.drawTicks) { + drawLine( + {x: item.tx1, y: item.ty1}, + {x: item.tx2, y: item.ty2}, + { + color: item.tickColor, + width: item.tickWidth, + borderDash: item.tickBorderDash, + borderDashOffset: item.tickBorderDashOffset + } + ); + } + } + } + } + drawBorder() { + const me = this; + const {chart, ctx, options: {grid}} = me; + const borderOpts = grid.setContext(me.getContext()); + const axisWidth = grid.drawBorder ? borderOpts.borderWidth : 0; + if (!axisWidth) { + return; + } + const lastLineWidth = grid.setContext(me.getContext(0)).lineWidth; + const borderValue = me._borderValue; + let x1, x2, y1, y2; + if (me.isHorizontal()) { + x1 = _alignPixel(chart, me.left, axisWidth) - axisWidth / 2; + x2 = _alignPixel(chart, me.right, lastLineWidth) + lastLineWidth / 2; + y1 = y2 = borderValue; + } else { + y1 = _alignPixel(chart, me.top, axisWidth) - axisWidth / 2; + y2 = _alignPixel(chart, me.bottom, lastLineWidth) + lastLineWidth / 2; + x1 = x2 = borderValue; + } + ctx.save(); + ctx.lineWidth = borderOpts.borderWidth; + ctx.strokeStyle = borderOpts.borderColor; + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + ctx.restore(); + } + drawLabels(chartArea) { + const me = this; + const optionTicks = me.options.ticks; + if (!optionTicks.display) { + return; + } + const ctx = me.ctx; + const area = me._computeLabelArea(); + if (area) { + clipArea(ctx, area); + } + const items = me._labelItems || (me._labelItems = me._computeLabelItems(chartArea)); + let i, ilen; + for (i = 0, ilen = items.length; i < ilen; ++i) { + const item = items[i]; + const tickFont = item.font; + const label = item.label; + if (item.backdrop) { + ctx.fillStyle = item.backdrop.color; + ctx.fillRect(item.backdrop.left, item.backdrop.top, item.backdrop.width, item.backdrop.height); + } + let y = item.textOffset; + renderText(ctx, label, 0, y, tickFont, item); + } + if (area) { + unclipArea(ctx); + } + } + drawTitle() { + const {ctx, options: {position, title, reverse}} = this; + if (!title.display) { + return; + } + const font = toFont(title.font); + const padding = toPadding(title.padding); + const align = title.align; + let offset = font.lineHeight / 2; + if (position === 'bottom') { + offset += padding.bottom; + if (isArray(title.text)) { + offset += font.lineHeight * (title.text.length - 1); + } + } else { + offset += padding.top; + } + const {titleX, titleY, maxWidth, rotation} = titleArgs(this, offset, position, align); + renderText(ctx, title.text, 0, 0, font, { + color: title.color, + maxWidth, + rotation, + textAlign: titleAlign(align, position, reverse), + textBaseline: 'middle', + translation: [titleX, titleY], + }); + } + draw(chartArea) { + const me = this; + if (!me._isVisible()) { + return; + } + me.drawBackground(); + me.drawGrid(chartArea); + me.drawBorder(); + me.drawTitle(); + me.drawLabels(chartArea); + } + _layers() { + const me = this; + const opts = me.options; + const tz = opts.ticks && opts.ticks.z || 0; + const gz = opts.grid && opts.grid.z || 0; + if (!me._isVisible() || me.draw !== Scale.prototype.draw) { + return [{ + z: tz, + draw(chartArea) { + me.draw(chartArea); + } + }]; + } + return [{ + z: gz, + draw(chartArea) { + me.drawBackground(); + me.drawGrid(chartArea); + me.drawTitle(); + } + }, { + z: gz + 1, + draw() { + me.drawBorder(); + } + }, { + z: tz, + draw(chartArea) { + me.drawLabels(chartArea); + } + }]; + } + getMatchingVisibleMetas(type) { + const me = this; + const metas = me.chart.getSortedVisibleDatasetMetas(); + const axisID = me.axis + 'AxisID'; + const result = []; + let i, ilen; + for (i = 0, ilen = metas.length; i < ilen; ++i) { + const meta = metas[i]; + if (meta[axisID] === me.id && (!type || meta.type === type)) { + result.push(meta); + } + } + return result; + } + _resolveTickFontOptions(index) { + const opts = this.options.ticks.setContext(this.getContext(index)); + return toFont(opts.font); + } + _maxDigits() { + const me = this; + const fontSize = me._resolveTickFontOptions(0).lineHeight; + return (me.isHorizontal() ? me.width : me.height) / fontSize; + } +} + +class TypedRegistry { + constructor(type, scope, override) { + this.type = type; + this.scope = scope; + this.override = override; + this.items = Object.create(null); + } + isForType(type) { + return Object.prototype.isPrototypeOf.call(this.type.prototype, type.prototype); + } + register(item) { + const me = this; + const proto = Object.getPrototypeOf(item); + let parentScope; + if (isIChartComponent(proto)) { + parentScope = me.register(proto); + } + const items = me.items; + const id = item.id; + const scope = me.scope + '.' + id; + if (!id) { + throw new Error('class does not have id: ' + item); + } + if (id in items) { + return scope; + } + items[id] = item; + registerDefaults(item, scope, parentScope); + if (me.override) { + defaults.override(item.id, item.overrides); + } + return scope; + } + get(id) { + return this.items[id]; + } + unregister(item) { + const items = this.items; + const id = item.id; + const scope = this.scope; + if (id in items) { + delete items[id]; + } + if (scope && id in defaults[scope]) { + delete defaults[scope][id]; + if (this.override) { + delete overrides[id]; + } + } + } +} +function registerDefaults(item, scope, parentScope) { + const itemDefaults = merge(Object.create(null), [ + parentScope ? defaults.get(parentScope) : {}, + defaults.get(scope), + item.defaults + ]); + defaults.set(scope, itemDefaults); + if (item.defaultRoutes) { + routeDefaults(scope, item.defaultRoutes); + } + if (item.descriptors) { + defaults.describe(scope, item.descriptors); + } +} +function routeDefaults(scope, routes) { + Object.keys(routes).forEach(property => { + const propertyParts = property.split('.'); + const sourceName = propertyParts.pop(); + const sourceScope = [scope].concat(propertyParts).join('.'); + const parts = routes[property].split('.'); + const targetName = parts.pop(); + const targetScope = parts.join('.'); + defaults.route(sourceScope, sourceName, targetScope, targetName); + }); +} +function isIChartComponent(proto) { + return 'id' in proto && 'defaults' in proto; +} + +class Registry { + constructor() { + this.controllers = new TypedRegistry(DatasetController, 'datasets', true); + this.elements = new TypedRegistry(Element, 'elements'); + this.plugins = new TypedRegistry(Object, 'plugins'); + this.scales = new TypedRegistry(Scale, 'scales'); + this._typedRegistries = [this.controllers, this.scales, this.elements]; + } + add(...args) { + this._each('register', args); + } + remove(...args) { + this._each('unregister', args); + } + addControllers(...args) { + this._each('register', args, this.controllers); + } + addElements(...args) { + this._each('register', args, this.elements); + } + addPlugins(...args) { + this._each('register', args, this.plugins); + } + addScales(...args) { + this._each('register', args, this.scales); + } + getController(id) { + return this._get(id, this.controllers, 'controller'); + } + getElement(id) { + return this._get(id, this.elements, 'element'); + } + getPlugin(id) { + return this._get(id, this.plugins, 'plugin'); + } + getScale(id) { + return this._get(id, this.scales, 'scale'); + } + removeControllers(...args) { + this._each('unregister', args, this.controllers); + } + removeElements(...args) { + this._each('unregister', args, this.elements); + } + removePlugins(...args) { + this._each('unregister', args, this.plugins); + } + removeScales(...args) { + this._each('unregister', args, this.scales); + } + _each(method, args, typedRegistry) { + const me = this; + [...args].forEach(arg => { + const reg = typedRegistry || me._getRegistryForType(arg); + if (typedRegistry || reg.isForType(arg) || (reg === me.plugins && arg.id)) { + me._exec(method, reg, arg); + } else { + each(arg, item => { + const itemReg = typedRegistry || me._getRegistryForType(item); + me._exec(method, itemReg, item); + }); + } + }); + } + _exec(method, registry, component) { + const camelMethod = _capitalize(method); + callback(component['before' + camelMethod], [], component); + registry[method](component); + callback(component['after' + camelMethod], [], component); + } + _getRegistryForType(type) { + for (let i = 0; i < this._typedRegistries.length; i++) { + const reg = this._typedRegistries[i]; + if (reg.isForType(type)) { + return reg; + } + } + return this.plugins; + } + _get(id, typedRegistry, type) { + const item = typedRegistry.get(id); + if (item === undefined) { + throw new Error('"' + id + '" is not a registered ' + type + '.'); + } + return item; + } +} +var registry = new Registry(); + +class PluginService { + constructor() { + this._init = []; + } + notify(chart, hook, args, filter) { + const me = this; + if (hook === 'beforeInit') { + me._init = me._createDescriptors(chart, true); + me._notify(me._init, chart, 'install'); + } + const descriptors = filter ? me._descriptors(chart).filter(filter) : me._descriptors(chart); + const result = me._notify(descriptors, chart, hook, args); + if (hook === 'destroy') { + me._notify(descriptors, chart, 'stop'); + me._notify(me._init, chart, 'uninstall'); + } + return result; + } + _notify(descriptors, chart, hook, args) { + args = args || {}; + for (const descriptor of descriptors) { + const plugin = descriptor.plugin; + const method = plugin[hook]; + const params = [chart, args, descriptor.options]; + if (callback(method, params, plugin) === false && args.cancelable) { + return false; + } + } + return true; + } + invalidate() { + if (!isNullOrUndef(this._cache)) { + this._oldCache = this._cache; + this._cache = undefined; + } + } + _descriptors(chart) { + if (this._cache) { + return this._cache; + } + const descriptors = this._cache = this._createDescriptors(chart); + this._notifyStateChanges(chart); + return descriptors; + } + _createDescriptors(chart, all) { + const config = chart && chart.config; + const options = valueOrDefault(config.options && config.options.plugins, {}); + const plugins = allPlugins(config); + return options === false && !all ? [] : createDescriptors(chart, plugins, options, all); + } + _notifyStateChanges(chart) { + const previousDescriptors = this._oldCache || []; + const descriptors = this._cache; + const diff = (a, b) => a.filter(x => !b.some(y => x.plugin.id === y.plugin.id)); + this._notify(diff(previousDescriptors, descriptors), chart, 'stop'); + this._notify(diff(descriptors, previousDescriptors), chart, 'start'); + } +} +function allPlugins(config) { + const plugins = []; + const keys = Object.keys(registry.plugins.items); + for (let i = 0; i < keys.length; i++) { + plugins.push(registry.getPlugin(keys[i])); + } + const local = config.plugins || []; + for (let i = 0; i < local.length; i++) { + const plugin = local[i]; + if (plugins.indexOf(plugin) === -1) { + plugins.push(plugin); + } + } + return plugins; +} +function getOpts(options, all) { + if (!all && options === false) { + return null; + } + if (options === true) { + return {}; + } + return options; +} +function createDescriptors(chart, plugins, options, all) { + const result = []; + const context = chart.getContext(); + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + const id = plugin.id; + const opts = getOpts(options[id], all); + if (opts === null) { + continue; + } + result.push({ + plugin, + options: pluginOpts(chart.config, plugin, opts, context) + }); + } + return result; +} +function pluginOpts(config, plugin, opts, context) { + const keys = config.pluginScopeKeys(plugin); + const scopes = config.getOptionScopes(opts, keys); + return config.createResolver(scopes, context, [''], {scriptable: false, indexable: false, allKeys: true}); +} + +function getIndexAxis(type, options) { + const datasetDefaults = defaults.datasets[type] || {}; + const datasetOptions = (options.datasets || {})[type] || {}; + return datasetOptions.indexAxis || options.indexAxis || datasetDefaults.indexAxis || 'x'; +} +function getAxisFromDefaultScaleID(id, indexAxis) { + let axis = id; + if (id === '_index_') { + axis = indexAxis; + } else if (id === '_value_') { + axis = indexAxis === 'x' ? 'y' : 'x'; + } + return axis; +} +function getDefaultScaleIDFromAxis(axis, indexAxis) { + return axis === indexAxis ? '_index_' : '_value_'; +} +function axisFromPosition(position) { + if (position === 'top' || position === 'bottom') { + return 'x'; + } + if (position === 'left' || position === 'right') { + return 'y'; + } +} +function determineAxis(id, scaleOptions) { + if (id === 'x' || id === 'y') { + return id; + } + return scaleOptions.axis || axisFromPosition(scaleOptions.position) || id.charAt(0).toLowerCase(); +} +function mergeScaleConfig(config, options) { + const chartDefaults = overrides[config.type] || {scales: {}}; + const configScales = options.scales || {}; + const chartIndexAxis = getIndexAxis(config.type, options); + const firstIDs = Object.create(null); + const scales = Object.create(null); + Object.keys(configScales).forEach(id => { + const scaleConf = configScales[id]; + const axis = determineAxis(id, scaleConf); + const defaultId = getDefaultScaleIDFromAxis(axis, chartIndexAxis); + const defaultScaleOptions = chartDefaults.scales || {}; + firstIDs[axis] = firstIDs[axis] || id; + scales[id] = mergeIf(Object.create(null), [{axis}, scaleConf, defaultScaleOptions[axis], defaultScaleOptions[defaultId]]); + }); + config.data.datasets.forEach(dataset => { + const type = dataset.type || config.type; + const indexAxis = dataset.indexAxis || getIndexAxis(type, options); + const datasetDefaults = overrides[type] || {}; + const defaultScaleOptions = datasetDefaults.scales || {}; + Object.keys(defaultScaleOptions).forEach(defaultID => { + const axis = getAxisFromDefaultScaleID(defaultID, indexAxis); + const id = dataset[axis + 'AxisID'] || firstIDs[axis] || axis; + scales[id] = scales[id] || Object.create(null); + mergeIf(scales[id], [{axis}, configScales[id], defaultScaleOptions[defaultID]]); + }); + }); + Object.keys(scales).forEach(key => { + const scale = scales[key]; + mergeIf(scale, [defaults.scales[scale.type], defaults.scale]); + }); + return scales; +} +function initOptions(config) { + const options = config.options || (config.options = {}); + options.plugins = valueOrDefault(options.plugins, {}); + options.scales = mergeScaleConfig(config, options); +} +function initData(data) { + data = data || {}; + data.datasets = data.datasets || []; + data.labels = data.labels || []; + return data; +} +function initConfig(config) { + config = config || {}; + config.data = initData(config.data); + initOptions(config); + return config; +} +const keyCache = new Map(); +const keysCached = new Set(); +function cachedKeys(cacheKey, generate) { + let keys = keyCache.get(cacheKey); + if (!keys) { + keys = generate(); + keyCache.set(cacheKey, keys); + keysCached.add(keys); + } + return keys; +} +const addIfFound = (set, obj, key) => { + const opts = resolveObjectKey(obj, key); + if (opts !== undefined) { + set.add(opts); + } +}; +class Config { + constructor(config) { + this._config = initConfig(config); + this._scopeCache = new Map(); + this._resolverCache = new Map(); + } + get type() { + return this._config.type; + } + set type(type) { + this._config.type = type; + } + get data() { + return this._config.data; + } + set data(data) { + this._config.data = initData(data); + } + get options() { + return this._config.options; + } + set options(options) { + this._config.options = options; + } + get plugins() { + return this._config.plugins; + } + update() { + const config = this._config; + this.clearCache(); + initOptions(config); + } + clearCache() { + this._scopeCache.clear(); + this._resolverCache.clear(); + } + datasetScopeKeys(datasetType) { + return cachedKeys(datasetType, + () => [[ + `datasets.${datasetType}`, + '' + ]]); + } + datasetAnimationScopeKeys(datasetType, transition) { + return cachedKeys(`${datasetType}.transition.${transition}`, + () => [ + [ + `datasets.${datasetType}.transitions.${transition}`, + `transitions.${transition}`, + ], + [ + `datasets.${datasetType}`, + '' + ] + ]); + } + datasetElementScopeKeys(datasetType, elementType) { + return cachedKeys(`${datasetType}-${elementType}`, + () => [[ + `datasets.${datasetType}.elements.${elementType}`, + `datasets.${datasetType}`, + `elements.${elementType}`, + '' + ]]); + } + pluginScopeKeys(plugin) { + const id = plugin.id; + const type = this.type; + return cachedKeys(`${type}-plugin-${id}`, + () => [[ + `plugins.${id}`, + ...plugin.additionalOptionScopes || [], + ]]); + } + _cachedScopes(mainScope, resetCache) { + const _scopeCache = this._scopeCache; + let cache = _scopeCache.get(mainScope); + if (!cache || resetCache) { + cache = new Map(); + _scopeCache.set(mainScope, cache); + } + return cache; + } + getOptionScopes(mainScope, keyLists, resetCache) { + const {options, type} = this; + const cache = this._cachedScopes(mainScope, resetCache); + const cached = cache.get(keyLists); + if (cached) { + return cached; + } + const scopes = new Set(); + keyLists.forEach(keys => { + if (mainScope) { + scopes.add(mainScope); + keys.forEach(key => addIfFound(scopes, mainScope, key)); + } + keys.forEach(key => addIfFound(scopes, options, key)); + keys.forEach(key => addIfFound(scopes, overrides[type] || {}, key)); + keys.forEach(key => addIfFound(scopes, defaults, key)); + keys.forEach(key => addIfFound(scopes, descriptors, key)); + }); + const array = Array.from(scopes); + if (keysCached.has(keyLists)) { + cache.set(keyLists, array); + } + return array; + } + chartOptionScopes() { + const {options, type} = this; + return [ + options, + overrides[type] || {}, + defaults.datasets[type] || {}, + {type}, + defaults, + descriptors + ]; + } + resolveNamedOptions(scopes, names, context, prefixes = ['']) { + const result = {$shared: true}; + const {resolver, subPrefixes} = getResolver(this._resolverCache, scopes, prefixes); + let options = resolver; + if (needContext(resolver, names)) { + result.$shared = false; + context = isFunction(context) ? context() : context; + const subResolver = this.createResolver(scopes, context, subPrefixes); + options = _attachContext(resolver, context, subResolver); + } + for (const prop of names) { + result[prop] = options[prop]; + } + return result; + } + createResolver(scopes, context, prefixes = [''], descriptorDefaults) { + const {resolver} = getResolver(this._resolverCache, scopes, prefixes); + return isObject(context) + ? _attachContext(resolver, context, undefined, descriptorDefaults) + : resolver; + } +} +function getResolver(resolverCache, scopes, prefixes) { + let cache = resolverCache.get(scopes); + if (!cache) { + cache = new Map(); + resolverCache.set(scopes, cache); + } + const cacheKey = prefixes.join(); + let cached = cache.get(cacheKey); + if (!cached) { + const resolver = _createResolver(scopes, prefixes); + cached = { + resolver, + subPrefixes: prefixes.filter(p => !p.toLowerCase().includes('hover')) + }; + cache.set(cacheKey, cached); + } + return cached; +} +function needContext(proxy, names) { + const {isScriptable, isIndexable} = _descriptors(proxy); + for (const prop of names) { + if ((isScriptable(prop) && isFunction(proxy[prop])) + || (isIndexable(prop) && isArray(proxy[prop]))) { + return true; + } + } + return false; +} + +var version = "3.4.1"; + +const KNOWN_POSITIONS = ['top', 'bottom', 'left', 'right', 'chartArea']; +function positionIsHorizontal(position, axis) { + return position === 'top' || position === 'bottom' || (KNOWN_POSITIONS.indexOf(position) === -1 && axis === 'x'); +} +function compare2Level(l1, l2) { + return function(a, b) { + return a[l1] === b[l1] + ? a[l2] - b[l2] + : a[l1] - b[l1]; + }; +} +function onAnimationsComplete(context) { + const chart = context.chart; + const animationOptions = chart.options.animation; + chart.notifyPlugins('afterRender'); + callback(animationOptions && animationOptions.onComplete, [context], chart); +} +function onAnimationProgress(context) { + const chart = context.chart; + const animationOptions = chart.options.animation; + callback(animationOptions && animationOptions.onProgress, [context], chart); +} +function isDomSupported() { + return typeof window !== 'undefined' && typeof document !== 'undefined'; +} +function getCanvas(item) { + if (isDomSupported() && typeof item === 'string') { + item = document.getElementById(item); + } else if (item && item.length) { + item = item[0]; + } + if (item && item.canvas) { + item = item.canvas; + } + return item; +} +const instances = {}; +const getChart = (key) => { + const canvas = getCanvas(key); + return Object.values(instances).filter((c) => c.canvas === canvas).pop(); +}; +class Chart { + constructor(item, config) { + const me = this; + this.config = config = new Config(config); + const initialCanvas = getCanvas(item); + const existingChart = getChart(initialCanvas); + if (existingChart) { + throw new Error( + 'Canvas is already in use. Chart with ID \'' + existingChart.id + '\'' + + ' must be destroyed before the canvas can be reused.' + ); + } + const options = config.createResolver(config.chartOptionScopes(), me.getContext()); + this.platform = me._initializePlatform(initialCanvas, config); + const context = me.platform.acquireContext(initialCanvas, options.aspectRatio); + const canvas = context && context.canvas; + const height = canvas && canvas.height; + const width = canvas && canvas.width; + this.id = uid(); + this.ctx = context; + this.canvas = canvas; + this.width = width; + this.height = height; + this._options = options; + this._aspectRatio = this.aspectRatio; + this._layers = []; + this._metasets = []; + this._stacks = undefined; + this.boxes = []; + this.currentDevicePixelRatio = undefined; + this.chartArea = undefined; + this._active = []; + this._lastEvent = undefined; + this._listeners = {}; + this._responsiveListeners = undefined; + this._sortedMetasets = []; + this.scales = {}; + this.scale = undefined; + this._plugins = new PluginService(); + this.$proxies = {}; + this._hiddenIndices = {}; + this.attached = false; + this._animationsDisabled = undefined; + this.$context = undefined; + this._doResize = debounce(() => this.update('resize'), options.resizeDelay || 0); + instances[me.id] = me; + if (!context || !canvas) { + console.error("Failed to create chart: can't acquire context from the given item"); + return; + } + animator.listen(me, 'complete', onAnimationsComplete); + animator.listen(me, 'progress', onAnimationProgress); + me._initialize(); + if (me.attached) { + me.update(); + } + } + get aspectRatio() { + const {options: {aspectRatio, maintainAspectRatio}, width, height, _aspectRatio} = this; + if (!isNullOrUndef(aspectRatio)) { + return aspectRatio; + } + if (maintainAspectRatio && _aspectRatio) { + return _aspectRatio; + } + return height ? width / height : null; + } + get data() { + return this.config.data; + } + set data(data) { + this.config.data = data; + } + get options() { + return this._options; + } + set options(options) { + this.config.options = options; + } + _initialize() { + const me = this; + me.notifyPlugins('beforeInit'); + if (me.options.responsive) { + me.resize(); + } else { + retinaScale(me, me.options.devicePixelRatio); + } + me.bindEvents(); + me.notifyPlugins('afterInit'); + return me; + } + _initializePlatform(canvas, config) { + if (config.platform) { + return new config.platform(); + } else if (!isDomSupported() || (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas)) { + return new BasicPlatform(); + } + return new DomPlatform(); + } + clear() { + clearCanvas(this.canvas, this.ctx); + return this; + } + stop() { + animator.stop(this); + return this; + } + resize(width, height) { + if (!animator.running(this)) { + this._resize(width, height); + } else { + this._resizeBeforeDraw = {width, height}; + } + } + _resize(width, height) { + const me = this; + const options = me.options; + const canvas = me.canvas; + const aspectRatio = options.maintainAspectRatio && me.aspectRatio; + const newSize = me.platform.getMaximumSize(canvas, width, height, aspectRatio); + const newRatio = options.devicePixelRatio || me.platform.getDevicePixelRatio(); + me.width = newSize.width; + me.height = newSize.height; + me._aspectRatio = me.aspectRatio; + if (!retinaScale(me, newRatio, true)) { + return; + } + me.notifyPlugins('resize', {size: newSize}); + callback(options.onResize, [me, newSize], me); + if (me.attached) { + if (me._doResize()) { + me.render(); + } + } + } + ensureScalesHaveIDs() { + const options = this.options; + const scalesOptions = options.scales || {}; + each(scalesOptions, (axisOptions, axisID) => { + axisOptions.id = axisID; + }); + } + buildOrUpdateScales() { + const me = this; + const options = me.options; + const scaleOpts = options.scales; + const scales = me.scales; + const updated = Object.keys(scales).reduce((obj, id) => { + obj[id] = false; + return obj; + }, {}); + let items = []; + if (scaleOpts) { + items = items.concat( + Object.keys(scaleOpts).map((id) => { + const scaleOptions = scaleOpts[id]; + const axis = determineAxis(id, scaleOptions); + const isRadial = axis === 'r'; + const isHorizontal = axis === 'x'; + return { + options: scaleOptions, + dposition: isRadial ? 'chartArea' : isHorizontal ? 'bottom' : 'left', + dtype: isRadial ? 'radialLinear' : isHorizontal ? 'category' : 'linear' + }; + }) + ); + } + each(items, (item) => { + const scaleOptions = item.options; + const id = scaleOptions.id; + const axis = determineAxis(id, scaleOptions); + const scaleType = valueOrDefault(scaleOptions.type, item.dtype); + if (scaleOptions.position === undefined || positionIsHorizontal(scaleOptions.position, axis) !== positionIsHorizontal(item.dposition)) { + scaleOptions.position = item.dposition; + } + updated[id] = true; + let scale = null; + if (id in scales && scales[id].type === scaleType) { + scale = scales[id]; + } else { + const scaleClass = registry.getScale(scaleType); + scale = new scaleClass({ + id, + type: scaleType, + ctx: me.ctx, + chart: me + }); + scales[scale.id] = scale; + } + scale.init(scaleOptions, options); + }); + each(updated, (hasUpdated, id) => { + if (!hasUpdated) { + delete scales[id]; + } + }); + each(scales, (scale) => { + layouts.configure(me, scale, scale.options); + layouts.addBox(me, scale); + }); + } + _updateMetasets() { + const me = this; + const metasets = me._metasets; + const numData = me.data.datasets.length; + const numMeta = metasets.length; + metasets.sort((a, b) => a.index - b.index); + if (numMeta > numData) { + for (let i = numData; i < numMeta; ++i) { + me._destroyDatasetMeta(i); + } + metasets.splice(numData, numMeta - numData); + } + me._sortedMetasets = metasets.slice(0).sort(compare2Level('order', 'index')); + } + _removeUnreferencedMetasets() { + const me = this; + const {_metasets: metasets, data: {datasets}} = me; + if (metasets.length > datasets.length) { + delete me._stacks; + } + metasets.forEach((meta, index) => { + if (datasets.filter(x => x === meta._dataset).length === 0) { + me._destroyDatasetMeta(index); + } + }); + } + buildOrUpdateControllers() { + const me = this; + const newControllers = []; + const datasets = me.data.datasets; + let i, ilen; + me._removeUnreferencedMetasets(); + for (i = 0, ilen = datasets.length; i < ilen; i++) { + const dataset = datasets[i]; + let meta = me.getDatasetMeta(i); + const type = dataset.type || me.config.type; + if (meta.type && meta.type !== type) { + me._destroyDatasetMeta(i); + meta = me.getDatasetMeta(i); + } + meta.type = type; + meta.indexAxis = dataset.indexAxis || getIndexAxis(type, me.options); + meta.order = dataset.order || 0; + meta.index = i; + meta.label = '' + dataset.label; + meta.visible = me.isDatasetVisible(i); + if (meta.controller) { + meta.controller.updateIndex(i); + meta.controller.linkScales(); + } else { + const ControllerClass = registry.getController(type); + const {datasetElementType, dataElementType} = defaults.datasets[type]; + Object.assign(ControllerClass.prototype, { + dataElementType: registry.getElement(dataElementType), + datasetElementType: datasetElementType && registry.getElement(datasetElementType) + }); + meta.controller = new ControllerClass(me, i); + newControllers.push(meta.controller); + } + } + me._updateMetasets(); + return newControllers; + } + _resetElements() { + const me = this; + each(me.data.datasets, (dataset, datasetIndex) => { + me.getDatasetMeta(datasetIndex).controller.reset(); + }, me); + } + reset() { + this._resetElements(); + this.notifyPlugins('reset'); + } + update(mode) { + const me = this; + const config = me.config; + config.update(); + me._options = config.createResolver(config.chartOptionScopes(), me.getContext()); + each(me.scales, (scale) => { + layouts.removeBox(me, scale); + }); + const animsDisabled = me._animationsDisabled = !me.options.animation; + me.ensureScalesHaveIDs(); + me.buildOrUpdateScales(); + const existingEvents = new Set(Object.keys(me._listeners)); + const newEvents = new Set(me.options.events); + if (!setsEqual(existingEvents, newEvents) || !!this._responsiveListeners !== me.options.responsive) { + me.unbindEvents(); + me.bindEvents(); + } + me._plugins.invalidate(); + if (me.notifyPlugins('beforeUpdate', {mode, cancelable: true}) === false) { + return; + } + const newControllers = me.buildOrUpdateControllers(); + me.notifyPlugins('beforeElementsUpdate'); + let minPadding = 0; + for (let i = 0, ilen = me.data.datasets.length; i < ilen; i++) { + const {controller} = me.getDatasetMeta(i); + const reset = !animsDisabled && newControllers.indexOf(controller) === -1; + controller.buildOrUpdateElements(reset); + minPadding = Math.max(+controller.getMaxOverflow(), minPadding); + } + me._minPadding = minPadding; + me._updateLayout(minPadding); + if (!animsDisabled) { + each(newControllers, (controller) => { + controller.reset(); + }); + } + me._updateDatasets(mode); + me.notifyPlugins('afterUpdate', {mode}); + me._layers.sort(compare2Level('z', '_idx')); + if (me._lastEvent) { + me._eventHandler(me._lastEvent, true); + } + me.render(); + } + _updateLayout(minPadding) { + const me = this; + if (me.notifyPlugins('beforeLayout', {cancelable: true}) === false) { + return; + } + layouts.update(me, me.width, me.height, minPadding); + const area = me.chartArea; + const noArea = area.width <= 0 || area.height <= 0; + me._layers = []; + each(me.boxes, (box) => { + if (noArea && box.position === 'chartArea') { + return; + } + if (box.configure) { + box.configure(); + } + me._layers.push(...box._layers()); + }, me); + me._layers.forEach((item, index) => { + item._idx = index; + }); + me.notifyPlugins('afterLayout'); + } + _updateDatasets(mode) { + const me = this; + const isFunction = typeof mode === 'function'; + if (me.notifyPlugins('beforeDatasetsUpdate', {mode, cancelable: true}) === false) { + return; + } + for (let i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + me._updateDataset(i, isFunction ? mode({datasetIndex: i}) : mode); + } + me.notifyPlugins('afterDatasetsUpdate', {mode}); + } + _updateDataset(index, mode) { + const me = this; + const meta = me.getDatasetMeta(index); + const args = {meta, index, mode, cancelable: true}; + if (me.notifyPlugins('beforeDatasetUpdate', args) === false) { + return; + } + meta.controller._update(mode); + args.cancelable = false; + me.notifyPlugins('afterDatasetUpdate', args); + } + render() { + const me = this; + if (me.notifyPlugins('beforeRender', {cancelable: true}) === false) { + return; + } + if (animator.has(me)) { + if (me.attached && !animator.running(me)) { + animator.start(me); + } + } else { + me.draw(); + onAnimationsComplete({chart: me}); + } + } + draw() { + const me = this; + let i; + if (me._resizeBeforeDraw) { + const {width, height} = me._resizeBeforeDraw; + me._resize(width, height); + me._resizeBeforeDraw = null; + } + me.clear(); + if (me.width <= 0 || me.height <= 0) { + return; + } + if (me.notifyPlugins('beforeDraw', {cancelable: true}) === false) { + return; + } + const layers = me._layers; + for (i = 0; i < layers.length && layers[i].z <= 0; ++i) { + layers[i].draw(me.chartArea); + } + me._drawDatasets(); + for (; i < layers.length; ++i) { + layers[i].draw(me.chartArea); + } + me.notifyPlugins('afterDraw'); + } + _getSortedDatasetMetas(filterVisible) { + const me = this; + const metasets = me._sortedMetasets; + const result = []; + let i, ilen; + for (i = 0, ilen = metasets.length; i < ilen; ++i) { + const meta = metasets[i]; + if (!filterVisible || meta.visible) { + result.push(meta); + } + } + return result; + } + getSortedVisibleDatasetMetas() { + return this._getSortedDatasetMetas(true); + } + _drawDatasets() { + const me = this; + if (me.notifyPlugins('beforeDatasetsDraw', {cancelable: true}) === false) { + return; + } + const metasets = me.getSortedVisibleDatasetMetas(); + for (let i = metasets.length - 1; i >= 0; --i) { + me._drawDataset(metasets[i]); + } + me.notifyPlugins('afterDatasetsDraw'); + } + _drawDataset(meta) { + const me = this; + const ctx = me.ctx; + const clip = meta._clip; + const useClip = !clip.disabled; + const area = me.chartArea; + const args = { + meta, + index: meta.index, + cancelable: true + }; + if (me.notifyPlugins('beforeDatasetDraw', args) === false) { + return; + } + if (useClip) { + clipArea(ctx, { + left: clip.left === false ? 0 : area.left - clip.left, + right: clip.right === false ? me.width : area.right + clip.right, + top: clip.top === false ? 0 : area.top - clip.top, + bottom: clip.bottom === false ? me.height : area.bottom + clip.bottom + }); + } + meta.controller.draw(); + if (useClip) { + unclipArea(ctx); + } + args.cancelable = false; + me.notifyPlugins('afterDatasetDraw', args); + } + getElementsAtEventForMode(e, mode, options, useFinalPosition) { + const method = Interaction.modes[mode]; + if (typeof method === 'function') { + return method(this, e, options, useFinalPosition); + } + return []; + } + getDatasetMeta(datasetIndex) { + const me = this; + const dataset = me.data.datasets[datasetIndex]; + const metasets = me._metasets; + let meta = metasets.filter(x => x && x._dataset === dataset).pop(); + if (!meta) { + meta = { + type: null, + data: [], + dataset: null, + controller: null, + hidden: null, + xAxisID: null, + yAxisID: null, + order: dataset && dataset.order || 0, + index: datasetIndex, + _dataset: dataset, + _parsed: [], + _sorted: false + }; + metasets.push(meta); + } + return meta; + } + getContext() { + return this.$context || (this.$context = {chart: this, type: 'chart'}); + } + getVisibleDatasetCount() { + return this.getSortedVisibleDatasetMetas().length; + } + isDatasetVisible(datasetIndex) { + const dataset = this.data.datasets[datasetIndex]; + if (!dataset) { + return false; + } + const meta = this.getDatasetMeta(datasetIndex); + return typeof meta.hidden === 'boolean' ? !meta.hidden : !dataset.hidden; + } + setDatasetVisibility(datasetIndex, visible) { + const meta = this.getDatasetMeta(datasetIndex); + meta.hidden = !visible; + } + toggleDataVisibility(index) { + this._hiddenIndices[index] = !this._hiddenIndices[index]; + } + getDataVisibility(index) { + return !this._hiddenIndices[index]; + } + _updateDatasetVisibility(datasetIndex, visible) { + const me = this; + const mode = visible ? 'show' : 'hide'; + const meta = me.getDatasetMeta(datasetIndex); + const anims = meta.controller._resolveAnimations(undefined, mode); + me.setDatasetVisibility(datasetIndex, visible); + anims.update(meta, {visible}); + me.update((ctx) => ctx.datasetIndex === datasetIndex ? mode : undefined); + } + hide(datasetIndex) { + this._updateDatasetVisibility(datasetIndex, false); + } + show(datasetIndex) { + this._updateDatasetVisibility(datasetIndex, true); + } + _destroyDatasetMeta(datasetIndex) { + const me = this; + const meta = me._metasets && me._metasets[datasetIndex]; + if (meta && meta.controller) { + meta.controller._destroy(); + delete me._metasets[datasetIndex]; + } + } + destroy() { + const me = this; + const {canvas, ctx} = me; + let i, ilen; + me.stop(); + animator.remove(me); + for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + me._destroyDatasetMeta(i); + } + me.config.clearCache(); + if (canvas) { + me.unbindEvents(); + clearCanvas(canvas, ctx); + me.platform.releaseContext(ctx); + me.canvas = null; + me.ctx = null; + } + me.notifyPlugins('destroy'); + delete instances[me.id]; + } + toBase64Image(...args) { + return this.canvas.toDataURL(...args); + } + bindEvents() { + this.bindUserEvents(); + if (this.options.responsive) { + this.bindResponsiveEvents(); + } else { + this.attached = true; + } + } + bindUserEvents() { + const me = this; + const listeners = me._listeners; + const platform = me.platform; + const _add = (type, listener) => { + platform.addEventListener(me, type, listener); + listeners[type] = listener; + }; + const listener = function(e, x, y) { + e.offsetX = x; + e.offsetY = y; + me._eventHandler(e); + }; + each(me.options.events, (type) => _add(type, listener)); + } + bindResponsiveEvents() { + const me = this; + if (!me._responsiveListeners) { + me._responsiveListeners = {}; + } + const listeners = me._responsiveListeners; + const platform = me.platform; + const _add = (type, listener) => { + platform.addEventListener(me, type, listener); + listeners[type] = listener; + }; + const _remove = (type, listener) => { + if (listeners[type]) { + platform.removeEventListener(me, type, listener); + delete listeners[type]; + } + }; + const listener = (width, height) => { + if (me.canvas) { + me.resize(width, height); + } + }; + let detached; + const attached = () => { + _remove('attach', attached); + me.attached = true; + me.resize(); + _add('resize', listener); + _add('detach', detached); + }; + detached = () => { + me.attached = false; + _remove('resize', listener); + _add('attach', attached); + }; + if (platform.isAttached(me.canvas)) { + attached(); + } else { + detached(); + } + } + unbindEvents() { + const me = this; + each(me._listeners, (listener, type) => { + me.platform.removeEventListener(me, type, listener); + }); + me._listeners = {}; + each(me._responsiveListeners, (listener, type) => { + me.platform.removeEventListener(me, type, listener); + }); + me._responsiveListeners = undefined; + } + updateHoverStyle(items, mode, enabled) { + const prefix = enabled ? 'set' : 'remove'; + let meta, item, i, ilen; + if (mode === 'dataset') { + meta = this.getDatasetMeta(items[0].datasetIndex); + meta.controller['_' + prefix + 'DatasetHoverStyle'](); + } + for (i = 0, ilen = items.length; i < ilen; ++i) { + item = items[i]; + const controller = item && this.getDatasetMeta(item.datasetIndex).controller; + if (controller) { + controller[prefix + 'HoverStyle'](item.element, item.datasetIndex, item.index); + } + } + } + getActiveElements() { + return this._active || []; + } + setActiveElements(activeElements) { + const me = this; + const lastActive = me._active || []; + const active = activeElements.map(({datasetIndex, index}) => { + const meta = me.getDatasetMeta(datasetIndex); + if (!meta) { + throw new Error('No dataset found at index ' + datasetIndex); + } + return { + datasetIndex, + element: meta.data[index], + index, + }; + }); + const changed = !_elementsEqual(active, lastActive); + if (changed) { + me._active = active; + me._updateHoverStyles(active, lastActive); + } + } + notifyPlugins(hook, args, filter) { + return this._plugins.notify(this, hook, args, filter); + } + _updateHoverStyles(active, lastActive, replay) { + const me = this; + const hoverOptions = me.options.hover; + const diff = (a, b) => a.filter(x => !b.some(y => x.datasetIndex === y.datasetIndex && x.index === y.index)); + const deactivated = diff(lastActive, active); + const activated = replay ? active : diff(active, lastActive); + if (deactivated.length) { + me.updateHoverStyle(deactivated, hoverOptions.mode, false); + } + if (activated.length && hoverOptions.mode) { + me.updateHoverStyle(activated, hoverOptions.mode, true); + } + } + _eventHandler(e, replay) { + const me = this; + const args = {event: e, replay, cancelable: true}; + const eventFilter = (plugin) => (plugin.options.events || this.options.events).includes(e.type); + if (me.notifyPlugins('beforeEvent', args, eventFilter) === false) { + return; + } + const changed = me._handleEvent(e, replay); + args.cancelable = false; + me.notifyPlugins('afterEvent', args, eventFilter); + if (changed || args.changed) { + me.render(); + } + return me; + } + _handleEvent(e, replay) { + const me = this; + const {_active: lastActive = [], options} = me; + const hoverOptions = options.hover; + const useFinalPosition = replay; + let active = []; + let changed = false; + let lastEvent = null; + if (e.type !== 'mouseout') { + active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions, useFinalPosition); + lastEvent = e.type === 'click' ? me._lastEvent : e; + } + me._lastEvent = null; + if (_isPointInArea(e, me.chartArea, me._minPadding)) { + callback(options.onHover, [e, active, me], me); + if (e.type === 'mouseup' || e.type === 'click' || e.type === 'contextmenu') { + callback(options.onClick, [e, active, me], me); + } + } + changed = !_elementsEqual(active, lastActive); + if (changed || replay) { + me._active = active; + me._updateHoverStyles(active, lastActive, replay); + } + me._lastEvent = lastEvent; + return changed; + } +} +const invalidatePlugins = () => each(Chart.instances, (chart) => chart._plugins.invalidate()); +const enumerable = true; +Object.defineProperties(Chart, { + defaults: { + enumerable, + value: defaults + }, + instances: { + enumerable, + value: instances + }, + overrides: { + enumerable, + value: overrides + }, + registry: { + enumerable, + value: registry + }, + version: { + enumerable, + value: version + }, + getChart: { + enumerable, + value: getChart + }, + register: { + enumerable, + value: (...items) => { + registry.add(...items); + invalidatePlugins(); + } + }, + unregister: { + enumerable, + value: (...items) => { + registry.remove(...items); + invalidatePlugins(); + } + } +}); + +function clipArc(ctx, element, endAngle) { + const {startAngle, pixelMargin, x, y, outerRadius, innerRadius} = element; + let angleMargin = pixelMargin / outerRadius; + ctx.beginPath(); + ctx.arc(x, y, outerRadius, startAngle - angleMargin, endAngle + angleMargin); + if (innerRadius > pixelMargin) { + angleMargin = pixelMargin / innerRadius; + ctx.arc(x, y, innerRadius, endAngle + angleMargin, startAngle - angleMargin, true); + } else { + ctx.arc(x, y, pixelMargin, endAngle + HALF_PI, startAngle - HALF_PI); + } + ctx.closePath(); + ctx.clip(); +} +function toRadiusCorners(value) { + return _readValueToProps(value, ['outerStart', 'outerEnd', 'innerStart', 'innerEnd']); +} +function parseBorderRadius$1(arc, innerRadius, outerRadius, angleDelta) { + const o = toRadiusCorners(arc.options.borderRadius); + const halfThickness = (outerRadius - innerRadius) / 2; + const innerLimit = Math.min(halfThickness, angleDelta * innerRadius / 2); + const computeOuterLimit = (val) => { + const outerArcLimit = (outerRadius - Math.min(halfThickness, val)) * angleDelta / 2; + return _limitValue(val, 0, Math.min(halfThickness, outerArcLimit)); + }; + return { + outerStart: computeOuterLimit(o.outerStart), + outerEnd: computeOuterLimit(o.outerEnd), + innerStart: _limitValue(o.innerStart, 0, innerLimit), + innerEnd: _limitValue(o.innerEnd, 0, innerLimit), + }; +} +function rThetaToXY(r, theta, x, y) { + return { + x: x + r * Math.cos(theta), + y: y + r * Math.sin(theta), + }; +} +function pathArc(ctx, element, offset, spacing, end) { + const {x, y, startAngle: start, pixelMargin, innerRadius: innerR} = element; + const outerRadius = Math.max(element.outerRadius + spacing + offset - pixelMargin, 0); + const innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0; + let spacingOffset = 0; + const alpha = end - start; + if (spacing) { + const noSpacingInnerRadius = innerR > 0 ? innerR - spacing : 0; + const noSpacingOuterRadius = outerRadius > 0 ? outerRadius - spacing : 0; + const avNogSpacingRadius = (noSpacingInnerRadius + noSpacingOuterRadius) / 2; + const adjustedAngle = avNogSpacingRadius !== 0 ? (alpha * avNogSpacingRadius) / (avNogSpacingRadius + spacing) : alpha; + spacingOffset = (alpha - adjustedAngle) / 2; + } + const beta = Math.max(0.001, alpha * outerRadius - offset / PI) / outerRadius; + const angleOffset = (alpha - beta) / 2; + const startAngle = start + angleOffset + spacingOffset; + const endAngle = end - angleOffset - spacingOffset; + const {outerStart, outerEnd, innerStart, innerEnd} = parseBorderRadius$1(element, innerRadius, outerRadius, endAngle - startAngle); + const outerStartAdjustedRadius = outerRadius - outerStart; + const outerEndAdjustedRadius = outerRadius - outerEnd; + const outerStartAdjustedAngle = startAngle + outerStart / outerStartAdjustedRadius; + const outerEndAdjustedAngle = endAngle - outerEnd / outerEndAdjustedRadius; + const innerStartAdjustedRadius = innerRadius + innerStart; + const innerEndAdjustedRadius = innerRadius + innerEnd; + const innerStartAdjustedAngle = startAngle + innerStart / innerStartAdjustedRadius; + const innerEndAdjustedAngle = endAngle - innerEnd / innerEndAdjustedRadius; + ctx.beginPath(); + ctx.arc(x, y, outerRadius, outerStartAdjustedAngle, outerEndAdjustedAngle); + if (outerEnd > 0) { + const pCenter = rThetaToXY(outerEndAdjustedRadius, outerEndAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, outerEnd, outerEndAdjustedAngle, endAngle + HALF_PI); + } + const p4 = rThetaToXY(innerEndAdjustedRadius, endAngle, x, y); + ctx.lineTo(p4.x, p4.y); + if (innerEnd > 0) { + const pCenter = rThetaToXY(innerEndAdjustedRadius, innerEndAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, innerEnd, endAngle + HALF_PI, innerEndAdjustedAngle + Math.PI); + } + ctx.arc(x, y, innerRadius, endAngle - (innerEnd / innerRadius), startAngle + (innerStart / innerRadius), true); + if (innerStart > 0) { + const pCenter = rThetaToXY(innerStartAdjustedRadius, innerStartAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, innerStart, innerStartAdjustedAngle + Math.PI, startAngle - HALF_PI); + } + const p8 = rThetaToXY(outerStartAdjustedRadius, startAngle, x, y); + ctx.lineTo(p8.x, p8.y); + if (outerStart > 0) { + const pCenter = rThetaToXY(outerStartAdjustedRadius, outerStartAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, outerStart, startAngle - HALF_PI, outerStartAdjustedAngle); + } + ctx.closePath(); +} +function drawArc(ctx, element, offset, spacing) { + const {fullCircles, startAngle, circumference} = element; + let endAngle = element.endAngle; + if (fullCircles) { + pathArc(ctx, element, offset, spacing, startAngle + TAU); + for (let i = 0; i < fullCircles; ++i) { + ctx.fill(); + } + if (!isNaN(circumference)) { + endAngle = startAngle + circumference % TAU; + if (circumference % TAU === 0) { + endAngle += TAU; + } + } + } + pathArc(ctx, element, offset, spacing, endAngle); + ctx.fill(); + return endAngle; +} +function drawFullCircleBorders(ctx, element, inner) { + const {x, y, startAngle, pixelMargin, fullCircles} = element; + const outerRadius = Math.max(element.outerRadius - pixelMargin, 0); + const innerRadius = element.innerRadius + pixelMargin; + let i; + if (inner) { + clipArc(ctx, element, startAngle + TAU); + } + ctx.beginPath(); + ctx.arc(x, y, innerRadius, startAngle + TAU, startAngle, true); + for (i = 0; i < fullCircles; ++i) { + ctx.stroke(); + } + ctx.beginPath(); + ctx.arc(x, y, outerRadius, startAngle, startAngle + TAU); + for (i = 0; i < fullCircles; ++i) { + ctx.stroke(); + } +} +function drawBorder(ctx, element, offset, spacing, endAngle) { + const {options} = element; + const inner = options.borderAlign === 'inner'; + if (!options.borderWidth) { + return; + } + if (inner) { + ctx.lineWidth = options.borderWidth * 2; + ctx.lineJoin = 'round'; + } else { + ctx.lineWidth = options.borderWidth; + ctx.lineJoin = 'bevel'; + } + if (element.fullCircles) { + drawFullCircleBorders(ctx, element, inner); + } + if (inner) { + clipArc(ctx, element, endAngle); + } + pathArc(ctx, element, offset, spacing, endAngle); + ctx.stroke(); +} +class ArcElement extends Element { + constructor(cfg) { + super(); + this.options = undefined; + this.circumference = undefined; + this.startAngle = undefined; + this.endAngle = undefined; + this.innerRadius = undefined; + this.outerRadius = undefined; + this.pixelMargin = 0; + this.fullCircles = 0; + if (cfg) { + Object.assign(this, cfg); + } + } + inRange(chartX, chartY, useFinalPosition) { + const point = this.getProps(['x', 'y'], useFinalPosition); + const {angle, distance} = getAngleFromPoint(point, {x: chartX, y: chartY}); + const {startAngle, endAngle, innerRadius, outerRadius, circumference} = this.getProps([ + 'startAngle', + 'endAngle', + 'innerRadius', + 'outerRadius', + 'circumference' + ], useFinalPosition); + const rAdjust = this.options.spacing / 2; + const betweenAngles = circumference >= TAU || _angleBetween(angle, startAngle, endAngle); + const withinRadius = (distance >= innerRadius + rAdjust && distance <= outerRadius + rAdjust); + return (betweenAngles && withinRadius); + } + getCenterPoint(useFinalPosition) { + const {x, y, startAngle, endAngle, innerRadius, outerRadius} = this.getProps([ + 'x', + 'y', + 'startAngle', + 'endAngle', + 'innerRadius', + 'outerRadius', + 'circumference', + ], useFinalPosition); + const {offset, spacing} = this.options; + const halfAngle = (startAngle + endAngle) / 2; + const halfRadius = (innerRadius + outerRadius + spacing + offset) / 2; + return { + x: x + Math.cos(halfAngle) * halfRadius, + y: y + Math.sin(halfAngle) * halfRadius + }; + } + tooltipPosition(useFinalPosition) { + return this.getCenterPoint(useFinalPosition); + } + draw(ctx) { + const me = this; + const {options, circumference} = me; + const offset = (options.offset || 0) / 2; + const spacing = (options.spacing || 0) / 2; + me.pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0; + me.fullCircles = circumference > TAU ? Math.floor(circumference / TAU) : 0; + if (circumference === 0 || me.innerRadius < 0 || me.outerRadius < 0) { + return; + } + ctx.save(); + let radiusOffset = 0; + if (offset) { + radiusOffset = offset / 2; + const halfAngle = (me.startAngle + me.endAngle) / 2; + ctx.translate(Math.cos(halfAngle) * radiusOffset, Math.sin(halfAngle) * radiusOffset); + if (me.circumference >= PI) { + radiusOffset = offset; + } + } + ctx.fillStyle = options.backgroundColor; + ctx.strokeStyle = options.borderColor; + const endAngle = drawArc(ctx, me, radiusOffset, spacing); + drawBorder(ctx, me, radiusOffset, spacing, endAngle); + ctx.restore(); + } +} +ArcElement.id = 'arc'; +ArcElement.defaults = { + borderAlign: 'center', + borderColor: '#fff', + borderRadius: 0, + borderWidth: 2, + offset: 0, + spacing: 0, + angle: undefined, +}; +ArcElement.defaultRoutes = { + backgroundColor: 'backgroundColor' +}; + +function setStyle(ctx, options, style = options) { + ctx.lineCap = valueOrDefault(style.borderCapStyle, options.borderCapStyle); + ctx.setLineDash(valueOrDefault(style.borderDash, options.borderDash)); + ctx.lineDashOffset = valueOrDefault(style.borderDashOffset, options.borderDashOffset); + ctx.lineJoin = valueOrDefault(style.borderJoinStyle, options.borderJoinStyle); + ctx.lineWidth = valueOrDefault(style.borderWidth, options.borderWidth); + ctx.strokeStyle = valueOrDefault(style.borderColor, options.borderColor); +} +function lineTo(ctx, previous, target) { + ctx.lineTo(target.x, target.y); +} +function getLineMethod(options) { + if (options.stepped) { + return _steppedLineTo; + } + if (options.tension || options.cubicInterpolationMode === 'monotone') { + return _bezierCurveTo; + } + return lineTo; +} +function pathVars(points, segment, params = {}) { + const count = points.length; + const {start: paramsStart = 0, end: paramsEnd = count - 1} = params; + const {start: segmentStart, end: segmentEnd} = segment; + const start = Math.max(paramsStart, segmentStart); + const end = Math.min(paramsEnd, segmentEnd); + const outside = paramsStart < segmentStart && paramsEnd < segmentStart || paramsStart > segmentEnd && paramsEnd > segmentEnd; + return { + count, + start, + loop: segment.loop, + ilen: end < start && !outside ? count + end - start : end - start + }; +} +function pathSegment(ctx, line, segment, params) { + const {points, options} = line; + const {count, start, loop, ilen} = pathVars(points, segment, params); + const lineMethod = getLineMethod(options); + let {move = true, reverse} = params || {}; + let i, point, prev; + for (i = 0; i <= ilen; ++i) { + point = points[(start + (reverse ? ilen - i : i)) % count]; + if (point.skip) { + continue; + } else if (move) { + ctx.moveTo(point.x, point.y); + move = false; + } else { + lineMethod(ctx, prev, point, reverse, options.stepped); + } + prev = point; + } + if (loop) { + point = points[(start + (reverse ? ilen : 0)) % count]; + lineMethod(ctx, prev, point, reverse, options.stepped); + } + return !!loop; +} +function fastPathSegment(ctx, line, segment, params) { + const points = line.points; + const {count, start, ilen} = pathVars(points, segment, params); + const {move = true, reverse} = params || {}; + let avgX = 0; + let countX = 0; + let i, point, prevX, minY, maxY, lastY; + const pointIndex = (index) => (start + (reverse ? ilen - index : index)) % count; + const drawX = () => { + if (minY !== maxY) { + ctx.lineTo(avgX, maxY); + ctx.lineTo(avgX, minY); + ctx.lineTo(avgX, lastY); + } + }; + if (move) { + point = points[pointIndex(0)]; + ctx.moveTo(point.x, point.y); + } + for (i = 0; i <= ilen; ++i) { + point = points[pointIndex(i)]; + if (point.skip) { + continue; + } + const x = point.x; + const y = point.y; + const truncX = x | 0; + if (truncX === prevX) { + if (y < minY) { + minY = y; + } else if (y > maxY) { + maxY = y; + } + avgX = (countX * avgX + x) / ++countX; + } else { + drawX(); + ctx.lineTo(x, y); + prevX = truncX; + countX = 0; + minY = maxY = y; + } + lastY = y; + } + drawX(); +} +function _getSegmentMethod(line) { + const opts = line.options; + const borderDash = opts.borderDash && opts.borderDash.length; + const useFastPath = !line._decimated && !line._loop && !opts.tension && opts.cubicInterpolationMode !== 'monotone' && !opts.stepped && !borderDash; + return useFastPath ? fastPathSegment : pathSegment; +} +function _getInterpolationMethod(options) { + if (options.stepped) { + return _steppedInterpolation; + } + if (options.tension || options.cubicInterpolationMode === 'monotone') { + return _bezierInterpolation; + } + return _pointInLine; +} +function strokePathWithCache(ctx, line, start, count) { + let path = line._path; + if (!path) { + path = line._path = new Path2D(); + if (line.path(path, start, count)) { + path.closePath(); + } + } + setStyle(ctx, line.options); + ctx.stroke(path); +} +function strokePathDirect(ctx, line, start, count) { + const {segments, options} = line; + const segmentMethod = _getSegmentMethod(line); + for (const segment of segments) { + setStyle(ctx, options, segment.style); + ctx.beginPath(); + if (segmentMethod(ctx, line, segment, {start, end: start + count - 1})) { + ctx.closePath(); + } + ctx.stroke(); + } +} +const usePath2D = typeof Path2D === 'function'; +function draw(ctx, line, start, count) { + if (usePath2D && line.segments.length === 1) { + strokePathWithCache(ctx, line, start, count); + } else { + strokePathDirect(ctx, line, start, count); + } +} +class LineElement extends Element { + constructor(cfg) { + super(); + this.animated = true; + this.options = undefined; + this._loop = undefined; + this._fullLoop = undefined; + this._path = undefined; + this._points = undefined; + this._segments = undefined; + this._decimated = false; + this._pointsUpdated = false; + if (cfg) { + Object.assign(this, cfg); + } + } + updateControlPoints(chartArea, indexAxis) { + const me = this; + const options = me.options; + if ((options.tension || options.cubicInterpolationMode === 'monotone') && !options.stepped && !me._pointsUpdated) { + const loop = options.spanGaps ? me._loop : me._fullLoop; + _updateBezierControlPoints(me._points, options, chartArea, loop, indexAxis); + me._pointsUpdated = true; + } + } + set points(points) { + const me = this; + me._points = points; + delete me._segments; + delete me._path; + me._pointsUpdated = false; + } + get points() { + return this._points; + } + get segments() { + return this._segments || (this._segments = _computeSegments(this, this.options.segment)); + } + first() { + const segments = this.segments; + const points = this.points; + return segments.length && points[segments[0].start]; + } + last() { + const segments = this.segments; + const points = this.points; + const count = segments.length; + return count && points[segments[count - 1].end]; + } + interpolate(point, property) { + const me = this; + const options = me.options; + const value = point[property]; + const points = me.points; + const segments = _boundSegments(me, {property, start: value, end: value}); + if (!segments.length) { + return; + } + const result = []; + const _interpolate = _getInterpolationMethod(options); + let i, ilen; + for (i = 0, ilen = segments.length; i < ilen; ++i) { + const {start, end} = segments[i]; + const p1 = points[start]; + const p2 = points[end]; + if (p1 === p2) { + result.push(p1); + continue; + } + const t = Math.abs((value - p1[property]) / (p2[property] - p1[property])); + const interpolated = _interpolate(p1, p2, t, options.stepped); + interpolated[property] = point[property]; + result.push(interpolated); + } + return result.length === 1 ? result[0] : result; + } + pathSegment(ctx, segment, params) { + const segmentMethod = _getSegmentMethod(this); + return segmentMethod(ctx, this, segment, params); + } + path(ctx, start, count) { + const me = this; + const segments = me.segments; + const segmentMethod = _getSegmentMethod(me); + let loop = me._loop; + start = start || 0; + count = count || (me.points.length - start); + for (const segment of segments) { + loop &= segmentMethod(ctx, me, segment, {start, end: start + count - 1}); + } + return !!loop; + } + draw(ctx, chartArea, start, count) { + const me = this; + const options = me.options || {}; + const points = me.points || []; + if (!points.length || !options.borderWidth) { + return; + } + ctx.save(); + draw(ctx, me, start, count); + ctx.restore(); + if (me.animated) { + me._pointsUpdated = false; + me._path = undefined; + } + } +} +LineElement.id = 'line'; +LineElement.defaults = { + borderCapStyle: 'butt', + borderDash: [], + borderDashOffset: 0, + borderJoinStyle: 'miter', + borderWidth: 3, + capBezierPoints: true, + cubicInterpolationMode: 'default', + fill: false, + spanGaps: false, + stepped: false, + tension: 0, +}; +LineElement.defaultRoutes = { + backgroundColor: 'backgroundColor', + borderColor: 'borderColor' +}; +LineElement.descriptors = { + _scriptable: true, + _indexable: (name) => name !== 'borderDash' && name !== 'fill', +}; + +function inRange$1(el, pos, axis, useFinalPosition) { + const options = el.options; + const {[axis]: value} = el.getProps([axis], useFinalPosition); + return (Math.abs(pos - value) < options.radius + options.hitRadius); +} +class PointElement extends Element { + constructor(cfg) { + super(); + this.options = undefined; + this.parsed = undefined; + this.skip = undefined; + this.stop = undefined; + if (cfg) { + Object.assign(this, cfg); + } + } + inRange(mouseX, mouseY, useFinalPosition) { + const options = this.options; + const {x, y} = this.getProps(['x', 'y'], useFinalPosition); + return ((Math.pow(mouseX - x, 2) + Math.pow(mouseY - y, 2)) < Math.pow(options.hitRadius + options.radius, 2)); + } + inXRange(mouseX, useFinalPosition) { + return inRange$1(this, mouseX, 'x', useFinalPosition); + } + inYRange(mouseY, useFinalPosition) { + return inRange$1(this, mouseY, 'y', useFinalPosition); + } + getCenterPoint(useFinalPosition) { + const {x, y} = this.getProps(['x', 'y'], useFinalPosition); + return {x, y}; + } + size(options) { + options = options || this.options || {}; + let radius = options.radius || 0; + radius = Math.max(radius, radius && options.hoverRadius || 0); + const borderWidth = radius && options.borderWidth || 0; + return (radius + borderWidth) * 2; + } + draw(ctx) { + const me = this; + const options = me.options; + if (me.skip || options.radius < 0.1) { + return; + } + ctx.strokeStyle = options.borderColor; + ctx.lineWidth = options.borderWidth; + ctx.fillStyle = options.backgroundColor; + drawPoint(ctx, options, me.x, me.y); + } + getRange() { + const options = this.options || {}; + return options.radius + options.hitRadius; + } +} +PointElement.id = 'point'; +PointElement.defaults = { + borderWidth: 1, + hitRadius: 1, + hoverBorderWidth: 1, + hoverRadius: 4, + pointStyle: 'circle', + radius: 3, + rotation: 0 +}; +PointElement.defaultRoutes = { + backgroundColor: 'backgroundColor', + borderColor: 'borderColor' +}; + +function getBarBounds(bar, useFinalPosition) { + const {x, y, base, width, height} = bar.getProps(['x', 'y', 'base', 'width', 'height'], useFinalPosition); + let left, right, top, bottom, half; + if (bar.horizontal) { + half = height / 2; + left = Math.min(x, base); + right = Math.max(x, base); + top = y - half; + bottom = y + half; + } else { + half = width / 2; + left = x - half; + right = x + half; + top = Math.min(y, base); + bottom = Math.max(y, base); + } + return {left, top, right, bottom}; +} +function parseBorderSkipped(bar) { + let edge = bar.options.borderSkipped; + const res = {}; + if (!edge) { + return res; + } + edge = bar.horizontal + ? parseEdge(edge, 'left', 'right', bar.base > bar.x) + : parseEdge(edge, 'bottom', 'top', bar.base < bar.y); + res[edge] = true; + return res; +} +function parseEdge(edge, a, b, reverse) { + if (reverse) { + edge = swap(edge, a, b); + edge = startEnd(edge, b, a); + } else { + edge = startEnd(edge, a, b); + } + return edge; +} +function swap(orig, v1, v2) { + return orig === v1 ? v2 : orig === v2 ? v1 : orig; +} +function startEnd(v, start, end) { + return v === 'start' ? start : v === 'end' ? end : v; +} +function skipOrLimit(skip, value, min, max) { + return skip ? 0 : Math.max(Math.min(value, max), min); +} +function parseBorderWidth(bar, maxW, maxH) { + const value = bar.options.borderWidth; + const skip = parseBorderSkipped(bar); + const o = toTRBL(value); + return { + t: skipOrLimit(skip.top, o.top, 0, maxH), + r: skipOrLimit(skip.right, o.right, 0, maxW), + b: skipOrLimit(skip.bottom, o.bottom, 0, maxH), + l: skipOrLimit(skip.left, o.left, 0, maxW) + }; +} +function parseBorderRadius(bar, maxW, maxH) { + const {enableBorderRadius} = bar.getProps(['enableBorderRadius']); + const value = bar.options.borderRadius; + const o = toTRBLCorners(value); + const maxR = Math.min(maxW, maxH); + const skip = parseBorderSkipped(bar); + const enableBorder = enableBorderRadius || isObject(value); + return { + topLeft: skipOrLimit(!enableBorder || skip.top || skip.left, o.topLeft, 0, maxR), + topRight: skipOrLimit(!enableBorder || skip.top || skip.right, o.topRight, 0, maxR), + bottomLeft: skipOrLimit(!enableBorder || skip.bottom || skip.left, o.bottomLeft, 0, maxR), + bottomRight: skipOrLimit(!enableBorder || skip.bottom || skip.right, o.bottomRight, 0, maxR) + }; +} +function boundingRects(bar) { + const bounds = getBarBounds(bar); + const width = bounds.right - bounds.left; + const height = bounds.bottom - bounds.top; + const border = parseBorderWidth(bar, width / 2, height / 2); + const radius = parseBorderRadius(bar, width / 2, height / 2); + return { + outer: { + x: bounds.left, + y: bounds.top, + w: width, + h: height, + radius + }, + inner: { + x: bounds.left + border.l, + y: bounds.top + border.t, + w: width - border.l - border.r, + h: height - border.t - border.b, + radius: { + topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)), + topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)), + bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)), + bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)), + } + } + }; +} +function inRange(bar, x, y, useFinalPosition) { + const skipX = x === null; + const skipY = y === null; + const skipBoth = skipX && skipY; + const bounds = bar && !skipBoth && getBarBounds(bar, useFinalPosition); + return bounds + && (skipX || x >= bounds.left && x <= bounds.right) + && (skipY || y >= bounds.top && y <= bounds.bottom); +} +function hasRadius(radius) { + return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight; +} +function addNormalRectPath(ctx, rect) { + ctx.rect(rect.x, rect.y, rect.w, rect.h); +} +class BarElement extends Element { + constructor(cfg) { + super(); + this.options = undefined; + this.horizontal = undefined; + this.base = undefined; + this.width = undefined; + this.height = undefined; + if (cfg) { + Object.assign(this, cfg); + } + } + draw(ctx) { + const options = this.options; + const {inner, outer} = boundingRects(this); + const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath; + ctx.save(); + if (outer.w !== inner.w || outer.h !== inner.h) { + ctx.beginPath(); + addRectPath(ctx, outer); + ctx.clip(); + addRectPath(ctx, inner); + ctx.fillStyle = options.borderColor; + ctx.fill('evenodd'); + } + ctx.beginPath(); + addRectPath(ctx, inner); + ctx.fillStyle = options.backgroundColor; + ctx.fill(); + ctx.restore(); + } + inRange(mouseX, mouseY, useFinalPosition) { + return inRange(this, mouseX, mouseY, useFinalPosition); + } + inXRange(mouseX, useFinalPosition) { + return inRange(this, mouseX, null, useFinalPosition); + } + inYRange(mouseY, useFinalPosition) { + return inRange(this, null, mouseY, useFinalPosition); + } + getCenterPoint(useFinalPosition) { + const {x, y, base, horizontal} = this.getProps(['x', 'y', 'base', 'horizontal'], useFinalPosition); + return { + x: horizontal ? (x + base) / 2 : x, + y: horizontal ? y : (y + base) / 2 + }; + } + getRange(axis) { + return axis === 'x' ? this.width / 2 : this.height / 2; + } +} +BarElement.id = 'bar'; +BarElement.defaults = { + borderSkipped: 'start', + borderWidth: 0, + borderRadius: 0, + enableBorderRadius: true, + pointStyle: undefined +}; +BarElement.defaultRoutes = { + backgroundColor: 'backgroundColor', + borderColor: 'borderColor' +}; + +var elements = /*#__PURE__*/Object.freeze({ +__proto__: null, +ArcElement: ArcElement, +LineElement: LineElement, +PointElement: PointElement, +BarElement: BarElement +}); + +function lttbDecimation(data, start, count, availableWidth, options) { + const samples = options.samples || availableWidth; + if (samples >= count) { + return data.slice(start, start + count); + } + const decimated = []; + const bucketWidth = (count - 2) / (samples - 2); + let sampledIndex = 0; + const endIndex = start + count - 1; + let a = start; + let i, maxAreaPoint, maxArea, area, nextA; + decimated[sampledIndex++] = data[a]; + for (i = 0; i < samples - 2; i++) { + let avgX = 0; + let avgY = 0; + let j; + const avgRangeStart = Math.floor((i + 1) * bucketWidth) + 1 + start; + const avgRangeEnd = Math.min(Math.floor((i + 2) * bucketWidth) + 1, count) + start; + const avgRangeLength = avgRangeEnd - avgRangeStart; + for (j = avgRangeStart; j < avgRangeEnd; j++) { + avgX += data[j].x; + avgY += data[j].y; + } + avgX /= avgRangeLength; + avgY /= avgRangeLength; + const rangeOffs = Math.floor(i * bucketWidth) + 1 + start; + const rangeTo = Math.floor((i + 1) * bucketWidth) + 1 + start; + const {x: pointAx, y: pointAy} = data[a]; + maxArea = area = -1; + for (j = rangeOffs; j < rangeTo; j++) { + area = 0.5 * Math.abs( + (pointAx - avgX) * (data[j].y - pointAy) - + (pointAx - data[j].x) * (avgY - pointAy) + ); + if (area > maxArea) { + maxArea = area; + maxAreaPoint = data[j]; + nextA = j; + } + } + decimated[sampledIndex++] = maxAreaPoint; + a = nextA; + } + decimated[sampledIndex++] = data[endIndex]; + return decimated; +} +function minMaxDecimation(data, start, count, availableWidth) { + let avgX = 0; + let countX = 0; + let i, point, x, y, prevX, minIndex, maxIndex, startIndex, minY, maxY; + const decimated = []; + const endIndex = start + count - 1; + const xMin = data[start].x; + const xMax = data[endIndex].x; + const dx = xMax - xMin; + for (i = start; i < start + count; ++i) { + point = data[i]; + x = (point.x - xMin) / dx * availableWidth; + y = point.y; + const truncX = x | 0; + if (truncX === prevX) { + if (y < minY) { + minY = y; + minIndex = i; + } else if (y > maxY) { + maxY = y; + maxIndex = i; + } + avgX = (countX * avgX + point.x) / ++countX; + } else { + const lastIndex = i - 1; + if (!isNullOrUndef(minIndex) && !isNullOrUndef(maxIndex)) { + const intermediateIndex1 = Math.min(minIndex, maxIndex); + const intermediateIndex2 = Math.max(minIndex, maxIndex); + if (intermediateIndex1 !== startIndex && intermediateIndex1 !== lastIndex) { + decimated.push({ + ...data[intermediateIndex1], + x: avgX, + }); + } + if (intermediateIndex2 !== startIndex && intermediateIndex2 !== lastIndex) { + decimated.push({ + ...data[intermediateIndex2], + x: avgX + }); + } + } + if (i > 0 && lastIndex !== startIndex) { + decimated.push(data[lastIndex]); + } + decimated.push(point); + prevX = truncX; + countX = 0; + minY = maxY = y; + minIndex = maxIndex = startIndex = i; + } + } + return decimated; +} +function cleanDecimatedDataset(dataset) { + if (dataset._decimated) { + const data = dataset._data; + delete dataset._decimated; + delete dataset._data; + Object.defineProperty(dataset, 'data', {value: data}); + } +} +function cleanDecimatedData(chart) { + chart.data.datasets.forEach((dataset) => { + cleanDecimatedDataset(dataset); + }); +} +function getStartAndCountOfVisiblePointsSimplified(meta, points) { + const pointCount = points.length; + let start = 0; + let count; + const {iScale} = meta; + const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); + if (minDefined) { + start = _limitValue(_lookupByKey(points, iScale.axis, min).lo, 0, pointCount - 1); + } + if (maxDefined) { + count = _limitValue(_lookupByKey(points, iScale.axis, max).hi + 1, start, pointCount) - start; + } else { + count = pointCount - start; + } + return {start, count}; +} +var plugin_decimation = { + id: 'decimation', + defaults: { + algorithm: 'min-max', + enabled: false, + }, + beforeElementsUpdate: (chart, args, options) => { + if (!options.enabled) { + cleanDecimatedData(chart); + return; + } + const availableWidth = chart.width; + chart.data.datasets.forEach((dataset, datasetIndex) => { + const {_data, indexAxis} = dataset; + const meta = chart.getDatasetMeta(datasetIndex); + const data = _data || dataset.data; + if (resolve([indexAxis, chart.options.indexAxis]) === 'y') { + return; + } + if (meta.type !== 'line') { + return; + } + const xAxis = chart.scales[meta.xAxisID]; + if (xAxis.type !== 'linear' && xAxis.type !== 'time') { + return; + } + if (chart.options.parsing) { + return; + } + let {start, count} = getStartAndCountOfVisiblePointsSimplified(meta, data); + if (count <= 4 * availableWidth) { + cleanDecimatedDataset(dataset); + return; + } + if (isNullOrUndef(_data)) { + dataset._data = data; + delete dataset.data; + Object.defineProperty(dataset, 'data', { + configurable: true, + enumerable: true, + get: function() { + return this._decimated; + }, + set: function(d) { + this._data = d; + } + }); + } + let decimated; + switch (options.algorithm) { + case 'lttb': + decimated = lttbDecimation(data, start, count, availableWidth, options); + break; + case 'min-max': + decimated = minMaxDecimation(data, start, count, availableWidth); + break; + default: + throw new Error(`Unsupported decimation algorithm '${options.algorithm}'`); + } + dataset._decimated = decimated; + }); + }, + destroy(chart) { + cleanDecimatedData(chart); + } +}; + +function getLineByIndex(chart, index) { + const meta = chart.getDatasetMeta(index); + const visible = meta && chart.isDatasetVisible(index); + return visible ? meta.dataset : null; +} +function parseFillOption(line) { + const options = line.options; + const fillOption = options.fill; + let fill = valueOrDefault(fillOption && fillOption.target, fillOption); + if (fill === undefined) { + fill = !!options.backgroundColor; + } + if (fill === false || fill === null) { + return false; + } + if (fill === true) { + return 'origin'; + } + return fill; +} +function decodeFill(line, index, count) { + const fill = parseFillOption(line); + if (isObject(fill)) { + return isNaN(fill.value) ? false : fill; + } + let target = parseFloat(fill); + if (isNumberFinite(target) && Math.floor(target) === target) { + if (fill[0] === '-' || fill[0] === '+') { + target = index + target; + } + if (target === index || target < 0 || target >= count) { + return false; + } + return target; + } + return ['origin', 'start', 'end', 'stack'].indexOf(fill) >= 0 && fill; +} +function computeLinearBoundary(source) { + const {scale = {}, fill} = source; + let target = null; + let horizontal; + if (fill === 'start') { + target = scale.bottom; + } else if (fill === 'end') { + target = scale.top; + } else if (isObject(fill)) { + target = scale.getPixelForValue(fill.value); + } else if (scale.getBasePixel) { + target = scale.getBasePixel(); + } + if (isNumberFinite(target)) { + horizontal = scale.isHorizontal(); + return { + x: horizontal ? target : null, + y: horizontal ? null : target + }; + } + return null; +} +class simpleArc { + constructor(opts) { + this.x = opts.x; + this.y = opts.y; + this.radius = opts.radius; + } + pathSegment(ctx, bounds, opts) { + const {x, y, radius} = this; + bounds = bounds || {start: 0, end: TAU}; + ctx.arc(x, y, radius, bounds.end, bounds.start, true); + return !opts.bounds; + } + interpolate(point) { + const {x, y, radius} = this; + const angle = point.angle; + return { + x: x + Math.cos(angle) * radius, + y: y + Math.sin(angle) * radius, + angle + }; + } +} +function computeCircularBoundary(source) { + const {scale, fill} = source; + const options = scale.options; + const length = scale.getLabels().length; + const target = []; + const start = options.reverse ? scale.max : scale.min; + const end = options.reverse ? scale.min : scale.max; + let i, center, value; + if (fill === 'start') { + value = start; + } else if (fill === 'end') { + value = end; + } else if (isObject(fill)) { + value = fill.value; + } else { + value = scale.getBaseValue(); + } + if (options.grid.circular) { + center = scale.getPointPositionForValue(0, start); + return new simpleArc({ + x: center.x, + y: center.y, + radius: scale.getDistanceFromCenterForValue(value) + }); + } + for (i = 0; i < length; ++i) { + target.push(scale.getPointPositionForValue(i, value)); + } + return target; +} +function computeBoundary(source) { + const scale = source.scale || {}; + if (scale.getPointPositionForValue) { + return computeCircularBoundary(source); + } + return computeLinearBoundary(source); +} +function findSegmentEnd(start, end, points) { + for (;end > start; end--) { + const point = points[end]; + if (!isNaN(point.x) && !isNaN(point.y)) { + break; + } + } + return end; +} +function pointsFromSegments(boundary, line) { + const {x = null, y = null} = boundary || {}; + const linePoints = line.points; + const points = []; + line.segments.forEach(({start, end}) => { + end = findSegmentEnd(start, end, linePoints); + const first = linePoints[start]; + const last = linePoints[end]; + if (y !== null) { + points.push({x: first.x, y}); + points.push({x: last.x, y}); + } else if (x !== null) { + points.push({x, y: first.y}); + points.push({x, y: last.y}); + } + }); + return points; +} +function buildStackLine(source) { + const {chart, scale, index, line} = source; + const points = []; + const segments = line.segments; + const sourcePoints = line.points; + const linesBelow = getLinesBelow(chart, index); + linesBelow.push(createBoundaryLine({x: null, y: scale.bottom}, line)); + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + for (let j = segment.start; j <= segment.end; j++) { + addPointsBelow(points, sourcePoints[j], linesBelow); + } + } + return new LineElement({points, options: {}}); +} +const isLineAndNotInHideAnimation = (meta) => meta.type === 'line' && !meta.hidden; +function getLinesBelow(chart, index) { + const below = []; + const metas = chart.getSortedVisibleDatasetMetas(); + for (let i = 0; i < metas.length; i++) { + const meta = metas[i]; + if (meta.index === index) { + break; + } + if (isLineAndNotInHideAnimation(meta)) { + below.unshift(meta.dataset); + } + } + return below; +} +function addPointsBelow(points, sourcePoint, linesBelow) { + const postponed = []; + for (let j = 0; j < linesBelow.length; j++) { + const line = linesBelow[j]; + const {first, last, point} = findPoint(line, sourcePoint, 'x'); + if (!point || (first && last)) { + continue; + } + if (first) { + postponed.unshift(point); + } else { + points.push(point); + if (!last) { + break; + } + } + } + points.push(...postponed); +} +function findPoint(line, sourcePoint, property) { + const point = line.interpolate(sourcePoint, property); + if (!point) { + return {}; + } + const pointValue = point[property]; + const segments = line.segments; + const linePoints = line.points; + let first = false; + let last = false; + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + const firstValue = linePoints[segment.start][property]; + const lastValue = linePoints[segment.end][property]; + if (pointValue >= firstValue && pointValue <= lastValue) { + first = pointValue === firstValue; + last = pointValue === lastValue; + break; + } + } + return {first, last, point}; +} +function getTarget(source) { + const {chart, fill, line} = source; + if (isNumberFinite(fill)) { + return getLineByIndex(chart, fill); + } + if (fill === 'stack') { + return buildStackLine(source); + } + const boundary = computeBoundary(source); + if (boundary instanceof simpleArc) { + return boundary; + } + return createBoundaryLine(boundary, line); +} +function createBoundaryLine(boundary, line) { + let points = []; + let _loop = false; + if (isArray(boundary)) { + _loop = true; + points = boundary; + } else { + points = pointsFromSegments(boundary, line); + } + return points.length ? new LineElement({ + points, + options: {tension: 0}, + _loop, + _fullLoop: _loop + }) : null; +} +function resolveTarget(sources, index, propagate) { + const source = sources[index]; + let fill = source.fill; + const visited = [index]; + let target; + if (!propagate) { + return fill; + } + while (fill !== false && visited.indexOf(fill) === -1) { + if (!isNumberFinite(fill)) { + return fill; + } + target = sources[fill]; + if (!target) { + return false; + } + if (target.visible) { + return fill; + } + visited.push(fill); + fill = target.fill; + } + return false; +} +function _clip(ctx, target, clipY) { + ctx.beginPath(); + target.path(ctx); + ctx.lineTo(target.last().x, clipY); + ctx.lineTo(target.first().x, clipY); + ctx.closePath(); + ctx.clip(); +} +function getBounds(property, first, last, loop) { + if (loop) { + return; + } + let start = first[property]; + let end = last[property]; + if (property === 'angle') { + start = _normalizeAngle(start); + end = _normalizeAngle(end); + } + return {property, start, end}; +} +function _getEdge(a, b, prop, fn) { + if (a && b) { + return fn(a[prop], b[prop]); + } + return a ? a[prop] : b ? b[prop] : 0; +} +function _segments(line, target, property) { + const segments = line.segments; + const points = line.points; + const tpoints = target.points; + const parts = []; + for (const segment of segments) { + let {start, end} = segment; + end = findSegmentEnd(start, end, points); + const bounds = getBounds(property, points[start], points[end], segment.loop); + if (!target.segments) { + parts.push({ + source: segment, + target: bounds, + start: points[start], + end: points[end] + }); + continue; + } + const targetSegments = _boundSegments(target, bounds); + for (const tgt of targetSegments) { + const subBounds = getBounds(property, tpoints[tgt.start], tpoints[tgt.end], tgt.loop); + const fillSources = _boundSegment(segment, points, subBounds); + for (const fillSource of fillSources) { + parts.push({ + source: fillSource, + target: tgt, + start: { + [property]: _getEdge(bounds, subBounds, 'start', Math.max) + }, + end: { + [property]: _getEdge(bounds, subBounds, 'end', Math.min) + } + }); + } + } + } + return parts; +} +function clipBounds(ctx, scale, bounds) { + const {top, bottom} = scale.chart.chartArea; + const {property, start, end} = bounds || {}; + if (property === 'x') { + ctx.beginPath(); + ctx.rect(start, top, end - start, bottom - top); + ctx.clip(); + } +} +function interpolatedLineTo(ctx, target, point, property) { + const interpolatedPoint = target.interpolate(point, property); + if (interpolatedPoint) { + ctx.lineTo(interpolatedPoint.x, interpolatedPoint.y); + } +} +function _fill(ctx, cfg) { + const {line, target, property, color, scale} = cfg; + const segments = _segments(line, target, property); + for (const {source: src, target: tgt, start, end} of segments) { + const {style: {backgroundColor = color} = {}} = src; + ctx.save(); + ctx.fillStyle = backgroundColor; + clipBounds(ctx, scale, getBounds(property, start, end)); + ctx.beginPath(); + const lineLoop = !!line.pathSegment(ctx, src); + if (lineLoop) { + ctx.closePath(); + } else { + interpolatedLineTo(ctx, target, end, property); + } + const targetLoop = !!target.pathSegment(ctx, tgt, {move: lineLoop, reverse: true}); + const loop = lineLoop && targetLoop; + if (!loop) { + interpolatedLineTo(ctx, target, start, property); + } + ctx.closePath(); + ctx.fill(loop ? 'evenodd' : 'nonzero'); + ctx.restore(); + } +} +function doFill(ctx, cfg) { + const {line, target, above, below, area, scale} = cfg; + const property = line._loop ? 'angle' : cfg.axis; + ctx.save(); + if (property === 'x' && below !== above) { + _clip(ctx, target, area.top); + _fill(ctx, {line, target, color: above, scale, property}); + ctx.restore(); + ctx.save(); + _clip(ctx, target, area.bottom); + } + _fill(ctx, {line, target, color: below, scale, property}); + ctx.restore(); +} +function drawfill(ctx, source, area) { + const target = getTarget(source); + const {line, scale, axis} = source; + const lineOpts = line.options; + const fillOption = lineOpts.fill; + const color = lineOpts.backgroundColor; + const {above = color, below = color} = fillOption || {}; + if (target && line.points.length) { + clipArea(ctx, area); + doFill(ctx, {line, target, above, below, area, scale, axis}); + unclipArea(ctx); + } +} +var plugin_filler = { + id: 'filler', + afterDatasetsUpdate(chart, _args, options) { + const count = (chart.data.datasets || []).length; + const sources = []; + let meta, i, line, source; + for (i = 0; i < count; ++i) { + meta = chart.getDatasetMeta(i); + line = meta.dataset; + source = null; + if (line && line.options && line instanceof LineElement) { + source = { + visible: chart.isDatasetVisible(i), + index: i, + fill: decodeFill(line, i, count), + chart, + axis: meta.controller.options.indexAxis, + scale: meta.vScale, + line, + }; + } + meta.$filler = source; + sources.push(source); + } + for (i = 0; i < count; ++i) { + source = sources[i]; + if (!source || source.fill === false) { + continue; + } + source.fill = resolveTarget(sources, i, options.propagate); + } + }, + beforeDraw(chart, _args, options) { + const draw = options.drawTime === 'beforeDraw'; + const metasets = chart.getSortedVisibleDatasetMetas(); + const area = chart.chartArea; + for (let i = metasets.length - 1; i >= 0; --i) { + const source = metasets[i].$filler; + if (!source) { + continue; + } + source.line.updateControlPoints(area, source.axis); + if (draw) { + drawfill(chart.ctx, source, area); + } + } + }, + beforeDatasetsDraw(chart, _args, options) { + if (options.drawTime !== 'beforeDatasetsDraw') { + return; + } + const metasets = chart.getSortedVisibleDatasetMetas(); + for (let i = metasets.length - 1; i >= 0; --i) { + const source = metasets[i].$filler; + if (source) { + drawfill(chart.ctx, source, chart.chartArea); + } + } + }, + beforeDatasetDraw(chart, args, options) { + const source = args.meta.$filler; + if (!source || source.fill === false || options.drawTime !== 'beforeDatasetDraw') { + return; + } + drawfill(chart.ctx, source, chart.chartArea); + }, + defaults: { + propagate: true, + drawTime: 'beforeDatasetDraw' + } +}; + +const getBoxSize = (labelOpts, fontSize) => { + let {boxHeight = fontSize, boxWidth = fontSize} = labelOpts; + if (labelOpts.usePointStyle) { + boxHeight = Math.min(boxHeight, fontSize); + boxWidth = Math.min(boxWidth, fontSize); + } + return { + boxWidth, + boxHeight, + itemHeight: Math.max(fontSize, boxHeight) + }; +}; +const itemsEqual = (a, b) => a !== null && b !== null && a.datasetIndex === b.datasetIndex && a.index === b.index; +class Legend extends Element { + constructor(config) { + super(); + this._added = false; + this.legendHitBoxes = []; + this._hoveredItem = null; + this.doughnutMode = false; + this.chart = config.chart; + this.options = config.options; + this.ctx = config.ctx; + this.legendItems = undefined; + this.columnSizes = undefined; + this.lineWidths = undefined; + this.maxHeight = undefined; + this.maxWidth = undefined; + this.top = undefined; + this.bottom = undefined; + this.left = undefined; + this.right = undefined; + this.height = undefined; + this.width = undefined; + this._margins = undefined; + this.position = undefined; + this.weight = undefined; + this.fullSize = undefined; + } + update(maxWidth, maxHeight, margins) { + const me = this; + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me._margins = margins; + me.setDimensions(); + me.buildLabels(); + me.fit(); + } + setDimensions() { + const me = this; + if (me.isHorizontal()) { + me.width = me.maxWidth; + me.left = me._margins.left; + me.right = me.width; + } else { + me.height = me.maxHeight; + me.top = me._margins.top; + me.bottom = me.height; + } + } + buildLabels() { + const me = this; + const labelOpts = me.options.labels || {}; + let legendItems = callback(labelOpts.generateLabels, [me.chart], me) || []; + if (labelOpts.filter) { + legendItems = legendItems.filter((item) => labelOpts.filter(item, me.chart.data)); + } + if (labelOpts.sort) { + legendItems = legendItems.sort((a, b) => labelOpts.sort(a, b, me.chart.data)); + } + if (me.options.reverse) { + legendItems.reverse(); + } + me.legendItems = legendItems; + } + fit() { + const me = this; + const {options, ctx} = me; + if (!options.display) { + me.width = me.height = 0; + return; + } + const labelOpts = options.labels; + const labelFont = toFont(labelOpts.font); + const fontSize = labelFont.size; + const titleHeight = me._computeTitleHeight(); + const {boxWidth, itemHeight} = getBoxSize(labelOpts, fontSize); + let width, height; + ctx.font = labelFont.string; + if (me.isHorizontal()) { + width = me.maxWidth; + height = me._fitRows(titleHeight, fontSize, boxWidth, itemHeight) + 10; + } else { + height = me.maxHeight; + width = me._fitCols(titleHeight, fontSize, boxWidth, itemHeight) + 10; + } + me.width = Math.min(width, options.maxWidth || me.maxWidth); + me.height = Math.min(height, options.maxHeight || me.maxHeight); + } + _fitRows(titleHeight, fontSize, boxWidth, itemHeight) { + const me = this; + const {ctx, maxWidth, options: {labels: {padding}}} = me; + const hitboxes = me.legendHitBoxes = []; + const lineWidths = me.lineWidths = [0]; + const lineHeight = itemHeight + padding; + let totalHeight = titleHeight; + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + let row = -1; + let top = -lineHeight; + me.legendItems.forEach((legendItem, i) => { + const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + if (i === 0 || lineWidths[lineWidths.length - 1] + itemWidth + 2 * padding > maxWidth) { + totalHeight += lineHeight; + lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0; + top += lineHeight; + row++; + } + hitboxes[i] = {left: 0, top, row, width: itemWidth, height: itemHeight}; + lineWidths[lineWidths.length - 1] += itemWidth + padding; + }); + return totalHeight; + } + _fitCols(titleHeight, fontSize, boxWidth, itemHeight) { + const me = this; + const {ctx, maxHeight, options: {labels: {padding}}} = me; + const hitboxes = me.legendHitBoxes = []; + const columnSizes = me.columnSizes = []; + const heightLimit = maxHeight - titleHeight; + let totalWidth = padding; + let currentColWidth = 0; + let currentColHeight = 0; + let left = 0; + let col = 0; + me.legendItems.forEach((legendItem, i) => { + const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + if (i > 0 && currentColHeight + itemHeight + 2 * padding > heightLimit) { + totalWidth += currentColWidth + padding; + columnSizes.push({width: currentColWidth, height: currentColHeight}); + left += currentColWidth + padding; + col++; + currentColWidth = currentColHeight = 0; + } + hitboxes[i] = {left, top: currentColHeight, col, width: itemWidth, height: itemHeight}; + currentColWidth = Math.max(currentColWidth, itemWidth); + currentColHeight += itemHeight + padding; + }); + totalWidth += currentColWidth; + columnSizes.push({width: currentColWidth, height: currentColHeight}); + return totalWidth; + } + adjustHitBoxes() { + const me = this; + if (!me.options.display) { + return; + } + const titleHeight = me._computeTitleHeight(); + const {legendHitBoxes: hitboxes, options: {align, labels: {padding}, rtl}} = me; + if (this.isHorizontal()) { + let row = 0; + let left = _alignStartEnd(align, me.left + padding, me.right - me.lineWidths[row]); + for (const hitbox of hitboxes) { + if (row !== hitbox.row) { + row = hitbox.row; + left = _alignStartEnd(align, me.left + padding, me.right - me.lineWidths[row]); + } + hitbox.top += me.top + titleHeight + padding; + hitbox.left = left; + left += hitbox.width + padding; + } + if (rtl) { + const boxMap = hitboxes.reduce((map, box) => { + map[box.row] = map[box.row] || []; + map[box.row].push(box); + return map; + }, {}); + const newBoxes = []; + Object.keys(boxMap).forEach(key => { + boxMap[key].reverse(); + newBoxes.push(...boxMap[key]); + }); + me.legendHitBoxes = newBoxes; + } + } else { + let col = 0; + let top = _alignStartEnd(align, me.top + titleHeight + padding, me.bottom - me.columnSizes[col].height); + for (const hitbox of hitboxes) { + if (hitbox.col !== col) { + col = hitbox.col; + top = _alignStartEnd(align, me.top + titleHeight + padding, me.bottom - me.columnSizes[col].height); + } + hitbox.top = top; + hitbox.left += me.left + padding; + top += hitbox.height + padding; + } + } + } + isHorizontal() { + return this.options.position === 'top' || this.options.position === 'bottom'; + } + draw() { + const me = this; + if (me.options.display) { + const ctx = me.ctx; + clipArea(ctx, me); + me._draw(); + unclipArea(ctx); + } + } + _draw() { + const me = this; + const {options: opts, columnSizes, lineWidths, ctx} = me; + const {align, labels: labelOpts} = opts; + const defaultColor = defaults.color; + const rtlHelper = getRtlAdapter(opts.rtl, me.left, me.width); + const labelFont = toFont(labelOpts.font); + const {color: fontColor, padding} = labelOpts; + const fontSize = labelFont.size; + const halfFontSize = fontSize / 2; + let cursor; + me.drawTitle(); + ctx.textAlign = rtlHelper.textAlign('left'); + ctx.textBaseline = 'middle'; + ctx.lineWidth = 0.5; + ctx.font = labelFont.string; + const {boxWidth, boxHeight, itemHeight} = getBoxSize(labelOpts, fontSize); + const drawLegendBox = function(x, y, legendItem) { + if (isNaN(boxWidth) || boxWidth <= 0 || isNaN(boxHeight) || boxHeight < 0) { + return; + } + ctx.save(); + const lineWidth = valueOrDefault(legendItem.lineWidth, 1); + ctx.fillStyle = valueOrDefault(legendItem.fillStyle, defaultColor); + ctx.lineCap = valueOrDefault(legendItem.lineCap, 'butt'); + ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, 0); + ctx.lineJoin = valueOrDefault(legendItem.lineJoin, 'miter'); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, defaultColor); + ctx.setLineDash(valueOrDefault(legendItem.lineDash, [])); + if (labelOpts.usePointStyle) { + const drawOptions = { + radius: boxWidth * Math.SQRT2 / 2, + pointStyle: legendItem.pointStyle, + rotation: legendItem.rotation, + borderWidth: lineWidth + }; + const centerX = rtlHelper.xPlus(x, boxWidth / 2); + const centerY = y + halfFontSize; + drawPoint(ctx, drawOptions, centerX, centerY); + } else { + const yBoxTop = y + Math.max((fontSize - boxHeight) / 2, 0); + const xBoxLeft = rtlHelper.leftForLtr(x, boxWidth); + const borderRadius = toTRBLCorners(legendItem.borderRadius); + ctx.beginPath(); + if (Object.values(borderRadius).some(v => v !== 0)) { + addRoundedRectPath(ctx, { + x: xBoxLeft, + y: yBoxTop, + w: boxWidth, + h: boxHeight, + radius: borderRadius, + }); + } else { + ctx.rect(xBoxLeft, yBoxTop, boxWidth, boxHeight); + } + ctx.fill(); + if (lineWidth !== 0) { + ctx.stroke(); + } + } + ctx.restore(); + }; + const fillText = function(x, y, legendItem) { + renderText(ctx, legendItem.text, x, y + (itemHeight / 2), labelFont, { + strikethrough: legendItem.hidden, + textAlign: rtlHelper.textAlign(legendItem.textAlign) + }); + }; + const isHorizontal = me.isHorizontal(); + const titleHeight = this._computeTitleHeight(); + if (isHorizontal) { + cursor = { + x: _alignStartEnd(align, me.left + padding, me.right - lineWidths[0]), + y: me.top + padding + titleHeight, + line: 0 + }; + } else { + cursor = { + x: me.left + padding, + y: _alignStartEnd(align, me.top + titleHeight + padding, me.bottom - columnSizes[0].height), + line: 0 + }; + } + overrideTextDirection(me.ctx, opts.textDirection); + const lineHeight = itemHeight + padding; + me.legendItems.forEach((legendItem, i) => { + ctx.strokeStyle = legendItem.fontColor || fontColor; + ctx.fillStyle = legendItem.fontColor || fontColor; + const textWidth = ctx.measureText(legendItem.text).width; + const textAlign = rtlHelper.textAlign(legendItem.textAlign || (legendItem.textAlign = labelOpts.textAlign)); + const width = boxWidth + halfFontSize + textWidth; + let x = cursor.x; + let y = cursor.y; + rtlHelper.setWidth(me.width); + if (isHorizontal) { + if (i > 0 && x + width + padding > me.right) { + y = cursor.y += lineHeight; + cursor.line++; + x = cursor.x = _alignStartEnd(align, me.left + padding, me.right - lineWidths[cursor.line]); + } + } else if (i > 0 && y + lineHeight > me.bottom) { + x = cursor.x = x + columnSizes[cursor.line].width + padding; + cursor.line++; + y = cursor.y = _alignStartEnd(align, me.top + titleHeight + padding, me.bottom - columnSizes[cursor.line].height); + } + const realX = rtlHelper.x(x); + drawLegendBox(realX, y, legendItem); + x = _textX(textAlign, x + boxWidth + halfFontSize, isHorizontal ? x + width : me.right, opts.rtl); + fillText(rtlHelper.x(x), y, legendItem); + if (isHorizontal) { + cursor.x += width + padding; + } else { + cursor.y += lineHeight; + } + }); + restoreTextDirection(me.ctx, opts.textDirection); + } + drawTitle() { + const me = this; + const opts = me.options; + const titleOpts = opts.title; + const titleFont = toFont(titleOpts.font); + const titlePadding = toPadding(titleOpts.padding); + if (!titleOpts.display) { + return; + } + const rtlHelper = getRtlAdapter(opts.rtl, me.left, me.width); + const ctx = me.ctx; + const position = titleOpts.position; + const halfFontSize = titleFont.size / 2; + const topPaddingPlusHalfFontSize = titlePadding.top + halfFontSize; + let y; + let left = me.left; + let maxWidth = me.width; + if (this.isHorizontal()) { + maxWidth = Math.max(...me.lineWidths); + y = me.top + topPaddingPlusHalfFontSize; + left = _alignStartEnd(opts.align, left, me.right - maxWidth); + } else { + const maxHeight = me.columnSizes.reduce((acc, size) => Math.max(acc, size.height), 0); + y = topPaddingPlusHalfFontSize + _alignStartEnd(opts.align, me.top, me.bottom - maxHeight - opts.labels.padding - me._computeTitleHeight()); + } + const x = _alignStartEnd(position, left, left + maxWidth); + ctx.textAlign = rtlHelper.textAlign(_toLeftRightCenter(position)); + ctx.textBaseline = 'middle'; + ctx.strokeStyle = titleOpts.color; + ctx.fillStyle = titleOpts.color; + ctx.font = titleFont.string; + renderText(ctx, titleOpts.text, x, y, titleFont); + } + _computeTitleHeight() { + const titleOpts = this.options.title; + const titleFont = toFont(titleOpts.font); + const titlePadding = toPadding(titleOpts.padding); + return titleOpts.display ? titleFont.lineHeight + titlePadding.height : 0; + } + _getLegendItemAt(x, y) { + const me = this; + let i, hitBox, lh; + if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { + lh = me.legendHitBoxes; + for (i = 0; i < lh.length; ++i) { + hitBox = lh[i]; + if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { + return me.legendItems[i]; + } + } + } + return null; + } + handleEvent(e) { + const me = this; + const opts = me.options; + if (!isListened(e.type, opts)) { + return; + } + const hoveredItem = me._getLegendItemAt(e.x, e.y); + if (e.type === 'mousemove') { + const previous = me._hoveredItem; + const sameItem = itemsEqual(previous, hoveredItem); + if (previous && !sameItem) { + callback(opts.onLeave, [e, previous, me], me); + } + me._hoveredItem = hoveredItem; + if (hoveredItem && !sameItem) { + callback(opts.onHover, [e, hoveredItem, me], me); + } + } else if (hoveredItem) { + callback(opts.onClick, [e, hoveredItem, me], me); + } + } +} +function isListened(type, opts) { + if (type === 'mousemove' && (opts.onHover || opts.onLeave)) { + return true; + } + if (opts.onClick && (type === 'click' || type === 'mouseup')) { + return true; + } + return false; +} +var plugin_legend = { + id: 'legend', + _element: Legend, + start(chart, _args, options) { + const legend = chart.legend = new Legend({ctx: chart.ctx, options, chart}); + layouts.configure(chart, legend, options); + layouts.addBox(chart, legend); + }, + stop(chart) { + layouts.removeBox(chart, chart.legend); + delete chart.legend; + }, + beforeUpdate(chart, _args, options) { + const legend = chart.legend; + layouts.configure(chart, legend, options); + legend.options = options; + }, + afterUpdate(chart) { + const legend = chart.legend; + legend.buildLabels(); + legend.adjustHitBoxes(); + }, + afterEvent(chart, args) { + if (!args.replay) { + chart.legend.handleEvent(args.event); + } + }, + defaults: { + display: true, + position: 'top', + align: 'center', + fullSize: true, + reverse: false, + weight: 1000, + onClick(e, legendItem, legend) { + const index = legendItem.datasetIndex; + const ci = legend.chart; + if (ci.isDatasetVisible(index)) { + ci.hide(index); + legendItem.hidden = true; + } else { + ci.show(index); + legendItem.hidden = false; + } + }, + onHover: null, + onLeave: null, + labels: { + color: (ctx) => ctx.chart.options.color, + boxWidth: 40, + padding: 10, + generateLabels(chart) { + const datasets = chart.data.datasets; + const {labels: {usePointStyle, pointStyle, textAlign, color}} = chart.legend.options; + return chart._getSortedDatasetMetas().map((meta) => { + const style = meta.controller.getStyle(usePointStyle ? 0 : undefined); + const borderWidth = toPadding(style.borderWidth); + return { + text: datasets[meta.index].label, + fillStyle: style.backgroundColor, + fontColor: color, + hidden: !meta.visible, + lineCap: style.borderCapStyle, + lineDash: style.borderDash, + lineDashOffset: style.borderDashOffset, + lineJoin: style.borderJoinStyle, + lineWidth: (borderWidth.width + borderWidth.height) / 4, + strokeStyle: style.borderColor, + pointStyle: pointStyle || style.pointStyle, + rotation: style.rotation, + textAlign: textAlign || style.textAlign, + borderRadius: 0, + datasetIndex: meta.index + }; + }, this); + } + }, + title: { + color: (ctx) => ctx.chart.options.color, + display: false, + position: 'center', + text: '', + } + }, + descriptors: { + _scriptable: (name) => !name.startsWith('on'), + labels: { + _scriptable: (name) => !['generateLabels', 'filter', 'sort'].includes(name), + } + }, +}; + +class Title extends Element { + constructor(config) { + super(); + this.chart = config.chart; + this.options = config.options; + this.ctx = config.ctx; + this._padding = undefined; + this.top = undefined; + this.bottom = undefined; + this.left = undefined; + this.right = undefined; + this.width = undefined; + this.height = undefined; + this.position = undefined; + this.weight = undefined; + this.fullSize = undefined; + } + update(maxWidth, maxHeight) { + const me = this; + const opts = me.options; + me.left = 0; + me.top = 0; + if (!opts.display) { + me.width = me.height = me.right = me.bottom = 0; + return; + } + me.width = me.right = maxWidth; + me.height = me.bottom = maxHeight; + const lineCount = isArray(opts.text) ? opts.text.length : 1; + me._padding = toPadding(opts.padding); + const textSize = lineCount * toFont(opts.font).lineHeight + me._padding.height; + if (me.isHorizontal()) { + me.height = textSize; + } else { + me.width = textSize; + } + } + isHorizontal() { + const pos = this.options.position; + return pos === 'top' || pos === 'bottom'; + } + _drawArgs(offset) { + const {top, left, bottom, right, options} = this; + const align = options.align; + let rotation = 0; + let maxWidth, titleX, titleY; + if (this.isHorizontal()) { + titleX = _alignStartEnd(align, left, right); + titleY = top + offset; + maxWidth = right - left; + } else { + if (options.position === 'left') { + titleX = left + offset; + titleY = _alignStartEnd(align, bottom, top); + rotation = PI * -0.5; + } else { + titleX = right - offset; + titleY = _alignStartEnd(align, top, bottom); + rotation = PI * 0.5; + } + maxWidth = bottom - top; + } + return {titleX, titleY, maxWidth, rotation}; + } + draw() { + const me = this; + const ctx = me.ctx; + const opts = me.options; + if (!opts.display) { + return; + } + const fontOpts = toFont(opts.font); + const lineHeight = fontOpts.lineHeight; + const offset = lineHeight / 2 + me._padding.top; + const {titleX, titleY, maxWidth, rotation} = me._drawArgs(offset); + renderText(ctx, opts.text, 0, 0, fontOpts, { + color: opts.color, + maxWidth, + rotation, + textAlign: _toLeftRightCenter(opts.align), + textBaseline: 'middle', + translation: [titleX, titleY], + }); + } +} +function createTitle(chart, titleOpts) { + const title = new Title({ + ctx: chart.ctx, + options: titleOpts, + chart + }); + layouts.configure(chart, title, titleOpts); + layouts.addBox(chart, title); + chart.titleBlock = title; +} +var plugin_title = { + id: 'title', + _element: Title, + start(chart, _args, options) { + createTitle(chart, options); + }, + stop(chart) { + const titleBlock = chart.titleBlock; + layouts.removeBox(chart, titleBlock); + delete chart.titleBlock; + }, + beforeUpdate(chart, _args, options) { + const title = chart.titleBlock; + layouts.configure(chart, title, options); + title.options = options; + }, + defaults: { + align: 'center', + display: false, + font: { + weight: 'bold', + }, + fullSize: true, + padding: 10, + position: 'top', + text: '', + weight: 2000 + }, + defaultRoutes: { + color: 'color' + }, + descriptors: { + _scriptable: true, + _indexable: false, + }, +}; + +const map = new WeakMap(); +var plugin_subtitle = { + id: 'subtitle', + start(chart, _args, options) { + const title = new Title({ + ctx: chart.ctx, + options, + chart + }); + layouts.configure(chart, title, options); + layouts.addBox(chart, title); + map.set(chart, title); + }, + stop(chart) { + layouts.removeBox(chart, map.get(chart)); + map.delete(chart); + }, + beforeUpdate(chart, _args, options) { + const title = map.get(chart); + layouts.configure(chart, title, options); + title.options = options; + }, + defaults: { + align: 'center', + display: false, + font: { + weight: 'normal', + }, + fullSize: true, + padding: 0, + position: 'top', + text: '', + weight: 1500 + }, + defaultRoutes: { + color: 'color' + }, + descriptors: { + _scriptable: true, + _indexable: false, + }, +}; + +const positioners = { + average(items) { + if (!items.length) { + return false; + } + let i, len; + let x = 0; + let y = 0; + let count = 0; + for (i = 0, len = items.length; i < len; ++i) { + const el = items[i].element; + if (el && el.hasValue()) { + const pos = el.tooltipPosition(); + x += pos.x; + y += pos.y; + ++count; + } + } + return { + x: x / count, + y: y / count + }; + }, + nearest(items, eventPosition) { + if (!items.length) { + return false; + } + let x = eventPosition.x; + let y = eventPosition.y; + let minDistance = Number.POSITIVE_INFINITY; + let i, len, nearestElement; + for (i = 0, len = items.length; i < len; ++i) { + const el = items[i].element; + if (el && el.hasValue()) { + const center = el.getCenterPoint(); + const d = distanceBetweenPoints(eventPosition, center); + if (d < minDistance) { + minDistance = d; + nearestElement = el; + } + } + } + if (nearestElement) { + const tp = nearestElement.tooltipPosition(); + x = tp.x; + y = tp.y; + } + return { + x, + y + }; + } +}; +function pushOrConcat(base, toPush) { + if (toPush) { + if (isArray(toPush)) { + Array.prototype.push.apply(base, toPush); + } else { + base.push(toPush); + } + } + return base; +} +function splitNewlines(str) { + if ((typeof str === 'string' || str instanceof String) && str.indexOf('\n') > -1) { + return str.split('\n'); + } + return str; +} +function createTooltipItem(chart, item) { + const {element, datasetIndex, index} = item; + const controller = chart.getDatasetMeta(datasetIndex).controller; + const {label, value} = controller.getLabelAndValue(index); + return { + chart, + label, + parsed: controller.getParsed(index), + raw: chart.data.datasets[datasetIndex].data[index], + formattedValue: value, + dataset: controller.getDataset(), + dataIndex: index, + datasetIndex, + element + }; +} +function getTooltipSize(tooltip, options) { + const ctx = tooltip._chart.ctx; + const {body, footer, title} = tooltip; + const {boxWidth, boxHeight} = options; + const bodyFont = toFont(options.bodyFont); + const titleFont = toFont(options.titleFont); + const footerFont = toFont(options.footerFont); + const titleLineCount = title.length; + const footerLineCount = footer.length; + const bodyLineItemCount = body.length; + const padding = toPadding(options.padding); + let height = padding.height; + let width = 0; + let combinedBodyLength = body.reduce((count, bodyItem) => count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length, 0); + combinedBodyLength += tooltip.beforeBody.length + tooltip.afterBody.length; + if (titleLineCount) { + height += titleLineCount * titleFont.lineHeight + + (titleLineCount - 1) * options.titleSpacing + + options.titleMarginBottom; + } + if (combinedBodyLength) { + const bodyLineHeight = options.displayColors ? Math.max(boxHeight, bodyFont.lineHeight) : bodyFont.lineHeight; + height += bodyLineItemCount * bodyLineHeight + + (combinedBodyLength - bodyLineItemCount) * bodyFont.lineHeight + + (combinedBodyLength - 1) * options.bodySpacing; + } + if (footerLineCount) { + height += options.footerMarginTop + + footerLineCount * footerFont.lineHeight + + (footerLineCount - 1) * options.footerSpacing; + } + let widthPadding = 0; + const maxLineWidth = function(line) { + width = Math.max(width, ctx.measureText(line).width + widthPadding); + }; + ctx.save(); + ctx.font = titleFont.string; + each(tooltip.title, maxLineWidth); + ctx.font = bodyFont.string; + each(tooltip.beforeBody.concat(tooltip.afterBody), maxLineWidth); + widthPadding = options.displayColors ? (boxWidth + 2) : 0; + each(body, (bodyItem) => { + each(bodyItem.before, maxLineWidth); + each(bodyItem.lines, maxLineWidth); + each(bodyItem.after, maxLineWidth); + }); + widthPadding = 0; + ctx.font = footerFont.string; + each(tooltip.footer, maxLineWidth); + ctx.restore(); + width += padding.width; + return {width, height}; +} +function determineYAlign(chart, size) { + const {y, height} = size; + if (y < height / 2) { + return 'top'; + } else if (y > (chart.height - height / 2)) { + return 'bottom'; + } + return 'center'; +} +function doesNotFitWithAlign(xAlign, chart, options, size) { + const {x, width} = size; + const caret = options.caretSize + options.caretPadding; + if (xAlign === 'left' && x + width + caret > chart.width) { + return true; + } + if (xAlign === 'right' && x - width - caret < 0) { + return true; + } +} +function determineXAlign(chart, options, size, yAlign) { + const {x, width} = size; + const {width: chartWidth, chartArea: {left, right}} = chart; + let xAlign = 'center'; + if (yAlign === 'center') { + xAlign = x <= (left + right) / 2 ? 'left' : 'right'; + } else if (x <= width / 2) { + xAlign = 'left'; + } else if (x >= chartWidth - width / 2) { + xAlign = 'right'; + } + if (doesNotFitWithAlign(xAlign, chart, options, size)) { + xAlign = 'center'; + } + return xAlign; +} +function determineAlignment(chart, options, size) { + const yAlign = options.yAlign || determineYAlign(chart, size); + return { + xAlign: options.xAlign || determineXAlign(chart, options, size, yAlign), + yAlign + }; +} +function alignX(size, xAlign) { + let {x, width} = size; + if (xAlign === 'right') { + x -= width; + } else if (xAlign === 'center') { + x -= (width / 2); + } + return x; +} +function alignY(size, yAlign, paddingAndSize) { + let {y, height} = size; + if (yAlign === 'top') { + y += paddingAndSize; + } else if (yAlign === 'bottom') { + y -= height + paddingAndSize; + } else { + y -= (height / 2); + } + return y; +} +function getBackgroundPoint(options, size, alignment, chart) { + const {caretSize, caretPadding, cornerRadius} = options; + const {xAlign, yAlign} = alignment; + const paddingAndSize = caretSize + caretPadding; + const radiusAndPadding = cornerRadius + caretPadding; + let x = alignX(size, xAlign); + const y = alignY(size, yAlign, paddingAndSize); + if (yAlign === 'center') { + if (xAlign === 'left') { + x += paddingAndSize; + } else if (xAlign === 'right') { + x -= paddingAndSize; + } + } else if (xAlign === 'left') { + x -= radiusAndPadding; + } else if (xAlign === 'right') { + x += radiusAndPadding; + } + return { + x: _limitValue(x, 0, chart.width - size.width), + y: _limitValue(y, 0, chart.height - size.height) + }; +} +function getAlignedX(tooltip, align, options) { + const padding = toPadding(options.padding); + return align === 'center' + ? tooltip.x + tooltip.width / 2 + : align === 'right' + ? tooltip.x + tooltip.width - padding.right + : tooltip.x + padding.left; +} +function getBeforeAfterBodyLines(callback) { + return pushOrConcat([], splitNewlines(callback)); +} +function createTooltipContext(parent, tooltip, tooltipItems) { + return Object.assign(Object.create(parent), { + tooltip, + tooltipItems, + type: 'tooltip' + }); +} +function overrideCallbacks(callbacks, context) { + const override = context && context.dataset && context.dataset.tooltip && context.dataset.tooltip.callbacks; + return override ? callbacks.override(override) : callbacks; +} +class Tooltip extends Element { + constructor(config) { + super(); + this.opacity = 0; + this._active = []; + this._chart = config._chart; + this._eventPosition = undefined; + this._size = undefined; + this._cachedAnimations = undefined; + this._tooltipItems = []; + this.$animations = undefined; + this.$context = undefined; + this.options = config.options; + this.dataPoints = undefined; + this.title = undefined; + this.beforeBody = undefined; + this.body = undefined; + this.afterBody = undefined; + this.footer = undefined; + this.xAlign = undefined; + this.yAlign = undefined; + this.x = undefined; + this.y = undefined; + this.height = undefined; + this.width = undefined; + this.caretX = undefined; + this.caretY = undefined; + this.labelColors = undefined; + this.labelPointStyles = undefined; + this.labelTextColors = undefined; + } + initialize(options) { + this.options = options; + this._cachedAnimations = undefined; + this.$context = undefined; + } + _resolveAnimations() { + const me = this; + const cached = me._cachedAnimations; + if (cached) { + return cached; + } + const chart = me._chart; + const options = me.options.setContext(me.getContext()); + const opts = options.enabled && chart.options.animation && options.animations; + const animations = new Animations(me._chart, opts); + if (opts._cacheable) { + me._cachedAnimations = Object.freeze(animations); + } + return animations; + } + getContext() { + const me = this; + return me.$context || + (me.$context = createTooltipContext(me._chart.getContext(), me, me._tooltipItems)); + } + getTitle(context, options) { + const me = this; + const {callbacks} = options; + const beforeTitle = callbacks.beforeTitle.apply(me, [context]); + const title = callbacks.title.apply(me, [context]); + const afterTitle = callbacks.afterTitle.apply(me, [context]); + let lines = []; + lines = pushOrConcat(lines, splitNewlines(beforeTitle)); + lines = pushOrConcat(lines, splitNewlines(title)); + lines = pushOrConcat(lines, splitNewlines(afterTitle)); + return lines; + } + getBeforeBody(tooltipItems, options) { + return getBeforeAfterBodyLines(options.callbacks.beforeBody.apply(this, [tooltipItems])); + } + getBody(tooltipItems, options) { + const me = this; + const {callbacks} = options; + const bodyItems = []; + each(tooltipItems, (context) => { + const bodyItem = { + before: [], + lines: [], + after: [] + }; + const scoped = overrideCallbacks(callbacks, context); + pushOrConcat(bodyItem.before, splitNewlines(scoped.beforeLabel.call(me, context))); + pushOrConcat(bodyItem.lines, scoped.label.call(me, context)); + pushOrConcat(bodyItem.after, splitNewlines(scoped.afterLabel.call(me, context))); + bodyItems.push(bodyItem); + }); + return bodyItems; + } + getAfterBody(tooltipItems, options) { + return getBeforeAfterBodyLines(options.callbacks.afterBody.apply(this, [tooltipItems])); + } + getFooter(tooltipItems, options) { + const me = this; + const {callbacks} = options; + const beforeFooter = callbacks.beforeFooter.apply(me, [tooltipItems]); + const footer = callbacks.footer.apply(me, [tooltipItems]); + const afterFooter = callbacks.afterFooter.apply(me, [tooltipItems]); + let lines = []; + lines = pushOrConcat(lines, splitNewlines(beforeFooter)); + lines = pushOrConcat(lines, splitNewlines(footer)); + lines = pushOrConcat(lines, splitNewlines(afterFooter)); + return lines; + } + _createItems(options) { + const me = this; + const active = me._active; + const data = me._chart.data; + const labelColors = []; + const labelPointStyles = []; + const labelTextColors = []; + let tooltipItems = []; + let i, len; + for (i = 0, len = active.length; i < len; ++i) { + tooltipItems.push(createTooltipItem(me._chart, active[i])); + } + if (options.filter) { + tooltipItems = tooltipItems.filter((element, index, array) => options.filter(element, index, array, data)); + } + if (options.itemSort) { + tooltipItems = tooltipItems.sort((a, b) => options.itemSort(a, b, data)); + } + each(tooltipItems, (context) => { + const scoped = overrideCallbacks(options.callbacks, context); + labelColors.push(scoped.labelColor.call(me, context)); + labelPointStyles.push(scoped.labelPointStyle.call(me, context)); + labelTextColors.push(scoped.labelTextColor.call(me, context)); + }); + me.labelColors = labelColors; + me.labelPointStyles = labelPointStyles; + me.labelTextColors = labelTextColors; + me.dataPoints = tooltipItems; + return tooltipItems; + } + update(changed, replay) { + const me = this; + const options = me.options.setContext(me.getContext()); + const active = me._active; + let properties; + let tooltipItems = []; + if (!active.length) { + if (me.opacity !== 0) { + properties = { + opacity: 0 + }; + } + } else { + const position = positioners[options.position].call(me, active, me._eventPosition); + tooltipItems = me._createItems(options); + me.title = me.getTitle(tooltipItems, options); + me.beforeBody = me.getBeforeBody(tooltipItems, options); + me.body = me.getBody(tooltipItems, options); + me.afterBody = me.getAfterBody(tooltipItems, options); + me.footer = me.getFooter(tooltipItems, options); + const size = me._size = getTooltipSize(me, options); + const positionAndSize = Object.assign({}, position, size); + const alignment = determineAlignment(me._chart, options, positionAndSize); + const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, me._chart); + me.xAlign = alignment.xAlign; + me.yAlign = alignment.yAlign; + properties = { + opacity: 1, + x: backgroundPoint.x, + y: backgroundPoint.y, + width: size.width, + height: size.height, + caretX: position.x, + caretY: position.y + }; + } + me._tooltipItems = tooltipItems; + me.$context = undefined; + if (properties) { + me._resolveAnimations().update(me, properties); + } + if (changed && options.external) { + options.external.call(me, {chart: me._chart, tooltip: me, replay}); + } + } + drawCaret(tooltipPoint, ctx, size, options) { + const caretPosition = this.getCaretPosition(tooltipPoint, size, options); + ctx.lineTo(caretPosition.x1, caretPosition.y1); + ctx.lineTo(caretPosition.x2, caretPosition.y2); + ctx.lineTo(caretPosition.x3, caretPosition.y3); + } + getCaretPosition(tooltipPoint, size, options) { + const {xAlign, yAlign} = this; + const {cornerRadius, caretSize} = options; + const {x: ptX, y: ptY} = tooltipPoint; + const {width, height} = size; + let x1, x2, x3, y1, y2, y3; + if (yAlign === 'center') { + y2 = ptY + (height / 2); + if (xAlign === 'left') { + x1 = ptX; + x2 = x1 - caretSize; + y1 = y2 + caretSize; + y3 = y2 - caretSize; + } else { + x1 = ptX + width; + x2 = x1 + caretSize; + y1 = y2 - caretSize; + y3 = y2 + caretSize; + } + x3 = x1; + } else { + if (xAlign === 'left') { + x2 = ptX + cornerRadius + (caretSize); + } else if (xAlign === 'right') { + x2 = ptX + width - cornerRadius - caretSize; + } else { + x2 = this.caretX; + } + if (yAlign === 'top') { + y1 = ptY; + y2 = y1 - caretSize; + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } else { + y1 = ptY + height; + y2 = y1 + caretSize; + x1 = x2 + caretSize; + x3 = x2 - caretSize; + } + y3 = y1; + } + return {x1, x2, x3, y1, y2, y3}; + } + drawTitle(pt, ctx, options) { + const me = this; + const title = me.title; + const length = title.length; + let titleFont, titleSpacing, i; + if (length) { + const rtlHelper = getRtlAdapter(options.rtl, me.x, me.width); + pt.x = getAlignedX(me, options.titleAlign, options); + ctx.textAlign = rtlHelper.textAlign(options.titleAlign); + ctx.textBaseline = 'middle'; + titleFont = toFont(options.titleFont); + titleSpacing = options.titleSpacing; + ctx.fillStyle = options.titleColor; + ctx.font = titleFont.string; + for (i = 0; i < length; ++i) { + ctx.fillText(title[i], rtlHelper.x(pt.x), pt.y + titleFont.lineHeight / 2); + pt.y += titleFont.lineHeight + titleSpacing; + if (i + 1 === length) { + pt.y += options.titleMarginBottom - titleSpacing; + } + } + } + } + _drawColorBox(ctx, pt, i, rtlHelper, options) { + const me = this; + const labelColors = me.labelColors[i]; + const labelPointStyle = me.labelPointStyles[i]; + const {boxHeight, boxWidth} = options; + const bodyFont = toFont(options.bodyFont); + const colorX = getAlignedX(me, 'left', options); + const rtlColorX = rtlHelper.x(colorX); + const yOffSet = boxHeight < bodyFont.lineHeight ? (bodyFont.lineHeight - boxHeight) / 2 : 0; + const colorY = pt.y + yOffSet; + if (options.usePointStyle) { + const drawOptions = { + radius: Math.min(boxWidth, boxHeight) / 2, + pointStyle: labelPointStyle.pointStyle, + rotation: labelPointStyle.rotation, + borderWidth: 1 + }; + const centerX = rtlHelper.leftForLtr(rtlColorX, boxWidth) + boxWidth / 2; + const centerY = colorY + boxHeight / 2; + ctx.strokeStyle = options.multiKeyBackground; + ctx.fillStyle = options.multiKeyBackground; + drawPoint(ctx, drawOptions, centerX, centerY); + ctx.strokeStyle = labelColors.borderColor; + ctx.fillStyle = labelColors.backgroundColor; + drawPoint(ctx, drawOptions, centerX, centerY); + } else { + ctx.lineWidth = labelColors.borderWidth || 1; + ctx.strokeStyle = labelColors.borderColor; + ctx.setLineDash(labelColors.borderDash || []); + ctx.lineDashOffset = labelColors.borderDashOffset || 0; + const outerX = rtlHelper.leftForLtr(rtlColorX, boxWidth); + const innerX = rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), boxWidth - 2); + const borderRadius = toTRBLCorners(labelColors.borderRadius); + if (Object.values(borderRadius).some(v => v !== 0)) { + ctx.beginPath(); + ctx.fillStyle = options.multiKeyBackground; + addRoundedRectPath(ctx, { + x: outerX, + y: colorY, + w: boxWidth, + h: boxHeight, + radius: borderRadius, + }); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = labelColors.backgroundColor; + ctx.beginPath(); + addRoundedRectPath(ctx, { + x: innerX, + y: colorY + 1, + w: boxWidth - 2, + h: boxHeight - 2, + radius: borderRadius, + }); + ctx.fill(); + } else { + ctx.fillStyle = options.multiKeyBackground; + ctx.fillRect(outerX, colorY, boxWidth, boxHeight); + ctx.strokeRect(outerX, colorY, boxWidth, boxHeight); + ctx.fillStyle = labelColors.backgroundColor; + ctx.fillRect(innerX, colorY + 1, boxWidth - 2, boxHeight - 2); + } + } + ctx.fillStyle = me.labelTextColors[i]; + } + drawBody(pt, ctx, options) { + const me = this; + const {body} = me; + const {bodySpacing, bodyAlign, displayColors, boxHeight, boxWidth} = options; + const bodyFont = toFont(options.bodyFont); + let bodyLineHeight = bodyFont.lineHeight; + let xLinePadding = 0; + const rtlHelper = getRtlAdapter(options.rtl, me.x, me.width); + const fillLineOfText = function(line) { + ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyLineHeight / 2); + pt.y += bodyLineHeight + bodySpacing; + }; + const bodyAlignForCalculation = rtlHelper.textAlign(bodyAlign); + let bodyItem, textColor, lines, i, j, ilen, jlen; + ctx.textAlign = bodyAlign; + ctx.textBaseline = 'middle'; + ctx.font = bodyFont.string; + pt.x = getAlignedX(me, bodyAlignForCalculation, options); + ctx.fillStyle = options.bodyColor; + each(me.beforeBody, fillLineOfText); + xLinePadding = displayColors && bodyAlignForCalculation !== 'right' + ? bodyAlign === 'center' ? (boxWidth / 2 + 1) : (boxWidth + 2) + : 0; + for (i = 0, ilen = body.length; i < ilen; ++i) { + bodyItem = body[i]; + textColor = me.labelTextColors[i]; + ctx.fillStyle = textColor; + each(bodyItem.before, fillLineOfText); + lines = bodyItem.lines; + if (displayColors && lines.length) { + me._drawColorBox(ctx, pt, i, rtlHelper, options); + bodyLineHeight = Math.max(bodyFont.lineHeight, boxHeight); + } + for (j = 0, jlen = lines.length; j < jlen; ++j) { + fillLineOfText(lines[j]); + bodyLineHeight = bodyFont.lineHeight; + } + each(bodyItem.after, fillLineOfText); + } + xLinePadding = 0; + bodyLineHeight = bodyFont.lineHeight; + each(me.afterBody, fillLineOfText); + pt.y -= bodySpacing; + } + drawFooter(pt, ctx, options) { + const me = this; + const footer = me.footer; + const length = footer.length; + let footerFont, i; + if (length) { + const rtlHelper = getRtlAdapter(options.rtl, me.x, me.width); + pt.x = getAlignedX(me, options.footerAlign, options); + pt.y += options.footerMarginTop; + ctx.textAlign = rtlHelper.textAlign(options.footerAlign); + ctx.textBaseline = 'middle'; + footerFont = toFont(options.footerFont); + ctx.fillStyle = options.footerColor; + ctx.font = footerFont.string; + for (i = 0; i < length; ++i) { + ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFont.lineHeight / 2); + pt.y += footerFont.lineHeight + options.footerSpacing; + } + } + } + drawBackground(pt, ctx, tooltipSize, options) { + const {xAlign, yAlign} = this; + const {x, y} = pt; + const {width, height} = tooltipSize; + const radius = options.cornerRadius; + ctx.fillStyle = options.backgroundColor; + ctx.strokeStyle = options.borderColor; + ctx.lineWidth = options.borderWidth; + ctx.beginPath(); + ctx.moveTo(x + radius, y); + if (yAlign === 'top') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + if (yAlign === 'center' && xAlign === 'right') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + if (yAlign === 'bottom') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + if (yAlign === 'center' && xAlign === 'left') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + ctx.fill(); + if (options.borderWidth > 0) { + ctx.stroke(); + } + } + _updateAnimationTarget(options) { + const me = this; + const chart = me._chart; + const anims = me.$animations; + const animX = anims && anims.x; + const animY = anims && anims.y; + if (animX || animY) { + const position = positioners[options.position].call(me, me._active, me._eventPosition); + if (!position) { + return; + } + const size = me._size = getTooltipSize(me, options); + const positionAndSize = Object.assign({}, position, me._size); + const alignment = determineAlignment(chart, options, positionAndSize); + const point = getBackgroundPoint(options, positionAndSize, alignment, chart); + if (animX._to !== point.x || animY._to !== point.y) { + me.xAlign = alignment.xAlign; + me.yAlign = alignment.yAlign; + me.width = size.width; + me.height = size.height; + me.caretX = position.x; + me.caretY = position.y; + me._resolveAnimations().update(me, point); + } + } + } + draw(ctx) { + const me = this; + const options = me.options.setContext(me.getContext()); + let opacity = me.opacity; + if (!opacity) { + return; + } + me._updateAnimationTarget(options); + const tooltipSize = { + width: me.width, + height: me.height + }; + const pt = { + x: me.x, + y: me.y + }; + opacity = Math.abs(opacity) < 1e-3 ? 0 : opacity; + const padding = toPadding(options.padding); + const hasTooltipContent = me.title.length || me.beforeBody.length || me.body.length || me.afterBody.length || me.footer.length; + if (options.enabled && hasTooltipContent) { + ctx.save(); + ctx.globalAlpha = opacity; + me.drawBackground(pt, ctx, tooltipSize, options); + overrideTextDirection(ctx, options.textDirection); + pt.y += padding.top; + me.drawTitle(pt, ctx, options); + me.drawBody(pt, ctx, options); + me.drawFooter(pt, ctx, options); + restoreTextDirection(ctx, options.textDirection); + ctx.restore(); + } + } + getActiveElements() { + return this._active || []; + } + setActiveElements(activeElements, eventPosition) { + const me = this; + const lastActive = me._active; + const active = activeElements.map(({datasetIndex, index}) => { + const meta = me._chart.getDatasetMeta(datasetIndex); + if (!meta) { + throw new Error('Cannot find a dataset at index ' + datasetIndex); + } + return { + datasetIndex, + element: meta.data[index], + index, + }; + }); + const changed = !_elementsEqual(lastActive, active); + const positionChanged = me._positionChanged(active, eventPosition); + if (changed || positionChanged) { + me._active = active; + me._eventPosition = eventPosition; + me.update(true); + } + } + handleEvent(e, replay) { + const me = this; + const options = me.options; + const lastActive = me._active || []; + let changed = false; + let active = []; + if (e.type !== 'mouseout') { + active = me._chart.getElementsAtEventForMode(e, options.mode, options, replay); + if (options.reverse) { + active.reverse(); + } + } + const positionChanged = me._positionChanged(active, e); + changed = replay || !_elementsEqual(active, lastActive) || positionChanged; + if (changed) { + me._active = active; + if (options.enabled || options.external) { + me._eventPosition = { + x: e.x, + y: e.y + }; + me.update(true, replay); + } + } + return changed; + } + _positionChanged(active, e) { + const {caretX, caretY, options} = this; + const position = positioners[options.position].call(this, active, e); + return position !== false && (caretX !== position.x || caretY !== position.y); + } +} +Tooltip.positioners = positioners; +var plugin_tooltip = { + id: 'tooltip', + _element: Tooltip, + positioners, + afterInit(chart, _args, options) { + if (options) { + chart.tooltip = new Tooltip({_chart: chart, options}); + } + }, + beforeUpdate(chart, _args, options) { + if (chart.tooltip) { + chart.tooltip.initialize(options); + } + }, + reset(chart, _args, options) { + if (chart.tooltip) { + chart.tooltip.initialize(options); + } + }, + afterDraw(chart) { + const tooltip = chart.tooltip; + const args = { + tooltip + }; + if (chart.notifyPlugins('beforeTooltipDraw', args) === false) { + return; + } + if (tooltip) { + tooltip.draw(chart.ctx); + } + chart.notifyPlugins('afterTooltipDraw', args); + }, + afterEvent(chart, args) { + if (chart.tooltip) { + const useFinalPosition = args.replay; + if (chart.tooltip.handleEvent(args.event, useFinalPosition)) { + args.changed = true; + } + } + }, + defaults: { + enabled: true, + external: null, + position: 'average', + backgroundColor: 'rgba(0,0,0,0.8)', + titleColor: '#fff', + titleFont: { + weight: 'bold', + }, + titleSpacing: 2, + titleMarginBottom: 6, + titleAlign: 'left', + bodyColor: '#fff', + bodySpacing: 2, + bodyFont: { + }, + bodyAlign: 'left', + footerColor: '#fff', + footerSpacing: 2, + footerMarginTop: 6, + footerFont: { + weight: 'bold', + }, + footerAlign: 'left', + padding: 6, + caretPadding: 2, + caretSize: 5, + cornerRadius: 6, + boxHeight: (ctx, opts) => opts.bodyFont.size, + boxWidth: (ctx, opts) => opts.bodyFont.size, + multiKeyBackground: '#fff', + displayColors: true, + borderColor: 'rgba(0,0,0,0)', + borderWidth: 0, + animation: { + duration: 400, + easing: 'easeOutQuart', + }, + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'width', 'height', 'caretX', 'caretY'], + }, + opacity: { + easing: 'linear', + duration: 200 + } + }, + callbacks: { + beforeTitle: noop, + title(tooltipItems) { + if (tooltipItems.length > 0) { + const item = tooltipItems[0]; + const labels = item.chart.data.labels; + const labelCount = labels ? labels.length : 0; + if (this && this.options && this.options.mode === 'dataset') { + return item.dataset.label || ''; + } else if (item.label) { + return item.label; + } else if (labelCount > 0 && item.dataIndex < labelCount) { + return labels[item.dataIndex]; + } + } + return ''; + }, + afterTitle: noop, + beforeBody: noop, + beforeLabel: noop, + label(tooltipItem) { + if (this && this.options && this.options.mode === 'dataset') { + return tooltipItem.label + ': ' + tooltipItem.formattedValue || tooltipItem.formattedValue; + } + let label = tooltipItem.dataset.label || ''; + if (label) { + label += ': '; + } + const value = tooltipItem.formattedValue; + if (!isNullOrUndef(value)) { + label += value; + } + return label; + }, + labelColor(tooltipItem) { + const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); + const options = meta.controller.getStyle(tooltipItem.dataIndex); + return { + borderColor: options.borderColor, + backgroundColor: options.backgroundColor, + borderWidth: options.borderWidth, + borderDash: options.borderDash, + borderDashOffset: options.borderDashOffset, + borderRadius: 0, + }; + }, + labelTextColor() { + return this.options.bodyColor; + }, + labelPointStyle(tooltipItem) { + const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); + const options = meta.controller.getStyle(tooltipItem.dataIndex); + return { + pointStyle: options.pointStyle, + rotation: options.rotation, + }; + }, + afterLabel: noop, + afterBody: noop, + beforeFooter: noop, + footer: noop, + afterFooter: noop + } + }, + defaultRoutes: { + bodyFont: 'font', + footerFont: 'font', + titleFont: 'font' + }, + descriptors: { + _scriptable: (name) => name !== 'filter' && name !== 'itemSort' && name !== 'external', + _indexable: false, + callbacks: { + _scriptable: false, + _indexable: false, + }, + animation: { + _fallback: false + }, + animations: { + _fallback: 'animation' + } + }, + additionalOptionScopes: ['interaction'] +}; + +var plugins = /*#__PURE__*/Object.freeze({ +__proto__: null, +Decimation: plugin_decimation, +Filler: plugin_filler, +Legend: plugin_legend, +SubTitle: plugin_subtitle, +Title: plugin_title, +Tooltip: plugin_tooltip +}); + +const addIfString = (labels, raw, index) => typeof raw === 'string' + ? labels.push(raw) - 1 + : isNaN(raw) ? null : index; +function findOrAddLabel(labels, raw, index) { + const first = labels.indexOf(raw); + if (first === -1) { + return addIfString(labels, raw, index); + } + const last = labels.lastIndexOf(raw); + return first !== last ? index : first; +} +const validIndex = (index, max) => index === null ? null : _limitValue(Math.round(index), 0, max); +class CategoryScale extends Scale { + constructor(cfg) { + super(cfg); + this._startValue = undefined; + this._valueRange = 0; + } + parse(raw, index) { + if (isNullOrUndef(raw)) { + return null; + } + const labels = this.getLabels(); + index = isFinite(index) && labels[index] === raw ? index + : findOrAddLabel(labels, raw, valueOrDefault(index, raw)); + return validIndex(index, labels.length - 1); + } + determineDataLimits() { + const me = this; + const {minDefined, maxDefined} = me.getUserBounds(); + let {min, max} = me.getMinMax(true); + if (me.options.bounds === 'ticks') { + if (!minDefined) { + min = 0; + } + if (!maxDefined) { + max = me.getLabels().length - 1; + } + } + me.min = min; + me.max = max; + } + buildTicks() { + const me = this; + const min = me.min; + const max = me.max; + const offset = me.options.offset; + const ticks = []; + let labels = me.getLabels(); + labels = (min === 0 && max === labels.length - 1) ? labels : labels.slice(min, max + 1); + me._valueRange = Math.max(labels.length - (offset ? 0 : 1), 1); + me._startValue = me.min - (offset ? 0.5 : 0); + for (let value = min; value <= max; value++) { + ticks.push({value}); + } + return ticks; + } + getLabelForValue(value) { + const me = this; + const labels = me.getLabels(); + if (value >= 0 && value < labels.length) { + return labels[value]; + } + return value; + } + configure() { + const me = this; + super.configure(); + if (!me.isHorizontal()) { + me._reversePixels = !me._reversePixels; + } + } + getPixelForValue(value) { + const me = this; + if (typeof value !== 'number') { + value = me.parse(value); + } + return value === null ? NaN : me.getPixelForDecimal((value - me._startValue) / me._valueRange); + } + getPixelForTick(index) { + const me = this; + const ticks = me.ticks; + if (index < 0 || index > ticks.length - 1) { + return null; + } + return me.getPixelForValue(ticks[index].value); + } + getValueForPixel(pixel) { + const me = this; + return Math.round(me._startValue + me.getDecimalForPixel(pixel) * me._valueRange); + } + getBasePixel() { + return this.bottom; + } +} +CategoryScale.id = 'category'; +CategoryScale.defaults = { + ticks: { + callback: CategoryScale.prototype.getLabelForValue + } +}; + +function generateTicks$1(generationOptions, dataRange) { + const ticks = []; + const MIN_SPACING = 1e-14; + const {bounds, step, min, max, precision, count, maxTicks, maxDigits, includeBounds} = generationOptions; + const unit = step || 1; + const maxSpaces = maxTicks - 1; + const {min: rmin, max: rmax} = dataRange; + const minDefined = !isNullOrUndef(min); + const maxDefined = !isNullOrUndef(max); + const countDefined = !isNullOrUndef(count); + const minSpacing = (rmax - rmin) / (maxDigits + 1); + let spacing = niceNum((rmax - rmin) / maxSpaces / unit) * unit; + let factor, niceMin, niceMax, numSpaces; + if (spacing < MIN_SPACING && !minDefined && !maxDefined) { + return [{value: rmin}, {value: rmax}]; + } + numSpaces = Math.ceil(rmax / spacing) - Math.floor(rmin / spacing); + if (numSpaces > maxSpaces) { + spacing = niceNum(numSpaces * spacing / maxSpaces / unit) * unit; + } + if (!isNullOrUndef(precision)) { + factor = Math.pow(10, precision); + spacing = Math.ceil(spacing * factor) / factor; + } + if (bounds === 'ticks') { + niceMin = Math.floor(rmin / spacing) * spacing; + niceMax = Math.ceil(rmax / spacing) * spacing; + } else { + niceMin = rmin; + niceMax = rmax; + } + if (minDefined && maxDefined && step && almostWhole((max - min) / step, spacing / 1000)) { + numSpaces = Math.round(Math.min((max - min) / spacing, maxTicks)); + spacing = (max - min) / numSpaces; + niceMin = min; + niceMax = max; + } else if (countDefined) { + niceMin = minDefined ? min : niceMin; + niceMax = maxDefined ? max : niceMax; + numSpaces = count - 1; + spacing = (niceMax - niceMin) / numSpaces; + } else { + numSpaces = (niceMax - niceMin) / spacing; + if (almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { + numSpaces = Math.round(numSpaces); + } else { + numSpaces = Math.ceil(numSpaces); + } + } + const decimalPlaces = Math.max( + _decimalPlaces(spacing), + _decimalPlaces(niceMin) + ); + factor = Math.pow(10, isNullOrUndef(precision) ? decimalPlaces : precision); + niceMin = Math.round(niceMin * factor) / factor; + niceMax = Math.round(niceMax * factor) / factor; + let j = 0; + if (minDefined) { + if (includeBounds && niceMin !== min) { + ticks.push({value: min}); + if (niceMin < min) { + j++; + } + if (almostEquals(Math.round((niceMin + j * spacing) * factor) / factor, min, relativeLabelSize(min, minSpacing, generationOptions))) { + j++; + } + } else if (niceMin < min) { + j++; + } + } + for (; j < numSpaces; ++j) { + ticks.push({value: Math.round((niceMin + j * spacing) * factor) / factor}); + } + if (maxDefined && includeBounds && niceMax !== max) { + if (almostEquals(ticks[ticks.length - 1].value, max, relativeLabelSize(max, minSpacing, generationOptions))) { + ticks[ticks.length - 1].value = max; + } else { + ticks.push({value: max}); + } + } else if (!maxDefined || niceMax === max) { + ticks.push({value: niceMax}); + } + return ticks; +} +function relativeLabelSize(value, minSpacing, {horizontal, minRotation}) { + const rad = toRadians(minRotation); + const ratio = (horizontal ? Math.sin(rad) : Math.cos(rad)) || 0.001; + const length = 0.75 * minSpacing * ('' + value).length; + return Math.min(minSpacing / ratio, length); +} +class LinearScaleBase extends Scale { + constructor(cfg) { + super(cfg); + this.start = undefined; + this.end = undefined; + this._startValue = undefined; + this._endValue = undefined; + this._valueRange = 0; + } + parse(raw, index) { + if (isNullOrUndef(raw)) { + return null; + } + if ((typeof raw === 'number' || raw instanceof Number) && !isFinite(+raw)) { + return null; + } + return +raw; + } + handleTickRangeOptions() { + const me = this; + const {beginAtZero} = me.options; + const {minDefined, maxDefined} = me.getUserBounds(); + let {min, max} = me; + const setMin = v => (min = minDefined ? min : v); + const setMax = v => (max = maxDefined ? max : v); + if (beginAtZero) { + const minSign = sign(min); + const maxSign = sign(max); + if (minSign < 0 && maxSign < 0) { + setMax(0); + } else if (minSign > 0 && maxSign > 0) { + setMin(0); + } + } + if (min === max) { + setMax(max + 1); + if (!beginAtZero) { + setMin(min - 1); + } + } + me.min = min; + me.max = max; + } + getTickLimit() { + const me = this; + const tickOpts = me.options.ticks; + let {maxTicksLimit, stepSize} = tickOpts; + let maxTicks; + if (stepSize) { + maxTicks = Math.ceil(me.max / stepSize) - Math.floor(me.min / stepSize) + 1; + } else { + maxTicks = me.computeTickLimit(); + maxTicksLimit = maxTicksLimit || 11; + } + if (maxTicksLimit) { + maxTicks = Math.min(maxTicksLimit, maxTicks); + } + return maxTicks; + } + computeTickLimit() { + return Number.POSITIVE_INFINITY; + } + buildTicks() { + const me = this; + const opts = me.options; + const tickOpts = opts.ticks; + let maxTicks = me.getTickLimit(); + maxTicks = Math.max(2, maxTicks); + const numericGeneratorOptions = { + maxTicks, + bounds: opts.bounds, + min: opts.min, + max: opts.max, + precision: tickOpts.precision, + step: tickOpts.stepSize, + count: tickOpts.count, + maxDigits: me._maxDigits(), + horizontal: me.isHorizontal(), + minRotation: tickOpts.minRotation || 0, + includeBounds: tickOpts.includeBounds !== false + }; + const dataRange = me._range || me; + const ticks = generateTicks$1(numericGeneratorOptions, dataRange); + if (opts.bounds === 'ticks') { + _setMinAndMaxByKey(ticks, me, 'value'); + } + if (opts.reverse) { + ticks.reverse(); + me.start = me.max; + me.end = me.min; + } else { + me.start = me.min; + me.end = me.max; + } + return ticks; + } + configure() { + const me = this; + const ticks = me.ticks; + let start = me.min; + let end = me.max; + super.configure(); + if (me.options.offset && ticks.length) { + const offset = (end - start) / Math.max(ticks.length - 1, 1) / 2; + start -= offset; + end += offset; + } + me._startValue = start; + me._endValue = end; + me._valueRange = end - start; + } + getLabelForValue(value) { + return formatNumber(value, this.chart.options.locale); + } +} + +class LinearScale extends LinearScaleBase { + determineDataLimits() { + const me = this; + const {min, max} = me.getMinMax(true); + me.min = isNumberFinite(min) ? min : 0; + me.max = isNumberFinite(max) ? max : 1; + me.handleTickRangeOptions(); + } + computeTickLimit() { + const me = this; + const horizontal = me.isHorizontal(); + const length = horizontal ? me.width : me.height; + const minRotation = toRadians(me.options.ticks.minRotation); + const ratio = (horizontal ? Math.sin(minRotation) : Math.cos(minRotation)) || 0.001; + const tickFont = me._resolveTickFontOptions(0); + return Math.ceil(length / Math.min(40, tickFont.lineHeight / ratio)); + } + getPixelForValue(value) { + return value === null ? NaN : this.getPixelForDecimal((value - this._startValue) / this._valueRange); + } + getValueForPixel(pixel) { + return this._startValue + this.getDecimalForPixel(pixel) * this._valueRange; + } +} +LinearScale.id = 'linear'; +LinearScale.defaults = { + ticks: { + callback: Ticks.formatters.numeric + } +}; + +function isMajor(tickVal) { + const remain = tickVal / (Math.pow(10, Math.floor(log10(tickVal)))); + return remain === 1; +} +function generateTicks(generationOptions, dataRange) { + const endExp = Math.floor(log10(dataRange.max)); + const endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); + const ticks = []; + let tickVal = finiteOrDefault(generationOptions.min, Math.pow(10, Math.floor(log10(dataRange.min)))); + let exp = Math.floor(log10(tickVal)); + let significand = Math.floor(tickVal / Math.pow(10, exp)); + let precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; + do { + ticks.push({value: tickVal, major: isMajor(tickVal)}); + ++significand; + if (significand === 10) { + significand = 1; + ++exp; + precision = exp >= 0 ? 1 : precision; + } + tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; + } while (exp < endExp || (exp === endExp && significand < endSignificand)); + const lastTick = finiteOrDefault(generationOptions.max, tickVal); + ticks.push({value: lastTick, major: isMajor(tickVal)}); + return ticks; +} +class LogarithmicScale extends Scale { + constructor(cfg) { + super(cfg); + this.start = undefined; + this.end = undefined; + this._startValue = undefined; + this._valueRange = 0; + } + parse(raw, index) { + const value = LinearScaleBase.prototype.parse.apply(this, [raw, index]); + if (value === 0) { + this._zero = true; + return undefined; + } + return isNumberFinite(value) && value > 0 ? value : null; + } + determineDataLimits() { + const me = this; + const {min, max} = me.getMinMax(true); + me.min = isNumberFinite(min) ? Math.max(0, min) : null; + me.max = isNumberFinite(max) ? Math.max(0, max) : null; + if (me.options.beginAtZero) { + me._zero = true; + } + me.handleTickRangeOptions(); + } + handleTickRangeOptions() { + const me = this; + const {minDefined, maxDefined} = me.getUserBounds(); + let min = me.min; + let max = me.max; + const setMin = v => (min = minDefined ? min : v); + const setMax = v => (max = maxDefined ? max : v); + const exp = (v, m) => Math.pow(10, Math.floor(log10(v)) + m); + if (min === max) { + if (min <= 0) { + setMin(1); + setMax(10); + } else { + setMin(exp(min, -1)); + setMax(exp(max, +1)); + } + } + if (min <= 0) { + setMin(exp(max, -1)); + } + if (max <= 0) { + setMax(exp(min, +1)); + } + if (me._zero && me.min !== me._suggestedMin && min === exp(me.min, 0)) { + setMin(exp(min, -1)); + } + me.min = min; + me.max = max; + } + buildTicks() { + const me = this; + const opts = me.options; + const generationOptions = { + min: me._userMin, + max: me._userMax + }; + const ticks = generateTicks(generationOptions, me); + if (opts.bounds === 'ticks') { + _setMinAndMaxByKey(ticks, me, 'value'); + } + if (opts.reverse) { + ticks.reverse(); + me.start = me.max; + me.end = me.min; + } else { + me.start = me.min; + me.end = me.max; + } + return ticks; + } + getLabelForValue(value) { + return value === undefined ? '0' : formatNumber(value, this.chart.options.locale); + } + configure() { + const me = this; + const start = me.min; + super.configure(); + me._startValue = log10(start); + me._valueRange = log10(me.max) - log10(start); + } + getPixelForValue(value) { + const me = this; + if (value === undefined || value === 0) { + value = me.min; + } + if (value === null || isNaN(value)) { + return NaN; + } + return me.getPixelForDecimal(value === me.min + ? 0 + : (log10(value) - me._startValue) / me._valueRange); + } + getValueForPixel(pixel) { + const me = this; + const decimal = me.getDecimalForPixel(pixel); + return Math.pow(10, me._startValue + decimal * me._valueRange); + } +} +LogarithmicScale.id = 'logarithmic'; +LogarithmicScale.defaults = { + ticks: { + callback: Ticks.formatters.logarithmic, + major: { + enabled: true + } + } +}; + +function getTickBackdropHeight(opts) { + const tickOpts = opts.ticks; + if (tickOpts.display && opts.display) { + const padding = toPadding(tickOpts.backdropPadding); + return valueOrDefault(tickOpts.font && tickOpts.font.size, defaults.font.size) + padding.height; + } + return 0; +} +function measureLabelSize(ctx, font, label) { + label = isArray(label) ? label : [label]; + return { + w: _longestText(ctx, font.string, label), + h: label.length * font.lineHeight + }; +} +function determineLimits(angle, pos, size, min, max) { + if (angle === min || angle === max) { + return { + start: pos - (size / 2), + end: pos + (size / 2) + }; + } else if (angle < min || angle > max) { + return { + start: pos - size, + end: pos + }; + } + return { + start: pos, + end: pos + size + }; +} +function fitWithPointLabels(scale) { + const furthestLimits = { + l: 0, + r: scale.width, + t: 0, + b: scale.height - scale.paddingTop + }; + const furthestAngles = {}; + const labelSizes = []; + const padding = []; + const valueCount = scale.getLabels().length; + for (let i = 0; i < valueCount; i++) { + const opts = scale.options.pointLabels.setContext(scale.getContext(i)); + padding[i] = opts.padding; + const pointPosition = scale.getPointPosition(i, scale.drawingArea + padding[i]); + const plFont = toFont(opts.font); + const textSize = measureLabelSize(scale.ctx, plFont, scale._pointLabels[i]); + labelSizes[i] = textSize; + const angleRadians = scale.getIndexAngle(i); + const angle = toDegrees(angleRadians); + const hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180); + const vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270); + if (hLimits.start < furthestLimits.l) { + furthestLimits.l = hLimits.start; + furthestAngles.l = angleRadians; + } + if (hLimits.end > furthestLimits.r) { + furthestLimits.r = hLimits.end; + furthestAngles.r = angleRadians; + } + if (vLimits.start < furthestLimits.t) { + furthestLimits.t = vLimits.start; + furthestAngles.t = angleRadians; + } + if (vLimits.end > furthestLimits.b) { + furthestLimits.b = vLimits.end; + furthestAngles.b = angleRadians; + } + } + scale._setReductions(scale.drawingArea, furthestLimits, furthestAngles); + scale._pointLabelItems = buildPointLabelItems(scale, labelSizes, padding); +} +function buildPointLabelItems(scale, labelSizes, padding) { + const items = []; + const valueCount = scale.getLabels().length; + const opts = scale.options; + const tickBackdropHeight = getTickBackdropHeight(opts); + const outerDistance = scale.getDistanceFromCenterForValue(opts.ticks.reverse ? scale.min : scale.max); + for (let i = 0; i < valueCount; i++) { + const extra = (i === 0 ? tickBackdropHeight / 2 : 0); + const pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + padding[i]); + const angle = toDegrees(scale.getIndexAngle(i)); + const size = labelSizes[i]; + const y = yForAngle(pointLabelPosition.y, size.h, angle); + const textAlign = getTextAlignForAngle(angle); + const left = leftForTextAlign(pointLabelPosition.x, size.w, textAlign); + items.push({ + x: pointLabelPosition.x, + y, + textAlign, + left, + top: y, + right: left + size.w, + bottom: y + size.h + }); + } + return items; +} +function getTextAlignForAngle(angle) { + if (angle === 0 || angle === 180) { + return 'center'; + } else if (angle < 180) { + return 'left'; + } + return 'right'; +} +function leftForTextAlign(x, w, align) { + if (align === 'right') { + x -= w; + } else if (align === 'center') { + x -= (w / 2); + } + return x; +} +function yForAngle(y, h, angle) { + if (angle === 90 || angle === 270) { + y -= (h / 2); + } else if (angle > 270 || angle < 90) { + y -= h; + } + return y; +} +function drawPointLabels(scale, labelCount) { + const {ctx, options: {pointLabels}} = scale; + for (let i = labelCount - 1; i >= 0; i--) { + const optsAtIndex = pointLabels.setContext(scale.getContext(i)); + const plFont = toFont(optsAtIndex.font); + const {x, y, textAlign, left, top, right, bottom} = scale._pointLabelItems[i]; + const {backdropColor} = optsAtIndex; + if (!isNullOrUndef(backdropColor)) { + const padding = toPadding(optsAtIndex.backdropPadding); + ctx.fillStyle = backdropColor; + ctx.fillRect(left - padding.left, top - padding.top, right - left + padding.width, bottom - top + padding.height); + } + renderText( + ctx, + scale._pointLabels[i], + x, + y + (plFont.lineHeight / 2), + plFont, + { + color: optsAtIndex.color, + textAlign: textAlign, + textBaseline: 'middle' + } + ); + } +} +function pathRadiusLine(scale, radius, circular, labelCount) { + const {ctx} = scale; + if (circular) { + ctx.arc(scale.xCenter, scale.yCenter, radius, 0, TAU); + } else { + let pointPosition = scale.getPointPosition(0, radius); + ctx.moveTo(pointPosition.x, pointPosition.y); + for (let i = 1; i < labelCount; i++) { + pointPosition = scale.getPointPosition(i, radius); + ctx.lineTo(pointPosition.x, pointPosition.y); + } + } +} +function drawRadiusLine(scale, gridLineOpts, radius, labelCount) { + const ctx = scale.ctx; + const circular = gridLineOpts.circular; + const {color, lineWidth} = gridLineOpts; + if ((!circular && !labelCount) || !color || !lineWidth || radius < 0) { + return; + } + ctx.save(); + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.setLineDash(gridLineOpts.borderDash); + ctx.lineDashOffset = gridLineOpts.borderDashOffset; + ctx.beginPath(); + pathRadiusLine(scale, radius, circular, labelCount); + ctx.closePath(); + ctx.stroke(); + ctx.restore(); +} +function numberOrZero(param) { + return isNumber(param) ? param : 0; +} +class RadialLinearScale extends LinearScaleBase { + constructor(cfg) { + super(cfg); + this.xCenter = undefined; + this.yCenter = undefined; + this.drawingArea = undefined; + this._pointLabels = []; + this._pointLabelItems = []; + } + setDimensions() { + const me = this; + me.width = me.maxWidth; + me.height = me.maxHeight; + me.paddingTop = getTickBackdropHeight(me.options) / 2; + me.xCenter = Math.floor(me.width / 2); + me.yCenter = Math.floor((me.height - me.paddingTop) / 2); + me.drawingArea = Math.min(me.height - me.paddingTop, me.width) / 2; + } + determineDataLimits() { + const me = this; + const {min, max} = me.getMinMax(false); + me.min = isNumberFinite(min) && !isNaN(min) ? min : 0; + me.max = isNumberFinite(max) && !isNaN(max) ? max : 0; + me.handleTickRangeOptions(); + } + computeTickLimit() { + return Math.ceil(this.drawingArea / getTickBackdropHeight(this.options)); + } + generateTickLabels(ticks) { + const me = this; + LinearScaleBase.prototype.generateTickLabels.call(me, ticks); + me._pointLabels = me.getLabels().map((value, index) => { + const label = callback(me.options.pointLabels.callback, [value, index], me); + return label || label === 0 ? label : ''; + }); + } + fit() { + const me = this; + const opts = me.options; + if (opts.display && opts.pointLabels.display) { + fitWithPointLabels(me); + } else { + me.setCenterPoint(0, 0, 0, 0); + } + } + _setReductions(largestPossibleRadius, furthestLimits, furthestAngles) { + const me = this; + let radiusReductionLeft = furthestLimits.l / Math.sin(furthestAngles.l); + let radiusReductionRight = Math.max(furthestLimits.r - me.width, 0) / Math.sin(furthestAngles.r); + let radiusReductionTop = -furthestLimits.t / Math.cos(furthestAngles.t); + let radiusReductionBottom = -Math.max(furthestLimits.b - (me.height - me.paddingTop), 0) / Math.cos(furthestAngles.b); + radiusReductionLeft = numberOrZero(radiusReductionLeft); + radiusReductionRight = numberOrZero(radiusReductionRight); + radiusReductionTop = numberOrZero(radiusReductionTop); + radiusReductionBottom = numberOrZero(radiusReductionBottom); + me.drawingArea = Math.max(largestPossibleRadius / 2, Math.min( + Math.floor(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2), + Math.floor(largestPossibleRadius - (radiusReductionTop + radiusReductionBottom) / 2))); + me.setCenterPoint(radiusReductionLeft, radiusReductionRight, radiusReductionTop, radiusReductionBottom); + } + setCenterPoint(leftMovement, rightMovement, topMovement, bottomMovement) { + const me = this; + const maxRight = me.width - rightMovement - me.drawingArea; + const maxLeft = leftMovement + me.drawingArea; + const maxTop = topMovement + me.drawingArea; + const maxBottom = (me.height - me.paddingTop) - bottomMovement - me.drawingArea; + me.xCenter = Math.floor(((maxLeft + maxRight) / 2) + me.left); + me.yCenter = Math.floor(((maxTop + maxBottom) / 2) + me.top + me.paddingTop); + } + getIndexAngle(index) { + const angleMultiplier = TAU / this.getLabels().length; + const startAngle = this.options.startAngle || 0; + return _normalizeAngle(index * angleMultiplier + toRadians(startAngle)); + } + getDistanceFromCenterForValue(value) { + const me = this; + if (isNullOrUndef(value)) { + return NaN; + } + const scalingFactor = me.drawingArea / (me.max - me.min); + if (me.options.reverse) { + return (me.max - value) * scalingFactor; + } + return (value - me.min) * scalingFactor; + } + getValueForDistanceFromCenter(distance) { + if (isNullOrUndef(distance)) { + return NaN; + } + const me = this; + const scaledDistance = distance / (me.drawingArea / (me.max - me.min)); + return me.options.reverse ? me.max - scaledDistance : me.min + scaledDistance; + } + getPointPosition(index, distanceFromCenter) { + const me = this; + const angle = me.getIndexAngle(index) - HALF_PI; + return { + x: Math.cos(angle) * distanceFromCenter + me.xCenter, + y: Math.sin(angle) * distanceFromCenter + me.yCenter, + angle + }; + } + getPointPositionForValue(index, value) { + return this.getPointPosition(index, this.getDistanceFromCenterForValue(value)); + } + getBasePosition(index) { + return this.getPointPositionForValue(index || 0, this.getBaseValue()); + } + getPointLabelPosition(index) { + const {left, top, right, bottom} = this._pointLabelItems[index]; + return { + left, + top, + right, + bottom, + }; + } + drawBackground() { + const me = this; + const {backgroundColor, grid: {circular}} = me.options; + if (backgroundColor) { + const ctx = me.ctx; + ctx.save(); + ctx.beginPath(); + pathRadiusLine(me, me.getDistanceFromCenterForValue(me._endValue), circular, me.getLabels().length); + ctx.closePath(); + ctx.fillStyle = backgroundColor; + ctx.fill(); + ctx.restore(); + } + } + drawGrid() { + const me = this; + const ctx = me.ctx; + const opts = me.options; + const {angleLines, grid} = opts; + const labelCount = me.getLabels().length; + let i, offset, position; + if (opts.pointLabels.display) { + drawPointLabels(me, labelCount); + } + if (grid.display) { + me.ticks.forEach((tick, index) => { + if (index !== 0) { + offset = me.getDistanceFromCenterForValue(tick.value); + const optsAtIndex = grid.setContext(me.getContext(index - 1)); + drawRadiusLine(me, optsAtIndex, offset, labelCount); + } + }); + } + if (angleLines.display) { + ctx.save(); + for (i = me.getLabels().length - 1; i >= 0; i--) { + const optsAtIndex = angleLines.setContext(me.getContext(i)); + const {color, lineWidth} = optsAtIndex; + if (!lineWidth || !color) { + continue; + } + ctx.lineWidth = lineWidth; + ctx.strokeStyle = color; + ctx.setLineDash(optsAtIndex.borderDash); + ctx.lineDashOffset = optsAtIndex.borderDashOffset; + offset = me.getDistanceFromCenterForValue(opts.ticks.reverse ? me.min : me.max); + position = me.getPointPosition(i, offset); + ctx.beginPath(); + ctx.moveTo(me.xCenter, me.yCenter); + ctx.lineTo(position.x, position.y); + ctx.stroke(); + } + ctx.restore(); + } + } + drawBorder() {} + drawLabels() { + const me = this; + const ctx = me.ctx; + const opts = me.options; + const tickOpts = opts.ticks; + if (!tickOpts.display) { + return; + } + const startAngle = me.getIndexAngle(0); + let offset, width; + ctx.save(); + ctx.translate(me.xCenter, me.yCenter); + ctx.rotate(startAngle); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + me.ticks.forEach((tick, index) => { + if (index === 0 && !opts.reverse) { + return; + } + const optsAtIndex = tickOpts.setContext(me.getContext(index)); + const tickFont = toFont(optsAtIndex.font); + offset = me.getDistanceFromCenterForValue(me.ticks[index].value); + if (optsAtIndex.showLabelBackdrop) { + ctx.font = tickFont.string; + width = ctx.measureText(tick.label).width; + ctx.fillStyle = optsAtIndex.backdropColor; + const padding = toPadding(optsAtIndex.backdropPadding); + ctx.fillRect( + -width / 2 - padding.left, + -offset - tickFont.size / 2 - padding.top, + width + padding.width, + tickFont.size + padding.height + ); + } + renderText(ctx, tick.label, 0, -offset, tickFont, { + color: optsAtIndex.color, + }); + }); + ctx.restore(); + } + drawTitle() {} +} +RadialLinearScale.id = 'radialLinear'; +RadialLinearScale.defaults = { + display: true, + animate: true, + position: 'chartArea', + angleLines: { + display: true, + lineWidth: 1, + borderDash: [], + borderDashOffset: 0.0 + }, + grid: { + circular: false + }, + startAngle: 0, + ticks: { + showLabelBackdrop: true, + callback: Ticks.formatters.numeric + }, + pointLabels: { + backdropColor: undefined, + backdropPadding: 2, + display: true, + font: { + size: 10 + }, + callback(label) { + return label; + }, + padding: 5 + } +}; +RadialLinearScale.defaultRoutes = { + 'angleLines.color': 'borderColor', + 'pointLabels.color': 'color', + 'ticks.color': 'color' +}; +RadialLinearScale.descriptors = { + angleLines: { + _fallback: 'grid' + } +}; + +const INTERVALS = { + millisecond: {common: true, size: 1, steps: 1000}, + second: {common: true, size: 1000, steps: 60}, + minute: {common: true, size: 60000, steps: 60}, + hour: {common: true, size: 3600000, steps: 24}, + day: {common: true, size: 86400000, steps: 30}, + week: {common: false, size: 604800000, steps: 4}, + month: {common: true, size: 2.628e9, steps: 12}, + quarter: {common: false, size: 7.884e9, steps: 4}, + year: {common: true, size: 3.154e10} +}; +const UNITS = (Object.keys(INTERVALS)); +function sorter(a, b) { + return a - b; +} +function parse(scale, input) { + if (isNullOrUndef(input)) { + return null; + } + const adapter = scale._adapter; + const {parser, round, isoWeekday} = scale._parseOpts; + let value = input; + if (typeof parser === 'function') { + value = parser(value); + } + if (!isNumberFinite(value)) { + value = typeof parser === 'string' + ? adapter.parse(value, parser) + : adapter.parse(value); + } + if (value === null) { + return null; + } + if (round) { + value = round === 'week' && (isNumber(isoWeekday) || isoWeekday === true) + ? adapter.startOf(value, 'isoWeek', isoWeekday) + : adapter.startOf(value, round); + } + return +value; +} +function determineUnitForAutoTicks(minUnit, min, max, capacity) { + const ilen = UNITS.length; + for (let i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) { + const interval = INTERVALS[UNITS[i]]; + const factor = interval.steps ? interval.steps : Number.MAX_SAFE_INTEGER; + if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) { + return UNITS[i]; + } + } + return UNITS[ilen - 1]; +} +function determineUnitForFormatting(scale, numTicks, minUnit, min, max) { + for (let i = UNITS.length - 1; i >= UNITS.indexOf(minUnit); i--) { + const unit = UNITS[i]; + if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= numTicks - 1) { + return unit; + } + } + return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0]; +} +function determineMajorUnit(unit) { + for (let i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) { + if (INTERVALS[UNITS[i]].common) { + return UNITS[i]; + } + } +} +function addTick(ticks, time, timestamps) { + if (!timestamps) { + ticks[time] = true; + } else if (timestamps.length) { + const {lo, hi} = _lookup(timestamps, time); + const timestamp = timestamps[lo] >= time ? timestamps[lo] : timestamps[hi]; + ticks[timestamp] = true; + } +} +function setMajorTicks(scale, ticks, map, majorUnit) { + const adapter = scale._adapter; + const first = +adapter.startOf(ticks[0].value, majorUnit); + const last = ticks[ticks.length - 1].value; + let major, index; + for (major = first; major <= last; major = +adapter.add(major, 1, majorUnit)) { + index = map[major]; + if (index >= 0) { + ticks[index].major = true; + } + } + return ticks; +} +function ticksFromTimestamps(scale, values, majorUnit) { + const ticks = []; + const map = {}; + const ilen = values.length; + let i, value; + for (i = 0; i < ilen; ++i) { + value = values[i]; + map[value] = i; + ticks.push({ + value, + major: false + }); + } + return (ilen === 0 || !majorUnit) ? ticks : setMajorTicks(scale, ticks, map, majorUnit); +} +class TimeScale extends Scale { + constructor(props) { + super(props); + this._cache = { + data: [], + labels: [], + all: [] + }; + this._unit = 'day'; + this._majorUnit = undefined; + this._offsets = {}; + this._normalized = false; + this._parseOpts = undefined; + } + init(scaleOpts, opts) { + const time = scaleOpts.time || (scaleOpts.time = {}); + const adapter = this._adapter = new adapters._date(scaleOpts.adapters.date); + mergeIf(time.displayFormats, adapter.formats()); + this._parseOpts = { + parser: time.parser, + round: time.round, + isoWeekday: time.isoWeekday + }; + super.init(scaleOpts); + this._normalized = opts.normalized; + } + parse(raw, index) { + if (raw === undefined) { + return null; + } + return parse(this, raw); + } + beforeLayout() { + super.beforeLayout(); + this._cache = { + data: [], + labels: [], + all: [] + }; + } + determineDataLimits() { + const me = this; + const options = me.options; + const adapter = me._adapter; + const unit = options.time.unit || 'day'; + let {min, max, minDefined, maxDefined} = me.getUserBounds(); + function _applyBounds(bounds) { + if (!minDefined && !isNaN(bounds.min)) { + min = Math.min(min, bounds.min); + } + if (!maxDefined && !isNaN(bounds.max)) { + max = Math.max(max, bounds.max); + } + } + if (!minDefined || !maxDefined) { + _applyBounds(me._getLabelBounds()); + if (options.bounds !== 'ticks' || options.ticks.source !== 'labels') { + _applyBounds(me.getMinMax(false)); + } + } + min = isNumberFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit); + max = isNumberFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit) + 1; + me.min = Math.min(min, max - 1); + me.max = Math.max(min + 1, max); + } + _getLabelBounds() { + const arr = this.getLabelTimestamps(); + let min = Number.POSITIVE_INFINITY; + let max = Number.NEGATIVE_INFINITY; + if (arr.length) { + min = arr[0]; + max = arr[arr.length - 1]; + } + return {min, max}; + } + buildTicks() { + const me = this; + const options = me.options; + const timeOpts = options.time; + const tickOpts = options.ticks; + const timestamps = tickOpts.source === 'labels' ? me.getLabelTimestamps() : me._generate(); + if (options.bounds === 'ticks' && timestamps.length) { + me.min = me._userMin || timestamps[0]; + me.max = me._userMax || timestamps[timestamps.length - 1]; + } + const min = me.min; + const max = me.max; + const ticks = _filterBetween(timestamps, min, max); + me._unit = timeOpts.unit || (tickOpts.autoSkip + ? determineUnitForAutoTicks(timeOpts.minUnit, me.min, me.max, me._getLabelCapacity(min)) + : determineUnitForFormatting(me, ticks.length, timeOpts.minUnit, me.min, me.max)); + me._majorUnit = !tickOpts.major.enabled || me._unit === 'year' ? undefined + : determineMajorUnit(me._unit); + me.initOffsets(timestamps); + if (options.reverse) { + ticks.reverse(); + } + return ticksFromTimestamps(me, ticks, me._majorUnit); + } + initOffsets(timestamps) { + const me = this; + let start = 0; + let end = 0; + let first, last; + if (me.options.offset && timestamps.length) { + first = me.getDecimalForValue(timestamps[0]); + if (timestamps.length === 1) { + start = 1 - first; + } else { + start = (me.getDecimalForValue(timestamps[1]) - first) / 2; + } + last = me.getDecimalForValue(timestamps[timestamps.length - 1]); + if (timestamps.length === 1) { + end = last; + } else { + end = (last - me.getDecimalForValue(timestamps[timestamps.length - 2])) / 2; + } + } + const limit = timestamps.length < 3 ? 0.5 : 0.25; + start = _limitValue(start, 0, limit); + end = _limitValue(end, 0, limit); + me._offsets = {start, end, factor: 1 / (start + 1 + end)}; + } + _generate() { + const me = this; + const adapter = me._adapter; + const min = me.min; + const max = me.max; + const options = me.options; + const timeOpts = options.time; + const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, me._getLabelCapacity(min)); + const stepSize = valueOrDefault(timeOpts.stepSize, 1); + const weekday = minor === 'week' ? timeOpts.isoWeekday : false; + const hasWeekday = isNumber(weekday) || weekday === true; + const ticks = {}; + let first = min; + let time, count; + if (hasWeekday) { + first = +adapter.startOf(first, 'isoWeek', weekday); + } + first = +adapter.startOf(first, hasWeekday ? 'day' : minor); + if (adapter.diff(max, min, minor) > 100000 * stepSize) { + throw new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor); + } + const timestamps = options.ticks.source === 'data' && me.getDataTimestamps(); + for (time = first, count = 0; time < max; time = +adapter.add(time, stepSize, minor), count++) { + addTick(ticks, time, timestamps); + } + if (time === max || options.bounds === 'ticks' || count === 1) { + addTick(ticks, time, timestamps); + } + return Object.keys(ticks).sort((a, b) => a - b).map(x => +x); + } + getLabelForValue(value) { + const me = this; + const adapter = me._adapter; + const timeOpts = me.options.time; + if (timeOpts.tooltipFormat) { + return adapter.format(value, timeOpts.tooltipFormat); + } + return adapter.format(value, timeOpts.displayFormats.datetime); + } + _tickFormatFunction(time, index, ticks, format) { + const me = this; + const options = me.options; + const formats = options.time.displayFormats; + const unit = me._unit; + const majorUnit = me._majorUnit; + const minorFormat = unit && formats[unit]; + const majorFormat = majorUnit && formats[majorUnit]; + const tick = ticks[index]; + const major = majorUnit && majorFormat && tick && tick.major; + const label = me._adapter.format(time, format || (major ? majorFormat : minorFormat)); + const formatter = options.ticks.callback; + return formatter ? callback(formatter, [label, index, ticks], me) : label; + } + generateTickLabels(ticks) { + let i, ilen, tick; + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + tick = ticks[i]; + tick.label = this._tickFormatFunction(tick.value, i, ticks); + } + } + getDecimalForValue(value) { + const me = this; + return value === null ? NaN : (value - me.min) / (me.max - me.min); + } + getPixelForValue(value) { + const me = this; + const offsets = me._offsets; + const pos = me.getDecimalForValue(value); + return me.getPixelForDecimal((offsets.start + pos) * offsets.factor); + } + getValueForPixel(pixel) { + const me = this; + const offsets = me._offsets; + const pos = me.getDecimalForPixel(pixel) / offsets.factor - offsets.end; + return me.min + pos * (me.max - me.min); + } + _getLabelSize(label) { + const me = this; + const ticksOpts = me.options.ticks; + const tickLabelWidth = me.ctx.measureText(label).width; + const angle = toRadians(me.isHorizontal() ? ticksOpts.maxRotation : ticksOpts.minRotation); + const cosRotation = Math.cos(angle); + const sinRotation = Math.sin(angle); + const tickFontSize = me._resolveTickFontOptions(0).size; + return { + w: (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation), + h: (tickLabelWidth * sinRotation) + (tickFontSize * cosRotation) + }; + } + _getLabelCapacity(exampleTime) { + const me = this; + const timeOpts = me.options.time; + const displayFormats = timeOpts.displayFormats; + const format = displayFormats[timeOpts.unit] || displayFormats.millisecond; + const exampleLabel = me._tickFormatFunction(exampleTime, 0, ticksFromTimestamps(me, [exampleTime], me._majorUnit), format); + const size = me._getLabelSize(exampleLabel); + const capacity = Math.floor(me.isHorizontal() ? me.width / size.w : me.height / size.h) - 1; + return capacity > 0 ? capacity : 1; + } + getDataTimestamps() { + const me = this; + let timestamps = me._cache.data || []; + let i, ilen; + if (timestamps.length) { + return timestamps; + } + const metas = me.getMatchingVisibleMetas(); + if (me._normalized && metas.length) { + return (me._cache.data = metas[0].controller.getAllParsedValues(me)); + } + for (i = 0, ilen = metas.length; i < ilen; ++i) { + timestamps = timestamps.concat(metas[i].controller.getAllParsedValues(me)); + } + return (me._cache.data = me.normalize(timestamps)); + } + getLabelTimestamps() { + const me = this; + const timestamps = me._cache.labels || []; + let i, ilen; + if (timestamps.length) { + return timestamps; + } + const labels = me.getLabels(); + for (i = 0, ilen = labels.length; i < ilen; ++i) { + timestamps.push(parse(me, labels[i])); + } + return (me._cache.labels = me._normalized ? timestamps : me.normalize(timestamps)); + } + normalize(values) { + return _arrayUnique(values.sort(sorter)); + } +} +TimeScale.id = 'time'; +TimeScale.defaults = { + bounds: 'data', + adapters: {}, + time: { + parser: false, + unit: false, + round: false, + isoWeekday: false, + minUnit: 'millisecond', + displayFormats: {} + }, + ticks: { + source: 'auto', + major: { + enabled: false + } + } +}; + +function interpolate(table, val, reverse) { + let lo = 0; + let hi = table.length - 1; + let prevSource, nextSource, prevTarget, nextTarget; + if (reverse) { + if (val >= table[lo].pos && val <= table[hi].pos) { + ({lo, hi} = _lookupByKey(table, 'pos', val)); + } + ({pos: prevSource, time: prevTarget} = table[lo]); + ({pos: nextSource, time: nextTarget} = table[hi]); + } else { + if (val >= table[lo].time && val <= table[hi].time) { + ({lo, hi} = _lookupByKey(table, 'time', val)); + } + ({time: prevSource, pos: prevTarget} = table[lo]); + ({time: nextSource, pos: nextTarget} = table[hi]); + } + const span = nextSource - prevSource; + return span ? prevTarget + (nextTarget - prevTarget) * (val - prevSource) / span : prevTarget; +} +class TimeSeriesScale extends TimeScale { + constructor(props) { + super(props); + this._table = []; + this._minPos = undefined; + this._tableRange = undefined; + } + initOffsets() { + const me = this; + const timestamps = me._getTimestampsForTable(); + const table = me._table = me.buildLookupTable(timestamps); + me._minPos = interpolate(table, me.min); + me._tableRange = interpolate(table, me.max) - me._minPos; + super.initOffsets(timestamps); + } + buildLookupTable(timestamps) { + const {min, max} = this; + const items = []; + const table = []; + let i, ilen, prev, curr, next; + for (i = 0, ilen = timestamps.length; i < ilen; ++i) { + curr = timestamps[i]; + if (curr >= min && curr <= max) { + items.push(curr); + } + } + if (items.length < 2) { + return [ + {time: min, pos: 0}, + {time: max, pos: 1} + ]; + } + for (i = 0, ilen = items.length; i < ilen; ++i) { + next = items[i + 1]; + prev = items[i - 1]; + curr = items[i]; + if (Math.round((next + prev) / 2) !== curr) { + table.push({time: curr, pos: i / (ilen - 1)}); + } + } + return table; + } + _getTimestampsForTable() { + const me = this; + let timestamps = me._cache.all || []; + if (timestamps.length) { + return timestamps; + } + const data = me.getDataTimestamps(); + const label = me.getLabelTimestamps(); + if (data.length && label.length) { + timestamps = me.normalize(data.concat(label)); + } else { + timestamps = data.length ? data : label; + } + timestamps = me._cache.all = timestamps; + return timestamps; + } + getDecimalForValue(value) { + return (interpolate(this._table, value) - this._minPos) / this._tableRange; + } + getValueForPixel(pixel) { + const me = this; + const offsets = me._offsets; + const decimal = me.getDecimalForPixel(pixel) / offsets.factor - offsets.end; + return interpolate(me._table, decimal * me._tableRange + me._minPos, true); + } +} +TimeSeriesScale.id = 'timeseries'; +TimeSeriesScale.defaults = TimeScale.defaults; + +var scales = /*#__PURE__*/Object.freeze({ +__proto__: null, +CategoryScale: CategoryScale, +LinearScale: LinearScale, +LogarithmicScale: LogarithmicScale, +RadialLinearScale: RadialLinearScale, +TimeScale: TimeScale, +TimeSeriesScale: TimeSeriesScale +}); + +const registerables = [ + controllers, + elements, + plugins, + scales, +]; + +export { Animation, Animations, ArcElement, BarController, BarElement, BasePlatform, BasicPlatform, BubbleController, CategoryScale, Chart, DatasetController, plugin_decimation as Decimation, DomPlatform, DoughnutController, Element, plugin_filler as Filler, Interaction, plugin_legend as Legend, LineController, LineElement, LinearScale, LogarithmicScale, PieController, PointElement, PolarAreaController, RadarController, RadialLinearScale, Scale, ScatterController, plugin_subtitle as SubTitle, Ticks, TimeScale, TimeSeriesScale, plugin_title as Title, plugin_tooltip as Tooltip, adapters as _adapters, animator, controllers, elements, layouts, plugins, registerables, registry, scales }; diff --git a/node_modules/chart.js/dist/chart.js b/node_modules/chart.js/dist/chart.js new file mode 100644 index 000000000..d7d2d8346 --- /dev/null +++ b/node_modules/chart.js/dist/chart.js @@ -0,0 +1,13050 @@ +/*! + * Chart.js v3.4.1 + * https://www.chartjs.org + * (c) 2021 Chart.js Contributors + * Released under the MIT License + */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : +typeof define === 'function' && define.amd ? define(factory) : +(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Chart = factory()); +}(this, (function () { 'use strict'; + +function fontString(pixelSize, fontStyle, fontFamily) { + return fontStyle + ' ' + pixelSize + 'px ' + fontFamily; +} +const requestAnimFrame = (function() { + if (typeof window === 'undefined') { + return function(callback) { + return callback(); + }; + } + return window.requestAnimationFrame; +}()); +function throttled(fn, thisArg, updateFn) { + const updateArgs = updateFn || ((args) => Array.prototype.slice.call(args)); + let ticking = false; + let args = []; + return function(...rest) { + args = updateArgs(rest); + if (!ticking) { + ticking = true; + requestAnimFrame.call(window, () => { + ticking = false; + fn.apply(thisArg, args); + }); + } + }; +} +function debounce(fn, delay) { + let timeout; + return function() { + if (delay) { + clearTimeout(timeout); + timeout = setTimeout(fn, delay); + } else { + fn(); + } + return delay; + }; +} +const _toLeftRightCenter = (align) => align === 'start' ? 'left' : align === 'end' ? 'right' : 'center'; +const _alignStartEnd = (align, start, end) => align === 'start' ? start : align === 'end' ? end : (start + end) / 2; +const _textX = (align, left, right, rtl) => { + const check = rtl ? 'left' : 'right'; + return align === check ? right : align === 'center' ? (left + right) / 2 : left; +}; + +class Animator { + constructor() { + this._request = null; + this._charts = new Map(); + this._running = false; + this._lastDate = undefined; + } + _notify(chart, anims, date, type) { + const callbacks = anims.listeners[type]; + const numSteps = anims.duration; + callbacks.forEach(fn => fn({ + chart, + initial: anims.initial, + numSteps, + currentStep: Math.min(date - anims.start, numSteps) + })); + } + _refresh() { + const me = this; + if (me._request) { + return; + } + me._running = true; + me._request = requestAnimFrame.call(window, () => { + me._update(); + me._request = null; + if (me._running) { + me._refresh(); + } + }); + } + _update(date = Date.now()) { + const me = this; + let remaining = 0; + me._charts.forEach((anims, chart) => { + if (!anims.running || !anims.items.length) { + return; + } + const items = anims.items; + let i = items.length - 1; + let draw = false; + let item; + for (; i >= 0; --i) { + item = items[i]; + if (item._active) { + if (item._total > anims.duration) { + anims.duration = item._total; + } + item.tick(date); + draw = true; + } else { + items[i] = items[items.length - 1]; + items.pop(); + } + } + if (draw) { + chart.draw(); + me._notify(chart, anims, date, 'progress'); + } + if (!items.length) { + anims.running = false; + me._notify(chart, anims, date, 'complete'); + anims.initial = false; + } + remaining += items.length; + }); + me._lastDate = date; + if (remaining === 0) { + me._running = false; + } + } + _getAnims(chart) { + const charts = this._charts; + let anims = charts.get(chart); + if (!anims) { + anims = { + running: false, + initial: true, + items: [], + listeners: { + complete: [], + progress: [] + } + }; + charts.set(chart, anims); + } + return anims; + } + listen(chart, event, cb) { + this._getAnims(chart).listeners[event].push(cb); + } + add(chart, items) { + if (!items || !items.length) { + return; + } + this._getAnims(chart).items.push(...items); + } + has(chart) { + return this._getAnims(chart).items.length > 0; + } + start(chart) { + const anims = this._charts.get(chart); + if (!anims) { + return; + } + anims.running = true; + anims.start = Date.now(); + anims.duration = anims.items.reduce((acc, cur) => Math.max(acc, cur._duration), 0); + this._refresh(); + } + running(chart) { + if (!this._running) { + return false; + } + const anims = this._charts.get(chart); + if (!anims || !anims.running || !anims.items.length) { + return false; + } + return true; + } + stop(chart) { + const anims = this._charts.get(chart); + if (!anims || !anims.items.length) { + return; + } + const items = anims.items; + let i = items.length - 1; + for (; i >= 0; --i) { + items[i].cancel(); + } + anims.items = []; + this._notify(chart, anims, Date.now(), 'complete'); + } + remove(chart) { + return this._charts.delete(chart); + } +} +var animator = new Animator(); + +/*! + * @kurkle/color v0.1.9 + * https://github.com/kurkle/color#readme + * (c) 2020 Jukka Kurkela + * Released under the MIT License + */ +const map$1 = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, A: 10, B: 11, C: 12, D: 13, E: 14, F: 15, a: 10, b: 11, c: 12, d: 13, e: 14, f: 15}; +const hex = '0123456789ABCDEF'; +const h1 = (b) => hex[b & 0xF]; +const h2 = (b) => hex[(b & 0xF0) >> 4] + hex[b & 0xF]; +const eq = (b) => (((b & 0xF0) >> 4) === (b & 0xF)); +function isShort(v) { + return eq(v.r) && eq(v.g) && eq(v.b) && eq(v.a); +} +function hexParse(str) { + var len = str.length; + var ret; + if (str[0] === '#') { + if (len === 4 || len === 5) { + ret = { + r: 255 & map$1[str[1]] * 17, + g: 255 & map$1[str[2]] * 17, + b: 255 & map$1[str[3]] * 17, + a: len === 5 ? map$1[str[4]] * 17 : 255 + }; + } else if (len === 7 || len === 9) { + ret = { + r: map$1[str[1]] << 4 | map$1[str[2]], + g: map$1[str[3]] << 4 | map$1[str[4]], + b: map$1[str[5]] << 4 | map$1[str[6]], + a: len === 9 ? (map$1[str[7]] << 4 | map$1[str[8]]) : 255 + }; + } + } + return ret; +} +function hexString(v) { + var f = isShort(v) ? h1 : h2; + return v + ? '#' + f(v.r) + f(v.g) + f(v.b) + (v.a < 255 ? f(v.a) : '') + : v; +} +function round(v) { + return v + 0.5 | 0; +} +const lim = (v, l, h) => Math.max(Math.min(v, h), l); +function p2b(v) { + return lim(round(v * 2.55), 0, 255); +} +function n2b(v) { + return lim(round(v * 255), 0, 255); +} +function b2n(v) { + return lim(round(v / 2.55) / 100, 0, 1); +} +function n2p(v) { + return lim(round(v * 100), 0, 100); +} +const RGB_RE = /^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/; +function rgbParse(str) { + const m = RGB_RE.exec(str); + let a = 255; + let r, g, b; + if (!m) { + return; + } + if (m[7] !== r) { + const v = +m[7]; + a = 255 & (m[8] ? p2b(v) : v * 255); + } + r = +m[1]; + g = +m[3]; + b = +m[5]; + r = 255 & (m[2] ? p2b(r) : r); + g = 255 & (m[4] ? p2b(g) : g); + b = 255 & (m[6] ? p2b(b) : b); + return { + r: r, + g: g, + b: b, + a: a + }; +} +function rgbString(v) { + return v && ( + v.a < 255 + ? `rgba(${v.r}, ${v.g}, ${v.b}, ${b2n(v.a)})` + : `rgb(${v.r}, ${v.g}, ${v.b})` + ); +} +const HUE_RE = /^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/; +function hsl2rgbn(h, s, l) { + const a = s * Math.min(l, 1 - l); + const f = (n, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return [f(0), f(8), f(4)]; +} +function hsv2rgbn(h, s, v) { + const f = (n, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0); + return [f(5), f(3), f(1)]; +} +function hwb2rgbn(h, w, b) { + const rgb = hsl2rgbn(h, 1, 0.5); + let i; + if (w + b > 1) { + i = 1 / (w + b); + w *= i; + b *= i; + } + for (i = 0; i < 3; i++) { + rgb[i] *= 1 - w - b; + rgb[i] += w; + } + return rgb; +} +function rgb2hsl(v) { + const range = 255; + const r = v.r / range; + const g = v.g / range; + const b = v.b / range; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const l = (max + min) / 2; + let h, s, d; + if (max !== min) { + d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + h = max === r + ? ((g - b) / d) + (g < b ? 6 : 0) + : max === g + ? (b - r) / d + 2 + : (r - g) / d + 4; + h = h * 60 + 0.5; + } + return [h | 0, s || 0, l]; +} +function calln(f, a, b, c) { + return ( + Array.isArray(a) + ? f(a[0], a[1], a[2]) + : f(a, b, c) + ).map(n2b); +} +function hsl2rgb(h, s, l) { + return calln(hsl2rgbn, h, s, l); +} +function hwb2rgb(h, w, b) { + return calln(hwb2rgbn, h, w, b); +} +function hsv2rgb(h, s, v) { + return calln(hsv2rgbn, h, s, v); +} +function hue(h) { + return (h % 360 + 360) % 360; +} +function hueParse(str) { + const m = HUE_RE.exec(str); + let a = 255; + let v; + if (!m) { + return; + } + if (m[5] !== v) { + a = m[6] ? p2b(+m[5]) : n2b(+m[5]); + } + const h = hue(+m[2]); + const p1 = +m[3] / 100; + const p2 = +m[4] / 100; + if (m[1] === 'hwb') { + v = hwb2rgb(h, p1, p2); + } else if (m[1] === 'hsv') { + v = hsv2rgb(h, p1, p2); + } else { + v = hsl2rgb(h, p1, p2); + } + return { + r: v[0], + g: v[1], + b: v[2], + a: a + }; +} +function rotate(v, deg) { + var h = rgb2hsl(v); + h[0] = hue(h[0] + deg); + h = hsl2rgb(h); + v.r = h[0]; + v.g = h[1]; + v.b = h[2]; +} +function hslString(v) { + if (!v) { + return; + } + const a = rgb2hsl(v); + const h = a[0]; + const s = n2p(a[1]); + const l = n2p(a[2]); + return v.a < 255 + ? `hsla(${h}, ${s}%, ${l}%, ${b2n(v.a)})` + : `hsl(${h}, ${s}%, ${l}%)`; +} +const map$1$1 = { + x: 'dark', + Z: 'light', + Y: 're', + X: 'blu', + W: 'gr', + V: 'medium', + U: 'slate', + A: 'ee', + T: 'ol', + S: 'or', + B: 'ra', + C: 'lateg', + D: 'ights', + R: 'in', + Q: 'turquois', + E: 'hi', + P: 'ro', + O: 'al', + N: 'le', + M: 'de', + L: 'yello', + F: 'en', + K: 'ch', + G: 'arks', + H: 'ea', + I: 'ightg', + J: 'wh' +}; +const names = { + OiceXe: 'f0f8ff', + antiquewEte: 'faebd7', + aqua: 'ffff', + aquamarRe: '7fffd4', + azuY: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '0', + blanKedOmond: 'ffebcd', + Xe: 'ff', + XeviTet: '8a2be2', + bPwn: 'a52a2a', + burlywood: 'deb887', + caMtXe: '5f9ea0', + KartYuse: '7fff00', + KocTate: 'd2691e', + cSO: 'ff7f50', + cSnflowerXe: '6495ed', + cSnsilk: 'fff8dc', + crimson: 'dc143c', + cyan: 'ffff', + xXe: '8b', + xcyan: '8b8b', + xgTMnPd: 'b8860b', + xWay: 'a9a9a9', + xgYF: '6400', + xgYy: 'a9a9a9', + xkhaki: 'bdb76b', + xmagFta: '8b008b', + xTivegYF: '556b2f', + xSange: 'ff8c00', + xScEd: '9932cc', + xYd: '8b0000', + xsOmon: 'e9967a', + xsHgYF: '8fbc8f', + xUXe: '483d8b', + xUWay: '2f4f4f', + xUgYy: '2f4f4f', + xQe: 'ced1', + xviTet: '9400d3', + dAppRk: 'ff1493', + dApskyXe: 'bfff', + dimWay: '696969', + dimgYy: '696969', + dodgerXe: '1e90ff', + fiYbrick: 'b22222', + flSOwEte: 'fffaf0', + foYstWAn: '228b22', + fuKsia: 'ff00ff', + gaRsbSo: 'dcdcdc', + ghostwEte: 'f8f8ff', + gTd: 'ffd700', + gTMnPd: 'daa520', + Way: '808080', + gYF: '8000', + gYFLw: 'adff2f', + gYy: '808080', + honeyMw: 'f0fff0', + hotpRk: 'ff69b4', + RdianYd: 'cd5c5c', + Rdigo: '4b0082', + ivSy: 'fffff0', + khaki: 'f0e68c', + lavFMr: 'e6e6fa', + lavFMrXsh: 'fff0f5', + lawngYF: '7cfc00', + NmoncEffon: 'fffacd', + ZXe: 'add8e6', + ZcSO: 'f08080', + Zcyan: 'e0ffff', + ZgTMnPdLw: 'fafad2', + ZWay: 'd3d3d3', + ZgYF: '90ee90', + ZgYy: 'd3d3d3', + ZpRk: 'ffb6c1', + ZsOmon: 'ffa07a', + ZsHgYF: '20b2aa', + ZskyXe: '87cefa', + ZUWay: '778899', + ZUgYy: '778899', + ZstAlXe: 'b0c4de', + ZLw: 'ffffe0', + lime: 'ff00', + limegYF: '32cd32', + lRF: 'faf0e6', + magFta: 'ff00ff', + maPon: '800000', + VaquamarRe: '66cdaa', + VXe: 'cd', + VScEd: 'ba55d3', + VpurpN: '9370db', + VsHgYF: '3cb371', + VUXe: '7b68ee', + VsprRggYF: 'fa9a', + VQe: '48d1cc', + VviTetYd: 'c71585', + midnightXe: '191970', + mRtcYam: 'f5fffa', + mistyPse: 'ffe4e1', + moccasR: 'ffe4b5', + navajowEte: 'ffdead', + navy: '80', + Tdlace: 'fdf5e6', + Tive: '808000', + TivedBb: '6b8e23', + Sange: 'ffa500', + SangeYd: 'ff4500', + ScEd: 'da70d6', + pOegTMnPd: 'eee8aa', + pOegYF: '98fb98', + pOeQe: 'afeeee', + pOeviTetYd: 'db7093', + papayawEp: 'ffefd5', + pHKpuff: 'ffdab9', + peru: 'cd853f', + pRk: 'ffc0cb', + plum: 'dda0dd', + powMrXe: 'b0e0e6', + purpN: '800080', + YbeccapurpN: '663399', + Yd: 'ff0000', + Psybrown: 'bc8f8f', + PyOXe: '4169e1', + saddNbPwn: '8b4513', + sOmon: 'fa8072', + sandybPwn: 'f4a460', + sHgYF: '2e8b57', + sHshell: 'fff5ee', + siFna: 'a0522d', + silver: 'c0c0c0', + skyXe: '87ceeb', + UXe: '6a5acd', + UWay: '708090', + UgYy: '708090', + snow: 'fffafa', + sprRggYF: 'ff7f', + stAlXe: '4682b4', + tan: 'd2b48c', + teO: '8080', + tEstN: 'd8bfd8', + tomato: 'ff6347', + Qe: '40e0d0', + viTet: 'ee82ee', + JHt: 'f5deb3', + wEte: 'ffffff', + wEtesmoke: 'f5f5f5', + Lw: 'ffff00', + LwgYF: '9acd32' +}; +function unpack() { + const unpacked = {}; + const keys = Object.keys(names); + const tkeys = Object.keys(map$1$1); + let i, j, k, ok, nk; + for (i = 0; i < keys.length; i++) { + ok = nk = keys[i]; + for (j = 0; j < tkeys.length; j++) { + k = tkeys[j]; + nk = nk.replace(k, map$1$1[k]); + } + k = parseInt(names[ok], 16); + unpacked[nk] = [k >> 16 & 0xFF, k >> 8 & 0xFF, k & 0xFF]; + } + return unpacked; +} +let names$1; +function nameParse(str) { + if (!names$1) { + names$1 = unpack(); + names$1.transparent = [0, 0, 0, 0]; + } + const a = names$1[str.toLowerCase()]; + return a && { + r: a[0], + g: a[1], + b: a[2], + a: a.length === 4 ? a[3] : 255 + }; +} +function modHSL(v, i, ratio) { + if (v) { + let tmp = rgb2hsl(v); + tmp[i] = Math.max(0, Math.min(tmp[i] + tmp[i] * ratio, i === 0 ? 360 : 1)); + tmp = hsl2rgb(tmp); + v.r = tmp[0]; + v.g = tmp[1]; + v.b = tmp[2]; + } +} +function clone$1(v, proto) { + return v ? Object.assign(proto || {}, v) : v; +} +function fromObject(input) { + var v = {r: 0, g: 0, b: 0, a: 255}; + if (Array.isArray(input)) { + if (input.length >= 3) { + v = {r: input[0], g: input[1], b: input[2], a: 255}; + if (input.length > 3) { + v.a = n2b(input[3]); + } + } + } else { + v = clone$1(input, {r: 0, g: 0, b: 0, a: 1}); + v.a = n2b(v.a); + } + return v; +} +function functionParse(str) { + if (str.charAt(0) === 'r') { + return rgbParse(str); + } + return hueParse(str); +} +class Color { + constructor(input) { + if (input instanceof Color) { + return input; + } + const type = typeof input; + let v; + if (type === 'object') { + v = fromObject(input); + } else if (type === 'string') { + v = hexParse(input) || nameParse(input) || functionParse(input); + } + this._rgb = v; + this._valid = !!v; + } + get valid() { + return this._valid; + } + get rgb() { + var v = clone$1(this._rgb); + if (v) { + v.a = b2n(v.a); + } + return v; + } + set rgb(obj) { + this._rgb = fromObject(obj); + } + rgbString() { + return this._valid ? rgbString(this._rgb) : this._rgb; + } + hexString() { + return this._valid ? hexString(this._rgb) : this._rgb; + } + hslString() { + return this._valid ? hslString(this._rgb) : this._rgb; + } + mix(color, weight) { + const me = this; + if (color) { + const c1 = me.rgb; + const c2 = color.rgb; + let w2; + const p = weight === w2 ? 0.5 : weight; + const w = 2 * p - 1; + const a = c1.a - c2.a; + const w1 = ((w * a === -1 ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + w2 = 1 - w1; + c1.r = 0xFF & w1 * c1.r + w2 * c2.r + 0.5; + c1.g = 0xFF & w1 * c1.g + w2 * c2.g + 0.5; + c1.b = 0xFF & w1 * c1.b + w2 * c2.b + 0.5; + c1.a = p * c1.a + (1 - p) * c2.a; + me.rgb = c1; + } + return me; + } + clone() { + return new Color(this.rgb); + } + alpha(a) { + this._rgb.a = n2b(a); + return this; + } + clearer(ratio) { + const rgb = this._rgb; + rgb.a *= 1 - ratio; + return this; + } + greyscale() { + const rgb = this._rgb; + const val = round(rgb.r * 0.3 + rgb.g * 0.59 + rgb.b * 0.11); + rgb.r = rgb.g = rgb.b = val; + return this; + } + opaquer(ratio) { + const rgb = this._rgb; + rgb.a *= 1 + ratio; + return this; + } + negate() { + const v = this._rgb; + v.r = 255 - v.r; + v.g = 255 - v.g; + v.b = 255 - v.b; + return this; + } + lighten(ratio) { + modHSL(this._rgb, 2, ratio); + return this; + } + darken(ratio) { + modHSL(this._rgb, 2, -ratio); + return this; + } + saturate(ratio) { + modHSL(this._rgb, 1, ratio); + return this; + } + desaturate(ratio) { + modHSL(this._rgb, 1, -ratio); + return this; + } + rotate(deg) { + rotate(this._rgb, deg); + return this; + } +} +function index_esm(input) { + return new Color(input); +} + +const isPatternOrGradient = (value) => value instanceof CanvasGradient || value instanceof CanvasPattern; +function color(value) { + return isPatternOrGradient(value) ? value : index_esm(value); +} +function getHoverColor(value) { + return isPatternOrGradient(value) + ? value + : index_esm(value).saturate(0.5).darken(0.1).hexString(); +} + +function noop() {} +const uid = (function() { + let id = 0; + return function() { + return id++; + }; +}()); +function isNullOrUndef(value) { + return value === null || typeof value === 'undefined'; +} +function isArray(value) { + if (Array.isArray && Array.isArray(value)) { + return true; + } + const type = Object.prototype.toString.call(value); + if (type.substr(0, 7) === '[object' && type.substr(-6) === 'Array]') { + return true; + } + return false; +} +function isObject(value) { + return value !== null && Object.prototype.toString.call(value) === '[object Object]'; +} +const isNumberFinite = (value) => (typeof value === 'number' || value instanceof Number) && isFinite(+value); +function finiteOrDefault(value, defaultValue) { + return isNumberFinite(value) ? value : defaultValue; +} +function valueOrDefault(value, defaultValue) { + return typeof value === 'undefined' ? defaultValue : value; +} +const toPercentage = (value, dimension) => + typeof value === 'string' && value.endsWith('%') ? + parseFloat(value) / 100 + : value / dimension; +const toDimension = (value, dimension) => + typeof value === 'string' && value.endsWith('%') ? + parseFloat(value) / 100 * dimension + : +value; +function callback(fn, args, thisArg) { + if (fn && typeof fn.call === 'function') { + return fn.apply(thisArg, args); + } +} +function each(loopable, fn, thisArg, reverse) { + let i, len, keys; + if (isArray(loopable)) { + len = loopable.length; + if (reverse) { + for (i = len - 1; i >= 0; i--) { + fn.call(thisArg, loopable[i], i); + } + } else { + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[i], i); + } + } + } else if (isObject(loopable)) { + keys = Object.keys(loopable); + len = keys.length; + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[keys[i]], keys[i]); + } + } +} +function _elementsEqual(a0, a1) { + let i, ilen, v0, v1; + if (!a0 || !a1 || a0.length !== a1.length) { + return false; + } + for (i = 0, ilen = a0.length; i < ilen; ++i) { + v0 = a0[i]; + v1 = a1[i]; + if (v0.datasetIndex !== v1.datasetIndex || v0.index !== v1.index) { + return false; + } + } + return true; +} +function clone(source) { + if (isArray(source)) { + return source.map(clone); + } + if (isObject(source)) { + const target = Object.create(null); + const keys = Object.keys(source); + const klen = keys.length; + let k = 0; + for (; k < klen; ++k) { + target[keys[k]] = clone(source[keys[k]]); + } + return target; + } + return source; +} +function isValidKey(key) { + return ['__proto__', 'prototype', 'constructor'].indexOf(key) === -1; +} +function _merger(key, target, source, options) { + if (!isValidKey(key)) { + return; + } + const tval = target[key]; + const sval = source[key]; + if (isObject(tval) && isObject(sval)) { + merge(tval, sval, options); + } else { + target[key] = clone(sval); + } +} +function merge(target, source, options) { + const sources = isArray(source) ? source : [source]; + const ilen = sources.length; + if (!isObject(target)) { + return target; + } + options = options || {}; + const merger = options.merger || _merger; + for (let i = 0; i < ilen; ++i) { + source = sources[i]; + if (!isObject(source)) { + continue; + } + const keys = Object.keys(source); + for (let k = 0, klen = keys.length; k < klen; ++k) { + merger(keys[k], target, source, options); + } + } + return target; +} +function mergeIf(target, source) { + return merge(target, source, {merger: _mergerIf}); +} +function _mergerIf(key, target, source) { + if (!isValidKey(key)) { + return; + } + const tval = target[key]; + const sval = source[key]; + if (isObject(tval) && isObject(sval)) { + mergeIf(tval, sval); + } else if (!Object.prototype.hasOwnProperty.call(target, key)) { + target[key] = clone(sval); + } +} +function _deprecated(scope, value, previous, current) { + if (value !== undefined) { + console.warn(scope + ': "' + previous + + '" is deprecated. Please use "' + current + '" instead'); + } +} +const emptyString = ''; +const dot = '.'; +function indexOfDotOrLength(key, start) { + const idx = key.indexOf(dot, start); + return idx === -1 ? key.length : idx; +} +function resolveObjectKey(obj, key) { + if (key === emptyString) { + return obj; + } + let pos = 0; + let idx = indexOfDotOrLength(key, pos); + while (obj && idx > pos) { + obj = obj[key.substr(pos, idx - pos)]; + pos = idx + 1; + idx = indexOfDotOrLength(key, pos); + } + return obj; +} +function _capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} +const defined = (value) => typeof value !== 'undefined'; +const isFunction = (value) => typeof value === 'function'; +const setsEqual = (a, b) => { + if (a.size !== b.size) { + return false; + } + for (const item of a) { + if (!b.has(item)) { + return false; + } + } + return true; +}; + +const overrides = Object.create(null); +const descriptors = Object.create(null); +function getScope$1(node, key) { + if (!key) { + return node; + } + const keys = key.split('.'); + for (let i = 0, n = keys.length; i < n; ++i) { + const k = keys[i]; + node = node[k] || (node[k] = Object.create(null)); + } + return node; +} +function set(root, scope, values) { + if (typeof scope === 'string') { + return merge(getScope$1(root, scope), values); + } + return merge(getScope$1(root, ''), scope); +} +class Defaults { + constructor(_descriptors) { + this.animation = undefined; + this.backgroundColor = 'rgba(0,0,0,0.1)'; + this.borderColor = 'rgba(0,0,0,0.1)'; + this.color = '#666'; + this.datasets = {}; + this.devicePixelRatio = (context) => context.chart.platform.getDevicePixelRatio(); + this.elements = {}; + this.events = [ + 'mousemove', + 'mouseout', + 'click', + 'touchstart', + 'touchmove' + ]; + this.font = { + family: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + size: 12, + style: 'normal', + lineHeight: 1.2, + weight: null + }; + this.hover = {}; + this.hoverBackgroundColor = (ctx, options) => getHoverColor(options.backgroundColor); + this.hoverBorderColor = (ctx, options) => getHoverColor(options.borderColor); + this.hoverColor = (ctx, options) => getHoverColor(options.color); + this.indexAxis = 'x'; + this.interaction = { + mode: 'nearest', + intersect: true + }; + this.maintainAspectRatio = true; + this.onHover = null; + this.onClick = null; + this.parsing = true; + this.plugins = {}; + this.responsive = true; + this.scale = undefined; + this.scales = {}; + this.showLine = true; + this.describe(_descriptors); + } + set(scope, values) { + return set(this, scope, values); + } + get(scope) { + return getScope$1(this, scope); + } + describe(scope, values) { + return set(descriptors, scope, values); + } + override(scope, values) { + return set(overrides, scope, values); + } + route(scope, name, targetScope, targetName) { + const scopeObject = getScope$1(this, scope); + const targetScopeObject = getScope$1(this, targetScope); + const privateName = '_' + name; + Object.defineProperties(scopeObject, { + [privateName]: { + value: scopeObject[name], + writable: true + }, + [name]: { + enumerable: true, + get() { + const local = this[privateName]; + const target = targetScopeObject[targetName]; + if (isObject(local)) { + return Object.assign({}, target, local); + } + return valueOrDefault(local, target); + }, + set(value) { + this[privateName] = value; + } + } + }); + } +} +var defaults = new Defaults({ + _scriptable: (name) => !name.startsWith('on'), + _indexable: (name) => name !== 'events', + hover: { + _fallback: 'interaction' + }, + interaction: { + _scriptable: false, + _indexable: false, + } +}); + +const PI = Math.PI; +const TAU = 2 * PI; +const PITAU = TAU + PI; +const INFINITY = Number.POSITIVE_INFINITY; +const RAD_PER_DEG = PI / 180; +const HALF_PI = PI / 2; +const QUARTER_PI = PI / 4; +const TWO_THIRDS_PI = PI * 2 / 3; +const log10 = Math.log10; +const sign = Math.sign; +function niceNum(range) { + const roundedRange = Math.round(range); + range = almostEquals(range, roundedRange, range / 1000) ? roundedRange : range; + const niceRange = Math.pow(10, Math.floor(log10(range))); + const fraction = range / niceRange; + const niceFraction = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10; + return niceFraction * niceRange; +} +function _factorize(value) { + const result = []; + const sqrt = Math.sqrt(value); + let i; + for (i = 1; i < sqrt; i++) { + if (value % i === 0) { + result.push(i); + result.push(value / i); + } + } + if (sqrt === (sqrt | 0)) { + result.push(sqrt); + } + result.sort((a, b) => a - b).pop(); + return result; +} +function isNumber(n) { + return !isNaN(parseFloat(n)) && isFinite(n); +} +function almostEquals(x, y, epsilon) { + return Math.abs(x - y) < epsilon; +} +function almostWhole(x, epsilon) { + const rounded = Math.round(x); + return ((rounded - epsilon) <= x) && ((rounded + epsilon) >= x); +} +function _setMinAndMaxByKey(array, target, property) { + let i, ilen, value; + for (i = 0, ilen = array.length; i < ilen; i++) { + value = array[i][property]; + if (!isNaN(value)) { + target.min = Math.min(target.min, value); + target.max = Math.max(target.max, value); + } + } +} +function toRadians(degrees) { + return degrees * (PI / 180); +} +function toDegrees(radians) { + return radians * (180 / PI); +} +function _decimalPlaces(x) { + if (!isNumberFinite(x)) { + return; + } + let e = 1; + let p = 0; + while (Math.round(x * e) / e !== x) { + e *= 10; + p++; + } + return p; +} +function getAngleFromPoint(centrePoint, anglePoint) { + const distanceFromXCenter = anglePoint.x - centrePoint.x; + const distanceFromYCenter = anglePoint.y - centrePoint.y; + const radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + let angle = Math.atan2(distanceFromYCenter, distanceFromXCenter); + if (angle < (-0.5 * PI)) { + angle += TAU; + } + return { + angle, + distance: radialDistanceFromCenter + }; +} +function distanceBetweenPoints(pt1, pt2) { + return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2)); +} +function _angleDiff(a, b) { + return (a - b + PITAU) % TAU - PI; +} +function _normalizeAngle(a) { + return (a % TAU + TAU) % TAU; +} +function _angleBetween(angle, start, end, sameAngleIsFullCircle) { + const a = _normalizeAngle(angle); + const s = _normalizeAngle(start); + const e = _normalizeAngle(end); + const angleToStart = _normalizeAngle(s - a); + const angleToEnd = _normalizeAngle(e - a); + const startToAngle = _normalizeAngle(a - s); + const endToAngle = _normalizeAngle(a - e); + return a === s || a === e || (sameAngleIsFullCircle && s === e) + || (angleToStart > angleToEnd && startToAngle < endToAngle); +} +function _limitValue(value, min, max) { + return Math.max(min, Math.min(max, value)); +} +function _int16Range(value) { + return _limitValue(value, -32768, 32767); +} + +function toFontString(font) { + if (!font || isNullOrUndef(font.size) || isNullOrUndef(font.family)) { + return null; + } + return (font.style ? font.style + ' ' : '') + + (font.weight ? font.weight + ' ' : '') + + font.size + 'px ' + + font.family; +} +function _measureText(ctx, data, gc, longest, string) { + let textWidth = data[string]; + if (!textWidth) { + textWidth = data[string] = ctx.measureText(string).width; + gc.push(string); + } + if (textWidth > longest) { + longest = textWidth; + } + return longest; +} +function _longestText(ctx, font, arrayOfThings, cache) { + cache = cache || {}; + let data = cache.data = cache.data || {}; + let gc = cache.garbageCollect = cache.garbageCollect || []; + if (cache.font !== font) { + data = cache.data = {}; + gc = cache.garbageCollect = []; + cache.font = font; + } + ctx.save(); + ctx.font = font; + let longest = 0; + const ilen = arrayOfThings.length; + let i, j, jlen, thing, nestedThing; + for (i = 0; i < ilen; i++) { + thing = arrayOfThings[i]; + if (thing !== undefined && thing !== null && isArray(thing) !== true) { + longest = _measureText(ctx, data, gc, longest, thing); + } else if (isArray(thing)) { + for (j = 0, jlen = thing.length; j < jlen; j++) { + nestedThing = thing[j]; + if (nestedThing !== undefined && nestedThing !== null && !isArray(nestedThing)) { + longest = _measureText(ctx, data, gc, longest, nestedThing); + } + } + } + } + ctx.restore(); + const gcLen = gc.length / 2; + if (gcLen > arrayOfThings.length) { + for (i = 0; i < gcLen; i++) { + delete data[gc[i]]; + } + gc.splice(0, gcLen); + } + return longest; +} +function _alignPixel(chart, pixel, width) { + const devicePixelRatio = chart.currentDevicePixelRatio; + const halfWidth = width !== 0 ? Math.max(width / 2, 0.5) : 0; + return Math.round((pixel - halfWidth) * devicePixelRatio) / devicePixelRatio + halfWidth; +} +function clearCanvas(canvas, ctx) { + ctx = ctx || canvas.getContext('2d'); + ctx.save(); + ctx.resetTransform(); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.restore(); +} +function drawPoint(ctx, options, x, y) { + let type, xOffset, yOffset, size, cornerRadius; + const style = options.pointStyle; + const rotation = options.rotation; + const radius = options.radius; + let rad = (rotation || 0) * RAD_PER_DEG; + if (style && typeof style === 'object') { + type = style.toString(); + if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rad); + ctx.drawImage(style, -style.width / 2, -style.height / 2, style.width, style.height); + ctx.restore(); + return; + } + } + if (isNaN(radius) || radius <= 0) { + return; + } + ctx.beginPath(); + switch (style) { + default: + ctx.arc(x, y, radius, 0, TAU); + ctx.closePath(); + break; + case 'triangle': + ctx.moveTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + ctx.closePath(); + break; + case 'rectRounded': + cornerRadius = radius * 0.516; + size = radius - cornerRadius; + xOffset = Math.cos(rad + QUARTER_PI) * size; + yOffset = Math.sin(rad + QUARTER_PI) * size; + ctx.arc(x - xOffset, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI); + ctx.arc(x + yOffset, y - xOffset, cornerRadius, rad - HALF_PI, rad); + ctx.arc(x + xOffset, y + yOffset, cornerRadius, rad, rad + HALF_PI); + ctx.arc(x - yOffset, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI); + ctx.closePath(); + break; + case 'rect': + if (!rotation) { + size = Math.SQRT1_2 * radius; + ctx.rect(x - size, y - size, 2 * size, 2 * size); + break; + } + rad += QUARTER_PI; + case 'rectRot': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + yOffset, y - xOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.lineTo(x - yOffset, y + xOffset); + ctx.closePath(); + break; + case 'crossRot': + rad += QUARTER_PI; + case 'cross': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + break; + case 'star': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + rad += QUARTER_PI; + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + break; + case 'line': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + break; + case 'dash': + ctx.moveTo(x, y); + ctx.lineTo(x + Math.cos(rad) * radius, y + Math.sin(rad) * radius); + break; + } + ctx.fill(); + if (options.borderWidth > 0) { + ctx.stroke(); + } +} +function _isPointInArea(point, area, margin) { + margin = margin || 0.5; + return point && point.x > area.left - margin && point.x < area.right + margin && + point.y > area.top - margin && point.y < area.bottom + margin; +} +function clipArea(ctx, area) { + ctx.save(); + ctx.beginPath(); + ctx.rect(area.left, area.top, area.right - area.left, area.bottom - area.top); + ctx.clip(); +} +function unclipArea(ctx) { + ctx.restore(); +} +function _steppedLineTo(ctx, previous, target, flip, mode) { + if (!previous) { + return ctx.lineTo(target.x, target.y); + } + if (mode === 'middle') { + const midpoint = (previous.x + target.x) / 2.0; + ctx.lineTo(midpoint, previous.y); + ctx.lineTo(midpoint, target.y); + } else if (mode === 'after' !== !!flip) { + ctx.lineTo(previous.x, target.y); + } else { + ctx.lineTo(target.x, previous.y); + } + ctx.lineTo(target.x, target.y); +} +function _bezierCurveTo(ctx, previous, target, flip) { + if (!previous) { + return ctx.lineTo(target.x, target.y); + } + ctx.bezierCurveTo( + flip ? previous.cp1x : previous.cp2x, + flip ? previous.cp1y : previous.cp2y, + flip ? target.cp2x : target.cp1x, + flip ? target.cp2y : target.cp1y, + target.x, + target.y); +} +function renderText(ctx, text, x, y, font, opts = {}) { + const lines = isArray(text) ? text : [text]; + const stroke = opts.strokeWidth > 0 && opts.strokeColor !== ''; + let i, line; + ctx.save(); + ctx.font = font.string; + setRenderOpts(ctx, opts); + for (i = 0; i < lines.length; ++i) { + line = lines[i]; + if (stroke) { + if (opts.strokeColor) { + ctx.strokeStyle = opts.strokeColor; + } + if (!isNullOrUndef(opts.strokeWidth)) { + ctx.lineWidth = opts.strokeWidth; + } + ctx.strokeText(line, x, y, opts.maxWidth); + } + ctx.fillText(line, x, y, opts.maxWidth); + decorateText(ctx, x, y, line, opts); + y += font.lineHeight; + } + ctx.restore(); +} +function setRenderOpts(ctx, opts) { + if (opts.translation) { + ctx.translate(opts.translation[0], opts.translation[1]); + } + if (!isNullOrUndef(opts.rotation)) { + ctx.rotate(opts.rotation); + } + if (opts.color) { + ctx.fillStyle = opts.color; + } + if (opts.textAlign) { + ctx.textAlign = opts.textAlign; + } + if (opts.textBaseline) { + ctx.textBaseline = opts.textBaseline; + } +} +function decorateText(ctx, x, y, line, opts) { + if (opts.strikethrough || opts.underline) { + const metrics = ctx.measureText(line); + const left = x - metrics.actualBoundingBoxLeft; + const right = x + metrics.actualBoundingBoxRight; + const top = y - metrics.actualBoundingBoxAscent; + const bottom = y + metrics.actualBoundingBoxDescent; + const yDecoration = opts.strikethrough ? (top + bottom) / 2 : bottom; + ctx.strokeStyle = ctx.fillStyle; + ctx.beginPath(); + ctx.lineWidth = opts.decorationWidth || 2; + ctx.moveTo(left, yDecoration); + ctx.lineTo(right, yDecoration); + ctx.stroke(); + } +} +function addRoundedRectPath(ctx, rect) { + const {x, y, w, h, radius} = rect; + ctx.arc(x + radius.topLeft, y + radius.topLeft, radius.topLeft, -HALF_PI, PI, true); + ctx.lineTo(x, y + h - radius.bottomLeft); + ctx.arc(x + radius.bottomLeft, y + h - radius.bottomLeft, radius.bottomLeft, PI, HALF_PI, true); + ctx.lineTo(x + w - radius.bottomRight, y + h); + ctx.arc(x + w - radius.bottomRight, y + h - radius.bottomRight, radius.bottomRight, HALF_PI, 0, true); + ctx.lineTo(x + w, y + radius.topRight); + ctx.arc(x + w - radius.topRight, y + radius.topRight, radius.topRight, 0, -HALF_PI, true); + ctx.lineTo(x + radius.topLeft, y); +} + +function _lookup(table, value, cmp) { + cmp = cmp || ((index) => table[index] < value); + let hi = table.length - 1; + let lo = 0; + let mid; + while (hi - lo > 1) { + mid = (lo + hi) >> 1; + if (cmp(mid)) { + lo = mid; + } else { + hi = mid; + } + } + return {lo, hi}; +} +const _lookupByKey = (table, key, value) => + _lookup(table, value, index => table[index][key] < value); +const _rlookupByKey = (table, key, value) => + _lookup(table, value, index => table[index][key] >= value); +function _filterBetween(values, min, max) { + let start = 0; + let end = values.length; + while (start < end && values[start] < min) { + start++; + } + while (end > start && values[end - 1] > max) { + end--; + } + return start > 0 || end < values.length + ? values.slice(start, end) + : values; +} +const arrayEvents = ['push', 'pop', 'shift', 'splice', 'unshift']; +function listenArrayEvents(array, listener) { + if (array._chartjs) { + array._chartjs.listeners.push(listener); + return; + } + Object.defineProperty(array, '_chartjs', { + configurable: true, + enumerable: false, + value: { + listeners: [listener] + } + }); + arrayEvents.forEach((key) => { + const method = '_onData' + _capitalize(key); + const base = array[key]; + Object.defineProperty(array, key, { + configurable: true, + enumerable: false, + value(...args) { + const res = base.apply(this, args); + array._chartjs.listeners.forEach((object) => { + if (typeof object[method] === 'function') { + object[method](...args); + } + }); + return res; + } + }); + }); +} +function unlistenArrayEvents(array, listener) { + const stub = array._chartjs; + if (!stub) { + return; + } + const listeners = stub.listeners; + const index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + if (listeners.length > 0) { + return; + } + arrayEvents.forEach((key) => { + delete array[key]; + }); + delete array._chartjs; +} +function _arrayUnique(items) { + const set = new Set(); + let i, ilen; + for (i = 0, ilen = items.length; i < ilen; ++i) { + set.add(items[i]); + } + if (set.size === ilen) { + return items; + } + return Array.from(set); +} + +function _getParentNode(domNode) { + let parent = domNode.parentNode; + if (parent && parent.toString() === '[object ShadowRoot]') { + parent = parent.host; + } + return parent; +} +function parseMaxStyle(styleValue, node, parentProperty) { + let valueInPixels; + if (typeof styleValue === 'string') { + valueInPixels = parseInt(styleValue, 10); + if (styleValue.indexOf('%') !== -1) { + valueInPixels = valueInPixels / 100 * node.parentNode[parentProperty]; + } + } else { + valueInPixels = styleValue; + } + return valueInPixels; +} +const getComputedStyle = (element) => window.getComputedStyle(element, null); +function getStyle(el, property) { + return getComputedStyle(el).getPropertyValue(property); +} +const positions = ['top', 'right', 'bottom', 'left']; +function getPositionedStyle(styles, style, suffix) { + const result = {}; + suffix = suffix ? '-' + suffix : ''; + for (let i = 0; i < 4; i++) { + const pos = positions[i]; + result[pos] = parseFloat(styles[style + '-' + pos + suffix]) || 0; + } + result.width = result.left + result.right; + result.height = result.top + result.bottom; + return result; +} +const useOffsetPos = (x, y, target) => (x > 0 || y > 0) && (!target || !target.shadowRoot); +function getCanvasPosition(evt, canvas) { + const e = evt.native || evt; + const touches = e.touches; + const source = touches && touches.length ? touches[0] : e; + const {offsetX, offsetY} = source; + let box = false; + let x, y; + if (useOffsetPos(offsetX, offsetY, e.target)) { + x = offsetX; + y = offsetY; + } else { + const rect = canvas.getBoundingClientRect(); + x = source.clientX - rect.left; + y = source.clientY - rect.top; + box = true; + } + return {x, y, box}; +} +function getRelativePosition$1(evt, chart) { + const {canvas, currentDevicePixelRatio} = chart; + const style = getComputedStyle(canvas); + const borderBox = style.boxSizing === 'border-box'; + const paddings = getPositionedStyle(style, 'padding'); + const borders = getPositionedStyle(style, 'border', 'width'); + const {x, y, box} = getCanvasPosition(evt, canvas); + const xOffset = paddings.left + (box && borders.left); + const yOffset = paddings.top + (box && borders.top); + let {width, height} = chart; + if (borderBox) { + width -= paddings.width + borders.width; + height -= paddings.height + borders.height; + } + return { + x: Math.round((x - xOffset) / width * canvas.width / currentDevicePixelRatio), + y: Math.round((y - yOffset) / height * canvas.height / currentDevicePixelRatio) + }; +} +function getContainerSize(canvas, width, height) { + let maxWidth, maxHeight; + if (width === undefined || height === undefined) { + const container = _getParentNode(canvas); + if (!container) { + width = canvas.clientWidth; + height = canvas.clientHeight; + } else { + const rect = container.getBoundingClientRect(); + const containerStyle = getComputedStyle(container); + const containerBorder = getPositionedStyle(containerStyle, 'border', 'width'); + const containerPadding = getPositionedStyle(containerStyle, 'padding'); + width = rect.width - containerPadding.width - containerBorder.width; + height = rect.height - containerPadding.height - containerBorder.height; + maxWidth = parseMaxStyle(containerStyle.maxWidth, container, 'clientWidth'); + maxHeight = parseMaxStyle(containerStyle.maxHeight, container, 'clientHeight'); + } + } + return { + width, + height, + maxWidth: maxWidth || INFINITY, + maxHeight: maxHeight || INFINITY + }; +} +const round1 = v => Math.round(v * 10) / 10; +function getMaximumSize(canvas, bbWidth, bbHeight, aspectRatio) { + const style = getComputedStyle(canvas); + const margins = getPositionedStyle(style, 'margin'); + const maxWidth = parseMaxStyle(style.maxWidth, canvas, 'clientWidth') || INFINITY; + const maxHeight = parseMaxStyle(style.maxHeight, canvas, 'clientHeight') || INFINITY; + const containerSize = getContainerSize(canvas, bbWidth, bbHeight); + let {width, height} = containerSize; + if (style.boxSizing === 'content-box') { + const borders = getPositionedStyle(style, 'border', 'width'); + const paddings = getPositionedStyle(style, 'padding'); + width -= paddings.width + borders.width; + height -= paddings.height + borders.height; + } + width = Math.max(0, width - margins.width); + height = Math.max(0, aspectRatio ? Math.floor(width / aspectRatio) : height - margins.height); + width = round1(Math.min(width, maxWidth, containerSize.maxWidth)); + height = round1(Math.min(height, maxHeight, containerSize.maxHeight)); + if (width && !height) { + height = round1(width / 2); + } + return { + width, + height + }; +} +function retinaScale(chart, forceRatio, forceStyle) { + const pixelRatio = forceRatio || 1; + const deviceHeight = Math.floor(chart.height * pixelRatio); + const deviceWidth = Math.floor(chart.width * pixelRatio); + chart.height = deviceHeight / pixelRatio; + chart.width = deviceWidth / pixelRatio; + const canvas = chart.canvas; + if (canvas.style && (forceStyle || (!canvas.style.height && !canvas.style.width))) { + canvas.style.height = `${chart.height}px`; + canvas.style.width = `${chart.width}px`; + } + if (chart.currentDevicePixelRatio !== pixelRatio + || canvas.height !== deviceHeight + || canvas.width !== deviceWidth) { + chart.currentDevicePixelRatio = pixelRatio; + canvas.height = deviceHeight; + canvas.width = deviceWidth; + chart.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + return true; + } + return false; +} +const supportsEventListenerOptions = (function() { + let passiveSupported = false; + try { + const options = { + get passive() { + passiveSupported = true; + return false; + } + }; + window.addEventListener('test', null, options); + window.removeEventListener('test', null, options); + } catch (e) { + } + return passiveSupported; +}()); +function readUsedSize(element, property) { + const value = getStyle(element, property); + const matches = value && value.match(/^(\d+)(\.\d+)?px$/); + return matches ? +matches[1] : undefined; +} + +function getRelativePosition(e, chart) { + if ('native' in e) { + return { + x: e.x, + y: e.y + }; + } + return getRelativePosition$1(e, chart); +} +function evaluateAllVisibleItems(chart, handler) { + const metasets = chart.getSortedVisibleDatasetMetas(); + let index, data, element; + for (let i = 0, ilen = metasets.length; i < ilen; ++i) { + ({index, data} = metasets[i]); + for (let j = 0, jlen = data.length; j < jlen; ++j) { + element = data[j]; + if (!element.skip) { + handler(element, index, j); + } + } + } +} +function binarySearch(metaset, axis, value, intersect) { + const {controller, data, _sorted} = metaset; + const iScale = controller._cachedMeta.iScale; + if (iScale && axis === iScale.axis && _sorted && data.length) { + const lookupMethod = iScale._reversePixels ? _rlookupByKey : _lookupByKey; + if (!intersect) { + return lookupMethod(data, axis, value); + } else if (controller._sharedOptions) { + const el = data[0]; + const range = typeof el.getRange === 'function' && el.getRange(axis); + if (range) { + const start = lookupMethod(data, axis, value - range); + const end = lookupMethod(data, axis, value + range); + return {lo: start.lo, hi: end.hi}; + } + } + } + return {lo: 0, hi: data.length - 1}; +} +function optimizedEvaluateItems(chart, axis, position, handler, intersect) { + const metasets = chart.getSortedVisibleDatasetMetas(); + const value = position[axis]; + for (let i = 0, ilen = metasets.length; i < ilen; ++i) { + const {index, data} = metasets[i]; + const {lo, hi} = binarySearch(metasets[i], axis, value, intersect); + for (let j = lo; j <= hi; ++j) { + const element = data[j]; + if (!element.skip) { + handler(element, index, j); + } + } + } +} +function getDistanceMetricForAxis(axis) { + const useX = axis.indexOf('x') !== -1; + const useY = axis.indexOf('y') !== -1; + return function(pt1, pt2) { + const deltaX = useX ? Math.abs(pt1.x - pt2.x) : 0; + const deltaY = useY ? Math.abs(pt1.y - pt2.y) : 0; + return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); + }; +} +function getIntersectItems(chart, position, axis, useFinalPosition) { + const items = []; + if (!_isPointInArea(position, chart.chartArea, chart._minPadding)) { + return items; + } + const evaluationFunc = function(element, datasetIndex, index) { + if (element.inRange(position.x, position.y, useFinalPosition)) { + items.push({element, datasetIndex, index}); + } + }; + optimizedEvaluateItems(chart, axis, position, evaluationFunc, true); + return items; +} +function getNearestItems(chart, position, axis, intersect, useFinalPosition) { + const distanceMetric = getDistanceMetricForAxis(axis); + let minDistance = Number.POSITIVE_INFINITY; + let items = []; + if (!_isPointInArea(position, chart.chartArea, chart._minPadding)) { + return items; + } + const evaluationFunc = function(element, datasetIndex, index) { + if (intersect && !element.inRange(position.x, position.y, useFinalPosition)) { + return; + } + const center = element.getCenterPoint(useFinalPosition); + if (!_isPointInArea(center, chart.chartArea, chart._minPadding)) { + return; + } + const distance = distanceMetric(position, center); + if (distance < minDistance) { + items = [{element, datasetIndex, index}]; + minDistance = distance; + } else if (distance === minDistance) { + items.push({element, datasetIndex, index}); + } + }; + optimizedEvaluateItems(chart, axis, position, evaluationFunc); + return items; +} +function getAxisItems(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const items = []; + const axis = options.axis; + const rangeMethod = axis === 'x' ? 'inXRange' : 'inYRange'; + let intersectsItem = false; + evaluateAllVisibleItems(chart, (element, datasetIndex, index) => { + if (element[rangeMethod](position[axis], useFinalPosition)) { + items.push({element, datasetIndex, index}); + } + if (element.inRange(position.x, position.y, useFinalPosition)) { + intersectsItem = true; + } + }); + if (options.intersect && !intersectsItem) { + return []; + } + return items; +} +var Interaction = { + modes: { + index(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const axis = options.axis || 'x'; + const items = options.intersect + ? getIntersectItems(chart, position, axis, useFinalPosition) + : getNearestItems(chart, position, axis, false, useFinalPosition); + const elements = []; + if (!items.length) { + return []; + } + chart.getSortedVisibleDatasetMetas().forEach((meta) => { + const index = items[0].index; + const element = meta.data[index]; + if (element && !element.skip) { + elements.push({element, datasetIndex: meta.index, index}); + } + }); + return elements; + }, + dataset(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const axis = options.axis || 'xy'; + let items = options.intersect + ? getIntersectItems(chart, position, axis, useFinalPosition) : + getNearestItems(chart, position, axis, false, useFinalPosition); + if (items.length > 0) { + const datasetIndex = items[0].datasetIndex; + const data = chart.getDatasetMeta(datasetIndex).data; + items = []; + for (let i = 0; i < data.length; ++i) { + items.push({element: data[i], datasetIndex, index: i}); + } + } + return items; + }, + point(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const axis = options.axis || 'xy'; + return getIntersectItems(chart, position, axis, useFinalPosition); + }, + nearest(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const axis = options.axis || 'xy'; + return getNearestItems(chart, position, axis, options.intersect, useFinalPosition); + }, + x(chart, e, options, useFinalPosition) { + options.axis = 'x'; + return getAxisItems(chart, e, options, useFinalPosition); + }, + y(chart, e, options, useFinalPosition) { + options.axis = 'y'; + return getAxisItems(chart, e, options, useFinalPosition); + } + } +}; + +const LINE_HEIGHT = new RegExp(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/); +const FONT_STYLE = new RegExp(/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/); +function toLineHeight(value, size) { + const matches = ('' + value).match(LINE_HEIGHT); + if (!matches || matches[1] === 'normal') { + return size * 1.2; + } + value = +matches[2]; + switch (matches[3]) { + case 'px': + return value; + case '%': + value /= 100; + break; + } + return size * value; +} +const numberOrZero$1 = v => +v || 0; +function _readValueToProps(value, props) { + const ret = {}; + const objProps = isObject(props); + const keys = objProps ? Object.keys(props) : props; + const read = isObject(value) + ? objProps + ? prop => valueOrDefault(value[prop], value[props[prop]]) + : prop => value[prop] + : () => value; + for (const prop of keys) { + ret[prop] = numberOrZero$1(read(prop)); + } + return ret; +} +function toTRBL(value) { + return _readValueToProps(value, {top: 'y', right: 'x', bottom: 'y', left: 'x'}); +} +function toTRBLCorners(value) { + return _readValueToProps(value, ['topLeft', 'topRight', 'bottomLeft', 'bottomRight']); +} +function toPadding(value) { + const obj = toTRBL(value); + obj.width = obj.left + obj.right; + obj.height = obj.top + obj.bottom; + return obj; +} +function toFont(options, fallback) { + options = options || {}; + fallback = fallback || defaults.font; + let size = valueOrDefault(options.size, fallback.size); + if (typeof size === 'string') { + size = parseInt(size, 10); + } + let style = valueOrDefault(options.style, fallback.style); + if (style && !('' + style).match(FONT_STYLE)) { + console.warn('Invalid font style specified: "' + style + '"'); + style = ''; + } + const font = { + family: valueOrDefault(options.family, fallback.family), + lineHeight: toLineHeight(valueOrDefault(options.lineHeight, fallback.lineHeight), size), + size, + style, + weight: valueOrDefault(options.weight, fallback.weight), + string: '' + }; + font.string = toFontString(font); + return font; +} +function resolve(inputs, context, index, info) { + let cacheable = true; + let i, ilen, value; + for (i = 0, ilen = inputs.length; i < ilen; ++i) { + value = inputs[i]; + if (value === undefined) { + continue; + } + if (context !== undefined && typeof value === 'function') { + value = value(context); + cacheable = false; + } + if (index !== undefined && isArray(value)) { + value = value[index % value.length]; + cacheable = false; + } + if (value !== undefined) { + if (info && !cacheable) { + info.cacheable = false; + } + return value; + } + } +} +function _addGrace(minmax, grace) { + const {min, max} = minmax; + return { + min: min - Math.abs(toDimension(grace, min)), + max: max + toDimension(grace, max) + }; +} + +const STATIC_POSITIONS = ['left', 'top', 'right', 'bottom']; +function filterByPosition(array, position) { + return array.filter(v => v.pos === position); +} +function filterDynamicPositionByAxis(array, axis) { + return array.filter(v => STATIC_POSITIONS.indexOf(v.pos) === -1 && v.box.axis === axis); +} +function sortByWeight(array, reverse) { + return array.sort((a, b) => { + const v0 = reverse ? b : a; + const v1 = reverse ? a : b; + return v0.weight === v1.weight ? + v0.index - v1.index : + v0.weight - v1.weight; + }); +} +function wrapBoxes(boxes) { + const layoutBoxes = []; + let i, ilen, box; + for (i = 0, ilen = (boxes || []).length; i < ilen; ++i) { + box = boxes[i]; + layoutBoxes.push({ + index: i, + box, + pos: box.position, + horizontal: box.isHorizontal(), + weight: box.weight + }); + } + return layoutBoxes; +} +function setLayoutDims(layouts, params) { + let i, ilen, layout; + for (i = 0, ilen = layouts.length; i < ilen; ++i) { + layout = layouts[i]; + if (layout.horizontal) { + layout.width = layout.box.fullSize && params.availableWidth; + layout.height = params.hBoxMaxHeight; + } else { + layout.width = params.vBoxMaxWidth; + layout.height = layout.box.fullSize && params.availableHeight; + } + } +} +function buildLayoutBoxes(boxes) { + const layoutBoxes = wrapBoxes(boxes); + const fullSize = sortByWeight(layoutBoxes.filter(wrap => wrap.box.fullSize), true); + const left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true); + const right = sortByWeight(filterByPosition(layoutBoxes, 'right')); + const top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true); + const bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom')); + const centerHorizontal = filterDynamicPositionByAxis(layoutBoxes, 'x'); + const centerVertical = filterDynamicPositionByAxis(layoutBoxes, 'y'); + return { + fullSize, + leftAndTop: left.concat(top), + rightAndBottom: right.concat(centerVertical).concat(bottom).concat(centerHorizontal), + chartArea: filterByPosition(layoutBoxes, 'chartArea'), + vertical: left.concat(right).concat(centerVertical), + horizontal: top.concat(bottom).concat(centerHorizontal) + }; +} +function getCombinedMax(maxPadding, chartArea, a, b) { + return Math.max(maxPadding[a], chartArea[a]) + Math.max(maxPadding[b], chartArea[b]); +} +function updateMaxPadding(maxPadding, boxPadding) { + maxPadding.top = Math.max(maxPadding.top, boxPadding.top); + maxPadding.left = Math.max(maxPadding.left, boxPadding.left); + maxPadding.bottom = Math.max(maxPadding.bottom, boxPadding.bottom); + maxPadding.right = Math.max(maxPadding.right, boxPadding.right); +} +function updateDims(chartArea, params, layout) { + const box = layout.box; + const maxPadding = chartArea.maxPadding; + if (!isObject(layout.pos)) { + if (layout.size) { + chartArea[layout.pos] -= layout.size; + } + layout.size = layout.horizontal ? box.height : box.width; + chartArea[layout.pos] += layout.size; + } + if (box.getPadding) { + updateMaxPadding(maxPadding, box.getPadding()); + } + const newWidth = Math.max(0, params.outerWidth - getCombinedMax(maxPadding, chartArea, 'left', 'right')); + const newHeight = Math.max(0, params.outerHeight - getCombinedMax(maxPadding, chartArea, 'top', 'bottom')); + const widthChanged = newWidth !== chartArea.w; + const heightChanged = newHeight !== chartArea.h; + chartArea.w = newWidth; + chartArea.h = newHeight; + return layout.horizontal + ? {same: widthChanged, other: heightChanged} + : {same: heightChanged, other: widthChanged}; +} +function handleMaxPadding(chartArea) { + const maxPadding = chartArea.maxPadding; + function updatePos(pos) { + const change = Math.max(maxPadding[pos] - chartArea[pos], 0); + chartArea[pos] += change; + return change; + } + chartArea.y += updatePos('top'); + chartArea.x += updatePos('left'); + updatePos('right'); + updatePos('bottom'); +} +function getMargins(horizontal, chartArea) { + const maxPadding = chartArea.maxPadding; + function marginForPositions(positions) { + const margin = {left: 0, top: 0, right: 0, bottom: 0}; + positions.forEach((pos) => { + margin[pos] = Math.max(chartArea[pos], maxPadding[pos]); + }); + return margin; + } + return horizontal + ? marginForPositions(['left', 'right']) + : marginForPositions(['top', 'bottom']); +} +function fitBoxes(boxes, chartArea, params) { + const refitBoxes = []; + let i, ilen, layout, box, refit, changed; + for (i = 0, ilen = boxes.length, refit = 0; i < ilen; ++i) { + layout = boxes[i]; + box = layout.box; + box.update( + layout.width || chartArea.w, + layout.height || chartArea.h, + getMargins(layout.horizontal, chartArea) + ); + const {same, other} = updateDims(chartArea, params, layout); + refit |= same && refitBoxes.length; + changed = changed || other; + if (!box.fullSize) { + refitBoxes.push(layout); + } + } + return refit && fitBoxes(refitBoxes, chartArea, params) || changed; +} +function placeBoxes(boxes, chartArea, params) { + const userPadding = params.padding; + let x = chartArea.x; + let y = chartArea.y; + let i, ilen, layout, box; + for (i = 0, ilen = boxes.length; i < ilen; ++i) { + layout = boxes[i]; + box = layout.box; + if (layout.horizontal) { + box.left = box.fullSize ? userPadding.left : chartArea.left; + box.right = box.fullSize ? params.outerWidth - userPadding.right : chartArea.left + chartArea.w; + box.top = y; + box.bottom = y + box.height; + box.width = box.right - box.left; + y = box.bottom; + } else { + box.left = x; + box.right = x + box.width; + box.top = box.fullSize ? userPadding.top : chartArea.top; + box.bottom = box.fullSize ? params.outerHeight - userPadding.bottom : chartArea.top + chartArea.h; + box.height = box.bottom - box.top; + x = box.right; + } + } + chartArea.x = x; + chartArea.y = y; +} +defaults.set('layout', { + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0 + } +}); +var layouts = { + addBox(chart, item) { + if (!chart.boxes) { + chart.boxes = []; + } + item.fullSize = item.fullSize || false; + item.position = item.position || 'top'; + item.weight = item.weight || 0; + item._layers = item._layers || function() { + return [{ + z: 0, + draw(chartArea) { + item.draw(chartArea); + } + }]; + }; + chart.boxes.push(item); + }, + removeBox(chart, layoutItem) { + const index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; + if (index !== -1) { + chart.boxes.splice(index, 1); + } + }, + configure(chart, item, options) { + item.fullSize = options.fullSize; + item.position = options.position; + item.weight = options.weight; + }, + update(chart, width, height, minPadding) { + if (!chart) { + return; + } + const padding = toPadding(chart.options.layout.padding); + const availableWidth = Math.max(width - padding.width, 0); + const availableHeight = Math.max(height - padding.height, 0); + const boxes = buildLayoutBoxes(chart.boxes); + const verticalBoxes = boxes.vertical; + const horizontalBoxes = boxes.horizontal; + each(chart.boxes, box => { + if (typeof box.beforeLayout === 'function') { + box.beforeLayout(); + } + }); + const visibleVerticalBoxCount = verticalBoxes.reduce((total, wrap) => + wrap.box.options && wrap.box.options.display === false ? total : total + 1, 0) || 1; + const params = Object.freeze({ + outerWidth: width, + outerHeight: height, + padding, + availableWidth, + availableHeight, + vBoxMaxWidth: availableWidth / 2 / visibleVerticalBoxCount, + hBoxMaxHeight: availableHeight / 2 + }); + const maxPadding = Object.assign({}, padding); + updateMaxPadding(maxPadding, toPadding(minPadding)); + const chartArea = Object.assign({ + maxPadding, + w: availableWidth, + h: availableHeight, + x: padding.left, + y: padding.top + }, padding); + setLayoutDims(verticalBoxes.concat(horizontalBoxes), params); + fitBoxes(boxes.fullSize, chartArea, params); + fitBoxes(verticalBoxes, chartArea, params); + if (fitBoxes(horizontalBoxes, chartArea, params)) { + fitBoxes(verticalBoxes, chartArea, params); + } + handleMaxPadding(chartArea); + placeBoxes(boxes.leftAndTop, chartArea, params); + chartArea.x += chartArea.w; + chartArea.y += chartArea.h; + placeBoxes(boxes.rightAndBottom, chartArea, params); + chart.chartArea = { + left: chartArea.left, + top: chartArea.top, + right: chartArea.left + chartArea.w, + bottom: chartArea.top + chartArea.h, + height: chartArea.h, + width: chartArea.w, + }; + each(boxes.chartArea, (layout) => { + const box = layout.box; + Object.assign(box, chart.chartArea); + box.update(chartArea.w, chartArea.h); + }); + } +}; + +class BasePlatform { + acquireContext(canvas, aspectRatio) {} + releaseContext(context) { + return false; + } + addEventListener(chart, type, listener) {} + removeEventListener(chart, type, listener) {} + getDevicePixelRatio() { + return 1; + } + getMaximumSize(element, width, height, aspectRatio) { + width = Math.max(0, width || element.width); + height = height || element.height; + return { + width, + height: Math.max(0, aspectRatio ? Math.floor(width / aspectRatio) : height) + }; + } + isAttached(canvas) { + return true; + } +} + +class BasicPlatform extends BasePlatform { + acquireContext(item) { + return item && item.getContext && item.getContext('2d') || null; + } +} + +const EXPANDO_KEY = '$chartjs'; +const EVENT_TYPES = { + touchstart: 'mousedown', + touchmove: 'mousemove', + touchend: 'mouseup', + pointerenter: 'mouseenter', + pointerdown: 'mousedown', + pointermove: 'mousemove', + pointerup: 'mouseup', + pointerleave: 'mouseout', + pointerout: 'mouseout' +}; +const isNullOrEmpty = value => value === null || value === ''; +function initCanvas(canvas, aspectRatio) { + const style = canvas.style; + const renderHeight = canvas.getAttribute('height'); + const renderWidth = canvas.getAttribute('width'); + canvas[EXPANDO_KEY] = { + initial: { + height: renderHeight, + width: renderWidth, + style: { + display: style.display, + height: style.height, + width: style.width + } + } + }; + style.display = style.display || 'block'; + style.boxSizing = style.boxSizing || 'border-box'; + if (isNullOrEmpty(renderWidth)) { + const displayWidth = readUsedSize(canvas, 'width'); + if (displayWidth !== undefined) { + canvas.width = displayWidth; + } + } + if (isNullOrEmpty(renderHeight)) { + if (canvas.style.height === '') { + canvas.height = canvas.width / (aspectRatio || 2); + } else { + const displayHeight = readUsedSize(canvas, 'height'); + if (displayHeight !== undefined) { + canvas.height = displayHeight; + } + } + } + return canvas; +} +const eventListenerOptions = supportsEventListenerOptions ? {passive: true} : false; +function addListener(node, type, listener) { + node.addEventListener(type, listener, eventListenerOptions); +} +function removeListener(chart, type, listener) { + chart.canvas.removeEventListener(type, listener, eventListenerOptions); +} +function fromNativeEvent(event, chart) { + const type = EVENT_TYPES[event.type] || event.type; + const {x, y} = getRelativePosition$1(event, chart); + return { + type, + chart, + native: event, + x: x !== undefined ? x : null, + y: y !== undefined ? y : null, + }; +} +function createAttachObserver(chart, type, listener) { + const canvas = chart.canvas; + const container = canvas && _getParentNode(canvas); + const element = container || canvas; + const observer = new MutationObserver(entries => { + const parent = _getParentNode(element); + entries.forEach(entry => { + for (let i = 0; i < entry.addedNodes.length; i++) { + const added = entry.addedNodes[i]; + if (added === element || added === parent) { + listener(entry.target); + } + } + }); + }); + observer.observe(document, {childList: true, subtree: true}); + return observer; +} +function createDetachObserver(chart, type, listener) { + const canvas = chart.canvas; + const container = canvas && _getParentNode(canvas); + if (!container) { + return; + } + const observer = new MutationObserver(entries => { + entries.forEach(entry => { + for (let i = 0; i < entry.removedNodes.length; i++) { + if (entry.removedNodes[i] === canvas) { + listener(); + break; + } + } + }); + }); + observer.observe(container, {childList: true}); + return observer; +} +const drpListeningCharts = new Map(); +let oldDevicePixelRatio = 0; +function onWindowResize() { + const dpr = window.devicePixelRatio; + if (dpr === oldDevicePixelRatio) { + return; + } + oldDevicePixelRatio = dpr; + drpListeningCharts.forEach((resize, chart) => { + if (chart.currentDevicePixelRatio !== dpr) { + resize(); + } + }); +} +function listenDevicePixelRatioChanges(chart, resize) { + if (!drpListeningCharts.size) { + window.addEventListener('resize', onWindowResize); + } + drpListeningCharts.set(chart, resize); +} +function unlistenDevicePixelRatioChanges(chart) { + drpListeningCharts.delete(chart); + if (!drpListeningCharts.size) { + window.removeEventListener('resize', onWindowResize); + } +} +function createResizeObserver(chart, type, listener) { + const canvas = chart.canvas; + const container = canvas && _getParentNode(canvas); + if (!container) { + return; + } + const resize = throttled((width, height) => { + const w = container.clientWidth; + listener(width, height); + if (w < container.clientWidth) { + listener(); + } + }, window); + const observer = new ResizeObserver(entries => { + const entry = entries[0]; + const width = entry.contentRect.width; + const height = entry.contentRect.height; + if (width === 0 && height === 0) { + return; + } + resize(width, height); + }); + observer.observe(container); + listenDevicePixelRatioChanges(chart, resize); + return observer; +} +function releaseObserver(chart, type, observer) { + if (observer) { + observer.disconnect(); + } + if (type === 'resize') { + unlistenDevicePixelRatioChanges(chart); + } +} +function createProxyAndListen(chart, type, listener) { + const canvas = chart.canvas; + const proxy = throttled((event) => { + if (chart.ctx !== null) { + listener(fromNativeEvent(event, chart)); + } + }, chart, (args) => { + const event = args[0]; + return [event, event.offsetX, event.offsetY]; + }); + addListener(canvas, type, proxy); + return proxy; +} +class DomPlatform extends BasePlatform { + acquireContext(canvas, aspectRatio) { + const context = canvas && canvas.getContext && canvas.getContext('2d'); + if (context && context.canvas === canvas) { + initCanvas(canvas, aspectRatio); + return context; + } + return null; + } + releaseContext(context) { + const canvas = context.canvas; + if (!canvas[EXPANDO_KEY]) { + return false; + } + const initial = canvas[EXPANDO_KEY].initial; + ['height', 'width'].forEach((prop) => { + const value = initial[prop]; + if (isNullOrUndef(value)) { + canvas.removeAttribute(prop); + } else { + canvas.setAttribute(prop, value); + } + }); + const style = initial.style || {}; + Object.keys(style).forEach((key) => { + canvas.style[key] = style[key]; + }); + canvas.width = canvas.width; + delete canvas[EXPANDO_KEY]; + return true; + } + addEventListener(chart, type, listener) { + this.removeEventListener(chart, type); + const proxies = chart.$proxies || (chart.$proxies = {}); + const handlers = { + attach: createAttachObserver, + detach: createDetachObserver, + resize: createResizeObserver + }; + const handler = handlers[type] || createProxyAndListen; + proxies[type] = handler(chart, type, listener); + } + removeEventListener(chart, type) { + const proxies = chart.$proxies || (chart.$proxies = {}); + const proxy = proxies[type]; + if (!proxy) { + return; + } + const handlers = { + attach: releaseObserver, + detach: releaseObserver, + resize: releaseObserver + }; + const handler = handlers[type] || removeListener; + handler(chart, type, proxy); + proxies[type] = undefined; + } + getDevicePixelRatio() { + return window.devicePixelRatio; + } + getMaximumSize(canvas, width, height, aspectRatio) { + return getMaximumSize(canvas, width, height, aspectRatio); + } + isAttached(canvas) { + const container = _getParentNode(canvas); + return !!(container && _getParentNode(container)); + } +} + +var platforms = /*#__PURE__*/Object.freeze({ +__proto__: null, +BasePlatform: BasePlatform, +BasicPlatform: BasicPlatform, +DomPlatform: DomPlatform +}); + +const atEdge = (t) => t === 0 || t === 1; +const elasticIn = (t, s, p) => -(Math.pow(2, 10 * (t -= 1)) * Math.sin((t - s) * TAU / p)); +const elasticOut = (t, s, p) => Math.pow(2, -10 * t) * Math.sin((t - s) * TAU / p) + 1; +const effects = { + linear: t => t, + easeInQuad: t => t * t, + easeOutQuad: t => -t * (t - 2), + easeInOutQuad: t => ((t /= 0.5) < 1) + ? 0.5 * t * t + : -0.5 * ((--t) * (t - 2) - 1), + easeInCubic: t => t * t * t, + easeOutCubic: t => (t -= 1) * t * t + 1, + easeInOutCubic: t => ((t /= 0.5) < 1) + ? 0.5 * t * t * t + : 0.5 * ((t -= 2) * t * t + 2), + easeInQuart: t => t * t * t * t, + easeOutQuart: t => -((t -= 1) * t * t * t - 1), + easeInOutQuart: t => ((t /= 0.5) < 1) + ? 0.5 * t * t * t * t + : -0.5 * ((t -= 2) * t * t * t - 2), + easeInQuint: t => t * t * t * t * t, + easeOutQuint: t => (t -= 1) * t * t * t * t + 1, + easeInOutQuint: t => ((t /= 0.5) < 1) + ? 0.5 * t * t * t * t * t + : 0.5 * ((t -= 2) * t * t * t * t + 2), + easeInSine: t => -Math.cos(t * HALF_PI) + 1, + easeOutSine: t => Math.sin(t * HALF_PI), + easeInOutSine: t => -0.5 * (Math.cos(PI * t) - 1), + easeInExpo: t => (t === 0) ? 0 : Math.pow(2, 10 * (t - 1)), + easeOutExpo: t => (t === 1) ? 1 : -Math.pow(2, -10 * t) + 1, + easeInOutExpo: t => atEdge(t) ? t : t < 0.5 + ? 0.5 * Math.pow(2, 10 * (t * 2 - 1)) + : 0.5 * (-Math.pow(2, -10 * (t * 2 - 1)) + 2), + easeInCirc: t => (t >= 1) ? t : -(Math.sqrt(1 - t * t) - 1), + easeOutCirc: t => Math.sqrt(1 - (t -= 1) * t), + easeInOutCirc: t => ((t /= 0.5) < 1) + ? -0.5 * (Math.sqrt(1 - t * t) - 1) + : 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1), + easeInElastic: t => atEdge(t) ? t : elasticIn(t, 0.075, 0.3), + easeOutElastic: t => atEdge(t) ? t : elasticOut(t, 0.075, 0.3), + easeInOutElastic(t) { + const s = 0.1125; + const p = 0.45; + return atEdge(t) ? t : + t < 0.5 + ? 0.5 * elasticIn(t * 2, s, p) + : 0.5 + 0.5 * elasticOut(t * 2 - 1, s, p); + }, + easeInBack(t) { + const s = 1.70158; + return t * t * ((s + 1) * t - s); + }, + easeOutBack(t) { + const s = 1.70158; + return (t -= 1) * t * ((s + 1) * t + s) + 1; + }, + easeInOutBack(t) { + let s = 1.70158; + if ((t /= 0.5) < 1) { + return 0.5 * (t * t * (((s *= (1.525)) + 1) * t - s)); + } + return 0.5 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + easeInBounce: t => 1 - effects.easeOutBounce(1 - t), + easeOutBounce(t) { + const m = 7.5625; + const d = 2.75; + if (t < (1 / d)) { + return m * t * t; + } + if (t < (2 / d)) { + return m * (t -= (1.5 / d)) * t + 0.75; + } + if (t < (2.5 / d)) { + return m * (t -= (2.25 / d)) * t + 0.9375; + } + return m * (t -= (2.625 / d)) * t + 0.984375; + }, + easeInOutBounce: t => (t < 0.5) + ? effects.easeInBounce(t * 2) * 0.5 + : effects.easeOutBounce(t * 2 - 1) * 0.5 + 0.5, +}; + +const transparent = 'transparent'; +const interpolators = { + boolean(from, to, factor) { + return factor > 0.5 ? to : from; + }, + color(from, to, factor) { + const c0 = color(from || transparent); + const c1 = c0.valid && color(to || transparent); + return c1 && c1.valid + ? c1.mix(c0, factor).hexString() + : to; + }, + number(from, to, factor) { + return from + (to - from) * factor; + } +}; +class Animation { + constructor(cfg, target, prop, to) { + const currentValue = target[prop]; + to = resolve([cfg.to, to, currentValue, cfg.from]); + const from = resolve([cfg.from, currentValue, to]); + this._active = true; + this._fn = cfg.fn || interpolators[cfg.type || typeof from]; + this._easing = effects[cfg.easing] || effects.linear; + this._start = Math.floor(Date.now() + (cfg.delay || 0)); + this._duration = this._total = Math.floor(cfg.duration); + this._loop = !!cfg.loop; + this._target = target; + this._prop = prop; + this._from = from; + this._to = to; + this._promises = undefined; + } + active() { + return this._active; + } + update(cfg, to, date) { + const me = this; + if (me._active) { + me._notify(false); + const currentValue = me._target[me._prop]; + const elapsed = date - me._start; + const remain = me._duration - elapsed; + me._start = date; + me._duration = Math.floor(Math.max(remain, cfg.duration)); + me._total += elapsed; + me._loop = !!cfg.loop; + me._to = resolve([cfg.to, to, currentValue, cfg.from]); + me._from = resolve([cfg.from, currentValue, to]); + } + } + cancel() { + const me = this; + if (me._active) { + me.tick(Date.now()); + me._active = false; + me._notify(false); + } + } + tick(date) { + const me = this; + const elapsed = date - me._start; + const duration = me._duration; + const prop = me._prop; + const from = me._from; + const loop = me._loop; + const to = me._to; + let factor; + me._active = from !== to && (loop || (elapsed < duration)); + if (!me._active) { + me._target[prop] = to; + me._notify(true); + return; + } + if (elapsed < 0) { + me._target[prop] = from; + return; + } + factor = (elapsed / duration) % 2; + factor = loop && factor > 1 ? 2 - factor : factor; + factor = me._easing(Math.min(1, Math.max(0, factor))); + me._target[prop] = me._fn(from, to, factor); + } + wait() { + const promises = this._promises || (this._promises = []); + return new Promise((res, rej) => { + promises.push({res, rej}); + }); + } + _notify(resolved) { + const method = resolved ? 'res' : 'rej'; + const promises = this._promises || []; + for (let i = 0; i < promises.length; i++) { + promises[i][method](); + } + } +} + +const numbers = ['x', 'y', 'borderWidth', 'radius', 'tension']; +const colors = ['color', 'borderColor', 'backgroundColor']; +defaults.set('animation', { + delay: undefined, + duration: 1000, + easing: 'easeOutQuart', + fn: undefined, + from: undefined, + loop: undefined, + to: undefined, + type: undefined, +}); +const animationOptions = Object.keys(defaults.animation); +defaults.describe('animation', { + _fallback: false, + _indexable: false, + _scriptable: (name) => name !== 'onProgress' && name !== 'onComplete' && name !== 'fn', +}); +defaults.set('animations', { + colors: { + type: 'color', + properties: colors + }, + numbers: { + type: 'number', + properties: numbers + }, +}); +defaults.describe('animations', { + _fallback: 'animation', +}); +defaults.set('transitions', { + active: { + animation: { + duration: 400 + } + }, + resize: { + animation: { + duration: 0 + } + }, + show: { + animations: { + colors: { + from: 'transparent' + }, + visible: { + type: 'boolean', + duration: 0 + }, + } + }, + hide: { + animations: { + colors: { + to: 'transparent' + }, + visible: { + type: 'boolean', + easing: 'linear', + fn: v => v | 0 + }, + } + } +}); +class Animations { + constructor(chart, config) { + this._chart = chart; + this._properties = new Map(); + this.configure(config); + } + configure(config) { + if (!isObject(config)) { + return; + } + const animatedProps = this._properties; + Object.getOwnPropertyNames(config).forEach(key => { + const cfg = config[key]; + if (!isObject(cfg)) { + return; + } + const resolved = {}; + for (const option of animationOptions) { + resolved[option] = cfg[option]; + } + (isArray(cfg.properties) && cfg.properties || [key]).forEach((prop) => { + if (prop === key || !animatedProps.has(prop)) { + animatedProps.set(prop, resolved); + } + }); + }); + } + _animateOptions(target, values) { + const newOptions = values.options; + const options = resolveTargetOptions(target, newOptions); + if (!options) { + return []; + } + const animations = this._createAnimations(options, newOptions); + if (newOptions.$shared) { + awaitAll(target.options.$animations, newOptions).then(() => { + target.options = newOptions; + }, () => { + }); + } + return animations; + } + _createAnimations(target, values) { + const animatedProps = this._properties; + const animations = []; + const running = target.$animations || (target.$animations = {}); + const props = Object.keys(values); + const date = Date.now(); + let i; + for (i = props.length - 1; i >= 0; --i) { + const prop = props[i]; + if (prop.charAt(0) === '$') { + continue; + } + if (prop === 'options') { + animations.push(...this._animateOptions(target, values)); + continue; + } + const value = values[prop]; + let animation = running[prop]; + const cfg = animatedProps.get(prop); + if (animation) { + if (cfg && animation.active()) { + animation.update(cfg, value, date); + continue; + } else { + animation.cancel(); + } + } + if (!cfg || !cfg.duration) { + target[prop] = value; + continue; + } + running[prop] = animation = new Animation(cfg, target, prop, value); + animations.push(animation); + } + return animations; + } + update(target, values) { + if (this._properties.size === 0) { + Object.assign(target, values); + return; + } + const animations = this._createAnimations(target, values); + if (animations.length) { + animator.add(this._chart, animations); + return true; + } + } +} +function awaitAll(animations, properties) { + const running = []; + const keys = Object.keys(properties); + for (let i = 0; i < keys.length; i++) { + const anim = animations[keys[i]]; + if (anim && anim.active()) { + running.push(anim.wait()); + } + } + return Promise.all(running); +} +function resolveTargetOptions(target, newOptions) { + if (!newOptions) { + return; + } + let options = target.options; + if (!options) { + target.options = newOptions; + return; + } + if (options.$shared) { + target.options = options = Object.assign({}, options, {$shared: false, $animations: {}}); + } + return options; +} + +function scaleClip(scale, allowedOverflow) { + const opts = scale && scale.options || {}; + const reverse = opts.reverse; + const min = opts.min === undefined ? allowedOverflow : 0; + const max = opts.max === undefined ? allowedOverflow : 0; + return { + start: reverse ? max : min, + end: reverse ? min : max + }; +} +function defaultClip(xScale, yScale, allowedOverflow) { + if (allowedOverflow === false) { + return false; + } + const x = scaleClip(xScale, allowedOverflow); + const y = scaleClip(yScale, allowedOverflow); + return { + top: y.end, + right: x.end, + bottom: y.start, + left: x.start + }; +} +function toClip(value) { + let t, r, b, l; + if (isObject(value)) { + t = value.top; + r = value.right; + b = value.bottom; + l = value.left; + } else { + t = r = b = l = value; + } + return { + top: t, + right: r, + bottom: b, + left: l, + disabled: value === false + }; +} +function getSortedDatasetIndices(chart, filterVisible) { + const keys = []; + const metasets = chart._getSortedDatasetMetas(filterVisible); + let i, ilen; + for (i = 0, ilen = metasets.length; i < ilen; ++i) { + keys.push(metasets[i].index); + } + return keys; +} +function applyStack(stack, value, dsIndex, options) { + const keys = stack.keys; + const singleMode = options.mode === 'single'; + let i, ilen, datasetIndex, otherValue; + if (value === null) { + return; + } + for (i = 0, ilen = keys.length; i < ilen; ++i) { + datasetIndex = +keys[i]; + if (datasetIndex === dsIndex) { + if (options.all) { + continue; + } + break; + } + otherValue = stack.values[datasetIndex]; + if (isNumberFinite(otherValue) && (singleMode || (value === 0 || sign(value) === sign(otherValue)))) { + value += otherValue; + } + } + return value; +} +function convertObjectDataToArray(data) { + const keys = Object.keys(data); + const adata = new Array(keys.length); + let i, ilen, key; + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + adata[i] = { + x: key, + y: data[key] + }; + } + return adata; +} +function isStacked(scale, meta) { + const stacked = scale && scale.options.stacked; + return stacked || (stacked === undefined && meta.stack !== undefined); +} +function getStackKey(indexScale, valueScale, meta) { + return `${indexScale.id}.${valueScale.id}.${meta.stack || meta.type}`; +} +function getUserBounds(scale) { + const {min, max, minDefined, maxDefined} = scale.getUserBounds(); + return { + min: minDefined ? min : Number.NEGATIVE_INFINITY, + max: maxDefined ? max : Number.POSITIVE_INFINITY + }; +} +function getOrCreateStack(stacks, stackKey, indexValue) { + const subStack = stacks[stackKey] || (stacks[stackKey] = {}); + return subStack[indexValue] || (subStack[indexValue] = {}); +} +function getLastIndexInStack(stack, vScale, positive) { + for (const meta of vScale.getMatchingVisibleMetas('bar').reverse()) { + const value = stack[meta.index]; + if ((positive && value > 0) || (!positive && value < 0)) { + return meta.index; + } + } + return null; +} +function updateStacks(controller, parsed) { + const {chart, _cachedMeta: meta} = controller; + const stacks = chart._stacks || (chart._stacks = {}); + const {iScale, vScale, index: datasetIndex} = meta; + const iAxis = iScale.axis; + const vAxis = vScale.axis; + const key = getStackKey(iScale, vScale, meta); + const ilen = parsed.length; + let stack; + for (let i = 0; i < ilen; ++i) { + const item = parsed[i]; + const {[iAxis]: index, [vAxis]: value} = item; + const itemStacks = item._stacks || (item._stacks = {}); + stack = itemStacks[vAxis] = getOrCreateStack(stacks, key, index); + stack[datasetIndex] = value; + stack._top = getLastIndexInStack(stack, vScale, true); + stack._bottom = getLastIndexInStack(stack, vScale, false); + } +} +function getFirstScaleId(chart, axis) { + const scales = chart.scales; + return Object.keys(scales).filter(key => scales[key].axis === axis).shift(); +} +function createDatasetContext(parent, index) { + return Object.assign(Object.create(parent), + { + active: false, + dataset: undefined, + datasetIndex: index, + index, + mode: 'default', + type: 'dataset' + } + ); +} +function createDataContext(parent, index, element) { + return Object.assign(Object.create(parent), { + active: false, + dataIndex: index, + parsed: undefined, + raw: undefined, + element, + index, + mode: 'default', + type: 'data' + }); +} +function clearStacks(meta, items) { + const axis = meta.vScale && meta.vScale.axis; + if (!axis) { + return; + } + items = items || meta._parsed; + for (const parsed of items) { + const stacks = parsed._stacks; + if (!stacks || stacks[axis] === undefined || stacks[axis][meta.index] === undefined) { + return; + } + delete stacks[axis][meta.index]; + } +} +const isDirectUpdateMode = (mode) => mode === 'reset' || mode === 'none'; +const cloneIfNotShared = (cached, shared) => shared ? cached : Object.assign({}, cached); +class DatasetController { + constructor(chart, datasetIndex) { + this.chart = chart; + this._ctx = chart.ctx; + this.index = datasetIndex; + this._cachedDataOpts = {}; + this._cachedMeta = this.getMeta(); + this._type = this._cachedMeta.type; + this.options = undefined; + this._parsing = false; + this._data = undefined; + this._objectData = undefined; + this._sharedOptions = undefined; + this._drawStart = undefined; + this._drawCount = undefined; + this.enableOptionSharing = false; + this.$context = undefined; + this._syncList = []; + this.initialize(); + } + initialize() { + const me = this; + const meta = me._cachedMeta; + me.configure(); + me.linkScales(); + meta._stacked = isStacked(meta.vScale, meta); + me.addElements(); + } + updateIndex(datasetIndex) { + if (this.index !== datasetIndex) { + clearStacks(this._cachedMeta); + } + this.index = datasetIndex; + } + linkScales() { + const me = this; + const chart = me.chart; + const meta = me._cachedMeta; + const dataset = me.getDataset(); + const chooseId = (axis, x, y, r) => axis === 'x' ? x : axis === 'r' ? r : y; + const xid = meta.xAxisID = valueOrDefault(dataset.xAxisID, getFirstScaleId(chart, 'x')); + const yid = meta.yAxisID = valueOrDefault(dataset.yAxisID, getFirstScaleId(chart, 'y')); + const rid = meta.rAxisID = valueOrDefault(dataset.rAxisID, getFirstScaleId(chart, 'r')); + const indexAxis = meta.indexAxis; + const iid = meta.iAxisID = chooseId(indexAxis, xid, yid, rid); + const vid = meta.vAxisID = chooseId(indexAxis, yid, xid, rid); + meta.xScale = me.getScaleForId(xid); + meta.yScale = me.getScaleForId(yid); + meta.rScale = me.getScaleForId(rid); + meta.iScale = me.getScaleForId(iid); + meta.vScale = me.getScaleForId(vid); + } + getDataset() { + return this.chart.data.datasets[this.index]; + } + getMeta() { + return this.chart.getDatasetMeta(this.index); + } + getScaleForId(scaleID) { + return this.chart.scales[scaleID]; + } + _getOtherScale(scale) { + const meta = this._cachedMeta; + return scale === meta.iScale + ? meta.vScale + : meta.iScale; + } + reset() { + this._update('reset'); + } + _destroy() { + const meta = this._cachedMeta; + if (this._data) { + unlistenArrayEvents(this._data, this); + } + if (meta._stacked) { + clearStacks(meta); + } + } + _dataCheck() { + const me = this; + const dataset = me.getDataset(); + const data = dataset.data || (dataset.data = []); + const _data = me._data; + if (isObject(data)) { + me._data = convertObjectDataToArray(data); + } else if (_data !== data) { + if (_data) { + unlistenArrayEvents(_data, me); + const meta = me._cachedMeta; + clearStacks(meta); + meta._parsed = []; + } + if (data && Object.isExtensible(data)) { + listenArrayEvents(data, me); + } + me._syncList = []; + me._data = data; + } + } + addElements() { + const me = this; + const meta = me._cachedMeta; + me._dataCheck(); + if (me.datasetElementType) { + meta.dataset = new me.datasetElementType(); + } + } + buildOrUpdateElements(resetNewElements) { + const me = this; + const meta = me._cachedMeta; + const dataset = me.getDataset(); + let stackChanged = false; + me._dataCheck(); + const oldStacked = meta._stacked; + meta._stacked = isStacked(meta.vScale, meta); + if (meta.stack !== dataset.stack) { + stackChanged = true; + clearStacks(meta); + meta.stack = dataset.stack; + } + me._resyncElements(resetNewElements); + if (stackChanged || oldStacked !== meta._stacked) { + updateStacks(me, meta._parsed); + } + } + configure() { + const me = this; + const config = me.chart.config; + const scopeKeys = config.datasetScopeKeys(me._type); + const scopes = config.getOptionScopes(me.getDataset(), scopeKeys, true); + me.options = config.createResolver(scopes, me.getContext()); + me._parsing = me.options.parsing; + } + parse(start, count) { + const me = this; + const {_cachedMeta: meta, _data: data} = me; + const {iScale, _stacked} = meta; + const iAxis = iScale.axis; + let sorted = start === 0 && count === data.length ? true : meta._sorted; + let prev = start > 0 && meta._parsed[start - 1]; + let i, cur, parsed; + if (me._parsing === false) { + meta._parsed = data; + meta._sorted = true; + parsed = data; + } else { + if (isArray(data[start])) { + parsed = me.parseArrayData(meta, data, start, count); + } else if (isObject(data[start])) { + parsed = me.parseObjectData(meta, data, start, count); + } else { + parsed = me.parsePrimitiveData(meta, data, start, count); + } + const isNotInOrderComparedToPrev = () => cur[iAxis] === null || (prev && cur[iAxis] < prev[iAxis]); + for (i = 0; i < count; ++i) { + meta._parsed[i + start] = cur = parsed[i]; + if (sorted) { + if (isNotInOrderComparedToPrev()) { + sorted = false; + } + prev = cur; + } + } + meta._sorted = sorted; + } + if (_stacked) { + updateStacks(me, parsed); + } + } + parsePrimitiveData(meta, data, start, count) { + const {iScale, vScale} = meta; + const iAxis = iScale.axis; + const vAxis = vScale.axis; + const labels = iScale.getLabels(); + const singleScale = iScale === vScale; + const parsed = new Array(count); + let i, ilen, index; + for (i = 0, ilen = count; i < ilen; ++i) { + index = i + start; + parsed[i] = { + [iAxis]: singleScale || iScale.parse(labels[index], index), + [vAxis]: vScale.parse(data[index], index) + }; + } + return parsed; + } + parseArrayData(meta, data, start, count) { + const {xScale, yScale} = meta; + const parsed = new Array(count); + let i, ilen, index, item; + for (i = 0, ilen = count; i < ilen; ++i) { + index = i + start; + item = data[index]; + parsed[i] = { + x: xScale.parse(item[0], index), + y: yScale.parse(item[1], index) + }; + } + return parsed; + } + parseObjectData(meta, data, start, count) { + const {xScale, yScale} = meta; + const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; + const parsed = new Array(count); + let i, ilen, index, item; + for (i = 0, ilen = count; i < ilen; ++i) { + index = i + start; + item = data[index]; + parsed[i] = { + x: xScale.parse(resolveObjectKey(item, xAxisKey), index), + y: yScale.parse(resolveObjectKey(item, yAxisKey), index) + }; + } + return parsed; + } + getParsed(index) { + return this._cachedMeta._parsed[index]; + } + getDataElement(index) { + return this._cachedMeta.data[index]; + } + applyStack(scale, parsed, mode) { + const chart = this.chart; + const meta = this._cachedMeta; + const value = parsed[scale.axis]; + const stack = { + keys: getSortedDatasetIndices(chart, true), + values: parsed._stacks[scale.axis] + }; + return applyStack(stack, value, meta.index, {mode}); + } + updateRangeFromParsed(range, scale, parsed, stack) { + const parsedValue = parsed[scale.axis]; + let value = parsedValue === null ? NaN : parsedValue; + const values = stack && parsed._stacks[scale.axis]; + if (stack && values) { + stack.values = values; + range.min = Math.min(range.min, value); + range.max = Math.max(range.max, value); + value = applyStack(stack, parsedValue, this._cachedMeta.index, {all: true}); + } + range.min = Math.min(range.min, value); + range.max = Math.max(range.max, value); + } + getMinMax(scale, canStack) { + const me = this; + const meta = me._cachedMeta; + const _parsed = meta._parsed; + const sorted = meta._sorted && scale === meta.iScale; + const ilen = _parsed.length; + const otherScale = me._getOtherScale(scale); + const stack = canStack && meta._stacked && {keys: getSortedDatasetIndices(me.chart, true), values: null}; + const range = {min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY}; + const {min: otherMin, max: otherMax} = getUserBounds(otherScale); + let i, value, parsed, otherValue; + function _skip() { + parsed = _parsed[i]; + value = parsed[scale.axis]; + otherValue = parsed[otherScale.axis]; + return !isNumberFinite(value) || otherMin > otherValue || otherMax < otherValue; + } + for (i = 0; i < ilen; ++i) { + if (_skip()) { + continue; + } + me.updateRangeFromParsed(range, scale, parsed, stack); + if (sorted) { + break; + } + } + if (sorted) { + for (i = ilen - 1; i >= 0; --i) { + if (_skip()) { + continue; + } + me.updateRangeFromParsed(range, scale, parsed, stack); + break; + } + } + return range; + } + getAllParsedValues(scale) { + const parsed = this._cachedMeta._parsed; + const values = []; + let i, ilen, value; + for (i = 0, ilen = parsed.length; i < ilen; ++i) { + value = parsed[i][scale.axis]; + if (isNumberFinite(value)) { + values.push(value); + } + } + return values; + } + getMaxOverflow() { + return false; + } + getLabelAndValue(index) { + const me = this; + const meta = me._cachedMeta; + const iScale = meta.iScale; + const vScale = meta.vScale; + const parsed = me.getParsed(index); + return { + label: iScale ? '' + iScale.getLabelForValue(parsed[iScale.axis]) : '', + value: vScale ? '' + vScale.getLabelForValue(parsed[vScale.axis]) : '' + }; + } + _update(mode) { + const me = this; + const meta = me._cachedMeta; + me.configure(); + me._cachedDataOpts = {}; + me.update(mode || 'default'); + meta._clip = toClip(valueOrDefault(me.options.clip, defaultClip(meta.xScale, meta.yScale, me.getMaxOverflow()))); + } + update(mode) {} + draw() { + const me = this; + const ctx = me._ctx; + const chart = me.chart; + const meta = me._cachedMeta; + const elements = meta.data || []; + const area = chart.chartArea; + const active = []; + const start = me._drawStart || 0; + const count = me._drawCount || (elements.length - start); + let i; + if (meta.dataset) { + meta.dataset.draw(ctx, area, start, count); + } + for (i = start; i < start + count; ++i) { + const element = elements[i]; + if (element.active) { + active.push(element); + } else { + element.draw(ctx, area); + } + } + for (i = 0; i < active.length; ++i) { + active[i].draw(ctx, area); + } + } + getStyle(index, active) { + const mode = active ? 'active' : 'default'; + return index === undefined && this._cachedMeta.dataset + ? this.resolveDatasetElementOptions(mode) + : this.resolveDataElementOptions(index || 0, mode); + } + getContext(index, active, mode) { + const me = this; + const dataset = me.getDataset(); + let context; + if (index >= 0 && index < me._cachedMeta.data.length) { + const element = me._cachedMeta.data[index]; + context = element.$context || + (element.$context = createDataContext(me.getContext(), index, element)); + context.parsed = me.getParsed(index); + context.raw = dataset.data[index]; + context.index = context.dataIndex = index; + } else { + context = me.$context || + (me.$context = createDatasetContext(me.chart.getContext(), me.index)); + context.dataset = dataset; + context.index = context.datasetIndex = me.index; + } + context.active = !!active; + context.mode = mode; + return context; + } + resolveDatasetElementOptions(mode) { + return this._resolveElementOptions(this.datasetElementType.id, mode); + } + resolveDataElementOptions(index, mode) { + return this._resolveElementOptions(this.dataElementType.id, mode, index); + } + _resolveElementOptions(elementType, mode = 'default', index) { + const me = this; + const active = mode === 'active'; + const cache = me._cachedDataOpts; + const cacheKey = elementType + '-' + mode; + const cached = cache[cacheKey]; + const sharing = me.enableOptionSharing && defined(index); + if (cached) { + return cloneIfNotShared(cached, sharing); + } + const config = me.chart.config; + const scopeKeys = config.datasetElementScopeKeys(me._type, elementType); + const prefixes = active ? [`${elementType}Hover`, 'hover', elementType, ''] : [elementType, '']; + const scopes = config.getOptionScopes(me.getDataset(), scopeKeys); + const names = Object.keys(defaults.elements[elementType]); + const context = () => me.getContext(index, active); + const values = config.resolveNamedOptions(scopes, names, context, prefixes); + if (values.$shared) { + values.$shared = sharing; + cache[cacheKey] = Object.freeze(cloneIfNotShared(values, sharing)); + } + return values; + } + _resolveAnimations(index, transition, active) { + const me = this; + const chart = me.chart; + const cache = me._cachedDataOpts; + const cacheKey = `animation-${transition}`; + const cached = cache[cacheKey]; + if (cached) { + return cached; + } + let options; + if (chart.options.animation !== false) { + const config = me.chart.config; + const scopeKeys = config.datasetAnimationScopeKeys(me._type, transition); + const scopes = config.getOptionScopes(me.getDataset(), scopeKeys); + options = config.createResolver(scopes, me.getContext(index, active, transition)); + } + const animations = new Animations(chart, options && options.animations); + if (options && options._cacheable) { + cache[cacheKey] = Object.freeze(animations); + } + return animations; + } + getSharedOptions(options) { + if (!options.$shared) { + return; + } + return this._sharedOptions || (this._sharedOptions = Object.assign({}, options)); + } + includeOptions(mode, sharedOptions) { + return !sharedOptions || isDirectUpdateMode(mode) || this.chart._animationsDisabled; + } + updateElement(element, index, properties, mode) { + if (isDirectUpdateMode(mode)) { + Object.assign(element, properties); + } else { + this._resolveAnimations(index, mode).update(element, properties); + } + } + updateSharedOptions(sharedOptions, mode, newOptions) { + if (sharedOptions && !isDirectUpdateMode(mode)) { + this._resolveAnimations(undefined, mode).update(sharedOptions, newOptions); + } + } + _setStyle(element, index, mode, active) { + element.active = active; + const options = this.getStyle(index, active); + this._resolveAnimations(index, mode, active).update(element, { + options: (!active && this.getSharedOptions(options)) || options + }); + } + removeHoverStyle(element, datasetIndex, index) { + this._setStyle(element, index, 'active', false); + } + setHoverStyle(element, datasetIndex, index) { + this._setStyle(element, index, 'active', true); + } + _removeDatasetHoverStyle() { + const element = this._cachedMeta.dataset; + if (element) { + this._setStyle(element, undefined, 'active', false); + } + } + _setDatasetHoverStyle() { + const element = this._cachedMeta.dataset; + if (element) { + this._setStyle(element, undefined, 'active', true); + } + } + _resyncElements(resetNewElements) { + const me = this; + const data = me._data; + const elements = me._cachedMeta.data; + for (const [method, arg1, arg2] of me._syncList) { + me[method](arg1, arg2); + } + me._syncList = []; + const numMeta = elements.length; + const numData = data.length; + const count = Math.min(numData, numMeta); + if (count) { + me.parse(0, count); + } + if (numData > numMeta) { + me._insertElements(numMeta, numData - numMeta, resetNewElements); + } else if (numData < numMeta) { + me._removeElements(numData, numMeta - numData); + } + } + _insertElements(start, count, resetNewElements = true) { + const me = this; + const meta = me._cachedMeta; + const data = meta.data; + const end = start + count; + let i; + const move = (arr) => { + arr.length += count; + for (i = arr.length - 1; i >= end; i--) { + arr[i] = arr[i - count]; + } + }; + move(data); + for (i = start; i < end; ++i) { + data[i] = new me.dataElementType(); + } + if (me._parsing) { + move(meta._parsed); + } + me.parse(start, count); + if (resetNewElements) { + me.updateElements(data, start, count, 'reset'); + } + } + updateElements(element, start, count, mode) {} + _removeElements(start, count) { + const me = this; + const meta = me._cachedMeta; + if (me._parsing) { + const removed = meta._parsed.splice(start, count); + if (meta._stacked) { + clearStacks(meta, removed); + } + } + meta.data.splice(start, count); + } + _onDataPush() { + const count = arguments.length; + this._syncList.push(['_insertElements', this.getDataset().data.length - count, count]); + } + _onDataPop() { + this._syncList.push(['_removeElements', this._cachedMeta.data.length - 1, 1]); + } + _onDataShift() { + this._syncList.push(['_removeElements', 0, 1]); + } + _onDataSplice(start, count) { + this._syncList.push(['_removeElements', start, count]); + this._syncList.push(['_insertElements', start, arguments.length - 2]); + } + _onDataUnshift() { + this._syncList.push(['_insertElements', 0, arguments.length]); + } +} +DatasetController.defaults = {}; +DatasetController.prototype.datasetElementType = null; +DatasetController.prototype.dataElementType = null; + +class Element { + constructor() { + this.x = undefined; + this.y = undefined; + this.active = false; + this.options = undefined; + this.$animations = undefined; + } + tooltipPosition(useFinalPosition) { + const {x, y} = this.getProps(['x', 'y'], useFinalPosition); + return {x, y}; + } + hasValue() { + return isNumber(this.x) && isNumber(this.y); + } + getProps(props, final) { + const me = this; + const anims = this.$animations; + if (!final || !anims) { + return me; + } + const ret = {}; + props.forEach(prop => { + ret[prop] = anims[prop] && anims[prop].active() ? anims[prop]._to : me[prop]; + }); + return ret; + } +} +Element.defaults = {}; +Element.defaultRoutes = undefined; + +const intlCache = new Map(); +function getNumberFormat(locale, options) { + options = options || {}; + const cacheKey = locale + JSON.stringify(options); + let formatter = intlCache.get(cacheKey); + if (!formatter) { + formatter = new Intl.NumberFormat(locale, options); + intlCache.set(cacheKey, formatter); + } + return formatter; +} +function formatNumber(num, locale, options) { + return getNumberFormat(locale, options).format(num); +} + +const formatters = { + values(value) { + return isArray(value) ? value : '' + value; + }, + numeric(tickValue, index, ticks) { + if (tickValue === 0) { + return '0'; + } + const locale = this.chart.options.locale; + let notation; + let delta = tickValue; + if (ticks.length > 1) { + const maxTick = Math.max(Math.abs(ticks[0].value), Math.abs(ticks[ticks.length - 1].value)); + if (maxTick < 1e-4 || maxTick > 1e+15) { + notation = 'scientific'; + } + delta = calculateDelta(tickValue, ticks); + } + const logDelta = log10(Math.abs(delta)); + const numDecimal = Math.max(Math.min(-1 * Math.floor(logDelta), 20), 0); + const options = {notation, minimumFractionDigits: numDecimal, maximumFractionDigits: numDecimal}; + Object.assign(options, this.options.ticks.format); + return formatNumber(tickValue, locale, options); + }, + logarithmic(tickValue, index, ticks) { + if (tickValue === 0) { + return '0'; + } + const remain = tickValue / (Math.pow(10, Math.floor(log10(tickValue)))); + if (remain === 1 || remain === 2 || remain === 5) { + return formatters.numeric.call(this, tickValue, index, ticks); + } + return ''; + } +}; +function calculateDelta(tickValue, ticks) { + let delta = ticks.length > 3 ? ticks[2].value - ticks[1].value : ticks[1].value - ticks[0].value; + if (Math.abs(delta) >= 1 && tickValue !== Math.floor(tickValue)) { + delta = tickValue - Math.floor(tickValue); + } + return delta; +} +var Ticks = {formatters}; + +defaults.set('scale', { + display: true, + offset: false, + reverse: false, + beginAtZero: false, + bounds: 'ticks', + grace: 0, + grid: { + display: true, + lineWidth: 1, + drawBorder: true, + drawOnChartArea: true, + drawTicks: true, + tickLength: 8, + tickWidth: (_ctx, options) => options.lineWidth, + tickColor: (_ctx, options) => options.color, + offset: false, + borderDash: [], + borderDashOffset: 0.0, + borderWidth: 1 + }, + title: { + display: false, + text: '', + padding: { + top: 4, + bottom: 4 + } + }, + ticks: { + minRotation: 0, + maxRotation: 50, + mirror: false, + textStrokeWidth: 0, + textStrokeColor: '', + padding: 3, + display: true, + autoSkip: true, + autoSkipPadding: 3, + labelOffset: 0, + callback: Ticks.formatters.values, + minor: {}, + major: {}, + align: 'center', + crossAlign: 'near', + showLabelBackdrop: false, + backdropColor: 'rgba(255, 255, 255, 0.75)', + backdropPadding: 2, + } +}); +defaults.route('scale.ticks', 'color', '', 'color'); +defaults.route('scale.grid', 'color', '', 'borderColor'); +defaults.route('scale.grid', 'borderColor', '', 'borderColor'); +defaults.route('scale.title', 'color', '', 'color'); +defaults.describe('scale', { + _fallback: false, + _scriptable: (name) => !name.startsWith('before') && !name.startsWith('after') && name !== 'callback' && name !== 'parser', + _indexable: (name) => name !== 'borderDash' && name !== 'tickBorderDash', +}); +defaults.describe('scales', { + _fallback: 'scale', +}); +defaults.describe('scale.ticks', { + _scriptable: (name) => name !== 'backdropPadding' && name !== 'callback', + _indexable: (name) => name !== 'backdropPadding', +}); + +function autoSkip(scale, ticks) { + const tickOpts = scale.options.ticks; + const ticksLimit = tickOpts.maxTicksLimit || determineMaxTicks(scale); + const majorIndices = tickOpts.major.enabled ? getMajorIndices(ticks) : []; + const numMajorIndices = majorIndices.length; + const first = majorIndices[0]; + const last = majorIndices[numMajorIndices - 1]; + const newTicks = []; + if (numMajorIndices > ticksLimit) { + skipMajors(ticks, newTicks, majorIndices, numMajorIndices / ticksLimit); + return newTicks; + } + const spacing = calculateSpacing(majorIndices, ticks, ticksLimit); + if (numMajorIndices > 0) { + let i, ilen; + const avgMajorSpacing = numMajorIndices > 1 ? Math.round((last - first) / (numMajorIndices - 1)) : null; + skip(ticks, newTicks, spacing, isNullOrUndef(avgMajorSpacing) ? 0 : first - avgMajorSpacing, first); + for (i = 0, ilen = numMajorIndices - 1; i < ilen; i++) { + skip(ticks, newTicks, spacing, majorIndices[i], majorIndices[i + 1]); + } + skip(ticks, newTicks, spacing, last, isNullOrUndef(avgMajorSpacing) ? ticks.length : last + avgMajorSpacing); + return newTicks; + } + skip(ticks, newTicks, spacing); + return newTicks; +} +function determineMaxTicks(scale) { + const offset = scale.options.offset; + const tickLength = scale._tickSize(); + const maxScale = scale._length / tickLength + (offset ? 0 : 1); + const maxChart = scale._maxLength / tickLength; + return Math.floor(Math.min(maxScale, maxChart)); +} +function calculateSpacing(majorIndices, ticks, ticksLimit) { + const evenMajorSpacing = getEvenSpacing(majorIndices); + const spacing = ticks.length / ticksLimit; + if (!evenMajorSpacing) { + return Math.max(spacing, 1); + } + const factors = _factorize(evenMajorSpacing); + for (let i = 0, ilen = factors.length - 1; i < ilen; i++) { + const factor = factors[i]; + if (factor > spacing) { + return factor; + } + } + return Math.max(spacing, 1); +} +function getMajorIndices(ticks) { + const result = []; + let i, ilen; + for (i = 0, ilen = ticks.length; i < ilen; i++) { + if (ticks[i].major) { + result.push(i); + } + } + return result; +} +function skipMajors(ticks, newTicks, majorIndices, spacing) { + let count = 0; + let next = majorIndices[0]; + let i; + spacing = Math.ceil(spacing); + for (i = 0; i < ticks.length; i++) { + if (i === next) { + newTicks.push(ticks[i]); + count++; + next = majorIndices[count * spacing]; + } + } +} +function skip(ticks, newTicks, spacing, majorStart, majorEnd) { + const start = valueOrDefault(majorStart, 0); + const end = Math.min(valueOrDefault(majorEnd, ticks.length), ticks.length); + let count = 0; + let length, i, next; + spacing = Math.ceil(spacing); + if (majorEnd) { + length = majorEnd - majorStart; + spacing = length / Math.floor(length / spacing); + } + next = start; + while (next < 0) { + count++; + next = Math.round(start + count * spacing); + } + for (i = Math.max(start, 0); i < end; i++) { + if (i === next) { + newTicks.push(ticks[i]); + count++; + next = Math.round(start + count * spacing); + } + } +} +function getEvenSpacing(arr) { + const len = arr.length; + let i, diff; + if (len < 2) { + return false; + } + for (diff = arr[0], i = 1; i < len; ++i) { + if (arr[i] - arr[i - 1] !== diff) { + return false; + } + } + return diff; +} + +const reverseAlign = (align) => align === 'left' ? 'right' : align === 'right' ? 'left' : align; +const offsetFromEdge = (scale, edge, offset) => edge === 'top' || edge === 'left' ? scale[edge] + offset : scale[edge] - offset; +function sample(arr, numItems) { + const result = []; + const increment = arr.length / numItems; + const len = arr.length; + let i = 0; + for (; i < len; i += increment) { + result.push(arr[Math.floor(i)]); + } + return result; +} +function getPixelForGridLine(scale, index, offsetGridLines) { + const length = scale.ticks.length; + const validIndex = Math.min(index, length - 1); + const start = scale._startPixel; + const end = scale._endPixel; + const epsilon = 1e-6; + let lineValue = scale.getPixelForTick(validIndex); + let offset; + if (offsetGridLines) { + if (length === 1) { + offset = Math.max(lineValue - start, end - lineValue); + } else if (index === 0) { + offset = (scale.getPixelForTick(1) - lineValue) / 2; + } else { + offset = (lineValue - scale.getPixelForTick(validIndex - 1)) / 2; + } + lineValue += validIndex < index ? offset : -offset; + if (lineValue < start - epsilon || lineValue > end + epsilon) { + return; + } + } + return lineValue; +} +function garbageCollect(caches, length) { + each(caches, (cache) => { + const gc = cache.gc; + const gcLen = gc.length / 2; + let i; + if (gcLen > length) { + for (i = 0; i < gcLen; ++i) { + delete cache.data[gc[i]]; + } + gc.splice(0, gcLen); + } + }); +} +function getTickMarkLength(options) { + return options.drawTicks ? options.tickLength : 0; +} +function getTitleHeight(options, fallback) { + if (!options.display) { + return 0; + } + const font = toFont(options.font, fallback); + const padding = toPadding(options.padding); + const lines = isArray(options.text) ? options.text.length : 1; + return (lines * font.lineHeight) + padding.height; +} +function createScaleContext(parent, scale) { + return Object.assign(Object.create(parent), { + scale, + type: 'scale' + }); +} +function createTickContext(parent, index, tick) { + return Object.assign(Object.create(parent), { + tick, + index, + type: 'tick' + }); +} +function titleAlign(align, position, reverse) { + let ret = _toLeftRightCenter(align); + if ((reverse && position !== 'right') || (!reverse && position === 'right')) { + ret = reverseAlign(ret); + } + return ret; +} +function titleArgs(scale, offset, position, align) { + const {top, left, bottom, right} = scale; + let rotation = 0; + let maxWidth, titleX, titleY; + if (scale.isHorizontal()) { + titleX = _alignStartEnd(align, left, right); + titleY = offsetFromEdge(scale, position, offset); + maxWidth = right - left; + } else { + titleX = offsetFromEdge(scale, position, offset); + titleY = _alignStartEnd(align, bottom, top); + rotation = position === 'left' ? -HALF_PI : HALF_PI; + } + return {titleX, titleY, maxWidth, rotation}; +} +class Scale extends Element { + constructor(cfg) { + super(); + this.id = cfg.id; + this.type = cfg.type; + this.options = undefined; + this.ctx = cfg.ctx; + this.chart = cfg.chart; + this.top = undefined; + this.bottom = undefined; + this.left = undefined; + this.right = undefined; + this.width = undefined; + this.height = undefined; + this._margins = { + left: 0, + right: 0, + top: 0, + bottom: 0 + }; + this.maxWidth = undefined; + this.maxHeight = undefined; + this.paddingTop = undefined; + this.paddingBottom = undefined; + this.paddingLeft = undefined; + this.paddingRight = undefined; + this.axis = undefined; + this.labelRotation = undefined; + this.min = undefined; + this.max = undefined; + this._range = undefined; + this.ticks = []; + this._gridLineItems = null; + this._labelItems = null; + this._labelSizes = null; + this._length = 0; + this._maxLength = 0; + this._longestTextCache = {}; + this._startPixel = undefined; + this._endPixel = undefined; + this._reversePixels = false; + this._userMax = undefined; + this._userMin = undefined; + this._suggestedMax = undefined; + this._suggestedMin = undefined; + this._ticksLength = 0; + this._borderValue = 0; + this._cache = {}; + this._dataLimitsCached = false; + this.$context = undefined; + } + init(options) { + const me = this; + me.options = options.setContext(me.getContext()); + me.axis = options.axis; + me._userMin = me.parse(options.min); + me._userMax = me.parse(options.max); + me._suggestedMin = me.parse(options.suggestedMin); + me._suggestedMax = me.parse(options.suggestedMax); + } + parse(raw, index) { + return raw; + } + getUserBounds() { + let {_userMin, _userMax, _suggestedMin, _suggestedMax} = this; + _userMin = finiteOrDefault(_userMin, Number.POSITIVE_INFINITY); + _userMax = finiteOrDefault(_userMax, Number.NEGATIVE_INFINITY); + _suggestedMin = finiteOrDefault(_suggestedMin, Number.POSITIVE_INFINITY); + _suggestedMax = finiteOrDefault(_suggestedMax, Number.NEGATIVE_INFINITY); + return { + min: finiteOrDefault(_userMin, _suggestedMin), + max: finiteOrDefault(_userMax, _suggestedMax), + minDefined: isNumberFinite(_userMin), + maxDefined: isNumberFinite(_userMax) + }; + } + getMinMax(canStack) { + const me = this; + let {min, max, minDefined, maxDefined} = me.getUserBounds(); + let range; + if (minDefined && maxDefined) { + return {min, max}; + } + const metas = me.getMatchingVisibleMetas(); + for (let i = 0, ilen = metas.length; i < ilen; ++i) { + range = metas[i].controller.getMinMax(me, canStack); + if (!minDefined) { + min = Math.min(min, range.min); + } + if (!maxDefined) { + max = Math.max(max, range.max); + } + } + return { + min: finiteOrDefault(min, finiteOrDefault(max, min)), + max: finiteOrDefault(max, finiteOrDefault(min, max)) + }; + } + getPadding() { + const me = this; + return { + left: me.paddingLeft || 0, + top: me.paddingTop || 0, + right: me.paddingRight || 0, + bottom: me.paddingBottom || 0 + }; + } + getTicks() { + return this.ticks; + } + getLabels() { + const data = this.chart.data; + return this.options.labels || (this.isHorizontal() ? data.xLabels : data.yLabels) || data.labels || []; + } + beforeLayout() { + this._cache = {}; + this._dataLimitsCached = false; + } + beforeUpdate() { + callback(this.options.beforeUpdate, [this]); + } + update(maxWidth, maxHeight, margins) { + const me = this; + const tickOpts = me.options.ticks; + const sampleSize = tickOpts.sampleSize; + me.beforeUpdate(); + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me._margins = margins = Object.assign({ + left: 0, + right: 0, + top: 0, + bottom: 0 + }, margins); + me.ticks = null; + me._labelSizes = null; + me._gridLineItems = null; + me._labelItems = null; + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + me._maxLength = me.isHorizontal() + ? me.width + margins.left + margins.right + : me.height + margins.top + margins.bottom; + if (!me._dataLimitsCached) { + me.beforeDataLimits(); + me.determineDataLimits(); + me.afterDataLimits(); + me._range = _addGrace(me, me.options.grace); + me._dataLimitsCached = true; + } + me.beforeBuildTicks(); + me.ticks = me.buildTicks() || []; + me.afterBuildTicks(); + const samplingEnabled = sampleSize < me.ticks.length; + me._convertTicksToLabels(samplingEnabled ? sample(me.ticks, sampleSize) : me.ticks); + me.configure(); + me.beforeCalculateLabelRotation(); + me.calculateLabelRotation(); + me.afterCalculateLabelRotation(); + if (tickOpts.display && (tickOpts.autoSkip || tickOpts.source === 'auto')) { + me.ticks = autoSkip(me, me.ticks); + me._labelSizes = null; + } + if (samplingEnabled) { + me._convertTicksToLabels(me.ticks); + } + me.beforeFit(); + me.fit(); + me.afterFit(); + me.afterUpdate(); + } + configure() { + const me = this; + let reversePixels = me.options.reverse; + let startPixel, endPixel; + if (me.isHorizontal()) { + startPixel = me.left; + endPixel = me.right; + } else { + startPixel = me.top; + endPixel = me.bottom; + reversePixels = !reversePixels; + } + me._startPixel = startPixel; + me._endPixel = endPixel; + me._reversePixels = reversePixels; + me._length = endPixel - startPixel; + me._alignToPixels = me.options.alignToPixels; + } + afterUpdate() { + callback(this.options.afterUpdate, [this]); + } + beforeSetDimensions() { + callback(this.options.beforeSetDimensions, [this]); + } + setDimensions() { + const me = this; + if (me.isHorizontal()) { + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; + me.top = 0; + me.bottom = me.height; + } + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + } + afterSetDimensions() { + callback(this.options.afterSetDimensions, [this]); + } + _callHooks(name) { + const me = this; + me.chart.notifyPlugins(name, me.getContext()); + callback(me.options[name], [me]); + } + beforeDataLimits() { + this._callHooks('beforeDataLimits'); + } + determineDataLimits() {} + afterDataLimits() { + this._callHooks('afterDataLimits'); + } + beforeBuildTicks() { + this._callHooks('beforeBuildTicks'); + } + buildTicks() { + return []; + } + afterBuildTicks() { + this._callHooks('afterBuildTicks'); + } + beforeTickToLabelConversion() { + callback(this.options.beforeTickToLabelConversion, [this]); + } + generateTickLabels(ticks) { + const me = this; + const tickOpts = me.options.ticks; + let i, ilen, tick; + for (i = 0, ilen = ticks.length; i < ilen; i++) { + tick = ticks[i]; + tick.label = callback(tickOpts.callback, [tick.value, i, ticks], me); + } + } + afterTickToLabelConversion() { + callback(this.options.afterTickToLabelConversion, [this]); + } + beforeCalculateLabelRotation() { + callback(this.options.beforeCalculateLabelRotation, [this]); + } + calculateLabelRotation() { + const me = this; + const options = me.options; + const tickOpts = options.ticks; + const numTicks = me.ticks.length; + const minRotation = tickOpts.minRotation || 0; + const maxRotation = tickOpts.maxRotation; + let labelRotation = minRotation; + let tickWidth, maxHeight, maxLabelDiagonal; + if (!me._isVisible() || !tickOpts.display || minRotation >= maxRotation || numTicks <= 1 || !me.isHorizontal()) { + me.labelRotation = minRotation; + return; + } + const labelSizes = me._getLabelSizes(); + const maxLabelWidth = labelSizes.widest.width; + const maxLabelHeight = labelSizes.highest.height; + const maxWidth = _limitValue(me.chart.width - maxLabelWidth, 0, me.maxWidth); + tickWidth = options.offset ? me.maxWidth / numTicks : maxWidth / (numTicks - 1); + if (maxLabelWidth + 6 > tickWidth) { + tickWidth = maxWidth / (numTicks - (options.offset ? 0.5 : 1)); + maxHeight = me.maxHeight - getTickMarkLength(options.grid) + - tickOpts.padding - getTitleHeight(options.title, me.chart.options.font); + maxLabelDiagonal = Math.sqrt(maxLabelWidth * maxLabelWidth + maxLabelHeight * maxLabelHeight); + labelRotation = toDegrees(Math.min( + Math.asin(Math.min((labelSizes.highest.height + 6) / tickWidth, 1)), + Math.asin(Math.min(maxHeight / maxLabelDiagonal, 1)) - Math.asin(maxLabelHeight / maxLabelDiagonal) + )); + labelRotation = Math.max(minRotation, Math.min(maxRotation, labelRotation)); + } + me.labelRotation = labelRotation; + } + afterCalculateLabelRotation() { + callback(this.options.afterCalculateLabelRotation, [this]); + } + beforeFit() { + callback(this.options.beforeFit, [this]); + } + fit() { + const me = this; + const minSize = { + width: 0, + height: 0 + }; + const {chart, options: {ticks: tickOpts, title: titleOpts, grid: gridOpts}} = me; + const display = me._isVisible(); + const isHorizontal = me.isHorizontal(); + if (display) { + const titleHeight = getTitleHeight(titleOpts, chart.options.font); + if (isHorizontal) { + minSize.width = me.maxWidth; + minSize.height = getTickMarkLength(gridOpts) + titleHeight; + } else { + minSize.height = me.maxHeight; + minSize.width = getTickMarkLength(gridOpts) + titleHeight; + } + if (tickOpts.display && me.ticks.length) { + const {first, last, widest, highest} = me._getLabelSizes(); + const tickPadding = tickOpts.padding * 2; + const angleRadians = toRadians(me.labelRotation); + const cos = Math.cos(angleRadians); + const sin = Math.sin(angleRadians); + if (isHorizontal) { + const labelHeight = tickOpts.mirror ? 0 : sin * widest.width + cos * highest.height; + minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight + tickPadding); + } else { + const labelWidth = tickOpts.mirror ? 0 : cos * widest.width + sin * highest.height; + minSize.width = Math.min(me.maxWidth, minSize.width + labelWidth + tickPadding); + } + me._calculatePadding(first, last, sin, cos); + } + } + me._handleMargins(); + if (isHorizontal) { + me.width = me._length = chart.width - me._margins.left - me._margins.right; + me.height = minSize.height; + } else { + me.width = minSize.width; + me.height = me._length = chart.height - me._margins.top - me._margins.bottom; + } + } + _calculatePadding(first, last, sin, cos) { + const me = this; + const {ticks: {align, padding}, position} = me.options; + const isRotated = me.labelRotation !== 0; + const labelsBelowTicks = position !== 'top' && me.axis === 'x'; + if (me.isHorizontal()) { + const offsetLeft = me.getPixelForTick(0) - me.left; + const offsetRight = me.right - me.getPixelForTick(me.ticks.length - 1); + let paddingLeft = 0; + let paddingRight = 0; + if (isRotated) { + if (labelsBelowTicks) { + paddingLeft = cos * first.width; + paddingRight = sin * last.height; + } else { + paddingLeft = sin * first.height; + paddingRight = cos * last.width; + } + } else if (align === 'start') { + paddingRight = last.width; + } else if (align === 'end') { + paddingLeft = first.width; + } else { + paddingLeft = first.width / 2; + paddingRight = last.width / 2; + } + me.paddingLeft = Math.max((paddingLeft - offsetLeft + padding) * me.width / (me.width - offsetLeft), 0); + me.paddingRight = Math.max((paddingRight - offsetRight + padding) * me.width / (me.width - offsetRight), 0); + } else { + let paddingTop = last.height / 2; + let paddingBottom = first.height / 2; + if (align === 'start') { + paddingTop = 0; + paddingBottom = first.height; + } else if (align === 'end') { + paddingTop = last.height; + paddingBottom = 0; + } + me.paddingTop = paddingTop + padding; + me.paddingBottom = paddingBottom + padding; + } + } + _handleMargins() { + const me = this; + if (me._margins) { + me._margins.left = Math.max(me.paddingLeft, me._margins.left); + me._margins.top = Math.max(me.paddingTop, me._margins.top); + me._margins.right = Math.max(me.paddingRight, me._margins.right); + me._margins.bottom = Math.max(me.paddingBottom, me._margins.bottom); + } + } + afterFit() { + callback(this.options.afterFit, [this]); + } + isHorizontal() { + const {axis, position} = this.options; + return position === 'top' || position === 'bottom' || axis === 'x'; + } + isFullSize() { + return this.options.fullSize; + } + _convertTicksToLabels(ticks) { + const me = this; + me.beforeTickToLabelConversion(); + me.generateTickLabels(ticks); + let i, ilen; + for (i = 0, ilen = ticks.length; i < ilen; i++) { + if (isNullOrUndef(ticks[i].label)) { + ticks.splice(i, 1); + ilen--; + i--; + } + } + me.afterTickToLabelConversion(); + } + _getLabelSizes() { + const me = this; + let labelSizes = me._labelSizes; + if (!labelSizes) { + const sampleSize = me.options.ticks.sampleSize; + let ticks = me.ticks; + if (sampleSize < ticks.length) { + ticks = sample(ticks, sampleSize); + } + me._labelSizes = labelSizes = me._computeLabelSizes(ticks, ticks.length); + } + return labelSizes; + } + _computeLabelSizes(ticks, length) { + const {ctx, _longestTextCache: caches} = this; + const widths = []; + const heights = []; + let widestLabelSize = 0; + let highestLabelSize = 0; + let i, j, jlen, label, tickFont, fontString, cache, lineHeight, width, height, nestedLabel; + for (i = 0; i < length; ++i) { + label = ticks[i].label; + tickFont = this._resolveTickFontOptions(i); + ctx.font = fontString = tickFont.string; + cache = caches[fontString] = caches[fontString] || {data: {}, gc: []}; + lineHeight = tickFont.lineHeight; + width = height = 0; + if (!isNullOrUndef(label) && !isArray(label)) { + width = _measureText(ctx, cache.data, cache.gc, width, label); + height = lineHeight; + } else if (isArray(label)) { + for (j = 0, jlen = label.length; j < jlen; ++j) { + nestedLabel = label[j]; + if (!isNullOrUndef(nestedLabel) && !isArray(nestedLabel)) { + width = _measureText(ctx, cache.data, cache.gc, width, nestedLabel); + height += lineHeight; + } + } + } + widths.push(width); + heights.push(height); + widestLabelSize = Math.max(width, widestLabelSize); + highestLabelSize = Math.max(height, highestLabelSize); + } + garbageCollect(caches, length); + const widest = widths.indexOf(widestLabelSize); + const highest = heights.indexOf(highestLabelSize); + const valueAt = (idx) => ({width: widths[idx] || 0, height: heights[idx] || 0}); + return { + first: valueAt(0), + last: valueAt(length - 1), + widest: valueAt(widest), + highest: valueAt(highest), + widths, + heights, + }; + } + getLabelForValue(value) { + return value; + } + getPixelForValue(value, index) { + return NaN; + } + getValueForPixel(pixel) {} + getPixelForTick(index) { + const ticks = this.ticks; + if (index < 0 || index > ticks.length - 1) { + return null; + } + return this.getPixelForValue(ticks[index].value); + } + getPixelForDecimal(decimal) { + const me = this; + if (me._reversePixels) { + decimal = 1 - decimal; + } + const pixel = me._startPixel + decimal * me._length; + return _int16Range(me._alignToPixels ? _alignPixel(me.chart, pixel, 0) : pixel); + } + getDecimalForPixel(pixel) { + const decimal = (pixel - this._startPixel) / this._length; + return this._reversePixels ? 1 - decimal : decimal; + } + getBasePixel() { + return this.getPixelForValue(this.getBaseValue()); + } + getBaseValue() { + const {min, max} = this; + return min < 0 && max < 0 ? max : + min > 0 && max > 0 ? min : + 0; + } + getContext(index) { + const me = this; + const ticks = me.ticks || []; + if (index >= 0 && index < ticks.length) { + const tick = ticks[index]; + return tick.$context || + (tick.$context = createTickContext(me.getContext(), index, tick)); + } + return me.$context || + (me.$context = createScaleContext(me.chart.getContext(), me)); + } + _tickSize() { + const me = this; + const optionTicks = me.options.ticks; + const rot = toRadians(me.labelRotation); + const cos = Math.abs(Math.cos(rot)); + const sin = Math.abs(Math.sin(rot)); + const labelSizes = me._getLabelSizes(); + const padding = optionTicks.autoSkipPadding || 0; + const w = labelSizes ? labelSizes.widest.width + padding : 0; + const h = labelSizes ? labelSizes.highest.height + padding : 0; + return me.isHorizontal() + ? h * cos > w * sin ? w / cos : h / sin + : h * sin < w * cos ? h / cos : w / sin; + } + _isVisible() { + const display = this.options.display; + if (display !== 'auto') { + return !!display; + } + return this.getMatchingVisibleMetas().length > 0; + } + _computeGridLineItems(chartArea) { + const me = this; + const axis = me.axis; + const chart = me.chart; + const options = me.options; + const {grid, position} = options; + const offset = grid.offset; + const isHorizontal = me.isHorizontal(); + const ticks = me.ticks; + const ticksLength = ticks.length + (offset ? 1 : 0); + const tl = getTickMarkLength(grid); + const items = []; + const borderOpts = grid.setContext(me.getContext()); + const axisWidth = borderOpts.drawBorder ? borderOpts.borderWidth : 0; + const axisHalfWidth = axisWidth / 2; + const alignBorderValue = function(pixel) { + return _alignPixel(chart, pixel, axisWidth); + }; + let borderValue, i, lineValue, alignedLineValue; + let tx1, ty1, tx2, ty2, x1, y1, x2, y2; + if (position === 'top') { + borderValue = alignBorderValue(me.bottom); + ty1 = me.bottom - tl; + ty2 = borderValue - axisHalfWidth; + y1 = alignBorderValue(chartArea.top) + axisHalfWidth; + y2 = chartArea.bottom; + } else if (position === 'bottom') { + borderValue = alignBorderValue(me.top); + y1 = chartArea.top; + y2 = alignBorderValue(chartArea.bottom) - axisHalfWidth; + ty1 = borderValue + axisHalfWidth; + ty2 = me.top + tl; + } else if (position === 'left') { + borderValue = alignBorderValue(me.right); + tx1 = me.right - tl; + tx2 = borderValue - axisHalfWidth; + x1 = alignBorderValue(chartArea.left) + axisHalfWidth; + x2 = chartArea.right; + } else if (position === 'right') { + borderValue = alignBorderValue(me.left); + x1 = chartArea.left; + x2 = alignBorderValue(chartArea.right) - axisHalfWidth; + tx1 = borderValue + axisHalfWidth; + tx2 = me.left + tl; + } else if (axis === 'x') { + if (position === 'center') { + borderValue = alignBorderValue((chartArea.top + chartArea.bottom) / 2 + 0.5); + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + borderValue = alignBorderValue(me.chart.scales[positionAxisID].getPixelForValue(value)); + } + y1 = chartArea.top; + y2 = chartArea.bottom; + ty1 = borderValue + axisHalfWidth; + ty2 = ty1 + tl; + } else if (axis === 'y') { + if (position === 'center') { + borderValue = alignBorderValue((chartArea.left + chartArea.right) / 2); + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + borderValue = alignBorderValue(me.chart.scales[positionAxisID].getPixelForValue(value)); + } + tx1 = borderValue - axisHalfWidth; + tx2 = tx1 - tl; + x1 = chartArea.left; + x2 = chartArea.right; + } + const limit = valueOrDefault(options.ticks.maxTicksLimit, ticksLength); + const step = Math.max(1, Math.ceil(ticksLength / limit)); + for (i = 0; i < ticksLength; i += step) { + const optsAtIndex = grid.setContext(me.getContext(i)); + const lineWidth = optsAtIndex.lineWidth; + const lineColor = optsAtIndex.color; + const borderDash = grid.borderDash || []; + const borderDashOffset = optsAtIndex.borderDashOffset; + const tickWidth = optsAtIndex.tickWidth; + const tickColor = optsAtIndex.tickColor; + const tickBorderDash = optsAtIndex.tickBorderDash || []; + const tickBorderDashOffset = optsAtIndex.tickBorderDashOffset; + lineValue = getPixelForGridLine(me, i, offset); + if (lineValue === undefined) { + continue; + } + alignedLineValue = _alignPixel(chart, lineValue, lineWidth); + if (isHorizontal) { + tx1 = tx2 = x1 = x2 = alignedLineValue; + } else { + ty1 = ty2 = y1 = y2 = alignedLineValue; + } + items.push({ + tx1, + ty1, + tx2, + ty2, + x1, + y1, + x2, + y2, + width: lineWidth, + color: lineColor, + borderDash, + borderDashOffset, + tickWidth, + tickColor, + tickBorderDash, + tickBorderDashOffset, + }); + } + me._ticksLength = ticksLength; + me._borderValue = borderValue; + return items; + } + _computeLabelItems(chartArea) { + const me = this; + const axis = me.axis; + const options = me.options; + const {position, ticks: optionTicks} = options; + const isHorizontal = me.isHorizontal(); + const ticks = me.ticks; + const {align, crossAlign, padding, mirror} = optionTicks; + const tl = getTickMarkLength(options.grid); + const tickAndPadding = tl + padding; + const hTickAndPadding = mirror ? -padding : tickAndPadding; + const rotation = -toRadians(me.labelRotation); + const items = []; + let i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset; + let textBaseline = 'middle'; + if (position === 'top') { + y = me.bottom - hTickAndPadding; + textAlign = me._getXAxisLabelAlignment(); + } else if (position === 'bottom') { + y = me.top + hTickAndPadding; + textAlign = me._getXAxisLabelAlignment(); + } else if (position === 'left') { + const ret = me._getYAxisLabelAlignment(tl); + textAlign = ret.textAlign; + x = ret.x; + } else if (position === 'right') { + const ret = me._getYAxisLabelAlignment(tl); + textAlign = ret.textAlign; + x = ret.x; + } else if (axis === 'x') { + if (position === 'center') { + y = ((chartArea.top + chartArea.bottom) / 2) + tickAndPadding; + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + y = me.chart.scales[positionAxisID].getPixelForValue(value) + tickAndPadding; + } + textAlign = me._getXAxisLabelAlignment(); + } else if (axis === 'y') { + if (position === 'center') { + x = ((chartArea.left + chartArea.right) / 2) - tickAndPadding; + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + x = me.chart.scales[positionAxisID].getPixelForValue(value); + } + textAlign = me._getYAxisLabelAlignment(tl).textAlign; + } + if (axis === 'y') { + if (align === 'start') { + textBaseline = 'top'; + } else if (align === 'end') { + textBaseline = 'bottom'; + } + } + const labelSizes = me._getLabelSizes(); + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + tick = ticks[i]; + label = tick.label; + const optsAtIndex = optionTicks.setContext(me.getContext(i)); + pixel = me.getPixelForTick(i) + optionTicks.labelOffset; + font = me._resolveTickFontOptions(i); + lineHeight = font.lineHeight; + lineCount = isArray(label) ? label.length : 1; + const halfCount = lineCount / 2; + const color = optsAtIndex.color; + const strokeColor = optsAtIndex.textStrokeColor; + const strokeWidth = optsAtIndex.textStrokeWidth; + if (isHorizontal) { + x = pixel; + if (position === 'top') { + if (crossAlign === 'near' || rotation !== 0) { + textOffset = -lineCount * lineHeight + lineHeight / 2; + } else if (crossAlign === 'center') { + textOffset = -labelSizes.highest.height / 2 - halfCount * lineHeight + lineHeight; + } else { + textOffset = -labelSizes.highest.height + lineHeight / 2; + } + } else { + if (crossAlign === 'near' || rotation !== 0) { + textOffset = lineHeight / 2; + } else if (crossAlign === 'center') { + textOffset = labelSizes.highest.height / 2 - halfCount * lineHeight; + } else { + textOffset = labelSizes.highest.height - lineCount * lineHeight; + } + } + if (mirror) { + textOffset *= -1; + } + } else { + y = pixel; + textOffset = (1 - lineCount) * lineHeight / 2; + } + let backdrop; + if (optsAtIndex.showLabelBackdrop) { + const labelPadding = toPadding(optsAtIndex.backdropPadding); + const height = labelSizes.heights[i]; + const width = labelSizes.widths[i]; + let top = y + textOffset - labelPadding.top; + let left = x - labelPadding.left; + switch (textBaseline) { + case 'middle': + top -= height / 2; + break; + case 'bottom': + top -= height; + break; + } + switch (textAlign) { + case 'center': + left -= width / 2; + break; + case 'right': + left -= width; + break; + } + backdrop = { + left, + top, + width: width + labelPadding.width, + height: height + labelPadding.height, + color: optsAtIndex.backdropColor, + }; + } + items.push({ + rotation, + label, + font, + color, + strokeColor, + strokeWidth, + textOffset, + textAlign, + textBaseline, + translation: [x, y], + backdrop, + }); + } + return items; + } + _getXAxisLabelAlignment() { + const me = this; + const {position, ticks} = me.options; + const rotation = -toRadians(me.labelRotation); + if (rotation) { + return position === 'top' ? 'left' : 'right'; + } + let align = 'center'; + if (ticks.align === 'start') { + align = 'left'; + } else if (ticks.align === 'end') { + align = 'right'; + } + return align; + } + _getYAxisLabelAlignment(tl) { + const me = this; + const {position, ticks: {crossAlign, mirror, padding}} = me.options; + const labelSizes = me._getLabelSizes(); + const tickAndPadding = tl + padding; + const widest = labelSizes.widest.width; + let textAlign; + let x; + if (position === 'left') { + if (mirror) { + textAlign = 'left'; + x = me.right + padding; + } else { + x = me.right - tickAndPadding; + if (crossAlign === 'near') { + textAlign = 'right'; + } else if (crossAlign === 'center') { + textAlign = 'center'; + x -= (widest / 2); + } else { + textAlign = 'left'; + x = me.left; + } + } + } else if (position === 'right') { + if (mirror) { + textAlign = 'right'; + x = me.left + padding; + } else { + x = me.left + tickAndPadding; + if (crossAlign === 'near') { + textAlign = 'left'; + } else if (crossAlign === 'center') { + textAlign = 'center'; + x += widest / 2; + } else { + textAlign = 'right'; + x = me.right; + } + } + } else { + textAlign = 'right'; + } + return {textAlign, x}; + } + _computeLabelArea() { + const me = this; + if (me.options.ticks.mirror) { + return; + } + const chart = me.chart; + const position = me.options.position; + if (position === 'left' || position === 'right') { + return {top: 0, left: me.left, bottom: chart.height, right: me.right}; + } if (position === 'top' || position === 'bottom') { + return {top: me.top, left: 0, bottom: me.bottom, right: chart.width}; + } + } + drawBackground() { + const {ctx, options: {backgroundColor}, left, top, width, height} = this; + if (backgroundColor) { + ctx.save(); + ctx.fillStyle = backgroundColor; + ctx.fillRect(left, top, width, height); + ctx.restore(); + } + } + getLineWidthForValue(value) { + const me = this; + const grid = me.options.grid; + if (!me._isVisible() || !grid.display) { + return 0; + } + const ticks = me.ticks; + const index = ticks.findIndex(t => t.value === value); + if (index >= 0) { + const opts = grid.setContext(me.getContext(index)); + return opts.lineWidth; + } + return 0; + } + drawGrid(chartArea) { + const me = this; + const grid = me.options.grid; + const ctx = me.ctx; + const items = me._gridLineItems || (me._gridLineItems = me._computeGridLineItems(chartArea)); + let i, ilen; + const drawLine = (p1, p2, style) => { + if (!style.width || !style.color) { + return; + } + ctx.save(); + ctx.lineWidth = style.width; + ctx.strokeStyle = style.color; + ctx.setLineDash(style.borderDash || []); + ctx.lineDashOffset = style.borderDashOffset; + ctx.beginPath(); + ctx.moveTo(p1.x, p1.y); + ctx.lineTo(p2.x, p2.y); + ctx.stroke(); + ctx.restore(); + }; + if (grid.display) { + for (i = 0, ilen = items.length; i < ilen; ++i) { + const item = items[i]; + if (grid.drawOnChartArea) { + drawLine( + {x: item.x1, y: item.y1}, + {x: item.x2, y: item.y2}, + item + ); + } + if (grid.drawTicks) { + drawLine( + {x: item.tx1, y: item.ty1}, + {x: item.tx2, y: item.ty2}, + { + color: item.tickColor, + width: item.tickWidth, + borderDash: item.tickBorderDash, + borderDashOffset: item.tickBorderDashOffset + } + ); + } + } + } + } + drawBorder() { + const me = this; + const {chart, ctx, options: {grid}} = me; + const borderOpts = grid.setContext(me.getContext()); + const axisWidth = grid.drawBorder ? borderOpts.borderWidth : 0; + if (!axisWidth) { + return; + } + const lastLineWidth = grid.setContext(me.getContext(0)).lineWidth; + const borderValue = me._borderValue; + let x1, x2, y1, y2; + if (me.isHorizontal()) { + x1 = _alignPixel(chart, me.left, axisWidth) - axisWidth / 2; + x2 = _alignPixel(chart, me.right, lastLineWidth) + lastLineWidth / 2; + y1 = y2 = borderValue; + } else { + y1 = _alignPixel(chart, me.top, axisWidth) - axisWidth / 2; + y2 = _alignPixel(chart, me.bottom, lastLineWidth) + lastLineWidth / 2; + x1 = x2 = borderValue; + } + ctx.save(); + ctx.lineWidth = borderOpts.borderWidth; + ctx.strokeStyle = borderOpts.borderColor; + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + ctx.restore(); + } + drawLabels(chartArea) { + const me = this; + const optionTicks = me.options.ticks; + if (!optionTicks.display) { + return; + } + const ctx = me.ctx; + const area = me._computeLabelArea(); + if (area) { + clipArea(ctx, area); + } + const items = me._labelItems || (me._labelItems = me._computeLabelItems(chartArea)); + let i, ilen; + for (i = 0, ilen = items.length; i < ilen; ++i) { + const item = items[i]; + const tickFont = item.font; + const label = item.label; + if (item.backdrop) { + ctx.fillStyle = item.backdrop.color; + ctx.fillRect(item.backdrop.left, item.backdrop.top, item.backdrop.width, item.backdrop.height); + } + let y = item.textOffset; + renderText(ctx, label, 0, y, tickFont, item); + } + if (area) { + unclipArea(ctx); + } + } + drawTitle() { + const {ctx, options: {position, title, reverse}} = this; + if (!title.display) { + return; + } + const font = toFont(title.font); + const padding = toPadding(title.padding); + const align = title.align; + let offset = font.lineHeight / 2; + if (position === 'bottom') { + offset += padding.bottom; + if (isArray(title.text)) { + offset += font.lineHeight * (title.text.length - 1); + } + } else { + offset += padding.top; + } + const {titleX, titleY, maxWidth, rotation} = titleArgs(this, offset, position, align); + renderText(ctx, title.text, 0, 0, font, { + color: title.color, + maxWidth, + rotation, + textAlign: titleAlign(align, position, reverse), + textBaseline: 'middle', + translation: [titleX, titleY], + }); + } + draw(chartArea) { + const me = this; + if (!me._isVisible()) { + return; + } + me.drawBackground(); + me.drawGrid(chartArea); + me.drawBorder(); + me.drawTitle(); + me.drawLabels(chartArea); + } + _layers() { + const me = this; + const opts = me.options; + const tz = opts.ticks && opts.ticks.z || 0; + const gz = opts.grid && opts.grid.z || 0; + if (!me._isVisible() || me.draw !== Scale.prototype.draw) { + return [{ + z: tz, + draw(chartArea) { + me.draw(chartArea); + } + }]; + } + return [{ + z: gz, + draw(chartArea) { + me.drawBackground(); + me.drawGrid(chartArea); + me.drawTitle(); + } + }, { + z: gz + 1, + draw() { + me.drawBorder(); + } + }, { + z: tz, + draw(chartArea) { + me.drawLabels(chartArea); + } + }]; + } + getMatchingVisibleMetas(type) { + const me = this; + const metas = me.chart.getSortedVisibleDatasetMetas(); + const axisID = me.axis + 'AxisID'; + const result = []; + let i, ilen; + for (i = 0, ilen = metas.length; i < ilen; ++i) { + const meta = metas[i]; + if (meta[axisID] === me.id && (!type || meta.type === type)) { + result.push(meta); + } + } + return result; + } + _resolveTickFontOptions(index) { + const opts = this.options.ticks.setContext(this.getContext(index)); + return toFont(opts.font); + } + _maxDigits() { + const me = this; + const fontSize = me._resolveTickFontOptions(0).lineHeight; + return (me.isHorizontal() ? me.width : me.height) / fontSize; + } +} + +function _createResolver(scopes, prefixes = [''], rootScopes = scopes, fallback, getTarget = () => scopes[0]) { + if (!defined(fallback)) { + fallback = _resolve('_fallback', scopes); + } + const cache = { + [Symbol.toStringTag]: 'Object', + _cacheable: true, + _scopes: scopes, + _rootScopes: rootScopes, + _fallback: fallback, + _getTarget: getTarget, + override: (scope) => _createResolver([scope, ...scopes], prefixes, rootScopes, fallback), + }; + return new Proxy(cache, { + deleteProperty(target, prop) { + delete target[prop]; + delete target._keys; + delete scopes[0][prop]; + return true; + }, + get(target, prop) { + return _cached(target, prop, + () => _resolveWithPrefixes(prop, prefixes, scopes, target)); + }, + getOwnPropertyDescriptor(target, prop) { + return Reflect.getOwnPropertyDescriptor(target._scopes[0], prop); + }, + getPrototypeOf() { + return Reflect.getPrototypeOf(scopes[0]); + }, + has(target, prop) { + return getKeysFromAllScopes(target).includes(prop); + }, + ownKeys(target) { + return getKeysFromAllScopes(target); + }, + set(target, prop, value) { + const storage = target._storage || (target._storage = getTarget()); + storage[prop] = value; + delete target[prop]; + delete target._keys; + return true; + } + }); +} +function _attachContext(proxy, context, subProxy, descriptorDefaults) { + const cache = { + _cacheable: false, + _proxy: proxy, + _context: context, + _subProxy: subProxy, + _stack: new Set(), + _descriptors: _descriptors(proxy, descriptorDefaults), + setContext: (ctx) => _attachContext(proxy, ctx, subProxy, descriptorDefaults), + override: (scope) => _attachContext(proxy.override(scope), context, subProxy, descriptorDefaults) + }; + return new Proxy(cache, { + deleteProperty(target, prop) { + delete target[prop]; + delete proxy[prop]; + return true; + }, + get(target, prop, receiver) { + return _cached(target, prop, + () => _resolveWithContext(target, prop, receiver)); + }, + getOwnPropertyDescriptor(target, prop) { + return target._descriptors.allKeys + ? Reflect.has(proxy, prop) ? {enumerable: true, configurable: true} : undefined + : Reflect.getOwnPropertyDescriptor(proxy, prop); + }, + getPrototypeOf() { + return Reflect.getPrototypeOf(proxy); + }, + has(target, prop) { + return Reflect.has(proxy, prop); + }, + ownKeys() { + return Reflect.ownKeys(proxy); + }, + set(target, prop, value) { + proxy[prop] = value; + delete target[prop]; + return true; + } + }); +} +function _descriptors(proxy, defaults = {scriptable: true, indexable: true}) { + const {_scriptable = defaults.scriptable, _indexable = defaults.indexable, _allKeys = defaults.allKeys} = proxy; + return { + allKeys: _allKeys, + scriptable: _scriptable, + indexable: _indexable, + isScriptable: isFunction(_scriptable) ? _scriptable : () => _scriptable, + isIndexable: isFunction(_indexable) ? _indexable : () => _indexable + }; +} +const readKey = (prefix, name) => prefix ? prefix + _capitalize(name) : name; +const needsSubResolver = (prop, value) => isObject(value) && prop !== 'adapters'; +function _cached(target, prop, resolve) { + let value = target[prop]; + if (defined(value)) { + return value; + } + value = resolve(); + if (defined(value)) { + target[prop] = value; + } + return value; +} +function _resolveWithContext(target, prop, receiver) { + const {_proxy, _context, _subProxy, _descriptors: descriptors} = target; + let value = _proxy[prop]; + if (isFunction(value) && descriptors.isScriptable(prop)) { + value = _resolveScriptable(prop, value, target, receiver); + } + if (isArray(value) && value.length) { + value = _resolveArray(prop, value, target, descriptors.isIndexable); + } + if (needsSubResolver(prop, value)) { + value = _attachContext(value, _context, _subProxy && _subProxy[prop], descriptors); + } + return value; +} +function _resolveScriptable(prop, value, target, receiver) { + const {_proxy, _context, _subProxy, _stack} = target; + if (_stack.has(prop)) { + throw new Error('Recursion detected: ' + Array.from(_stack).join('->') + '->' + prop); + } + _stack.add(prop); + value = value(_context, _subProxy || receiver); + _stack.delete(prop); + if (isObject(value)) { + value = createSubResolver(_proxy._scopes, _proxy, prop, value); + } + return value; +} +function _resolveArray(prop, value, target, isIndexable) { + const {_proxy, _context, _subProxy, _descriptors: descriptors} = target; + if (defined(_context.index) && isIndexable(prop)) { + value = value[_context.index % value.length]; + } else if (isObject(value[0])) { + const arr = value; + const scopes = _proxy._scopes.filter(s => s !== arr); + value = []; + for (const item of arr) { + const resolver = createSubResolver(scopes, _proxy, prop, item); + value.push(_attachContext(resolver, _context, _subProxy && _subProxy[prop], descriptors)); + } + } + return value; +} +function resolveFallback(fallback, prop, value) { + return isFunction(fallback) ? fallback(prop, value) : fallback; +} +const getScope = (key, parent) => key === true ? parent + : typeof key === 'string' ? resolveObjectKey(parent, key) : undefined; +function addScopes(set, parentScopes, key, parentFallback) { + for (const parent of parentScopes) { + const scope = getScope(key, parent); + if (scope) { + set.add(scope); + const fallback = resolveFallback(scope._fallback, key, scope); + if (defined(fallback) && fallback !== key && fallback !== parentFallback) { + return fallback; + } + } else if (scope === false && defined(parentFallback) && key !== parentFallback) { + return null; + } + } + return false; +} +function createSubResolver(parentScopes, resolver, prop, value) { + const rootScopes = resolver._rootScopes; + const fallback = resolveFallback(resolver._fallback, prop, value); + const allScopes = [...parentScopes, ...rootScopes]; + const set = new Set(); + set.add(value); + let key = addScopesFromKey(set, allScopes, prop, fallback || prop); + if (key === null) { + return false; + } + if (defined(fallback) && fallback !== prop) { + key = addScopesFromKey(set, allScopes, fallback, key); + if (key === null) { + return false; + } + } + return _createResolver(Array.from(set), [''], rootScopes, fallback, + () => subGetTarget(resolver, prop, value)); +} +function addScopesFromKey(set, allScopes, key, fallback) { + while (key) { + key = addScopes(set, allScopes, key, fallback); + } + return key; +} +function subGetTarget(resolver, prop, value) { + const parent = resolver._getTarget(); + if (!(prop in parent)) { + parent[prop] = {}; + } + const target = parent[prop]; + if (isArray(target) && isObject(value)) { + return value; + } + return target; +} +function _resolveWithPrefixes(prop, prefixes, scopes, proxy) { + let value; + for (const prefix of prefixes) { + value = _resolve(readKey(prefix, prop), scopes); + if (defined(value)) { + return needsSubResolver(prop, value) + ? createSubResolver(scopes, proxy, prop, value) + : value; + } + } +} +function _resolve(key, scopes) { + for (const scope of scopes) { + if (!scope) { + continue; + } + const value = scope[key]; + if (defined(value)) { + return value; + } + } +} +function getKeysFromAllScopes(target) { + let keys = target._keys; + if (!keys) { + keys = target._keys = resolveKeysFromAllScopes(target._scopes); + } + return keys; +} +function resolveKeysFromAllScopes(scopes) { + const set = new Set(); + for (const scope of scopes) { + for (const key of Object.keys(scope).filter(k => !k.startsWith('_'))) { + set.add(key); + } + } + return Array.from(set); +} + +const EPSILON = Number.EPSILON || 1e-14; +const getPoint = (points, i) => i < points.length && !points[i].skip && points[i]; +const getValueAxis = (indexAxis) => indexAxis === 'x' ? 'y' : 'x'; +function splineCurve(firstPoint, middlePoint, afterPoint, t) { + const previous = firstPoint.skip ? middlePoint : firstPoint; + const current = middlePoint; + const next = afterPoint.skip ? middlePoint : afterPoint; + const d01 = distanceBetweenPoints(current, previous); + const d12 = distanceBetweenPoints(next, current); + let s01 = d01 / (d01 + d12); + let s12 = d12 / (d01 + d12); + s01 = isNaN(s01) ? 0 : s01; + s12 = isNaN(s12) ? 0 : s12; + const fa = t * s01; + const fb = t * s12; + return { + previous: { + x: current.x - fa * (next.x - previous.x), + y: current.y - fa * (next.y - previous.y) + }, + next: { + x: current.x + fb * (next.x - previous.x), + y: current.y + fb * (next.y - previous.y) + } + }; +} +function monotoneAdjust(points, deltaK, mK) { + const pointsLen = points.length; + let alphaK, betaK, tauK, squaredMagnitude, pointCurrent; + let pointAfter = getPoint(points, 0); + for (let i = 0; i < pointsLen - 1; ++i) { + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent || !pointAfter) { + continue; + } + if (almostEquals(deltaK[i], 0, EPSILON)) { + mK[i] = mK[i + 1] = 0; + continue; + } + alphaK = mK[i] / deltaK[i]; + betaK = mK[i + 1] / deltaK[i]; + squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2); + if (squaredMagnitude <= 9) { + continue; + } + tauK = 3 / Math.sqrt(squaredMagnitude); + mK[i] = alphaK * tauK * deltaK[i]; + mK[i + 1] = betaK * tauK * deltaK[i]; + } +} +function monotoneCompute(points, mK, indexAxis = 'x') { + const valueAxis = getValueAxis(indexAxis); + const pointsLen = points.length; + let delta, pointBefore, pointCurrent; + let pointAfter = getPoint(points, 0); + for (let i = 0; i < pointsLen; ++i) { + pointBefore = pointCurrent; + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent) { + continue; + } + const iPixel = pointCurrent[indexAxis]; + const vPixel = pointCurrent[valueAxis]; + if (pointBefore) { + delta = (iPixel - pointBefore[indexAxis]) / 3; + pointCurrent[`cp1${indexAxis}`] = iPixel - delta; + pointCurrent[`cp1${valueAxis}`] = vPixel - delta * mK[i]; + } + if (pointAfter) { + delta = (pointAfter[indexAxis] - iPixel) / 3; + pointCurrent[`cp2${indexAxis}`] = iPixel + delta; + pointCurrent[`cp2${valueAxis}`] = vPixel + delta * mK[i]; + } + } +} +function splineCurveMonotone(points, indexAxis = 'x') { + const valueAxis = getValueAxis(indexAxis); + const pointsLen = points.length; + const deltaK = Array(pointsLen).fill(0); + const mK = Array(pointsLen); + let i, pointBefore, pointCurrent; + let pointAfter = getPoint(points, 0); + for (i = 0; i < pointsLen; ++i) { + pointBefore = pointCurrent; + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent) { + continue; + } + if (pointAfter) { + const slopeDelta = pointAfter[indexAxis] - pointCurrent[indexAxis]; + deltaK[i] = slopeDelta !== 0 ? (pointAfter[valueAxis] - pointCurrent[valueAxis]) / slopeDelta : 0; + } + mK[i] = !pointBefore ? deltaK[i] + : !pointAfter ? deltaK[i - 1] + : (sign(deltaK[i - 1]) !== sign(deltaK[i])) ? 0 + : (deltaK[i - 1] + deltaK[i]) / 2; + } + monotoneAdjust(points, deltaK, mK); + monotoneCompute(points, mK, indexAxis); +} +function capControlPoint(pt, min, max) { + return Math.max(Math.min(pt, max), min); +} +function capBezierPoints(points, area) { + let i, ilen, point, inArea, inAreaPrev; + let inAreaNext = _isPointInArea(points[0], area); + for (i = 0, ilen = points.length; i < ilen; ++i) { + inAreaPrev = inArea; + inArea = inAreaNext; + inAreaNext = i < ilen - 1 && _isPointInArea(points[i + 1], area); + if (!inArea) { + continue; + } + point = points[i]; + if (inAreaPrev) { + point.cp1x = capControlPoint(point.cp1x, area.left, area.right); + point.cp1y = capControlPoint(point.cp1y, area.top, area.bottom); + } + if (inAreaNext) { + point.cp2x = capControlPoint(point.cp2x, area.left, area.right); + point.cp2y = capControlPoint(point.cp2y, area.top, area.bottom); + } + } +} +function _updateBezierControlPoints(points, options, area, loop, indexAxis) { + let i, ilen, point, controlPoints; + if (options.spanGaps) { + points = points.filter((pt) => !pt.skip); + } + if (options.cubicInterpolationMode === 'monotone') { + splineCurveMonotone(points, indexAxis); + } else { + let prev = loop ? points[points.length - 1] : points[0]; + for (i = 0, ilen = points.length; i < ilen; ++i) { + point = points[i]; + controlPoints = splineCurve( + prev, + point, + points[Math.min(i + 1, ilen - (loop ? 0 : 1)) % ilen], + options.tension + ); + point.cp1x = controlPoints.previous.x; + point.cp1y = controlPoints.previous.y; + point.cp2x = controlPoints.next.x; + point.cp2y = controlPoints.next.y; + prev = point; + } + } + if (options.capBezierPoints) { + capBezierPoints(points, area); + } +} + +function _pointInLine(p1, p2, t, mode) { + return { + x: p1.x + t * (p2.x - p1.x), + y: p1.y + t * (p2.y - p1.y) + }; +} +function _steppedInterpolation(p1, p2, t, mode) { + return { + x: p1.x + t * (p2.x - p1.x), + y: mode === 'middle' ? t < 0.5 ? p1.y : p2.y + : mode === 'after' ? t < 1 ? p1.y : p2.y + : t > 0 ? p2.y : p1.y + }; +} +function _bezierInterpolation(p1, p2, t, mode) { + const cp1 = {x: p1.cp2x, y: p1.cp2y}; + const cp2 = {x: p2.cp1x, y: p2.cp1y}; + const a = _pointInLine(p1, cp1, t); + const b = _pointInLine(cp1, cp2, t); + const c = _pointInLine(cp2, p2, t); + const d = _pointInLine(a, b, t); + const e = _pointInLine(b, c, t); + return _pointInLine(d, e, t); +} + +const getRightToLeftAdapter = function(rectX, width) { + return { + x(x) { + return rectX + rectX + width - x; + }, + setWidth(w) { + width = w; + }, + textAlign(align) { + if (align === 'center') { + return align; + } + return align === 'right' ? 'left' : 'right'; + }, + xPlus(x, value) { + return x - value; + }, + leftForLtr(x, itemWidth) { + return x - itemWidth; + }, + }; +}; +const getLeftToRightAdapter = function() { + return { + x(x) { + return x; + }, + setWidth(w) { + }, + textAlign(align) { + return align; + }, + xPlus(x, value) { + return x + value; + }, + leftForLtr(x, _itemWidth) { + return x; + }, + }; +}; +function getRtlAdapter(rtl, rectX, width) { + return rtl ? getRightToLeftAdapter(rectX, width) : getLeftToRightAdapter(); +} +function overrideTextDirection(ctx, direction) { + let style, original; + if (direction === 'ltr' || direction === 'rtl') { + style = ctx.canvas.style; + original = [ + style.getPropertyValue('direction'), + style.getPropertyPriority('direction'), + ]; + style.setProperty('direction', direction, 'important'); + ctx.prevTextDirection = original; + } +} +function restoreTextDirection(ctx, original) { + if (original !== undefined) { + delete ctx.prevTextDirection; + ctx.canvas.style.setProperty('direction', original[0], original[1]); + } +} + +function propertyFn(property) { + if (property === 'angle') { + return { + between: _angleBetween, + compare: _angleDiff, + normalize: _normalizeAngle, + }; + } + return { + between: (n, s, e) => n >= Math.min(s, e) && n <= Math.max(e, s), + compare: (a, b) => a - b, + normalize: x => x + }; +} +function normalizeSegment({start, end, count, loop, style}) { + return { + start: start % count, + end: end % count, + loop: loop && (end - start + 1) % count === 0, + style + }; +} +function getSegment(segment, points, bounds) { + const {property, start: startBound, end: endBound} = bounds; + const {between, normalize} = propertyFn(property); + const count = points.length; + let {start, end, loop} = segment; + let i, ilen; + if (loop) { + start += count; + end += count; + for (i = 0, ilen = count; i < ilen; ++i) { + if (!between(normalize(points[start % count][property]), startBound, endBound)) { + break; + } + start--; + end--; + } + start %= count; + end %= count; + } + if (end < start) { + end += count; + } + return {start, end, loop, style: segment.style}; +} +function _boundSegment(segment, points, bounds) { + if (!bounds) { + return [segment]; + } + const {property, start: startBound, end: endBound} = bounds; + const count = points.length; + const {compare, between, normalize} = propertyFn(property); + const {start, end, loop, style} = getSegment(segment, points, bounds); + const result = []; + let inside = false; + let subStart = null; + let value, point, prevValue; + const startIsBefore = () => between(startBound, prevValue, value) && compare(startBound, prevValue) !== 0; + const endIsBefore = () => compare(endBound, value) === 0 || between(endBound, prevValue, value); + const shouldStart = () => inside || startIsBefore(); + const shouldStop = () => !inside || endIsBefore(); + for (let i = start, prev = start; i <= end; ++i) { + point = points[i % count]; + if (point.skip) { + continue; + } + value = normalize(point[property]); + if (value === prevValue) { + continue; + } + inside = between(value, startBound, endBound); + if (subStart === null && shouldStart()) { + subStart = compare(value, startBound) === 0 ? i : prev; + } + if (subStart !== null && shouldStop()) { + result.push(normalizeSegment({start: subStart, end: i, loop, count, style})); + subStart = null; + } + prev = i; + prevValue = value; + } + if (subStart !== null) { + result.push(normalizeSegment({start: subStart, end, loop, count, style})); + } + return result; +} +function _boundSegments(line, bounds) { + const result = []; + const segments = line.segments; + for (let i = 0; i < segments.length; i++) { + const sub = _boundSegment(segments[i], line.points, bounds); + if (sub.length) { + result.push(...sub); + } + } + return result; +} +function findStartAndEnd(points, count, loop, spanGaps) { + let start = 0; + let end = count - 1; + if (loop && !spanGaps) { + while (start < count && !points[start].skip) { + start++; + } + } + while (start < count && points[start].skip) { + start++; + } + start %= count; + if (loop) { + end += start; + } + while (end > start && points[end % count].skip) { + end--; + } + end %= count; + return {start, end}; +} +function solidSegments(points, start, max, loop) { + const count = points.length; + const result = []; + let last = start; + let prev = points[start]; + let end; + for (end = start + 1; end <= max; ++end) { + const cur = points[end % count]; + if (cur.skip || cur.stop) { + if (!prev.skip) { + loop = false; + result.push({start: start % count, end: (end - 1) % count, loop}); + start = last = cur.stop ? end : null; + } + } else { + last = end; + if (prev.skip) { + start = end; + } + } + prev = cur; + } + if (last !== null) { + result.push({start: start % count, end: last % count, loop}); + } + return result; +} +function _computeSegments(line, segmentOptions) { + const points = line.points; + const spanGaps = line.options.spanGaps; + const count = points.length; + if (!count) { + return []; + } + const loop = !!line._loop; + const {start, end} = findStartAndEnd(points, count, loop, spanGaps); + if (spanGaps === true) { + return splitByStyles([{start, end, loop}], points, segmentOptions); + } + const max = end < start ? end + count : end; + const completeLoop = !!line._fullLoop && start === 0 && end === count - 1; + return splitByStyles(solidSegments(points, start, max, completeLoop), points, segmentOptions); +} +function splitByStyles(segments, points, segmentOptions) { + if (!segmentOptions || !segmentOptions.setContext || !points) { + return segments; + } + return doSplitByStyles(segments, points, segmentOptions); +} +function doSplitByStyles(segments, points, segmentOptions) { + const count = points.length; + const result = []; + let start = segments[0].start; + let i = start; + for (const segment of segments) { + let prevStyle, style; + let prev = points[start % count]; + for (i = start + 1; i <= segment.end; i++) { + const pt = points[i % count]; + style = readStyle(segmentOptions.setContext({type: 'segment', p0: prev, p1: pt})); + if (styleChanged(style, prevStyle)) { + result.push({start: start, end: i - 1, loop: segment.loop, style: prevStyle}); + prevStyle = style; + start = i - 1; + } + prev = pt; + prevStyle = style; + } + if (start < i - 1) { + result.push({start, end: i - 1, loop: segment.loop, style}); + start = i - 1; + } + } + return result; +} +function readStyle(options) { + return { + backgroundColor: options.backgroundColor, + borderCapStyle: options.borderCapStyle, + borderDash: options.borderDash, + borderDashOffset: options.borderDashOffset, + borderJoinStyle: options.borderJoinStyle, + borderWidth: options.borderWidth, + borderColor: options.borderColor + }; +} +function styleChanged(style, prevStyle) { + return prevStyle && JSON.stringify(style) !== JSON.stringify(prevStyle); +} + +var helpers = /*#__PURE__*/Object.freeze({ +__proto__: null, +easingEffects: effects, +color: color, +getHoverColor: getHoverColor, +noop: noop, +uid: uid, +isNullOrUndef: isNullOrUndef, +isArray: isArray, +isObject: isObject, +isFinite: isNumberFinite, +finiteOrDefault: finiteOrDefault, +valueOrDefault: valueOrDefault, +toPercentage: toPercentage, +toDimension: toDimension, +callback: callback, +each: each, +_elementsEqual: _elementsEqual, +clone: clone, +_merger: _merger, +merge: merge, +mergeIf: mergeIf, +_mergerIf: _mergerIf, +_deprecated: _deprecated, +resolveObjectKey: resolveObjectKey, +_capitalize: _capitalize, +defined: defined, +isFunction: isFunction, +setsEqual: setsEqual, +toFontString: toFontString, +_measureText: _measureText, +_longestText: _longestText, +_alignPixel: _alignPixel, +clearCanvas: clearCanvas, +drawPoint: drawPoint, +_isPointInArea: _isPointInArea, +clipArea: clipArea, +unclipArea: unclipArea, +_steppedLineTo: _steppedLineTo, +_bezierCurveTo: _bezierCurveTo, +renderText: renderText, +addRoundedRectPath: addRoundedRectPath, +_lookup: _lookup, +_lookupByKey: _lookupByKey, +_rlookupByKey: _rlookupByKey, +_filterBetween: _filterBetween, +listenArrayEvents: listenArrayEvents, +unlistenArrayEvents: unlistenArrayEvents, +_arrayUnique: _arrayUnique, +_createResolver: _createResolver, +_attachContext: _attachContext, +_descriptors: _descriptors, +splineCurve: splineCurve, +splineCurveMonotone: splineCurveMonotone, +_updateBezierControlPoints: _updateBezierControlPoints, +_getParentNode: _getParentNode, +getStyle: getStyle, +getRelativePosition: getRelativePosition$1, +getMaximumSize: getMaximumSize, +retinaScale: retinaScale, +supportsEventListenerOptions: supportsEventListenerOptions, +readUsedSize: readUsedSize, +fontString: fontString, +requestAnimFrame: requestAnimFrame, +throttled: throttled, +debounce: debounce, +_toLeftRightCenter: _toLeftRightCenter, +_alignStartEnd: _alignStartEnd, +_textX: _textX, +_pointInLine: _pointInLine, +_steppedInterpolation: _steppedInterpolation, +_bezierInterpolation: _bezierInterpolation, +formatNumber: formatNumber, +toLineHeight: toLineHeight, +_readValueToProps: _readValueToProps, +toTRBL: toTRBL, +toTRBLCorners: toTRBLCorners, +toPadding: toPadding, +toFont: toFont, +resolve: resolve, +_addGrace: _addGrace, +PI: PI, +TAU: TAU, +PITAU: PITAU, +INFINITY: INFINITY, +RAD_PER_DEG: RAD_PER_DEG, +HALF_PI: HALF_PI, +QUARTER_PI: QUARTER_PI, +TWO_THIRDS_PI: TWO_THIRDS_PI, +log10: log10, +sign: sign, +niceNum: niceNum, +_factorize: _factorize, +isNumber: isNumber, +almostEquals: almostEquals, +almostWhole: almostWhole, +_setMinAndMaxByKey: _setMinAndMaxByKey, +toRadians: toRadians, +toDegrees: toDegrees, +_decimalPlaces: _decimalPlaces, +getAngleFromPoint: getAngleFromPoint, +distanceBetweenPoints: distanceBetweenPoints, +_angleDiff: _angleDiff, +_normalizeAngle: _normalizeAngle, +_angleBetween: _angleBetween, +_limitValue: _limitValue, +_int16Range: _int16Range, +getRtlAdapter: getRtlAdapter, +overrideTextDirection: overrideTextDirection, +restoreTextDirection: restoreTextDirection, +_boundSegment: _boundSegment, +_boundSegments: _boundSegments, +_computeSegments: _computeSegments +}); + +class TypedRegistry { + constructor(type, scope, override) { + this.type = type; + this.scope = scope; + this.override = override; + this.items = Object.create(null); + } + isForType(type) { + return Object.prototype.isPrototypeOf.call(this.type.prototype, type.prototype); + } + register(item) { + const me = this; + const proto = Object.getPrototypeOf(item); + let parentScope; + if (isIChartComponent(proto)) { + parentScope = me.register(proto); + } + const items = me.items; + const id = item.id; + const scope = me.scope + '.' + id; + if (!id) { + throw new Error('class does not have id: ' + item); + } + if (id in items) { + return scope; + } + items[id] = item; + registerDefaults(item, scope, parentScope); + if (me.override) { + defaults.override(item.id, item.overrides); + } + return scope; + } + get(id) { + return this.items[id]; + } + unregister(item) { + const items = this.items; + const id = item.id; + const scope = this.scope; + if (id in items) { + delete items[id]; + } + if (scope && id in defaults[scope]) { + delete defaults[scope][id]; + if (this.override) { + delete overrides[id]; + } + } + } +} +function registerDefaults(item, scope, parentScope) { + const itemDefaults = merge(Object.create(null), [ + parentScope ? defaults.get(parentScope) : {}, + defaults.get(scope), + item.defaults + ]); + defaults.set(scope, itemDefaults); + if (item.defaultRoutes) { + routeDefaults(scope, item.defaultRoutes); + } + if (item.descriptors) { + defaults.describe(scope, item.descriptors); + } +} +function routeDefaults(scope, routes) { + Object.keys(routes).forEach(property => { + const propertyParts = property.split('.'); + const sourceName = propertyParts.pop(); + const sourceScope = [scope].concat(propertyParts).join('.'); + const parts = routes[property].split('.'); + const targetName = parts.pop(); + const targetScope = parts.join('.'); + defaults.route(sourceScope, sourceName, targetScope, targetName); + }); +} +function isIChartComponent(proto) { + return 'id' in proto && 'defaults' in proto; +} + +class Registry { + constructor() { + this.controllers = new TypedRegistry(DatasetController, 'datasets', true); + this.elements = new TypedRegistry(Element, 'elements'); + this.plugins = new TypedRegistry(Object, 'plugins'); + this.scales = new TypedRegistry(Scale, 'scales'); + this._typedRegistries = [this.controllers, this.scales, this.elements]; + } + add(...args) { + this._each('register', args); + } + remove(...args) { + this._each('unregister', args); + } + addControllers(...args) { + this._each('register', args, this.controllers); + } + addElements(...args) { + this._each('register', args, this.elements); + } + addPlugins(...args) { + this._each('register', args, this.plugins); + } + addScales(...args) { + this._each('register', args, this.scales); + } + getController(id) { + return this._get(id, this.controllers, 'controller'); + } + getElement(id) { + return this._get(id, this.elements, 'element'); + } + getPlugin(id) { + return this._get(id, this.plugins, 'plugin'); + } + getScale(id) { + return this._get(id, this.scales, 'scale'); + } + removeControllers(...args) { + this._each('unregister', args, this.controllers); + } + removeElements(...args) { + this._each('unregister', args, this.elements); + } + removePlugins(...args) { + this._each('unregister', args, this.plugins); + } + removeScales(...args) { + this._each('unregister', args, this.scales); + } + _each(method, args, typedRegistry) { + const me = this; + [...args].forEach(arg => { + const reg = typedRegistry || me._getRegistryForType(arg); + if (typedRegistry || reg.isForType(arg) || (reg === me.plugins && arg.id)) { + me._exec(method, reg, arg); + } else { + each(arg, item => { + const itemReg = typedRegistry || me._getRegistryForType(item); + me._exec(method, itemReg, item); + }); + } + }); + } + _exec(method, registry, component) { + const camelMethod = _capitalize(method); + callback(component['before' + camelMethod], [], component); + registry[method](component); + callback(component['after' + camelMethod], [], component); + } + _getRegistryForType(type) { + for (let i = 0; i < this._typedRegistries.length; i++) { + const reg = this._typedRegistries[i]; + if (reg.isForType(type)) { + return reg; + } + } + return this.plugins; + } + _get(id, typedRegistry, type) { + const item = typedRegistry.get(id); + if (item === undefined) { + throw new Error('"' + id + '" is not a registered ' + type + '.'); + } + return item; + } +} +var registry = new Registry(); + +class PluginService { + constructor() { + this._init = []; + } + notify(chart, hook, args, filter) { + const me = this; + if (hook === 'beforeInit') { + me._init = me._createDescriptors(chart, true); + me._notify(me._init, chart, 'install'); + } + const descriptors = filter ? me._descriptors(chart).filter(filter) : me._descriptors(chart); + const result = me._notify(descriptors, chart, hook, args); + if (hook === 'destroy') { + me._notify(descriptors, chart, 'stop'); + me._notify(me._init, chart, 'uninstall'); + } + return result; + } + _notify(descriptors, chart, hook, args) { + args = args || {}; + for (const descriptor of descriptors) { + const plugin = descriptor.plugin; + const method = plugin[hook]; + const params = [chart, args, descriptor.options]; + if (callback(method, params, plugin) === false && args.cancelable) { + return false; + } + } + return true; + } + invalidate() { + if (!isNullOrUndef(this._cache)) { + this._oldCache = this._cache; + this._cache = undefined; + } + } + _descriptors(chart) { + if (this._cache) { + return this._cache; + } + const descriptors = this._cache = this._createDescriptors(chart); + this._notifyStateChanges(chart); + return descriptors; + } + _createDescriptors(chart, all) { + const config = chart && chart.config; + const options = valueOrDefault(config.options && config.options.plugins, {}); + const plugins = allPlugins(config); + return options === false && !all ? [] : createDescriptors(chart, plugins, options, all); + } + _notifyStateChanges(chart) { + const previousDescriptors = this._oldCache || []; + const descriptors = this._cache; + const diff = (a, b) => a.filter(x => !b.some(y => x.plugin.id === y.plugin.id)); + this._notify(diff(previousDescriptors, descriptors), chart, 'stop'); + this._notify(diff(descriptors, previousDescriptors), chart, 'start'); + } +} +function allPlugins(config) { + const plugins = []; + const keys = Object.keys(registry.plugins.items); + for (let i = 0; i < keys.length; i++) { + plugins.push(registry.getPlugin(keys[i])); + } + const local = config.plugins || []; + for (let i = 0; i < local.length; i++) { + const plugin = local[i]; + if (plugins.indexOf(plugin) === -1) { + plugins.push(plugin); + } + } + return plugins; +} +function getOpts(options, all) { + if (!all && options === false) { + return null; + } + if (options === true) { + return {}; + } + return options; +} +function createDescriptors(chart, plugins, options, all) { + const result = []; + const context = chart.getContext(); + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + const id = plugin.id; + const opts = getOpts(options[id], all); + if (opts === null) { + continue; + } + result.push({ + plugin, + options: pluginOpts(chart.config, plugin, opts, context) + }); + } + return result; +} +function pluginOpts(config, plugin, opts, context) { + const keys = config.pluginScopeKeys(plugin); + const scopes = config.getOptionScopes(opts, keys); + return config.createResolver(scopes, context, [''], {scriptable: false, indexable: false, allKeys: true}); +} + +function getIndexAxis(type, options) { + const datasetDefaults = defaults.datasets[type] || {}; + const datasetOptions = (options.datasets || {})[type] || {}; + return datasetOptions.indexAxis || options.indexAxis || datasetDefaults.indexAxis || 'x'; +} +function getAxisFromDefaultScaleID(id, indexAxis) { + let axis = id; + if (id === '_index_') { + axis = indexAxis; + } else if (id === '_value_') { + axis = indexAxis === 'x' ? 'y' : 'x'; + } + return axis; +} +function getDefaultScaleIDFromAxis(axis, indexAxis) { + return axis === indexAxis ? '_index_' : '_value_'; +} +function axisFromPosition(position) { + if (position === 'top' || position === 'bottom') { + return 'x'; + } + if (position === 'left' || position === 'right') { + return 'y'; + } +} +function determineAxis(id, scaleOptions) { + if (id === 'x' || id === 'y') { + return id; + } + return scaleOptions.axis || axisFromPosition(scaleOptions.position) || id.charAt(0).toLowerCase(); +} +function mergeScaleConfig(config, options) { + const chartDefaults = overrides[config.type] || {scales: {}}; + const configScales = options.scales || {}; + const chartIndexAxis = getIndexAxis(config.type, options); + const firstIDs = Object.create(null); + const scales = Object.create(null); + Object.keys(configScales).forEach(id => { + const scaleConf = configScales[id]; + const axis = determineAxis(id, scaleConf); + const defaultId = getDefaultScaleIDFromAxis(axis, chartIndexAxis); + const defaultScaleOptions = chartDefaults.scales || {}; + firstIDs[axis] = firstIDs[axis] || id; + scales[id] = mergeIf(Object.create(null), [{axis}, scaleConf, defaultScaleOptions[axis], defaultScaleOptions[defaultId]]); + }); + config.data.datasets.forEach(dataset => { + const type = dataset.type || config.type; + const indexAxis = dataset.indexAxis || getIndexAxis(type, options); + const datasetDefaults = overrides[type] || {}; + const defaultScaleOptions = datasetDefaults.scales || {}; + Object.keys(defaultScaleOptions).forEach(defaultID => { + const axis = getAxisFromDefaultScaleID(defaultID, indexAxis); + const id = dataset[axis + 'AxisID'] || firstIDs[axis] || axis; + scales[id] = scales[id] || Object.create(null); + mergeIf(scales[id], [{axis}, configScales[id], defaultScaleOptions[defaultID]]); + }); + }); + Object.keys(scales).forEach(key => { + const scale = scales[key]; + mergeIf(scale, [defaults.scales[scale.type], defaults.scale]); + }); + return scales; +} +function initOptions(config) { + const options = config.options || (config.options = {}); + options.plugins = valueOrDefault(options.plugins, {}); + options.scales = mergeScaleConfig(config, options); +} +function initData(data) { + data = data || {}; + data.datasets = data.datasets || []; + data.labels = data.labels || []; + return data; +} +function initConfig(config) { + config = config || {}; + config.data = initData(config.data); + initOptions(config); + return config; +} +const keyCache = new Map(); +const keysCached = new Set(); +function cachedKeys(cacheKey, generate) { + let keys = keyCache.get(cacheKey); + if (!keys) { + keys = generate(); + keyCache.set(cacheKey, keys); + keysCached.add(keys); + } + return keys; +} +const addIfFound = (set, obj, key) => { + const opts = resolveObjectKey(obj, key); + if (opts !== undefined) { + set.add(opts); + } +}; +class Config { + constructor(config) { + this._config = initConfig(config); + this._scopeCache = new Map(); + this._resolverCache = new Map(); + } + get type() { + return this._config.type; + } + set type(type) { + this._config.type = type; + } + get data() { + return this._config.data; + } + set data(data) { + this._config.data = initData(data); + } + get options() { + return this._config.options; + } + set options(options) { + this._config.options = options; + } + get plugins() { + return this._config.plugins; + } + update() { + const config = this._config; + this.clearCache(); + initOptions(config); + } + clearCache() { + this._scopeCache.clear(); + this._resolverCache.clear(); + } + datasetScopeKeys(datasetType) { + return cachedKeys(datasetType, + () => [[ + `datasets.${datasetType}`, + '' + ]]); + } + datasetAnimationScopeKeys(datasetType, transition) { + return cachedKeys(`${datasetType}.transition.${transition}`, + () => [ + [ + `datasets.${datasetType}.transitions.${transition}`, + `transitions.${transition}`, + ], + [ + `datasets.${datasetType}`, + '' + ] + ]); + } + datasetElementScopeKeys(datasetType, elementType) { + return cachedKeys(`${datasetType}-${elementType}`, + () => [[ + `datasets.${datasetType}.elements.${elementType}`, + `datasets.${datasetType}`, + `elements.${elementType}`, + '' + ]]); + } + pluginScopeKeys(plugin) { + const id = plugin.id; + const type = this.type; + return cachedKeys(`${type}-plugin-${id}`, + () => [[ + `plugins.${id}`, + ...plugin.additionalOptionScopes || [], + ]]); + } + _cachedScopes(mainScope, resetCache) { + const _scopeCache = this._scopeCache; + let cache = _scopeCache.get(mainScope); + if (!cache || resetCache) { + cache = new Map(); + _scopeCache.set(mainScope, cache); + } + return cache; + } + getOptionScopes(mainScope, keyLists, resetCache) { + const {options, type} = this; + const cache = this._cachedScopes(mainScope, resetCache); + const cached = cache.get(keyLists); + if (cached) { + return cached; + } + const scopes = new Set(); + keyLists.forEach(keys => { + if (mainScope) { + scopes.add(mainScope); + keys.forEach(key => addIfFound(scopes, mainScope, key)); + } + keys.forEach(key => addIfFound(scopes, options, key)); + keys.forEach(key => addIfFound(scopes, overrides[type] || {}, key)); + keys.forEach(key => addIfFound(scopes, defaults, key)); + keys.forEach(key => addIfFound(scopes, descriptors, key)); + }); + const array = Array.from(scopes); + if (keysCached.has(keyLists)) { + cache.set(keyLists, array); + } + return array; + } + chartOptionScopes() { + const {options, type} = this; + return [ + options, + overrides[type] || {}, + defaults.datasets[type] || {}, + {type}, + defaults, + descriptors + ]; + } + resolveNamedOptions(scopes, names, context, prefixes = ['']) { + const result = {$shared: true}; + const {resolver, subPrefixes} = getResolver(this._resolverCache, scopes, prefixes); + let options = resolver; + if (needContext(resolver, names)) { + result.$shared = false; + context = isFunction(context) ? context() : context; + const subResolver = this.createResolver(scopes, context, subPrefixes); + options = _attachContext(resolver, context, subResolver); + } + for (const prop of names) { + result[prop] = options[prop]; + } + return result; + } + createResolver(scopes, context, prefixes = [''], descriptorDefaults) { + const {resolver} = getResolver(this._resolverCache, scopes, prefixes); + return isObject(context) + ? _attachContext(resolver, context, undefined, descriptorDefaults) + : resolver; + } +} +function getResolver(resolverCache, scopes, prefixes) { + let cache = resolverCache.get(scopes); + if (!cache) { + cache = new Map(); + resolverCache.set(scopes, cache); + } + const cacheKey = prefixes.join(); + let cached = cache.get(cacheKey); + if (!cached) { + const resolver = _createResolver(scopes, prefixes); + cached = { + resolver, + subPrefixes: prefixes.filter(p => !p.toLowerCase().includes('hover')) + }; + cache.set(cacheKey, cached); + } + return cached; +} +function needContext(proxy, names) { + const {isScriptable, isIndexable} = _descriptors(proxy); + for (const prop of names) { + if ((isScriptable(prop) && isFunction(proxy[prop])) + || (isIndexable(prop) && isArray(proxy[prop]))) { + return true; + } + } + return false; +} + +var version = "3.4.1"; + +const KNOWN_POSITIONS = ['top', 'bottom', 'left', 'right', 'chartArea']; +function positionIsHorizontal(position, axis) { + return position === 'top' || position === 'bottom' || (KNOWN_POSITIONS.indexOf(position) === -1 && axis === 'x'); +} +function compare2Level(l1, l2) { + return function(a, b) { + return a[l1] === b[l1] + ? a[l2] - b[l2] + : a[l1] - b[l1]; + }; +} +function onAnimationsComplete(context) { + const chart = context.chart; + const animationOptions = chart.options.animation; + chart.notifyPlugins('afterRender'); + callback(animationOptions && animationOptions.onComplete, [context], chart); +} +function onAnimationProgress(context) { + const chart = context.chart; + const animationOptions = chart.options.animation; + callback(animationOptions && animationOptions.onProgress, [context], chart); +} +function isDomSupported() { + return typeof window !== 'undefined' && typeof document !== 'undefined'; +} +function getCanvas(item) { + if (isDomSupported() && typeof item === 'string') { + item = document.getElementById(item); + } else if (item && item.length) { + item = item[0]; + } + if (item && item.canvas) { + item = item.canvas; + } + return item; +} +const instances = {}; +const getChart = (key) => { + const canvas = getCanvas(key); + return Object.values(instances).filter((c) => c.canvas === canvas).pop(); +}; +class Chart { + constructor(item, config) { + const me = this; + this.config = config = new Config(config); + const initialCanvas = getCanvas(item); + const existingChart = getChart(initialCanvas); + if (existingChart) { + throw new Error( + 'Canvas is already in use. Chart with ID \'' + existingChart.id + '\'' + + ' must be destroyed before the canvas can be reused.' + ); + } + const options = config.createResolver(config.chartOptionScopes(), me.getContext()); + this.platform = me._initializePlatform(initialCanvas, config); + const context = me.platform.acquireContext(initialCanvas, options.aspectRatio); + const canvas = context && context.canvas; + const height = canvas && canvas.height; + const width = canvas && canvas.width; + this.id = uid(); + this.ctx = context; + this.canvas = canvas; + this.width = width; + this.height = height; + this._options = options; + this._aspectRatio = this.aspectRatio; + this._layers = []; + this._metasets = []; + this._stacks = undefined; + this.boxes = []; + this.currentDevicePixelRatio = undefined; + this.chartArea = undefined; + this._active = []; + this._lastEvent = undefined; + this._listeners = {}; + this._responsiveListeners = undefined; + this._sortedMetasets = []; + this.scales = {}; + this.scale = undefined; + this._plugins = new PluginService(); + this.$proxies = {}; + this._hiddenIndices = {}; + this.attached = false; + this._animationsDisabled = undefined; + this.$context = undefined; + this._doResize = debounce(() => this.update('resize'), options.resizeDelay || 0); + instances[me.id] = me; + if (!context || !canvas) { + console.error("Failed to create chart: can't acquire context from the given item"); + return; + } + animator.listen(me, 'complete', onAnimationsComplete); + animator.listen(me, 'progress', onAnimationProgress); + me._initialize(); + if (me.attached) { + me.update(); + } + } + get aspectRatio() { + const {options: {aspectRatio, maintainAspectRatio}, width, height, _aspectRatio} = this; + if (!isNullOrUndef(aspectRatio)) { + return aspectRatio; + } + if (maintainAspectRatio && _aspectRatio) { + return _aspectRatio; + } + return height ? width / height : null; + } + get data() { + return this.config.data; + } + set data(data) { + this.config.data = data; + } + get options() { + return this._options; + } + set options(options) { + this.config.options = options; + } + _initialize() { + const me = this; + me.notifyPlugins('beforeInit'); + if (me.options.responsive) { + me.resize(); + } else { + retinaScale(me, me.options.devicePixelRatio); + } + me.bindEvents(); + me.notifyPlugins('afterInit'); + return me; + } + _initializePlatform(canvas, config) { + if (config.platform) { + return new config.platform(); + } else if (!isDomSupported() || (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas)) { + return new BasicPlatform(); + } + return new DomPlatform(); + } + clear() { + clearCanvas(this.canvas, this.ctx); + return this; + } + stop() { + animator.stop(this); + return this; + } + resize(width, height) { + if (!animator.running(this)) { + this._resize(width, height); + } else { + this._resizeBeforeDraw = {width, height}; + } + } + _resize(width, height) { + const me = this; + const options = me.options; + const canvas = me.canvas; + const aspectRatio = options.maintainAspectRatio && me.aspectRatio; + const newSize = me.platform.getMaximumSize(canvas, width, height, aspectRatio); + const newRatio = options.devicePixelRatio || me.platform.getDevicePixelRatio(); + me.width = newSize.width; + me.height = newSize.height; + me._aspectRatio = me.aspectRatio; + if (!retinaScale(me, newRatio, true)) { + return; + } + me.notifyPlugins('resize', {size: newSize}); + callback(options.onResize, [me, newSize], me); + if (me.attached) { + if (me._doResize()) { + me.render(); + } + } + } + ensureScalesHaveIDs() { + const options = this.options; + const scalesOptions = options.scales || {}; + each(scalesOptions, (axisOptions, axisID) => { + axisOptions.id = axisID; + }); + } + buildOrUpdateScales() { + const me = this; + const options = me.options; + const scaleOpts = options.scales; + const scales = me.scales; + const updated = Object.keys(scales).reduce((obj, id) => { + obj[id] = false; + return obj; + }, {}); + let items = []; + if (scaleOpts) { + items = items.concat( + Object.keys(scaleOpts).map((id) => { + const scaleOptions = scaleOpts[id]; + const axis = determineAxis(id, scaleOptions); + const isRadial = axis === 'r'; + const isHorizontal = axis === 'x'; + return { + options: scaleOptions, + dposition: isRadial ? 'chartArea' : isHorizontal ? 'bottom' : 'left', + dtype: isRadial ? 'radialLinear' : isHorizontal ? 'category' : 'linear' + }; + }) + ); + } + each(items, (item) => { + const scaleOptions = item.options; + const id = scaleOptions.id; + const axis = determineAxis(id, scaleOptions); + const scaleType = valueOrDefault(scaleOptions.type, item.dtype); + if (scaleOptions.position === undefined || positionIsHorizontal(scaleOptions.position, axis) !== positionIsHorizontal(item.dposition)) { + scaleOptions.position = item.dposition; + } + updated[id] = true; + let scale = null; + if (id in scales && scales[id].type === scaleType) { + scale = scales[id]; + } else { + const scaleClass = registry.getScale(scaleType); + scale = new scaleClass({ + id, + type: scaleType, + ctx: me.ctx, + chart: me + }); + scales[scale.id] = scale; + } + scale.init(scaleOptions, options); + }); + each(updated, (hasUpdated, id) => { + if (!hasUpdated) { + delete scales[id]; + } + }); + each(scales, (scale) => { + layouts.configure(me, scale, scale.options); + layouts.addBox(me, scale); + }); + } + _updateMetasets() { + const me = this; + const metasets = me._metasets; + const numData = me.data.datasets.length; + const numMeta = metasets.length; + metasets.sort((a, b) => a.index - b.index); + if (numMeta > numData) { + for (let i = numData; i < numMeta; ++i) { + me._destroyDatasetMeta(i); + } + metasets.splice(numData, numMeta - numData); + } + me._sortedMetasets = metasets.slice(0).sort(compare2Level('order', 'index')); + } + _removeUnreferencedMetasets() { + const me = this; + const {_metasets: metasets, data: {datasets}} = me; + if (metasets.length > datasets.length) { + delete me._stacks; + } + metasets.forEach((meta, index) => { + if (datasets.filter(x => x === meta._dataset).length === 0) { + me._destroyDatasetMeta(index); + } + }); + } + buildOrUpdateControllers() { + const me = this; + const newControllers = []; + const datasets = me.data.datasets; + let i, ilen; + me._removeUnreferencedMetasets(); + for (i = 0, ilen = datasets.length; i < ilen; i++) { + const dataset = datasets[i]; + let meta = me.getDatasetMeta(i); + const type = dataset.type || me.config.type; + if (meta.type && meta.type !== type) { + me._destroyDatasetMeta(i); + meta = me.getDatasetMeta(i); + } + meta.type = type; + meta.indexAxis = dataset.indexAxis || getIndexAxis(type, me.options); + meta.order = dataset.order || 0; + meta.index = i; + meta.label = '' + dataset.label; + meta.visible = me.isDatasetVisible(i); + if (meta.controller) { + meta.controller.updateIndex(i); + meta.controller.linkScales(); + } else { + const ControllerClass = registry.getController(type); + const {datasetElementType, dataElementType} = defaults.datasets[type]; + Object.assign(ControllerClass.prototype, { + dataElementType: registry.getElement(dataElementType), + datasetElementType: datasetElementType && registry.getElement(datasetElementType) + }); + meta.controller = new ControllerClass(me, i); + newControllers.push(meta.controller); + } + } + me._updateMetasets(); + return newControllers; + } + _resetElements() { + const me = this; + each(me.data.datasets, (dataset, datasetIndex) => { + me.getDatasetMeta(datasetIndex).controller.reset(); + }, me); + } + reset() { + this._resetElements(); + this.notifyPlugins('reset'); + } + update(mode) { + const me = this; + const config = me.config; + config.update(); + me._options = config.createResolver(config.chartOptionScopes(), me.getContext()); + each(me.scales, (scale) => { + layouts.removeBox(me, scale); + }); + const animsDisabled = me._animationsDisabled = !me.options.animation; + me.ensureScalesHaveIDs(); + me.buildOrUpdateScales(); + const existingEvents = new Set(Object.keys(me._listeners)); + const newEvents = new Set(me.options.events); + if (!setsEqual(existingEvents, newEvents) || !!this._responsiveListeners !== me.options.responsive) { + me.unbindEvents(); + me.bindEvents(); + } + me._plugins.invalidate(); + if (me.notifyPlugins('beforeUpdate', {mode, cancelable: true}) === false) { + return; + } + const newControllers = me.buildOrUpdateControllers(); + me.notifyPlugins('beforeElementsUpdate'); + let minPadding = 0; + for (let i = 0, ilen = me.data.datasets.length; i < ilen; i++) { + const {controller} = me.getDatasetMeta(i); + const reset = !animsDisabled && newControllers.indexOf(controller) === -1; + controller.buildOrUpdateElements(reset); + minPadding = Math.max(+controller.getMaxOverflow(), minPadding); + } + me._minPadding = minPadding; + me._updateLayout(minPadding); + if (!animsDisabled) { + each(newControllers, (controller) => { + controller.reset(); + }); + } + me._updateDatasets(mode); + me.notifyPlugins('afterUpdate', {mode}); + me._layers.sort(compare2Level('z', '_idx')); + if (me._lastEvent) { + me._eventHandler(me._lastEvent, true); + } + me.render(); + } + _updateLayout(minPadding) { + const me = this; + if (me.notifyPlugins('beforeLayout', {cancelable: true}) === false) { + return; + } + layouts.update(me, me.width, me.height, minPadding); + const area = me.chartArea; + const noArea = area.width <= 0 || area.height <= 0; + me._layers = []; + each(me.boxes, (box) => { + if (noArea && box.position === 'chartArea') { + return; + } + if (box.configure) { + box.configure(); + } + me._layers.push(...box._layers()); + }, me); + me._layers.forEach((item, index) => { + item._idx = index; + }); + me.notifyPlugins('afterLayout'); + } + _updateDatasets(mode) { + const me = this; + const isFunction = typeof mode === 'function'; + if (me.notifyPlugins('beforeDatasetsUpdate', {mode, cancelable: true}) === false) { + return; + } + for (let i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + me._updateDataset(i, isFunction ? mode({datasetIndex: i}) : mode); + } + me.notifyPlugins('afterDatasetsUpdate', {mode}); + } + _updateDataset(index, mode) { + const me = this; + const meta = me.getDatasetMeta(index); + const args = {meta, index, mode, cancelable: true}; + if (me.notifyPlugins('beforeDatasetUpdate', args) === false) { + return; + } + meta.controller._update(mode); + args.cancelable = false; + me.notifyPlugins('afterDatasetUpdate', args); + } + render() { + const me = this; + if (me.notifyPlugins('beforeRender', {cancelable: true}) === false) { + return; + } + if (animator.has(me)) { + if (me.attached && !animator.running(me)) { + animator.start(me); + } + } else { + me.draw(); + onAnimationsComplete({chart: me}); + } + } + draw() { + const me = this; + let i; + if (me._resizeBeforeDraw) { + const {width, height} = me._resizeBeforeDraw; + me._resize(width, height); + me._resizeBeforeDraw = null; + } + me.clear(); + if (me.width <= 0 || me.height <= 0) { + return; + } + if (me.notifyPlugins('beforeDraw', {cancelable: true}) === false) { + return; + } + const layers = me._layers; + for (i = 0; i < layers.length && layers[i].z <= 0; ++i) { + layers[i].draw(me.chartArea); + } + me._drawDatasets(); + for (; i < layers.length; ++i) { + layers[i].draw(me.chartArea); + } + me.notifyPlugins('afterDraw'); + } + _getSortedDatasetMetas(filterVisible) { + const me = this; + const metasets = me._sortedMetasets; + const result = []; + let i, ilen; + for (i = 0, ilen = metasets.length; i < ilen; ++i) { + const meta = metasets[i]; + if (!filterVisible || meta.visible) { + result.push(meta); + } + } + return result; + } + getSortedVisibleDatasetMetas() { + return this._getSortedDatasetMetas(true); + } + _drawDatasets() { + const me = this; + if (me.notifyPlugins('beforeDatasetsDraw', {cancelable: true}) === false) { + return; + } + const metasets = me.getSortedVisibleDatasetMetas(); + for (let i = metasets.length - 1; i >= 0; --i) { + me._drawDataset(metasets[i]); + } + me.notifyPlugins('afterDatasetsDraw'); + } + _drawDataset(meta) { + const me = this; + const ctx = me.ctx; + const clip = meta._clip; + const useClip = !clip.disabled; + const area = me.chartArea; + const args = { + meta, + index: meta.index, + cancelable: true + }; + if (me.notifyPlugins('beforeDatasetDraw', args) === false) { + return; + } + if (useClip) { + clipArea(ctx, { + left: clip.left === false ? 0 : area.left - clip.left, + right: clip.right === false ? me.width : area.right + clip.right, + top: clip.top === false ? 0 : area.top - clip.top, + bottom: clip.bottom === false ? me.height : area.bottom + clip.bottom + }); + } + meta.controller.draw(); + if (useClip) { + unclipArea(ctx); + } + args.cancelable = false; + me.notifyPlugins('afterDatasetDraw', args); + } + getElementsAtEventForMode(e, mode, options, useFinalPosition) { + const method = Interaction.modes[mode]; + if (typeof method === 'function') { + return method(this, e, options, useFinalPosition); + } + return []; + } + getDatasetMeta(datasetIndex) { + const me = this; + const dataset = me.data.datasets[datasetIndex]; + const metasets = me._metasets; + let meta = metasets.filter(x => x && x._dataset === dataset).pop(); + if (!meta) { + meta = { + type: null, + data: [], + dataset: null, + controller: null, + hidden: null, + xAxisID: null, + yAxisID: null, + order: dataset && dataset.order || 0, + index: datasetIndex, + _dataset: dataset, + _parsed: [], + _sorted: false + }; + metasets.push(meta); + } + return meta; + } + getContext() { + return this.$context || (this.$context = {chart: this, type: 'chart'}); + } + getVisibleDatasetCount() { + return this.getSortedVisibleDatasetMetas().length; + } + isDatasetVisible(datasetIndex) { + const dataset = this.data.datasets[datasetIndex]; + if (!dataset) { + return false; + } + const meta = this.getDatasetMeta(datasetIndex); + return typeof meta.hidden === 'boolean' ? !meta.hidden : !dataset.hidden; + } + setDatasetVisibility(datasetIndex, visible) { + const meta = this.getDatasetMeta(datasetIndex); + meta.hidden = !visible; + } + toggleDataVisibility(index) { + this._hiddenIndices[index] = !this._hiddenIndices[index]; + } + getDataVisibility(index) { + return !this._hiddenIndices[index]; + } + _updateDatasetVisibility(datasetIndex, visible) { + const me = this; + const mode = visible ? 'show' : 'hide'; + const meta = me.getDatasetMeta(datasetIndex); + const anims = meta.controller._resolveAnimations(undefined, mode); + me.setDatasetVisibility(datasetIndex, visible); + anims.update(meta, {visible}); + me.update((ctx) => ctx.datasetIndex === datasetIndex ? mode : undefined); + } + hide(datasetIndex) { + this._updateDatasetVisibility(datasetIndex, false); + } + show(datasetIndex) { + this._updateDatasetVisibility(datasetIndex, true); + } + _destroyDatasetMeta(datasetIndex) { + const me = this; + const meta = me._metasets && me._metasets[datasetIndex]; + if (meta && meta.controller) { + meta.controller._destroy(); + delete me._metasets[datasetIndex]; + } + } + destroy() { + const me = this; + const {canvas, ctx} = me; + let i, ilen; + me.stop(); + animator.remove(me); + for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + me._destroyDatasetMeta(i); + } + me.config.clearCache(); + if (canvas) { + me.unbindEvents(); + clearCanvas(canvas, ctx); + me.platform.releaseContext(ctx); + me.canvas = null; + me.ctx = null; + } + me.notifyPlugins('destroy'); + delete instances[me.id]; + } + toBase64Image(...args) { + return this.canvas.toDataURL(...args); + } + bindEvents() { + this.bindUserEvents(); + if (this.options.responsive) { + this.bindResponsiveEvents(); + } else { + this.attached = true; + } + } + bindUserEvents() { + const me = this; + const listeners = me._listeners; + const platform = me.platform; + const _add = (type, listener) => { + platform.addEventListener(me, type, listener); + listeners[type] = listener; + }; + const listener = function(e, x, y) { + e.offsetX = x; + e.offsetY = y; + me._eventHandler(e); + }; + each(me.options.events, (type) => _add(type, listener)); + } + bindResponsiveEvents() { + const me = this; + if (!me._responsiveListeners) { + me._responsiveListeners = {}; + } + const listeners = me._responsiveListeners; + const platform = me.platform; + const _add = (type, listener) => { + platform.addEventListener(me, type, listener); + listeners[type] = listener; + }; + const _remove = (type, listener) => { + if (listeners[type]) { + platform.removeEventListener(me, type, listener); + delete listeners[type]; + } + }; + const listener = (width, height) => { + if (me.canvas) { + me.resize(width, height); + } + }; + let detached; + const attached = () => { + _remove('attach', attached); + me.attached = true; + me.resize(); + _add('resize', listener); + _add('detach', detached); + }; + detached = () => { + me.attached = false; + _remove('resize', listener); + _add('attach', attached); + }; + if (platform.isAttached(me.canvas)) { + attached(); + } else { + detached(); + } + } + unbindEvents() { + const me = this; + each(me._listeners, (listener, type) => { + me.platform.removeEventListener(me, type, listener); + }); + me._listeners = {}; + each(me._responsiveListeners, (listener, type) => { + me.platform.removeEventListener(me, type, listener); + }); + me._responsiveListeners = undefined; + } + updateHoverStyle(items, mode, enabled) { + const prefix = enabled ? 'set' : 'remove'; + let meta, item, i, ilen; + if (mode === 'dataset') { + meta = this.getDatasetMeta(items[0].datasetIndex); + meta.controller['_' + prefix + 'DatasetHoverStyle'](); + } + for (i = 0, ilen = items.length; i < ilen; ++i) { + item = items[i]; + const controller = item && this.getDatasetMeta(item.datasetIndex).controller; + if (controller) { + controller[prefix + 'HoverStyle'](item.element, item.datasetIndex, item.index); + } + } + } + getActiveElements() { + return this._active || []; + } + setActiveElements(activeElements) { + const me = this; + const lastActive = me._active || []; + const active = activeElements.map(({datasetIndex, index}) => { + const meta = me.getDatasetMeta(datasetIndex); + if (!meta) { + throw new Error('No dataset found at index ' + datasetIndex); + } + return { + datasetIndex, + element: meta.data[index], + index, + }; + }); + const changed = !_elementsEqual(active, lastActive); + if (changed) { + me._active = active; + me._updateHoverStyles(active, lastActive); + } + } + notifyPlugins(hook, args, filter) { + return this._plugins.notify(this, hook, args, filter); + } + _updateHoverStyles(active, lastActive, replay) { + const me = this; + const hoverOptions = me.options.hover; + const diff = (a, b) => a.filter(x => !b.some(y => x.datasetIndex === y.datasetIndex && x.index === y.index)); + const deactivated = diff(lastActive, active); + const activated = replay ? active : diff(active, lastActive); + if (deactivated.length) { + me.updateHoverStyle(deactivated, hoverOptions.mode, false); + } + if (activated.length && hoverOptions.mode) { + me.updateHoverStyle(activated, hoverOptions.mode, true); + } + } + _eventHandler(e, replay) { + const me = this; + const args = {event: e, replay, cancelable: true}; + const eventFilter = (plugin) => (plugin.options.events || this.options.events).includes(e.type); + if (me.notifyPlugins('beforeEvent', args, eventFilter) === false) { + return; + } + const changed = me._handleEvent(e, replay); + args.cancelable = false; + me.notifyPlugins('afterEvent', args, eventFilter); + if (changed || args.changed) { + me.render(); + } + return me; + } + _handleEvent(e, replay) { + const me = this; + const {_active: lastActive = [], options} = me; + const hoverOptions = options.hover; + const useFinalPosition = replay; + let active = []; + let changed = false; + let lastEvent = null; + if (e.type !== 'mouseout') { + active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions, useFinalPosition); + lastEvent = e.type === 'click' ? me._lastEvent : e; + } + me._lastEvent = null; + if (_isPointInArea(e, me.chartArea, me._minPadding)) { + callback(options.onHover, [e, active, me], me); + if (e.type === 'mouseup' || e.type === 'click' || e.type === 'contextmenu') { + callback(options.onClick, [e, active, me], me); + } + } + changed = !_elementsEqual(active, lastActive); + if (changed || replay) { + me._active = active; + me._updateHoverStyles(active, lastActive, replay); + } + me._lastEvent = lastEvent; + return changed; + } +} +const invalidatePlugins = () => each(Chart.instances, (chart) => chart._plugins.invalidate()); +const enumerable = true; +Object.defineProperties(Chart, { + defaults: { + enumerable, + value: defaults + }, + instances: { + enumerable, + value: instances + }, + overrides: { + enumerable, + value: overrides + }, + registry: { + enumerable, + value: registry + }, + version: { + enumerable, + value: version + }, + getChart: { + enumerable, + value: getChart + }, + register: { + enumerable, + value: (...items) => { + registry.add(...items); + invalidatePlugins(); + } + }, + unregister: { + enumerable, + value: (...items) => { + registry.remove(...items); + invalidatePlugins(); + } + } +}); + +function abstract() { + throw new Error('This method is not implemented: Check that a complete date adapter is provided.'); +} +class DateAdapter { + constructor(options) { + this.options = options || {}; + } + formats() { + return abstract(); + } + parse(value, format) { + return abstract(); + } + format(timestamp, format) { + return abstract(); + } + add(timestamp, amount, unit) { + return abstract(); + } + diff(a, b, unit) { + return abstract(); + } + startOf(timestamp, unit, weekday) { + return abstract(); + } + endOf(timestamp, unit) { + return abstract(); + } +} +DateAdapter.override = function(members) { + Object.assign(DateAdapter.prototype, members); +}; +var _adapters = { + _date: DateAdapter +}; + +function getAllScaleValues(scale) { + if (!scale._cache.$bar) { + const metas = scale.getMatchingVisibleMetas('bar'); + let values = []; + for (let i = 0, ilen = metas.length; i < ilen; i++) { + values = values.concat(metas[i].controller.getAllParsedValues(scale)); + } + scale._cache.$bar = _arrayUnique(values.sort((a, b) => a - b)); + } + return scale._cache.$bar; +} +function computeMinSampleSize(scale) { + const values = getAllScaleValues(scale); + let min = scale._length; + let i, ilen, curr, prev; + const updateMinAndPrev = () => { + if (curr === 32767 || curr === -32768) { + return; + } + if (defined(prev)) { + min = Math.min(min, Math.abs(curr - prev) || min); + } + prev = curr; + }; + for (i = 0, ilen = values.length; i < ilen; ++i) { + curr = scale.getPixelForValue(values[i]); + updateMinAndPrev(); + } + prev = undefined; + for (i = 0, ilen = scale.ticks.length; i < ilen; ++i) { + curr = scale.getPixelForTick(i); + updateMinAndPrev(); + } + return min; +} +function computeFitCategoryTraits(index, ruler, options, stackCount) { + const thickness = options.barThickness; + let size, ratio; + if (isNullOrUndef(thickness)) { + size = ruler.min * options.categoryPercentage; + ratio = options.barPercentage; + } else { + size = thickness * stackCount; + ratio = 1; + } + return { + chunk: size / stackCount, + ratio, + start: ruler.pixels[index] - (size / 2) + }; +} +function computeFlexCategoryTraits(index, ruler, options, stackCount) { + const pixels = ruler.pixels; + const curr = pixels[index]; + let prev = index > 0 ? pixels[index - 1] : null; + let next = index < pixels.length - 1 ? pixels[index + 1] : null; + const percent = options.categoryPercentage; + if (prev === null) { + prev = curr - (next === null ? ruler.end - ruler.start : next - curr); + } + if (next === null) { + next = curr + curr - prev; + } + const start = curr - (curr - Math.min(prev, next)) / 2 * percent; + const size = Math.abs(next - prev) / 2 * percent; + return { + chunk: size / stackCount, + ratio: options.barPercentage, + start + }; +} +function parseFloatBar(entry, item, vScale, i) { + const startValue = vScale.parse(entry[0], i); + const endValue = vScale.parse(entry[1], i); + const min = Math.min(startValue, endValue); + const max = Math.max(startValue, endValue); + let barStart = min; + let barEnd = max; + if (Math.abs(min) > Math.abs(max)) { + barStart = max; + barEnd = min; + } + item[vScale.axis] = barEnd; + item._custom = { + barStart, + barEnd, + start: startValue, + end: endValue, + min, + max + }; +} +function parseValue(entry, item, vScale, i) { + if (isArray(entry)) { + parseFloatBar(entry, item, vScale, i); + } else { + item[vScale.axis] = vScale.parse(entry, i); + } + return item; +} +function parseArrayOrPrimitive(meta, data, start, count) { + const iScale = meta.iScale; + const vScale = meta.vScale; + const labels = iScale.getLabels(); + const singleScale = iScale === vScale; + const parsed = []; + let i, ilen, item, entry; + for (i = start, ilen = start + count; i < ilen; ++i) { + entry = data[i]; + item = {}; + item[iScale.axis] = singleScale || iScale.parse(labels[i], i); + parsed.push(parseValue(entry, item, vScale, i)); + } + return parsed; +} +function isFloatBar(custom) { + return custom && custom.barStart !== undefined && custom.barEnd !== undefined; +} +class BarController extends DatasetController { + parsePrimitiveData(meta, data, start, count) { + return parseArrayOrPrimitive(meta, data, start, count); + } + parseArrayData(meta, data, start, count) { + return parseArrayOrPrimitive(meta, data, start, count); + } + parseObjectData(meta, data, start, count) { + const {iScale, vScale} = meta; + const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; + const iAxisKey = iScale.axis === 'x' ? xAxisKey : yAxisKey; + const vAxisKey = vScale.axis === 'x' ? xAxisKey : yAxisKey; + const parsed = []; + let i, ilen, item, obj; + for (i = start, ilen = start + count; i < ilen; ++i) { + obj = data[i]; + item = {}; + item[iScale.axis] = iScale.parse(resolveObjectKey(obj, iAxisKey), i); + parsed.push(parseValue(resolveObjectKey(obj, vAxisKey), item, vScale, i)); + } + return parsed; + } + updateRangeFromParsed(range, scale, parsed, stack) { + super.updateRangeFromParsed(range, scale, parsed, stack); + const custom = parsed._custom; + if (custom && scale === this._cachedMeta.vScale) { + range.min = Math.min(range.min, custom.min); + range.max = Math.max(range.max, custom.max); + } + } + getMaxOverflow() { + return 0; + } + getLabelAndValue(index) { + const me = this; + const meta = me._cachedMeta; + const {iScale, vScale} = meta; + const parsed = me.getParsed(index); + const custom = parsed._custom; + const value = isFloatBar(custom) + ? '[' + custom.start + ', ' + custom.end + ']' + : '' + vScale.getLabelForValue(parsed[vScale.axis]); + return { + label: '' + iScale.getLabelForValue(parsed[iScale.axis]), + value + }; + } + initialize() { + const me = this; + me.enableOptionSharing = true; + super.initialize(); + const meta = me._cachedMeta; + meta.stack = me.getDataset().stack; + } + update(mode) { + const me = this; + const meta = me._cachedMeta; + me.updateElements(meta.data, 0, meta.data.length, mode); + } + updateElements(bars, start, count, mode) { + const me = this; + const reset = mode === 'reset'; + const vScale = me._cachedMeta.vScale; + const base = vScale.getBasePixel(); + const horizontal = vScale.isHorizontal(); + const ruler = me._getRuler(); + const firstOpts = me.resolveDataElementOptions(start, mode); + const sharedOptions = me.getSharedOptions(firstOpts); + const includeOptions = me.includeOptions(mode, sharedOptions); + me.updateSharedOptions(sharedOptions, mode, firstOpts); + for (let i = start; i < start + count; i++) { + const parsed = me.getParsed(i); + const vpixels = reset || isNullOrUndef(parsed[vScale.axis]) ? {base, head: base} : me._calculateBarValuePixels(i); + const ipixels = me._calculateBarIndexPixels(i, ruler); + const stack = (parsed._stacks || {})[vScale.axis]; + const properties = { + horizontal, + base: vpixels.base, + enableBorderRadius: !stack || isFloatBar(parsed._custom) || (me.index === stack._top || me.index === stack._bottom), + x: horizontal ? vpixels.head : ipixels.center, + y: horizontal ? ipixels.center : vpixels.head, + height: horizontal ? ipixels.size : Math.abs(vpixels.size), + width: horizontal ? Math.abs(vpixels.size) : ipixels.size + }; + if (includeOptions) { + properties.options = sharedOptions || me.resolveDataElementOptions(i, bars[i].active ? 'active' : mode); + } + me.updateElement(bars[i], i, properties, mode); + } + } + _getStacks(last, dataIndex) { + const me = this; + const meta = me._cachedMeta; + const iScale = meta.iScale; + const metasets = iScale.getMatchingVisibleMetas(me._type); + const stacked = iScale.options.stacked; + const ilen = metasets.length; + const stacks = []; + let i, item; + for (i = 0; i < ilen; ++i) { + item = metasets[i]; + if (!item.controller.options.grouped) { + continue; + } + if (typeof dataIndex !== 'undefined') { + const val = item.controller.getParsed(dataIndex)[ + item.controller._cachedMeta.vScale.axis + ]; + if (isNullOrUndef(val) || isNaN(val)) { + continue; + } + } + if (stacked === false || stacks.indexOf(item.stack) === -1 || + (stacked === undefined && item.stack === undefined)) { + stacks.push(item.stack); + } + if (item.index === last) { + break; + } + } + if (!stacks.length) { + stacks.push(undefined); + } + return stacks; + } + _getStackCount(index) { + return this._getStacks(undefined, index).length; + } + _getStackIndex(datasetIndex, name, dataIndex) { + const stacks = this._getStacks(datasetIndex, dataIndex); + const index = (name !== undefined) + ? stacks.indexOf(name) + : -1; + return (index === -1) + ? stacks.length - 1 + : index; + } + _getRuler() { + const me = this; + const opts = me.options; + const meta = me._cachedMeta; + const iScale = meta.iScale; + const pixels = []; + let i, ilen; + for (i = 0, ilen = meta.data.length; i < ilen; ++i) { + pixels.push(iScale.getPixelForValue(me.getParsed(i)[iScale.axis], i)); + } + const barThickness = opts.barThickness; + const min = barThickness || computeMinSampleSize(iScale); + return { + min, + pixels, + start: iScale._startPixel, + end: iScale._endPixel, + stackCount: me._getStackCount(), + scale: iScale, + grouped: opts.grouped, + ratio: barThickness ? 1 : opts.categoryPercentage * opts.barPercentage + }; + } + _calculateBarValuePixels(index) { + const me = this; + const {vScale, _stacked} = me._cachedMeta; + const {base: baseValue, minBarLength} = me.options; + const parsed = me.getParsed(index); + const custom = parsed._custom; + const floating = isFloatBar(custom); + let value = parsed[vScale.axis]; + let start = 0; + let length = _stacked ? me.applyStack(vScale, parsed, _stacked) : value; + let head, size; + if (length !== value) { + start = length - value; + length = value; + } + if (floating) { + value = custom.barStart; + length = custom.barEnd - custom.barStart; + if (value !== 0 && sign(value) !== sign(custom.barEnd)) { + start = 0; + } + start += value; + } + const startValue = !isNullOrUndef(baseValue) && !floating ? baseValue : start; + let base = vScale.getPixelForValue(startValue); + if (this.chart.getDataVisibility(index)) { + head = vScale.getPixelForValue(start + length); + } else { + head = base; + } + size = head - base; + if (minBarLength !== undefined && Math.abs(size) < minBarLength) { + size = size < 0 ? -minBarLength : minBarLength; + if (value === 0) { + base -= size / 2; + } + head = base + size; + } + const actualBase = baseValue || 0; + if (base === vScale.getPixelForValue(actualBase)) { + const halfGrid = vScale.getLineWidthForValue(actualBase) / 2; + if (size > 0) { + base += halfGrid; + size -= halfGrid; + } else if (size < 0) { + base -= halfGrid; + size += halfGrid; + } + } + return { + size, + base, + head, + center: head + size / 2 + }; + } + _calculateBarIndexPixels(index, ruler) { + const me = this; + const scale = ruler.scale; + const options = me.options; + const skipNull = options.skipNull; + const maxBarThickness = valueOrDefault(options.maxBarThickness, Infinity); + let center, size; + if (ruler.grouped) { + const stackCount = skipNull ? me._getStackCount(index) : ruler.stackCount; + const range = options.barThickness === 'flex' + ? computeFlexCategoryTraits(index, ruler, options, stackCount) + : computeFitCategoryTraits(index, ruler, options, stackCount); + const stackIndex = me._getStackIndex(me.index, me._cachedMeta.stack, skipNull ? index : undefined); + center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + size = Math.min(maxBarThickness, range.chunk * range.ratio); + } else { + center = scale.getPixelForValue(me.getParsed(index)[scale.axis], index); + size = Math.min(maxBarThickness, ruler.min * ruler.ratio); + } + return { + base: center - size / 2, + head: center + size / 2, + center, + size + }; + } + draw() { + const me = this; + const meta = me._cachedMeta; + const vScale = meta.vScale; + const rects = meta.data; + const ilen = rects.length; + let i = 0; + for (; i < ilen; ++i) { + if (me.getParsed(i)[vScale.axis] !== null) { + rects[i].draw(me._ctx); + } + } + } +} +BarController.id = 'bar'; +BarController.defaults = { + datasetElementType: false, + dataElementType: 'bar', + categoryPercentage: 0.8, + barPercentage: 0.9, + grouped: true, + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'base', 'width', 'height'] + } + } +}; +BarController.overrides = { + interaction: { + mode: 'index' + }, + scales: { + _index_: { + type: 'category', + offset: true, + grid: { + offset: true + } + }, + _value_: { + type: 'linear', + beginAtZero: true, + } + } +}; + +class BubbleController extends DatasetController { + initialize() { + this.enableOptionSharing = true; + super.initialize(); + } + parseObjectData(meta, data, start, count) { + const {xScale, yScale} = meta; + const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; + const parsed = []; + let i, ilen, item; + for (i = start, ilen = start + count; i < ilen; ++i) { + item = data[i]; + parsed.push({ + x: xScale.parse(resolveObjectKey(item, xAxisKey), i), + y: yScale.parse(resolveObjectKey(item, yAxisKey), i), + _custom: item && item.r && +item.r + }); + } + return parsed; + } + getMaxOverflow() { + const {data, _parsed} = this._cachedMeta; + let max = 0; + for (let i = data.length - 1; i >= 0; --i) { + max = Math.max(max, data[i].size() / 2, _parsed[i]._custom); + } + return max > 0 && max; + } + getLabelAndValue(index) { + const me = this; + const meta = me._cachedMeta; + const {xScale, yScale} = meta; + const parsed = me.getParsed(index); + const x = xScale.getLabelForValue(parsed.x); + const y = yScale.getLabelForValue(parsed.y); + const r = parsed._custom; + return { + label: meta.label, + value: '(' + x + ', ' + y + (r ? ', ' + r : '') + ')' + }; + } + update(mode) { + const me = this; + const points = me._cachedMeta.data; + me.updateElements(points, 0, points.length, mode); + } + updateElements(points, start, count, mode) { + const me = this; + const reset = mode === 'reset'; + const {iScale, vScale} = me._cachedMeta; + const firstOpts = me.resolveDataElementOptions(start, mode); + const sharedOptions = me.getSharedOptions(firstOpts); + const includeOptions = me.includeOptions(mode, sharedOptions); + const iAxis = iScale.axis; + const vAxis = vScale.axis; + for (let i = start; i < start + count; i++) { + const point = points[i]; + const parsed = !reset && me.getParsed(i); + const properties = {}; + const iPixel = properties[iAxis] = reset ? iScale.getPixelForDecimal(0.5) : iScale.getPixelForValue(parsed[iAxis]); + const vPixel = properties[vAxis] = reset ? vScale.getBasePixel() : vScale.getPixelForValue(parsed[vAxis]); + properties.skip = isNaN(iPixel) || isNaN(vPixel); + if (includeOptions) { + properties.options = me.resolveDataElementOptions(i, point.active ? 'active' : mode); + if (reset) { + properties.options.radius = 0; + } + } + me.updateElement(point, i, properties, mode); + } + me.updateSharedOptions(sharedOptions, mode, firstOpts); + } + resolveDataElementOptions(index, mode) { + const parsed = this.getParsed(index); + let values = super.resolveDataElementOptions(index, mode); + if (values.$shared) { + values = Object.assign({}, values, {$shared: false}); + } + const radius = values.radius; + if (mode !== 'active') { + values.radius = 0; + } + values.radius += valueOrDefault(parsed && parsed._custom, radius); + return values; + } +} +BubbleController.id = 'bubble'; +BubbleController.defaults = { + datasetElementType: false, + dataElementType: 'point', + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'borderWidth', 'radius'] + } + } +}; +BubbleController.overrides = { + scales: { + x: { + type: 'linear' + }, + y: { + type: 'linear' + } + }, + plugins: { + tooltip: { + callbacks: { + title() { + return ''; + } + } + } + } +}; + +function getRatioAndOffset(rotation, circumference, cutout) { + let ratioX = 1; + let ratioY = 1; + let offsetX = 0; + let offsetY = 0; + if (circumference < TAU) { + const startAngle = rotation; + const endAngle = startAngle + circumference; + const startX = Math.cos(startAngle); + const startY = Math.sin(startAngle); + const endX = Math.cos(endAngle); + const endY = Math.sin(endAngle); + const calcMax = (angle, a, b) => _angleBetween(angle, startAngle, endAngle, true) ? 1 : Math.max(a, a * cutout, b, b * cutout); + const calcMin = (angle, a, b) => _angleBetween(angle, startAngle, endAngle, true) ? -1 : Math.min(a, a * cutout, b, b * cutout); + const maxX = calcMax(0, startX, endX); + const maxY = calcMax(HALF_PI, startY, endY); + const minX = calcMin(PI, startX, endX); + const minY = calcMin(PI + HALF_PI, startY, endY); + ratioX = (maxX - minX) / 2; + ratioY = (maxY - minY) / 2; + offsetX = -(maxX + minX) / 2; + offsetY = -(maxY + minY) / 2; + } + return {ratioX, ratioY, offsetX, offsetY}; +} +class DoughnutController extends DatasetController { + constructor(chart, datasetIndex) { + super(chart, datasetIndex); + this.enableOptionSharing = true; + this.innerRadius = undefined; + this.outerRadius = undefined; + this.offsetX = undefined; + this.offsetY = undefined; + } + linkScales() {} + parse(start, count) { + const data = this.getDataset().data; + const meta = this._cachedMeta; + let i, ilen; + for (i = start, ilen = start + count; i < ilen; ++i) { + meta._parsed[i] = +data[i]; + } + } + _getRotation() { + return toRadians(this.options.rotation - 90); + } + _getCircumference() { + return toRadians(this.options.circumference); + } + _getRotationExtents() { + let min = TAU; + let max = -TAU; + const me = this; + for (let i = 0; i < me.chart.data.datasets.length; ++i) { + if (me.chart.isDatasetVisible(i)) { + const controller = me.chart.getDatasetMeta(i).controller; + const rotation = controller._getRotation(); + const circumference = controller._getCircumference(); + min = Math.min(min, rotation); + max = Math.max(max, rotation + circumference); + } + } + return { + rotation: min, + circumference: max - min, + }; + } + update(mode) { + const me = this; + const chart = me.chart; + const {chartArea} = chart; + const meta = me._cachedMeta; + const arcs = meta.data; + const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs) + me.options.spacing; + const maxSize = Math.max((Math.min(chartArea.width, chartArea.height) - spacing) / 2, 0); + const cutout = Math.min(toPercentage(me.options.cutout, maxSize), 1); + const chartWeight = me._getRingWeight(me.index); + const {circumference, rotation} = me._getRotationExtents(); + const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(rotation, circumference, cutout); + const maxWidth = (chartArea.width - spacing) / ratioX; + const maxHeight = (chartArea.height - spacing) / ratioY; + const maxRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0); + const outerRadius = toDimension(me.options.radius, maxRadius); + const innerRadius = Math.max(outerRadius * cutout, 0); + const radiusLength = (outerRadius - innerRadius) / me._getVisibleDatasetWeightTotal(); + me.offsetX = offsetX * outerRadius; + me.offsetY = offsetY * outerRadius; + meta.total = me.calculateTotal(); + me.outerRadius = outerRadius - radiusLength * me._getRingWeightOffset(me.index); + me.innerRadius = Math.max(me.outerRadius - radiusLength * chartWeight, 0); + me.updateElements(arcs, 0, arcs.length, mode); + } + _circumference(i, reset) { + const me = this; + const opts = me.options; + const meta = me._cachedMeta; + const circumference = me._getCircumference(); + if ((reset && opts.animation.animateRotate) || !this.chart.getDataVisibility(i) || meta._parsed[i] === null) { + return 0; + } + return me.calculateCircumference(meta._parsed[i] * circumference / TAU); + } + updateElements(arcs, start, count, mode) { + const me = this; + const reset = mode === 'reset'; + const chart = me.chart; + const chartArea = chart.chartArea; + const opts = chart.options; + const animationOpts = opts.animation; + const centerX = (chartArea.left + chartArea.right) / 2; + const centerY = (chartArea.top + chartArea.bottom) / 2; + const animateScale = reset && animationOpts.animateScale; + const innerRadius = animateScale ? 0 : me.innerRadius; + const outerRadius = animateScale ? 0 : me.outerRadius; + const firstOpts = me.resolveDataElementOptions(start, mode); + const sharedOptions = me.getSharedOptions(firstOpts); + const includeOptions = me.includeOptions(mode, sharedOptions); + let startAngle = me._getRotation(); + let i; + for (i = 0; i < start; ++i) { + startAngle += me._circumference(i, reset); + } + for (i = start; i < start + count; ++i) { + const circumference = me._circumference(i, reset); + const arc = arcs[i]; + const properties = { + x: centerX + me.offsetX, + y: centerY + me.offsetY, + startAngle, + endAngle: startAngle + circumference, + circumference, + outerRadius, + innerRadius + }; + if (includeOptions) { + properties.options = sharedOptions || me.resolveDataElementOptions(i, arc.active ? 'active' : mode); + } + startAngle += circumference; + me.updateElement(arc, i, properties, mode); + } + me.updateSharedOptions(sharedOptions, mode, firstOpts); + } + calculateTotal() { + const meta = this._cachedMeta; + const metaData = meta.data; + let total = 0; + let i; + for (i = 0; i < metaData.length; i++) { + const value = meta._parsed[i]; + if (value !== null && !isNaN(value) && this.chart.getDataVisibility(i)) { + total += Math.abs(value); + } + } + return total; + } + calculateCircumference(value) { + const total = this._cachedMeta.total; + if (total > 0 && !isNaN(value)) { + return TAU * (Math.abs(value) / total); + } + return 0; + } + getLabelAndValue(index) { + const me = this; + const meta = me._cachedMeta; + const chart = me.chart; + const labels = chart.data.labels || []; + const value = formatNumber(meta._parsed[index], chart.options.locale); + return { + label: labels[index] || '', + value, + }; + } + getMaxBorderWidth(arcs) { + const me = this; + let max = 0; + const chart = me.chart; + let i, ilen, meta, controller, options; + if (!arcs) { + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + if (chart.isDatasetVisible(i)) { + meta = chart.getDatasetMeta(i); + arcs = meta.data; + controller = meta.controller; + if (controller !== me) { + controller.configure(); + } + break; + } + } + } + if (!arcs) { + return 0; + } + for (i = 0, ilen = arcs.length; i < ilen; ++i) { + options = controller.resolveDataElementOptions(i); + if (options.borderAlign !== 'inner') { + max = Math.max(max, options.borderWidth || 0, options.hoverBorderWidth || 0); + } + } + return max; + } + getMaxOffset(arcs) { + let max = 0; + for (let i = 0, ilen = arcs.length; i < ilen; ++i) { + const options = this.resolveDataElementOptions(i); + max = Math.max(max, options.offset || 0, options.hoverOffset || 0); + } + return max; + } + _getRingWeightOffset(datasetIndex) { + let ringWeightOffset = 0; + for (let i = 0; i < datasetIndex; ++i) { + if (this.chart.isDatasetVisible(i)) { + ringWeightOffset += this._getRingWeight(i); + } + } + return ringWeightOffset; + } + _getRingWeight(datasetIndex) { + return Math.max(valueOrDefault(this.chart.data.datasets[datasetIndex].weight, 1), 0); + } + _getVisibleDatasetWeightTotal() { + return this._getRingWeightOffset(this.chart.data.datasets.length) || 1; + } +} +DoughnutController.id = 'doughnut'; +DoughnutController.defaults = { + datasetElementType: false, + dataElementType: 'arc', + animation: { + animateRotate: true, + animateScale: false + }, + animations: { + numbers: { + type: 'number', + properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth', 'spacing'] + }, + }, + cutout: '50%', + rotation: 0, + circumference: 360, + radius: '100%', + spacing: 0, + indexAxis: 'r', +}; +DoughnutController.descriptors = { + _scriptable: (name) => name !== 'spacing', + _indexable: (name) => name !== 'spacing', +}; +DoughnutController.overrides = { + aspectRatio: 1, + plugins: { + legend: { + labels: { + generateLabels(chart) { + const data = chart.data; + if (data.labels.length && data.datasets.length) { + const {labels: {pointStyle}} = chart.legend.options; + return data.labels.map((label, i) => { + const meta = chart.getDatasetMeta(0); + const style = meta.controller.getStyle(i); + return { + text: label, + fillStyle: style.backgroundColor, + strokeStyle: style.borderColor, + lineWidth: style.borderWidth, + pointStyle: pointStyle, + hidden: !chart.getDataVisibility(i), + index: i + }; + }); + } + return []; + } + }, + onClick(e, legendItem, legend) { + legend.chart.toggleDataVisibility(legendItem.index); + legend.chart.update(); + } + }, + tooltip: { + callbacks: { + title() { + return ''; + }, + label(tooltipItem) { + let dataLabel = tooltipItem.label; + const value = ': ' + tooltipItem.formattedValue; + if (isArray(dataLabel)) { + dataLabel = dataLabel.slice(); + dataLabel[0] += value; + } else { + dataLabel += value; + } + return dataLabel; + } + } + } + } +}; + +class LineController extends DatasetController { + initialize() { + this.enableOptionSharing = true; + super.initialize(); + } + update(mode) { + const me = this; + const meta = me._cachedMeta; + const {dataset: line, data: points = [], _dataset} = meta; + const animationsDisabled = me.chart._animationsDisabled; + let {start, count} = getStartAndCountOfVisiblePoints(meta, points, animationsDisabled); + me._drawStart = start; + me._drawCount = count; + if (scaleRangesChanged(meta)) { + start = 0; + count = points.length; + } + line._decimated = !!_dataset._decimated; + line.points = points; + const options = me.resolveDatasetElementOptions(mode); + if (!me.options.showLine) { + options.borderWidth = 0; + } + options.segment = me.options.segment; + me.updateElement(line, undefined, { + animated: !animationsDisabled, + options + }, mode); + me.updateElements(points, start, count, mode); + } + updateElements(points, start, count, mode) { + const me = this; + const reset = mode === 'reset'; + const {iScale, vScale, _stacked} = me._cachedMeta; + const firstOpts = me.resolveDataElementOptions(start, mode); + const sharedOptions = me.getSharedOptions(firstOpts); + const includeOptions = me.includeOptions(mode, sharedOptions); + const iAxis = iScale.axis; + const vAxis = vScale.axis; + const spanGaps = me.options.spanGaps; + const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY; + const directUpdate = me.chart._animationsDisabled || reset || mode === 'none'; + let prevParsed = start > 0 && me.getParsed(start - 1); + for (let i = start; i < start + count; ++i) { + const point = points[i]; + const parsed = me.getParsed(i); + const properties = directUpdate ? point : {}; + const nullData = isNullOrUndef(parsed[vAxis]); + const iPixel = properties[iAxis] = iScale.getPixelForValue(parsed[iAxis], i); + const vPixel = properties[vAxis] = reset || nullData ? vScale.getBasePixel() : vScale.getPixelForValue(_stacked ? me.applyStack(vScale, parsed, _stacked) : parsed[vAxis], i); + properties.skip = isNaN(iPixel) || isNaN(vPixel) || nullData; + properties.stop = i > 0 && (parsed[iAxis] - prevParsed[iAxis]) > maxGapLength; + properties.parsed = parsed; + if (includeOptions) { + properties.options = sharedOptions || me.resolveDataElementOptions(i, point.active ? 'active' : mode); + } + if (!directUpdate) { + me.updateElement(point, i, properties, mode); + } + prevParsed = parsed; + } + me.updateSharedOptions(sharedOptions, mode, firstOpts); + } + getMaxOverflow() { + const me = this; + const meta = me._cachedMeta; + const dataset = meta.dataset; + const border = dataset.options && dataset.options.borderWidth || 0; + const data = meta.data || []; + if (!data.length) { + return border; + } + const firstPoint = data[0].size(me.resolveDataElementOptions(0)); + const lastPoint = data[data.length - 1].size(me.resolveDataElementOptions(data.length - 1)); + return Math.max(border, firstPoint, lastPoint) / 2; + } + draw() { + const meta = this._cachedMeta; + meta.dataset.updateControlPoints(this.chart.chartArea, meta.iScale.axis); + super.draw(); + } +} +LineController.id = 'line'; +LineController.defaults = { + datasetElementType: 'line', + dataElementType: 'point', + showLine: true, + spanGaps: false, +}; +LineController.overrides = { + scales: { + _index_: { + type: 'category', + }, + _value_: { + type: 'linear', + }, + } +}; +function getStartAndCountOfVisiblePoints(meta, points, animationsDisabled) { + const pointCount = points.length; + let start = 0; + let count = pointCount; + if (meta._sorted) { + const {iScale, _parsed} = meta; + const axis = iScale.axis; + const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); + if (minDefined) { + start = _limitValue(Math.min( + _lookupByKey(_parsed, iScale.axis, min).lo, + animationsDisabled ? pointCount : _lookupByKey(points, axis, iScale.getPixelForValue(min)).lo), + 0, pointCount - 1); + } + if (maxDefined) { + count = _limitValue(Math.max( + _lookupByKey(_parsed, iScale.axis, max).hi + 1, + animationsDisabled ? 0 : _lookupByKey(points, axis, iScale.getPixelForValue(max)).hi + 1), + start, pointCount) - start; + } else { + count = pointCount - start; + } + } + return {start, count}; +} +function scaleRangesChanged(meta) { + const {xScale, yScale, _scaleRanges} = meta; + const newRanges = { + xmin: xScale.min, + xmax: xScale.max, + ymin: yScale.min, + ymax: yScale.max + }; + if (!_scaleRanges) { + meta._scaleRanges = newRanges; + return true; + } + const changed = _scaleRanges.xmin !== xScale.min + || _scaleRanges.xmax !== xScale.max + || _scaleRanges.ymin !== yScale.min + || _scaleRanges.ymax !== yScale.max; + Object.assign(_scaleRanges, newRanges); + return changed; +} + +class PolarAreaController extends DatasetController { + constructor(chart, datasetIndex) { + super(chart, datasetIndex); + this.innerRadius = undefined; + this.outerRadius = undefined; + } + getLabelAndValue(index) { + const me = this; + const meta = me._cachedMeta; + const chart = me.chart; + const labels = chart.data.labels || []; + const value = formatNumber(meta._parsed[index].r, chart.options.locale); + return { + label: labels[index] || '', + value, + }; + } + update(mode) { + const arcs = this._cachedMeta.data; + this._updateRadius(); + this.updateElements(arcs, 0, arcs.length, mode); + } + _updateRadius() { + const me = this; + const chart = me.chart; + const chartArea = chart.chartArea; + const opts = chart.options; + const minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top); + const outerRadius = Math.max(minSize / 2, 0); + const innerRadius = Math.max(opts.cutoutPercentage ? (outerRadius / 100) * (opts.cutoutPercentage) : 1, 0); + const radiusLength = (outerRadius - innerRadius) / chart.getVisibleDatasetCount(); + me.outerRadius = outerRadius - (radiusLength * me.index); + me.innerRadius = me.outerRadius - radiusLength; + } + updateElements(arcs, start, count, mode) { + const me = this; + const reset = mode === 'reset'; + const chart = me.chart; + const dataset = me.getDataset(); + const opts = chart.options; + const animationOpts = opts.animation; + const scale = me._cachedMeta.rScale; + const centerX = scale.xCenter; + const centerY = scale.yCenter; + const datasetStartAngle = scale.getIndexAngle(0) - 0.5 * PI; + let angle = datasetStartAngle; + let i; + const defaultAngle = 360 / me.countVisibleElements(); + for (i = 0; i < start; ++i) { + angle += me._computeAngle(i, mode, defaultAngle); + } + for (i = start; i < start + count; i++) { + const arc = arcs[i]; + let startAngle = angle; + let endAngle = angle + me._computeAngle(i, mode, defaultAngle); + let outerRadius = chart.getDataVisibility(i) ? scale.getDistanceFromCenterForValue(dataset.data[i]) : 0; + angle = endAngle; + if (reset) { + if (animationOpts.animateScale) { + outerRadius = 0; + } + if (animationOpts.animateRotate) { + startAngle = endAngle = datasetStartAngle; + } + } + const properties = { + x: centerX, + y: centerY, + innerRadius: 0, + outerRadius, + startAngle, + endAngle, + options: me.resolveDataElementOptions(i, arc.active ? 'active' : mode) + }; + me.updateElement(arc, i, properties, mode); + } + } + countVisibleElements() { + const dataset = this.getDataset(); + const meta = this._cachedMeta; + let count = 0; + meta.data.forEach((element, index) => { + if (!isNaN(dataset.data[index]) && this.chart.getDataVisibility(index)) { + count++; + } + }); + return count; + } + _computeAngle(index, mode, defaultAngle) { + return this.chart.getDataVisibility(index) + ? toRadians(this.resolveDataElementOptions(index, mode).angle || defaultAngle) + : 0; + } +} +PolarAreaController.id = 'polarArea'; +PolarAreaController.defaults = { + dataElementType: 'arc', + animation: { + animateRotate: true, + animateScale: true + }, + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius'] + }, + }, + indexAxis: 'r', + startAngle: 0, +}; +PolarAreaController.overrides = { + aspectRatio: 1, + plugins: { + legend: { + labels: { + generateLabels(chart) { + const data = chart.data; + if (data.labels.length && data.datasets.length) { + const {labels: {pointStyle}} = chart.legend.options; + return data.labels.map((label, i) => { + const meta = chart.getDatasetMeta(0); + const style = meta.controller.getStyle(i); + return { + text: label, + fillStyle: style.backgroundColor, + strokeStyle: style.borderColor, + lineWidth: style.borderWidth, + pointStyle: pointStyle, + hidden: !chart.getDataVisibility(i), + index: i + }; + }); + } + return []; + } + }, + onClick(e, legendItem, legend) { + legend.chart.toggleDataVisibility(legendItem.index); + legend.chart.update(); + } + }, + tooltip: { + callbacks: { + title() { + return ''; + }, + label(context) { + return context.chart.data.labels[context.dataIndex] + ': ' + context.formattedValue; + } + } + } + }, + scales: { + r: { + type: 'radialLinear', + angleLines: { + display: false + }, + beginAtZero: true, + grid: { + circular: true + }, + pointLabels: { + display: false + }, + startAngle: 0 + } + } +}; + +class PieController extends DoughnutController { +} +PieController.id = 'pie'; +PieController.defaults = { + cutout: 0, + rotation: 0, + circumference: 360, + radius: '100%' +}; + +class RadarController extends DatasetController { + getLabelAndValue(index) { + const me = this; + const vScale = me._cachedMeta.vScale; + const parsed = me.getParsed(index); + return { + label: vScale.getLabels()[index], + value: '' + vScale.getLabelForValue(parsed[vScale.axis]) + }; + } + update(mode) { + const me = this; + const meta = me._cachedMeta; + const line = meta.dataset; + const points = meta.data || []; + const labels = meta.iScale.getLabels(); + line.points = points; + if (mode !== 'resize') { + const options = me.resolveDatasetElementOptions(mode); + if (!me.options.showLine) { + options.borderWidth = 0; + } + const properties = { + _loop: true, + _fullLoop: labels.length === points.length, + options + }; + me.updateElement(line, undefined, properties, mode); + } + me.updateElements(points, 0, points.length, mode); + } + updateElements(points, start, count, mode) { + const me = this; + const dataset = me.getDataset(); + const scale = me._cachedMeta.rScale; + const reset = mode === 'reset'; + for (let i = start; i < start + count; i++) { + const point = points[i]; + const options = me.resolveDataElementOptions(i, point.active ? 'active' : mode); + const pointPosition = scale.getPointPositionForValue(i, dataset.data[i]); + const x = reset ? scale.xCenter : pointPosition.x; + const y = reset ? scale.yCenter : pointPosition.y; + const properties = { + x, + y, + angle: pointPosition.angle, + skip: isNaN(x) || isNaN(y), + options + }; + me.updateElement(point, i, properties, mode); + } + } +} +RadarController.id = 'radar'; +RadarController.defaults = { + datasetElementType: 'line', + dataElementType: 'point', + indexAxis: 'r', + showLine: true, + elements: { + line: { + fill: 'start' + } + }, +}; +RadarController.overrides = { + aspectRatio: 1, + scales: { + r: { + type: 'radialLinear', + } + } +}; + +class ScatterController extends LineController { +} +ScatterController.id = 'scatter'; +ScatterController.defaults = { + showLine: false, + fill: false +}; +ScatterController.overrides = { + interaction: { + mode: 'point' + }, + plugins: { + tooltip: { + callbacks: { + title() { + return ''; + }, + label(item) { + return '(' + item.label + ', ' + item.formattedValue + ')'; + } + } + } + }, + scales: { + x: { + type: 'linear' + }, + y: { + type: 'linear' + } + } +}; + +var controllers = /*#__PURE__*/Object.freeze({ +__proto__: null, +BarController: BarController, +BubbleController: BubbleController, +DoughnutController: DoughnutController, +LineController: LineController, +PolarAreaController: PolarAreaController, +PieController: PieController, +RadarController: RadarController, +ScatterController: ScatterController +}); + +function clipArc(ctx, element, endAngle) { + const {startAngle, pixelMargin, x, y, outerRadius, innerRadius} = element; + let angleMargin = pixelMargin / outerRadius; + ctx.beginPath(); + ctx.arc(x, y, outerRadius, startAngle - angleMargin, endAngle + angleMargin); + if (innerRadius > pixelMargin) { + angleMargin = pixelMargin / innerRadius; + ctx.arc(x, y, innerRadius, endAngle + angleMargin, startAngle - angleMargin, true); + } else { + ctx.arc(x, y, pixelMargin, endAngle + HALF_PI, startAngle - HALF_PI); + } + ctx.closePath(); + ctx.clip(); +} +function toRadiusCorners(value) { + return _readValueToProps(value, ['outerStart', 'outerEnd', 'innerStart', 'innerEnd']); +} +function parseBorderRadius$1(arc, innerRadius, outerRadius, angleDelta) { + const o = toRadiusCorners(arc.options.borderRadius); + const halfThickness = (outerRadius - innerRadius) / 2; + const innerLimit = Math.min(halfThickness, angleDelta * innerRadius / 2); + const computeOuterLimit = (val) => { + const outerArcLimit = (outerRadius - Math.min(halfThickness, val)) * angleDelta / 2; + return _limitValue(val, 0, Math.min(halfThickness, outerArcLimit)); + }; + return { + outerStart: computeOuterLimit(o.outerStart), + outerEnd: computeOuterLimit(o.outerEnd), + innerStart: _limitValue(o.innerStart, 0, innerLimit), + innerEnd: _limitValue(o.innerEnd, 0, innerLimit), + }; +} +function rThetaToXY(r, theta, x, y) { + return { + x: x + r * Math.cos(theta), + y: y + r * Math.sin(theta), + }; +} +function pathArc(ctx, element, offset, spacing, end) { + const {x, y, startAngle: start, pixelMargin, innerRadius: innerR} = element; + const outerRadius = Math.max(element.outerRadius + spacing + offset - pixelMargin, 0); + const innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0; + let spacingOffset = 0; + const alpha = end - start; + if (spacing) { + const noSpacingInnerRadius = innerR > 0 ? innerR - spacing : 0; + const noSpacingOuterRadius = outerRadius > 0 ? outerRadius - spacing : 0; + const avNogSpacingRadius = (noSpacingInnerRadius + noSpacingOuterRadius) / 2; + const adjustedAngle = avNogSpacingRadius !== 0 ? (alpha * avNogSpacingRadius) / (avNogSpacingRadius + spacing) : alpha; + spacingOffset = (alpha - adjustedAngle) / 2; + } + const beta = Math.max(0.001, alpha * outerRadius - offset / PI) / outerRadius; + const angleOffset = (alpha - beta) / 2; + const startAngle = start + angleOffset + spacingOffset; + const endAngle = end - angleOffset - spacingOffset; + const {outerStart, outerEnd, innerStart, innerEnd} = parseBorderRadius$1(element, innerRadius, outerRadius, endAngle - startAngle); + const outerStartAdjustedRadius = outerRadius - outerStart; + const outerEndAdjustedRadius = outerRadius - outerEnd; + const outerStartAdjustedAngle = startAngle + outerStart / outerStartAdjustedRadius; + const outerEndAdjustedAngle = endAngle - outerEnd / outerEndAdjustedRadius; + const innerStartAdjustedRadius = innerRadius + innerStart; + const innerEndAdjustedRadius = innerRadius + innerEnd; + const innerStartAdjustedAngle = startAngle + innerStart / innerStartAdjustedRadius; + const innerEndAdjustedAngle = endAngle - innerEnd / innerEndAdjustedRadius; + ctx.beginPath(); + ctx.arc(x, y, outerRadius, outerStartAdjustedAngle, outerEndAdjustedAngle); + if (outerEnd > 0) { + const pCenter = rThetaToXY(outerEndAdjustedRadius, outerEndAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, outerEnd, outerEndAdjustedAngle, endAngle + HALF_PI); + } + const p4 = rThetaToXY(innerEndAdjustedRadius, endAngle, x, y); + ctx.lineTo(p4.x, p4.y); + if (innerEnd > 0) { + const pCenter = rThetaToXY(innerEndAdjustedRadius, innerEndAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, innerEnd, endAngle + HALF_PI, innerEndAdjustedAngle + Math.PI); + } + ctx.arc(x, y, innerRadius, endAngle - (innerEnd / innerRadius), startAngle + (innerStart / innerRadius), true); + if (innerStart > 0) { + const pCenter = rThetaToXY(innerStartAdjustedRadius, innerStartAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, innerStart, innerStartAdjustedAngle + Math.PI, startAngle - HALF_PI); + } + const p8 = rThetaToXY(outerStartAdjustedRadius, startAngle, x, y); + ctx.lineTo(p8.x, p8.y); + if (outerStart > 0) { + const pCenter = rThetaToXY(outerStartAdjustedRadius, outerStartAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, outerStart, startAngle - HALF_PI, outerStartAdjustedAngle); + } + ctx.closePath(); +} +function drawArc(ctx, element, offset, spacing) { + const {fullCircles, startAngle, circumference} = element; + let endAngle = element.endAngle; + if (fullCircles) { + pathArc(ctx, element, offset, spacing, startAngle + TAU); + for (let i = 0; i < fullCircles; ++i) { + ctx.fill(); + } + if (!isNaN(circumference)) { + endAngle = startAngle + circumference % TAU; + if (circumference % TAU === 0) { + endAngle += TAU; + } + } + } + pathArc(ctx, element, offset, spacing, endAngle); + ctx.fill(); + return endAngle; +} +function drawFullCircleBorders(ctx, element, inner) { + const {x, y, startAngle, pixelMargin, fullCircles} = element; + const outerRadius = Math.max(element.outerRadius - pixelMargin, 0); + const innerRadius = element.innerRadius + pixelMargin; + let i; + if (inner) { + clipArc(ctx, element, startAngle + TAU); + } + ctx.beginPath(); + ctx.arc(x, y, innerRadius, startAngle + TAU, startAngle, true); + for (i = 0; i < fullCircles; ++i) { + ctx.stroke(); + } + ctx.beginPath(); + ctx.arc(x, y, outerRadius, startAngle, startAngle + TAU); + for (i = 0; i < fullCircles; ++i) { + ctx.stroke(); + } +} +function drawBorder(ctx, element, offset, spacing, endAngle) { + const {options} = element; + const inner = options.borderAlign === 'inner'; + if (!options.borderWidth) { + return; + } + if (inner) { + ctx.lineWidth = options.borderWidth * 2; + ctx.lineJoin = 'round'; + } else { + ctx.lineWidth = options.borderWidth; + ctx.lineJoin = 'bevel'; + } + if (element.fullCircles) { + drawFullCircleBorders(ctx, element, inner); + } + if (inner) { + clipArc(ctx, element, endAngle); + } + pathArc(ctx, element, offset, spacing, endAngle); + ctx.stroke(); +} +class ArcElement extends Element { + constructor(cfg) { + super(); + this.options = undefined; + this.circumference = undefined; + this.startAngle = undefined; + this.endAngle = undefined; + this.innerRadius = undefined; + this.outerRadius = undefined; + this.pixelMargin = 0; + this.fullCircles = 0; + if (cfg) { + Object.assign(this, cfg); + } + } + inRange(chartX, chartY, useFinalPosition) { + const point = this.getProps(['x', 'y'], useFinalPosition); + const {angle, distance} = getAngleFromPoint(point, {x: chartX, y: chartY}); + const {startAngle, endAngle, innerRadius, outerRadius, circumference} = this.getProps([ + 'startAngle', + 'endAngle', + 'innerRadius', + 'outerRadius', + 'circumference' + ], useFinalPosition); + const rAdjust = this.options.spacing / 2; + const betweenAngles = circumference >= TAU || _angleBetween(angle, startAngle, endAngle); + const withinRadius = (distance >= innerRadius + rAdjust && distance <= outerRadius + rAdjust); + return (betweenAngles && withinRadius); + } + getCenterPoint(useFinalPosition) { + const {x, y, startAngle, endAngle, innerRadius, outerRadius} = this.getProps([ + 'x', + 'y', + 'startAngle', + 'endAngle', + 'innerRadius', + 'outerRadius', + 'circumference', + ], useFinalPosition); + const {offset, spacing} = this.options; + const halfAngle = (startAngle + endAngle) / 2; + const halfRadius = (innerRadius + outerRadius + spacing + offset) / 2; + return { + x: x + Math.cos(halfAngle) * halfRadius, + y: y + Math.sin(halfAngle) * halfRadius + }; + } + tooltipPosition(useFinalPosition) { + return this.getCenterPoint(useFinalPosition); + } + draw(ctx) { + const me = this; + const {options, circumference} = me; + const offset = (options.offset || 0) / 2; + const spacing = (options.spacing || 0) / 2; + me.pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0; + me.fullCircles = circumference > TAU ? Math.floor(circumference / TAU) : 0; + if (circumference === 0 || me.innerRadius < 0 || me.outerRadius < 0) { + return; + } + ctx.save(); + let radiusOffset = 0; + if (offset) { + radiusOffset = offset / 2; + const halfAngle = (me.startAngle + me.endAngle) / 2; + ctx.translate(Math.cos(halfAngle) * radiusOffset, Math.sin(halfAngle) * radiusOffset); + if (me.circumference >= PI) { + radiusOffset = offset; + } + } + ctx.fillStyle = options.backgroundColor; + ctx.strokeStyle = options.borderColor; + const endAngle = drawArc(ctx, me, radiusOffset, spacing); + drawBorder(ctx, me, radiusOffset, spacing, endAngle); + ctx.restore(); + } +} +ArcElement.id = 'arc'; +ArcElement.defaults = { + borderAlign: 'center', + borderColor: '#fff', + borderRadius: 0, + borderWidth: 2, + offset: 0, + spacing: 0, + angle: undefined, +}; +ArcElement.defaultRoutes = { + backgroundColor: 'backgroundColor' +}; + +function setStyle(ctx, options, style = options) { + ctx.lineCap = valueOrDefault(style.borderCapStyle, options.borderCapStyle); + ctx.setLineDash(valueOrDefault(style.borderDash, options.borderDash)); + ctx.lineDashOffset = valueOrDefault(style.borderDashOffset, options.borderDashOffset); + ctx.lineJoin = valueOrDefault(style.borderJoinStyle, options.borderJoinStyle); + ctx.lineWidth = valueOrDefault(style.borderWidth, options.borderWidth); + ctx.strokeStyle = valueOrDefault(style.borderColor, options.borderColor); +} +function lineTo(ctx, previous, target) { + ctx.lineTo(target.x, target.y); +} +function getLineMethod(options) { + if (options.stepped) { + return _steppedLineTo; + } + if (options.tension || options.cubicInterpolationMode === 'monotone') { + return _bezierCurveTo; + } + return lineTo; +} +function pathVars(points, segment, params = {}) { + const count = points.length; + const {start: paramsStart = 0, end: paramsEnd = count - 1} = params; + const {start: segmentStart, end: segmentEnd} = segment; + const start = Math.max(paramsStart, segmentStart); + const end = Math.min(paramsEnd, segmentEnd); + const outside = paramsStart < segmentStart && paramsEnd < segmentStart || paramsStart > segmentEnd && paramsEnd > segmentEnd; + return { + count, + start, + loop: segment.loop, + ilen: end < start && !outside ? count + end - start : end - start + }; +} +function pathSegment(ctx, line, segment, params) { + const {points, options} = line; + const {count, start, loop, ilen} = pathVars(points, segment, params); + const lineMethod = getLineMethod(options); + let {move = true, reverse} = params || {}; + let i, point, prev; + for (i = 0; i <= ilen; ++i) { + point = points[(start + (reverse ? ilen - i : i)) % count]; + if (point.skip) { + continue; + } else if (move) { + ctx.moveTo(point.x, point.y); + move = false; + } else { + lineMethod(ctx, prev, point, reverse, options.stepped); + } + prev = point; + } + if (loop) { + point = points[(start + (reverse ? ilen : 0)) % count]; + lineMethod(ctx, prev, point, reverse, options.stepped); + } + return !!loop; +} +function fastPathSegment(ctx, line, segment, params) { + const points = line.points; + const {count, start, ilen} = pathVars(points, segment, params); + const {move = true, reverse} = params || {}; + let avgX = 0; + let countX = 0; + let i, point, prevX, minY, maxY, lastY; + const pointIndex = (index) => (start + (reverse ? ilen - index : index)) % count; + const drawX = () => { + if (minY !== maxY) { + ctx.lineTo(avgX, maxY); + ctx.lineTo(avgX, minY); + ctx.lineTo(avgX, lastY); + } + }; + if (move) { + point = points[pointIndex(0)]; + ctx.moveTo(point.x, point.y); + } + for (i = 0; i <= ilen; ++i) { + point = points[pointIndex(i)]; + if (point.skip) { + continue; + } + const x = point.x; + const y = point.y; + const truncX = x | 0; + if (truncX === prevX) { + if (y < minY) { + minY = y; + } else if (y > maxY) { + maxY = y; + } + avgX = (countX * avgX + x) / ++countX; + } else { + drawX(); + ctx.lineTo(x, y); + prevX = truncX; + countX = 0; + minY = maxY = y; + } + lastY = y; + } + drawX(); +} +function _getSegmentMethod(line) { + const opts = line.options; + const borderDash = opts.borderDash && opts.borderDash.length; + const useFastPath = !line._decimated && !line._loop && !opts.tension && opts.cubicInterpolationMode !== 'monotone' && !opts.stepped && !borderDash; + return useFastPath ? fastPathSegment : pathSegment; +} +function _getInterpolationMethod(options) { + if (options.stepped) { + return _steppedInterpolation; + } + if (options.tension || options.cubicInterpolationMode === 'monotone') { + return _bezierInterpolation; + } + return _pointInLine; +} +function strokePathWithCache(ctx, line, start, count) { + let path = line._path; + if (!path) { + path = line._path = new Path2D(); + if (line.path(path, start, count)) { + path.closePath(); + } + } + setStyle(ctx, line.options); + ctx.stroke(path); +} +function strokePathDirect(ctx, line, start, count) { + const {segments, options} = line; + const segmentMethod = _getSegmentMethod(line); + for (const segment of segments) { + setStyle(ctx, options, segment.style); + ctx.beginPath(); + if (segmentMethod(ctx, line, segment, {start, end: start + count - 1})) { + ctx.closePath(); + } + ctx.stroke(); + } +} +const usePath2D = typeof Path2D === 'function'; +function draw(ctx, line, start, count) { + if (usePath2D && line.segments.length === 1) { + strokePathWithCache(ctx, line, start, count); + } else { + strokePathDirect(ctx, line, start, count); + } +} +class LineElement extends Element { + constructor(cfg) { + super(); + this.animated = true; + this.options = undefined; + this._loop = undefined; + this._fullLoop = undefined; + this._path = undefined; + this._points = undefined; + this._segments = undefined; + this._decimated = false; + this._pointsUpdated = false; + if (cfg) { + Object.assign(this, cfg); + } + } + updateControlPoints(chartArea, indexAxis) { + const me = this; + const options = me.options; + if ((options.tension || options.cubicInterpolationMode === 'monotone') && !options.stepped && !me._pointsUpdated) { + const loop = options.spanGaps ? me._loop : me._fullLoop; + _updateBezierControlPoints(me._points, options, chartArea, loop, indexAxis); + me._pointsUpdated = true; + } + } + set points(points) { + const me = this; + me._points = points; + delete me._segments; + delete me._path; + me._pointsUpdated = false; + } + get points() { + return this._points; + } + get segments() { + return this._segments || (this._segments = _computeSegments(this, this.options.segment)); + } + first() { + const segments = this.segments; + const points = this.points; + return segments.length && points[segments[0].start]; + } + last() { + const segments = this.segments; + const points = this.points; + const count = segments.length; + return count && points[segments[count - 1].end]; + } + interpolate(point, property) { + const me = this; + const options = me.options; + const value = point[property]; + const points = me.points; + const segments = _boundSegments(me, {property, start: value, end: value}); + if (!segments.length) { + return; + } + const result = []; + const _interpolate = _getInterpolationMethod(options); + let i, ilen; + for (i = 0, ilen = segments.length; i < ilen; ++i) { + const {start, end} = segments[i]; + const p1 = points[start]; + const p2 = points[end]; + if (p1 === p2) { + result.push(p1); + continue; + } + const t = Math.abs((value - p1[property]) / (p2[property] - p1[property])); + const interpolated = _interpolate(p1, p2, t, options.stepped); + interpolated[property] = point[property]; + result.push(interpolated); + } + return result.length === 1 ? result[0] : result; + } + pathSegment(ctx, segment, params) { + const segmentMethod = _getSegmentMethod(this); + return segmentMethod(ctx, this, segment, params); + } + path(ctx, start, count) { + const me = this; + const segments = me.segments; + const segmentMethod = _getSegmentMethod(me); + let loop = me._loop; + start = start || 0; + count = count || (me.points.length - start); + for (const segment of segments) { + loop &= segmentMethod(ctx, me, segment, {start, end: start + count - 1}); + } + return !!loop; + } + draw(ctx, chartArea, start, count) { + const me = this; + const options = me.options || {}; + const points = me.points || []; + if (!points.length || !options.borderWidth) { + return; + } + ctx.save(); + draw(ctx, me, start, count); + ctx.restore(); + if (me.animated) { + me._pointsUpdated = false; + me._path = undefined; + } + } +} +LineElement.id = 'line'; +LineElement.defaults = { + borderCapStyle: 'butt', + borderDash: [], + borderDashOffset: 0, + borderJoinStyle: 'miter', + borderWidth: 3, + capBezierPoints: true, + cubicInterpolationMode: 'default', + fill: false, + spanGaps: false, + stepped: false, + tension: 0, +}; +LineElement.defaultRoutes = { + backgroundColor: 'backgroundColor', + borderColor: 'borderColor' +}; +LineElement.descriptors = { + _scriptable: true, + _indexable: (name) => name !== 'borderDash' && name !== 'fill', +}; + +function inRange$1(el, pos, axis, useFinalPosition) { + const options = el.options; + const {[axis]: value} = el.getProps([axis], useFinalPosition); + return (Math.abs(pos - value) < options.radius + options.hitRadius); +} +class PointElement extends Element { + constructor(cfg) { + super(); + this.options = undefined; + this.parsed = undefined; + this.skip = undefined; + this.stop = undefined; + if (cfg) { + Object.assign(this, cfg); + } + } + inRange(mouseX, mouseY, useFinalPosition) { + const options = this.options; + const {x, y} = this.getProps(['x', 'y'], useFinalPosition); + return ((Math.pow(mouseX - x, 2) + Math.pow(mouseY - y, 2)) < Math.pow(options.hitRadius + options.radius, 2)); + } + inXRange(mouseX, useFinalPosition) { + return inRange$1(this, mouseX, 'x', useFinalPosition); + } + inYRange(mouseY, useFinalPosition) { + return inRange$1(this, mouseY, 'y', useFinalPosition); + } + getCenterPoint(useFinalPosition) { + const {x, y} = this.getProps(['x', 'y'], useFinalPosition); + return {x, y}; + } + size(options) { + options = options || this.options || {}; + let radius = options.radius || 0; + radius = Math.max(radius, radius && options.hoverRadius || 0); + const borderWidth = radius && options.borderWidth || 0; + return (radius + borderWidth) * 2; + } + draw(ctx) { + const me = this; + const options = me.options; + if (me.skip || options.radius < 0.1) { + return; + } + ctx.strokeStyle = options.borderColor; + ctx.lineWidth = options.borderWidth; + ctx.fillStyle = options.backgroundColor; + drawPoint(ctx, options, me.x, me.y); + } + getRange() { + const options = this.options || {}; + return options.radius + options.hitRadius; + } +} +PointElement.id = 'point'; +PointElement.defaults = { + borderWidth: 1, + hitRadius: 1, + hoverBorderWidth: 1, + hoverRadius: 4, + pointStyle: 'circle', + radius: 3, + rotation: 0 +}; +PointElement.defaultRoutes = { + backgroundColor: 'backgroundColor', + borderColor: 'borderColor' +}; + +function getBarBounds(bar, useFinalPosition) { + const {x, y, base, width, height} = bar.getProps(['x', 'y', 'base', 'width', 'height'], useFinalPosition); + let left, right, top, bottom, half; + if (bar.horizontal) { + half = height / 2; + left = Math.min(x, base); + right = Math.max(x, base); + top = y - half; + bottom = y + half; + } else { + half = width / 2; + left = x - half; + right = x + half; + top = Math.min(y, base); + bottom = Math.max(y, base); + } + return {left, top, right, bottom}; +} +function parseBorderSkipped(bar) { + let edge = bar.options.borderSkipped; + const res = {}; + if (!edge) { + return res; + } + edge = bar.horizontal + ? parseEdge(edge, 'left', 'right', bar.base > bar.x) + : parseEdge(edge, 'bottom', 'top', bar.base < bar.y); + res[edge] = true; + return res; +} +function parseEdge(edge, a, b, reverse) { + if (reverse) { + edge = swap(edge, a, b); + edge = startEnd(edge, b, a); + } else { + edge = startEnd(edge, a, b); + } + return edge; +} +function swap(orig, v1, v2) { + return orig === v1 ? v2 : orig === v2 ? v1 : orig; +} +function startEnd(v, start, end) { + return v === 'start' ? start : v === 'end' ? end : v; +} +function skipOrLimit(skip, value, min, max) { + return skip ? 0 : Math.max(Math.min(value, max), min); +} +function parseBorderWidth(bar, maxW, maxH) { + const value = bar.options.borderWidth; + const skip = parseBorderSkipped(bar); + const o = toTRBL(value); + return { + t: skipOrLimit(skip.top, o.top, 0, maxH), + r: skipOrLimit(skip.right, o.right, 0, maxW), + b: skipOrLimit(skip.bottom, o.bottom, 0, maxH), + l: skipOrLimit(skip.left, o.left, 0, maxW) + }; +} +function parseBorderRadius(bar, maxW, maxH) { + const {enableBorderRadius} = bar.getProps(['enableBorderRadius']); + const value = bar.options.borderRadius; + const o = toTRBLCorners(value); + const maxR = Math.min(maxW, maxH); + const skip = parseBorderSkipped(bar); + const enableBorder = enableBorderRadius || isObject(value); + return { + topLeft: skipOrLimit(!enableBorder || skip.top || skip.left, o.topLeft, 0, maxR), + topRight: skipOrLimit(!enableBorder || skip.top || skip.right, o.topRight, 0, maxR), + bottomLeft: skipOrLimit(!enableBorder || skip.bottom || skip.left, o.bottomLeft, 0, maxR), + bottomRight: skipOrLimit(!enableBorder || skip.bottom || skip.right, o.bottomRight, 0, maxR) + }; +} +function boundingRects(bar) { + const bounds = getBarBounds(bar); + const width = bounds.right - bounds.left; + const height = bounds.bottom - bounds.top; + const border = parseBorderWidth(bar, width / 2, height / 2); + const radius = parseBorderRadius(bar, width / 2, height / 2); + return { + outer: { + x: bounds.left, + y: bounds.top, + w: width, + h: height, + radius + }, + inner: { + x: bounds.left + border.l, + y: bounds.top + border.t, + w: width - border.l - border.r, + h: height - border.t - border.b, + radius: { + topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)), + topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)), + bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)), + bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)), + } + } + }; +} +function inRange(bar, x, y, useFinalPosition) { + const skipX = x === null; + const skipY = y === null; + const skipBoth = skipX && skipY; + const bounds = bar && !skipBoth && getBarBounds(bar, useFinalPosition); + return bounds + && (skipX || x >= bounds.left && x <= bounds.right) + && (skipY || y >= bounds.top && y <= bounds.bottom); +} +function hasRadius(radius) { + return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight; +} +function addNormalRectPath(ctx, rect) { + ctx.rect(rect.x, rect.y, rect.w, rect.h); +} +class BarElement extends Element { + constructor(cfg) { + super(); + this.options = undefined; + this.horizontal = undefined; + this.base = undefined; + this.width = undefined; + this.height = undefined; + if (cfg) { + Object.assign(this, cfg); + } + } + draw(ctx) { + const options = this.options; + const {inner, outer} = boundingRects(this); + const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath; + ctx.save(); + if (outer.w !== inner.w || outer.h !== inner.h) { + ctx.beginPath(); + addRectPath(ctx, outer); + ctx.clip(); + addRectPath(ctx, inner); + ctx.fillStyle = options.borderColor; + ctx.fill('evenodd'); + } + ctx.beginPath(); + addRectPath(ctx, inner); + ctx.fillStyle = options.backgroundColor; + ctx.fill(); + ctx.restore(); + } + inRange(mouseX, mouseY, useFinalPosition) { + return inRange(this, mouseX, mouseY, useFinalPosition); + } + inXRange(mouseX, useFinalPosition) { + return inRange(this, mouseX, null, useFinalPosition); + } + inYRange(mouseY, useFinalPosition) { + return inRange(this, null, mouseY, useFinalPosition); + } + getCenterPoint(useFinalPosition) { + const {x, y, base, horizontal} = this.getProps(['x', 'y', 'base', 'horizontal'], useFinalPosition); + return { + x: horizontal ? (x + base) / 2 : x, + y: horizontal ? y : (y + base) / 2 + }; + } + getRange(axis) { + return axis === 'x' ? this.width / 2 : this.height / 2; + } +} +BarElement.id = 'bar'; +BarElement.defaults = { + borderSkipped: 'start', + borderWidth: 0, + borderRadius: 0, + enableBorderRadius: true, + pointStyle: undefined +}; +BarElement.defaultRoutes = { + backgroundColor: 'backgroundColor', + borderColor: 'borderColor' +}; + +var elements = /*#__PURE__*/Object.freeze({ +__proto__: null, +ArcElement: ArcElement, +LineElement: LineElement, +PointElement: PointElement, +BarElement: BarElement +}); + +function lttbDecimation(data, start, count, availableWidth, options) { + const samples = options.samples || availableWidth; + if (samples >= count) { + return data.slice(start, start + count); + } + const decimated = []; + const bucketWidth = (count - 2) / (samples - 2); + let sampledIndex = 0; + const endIndex = start + count - 1; + let a = start; + let i, maxAreaPoint, maxArea, area, nextA; + decimated[sampledIndex++] = data[a]; + for (i = 0; i < samples - 2; i++) { + let avgX = 0; + let avgY = 0; + let j; + const avgRangeStart = Math.floor((i + 1) * bucketWidth) + 1 + start; + const avgRangeEnd = Math.min(Math.floor((i + 2) * bucketWidth) + 1, count) + start; + const avgRangeLength = avgRangeEnd - avgRangeStart; + for (j = avgRangeStart; j < avgRangeEnd; j++) { + avgX += data[j].x; + avgY += data[j].y; + } + avgX /= avgRangeLength; + avgY /= avgRangeLength; + const rangeOffs = Math.floor(i * bucketWidth) + 1 + start; + const rangeTo = Math.floor((i + 1) * bucketWidth) + 1 + start; + const {x: pointAx, y: pointAy} = data[a]; + maxArea = area = -1; + for (j = rangeOffs; j < rangeTo; j++) { + area = 0.5 * Math.abs( + (pointAx - avgX) * (data[j].y - pointAy) - + (pointAx - data[j].x) * (avgY - pointAy) + ); + if (area > maxArea) { + maxArea = area; + maxAreaPoint = data[j]; + nextA = j; + } + } + decimated[sampledIndex++] = maxAreaPoint; + a = nextA; + } + decimated[sampledIndex++] = data[endIndex]; + return decimated; +} +function minMaxDecimation(data, start, count, availableWidth) { + let avgX = 0; + let countX = 0; + let i, point, x, y, prevX, minIndex, maxIndex, startIndex, minY, maxY; + const decimated = []; + const endIndex = start + count - 1; + const xMin = data[start].x; + const xMax = data[endIndex].x; + const dx = xMax - xMin; + for (i = start; i < start + count; ++i) { + point = data[i]; + x = (point.x - xMin) / dx * availableWidth; + y = point.y; + const truncX = x | 0; + if (truncX === prevX) { + if (y < minY) { + minY = y; + minIndex = i; + } else if (y > maxY) { + maxY = y; + maxIndex = i; + } + avgX = (countX * avgX + point.x) / ++countX; + } else { + const lastIndex = i - 1; + if (!isNullOrUndef(minIndex) && !isNullOrUndef(maxIndex)) { + const intermediateIndex1 = Math.min(minIndex, maxIndex); + const intermediateIndex2 = Math.max(minIndex, maxIndex); + if (intermediateIndex1 !== startIndex && intermediateIndex1 !== lastIndex) { + decimated.push({ + ...data[intermediateIndex1], + x: avgX, + }); + } + if (intermediateIndex2 !== startIndex && intermediateIndex2 !== lastIndex) { + decimated.push({ + ...data[intermediateIndex2], + x: avgX + }); + } + } + if (i > 0 && lastIndex !== startIndex) { + decimated.push(data[lastIndex]); + } + decimated.push(point); + prevX = truncX; + countX = 0; + minY = maxY = y; + minIndex = maxIndex = startIndex = i; + } + } + return decimated; +} +function cleanDecimatedDataset(dataset) { + if (dataset._decimated) { + const data = dataset._data; + delete dataset._decimated; + delete dataset._data; + Object.defineProperty(dataset, 'data', {value: data}); + } +} +function cleanDecimatedData(chart) { + chart.data.datasets.forEach((dataset) => { + cleanDecimatedDataset(dataset); + }); +} +function getStartAndCountOfVisiblePointsSimplified(meta, points) { + const pointCount = points.length; + let start = 0; + let count; + const {iScale} = meta; + const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); + if (minDefined) { + start = _limitValue(_lookupByKey(points, iScale.axis, min).lo, 0, pointCount - 1); + } + if (maxDefined) { + count = _limitValue(_lookupByKey(points, iScale.axis, max).hi + 1, start, pointCount) - start; + } else { + count = pointCount - start; + } + return {start, count}; +} +var plugin_decimation = { + id: 'decimation', + defaults: { + algorithm: 'min-max', + enabled: false, + }, + beforeElementsUpdate: (chart, args, options) => { + if (!options.enabled) { + cleanDecimatedData(chart); + return; + } + const availableWidth = chart.width; + chart.data.datasets.forEach((dataset, datasetIndex) => { + const {_data, indexAxis} = dataset; + const meta = chart.getDatasetMeta(datasetIndex); + const data = _data || dataset.data; + if (resolve([indexAxis, chart.options.indexAxis]) === 'y') { + return; + } + if (meta.type !== 'line') { + return; + } + const xAxis = chart.scales[meta.xAxisID]; + if (xAxis.type !== 'linear' && xAxis.type !== 'time') { + return; + } + if (chart.options.parsing) { + return; + } + let {start, count} = getStartAndCountOfVisiblePointsSimplified(meta, data); + if (count <= 4 * availableWidth) { + cleanDecimatedDataset(dataset); + return; + } + if (isNullOrUndef(_data)) { + dataset._data = data; + delete dataset.data; + Object.defineProperty(dataset, 'data', { + configurable: true, + enumerable: true, + get: function() { + return this._decimated; + }, + set: function(d) { + this._data = d; + } + }); + } + let decimated; + switch (options.algorithm) { + case 'lttb': + decimated = lttbDecimation(data, start, count, availableWidth, options); + break; + case 'min-max': + decimated = minMaxDecimation(data, start, count, availableWidth); + break; + default: + throw new Error(`Unsupported decimation algorithm '${options.algorithm}'`); + } + dataset._decimated = decimated; + }); + }, + destroy(chart) { + cleanDecimatedData(chart); + } +}; + +function getLineByIndex(chart, index) { + const meta = chart.getDatasetMeta(index); + const visible = meta && chart.isDatasetVisible(index); + return visible ? meta.dataset : null; +} +function parseFillOption(line) { + const options = line.options; + const fillOption = options.fill; + let fill = valueOrDefault(fillOption && fillOption.target, fillOption); + if (fill === undefined) { + fill = !!options.backgroundColor; + } + if (fill === false || fill === null) { + return false; + } + if (fill === true) { + return 'origin'; + } + return fill; +} +function decodeFill(line, index, count) { + const fill = parseFillOption(line); + if (isObject(fill)) { + return isNaN(fill.value) ? false : fill; + } + let target = parseFloat(fill); + if (isNumberFinite(target) && Math.floor(target) === target) { + if (fill[0] === '-' || fill[0] === '+') { + target = index + target; + } + if (target === index || target < 0 || target >= count) { + return false; + } + return target; + } + return ['origin', 'start', 'end', 'stack'].indexOf(fill) >= 0 && fill; +} +function computeLinearBoundary(source) { + const {scale = {}, fill} = source; + let target = null; + let horizontal; + if (fill === 'start') { + target = scale.bottom; + } else if (fill === 'end') { + target = scale.top; + } else if (isObject(fill)) { + target = scale.getPixelForValue(fill.value); + } else if (scale.getBasePixel) { + target = scale.getBasePixel(); + } + if (isNumberFinite(target)) { + horizontal = scale.isHorizontal(); + return { + x: horizontal ? target : null, + y: horizontal ? null : target + }; + } + return null; +} +class simpleArc { + constructor(opts) { + this.x = opts.x; + this.y = opts.y; + this.radius = opts.radius; + } + pathSegment(ctx, bounds, opts) { + const {x, y, radius} = this; + bounds = bounds || {start: 0, end: TAU}; + ctx.arc(x, y, radius, bounds.end, bounds.start, true); + return !opts.bounds; + } + interpolate(point) { + const {x, y, radius} = this; + const angle = point.angle; + return { + x: x + Math.cos(angle) * radius, + y: y + Math.sin(angle) * radius, + angle + }; + } +} +function computeCircularBoundary(source) { + const {scale, fill} = source; + const options = scale.options; + const length = scale.getLabels().length; + const target = []; + const start = options.reverse ? scale.max : scale.min; + const end = options.reverse ? scale.min : scale.max; + let i, center, value; + if (fill === 'start') { + value = start; + } else if (fill === 'end') { + value = end; + } else if (isObject(fill)) { + value = fill.value; + } else { + value = scale.getBaseValue(); + } + if (options.grid.circular) { + center = scale.getPointPositionForValue(0, start); + return new simpleArc({ + x: center.x, + y: center.y, + radius: scale.getDistanceFromCenterForValue(value) + }); + } + for (i = 0; i < length; ++i) { + target.push(scale.getPointPositionForValue(i, value)); + } + return target; +} +function computeBoundary(source) { + const scale = source.scale || {}; + if (scale.getPointPositionForValue) { + return computeCircularBoundary(source); + } + return computeLinearBoundary(source); +} +function findSegmentEnd(start, end, points) { + for (;end > start; end--) { + const point = points[end]; + if (!isNaN(point.x) && !isNaN(point.y)) { + break; + } + } + return end; +} +function pointsFromSegments(boundary, line) { + const {x = null, y = null} = boundary || {}; + const linePoints = line.points; + const points = []; + line.segments.forEach(({start, end}) => { + end = findSegmentEnd(start, end, linePoints); + const first = linePoints[start]; + const last = linePoints[end]; + if (y !== null) { + points.push({x: first.x, y}); + points.push({x: last.x, y}); + } else if (x !== null) { + points.push({x, y: first.y}); + points.push({x, y: last.y}); + } + }); + return points; +} +function buildStackLine(source) { + const {chart, scale, index, line} = source; + const points = []; + const segments = line.segments; + const sourcePoints = line.points; + const linesBelow = getLinesBelow(chart, index); + linesBelow.push(createBoundaryLine({x: null, y: scale.bottom}, line)); + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + for (let j = segment.start; j <= segment.end; j++) { + addPointsBelow(points, sourcePoints[j], linesBelow); + } + } + return new LineElement({points, options: {}}); +} +const isLineAndNotInHideAnimation = (meta) => meta.type === 'line' && !meta.hidden; +function getLinesBelow(chart, index) { + const below = []; + const metas = chart.getSortedVisibleDatasetMetas(); + for (let i = 0; i < metas.length; i++) { + const meta = metas[i]; + if (meta.index === index) { + break; + } + if (isLineAndNotInHideAnimation(meta)) { + below.unshift(meta.dataset); + } + } + return below; +} +function addPointsBelow(points, sourcePoint, linesBelow) { + const postponed = []; + for (let j = 0; j < linesBelow.length; j++) { + const line = linesBelow[j]; + const {first, last, point} = findPoint(line, sourcePoint, 'x'); + if (!point || (first && last)) { + continue; + } + if (first) { + postponed.unshift(point); + } else { + points.push(point); + if (!last) { + break; + } + } + } + points.push(...postponed); +} +function findPoint(line, sourcePoint, property) { + const point = line.interpolate(sourcePoint, property); + if (!point) { + return {}; + } + const pointValue = point[property]; + const segments = line.segments; + const linePoints = line.points; + let first = false; + let last = false; + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + const firstValue = linePoints[segment.start][property]; + const lastValue = linePoints[segment.end][property]; + if (pointValue >= firstValue && pointValue <= lastValue) { + first = pointValue === firstValue; + last = pointValue === lastValue; + break; + } + } + return {first, last, point}; +} +function getTarget(source) { + const {chart, fill, line} = source; + if (isNumberFinite(fill)) { + return getLineByIndex(chart, fill); + } + if (fill === 'stack') { + return buildStackLine(source); + } + const boundary = computeBoundary(source); + if (boundary instanceof simpleArc) { + return boundary; + } + return createBoundaryLine(boundary, line); +} +function createBoundaryLine(boundary, line) { + let points = []; + let _loop = false; + if (isArray(boundary)) { + _loop = true; + points = boundary; + } else { + points = pointsFromSegments(boundary, line); + } + return points.length ? new LineElement({ + points, + options: {tension: 0}, + _loop, + _fullLoop: _loop + }) : null; +} +function resolveTarget(sources, index, propagate) { + const source = sources[index]; + let fill = source.fill; + const visited = [index]; + let target; + if (!propagate) { + return fill; + } + while (fill !== false && visited.indexOf(fill) === -1) { + if (!isNumberFinite(fill)) { + return fill; + } + target = sources[fill]; + if (!target) { + return false; + } + if (target.visible) { + return fill; + } + visited.push(fill); + fill = target.fill; + } + return false; +} +function _clip(ctx, target, clipY) { + ctx.beginPath(); + target.path(ctx); + ctx.lineTo(target.last().x, clipY); + ctx.lineTo(target.first().x, clipY); + ctx.closePath(); + ctx.clip(); +} +function getBounds(property, first, last, loop) { + if (loop) { + return; + } + let start = first[property]; + let end = last[property]; + if (property === 'angle') { + start = _normalizeAngle(start); + end = _normalizeAngle(end); + } + return {property, start, end}; +} +function _getEdge(a, b, prop, fn) { + if (a && b) { + return fn(a[prop], b[prop]); + } + return a ? a[prop] : b ? b[prop] : 0; +} +function _segments(line, target, property) { + const segments = line.segments; + const points = line.points; + const tpoints = target.points; + const parts = []; + for (const segment of segments) { + let {start, end} = segment; + end = findSegmentEnd(start, end, points); + const bounds = getBounds(property, points[start], points[end], segment.loop); + if (!target.segments) { + parts.push({ + source: segment, + target: bounds, + start: points[start], + end: points[end] + }); + continue; + } + const targetSegments = _boundSegments(target, bounds); + for (const tgt of targetSegments) { + const subBounds = getBounds(property, tpoints[tgt.start], tpoints[tgt.end], tgt.loop); + const fillSources = _boundSegment(segment, points, subBounds); + for (const fillSource of fillSources) { + parts.push({ + source: fillSource, + target: tgt, + start: { + [property]: _getEdge(bounds, subBounds, 'start', Math.max) + }, + end: { + [property]: _getEdge(bounds, subBounds, 'end', Math.min) + } + }); + } + } + } + return parts; +} +function clipBounds(ctx, scale, bounds) { + const {top, bottom} = scale.chart.chartArea; + const {property, start, end} = bounds || {}; + if (property === 'x') { + ctx.beginPath(); + ctx.rect(start, top, end - start, bottom - top); + ctx.clip(); + } +} +function interpolatedLineTo(ctx, target, point, property) { + const interpolatedPoint = target.interpolate(point, property); + if (interpolatedPoint) { + ctx.lineTo(interpolatedPoint.x, interpolatedPoint.y); + } +} +function _fill(ctx, cfg) { + const {line, target, property, color, scale} = cfg; + const segments = _segments(line, target, property); + for (const {source: src, target: tgt, start, end} of segments) { + const {style: {backgroundColor = color} = {}} = src; + ctx.save(); + ctx.fillStyle = backgroundColor; + clipBounds(ctx, scale, getBounds(property, start, end)); + ctx.beginPath(); + const lineLoop = !!line.pathSegment(ctx, src); + if (lineLoop) { + ctx.closePath(); + } else { + interpolatedLineTo(ctx, target, end, property); + } + const targetLoop = !!target.pathSegment(ctx, tgt, {move: lineLoop, reverse: true}); + const loop = lineLoop && targetLoop; + if (!loop) { + interpolatedLineTo(ctx, target, start, property); + } + ctx.closePath(); + ctx.fill(loop ? 'evenodd' : 'nonzero'); + ctx.restore(); + } +} +function doFill(ctx, cfg) { + const {line, target, above, below, area, scale} = cfg; + const property = line._loop ? 'angle' : cfg.axis; + ctx.save(); + if (property === 'x' && below !== above) { + _clip(ctx, target, area.top); + _fill(ctx, {line, target, color: above, scale, property}); + ctx.restore(); + ctx.save(); + _clip(ctx, target, area.bottom); + } + _fill(ctx, {line, target, color: below, scale, property}); + ctx.restore(); +} +function drawfill(ctx, source, area) { + const target = getTarget(source); + const {line, scale, axis} = source; + const lineOpts = line.options; + const fillOption = lineOpts.fill; + const color = lineOpts.backgroundColor; + const {above = color, below = color} = fillOption || {}; + if (target && line.points.length) { + clipArea(ctx, area); + doFill(ctx, {line, target, above, below, area, scale, axis}); + unclipArea(ctx); + } +} +var plugin_filler = { + id: 'filler', + afterDatasetsUpdate(chart, _args, options) { + const count = (chart.data.datasets || []).length; + const sources = []; + let meta, i, line, source; + for (i = 0; i < count; ++i) { + meta = chart.getDatasetMeta(i); + line = meta.dataset; + source = null; + if (line && line.options && line instanceof LineElement) { + source = { + visible: chart.isDatasetVisible(i), + index: i, + fill: decodeFill(line, i, count), + chart, + axis: meta.controller.options.indexAxis, + scale: meta.vScale, + line, + }; + } + meta.$filler = source; + sources.push(source); + } + for (i = 0; i < count; ++i) { + source = sources[i]; + if (!source || source.fill === false) { + continue; + } + source.fill = resolveTarget(sources, i, options.propagate); + } + }, + beforeDraw(chart, _args, options) { + const draw = options.drawTime === 'beforeDraw'; + const metasets = chart.getSortedVisibleDatasetMetas(); + const area = chart.chartArea; + for (let i = metasets.length - 1; i >= 0; --i) { + const source = metasets[i].$filler; + if (!source) { + continue; + } + source.line.updateControlPoints(area, source.axis); + if (draw) { + drawfill(chart.ctx, source, area); + } + } + }, + beforeDatasetsDraw(chart, _args, options) { + if (options.drawTime !== 'beforeDatasetsDraw') { + return; + } + const metasets = chart.getSortedVisibleDatasetMetas(); + for (let i = metasets.length - 1; i >= 0; --i) { + const source = metasets[i].$filler; + if (source) { + drawfill(chart.ctx, source, chart.chartArea); + } + } + }, + beforeDatasetDraw(chart, args, options) { + const source = args.meta.$filler; + if (!source || source.fill === false || options.drawTime !== 'beforeDatasetDraw') { + return; + } + drawfill(chart.ctx, source, chart.chartArea); + }, + defaults: { + propagate: true, + drawTime: 'beforeDatasetDraw' + } +}; + +const getBoxSize = (labelOpts, fontSize) => { + let {boxHeight = fontSize, boxWidth = fontSize} = labelOpts; + if (labelOpts.usePointStyle) { + boxHeight = Math.min(boxHeight, fontSize); + boxWidth = Math.min(boxWidth, fontSize); + } + return { + boxWidth, + boxHeight, + itemHeight: Math.max(fontSize, boxHeight) + }; +}; +const itemsEqual = (a, b) => a !== null && b !== null && a.datasetIndex === b.datasetIndex && a.index === b.index; +class Legend extends Element { + constructor(config) { + super(); + this._added = false; + this.legendHitBoxes = []; + this._hoveredItem = null; + this.doughnutMode = false; + this.chart = config.chart; + this.options = config.options; + this.ctx = config.ctx; + this.legendItems = undefined; + this.columnSizes = undefined; + this.lineWidths = undefined; + this.maxHeight = undefined; + this.maxWidth = undefined; + this.top = undefined; + this.bottom = undefined; + this.left = undefined; + this.right = undefined; + this.height = undefined; + this.width = undefined; + this._margins = undefined; + this.position = undefined; + this.weight = undefined; + this.fullSize = undefined; + } + update(maxWidth, maxHeight, margins) { + const me = this; + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me._margins = margins; + me.setDimensions(); + me.buildLabels(); + me.fit(); + } + setDimensions() { + const me = this; + if (me.isHorizontal()) { + me.width = me.maxWidth; + me.left = me._margins.left; + me.right = me.width; + } else { + me.height = me.maxHeight; + me.top = me._margins.top; + me.bottom = me.height; + } + } + buildLabels() { + const me = this; + const labelOpts = me.options.labels || {}; + let legendItems = callback(labelOpts.generateLabels, [me.chart], me) || []; + if (labelOpts.filter) { + legendItems = legendItems.filter((item) => labelOpts.filter(item, me.chart.data)); + } + if (labelOpts.sort) { + legendItems = legendItems.sort((a, b) => labelOpts.sort(a, b, me.chart.data)); + } + if (me.options.reverse) { + legendItems.reverse(); + } + me.legendItems = legendItems; + } + fit() { + const me = this; + const {options, ctx} = me; + if (!options.display) { + me.width = me.height = 0; + return; + } + const labelOpts = options.labels; + const labelFont = toFont(labelOpts.font); + const fontSize = labelFont.size; + const titleHeight = me._computeTitleHeight(); + const {boxWidth, itemHeight} = getBoxSize(labelOpts, fontSize); + let width, height; + ctx.font = labelFont.string; + if (me.isHorizontal()) { + width = me.maxWidth; + height = me._fitRows(titleHeight, fontSize, boxWidth, itemHeight) + 10; + } else { + height = me.maxHeight; + width = me._fitCols(titleHeight, fontSize, boxWidth, itemHeight) + 10; + } + me.width = Math.min(width, options.maxWidth || me.maxWidth); + me.height = Math.min(height, options.maxHeight || me.maxHeight); + } + _fitRows(titleHeight, fontSize, boxWidth, itemHeight) { + const me = this; + const {ctx, maxWidth, options: {labels: {padding}}} = me; + const hitboxes = me.legendHitBoxes = []; + const lineWidths = me.lineWidths = [0]; + const lineHeight = itemHeight + padding; + let totalHeight = titleHeight; + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + let row = -1; + let top = -lineHeight; + me.legendItems.forEach((legendItem, i) => { + const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + if (i === 0 || lineWidths[lineWidths.length - 1] + itemWidth + 2 * padding > maxWidth) { + totalHeight += lineHeight; + lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0; + top += lineHeight; + row++; + } + hitboxes[i] = {left: 0, top, row, width: itemWidth, height: itemHeight}; + lineWidths[lineWidths.length - 1] += itemWidth + padding; + }); + return totalHeight; + } + _fitCols(titleHeight, fontSize, boxWidth, itemHeight) { + const me = this; + const {ctx, maxHeight, options: {labels: {padding}}} = me; + const hitboxes = me.legendHitBoxes = []; + const columnSizes = me.columnSizes = []; + const heightLimit = maxHeight - titleHeight; + let totalWidth = padding; + let currentColWidth = 0; + let currentColHeight = 0; + let left = 0; + let col = 0; + me.legendItems.forEach((legendItem, i) => { + const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + if (i > 0 && currentColHeight + itemHeight + 2 * padding > heightLimit) { + totalWidth += currentColWidth + padding; + columnSizes.push({width: currentColWidth, height: currentColHeight}); + left += currentColWidth + padding; + col++; + currentColWidth = currentColHeight = 0; + } + hitboxes[i] = {left, top: currentColHeight, col, width: itemWidth, height: itemHeight}; + currentColWidth = Math.max(currentColWidth, itemWidth); + currentColHeight += itemHeight + padding; + }); + totalWidth += currentColWidth; + columnSizes.push({width: currentColWidth, height: currentColHeight}); + return totalWidth; + } + adjustHitBoxes() { + const me = this; + if (!me.options.display) { + return; + } + const titleHeight = me._computeTitleHeight(); + const {legendHitBoxes: hitboxes, options: {align, labels: {padding}, rtl}} = me; + if (this.isHorizontal()) { + let row = 0; + let left = _alignStartEnd(align, me.left + padding, me.right - me.lineWidths[row]); + for (const hitbox of hitboxes) { + if (row !== hitbox.row) { + row = hitbox.row; + left = _alignStartEnd(align, me.left + padding, me.right - me.lineWidths[row]); + } + hitbox.top += me.top + titleHeight + padding; + hitbox.left = left; + left += hitbox.width + padding; + } + if (rtl) { + const boxMap = hitboxes.reduce((map, box) => { + map[box.row] = map[box.row] || []; + map[box.row].push(box); + return map; + }, {}); + const newBoxes = []; + Object.keys(boxMap).forEach(key => { + boxMap[key].reverse(); + newBoxes.push(...boxMap[key]); + }); + me.legendHitBoxes = newBoxes; + } + } else { + let col = 0; + let top = _alignStartEnd(align, me.top + titleHeight + padding, me.bottom - me.columnSizes[col].height); + for (const hitbox of hitboxes) { + if (hitbox.col !== col) { + col = hitbox.col; + top = _alignStartEnd(align, me.top + titleHeight + padding, me.bottom - me.columnSizes[col].height); + } + hitbox.top = top; + hitbox.left += me.left + padding; + top += hitbox.height + padding; + } + } + } + isHorizontal() { + return this.options.position === 'top' || this.options.position === 'bottom'; + } + draw() { + const me = this; + if (me.options.display) { + const ctx = me.ctx; + clipArea(ctx, me); + me._draw(); + unclipArea(ctx); + } + } + _draw() { + const me = this; + const {options: opts, columnSizes, lineWidths, ctx} = me; + const {align, labels: labelOpts} = opts; + const defaultColor = defaults.color; + const rtlHelper = getRtlAdapter(opts.rtl, me.left, me.width); + const labelFont = toFont(labelOpts.font); + const {color: fontColor, padding} = labelOpts; + const fontSize = labelFont.size; + const halfFontSize = fontSize / 2; + let cursor; + me.drawTitle(); + ctx.textAlign = rtlHelper.textAlign('left'); + ctx.textBaseline = 'middle'; + ctx.lineWidth = 0.5; + ctx.font = labelFont.string; + const {boxWidth, boxHeight, itemHeight} = getBoxSize(labelOpts, fontSize); + const drawLegendBox = function(x, y, legendItem) { + if (isNaN(boxWidth) || boxWidth <= 0 || isNaN(boxHeight) || boxHeight < 0) { + return; + } + ctx.save(); + const lineWidth = valueOrDefault(legendItem.lineWidth, 1); + ctx.fillStyle = valueOrDefault(legendItem.fillStyle, defaultColor); + ctx.lineCap = valueOrDefault(legendItem.lineCap, 'butt'); + ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, 0); + ctx.lineJoin = valueOrDefault(legendItem.lineJoin, 'miter'); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, defaultColor); + ctx.setLineDash(valueOrDefault(legendItem.lineDash, [])); + if (labelOpts.usePointStyle) { + const drawOptions = { + radius: boxWidth * Math.SQRT2 / 2, + pointStyle: legendItem.pointStyle, + rotation: legendItem.rotation, + borderWidth: lineWidth + }; + const centerX = rtlHelper.xPlus(x, boxWidth / 2); + const centerY = y + halfFontSize; + drawPoint(ctx, drawOptions, centerX, centerY); + } else { + const yBoxTop = y + Math.max((fontSize - boxHeight) / 2, 0); + const xBoxLeft = rtlHelper.leftForLtr(x, boxWidth); + const borderRadius = toTRBLCorners(legendItem.borderRadius); + ctx.beginPath(); + if (Object.values(borderRadius).some(v => v !== 0)) { + addRoundedRectPath(ctx, { + x: xBoxLeft, + y: yBoxTop, + w: boxWidth, + h: boxHeight, + radius: borderRadius, + }); + } else { + ctx.rect(xBoxLeft, yBoxTop, boxWidth, boxHeight); + } + ctx.fill(); + if (lineWidth !== 0) { + ctx.stroke(); + } + } + ctx.restore(); + }; + const fillText = function(x, y, legendItem) { + renderText(ctx, legendItem.text, x, y + (itemHeight / 2), labelFont, { + strikethrough: legendItem.hidden, + textAlign: rtlHelper.textAlign(legendItem.textAlign) + }); + }; + const isHorizontal = me.isHorizontal(); + const titleHeight = this._computeTitleHeight(); + if (isHorizontal) { + cursor = { + x: _alignStartEnd(align, me.left + padding, me.right - lineWidths[0]), + y: me.top + padding + titleHeight, + line: 0 + }; + } else { + cursor = { + x: me.left + padding, + y: _alignStartEnd(align, me.top + titleHeight + padding, me.bottom - columnSizes[0].height), + line: 0 + }; + } + overrideTextDirection(me.ctx, opts.textDirection); + const lineHeight = itemHeight + padding; + me.legendItems.forEach((legendItem, i) => { + ctx.strokeStyle = legendItem.fontColor || fontColor; + ctx.fillStyle = legendItem.fontColor || fontColor; + const textWidth = ctx.measureText(legendItem.text).width; + const textAlign = rtlHelper.textAlign(legendItem.textAlign || (legendItem.textAlign = labelOpts.textAlign)); + const width = boxWidth + halfFontSize + textWidth; + let x = cursor.x; + let y = cursor.y; + rtlHelper.setWidth(me.width); + if (isHorizontal) { + if (i > 0 && x + width + padding > me.right) { + y = cursor.y += lineHeight; + cursor.line++; + x = cursor.x = _alignStartEnd(align, me.left + padding, me.right - lineWidths[cursor.line]); + } + } else if (i > 0 && y + lineHeight > me.bottom) { + x = cursor.x = x + columnSizes[cursor.line].width + padding; + cursor.line++; + y = cursor.y = _alignStartEnd(align, me.top + titleHeight + padding, me.bottom - columnSizes[cursor.line].height); + } + const realX = rtlHelper.x(x); + drawLegendBox(realX, y, legendItem); + x = _textX(textAlign, x + boxWidth + halfFontSize, isHorizontal ? x + width : me.right, opts.rtl); + fillText(rtlHelper.x(x), y, legendItem); + if (isHorizontal) { + cursor.x += width + padding; + } else { + cursor.y += lineHeight; + } + }); + restoreTextDirection(me.ctx, opts.textDirection); + } + drawTitle() { + const me = this; + const opts = me.options; + const titleOpts = opts.title; + const titleFont = toFont(titleOpts.font); + const titlePadding = toPadding(titleOpts.padding); + if (!titleOpts.display) { + return; + } + const rtlHelper = getRtlAdapter(opts.rtl, me.left, me.width); + const ctx = me.ctx; + const position = titleOpts.position; + const halfFontSize = titleFont.size / 2; + const topPaddingPlusHalfFontSize = titlePadding.top + halfFontSize; + let y; + let left = me.left; + let maxWidth = me.width; + if (this.isHorizontal()) { + maxWidth = Math.max(...me.lineWidths); + y = me.top + topPaddingPlusHalfFontSize; + left = _alignStartEnd(opts.align, left, me.right - maxWidth); + } else { + const maxHeight = me.columnSizes.reduce((acc, size) => Math.max(acc, size.height), 0); + y = topPaddingPlusHalfFontSize + _alignStartEnd(opts.align, me.top, me.bottom - maxHeight - opts.labels.padding - me._computeTitleHeight()); + } + const x = _alignStartEnd(position, left, left + maxWidth); + ctx.textAlign = rtlHelper.textAlign(_toLeftRightCenter(position)); + ctx.textBaseline = 'middle'; + ctx.strokeStyle = titleOpts.color; + ctx.fillStyle = titleOpts.color; + ctx.font = titleFont.string; + renderText(ctx, titleOpts.text, x, y, titleFont); + } + _computeTitleHeight() { + const titleOpts = this.options.title; + const titleFont = toFont(titleOpts.font); + const titlePadding = toPadding(titleOpts.padding); + return titleOpts.display ? titleFont.lineHeight + titlePadding.height : 0; + } + _getLegendItemAt(x, y) { + const me = this; + let i, hitBox, lh; + if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { + lh = me.legendHitBoxes; + for (i = 0; i < lh.length; ++i) { + hitBox = lh[i]; + if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { + return me.legendItems[i]; + } + } + } + return null; + } + handleEvent(e) { + const me = this; + const opts = me.options; + if (!isListened(e.type, opts)) { + return; + } + const hoveredItem = me._getLegendItemAt(e.x, e.y); + if (e.type === 'mousemove') { + const previous = me._hoveredItem; + const sameItem = itemsEqual(previous, hoveredItem); + if (previous && !sameItem) { + callback(opts.onLeave, [e, previous, me], me); + } + me._hoveredItem = hoveredItem; + if (hoveredItem && !sameItem) { + callback(opts.onHover, [e, hoveredItem, me], me); + } + } else if (hoveredItem) { + callback(opts.onClick, [e, hoveredItem, me], me); + } + } +} +function isListened(type, opts) { + if (type === 'mousemove' && (opts.onHover || opts.onLeave)) { + return true; + } + if (opts.onClick && (type === 'click' || type === 'mouseup')) { + return true; + } + return false; +} +var plugin_legend = { + id: 'legend', + _element: Legend, + start(chart, _args, options) { + const legend = chart.legend = new Legend({ctx: chart.ctx, options, chart}); + layouts.configure(chart, legend, options); + layouts.addBox(chart, legend); + }, + stop(chart) { + layouts.removeBox(chart, chart.legend); + delete chart.legend; + }, + beforeUpdate(chart, _args, options) { + const legend = chart.legend; + layouts.configure(chart, legend, options); + legend.options = options; + }, + afterUpdate(chart) { + const legend = chart.legend; + legend.buildLabels(); + legend.adjustHitBoxes(); + }, + afterEvent(chart, args) { + if (!args.replay) { + chart.legend.handleEvent(args.event); + } + }, + defaults: { + display: true, + position: 'top', + align: 'center', + fullSize: true, + reverse: false, + weight: 1000, + onClick(e, legendItem, legend) { + const index = legendItem.datasetIndex; + const ci = legend.chart; + if (ci.isDatasetVisible(index)) { + ci.hide(index); + legendItem.hidden = true; + } else { + ci.show(index); + legendItem.hidden = false; + } + }, + onHover: null, + onLeave: null, + labels: { + color: (ctx) => ctx.chart.options.color, + boxWidth: 40, + padding: 10, + generateLabels(chart) { + const datasets = chart.data.datasets; + const {labels: {usePointStyle, pointStyle, textAlign, color}} = chart.legend.options; + return chart._getSortedDatasetMetas().map((meta) => { + const style = meta.controller.getStyle(usePointStyle ? 0 : undefined); + const borderWidth = toPadding(style.borderWidth); + return { + text: datasets[meta.index].label, + fillStyle: style.backgroundColor, + fontColor: color, + hidden: !meta.visible, + lineCap: style.borderCapStyle, + lineDash: style.borderDash, + lineDashOffset: style.borderDashOffset, + lineJoin: style.borderJoinStyle, + lineWidth: (borderWidth.width + borderWidth.height) / 4, + strokeStyle: style.borderColor, + pointStyle: pointStyle || style.pointStyle, + rotation: style.rotation, + textAlign: textAlign || style.textAlign, + borderRadius: 0, + datasetIndex: meta.index + }; + }, this); + } + }, + title: { + color: (ctx) => ctx.chart.options.color, + display: false, + position: 'center', + text: '', + } + }, + descriptors: { + _scriptable: (name) => !name.startsWith('on'), + labels: { + _scriptable: (name) => !['generateLabels', 'filter', 'sort'].includes(name), + } + }, +}; + +class Title extends Element { + constructor(config) { + super(); + this.chart = config.chart; + this.options = config.options; + this.ctx = config.ctx; + this._padding = undefined; + this.top = undefined; + this.bottom = undefined; + this.left = undefined; + this.right = undefined; + this.width = undefined; + this.height = undefined; + this.position = undefined; + this.weight = undefined; + this.fullSize = undefined; + } + update(maxWidth, maxHeight) { + const me = this; + const opts = me.options; + me.left = 0; + me.top = 0; + if (!opts.display) { + me.width = me.height = me.right = me.bottom = 0; + return; + } + me.width = me.right = maxWidth; + me.height = me.bottom = maxHeight; + const lineCount = isArray(opts.text) ? opts.text.length : 1; + me._padding = toPadding(opts.padding); + const textSize = lineCount * toFont(opts.font).lineHeight + me._padding.height; + if (me.isHorizontal()) { + me.height = textSize; + } else { + me.width = textSize; + } + } + isHorizontal() { + const pos = this.options.position; + return pos === 'top' || pos === 'bottom'; + } + _drawArgs(offset) { + const {top, left, bottom, right, options} = this; + const align = options.align; + let rotation = 0; + let maxWidth, titleX, titleY; + if (this.isHorizontal()) { + titleX = _alignStartEnd(align, left, right); + titleY = top + offset; + maxWidth = right - left; + } else { + if (options.position === 'left') { + titleX = left + offset; + titleY = _alignStartEnd(align, bottom, top); + rotation = PI * -0.5; + } else { + titleX = right - offset; + titleY = _alignStartEnd(align, top, bottom); + rotation = PI * 0.5; + } + maxWidth = bottom - top; + } + return {titleX, titleY, maxWidth, rotation}; + } + draw() { + const me = this; + const ctx = me.ctx; + const opts = me.options; + if (!opts.display) { + return; + } + const fontOpts = toFont(opts.font); + const lineHeight = fontOpts.lineHeight; + const offset = lineHeight / 2 + me._padding.top; + const {titleX, titleY, maxWidth, rotation} = me._drawArgs(offset); + renderText(ctx, opts.text, 0, 0, fontOpts, { + color: opts.color, + maxWidth, + rotation, + textAlign: _toLeftRightCenter(opts.align), + textBaseline: 'middle', + translation: [titleX, titleY], + }); + } +} +function createTitle(chart, titleOpts) { + const title = new Title({ + ctx: chart.ctx, + options: titleOpts, + chart + }); + layouts.configure(chart, title, titleOpts); + layouts.addBox(chart, title); + chart.titleBlock = title; +} +var plugin_title = { + id: 'title', + _element: Title, + start(chart, _args, options) { + createTitle(chart, options); + }, + stop(chart) { + const titleBlock = chart.titleBlock; + layouts.removeBox(chart, titleBlock); + delete chart.titleBlock; + }, + beforeUpdate(chart, _args, options) { + const title = chart.titleBlock; + layouts.configure(chart, title, options); + title.options = options; + }, + defaults: { + align: 'center', + display: false, + font: { + weight: 'bold', + }, + fullSize: true, + padding: 10, + position: 'top', + text: '', + weight: 2000 + }, + defaultRoutes: { + color: 'color' + }, + descriptors: { + _scriptable: true, + _indexable: false, + }, +}; + +const map = new WeakMap(); +var plugin_subtitle = { + id: 'subtitle', + start(chart, _args, options) { + const title = new Title({ + ctx: chart.ctx, + options, + chart + }); + layouts.configure(chart, title, options); + layouts.addBox(chart, title); + map.set(chart, title); + }, + stop(chart) { + layouts.removeBox(chart, map.get(chart)); + map.delete(chart); + }, + beforeUpdate(chart, _args, options) { + const title = map.get(chart); + layouts.configure(chart, title, options); + title.options = options; + }, + defaults: { + align: 'center', + display: false, + font: { + weight: 'normal', + }, + fullSize: true, + padding: 0, + position: 'top', + text: '', + weight: 1500 + }, + defaultRoutes: { + color: 'color' + }, + descriptors: { + _scriptable: true, + _indexable: false, + }, +}; + +const positioners = { + average(items) { + if (!items.length) { + return false; + } + let i, len; + let x = 0; + let y = 0; + let count = 0; + for (i = 0, len = items.length; i < len; ++i) { + const el = items[i].element; + if (el && el.hasValue()) { + const pos = el.tooltipPosition(); + x += pos.x; + y += pos.y; + ++count; + } + } + return { + x: x / count, + y: y / count + }; + }, + nearest(items, eventPosition) { + if (!items.length) { + return false; + } + let x = eventPosition.x; + let y = eventPosition.y; + let minDistance = Number.POSITIVE_INFINITY; + let i, len, nearestElement; + for (i = 0, len = items.length; i < len; ++i) { + const el = items[i].element; + if (el && el.hasValue()) { + const center = el.getCenterPoint(); + const d = distanceBetweenPoints(eventPosition, center); + if (d < minDistance) { + minDistance = d; + nearestElement = el; + } + } + } + if (nearestElement) { + const tp = nearestElement.tooltipPosition(); + x = tp.x; + y = tp.y; + } + return { + x, + y + }; + } +}; +function pushOrConcat(base, toPush) { + if (toPush) { + if (isArray(toPush)) { + Array.prototype.push.apply(base, toPush); + } else { + base.push(toPush); + } + } + return base; +} +function splitNewlines(str) { + if ((typeof str === 'string' || str instanceof String) && str.indexOf('\n') > -1) { + return str.split('\n'); + } + return str; +} +function createTooltipItem(chart, item) { + const {element, datasetIndex, index} = item; + const controller = chart.getDatasetMeta(datasetIndex).controller; + const {label, value} = controller.getLabelAndValue(index); + return { + chart, + label, + parsed: controller.getParsed(index), + raw: chart.data.datasets[datasetIndex].data[index], + formattedValue: value, + dataset: controller.getDataset(), + dataIndex: index, + datasetIndex, + element + }; +} +function getTooltipSize(tooltip, options) { + const ctx = tooltip._chart.ctx; + const {body, footer, title} = tooltip; + const {boxWidth, boxHeight} = options; + const bodyFont = toFont(options.bodyFont); + const titleFont = toFont(options.titleFont); + const footerFont = toFont(options.footerFont); + const titleLineCount = title.length; + const footerLineCount = footer.length; + const bodyLineItemCount = body.length; + const padding = toPadding(options.padding); + let height = padding.height; + let width = 0; + let combinedBodyLength = body.reduce((count, bodyItem) => count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length, 0); + combinedBodyLength += tooltip.beforeBody.length + tooltip.afterBody.length; + if (titleLineCount) { + height += titleLineCount * titleFont.lineHeight + + (titleLineCount - 1) * options.titleSpacing + + options.titleMarginBottom; + } + if (combinedBodyLength) { + const bodyLineHeight = options.displayColors ? Math.max(boxHeight, bodyFont.lineHeight) : bodyFont.lineHeight; + height += bodyLineItemCount * bodyLineHeight + + (combinedBodyLength - bodyLineItemCount) * bodyFont.lineHeight + + (combinedBodyLength - 1) * options.bodySpacing; + } + if (footerLineCount) { + height += options.footerMarginTop + + footerLineCount * footerFont.lineHeight + + (footerLineCount - 1) * options.footerSpacing; + } + let widthPadding = 0; + const maxLineWidth = function(line) { + width = Math.max(width, ctx.measureText(line).width + widthPadding); + }; + ctx.save(); + ctx.font = titleFont.string; + each(tooltip.title, maxLineWidth); + ctx.font = bodyFont.string; + each(tooltip.beforeBody.concat(tooltip.afterBody), maxLineWidth); + widthPadding = options.displayColors ? (boxWidth + 2) : 0; + each(body, (bodyItem) => { + each(bodyItem.before, maxLineWidth); + each(bodyItem.lines, maxLineWidth); + each(bodyItem.after, maxLineWidth); + }); + widthPadding = 0; + ctx.font = footerFont.string; + each(tooltip.footer, maxLineWidth); + ctx.restore(); + width += padding.width; + return {width, height}; +} +function determineYAlign(chart, size) { + const {y, height} = size; + if (y < height / 2) { + return 'top'; + } else if (y > (chart.height - height / 2)) { + return 'bottom'; + } + return 'center'; +} +function doesNotFitWithAlign(xAlign, chart, options, size) { + const {x, width} = size; + const caret = options.caretSize + options.caretPadding; + if (xAlign === 'left' && x + width + caret > chart.width) { + return true; + } + if (xAlign === 'right' && x - width - caret < 0) { + return true; + } +} +function determineXAlign(chart, options, size, yAlign) { + const {x, width} = size; + const {width: chartWidth, chartArea: {left, right}} = chart; + let xAlign = 'center'; + if (yAlign === 'center') { + xAlign = x <= (left + right) / 2 ? 'left' : 'right'; + } else if (x <= width / 2) { + xAlign = 'left'; + } else if (x >= chartWidth - width / 2) { + xAlign = 'right'; + } + if (doesNotFitWithAlign(xAlign, chart, options, size)) { + xAlign = 'center'; + } + return xAlign; +} +function determineAlignment(chart, options, size) { + const yAlign = options.yAlign || determineYAlign(chart, size); + return { + xAlign: options.xAlign || determineXAlign(chart, options, size, yAlign), + yAlign + }; +} +function alignX(size, xAlign) { + let {x, width} = size; + if (xAlign === 'right') { + x -= width; + } else if (xAlign === 'center') { + x -= (width / 2); + } + return x; +} +function alignY(size, yAlign, paddingAndSize) { + let {y, height} = size; + if (yAlign === 'top') { + y += paddingAndSize; + } else if (yAlign === 'bottom') { + y -= height + paddingAndSize; + } else { + y -= (height / 2); + } + return y; +} +function getBackgroundPoint(options, size, alignment, chart) { + const {caretSize, caretPadding, cornerRadius} = options; + const {xAlign, yAlign} = alignment; + const paddingAndSize = caretSize + caretPadding; + const radiusAndPadding = cornerRadius + caretPadding; + let x = alignX(size, xAlign); + const y = alignY(size, yAlign, paddingAndSize); + if (yAlign === 'center') { + if (xAlign === 'left') { + x += paddingAndSize; + } else if (xAlign === 'right') { + x -= paddingAndSize; + } + } else if (xAlign === 'left') { + x -= radiusAndPadding; + } else if (xAlign === 'right') { + x += radiusAndPadding; + } + return { + x: _limitValue(x, 0, chart.width - size.width), + y: _limitValue(y, 0, chart.height - size.height) + }; +} +function getAlignedX(tooltip, align, options) { + const padding = toPadding(options.padding); + return align === 'center' + ? tooltip.x + tooltip.width / 2 + : align === 'right' + ? tooltip.x + tooltip.width - padding.right + : tooltip.x + padding.left; +} +function getBeforeAfterBodyLines(callback) { + return pushOrConcat([], splitNewlines(callback)); +} +function createTooltipContext(parent, tooltip, tooltipItems) { + return Object.assign(Object.create(parent), { + tooltip, + tooltipItems, + type: 'tooltip' + }); +} +function overrideCallbacks(callbacks, context) { + const override = context && context.dataset && context.dataset.tooltip && context.dataset.tooltip.callbacks; + return override ? callbacks.override(override) : callbacks; +} +class Tooltip extends Element { + constructor(config) { + super(); + this.opacity = 0; + this._active = []; + this._chart = config._chart; + this._eventPosition = undefined; + this._size = undefined; + this._cachedAnimations = undefined; + this._tooltipItems = []; + this.$animations = undefined; + this.$context = undefined; + this.options = config.options; + this.dataPoints = undefined; + this.title = undefined; + this.beforeBody = undefined; + this.body = undefined; + this.afterBody = undefined; + this.footer = undefined; + this.xAlign = undefined; + this.yAlign = undefined; + this.x = undefined; + this.y = undefined; + this.height = undefined; + this.width = undefined; + this.caretX = undefined; + this.caretY = undefined; + this.labelColors = undefined; + this.labelPointStyles = undefined; + this.labelTextColors = undefined; + } + initialize(options) { + this.options = options; + this._cachedAnimations = undefined; + this.$context = undefined; + } + _resolveAnimations() { + const me = this; + const cached = me._cachedAnimations; + if (cached) { + return cached; + } + const chart = me._chart; + const options = me.options.setContext(me.getContext()); + const opts = options.enabled && chart.options.animation && options.animations; + const animations = new Animations(me._chart, opts); + if (opts._cacheable) { + me._cachedAnimations = Object.freeze(animations); + } + return animations; + } + getContext() { + const me = this; + return me.$context || + (me.$context = createTooltipContext(me._chart.getContext(), me, me._tooltipItems)); + } + getTitle(context, options) { + const me = this; + const {callbacks} = options; + const beforeTitle = callbacks.beforeTitle.apply(me, [context]); + const title = callbacks.title.apply(me, [context]); + const afterTitle = callbacks.afterTitle.apply(me, [context]); + let lines = []; + lines = pushOrConcat(lines, splitNewlines(beforeTitle)); + lines = pushOrConcat(lines, splitNewlines(title)); + lines = pushOrConcat(lines, splitNewlines(afterTitle)); + return lines; + } + getBeforeBody(tooltipItems, options) { + return getBeforeAfterBodyLines(options.callbacks.beforeBody.apply(this, [tooltipItems])); + } + getBody(tooltipItems, options) { + const me = this; + const {callbacks} = options; + const bodyItems = []; + each(tooltipItems, (context) => { + const bodyItem = { + before: [], + lines: [], + after: [] + }; + const scoped = overrideCallbacks(callbacks, context); + pushOrConcat(bodyItem.before, splitNewlines(scoped.beforeLabel.call(me, context))); + pushOrConcat(bodyItem.lines, scoped.label.call(me, context)); + pushOrConcat(bodyItem.after, splitNewlines(scoped.afterLabel.call(me, context))); + bodyItems.push(bodyItem); + }); + return bodyItems; + } + getAfterBody(tooltipItems, options) { + return getBeforeAfterBodyLines(options.callbacks.afterBody.apply(this, [tooltipItems])); + } + getFooter(tooltipItems, options) { + const me = this; + const {callbacks} = options; + const beforeFooter = callbacks.beforeFooter.apply(me, [tooltipItems]); + const footer = callbacks.footer.apply(me, [tooltipItems]); + const afterFooter = callbacks.afterFooter.apply(me, [tooltipItems]); + let lines = []; + lines = pushOrConcat(lines, splitNewlines(beforeFooter)); + lines = pushOrConcat(lines, splitNewlines(footer)); + lines = pushOrConcat(lines, splitNewlines(afterFooter)); + return lines; + } + _createItems(options) { + const me = this; + const active = me._active; + const data = me._chart.data; + const labelColors = []; + const labelPointStyles = []; + const labelTextColors = []; + let tooltipItems = []; + let i, len; + for (i = 0, len = active.length; i < len; ++i) { + tooltipItems.push(createTooltipItem(me._chart, active[i])); + } + if (options.filter) { + tooltipItems = tooltipItems.filter((element, index, array) => options.filter(element, index, array, data)); + } + if (options.itemSort) { + tooltipItems = tooltipItems.sort((a, b) => options.itemSort(a, b, data)); + } + each(tooltipItems, (context) => { + const scoped = overrideCallbacks(options.callbacks, context); + labelColors.push(scoped.labelColor.call(me, context)); + labelPointStyles.push(scoped.labelPointStyle.call(me, context)); + labelTextColors.push(scoped.labelTextColor.call(me, context)); + }); + me.labelColors = labelColors; + me.labelPointStyles = labelPointStyles; + me.labelTextColors = labelTextColors; + me.dataPoints = tooltipItems; + return tooltipItems; + } + update(changed, replay) { + const me = this; + const options = me.options.setContext(me.getContext()); + const active = me._active; + let properties; + let tooltipItems = []; + if (!active.length) { + if (me.opacity !== 0) { + properties = { + opacity: 0 + }; + } + } else { + const position = positioners[options.position].call(me, active, me._eventPosition); + tooltipItems = me._createItems(options); + me.title = me.getTitle(tooltipItems, options); + me.beforeBody = me.getBeforeBody(tooltipItems, options); + me.body = me.getBody(tooltipItems, options); + me.afterBody = me.getAfterBody(tooltipItems, options); + me.footer = me.getFooter(tooltipItems, options); + const size = me._size = getTooltipSize(me, options); + const positionAndSize = Object.assign({}, position, size); + const alignment = determineAlignment(me._chart, options, positionAndSize); + const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, me._chart); + me.xAlign = alignment.xAlign; + me.yAlign = alignment.yAlign; + properties = { + opacity: 1, + x: backgroundPoint.x, + y: backgroundPoint.y, + width: size.width, + height: size.height, + caretX: position.x, + caretY: position.y + }; + } + me._tooltipItems = tooltipItems; + me.$context = undefined; + if (properties) { + me._resolveAnimations().update(me, properties); + } + if (changed && options.external) { + options.external.call(me, {chart: me._chart, tooltip: me, replay}); + } + } + drawCaret(tooltipPoint, ctx, size, options) { + const caretPosition = this.getCaretPosition(tooltipPoint, size, options); + ctx.lineTo(caretPosition.x1, caretPosition.y1); + ctx.lineTo(caretPosition.x2, caretPosition.y2); + ctx.lineTo(caretPosition.x3, caretPosition.y3); + } + getCaretPosition(tooltipPoint, size, options) { + const {xAlign, yAlign} = this; + const {cornerRadius, caretSize} = options; + const {x: ptX, y: ptY} = tooltipPoint; + const {width, height} = size; + let x1, x2, x3, y1, y2, y3; + if (yAlign === 'center') { + y2 = ptY + (height / 2); + if (xAlign === 'left') { + x1 = ptX; + x2 = x1 - caretSize; + y1 = y2 + caretSize; + y3 = y2 - caretSize; + } else { + x1 = ptX + width; + x2 = x1 + caretSize; + y1 = y2 - caretSize; + y3 = y2 + caretSize; + } + x3 = x1; + } else { + if (xAlign === 'left') { + x2 = ptX + cornerRadius + (caretSize); + } else if (xAlign === 'right') { + x2 = ptX + width - cornerRadius - caretSize; + } else { + x2 = this.caretX; + } + if (yAlign === 'top') { + y1 = ptY; + y2 = y1 - caretSize; + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } else { + y1 = ptY + height; + y2 = y1 + caretSize; + x1 = x2 + caretSize; + x3 = x2 - caretSize; + } + y3 = y1; + } + return {x1, x2, x3, y1, y2, y3}; + } + drawTitle(pt, ctx, options) { + const me = this; + const title = me.title; + const length = title.length; + let titleFont, titleSpacing, i; + if (length) { + const rtlHelper = getRtlAdapter(options.rtl, me.x, me.width); + pt.x = getAlignedX(me, options.titleAlign, options); + ctx.textAlign = rtlHelper.textAlign(options.titleAlign); + ctx.textBaseline = 'middle'; + titleFont = toFont(options.titleFont); + titleSpacing = options.titleSpacing; + ctx.fillStyle = options.titleColor; + ctx.font = titleFont.string; + for (i = 0; i < length; ++i) { + ctx.fillText(title[i], rtlHelper.x(pt.x), pt.y + titleFont.lineHeight / 2); + pt.y += titleFont.lineHeight + titleSpacing; + if (i + 1 === length) { + pt.y += options.titleMarginBottom - titleSpacing; + } + } + } + } + _drawColorBox(ctx, pt, i, rtlHelper, options) { + const me = this; + const labelColors = me.labelColors[i]; + const labelPointStyle = me.labelPointStyles[i]; + const {boxHeight, boxWidth} = options; + const bodyFont = toFont(options.bodyFont); + const colorX = getAlignedX(me, 'left', options); + const rtlColorX = rtlHelper.x(colorX); + const yOffSet = boxHeight < bodyFont.lineHeight ? (bodyFont.lineHeight - boxHeight) / 2 : 0; + const colorY = pt.y + yOffSet; + if (options.usePointStyle) { + const drawOptions = { + radius: Math.min(boxWidth, boxHeight) / 2, + pointStyle: labelPointStyle.pointStyle, + rotation: labelPointStyle.rotation, + borderWidth: 1 + }; + const centerX = rtlHelper.leftForLtr(rtlColorX, boxWidth) + boxWidth / 2; + const centerY = colorY + boxHeight / 2; + ctx.strokeStyle = options.multiKeyBackground; + ctx.fillStyle = options.multiKeyBackground; + drawPoint(ctx, drawOptions, centerX, centerY); + ctx.strokeStyle = labelColors.borderColor; + ctx.fillStyle = labelColors.backgroundColor; + drawPoint(ctx, drawOptions, centerX, centerY); + } else { + ctx.lineWidth = labelColors.borderWidth || 1; + ctx.strokeStyle = labelColors.borderColor; + ctx.setLineDash(labelColors.borderDash || []); + ctx.lineDashOffset = labelColors.borderDashOffset || 0; + const outerX = rtlHelper.leftForLtr(rtlColorX, boxWidth); + const innerX = rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), boxWidth - 2); + const borderRadius = toTRBLCorners(labelColors.borderRadius); + if (Object.values(borderRadius).some(v => v !== 0)) { + ctx.beginPath(); + ctx.fillStyle = options.multiKeyBackground; + addRoundedRectPath(ctx, { + x: outerX, + y: colorY, + w: boxWidth, + h: boxHeight, + radius: borderRadius, + }); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = labelColors.backgroundColor; + ctx.beginPath(); + addRoundedRectPath(ctx, { + x: innerX, + y: colorY + 1, + w: boxWidth - 2, + h: boxHeight - 2, + radius: borderRadius, + }); + ctx.fill(); + } else { + ctx.fillStyle = options.multiKeyBackground; + ctx.fillRect(outerX, colorY, boxWidth, boxHeight); + ctx.strokeRect(outerX, colorY, boxWidth, boxHeight); + ctx.fillStyle = labelColors.backgroundColor; + ctx.fillRect(innerX, colorY + 1, boxWidth - 2, boxHeight - 2); + } + } + ctx.fillStyle = me.labelTextColors[i]; + } + drawBody(pt, ctx, options) { + const me = this; + const {body} = me; + const {bodySpacing, bodyAlign, displayColors, boxHeight, boxWidth} = options; + const bodyFont = toFont(options.bodyFont); + let bodyLineHeight = bodyFont.lineHeight; + let xLinePadding = 0; + const rtlHelper = getRtlAdapter(options.rtl, me.x, me.width); + const fillLineOfText = function(line) { + ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyLineHeight / 2); + pt.y += bodyLineHeight + bodySpacing; + }; + const bodyAlignForCalculation = rtlHelper.textAlign(bodyAlign); + let bodyItem, textColor, lines, i, j, ilen, jlen; + ctx.textAlign = bodyAlign; + ctx.textBaseline = 'middle'; + ctx.font = bodyFont.string; + pt.x = getAlignedX(me, bodyAlignForCalculation, options); + ctx.fillStyle = options.bodyColor; + each(me.beforeBody, fillLineOfText); + xLinePadding = displayColors && bodyAlignForCalculation !== 'right' + ? bodyAlign === 'center' ? (boxWidth / 2 + 1) : (boxWidth + 2) + : 0; + for (i = 0, ilen = body.length; i < ilen; ++i) { + bodyItem = body[i]; + textColor = me.labelTextColors[i]; + ctx.fillStyle = textColor; + each(bodyItem.before, fillLineOfText); + lines = bodyItem.lines; + if (displayColors && lines.length) { + me._drawColorBox(ctx, pt, i, rtlHelper, options); + bodyLineHeight = Math.max(bodyFont.lineHeight, boxHeight); + } + for (j = 0, jlen = lines.length; j < jlen; ++j) { + fillLineOfText(lines[j]); + bodyLineHeight = bodyFont.lineHeight; + } + each(bodyItem.after, fillLineOfText); + } + xLinePadding = 0; + bodyLineHeight = bodyFont.lineHeight; + each(me.afterBody, fillLineOfText); + pt.y -= bodySpacing; + } + drawFooter(pt, ctx, options) { + const me = this; + const footer = me.footer; + const length = footer.length; + let footerFont, i; + if (length) { + const rtlHelper = getRtlAdapter(options.rtl, me.x, me.width); + pt.x = getAlignedX(me, options.footerAlign, options); + pt.y += options.footerMarginTop; + ctx.textAlign = rtlHelper.textAlign(options.footerAlign); + ctx.textBaseline = 'middle'; + footerFont = toFont(options.footerFont); + ctx.fillStyle = options.footerColor; + ctx.font = footerFont.string; + for (i = 0; i < length; ++i) { + ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFont.lineHeight / 2); + pt.y += footerFont.lineHeight + options.footerSpacing; + } + } + } + drawBackground(pt, ctx, tooltipSize, options) { + const {xAlign, yAlign} = this; + const {x, y} = pt; + const {width, height} = tooltipSize; + const radius = options.cornerRadius; + ctx.fillStyle = options.backgroundColor; + ctx.strokeStyle = options.borderColor; + ctx.lineWidth = options.borderWidth; + ctx.beginPath(); + ctx.moveTo(x + radius, y); + if (yAlign === 'top') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + if (yAlign === 'center' && xAlign === 'right') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + if (yAlign === 'bottom') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + if (yAlign === 'center' && xAlign === 'left') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + ctx.fill(); + if (options.borderWidth > 0) { + ctx.stroke(); + } + } + _updateAnimationTarget(options) { + const me = this; + const chart = me._chart; + const anims = me.$animations; + const animX = anims && anims.x; + const animY = anims && anims.y; + if (animX || animY) { + const position = positioners[options.position].call(me, me._active, me._eventPosition); + if (!position) { + return; + } + const size = me._size = getTooltipSize(me, options); + const positionAndSize = Object.assign({}, position, me._size); + const alignment = determineAlignment(chart, options, positionAndSize); + const point = getBackgroundPoint(options, positionAndSize, alignment, chart); + if (animX._to !== point.x || animY._to !== point.y) { + me.xAlign = alignment.xAlign; + me.yAlign = alignment.yAlign; + me.width = size.width; + me.height = size.height; + me.caretX = position.x; + me.caretY = position.y; + me._resolveAnimations().update(me, point); + } + } + } + draw(ctx) { + const me = this; + const options = me.options.setContext(me.getContext()); + let opacity = me.opacity; + if (!opacity) { + return; + } + me._updateAnimationTarget(options); + const tooltipSize = { + width: me.width, + height: me.height + }; + const pt = { + x: me.x, + y: me.y + }; + opacity = Math.abs(opacity) < 1e-3 ? 0 : opacity; + const padding = toPadding(options.padding); + const hasTooltipContent = me.title.length || me.beforeBody.length || me.body.length || me.afterBody.length || me.footer.length; + if (options.enabled && hasTooltipContent) { + ctx.save(); + ctx.globalAlpha = opacity; + me.drawBackground(pt, ctx, tooltipSize, options); + overrideTextDirection(ctx, options.textDirection); + pt.y += padding.top; + me.drawTitle(pt, ctx, options); + me.drawBody(pt, ctx, options); + me.drawFooter(pt, ctx, options); + restoreTextDirection(ctx, options.textDirection); + ctx.restore(); + } + } + getActiveElements() { + return this._active || []; + } + setActiveElements(activeElements, eventPosition) { + const me = this; + const lastActive = me._active; + const active = activeElements.map(({datasetIndex, index}) => { + const meta = me._chart.getDatasetMeta(datasetIndex); + if (!meta) { + throw new Error('Cannot find a dataset at index ' + datasetIndex); + } + return { + datasetIndex, + element: meta.data[index], + index, + }; + }); + const changed = !_elementsEqual(lastActive, active); + const positionChanged = me._positionChanged(active, eventPosition); + if (changed || positionChanged) { + me._active = active; + me._eventPosition = eventPosition; + me.update(true); + } + } + handleEvent(e, replay) { + const me = this; + const options = me.options; + const lastActive = me._active || []; + let changed = false; + let active = []; + if (e.type !== 'mouseout') { + active = me._chart.getElementsAtEventForMode(e, options.mode, options, replay); + if (options.reverse) { + active.reverse(); + } + } + const positionChanged = me._positionChanged(active, e); + changed = replay || !_elementsEqual(active, lastActive) || positionChanged; + if (changed) { + me._active = active; + if (options.enabled || options.external) { + me._eventPosition = { + x: e.x, + y: e.y + }; + me.update(true, replay); + } + } + return changed; + } + _positionChanged(active, e) { + const {caretX, caretY, options} = this; + const position = positioners[options.position].call(this, active, e); + return position !== false && (caretX !== position.x || caretY !== position.y); + } +} +Tooltip.positioners = positioners; +var plugin_tooltip = { + id: 'tooltip', + _element: Tooltip, + positioners, + afterInit(chart, _args, options) { + if (options) { + chart.tooltip = new Tooltip({_chart: chart, options}); + } + }, + beforeUpdate(chart, _args, options) { + if (chart.tooltip) { + chart.tooltip.initialize(options); + } + }, + reset(chart, _args, options) { + if (chart.tooltip) { + chart.tooltip.initialize(options); + } + }, + afterDraw(chart) { + const tooltip = chart.tooltip; + const args = { + tooltip + }; + if (chart.notifyPlugins('beforeTooltipDraw', args) === false) { + return; + } + if (tooltip) { + tooltip.draw(chart.ctx); + } + chart.notifyPlugins('afterTooltipDraw', args); + }, + afterEvent(chart, args) { + if (chart.tooltip) { + const useFinalPosition = args.replay; + if (chart.tooltip.handleEvent(args.event, useFinalPosition)) { + args.changed = true; + } + } + }, + defaults: { + enabled: true, + external: null, + position: 'average', + backgroundColor: 'rgba(0,0,0,0.8)', + titleColor: '#fff', + titleFont: { + weight: 'bold', + }, + titleSpacing: 2, + titleMarginBottom: 6, + titleAlign: 'left', + bodyColor: '#fff', + bodySpacing: 2, + bodyFont: { + }, + bodyAlign: 'left', + footerColor: '#fff', + footerSpacing: 2, + footerMarginTop: 6, + footerFont: { + weight: 'bold', + }, + footerAlign: 'left', + padding: 6, + caretPadding: 2, + caretSize: 5, + cornerRadius: 6, + boxHeight: (ctx, opts) => opts.bodyFont.size, + boxWidth: (ctx, opts) => opts.bodyFont.size, + multiKeyBackground: '#fff', + displayColors: true, + borderColor: 'rgba(0,0,0,0)', + borderWidth: 0, + animation: { + duration: 400, + easing: 'easeOutQuart', + }, + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'width', 'height', 'caretX', 'caretY'], + }, + opacity: { + easing: 'linear', + duration: 200 + } + }, + callbacks: { + beforeTitle: noop, + title(tooltipItems) { + if (tooltipItems.length > 0) { + const item = tooltipItems[0]; + const labels = item.chart.data.labels; + const labelCount = labels ? labels.length : 0; + if (this && this.options && this.options.mode === 'dataset') { + return item.dataset.label || ''; + } else if (item.label) { + return item.label; + } else if (labelCount > 0 && item.dataIndex < labelCount) { + return labels[item.dataIndex]; + } + } + return ''; + }, + afterTitle: noop, + beforeBody: noop, + beforeLabel: noop, + label(tooltipItem) { + if (this && this.options && this.options.mode === 'dataset') { + return tooltipItem.label + ': ' + tooltipItem.formattedValue || tooltipItem.formattedValue; + } + let label = tooltipItem.dataset.label || ''; + if (label) { + label += ': '; + } + const value = tooltipItem.formattedValue; + if (!isNullOrUndef(value)) { + label += value; + } + return label; + }, + labelColor(tooltipItem) { + const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); + const options = meta.controller.getStyle(tooltipItem.dataIndex); + return { + borderColor: options.borderColor, + backgroundColor: options.backgroundColor, + borderWidth: options.borderWidth, + borderDash: options.borderDash, + borderDashOffset: options.borderDashOffset, + borderRadius: 0, + }; + }, + labelTextColor() { + return this.options.bodyColor; + }, + labelPointStyle(tooltipItem) { + const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); + const options = meta.controller.getStyle(tooltipItem.dataIndex); + return { + pointStyle: options.pointStyle, + rotation: options.rotation, + }; + }, + afterLabel: noop, + afterBody: noop, + beforeFooter: noop, + footer: noop, + afterFooter: noop + } + }, + defaultRoutes: { + bodyFont: 'font', + footerFont: 'font', + titleFont: 'font' + }, + descriptors: { + _scriptable: (name) => name !== 'filter' && name !== 'itemSort' && name !== 'external', + _indexable: false, + callbacks: { + _scriptable: false, + _indexable: false, + }, + animation: { + _fallback: false + }, + animations: { + _fallback: 'animation' + } + }, + additionalOptionScopes: ['interaction'] +}; + +var plugins = /*#__PURE__*/Object.freeze({ +__proto__: null, +Decimation: plugin_decimation, +Filler: plugin_filler, +Legend: plugin_legend, +SubTitle: plugin_subtitle, +Title: plugin_title, +Tooltip: plugin_tooltip +}); + +const addIfString = (labels, raw, index) => typeof raw === 'string' + ? labels.push(raw) - 1 + : isNaN(raw) ? null : index; +function findOrAddLabel(labels, raw, index) { + const first = labels.indexOf(raw); + if (first === -1) { + return addIfString(labels, raw, index); + } + const last = labels.lastIndexOf(raw); + return first !== last ? index : first; +} +const validIndex = (index, max) => index === null ? null : _limitValue(Math.round(index), 0, max); +class CategoryScale extends Scale { + constructor(cfg) { + super(cfg); + this._startValue = undefined; + this._valueRange = 0; + } + parse(raw, index) { + if (isNullOrUndef(raw)) { + return null; + } + const labels = this.getLabels(); + index = isFinite(index) && labels[index] === raw ? index + : findOrAddLabel(labels, raw, valueOrDefault(index, raw)); + return validIndex(index, labels.length - 1); + } + determineDataLimits() { + const me = this; + const {minDefined, maxDefined} = me.getUserBounds(); + let {min, max} = me.getMinMax(true); + if (me.options.bounds === 'ticks') { + if (!minDefined) { + min = 0; + } + if (!maxDefined) { + max = me.getLabels().length - 1; + } + } + me.min = min; + me.max = max; + } + buildTicks() { + const me = this; + const min = me.min; + const max = me.max; + const offset = me.options.offset; + const ticks = []; + let labels = me.getLabels(); + labels = (min === 0 && max === labels.length - 1) ? labels : labels.slice(min, max + 1); + me._valueRange = Math.max(labels.length - (offset ? 0 : 1), 1); + me._startValue = me.min - (offset ? 0.5 : 0); + for (let value = min; value <= max; value++) { + ticks.push({value}); + } + return ticks; + } + getLabelForValue(value) { + const me = this; + const labels = me.getLabels(); + if (value >= 0 && value < labels.length) { + return labels[value]; + } + return value; + } + configure() { + const me = this; + super.configure(); + if (!me.isHorizontal()) { + me._reversePixels = !me._reversePixels; + } + } + getPixelForValue(value) { + const me = this; + if (typeof value !== 'number') { + value = me.parse(value); + } + return value === null ? NaN : me.getPixelForDecimal((value - me._startValue) / me._valueRange); + } + getPixelForTick(index) { + const me = this; + const ticks = me.ticks; + if (index < 0 || index > ticks.length - 1) { + return null; + } + return me.getPixelForValue(ticks[index].value); + } + getValueForPixel(pixel) { + const me = this; + return Math.round(me._startValue + me.getDecimalForPixel(pixel) * me._valueRange); + } + getBasePixel() { + return this.bottom; + } +} +CategoryScale.id = 'category'; +CategoryScale.defaults = { + ticks: { + callback: CategoryScale.prototype.getLabelForValue + } +}; + +function generateTicks$1(generationOptions, dataRange) { + const ticks = []; + const MIN_SPACING = 1e-14; + const {bounds, step, min, max, precision, count, maxTicks, maxDigits, includeBounds} = generationOptions; + const unit = step || 1; + const maxSpaces = maxTicks - 1; + const {min: rmin, max: rmax} = dataRange; + const minDefined = !isNullOrUndef(min); + const maxDefined = !isNullOrUndef(max); + const countDefined = !isNullOrUndef(count); + const minSpacing = (rmax - rmin) / (maxDigits + 1); + let spacing = niceNum((rmax - rmin) / maxSpaces / unit) * unit; + let factor, niceMin, niceMax, numSpaces; + if (spacing < MIN_SPACING && !minDefined && !maxDefined) { + return [{value: rmin}, {value: rmax}]; + } + numSpaces = Math.ceil(rmax / spacing) - Math.floor(rmin / spacing); + if (numSpaces > maxSpaces) { + spacing = niceNum(numSpaces * spacing / maxSpaces / unit) * unit; + } + if (!isNullOrUndef(precision)) { + factor = Math.pow(10, precision); + spacing = Math.ceil(spacing * factor) / factor; + } + if (bounds === 'ticks') { + niceMin = Math.floor(rmin / spacing) * spacing; + niceMax = Math.ceil(rmax / spacing) * spacing; + } else { + niceMin = rmin; + niceMax = rmax; + } + if (minDefined && maxDefined && step && almostWhole((max - min) / step, spacing / 1000)) { + numSpaces = Math.round(Math.min((max - min) / spacing, maxTicks)); + spacing = (max - min) / numSpaces; + niceMin = min; + niceMax = max; + } else if (countDefined) { + niceMin = minDefined ? min : niceMin; + niceMax = maxDefined ? max : niceMax; + numSpaces = count - 1; + spacing = (niceMax - niceMin) / numSpaces; + } else { + numSpaces = (niceMax - niceMin) / spacing; + if (almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { + numSpaces = Math.round(numSpaces); + } else { + numSpaces = Math.ceil(numSpaces); + } + } + const decimalPlaces = Math.max( + _decimalPlaces(spacing), + _decimalPlaces(niceMin) + ); + factor = Math.pow(10, isNullOrUndef(precision) ? decimalPlaces : precision); + niceMin = Math.round(niceMin * factor) / factor; + niceMax = Math.round(niceMax * factor) / factor; + let j = 0; + if (minDefined) { + if (includeBounds && niceMin !== min) { + ticks.push({value: min}); + if (niceMin < min) { + j++; + } + if (almostEquals(Math.round((niceMin + j * spacing) * factor) / factor, min, relativeLabelSize(min, minSpacing, generationOptions))) { + j++; + } + } else if (niceMin < min) { + j++; + } + } + for (; j < numSpaces; ++j) { + ticks.push({value: Math.round((niceMin + j * spacing) * factor) / factor}); + } + if (maxDefined && includeBounds && niceMax !== max) { + if (almostEquals(ticks[ticks.length - 1].value, max, relativeLabelSize(max, minSpacing, generationOptions))) { + ticks[ticks.length - 1].value = max; + } else { + ticks.push({value: max}); + } + } else if (!maxDefined || niceMax === max) { + ticks.push({value: niceMax}); + } + return ticks; +} +function relativeLabelSize(value, minSpacing, {horizontal, minRotation}) { + const rad = toRadians(minRotation); + const ratio = (horizontal ? Math.sin(rad) : Math.cos(rad)) || 0.001; + const length = 0.75 * minSpacing * ('' + value).length; + return Math.min(minSpacing / ratio, length); +} +class LinearScaleBase extends Scale { + constructor(cfg) { + super(cfg); + this.start = undefined; + this.end = undefined; + this._startValue = undefined; + this._endValue = undefined; + this._valueRange = 0; + } + parse(raw, index) { + if (isNullOrUndef(raw)) { + return null; + } + if ((typeof raw === 'number' || raw instanceof Number) && !isFinite(+raw)) { + return null; + } + return +raw; + } + handleTickRangeOptions() { + const me = this; + const {beginAtZero} = me.options; + const {minDefined, maxDefined} = me.getUserBounds(); + let {min, max} = me; + const setMin = v => (min = minDefined ? min : v); + const setMax = v => (max = maxDefined ? max : v); + if (beginAtZero) { + const minSign = sign(min); + const maxSign = sign(max); + if (minSign < 0 && maxSign < 0) { + setMax(0); + } else if (minSign > 0 && maxSign > 0) { + setMin(0); + } + } + if (min === max) { + setMax(max + 1); + if (!beginAtZero) { + setMin(min - 1); + } + } + me.min = min; + me.max = max; + } + getTickLimit() { + const me = this; + const tickOpts = me.options.ticks; + let {maxTicksLimit, stepSize} = tickOpts; + let maxTicks; + if (stepSize) { + maxTicks = Math.ceil(me.max / stepSize) - Math.floor(me.min / stepSize) + 1; + } else { + maxTicks = me.computeTickLimit(); + maxTicksLimit = maxTicksLimit || 11; + } + if (maxTicksLimit) { + maxTicks = Math.min(maxTicksLimit, maxTicks); + } + return maxTicks; + } + computeTickLimit() { + return Number.POSITIVE_INFINITY; + } + buildTicks() { + const me = this; + const opts = me.options; + const tickOpts = opts.ticks; + let maxTicks = me.getTickLimit(); + maxTicks = Math.max(2, maxTicks); + const numericGeneratorOptions = { + maxTicks, + bounds: opts.bounds, + min: opts.min, + max: opts.max, + precision: tickOpts.precision, + step: tickOpts.stepSize, + count: tickOpts.count, + maxDigits: me._maxDigits(), + horizontal: me.isHorizontal(), + minRotation: tickOpts.minRotation || 0, + includeBounds: tickOpts.includeBounds !== false + }; + const dataRange = me._range || me; + const ticks = generateTicks$1(numericGeneratorOptions, dataRange); + if (opts.bounds === 'ticks') { + _setMinAndMaxByKey(ticks, me, 'value'); + } + if (opts.reverse) { + ticks.reverse(); + me.start = me.max; + me.end = me.min; + } else { + me.start = me.min; + me.end = me.max; + } + return ticks; + } + configure() { + const me = this; + const ticks = me.ticks; + let start = me.min; + let end = me.max; + super.configure(); + if (me.options.offset && ticks.length) { + const offset = (end - start) / Math.max(ticks.length - 1, 1) / 2; + start -= offset; + end += offset; + } + me._startValue = start; + me._endValue = end; + me._valueRange = end - start; + } + getLabelForValue(value) { + return formatNumber(value, this.chart.options.locale); + } +} + +class LinearScale extends LinearScaleBase { + determineDataLimits() { + const me = this; + const {min, max} = me.getMinMax(true); + me.min = isNumberFinite(min) ? min : 0; + me.max = isNumberFinite(max) ? max : 1; + me.handleTickRangeOptions(); + } + computeTickLimit() { + const me = this; + const horizontal = me.isHorizontal(); + const length = horizontal ? me.width : me.height; + const minRotation = toRadians(me.options.ticks.minRotation); + const ratio = (horizontal ? Math.sin(minRotation) : Math.cos(minRotation)) || 0.001; + const tickFont = me._resolveTickFontOptions(0); + return Math.ceil(length / Math.min(40, tickFont.lineHeight / ratio)); + } + getPixelForValue(value) { + return value === null ? NaN : this.getPixelForDecimal((value - this._startValue) / this._valueRange); + } + getValueForPixel(pixel) { + return this._startValue + this.getDecimalForPixel(pixel) * this._valueRange; + } +} +LinearScale.id = 'linear'; +LinearScale.defaults = { + ticks: { + callback: Ticks.formatters.numeric + } +}; + +function isMajor(tickVal) { + const remain = tickVal / (Math.pow(10, Math.floor(log10(tickVal)))); + return remain === 1; +} +function generateTicks(generationOptions, dataRange) { + const endExp = Math.floor(log10(dataRange.max)); + const endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); + const ticks = []; + let tickVal = finiteOrDefault(generationOptions.min, Math.pow(10, Math.floor(log10(dataRange.min)))); + let exp = Math.floor(log10(tickVal)); + let significand = Math.floor(tickVal / Math.pow(10, exp)); + let precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; + do { + ticks.push({value: tickVal, major: isMajor(tickVal)}); + ++significand; + if (significand === 10) { + significand = 1; + ++exp; + precision = exp >= 0 ? 1 : precision; + } + tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; + } while (exp < endExp || (exp === endExp && significand < endSignificand)); + const lastTick = finiteOrDefault(generationOptions.max, tickVal); + ticks.push({value: lastTick, major: isMajor(tickVal)}); + return ticks; +} +class LogarithmicScale extends Scale { + constructor(cfg) { + super(cfg); + this.start = undefined; + this.end = undefined; + this._startValue = undefined; + this._valueRange = 0; + } + parse(raw, index) { + const value = LinearScaleBase.prototype.parse.apply(this, [raw, index]); + if (value === 0) { + this._zero = true; + return undefined; + } + return isNumberFinite(value) && value > 0 ? value : null; + } + determineDataLimits() { + const me = this; + const {min, max} = me.getMinMax(true); + me.min = isNumberFinite(min) ? Math.max(0, min) : null; + me.max = isNumberFinite(max) ? Math.max(0, max) : null; + if (me.options.beginAtZero) { + me._zero = true; + } + me.handleTickRangeOptions(); + } + handleTickRangeOptions() { + const me = this; + const {minDefined, maxDefined} = me.getUserBounds(); + let min = me.min; + let max = me.max; + const setMin = v => (min = minDefined ? min : v); + const setMax = v => (max = maxDefined ? max : v); + const exp = (v, m) => Math.pow(10, Math.floor(log10(v)) + m); + if (min === max) { + if (min <= 0) { + setMin(1); + setMax(10); + } else { + setMin(exp(min, -1)); + setMax(exp(max, +1)); + } + } + if (min <= 0) { + setMin(exp(max, -1)); + } + if (max <= 0) { + setMax(exp(min, +1)); + } + if (me._zero && me.min !== me._suggestedMin && min === exp(me.min, 0)) { + setMin(exp(min, -1)); + } + me.min = min; + me.max = max; + } + buildTicks() { + const me = this; + const opts = me.options; + const generationOptions = { + min: me._userMin, + max: me._userMax + }; + const ticks = generateTicks(generationOptions, me); + if (opts.bounds === 'ticks') { + _setMinAndMaxByKey(ticks, me, 'value'); + } + if (opts.reverse) { + ticks.reverse(); + me.start = me.max; + me.end = me.min; + } else { + me.start = me.min; + me.end = me.max; + } + return ticks; + } + getLabelForValue(value) { + return value === undefined ? '0' : formatNumber(value, this.chart.options.locale); + } + configure() { + const me = this; + const start = me.min; + super.configure(); + me._startValue = log10(start); + me._valueRange = log10(me.max) - log10(start); + } + getPixelForValue(value) { + const me = this; + if (value === undefined || value === 0) { + value = me.min; + } + if (value === null || isNaN(value)) { + return NaN; + } + return me.getPixelForDecimal(value === me.min + ? 0 + : (log10(value) - me._startValue) / me._valueRange); + } + getValueForPixel(pixel) { + const me = this; + const decimal = me.getDecimalForPixel(pixel); + return Math.pow(10, me._startValue + decimal * me._valueRange); + } +} +LogarithmicScale.id = 'logarithmic'; +LogarithmicScale.defaults = { + ticks: { + callback: Ticks.formatters.logarithmic, + major: { + enabled: true + } + } +}; + +function getTickBackdropHeight(opts) { + const tickOpts = opts.ticks; + if (tickOpts.display && opts.display) { + const padding = toPadding(tickOpts.backdropPadding); + return valueOrDefault(tickOpts.font && tickOpts.font.size, defaults.font.size) + padding.height; + } + return 0; +} +function measureLabelSize(ctx, font, label) { + label = isArray(label) ? label : [label]; + return { + w: _longestText(ctx, font.string, label), + h: label.length * font.lineHeight + }; +} +function determineLimits(angle, pos, size, min, max) { + if (angle === min || angle === max) { + return { + start: pos - (size / 2), + end: pos + (size / 2) + }; + } else if (angle < min || angle > max) { + return { + start: pos - size, + end: pos + }; + } + return { + start: pos, + end: pos + size + }; +} +function fitWithPointLabels(scale) { + const furthestLimits = { + l: 0, + r: scale.width, + t: 0, + b: scale.height - scale.paddingTop + }; + const furthestAngles = {}; + const labelSizes = []; + const padding = []; + const valueCount = scale.getLabels().length; + for (let i = 0; i < valueCount; i++) { + const opts = scale.options.pointLabels.setContext(scale.getContext(i)); + padding[i] = opts.padding; + const pointPosition = scale.getPointPosition(i, scale.drawingArea + padding[i]); + const plFont = toFont(opts.font); + const textSize = measureLabelSize(scale.ctx, plFont, scale._pointLabels[i]); + labelSizes[i] = textSize; + const angleRadians = scale.getIndexAngle(i); + const angle = toDegrees(angleRadians); + const hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180); + const vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270); + if (hLimits.start < furthestLimits.l) { + furthestLimits.l = hLimits.start; + furthestAngles.l = angleRadians; + } + if (hLimits.end > furthestLimits.r) { + furthestLimits.r = hLimits.end; + furthestAngles.r = angleRadians; + } + if (vLimits.start < furthestLimits.t) { + furthestLimits.t = vLimits.start; + furthestAngles.t = angleRadians; + } + if (vLimits.end > furthestLimits.b) { + furthestLimits.b = vLimits.end; + furthestAngles.b = angleRadians; + } + } + scale._setReductions(scale.drawingArea, furthestLimits, furthestAngles); + scale._pointLabelItems = buildPointLabelItems(scale, labelSizes, padding); +} +function buildPointLabelItems(scale, labelSizes, padding) { + const items = []; + const valueCount = scale.getLabels().length; + const opts = scale.options; + const tickBackdropHeight = getTickBackdropHeight(opts); + const outerDistance = scale.getDistanceFromCenterForValue(opts.ticks.reverse ? scale.min : scale.max); + for (let i = 0; i < valueCount; i++) { + const extra = (i === 0 ? tickBackdropHeight / 2 : 0); + const pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + padding[i]); + const angle = toDegrees(scale.getIndexAngle(i)); + const size = labelSizes[i]; + const y = yForAngle(pointLabelPosition.y, size.h, angle); + const textAlign = getTextAlignForAngle(angle); + const left = leftForTextAlign(pointLabelPosition.x, size.w, textAlign); + items.push({ + x: pointLabelPosition.x, + y, + textAlign, + left, + top: y, + right: left + size.w, + bottom: y + size.h + }); + } + return items; +} +function getTextAlignForAngle(angle) { + if (angle === 0 || angle === 180) { + return 'center'; + } else if (angle < 180) { + return 'left'; + } + return 'right'; +} +function leftForTextAlign(x, w, align) { + if (align === 'right') { + x -= w; + } else if (align === 'center') { + x -= (w / 2); + } + return x; +} +function yForAngle(y, h, angle) { + if (angle === 90 || angle === 270) { + y -= (h / 2); + } else if (angle > 270 || angle < 90) { + y -= h; + } + return y; +} +function drawPointLabels(scale, labelCount) { + const {ctx, options: {pointLabels}} = scale; + for (let i = labelCount - 1; i >= 0; i--) { + const optsAtIndex = pointLabels.setContext(scale.getContext(i)); + const plFont = toFont(optsAtIndex.font); + const {x, y, textAlign, left, top, right, bottom} = scale._pointLabelItems[i]; + const {backdropColor} = optsAtIndex; + if (!isNullOrUndef(backdropColor)) { + const padding = toPadding(optsAtIndex.backdropPadding); + ctx.fillStyle = backdropColor; + ctx.fillRect(left - padding.left, top - padding.top, right - left + padding.width, bottom - top + padding.height); + } + renderText( + ctx, + scale._pointLabels[i], + x, + y + (plFont.lineHeight / 2), + plFont, + { + color: optsAtIndex.color, + textAlign: textAlign, + textBaseline: 'middle' + } + ); + } +} +function pathRadiusLine(scale, radius, circular, labelCount) { + const {ctx} = scale; + if (circular) { + ctx.arc(scale.xCenter, scale.yCenter, radius, 0, TAU); + } else { + let pointPosition = scale.getPointPosition(0, radius); + ctx.moveTo(pointPosition.x, pointPosition.y); + for (let i = 1; i < labelCount; i++) { + pointPosition = scale.getPointPosition(i, radius); + ctx.lineTo(pointPosition.x, pointPosition.y); + } + } +} +function drawRadiusLine(scale, gridLineOpts, radius, labelCount) { + const ctx = scale.ctx; + const circular = gridLineOpts.circular; + const {color, lineWidth} = gridLineOpts; + if ((!circular && !labelCount) || !color || !lineWidth || radius < 0) { + return; + } + ctx.save(); + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.setLineDash(gridLineOpts.borderDash); + ctx.lineDashOffset = gridLineOpts.borderDashOffset; + ctx.beginPath(); + pathRadiusLine(scale, radius, circular, labelCount); + ctx.closePath(); + ctx.stroke(); + ctx.restore(); +} +function numberOrZero(param) { + return isNumber(param) ? param : 0; +} +class RadialLinearScale extends LinearScaleBase { + constructor(cfg) { + super(cfg); + this.xCenter = undefined; + this.yCenter = undefined; + this.drawingArea = undefined; + this._pointLabels = []; + this._pointLabelItems = []; + } + setDimensions() { + const me = this; + me.width = me.maxWidth; + me.height = me.maxHeight; + me.paddingTop = getTickBackdropHeight(me.options) / 2; + me.xCenter = Math.floor(me.width / 2); + me.yCenter = Math.floor((me.height - me.paddingTop) / 2); + me.drawingArea = Math.min(me.height - me.paddingTop, me.width) / 2; + } + determineDataLimits() { + const me = this; + const {min, max} = me.getMinMax(false); + me.min = isNumberFinite(min) && !isNaN(min) ? min : 0; + me.max = isNumberFinite(max) && !isNaN(max) ? max : 0; + me.handleTickRangeOptions(); + } + computeTickLimit() { + return Math.ceil(this.drawingArea / getTickBackdropHeight(this.options)); + } + generateTickLabels(ticks) { + const me = this; + LinearScaleBase.prototype.generateTickLabels.call(me, ticks); + me._pointLabels = me.getLabels().map((value, index) => { + const label = callback(me.options.pointLabels.callback, [value, index], me); + return label || label === 0 ? label : ''; + }); + } + fit() { + const me = this; + const opts = me.options; + if (opts.display && opts.pointLabels.display) { + fitWithPointLabels(me); + } else { + me.setCenterPoint(0, 0, 0, 0); + } + } + _setReductions(largestPossibleRadius, furthestLimits, furthestAngles) { + const me = this; + let radiusReductionLeft = furthestLimits.l / Math.sin(furthestAngles.l); + let radiusReductionRight = Math.max(furthestLimits.r - me.width, 0) / Math.sin(furthestAngles.r); + let radiusReductionTop = -furthestLimits.t / Math.cos(furthestAngles.t); + let radiusReductionBottom = -Math.max(furthestLimits.b - (me.height - me.paddingTop), 0) / Math.cos(furthestAngles.b); + radiusReductionLeft = numberOrZero(radiusReductionLeft); + radiusReductionRight = numberOrZero(radiusReductionRight); + radiusReductionTop = numberOrZero(radiusReductionTop); + radiusReductionBottom = numberOrZero(radiusReductionBottom); + me.drawingArea = Math.max(largestPossibleRadius / 2, Math.min( + Math.floor(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2), + Math.floor(largestPossibleRadius - (radiusReductionTop + radiusReductionBottom) / 2))); + me.setCenterPoint(radiusReductionLeft, radiusReductionRight, radiusReductionTop, radiusReductionBottom); + } + setCenterPoint(leftMovement, rightMovement, topMovement, bottomMovement) { + const me = this; + const maxRight = me.width - rightMovement - me.drawingArea; + const maxLeft = leftMovement + me.drawingArea; + const maxTop = topMovement + me.drawingArea; + const maxBottom = (me.height - me.paddingTop) - bottomMovement - me.drawingArea; + me.xCenter = Math.floor(((maxLeft + maxRight) / 2) + me.left); + me.yCenter = Math.floor(((maxTop + maxBottom) / 2) + me.top + me.paddingTop); + } + getIndexAngle(index) { + const angleMultiplier = TAU / this.getLabels().length; + const startAngle = this.options.startAngle || 0; + return _normalizeAngle(index * angleMultiplier + toRadians(startAngle)); + } + getDistanceFromCenterForValue(value) { + const me = this; + if (isNullOrUndef(value)) { + return NaN; + } + const scalingFactor = me.drawingArea / (me.max - me.min); + if (me.options.reverse) { + return (me.max - value) * scalingFactor; + } + return (value - me.min) * scalingFactor; + } + getValueForDistanceFromCenter(distance) { + if (isNullOrUndef(distance)) { + return NaN; + } + const me = this; + const scaledDistance = distance / (me.drawingArea / (me.max - me.min)); + return me.options.reverse ? me.max - scaledDistance : me.min + scaledDistance; + } + getPointPosition(index, distanceFromCenter) { + const me = this; + const angle = me.getIndexAngle(index) - HALF_PI; + return { + x: Math.cos(angle) * distanceFromCenter + me.xCenter, + y: Math.sin(angle) * distanceFromCenter + me.yCenter, + angle + }; + } + getPointPositionForValue(index, value) { + return this.getPointPosition(index, this.getDistanceFromCenterForValue(value)); + } + getBasePosition(index) { + return this.getPointPositionForValue(index || 0, this.getBaseValue()); + } + getPointLabelPosition(index) { + const {left, top, right, bottom} = this._pointLabelItems[index]; + return { + left, + top, + right, + bottom, + }; + } + drawBackground() { + const me = this; + const {backgroundColor, grid: {circular}} = me.options; + if (backgroundColor) { + const ctx = me.ctx; + ctx.save(); + ctx.beginPath(); + pathRadiusLine(me, me.getDistanceFromCenterForValue(me._endValue), circular, me.getLabels().length); + ctx.closePath(); + ctx.fillStyle = backgroundColor; + ctx.fill(); + ctx.restore(); + } + } + drawGrid() { + const me = this; + const ctx = me.ctx; + const opts = me.options; + const {angleLines, grid} = opts; + const labelCount = me.getLabels().length; + let i, offset, position; + if (opts.pointLabels.display) { + drawPointLabels(me, labelCount); + } + if (grid.display) { + me.ticks.forEach((tick, index) => { + if (index !== 0) { + offset = me.getDistanceFromCenterForValue(tick.value); + const optsAtIndex = grid.setContext(me.getContext(index - 1)); + drawRadiusLine(me, optsAtIndex, offset, labelCount); + } + }); + } + if (angleLines.display) { + ctx.save(); + for (i = me.getLabels().length - 1; i >= 0; i--) { + const optsAtIndex = angleLines.setContext(me.getContext(i)); + const {color, lineWidth} = optsAtIndex; + if (!lineWidth || !color) { + continue; + } + ctx.lineWidth = lineWidth; + ctx.strokeStyle = color; + ctx.setLineDash(optsAtIndex.borderDash); + ctx.lineDashOffset = optsAtIndex.borderDashOffset; + offset = me.getDistanceFromCenterForValue(opts.ticks.reverse ? me.min : me.max); + position = me.getPointPosition(i, offset); + ctx.beginPath(); + ctx.moveTo(me.xCenter, me.yCenter); + ctx.lineTo(position.x, position.y); + ctx.stroke(); + } + ctx.restore(); + } + } + drawBorder() {} + drawLabels() { + const me = this; + const ctx = me.ctx; + const opts = me.options; + const tickOpts = opts.ticks; + if (!tickOpts.display) { + return; + } + const startAngle = me.getIndexAngle(0); + let offset, width; + ctx.save(); + ctx.translate(me.xCenter, me.yCenter); + ctx.rotate(startAngle); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + me.ticks.forEach((tick, index) => { + if (index === 0 && !opts.reverse) { + return; + } + const optsAtIndex = tickOpts.setContext(me.getContext(index)); + const tickFont = toFont(optsAtIndex.font); + offset = me.getDistanceFromCenterForValue(me.ticks[index].value); + if (optsAtIndex.showLabelBackdrop) { + ctx.font = tickFont.string; + width = ctx.measureText(tick.label).width; + ctx.fillStyle = optsAtIndex.backdropColor; + const padding = toPadding(optsAtIndex.backdropPadding); + ctx.fillRect( + -width / 2 - padding.left, + -offset - tickFont.size / 2 - padding.top, + width + padding.width, + tickFont.size + padding.height + ); + } + renderText(ctx, tick.label, 0, -offset, tickFont, { + color: optsAtIndex.color, + }); + }); + ctx.restore(); + } + drawTitle() {} +} +RadialLinearScale.id = 'radialLinear'; +RadialLinearScale.defaults = { + display: true, + animate: true, + position: 'chartArea', + angleLines: { + display: true, + lineWidth: 1, + borderDash: [], + borderDashOffset: 0.0 + }, + grid: { + circular: false + }, + startAngle: 0, + ticks: { + showLabelBackdrop: true, + callback: Ticks.formatters.numeric + }, + pointLabels: { + backdropColor: undefined, + backdropPadding: 2, + display: true, + font: { + size: 10 + }, + callback(label) { + return label; + }, + padding: 5 + } +}; +RadialLinearScale.defaultRoutes = { + 'angleLines.color': 'borderColor', + 'pointLabels.color': 'color', + 'ticks.color': 'color' +}; +RadialLinearScale.descriptors = { + angleLines: { + _fallback: 'grid' + } +}; + +const INTERVALS = { + millisecond: {common: true, size: 1, steps: 1000}, + second: {common: true, size: 1000, steps: 60}, + minute: {common: true, size: 60000, steps: 60}, + hour: {common: true, size: 3600000, steps: 24}, + day: {common: true, size: 86400000, steps: 30}, + week: {common: false, size: 604800000, steps: 4}, + month: {common: true, size: 2.628e9, steps: 12}, + quarter: {common: false, size: 7.884e9, steps: 4}, + year: {common: true, size: 3.154e10} +}; +const UNITS = (Object.keys(INTERVALS)); +function sorter(a, b) { + return a - b; +} +function parse(scale, input) { + if (isNullOrUndef(input)) { + return null; + } + const adapter = scale._adapter; + const {parser, round, isoWeekday} = scale._parseOpts; + let value = input; + if (typeof parser === 'function') { + value = parser(value); + } + if (!isNumberFinite(value)) { + value = typeof parser === 'string' + ? adapter.parse(value, parser) + : adapter.parse(value); + } + if (value === null) { + return null; + } + if (round) { + value = round === 'week' && (isNumber(isoWeekday) || isoWeekday === true) + ? adapter.startOf(value, 'isoWeek', isoWeekday) + : adapter.startOf(value, round); + } + return +value; +} +function determineUnitForAutoTicks(minUnit, min, max, capacity) { + const ilen = UNITS.length; + for (let i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) { + const interval = INTERVALS[UNITS[i]]; + const factor = interval.steps ? interval.steps : Number.MAX_SAFE_INTEGER; + if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) { + return UNITS[i]; + } + } + return UNITS[ilen - 1]; +} +function determineUnitForFormatting(scale, numTicks, minUnit, min, max) { + for (let i = UNITS.length - 1; i >= UNITS.indexOf(minUnit); i--) { + const unit = UNITS[i]; + if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= numTicks - 1) { + return unit; + } + } + return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0]; +} +function determineMajorUnit(unit) { + for (let i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) { + if (INTERVALS[UNITS[i]].common) { + return UNITS[i]; + } + } +} +function addTick(ticks, time, timestamps) { + if (!timestamps) { + ticks[time] = true; + } else if (timestamps.length) { + const {lo, hi} = _lookup(timestamps, time); + const timestamp = timestamps[lo] >= time ? timestamps[lo] : timestamps[hi]; + ticks[timestamp] = true; + } +} +function setMajorTicks(scale, ticks, map, majorUnit) { + const adapter = scale._adapter; + const first = +adapter.startOf(ticks[0].value, majorUnit); + const last = ticks[ticks.length - 1].value; + let major, index; + for (major = first; major <= last; major = +adapter.add(major, 1, majorUnit)) { + index = map[major]; + if (index >= 0) { + ticks[index].major = true; + } + } + return ticks; +} +function ticksFromTimestamps(scale, values, majorUnit) { + const ticks = []; + const map = {}; + const ilen = values.length; + let i, value; + for (i = 0; i < ilen; ++i) { + value = values[i]; + map[value] = i; + ticks.push({ + value, + major: false + }); + } + return (ilen === 0 || !majorUnit) ? ticks : setMajorTicks(scale, ticks, map, majorUnit); +} +class TimeScale extends Scale { + constructor(props) { + super(props); + this._cache = { + data: [], + labels: [], + all: [] + }; + this._unit = 'day'; + this._majorUnit = undefined; + this._offsets = {}; + this._normalized = false; + this._parseOpts = undefined; + } + init(scaleOpts, opts) { + const time = scaleOpts.time || (scaleOpts.time = {}); + const adapter = this._adapter = new _adapters._date(scaleOpts.adapters.date); + mergeIf(time.displayFormats, adapter.formats()); + this._parseOpts = { + parser: time.parser, + round: time.round, + isoWeekday: time.isoWeekday + }; + super.init(scaleOpts); + this._normalized = opts.normalized; + } + parse(raw, index) { + if (raw === undefined) { + return null; + } + return parse(this, raw); + } + beforeLayout() { + super.beforeLayout(); + this._cache = { + data: [], + labels: [], + all: [] + }; + } + determineDataLimits() { + const me = this; + const options = me.options; + const adapter = me._adapter; + const unit = options.time.unit || 'day'; + let {min, max, minDefined, maxDefined} = me.getUserBounds(); + function _applyBounds(bounds) { + if (!minDefined && !isNaN(bounds.min)) { + min = Math.min(min, bounds.min); + } + if (!maxDefined && !isNaN(bounds.max)) { + max = Math.max(max, bounds.max); + } + } + if (!minDefined || !maxDefined) { + _applyBounds(me._getLabelBounds()); + if (options.bounds !== 'ticks' || options.ticks.source !== 'labels') { + _applyBounds(me.getMinMax(false)); + } + } + min = isNumberFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit); + max = isNumberFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit) + 1; + me.min = Math.min(min, max - 1); + me.max = Math.max(min + 1, max); + } + _getLabelBounds() { + const arr = this.getLabelTimestamps(); + let min = Number.POSITIVE_INFINITY; + let max = Number.NEGATIVE_INFINITY; + if (arr.length) { + min = arr[0]; + max = arr[arr.length - 1]; + } + return {min, max}; + } + buildTicks() { + const me = this; + const options = me.options; + const timeOpts = options.time; + const tickOpts = options.ticks; + const timestamps = tickOpts.source === 'labels' ? me.getLabelTimestamps() : me._generate(); + if (options.bounds === 'ticks' && timestamps.length) { + me.min = me._userMin || timestamps[0]; + me.max = me._userMax || timestamps[timestamps.length - 1]; + } + const min = me.min; + const max = me.max; + const ticks = _filterBetween(timestamps, min, max); + me._unit = timeOpts.unit || (tickOpts.autoSkip + ? determineUnitForAutoTicks(timeOpts.minUnit, me.min, me.max, me._getLabelCapacity(min)) + : determineUnitForFormatting(me, ticks.length, timeOpts.minUnit, me.min, me.max)); + me._majorUnit = !tickOpts.major.enabled || me._unit === 'year' ? undefined + : determineMajorUnit(me._unit); + me.initOffsets(timestamps); + if (options.reverse) { + ticks.reverse(); + } + return ticksFromTimestamps(me, ticks, me._majorUnit); + } + initOffsets(timestamps) { + const me = this; + let start = 0; + let end = 0; + let first, last; + if (me.options.offset && timestamps.length) { + first = me.getDecimalForValue(timestamps[0]); + if (timestamps.length === 1) { + start = 1 - first; + } else { + start = (me.getDecimalForValue(timestamps[1]) - first) / 2; + } + last = me.getDecimalForValue(timestamps[timestamps.length - 1]); + if (timestamps.length === 1) { + end = last; + } else { + end = (last - me.getDecimalForValue(timestamps[timestamps.length - 2])) / 2; + } + } + const limit = timestamps.length < 3 ? 0.5 : 0.25; + start = _limitValue(start, 0, limit); + end = _limitValue(end, 0, limit); + me._offsets = {start, end, factor: 1 / (start + 1 + end)}; + } + _generate() { + const me = this; + const adapter = me._adapter; + const min = me.min; + const max = me.max; + const options = me.options; + const timeOpts = options.time; + const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, me._getLabelCapacity(min)); + const stepSize = valueOrDefault(timeOpts.stepSize, 1); + const weekday = minor === 'week' ? timeOpts.isoWeekday : false; + const hasWeekday = isNumber(weekday) || weekday === true; + const ticks = {}; + let first = min; + let time, count; + if (hasWeekday) { + first = +adapter.startOf(first, 'isoWeek', weekday); + } + first = +adapter.startOf(first, hasWeekday ? 'day' : minor); + if (adapter.diff(max, min, minor) > 100000 * stepSize) { + throw new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor); + } + const timestamps = options.ticks.source === 'data' && me.getDataTimestamps(); + for (time = first, count = 0; time < max; time = +adapter.add(time, stepSize, minor), count++) { + addTick(ticks, time, timestamps); + } + if (time === max || options.bounds === 'ticks' || count === 1) { + addTick(ticks, time, timestamps); + } + return Object.keys(ticks).sort((a, b) => a - b).map(x => +x); + } + getLabelForValue(value) { + const me = this; + const adapter = me._adapter; + const timeOpts = me.options.time; + if (timeOpts.tooltipFormat) { + return adapter.format(value, timeOpts.tooltipFormat); + } + return adapter.format(value, timeOpts.displayFormats.datetime); + } + _tickFormatFunction(time, index, ticks, format) { + const me = this; + const options = me.options; + const formats = options.time.displayFormats; + const unit = me._unit; + const majorUnit = me._majorUnit; + const minorFormat = unit && formats[unit]; + const majorFormat = majorUnit && formats[majorUnit]; + const tick = ticks[index]; + const major = majorUnit && majorFormat && tick && tick.major; + const label = me._adapter.format(time, format || (major ? majorFormat : minorFormat)); + const formatter = options.ticks.callback; + return formatter ? callback(formatter, [label, index, ticks], me) : label; + } + generateTickLabels(ticks) { + let i, ilen, tick; + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + tick = ticks[i]; + tick.label = this._tickFormatFunction(tick.value, i, ticks); + } + } + getDecimalForValue(value) { + const me = this; + return value === null ? NaN : (value - me.min) / (me.max - me.min); + } + getPixelForValue(value) { + const me = this; + const offsets = me._offsets; + const pos = me.getDecimalForValue(value); + return me.getPixelForDecimal((offsets.start + pos) * offsets.factor); + } + getValueForPixel(pixel) { + const me = this; + const offsets = me._offsets; + const pos = me.getDecimalForPixel(pixel) / offsets.factor - offsets.end; + return me.min + pos * (me.max - me.min); + } + _getLabelSize(label) { + const me = this; + const ticksOpts = me.options.ticks; + const tickLabelWidth = me.ctx.measureText(label).width; + const angle = toRadians(me.isHorizontal() ? ticksOpts.maxRotation : ticksOpts.minRotation); + const cosRotation = Math.cos(angle); + const sinRotation = Math.sin(angle); + const tickFontSize = me._resolveTickFontOptions(0).size; + return { + w: (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation), + h: (tickLabelWidth * sinRotation) + (tickFontSize * cosRotation) + }; + } + _getLabelCapacity(exampleTime) { + const me = this; + const timeOpts = me.options.time; + const displayFormats = timeOpts.displayFormats; + const format = displayFormats[timeOpts.unit] || displayFormats.millisecond; + const exampleLabel = me._tickFormatFunction(exampleTime, 0, ticksFromTimestamps(me, [exampleTime], me._majorUnit), format); + const size = me._getLabelSize(exampleLabel); + const capacity = Math.floor(me.isHorizontal() ? me.width / size.w : me.height / size.h) - 1; + return capacity > 0 ? capacity : 1; + } + getDataTimestamps() { + const me = this; + let timestamps = me._cache.data || []; + let i, ilen; + if (timestamps.length) { + return timestamps; + } + const metas = me.getMatchingVisibleMetas(); + if (me._normalized && metas.length) { + return (me._cache.data = metas[0].controller.getAllParsedValues(me)); + } + for (i = 0, ilen = metas.length; i < ilen; ++i) { + timestamps = timestamps.concat(metas[i].controller.getAllParsedValues(me)); + } + return (me._cache.data = me.normalize(timestamps)); + } + getLabelTimestamps() { + const me = this; + const timestamps = me._cache.labels || []; + let i, ilen; + if (timestamps.length) { + return timestamps; + } + const labels = me.getLabels(); + for (i = 0, ilen = labels.length; i < ilen; ++i) { + timestamps.push(parse(me, labels[i])); + } + return (me._cache.labels = me._normalized ? timestamps : me.normalize(timestamps)); + } + normalize(values) { + return _arrayUnique(values.sort(sorter)); + } +} +TimeScale.id = 'time'; +TimeScale.defaults = { + bounds: 'data', + adapters: {}, + time: { + parser: false, + unit: false, + round: false, + isoWeekday: false, + minUnit: 'millisecond', + displayFormats: {} + }, + ticks: { + source: 'auto', + major: { + enabled: false + } + } +}; + +function interpolate(table, val, reverse) { + let lo = 0; + let hi = table.length - 1; + let prevSource, nextSource, prevTarget, nextTarget; + if (reverse) { + if (val >= table[lo].pos && val <= table[hi].pos) { + ({lo, hi} = _lookupByKey(table, 'pos', val)); + } + ({pos: prevSource, time: prevTarget} = table[lo]); + ({pos: nextSource, time: nextTarget} = table[hi]); + } else { + if (val >= table[lo].time && val <= table[hi].time) { + ({lo, hi} = _lookupByKey(table, 'time', val)); + } + ({time: prevSource, pos: prevTarget} = table[lo]); + ({time: nextSource, pos: nextTarget} = table[hi]); + } + const span = nextSource - prevSource; + return span ? prevTarget + (nextTarget - prevTarget) * (val - prevSource) / span : prevTarget; +} +class TimeSeriesScale extends TimeScale { + constructor(props) { + super(props); + this._table = []; + this._minPos = undefined; + this._tableRange = undefined; + } + initOffsets() { + const me = this; + const timestamps = me._getTimestampsForTable(); + const table = me._table = me.buildLookupTable(timestamps); + me._minPos = interpolate(table, me.min); + me._tableRange = interpolate(table, me.max) - me._minPos; + super.initOffsets(timestamps); + } + buildLookupTable(timestamps) { + const {min, max} = this; + const items = []; + const table = []; + let i, ilen, prev, curr, next; + for (i = 0, ilen = timestamps.length; i < ilen; ++i) { + curr = timestamps[i]; + if (curr >= min && curr <= max) { + items.push(curr); + } + } + if (items.length < 2) { + return [ + {time: min, pos: 0}, + {time: max, pos: 1} + ]; + } + for (i = 0, ilen = items.length; i < ilen; ++i) { + next = items[i + 1]; + prev = items[i - 1]; + curr = items[i]; + if (Math.round((next + prev) / 2) !== curr) { + table.push({time: curr, pos: i / (ilen - 1)}); + } + } + return table; + } + _getTimestampsForTable() { + const me = this; + let timestamps = me._cache.all || []; + if (timestamps.length) { + return timestamps; + } + const data = me.getDataTimestamps(); + const label = me.getLabelTimestamps(); + if (data.length && label.length) { + timestamps = me.normalize(data.concat(label)); + } else { + timestamps = data.length ? data : label; + } + timestamps = me._cache.all = timestamps; + return timestamps; + } + getDecimalForValue(value) { + return (interpolate(this._table, value) - this._minPos) / this._tableRange; + } + getValueForPixel(pixel) { + const me = this; + const offsets = me._offsets; + const decimal = me.getDecimalForPixel(pixel) / offsets.factor - offsets.end; + return interpolate(me._table, decimal * me._tableRange + me._minPos, true); + } +} +TimeSeriesScale.id = 'timeseries'; +TimeSeriesScale.defaults = TimeScale.defaults; + +var scales = /*#__PURE__*/Object.freeze({ +__proto__: null, +CategoryScale: CategoryScale, +LinearScale: LinearScale, +LogarithmicScale: LogarithmicScale, +RadialLinearScale: RadialLinearScale, +TimeScale: TimeScale, +TimeSeriesScale: TimeSeriesScale +}); + +Chart.register(controllers, scales, elements, plugins); +Chart.helpers = {...helpers}; +Chart._adapters = _adapters; +Chart.Animation = Animation; +Chart.Animations = Animations; +Chart.animator = animator; +Chart.controllers = registry.controllers.items; +Chart.DatasetController = DatasetController; +Chart.Element = Element; +Chart.elements = elements; +Chart.Interaction = Interaction; +Chart.layouts = layouts; +Chart.platforms = platforms; +Chart.Scale = Scale; +Chart.Ticks = Ticks; +Object.assign(Chart, controllers, scales, elements, plugins, platforms); +Chart.Chart = Chart; +if (typeof window !== 'undefined') { + window.Chart = Chart; +} + +return Chart; + +}))); diff --git a/node_modules/chart.js/dist/chart.min.js b/node_modules/chart.js/dist/chart.min.js new file mode 100644 index 000000000..b982f045c --- /dev/null +++ b/node_modules/chart.js/dist/chart.min.js @@ -0,0 +1,13 @@ +/*! + * Chart.js v3.4.1 + * https://www.chartjs.org + * (c) 2021 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";const t="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function e(e,i,n){const o=n||(t=>Array.prototype.slice.call(t));let s=!1,a=[];return function(...n){a=o(n),s||(s=!0,t.call(window,(()=>{s=!1,e.apply(i,a)})))}}function i(t,e){let i;return function(){return e?(clearTimeout(i),i=setTimeout(t,e)):t(),e}}const n=t=>"start"===t?"left":"end"===t?"right":"center",o=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,s=(t,e,i,n)=>t===(n?"left":"right")?i:"center"===t?(e+i)/2:e;var a=new class{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,n){const o=e.listeners[n],s=e.duration;o.forEach((n=>n({chart:t,initial:e.initial,numSteps:s,currentStep:Math.min(i-e.start,s)})))}_refresh(){const e=this;e._request||(e._running=!0,e._request=t.call(window,(()=>{e._update(),e._request=null,e._running&&e._refresh()})))}_update(t=Date.now()){const e=this;let i=0;e._charts.forEach(((n,o)=>{if(!n.running||!n.items.length)return;const s=n.items;let a,r=s.length-1,l=!1;for(;r>=0;--r)a=s[r],a._active?(a._total>n.duration&&(n.duration=a._total),a.tick(t),l=!0):(s[r]=s[s.length-1],s.pop());l&&(o.draw(),e._notify(o,n,t,"progress")),s.length||(n.running=!1,e._notify(o,n,t,"complete"),n.initial=!1),i+=s.length})),e._lastDate=t,0===i&&(e._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let n=i.length-1;for(;n>=0;--n)i[n].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}; +/*! + * @kurkle/color v0.1.9 + * https://github.com/kurkle/color#readme + * (c) 2020 Jukka Kurkela + * Released under the MIT License + */const r={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},l="0123456789ABCDEF",c=t=>l[15&t],h=t=>l[(240&t)>>4]+l[15&t],d=t=>(240&t)>>4==(15&t);function u(t){var e=function(t){return d(t.r)&&d(t.g)&&d(t.b)&&d(t.a)}(t)?c:h;return t?"#"+e(t.r)+e(t.g)+e(t.b)+(t.a<255?e(t.a):""):t}function f(t){return t+.5|0}const g=(t,e,i)=>Math.max(Math.min(t,i),e);function p(t){return g(f(2.55*t),0,255)}function m(t){return g(f(255*t),0,255)}function x(t){return g(f(t/2.55)/100,0,1)}function b(t){return g(f(100*t),0,100)}const _=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const y=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function v(t,e,i){const n=e*Math.min(i,1-i),o=(e,o=(e+t/30)%12)=>i-n*Math.max(Math.min(o-3,9-o,1),-1);return[o(0),o(8),o(4)]}function w(t,e,i){const n=(n,o=(n+t/60)%6)=>i-i*e*Math.max(Math.min(o,4-o,1),0);return[n(5),n(3),n(1)]}function M(t,e,i){const n=v(t,1,.5);let o;for(e+i>1&&(o=1/(e+i),e*=o,i*=o),o=0;o<3;o++)n[o]*=1-e-i,n[o]+=e;return n}function k(t){const e=t.r/255,i=t.g/255,n=t.b/255,o=Math.max(e,i,n),s=Math.min(e,i,n),a=(o+s)/2;let r,l,c;return o!==s&&(c=o-s,l=a>.5?c/(2-o-s):c/(o+s),r=o===e?(i-n)/c+(i>16&255,s>>8&255,255&s]}return t}(),A.transparent=[0,0,0,0]);const e=A[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}function R(t,e,i){if(t){let n=k(t);n[e]=Math.max(0,Math.min(n[e]+n[e]*i,0===e?360:1)),n=P(n),t.r=n[0],t.g=n[1],t.b=n[2]}}function E(t,e){return t?Object.assign(e||{},t):t}function z(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=m(t[3]))):(e=E(t,{r:0,g:0,b:0,a:1})).a=m(e.a),e}function I(t){return"r"===t.charAt(0)?function(t){const e=_.exec(t);let i,n,o,s=255;if(e){if(e[7]!==i){const t=+e[7];s=255&(e[8]?p(t):255*t)}return i=+e[1],n=+e[3],o=+e[5],i=255&(e[2]?p(i):i),n=255&(e[4]?p(n):n),o=255&(e[6]?p(o):o),{r:i,g:n,b:o,a:s}}}(t):C(t)}class F{constructor(t){if(t instanceof F)return t;const e=typeof t;let i;var n,o,s;"object"===e?i=z(t):"string"===e&&(s=(n=t).length,"#"===n[0]&&(4===s||5===s?o={r:255&17*r[n[1]],g:255&17*r[n[2]],b:255&17*r[n[3]],a:5===s?17*r[n[4]]:255}:7!==s&&9!==s||(o={r:r[n[1]]<<4|r[n[2]],g:r[n[3]]<<4|r[n[4]],b:r[n[5]]<<4|r[n[6]],a:9===s?r[n[7]]<<4|r[n[8]]:255})),i=o||L(t)||I(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=E(this._rgb);return t&&(t.a=x(t.a)),t}set rgb(t){this._rgb=z(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${x(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):this._rgb;var t}hexString(){return this._valid?u(this._rgb):this._rgb}hslString(){return this._valid?function(t){if(!t)return;const e=k(t),i=e[0],n=b(e[1]),o=b(e[2]);return t.a<255?`hsla(${i}, ${n}%, ${o}%, ${x(t.a)})`:`hsl(${i}, ${n}%, ${o}%)`}(this._rgb):this._rgb}mix(t,e){const i=this;if(t){const n=i.rgb,o=t.rgb;let s;const a=e===s?.5:e,r=2*a-1,l=n.a-o.a,c=((r*l==-1?r:(r+l)/(1+r*l))+1)/2;s=1-c,n.r=255&c*n.r+s*o.r+.5,n.g=255&c*n.g+s*o.g+.5,n.b=255&c*n.b+s*o.b+.5,n.a=a*n.a+(1-a)*o.a,i.rgb=n}return i}clone(){return new F(this.rgb)}alpha(t){return this._rgb.a=m(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=f(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return R(this._rgb,2,t),this}darken(t){return R(this._rgb,2,-t),this}saturate(t){return R(this._rgb,1,t),this}desaturate(t){return R(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=k(t);i[0]=D(i[0]+e),i=P(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function B(t){return new F(t)}const V=t=>t instanceof CanvasGradient||t instanceof CanvasPattern;function W(t){return V(t)?t:B(t)}function N(t){return V(t)?t:B(t).saturate(.5).darken(.1).hexString()}function H(){}const j=function(){let t=0;return function(){return t++}}();function $(t){return null==t}function Y(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.substr(0,7)&&"Array]"===e.substr(-6)}function U(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}const X=t=>("number"==typeof t||t instanceof Number)&&isFinite(+t);function q(t,e){return X(t)?t:e}function K(t,e){return void 0===t?e:t}const G=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:t/e,Z=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function Q(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function J(t,e,i,n){let o,s,a;if(Y(t))if(s=t.length,n)for(o=s-1;o>=0;o--)e.call(i,t[o],o);else for(o=0;oi;)t=t[e.substr(i,n-i)],i=n+1,n=rt(e,i);return t}function ct(t){return t.charAt(0).toUpperCase()+t.slice(1)}const ht=t=>void 0!==t,dt=t=>"function"==typeof t,ut=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0},ft=Object.create(null),gt=Object.create(null);function pt(t,e){if(!e)return t;const i=e.split(".");for(let e=0,n=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>N(e.backgroundColor),this.hoverBorderColor=(t,e)=>N(e.borderColor),this.hoverColor=(t,e)=>N(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.describe(t)}set(t,e){return mt(this,t,e)}get(t){return pt(this,t)}describe(t,e){return mt(gt,t,e)}override(t,e){return mt(ft,t,e)}route(t,e,i,n){const o=pt(this,t),s=pt(this,i),a="_"+e;Object.defineProperties(o,{[a]:{value:o[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[a],e=s[n];return U(t)?Object.assign({},e,t):K(t,e)},set(t){this[a]=t}}})}}({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}});const bt=Math.PI,_t=2*bt,yt=_t+bt,vt=Number.POSITIVE_INFINITY,wt=bt/180,Mt=bt/2,kt=bt/4,St=2*bt/3,Pt=Math.log10,Dt=Math.sign;function Ct(t){const e=Math.round(t);t=At(t,e,t/1e3)?e:t;const i=Math.pow(10,Math.floor(Pt(t))),n=t/i;return(n<=1?1:n<=2?2:n<=5?5:10)*i}function Ot(t){const e=[],i=Math.sqrt(t);let n;for(n=1;nt-e)).pop(),e}function Tt(t){return!isNaN(parseFloat(t))&&isFinite(t)}function At(t,e,i){return Math.abs(t-e)=t}function Rt(t,e,i){let n,o,s;for(n=0,o=t.length;nl&&cn&&(n=s),n}function Ut(t,e,i,n){let o=(n=n||{}).data=n.data||{},s=n.garbageCollect=n.garbageCollect||[];n.font!==e&&(o=n.data={},s=n.garbageCollect=[],n.font=e),t.save(),t.font=e;let a=0;const r=i.length;let l,c,h,d,u;for(l=0;li.length){for(l=0;l0&&t.stroke()}}function Gt(t,e,i){return i=i||.5,t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==s.strokeColor;let l,c;for(t.save(),t.font=o.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]);$(e.rotation)||t.rotate(e.rotation);e.color&&(t.fillStyle=e.color);e.textAlign&&(t.textAlign=e.textAlign);e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,s),l=0;lt[i]1;)n=s+o>>1,i(n)?s=n:o=n;return{lo:s,hi:o}}const se=(t,e,i)=>oe(t,i,(n=>t[n][e]oe(t,i,(n=>t[n][e]>=i));function re(t,e,i){let n=0,o=t.length;for(;nn&&t[o-1]>i;)o--;return n>0||o{const i="_onData"+ct(e),n=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const o=n.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),o}})})))}function he(t,e){const i=t._chartjs;if(!i)return;const n=i.listeners,o=n.indexOf(e);-1!==o&&n.splice(o,1),n.length>0||(le.forEach((e=>{delete t[e]})),delete t._chartjs)}function de(t){const e=new Set;let i,n;for(i=0,n=t.length;iwindow.getComputedStyle(t,null);function pe(t,e){return ge(t).getPropertyValue(e)}const me=["top","right","bottom","left"];function xe(t,e,i){const n={};i=i?"-"+i:"";for(let o=0;o<4;o++){const s=me[o];n[s]=parseFloat(t[e+"-"+s+i])||0}return n.width=n.left+n.right,n.height=n.top+n.bottom,n}function be(t,e){const{canvas:i,currentDevicePixelRatio:n}=e,o=ge(i),s="border-box"===o.boxSizing,a=xe(o,"padding"),r=xe(o,"border","width"),{x:l,y:c,box:h}=function(t,e){const i=t.native||t,n=i.touches,o=n&&n.length?n[0]:i,{offsetX:s,offsetY:a}=o;let r,l,c=!1;if(((t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot))(s,a,i.target))r=s,l=a;else{const t=e.getBoundingClientRect();r=o.clientX-t.left,l=o.clientY-t.top,c=!0}return{x:r,y:l,box:c}}(t,i),d=a.left+(h&&r.left),u=a.top+(h&&r.top);let{width:f,height:g}=e;return s&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/n),y:Math.round((c-u)/g*i.height/n)}}const _e=t=>Math.round(10*t)/10;function ye(t,e,i,n){const o=ge(t),s=xe(o,"margin"),a=fe(o.maxWidth,t,"clientWidth")||vt,r=fe(o.maxHeight,t,"clientHeight")||vt,l=function(t,e,i){let n,o;if(void 0===e||void 0===i){const s=ue(t);if(s){const t=s.getBoundingClientRect(),a=ge(s),r=xe(a,"border","width"),l=xe(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,n=fe(a.maxWidth,s,"clientWidth"),o=fe(a.maxHeight,s,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:n||vt,maxHeight:o||vt}}(t,e,i);let{width:c,height:h}=l;if("content-box"===o.boxSizing){const t=xe(o,"border","width"),e=xe(o,"padding");c-=e.width+t.width,h-=e.height+t.height}return c=Math.max(0,c-s.width),h=Math.max(0,n?Math.floor(c/n):h-s.height),c=_e(Math.min(c,a,l.maxWidth)),h=_e(Math.min(h,r,l.maxHeight)),c&&!h&&(h=_e(c/2)),{width:c,height:h}}function ve(t,e,i){const n=e||1,o=Math.floor(t.height*n),s=Math.floor(t.width*n);t.height=o/n,t.width=s/n;const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==n||a.height!==o||a.width!==s)&&(t.currentDevicePixelRatio=n,a.height=o,a.width=s,t.ctx.setTransform(n,0,0,n,0,0),!0)}const we=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};window.addEventListener("test",null,e),window.removeEventListener("test",null,e)}catch(t){}return t}();function Me(t,e){const i=pe(t,e),n=i&&i.match(/^(\d+)(\.\d+)?px$/);return n?+n[1]:void 0}function ke(t,e){return"native"in t?{x:t.x,y:t.y}:be(t,e)}function Se(t,e,i,n){const{controller:o,data:s,_sorted:a}=t,r=o._cachedMeta.iScale;if(r&&e===r.axis&&a&&s.length){const t=r._reversePixels?ae:se;if(!n)return t(s,e,i);if(o._sharedOptions){const n=s[0],o="function"==typeof n.getRange&&n.getRange(e);if(o){const n=t(s,e,i-o),a=t(s,e,i+o);return{lo:n.lo,hi:a.hi}}}}return{lo:0,hi:s.length-1}}function Pe(t,e,i,n,o){const s=t.getSortedVisibleDatasetMetas(),a=i[e];for(let t=0,i=s.length;t{t[r](o[a],n)&&s.push({element:t,datasetIndex:e,index:i}),t.inRange(o.x,o.y,n)&&(l=!0)})),i.intersect&&!l?[]:s}var Te={modes:{index(t,e,i,n){const o=ke(e,t),s=i.axis||"x",a=i.intersect?De(t,o,s,n):Ce(t,o,s,!1,n),r=[];return a.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=a[0].index,i=t.data[e];i&&!i.skip&&r.push({element:i,datasetIndex:t.index,index:e})})),r):[]},dataset(t,e,i,n){const o=ke(e,t),s=i.axis||"xy";let a=i.intersect?De(t,o,s,n):Ce(t,o,s,!1,n);if(a.length>0){const e=a[0].datasetIndex,i=t.getDatasetMeta(e).data;a=[];for(let t=0;tDe(t,ke(e,t),i.axis||"xy",n),nearest:(t,e,i,n)=>Ce(t,ke(e,t),i.axis||"xy",i.intersect,n),x:(t,e,i,n)=>(i.axis="x",Oe(t,e,i,n)),y:(t,e,i,n)=>(i.axis="y",Oe(t,e,i,n))}};const Ae=new RegExp(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/),Le=new RegExp(/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/);function Re(t,e){const i=(""+t).match(Ae);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}function Ee(t,e){const i={},n=U(e),o=n?Object.keys(e):e,s=U(t)?n?i=>K(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of o)i[t]=+s(t)||0;return i}function ze(t){return Ee(t,{top:"y",right:"x",bottom:"y",left:"x"})}function Ie(t){return Ee(t,["topLeft","topRight","bottomLeft","bottomRight"])}function Fe(t){const e=ze(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Be(t,e){t=t||{},e=e||xt.font;let i=K(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let n=K(t.style,e.style);n&&!(""+n).match(Le)&&(console.warn('Invalid font style specified: "'+n+'"'),n="");const o={family:K(t.family,e.family),lineHeight:Re(K(t.lineHeight,e.lineHeight),i),size:i,style:n,weight:K(t.weight,e.weight),string:""};return o.string=$t(o),o}function Ve(t,e,i,n){let o,s,a,r=!0;for(o=0,s=t.length;ot.pos===e))}function je(t,e){return t.filter((t=>-1===Ne.indexOf(t.pos)&&t.box.axis===e))}function $e(t,e){return t.sort(((t,i)=>{const n=e?i:t,o=e?t:i;return n.weight===o.weight?n.index-o.index:n.weight-o.weight}))}function Ye(t,e,i,n){return Math.max(t[i],e[i])+Math.max(t[n],e[n])}function Ue(t,e){t.top=Math.max(t.top,e.top),t.left=Math.max(t.left,e.left),t.bottom=Math.max(t.bottom,e.bottom),t.right=Math.max(t.right,e.right)}function Xe(t,e,i){const n=i.box,o=t.maxPadding;U(i.pos)||(i.size&&(t[i.pos]-=i.size),i.size=i.horizontal?n.height:n.width,t[i.pos]+=i.size),n.getPadding&&Ue(o,n.getPadding());const s=Math.max(0,e.outerWidth-Ye(o,t,"left","right")),a=Math.max(0,e.outerHeight-Ye(o,t,"top","bottom")),r=s!==t.w,l=a!==t.h;return t.w=s,t.h=a,i.horizontal?{same:r,other:l}:{same:l,other:r}}function qe(t,e){const i=e.maxPadding;function n(t){const n={left:0,top:0,right:0,bottom:0};return t.forEach((t=>{n[t]=Math.max(e[t],i[t])})),n}return n(t?["left","right"]:["top","bottom"])}function Ke(t,e,i){const n=[];let o,s,a,r,l,c;for(o=0,s=t.length,l=0;ot.box.fullSize)),!0),n=$e(He(e,"left"),!0),o=$e(He(e,"right")),s=$e(He(e,"top"),!0),a=$e(He(e,"bottom")),r=je(e,"x"),l=je(e,"y");return{fullSize:i,leftAndTop:n.concat(s),rightAndBottom:o.concat(l).concat(a).concat(r),chartArea:He(e,"chartArea"),vertical:n.concat(o).concat(l),horizontal:s.concat(a).concat(r)}}(t.boxes),l=r.vertical,c=r.horizontal;J(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const h=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:o,availableWidth:s,availableHeight:a,vBoxMaxWidth:s/2/h,hBoxMaxHeight:a/2}),u=Object.assign({},o);Ue(u,Fe(n));const f=Object.assign({maxPadding:u,w:s,h:a,x:o.left,y:o.top},o);!function(t,e){let i,n,o;for(i=0,n=t.length;i{const i=e.box;Object.assign(i,t.chartArea),i.update(f.w,f.h)}))}};class Qe{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,n){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,n?Math.floor(e/n):i)}}isAttached(t){return!0}}class Je extends Qe{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}}const ti={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ei=t=>null===t||""===t;const ii=!!we&&{passive:!0};function ni(t,e,i){t.canvas.removeEventListener(e,i,ii)}function oi(t,e,i){const n=t.canvas,o=n&&ue(n)||n,s=new MutationObserver((t=>{const e=ue(o);t.forEach((t=>{for(let n=0;n{t.forEach((t=>{for(let e=0;e{i.currentDevicePixelRatio!==t&&e()})))}function ci(t,i,n){const o=t.canvas,s=o&&ue(o);if(!s)return;const a=e(((t,e)=>{const i=s.clientWidth;n(t,e),i{const e=t[0],i=e.contentRect.width,n=e.contentRect.height;0===i&&0===n||a(i,n)}));return r.observe(s),function(t,e){ai.size||window.addEventListener("resize",li),ai.set(t,e)}(t,a),r}function hi(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){ai.delete(t),ai.size||window.removeEventListener("resize",li)}(t)}function di(t,i,n){const o=t.canvas,s=e((e=>{null!==t.ctx&&n(function(t,e){const i=ti[t.type]||t.type,{x:n,y:o}=be(t,e);return{type:i,chart:e,native:t,x:void 0!==n?n:null,y:void 0!==o?o:null}}(e,t))}),t,(t=>{const e=t[0];return[e,e.offsetX,e.offsetY]}));return function(t,e,i){t.addEventListener(e,i,ii)}(o,i,s),s}class ui extends Qe{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,n=t.getAttribute("height"),o=t.getAttribute("width");if(t.$chartjs={initial:{height:n,width:o,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ei(o)){const e=Me(t,"width");void 0!==e&&(t.width=e)}if(ei(n))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Me(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e.$chartjs)return!1;const i=e.$chartjs.initial;["height","width"].forEach((t=>{const n=i[t];$(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e.$chartjs,!0}addEventListener(t,e,i){this.removeEventListener(t,e);const n=t.$proxies||(t.$proxies={}),o={attach:oi,detach:si,resize:ci}[e]||di;n[e]=o(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),n=i[e];if(!n)return;({attach:hi,detach:hi,resize:hi}[e]||ni)(t,e,n),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,n){return ye(t,e,i,n)}isAttached(t){const e=ue(t);return!(!e||!ue(e))}}var fi=Object.freeze({__proto__:null,BasePlatform:Qe,BasicPlatform:Je,DomPlatform:ui});const gi=t=>0===t||1===t,pi=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*_t/i),mi=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*_t/i)+1,xi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*Mt),easeOutSine:t=>Math.sin(t*Mt),easeInOutSine:t=>-.5*(Math.cos(bt*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>gi(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>gi(t)?t:pi(t,.075,.3),easeOutElastic:t=>gi(t)?t:mi(t,.075,.3),easeInOutElastic(t){const e=.1125;return gi(t)?t:t<.5?.5*pi(2*t,e,.45):.5+.5*mi(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-xi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*xi.easeInBounce(2*t):.5*xi.easeOutBounce(2*t-1)+.5},bi="transparent",_i={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const n=W(t||bi),o=n.valid&&W(e||bi);return o&&o.valid?o.mix(n,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class yi{constructor(t,e,i,n){const o=e[i];n=Ve([t.to,n,o,t.from]);const s=Ve([t.from,o,n]);this._active=!0,this._fn=t.fn||_i[t.type||typeof s],this._easing=xi[t.easing]||xi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=s,this._to=n,this._promises=void 0}active(){return this._active}update(t,e,i){const n=this;if(n._active){n._notify(!1);const o=n._target[n._prop],s=i-n._start,a=n._duration-s;n._start=i,n._duration=Math.floor(Math.max(a,t.duration)),n._total+=s,n._loop=!!t.loop,n._to=Ve([t.to,e,o,t.from]),n._from=Ve([t.from,o,e])}}cancel(){const t=this;t._active&&(t.tick(Date.now()),t._active=!1,t._notify(!1))}tick(t){const e=this,i=t-e._start,n=e._duration,o=e._prop,s=e._from,a=e._loop,r=e._to;let l;if(e._active=s!==r&&(a||i1?2-l:l,l=e._easing(Math.min(1,Math.max(0,l))),e._target[o]=e._fn(s,r,l))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),xt.set("animations",{colors:{type:"color",properties:["color","borderColor","backgroundColor"]},numbers:{type:"number",properties:["x","y","borderWidth","radius","tension"]}}),xt.describe("animations",{_fallback:"animation"}),xt.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}});class wi{constructor(t,e){this._chart=t,this._properties=new Map,this.configure(e)}configure(t){if(!U(t))return;const e=this._properties;Object.getOwnPropertyNames(t).forEach((i=>{const n=t[i];if(!U(n))return;const o={};for(const t of vi)o[t]=n[t];(Y(n.properties)&&n.properties||[i]).forEach((t=>{t!==i&&e.has(t)||e.set(t,o)}))}))}_animateOptions(t,e){const i=e.options,n=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!n)return[];const o=this._createAnimations(n,i);return i.$shared&&function(t,e){const i=[],n=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),o}_createAnimations(t,e){const i=this._properties,n=[],o=t.$animations||(t.$animations={}),s=Object.keys(e),a=Date.now();let r;for(r=s.length-1;r>=0;--r){const l=s[r];if("$"===l.charAt(0))continue;if("options"===l){n.push(...this._animateOptions(t,e));continue}const c=e[l];let h=o[l];const d=i.get(l);if(h){if(d&&h.active()){h.update(d,c,a);continue}h.cancel()}d&&d.duration?(o[l]=h=new yi(d,t,l,c),n.push(h)):t[l]=c}return n}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(a.add(this._chart,i),!0):void 0}}function Mi(t,e){const i=t&&t.options||{},n=i.reverse,o=void 0===i.min?e:0,s=void 0===i.max?e:0;return{start:n?s:o,end:n?o:s}}function ki(t,e){const i=[],n=t._getSortedDatasetMetas(e);let o,s;for(o=0,s=n.length;o0||!i&&e<0)return n.index}return null}function Oi(t,e){const{chart:i,_cachedMeta:n}=t,o=i._stacks||(i._stacks={}),{iScale:s,vScale:a,index:r}=n,l=s.axis,c=a.axis,h=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(s,a,n),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Ai(t,e){const i=t.vScale&&t.vScale.axis;if(i){e=e||t._parsed;for(const n of e){const e=n._stacks;if(!e||void 0===e[i]||void 0===e[i][t.index])return;delete e[i][t.index]}}}const Li=t=>"reset"===t||"none"===t,Ri=(t,e)=>e?t:Object.assign({},t);class Ei{constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.$context=void 0,this._syncList=[],this.initialize()}initialize(){const t=this,e=t._cachedMeta;t.configure(),t.linkScales(),e._stacked=Pi(e.vScale,e),t.addElements()}updateIndex(t){this.index!==t&&Ai(this._cachedMeta),this.index=t}linkScales(){const t=this,e=t.chart,i=t._cachedMeta,n=t.getDataset(),o=(t,e,i,n)=>"x"===t?e:"r"===t?n:i,s=i.xAxisID=K(n.xAxisID,Ti(e,"x")),a=i.yAxisID=K(n.yAxisID,Ti(e,"y")),r=i.rAxisID=K(n.rAxisID,Ti(e,"r")),l=i.indexAxis,c=i.iAxisID=o(l,s,a,r),h=i.vAxisID=o(l,a,s,r);i.xScale=t.getScaleForId(s),i.yScale=t.getScaleForId(a),i.rScale=t.getScaleForId(r),i.iScale=t.getScaleForId(c),i.vScale=t.getScaleForId(h)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&he(this._data,this),t._stacked&&Ai(t)}_dataCheck(){const t=this,e=t.getDataset(),i=e.data||(e.data=[]),n=t._data;if(U(i))t._data=function(t){const e=Object.keys(t),i=new Array(e.length);let n,o,s;for(n=0,o=e.length;n0&&n._parsed[t-1];if(!1===i._parsing)n._parsed=o,n._sorted=!0,h=o;else{h=Y(o[t])?i.parseArrayData(n,o,t,e):U(o[t])?i.parseObjectData(n,o,t,e):i.parsePrimitiveData(n,o,t,e);const s=()=>null===c[r]||u&&c[r]p||d=0;--u)if(!m()){i.updateRangeFromParsed(c,t,g,l);break}return c}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let n,o,s;for(n=0,o=e.length;n=0&&tn.getContext(i,o)),d);return g.$shared&&(g.$shared=l,s[a]=Object.freeze(Ri(g,l))),g}_resolveAnimations(t,e,i){const n=this,o=n.chart,s=n._cachedDataOpts,a=`animation-${e}`,r=s[a];if(r)return r;let l;if(!1!==o.options.animation){const o=n.chart.config,s=o.datasetAnimationScopeKeys(n._type,e),a=o.getOptionScopes(n.getDataset(),s);l=o.createResolver(a,n.getContext(t,i,e))}const c=new wi(o,l&&l.animations);return l&&l._cacheable&&(s[a]=Object.freeze(c)),c}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Li(t)||this.chart._animationsDisabled}updateElement(t,e,i,n){Li(n)?Object.assign(t,i):this._resolveAnimations(e,n).update(t,i)}updateSharedOptions(t,e,i){t&&!Li(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,n){t.active=n;const o=this.getStyle(e,n);this._resolveAnimations(e,i,n).update(t,{options:!n&&this.getSharedOptions(o)||o})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this,i=e._data,n=e._cachedMeta.data;for(const[t,i,n]of e._syncList)e[t](i,n);e._syncList=[];const o=n.length,s=i.length,a=Math.min(s,o);a&&e.parse(0,a),s>o?e._insertElements(o,s-o,t):s{for(t.length+=e,r=t.length-1;r>=a;r--)t[r]=t[r-e]};for(l(s),r=t;r{o[t]=n[t]&&n[t].active()?n[t]._to:i[t]})),o}}zi.defaults={},zi.defaultRoutes=void 0;const Ii=new Map;function Fi(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let n=Ii.get(i);return n||(n=new Intl.NumberFormat(t,e),Ii.set(i,n)),n}(e,i).format(t)}const Bi={values:t=>Y(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const n=this.chart.options.locale;let o,s=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(o="scientific"),s=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=Pt(Math.abs(s)),r=Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:o,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),Fi(t,n,l)},logarithmic(t,e,i){if(0===t)return"0";const n=t/Math.pow(10,Math.floor(Pt(t)));return 1===n||2===n||5===n?Bi.numeric.call(this,t,e,i):""}};var Vi={formatters:Bi};function Wi(t,e){const i=t.options.ticks,n=i.maxTicksLimit||function(t){const e=t.options.offset,i=t._tickSize(),n=t._length/i+(e?0:1),o=t._maxLength/i;return Math.floor(Math.min(n,o))}(t),o=i.major.enabled?function(t){const e=[];let i,n;for(i=0,n=t.length;in)return function(t,e,i,n){let o,s=0,a=i[0];for(n=Math.ceil(n),o=0;oo)return e}return Math.max(o,1)}(o,e,n);if(s>0){let t,i;const n=s>1?Math.round((r-a)/(s-1)):null;for(Ni(e,l,c,$(n)?0:a-n,a),t=0,i=s-1;te.lineWidth,tickColor:(t,e)=>e.color,offset:!1,borderDash:[],borderDashOffset:0,borderWidth:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:Vi.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),xt.route("scale.ticks","color","","color"),xt.route("scale.grid","color","","borderColor"),xt.route("scale.grid","borderColor","","borderColor"),xt.route("scale.title","color","","color"),xt.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t}),xt.describe("scales",{_fallback:"scale"}),xt.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t});const Hi=(t,e,i)=>"top"===e||"left"===e?t[e]+i:t[e]-i;function ji(t,e){const i=[],n=t.length/e,o=t.length;let s=0;for(;sa+r)))return c}function Yi(t){return t.drawTicks?t.tickLength:0}function Ui(t,e){if(!t.display)return 0;const i=Be(t.font,e),n=Fe(t.padding);return(Y(t.text)?t.text.length:1)*i.lineHeight+n.height}function Xi(t,e,i){let o=n(t);return(i&&"right"!==e||!i&&"right"===e)&&(o=(t=>"left"===t?"right":"right"===t?"left":t)(o)),o}class qi extends zi{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){const e=this;e.options=t.setContext(e.getContext()),e.axis=t.axis,e._userMin=e.parse(t.min),e._userMax=e.parse(t.max),e._suggestedMin=e.parse(t.suggestedMin),e._suggestedMax=e.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:n}=this;return t=q(t,Number.POSITIVE_INFINITY),e=q(e,Number.NEGATIVE_INFINITY),i=q(i,Number.POSITIVE_INFINITY),n=q(n,Number.NEGATIVE_INFINITY),{min:q(t,i),max:q(e,n),minDefined:X(t),maxDefined:X(e)}}getMinMax(t){const e=this;let i,{min:n,max:o,minDefined:s,maxDefined:a}=e.getUserBounds();if(s&&a)return{min:n,max:o};const r=e.getMatchingVisibleMetas();for(let l=0,c=r.length;l=s||n<=1||!t.isHorizontal())return void(t.labelRotation=o);const h=t._getLabelSizes(),d=h.widest.width,u=h.highest.height,f=Ht(t.chart.width-d,0,t.maxWidth);a=e.offset?t.maxWidth/n:f/(n-1),d+6>a&&(a=f/(n-(e.offset?.5:1)),r=t.maxHeight-Yi(e.grid)-i.padding-Ui(e.title,t.chart.options.font),l=Math.sqrt(d*d+u*u),c=zt(Math.min(Math.asin(Math.min((h.highest.height+6)/a,1)),Math.asin(Math.min(r/l,1))-Math.asin(u/l))),c=Math.max(o,Math.min(s,c))),t.labelRotation=c}afterCalculateLabelRotation(){Q(this.options.afterCalculateLabelRotation,[this])}beforeFit(){Q(this.options.beforeFit,[this])}fit(){const t=this,e={width:0,height:0},{chart:i,options:{ticks:n,title:o,grid:s}}=t,a=t._isVisible(),r=t.isHorizontal();if(a){const a=Ui(o,i.options.font);if(r?(e.width=t.maxWidth,e.height=Yi(s)+a):(e.height=t.maxHeight,e.width=Yi(s)+a),n.display&&t.ticks.length){const{first:i,last:o,widest:s,highest:a}=t._getLabelSizes(),l=2*n.padding,c=Et(t.labelRotation),h=Math.cos(c),d=Math.sin(c);if(r){const i=n.mirror?0:d*s.width+h*a.height;e.height=Math.min(t.maxHeight,e.height+i+l)}else{const i=n.mirror?0:h*s.width+d*a.height;e.width=Math.min(t.maxWidth,e.width+i+l)}t._calculatePadding(i,o,d,h)}}t._handleMargins(),r?(t.width=t._length=i.width-t._margins.left-t._margins.right,t.height=e.height):(t.width=e.width,t.height=t._length=i.height-t._margins.top-t._margins.bottom)}_calculatePadding(t,e,i,n){const o=this,{ticks:{align:s,padding:a},position:r}=o.options,l=0!==o.labelRotation,c="top"!==r&&"x"===o.axis;if(o.isHorizontal()){const r=o.getPixelForTick(0)-o.left,h=o.right-o.getPixelForTick(o.ticks.length-1);let d=0,u=0;l?c?(d=n*t.width,u=i*e.height):(d=i*t.height,u=n*e.width):"start"===s?u=e.width:"end"===s?d=t.width:(d=t.width/2,u=e.width/2),o.paddingLeft=Math.max((d-r+a)*o.width/(o.width-r),0),o.paddingRight=Math.max((u-h+a)*o.width/(o.width-h),0)}else{let i=e.height/2,n=t.height/2;"start"===s?(i=0,n=t.height):"end"===s&&(i=e.height,n=0),o.paddingTop=i+a,o.paddingBottom=n+a}}_handleMargins(){const t=this;t._margins&&(t._margins.left=Math.max(t.paddingLeft,t._margins.left),t._margins.top=Math.max(t.paddingTop,t._margins.top),t._margins.right=Math.max(t.paddingRight,t._margins.right),t._margins.bottom=Math.max(t.paddingBottom,t._margins.bottom))}afterFit(){Q(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){const e=this;let i,n;for(e.beforeTickToLabelConversion(),e.generateTickLabels(t),i=0,n=t.length;i{const i=t.gc,n=i.length/2;let o;if(n>e){for(o=0;o({width:o[t]||0,height:s[t]||0});return{first:v(0),last:v(e-1),widest:v(_),highest:v(y),widths:o,heights:s}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){const e=this;e._reversePixels&&(t=1-t);const i=e._startPixel+t*e._length;return jt(e._alignToPixels?Xt(e.chart,i,0):i)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this,i=e.ticks||[];if(t>=0&&tr*o?r/n:l/o:l*o0}_computeGridLineItems(t){const e=this,i=e.axis,n=e.chart,o=e.options,{grid:s,position:a}=o,r=s.offset,l=e.isHorizontal(),c=e.ticks.length+(r?1:0),h=Yi(s),d=[],u=s.setContext(e.getContext()),f=u.drawBorder?u.borderWidth:0,g=f/2,p=function(t){return Xt(n,t,f)};let m,x,b,_,y,v,w,M,k,S,P,D;if("top"===a)m=p(e.bottom),v=e.bottom-h,M=m-g,S=p(t.top)+g,D=t.bottom;else if("bottom"===a)m=p(e.top),S=t.top,D=p(t.bottom)-g,v=m+g,M=e.top+h;else if("left"===a)m=p(e.right),y=e.right-h,w=m-g,k=p(t.left)+g,P=t.right;else if("right"===a)m=p(e.left),k=t.left,P=p(t.right)-g,y=m+g,w=e.left+h;else if("x"===i){if("center"===a)m=p((t.top+t.bottom)/2+.5);else if(U(a)){const t=Object.keys(a)[0],i=a[t];m=p(e.chart.scales[t].getPixelForValue(i))}S=t.top,D=t.bottom,v=m+g,M=v+h}else if("y"===i){if("center"===a)m=p((t.left+t.right)/2);else if(U(a)){const t=Object.keys(a)[0],i=a[t];m=p(e.chart.scales[t].getPixelForValue(i))}y=m-g,w=y-h,k=t.left,P=t.right}const C=K(o.ticks.maxTicksLimit,c),O=Math.max(1,Math.ceil(c/C));for(x=0;xe.value===t));if(n>=0){return i.setContext(e.getContext(n)).lineWidth}return 0}drawGrid(t){const e=this,i=e.options.grid,n=e.ctx,o=e._gridLineItems||(e._gridLineItems=e._computeGridLineItems(t));let s,a;const r=(t,e,i)=>{i.width&&i.color&&(n.save(),n.lineWidth=i.width,n.strokeStyle=i.color,n.setLineDash(i.borderDash||[]),n.lineDashOffset=i.borderDashOffset,n.beginPath(),n.moveTo(t.x,t.y),n.lineTo(e.x,e.y),n.stroke(),n.restore())};if(i.display)for(s=0,a=o.length;st[0])){ht(n)||(n=rn("_fallback",t));const s={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:i,_fallback:n,_getTarget:o,override:o=>Ki([o,...t],e,i,n)};return new Proxy(s,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,n)=>tn(i,n,(()=>function(t,e,i,n){let o;for(const s of e)if(o=rn(Qi(s,t),i),ht(o))return Ji(t,o)?sn(i,n,t,o):o}(n,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ln(t).includes(e),ownKeys:t=>ln(t),set:(t,e,i)=>((t._storage||(t._storage=o()))[e]=i,delete t[e],delete t._keys,!0)})}function Gi(t,e,i,n){const o={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Zi(t,n),setContext:e=>Gi(t,e,i,n),override:o=>Gi(t.override(o),e,i,n)};return new Proxy(o,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>tn(t,e,(()=>function(t,e,i){const{_proxy:n,_context:o,_subProxy:s,_descriptors:a}=t;let r=n[e];dt(r)&&a.isScriptable(e)&&(r=function(t,e,i,n){const{_proxy:o,_context:s,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t),e=e(s,a||n),r.delete(t),U(e)&&(e=sn(o._scopes,o,t,e));return e}(e,r,t,i));Y(r)&&r.length&&(r=function(t,e,i,n){const{_proxy:o,_context:s,_subProxy:a,_descriptors:r}=i;if(ht(s.index)&&n(t))e=e[s.index%e.length];else if(U(e[0])){const i=e,n=o._scopes.filter((t=>t!==i));e=[];for(const l of i){const i=sn(n,o,t,l);e.push(Gi(i,s,a&&a[t],r))}}return e}(e,r,t,a.isIndexable));Ji(e,r)&&(r=Gi(r,o,s&&s[e],a));return r}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,n)=>(t[i]=n,delete e[i],!0)})}function Zi(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:n=e.indexable,_allKeys:o=e.allKeys}=t;return{allKeys:o,scriptable:i,indexable:n,isScriptable:dt(i)?i:()=>i,isIndexable:dt(n)?n:()=>n}}const Qi=(t,e)=>t?t+ct(e):e,Ji=(t,e)=>U(e)&&"adapters"!==t;function tn(t,e,i){let n=t[e];return ht(n)||(n=i(),ht(n)&&(t[e]=n)),n}function en(t,e,i){return dt(t)?t(e,i):t}const nn=(t,e)=>!0===t?e:"string"==typeof t?lt(e,t):void 0;function on(t,e,i,n){for(const o of e){const e=nn(i,o);if(e){t.add(e);const o=en(e._fallback,i,e);if(ht(o)&&o!==i&&o!==n)return o}else if(!1===e&&ht(n)&&i!==n)return null}return!1}function sn(t,e,i,n){const o=e._rootScopes,s=en(e._fallback,i,n),a=[...t,...o],r=new Set;r.add(n);let l=an(r,a,i,s||i);return null!==l&&((!ht(s)||s===i||(l=an(r,a,s,l),null!==l))&&Ki(Array.from(r),[""],o,s,(()=>function(t,e,i){const n=t._getTarget();e in n||(n[e]={});const o=n[e];if(Y(o)&&U(i))return i;return o}(e,i,n))))}function an(t,e,i,n){for(;i;)i=on(t,e,i,n);return i}function rn(t,e){for(const i of e){if(!i)continue;const e=i[t];if(ht(e))return e}}function ln(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}const cn=Number.EPSILON||1e-14,hn=(t,e)=>e"x"===t?"y":"x";function un(t,e,i,n){const o=t.skip?e:t,s=e,a=i.skip?e:i,r=Bt(s,o),l=Bt(a,s);let c=r/(r+l),h=l/(r+l);c=isNaN(c)?0:c,h=isNaN(h)?0:h;const d=n*c,u=n*h;return{previous:{x:s.x-d*(a.x-o.x),y:s.y-d*(a.y-o.y)},next:{x:s.x+u*(a.x-o.x),y:s.y+u*(a.y-o.y)}}}function fn(t,e="x"){const i=dn(e),n=t.length,o=Array(n).fill(0),s=Array(n);let a,r,l,c=hn(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)fn(t,o);else{let i=n?t[t.length-1]:t[0];for(s=0,a=t.length;s0?e.y:t.y}}function bn(t,e,i,n){const o={x:t.cp2x,y:t.cp2y},s={x:e.cp1x,y:e.cp1y},a=mn(t,o,i),r=mn(o,s,i),l=mn(s,e,i),c=mn(a,r,i),h=mn(r,l,i);return mn(c,h,i)}function _n(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function yn(t,e){let i,n;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,n=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=n)}function vn(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function wn(t){return"angle"===t?{between:Nt,compare:Vt,normalize:Wt}:{between:(t,e,i)=>t>=Math.min(e,i)&&t<=Math.max(i,e),compare:(t,e)=>t-e,normalize:t=>t}}function Mn({start:t,end:e,count:i,loop:n,style:o}){return{start:t%i,end:e%i,loop:n&&(e-t+1)%i==0,style:o}}function kn(t,e,i){if(!i)return[t];const{property:n,start:o,end:s}=i,a=e.length,{compare:r,between:l,normalize:c}=wn(n),{start:h,end:d,loop:u,style:f}=function(t,e,i){const{property:n,start:o,end:s}=i,{between:a,normalize:r}=wn(n),l=e.length;let c,h,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,c=0,h=l;cb||l(o,x,p)&&0!==r(o,x),v=()=>!b||0===r(s,p)||l(s,x,p);for(let t=h,i=h;t<=d;++t)m=e[t%a],m.skip||(p=c(m[n]),p!==x&&(b=l(p,o,s),null===_&&y()&&(_=0===r(p,o)?t:i),null!==_&&v()&&(g.push(Mn({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,x=p));return null!==_&&g.push(Mn({start:_,end:d,loop:u,count:a,style:f})),g}function Sn(t,e){const i=[],n=t.segments;for(let o=0;oo&&t[s%e].skip;)s--;return s%=e,{start:o,end:s}}(i,o,s,n);if(!0===n)return Dn([{start:a,end:r,loop:s}],i,e);return Dn(function(t,e,i,n){const o=t.length,s=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%o];i.skip||i.stop?l.skip||(n=!1,s.push({start:e%o,end:(a-1)%o,loop:n}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&s.push({start:e%o,end:r%o,loop:n}),s}(i,a,r{const n=i.split("."),o=n.pop(),s=[t].concat(n).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");xt.route(s,o,l,r)}))}(e,t.defaultRoutes);t.descriptors&&xt.describe(e,t.descriptors)}(t,a,n),e.override&&xt.override(t.id,t.overrides)),a}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,n=this.scope;i in e&&delete e[i],n&&i in xt[n]&&(delete xt[n][i],this.override&&delete ft[i])}}var Ln=new class{constructor(){this.controllers=new An(Ei,"datasets",!0),this.elements=new An(zi,"elements"),this.plugins=new An(Object,"plugins"),this.scales=new An(qi,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){const n=this;[...e].forEach((e=>{const o=i||n._getRegistryForType(e);i||o.isForType(e)||o===n.plugins&&e.id?n._exec(t,o,e):J(e,(e=>{const o=i||n._getRegistryForType(e);n._exec(t,o,e)}))}))}_exec(t,e,i){const n=ct(t);Q(i["before"+n],[],i),e[t](i),Q(i["after"+n],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(n(e,i),t,"stop"),this._notify(n(i,e),t,"start")}}function En(t,e){return e||!1!==t?!0===t?{}:t:null}function zn(t,e,i,n){const o=t.pluginScopeKeys(e),s=t.getOptionScopes(i,o);return t.createResolver(s,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function In(t,e){const i=xt.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function Fn(t,e){return"x"===t||"y"===t?t:e.axis||("top"===(i=e.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.charAt(0).toLowerCase();var i}function Bn(t){const e=t.options||(t.options={});e.plugins=K(e.plugins,{}),e.scales=function(t,e){const i=ft[t.type]||{scales:{}},n=e.scales||{},o=In(t.type,e),s=Object.create(null),a=Object.create(null);return Object.keys(n).forEach((t=>{const e=n[t],r=Fn(t,e),l=function(t,e){return t===e?"_index_":"_value_"}(r,o),c=i.scales||{};s[r]=s[r]||t,a[t]=st(Object.create(null),[{axis:r},e,c[r],c[l]])})),t.data.datasets.forEach((i=>{const o=i.type||t.type,r=i.indexAxis||In(o,e),l=(ft[o]||{}).scales||{};Object.keys(l).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,r),o=i[e+"AxisID"]||s[e]||e;a[o]=a[o]||Object.create(null),st(a[o],[{axis:e},n[o],l[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];st(e,[xt.scales[e.type],xt.scale])})),a}(t,e)}function Vn(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const Wn=new Map,Nn=new Set;function Hn(t,e){let i=Wn.get(t);return i||(i=e(),Wn.set(t,i),Nn.add(i)),i}const jn=(t,e,i)=>{const n=lt(e,i);void 0!==n&&t.add(n)};class $n{constructor(t){this._config=function(t){return(t=t||{}).data=Vn(t.data),Bn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=Vn(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),Bn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return Hn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return Hn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return Hn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return Hn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let n=i.get(t);return n&&!e||(n=new Map,i.set(t,n)),n}getOptionScopes(t,e,i){const{options:n,type:o}=this,s=this._cachedScopes(t,i),a=s.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>jn(r,t,e)))),e.forEach((t=>jn(r,n,t))),e.forEach((t=>jn(r,ft[o]||{},t))),e.forEach((t=>jn(r,xt,t))),e.forEach((t=>jn(r,gt,t)))}));const l=Array.from(r);return Nn.has(e)&&s.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,ft[e]||{},xt.datasets[e]||{},{type:e},xt,gt]}resolveNamedOptions(t,e,i,n=[""]){const o={$shared:!0},{resolver:s,subPrefixes:a}=Yn(this._resolverCache,t,n);let r=s;if(function(t,e){const{isScriptable:i,isIndexable:n}=Zi(t);for(const o of e)if(i(o)&&dt(t[o])||n(o)&&Y(t[o]))return!0;return!1}(s,e)){o.$shared=!1;r=Gi(s,i=dt(i)?i():i,this.createResolver(t,i,a))}for(const t of e)o[t]=r[t];return o}createResolver(t,e,i=[""],n){const{resolver:o}=Yn(this._resolverCache,t,i);return U(e)?Gi(o,e,void 0,n):o}}function Yn(t,e,i){let n=t.get(e);n||(n=new Map,t.set(e,n));const o=i.join();let s=n.get(o);if(!s){s={resolver:Ki(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},n.set(o,s)}return s}const Un=["top","bottom","left","right","chartArea"];function Xn(t,e){return"top"===t||"bottom"===t||-1===Un.indexOf(t)&&"x"===e}function qn(t,e){return function(i,n){return i[t]===n[t]?i[e]-n[e]:i[t]-n[t]}}function Kn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),Q(i&&i.onComplete,[t],e)}function Gn(t){const e=t.chart,i=e.options.animation;Q(i&&i.onProgress,[t],e)}function Zn(){return"undefined"!=typeof window&&"undefined"!=typeof document}function Qn(t){return Zn()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Jn={},to=t=>{const e=Qn(t);return Object.values(Jn).filter((t=>t.canvas===e)).pop()};class eo{constructor(t,e){const n=this;this.config=e=new $n(e);const o=Qn(t),s=to(o);if(s)throw new Error("Canvas is already in use. Chart with ID '"+s.id+"' must be destroyed before the canvas can be reused.");const r=e.createResolver(e.chartOptionScopes(),n.getContext());this.platform=n._initializePlatform(o,e);const l=n.platform.acquireContext(o,r.aspectRatio),c=l&&l.canvas,h=c&&c.height,d=c&&c.width;this.id=j(),this.ctx=l,this.canvas=c,this.width=d,this.height=h,this._options=r,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this.scale=void 0,this._plugins=new Rn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=i((()=>this.update("resize")),r.resizeDelay||0),Jn[n.id]=n,l&&c?(a.listen(n,"complete",Kn),a.listen(n,"progress",Gn),n._initialize(),n.attached&&n.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return $(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}_initialize(){const t=this;return t.notifyPlugins("beforeInit"),t.options.responsive?t.resize():ve(t,t.options.devicePixelRatio),t.bindEvents(),t.notifyPlugins("afterInit"),t}_initializePlatform(t,e){return e.platform?new e.platform:!Zn()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?new Je:new ui}clear(){return qt(this.canvas,this.ctx),this}stop(){return a.stop(this),this}resize(t,e){a.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this,n=i.options,o=i.canvas,s=n.maintainAspectRatio&&i.aspectRatio,a=i.platform.getMaximumSize(o,t,e,s),r=n.devicePixelRatio||i.platform.getDevicePixelRatio();i.width=a.width,i.height=a.height,i._aspectRatio=i.aspectRatio,ve(i,r,!0)&&(i.notifyPlugins("resize",{size:a}),Q(n.onResize,[i,a],i),i.attached&&i._doResize()&&i.render())}ensureScalesHaveIDs(){J(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this,e=t.options,i=e.scales,n=t.scales,o=Object.keys(n).reduce(((t,e)=>(t[e]=!1,t)),{});let s=[];i&&(s=s.concat(Object.keys(i).map((t=>{const e=i[t],n=Fn(t,e),o="r"===n,s="x"===n;return{options:e,dposition:o?"chartArea":s?"bottom":"left",dtype:o?"radialLinear":s?"category":"linear"}})))),J(s,(i=>{const s=i.options,a=s.id,r=Fn(a,s),l=K(s.type,i.dtype);void 0!==s.position&&Xn(s.position,r)===Xn(i.dposition)||(s.position=i.dposition),o[a]=!0;let c=null;if(a in n&&n[a].type===l)c=n[a];else{c=new(Ln.getScale(l))({id:a,type:l,ctx:t.ctx,chart:t}),n[c.id]=c}c.init(s,e)})),J(o,((t,e)=>{t||delete n[e]})),J(n,(e=>{Ze.configure(t,e,e.options),Ze.addBox(t,e)}))}_updateMetasets(){const t=this,e=t._metasets,i=t.data.datasets.length,n=e.length;if(e.sort(((t,e)=>t.index-e.index)),n>i){for(let e=i;ei.length&&delete t._stacks,e.forEach(((e,n)=>{0===i.filter((t=>t===e._dataset)).length&&t._destroyDatasetMeta(n)}))}buildOrUpdateControllers(){const t=this,e=[],i=t.data.datasets;let n,o;for(t._removeUnreferencedMetasets(),n=0,o=i.length;n{t.getDatasetMeta(i).controller.reset()}),t)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this,i=e.config;i.update(),e._options=i.createResolver(i.chartOptionScopes(),e.getContext()),J(e.scales,(t=>{Ze.removeBox(e,t)}));const n=e._animationsDisabled=!e.options.animation;e.ensureScalesHaveIDs(),e.buildOrUpdateScales();const o=new Set(Object.keys(e._listeners)),s=new Set(e.options.events);if(ut(o,s)&&!!this._responsiveListeners===e.options.responsive||(e.unbindEvents(),e.bindEvents()),e._plugins.invalidate(),!1===e.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const a=e.buildOrUpdateControllers();e.notifyPlugins("beforeElementsUpdate");let r=0;for(let t=0,i=e.data.datasets.length;t{t.reset()})),e._updateDatasets(t),e.notifyPlugins("afterUpdate",{mode:t}),e._layers.sort(qn("z","_idx")),e._lastEvent&&e._eventHandler(e._lastEvent,!0),e.render()}_updateLayout(t){const e=this;if(!1===e.notifyPlugins("beforeLayout",{cancelable:!0}))return;Ze.update(e,e.width,e.height,t);const i=e.chartArea,n=i.width<=0||i.height<=0;e._layers=[],J(e.boxes,(t=>{n&&"chartArea"===t.position||(t.configure&&t.configure(),e._layers.push(...t._layers()))}),e),e._layers.forEach(((t,e)=>{t._idx=e})),e.notifyPlugins("afterLayout")}_updateDatasets(t){const e=this,i="function"==typeof t;if(!1!==e.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let n=0,o=e.data.datasets.length;n=0;--i)t._drawDataset(e[i]);t.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this,i=e.ctx,n=t._clip,o=!n.disabled,s=e.chartArea,a={meta:t,index:t.index,cancelable:!0};!1!==e.notifyPlugins("beforeDatasetDraw",a)&&(o&&Zt(i,{left:!1===n.left?0:s.left-n.left,right:!1===n.right?e.width:s.right+n.right,top:!1===n.top?0:s.top-n.top,bottom:!1===n.bottom?e.height:s.bottom+n.bottom}),t.controller.draw(),o&&Qt(i),a.cancelable=!1,e.notifyPlugins("afterDatasetDraw",a))}getElementsAtEventForMode(t,e,i,n){const o=Te.modes[e];return"function"==typeof o?o(this,t,i,n):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let n=i.filter((t=>t&&t._dataset===e)).pop();return n||(n={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(n)),n}getContext(){return this.$context||(this.$context={chart:this,type:"chart"})}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateDatasetVisibility(t,e){const i=this,n=e?"show":"hide",o=i.getDatasetMeta(t),s=o.controller._resolveAnimations(void 0,n);i.setDatasetVisibility(t,e),s.update(o,{visible:e}),i.update((e=>e.datasetIndex===t?n:void 0))}hide(t){this._updateDatasetVisibility(t,!1)}show(t){this._updateDatasetVisibility(t,!0)}_destroyDatasetMeta(t){const e=this,i=e._metasets&&e._metasets[t];i&&i.controller&&(i.controller._destroy(),delete e._metasets[t])}destroy(){const t=this,{canvas:e,ctx:i}=t;let n,o;for(t.stop(),a.remove(t),n=0,o=t.data.datasets.length;n((n,o)=>{i.addEventListener(t,n,o),e[n]=o})(o,n)))}bindResponsiveEvents(){const t=this;t._responsiveListeners||(t._responsiveListeners={});const e=t._responsiveListeners,i=t.platform,n=(n,o)=>{i.addEventListener(t,n,o),e[n]=o},o=(n,o)=>{e[n]&&(i.removeEventListener(t,n,o),delete e[n])},s=(e,i)=>{t.canvas&&t.resize(e,i)};let a;const r=()=>{o("attach",r),t.attached=!0,t.resize(),n("resize",s),n("detach",a)};a=()=>{t.attached=!1,o("resize",s),n("attach",r)},i.isAttached(t.canvas)?r():a()}unbindEvents(){const t=this;J(t._listeners,((e,i)=>{t.platform.removeEventListener(t,i,e)})),t._listeners={},J(t._responsiveListeners,((e,i)=>{t.platform.removeEventListener(t,i,e)})),t._responsiveListeners=void 0}updateHoverStyle(t,e,i){const n=i?"set":"remove";let o,s,a,r;for("dataset"===e&&(o=this.getDatasetMeta(t[0].datasetIndex),o.controller["_"+n+"DatasetHoverStyle"]()),a=0,r=t.length;a{const n=e.getDatasetMeta(t);if(!n)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:n.data[i],index:i}}));!tt(n,i)&&(e._active=n,e._updateHoverStyles(n,i))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}_updateHoverStyles(t,e,i){const n=this,o=n.options.hover,s=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),a=s(e,t),r=i?t:s(t,e);a.length&&n.updateHoverStyle(a,o.mode,!1),r.length&&o.mode&&n.updateHoverStyle(r,o.mode,!0)}_eventHandler(t,e){const i=this,n={event:t,replay:e,cancelable:!0},o=e=>(e.options.events||this.options.events).includes(t.type);if(!1===i.notifyPlugins("beforeEvent",n,o))return;const s=i._handleEvent(t,e);return n.cancelable=!1,i.notifyPlugins("afterEvent",n,o),(s||n.changed)&&i.render(),i}_handleEvent(t,e){const i=this,{_active:n=[],options:o}=i,s=o.hover,a=e;let r=[],l=!1,c=null;return"mouseout"!==t.type&&(r=i.getElementsAtEventForMode(t,s.mode,s,a),c="click"===t.type?i._lastEvent:t),i._lastEvent=null,Gt(t,i.chartArea,i._minPadding)&&(Q(o.onHover,[t,r,i],i),"mouseup"!==t.type&&"click"!==t.type&&"contextmenu"!==t.type||Q(o.onClick,[t,r,i],i)),l=!tt(r,n),(l||e)&&(i._active=r,i._updateHoverStyles(r,n,e)),i._lastEvent=c,l}}const io=()=>J(eo.instances,(t=>t._plugins.invalidate())),no=!0;function oo(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}Object.defineProperties(eo,{defaults:{enumerable:no,value:xt},instances:{enumerable:no,value:Jn},overrides:{enumerable:no,value:ft},registry:{enumerable:no,value:Ln},version:{enumerable:no,value:"3.4.1"},getChart:{enumerable:no,value:to},register:{enumerable:no,value:(...t)=>{Ln.add(...t),io()}},unregister:{enumerable:no,value:(...t)=>{Ln.remove(...t),io()}}});class so{constructor(t){this.options=t||{}}formats(){return oo()}parse(t,e){return oo()}format(t,e){return oo()}add(t,e,i){return oo()}diff(t,e,i){return oo()}startOf(t,e,i){return oo()}endOf(t,e){return oo()}}so.override=function(t){Object.assign(so.prototype,t)};var ao={_date:so};function ro(t){const e=function(t){if(!t._cache.$bar){const e=t.getMatchingVisibleMetas("bar");let i=[];for(let n=0,o=e.length;nt-e)))}return t._cache.$bar}(t);let i,n,o,s,a=t._length;const r=()=>{32767!==o&&-32768!==o&&(ht(s)&&(a=Math.min(a,Math.abs(o-s)||a)),s=o)};for(i=0,n=e.length;iMath.abs(r)&&(l=r,c=a),e[i.axis]=c,e._custom={barStart:l,barEnd:c,start:o,end:s,min:a,max:r}}(t,e,i,n):e[i.axis]=i.parse(t,n),e}function co(t,e,i,n){const o=t.iScale,s=t.vScale,a=o.getLabels(),r=o===s,l=[];let c,h,d,u;for(c=i,h=i+n;c0?(p+=t,h-=t):h<0&&(p-=t,h+=t)}return{size:h,base:p,head:c,center:c+h/2}}_calculateBarIndexPixels(t,e){const i=this,n=e.scale,o=i.options,s=o.skipNull,a=K(o.maxBarThickness,1/0);let r,l;if(e.grouped){const n=s?i._getStackCount(t):e.stackCount,c="flex"===o.barThickness?function(t,e,i,n){const o=e.pixels,s=o[t];let a=t>0?o[t-1]:null,r=t=0;--n)i=Math.max(i,t[n].size()/2,e[n]._custom);return i>0&&i}getLabelAndValue(t){const e=this._cachedMeta,{xScale:i,yScale:n}=e,o=this.getParsed(t),s=i.getLabelForValue(o.x),a=n.getLabelForValue(o.y),r=o._custom;return{label:e.label,value:"("+s+", "+a+(r?", "+r:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,n){const o=this,s="reset"===n,{iScale:a,vScale:r}=o._cachedMeta,l=o.resolveDataElementOptions(e,n),c=o.getSharedOptions(l),h=o.includeOptions(n,c),d=a.axis,u=r.axis;for(let l=e;l""}}}};class go extends Ei{constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,n=this._cachedMeta;let o,s;for(o=t,s=t+e;oNt(t,r,l,!0)?1:Math.max(e,e*i,n,n*i),g=(t,e,n)=>Nt(t,r,l,!0)?-1:Math.min(e,e*i,n,n*i),p=f(0,c,d),m=f(Mt,h,u),x=g(bt,c,d),b=g(bt+Mt,h,u);n=(p-x)/2,o=(m-b)/2,s=-(p+x)/2,a=-(m+b)/2}return{ratioX:n,ratioY:o,offsetX:s,offsetY:a}}(d,h,l),m=(n.width-a)/u,x=(n.height-a)/f,b=Math.max(Math.min(m,x)/2,0),_=Z(e.options.radius,b),y=(_-Math.max(_*l,0))/e._getVisibleDatasetWeightTotal();e.offsetX=g*_,e.offsetY=p*_,o.total=e.calculateTotal(),e.outerRadius=_-y*e._getRingWeightOffset(e.index),e.innerRadius=Math.max(e.outerRadius-y*c,0),e.updateElements(s,0,s.length,t)}_circumference(t,e){const i=this,n=i.options,o=i._cachedMeta,s=i._getCircumference();return e&&n.animation.animateRotate||!this.chart.getDataVisibility(t)||null===o._parsed[t]?0:i.calculateCircumference(o._parsed[t]*s/_t)}updateElements(t,e,i,n){const o=this,s="reset"===n,a=o.chart,r=a.chartArea,l=a.options.animation,c=(r.left+r.right)/2,h=(r.top+r.bottom)/2,d=s&&l.animateScale,u=d?0:o.innerRadius,f=d?0:o.outerRadius,g=o.resolveDataElementOptions(e,n),p=o.getSharedOptions(g),m=o.includeOptions(n,p);let x,b=o._getRotation();for(x=0;x0&&!isNaN(t)?_t*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,n=i.data.labels||[],o=Fi(e._parsed[t],i.options.locale);return{label:n[t]||"",value:o}}getMaxBorderWidth(t){const e=this;let i=0;const n=e.chart;let o,s,a,r,l;if(!t)for(o=0,s=n.data.datasets.length;o"spacing"!==t,_indexable:t=>"spacing"!==t},go.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}},tooltip:{callbacks:{title:()=>"",label(t){let e=t.label;const i=": "+t.formattedValue;return Y(e)?(e=e.slice(),e[0]+=i):e+=i,e}}}}};class po extends Ei{initialize(){this.enableOptionSharing=!0,super.initialize()}update(t){const e=this,i=e._cachedMeta,{dataset:n,data:o=[],_dataset:s}=i,a=e.chart._animationsDisabled;let{start:r,count:l}=function(t,e,i){const n=e.length;let o=0,s=n;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:c,max:h,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(o=Ht(Math.min(se(r,a.axis,c).lo,i?n:se(e,l,a.getPixelForValue(c)).lo),0,n-1)),s=u?Ht(Math.max(se(r,a.axis,h).hi+1,i?0:se(e,l,a.getPixelForValue(h)).hi+1),o,n)-o:n-o}return{start:o,count:s}}(i,o,a);e._drawStart=r,e._drawCount=l,function(t){const{xScale:e,yScale:i,_scaleRanges:n}=t,o={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!n)return t._scaleRanges=o,!0;const s=n.xmin!==e.min||n.xmax!==e.max||n.ymin!==i.min||n.ymax!==i.max;return Object.assign(n,o),s}(i)&&(r=0,l=o.length),n._decimated=!!s._decimated,n.points=o;const c=e.resolveDatasetElementOptions(t);e.options.showLine||(c.borderWidth=0),c.segment=e.options.segment,e.updateElement(n,void 0,{animated:!a,options:c},t),e.updateElements(o,r,l,t)}updateElements(t,e,i,n){const o=this,s="reset"===n,{iScale:a,vScale:r,_stacked:l}=o._cachedMeta,c=o.resolveDataElementOptions(e,n),h=o.getSharedOptions(c),d=o.includeOptions(n,h),u=a.axis,f=r.axis,g=o.options.spanGaps,p=Tt(g)?g:Number.POSITIVE_INFINITY,m=o.chart._animationsDisabled||s||"none"===n;let x=e>0&&o.getParsed(e-1);for(let c=e;c0&&i[u]-x[u]>p,g.parsed=i,d&&(g.options=h||o.resolveDataElementOptions(c,e.active?"active":n)),m||o.updateElement(e,c,g,n),x=i}o.updateSharedOptions(h,n,c)}getMaxOverflow(){const t=this,e=t._cachedMeta,i=e.dataset,n=i.options&&i.options.borderWidth||0,o=e.data||[];if(!o.length)return n;const s=o[0].size(t.resolveDataElementOptions(0)),a=o[o.length-1].size(t.resolveDataElementOptions(o.length-1));return Math.max(n,s,a)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}}po.id="line",po.defaults={datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1},po.overrides={scales:{_index_:{type:"category"},_value_:{type:"linear"}}};class mo extends Ei{constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,n=i.data.labels||[],o=Fi(e._parsed[t].r,i.options.locale);return{label:n[t]||"",value:o}}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}_updateRadius(){const t=this,e=t.chart,i=e.chartArea,n=e.options,o=Math.min(i.right-i.left,i.bottom-i.top),s=Math.max(o/2,0),a=(s-Math.max(n.cutoutPercentage?s/100*n.cutoutPercentage:1,0))/e.getVisibleDatasetCount();t.outerRadius=s-a*t.index,t.innerRadius=t.outerRadius-a}updateElements(t,e,i,n){const o=this,s="reset"===n,a=o.chart,r=o.getDataset(),l=a.options.animation,c=o._cachedMeta.rScale,h=c.xCenter,d=c.yCenter,u=c.getIndexAngle(0)-.5*bt;let f,g=u;const p=360/o.countVisibleElements();for(f=0;f{!isNaN(t.data[n])&&this.chart.getDataVisibility(n)&&i++})),i}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?Et(this.resolveDataElementOptions(t,e).angle||i):0}}mo.id="polarArea",mo.defaults={dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0},mo.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}},tooltip:{callbacks:{title:()=>"",label:t=>t.chart.data.labels[t.dataIndex]+": "+t.formattedValue}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};class xo extends go{}xo.id="pie",xo.defaults={cutout:0,rotation:0,circumference:360,radius:"100%"};class bo extends Ei{getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}update(t){const e=this,i=e._cachedMeta,n=i.dataset,o=i.data||[],s=i.iScale.getLabels();if(n.points=o,"resize"!==t){const i=e.resolveDatasetElementOptions(t);e.options.showLine||(i.borderWidth=0);const a={_loop:!0,_fullLoop:s.length===o.length,options:i};e.updateElement(n,void 0,a,t)}e.updateElements(o,0,o.length,t)}updateElements(t,e,i,n){const o=this,s=o.getDataset(),a=o._cachedMeta.rScale,r="reset"===n;for(let l=e;l"",label:t=>"("+t.label+", "+t.formattedValue+")"}}},scales:{x:{type:"linear"},y:{type:"linear"}}};var yo=Object.freeze({__proto__:null,BarController:uo,BubbleController:fo,DoughnutController:go,LineController:po,PolarAreaController:mo,PieController:xo,RadarController:bo,ScatterController:_o});function vo(t,e,i){const{startAngle:n,pixelMargin:o,x:s,y:a,outerRadius:r,innerRadius:l}=e;let c=o/r;t.beginPath(),t.arc(s,a,r,n-c,i+c),l>o?(c=o/l,t.arc(s,a,l,i+c,n-c,!0)):t.arc(s,a,o,i+Mt,n-Mt),t.closePath(),t.clip()}function wo(t,e,i,n){const o=Ee(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const s=(i-e)/2,a=Math.min(s,n*e/2),r=t=>{const e=(i-Math.min(s,t))*n/2;return Ht(t,0,Math.min(s,e))};return{outerStart:r(o.outerStart),outerEnd:r(o.outerEnd),innerStart:Ht(o.innerStart,0,a),innerEnd:Ht(o.innerEnd,0,a)}}function Mo(t,e,i,n){return{x:i+t*Math.cos(e),y:n+t*Math.sin(e)}}function ko(t,e,i,n,o){const{x:s,y:a,startAngle:r,pixelMargin:l,innerRadius:c}=e,h=Math.max(e.outerRadius+n+i-l,0),d=c>0?c+n+i+l:0;let u=0;const f=o-r;if(n){const t=((c>0?c-n:0)+(h>0?h-n:0))/2;u=(f-(0!==t?f*t/(t+n):f))/2}const g=(f-Math.max(.001,f*h-i/bt)/h)/2,p=r+g+u,m=o-g-u,{outerStart:x,outerEnd:b,innerStart:_,innerEnd:y}=wo(e,d,h,m-p),v=h-x,w=h-b,M=p+x/v,k=m-b/w,S=d+_,P=d+y,D=p+_/S,C=m-y/P;if(t.beginPath(),t.arc(s,a,h,M,k),b>0){const e=Mo(w,k,s,a);t.arc(e.x,e.y,b,k,m+Mt)}const O=Mo(P,m,s,a);if(t.lineTo(O.x,O.y),y>0){const e=Mo(P,C,s,a);t.arc(e.x,e.y,y,m+Mt,C+Math.PI)}if(t.arc(s,a,d,m-y/d,p+_/d,!0),_>0){const e=Mo(S,D,s,a);t.arc(e.x,e.y,_,D+Math.PI,p-Mt)}const T=Mo(v,p,s,a);if(t.lineTo(T.x,T.y),x>0){const e=Mo(v,M,s,a);t.arc(e.x,e.y,x,p-Mt,M)}t.closePath()}function So(t,e,i,n,o){const{options:s}=e,a="inner"===s.borderAlign;s.borderWidth&&(a?(t.lineWidth=2*s.borderWidth,t.lineJoin="round"):(t.lineWidth=s.borderWidth,t.lineJoin="bevel"),e.fullCircles&&function(t,e,i){const{x:n,y:o,startAngle:s,pixelMargin:a,fullCircles:r}=e,l=Math.max(e.outerRadius-a,0),c=e.innerRadius+a;let h;for(i&&vo(t,e,s+_t),t.beginPath(),t.arc(n,o,c,s+_t,s,!0),h=0;h=_t||Nt(o,a,r))&&(s>=l+d&&s<=c+d)}getCenterPoint(t){const{x:e,y:i,startAngle:n,endAngle:o,innerRadius:s,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius","circumference"],t),{offset:r,spacing:l}=this.options,c=(n+o)/2,h=(s+a+l+r)/2;return{x:e+Math.cos(c)*h,y:i+Math.sin(c)*h}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const e=this,{options:i,circumference:n}=e,o=(i.offset||0)/2,s=(i.spacing||0)/2;if(e.pixelMargin="inner"===i.borderAlign?.33:0,e.fullCircles=n>_t?Math.floor(n/_t):0,0===n||e.innerRadius<0||e.outerRadius<0)return;t.save();let a=0;if(o){a=o/2;const i=(e.startAngle+e.endAngle)/2;t.translate(Math.cos(i)*a,Math.sin(i)*a),e.circumference>=bt&&(a=o)}t.fillStyle=i.backgroundColor,t.strokeStyle=i.borderColor;const r=function(t,e,i,n){const{fullCircles:o,startAngle:s,circumference:a}=e;let r=e.endAngle;if(o){ko(t,e,i,n,s+_t);for(let e=0;er&&s>r;return{count:n,start:l,loop:e.loop,ilen:c(a+(c?r-t:t))%s,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=o[b(0)],t.moveTo(d.x,d.y)),h=0;h<=r;++h){if(d=o[b(h)],d.skip)continue;const e=d.x,i=d.y,n=0|e;n===u?(ig&&(g=i),m=(x*m+e)/++x):(_(),t.lineTo(e,i),u=n,x=0,f=g=i),p=i}_()}function Lo(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?Ao:To}Po.id="arc",Po.defaults={borderAlign:"center",borderColor:"#fff",borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0},Po.defaultRoutes={backgroundColor:"backgroundColor"};const Ro="function"==typeof Path2D;function Eo(t,e,i,n){Ro&&1===e.segments.length?function(t,e,i,n){let o=e._path;o||(o=e._path=new Path2D,e.path(o,i,n)&&o.closePath()),Do(t,e.options),t.stroke(o)}(t,e,i,n):function(t,e,i,n){const{segments:o,options:s}=e,a=Lo(e);for(const r of o)Do(t,s,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+n-1})&&t.closePath(),t.stroke()}(t,e,i,n)}class zo extends zi{constructor(t){super(),this.animated=!0,this.options=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this,n=i.options;if((n.tension||"monotone"===n.cubicInterpolationMode)&&!n.stepped&&!i._pointsUpdated){const o=n.spanGaps?i._loop:i._fullLoop;pn(i._points,n,t,o,e),i._pointsUpdated=!0}}set points(t){const e=this;e._points=t,delete e._segments,delete e._path,e._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=Pn(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this,n=i.options,o=t[e],s=i.points,a=Sn(i,{property:e,start:o,end:o});if(!a.length)return;const r=[],l=function(t){return t.stepped?xn:t.tension||"monotone"===t.cubicInterpolationMode?bn:mn}(n);let c,h;for(c=0,h=a.length;c"borderDash"!==t&&"fill"!==t};class Fo extends zi{constructor(t){super(),this.options=void 0,this.parsed=void 0,this.skip=void 0,this.stop=void 0,t&&Object.assign(this,t)}inRange(t,e,i){const n=this.options,{x:o,y:s}=this.getProps(["x","y"],i);return Math.pow(t-o,2)+Math.pow(e-s,2)t.x):Wo(e,"bottom","top",t.base=a.left&&e<=a.right)&&(s||i>=a.top&&i<=a.bottom)}function Yo(t,e){t.rect(e.x,e.y,e.w,e.h)}Fo.id="point",Fo.defaults={borderWidth:1,hitRadius:1,hoverBorderWidth:1,hoverRadius:4,pointStyle:"circle",radius:3,rotation:0},Fo.defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};class Uo extends zi{constructor(t){super(),this.options=void 0,this.horizontal=void 0,this.base=void 0,this.width=void 0,this.height=void 0,t&&Object.assign(this,t)}draw(t){const e=this.options,{inner:i,outer:n}=jo(this),o=(s=n.radius).topLeft||s.topRight||s.bottomLeft||s.bottomRight?ne:Yo;var s;t.save(),n.w===i.w&&n.h===i.h||(t.beginPath(),o(t,n),t.clip(),o(t,i),t.fillStyle=e.borderColor,t.fill("evenodd")),t.beginPath(),o(t,i),t.fillStyle=e.backgroundColor,t.fill(),t.restore()}inRange(t,e,i){return $o(this,t,e,i)}inXRange(t,e){return $o(this,t,null,e)}inYRange(t,e){return $o(this,null,t,e)}getCenterPoint(t){const{x:e,y:i,base:n,horizontal:o}=this.getProps(["x","y","base","horizontal"],t);return{x:o?(e+n)/2:e,y:o?i:(i+n)/2}}getRange(t){return"x"===t?this.width/2:this.height/2}}Uo.id="bar",Uo.defaults={borderSkipped:"start",borderWidth:0,borderRadius:0,enableBorderRadius:!0,pointStyle:void 0},Uo.defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};var Xo=Object.freeze({__proto__:null,ArcElement:Po,LineElement:zo,PointElement:Fo,BarElement:Uo});function qo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{value:e})}}function Ko(t){t.data.datasets.forEach((t=>{qo(t)}))}var Go={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Ko(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:s,indexAxis:a}=e,r=t.getDatasetMeta(o),l=s||e.data;if("y"===Ve([a,t.options.indexAxis]))return;if("line"!==r.type)return;const c=t.scales[r.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let h,{start:d,count:u}=function(t,e){const i=e.length;let n,o=0;const{iScale:s}=t,{min:a,max:r,minDefined:l,maxDefined:c}=s.getUserBounds();return l&&(o=Ht(se(e,s.axis,a).lo,0,i-1)),n=c?Ht(se(e,s.axis,r).hi+1,o,i)-o:i-o,{start:o,count:n}}(r,l);if(u<=4*n)qo(e);else{switch($(s)&&(e._data=l,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":h=function(t,e,i,n,o){const s=o.samples||n;if(s>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(s-2);let l=0;const c=e+i-1;let h,d,u,f,g,p=e;for(a[l++]=t[p],h=0;hu&&(u=f,d=t[n],g=n);a[l++]=d,p=g}return a[l++]=t[c],a}(l,d,u,n,i);break;case"min-max":h=function(t,e,i,n){let o,s,a,r,l,c,h,d,u,f,g=0,p=0;const m=[],x=e+i-1,b=t[e].x,_=t[x].x-b;for(o=e;of&&(f=r,h=o),g=(p*g+s.x)/++p;else{const i=o-1;if(!$(c)&&!$(h)){const e=Math.min(c,h),n=Math.max(c,h);e!==d&&e!==i&&m.push({...t[e],x:g}),n!==d&&n!==i&&m.push({...t[n],x:g})}o>0&&i!==d&&m.push(t[i]),m.push(s),l=e,p=0,u=f=r,c=h=d=o}}return m}(l,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=h}}))},destroy(t){Ko(t)}};function Zo(t,e,i){const n=function(t){const e=t.options,i=e.fill;let n=K(i&&i.target,i);return void 0===n&&(n=!!e.backgroundColor),!1!==n&&null!==n&&(!0===n?"origin":n)}(t);if(U(n))return!isNaN(n.value)&&n;let o=parseFloat(n);return X(o)&&Math.floor(o)===o?("-"!==n[0]&&"+"!==n[0]||(o=e+o),!(o===e||o<0||o>=i)&&o):["origin","start","end","stack"].indexOf(n)>=0&&n}class Qo{constructor(t){this.x=t.x,this.y=t.y,this.radius=t.radius}pathSegment(t,e,i){const{x:n,y:o,radius:s}=this;return e=e||{start:0,end:_t},t.arc(n,o,s,e.end,e.start,!0),!i.bounds}interpolate(t){const{x:e,y:i,radius:n}=this,o=t.angle;return{x:e+Math.cos(o)*n,y:i+Math.sin(o)*n,angle:o}}}function Jo(t){return(t.scale||{}).getPointPositionForValue?function(t){const{scale:e,fill:i}=t,n=e.options,o=e.getLabels().length,s=[],a=n.reverse?e.max:e.min,r=n.reverse?e.min:e.max;let l,c,h;if(h="start"===i?a:"end"===i?r:U(i)?i.value:e.getBaseValue(),n.grid.circular)return c=e.getPointPositionForValue(0,a),new Qo({x:c.x,y:c.y,radius:e.getDistanceFromCenterForValue(h)});for(l=0;lt;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function es(t){const{chart:e,scale:i,index:n,line:o}=t,s=[],a=o.segments,r=o.points,l=function(t,e){const i=[],n=t.getSortedVisibleDatasetMetas();for(let t=0;t"line"===t.type&&!t.hidden;function ns(t,e,i){const n=[];for(let o=0;o=n&&o<=c){r=o===n,l=o===c;break}}return{first:r,last:l,point:n}}function ss(t,e){let i=[],n=!1;return Y(t)?(n=!0,i=t):i=function(t,e){const{x:i=null,y:n=null}=t||{},o=e.points,s=[];return e.segments.forEach((({start:t,end:e})=>{e=ts(t,e,o);const a=o[t],r=o[e];null!==n?(s.push({x:a.x,y:n}),s.push({x:r.x,y:n})):null!==i&&(s.push({x:i,y:a.y}),s.push({x:i,y:r.y}))})),s}(t,e),i.length?new zo({points:i,options:{tension:0},_loop:n,_fullLoop:n}):null}function as(t,e,i){let n=t[e].fill;const o=[e];let s;if(!i)return n;for(;!1!==n&&-1===o.indexOf(n);){if(!X(n))return n;if(s=t[n],!s)return!1;if(s.visible)return n;o.push(n),n=s.fill}return!1}function rs(t,e,i){t.beginPath(),e.path(t),t.lineTo(e.last().x,i),t.lineTo(e.first().x,i),t.closePath(),t.clip()}function ls(t,e,i,n){if(n)return;let o=e[t],s=i[t];return"angle"===t&&(o=Wt(o),s=Wt(s)),{property:t,start:o,end:s}}function cs(t,e,i,n){return t&&e?n(t[i],e[i]):t?t[i]:e?e[i]:0}function hs(t,e,i){const{top:n,bottom:o}=e.chart.chartArea,{property:s,start:a,end:r}=i||{};"x"===s&&(t.beginPath(),t.rect(a,n,r-a,o-n),t.clip())}function ds(t,e,i,n){const o=e.interpolate(i,n);o&&t.lineTo(o.x,o.y)}function us(t,e){const{line:i,target:n,property:o,color:s,scale:a}=e,r=function(t,e,i){const n=t.segments,o=t.points,s=e.points,a=[];for(const t of n){let{start:n,end:r}=t;r=ts(n,r,o);const l=ls(i,o[n],o[r],t.loop);if(!e.segments){a.push({source:t,target:l,start:o[n],end:o[r]});continue}const c=Sn(e,l);for(const e of c){const n=ls(i,s[e.start],s[e.end],e.loop),r=kn(t,o,n);for(const t of r)a.push({source:t,target:e,start:{[i]:cs(l,n,"start",Math.max)},end:{[i]:cs(l,n,"end",Math.min)}})}}return a}(i,n,o);for(const{source:e,target:l,start:c,end:h}of r){const{style:{backgroundColor:r=s}={}}=e;t.save(),t.fillStyle=r,hs(t,a,ls(o,c,h)),t.beginPath();const d=!!i.pathSegment(t,e);d?t.closePath():ds(t,n,h,o);const u=!!n.pathSegment(t,l,{move:d,reverse:!0}),f=d&&u;f||ds(t,n,c,o),t.closePath(),t.fill(f?"evenodd":"nonzero"),t.restore()}}function fs(t,e,i){const n=function(t){const{chart:e,fill:i,line:n}=t;if(X(i))return function(t,e){const i=t.getDatasetMeta(e);return i&&t.isDatasetVisible(e)?i.dataset:null}(e,i);if("stack"===i)return es(t);const o=Jo(t);return o instanceof Qo?o:ss(o,n)}(e),{line:o,scale:s,axis:a}=e,r=o.options,l=r.fill,c=r.backgroundColor,{above:h=c,below:d=c}=l||{};n&&o.points.length&&(Zt(t,i),function(t,e){const{line:i,target:n,above:o,below:s,area:a,scale:r}=e,l=i._loop?"angle":e.axis;t.save(),"x"===l&&s!==o&&(rs(t,n,a.top),us(t,{line:i,target:n,color:o,scale:r,property:l}),t.restore(),t.save(),rs(t,n,a.bottom)),us(t,{line:i,target:n,color:s,scale:r,property:l}),t.restore()}(t,{line:o,target:n,above:h,below:d,area:i,scale:s,axis:a}),Qt(t))}var gs={id:"filler",afterDatasetsUpdate(t,e,i){const n=(t.data.datasets||[]).length,o=[];let s,a,r,l;for(a=0;a=0;--e){const i=o[e].$filler;i&&(i.line.updateControlPoints(s,i.axis),n&&fs(t.ctx,i,s))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const n=t.getSortedVisibleDatasetMetas();for(let e=n.length-1;e>=0;--e){const i=n[e].$filler;i&&fs(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const n=e.meta.$filler;n&&!1!==n.fill&&"beforeDatasetDraw"===i.drawTime&&fs(t.ctx,n,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const ps=(t,e)=>{let{boxHeight:i=e,boxWidth:n=e}=t;return t.usePointStyle&&(i=Math.min(i,e),n=Math.min(n,e)),{boxWidth:n,boxHeight:i,itemHeight:Math.max(e,i)}};class ms extends zi{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){const n=this;n.maxWidth=t,n.maxHeight=e,n._margins=i,n.setDimensions(),n.buildLabels(),n.fit()}setDimensions(){const t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=t._margins.left,t.right=t.width):(t.height=t.maxHeight,t.top=t._margins.top,t.bottom=t.height)}buildLabels(){const t=this,e=t.options.labels||{};let i=Q(e.generateLabels,[t.chart],t)||[];e.filter&&(i=i.filter((i=>e.filter(i,t.chart.data)))),e.sort&&(i=i.sort(((i,n)=>e.sort(i,n,t.chart.data)))),t.options.reverse&&i.reverse(),t.legendItems=i}fit(){const t=this,{options:e,ctx:i}=t;if(!e.display)return void(t.width=t.height=0);const n=e.labels,o=Be(n.font),s=o.size,a=t._computeTitleHeight(),{boxWidth:r,itemHeight:l}=ps(n,s);let c,h;i.font=o.string,t.isHorizontal()?(c=t.maxWidth,h=t._fitRows(a,s,r,l)+10):(h=t.maxHeight,c=t._fitCols(a,s,r,l)+10),t.width=Math.min(c,e.maxWidth||t.maxWidth),t.height=Math.min(h,e.maxHeight||t.maxHeight)}_fitRows(t,e,i,n){const o=this,{ctx:s,maxWidth:a,options:{labels:{padding:r}}}=o,l=o.legendHitBoxes=[],c=o.lineWidths=[0],h=n+r;let d=t;s.textAlign="left",s.textBaseline="middle";let u=-1,f=-h;return o.legendItems.forEach(((t,o)=>{const g=i+e/2+s.measureText(t.text).width;(0===o||c[c.length-1]+g+2*r>a)&&(d+=h,c[c.length-(o>0?0:1)]=0,f+=h,u++),l[o]={left:0,top:f,row:u,width:g,height:n},c[c.length-1]+=g+r})),d}_fitCols(t,e,i,n){const o=this,{ctx:s,maxHeight:a,options:{labels:{padding:r}}}=o,l=o.legendHitBoxes=[],c=o.columnSizes=[],h=a-t;let d=r,u=0,f=0,g=0,p=0;return o.legendItems.forEach(((t,o)=>{const a=i+e/2+s.measureText(t.text).width;o>0&&f+n+2*r>h&&(d+=u+r,c.push({width:u,height:f}),g+=u+r,p++,u=f=0),l[o]={left:g,top:f,col:p,width:a,height:n},u=Math.max(u,a),f+=n+r})),d+=u,c.push({width:u,height:f}),d}adjustHitBoxes(){const t=this;if(!t.options.display)return;const e=t._computeTitleHeight(),{legendHitBoxes:i,options:{align:n,labels:{padding:s},rtl:a}}=t;if(this.isHorizontal()){let r=0,l=o(n,t.left+s,t.right-t.lineWidths[r]);for(const a of i)r!==a.row&&(r=a.row,l=o(n,t.left+s,t.right-t.lineWidths[r])),a.top+=t.top+e+s,a.left=l,l+=a.width+s;if(a){const e=i.reduce(((t,e)=>(t[e.row]=t[e.row]||[],t[e.row].push(e),t)),{}),n=[];Object.keys(e).forEach((t=>{e[t].reverse(),n.push(...e[t])})),t.legendHitBoxes=n}}else{let a=0,r=o(n,t.top+e+s,t.bottom-t.columnSizes[a].height);for(const l of i)l.col!==a&&(a=l.col,r=o(n,t.top+e+s,t.bottom-t.columnSizes[a].height)),l.top=r,l.left+=t.left+s,r+=l.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){const t=this;if(t.options.display){const e=t.ctx;Zt(e,t),t._draw(),Qt(e)}}_draw(){const t=this,{options:e,columnSizes:i,lineWidths:n,ctx:a}=t,{align:r,labels:l}=e,c=xt.color,h=_n(e.rtl,t.left,t.width),d=Be(l.font),{color:u,padding:f}=l,g=d.size,p=g/2;let m;t.drawTitle(),a.textAlign=h.textAlign("left"),a.textBaseline="middle",a.lineWidth=.5,a.font=d.string;const{boxWidth:x,boxHeight:b,itemHeight:_}=ps(l,g),y=t.isHorizontal(),v=this._computeTitleHeight();m=y?{x:o(r,t.left+f,t.right-n[0]),y:t.top+f+v,line:0}:{x:t.left+f,y:o(r,t.top+v+f,t.bottom-i[0].height),line:0},yn(t.ctx,e.textDirection);const w=_+f;t.legendItems.forEach(((M,k)=>{a.strokeStyle=M.fontColor||u,a.fillStyle=M.fontColor||u;const S=a.measureText(M.text).width,P=h.textAlign(M.textAlign||(M.textAlign=l.textAlign)),D=x+p+S;let C=m.x,O=m.y;h.setWidth(t.width),y?k>0&&C+D+f>t.right&&(O=m.y+=w,m.line++,C=m.x=o(r,t.left+f,t.right-n[m.line])):k>0&&O+w>t.bottom&&(C=m.x=C+i[m.line].width+f,m.line++,O=m.y=o(r,t.top+v+f,t.bottom-i[m.line].height));!function(t,e,i){if(isNaN(x)||x<=0||isNaN(b)||b<0)return;a.save();const n=K(i.lineWidth,1);if(a.fillStyle=K(i.fillStyle,c),a.lineCap=K(i.lineCap,"butt"),a.lineDashOffset=K(i.lineDashOffset,0),a.lineJoin=K(i.lineJoin,"miter"),a.lineWidth=n,a.strokeStyle=K(i.strokeStyle,c),a.setLineDash(K(i.lineDash,[])),l.usePointStyle){const o={radius:x*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},s=h.xPlus(t,x/2);Kt(a,o,s,e+p)}else{const o=e+Math.max((g-b)/2,0),s=h.leftForLtr(t,x),r=Ie(i.borderRadius);a.beginPath(),Object.values(r).some((t=>0!==t))?ne(a,{x:s,y:o,w:x,h:b,radius:r}):a.rect(s,o,x,b),a.fill(),0!==n&&a.stroke()}a.restore()}(h.x(C),O,M),C=s(P,C+x+p,y?C+D:t.right,e.rtl),function(t,e,i){ee(a,i.text,t,e+_/2,d,{strikethrough:i.hidden,textAlign:h.textAlign(i.textAlign)})}(h.x(C),O,M),y?m.x+=D+f:m.y+=w})),vn(t.ctx,e.textDirection)}drawTitle(){const t=this,e=t.options,i=e.title,s=Be(i.font),a=Fe(i.padding);if(!i.display)return;const r=_n(e.rtl,t.left,t.width),l=t.ctx,c=i.position,h=s.size/2,d=a.top+h;let u,f=t.left,g=t.width;if(this.isHorizontal())g=Math.max(...t.lineWidths),u=t.top+d,f=o(e.align,f,t.right-g);else{const i=t.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);u=d+o(e.align,t.top,t.bottom-i-e.labels.padding-t._computeTitleHeight())}const p=o(c,f,f+g);l.textAlign=r.textAlign(n(c)),l.textBaseline="middle",l.strokeStyle=i.color,l.fillStyle=i.color,l.font=s.string,ee(l,i.text,p,u,s)}_computeTitleHeight(){const t=this.options.title,e=Be(t.font),i=Fe(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){const i=this;let n,o,s;if(t>=i.left&&t<=i.right&&e>=i.top&&e<=i.bottom)for(s=i.legendHitBoxes,n=0;n=o.left&&t<=o.left+o.width&&e>=o.top&&e<=o.top+o.height)return i.legendItems[n];return null}handleEvent(t){const e=this,i=e.options;if(!function(t,e){if("mousemove"===t&&(e.onHover||e.onLeave))return!0;if(e.onClick&&("click"===t||"mouseup"===t))return!0;return!1}(t.type,i))return;const n=e._getLegendItemAt(t.x,t.y);if("mousemove"===t.type){const a=e._hoveredItem,r=(s=n,null!==(o=a)&&null!==s&&o.datasetIndex===s.datasetIndex&&o.index===s.index);a&&!r&&Q(i.onLeave,[t,a,e],e),e._hoveredItem=n,n&&!r&&Q(i.onHover,[t,n,e],e)}else n&&Q(i.onClick,[t,n,e],e);var o,s}}var xs={id:"legend",_element:ms,start(t,e,i){const n=t.legend=new ms({ctx:t.ctx,options:i,chart:t});Ze.configure(t,n,i),Ze.addBox(t,n)},stop(t){Ze.removeBox(t,t.legend),delete t.legend},beforeUpdate(t,e,i){const n=t.legend;Ze.configure(t,n,i),n.options=i},afterUpdate(t){const e=t.legend;e.buildLabels(),e.adjustHitBoxes()},afterEvent(t,e){e.replay||t.legend.handleEvent(e.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(t,e,i){const n=e.datasetIndex,o=i.chart;o.isDatasetVisible(n)?(o.hide(n),e.hidden=!0):(o.show(n),e.hidden=!1)},onHover:null,onLeave:null,labels:{color:t=>t.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:n,textAlign:o,color:s}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const a=t.controller.getStyle(i?0:void 0),r=Fe(a.borderWidth);return{text:e[t.index].label,fillStyle:a.backgroundColor,fontColor:s,hidden:!t.visible,lineCap:a.borderCapStyle,lineDash:a.borderDash,lineDashOffset:a.borderDashOffset,lineJoin:a.borderJoinStyle,lineWidth:(r.width+r.height)/4,strokeStyle:a.borderColor,pointStyle:n||a.pointStyle,rotation:a.rotation,textAlign:o||a.textAlign,borderRadius:0,datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class bs extends zi{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this,n=i.options;if(i.left=0,i.top=0,!n.display)return void(i.width=i.height=i.right=i.bottom=0);i.width=i.right=t,i.height=i.bottom=e;const o=Y(n.text)?n.text.length:1;i._padding=Fe(n.padding);const s=o*Be(n.font).lineHeight+i._padding.height;i.isHorizontal()?i.height=s:i.width=s}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:n,right:s,options:a}=this,r=a.align;let l,c,h,d=0;return this.isHorizontal()?(c=o(r,i,s),h=e+t,l=s-i):("left"===a.position?(c=i+t,h=o(r,n,e),d=-.5*bt):(c=s-t,h=o(r,e,n),d=.5*bt),l=n-e),{titleX:c,titleY:h,maxWidth:l,rotation:d}}draw(){const t=this,e=t.ctx,i=t.options;if(!i.display)return;const o=Be(i.font),s=o.lineHeight/2+t._padding.top,{titleX:a,titleY:r,maxWidth:l,rotation:c}=t._drawArgs(s);ee(e,i.text,0,0,o,{color:i.color,maxWidth:l,rotation:c,textAlign:n(i.align),textBaseline:"middle",translation:[a,r]})}}var _s={id:"title",_element:bs,start(t,e,i){!function(t,e){const i=new bs({ctx:t.ctx,options:e,chart:t});Ze.configure(t,i,e),Ze.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;Ze.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const n=t.titleBlock;Ze.configure(t,n,i),n.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const ys=new WeakMap;var vs={id:"subtitle",start(t,e,i){const n=new bs({ctx:t.ctx,options:i,chart:t});Ze.configure(t,n,i),Ze.addBox(t,n),ys.set(t,n)},stop(t){Ze.removeBox(t,ys.get(t)),ys.delete(t)},beforeUpdate(t,e,i){const n=ys.get(t);Ze.configure(t,n,i),n.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const ws={average(t){if(!t.length)return!1;let e,i,n=0,o=0,s=0;for(e=0,i=t.length;e-1?t.split("\n"):t}function Ss(t,e){const{element:i,datasetIndex:n,index:o}=e,s=t.getDatasetMeta(n).controller,{label:a,value:r}=s.getLabelAndValue(o);return{chart:t,label:a,parsed:s.getParsed(o),raw:t.data.datasets[n].data[o],formattedValue:r,dataset:s.getDataset(),dataIndex:o,datasetIndex:n,element:i}}function Ps(t,e){const i=t._chart.ctx,{body:n,footer:o,title:s}=t,{boxWidth:a,boxHeight:r}=e,l=Be(e.bodyFont),c=Be(e.titleFont),h=Be(e.footerFont),d=s.length,u=o.length,f=n.length,g=Fe(e.padding);let p=g.height,m=0,x=n.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(p+=d*c.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){p+=f*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-f)*l.lineHeight+(x-1)*e.bodySpacing}u&&(p+=e.footerMarginTop+u*h.lineHeight+(u-1)*e.footerSpacing);let b=0;const _=function(t){m=Math.max(m,i.measureText(t).width+b)};return i.save(),i.font=c.string,J(t.title,_),i.font=l.string,J(t.beforeBody.concat(t.afterBody),_),b=e.displayColors?a+2:0,J(n,(t=>{J(t.before,_),J(t.lines,_),J(t.after,_)})),b=0,i.font=h.string,J(t.footer,_),i.restore(),m+=g.width,{width:m,height:p}}function Ds(t,e,i,n){const{x:o,width:s}=i,{width:a,chartArea:{left:r,right:l}}=t;let c="center";return"center"===n?c=o<=(r+l)/2?"left":"right":o<=s/2?c="left":o>=a-s/2&&(c="right"),function(t,e,i,n){const{x:o,width:s}=n,a=i.caretSize+i.caretPadding;return"left"===t&&o+s+a>e.width||"right"===t&&o-s-a<0||void 0}(c,t,e,i)&&(c="center"),c}function Cs(t,e,i){const n=e.yAlign||function(t,e){const{y:i,height:n}=e;return it.height-n/2?"bottom":"center"}(t,i);return{xAlign:e.xAlign||Ds(t,e,i,n),yAlign:n}}function Os(t,e,i,n){const{caretSize:o,caretPadding:s,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,c=o+s,h=a+s;let d=function(t,e){let{x:i,width:n}=t;return"right"===e?i-=n:"center"===e&&(i-=n/2),i}(e,r);const u=function(t,e,i){let{y:n,height:o}=t;return"top"===e?n+=i:n-="bottom"===e?o+i:o/2,n}(e,l,c);return"center"===l?"left"===r?d+=c:"right"===r&&(d-=c):"left"===r?d-=h:"right"===r&&(d+=h),{x:Ht(d,0,n.width-e.width),y:Ht(u,0,n.height-e.height)}}function Ts(t,e,i){const n=Fe(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-n.right:t.x+n.left}function As(t){return Ms([],ks(t))}function Ls(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}class Rs extends zi{constructor(t){super(),this.opacity=0,this._active=[],this._chart=t._chart,this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const t=this,e=t._cachedAnimations;if(e)return e;const i=t._chart,n=t.options.setContext(t.getContext()),o=n.enabled&&i.options.animation&&n.animations,s=new wi(t._chart,o);return o._cacheable&&(t._cachedAnimations=Object.freeze(s)),s}getContext(){const t=this;return t.$context||(t.$context=(e=t._chart.getContext(),i=t,n=t._tooltipItems,Object.assign(Object.create(e),{tooltip:i,tooltipItems:n,type:"tooltip"})));var e,i,n}getTitle(t,e){const i=this,{callbacks:n}=e,o=n.beforeTitle.apply(i,[t]),s=n.title.apply(i,[t]),a=n.afterTitle.apply(i,[t]);let r=[];return r=Ms(r,ks(o)),r=Ms(r,ks(s)),r=Ms(r,ks(a)),r}getBeforeBody(t,e){return As(e.callbacks.beforeBody.apply(this,[t]))}getBody(t,e){const i=this,{callbacks:n}=e,o=[];return J(t,(t=>{const e={before:[],lines:[],after:[]},s=Ls(n,t);Ms(e.before,ks(s.beforeLabel.call(i,t))),Ms(e.lines,s.label.call(i,t)),Ms(e.after,ks(s.afterLabel.call(i,t))),o.push(e)})),o}getAfterBody(t,e){return As(e.callbacks.afterBody.apply(this,[t]))}getFooter(t,e){const i=this,{callbacks:n}=e,o=n.beforeFooter.apply(i,[t]),s=n.footer.apply(i,[t]),a=n.afterFooter.apply(i,[t]);let r=[];return r=Ms(r,ks(o)),r=Ms(r,ks(s)),r=Ms(r,ks(a)),r}_createItems(t){const e=this,i=e._active,n=e._chart.data,o=[],s=[],a=[];let r,l,c=[];for(r=0,l=i.length;rt.filter(e,i,o,n)))),t.itemSort&&(c=c.sort(((e,i)=>t.itemSort(e,i,n)))),J(c,(i=>{const n=Ls(t.callbacks,i);o.push(n.labelColor.call(e,i)),s.push(n.labelPointStyle.call(e,i)),a.push(n.labelTextColor.call(e,i))})),e.labelColors=o,e.labelPointStyles=s,e.labelTextColors=a,e.dataPoints=c,c}update(t,e){const i=this,n=i.options.setContext(i.getContext()),o=i._active;let s,a=[];if(o.length){const t=ws[n.position].call(i,o,i._eventPosition);a=i._createItems(n),i.title=i.getTitle(a,n),i.beforeBody=i.getBeforeBody(a,n),i.body=i.getBody(a,n),i.afterBody=i.getAfterBody(a,n),i.footer=i.getFooter(a,n);const e=i._size=Ps(i,n),r=Object.assign({},t,e),l=Cs(i._chart,n,r),c=Os(n,r,l,i._chart);i.xAlign=l.xAlign,i.yAlign=l.yAlign,s={opacity:1,x:c.x,y:c.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==i.opacity&&(s={opacity:0});i._tooltipItems=a,i.$context=void 0,s&&i._resolveAnimations().update(i,s),t&&n.external&&n.external.call(i,{chart:i._chart,tooltip:i,replay:e})}drawCaret(t,e,i,n){const o=this.getCaretPosition(t,i,n);e.lineTo(o.x1,o.y1),e.lineTo(o.x2,o.y2),e.lineTo(o.x3,o.y3)}getCaretPosition(t,e,i){const{xAlign:n,yAlign:o}=this,{cornerRadius:s,caretSize:a}=i,{x:r,y:l}=t,{width:c,height:h}=e;let d,u,f,g,p,m;return"center"===o?(p=l+h/2,"left"===n?(d=r,u=d-a,g=p+a,m=p-a):(d=r+c,u=d+a,g=p-a,m=p+a),f=d):(u="left"===n?r+s+a:"right"===n?r+c-s-a:this.caretX,"top"===o?(g=l,p=g-a,d=u-a,f=u+a):(g=l+h,p=g+a,d=u+a,f=u-a),m=g),{x1:d,x2:u,x3:f,y1:g,y2:p,y3:m}}drawTitle(t,e,i){const n=this,o=n.title,s=o.length;let a,r,l;if(s){const c=_n(i.rtl,n.x,n.width);for(t.x=Ts(n,i.titleAlign,i),e.textAlign=c.textAlign(i.titleAlign),e.textBaseline="middle",a=Be(i.titleFont),r=i.titleSpacing,e.fillStyle=i.titleColor,e.font=a.string,l=0;l0!==t))?(t.beginPath(),t.fillStyle=o.multiKeyBackground,ne(t,{x:e,y:g,w:c,h:l,radius:s}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),ne(t,{x:i,y:g+1,w:c-2,h:l-2,radius:s}),t.fill()):(t.fillStyle=o.multiKeyBackground,t.fillRect(e,g,c,l),t.strokeRect(e,g,c,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,c-2,l-2))}t.fillStyle=s.labelTextColors[i]}drawBody(t,e,i){const n=this,{body:o}=n,{bodySpacing:s,bodyAlign:a,displayColors:r,boxHeight:l,boxWidth:c}=i,h=Be(i.bodyFont);let d=h.lineHeight,u=0;const f=_n(i.rtl,n.x,n.width),g=function(i){e.fillText(i,f.x(t.x+u),t.y+d/2),t.y+=d+s},p=f.textAlign(a);let m,x,b,_,y,v,w;for(e.textAlign=a,e.textBaseline="middle",e.font=h.string,t.x=Ts(n,p,i),e.fillStyle=i.bodyColor,J(n.beforeBody,g),u=r&&"right"!==p?"center"===a?c/2+1:c+2:0,_=0,v=o.length;_0&&e.stroke()}_updateAnimationTarget(t){const e=this,i=e._chart,n=e.$animations,o=n&&n.x,s=n&&n.y;if(o||s){const n=ws[t.position].call(e,e._active,e._eventPosition);if(!n)return;const a=e._size=Ps(e,t),r=Object.assign({},n,e._size),l=Cs(i,t,r),c=Os(t,r,l,i);o._to===c.x&&s._to===c.y||(e.xAlign=l.xAlign,e.yAlign=l.yAlign,e.width=a.width,e.height=a.height,e.caretX=n.x,e.caretY=n.y,e._resolveAnimations().update(e,c))}}draw(t){const e=this,i=e.options.setContext(e.getContext());let n=e.opacity;if(!n)return;e._updateAnimationTarget(i);const o={width:e.width,height:e.height},s={x:e.x,y:e.y};n=Math.abs(n)<.001?0:n;const a=Fe(i.padding),r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;i.enabled&&r&&(t.save(),t.globalAlpha=n,e.drawBackground(s,t,o,i),yn(t,i.textDirection),s.y+=a.top,e.drawTitle(s,t,i),e.drawBody(s,t,i),e.drawFooter(s,t,i),vn(t,i.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this,n=i._active,o=t.map((({datasetIndex:t,index:e})=>{const n=i._chart.getDatasetMeta(t);if(!n)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:n.data[e],index:e}})),s=!tt(n,o),a=i._positionChanged(o,e);(s||a)&&(i._active=o,i._eventPosition=e,i.update(!0))}handleEvent(t,e){const i=this,n=i.options,o=i._active||[];let s=!1,a=[];"mouseout"!==t.type&&(a=i._chart.getElementsAtEventForMode(t,n.mode,n,e),n.reverse&&a.reverse());const r=i._positionChanged(a,t);return s=e||!tt(a,o)||r,s&&(i._active=a,(n.enabled||n.external)&&(i._eventPosition={x:t.x,y:t.y},i.update(!0,e))),s}_positionChanged(t,e){const{caretX:i,caretY:n,options:o}=this,s=ws[o.position].call(this,t,e);return!1!==s&&(i!==s.x||n!==s.y)}}Rs.positioners=ws;var Es={id:"tooltip",_element:Rs,positioners:ws,afterInit(t,e,i){i&&(t.tooltip=new Rs({_chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip,i={tooltip:e};!1!==t.notifyPlugins("beforeTooltipDraw",i)&&(e&&e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i))},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:{beforeTitle:H,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,n=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(n>0&&e.dataIndex"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]},zs=Object.freeze({__proto__:null,Decimation:Go,Filler:gs,Legend:xs,SubTitle:vs,Title:_s,Tooltip:Es});function Is(t,e,i){const n=t.indexOf(e);if(-1===n)return((t,e,i)=>"string"==typeof e?t.push(e)-1:isNaN(e)?null:i)(t,e,i);return n!==t.lastIndexOf(e)?i:n}class Fs extends qi{constructor(t){super(t),this._startValue=void 0,this._valueRange=0}parse(t,e){if($(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:Ht(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:Is(i,t,K(e,t)),i.length-1)}determineDataLimits(){const t=this,{minDefined:e,maxDefined:i}=t.getUserBounds();let{min:n,max:o}=t.getMinMax(!0);"ticks"===t.options.bounds&&(e||(n=0),i||(o=t.getLabels().length-1)),t.min=n,t.max=o}buildTicks(){const t=this,e=t.min,i=t.max,n=t.options.offset,o=[];let s=t.getLabels();s=0===e&&i===s.length-1?s:s.slice(e,i+1),t._valueRange=Math.max(s.length-(n?0:1),1),t._startValue=t.min-(n?.5:0);for(let t=e;t<=i;t++)o.push({value:t});return o}getLabelForValue(t){const e=this.getLabels();return t>=0&&te.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){const e=this;return Math.round(e._startValue+e.getDecimalForPixel(t)*e._valueRange)}getBasePixel(){return this.bottom}}function Bs(t,e,{horizontal:i,minRotation:n}){const o=Et(n),s=(i?Math.sin(o):Math.cos(o))||.001,a=.75*e*(""+t).length;return Math.min(e/s,a)}Fs.id="category",Fs.defaults={ticks:{callback:Fs.prototype.getLabelForValue}};class Vs extends qi{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(t,e){return $(t)||("number"==typeof t||t instanceof Number)&&!isFinite(+t)?null:+t}handleTickRangeOptions(){const t=this,{beginAtZero:e}=t.options,{minDefined:i,maxDefined:n}=t.getUserBounds();let{min:o,max:s}=t;const a=t=>o=i?o:t,r=t=>s=n?s:t;if(e){const t=Dt(o),e=Dt(s);t<0&&e<0?r(0):t>0&&e>0&&a(0)}o===s&&(r(s+1),e||a(o-1)),t.min=o,t.max=s}getTickLimit(){const t=this,e=t.options.ticks;let i,{maxTicksLimit:n,stepSize:o}=e;return o?i=Math.ceil(t.max/o)-Math.floor(t.min/o)+1:(i=t.computeTickLimit(),n=n||11),n&&(i=Math.min(n,i)),i}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this,e=t.options,i=e.ticks;let n=t.getTickLimit();n=Math.max(2,n);const o=function(t,e){const i=[],{bounds:n,step:o,min:s,max:a,precision:r,count:l,maxTicks:c,maxDigits:h,includeBounds:d}=t,u=o||1,f=c-1,{min:g,max:p}=e,m=!$(s),x=!$(a),b=!$(l),_=(p-g)/(h+1);let y,v,w,M,k=Ct((p-g)/f/u)*u;if(k<1e-14&&!m&&!x)return[{value:g},{value:p}];M=Math.ceil(p/k)-Math.floor(g/k),M>f&&(k=Ct(M*k/f/u)*u),$(r)||(y=Math.pow(10,r),k=Math.ceil(k*y)/y),"ticks"===n?(v=Math.floor(g/k)*k,w=Math.ceil(p/k)*k):(v=g,w=p),m&&x&&o&&Lt((a-s)/o,k/1e3)?(M=Math.round(Math.min((a-s)/k,c)),k=(a-s)/M,v=s,w=a):b?(v=m?s:v,w=x?a:w,M=l-1,k=(w-v)/M):(M=(w-v)/k,M=At(M,Math.round(M),k/1e3)?Math.round(M):Math.ceil(M));const S=Math.max(It(k),It(v));y=Math.pow(10,$(r)?S:r),v=Math.round(v*y)/y,w=Math.round(w*y)/y;let P=0;for(m&&(d&&v!==s?(i.push({value:s}),v0?i:null;this._zero=!0}determineDataLimits(){const t=this,{min:e,max:i}=t.getMinMax(!0);t.min=X(e)?Math.max(0,e):null,t.max=X(i)?Math.max(0,i):null,t.options.beginAtZero&&(t._zero=!0),t.handleTickRangeOptions()}handleTickRangeOptions(){const t=this,{minDefined:e,maxDefined:i}=t.getUserBounds();let n=t.min,o=t.max;const s=t=>n=e?n:t,a=t=>o=i?o:t,r=(t,e)=>Math.pow(10,Math.floor(Pt(t))+e);n===o&&(n<=0?(s(1),a(10)):(s(r(n,-1)),a(r(o,1)))),n<=0&&s(r(o,-1)),o<=0&&a(r(n,1)),t._zero&&t.min!==t._suggestedMin&&n===r(t.min,0)&&s(r(n,-1)),t.min=n,t.max=o}buildTicks(){const t=this,e=t.options,i=function(t,e){const i=Math.floor(Pt(e.max)),n=Math.ceil(e.max/Math.pow(10,i)),o=[];let s=q(t.min,Math.pow(10,Math.floor(Pt(e.min)))),a=Math.floor(Pt(s)),r=Math.floor(s/Math.pow(10,a)),l=a<0?Math.pow(10,Math.abs(a)):1;do{o.push({value:s,major:Ns(s)}),++r,10===r&&(r=1,++a,l=a>=0?1:l),s=Math.round(r*Math.pow(10,a)*l)/l}while(ao?{start:e-i,end:e}:{start:e,end:e+i}}function Ys(t){const e={l:0,r:t.width,t:0,b:t.height-t.paddingTop},i={},n=[],o=[],s=t.getLabels().length;for(let c=0;ce.r&&(e.r=p.end,i.r=f),m.starte.b&&(e.b=m.end,i.b=f)}var a,r,l;t._setReductions(t.drawingArea,e,i),t._pointLabelItems=function(t,e,i){const n=[],o=t.getLabels().length,s=t.options,a=js(s),r=t.getDistanceFromCenterForValue(s.ticks.reverse?t.min:t.max);for(let s=0;s270||i<90)&&(t-=e),t}function Ks(t,e,i,n){const{ctx:o}=t;if(i)o.arc(t.xCenter,t.yCenter,e,0,_t);else{let i=t.getPointPosition(0,e);o.moveTo(i.x,i.y);for(let s=1;s{const n=Q(e.options.pointLabels.callback,[t,i],e);return n||0===n?n:""}))}fit(){const t=this,e=t.options;e.display&&e.pointLabels.display?Ys(t):t.setCenterPoint(0,0,0,0)}_setReductions(t,e,i){const n=this;let o=e.l/Math.sin(i.l),s=Math.max(e.r-n.width,0)/Math.sin(i.r),a=-e.t/Math.cos(i.t),r=-Math.max(e.b-(n.height-n.paddingTop),0)/Math.cos(i.b);o=Gs(o),s=Gs(s),a=Gs(a),r=Gs(r),n.drawingArea=Math.max(t/2,Math.min(Math.floor(t-(o+s)/2),Math.floor(t-(a+r)/2))),n.setCenterPoint(o,s,a,r)}setCenterPoint(t,e,i,n){const o=this,s=o.width-e-o.drawingArea,a=t+o.drawingArea,r=i+o.drawingArea,l=o.height-o.paddingTop-n-o.drawingArea;o.xCenter=Math.floor((a+s)/2+o.left),o.yCenter=Math.floor((r+l)/2+o.top+o.paddingTop)}getIndexAngle(t){return Wt(t*(_t/this.getLabels().length)+Et(this.options.startAngle||0))}getDistanceFromCenterForValue(t){const e=this;if($(t))return NaN;const i=e.drawingArea/(e.max-e.min);return e.options.reverse?(e.max-t)*i:(t-e.min)*i}getValueForDistanceFromCenter(t){if($(t))return NaN;const e=this,i=t/(e.drawingArea/(e.max-e.min));return e.options.reverse?e.max-i:e.min+i}getPointPosition(t,e){const i=this,n=i.getIndexAngle(t)-Mt;return{x:Math.cos(n)*e+i.xCenter,y:Math.sin(n)*e+i.yCenter,angle:n}}getPointPositionForValue(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))}getBasePosition(t){return this.getPointPositionForValue(t||0,this.getBaseValue())}getPointLabelPosition(t){const{left:e,top:i,right:n,bottom:o}=this._pointLabelItems[t];return{left:e,top:i,right:n,bottom:o}}drawBackground(){const t=this,{backgroundColor:e,grid:{circular:i}}=t.options;if(e){const n=t.ctx;n.save(),n.beginPath(),Ks(t,t.getDistanceFromCenterForValue(t._endValue),i,t.getLabels().length),n.closePath(),n.fillStyle=e,n.fill(),n.restore()}}drawGrid(){const t=this,e=t.ctx,i=t.options,{angleLines:n,grid:o}=i,s=t.getLabels().length;let a,r,l;if(i.pointLabels.display&&function(t,e){const{ctx:i,options:{pointLabels:n}}=t;for(let o=e-1;o>=0;o--){const e=n.setContext(t.getContext(o)),s=Be(e.font),{x:a,y:r,textAlign:l,left:c,top:h,right:d,bottom:u}=t._pointLabelItems[o],{backdropColor:f}=e;if(!$(f)){const t=Fe(e.backdropPadding);i.fillStyle=f,i.fillRect(c-t.left,h-t.top,d-c+t.width,u-h+t.height)}ee(i,t._pointLabels[o],a,r+s.lineHeight/2,s,{color:e.color,textAlign:l,textBaseline:"middle"})}}(t,s),o.display&&t.ticks.forEach(((e,i)=>{if(0!==i){r=t.getDistanceFromCenterForValue(e.value);const n=o.setContext(t.getContext(i-1));!function(t,e,i,n){const o=t.ctx,s=e.circular,{color:a,lineWidth:r}=e;!s&&!n||!a||!r||i<0||(o.save(),o.strokeStyle=a,o.lineWidth=r,o.setLineDash(e.borderDash),o.lineDashOffset=e.borderDashOffset,o.beginPath(),Ks(t,i,s,n),o.closePath(),o.stroke(),o.restore())}(t,n,r,s)}})),n.display){for(e.save(),a=t.getLabels().length-1;a>=0;a--){const o=n.setContext(t.getContext(a)),{color:s,lineWidth:c}=o;c&&s&&(e.lineWidth=c,e.strokeStyle=s,e.setLineDash(o.borderDash),e.lineDashOffset=o.borderDashOffset,r=t.getDistanceFromCenterForValue(i.ticks.reverse?t.min:t.max),l=t.getPointPosition(a,r),e.beginPath(),e.moveTo(t.xCenter,t.yCenter),e.lineTo(l.x,l.y),e.stroke())}e.restore()}}drawBorder(){}drawLabels(){const t=this,e=t.ctx,i=t.options,n=i.ticks;if(!n.display)return;const o=t.getIndexAngle(0);let s,a;e.save(),e.translate(t.xCenter,t.yCenter),e.rotate(o),e.textAlign="center",e.textBaseline="middle",t.ticks.forEach(((o,r)=>{if(0===r&&!i.reverse)return;const l=n.setContext(t.getContext(r)),c=Be(l.font);if(s=t.getDistanceFromCenterForValue(t.ticks[r].value),l.showLabelBackdrop){e.font=c.string,a=e.measureText(o.label).width,e.fillStyle=l.backdropColor;const t=Fe(l.backdropPadding);e.fillRect(-a/2-t.left,-s-c.size/2-t.top,a+t.width,c.size+t.height)}ee(e,o.label,0,-s,c,{color:l.color})})),e.restore()}drawTitle(){}}Zs.id="radialLinear",Zs.defaults={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:Vi.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback:t=>t,padding:5}},Zs.defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"},Zs.descriptors={angleLines:{_fallback:"grid"}};const Qs={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Js=Object.keys(Qs);function ta(t,e){return t-e}function ea(t,e){if($(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:s}=t._parseOpts;let a=e;return"function"==typeof n&&(a=n(a)),X(a)||(a="string"==typeof n?i.parse(a,n):i.parse(a)),null===a?null:(o&&(a="week"!==o||!Tt(s)&&!0!==s?i.startOf(a,o):i.startOf(a,"isoWeek",s)),+a)}function ia(t,e,i,n){const o=Js.length;for(let s=Js.indexOf(t);s=e?i[n]:i[o]]=!0}}else t[e]=!0}function oa(t,e,i){const n=[],o={},s=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,n,o,i):n}class sa extends qi{constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e){const i=t.time||(t.time={}),n=this._adapter=new ao._date(t.adapters.date);st(i.displayFormats,n.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:ea(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this,e=t.options,i=t._adapter,n=e.time.unit||"day";let{min:o,max:s,minDefined:a,maxDefined:r}=t.getUserBounds();function l(t){a||isNaN(t.min)||(o=Math.min(o,t.min)),r||isNaN(t.max)||(s=Math.max(s,t.max))}a&&r||(l(t._getLabelBounds()),"ticks"===e.bounds&&"labels"===e.ticks.source||l(t.getMinMax(!1))),o=X(o)&&!isNaN(o)?o:+i.startOf(Date.now(),n),s=X(s)&&!isNaN(s)?s:+i.endOf(Date.now(),n)+1,t.min=Math.min(o,s-1),t.max=Math.max(o+1,s)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this,e=t.options,i=e.time,n=e.ticks,o="labels"===n.source?t.getLabelTimestamps():t._generate();"ticks"===e.bounds&&o.length&&(t.min=t._userMin||o[0],t.max=t._userMax||o[o.length-1]);const s=t.min,a=re(o,s,t.max);return t._unit=i.unit||(n.autoSkip?ia(i.minUnit,t.min,t.max,t._getLabelCapacity(s)):function(t,e,i,n,o){for(let s=Js.length-1;s>=Js.indexOf(i);s--){const i=Js[s];if(Qs[i].common&&t._adapter.diff(o,n,i)>=e-1)return i}return Js[i?Js.indexOf(i):0]}(t,a.length,i.minUnit,t.min,t.max)),t._majorUnit=n.major.enabled&&"year"!==t._unit?function(t){for(let e=Js.indexOf(t)+1,i=Js.length;e1e5*r)throw new Error(i+" and "+n+" are too far apart with stepSize of "+r+" "+a);const g="data"===o.ticks.source&&t.getDataTimestamps();for(d=f,u=0;dt-e)).map((t=>+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}_tickFormatFunction(t,e,i,n){const o=this,s=o.options,a=s.time.displayFormats,r=o._unit,l=o._majorUnit,c=r&&a[r],h=l&&a[l],d=i[e],u=l&&h&&d&&d.major,f=o._adapter.format(t,n||(u?h:c)),g=s.ticks.callback;return g?Q(g,[f,e,i],o):f}generateTickLabels(t){let e,i,n;for(e=0,i=t.length;e0?r:1}getDataTimestamps(){const t=this;let e,i,n=t._cache.data||[];if(n.length)return n;const o=t.getMatchingVisibleMetas();if(t._normalized&&o.length)return t._cache.data=o[0].controller.getAllParsedValues(t);for(e=0,i=o.length;e=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=se(t,"pos",e)),({pos:n,time:s}=t[r]),({pos:o,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=se(t,"time",e)),({time:n,pos:s}=t[r]),({time:o,pos:a}=t[l]));const c=o-n;return c?s+(a-s)*(e-n)/c:s}sa.id="time",sa.defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",major:{enabled:!1}}};class ra extends sa{constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this,e=t._getTimestampsForTable(),i=t._table=t.buildLookupTable(e);t._minPos=aa(i,t.min),t._tableRange=aa(i,t.max)-t._minPos,super.initOffsets(e)}buildLookupTable(t){const{min:e,max:i}=this,n=[],o=[];let s,a,r,l,c;for(s=0,a=t.length;s=e&&l<=i&&n.push(l);if(n.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(s=0,a=n.length;s Array.prototype.slice.call(args)); + let ticking = false; + let args = []; + return function(...rest) { + args = updateArgs(rest); + if (!ticking) { + ticking = true; + requestAnimFrame.call(window, () => { + ticking = false; + fn.apply(thisArg, args); + }); + } + }; +} +function debounce(fn, delay) { + let timeout; + return function() { + if (delay) { + clearTimeout(timeout); + timeout = setTimeout(fn, delay); + } else { + fn(); + } + return delay; + }; +} +const _toLeftRightCenter = (align) => align === 'start' ? 'left' : align === 'end' ? 'right' : 'center'; +const _alignStartEnd = (align, start, end) => align === 'start' ? start : align === 'end' ? end : (start + end) / 2; +const _textX = (align, left, right, rtl) => { + const check = rtl ? 'left' : 'right'; + return align === check ? right : align === 'center' ? (left + right) / 2 : left; +}; + +function noop() {} +const uid = (function() { + let id = 0; + return function() { + return id++; + }; +}()); +function isNullOrUndef(value) { + return value === null || typeof value === 'undefined'; +} +function isArray(value) { + if (Array.isArray && Array.isArray(value)) { + return true; + } + const type = Object.prototype.toString.call(value); + if (type.substr(0, 7) === '[object' && type.substr(-6) === 'Array]') { + return true; + } + return false; +} +function isObject(value) { + return value !== null && Object.prototype.toString.call(value) === '[object Object]'; +} +const isNumberFinite = (value) => (typeof value === 'number' || value instanceof Number) && isFinite(+value); +function finiteOrDefault(value, defaultValue) { + return isNumberFinite(value) ? value : defaultValue; +} +function valueOrDefault(value, defaultValue) { + return typeof value === 'undefined' ? defaultValue : value; +} +const toPercentage = (value, dimension) => + typeof value === 'string' && value.endsWith('%') ? + parseFloat(value) / 100 + : value / dimension; +const toDimension = (value, dimension) => + typeof value === 'string' && value.endsWith('%') ? + parseFloat(value) / 100 * dimension + : +value; +function callback(fn, args, thisArg) { + if (fn && typeof fn.call === 'function') { + return fn.apply(thisArg, args); + } +} +function each(loopable, fn, thisArg, reverse) { + let i, len, keys; + if (isArray(loopable)) { + len = loopable.length; + if (reverse) { + for (i = len - 1; i >= 0; i--) { + fn.call(thisArg, loopable[i], i); + } + } else { + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[i], i); + } + } + } else if (isObject(loopable)) { + keys = Object.keys(loopable); + len = keys.length; + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[keys[i]], keys[i]); + } + } +} +function _elementsEqual(a0, a1) { + let i, ilen, v0, v1; + if (!a0 || !a1 || a0.length !== a1.length) { + return false; + } + for (i = 0, ilen = a0.length; i < ilen; ++i) { + v0 = a0[i]; + v1 = a1[i]; + if (v0.datasetIndex !== v1.datasetIndex || v0.index !== v1.index) { + return false; + } + } + return true; +} +function clone$1(source) { + if (isArray(source)) { + return source.map(clone$1); + } + if (isObject(source)) { + const target = Object.create(null); + const keys = Object.keys(source); + const klen = keys.length; + let k = 0; + for (; k < klen; ++k) { + target[keys[k]] = clone$1(source[keys[k]]); + } + return target; + } + return source; +} +function isValidKey(key) { + return ['__proto__', 'prototype', 'constructor'].indexOf(key) === -1; +} +function _merger(key, target, source, options) { + if (!isValidKey(key)) { + return; + } + const tval = target[key]; + const sval = source[key]; + if (isObject(tval) && isObject(sval)) { + merge(tval, sval, options); + } else { + target[key] = clone$1(sval); + } +} +function merge(target, source, options) { + const sources = isArray(source) ? source : [source]; + const ilen = sources.length; + if (!isObject(target)) { + return target; + } + options = options || {}; + const merger = options.merger || _merger; + for (let i = 0; i < ilen; ++i) { + source = sources[i]; + if (!isObject(source)) { + continue; + } + const keys = Object.keys(source); + for (let k = 0, klen = keys.length; k < klen; ++k) { + merger(keys[k], target, source, options); + } + } + return target; +} +function mergeIf(target, source) { + return merge(target, source, {merger: _mergerIf}); +} +function _mergerIf(key, target, source) { + if (!isValidKey(key)) { + return; + } + const tval = target[key]; + const sval = source[key]; + if (isObject(tval) && isObject(sval)) { + mergeIf(tval, sval); + } else if (!Object.prototype.hasOwnProperty.call(target, key)) { + target[key] = clone$1(sval); + } +} +function _deprecated(scope, value, previous, current) { + if (value !== undefined) { + console.warn(scope + ': "' + previous + + '" is deprecated. Please use "' + current + '" instead'); + } +} +const emptyString = ''; +const dot = '.'; +function indexOfDotOrLength(key, start) { + const idx = key.indexOf(dot, start); + return idx === -1 ? key.length : idx; +} +function resolveObjectKey(obj, key) { + if (key === emptyString) { + return obj; + } + let pos = 0; + let idx = indexOfDotOrLength(key, pos); + while (obj && idx > pos) { + obj = obj[key.substr(pos, idx - pos)]; + pos = idx + 1; + idx = indexOfDotOrLength(key, pos); + } + return obj; +} +function _capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} +const defined = (value) => typeof value !== 'undefined'; +const isFunction = (value) => typeof value === 'function'; +const setsEqual = (a, b) => { + if (a.size !== b.size) { + return false; + } + for (const item of a) { + if (!b.has(item)) { + return false; + } + } + return true; +}; + +const PI = Math.PI; +const TAU = 2 * PI; +const PITAU = TAU + PI; +const INFINITY = Number.POSITIVE_INFINITY; +const RAD_PER_DEG = PI / 180; +const HALF_PI = PI / 2; +const QUARTER_PI = PI / 4; +const TWO_THIRDS_PI = PI * 2 / 3; +const log10 = Math.log10; +const sign = Math.sign; +function niceNum(range) { + const roundedRange = Math.round(range); + range = almostEquals(range, roundedRange, range / 1000) ? roundedRange : range; + const niceRange = Math.pow(10, Math.floor(log10(range))); + const fraction = range / niceRange; + const niceFraction = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10; + return niceFraction * niceRange; +} +function _factorize(value) { + const result = []; + const sqrt = Math.sqrt(value); + let i; + for (i = 1; i < sqrt; i++) { + if (value % i === 0) { + result.push(i); + result.push(value / i); + } + } + if (sqrt === (sqrt | 0)) { + result.push(sqrt); + } + result.sort((a, b) => a - b).pop(); + return result; +} +function isNumber(n) { + return !isNaN(parseFloat(n)) && isFinite(n); +} +function almostEquals(x, y, epsilon) { + return Math.abs(x - y) < epsilon; +} +function almostWhole(x, epsilon) { + const rounded = Math.round(x); + return ((rounded - epsilon) <= x) && ((rounded + epsilon) >= x); +} +function _setMinAndMaxByKey(array, target, property) { + let i, ilen, value; + for (i = 0, ilen = array.length; i < ilen; i++) { + value = array[i][property]; + if (!isNaN(value)) { + target.min = Math.min(target.min, value); + target.max = Math.max(target.max, value); + } + } +} +function toRadians(degrees) { + return degrees * (PI / 180); +} +function toDegrees(radians) { + return radians * (180 / PI); +} +function _decimalPlaces(x) { + if (!isNumberFinite(x)) { + return; + } + let e = 1; + let p = 0; + while (Math.round(x * e) / e !== x) { + e *= 10; + p++; + } + return p; +} +function getAngleFromPoint(centrePoint, anglePoint) { + const distanceFromXCenter = anglePoint.x - centrePoint.x; + const distanceFromYCenter = anglePoint.y - centrePoint.y; + const radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + let angle = Math.atan2(distanceFromYCenter, distanceFromXCenter); + if (angle < (-0.5 * PI)) { + angle += TAU; + } + return { + angle, + distance: radialDistanceFromCenter + }; +} +function distanceBetweenPoints(pt1, pt2) { + return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2)); +} +function _angleDiff(a, b) { + return (a - b + PITAU) % TAU - PI; +} +function _normalizeAngle(a) { + return (a % TAU + TAU) % TAU; +} +function _angleBetween(angle, start, end, sameAngleIsFullCircle) { + const a = _normalizeAngle(angle); + const s = _normalizeAngle(start); + const e = _normalizeAngle(end); + const angleToStart = _normalizeAngle(s - a); + const angleToEnd = _normalizeAngle(e - a); + const startToAngle = _normalizeAngle(a - s); + const endToAngle = _normalizeAngle(a - e); + return a === s || a === e || (sameAngleIsFullCircle && s === e) + || (angleToStart > angleToEnd && startToAngle < endToAngle); +} +function _limitValue(value, min, max) { + return Math.max(min, Math.min(max, value)); +} +function _int16Range(value) { + return _limitValue(value, -32768, 32767); +} + +const atEdge = (t) => t === 0 || t === 1; +const elasticIn = (t, s, p) => -(Math.pow(2, 10 * (t -= 1)) * Math.sin((t - s) * TAU / p)); +const elasticOut = (t, s, p) => Math.pow(2, -10 * t) * Math.sin((t - s) * TAU / p) + 1; +const effects = { + linear: t => t, + easeInQuad: t => t * t, + easeOutQuad: t => -t * (t - 2), + easeInOutQuad: t => ((t /= 0.5) < 1) + ? 0.5 * t * t + : -0.5 * ((--t) * (t - 2) - 1), + easeInCubic: t => t * t * t, + easeOutCubic: t => (t -= 1) * t * t + 1, + easeInOutCubic: t => ((t /= 0.5) < 1) + ? 0.5 * t * t * t + : 0.5 * ((t -= 2) * t * t + 2), + easeInQuart: t => t * t * t * t, + easeOutQuart: t => -((t -= 1) * t * t * t - 1), + easeInOutQuart: t => ((t /= 0.5) < 1) + ? 0.5 * t * t * t * t + : -0.5 * ((t -= 2) * t * t * t - 2), + easeInQuint: t => t * t * t * t * t, + easeOutQuint: t => (t -= 1) * t * t * t * t + 1, + easeInOutQuint: t => ((t /= 0.5) < 1) + ? 0.5 * t * t * t * t * t + : 0.5 * ((t -= 2) * t * t * t * t + 2), + easeInSine: t => -Math.cos(t * HALF_PI) + 1, + easeOutSine: t => Math.sin(t * HALF_PI), + easeInOutSine: t => -0.5 * (Math.cos(PI * t) - 1), + easeInExpo: t => (t === 0) ? 0 : Math.pow(2, 10 * (t - 1)), + easeOutExpo: t => (t === 1) ? 1 : -Math.pow(2, -10 * t) + 1, + easeInOutExpo: t => atEdge(t) ? t : t < 0.5 + ? 0.5 * Math.pow(2, 10 * (t * 2 - 1)) + : 0.5 * (-Math.pow(2, -10 * (t * 2 - 1)) + 2), + easeInCirc: t => (t >= 1) ? t : -(Math.sqrt(1 - t * t) - 1), + easeOutCirc: t => Math.sqrt(1 - (t -= 1) * t), + easeInOutCirc: t => ((t /= 0.5) < 1) + ? -0.5 * (Math.sqrt(1 - t * t) - 1) + : 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1), + easeInElastic: t => atEdge(t) ? t : elasticIn(t, 0.075, 0.3), + easeOutElastic: t => atEdge(t) ? t : elasticOut(t, 0.075, 0.3), + easeInOutElastic(t) { + const s = 0.1125; + const p = 0.45; + return atEdge(t) ? t : + t < 0.5 + ? 0.5 * elasticIn(t * 2, s, p) + : 0.5 + 0.5 * elasticOut(t * 2 - 1, s, p); + }, + easeInBack(t) { + const s = 1.70158; + return t * t * ((s + 1) * t - s); + }, + easeOutBack(t) { + const s = 1.70158; + return (t -= 1) * t * ((s + 1) * t + s) + 1; + }, + easeInOutBack(t) { + let s = 1.70158; + if ((t /= 0.5) < 1) { + return 0.5 * (t * t * (((s *= (1.525)) + 1) * t - s)); + } + return 0.5 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + easeInBounce: t => 1 - effects.easeOutBounce(1 - t), + easeOutBounce(t) { + const m = 7.5625; + const d = 2.75; + if (t < (1 / d)) { + return m * t * t; + } + if (t < (2 / d)) { + return m * (t -= (1.5 / d)) * t + 0.75; + } + if (t < (2.5 / d)) { + return m * (t -= (2.25 / d)) * t + 0.9375; + } + return m * (t -= (2.625 / d)) * t + 0.984375; + }, + easeInOutBounce: t => (t < 0.5) + ? effects.easeInBounce(t * 2) * 0.5 + : effects.easeOutBounce(t * 2 - 1) * 0.5 + 0.5, +}; + +/*! + * @kurkle/color v0.1.9 + * https://github.com/kurkle/color#readme + * (c) 2020 Jukka Kurkela + * Released under the MIT License + */ +const map = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, A: 10, B: 11, C: 12, D: 13, E: 14, F: 15, a: 10, b: 11, c: 12, d: 13, e: 14, f: 15}; +const hex = '0123456789ABCDEF'; +const h1 = (b) => hex[b & 0xF]; +const h2 = (b) => hex[(b & 0xF0) >> 4] + hex[b & 0xF]; +const eq = (b) => (((b & 0xF0) >> 4) === (b & 0xF)); +function isShort(v) { + return eq(v.r) && eq(v.g) && eq(v.b) && eq(v.a); +} +function hexParse(str) { + var len = str.length; + var ret; + if (str[0] === '#') { + if (len === 4 || len === 5) { + ret = { + r: 255 & map[str[1]] * 17, + g: 255 & map[str[2]] * 17, + b: 255 & map[str[3]] * 17, + a: len === 5 ? map[str[4]] * 17 : 255 + }; + } else if (len === 7 || len === 9) { + ret = { + r: map[str[1]] << 4 | map[str[2]], + g: map[str[3]] << 4 | map[str[4]], + b: map[str[5]] << 4 | map[str[6]], + a: len === 9 ? (map[str[7]] << 4 | map[str[8]]) : 255 + }; + } + } + return ret; +} +function hexString(v) { + var f = isShort(v) ? h1 : h2; + return v + ? '#' + f(v.r) + f(v.g) + f(v.b) + (v.a < 255 ? f(v.a) : '') + : v; +} +function round(v) { + return v + 0.5 | 0; +} +const lim = (v, l, h) => Math.max(Math.min(v, h), l); +function p2b(v) { + return lim(round(v * 2.55), 0, 255); +} +function n2b(v) { + return lim(round(v * 255), 0, 255); +} +function b2n(v) { + return lim(round(v / 2.55) / 100, 0, 1); +} +function n2p(v) { + return lim(round(v * 100), 0, 100); +} +const RGB_RE = /^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/; +function rgbParse(str) { + const m = RGB_RE.exec(str); + let a = 255; + let r, g, b; + if (!m) { + return; + } + if (m[7] !== r) { + const v = +m[7]; + a = 255 & (m[8] ? p2b(v) : v * 255); + } + r = +m[1]; + g = +m[3]; + b = +m[5]; + r = 255 & (m[2] ? p2b(r) : r); + g = 255 & (m[4] ? p2b(g) : g); + b = 255 & (m[6] ? p2b(b) : b); + return { + r: r, + g: g, + b: b, + a: a + }; +} +function rgbString(v) { + return v && ( + v.a < 255 + ? `rgba(${v.r}, ${v.g}, ${v.b}, ${b2n(v.a)})` + : `rgb(${v.r}, ${v.g}, ${v.b})` + ); +} +const HUE_RE = /^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/; +function hsl2rgbn(h, s, l) { + const a = s * Math.min(l, 1 - l); + const f = (n, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return [f(0), f(8), f(4)]; +} +function hsv2rgbn(h, s, v) { + const f = (n, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0); + return [f(5), f(3), f(1)]; +} +function hwb2rgbn(h, w, b) { + const rgb = hsl2rgbn(h, 1, 0.5); + let i; + if (w + b > 1) { + i = 1 / (w + b); + w *= i; + b *= i; + } + for (i = 0; i < 3; i++) { + rgb[i] *= 1 - w - b; + rgb[i] += w; + } + return rgb; +} +function rgb2hsl(v) { + const range = 255; + const r = v.r / range; + const g = v.g / range; + const b = v.b / range; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const l = (max + min) / 2; + let h, s, d; + if (max !== min) { + d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + h = max === r + ? ((g - b) / d) + (g < b ? 6 : 0) + : max === g + ? (b - r) / d + 2 + : (r - g) / d + 4; + h = h * 60 + 0.5; + } + return [h | 0, s || 0, l]; +} +function calln(f, a, b, c) { + return ( + Array.isArray(a) + ? f(a[0], a[1], a[2]) + : f(a, b, c) + ).map(n2b); +} +function hsl2rgb(h, s, l) { + return calln(hsl2rgbn, h, s, l); +} +function hwb2rgb(h, w, b) { + return calln(hwb2rgbn, h, w, b); +} +function hsv2rgb(h, s, v) { + return calln(hsv2rgbn, h, s, v); +} +function hue(h) { + return (h % 360 + 360) % 360; +} +function hueParse(str) { + const m = HUE_RE.exec(str); + let a = 255; + let v; + if (!m) { + return; + } + if (m[5] !== v) { + a = m[6] ? p2b(+m[5]) : n2b(+m[5]); + } + const h = hue(+m[2]); + const p1 = +m[3] / 100; + const p2 = +m[4] / 100; + if (m[1] === 'hwb') { + v = hwb2rgb(h, p1, p2); + } else if (m[1] === 'hsv') { + v = hsv2rgb(h, p1, p2); + } else { + v = hsl2rgb(h, p1, p2); + } + return { + r: v[0], + g: v[1], + b: v[2], + a: a + }; +} +function rotate(v, deg) { + var h = rgb2hsl(v); + h[0] = hue(h[0] + deg); + h = hsl2rgb(h); + v.r = h[0]; + v.g = h[1]; + v.b = h[2]; +} +function hslString(v) { + if (!v) { + return; + } + const a = rgb2hsl(v); + const h = a[0]; + const s = n2p(a[1]); + const l = n2p(a[2]); + return v.a < 255 + ? `hsla(${h}, ${s}%, ${l}%, ${b2n(v.a)})` + : `hsl(${h}, ${s}%, ${l}%)`; +} +const map$1 = { + x: 'dark', + Z: 'light', + Y: 're', + X: 'blu', + W: 'gr', + V: 'medium', + U: 'slate', + A: 'ee', + T: 'ol', + S: 'or', + B: 'ra', + C: 'lateg', + D: 'ights', + R: 'in', + Q: 'turquois', + E: 'hi', + P: 'ro', + O: 'al', + N: 'le', + M: 'de', + L: 'yello', + F: 'en', + K: 'ch', + G: 'arks', + H: 'ea', + I: 'ightg', + J: 'wh' +}; +const names = { + OiceXe: 'f0f8ff', + antiquewEte: 'faebd7', + aqua: 'ffff', + aquamarRe: '7fffd4', + azuY: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '0', + blanKedOmond: 'ffebcd', + Xe: 'ff', + XeviTet: '8a2be2', + bPwn: 'a52a2a', + burlywood: 'deb887', + caMtXe: '5f9ea0', + KartYuse: '7fff00', + KocTate: 'd2691e', + cSO: 'ff7f50', + cSnflowerXe: '6495ed', + cSnsilk: 'fff8dc', + crimson: 'dc143c', + cyan: 'ffff', + xXe: '8b', + xcyan: '8b8b', + xgTMnPd: 'b8860b', + xWay: 'a9a9a9', + xgYF: '6400', + xgYy: 'a9a9a9', + xkhaki: 'bdb76b', + xmagFta: '8b008b', + xTivegYF: '556b2f', + xSange: 'ff8c00', + xScEd: '9932cc', + xYd: '8b0000', + xsOmon: 'e9967a', + xsHgYF: '8fbc8f', + xUXe: '483d8b', + xUWay: '2f4f4f', + xUgYy: '2f4f4f', + xQe: 'ced1', + xviTet: '9400d3', + dAppRk: 'ff1493', + dApskyXe: 'bfff', + dimWay: '696969', + dimgYy: '696969', + dodgerXe: '1e90ff', + fiYbrick: 'b22222', + flSOwEte: 'fffaf0', + foYstWAn: '228b22', + fuKsia: 'ff00ff', + gaRsbSo: 'dcdcdc', + ghostwEte: 'f8f8ff', + gTd: 'ffd700', + gTMnPd: 'daa520', + Way: '808080', + gYF: '8000', + gYFLw: 'adff2f', + gYy: '808080', + honeyMw: 'f0fff0', + hotpRk: 'ff69b4', + RdianYd: 'cd5c5c', + Rdigo: '4b0082', + ivSy: 'fffff0', + khaki: 'f0e68c', + lavFMr: 'e6e6fa', + lavFMrXsh: 'fff0f5', + lawngYF: '7cfc00', + NmoncEffon: 'fffacd', + ZXe: 'add8e6', + ZcSO: 'f08080', + Zcyan: 'e0ffff', + ZgTMnPdLw: 'fafad2', + ZWay: 'd3d3d3', + ZgYF: '90ee90', + ZgYy: 'd3d3d3', + ZpRk: 'ffb6c1', + ZsOmon: 'ffa07a', + ZsHgYF: '20b2aa', + ZskyXe: '87cefa', + ZUWay: '778899', + ZUgYy: '778899', + ZstAlXe: 'b0c4de', + ZLw: 'ffffe0', + lime: 'ff00', + limegYF: '32cd32', + lRF: 'faf0e6', + magFta: 'ff00ff', + maPon: '800000', + VaquamarRe: '66cdaa', + VXe: 'cd', + VScEd: 'ba55d3', + VpurpN: '9370db', + VsHgYF: '3cb371', + VUXe: '7b68ee', + VsprRggYF: 'fa9a', + VQe: '48d1cc', + VviTetYd: 'c71585', + midnightXe: '191970', + mRtcYam: 'f5fffa', + mistyPse: 'ffe4e1', + moccasR: 'ffe4b5', + navajowEte: 'ffdead', + navy: '80', + Tdlace: 'fdf5e6', + Tive: '808000', + TivedBb: '6b8e23', + Sange: 'ffa500', + SangeYd: 'ff4500', + ScEd: 'da70d6', + pOegTMnPd: 'eee8aa', + pOegYF: '98fb98', + pOeQe: 'afeeee', + pOeviTetYd: 'db7093', + papayawEp: 'ffefd5', + pHKpuff: 'ffdab9', + peru: 'cd853f', + pRk: 'ffc0cb', + plum: 'dda0dd', + powMrXe: 'b0e0e6', + purpN: '800080', + YbeccapurpN: '663399', + Yd: 'ff0000', + Psybrown: 'bc8f8f', + PyOXe: '4169e1', + saddNbPwn: '8b4513', + sOmon: 'fa8072', + sandybPwn: 'f4a460', + sHgYF: '2e8b57', + sHshell: 'fff5ee', + siFna: 'a0522d', + silver: 'c0c0c0', + skyXe: '87ceeb', + UXe: '6a5acd', + UWay: '708090', + UgYy: '708090', + snow: 'fffafa', + sprRggYF: 'ff7f', + stAlXe: '4682b4', + tan: 'd2b48c', + teO: '8080', + tEstN: 'd8bfd8', + tomato: 'ff6347', + Qe: '40e0d0', + viTet: 'ee82ee', + JHt: 'f5deb3', + wEte: 'ffffff', + wEtesmoke: 'f5f5f5', + Lw: 'ffff00', + LwgYF: '9acd32' +}; +function unpack() { + const unpacked = {}; + const keys = Object.keys(names); + const tkeys = Object.keys(map$1); + let i, j, k, ok, nk; + for (i = 0; i < keys.length; i++) { + ok = nk = keys[i]; + for (j = 0; j < tkeys.length; j++) { + k = tkeys[j]; + nk = nk.replace(k, map$1[k]); + } + k = parseInt(names[ok], 16); + unpacked[nk] = [k >> 16 & 0xFF, k >> 8 & 0xFF, k & 0xFF]; + } + return unpacked; +} +let names$1; +function nameParse(str) { + if (!names$1) { + names$1 = unpack(); + names$1.transparent = [0, 0, 0, 0]; + } + const a = names$1[str.toLowerCase()]; + return a && { + r: a[0], + g: a[1], + b: a[2], + a: a.length === 4 ? a[3] : 255 + }; +} +function modHSL(v, i, ratio) { + if (v) { + let tmp = rgb2hsl(v); + tmp[i] = Math.max(0, Math.min(tmp[i] + tmp[i] * ratio, i === 0 ? 360 : 1)); + tmp = hsl2rgb(tmp); + v.r = tmp[0]; + v.g = tmp[1]; + v.b = tmp[2]; + } +} +function clone(v, proto) { + return v ? Object.assign(proto || {}, v) : v; +} +function fromObject(input) { + var v = {r: 0, g: 0, b: 0, a: 255}; + if (Array.isArray(input)) { + if (input.length >= 3) { + v = {r: input[0], g: input[1], b: input[2], a: 255}; + if (input.length > 3) { + v.a = n2b(input[3]); + } + } + } else { + v = clone(input, {r: 0, g: 0, b: 0, a: 1}); + v.a = n2b(v.a); + } + return v; +} +function functionParse(str) { + if (str.charAt(0) === 'r') { + return rgbParse(str); + } + return hueParse(str); +} +class Color { + constructor(input) { + if (input instanceof Color) { + return input; + } + const type = typeof input; + let v; + if (type === 'object') { + v = fromObject(input); + } else if (type === 'string') { + v = hexParse(input) || nameParse(input) || functionParse(input); + } + this._rgb = v; + this._valid = !!v; + } + get valid() { + return this._valid; + } + get rgb() { + var v = clone(this._rgb); + if (v) { + v.a = b2n(v.a); + } + return v; + } + set rgb(obj) { + this._rgb = fromObject(obj); + } + rgbString() { + return this._valid ? rgbString(this._rgb) : this._rgb; + } + hexString() { + return this._valid ? hexString(this._rgb) : this._rgb; + } + hslString() { + return this._valid ? hslString(this._rgb) : this._rgb; + } + mix(color, weight) { + const me = this; + if (color) { + const c1 = me.rgb; + const c2 = color.rgb; + let w2; + const p = weight === w2 ? 0.5 : weight; + const w = 2 * p - 1; + const a = c1.a - c2.a; + const w1 = ((w * a === -1 ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + w2 = 1 - w1; + c1.r = 0xFF & w1 * c1.r + w2 * c2.r + 0.5; + c1.g = 0xFF & w1 * c1.g + w2 * c2.g + 0.5; + c1.b = 0xFF & w1 * c1.b + w2 * c2.b + 0.5; + c1.a = p * c1.a + (1 - p) * c2.a; + me.rgb = c1; + } + return me; + } + clone() { + return new Color(this.rgb); + } + alpha(a) { + this._rgb.a = n2b(a); + return this; + } + clearer(ratio) { + const rgb = this._rgb; + rgb.a *= 1 - ratio; + return this; + } + greyscale() { + const rgb = this._rgb; + const val = round(rgb.r * 0.3 + rgb.g * 0.59 + rgb.b * 0.11); + rgb.r = rgb.g = rgb.b = val; + return this; + } + opaquer(ratio) { + const rgb = this._rgb; + rgb.a *= 1 + ratio; + return this; + } + negate() { + const v = this._rgb; + v.r = 255 - v.r; + v.g = 255 - v.g; + v.b = 255 - v.b; + return this; + } + lighten(ratio) { + modHSL(this._rgb, 2, ratio); + return this; + } + darken(ratio) { + modHSL(this._rgb, 2, -ratio); + return this; + } + saturate(ratio) { + modHSL(this._rgb, 1, ratio); + return this; + } + desaturate(ratio) { + modHSL(this._rgb, 1, -ratio); + return this; + } + rotate(deg) { + rotate(this._rgb, deg); + return this; + } +} +function index_esm(input) { + return new Color(input); +} + +const isPatternOrGradient = (value) => value instanceof CanvasGradient || value instanceof CanvasPattern; +function color(value) { + return isPatternOrGradient(value) ? value : index_esm(value); +} +function getHoverColor(value) { + return isPatternOrGradient(value) + ? value + : index_esm(value).saturate(0.5).darken(0.1).hexString(); +} + +const overrides = Object.create(null); +const descriptors = Object.create(null); +function getScope$1(node, key) { + if (!key) { + return node; + } + const keys = key.split('.'); + for (let i = 0, n = keys.length; i < n; ++i) { + const k = keys[i]; + node = node[k] || (node[k] = Object.create(null)); + } + return node; +} +function set(root, scope, values) { + if (typeof scope === 'string') { + return merge(getScope$1(root, scope), values); + } + return merge(getScope$1(root, ''), scope); +} +class Defaults { + constructor(_descriptors) { + this.animation = undefined; + this.backgroundColor = 'rgba(0,0,0,0.1)'; + this.borderColor = 'rgba(0,0,0,0.1)'; + this.color = '#666'; + this.datasets = {}; + this.devicePixelRatio = (context) => context.chart.platform.getDevicePixelRatio(); + this.elements = {}; + this.events = [ + 'mousemove', + 'mouseout', + 'click', + 'touchstart', + 'touchmove' + ]; + this.font = { + family: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + size: 12, + style: 'normal', + lineHeight: 1.2, + weight: null + }; + this.hover = {}; + this.hoverBackgroundColor = (ctx, options) => getHoverColor(options.backgroundColor); + this.hoverBorderColor = (ctx, options) => getHoverColor(options.borderColor); + this.hoverColor = (ctx, options) => getHoverColor(options.color); + this.indexAxis = 'x'; + this.interaction = { + mode: 'nearest', + intersect: true + }; + this.maintainAspectRatio = true; + this.onHover = null; + this.onClick = null; + this.parsing = true; + this.plugins = {}; + this.responsive = true; + this.scale = undefined; + this.scales = {}; + this.showLine = true; + this.describe(_descriptors); + } + set(scope, values) { + return set(this, scope, values); + } + get(scope) { + return getScope$1(this, scope); + } + describe(scope, values) { + return set(descriptors, scope, values); + } + override(scope, values) { + return set(overrides, scope, values); + } + route(scope, name, targetScope, targetName) { + const scopeObject = getScope$1(this, scope); + const targetScopeObject = getScope$1(this, targetScope); + const privateName = '_' + name; + Object.defineProperties(scopeObject, { + [privateName]: { + value: scopeObject[name], + writable: true + }, + [name]: { + enumerable: true, + get() { + const local = this[privateName]; + const target = targetScopeObject[targetName]; + if (isObject(local)) { + return Object.assign({}, target, local); + } + return valueOrDefault(local, target); + }, + set(value) { + this[privateName] = value; + } + } + }); + } +} +var defaults = new Defaults({ + _scriptable: (name) => !name.startsWith('on'), + _indexable: (name) => name !== 'events', + hover: { + _fallback: 'interaction' + }, + interaction: { + _scriptable: false, + _indexable: false, + } +}); + +function toFontString(font) { + if (!font || isNullOrUndef(font.size) || isNullOrUndef(font.family)) { + return null; + } + return (font.style ? font.style + ' ' : '') + + (font.weight ? font.weight + ' ' : '') + + font.size + 'px ' + + font.family; +} +function _measureText(ctx, data, gc, longest, string) { + let textWidth = data[string]; + if (!textWidth) { + textWidth = data[string] = ctx.measureText(string).width; + gc.push(string); + } + if (textWidth > longest) { + longest = textWidth; + } + return longest; +} +function _longestText(ctx, font, arrayOfThings, cache) { + cache = cache || {}; + let data = cache.data = cache.data || {}; + let gc = cache.garbageCollect = cache.garbageCollect || []; + if (cache.font !== font) { + data = cache.data = {}; + gc = cache.garbageCollect = []; + cache.font = font; + } + ctx.save(); + ctx.font = font; + let longest = 0; + const ilen = arrayOfThings.length; + let i, j, jlen, thing, nestedThing; + for (i = 0; i < ilen; i++) { + thing = arrayOfThings[i]; + if (thing !== undefined && thing !== null && isArray(thing) !== true) { + longest = _measureText(ctx, data, gc, longest, thing); + } else if (isArray(thing)) { + for (j = 0, jlen = thing.length; j < jlen; j++) { + nestedThing = thing[j]; + if (nestedThing !== undefined && nestedThing !== null && !isArray(nestedThing)) { + longest = _measureText(ctx, data, gc, longest, nestedThing); + } + } + } + } + ctx.restore(); + const gcLen = gc.length / 2; + if (gcLen > arrayOfThings.length) { + for (i = 0; i < gcLen; i++) { + delete data[gc[i]]; + } + gc.splice(0, gcLen); + } + return longest; +} +function _alignPixel(chart, pixel, width) { + const devicePixelRatio = chart.currentDevicePixelRatio; + const halfWidth = width !== 0 ? Math.max(width / 2, 0.5) : 0; + return Math.round((pixel - halfWidth) * devicePixelRatio) / devicePixelRatio + halfWidth; +} +function clearCanvas(canvas, ctx) { + ctx = ctx || canvas.getContext('2d'); + ctx.save(); + ctx.resetTransform(); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.restore(); +} +function drawPoint(ctx, options, x, y) { + let type, xOffset, yOffset, size, cornerRadius; + const style = options.pointStyle; + const rotation = options.rotation; + const radius = options.radius; + let rad = (rotation || 0) * RAD_PER_DEG; + if (style && typeof style === 'object') { + type = style.toString(); + if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rad); + ctx.drawImage(style, -style.width / 2, -style.height / 2, style.width, style.height); + ctx.restore(); + return; + } + } + if (isNaN(radius) || radius <= 0) { + return; + } + ctx.beginPath(); + switch (style) { + default: + ctx.arc(x, y, radius, 0, TAU); + ctx.closePath(); + break; + case 'triangle': + ctx.moveTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + ctx.closePath(); + break; + case 'rectRounded': + cornerRadius = radius * 0.516; + size = radius - cornerRadius; + xOffset = Math.cos(rad + QUARTER_PI) * size; + yOffset = Math.sin(rad + QUARTER_PI) * size; + ctx.arc(x - xOffset, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI); + ctx.arc(x + yOffset, y - xOffset, cornerRadius, rad - HALF_PI, rad); + ctx.arc(x + xOffset, y + yOffset, cornerRadius, rad, rad + HALF_PI); + ctx.arc(x - yOffset, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI); + ctx.closePath(); + break; + case 'rect': + if (!rotation) { + size = Math.SQRT1_2 * radius; + ctx.rect(x - size, y - size, 2 * size, 2 * size); + break; + } + rad += QUARTER_PI; + case 'rectRot': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + yOffset, y - xOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.lineTo(x - yOffset, y + xOffset); + ctx.closePath(); + break; + case 'crossRot': + rad += QUARTER_PI; + case 'cross': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + break; + case 'star': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + rad += QUARTER_PI; + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + break; + case 'line': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + break; + case 'dash': + ctx.moveTo(x, y); + ctx.lineTo(x + Math.cos(rad) * radius, y + Math.sin(rad) * radius); + break; + } + ctx.fill(); + if (options.borderWidth > 0) { + ctx.stroke(); + } +} +function _isPointInArea(point, area, margin) { + margin = margin || 0.5; + return point && point.x > area.left - margin && point.x < area.right + margin && + point.y > area.top - margin && point.y < area.bottom + margin; +} +function clipArea(ctx, area) { + ctx.save(); + ctx.beginPath(); + ctx.rect(area.left, area.top, area.right - area.left, area.bottom - area.top); + ctx.clip(); +} +function unclipArea(ctx) { + ctx.restore(); +} +function _steppedLineTo(ctx, previous, target, flip, mode) { + if (!previous) { + return ctx.lineTo(target.x, target.y); + } + if (mode === 'middle') { + const midpoint = (previous.x + target.x) / 2.0; + ctx.lineTo(midpoint, previous.y); + ctx.lineTo(midpoint, target.y); + } else if (mode === 'after' !== !!flip) { + ctx.lineTo(previous.x, target.y); + } else { + ctx.lineTo(target.x, previous.y); + } + ctx.lineTo(target.x, target.y); +} +function _bezierCurveTo(ctx, previous, target, flip) { + if (!previous) { + return ctx.lineTo(target.x, target.y); + } + ctx.bezierCurveTo( + flip ? previous.cp1x : previous.cp2x, + flip ? previous.cp1y : previous.cp2y, + flip ? target.cp2x : target.cp1x, + flip ? target.cp2y : target.cp1y, + target.x, + target.y); +} +function renderText(ctx, text, x, y, font, opts = {}) { + const lines = isArray(text) ? text : [text]; + const stroke = opts.strokeWidth > 0 && opts.strokeColor !== ''; + let i, line; + ctx.save(); + ctx.font = font.string; + setRenderOpts(ctx, opts); + for (i = 0; i < lines.length; ++i) { + line = lines[i]; + if (stroke) { + if (opts.strokeColor) { + ctx.strokeStyle = opts.strokeColor; + } + if (!isNullOrUndef(opts.strokeWidth)) { + ctx.lineWidth = opts.strokeWidth; + } + ctx.strokeText(line, x, y, opts.maxWidth); + } + ctx.fillText(line, x, y, opts.maxWidth); + decorateText(ctx, x, y, line, opts); + y += font.lineHeight; + } + ctx.restore(); +} +function setRenderOpts(ctx, opts) { + if (opts.translation) { + ctx.translate(opts.translation[0], opts.translation[1]); + } + if (!isNullOrUndef(opts.rotation)) { + ctx.rotate(opts.rotation); + } + if (opts.color) { + ctx.fillStyle = opts.color; + } + if (opts.textAlign) { + ctx.textAlign = opts.textAlign; + } + if (opts.textBaseline) { + ctx.textBaseline = opts.textBaseline; + } +} +function decorateText(ctx, x, y, line, opts) { + if (opts.strikethrough || opts.underline) { + const metrics = ctx.measureText(line); + const left = x - metrics.actualBoundingBoxLeft; + const right = x + metrics.actualBoundingBoxRight; + const top = y - metrics.actualBoundingBoxAscent; + const bottom = y + metrics.actualBoundingBoxDescent; + const yDecoration = opts.strikethrough ? (top + bottom) / 2 : bottom; + ctx.strokeStyle = ctx.fillStyle; + ctx.beginPath(); + ctx.lineWidth = opts.decorationWidth || 2; + ctx.moveTo(left, yDecoration); + ctx.lineTo(right, yDecoration); + ctx.stroke(); + } +} +function addRoundedRectPath(ctx, rect) { + const {x, y, w, h, radius} = rect; + ctx.arc(x + radius.topLeft, y + radius.topLeft, radius.topLeft, -HALF_PI, PI, true); + ctx.lineTo(x, y + h - radius.bottomLeft); + ctx.arc(x + radius.bottomLeft, y + h - radius.bottomLeft, radius.bottomLeft, PI, HALF_PI, true); + ctx.lineTo(x + w - radius.bottomRight, y + h); + ctx.arc(x + w - radius.bottomRight, y + h - radius.bottomRight, radius.bottomRight, HALF_PI, 0, true); + ctx.lineTo(x + w, y + radius.topRight); + ctx.arc(x + w - radius.topRight, y + radius.topRight, radius.topRight, 0, -HALF_PI, true); + ctx.lineTo(x + radius.topLeft, y); +} + +const LINE_HEIGHT = new RegExp(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/); +const FONT_STYLE = new RegExp(/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/); +function toLineHeight(value, size) { + const matches = ('' + value).match(LINE_HEIGHT); + if (!matches || matches[1] === 'normal') { + return size * 1.2; + } + value = +matches[2]; + switch (matches[3]) { + case 'px': + return value; + case '%': + value /= 100; + break; + } + return size * value; +} +const numberOrZero = v => +v || 0; +function _readValueToProps(value, props) { + const ret = {}; + const objProps = isObject(props); + const keys = objProps ? Object.keys(props) : props; + const read = isObject(value) + ? objProps + ? prop => valueOrDefault(value[prop], value[props[prop]]) + : prop => value[prop] + : () => value; + for (const prop of keys) { + ret[prop] = numberOrZero(read(prop)); + } + return ret; +} +function toTRBL(value) { + return _readValueToProps(value, {top: 'y', right: 'x', bottom: 'y', left: 'x'}); +} +function toTRBLCorners(value) { + return _readValueToProps(value, ['topLeft', 'topRight', 'bottomLeft', 'bottomRight']); +} +function toPadding(value) { + const obj = toTRBL(value); + obj.width = obj.left + obj.right; + obj.height = obj.top + obj.bottom; + return obj; +} +function toFont(options, fallback) { + options = options || {}; + fallback = fallback || defaults.font; + let size = valueOrDefault(options.size, fallback.size); + if (typeof size === 'string') { + size = parseInt(size, 10); + } + let style = valueOrDefault(options.style, fallback.style); + if (style && !('' + style).match(FONT_STYLE)) { + console.warn('Invalid font style specified: "' + style + '"'); + style = ''; + } + const font = { + family: valueOrDefault(options.family, fallback.family), + lineHeight: toLineHeight(valueOrDefault(options.lineHeight, fallback.lineHeight), size), + size, + style, + weight: valueOrDefault(options.weight, fallback.weight), + string: '' + }; + font.string = toFontString(font); + return font; +} +function resolve(inputs, context, index, info) { + let cacheable = true; + let i, ilen, value; + for (i = 0, ilen = inputs.length; i < ilen; ++i) { + value = inputs[i]; + if (value === undefined) { + continue; + } + if (context !== undefined && typeof value === 'function') { + value = value(context); + cacheable = false; + } + if (index !== undefined && isArray(value)) { + value = value[index % value.length]; + cacheable = false; + } + if (value !== undefined) { + if (info && !cacheable) { + info.cacheable = false; + } + return value; + } + } +} +function _addGrace(minmax, grace) { + const {min, max} = minmax; + return { + min: min - Math.abs(toDimension(grace, min)), + max: max + toDimension(grace, max) + }; +} + +function _lookup(table, value, cmp) { + cmp = cmp || ((index) => table[index] < value); + let hi = table.length - 1; + let lo = 0; + let mid; + while (hi - lo > 1) { + mid = (lo + hi) >> 1; + if (cmp(mid)) { + lo = mid; + } else { + hi = mid; + } + } + return {lo, hi}; +} +const _lookupByKey = (table, key, value) => + _lookup(table, value, index => table[index][key] < value); +const _rlookupByKey = (table, key, value) => + _lookup(table, value, index => table[index][key] >= value); +function _filterBetween(values, min, max) { + let start = 0; + let end = values.length; + while (start < end && values[start] < min) { + start++; + } + while (end > start && values[end - 1] > max) { + end--; + } + return start > 0 || end < values.length + ? values.slice(start, end) + : values; +} +const arrayEvents = ['push', 'pop', 'shift', 'splice', 'unshift']; +function listenArrayEvents(array, listener) { + if (array._chartjs) { + array._chartjs.listeners.push(listener); + return; + } + Object.defineProperty(array, '_chartjs', { + configurable: true, + enumerable: false, + value: { + listeners: [listener] + } + }); + arrayEvents.forEach((key) => { + const method = '_onData' + _capitalize(key); + const base = array[key]; + Object.defineProperty(array, key, { + configurable: true, + enumerable: false, + value(...args) { + const res = base.apply(this, args); + array._chartjs.listeners.forEach((object) => { + if (typeof object[method] === 'function') { + object[method](...args); + } + }); + return res; + } + }); + }); +} +function unlistenArrayEvents(array, listener) { + const stub = array._chartjs; + if (!stub) { + return; + } + const listeners = stub.listeners; + const index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + if (listeners.length > 0) { + return; + } + arrayEvents.forEach((key) => { + delete array[key]; + }); + delete array._chartjs; +} +function _arrayUnique(items) { + const set = new Set(); + let i, ilen; + for (i = 0, ilen = items.length; i < ilen; ++i) { + set.add(items[i]); + } + if (set.size === ilen) { + return items; + } + return Array.from(set); +} + +function _createResolver(scopes, prefixes = [''], rootScopes = scopes, fallback, getTarget = () => scopes[0]) { + if (!defined(fallback)) { + fallback = _resolve('_fallback', scopes); + } + const cache = { + [Symbol.toStringTag]: 'Object', + _cacheable: true, + _scopes: scopes, + _rootScopes: rootScopes, + _fallback: fallback, + _getTarget: getTarget, + override: (scope) => _createResolver([scope, ...scopes], prefixes, rootScopes, fallback), + }; + return new Proxy(cache, { + deleteProperty(target, prop) { + delete target[prop]; + delete target._keys; + delete scopes[0][prop]; + return true; + }, + get(target, prop) { + return _cached(target, prop, + () => _resolveWithPrefixes(prop, prefixes, scopes, target)); + }, + getOwnPropertyDescriptor(target, prop) { + return Reflect.getOwnPropertyDescriptor(target._scopes[0], prop); + }, + getPrototypeOf() { + return Reflect.getPrototypeOf(scopes[0]); + }, + has(target, prop) { + return getKeysFromAllScopes(target).includes(prop); + }, + ownKeys(target) { + return getKeysFromAllScopes(target); + }, + set(target, prop, value) { + const storage = target._storage || (target._storage = getTarget()); + storage[prop] = value; + delete target[prop]; + delete target._keys; + return true; + } + }); +} +function _attachContext(proxy, context, subProxy, descriptorDefaults) { + const cache = { + _cacheable: false, + _proxy: proxy, + _context: context, + _subProxy: subProxy, + _stack: new Set(), + _descriptors: _descriptors(proxy, descriptorDefaults), + setContext: (ctx) => _attachContext(proxy, ctx, subProxy, descriptorDefaults), + override: (scope) => _attachContext(proxy.override(scope), context, subProxy, descriptorDefaults) + }; + return new Proxy(cache, { + deleteProperty(target, prop) { + delete target[prop]; + delete proxy[prop]; + return true; + }, + get(target, prop, receiver) { + return _cached(target, prop, + () => _resolveWithContext(target, prop, receiver)); + }, + getOwnPropertyDescriptor(target, prop) { + return target._descriptors.allKeys + ? Reflect.has(proxy, prop) ? {enumerable: true, configurable: true} : undefined + : Reflect.getOwnPropertyDescriptor(proxy, prop); + }, + getPrototypeOf() { + return Reflect.getPrototypeOf(proxy); + }, + has(target, prop) { + return Reflect.has(proxy, prop); + }, + ownKeys() { + return Reflect.ownKeys(proxy); + }, + set(target, prop, value) { + proxy[prop] = value; + delete target[prop]; + return true; + } + }); +} +function _descriptors(proxy, defaults = {scriptable: true, indexable: true}) { + const {_scriptable = defaults.scriptable, _indexable = defaults.indexable, _allKeys = defaults.allKeys} = proxy; + return { + allKeys: _allKeys, + scriptable: _scriptable, + indexable: _indexable, + isScriptable: isFunction(_scriptable) ? _scriptable : () => _scriptable, + isIndexable: isFunction(_indexable) ? _indexable : () => _indexable + }; +} +const readKey = (prefix, name) => prefix ? prefix + _capitalize(name) : name; +const needsSubResolver = (prop, value) => isObject(value) && prop !== 'adapters'; +function _cached(target, prop, resolve) { + let value = target[prop]; + if (defined(value)) { + return value; + } + value = resolve(); + if (defined(value)) { + target[prop] = value; + } + return value; +} +function _resolveWithContext(target, prop, receiver) { + const {_proxy, _context, _subProxy, _descriptors: descriptors} = target; + let value = _proxy[prop]; + if (isFunction(value) && descriptors.isScriptable(prop)) { + value = _resolveScriptable(prop, value, target, receiver); + } + if (isArray(value) && value.length) { + value = _resolveArray(prop, value, target, descriptors.isIndexable); + } + if (needsSubResolver(prop, value)) { + value = _attachContext(value, _context, _subProxy && _subProxy[prop], descriptors); + } + return value; +} +function _resolveScriptable(prop, value, target, receiver) { + const {_proxy, _context, _subProxy, _stack} = target; + if (_stack.has(prop)) { + throw new Error('Recursion detected: ' + Array.from(_stack).join('->') + '->' + prop); + } + _stack.add(prop); + value = value(_context, _subProxy || receiver); + _stack.delete(prop); + if (isObject(value)) { + value = createSubResolver(_proxy._scopes, _proxy, prop, value); + } + return value; +} +function _resolveArray(prop, value, target, isIndexable) { + const {_proxy, _context, _subProxy, _descriptors: descriptors} = target; + if (defined(_context.index) && isIndexable(prop)) { + value = value[_context.index % value.length]; + } else if (isObject(value[0])) { + const arr = value; + const scopes = _proxy._scopes.filter(s => s !== arr); + value = []; + for (const item of arr) { + const resolver = createSubResolver(scopes, _proxy, prop, item); + value.push(_attachContext(resolver, _context, _subProxy && _subProxy[prop], descriptors)); + } + } + return value; +} +function resolveFallback(fallback, prop, value) { + return isFunction(fallback) ? fallback(prop, value) : fallback; +} +const getScope = (key, parent) => key === true ? parent + : typeof key === 'string' ? resolveObjectKey(parent, key) : undefined; +function addScopes(set, parentScopes, key, parentFallback) { + for (const parent of parentScopes) { + const scope = getScope(key, parent); + if (scope) { + set.add(scope); + const fallback = resolveFallback(scope._fallback, key, scope); + if (defined(fallback) && fallback !== key && fallback !== parentFallback) { + return fallback; + } + } else if (scope === false && defined(parentFallback) && key !== parentFallback) { + return null; + } + } + return false; +} +function createSubResolver(parentScopes, resolver, prop, value) { + const rootScopes = resolver._rootScopes; + const fallback = resolveFallback(resolver._fallback, prop, value); + const allScopes = [...parentScopes, ...rootScopes]; + const set = new Set(); + set.add(value); + let key = addScopesFromKey(set, allScopes, prop, fallback || prop); + if (key === null) { + return false; + } + if (defined(fallback) && fallback !== prop) { + key = addScopesFromKey(set, allScopes, fallback, key); + if (key === null) { + return false; + } + } + return _createResolver(Array.from(set), [''], rootScopes, fallback, + () => subGetTarget(resolver, prop, value)); +} +function addScopesFromKey(set, allScopes, key, fallback) { + while (key) { + key = addScopes(set, allScopes, key, fallback); + } + return key; +} +function subGetTarget(resolver, prop, value) { + const parent = resolver._getTarget(); + if (!(prop in parent)) { + parent[prop] = {}; + } + const target = parent[prop]; + if (isArray(target) && isObject(value)) { + return value; + } + return target; +} +function _resolveWithPrefixes(prop, prefixes, scopes, proxy) { + let value; + for (const prefix of prefixes) { + value = _resolve(readKey(prefix, prop), scopes); + if (defined(value)) { + return needsSubResolver(prop, value) + ? createSubResolver(scopes, proxy, prop, value) + : value; + } + } +} +function _resolve(key, scopes) { + for (const scope of scopes) { + if (!scope) { + continue; + } + const value = scope[key]; + if (defined(value)) { + return value; + } + } +} +function getKeysFromAllScopes(target) { + let keys = target._keys; + if (!keys) { + keys = target._keys = resolveKeysFromAllScopes(target._scopes); + } + return keys; +} +function resolveKeysFromAllScopes(scopes) { + const set = new Set(); + for (const scope of scopes) { + for (const key of Object.keys(scope).filter(k => !k.startsWith('_'))) { + set.add(key); + } + } + return Array.from(set); +} + +const EPSILON = Number.EPSILON || 1e-14; +const getPoint = (points, i) => i < points.length && !points[i].skip && points[i]; +const getValueAxis = (indexAxis) => indexAxis === 'x' ? 'y' : 'x'; +function splineCurve(firstPoint, middlePoint, afterPoint, t) { + const previous = firstPoint.skip ? middlePoint : firstPoint; + const current = middlePoint; + const next = afterPoint.skip ? middlePoint : afterPoint; + const d01 = distanceBetweenPoints(current, previous); + const d12 = distanceBetweenPoints(next, current); + let s01 = d01 / (d01 + d12); + let s12 = d12 / (d01 + d12); + s01 = isNaN(s01) ? 0 : s01; + s12 = isNaN(s12) ? 0 : s12; + const fa = t * s01; + const fb = t * s12; + return { + previous: { + x: current.x - fa * (next.x - previous.x), + y: current.y - fa * (next.y - previous.y) + }, + next: { + x: current.x + fb * (next.x - previous.x), + y: current.y + fb * (next.y - previous.y) + } + }; +} +function monotoneAdjust(points, deltaK, mK) { + const pointsLen = points.length; + let alphaK, betaK, tauK, squaredMagnitude, pointCurrent; + let pointAfter = getPoint(points, 0); + for (let i = 0; i < pointsLen - 1; ++i) { + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent || !pointAfter) { + continue; + } + if (almostEquals(deltaK[i], 0, EPSILON)) { + mK[i] = mK[i + 1] = 0; + continue; + } + alphaK = mK[i] / deltaK[i]; + betaK = mK[i + 1] / deltaK[i]; + squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2); + if (squaredMagnitude <= 9) { + continue; + } + tauK = 3 / Math.sqrt(squaredMagnitude); + mK[i] = alphaK * tauK * deltaK[i]; + mK[i + 1] = betaK * tauK * deltaK[i]; + } +} +function monotoneCompute(points, mK, indexAxis = 'x') { + const valueAxis = getValueAxis(indexAxis); + const pointsLen = points.length; + let delta, pointBefore, pointCurrent; + let pointAfter = getPoint(points, 0); + for (let i = 0; i < pointsLen; ++i) { + pointBefore = pointCurrent; + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent) { + continue; + } + const iPixel = pointCurrent[indexAxis]; + const vPixel = pointCurrent[valueAxis]; + if (pointBefore) { + delta = (iPixel - pointBefore[indexAxis]) / 3; + pointCurrent[`cp1${indexAxis}`] = iPixel - delta; + pointCurrent[`cp1${valueAxis}`] = vPixel - delta * mK[i]; + } + if (pointAfter) { + delta = (pointAfter[indexAxis] - iPixel) / 3; + pointCurrent[`cp2${indexAxis}`] = iPixel + delta; + pointCurrent[`cp2${valueAxis}`] = vPixel + delta * mK[i]; + } + } +} +function splineCurveMonotone(points, indexAxis = 'x') { + const valueAxis = getValueAxis(indexAxis); + const pointsLen = points.length; + const deltaK = Array(pointsLen).fill(0); + const mK = Array(pointsLen); + let i, pointBefore, pointCurrent; + let pointAfter = getPoint(points, 0); + for (i = 0; i < pointsLen; ++i) { + pointBefore = pointCurrent; + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent) { + continue; + } + if (pointAfter) { + const slopeDelta = pointAfter[indexAxis] - pointCurrent[indexAxis]; + deltaK[i] = slopeDelta !== 0 ? (pointAfter[valueAxis] - pointCurrent[valueAxis]) / slopeDelta : 0; + } + mK[i] = !pointBefore ? deltaK[i] + : !pointAfter ? deltaK[i - 1] + : (sign(deltaK[i - 1]) !== sign(deltaK[i])) ? 0 + : (deltaK[i - 1] + deltaK[i]) / 2; + } + monotoneAdjust(points, deltaK, mK); + monotoneCompute(points, mK, indexAxis); +} +function capControlPoint(pt, min, max) { + return Math.max(Math.min(pt, max), min); +} +function capBezierPoints(points, area) { + let i, ilen, point, inArea, inAreaPrev; + let inAreaNext = _isPointInArea(points[0], area); + for (i = 0, ilen = points.length; i < ilen; ++i) { + inAreaPrev = inArea; + inArea = inAreaNext; + inAreaNext = i < ilen - 1 && _isPointInArea(points[i + 1], area); + if (!inArea) { + continue; + } + point = points[i]; + if (inAreaPrev) { + point.cp1x = capControlPoint(point.cp1x, area.left, area.right); + point.cp1y = capControlPoint(point.cp1y, area.top, area.bottom); + } + if (inAreaNext) { + point.cp2x = capControlPoint(point.cp2x, area.left, area.right); + point.cp2y = capControlPoint(point.cp2y, area.top, area.bottom); + } + } +} +function _updateBezierControlPoints(points, options, area, loop, indexAxis) { + let i, ilen, point, controlPoints; + if (options.spanGaps) { + points = points.filter((pt) => !pt.skip); + } + if (options.cubicInterpolationMode === 'monotone') { + splineCurveMonotone(points, indexAxis); + } else { + let prev = loop ? points[points.length - 1] : points[0]; + for (i = 0, ilen = points.length; i < ilen; ++i) { + point = points[i]; + controlPoints = splineCurve( + prev, + point, + points[Math.min(i + 1, ilen - (loop ? 0 : 1)) % ilen], + options.tension + ); + point.cp1x = controlPoints.previous.x; + point.cp1y = controlPoints.previous.y; + point.cp2x = controlPoints.next.x; + point.cp2y = controlPoints.next.y; + prev = point; + } + } + if (options.capBezierPoints) { + capBezierPoints(points, area); + } +} + +function _getParentNode(domNode) { + let parent = domNode.parentNode; + if (parent && parent.toString() === '[object ShadowRoot]') { + parent = parent.host; + } + return parent; +} +function parseMaxStyle(styleValue, node, parentProperty) { + let valueInPixels; + if (typeof styleValue === 'string') { + valueInPixels = parseInt(styleValue, 10); + if (styleValue.indexOf('%') !== -1) { + valueInPixels = valueInPixels / 100 * node.parentNode[parentProperty]; + } + } else { + valueInPixels = styleValue; + } + return valueInPixels; +} +const getComputedStyle = (element) => window.getComputedStyle(element, null); +function getStyle(el, property) { + return getComputedStyle(el).getPropertyValue(property); +} +const positions = ['top', 'right', 'bottom', 'left']; +function getPositionedStyle(styles, style, suffix) { + const result = {}; + suffix = suffix ? '-' + suffix : ''; + for (let i = 0; i < 4; i++) { + const pos = positions[i]; + result[pos] = parseFloat(styles[style + '-' + pos + suffix]) || 0; + } + result.width = result.left + result.right; + result.height = result.top + result.bottom; + return result; +} +const useOffsetPos = (x, y, target) => (x > 0 || y > 0) && (!target || !target.shadowRoot); +function getCanvasPosition(evt, canvas) { + const e = evt.native || evt; + const touches = e.touches; + const source = touches && touches.length ? touches[0] : e; + const {offsetX, offsetY} = source; + let box = false; + let x, y; + if (useOffsetPos(offsetX, offsetY, e.target)) { + x = offsetX; + y = offsetY; + } else { + const rect = canvas.getBoundingClientRect(); + x = source.clientX - rect.left; + y = source.clientY - rect.top; + box = true; + } + return {x, y, box}; +} +function getRelativePosition(evt, chart) { + const {canvas, currentDevicePixelRatio} = chart; + const style = getComputedStyle(canvas); + const borderBox = style.boxSizing === 'border-box'; + const paddings = getPositionedStyle(style, 'padding'); + const borders = getPositionedStyle(style, 'border', 'width'); + const {x, y, box} = getCanvasPosition(evt, canvas); + const xOffset = paddings.left + (box && borders.left); + const yOffset = paddings.top + (box && borders.top); + let {width, height} = chart; + if (borderBox) { + width -= paddings.width + borders.width; + height -= paddings.height + borders.height; + } + return { + x: Math.round((x - xOffset) / width * canvas.width / currentDevicePixelRatio), + y: Math.round((y - yOffset) / height * canvas.height / currentDevicePixelRatio) + }; +} +function getContainerSize(canvas, width, height) { + let maxWidth, maxHeight; + if (width === undefined || height === undefined) { + const container = _getParentNode(canvas); + if (!container) { + width = canvas.clientWidth; + height = canvas.clientHeight; + } else { + const rect = container.getBoundingClientRect(); + const containerStyle = getComputedStyle(container); + const containerBorder = getPositionedStyle(containerStyle, 'border', 'width'); + const containerPadding = getPositionedStyle(containerStyle, 'padding'); + width = rect.width - containerPadding.width - containerBorder.width; + height = rect.height - containerPadding.height - containerBorder.height; + maxWidth = parseMaxStyle(containerStyle.maxWidth, container, 'clientWidth'); + maxHeight = parseMaxStyle(containerStyle.maxHeight, container, 'clientHeight'); + } + } + return { + width, + height, + maxWidth: maxWidth || INFINITY, + maxHeight: maxHeight || INFINITY + }; +} +const round1 = v => Math.round(v * 10) / 10; +function getMaximumSize(canvas, bbWidth, bbHeight, aspectRatio) { + const style = getComputedStyle(canvas); + const margins = getPositionedStyle(style, 'margin'); + const maxWidth = parseMaxStyle(style.maxWidth, canvas, 'clientWidth') || INFINITY; + const maxHeight = parseMaxStyle(style.maxHeight, canvas, 'clientHeight') || INFINITY; + const containerSize = getContainerSize(canvas, bbWidth, bbHeight); + let {width, height} = containerSize; + if (style.boxSizing === 'content-box') { + const borders = getPositionedStyle(style, 'border', 'width'); + const paddings = getPositionedStyle(style, 'padding'); + width -= paddings.width + borders.width; + height -= paddings.height + borders.height; + } + width = Math.max(0, width - margins.width); + height = Math.max(0, aspectRatio ? Math.floor(width / aspectRatio) : height - margins.height); + width = round1(Math.min(width, maxWidth, containerSize.maxWidth)); + height = round1(Math.min(height, maxHeight, containerSize.maxHeight)); + if (width && !height) { + height = round1(width / 2); + } + return { + width, + height + }; +} +function retinaScale(chart, forceRatio, forceStyle) { + const pixelRatio = forceRatio || 1; + const deviceHeight = Math.floor(chart.height * pixelRatio); + const deviceWidth = Math.floor(chart.width * pixelRatio); + chart.height = deviceHeight / pixelRatio; + chart.width = deviceWidth / pixelRatio; + const canvas = chart.canvas; + if (canvas.style && (forceStyle || (!canvas.style.height && !canvas.style.width))) { + canvas.style.height = `${chart.height}px`; + canvas.style.width = `${chart.width}px`; + } + if (chart.currentDevicePixelRatio !== pixelRatio + || canvas.height !== deviceHeight + || canvas.width !== deviceWidth) { + chart.currentDevicePixelRatio = pixelRatio; + canvas.height = deviceHeight; + canvas.width = deviceWidth; + chart.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + return true; + } + return false; +} +const supportsEventListenerOptions = (function() { + let passiveSupported = false; + try { + const options = { + get passive() { + passiveSupported = true; + return false; + } + }; + window.addEventListener('test', null, options); + window.removeEventListener('test', null, options); + } catch (e) { + } + return passiveSupported; +}()); +function readUsedSize(element, property) { + const value = getStyle(element, property); + const matches = value && value.match(/^(\d+)(\.\d+)?px$/); + return matches ? +matches[1] : undefined; +} + +function _pointInLine(p1, p2, t, mode) { + return { + x: p1.x + t * (p2.x - p1.x), + y: p1.y + t * (p2.y - p1.y) + }; +} +function _steppedInterpolation(p1, p2, t, mode) { + return { + x: p1.x + t * (p2.x - p1.x), + y: mode === 'middle' ? t < 0.5 ? p1.y : p2.y + : mode === 'after' ? t < 1 ? p1.y : p2.y + : t > 0 ? p2.y : p1.y + }; +} +function _bezierInterpolation(p1, p2, t, mode) { + const cp1 = {x: p1.cp2x, y: p1.cp2y}; + const cp2 = {x: p2.cp1x, y: p2.cp1y}; + const a = _pointInLine(p1, cp1, t); + const b = _pointInLine(cp1, cp2, t); + const c = _pointInLine(cp2, p2, t); + const d = _pointInLine(a, b, t); + const e = _pointInLine(b, c, t); + return _pointInLine(d, e, t); +} + +const intlCache = new Map(); +function getNumberFormat(locale, options) { + options = options || {}; + const cacheKey = locale + JSON.stringify(options); + let formatter = intlCache.get(cacheKey); + if (!formatter) { + formatter = new Intl.NumberFormat(locale, options); + intlCache.set(cacheKey, formatter); + } + return formatter; +} +function formatNumber(num, locale, options) { + return getNumberFormat(locale, options).format(num); +} + +const getRightToLeftAdapter = function(rectX, width) { + return { + x(x) { + return rectX + rectX + width - x; + }, + setWidth(w) { + width = w; + }, + textAlign(align) { + if (align === 'center') { + return align; + } + return align === 'right' ? 'left' : 'right'; + }, + xPlus(x, value) { + return x - value; + }, + leftForLtr(x, itemWidth) { + return x - itemWidth; + }, + }; +}; +const getLeftToRightAdapter = function() { + return { + x(x) { + return x; + }, + setWidth(w) { + }, + textAlign(align) { + return align; + }, + xPlus(x, value) { + return x + value; + }, + leftForLtr(x, _itemWidth) { + return x; + }, + }; +}; +function getRtlAdapter(rtl, rectX, width) { + return rtl ? getRightToLeftAdapter(rectX, width) : getLeftToRightAdapter(); +} +function overrideTextDirection(ctx, direction) { + let style, original; + if (direction === 'ltr' || direction === 'rtl') { + style = ctx.canvas.style; + original = [ + style.getPropertyValue('direction'), + style.getPropertyPriority('direction'), + ]; + style.setProperty('direction', direction, 'important'); + ctx.prevTextDirection = original; + } +} +function restoreTextDirection(ctx, original) { + if (original !== undefined) { + delete ctx.prevTextDirection; + ctx.canvas.style.setProperty('direction', original[0], original[1]); + } +} + +function propertyFn(property) { + if (property === 'angle') { + return { + between: _angleBetween, + compare: _angleDiff, + normalize: _normalizeAngle, + }; + } + return { + between: (n, s, e) => n >= Math.min(s, e) && n <= Math.max(e, s), + compare: (a, b) => a - b, + normalize: x => x + }; +} +function normalizeSegment({start, end, count, loop, style}) { + return { + start: start % count, + end: end % count, + loop: loop && (end - start + 1) % count === 0, + style + }; +} +function getSegment(segment, points, bounds) { + const {property, start: startBound, end: endBound} = bounds; + const {between, normalize} = propertyFn(property); + const count = points.length; + let {start, end, loop} = segment; + let i, ilen; + if (loop) { + start += count; + end += count; + for (i = 0, ilen = count; i < ilen; ++i) { + if (!between(normalize(points[start % count][property]), startBound, endBound)) { + break; + } + start--; + end--; + } + start %= count; + end %= count; + } + if (end < start) { + end += count; + } + return {start, end, loop, style: segment.style}; +} +function _boundSegment(segment, points, bounds) { + if (!bounds) { + return [segment]; + } + const {property, start: startBound, end: endBound} = bounds; + const count = points.length; + const {compare, between, normalize} = propertyFn(property); + const {start, end, loop, style} = getSegment(segment, points, bounds); + const result = []; + let inside = false; + let subStart = null; + let value, point, prevValue; + const startIsBefore = () => between(startBound, prevValue, value) && compare(startBound, prevValue) !== 0; + const endIsBefore = () => compare(endBound, value) === 0 || between(endBound, prevValue, value); + const shouldStart = () => inside || startIsBefore(); + const shouldStop = () => !inside || endIsBefore(); + for (let i = start, prev = start; i <= end; ++i) { + point = points[i % count]; + if (point.skip) { + continue; + } + value = normalize(point[property]); + if (value === prevValue) { + continue; + } + inside = between(value, startBound, endBound); + if (subStart === null && shouldStart()) { + subStart = compare(value, startBound) === 0 ? i : prev; + } + if (subStart !== null && shouldStop()) { + result.push(normalizeSegment({start: subStart, end: i, loop, count, style})); + subStart = null; + } + prev = i; + prevValue = value; + } + if (subStart !== null) { + result.push(normalizeSegment({start: subStart, end, loop, count, style})); + } + return result; +} +function _boundSegments(line, bounds) { + const result = []; + const segments = line.segments; + for (let i = 0; i < segments.length; i++) { + const sub = _boundSegment(segments[i], line.points, bounds); + if (sub.length) { + result.push(...sub); + } + } + return result; +} +function findStartAndEnd(points, count, loop, spanGaps) { + let start = 0; + let end = count - 1; + if (loop && !spanGaps) { + while (start < count && !points[start].skip) { + start++; + } + } + while (start < count && points[start].skip) { + start++; + } + start %= count; + if (loop) { + end += start; + } + while (end > start && points[end % count].skip) { + end--; + } + end %= count; + return {start, end}; +} +function solidSegments(points, start, max, loop) { + const count = points.length; + const result = []; + let last = start; + let prev = points[start]; + let end; + for (end = start + 1; end <= max; ++end) { + const cur = points[end % count]; + if (cur.skip || cur.stop) { + if (!prev.skip) { + loop = false; + result.push({start: start % count, end: (end - 1) % count, loop}); + start = last = cur.stop ? end : null; + } + } else { + last = end; + if (prev.skip) { + start = end; + } + } + prev = cur; + } + if (last !== null) { + result.push({start: start % count, end: last % count, loop}); + } + return result; +} +function _computeSegments(line, segmentOptions) { + const points = line.points; + const spanGaps = line.options.spanGaps; + const count = points.length; + if (!count) { + return []; + } + const loop = !!line._loop; + const {start, end} = findStartAndEnd(points, count, loop, spanGaps); + if (spanGaps === true) { + return splitByStyles([{start, end, loop}], points, segmentOptions); + } + const max = end < start ? end + count : end; + const completeLoop = !!line._fullLoop && start === 0 && end === count - 1; + return splitByStyles(solidSegments(points, start, max, completeLoop), points, segmentOptions); +} +function splitByStyles(segments, points, segmentOptions) { + if (!segmentOptions || !segmentOptions.setContext || !points) { + return segments; + } + return doSplitByStyles(segments, points, segmentOptions); +} +function doSplitByStyles(segments, points, segmentOptions) { + const count = points.length; + const result = []; + let start = segments[0].start; + let i = start; + for (const segment of segments) { + let prevStyle, style; + let prev = points[start % count]; + for (i = start + 1; i <= segment.end; i++) { + const pt = points[i % count]; + style = readStyle(segmentOptions.setContext({type: 'segment', p0: prev, p1: pt})); + if (styleChanged(style, prevStyle)) { + result.push({start: start, end: i - 1, loop: segment.loop, style: prevStyle}); + prevStyle = style; + start = i - 1; + } + prev = pt; + prevStyle = style; + } + if (start < i - 1) { + result.push({start, end: i - 1, loop: segment.loop, style}); + start = i - 1; + } + } + return result; +} +function readStyle(options) { + return { + backgroundColor: options.backgroundColor, + borderCapStyle: options.borderCapStyle, + borderDash: options.borderDash, + borderDashOffset: options.borderDashOffset, + borderJoinStyle: options.borderJoinStyle, + borderWidth: options.borderWidth, + borderColor: options.borderColor + }; +} +function styleChanged(style, prevStyle) { + return prevStyle && JSON.stringify(style) !== JSON.stringify(prevStyle); +} + +export { merge as $, toPadding as A, each as B, getMaximumSize as C, _getParentNode as D, readUsedSize as E, throttled as F, supportsEventListenerOptions as G, HALF_PI as H, log10 as I, _factorize as J, finiteOrDefault as K, callback as L, _addGrace as M, toDegrees as N, _measureText as O, PI as P, _int16Range as Q, _alignPixel as R, clipArea as S, TAU as T, renderText as U, unclipArea as V, toFont as W, _toLeftRightCenter as X, _alignStartEnd as Y, overrides as Z, _arrayUnique as _, resolve as a, _capitalize as a0, descriptors as a1, isFunction as a2, _attachContext as a3, _createResolver as a4, _descriptors as a5, mergeIf as a6, uid as a7, debounce as a8, retinaScale as a9, niceNum as aA, almostWhole as aB, almostEquals as aC, _decimalPlaces as aD, _longestText as aE, _filterBetween as aF, _lookup as aG, getHoverColor as aH, clone$1 as aI, _merger as aJ, _mergerIf as aK, _deprecated as aL, toFontString as aM, splineCurve as aN, splineCurveMonotone as aO, getStyle as aP, fontString as aQ, toLineHeight as aR, PITAU as aS, INFINITY as aT, RAD_PER_DEG as aU, QUARTER_PI as aV, TWO_THIRDS_PI as aW, _angleDiff as aX, clearCanvas as aa, setsEqual as ab, _elementsEqual as ac, getAngleFromPoint as ad, _readValueToProps as ae, _updateBezierControlPoints as af, _computeSegments as ag, _boundSegments as ah, _steppedInterpolation as ai, _bezierInterpolation as aj, _pointInLine as ak, _steppedLineTo as al, _bezierCurveTo as am, drawPoint as an, addRoundedRectPath as ao, toTRBL as ap, toTRBLCorners as aq, _boundSegment as ar, _normalizeAngle as as, getRtlAdapter as at, overrideTextDirection as au, _textX as av, restoreTextDirection as aw, noop as ax, distanceBetweenPoints as ay, _setMinAndMaxByKey as az, isArray as b, color as c, defaults as d, effects as e, resolveObjectKey as f, isNumberFinite as g, defined as h, isObject as i, isNullOrUndef as j, toPercentage as k, listenArrayEvents as l, toDimension as m, formatNumber as n, _angleBetween as o, isNumber as p, _limitValue as q, requestAnimFrame as r, sign as s, toRadians as t, unlistenArrayEvents as u, valueOrDefault as v, _lookupByKey as w, getRelativePosition as x, _isPointInArea as y, _rlookupByKey as z }; diff --git a/node_modules/chart.js/dist/helpers.esm.js b/node_modules/chart.js/dist/helpers.esm.js new file mode 100644 index 000000000..403cf04bc --- /dev/null +++ b/node_modules/chart.js/dist/helpers.esm.js @@ -0,0 +1,7 @@ +/*! + * Chart.js v3.4.1 + * https://www.chartjs.org + * (c) 2021 Chart.js Contributors + * Released under the MIT License + */ +export { H as HALF_PI, aT as INFINITY, P as PI, aS as PITAU, aV as QUARTER_PI, aU as RAD_PER_DEG, T as TAU, aW as TWO_THIRDS_PI, M as _addGrace, R as _alignPixel, Y as _alignStartEnd, o as _angleBetween, aX as _angleDiff, _ as _arrayUnique, a3 as _attachContext, am as _bezierCurveTo, aj as _bezierInterpolation, ar as _boundSegment, ah as _boundSegments, a0 as _capitalize, ag as _computeSegments, a4 as _createResolver, aD as _decimalPlaces, aL as _deprecated, a5 as _descriptors, ac as _elementsEqual, J as _factorize, aF as _filterBetween, D as _getParentNode, Q as _int16Range, y as _isPointInArea, q as _limitValue, aE as _longestText, aG as _lookup, w as _lookupByKey, O as _measureText, aJ as _merger, aK as _mergerIf, as as _normalizeAngle, ak as _pointInLine, ae as _readValueToProps, z as _rlookupByKey, az as _setMinAndMaxByKey, ai as _steppedInterpolation, al as _steppedLineTo, av as _textX, X as _toLeftRightCenter, af as _updateBezierControlPoints, ao as addRoundedRectPath, aC as almostEquals, aB as almostWhole, L as callback, aa as clearCanvas, S as clipArea, aI as clone, c as color, a8 as debounce, h as defined, ay as distanceBetweenPoints, an as drawPoint, B as each, e as easingEffects, K as finiteOrDefault, aQ as fontString, n as formatNumber, ad as getAngleFromPoint, aH as getHoverColor, C as getMaximumSize, x as getRelativePosition, at as getRtlAdapter, aP as getStyle, b as isArray, g as isFinite, a2 as isFunction, j as isNullOrUndef, p as isNumber, i as isObject, l as listenArrayEvents, I as log10, $ as merge, a6 as mergeIf, aA as niceNum, ax as noop, au as overrideTextDirection, E as readUsedSize, U as renderText, r as requestAnimFrame, a as resolve, f as resolveObjectKey, aw as restoreTextDirection, a9 as retinaScale, ab as setsEqual, s as sign, aN as splineCurve, aO as splineCurveMonotone, G as supportsEventListenerOptions, F as throttled, N as toDegrees, m as toDimension, W as toFont, aM as toFontString, aR as toLineHeight, A as toPadding, k as toPercentage, t as toRadians, ap as toTRBL, aq as toTRBLCorners, a7 as uid, V as unclipArea, u as unlistenArrayEvents, v as valueOrDefault } from './chunks/helpers.segment.js'; diff --git a/node_modules/chart.js/helpers/helpers.esm.d.ts b/node_modules/chart.js/helpers/helpers.esm.d.ts new file mode 100644 index 000000000..2c3468e72 --- /dev/null +++ b/node_modules/chart.js/helpers/helpers.esm.d.ts @@ -0,0 +1 @@ +export * from '../types/helpers'; diff --git a/node_modules/chart.js/helpers/helpers.esm.js b/node_modules/chart.js/helpers/helpers.esm.js new file mode 100644 index 000000000..ca4eee527 --- /dev/null +++ b/node_modules/chart.js/helpers/helpers.esm.js @@ -0,0 +1 @@ +export * from '../dist/helpers.esm'; diff --git a/node_modules/chart.js/helpers/helpers.js b/node_modules/chart.js/helpers/helpers.js new file mode 100644 index 000000000..a762f589b --- /dev/null +++ b/node_modules/chart.js/helpers/helpers.js @@ -0,0 +1 @@ +module.exports = require('..').helpers; diff --git a/node_modules/chart.js/helpers/package.json b/node_modules/chart.js/helpers/package.json new file mode 100644 index 000000000..d97b75cbf --- /dev/null +++ b/node_modules/chart.js/helpers/package.json @@ -0,0 +1,8 @@ +{ + "name": "chart.js-helpers", + "private": true, + "description": "helper package", + "main": "helpers.js", + "module": "helpers.esm.js", + "types": "helpers.esm.d.ts" +} \ No newline at end of file diff --git a/node_modules/chart.js/package.json b/node_modules/chart.js/package.json new file mode 100644 index 000000000..f675e3689 --- /dev/null +++ b/node_modules/chart.js/package.json @@ -0,0 +1,134 @@ +{ + "_from": "chart.js", + "_id": "chart.js@3.4.1", + "_inBundle": false, + "_integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g==", + "_location": "/chart.js", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "chart.js", + "name": "chart.js", + "escapedName": "chart.js", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz", + "_shasum": "ff3b2b2a04a37b83618b4a6399a5f87ccc0f1e8a", + "_spec": "chart.js", + "_where": "/var/www/matomo/matomo/matomo-for-wordpress", + "bugs": { + "url": "https://github.com/chartjs/Chart.js/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Simple HTML5 charts using the canvas element.", + "devDependencies": { + "@kurkle/color": "^0.1.9", + "@rollup/plugin-commonjs": "^19.0.0", + "@rollup/plugin-inject": "^4.0.2", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^13.0.0", + "@simonbrunel/vuepress-plugin-versions": "^0.2.0", + "@typescript-eslint/eslint-plugin": "^4.21.0", + "@typescript-eslint/parser": "^4.21.0", + "@vuepress/plugin-google-analytics": "1.8.2", + "@vuepress/plugin-html-redirect": "^0.1.2", + "chartjs-adapter-luxon": "^1.0.0", + "chartjs-adapter-moment": "^1.0.0", + "chartjs-test-utils": "^0.3.0", + "concurrently": "^6.0.1", + "coveralls": "^3.1.0", + "cross-env": "^7.0.3", + "eslint": "^7.23.0", + "eslint-config-chartjs": "^0.3.0", + "eslint-plugin-es": "^4.1.0", + "eslint-plugin-html": "^6.1.2", + "eslint-plugin-markdown": "^2.1.0", + "glob": "^7.1.6", + "jasmine": "^3.7.0", + "jasmine-core": "^3.7.1", + "karma": "^6.3.2", + "karma-chrome-launcher": "^3.1.0", + "karma-coverage": "^2.0.3", + "karma-edge-launcher": "^0.4.2", + "karma-firefox-launcher": "^2.1.0", + "karma-jasmine": "^4.0.1", + "karma-jasmine-html-reporter": "^1.5.4", + "karma-rollup-preprocessor": "^7.0.7", + "karma-safari-private-launcher": "^1.0.0", + "karma-spec-reporter": "0.0.32", + "luxon": "^1.26.0", + "markdown-it-include": "^2.0.0", + "moment": "^2.29.1", + "pixelmatch": "^5.2.1", + "rollup": "^2.44.0", + "rollup-plugin-analyzer": "^4.0.0", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-istanbul": "^3.0.0", + "rollup-plugin-terser": "^7.0.2", + "typedoc": "^0.21.2", + "typedoc-plugin-markdown": "^3.6.1", + "typescript": "~4.1.0", + "vue-tabs-component": "^1.5.0", + "vuepress": "^1.8.2", + "vuepress-plugin-code-copy": "^1.0.6", + "vuepress-plugin-flexsearch": "^0.2.0", + "vuepress-plugin-redirect": "^1.2.5", + "vuepress-plugin-tabs": "^0.3.0", + "vuepress-plugin-typedoc": "^0.8.1", + "vuepress-theme-chartjs": "^0.2.0", + "yargs": "^17.0.1" + }, + "files": [ + "auto/**/*.js", + "auto/**/*.d.ts", + "dist/*.js", + "dist/chunks/*.js", + "types/*.d.ts", + "types/helpers/*.d.ts", + "helpers/**/*.js", + "helpers/**/*.d.ts" + ], + "homepage": "https://www.chartjs.org", + "jsdelivr": "dist/chart.min.js", + "keywords": [ + "canvas", + "charts", + "data", + "graphs", + "html5", + "responsive" + ], + "license": "MIT", + "main": "dist/chart.js", + "module": "dist/chart.esm.js", + "name": "chart.js", + "repository": { + "type": "git", + "url": "git+https://github.com/chartjs/Chart.js.git" + }, + "scripts": { + "autobuild": "rollup -c -w", + "build": "rollup -c", + "dev": "karma start --auto-watch --no-single-run --browsers chrome --grep", + "dev:ff": "karma start --auto-watch --no-single-run --browsers firefox --grep", + "docs": "npm run build && vuepress build docs --no-cache", + "docs:dev": "npm run build && vuepress dev docs --no-cache", + "lint": "concurrently \"npm:lint-*\"", + "lint-js": "eslint \"src/**/*.js\" \"test/**/*.js\" \"docs/**/*.js\"", + "lint-md": "eslint \"**/*.md\"", + "lint-tsc": "tsc", + "lint-types": "eslint \"types/**/*.ts\" && tsc -p types/tests/", + "test": "npm run lint && cross-env NODE_ENV=test karma start --auto-watch --single-run --coverage --grep" + }, + "types": "types/index.esm.d.ts", + "unpkg": "dist/chart.min.js", + "version": "3.4.1" +} diff --git a/node_modules/chart.js/types/adapters.d.ts b/node_modules/chart.js/types/adapters.d.ts new file mode 100644 index 000000000..65f48a180 --- /dev/null +++ b/node_modules/chart.js/types/adapters.d.ts @@ -0,0 +1,63 @@ +export type TimeUnit = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; + +export interface DateAdapter { + // Override one or multiple of the methods to adjust to the logic of the current date library. + override(members: Partial): void; + readonly options: any; + + /** + * Returns a map of time formats for the supported formatting units defined + * in Unit as well as 'datetime' representing a detailed date/time string. + * @returns {{string: string}} + */ + formats(): { [key: string]: string }; + /** + * Parses the given `value` and return the associated timestamp. + * @param {any} value - the value to parse (usually comes from the data) + * @param {string} [format] - the expected data format + */ + parse(value: any, format?: TimeUnit): number | null; + /** + * Returns the formatted date in the specified `format` for a given `timestamp`. + * @param {number} timestamp - the timestamp to format + * @param {string} format - the date/time token + * @return {string} + */ + format(timestamp: number, format: TimeUnit): string; + /** + * Adds the specified `amount` of `unit` to the given `timestamp`. + * @param {number} timestamp - the input timestamp + * @param {number} amount - the amount to add + * @param {Unit} unit - the unit as string + * @return {number} + */ + add(timestamp: number, amount: number, unit: TimeUnit): number; + /** + * Returns the number of `unit` between the given timestamps. + * @param {number} a - the input timestamp (reference) + * @param {number} b - the timestamp to subtract + * @param {Unit} unit - the unit as string + * @return {number} + */ + diff(a: number, b: number, unit: TimeUnit): number; + /** + * Returns start of `unit` for the given `timestamp`. + * @param {number} timestamp - the input timestamp + * @param {Unit|'isoWeek'} unit - the unit as string + * @param {number} [weekday] - the ISO day of the week with 1 being Monday + * and 7 being Sunday (only needed if param *unit* is `isoWeek`). + * @return {number} + */ + startOf(timestamp: number, unit: TimeUnit | 'isoWeek', weekday?: number): number; + /** + * Returns end of `unit` for the given `timestamp`. + * @param {number} timestamp - the input timestamp + * @param {Unit|'isoWeek'} unit - the unit as string + * @return {number} + */ + endOf(timestamp: number, unit: TimeUnit | 'isoWeek'): number; +} + +export const _adapters: { + _date: DateAdapter; +}; diff --git a/node_modules/chart.js/types/animation.d.ts b/node_modules/chart.js/types/animation.d.ts new file mode 100644 index 000000000..8eff8baac --- /dev/null +++ b/node_modules/chart.js/types/animation.d.ts @@ -0,0 +1,32 @@ +import { Chart } from './index.esm'; +import { AnyObject } from './basic'; + +export class Animation { + constructor(cfg: AnyObject, target: AnyObject, prop: string, to?: unknown); + active(): boolean; + update(cfg: AnyObject, to: unknown, date: number): void; + cancel(): void; + tick(date: number): void; +} + +export interface AnimationEvent { + chart: Chart; + numSteps: number; + currentState: number; +} + +export class Animator { + listen(chart: Chart, event: 'complete' | 'progress', cb: (event: AnimationEvent) => void): void; + add(chart: Chart, items: readonly Animation[]): void; + has(chart: Chart): boolean; + start(chart: Chart): void; + running(chart: Chart): boolean; + stop(chart: Chart): void; + remove(chart: Chart): boolean; +} + +export class Animations { + constructor(chart: Chart, animations: AnyObject); + configure(animations: AnyObject): void; + update(target: AnyObject, values: AnyObject): undefined | boolean; +} diff --git a/node_modules/chart.js/types/basic.d.ts b/node_modules/chart.js/types/basic.d.ts new file mode 100644 index 000000000..1692c9cb3 --- /dev/null +++ b/node_modules/chart.js/types/basic.d.ts @@ -0,0 +1,3 @@ + +export type AnyObject = Record; +export type EmptyObject = Record; diff --git a/node_modules/chart.js/types/color.d.ts b/node_modules/chart.js/types/color.d.ts new file mode 100644 index 000000000..4a68f98bb --- /dev/null +++ b/node_modules/chart.js/types/color.d.ts @@ -0,0 +1 @@ +export type Color = string | CanvasGradient | CanvasPattern; diff --git a/node_modules/chart.js/types/element.d.ts b/node_modules/chart.js/types/element.d.ts new file mode 100644 index 000000000..46ffa781e --- /dev/null +++ b/node_modules/chart.js/types/element.d.ts @@ -0,0 +1,30 @@ +import { Point } from './geometric'; + +export interface Element { + readonly x: number; + readonly y: number; + readonly active: boolean; + readonly options: O; + + tooltipPosition(useFinalPosition?: boolean): Point; + hasValue(): boolean; + getProps

    (props: [P], final?: boolean): Pick; + getProps

    (props: [P, P2], final?: boolean): Pick; + getProps

    ( + props: [P, P2, P3], + final?: boolean + ): Pick; + getProps

    ( + props: [P, P2, P3, P4], + final?: boolean + ): Pick; + getProps

    ( + props: [P, P2, P3, P4, P5], + final?: boolean + ): Pick; + getProps(props: (keyof T)[], final?: boolean): T; +} +export const Element: { + prototype: Element; + new (): Element; +}; diff --git a/node_modules/chart.js/types/geometric.d.ts b/node_modules/chart.js/types/geometric.d.ts new file mode 100644 index 000000000..3677668a6 --- /dev/null +++ b/node_modules/chart.js/types/geometric.d.ts @@ -0,0 +1,13 @@ +export interface ChartArea { + top: number; + left: number; + right: number; + bottom: number; + width: number; + height: number; +} + +export interface Point { + x: number; + y: number; +} diff --git a/node_modules/chart.js/types/helpers/helpers.canvas.d.ts b/node_modules/chart.js/types/helpers/helpers.canvas.d.ts new file mode 100644 index 000000000..2d889df9f --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.canvas.d.ts @@ -0,0 +1,99 @@ +import { PointStyle } from '../index.esm'; +import { Color } from '../color'; +import { ChartArea } from '../geometric'; +import { CanvasFontSpec } from './helpers.options'; + +export function clearCanvas(canvas: HTMLCanvasElement, ctx?: CanvasRenderingContext2D): void; + +export function clipArea(ctx: CanvasRenderingContext2D, area: ChartArea): void; + +export function unclipArea(ctx: CanvasRenderingContext2D): void; + +export interface DrawPointOptions { + pointStyle: PointStyle; + rotation?: number; + radius: number; + borderWidth: number; +} + +export function drawPoint(ctx: CanvasRenderingContext2D, options: DrawPointOptions, x: number, y: number): void; + +/** + * Converts the given font object into a CSS font string. + * @param font a font object + * @return The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font + */ +export function toFontString(font: { size: number; family: string; style?: string; weight?: string }): string | null; + +export interface RenderTextOpts { + /** + * The fill color of the text. If unset, the existing + * fillStyle property of the canvas is unchanged. + */ + color?: Color; + + /** + * The width of the strikethrough / underline + * @default 2 + */ + decorationWidth?: number; + + /** + * The max width of the text in pixels + */ + maxWidth?: number; + + /** + * A rotation to be applied to the canvas + * This is applied after the translation is applied + */ + rotation?: number; + + /** + * Apply a strikethrough effect to the text + */ + strikethrough?: boolean; + + /** + * The color of the text stroke. If unset, the existing + * strokeStyle property of the context is unchanged + */ + strokeColor?: Color; + + /** + * The text stroke width. If unset, the existing + * lineWidth property of the context is unchanged + */ + strokeWidth?: number; + + /** + * The text alignment to use. If unset, the existing + * textAlign property of the context is unchanged + */ + textAlign: CanvasTextAlign; + + /** + * The text baseline to use. If unset, the existing + * textBaseline property of the context is unchanged + */ + textBaseline: CanvasTextBaseline; + + /** + * If specified, a translation to apply to the context + */ + translation?: [number, number]; + + /** + * Underline the text + */ + underline?: boolean; +} + +export function renderText( + ctx: CanvasRenderingContext2D, + text: string | string[], + x: number, + y: number, + font: CanvasFontSpec, + opts?: RenderTextOpts +): void; diff --git a/node_modules/chart.js/types/helpers/helpers.collection.d.ts b/node_modules/chart.js/types/helpers/helpers.collection.d.ts new file mode 100644 index 000000000..a617a2b48 --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.collection.d.ts @@ -0,0 +1,20 @@ +export interface ArrayListener { + _onDataPush?(...item: T[]): void; + _onDataPop?(): void; + _onDataShift?(): void; + _onDataSplice?(index: number, deleteCount: number, ...items: T[]): void; + _onDataUnshift?(...item: T[]): void; +} + +/** + * Hooks the array methods that add or remove values ('push', pop', 'shift', 'splice', + * 'unshift') and notify the listener AFTER the array has been altered. Listeners are + * called on the '_onData*' callbacks (e.g. _onDataPush, etc.) with same arguments. + */ +export function listenArrayEvents(array: T[], listener: ArrayListener): void; + +/** + * Removes the given array event listener and cleanup extra attached properties (such as + * the _chartjs stub and overridden methods) if array doesn't have any more listeners. + */ +export function unlistenArrayEvents(array: T[], listener: ArrayListener): void; diff --git a/node_modules/chart.js/types/helpers/helpers.color.d.ts b/node_modules/chart.js/types/helpers/helpers.color.d.ts new file mode 100644 index 000000000..1f6282e52 --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.color.d.ts @@ -0,0 +1,33 @@ +export function color(value: CanvasGradient): CanvasGradient; +export function color(value: CanvasPattern): CanvasPattern; + +export interface ColorModel { + rgbString(): string; + hexString(): string; + hslString(): string; + rgb: { r: number; g: number; b: number; a: number }; + valid: boolean; + mix(color: ColorModel, weight: number): this; + clone(): ColorModel; + alpha(a: number): ColorModel; + clearer(ration: number): ColorModel; + greyscale(): ColorModel; + opaquer(ratio: number): ColorModel; + negate(): ColorModel; + lighten(ratio: number): ColorModel; + darken(ratio: number): ColorModel; + saturate(ratio: number): ColorModel; + desaturate(ratio: number): ColorModel; + rotate(deg: number): this; +} +export function color( + value: + | string + | { r: number; g: number; b: number; a: number } + | [number, number, number] + | [number, number, number, number] +): ColorModel; + +export function getHoverColor(value: CanvasGradient): CanvasGradient; +export function getHoverColor(value: CanvasPattern): CanvasPattern; +export function getHoverColor(value: string): string; diff --git a/node_modules/chart.js/types/helpers/helpers.core.d.ts b/node_modules/chart.js/types/helpers/helpers.core.d.ts new file mode 100644 index 000000000..f09280906 --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.core.d.ts @@ -0,0 +1,140 @@ +import { AnyObject } from '../basic'; + +/** + * An empty function that can be used, for example, for optional callback. + */ +export function noop(): void; + +/** + * Returns a unique id, sequentially generated from a global variable. + * @returns {number} + * @function + */ +export function uid(): number; +/** + * Returns true if `value` is neither null nor undefined, else returns false. + * @param {*} value - The value to test. + * @returns {boolean} + * @since 2.7.0 + */ +export function isNullOrUndef(value: unknown): value is null | undefined; +/** + * Returns true if `value` is an array (including typed arrays), else returns false. + * @param {*} value - The value to test. + * @returns {boolean} + * @function + */ +export function isArray(value: unknown): value is ArrayLike; +/** + * Returns true if `value` is an object (excluding null), else returns false. + * @param {*} value - The value to test. + * @returns {boolean} + * @since 2.7.0 + */ +export function isObject(value: unknown): value is AnyObject; +/** + * Returns true if `value` is a finite number, else returns false + * @param {*} value - The value to test. + * @returns {boolean} + */ +export function isFinite(value: unknown): value is number; +/** + * Returns `value` if defined, else returns `defaultValue`. + * @param {*} value - The value to return if defined. + * @param {*} defaultValue - The value to return if `value` is undefined. + * @returns {*} + */ +export function valueOrDefault(value: T | undefined, defaultValue: T): T; +/** + * Calls `fn` with the given `args` in the scope defined by `thisArg` and returns the + * value returned by `fn`. If `fn` is not a function, this method returns undefined. + * @param fn - The function to call. + * @param args - The arguments with which `fn` should be called. + * @param [thisArg] - The value of `this` provided for the call to `fn`. + * @returns {*} + */ +export function callback R, TA, R>( + fn: T | undefined, + args: unknown[], + thisArg?: TA +): R | undefined; + +/** + * Note(SB) for performance sake, this method should only be used when loopable type + * is unknown or in none intensive code (not called often and small loopable). Else + * it's preferable to use a regular for() loop and save extra function calls. + * @param loopable - The object or array to be iterated. + * @param fn - The function to call for each item. + * @param [thisArg] - The value of `this` provided for the call to `fn`. + * @param [reverse] - If true, iterates backward on the loopable. + */ +export function each( + loopable: T[], + fn: (this: TA, v: T, i: number) => void, + thisArg?: TA, + reverse?: boolean +): void; +/** + * Note(SB) for performance sake, this method should only be used when loopable type + * is unknown or in none intensive code (not called often and small loopable). Else + * it's preferable to use a regular for() loop and save extra function calls. + * @param loopable - The object or array to be iterated. + * @param fn - The function to call for each item. + * @param [thisArg] - The value of `this` provided for the call to `fn`. + * @param [reverse] - If true, iterates backward on the loopable. + */ +export function each( + loopable: { [key: string]: T }, + fn: (this: TA, v: T, k: string) => void, + thisArg?: TA, + reverse?: boolean +): void; + +/** + * Returns a deep copy of `source` without keeping references on objects and arrays. + * @param source - The value to clone. + */ +export function clone(source: T): T; + +export interface MergeOptions { + merger?: (key: string, target: AnyObject, source: AnyObject, options: AnyObject) => AnyObject; +} +/** + * Recursively deep copies `source` properties into `target` with the given `options`. + * IMPORTANT: `target` is not cloned and will be updated with `source` properties. + * @param target - The target object in which all sources are merged into. + * @param source - Object(s) to merge into `target`. + * @param {object} [options] - Merging options: + * @param {function} [options.merger] - The merge method (key, target, source, options) + * @returns {object} The `target` object. + */ +export function merge(target: T, source: [], options?: MergeOptions): T; +export function merge(target: T, source: S1, options?: MergeOptions): T & S1; +export function merge(target: T, source: [S1], options?: MergeOptions): T & S1; +export function merge(target: T, source: [S1, S2], options?: MergeOptions): T & S1 & S2; +export function merge(target: T, source: [S1, S2, S3], options?: MergeOptions): T & S1 & S2 & S3; +export function merge( + target: T, + source: [S1, S2, S3, S4], + options?: MergeOptions +): T & S1 & S2 & S3 & S4; +export function merge(target: T, source: AnyObject[], options?: MergeOptions): AnyObject; + +/** + * Recursively deep copies `source` properties into `target` *only* if not defined in target. + * IMPORTANT: `target` is not cloned and will be updated with `source` properties. + * @param target - The target object in which all sources are merged into. + * @param source - Object(s) to merge into `target`. + * @returns The `target` object. + */ +export function mergeIf(target: T, source: []): T; +export function mergeIf(target: T, source: S1): T & S1; +export function mergeIf(target: T, source: [S1]): T & S1; +export function mergeIf(target: T, source: [S1, S2]): T & S1 & S2; +export function mergeIf(target: T, source: [S1, S2, S3]): T & S1 & S2 & S3; +export function mergeIf(target: T, source: [S1, S2, S3, S4]): T & S1 & S2 & S3 & S4; +export function mergeIf(target: T, source: AnyObject[]): AnyObject; + +export function resolveObjectKey(obj: AnyObject, key: string): AnyObject; + +export function setsEqual(a: Set, b: Set): boolean; diff --git a/node_modules/chart.js/types/helpers/helpers.curve.d.ts b/node_modules/chart.js/types/helpers/helpers.curve.d.ts new file mode 100644 index 000000000..d845e1579 --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.curve.d.ts @@ -0,0 +1,34 @@ +export interface SplinePoint { + x: number; + y: number; +} + +/** + * Props to Rob Spencer at scaled innovation for his post on splining between points + * http://scaledinnovation.com/analytics/splines/aboutSplines.html + */ +export function splineCurve( + firstPoint: SplinePoint & { skip?: boolean }, + middlePoint: SplinePoint, + afterPoint: SplinePoint, + t: number +): { + previous: SplinePoint; + next: SplinePoint; +}; + +export interface MonotoneSplinePoint extends SplinePoint { + skip: boolean; + cp1x?: number; + cp1y?: number; + cp2x?: number; + cp2y?: number; +} + +/** + * This function calculates Bézier control points in a similar way than |splineCurve|, + * but preserves monotonicity of the provided data and ensures no local extremums are added + * between the dataset discrete points due to the interpolation. + * @see https://en.wikipedia.org/wiki/Monotone_cubic_interpolation + */ +export function splineCurveMonotone(points: readonly MonotoneSplinePoint[], indexAxis?: 'x' | 'y'): void; diff --git a/node_modules/chart.js/types/helpers/helpers.dom.d.ts b/node_modules/chart.js/types/helpers/helpers.dom.d.ts new file mode 100644 index 000000000..ba438d608 --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.dom.d.ts @@ -0,0 +1,17 @@ +export function getMaximumSize(node: HTMLElement, width?: number, height?: number, aspectRatio?: number): { width: number, height: number }; +export function getRelativePosition( + evt: MouseEvent, + chart: { readonly canvas: HTMLCanvasElement } +): { x: number; y: number }; +export function getStyle(el: HTMLElement, property: string): string; +export function retinaScale( + chart: { + currentDevicePixelRatio: number; + readonly canvas: HTMLCanvasElement; + readonly width: number; + readonly height: number; + readonly ctx: CanvasRenderingContext2D; + }, + forceRatio: number, + forceStyle?: boolean +): void; diff --git a/node_modules/chart.js/types/helpers/helpers.easing.d.ts b/node_modules/chart.js/types/helpers/helpers.easing.d.ts new file mode 100644 index 000000000..b86d6532a --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.easing.d.ts @@ -0,0 +1,5 @@ +import { EasingFunction } from '../index.esm'; + +export type EasingFunctionSignature = (t: number) => number; + +export const easingEffects: Record; diff --git a/node_modules/chart.js/types/helpers/helpers.extras.d.ts b/node_modules/chart.js/types/helpers/helpers.extras.d.ts new file mode 100644 index 000000000..d3ddc784e --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.extras.d.ts @@ -0,0 +1,23 @@ +export function fontString(pixelSize: number, fontStyle: string, fontFamily: string): string; + +/** + * Request animation polyfill + */ +export function requestAnimFrame(cb: () => void): void; + +/** + * Throttles calling `fn` once per animation frame + * Latest argments are used on the actual call + * @param {function} fn + * @param {*} thisArg + * @param {function} [updateFn] + */ +export function throttled(fn: (...args: any[]) => void, thisArg: any, updateFn?: (...args: any[]) => any[]): (...args: any[]) => void; + +/** + * Debounces calling `fn` for `delay` ms + * @param {function} fn - Function to call. No arguments are passed. + * @param {number} delay - Delay in ms. 0 = immediate invocation. + * @returns {function} + */ +export function debounce(fn: () => void, delay: number): () => number; diff --git a/node_modules/chart.js/types/helpers/helpers.interpolation.d.ts b/node_modules/chart.js/types/helpers/helpers.interpolation.d.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.interpolation.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/node_modules/chart.js/types/helpers/helpers.intl.d.ts b/node_modules/chart.js/types/helpers/helpers.intl.d.ts new file mode 100644 index 000000000..3a896f4ad --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.intl.d.ts @@ -0,0 +1,7 @@ +/** + * Format a number using a localized number formatter. + * @param num The number to format + * @param locale The locale to pass to the Intl.NumberFormat constructor + * @param options Number format options + */ +export function formatNumber(num: number, locale: string, options: Intl.NumberFormatOptions): string; diff --git a/node_modules/chart.js/types/helpers/helpers.math.d.ts b/node_modules/chart.js/types/helpers/helpers.math.d.ts new file mode 100644 index 000000000..35cb65328 --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.math.d.ts @@ -0,0 +1,16 @@ +export function log10(x: number): number; +export function isNumber(v: any): boolean; +export function almostEquals(x: number, y: number, epsilon: number): boolean; +export function almostWhole(x: number, epsilon: number): number; +export function sign(x: number): number; +export function toRadians(degrees: number): number; +export function toDegrees(radians: number): number; +/** + * Gets the angle from vertical upright to the point about a centre. + */ +export function getAngleFromPoint( + centrePoint: { x: number; y: number }, + anglePoint: { x: number; y: number } +): { angle: number; distance: number }; + +export function distanceBetweenPoints(pt1: { x: number; y: number }, pt2: { x: number; y: number }): number; diff --git a/node_modules/chart.js/types/helpers/helpers.options.d.ts b/node_modules/chart.js/types/helpers/helpers.options.d.ts new file mode 100644 index 000000000..c98404d1c --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.options.d.ts @@ -0,0 +1,50 @@ +import { FontSpec } from '../index.esm'; + +export interface CanvasFontSpec extends FontSpec { + string: string; +} +/** + * Parses font options and returns the font object. + * @param {object} options - A object that contains font options to be parsed. + * @return {object} The font object. + */ +export function toFont(options: Partial): CanvasFontSpec; + +/** + * Converts the given line height `value` in pixels for a specific font `size`. + * @param {number|string} value - The lineHeight to parse (eg. 1.6, '14px', '75%', '1.6em'). + * @param {number} size - The font size (in pixels) used to resolve relative `value`. + * @returns {number} The effective line height in pixels (size * 1.2 if value is invalid). + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/line-height + * @since 2.7.0 + */ +export function toLineHeight(value: string, size: number): number; + +/** + * Converts the given value into a padding object with pre-computed width/height. + * @param {number|object} value - If a number, set the value to all TRBL component; + * else, if an object, use defined properties and sets undefined ones to 0. + * @returns {object} The padding values (top, right, bottom, left, width, height) + * @since 2.7.0 + */ +export function toPadding( + value?: number | { top?: number; left?: number; right?: number; bottom?: number; x?:number, y?: number } +): { top: number; left: number; right: number; bottom: number; width: number; height: number }; + +/** + * Evaluates the given `inputs` sequentially and returns the first defined value. + * @param inputs - An array of values, falling back to the last value. + * @param [context] - If defined and the current value is a function, the value + * is called with `context` as first argument and the result becomes the new input. + * @param [index] - If defined and the current value is an array, the value + * at `index` become the new input. + * @param [info] - object to return information about resolution in + * @param [info.cacheable] - Will be set to `false` if option is not cacheable. + * @since 2.7.0 + */ +export function resolve( + inputs: undefined | T | ((c: C) => T) | readonly T[], + context?: C, + index?: number, + info?: { cacheable?: boolean } +): T | undefined; diff --git a/node_modules/chart.js/types/helpers/helpers.rtl.d.ts b/node_modules/chart.js/types/helpers/helpers.rtl.d.ts new file mode 100644 index 000000000..f366105bb --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.rtl.d.ts @@ -0,0 +1,12 @@ +export interface RTLAdapter { + x(x: number): number; + setWidth(w: number): void; + textAlign(align: 'center' | 'left' | 'right'): 'center' | 'left' | 'right'; + xPlus(x: number, value: number): number; + leftForLtr(x: number, itemWidth: number): number; +} +export function getRtlAdapter(rtl: boolean, rectX: number, width: number): RTLAdapter; + +export function overrideTextDirection(ctx: CanvasRenderingContext2D, direction: 'ltr' | 'rtl'): void; + +export function restoreTextDirection(ctx: CanvasRenderingContext2D, original?: [string, string]): void; diff --git a/node_modules/chart.js/types/helpers/helpers.segment.d.ts b/node_modules/chart.js/types/helpers/helpers.segment.d.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/node_modules/chart.js/types/helpers/helpers.segment.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/node_modules/chart.js/types/helpers/index.d.ts b/node_modules/chart.js/types/helpers/index.d.ts new file mode 100644 index 000000000..013326924 --- /dev/null +++ b/node_modules/chart.js/types/helpers/index.d.ts @@ -0,0 +1,15 @@ +export * from './helpers.canvas'; +export * from './helpers.collection'; +export * from './helpers.color'; +export * from './helpers.core'; +export * from './helpers.curve'; +export * from './helpers.dom'; +export * from './helpers.easing'; +export * from './helpers.extras'; +export * from './helpers.interpolation'; +export * from './helpers.intl'; +export * from './helpers.math'; +export * from './helpers.options'; +export * from './helpers.canvas'; +export * from './helpers.rtl'; +export * from './helpers.segment'; diff --git a/node_modules/chart.js/types/index.esm.d.ts b/node_modules/chart.js/types/index.esm.d.ts new file mode 100644 index 000000000..e56376fae --- /dev/null +++ b/node_modules/chart.js/types/index.esm.d.ts @@ -0,0 +1,3421 @@ +import { DeepPartial, DistributiveArray, UnionToIntersection } from './utils'; + +import { TimeUnit } from './adapters'; +import { AnimationEvent } from './animation'; +import { AnyObject, EmptyObject } from './basic'; +import { Color } from './color'; +import { Element } from './element'; +import { ChartArea, Point } from './geometric'; +import { LayoutItem, LayoutPosition } from './layout'; + +export { DateAdapter, TimeUnit, _adapters } from './adapters'; +export { Animation, Animations, Animator, AnimationEvent } from './animation'; +export { Color } from './color'; +export { Element } from './element'; +export { ChartArea, Point } from './geometric'; +export { LayoutItem, LayoutPosition } from './layout'; + +export interface ScriptableContext { + active: boolean; + chart: UnionToIntersection>; + dataIndex: number; + dataset: UnionToIntersection>; + datasetIndex: number; + parsed: UnionToIntersection>; + raw: unknown; +} + +export interface ScriptableLineSegmentContext { + type: 'segment', + p0: PointElement, + p1: PointElement +} + +export type Scriptable = T | ((ctx: TContext, options: AnyObject) => T); +export type ScriptableOptions = { [P in keyof T]: Scriptable }; +export type ScriptableAndArray = readonly T[] | Scriptable; +export type ScriptableAndArrayOptions = { [P in keyof T]: ScriptableAndArray }; + +export interface ParsingOptions { + /** + * How to parse the dataset. The parsing can be disabled by specifying parsing: false at chart options or dataset. If parsing is disabled, data must be sorted and in the formats the associated chart type and scales use internally. + */ + parsing: + { + [key: string]: string; + } + | false; + + /** + * Chart.js is fastest if you provide data with indices that are unique, sorted, and consistent across datasets and provide the normalized: true option to let Chart.js know that you have done so. + */ + normalized: boolean; +} + +export interface ControllerDatasetOptions extends ParsingOptions { + /** + * The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. + * @default 'x' + */ + indexAxis: 'x' | 'y'; + /** + * How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. 0 = clip at chartArea. Clipping can also be configured per side: clip: {left: 5, top: false, right: -2, bottom: 0} + */ + clip: number | ChartArea; + /** + * The label for the dataset which appears in the legend and tooltips. + */ + label: string; + /** + * The drawing order of dataset. Also affects order for stacking, tooltip and legend. + */ + order: number; + + /** + * The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). + */ + stack: string; + /** + * Configures the visibility state of the dataset. Set it to true, to hide the dataset from the chart. + * @default false + */ + hidden: boolean; +} + +export interface BarControllerDatasetOptions + extends ControllerDatasetOptions, + ScriptableAndArrayOptions>, + ScriptableAndArrayOptions>, + AnimationOptions<'bar'> { + /** + * The ID of the x axis to plot this dataset on. + */ + xAxisID: string; + /** + * The ID of the y axis to plot this dataset on. + */ + yAxisID: string; + + /** + * Percent (0-1) of the available width each bar should be within the category width. 1.0 will take the whole category width and put the bars right next to each other. + * @default 0.9 + */ + barPercentage: number; + /** + * Percent (0-1) of the available width each category should be within the sample width. + * @default 0.8 + */ + categoryPercentage: number; + + /** + * Manually set width of each bar in pixels. If set to 'flex', it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. + */ + barThickness: number | 'flex'; + + /** + * Set this to ensure that bars are not sized thicker than this. + */ + maxBarThickness: number; + + /** + * Set this to ensure that bars have a minimum length in pixels. + */ + minBarLength: number; + + /** + * Point style for the legend + * @default 'circle; + */ + pointStyle: PointStyle; +} + +export interface BarControllerChartOptions { + /** + * Should null or undefined values be omitted from drawing + */ + skipNull?: boolean; +} + +export type BarController = DatasetController +export const BarController: ChartComponent & { + prototype: BarController; + new (chart: Chart, datasetIndex: number): BarController; +}; + +export interface BubbleControllerDatasetOptions + extends ControllerDatasetOptions, + ScriptableAndArrayOptions>, + ScriptableAndArrayOptions> {} + +export interface BubbleDataPoint { + /** + * X Value + */ + x: number; + + /** + * Y Value + */ + y: number; + + /** + * Bubble radius in pixels (not scaled). + */ + r: number; +} + +export type BubbleController = DatasetController +export const BubbleController: ChartComponent & { + prototype: BubbleController; + new (chart: Chart, datasetIndex: number): BubbleController; +}; + +export interface LineControllerDatasetOptions + extends ControllerDatasetOptions, + ScriptableAndArrayOptions>, + ScriptableAndArrayOptions>, + ScriptableOptions>, + ScriptableOptions>, + AnimationOptions<'line'> { + /** + * The ID of the x axis to plot this dataset on. + */ + xAxisID: string; + /** + * The ID of the y axis to plot this dataset on. + */ + yAxisID: string; + + /** + * If true, lines will be drawn between points with no or null data. If false, points with NaN data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. + * @default false + */ + spanGaps: boolean | number; + + showLine: boolean; +} + +export interface LineControllerChartOptions { + /** + * If true, lines will be drawn between points with no or null data. If false, points with NaN data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. + * @default false + */ + spanGaps: boolean | number; + /** + * If false, the lines between points are not drawn. + * @default true + */ + showLine: boolean; +} + +export type LineController = DatasetController +export const LineController: ChartComponent & { + prototype: LineController; + new (chart: Chart, datasetIndex: number): LineController; +}; + +export type ScatterControllerDatasetOptions = LineControllerDatasetOptions; + +export interface ScatterDataPoint { + x: number; + y: number; +} + +export type ScatterControllerChartOptions = LineControllerChartOptions; + +export type ScatterController = LineController +export const ScatterController: ChartComponent & { + prototype: ScatterController; + new (chart: Chart, datasetIndex: number): ScatterController; +}; + +export interface DoughnutControllerDatasetOptions + extends ControllerDatasetOptions, + ScriptableAndArrayOptions>, + ScriptableAndArrayOptions>, + AnimationOptions<'doughnut'> { + + /** + * Sweep to allow arcs to cover. + * @default 360 + */ + circumference: number; + + /** + * Starting angle to draw this dataset from. + * @default 0 + */ + rotation: number; + + /** + * The relative thickness of the dataset. Providing a value for weight will cause the pie or doughnut dataset to be drawn with a thickness relative to the sum of all the dataset weight values. + * @default 1 + */ + weight: number; + + /** + * Similar to the `offset` option, but applies to all arcs. This can be used to to add spaces + * between arcs + * @default 0 + */ + spacing: number; +} + +export interface DoughnutAnimationOptions { + /** + * If true, the chart will animate in with a rotation animation. This property is in the options.animation object. + * @default true + */ + animateRotate: boolean; + + /** + * If true, will animate scaling the chart from the center outwards. + * @default false + */ + animateScale: boolean; +} + +export interface DoughnutControllerChartOptions { + /** + * Sweep to allow arcs to cover. + * @default 360 + */ + circumference: number; + + /** + * The portion of the chart that is cut out of the middle. ('50%' - for doughnut, 0 - for pie) + * String ending with '%' means percentage, number means pixels. + * @default 50 + */ + cutout: Scriptable>; + + /** + * The outer radius of the chart. String ending with '%' means percentage of maximum radius, number means pixels. + * @default '100%' + */ + radius: Scriptable>; + + /** + * Starting angle to draw arcs from. + * @default 0 + */ + rotation: number; + + /** + * Spacing between the arcs + * @default 0 + */ + spacing: number; + + animation: DoughnutAnimationOptions; +} + +export type DoughnutDataPoint = number; + +export interface DoughnutController extends DatasetController { + readonly innerRadius: number; + readonly outerRadius: number; + readonly offsetX: number; + readonly offsetY: number; + + calculateTotal(): number; + calculateCircumference(value: number): number; +} + +export const DoughnutController: ChartComponent & { + prototype: DoughnutController; + new (chart: Chart, datasetIndex: number): DoughnutController; +}; + +export type PieControllerDatasetOptions = DoughnutControllerDatasetOptions; +export type PieControllerChartOptions = DoughnutControllerChartOptions; +export type PieAnimationOptions = DoughnutAnimationOptions; + +export type PieDataPoint = DoughnutDataPoint; + +export type PieController = DoughnutController +export const PieController: ChartComponent & { + prototype: PieController; + new (chart: Chart, datasetIndex: number): PieController; +}; + +export interface PolarAreaControllerDatasetOptions extends DoughnutControllerDatasetOptions { + /** + * Arc angle to cover. - for polar only + * @default circumference / (arc count) + */ + angle: number; +} + +export type PolarAreaAnimationOptions = DoughnutAnimationOptions; + +export interface PolarAreaControllerChartOptions { + /** + * Starting angle to draw arcs for the first item in a dataset. In degrees, 0 is at top. + * @default 0 + */ + startAngle: number; + + animation: PolarAreaAnimationOptions; +} + +export interface PolarAreaController extends DoughnutController { + countVisibleElements(): number; +} +export const PolarAreaController: ChartComponent & { + prototype: PolarAreaController; + new (chart: Chart, datasetIndex: number): PolarAreaController; +}; + +export interface RadarControllerDatasetOptions + extends ControllerDatasetOptions, + ScriptableOptions>, + ScriptableOptions>, + ScriptableOptions>, + ScriptableOptions>, + AnimationOptions<'radar'> { + /** + * The ID of the x axis to plot this dataset on. + */ + xAxisID: string; + /** + * The ID of the y axis to plot this dataset on. + */ + yAxisID: string; + + /** + * If true, lines will be drawn between points with no or null data. If false, points with NaN data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. + */ + spanGaps: boolean | number; + + /** + * If false, the line is not drawn for this dataset. + */ + showLine: boolean; +} + +export type RadarControllerChartOptions = LineControllerChartOptions; + +export type RadarController = DatasetController +export const RadarController: ChartComponent & { + prototype: RadarController; + new (chart: Chart, datasetIndex: number): RadarController; +}; +export interface ChartMeta { + type: string; + controller: DatasetController; + order: number; + + label: string; + index: number; + visible: boolean; + + stack: number; + + indexAxis: 'x' | 'y'; + + data: TElement[]; + dataset?: TDatasetElement; + + hidden: boolean; + + xAxisID?: string; + yAxisID?: string; + rAxisID?: string; + iAxisID: string; + vAxisID: string; + + xScale?: Scale; + yScale?: Scale; + rScale?: Scale; + iScale?: Scale; + vScale?: Scale; + + _sorted: boolean; + _stacked: boolean | 'single'; + _parsed: unknown[]; +} + +export interface ActiveDataPoint { + datasetIndex: number; + index: number; +} + +export interface ActiveElement extends ActiveDataPoint { + element: Element; +} + +export declare class Chart< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown + > { + readonly platform: BasePlatform; + readonly id: string; + readonly canvas: HTMLCanvasElement; + readonly ctx: CanvasRenderingContext2D; + readonly config: ChartConfiguration + readonly width: number; + readonly height: number; + readonly aspectRatio: number; + readonly boxes: LayoutItem[]; + readonly currentDevicePixelRatio: number; + readonly chartArea: ChartArea; + readonly scales: { [key: string]: Scale }; + readonly attached: boolean; + + data: ChartData; + options: ChartOptions; + + constructor(item: ChartItem, config: ChartConfiguration); + + clear(): this; + stop(): this; + + resize(width?: number, height?: number): void; + ensureScalesHaveIDs(): void; + buildOrUpdateScales(): void; + buildOrUpdateControllers(): void; + reset(): void; + update(mode?: UpdateMode): void; + render(): void; + draw(): void; + + getElementsAtEventForMode(e: Event, mode: string, options: InteractionOptions, useFinalPosition: boolean): InteractionItem[]; + + getSortedVisibleDatasetMetas(): ChartMeta[]; + getDatasetMeta(datasetIndex: number): ChartMeta; + getVisibleDatasetCount(): number; + isDatasetVisible(datasetIndex: number): boolean; + setDatasetVisibility(datasetIndex: number, visible: boolean): void; + toggleDataVisibility(index: number): void; + getDataVisibility(index: number): boolean; + hide(datasetIndex: number): void; + show(datasetIndex: number): void; + + getActiveElements(): ActiveElement[]; + setActiveElements(active: ActiveDataPoint[]): void; + + destroy(): void; + toBase64Image(type?: string, quality?: unknown): string; + bindEvents(): void; + unbindEvents(): void; + updateHoverStyle(items: Element, mode: 'dataset', enabled: boolean): void; + + notifyPlugins(hook: string, args?: AnyObject): boolean | void; + + static readonly defaults: Defaults; + static readonly overrides: Overrides; + static readonly version: string; + static readonly instances: { [key: string]: Chart }; + static readonly registry: Registry; + static getChart(key: string | CanvasRenderingContext2D | HTMLCanvasElement): Chart | undefined; + static register(...items: ChartComponentLike[]): void; + static unregister(...items: ChartComponentLike[]): void; +} + +export const registerables: readonly ChartComponentLike[]; + +export declare type ChartItem = + | string + | CanvasRenderingContext2D + | OffscreenCanvasRenderingContext2D + | HTMLCanvasElement + | OffscreenCanvas + | { canvas: HTMLCanvasElement | OffscreenCanvas } + | ArrayLike; + +export declare enum UpdateModeEnum { + resize = 'resize', + reset = 'reset', + none = 'none', + hide = 'hide', + show = 'show', + normal = 'normal', + active = 'active' +} + +export type UpdateMode = keyof typeof UpdateModeEnum; + +export class DatasetController< + TType extends ChartType = ChartType, + TElement extends Element = Element, + TDatasetElement extends Element = Element, + TParsedData = ParsedDataType, +> { + constructor(chart: Chart, datasetIndex: number); + + readonly chart: Chart; + readonly index: number; + readonly _cachedMeta: ChartMeta; + enableOptionSharing: boolean; + + linkScales(): void; + getAllParsedValues(scale: Scale): number[]; + protected getLabelAndValue(index: number): { label: string; value: string }; + updateElements(elements: TElement[], start: number, count: number, mode: UpdateMode): void; + update(mode: UpdateMode): void; + updateIndex(datasetIndex: number): void; + protected getMaxOverflow(): boolean | number; + draw(): void; + reset(): void; + getDataset(): ChartDataset; + getMeta(): ChartMeta; + getScaleForId(scaleID: string): Scale | undefined; + configure(): void; + initialize(): void; + addElements(): void; + buildOrUpdateElements(resetNewElements?: boolean): void; + + getStyle(index: number, active: boolean): AnyObject; + protected resolveDatasetElementOptions(mode: UpdateMode): AnyObject; + protected resolveDataElementOptions(index: number, mode: UpdateMode): AnyObject; + /** + * Utility for checking if the options are shared and should be animated separately. + * @protected + */ + protected getSharedOptions(options: AnyObject): undefined | AnyObject; + /** + * Utility for determining if `options` should be included in the updated properties + * @protected + */ + protected includeOptions(mode: UpdateMode, sharedOptions: AnyObject): boolean; + /** + * Utility for updating an element with new properties, using animations when appropriate. + * @protected + */ + + protected updateElement(element: TElement | TDatasetElement, index: number | undefined, properties: AnyObject, mode: UpdateMode): void; + /** + * Utility to animate the shared options, that are potentially affecting multiple elements. + * @protected + */ + + protected updateSharedOptions(sharedOptions: AnyObject, mode: UpdateMode, newOptions: AnyObject): void; + removeHoverStyle(element: TElement, datasetIndex: number, index: number): void; + setHoverStyle(element: TElement, datasetIndex: number, index: number): void; + + parse(start: number, count: number): void; + protected parsePrimitiveData(meta: ChartMeta, data: AnyObject[], start: number, count: number): AnyObject[]; + protected parseArrayData(meta: ChartMeta, data: AnyObject[], start: number, count: number): AnyObject[]; + protected parseObjectData(meta: ChartMeta, data: AnyObject[], start: number, count: number): AnyObject[]; + protected getParsed(index: number): TParsedData; + protected applyStack(scale: Scale, parsed: unknown[]): number; + protected updateRangeFromParsed( + range: { min: number; max: number }, + scale: Scale, + parsed: unknown[], + stack: boolean | string + ): void; + protected getMinMax(scale: Scale, canStack?: boolean): { min: number; max: number }; +} + +export interface DatasetControllerChartComponent extends ChartComponent { + defaults: { + datasetElementType?: string | null | false; + dataElementType?: string | null | false; + }; +} + +export interface Defaults extends CoreChartOptions, ElementChartOptions, PluginChartOptions { + + scale: ScaleOptionsByType; + scales: { + [key in ScaleType]: ScaleOptionsByType; + }; + + set(values: AnyObject): AnyObject; + set(scope: string, values: AnyObject): AnyObject; + get(scope: string): AnyObject; + + describe(scope: string, values: AnyObject): AnyObject; + override(scope: string, values: AnyObject): AnyObject; + + /** + * Routes the named defaults to fallback to another scope/name. + * This routing is useful when those target values, like defaults.color, are changed runtime. + * If the values would be copied, the runtime change would not take effect. By routing, the + * fallback is evaluated at each access, so its always up to date. + * + * Example: + * + * defaults.route('elements.arc', 'backgroundColor', '', 'color') + * - reads the backgroundColor from defaults.color when undefined locally + * + * @param scope Scope this route applies to. + * @param name Property name that should be routed to different namespace when not defined here. + * @param targetScope The namespace where those properties should be routed to. + * Empty string ('') is the root of defaults. + * @param targetName The target name in the target scope the property should be routed to. + */ + route(scope: string, name: string, targetScope: string, targetName: string): void; +} + +export type Overrides = { + [key in ChartType]: + CoreChartOptions & + ElementChartOptions & + PluginChartOptions & + DatasetChartOptions & + ScaleChartOptions & + ChartTypeRegistry[key]['chartOptions']; +} + +export const defaults: Defaults; +export interface InteractionOptions { + axis?: string; + intersect?: boolean; +} + +export interface InteractionItem { + element: Element; + datasetIndex: number; + index: number; +} + +export type InteractionModeFunction = ( + chart: Chart, + e: ChartEvent, + options: InteractionOptions, + useFinalPosition?: boolean +) => InteractionItem[]; + +export interface InteractionModeMap { + /** + * Returns items at the same index. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect mode is false, we find the nearest item and return the items at the same index as that item + */ + index: InteractionModeFunction; + + /** + * Returns items in the same dataset. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect is false, we find the nearest item and return the items in that dataset + */ + dataset: InteractionModeFunction; + /** + * Point mode returns all elements that hit test based on the event position + * of the event + */ + point: InteractionModeFunction; + /** + * nearest mode returns the element closest to the point + */ + nearest: InteractionModeFunction; + /** + * x mode returns the elements that hit-test at the current x coordinate + */ + x: InteractionModeFunction; + /** + * y mode returns the elements that hit-test at the current y coordinate + */ + y: InteractionModeFunction; +} + +export type InteractionMode = keyof InteractionModeMap; + +export const Interaction: { + modes: InteractionModeMap; +}; + +export const layouts: { + /** + * Register a box to a chart. + * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. + * @param {Chart} chart - the chart to use + * @param {LayoutItem} item - the item to add to be laid out + */ + addBox(chart: Chart, item: LayoutItem): void; + + /** + * Remove a layoutItem from a chart + * @param {Chart} chart - the chart to remove the box from + * @param {LayoutItem} layoutItem - the item to remove from the layout + */ + removeBox(chart: Chart, layoutItem: LayoutItem): void; + + /** + * Sets (or updates) options on the given `item`. + * @param {Chart} chart - the chart in which the item lives (or will be added to) + * @param {LayoutItem} item - the item to configure with the given options + * @param options - the new item options. + */ + configure( + chart: Chart, + item: LayoutItem, + options: { fullSize?: number; position?: LayoutPosition; weight?: number } + ): void; + + /** + * Fits boxes of the given chart into the given size by having each box measure itself + * then running a fitting algorithm + * @param {Chart} chart - the chart + * @param {number} width - the width to fit into + * @param {number} height - the height to fit into + */ + update(chart: Chart, width: number, height: number): void; +}; + +export interface Plugin extends ExtendedPlugin { + id: string; + + /** + * @desc Called when plugin is installed for this chart instance. This hook is also invoked for disabled plugins (options === false). + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @since 3.0.0 + */ + install?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called when a plugin is starting. This happens when chart is created or plugin is enabled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @since 3.0.0 + */ + start?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called when a plugin stopping. This happens when chart is destroyed or plugin is disabled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @since 3.0.0 + */ + stop?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before initializing `chart`. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + beforeInit?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called after `chart` has been initialized and before the first update. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + afterInit?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before updating `chart`. If any plugin returns `false`, the update + * is cancelled (and thus subsequent render(s)) until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {UpdateMode} args.mode - The update mode + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart update. + */ + beforeUpdate?(chart: Chart, args: { mode: UpdateMode, cancelable: true }, options: O): boolean | void; + /** + * @desc Called after `chart` has been updated and before rendering. Note that this + * hook will not be called if the chart update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {UpdateMode} args.mode - The update mode + * @param {object} options - The plugin options. + */ + afterUpdate?(chart: Chart, args: { mode: UpdateMode }, options: O): void; + /** + * @desc Called during the update process, before any chart elements have been created. + * This can be used for data decimation by changing the data array inside a dataset. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + beforeElementsUpdate?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called during chart reset + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @since version 3.0.0 + */ + reset?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before updating the `chart` datasets. If any plugin returns `false`, + * the datasets update is cancelled until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {UpdateMode} args.mode - The update mode. + * @param {object} options - The plugin options. + * @returns {boolean} false to cancel the datasets update. + * @since version 2.1.5 + */ + beforeDatasetsUpdate?(chart: Chart, args: { mode: UpdateMode }, options: O): boolean | void; + /** + * @desc Called after the `chart` datasets have been updated. Note that this hook + * will not be called if the datasets update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {UpdateMode} args.mode - The update mode. + * @param {object} options - The plugin options. + * @since version 2.1.5 + */ + afterDatasetsUpdate?(chart: Chart, args: { mode: UpdateMode, cancelable: true }, options: O): void; + /** + * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin + * returns `false`, the datasets update is cancelled until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {UpdateMode} args.mode - The update mode. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart datasets drawing. + */ + beforeDatasetUpdate?(chart: Chart, args: { index: number; meta: ChartMeta, mode: UpdateMode, cancelable: true }, options: O): boolean | void; + /** + * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note + * that this hook will not be called if the datasets update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {UpdateMode} args.mode - The update mode. + * @param {object} options - The plugin options. + */ + afterDatasetUpdate?(chart: Chart, args: { index: number; meta: ChartMeta, mode: UpdateMode, cancelable: false }, options: O): void; + /** + * @desc Called before laying out `chart`. If any plugin returns `false`, + * the layout update is cancelled until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart layout. + */ + beforeLayout?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; + /** + * @desc Called before scale data limits are calculated. This hook is called separately for each scale in the chart. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Scale} args.scale - The scale. + * @param {object} options - The plugin options. + */ + beforeDataLimits?(chart: Chart, args: { scale: Scale }, options: O): void; + /** + * @desc Called after scale data limits are calculated. This hook is called separately for each scale in the chart. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Scale} args.scale - The scale. + * @param {object} options - The plugin options. + */ + afterDataLimits?(chart: Chart, args: { scale: Scale }, options: O): void; + /** + * @desc Called before scale bulds its ticks. This hook is called separately for each scale in the chart. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Scale} args.scale - The scale. + * @param {object} options - The plugin options. + */ + beforeBuildTicks?(chart: Chart, args: { scale: Scale }, options: O): void; + /** + * @desc Called after scale has build its ticks. This hook is called separately for each scale in the chart. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Scale} args.scale - The scale. + * @param {object} options - The plugin options. + */ + afterBuildTicks?(chart: Chart, args: { scale: Scale }, options: O): void; + /** + * @desc Called after the `chart` has been laid out. Note that this hook will not + * be called if the layout update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + afterLayout?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before rendering `chart`. If any plugin returns `false`, + * the rendering is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart rendering. + */ + beforeRender?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; + /** + * @desc Called after the `chart` has been fully rendered (and animation completed). Note + * that this hook will not be called if the rendering has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + afterRender?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before drawing `chart` at every animation frame. If any plugin returns `false`, + * the frame drawing is cancelled untilanother `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart drawing. + */ + beforeDraw?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; + /** + * @desc Called after the `chart` has been drawn. Note that this hook will not be called + * if the drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + afterDraw?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, + * the datasets drawing is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart datasets drawing. + */ + beforeDatasetsDraw?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; + /** + * @desc Called after the `chart` datasets have been drawn. Note that this hook + * will not be called if the datasets drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + afterDatasetsDraw?(chart: Chart, args: EmptyObject, options: O, cancelable: false): void; + /** + * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets + * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing + * is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart datasets drawing. + */ + beforeDatasetDraw?(chart: Chart, args: { index: number; meta: ChartMeta }, options: O): boolean | void; + /** + * @desc Called after the `chart` datasets at the given `args.index` have been drawn + * (datasets are drawn in the reverse order). Note that this hook will not be called + * if the datasets drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {object} options - The plugin options. + */ + afterDatasetDraw?(chart: Chart, args: { index: number; meta: ChartMeta }, options: O): void; + /** + * @desc Called before processing the specified `event`. If any plugin returns `false`, + * the event will be discarded. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {ChartEvent} args.event - The event object. + * @param {boolean} args.replay - True if this event is replayed from `Chart.update` + * @param {object} options - The plugin options. + */ + beforeEvent?(chart: Chart, args: { event: ChartEvent, replay: boolean, cancelable: true }, options: O): boolean | void; + /** + * @desc Called after the `event` has been consumed. Note that this hook + * will not be called if the `event` has been previously discarded. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {ChartEvent} args.event - The event object. + * @param {boolean} args.replay - True if this event is replayed from `Chart.update` + * @param {boolean} [args.changed] - Set to true if the plugin needs a render. Should only be changed to true, because this args object is passed through all plugins. + * @param {object} options - The plugin options. + */ + afterEvent?(chart: Chart, args: { event: ChartEvent, replay: boolean, changed?: boolean, cancelable: false }, options: O): void; + /** + * @desc Called after the chart as been resized. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {number} args.size - The new canvas display size (eq. canvas.style width & height). + * @param {object} options - The plugin options. + */ + resize?(chart: Chart, args: { size: { width: number, height: number } }, options: O): void; + /** + * Called after the chart has been destroyed. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + destroy?(chart: Chart, args: EmptyObject, options: O): void; + /** + * Called after chart is destroyed on all plugins that were installed for that chart. This hook is also invoked for disabled plugins (options === false). + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @since 3.0.0 + */ + uninstall?(chart: Chart, args: EmptyObject, options: O): void; +} + +export declare type ChartComponentLike = ChartComponent | ChartComponent[] | { [key: string]: ChartComponent }; + +/** + * Please use the module's default export which provides a singleton instance + * Note: class is exported for typedoc + */ +export interface Registry { + readonly controllers: TypedRegistry; + readonly elements: TypedRegistry; + readonly plugins: TypedRegistry; + readonly scales: TypedRegistry; + + add(...args: ChartComponentLike[]): void; + remove(...args: ChartComponentLike[]): void; + + addControllers(...args: ChartComponentLike[]): void; + addElements(...args: ChartComponentLike[]): void; + addPlugins(...args: ChartComponentLike[]): void; + addScales(...args: ChartComponentLike[]): void; + + getController(id: string): DatasetController | undefined; + getElement(id: string): Element | undefined; + getPlugin(id: string): Plugin | undefined; + getScale(id: string): Scale | undefined; +} + +export const registry: Registry; + +export interface Tick { + value: number; + label?: string | string[]; + major?: boolean; +} + +export interface CoreScaleOptions { + /** + * Controls the axis global visibility (visible when true, hidden when false). When display: 'auto', the axis is visible only if at least one associated dataset is visible. + * @default true + */ + display: boolean | 'auto'; + /** + * Align pixel values to device pixels + */ + alignToPixels: boolean; + /** + * Reverse the scale. + * @default false + */ + reverse: boolean; + /** + * The weight used to sort the axis. Higher weights are further away from the chart area. + * @default true + */ + weight: number; + /** + * Callback called before the update process starts. + */ + beforeUpdate(axis: Scale): void; + /** + * Callback that runs before dimensions are set. + */ + beforeSetDimensions(axis: Scale): void; + /** + * Callback that runs after dimensions are set. + */ + afterSetDimensions(axis: Scale): void; + /** + * Callback that runs before data limits are determined. + */ + beforeDataLimits(axis: Scale): void; + /** + * Callback that runs after data limits are determined. + */ + afterDataLimits(axis: Scale): void; + /** + * Callback that runs before ticks are created. + */ + beforeBuildTicks(axis: Scale): void; + /** + * Callback that runs after ticks are created. Useful for filtering ticks. + */ + afterBuildTicks(axis: Scale): void; + /** + * Callback that runs before ticks are converted into strings. + */ + beforeTickToLabelConversion(axis: Scale): void; + /** + * Callback that runs after ticks are converted into strings. + */ + afterTickToLabelConversion(axis: Scale): void; + /** + * Callback that runs before tick rotation is determined. + */ + beforeCalculateLabelRotation(axis: Scale): void; + /** + * Callback that runs after tick rotation is determined. + */ + afterCalculateLabelRotation(axis: Scale): void; + /** + * Callback that runs before the scale fits to the canvas. + */ + beforeFit(axis: Scale): void; + /** + * Callback that runs after the scale fits to the canvas. + */ + afterFit(axis: Scale): void; + /** + * Callback that runs at the end of the update process. + */ + afterUpdate(axis: Scale): void; +} + +export interface Scale extends Element<{}, O>, LayoutItem { + readonly id: string; + readonly type: string; + readonly ctx: CanvasRenderingContext2D; + readonly chart: Chart; + + maxWidth: number; + maxHeight: number; + + paddingTop: number; + paddingBottom: number; + paddingLeft: number; + paddingRight: number; + + axis: string; + labelRotation: number; + min: number; + max: number; + ticks: Tick[]; + getMatchingVisibleMetas(type?: string): ChartMeta[]; + + drawTitle(chartArea: ChartArea): void; + drawLabels(chartArea: ChartArea): void; + drawGrid(chartArea: ChartArea): void; + + /** + * @param {number} pixel + * @return {number} + */ + getDecimalForPixel(pixel: number): number; + /** + * Utility for getting the pixel location of a percentage of scale + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {number} decimal + * @return {number} + */ + getPixelForDecimal(decimal: number): number; + /** + * Returns the location of the tick at the given index + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {number} index + * @return {number} + */ + getPixelForTick(index: number): number; + /** + * Used to get the label to display in the tooltip for the given value + * @param {*} value + * @return {string} + */ + getLabelForValue(value: number): string; + + /** + * Returns the grid line width at given value + */ + getLineWidthForValue(value: number): number; + + /** + * Returns the location of the given data point. Value can either be an index or a numerical value + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {*} value + * @param {number} [index] + * @return {number} + */ + getPixelForValue(value: number, index?: number): number; + + /** + * Used to get the data value from a given pixel. This is the inverse of getPixelForValue + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {number} pixel + * @return {*} + */ + getValueForPixel(pixel: number): number | undefined; + + getBaseValue(): number; + /** + * Returns the pixel for the minimum chart value + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @return {number} + */ + getBasePixel(): number; + + init(options: O): void; + parse(raw: unknown, index: number): unknown; + getUserBounds(): { min: number; max: number; minDefined: boolean; maxDefined: boolean }; + getMinMax(canStack: boolean): { min: number; max: number }; + getTicks(): Tick[]; + getLabels(): string[]; + beforeUpdate(): void; + configure(): void; + afterUpdate(): void; + beforeSetDimensions(): void; + setDimensions(): void; + afterSetDimensions(): void; + beforeDataLimits(): void; + determineDataLimits(): void; + afterDataLimits(): void; + beforeBuildTicks(): void; + buildTicks(): Tick[]; + afterBuildTicks(): void; + beforeTickToLabelConversion(): void; + generateTickLabels(ticks: Tick[]): void; + afterTickToLabelConversion(): void; + beforeCalculateLabelRotation(): void; + calculateLabelRotation(): void; + afterCalculateLabelRotation(): void; + beforeFit(): void; + fit(): void; + afterFit(): void; + + isFullSize(): boolean; +} +export declare class Scale { + constructor(cfg: {id: string, type: string, ctx: CanvasRenderingContext2D, chart: Chart}); +} + +export interface ScriptableScaleContext { + chart: Chart; + scale: Scale; + index: number; + tick: Tick; +} + +export const Ticks: { + formatters: { + /** + * Formatter for value labels + * @param value the value to display + * @return {string|string[]} the label to display + */ + values(value: unknown): string | string[]; + /** + * Formatter for numeric ticks + * @param tickValue the value to be formatted + * @param index the position of the tickValue parameter in the ticks array + * @param ticks the list of ticks being converted + * @return string representation of the tickValue parameter + */ + numeric(tickValue: number, index: number, ticks: { value: number }[]): string; + /** + * Formatter for logarithmic ticks + * @param tickValue the value to be formatted + * @param index the position of the tickValue parameter in the ticks array + * @param ticks the list of ticks being converted + * @return string representation of the tickValue parameter + */ + logarithmic(tickValue: number, index: number, ticks: { value: number }[]): string; + }; +}; + +export interface TypedRegistry { + /** + * @param {ChartComponent} item + * @returns {string} The scope where items defaults were registered to. + */ + register(item: ChartComponent): string; + get(id: string): T | undefined; + unregister(item: ChartComponent): void; +} + +export interface ChartEvent { + type: + | 'contextmenu' + | 'mouseenter' + | 'mousedown' + | 'mousemove' + | 'mouseup' + | 'mouseout' + | 'click' + | 'dblclick' + | 'keydown' + | 'keypress' + | 'keyup' + | 'resize'; + native: Event | null; + x: number | null; + y: number | null; +} +export interface ChartComponent { + id: string; + defaults?: AnyObject; + defaultRoutes?: { [property: string]: string }; + + beforeRegister?(): void; + afterRegister?(): void; + beforeUnregister?(): void; + afterUnregister?(): void; +} + +export interface CoreInteractionOptions { + /** + * Sets which elements appear in the tooltip. See Interaction Modes for details. + * @default 'nearest' + */ + mode: InteractionMode; + /** + * if true, the hover mode only applies when the mouse position intersects an item on the chart. + * @default true + */ + intersect: boolean; + + /** + * Can be set to 'x', 'y', or 'xy' to define which directions are used in calculating distances. Defaults to 'x' for 'index' mode and 'xy' in dataset and 'nearest' modes. + */ + axis: 'x' | 'y' | 'xy'; +} + +export interface CoreChartOptions extends ParsingOptions, AnimationOptions { + + datasets: { + [key in ChartType]: ChartTypeRegistry[key]['datasetOptions'] + } + + /** + * The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. + * @default 'x' + */ + indexAxis: 'x' | 'y'; + + /** + * base color + * @see Defaults.color + */ + color: Color; + /** + * base background color + * @see Defaults.backgroundColor + */ + backgroundColor: Color; + /** + * base border color + * @see Defaults.borderColor + */ + borderColor: Color; + /** + * base font + * @see Defaults.font + */ + font: FontSpec; + /** + * Resizes the chart canvas when its container does (important note...). + * @default true + */ + responsive: boolean; + /** + * Maintain the original canvas aspect ratio (width / height) when resizing. + * @default true + */ + maintainAspectRatio: boolean; + + /** + * Canvas aspect ratio (i.e. width / height, a value of 1 representing a square canvas). Note that this option is ignored if the height is explicitly defined either as attribute or via the style. + * @default 2 + */ + aspectRatio: number; + + /** + * Locale used for number formatting (using `Intl.NumberFormat`). + * @default user's browser setting + */ + locale: string; + + /** + * Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. + */ + onResize(chart: Chart, size: { width: number; height: number }): void; + + /** + * Override the window's default devicePixelRatio. + * @default window.devicePixelRatio + */ + devicePixelRatio: number; + + interaction: CoreInteractionOptions; + + hover: CoreInteractionOptions; + + /** + * The events option defines the browser events that the chart should listen to for tooltips and hovering. + * @default ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'] + */ + events: ( + 'mousemove' | + 'mouseout' | + 'click' | + 'touchstart' | + 'touchmove' | + 'touchend' | + 'pointerenter' | + 'pointerdown' | + 'pointermove' | + 'pointerup' | + 'pointerleave' | + 'pointerout' + )[]; + + /** + * Called when any of the events fire. Passed the event, an array of active elements (bars, points, etc), and the chart. + */ + onHover(event: ChartEvent, elements: ActiveElement[], chart: Chart): void; + + /** + * Called if the event is of type 'mouseup' or 'click'. Passed the event, an array of active elements, and the chart. + */ + onClick(event: ChartEvent, elements: ActiveElement[], chart: Chart): void; + + layout: { + padding: Scriptable>; + }; +} + +export type EasingFunction = + | 'linear' + | 'easeInQuad' + | 'easeOutQuad' + | 'easeInOutQuad' + | 'easeInCubic' + | 'easeOutCubic' + | 'easeInOutCubic' + | 'easeInQuart' + | 'easeOutQuart' + | 'easeInOutQuart' + | 'easeInQuint' + | 'easeOutQuint' + | 'easeInOutQuint' + | 'easeInSine' + | 'easeOutSine' + | 'easeInOutSine' + | 'easeInExpo' + | 'easeOutExpo' + | 'easeInOutExpo' + | 'easeInCirc' + | 'easeOutCirc' + | 'easeInOutCirc' + | 'easeInElastic' + | 'easeOutElastic' + | 'easeInOutElastic' + | 'easeInBack' + | 'easeOutBack' + | 'easeInOutBack' + | 'easeInBounce' + | 'easeOutBounce' + | 'easeInOutBounce'; + +export type AnimationSpec = { + /** + * The number of milliseconds an animation takes. + * @default 1000 + */ + duration?: Scriptable>; + /** + * Easing function to use + * @default 'easeOutQuart' + */ + easing?: Scriptable>; + + /** + * Delay before starting the animations. + * @default 0 + */ + delay?: Scriptable>; + + /** + * If set to true, the animations loop endlessly. + * @default false + */ + loop?: Scriptable>; +} + +export type AnimationsSpec = { + [name: string]: false | AnimationSpec & { + properties: string[]; + + /** + * Type of property, determines the interpolator used. Possible values: 'number', 'color' and 'boolean'. Only really needed for 'color', because typeof does not get that right. + */ + type: 'color' | 'number' | 'boolean'; + + fn: (from: T, to: T, factor: number) => T; + + /** + * Start value for the animation. Current value is used when undefined + */ + from: Scriptable>; + /** + * + */ + to: Scriptable>; + } +} + +export type TransitionSpec = { + animation: AnimationSpec; + animations: AnimationsSpec; +} + +export type TransitionsSpec = { + [mode: string]: TransitionSpec +} + +export type AnimationOptions = { + animation: false | AnimationSpec & { + /** + * Callback called on each step of an animation. + */ + onProgress?: (this: Chart, event: AnimationEvent) => void; + /** + * Callback called when all animations are completed. + */ + onComplete?: (this: Chart, event: AnimationEvent) => void; + }; + animations: AnimationsSpec; + transitions: TransitionsSpec; +}; + +export interface FontSpec { + /** + * Default font family for all text, follows CSS font-family options. + * @default "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" + */ + family: string; + /** + * Default font size (in px) for text. Does not apply to radialLinear scale point labels. + * @default 12 + */ + size: number; + /** + * Default font style. Does not apply to tooltip title or footer. Does not apply to chart title. Follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit) + * @default 'normal' + */ + style: 'normal' | 'italic' | 'oblique' | 'initial' | 'inherit'; + /** + * Default font weight (boldness). (see MDN). + */ + weight: string | null; + /** + * Height of an individual line of text (see MDN). + * @default 1.2 + */ + lineHeight: number | string; +} + +export type TextAlign = 'left' | 'center' | 'right'; + +export interface VisualElement { + draw(ctx: CanvasRenderingContext2D): void; + inRange(mouseX: number, mouseY: number, useFinalPosition?: boolean): boolean; + inXRange(mouseX: number, useFinalPosition?: boolean): boolean; + inYRange(mouseY: number, useFinalPosition?: boolean): boolean; + getCenterPoint(useFinalPosition?: boolean): { x: number; y: number }; + getRange?(axis: 'x' | 'y'): number; +} + +export interface CommonElementOptions { + borderWidth: number; + borderColor: Color; + backgroundColor: Color; +} + +export interface CommonHoverOptions { + hoverBorderWidth: number; + hoverBorderColor: Color; + hoverBackgroundColor: Color; +} + +export interface Segment { + start: number; + end: number; + loop: boolean; +} + +export interface ArcProps { + x: number; + y: number; + startAngle: number; + endAngle: number; + innerRadius: number; + outerRadius: number; + circumference: number; +} + +export interface ArcBorderRadius { + outerStart: number; + outerEnd: number; + innerStart: number; + innerEnd: number; +} + +export interface ArcOptions extends CommonElementOptions { + /** + * Arc stroke alignment. + */ + borderAlign: 'center' | 'inner'; + /** + * Arc offset (in pixels). + */ + offset: number; + /** + * Sets the border radius for arcs + * @default 0 + */ + borderRadius: number | ArcBorderRadius; +} + +export interface ArcHoverOptions extends CommonHoverOptions { + hoverOffset: number; +} + +export interface ArcElement + extends Element, + VisualElement {} + +export const ArcElement: ChartComponent & { + prototype: ArcElement; + new (cfg: AnyObject): ArcElement; +}; + +export interface LineProps {} + +export interface LineOptions extends CommonElementOptions { + /** + * Line cap style. See MDN. + * @default 'butt' + */ + borderCapStyle: CanvasLineCap; + /** + * Line dash. See MDN. + * @default [] + */ + borderDash: number[]; + /** + * Line dash offset. See MDN. + * @default 0.0 + */ + borderDashOffset: number; + /** + * Line join style. See MDN. + * @default 'miter' + */ + borderJoinStyle: CanvasLineJoin; + /** + * true to keep Bézier control inside the chart, false for no restriction. + * @default true + */ + capBezierPoints: boolean; + /** + * Interpolation mode to apply. + * @default 'default' + */ + cubicInterpolationMode: 'default' | 'monotone'; + /** + * Bézier curve tension (0 for no Bézier curves). + * @default 0 + */ + tension: number; + /** + * true to show the line as a stepped line (tension will be ignored). + * @default false + */ + stepped: 'before' | 'after' | 'middle' | boolean; + /** + * Both line and radar charts support a fill option on the dataset object which can be used to create area between two datasets or a dataset and a boundary, i.e. the scale origin, start or end + */ + fill: FillTarget | ComplexFillTarget; + + segment: { + backgroundColor: Scriptable, + borderColor: Scriptable, + borderCapStyle: Scriptable; + borderDash: Scriptable; + borderDashOffset: Scriptable; + borderJoinStyle: Scriptable; + borderWidth: Scriptable; + }; +} + +export interface LineHoverOptions extends CommonHoverOptions { + hoverBorderCapStyle: CanvasLineCap; + hoverBorderDash: number[]; + hoverBorderDashOffset: number; + hoverBorderJoinStyle: CanvasLineJoin; +} + +export interface LineElement + extends Element, + VisualElement { + updateControlPoints(chartArea: ChartArea, indexAxis?: 'x' | 'y'): void; + points: Point[]; + readonly segments: Segment[]; + first(): Point | false; + last(): Point | false; + interpolate(point: Point, property: 'x' | 'y'): undefined | Point | Point[]; + pathSegment(ctx: CanvasRenderingContext2D, segment: Segment, params: AnyObject): undefined | boolean; + path(ctx: CanvasRenderingContext2D): boolean; +} + +export const LineElement: ChartComponent & { + prototype: LineElement; + new (cfg: AnyObject): LineElement; +}; + +export interface PointProps { + x: number; + y: number; +} + +export type PointStyle = + | 'circle' + | 'cross' + | 'crossRot' + | 'dash' + | 'line' + | 'rect' + | 'rectRounded' + | 'rectRot' + | 'star' + | 'triangle' + | HTMLImageElement + | HTMLCanvasElement; + +export interface PointOptions extends CommonElementOptions { + /** + * Point radius + * @default 3 + */ + radius: number; + /** + * Extra radius added to point radius for hit detection. + * @default 1 + */ + hitRadius: number; + /** + * Point style + * @default 'circle; + */ + pointStyle: PointStyle; + /** + * Point rotation (in degrees). + * @default 0 + */ + rotation: number; +} + +export interface PointHoverOptions extends CommonHoverOptions { + /** + * Point radius when hovered. + * @default 4 + */ + hoverRadius: number; +} + +export interface PointPrefixedOptions { + /** + * The fill color for points. + */ + pointBackgroundColor: Color; + /** + * The border color for points. + */ + pointBorderColor: Color; + /** + * The width of the point border in pixels. + */ + pointBorderWidth: number; + /** + * The pixel size of the non-displayed point that reacts to mouse events. + */ + pointHitRadius: number; + /** + * The radius of the point shape. If set to 0, the point is not rendered. + */ + pointRadius: number; + /** + * The rotation of the point in degrees. + */ + pointRotation: number; + /** + * Style of the point. + */ + pointStyle: PointStyle; +} + +export interface PointPrefixedHoverOptions { + /** + * Point background color when hovered. + */ + pointHoverBackgroundColor: Color; + /** + * Point border color when hovered. + */ + pointHoverBorderColor: Color; + /** + * Border width of point when hovered. + */ + pointHoverBorderWidth: number; + /** + * The radius of the point when hovered. + */ + pointHoverRadius: number; +} + +export interface PointElement + extends Element, + VisualElement { + readonly skip: boolean; + readonly parsed: CartesianParsedData; +} + +export const PointElement: ChartComponent & { + prototype: PointElement; + new (cfg: AnyObject): PointElement; +}; + +export interface BarProps { + x: number; + y: number; + base: number; + horizontal: boolean; + width: number; + height: number; +} + +export interface BarOptions extends CommonElementOptions { + /** + * The base value for the bar in data units along the value axis. + */ + base: number; + + /** + * Skipped (excluded) border: 'start', 'end', 'left', 'right', 'bottom', 'top' or false (none). + * @default 'start' + */ + borderSkipped: 'start' | 'end' | 'left' | 'right' | 'bottom' | 'top' | false; + + /** + * Border radius + * @default 0 + */ + borderRadius: number | BorderRadius; +} + +export interface BorderRadius { + topLeft: number; + topRight: number; + bottomLeft: number; + bottomRight: number; +} + +export interface BarHoverOptions extends CommonHoverOptions { + hoverBorderRadius: number | BorderRadius; +} + +export interface BarElement< + T extends BarProps = BarProps, + O extends BarOptions = BarOptions +> extends Element, VisualElement {} + +export const BarElement: ChartComponent & { + prototype: BarElement; + new (cfg: AnyObject): BarElement; +}; + +export interface ElementOptionsByType { + arc: ScriptableAndArrayOptions>; + bar: ScriptableAndArrayOptions>; + line: ScriptableAndArrayOptions>; + point: ScriptableAndArrayOptions>; +} + +export type ElementChartOptions = { + elements: ElementOptionsByType +}; + +export class BasePlatform { + /** + * Called at chart construction time, returns a context2d instance implementing + * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. + * @param {HTMLCanvasElement} canvas - The canvas from which to acquire context (platform specific) + * @param options - The chart options + */ + acquireContext( + canvas: HTMLCanvasElement, + options?: CanvasRenderingContext2DSettings + ): CanvasRenderingContext2D | null; + /** + * Called at chart destruction time, releases any resources associated to the context + * previously returned by the acquireContext() method. + * @param {CanvasRenderingContext2D} context - The context2d instance + * @returns {boolean} true if the method succeeded, else false + */ + releaseContext(context: CanvasRenderingContext2D): boolean; + /** + * Registers the specified listener on the given chart. + * @param {Chart} chart - Chart from which to listen for event + * @param {string} type - The ({@link ChartEvent}) type to listen for + * @param listener - Receives a notification (an object that implements + * the {@link ChartEvent} interface) when an event of the specified type occurs. + */ + addEventListener(chart: Chart, type: string, listener: (e: ChartEvent) => void): void; + /** + * Removes the specified listener previously registered with addEventListener. + * @param {Chart} chart - Chart from which to remove the listener + * @param {string} type - The ({@link ChartEvent}) type to remove + * @param listener - The listener function to remove from the event target. + */ + removeEventListener(chart: Chart, type: string, listener: (e: ChartEvent) => void): void; + /** + * @returns {number} the current devicePixelRatio of the device this platform is connected to. + */ + getDevicePixelRatio(): number; + /** + * @param {HTMLCanvasElement} canvas - The canvas for which to calculate the maximum size + * @param {number} [width] - Parent element's content width + * @param {number} [height] - Parent element's content height + * @param {number} [aspectRatio] - The aspect ratio to maintain + * @returns { width: number, height: number } the maximum size available. + */ + getMaximumSize(canvas: HTMLCanvasElement, width?: number, height?: number, aspectRatio?: number): { width: number, height: number }; + /** + * @param {HTMLCanvasElement} canvas + * @returns {boolean} true if the canvas is attached to the platform, false if not. + */ + isAttached(canvas: HTMLCanvasElement): boolean; +} + +export class BasicPlatform extends BasePlatform {} +export class DomPlatform extends BasePlatform {} + +export const Decimation: Plugin; + +export const enum DecimationAlgorithm { + lttb = 'lttb', + minmax = 'min-max', +} +interface BaseDecimationOptions { + enabled: boolean; +} + +interface LttbDecimationOptions extends BaseDecimationOptions { + algorithm: DecimationAlgorithm.lttb | 'lttb'; + samples?: number; +} + +interface MinMaxDecimationOptions extends BaseDecimationOptions { + algorithm: DecimationAlgorithm.minmax | 'min-max'; +} + +export type DecimationOptions = LttbDecimationOptions | MinMaxDecimationOptions; + +export const Filler: Plugin; +export interface FillerOptions { + drawTime: 'beforeDatasetDraw' | 'beforeDatasetsDraw'; + propagate: boolean; +} + +export type FillTarget = number | string | { value: number } | 'start' | 'end' | 'origin' | 'stack' | boolean; + +export interface ComplexFillTarget { + /** + * The accepted values are the same as the filling mode values, so you may use absolute and relative dataset indexes and/or boundaries. + */ + target: FillTarget; + /** + * If no color is set, the default color will be the background color of the chart. + */ + above: Color; + /** + * Same as the above. + */ + below: Color; +} + +export interface FillerControllerDatasetOptions { + /** + * Both line and radar charts support a fill option on the dataset object which can be used to create area between two datasets or a dataset and a boundary, i.e. the scale origin, start or end + */ + fill: FillTarget | ComplexFillTarget; +} + +export const Legend: Plugin; + +export interface LegendItem { + /** + * Label that will be displayed + */ + text: string; + + /** + * Border radius of the legend box + * @since 3.1.0 + */ + borderRadius?: number | BorderRadius; + + /** + * Index of the associated dataset + */ + datasetIndex: number; + + /** + * Fill style of the legend box + */ + fillStyle?: Color; + + /** + * Font color for the text + * Defaults to LegendOptions.labels.color + */ + fontColor?: Color; + + /** + * If true, this item represents a hidden dataset. Label will be rendered with a strike-through effect + */ + hidden?: boolean; + + /** + * For box border. + * @see https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap + */ + lineCap?: CanvasLineCap; + + /** + * For box border. + * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash + */ + lineDash?: number[]; + + /** + * For box border. + * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset + */ + lineDashOffset?: number; + + /** + * For box border. + * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin + */ + lineJoin?: CanvasLineJoin; + + /** + * Width of box border + */ + lineWidth?: number; + + /** + * Stroke style of the legend box + */ + strokeStyle?: Color; + + /** + * Point style of the legend box (only used if usePointStyle is true) + */ + pointStyle?: PointStyle; + + /** + * Rotation of the point in degrees (only used if usePointStyle is true) + */ + rotation?: number; + + /** + * Text alignment + */ + textAlign?: TextAlign; +} + +export interface LegendElement extends Element, LayoutItem {} + +export interface LegendOptions { + /** + * Is the legend shown? + * @default true + */ + display: boolean; + /** + * Position of the legend. + * @default 'top' + */ + position: LayoutPosition; + /** + * Alignment of the legend. + * @default 'center' + */ + align: 'start' | 'center' | 'end'; + /** + * Maximum height of the legend, in pixels + */ + maxHeight: number; + /** + * Maximum width of the legend, in pixels + */ + maxWidth: number; + /** + * Marks that this box should take the full width/height of the canvas (moving other boxes). This is unlikely to need to be changed in day-to-day use. + * @default true + */ + fullSize: boolean; + /** + * Legend will show datasets in reverse order. + * @default false + */ + reverse: boolean; + /** + * A callback that is called when a click event is registered on a label item. + */ + onClick(this: LegendElement, e: ChartEvent, legendItem: LegendItem, legend: LegendElement): void; + /** + * A callback that is called when a 'mousemove' event is registered on top of a label item + */ + onHover(this: LegendElement, e: ChartEvent, legendItem: LegendItem, legend: LegendElement): void; + /** + * A callback that is called when a 'mousemove' event is registered outside of a previously hovered label item. + */ + onLeave(this: LegendElement, e: ChartEvent, legendItem: LegendItem, legend: LegendElement): void; + + labels: { + /** + * Width of colored box. + * @default 40 + */ + boxWidth: number; + /** + * Height of the coloured box. + * @default fontSize + */ + boxHeight: number; + /** + * Color of label + * @see Defaults.color + */ + color: Color; + /** + * Font of label + * @see Defaults.font + */ + font: FontSpec; + /** + * Padding between rows of colored boxes. + * @default 10 + */ + padding: number; + /** + * Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See Legend Item for details. + */ + generateLabels(chart: Chart): LegendItem[]; + + /** + * Filters legend items out of the legend. Receives 2 parameters, a Legend Item and the chart data + */ + filter(item: LegendItem, data: ChartData): boolean; + + /** + * Sorts the legend items + */ + sort(a: LegendItem, b: LegendItem, data: ChartData): number; + + /** + * Override point style for the legend. Only applies if usePointStyle is true + */ + pointStyle: PointStyle; + + /** + * Text alignment + */ + textAlign?: TextAlign; + + /** + * Label style will match corresponding point style (size is based on the minimum value between boxWidth and font.size). + * @default false + */ + usePointStyle: boolean; + }; + /** + * true for rendering the legends from right to left. + */ + rtl: boolean; + /** + * This will force the text direction 'rtl' or 'ltr' on the canvas for rendering the legend, regardless of the css specified on the canvas + * @default canvas' default + */ + textDirection: string; + + title: { + /** + * Is the legend title displayed. + * @default false + */ + display: boolean; + /** + * Color of title + * @see Defaults.color + */ + color: Color; + /** + * see Fonts + */ + font: FontSpec; + position: 'center' | 'start' | 'end'; + padding?: number | ChartArea; + /** + * The string title. + */ + text: string; + }; +} + +export const Title: Plugin; + +export interface TitleOptions { + /** + * Alignment of the title. + * @default 'center' + */ + align: 'start' | 'center' | 'end'; + /** + * Is the title shown? + * @default false + */ + display: boolean; + /** + * Position of title + * @default 'top' + */ + position: 'top' | 'left' | 'bottom' | 'right'; + /** + * Color of text + * @see Defaults.color + */ + color: Color; + font: FontSpec; + + /** + * Marks that this box should take the full width/height of the canvas (moving other boxes). If set to `false`, places the box above/beside the + * chart area + * @default true + */ + fullSize: boolean; + /** + * Adds padding above and below the title text if a single number is specified. It is also possible to change top and bottom padding separately. + */ + padding: number | { top: number; bottom: number }; + /** + * Title text to display. If specified as an array, text is rendered on multiple lines. + */ + text: string | string[]; +} + +export type TooltipXAlignment = 'left' | 'center' | 'right'; +export type TooltipYAlignment = 'top' | 'center' | 'bottom'; +export interface TooltipLabelStyle { + borderColor: Color; + backgroundColor: Color; + + /** + * Width of border line + * @since 3.1.0 + */ + borderWidth?: number; + + /** + * Border dash + * @since 3.1.0 + */ + borderDash?: [number, number]; + + /** + * Border dash offset + * @since 3.1.0 + */ + borderDashOffset?: number; + + /** + * borderRadius + * @since 3.1.0 + */ + borderRadius?: number | BorderRadius; +} +export interface TooltipModel { + // The items that we are rendering in the tooltip. See Tooltip Item Interface section + dataPoints: TooltipItem[]; + + // Positioning + xAlign: TooltipXAlignment; + yAlign: TooltipYAlignment; + + // X and Y properties are the top left of the tooltip + x: number; + y: number; + width: number; + height: number; + // Where the tooltip points to + caretX: number; + caretY: number; + + // Body + // The body lines that need to be rendered + // Each object contains 3 parameters + // before: string[] // lines of text before the line with the color square + // lines: string[]; // lines of text to render as the main item with color square + // after: string[]; // lines of text to render after the main lines + body: { before: string[]; lines: string[]; after: string[] }[]; + // lines of text that appear after the title but before the body + beforeBody: string[]; + // line of text that appear after the body and before the footer + afterBody: string[]; + + // Title + // lines of text that form the title + title: string[]; + + // Footer + // lines of text that form the footer + footer: string[]; + + // Styles to render for each item in body[]. This is the styling of the squares in the tooltip + labelColors: TooltipLabelStyle[]; + labelTextColors: Color[]; + labelPointStyles: { pointStyle: PointStyle; rotation: number }[]; + + // 0 opacity is a hidden tooltip + opacity: number; + + // tooltip options + options: TooltipOptions; +} + +export const Tooltip: Plugin & { + readonly positioners: { + [key: string]: (items: readonly ActiveElement[], eventPosition: { x: number; y: number }) => { x: number; y: number } | false; + }; + + getActiveElements(): ActiveElement[]; + setActiveElements(active: ActiveDataPoint[], eventPosition: { x: number, y: number }): void; +}; + +export interface TooltipCallbacks< + TType extends ChartType, + Model = TooltipModel, + Item = TooltipItem> { + + beforeTitle(this: Model, tooltipItems: Item[]): string | string[]; + title(this: Model, tooltipItems: Item[]): string | string[]; + afterTitle(this: Model, tooltipItems: Item[]): string | string[]; + + beforeBody(this: Model, tooltipItems: Item[]): string | string[]; + afterBody(this: Model, tooltipItems: Item[]): string | string[]; + + beforeLabel(this: Model, tooltipItem: Item): string | string[]; + label(this: Model, tooltipItem: Item): string | string[]; + afterLabel(this: Model, tooltipItem: Item): string | string[]; + + labelColor(this: Model, tooltipItem: Item): TooltipLabelStyle; + labelTextColor(this: Model, tooltipItem: Item): Color; + labelPointStyle(this: Model, tooltipItem: Item): { pointStyle: PointStyle; rotation: number }; + + beforeFooter(this: Model, tooltipItems: Item[]): string | string[]; + footer(this: Model, tooltipItems: Item[]): string | string[]; + afterFooter(this: Model, tooltipItems: Item[]): string | string[]; +} + +export interface ExtendedPlugin< + TType extends ChartType, + O = AnyObject, + Model = TooltipModel> { + /** + * @desc Called before drawing the `tooltip`. If any plugin returns `false`, + * the tooltip drawing is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Tooltip} args.tooltip - The tooltip. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart tooltip drawing. + */ + beforeTooltipDraw?(chart: Chart, args: { tooltip: Model }, options: O): boolean | void; + /** + * @desc Called after drawing the `tooltip`. Note that this hook will not + * be called if the tooltip drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Tooltip} args.tooltip - The tooltip. + * @param {object} options - The plugin options. + */ + afterTooltipDraw?(chart: Chart, args: { tooltip: Model }, options: O): void; +} + +export interface ScriptableTooltipContext { + chart: UnionToIntersection>; + tooltip: UnionToIntersection>; + tooltipItems: TooltipItem[]; +} + +export interface TooltipOptions extends CoreInteractionOptions { + /** + * Are on-canvas tooltips enabled? + * @default true + */ + enabled: Scriptable>; + /** + * See external tooltip section. + */ + external(this: TooltipModel, args: { chart: Chart; tooltip: TooltipModel }): void; + /** + * The mode for positioning the tooltip + */ + position: Scriptable<'average' | 'nearest', ScriptableTooltipContext> + + /** + * Override the tooltip alignment calculations + */ + xAlign: Scriptable>; + yAlign: Scriptable>; + + /** + * Sort tooltip items. + */ + itemSort: (a: TooltipItem, b: TooltipItem, data: ChartData) => number; + + filter: (e: TooltipItem, index: number, array: TooltipItem[], data: ChartData) => boolean; + + /** + * Background color of the tooltip. + * @default 'rgba(0, 0, 0, 0.8)' + */ + backgroundColor: Scriptable>; + /** + * Color of title + * @default '#fff' + */ + titleColor: Scriptable>; + /** + * See Fonts + * @default {weight: 'bold'} + */ + titleFont: Scriptable>; + /** + * Spacing to add to top and bottom of each title line. + * @default 2 + */ + titleSpacing: Scriptable>; + /** + * Margin to add on bottom of title section. + * @default 6 + */ + titleMarginBottom: Scriptable>; + /** + * Horizontal alignment of the title text lines. + * @default 'left' + */ + titleAlign: Scriptable>; + /** + * Spacing to add to top and bottom of each tooltip item. + * @default 2 + */ + bodySpacing: Scriptable>; + /** + * Color of body + * @default '#fff' + */ + bodyColor: Scriptable>; + /** + * See Fonts. + * @default {} + */ + bodyFont: Scriptable>; + /** + * Horizontal alignment of the body text lines. + * @default 'left' + */ + bodyAlign: Scriptable>; + /** + * Spacing to add to top and bottom of each footer line. + * @default 2 + */ + footerSpacing: Scriptable>; + /** + * Margin to add before drawing the footer. + * @default 6 + */ + footerMarginTop: Scriptable>; + /** + * Color of footer + * @default '#fff' + */ + footerColor: Scriptable>; + /** + * See Fonts + * @default {weight: 'bold'} + */ + footerFont: Scriptable>; + /** + * Horizontal alignment of the footer text lines. + * @default 'left' + */ + footerAlign: Scriptable>; + /** + * Padding to add to the tooltip + * @default 6 + */ + padding: Scriptable>; + /** + * Extra distance to move the end of the tooltip arrow away from the tooltip point. + * @default 2 + */ + caretPadding: Scriptable>; + /** + * Size, in px, of the tooltip arrow. + * @default 5 + */ + caretSize: Scriptable>; + /** + * Radius of tooltip corner curves. + * @default 6 + */ + cornerRadius: Scriptable>; + /** + * Color to draw behind the colored boxes when multiple items are in the tooltip. + * @default '#fff' + */ + multiKeyBackground: Scriptable>; + /** + * If true, color boxes are shown in the tooltip. + * @default true + */ + displayColors: Scriptable>; + /** + * Width of the color box if displayColors is true. + * @default bodyFont.size + */ + boxWidth: Scriptable>; + /** + * Height of the color box if displayColors is true. + * @default bodyFont.size + */ + boxHeight: Scriptable>; + /** + * Use the corresponding point style (from dataset options) instead of color boxes, ex: star, triangle etc. (size is based on the minimum value between boxWidth and boxHeight) + * @default false + */ + usePointStyle: Scriptable>; + /** + * Color of the border. + * @default 'rgba(0, 0, 0, 0)' + */ + borderColor: Scriptable>; + /** + * Size of the border. + * @default 0 + */ + borderWidth: Scriptable>; + /** + * true for rendering the legends from right to left. + */ + rtl: Scriptable>; + + /** + * This will force the text direction 'rtl' or 'ltr on the canvas for rendering the tooltips, regardless of the css specified on the canvas + * @default canvas's default + */ + textDirection: Scriptable>; + + animation: AnimationSpec; + animations: AnimationsSpec; + callbacks: TooltipCallbacks; +} + +export interface TooltipItem { + /** + * The chart the tooltip is being shown on + */ + chart: Chart; + + /** + * Label for the tooltip + */ + label: string; + + /** + * Parsed data values for the given `dataIndex` and `datasetIndex` + */ + parsed: UnionToIntersection>; + + /** + * Raw data values for the given `dataIndex` and `datasetIndex` + */ + raw: unknown; + + /** + * Formatted value for the tooltip + */ + formattedValue: string; + + /** + * The dataset the item comes from + */ + dataset: ChartDataset; + + /** + * Index of the dataset the item comes from + */ + datasetIndex: number; + + /** + * Index of this data item in the dataset + */ + dataIndex: number; + + /** + * The chart element (point, arc, bar, etc.) for this tooltip item + */ + element: Element; +} + +export interface PluginOptionsByType { + decimation: DecimationOptions; + filler: FillerOptions; + legend: LegendOptions; + title: TitleOptions; + tooltip: TooltipOptions; +} +export interface PluginChartOptions { + plugins: PluginOptionsByType; +} + +export interface GridLineOptions { + /** + * @default true + */ + display: boolean; + borderColor: Color; + borderWidth: number; + /** + * @default false + */ + circular: boolean; + /** + * @default 'rgba(0, 0, 0, 0.1)' + */ + color: Scriptable | readonly Color[]; + /** + * @default [] + */ + borderDash: number[]; + /** + * @default 0 + */ + borderDashOffset: Scriptable; + /** + * @default 1 + */ + lineWidth: Scriptable | readonly number[]; + + /** + * @default true + */ + drawBorder: boolean; + /** + * @default true + */ + drawOnChartArea: boolean; + /** + * @default true + */ + drawTicks: boolean; + /** + * @default [] + */ + tickBorderDash: number[]; + /** + * @default 0 + */ + tickBorderDashOffset: Scriptable; + /** + * @default 'rgba(0, 0, 0, 0.1)' + */ + tickColor: Scriptable | readonly Color[]; + /** + * @default 10 + */ + tickLength: number; + /** + * @default 1 + */ + tickWidth: number; + /** + * @default false + */ + offset: boolean; +} + +export interface TickOptions { + /** + * Color of label backdrops. + * @default 'rgba(255, 255, 255, 0.75)' + */ + backdropColor: Scriptable; + /** + * Padding of tick backdrop. + * @default 2 + */ + backdropPadding: number | ChartArea; + + /** + * Returns the string representation of the tick value as it should be displayed on the chart. See callback. + */ + callback: (tickValue: number | string, index: number, ticks: Tick[]) => string | number | null | undefined; + /** + * If true, show tick labels. + * @default true + */ + display: boolean; + /** + * Color of tick + * @see Defaults.color + */ + color: Scriptable; + /** + * see Fonts + */ + font: Scriptable; + /** + * Sets the offset of the tick labels from the axis + */ + padding: number; + /** + * If true, draw a background behind the tick labels. + * @default false + */ + showLabelBackdrop: Scriptable; + /** + * The color of the stroke around the text. + * @default undefined + */ + textStrokeColor: Scriptable; + /** + * Stroke width around the text. + * @default 0 + */ + textStrokeWidth: Scriptable; + /** + * z-index of tick layer. Useful when ticks are drawn on chart area. Values <= 0 are drawn under datasets, > 0 on top. + * @default 0 + */ + z: number; + + major: { + /** + * If true, major ticks are generated. A major tick will affect autoskipping and major will be defined on ticks in the scriptable options context. + * @default false + */ + enabled: boolean; + }; +} + +export interface CartesianScaleOptions extends CoreScaleOptions { + /** + * Position of the axis. + */ + position: 'left' | 'top' | 'right' | 'bottom' | 'center' | { [scale: string]: number }; + /** + * Which type of axis this is. Possible values are: 'x', 'y'. If not set, this is inferred from the first character of the ID which should be 'x' or 'y'. + */ + axis: 'x' | 'y'; + + /** + * User defined minimum value for the scale, overrides minimum value from data. + */ + min: number; + + /** + * User defined maximum value for the scale, overrides maximum value from data. + */ + max: number; + + /** + * If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to true for a bar chart by default. + * @default false + */ + offset: boolean; + + grid: GridLineOptions; + + title: { + display: boolean; + text: string | string[]; + color: Color; + font: FontSpec; + padding: { + top: number; + bottom: number; + }; + }; + + /** + * If true, data will be comprised between datasets of data + * @default false + */ + stacked?: boolean | 'single'; + + ticks: TickOptions & { + /** + * The number of ticks to examine when deciding how many labels will fit. Setting a smaller value will be faster, but may be less accurate when there is large variability in label length. + * @default ticks.length + */ + sampleSize: number; + /** + * The label alignment + * @default 'center' + */ + align: 'start' | 'center' | 'end'; + /** + * If true, automatically calculates how many labels can be shown and hides labels accordingly. Labels will be rotated up to maxRotation before skipping any. Turn autoSkip off to show all labels no matter what. + * @default true + */ + autoSkip: boolean; + /** + * Padding between the ticks on the horizontal axis when autoSkip is enabled. + * @default 0 + */ + autoSkipPadding: number; + + /** + * How is the label positioned perpendicular to the axis direction. + * This only applies when the rotation is 0 and the axis position is one of "top", "left", "right", or "bottom" + * @default 'near' + */ + crossAlign: 'near' | 'center' | 'far'; + + /** + * Should the defined `min` and `max` values be presented as ticks even if they are not "nice". + * @default: true + */ + includeBounds: boolean; + + /** + * Distance in pixels to offset the label from the centre point of the tick (in the x direction for the x axis, and the y direction for the y axis). Note: this can cause labels at the edges to be cropped by the edge of the canvas + * @default 0 + */ + labelOffset: number; + + /** + * Minimum rotation for tick labels. Note: Only applicable to horizontal scales. + * @default 0 + */ + minRotation: number; + /** + * Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. Note: Only applicable to horizontal scales. + * @default 50 + */ + maxRotation: number; + /** + * Flips tick labels around axis, displaying the labels inside the chart instead of outside. Note: Only applicable to vertical scales. + * @default false + */ + mirror: boolean; + /** + * Padding between the tick label and the axis. When set on a vertical axis, this applies in the horizontal (X) direction. When set on a horizontal axis, this applies in the vertical (Y) direction. + * @default 0 + */ + padding: number; + }; +} + +export type CategoryScaleOptions = CartesianScaleOptions & { + min: string | number; + max: string | number; + labels: string[] | string[][]; +}; + +export type CategoryScale = Scale +export const CategoryScale: ChartComponent & { + prototype: CategoryScale; + new (cfg: AnyObject): CategoryScale; +}; + +export type LinearScaleOptions = CartesianScaleOptions & { + + /** + * if true, scale will include 0 if it is not already included. + * @default true + */ + beginAtZero: boolean; + + /** + * Adjustment used when calculating the maximum data value. + */ + suggestedMin?: number; + /** + * Adjustment used when calculating the minimum data value. + */ + suggestedMax?: number; + /** + * Percentage (string ending with %) or amount (number) for added room in the scale range above and below data. + */ + grace?: string | number; + + ticks: { + /** + * The Intl.NumberFormat options used by the default label formatter + */ + format: Intl.NumberFormatOptions; + + /** + * Maximum number of ticks and gridlines to show. + * @default 11 + */ + maxTicksLimit: number; + /** + * if defined and stepSize is not specified, the step size will be rounded to this many decimal places. + */ + precision: number; + + /** + * User defined fixed step size for the scale + */ + stepSize: number; + + /** + * User defined count of ticks + */ + count: number; + }; +}; + +export type LinearScale = Scale +export const LinearScale: ChartComponent & { + prototype: LinearScale; + new (cfg: AnyObject): LinearScale; +}; + +export type LogarithmicScaleOptions = CartesianScaleOptions & { + + /** + * Adjustment used when calculating the maximum data value. + */ + suggestedMin?: number; + /** + * Adjustment used when calculating the minimum data value. + */ + suggestedMax?: number; + + ticks: { + /** + * The Intl.NumberFormat options used by the default label formatter + */ + format: Intl.NumberFormatOptions; + }; +}; + +export type LogarithmicScale = Scale +export const LogarithmicScale: ChartComponent & { + prototype: LogarithmicScale; + new (cfg: AnyObject): LogarithmicScale; +}; + +export type TimeScaleOptions = CartesianScaleOptions & { + /** + * Scale boundary strategy (bypassed by min/max time options) + * - `data`: make sure data are fully visible, ticks outside are removed + * - `ticks`: make sure ticks are fully visible, data outside are truncated + * @since 2.7.0 + * @default 'data' + */ + bounds: 'ticks' | 'data'; + + /** + * options for creating a new adapter instance + */ + adapters: { + date: unknown; + }; + + time: { + /** + * Custom parser for dates. + */ + parser: string | ((v: unknown) => number); + /** + * If defined, dates will be rounded to the start of this unit. See Time Units below for the allowed units. + */ + round: false | TimeUnit; + /** + * If boolean and true and the unit is set to 'week', then the first day of the week will be Monday. Otherwise, it will be Sunday. + * If `number`, the index of the first day of the week (0 - Sunday, 6 - Saturday). + * @default false + */ + isoWeekday: boolean | number; + /** + * Sets how different time units are displayed. + */ + displayFormats: { + [key: string]: string; + }; + /** + * The format string to use for the tooltip. + */ + tooltipFormat: string; + /** + * If defined, will force the unit to be a certain type. See Time Units section below for details. + * @default false + */ + unit: false | TimeUnit; + + /** + * The number of units between grid lines. + * @default 1 + */ + stepSize: number; + /** + * The minimum display format to be used for a time unit. + * @default 'millisecond' + */ + minUnit: TimeUnit; + }; + + ticks: { + /** + * Ticks generation input values: + * - 'auto': generates "optimal" ticks based on scale size and time options. + * - 'data': generates ticks from data (including labels from data {t|x|y} objects). + * - 'labels': generates ticks from user given `data.labels` values ONLY. + * @see https://github.com/chartjs/Chart.js/pull/4507 + * @since 2.7.0 + * @default 'auto' + */ + source: 'labels' | 'auto' | 'data'; + }; +}; + +export interface TimeScale extends Scale { + getDataTimestamps(): number[]; + getLabelTimestamps(): string[]; + normalize(values: number[]): number[]; +} + +export const TimeScale: ChartComponent & { + prototype: TimeScale; + new (cfg: AnyObject): TimeScale; +}; + +export type TimeSeriesScale = TimeScale +export const TimeSeriesScale: ChartComponent & { + prototype: TimeSeriesScale; + new (cfg: AnyObject): TimeSeriesScale; +}; + +export type RadialLinearScaleOptions = CoreScaleOptions & { + animate: boolean; + + angleLines: { + /** + * if true, angle lines are shown. + * @default true + */ + display: boolean; + /** + * Color of angled lines. + * @default 'rgba(0, 0, 0, 0.1)' + */ + color: Scriptable; + /** + * Width of angled lines. + * @default 1 + */ + lineWidth: Scriptable; + /** + * Length and spacing of dashes on angled lines. See MDN. + * @default [] + */ + borderDash: Scriptable; + /** + * Offset for line dashes. See MDN. + * @default 0 + */ + borderDashOffset: Scriptable; + }; + + /** + * if true, scale will include 0 if it is not already included. + * @default false + */ + beginAtZero: boolean; + + grid: GridLineOptions; + + /** + * User defined minimum number for the scale, overrides minimum value from data. + */ + min: number; + /** + * User defined maximum number for the scale, overrides maximum value from data. + */ + max: number; + + pointLabels: { + /** + * Background color of the point label. + * @default undefined + */ + backdropColor: Scriptable; + /** + * Padding of label backdrop. + * @default 2 + */ + backdropPadding: Scriptable; + + /** + * if true, point labels are shown. + * @default true + */ + display: boolean; + /** + * Color of label + * @see Defaults.color + */ + color: Scriptable; + /** + */ + font: Scriptable; + + /** + * Callback function to transform data labels to point labels. The default implementation simply returns the current string. + */ + callback: (label: string, index: number) => string; + }; + + /** + * Adjustment used when calculating the maximum data value. + */ + suggestedMax: number; + /** + * Adjustment used when calculating the minimum data value. + */ + suggestedMin: number; + + ticks: TickOptions & { + /** + * The Intl.NumberFormat options used by the default label formatter + */ + format: Intl.NumberFormatOptions; + + /** + * Maximum number of ticks and gridlines to show. + * @default 11 + */ + maxTicksLimit: number; + + /** + * if defined and stepSize is not specified, the step size will be rounded to this many decimal places. + */ + precision: number; + + /** + * User defined fixed step size for the scale. + */ + stepSize: number; + + /** + * User defined number of ticks + */ + count: number; + }; +}; + +export interface RadialLinearScale extends Scale { + setCenterPoint(leftMovement: number, rightMovement: number, topMovement: number, bottomMovement: number): void; + getIndexAngle(index: number): number; + getDistanceFromCenterForValue(value: number): number; + getValueForDistanceFromCenter(distance: number): number; + getPointPosition(index: number, distanceFromCenter: number): { x: number; y: number; angle: number }; + getPointPositionForValue(index: number, value: number): { x: number; y: number; angle: number }; + getPointLabelPosition(index: number): ChartArea; + getBasePosition(index: number): { x: number; y: number; angle: number }; +} +export const RadialLinearScale: ChartComponent & { + prototype: RadialLinearScale; + new (cfg: AnyObject): RadialLinearScale; +}; + +export interface CartesianScaleTypeRegistry { + linear: { + options: LinearScaleOptions; + }; + logarithmic: { + options: LogarithmicScaleOptions; + }; + category: { + options: CategoryScaleOptions; + }; + time: { + options: TimeScaleOptions; + }; + timeseries: { + options: TimeScaleOptions; + }; +} + +export interface RadialScaleTypeRegistry { + radialLinear: { + options: RadialLinearScaleOptions; + }; +} + +export interface ScaleTypeRegistry extends CartesianScaleTypeRegistry, RadialScaleTypeRegistry { +} + +export type ScaleType = keyof ScaleTypeRegistry; + +interface CartesianParsedData { + x: number; + y: number; + + // Only specified when stacked bars are enabled + _stacks?: { + // Key is the stack ID which is generally the axis ID + [key: string]: { + // Inner key is the datasetIndex + [key: number]: number; + } + } +} + +interface BarParsedData extends CartesianParsedData { + // Only specified if floating bars are show + _custom?: { + barStart: number; + barEnd: number; + start: number; + end: number; + min: number; + max: number; + } +} + +interface BubbleParsedData extends CartesianParsedData { + // The bubble radius value + _custom: number; +} + +interface RadialParsedData { + r: number; +} + +export interface ChartTypeRegistry { + bar: { + chartOptions: BarControllerChartOptions; + datasetOptions: BarControllerDatasetOptions; + defaultDataPoint: number; + parsedDataType: BarParsedData, + scales: keyof CartesianScaleTypeRegistry; + }; + line: { + chartOptions: LineControllerChartOptions; + datasetOptions: LineControllerDatasetOptions & FillerControllerDatasetOptions; + defaultDataPoint: ScatterDataPoint | number | null; + parsedDataType: CartesianParsedData; + scales: keyof CartesianScaleTypeRegistry; + }; + scatter: { + chartOptions: ScatterControllerChartOptions; + datasetOptions: ScatterControllerDatasetOptions; + defaultDataPoint: ScatterDataPoint | number | null; + parsedDataType: CartesianParsedData; + scales: keyof CartesianScaleTypeRegistry; + }; + bubble: { + chartOptions: unknown; + datasetOptions: BubbleControllerDatasetOptions; + defaultDataPoint: BubbleDataPoint; + parsedDataType: BubbleParsedData; + scales: keyof CartesianScaleTypeRegistry; + }; + pie: { + chartOptions: PieControllerChartOptions; + datasetOptions: PieControllerDatasetOptions; + defaultDataPoint: PieDataPoint; + parsedDataType: number; + scales: keyof CartesianScaleTypeRegistry; + }; + doughnut: { + chartOptions: DoughnutControllerChartOptions; + datasetOptions: DoughnutControllerDatasetOptions; + defaultDataPoint: DoughnutDataPoint; + parsedDataType: number; + scales: keyof CartesianScaleTypeRegistry; + }; + polarArea: { + chartOptions: PolarAreaControllerChartOptions; + datasetOptions: PolarAreaControllerDatasetOptions; + defaultDataPoint: number; + parsedDataType: RadialParsedData; + scales: keyof RadialScaleTypeRegistry; + }; + radar: { + chartOptions: RadarControllerChartOptions; + datasetOptions: RadarControllerDatasetOptions & FillerControllerDatasetOptions; + defaultDataPoint: number | null; + parsedDataType: RadialParsedData; + scales: keyof RadialScaleTypeRegistry; + }; +} + +export type ChartType = keyof ChartTypeRegistry; + +export type ScaleOptionsByType = + { [key in ScaleType]: { type: key } & ScaleTypeRegistry[key]['options'] }[TScale] +; + +// Convenience alias for creating and manipulating scale options in user code +export type ScaleOptions = DeepPartial>; + +export type DatasetChartOptions = { + [key in TType]: { + datasets: ChartTypeRegistry[key]['datasetOptions']; + }; +}; + +export type ScaleChartOptions = { + scales: { + [key: string]: ScaleOptionsByType; + }; +}; + +export type ChartOptions = DeepPartial< + CoreChartOptions & + ElementChartOptions & + PluginChartOptions & + DatasetChartOptions & + ScaleChartOptions & + ChartTypeRegistry[TType]['chartOptions'] +>; + +export type DefaultDataPoint = DistributiveArray; + +export type ParsedDataType = ChartTypeRegistry[TType]['parsedDataType']; + +export interface ChartDatasetProperties { + type?: TType; + data: TData; +} + +export type ChartDataset< + TType extends ChartType = ChartType, + TData = DefaultDataPoint +> = DeepPartial< + { [key in ChartType]: { type: key } & ChartTypeRegistry[key]['datasetOptions'] }[TType] +> & ChartDatasetProperties; + +export interface ChartData< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> { + labels?: TLabel[]; + datasets: ChartDataset[]; +} + +export interface ChartConfiguration< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> { + type: TType; + data: ChartData; + options?: ChartOptions; + plugins?: Plugin[]; +} diff --git a/node_modules/chart.js/types/layout.d.ts b/node_modules/chart.js/types/layout.d.ts new file mode 100644 index 000000000..36f3237f3 --- /dev/null +++ b/node_modules/chart.js/types/layout.d.ts @@ -0,0 +1,65 @@ +import { ChartArea } from './geometric'; + +export type LayoutPosition = 'left' | 'top' | 'right' | 'bottom' | 'center' | 'chartArea' | {[scaleId: string]: number}; + +export interface LayoutItem { + /** + * The position of the item in the chart layout. Possible values are + */ + position: LayoutPosition; + /** + * The weight used to sort the item. Higher weights are further away from the chart area + */ + weight: number; + /** + * if true, and the item is horizontal, then push vertical boxes down + */ + fullSize: boolean; + /** + * Width of item. Must be valid after update() + */ + width: number; + /** + * Height of item. Must be valid after update() + */ + height: number; + /** + * Left edge of the item. Set by layout system and cannot be used in update + */ + left: number; + /** + * Top edge of the item. Set by layout system and cannot be used in update + */ + top: number; + /** + * Right edge of the item. Set by layout system and cannot be used in update + */ + right: number; + /** + * Bottom edge of the item. Set by layout system and cannot be used in update + */ + bottom: number; + + /** + * Called before the layout process starts + */ + beforeLayout?(): void; + /** + * Draws the element + */ + draw(chartArea: ChartArea): void; + /** + * Returns an object with padding on the edges + */ + getPadding?(): ChartArea; + /** + * returns true if the layout item is horizontal (ie. top or bottom) + */ + isHorizontal(): boolean; + /** + * Takes two parameters: width and height. + * @param width + * @param height + */ + update(width: number, height: number, margins?: ChartArea): void; +} diff --git a/node_modules/chart.js/types/utils.d.ts b/node_modules/chart.js/types/utils.d.ts new file mode 100644 index 000000000..592aa6381 --- /dev/null +++ b/node_modules/chart.js/types/utils.d.ts @@ -0,0 +1,18 @@ + +// DeepPartial implementation taken from the utility-types NPM package, which is +// Copyright (c) 2016 Piotr Witek (http://piotrwitek.github.io) +// and used under the terms of the MIT license +export type DeepPartial = T extends Function + ? T + : T extends Array + ? _DeepPartialArray + : T extends object + ? _DeepPartialObject + : T | undefined; + type _DeepPartialArray = Array> +type _DeepPartialObject = { [P in keyof T]?: DeepPartial }; + +export type DistributiveArray = [T] extends [unknown] ? Array : never + +// From https://stackoverflow.com/a/50375286 +export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..7b856ed5f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,11 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "chart.js": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz", + "integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g==" + } + } +} diff --git a/plugins/WordPress/Menu.php b/plugins/WordPress/Menu.php index ee368e34e..2e5a23f55 100644 --- a/plugins/WordPress/Menu.php +++ b/plugins/WordPress/Menu.php @@ -23,7 +23,6 @@ public function configureAdminMenu(MenuAdmin $menu) $menu->remove('CoreAdminHome_MenuMeasurables', 'SitesManager_MenuManage'); $menu->remove('SitesManager_Sites', 'SitesManager_MenuManage'); $menu->remove('CoreAdminHome_MenuSystem', 'UsersManager_MenuUsers'); - $menu->remove('UsersManager_MenuPersonal', 'General_Settings'); $menu->remove('UsersManager_MenuPersonal', 'General_Security'); $menu->remove('CoreAdminHome_MenuMeasurables', 'CoreAdminHome_TrackingCode'); $menu->remove('CoreAdminHome_MenuMeasurables', 'General_Settings'); diff --git a/plugins/WordPress/WordPress.php b/plugins/WordPress/WordPress.php index e33e36340..d87521dfc 100644 --- a/plugins/WordPress/WordPress.php +++ b/plugins/WordPress/WordPress.php @@ -53,6 +53,7 @@ public function registerEvents() 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', 'CustomJsTracker.manipulateJsTracker' => 'updateHeatmapTrackerPath', 'Visualization.beforeRender' => 'onBeforeRenderView', + 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', ); } @@ -331,10 +332,9 @@ public function onDispatchRequest(&$module, &$action, &$parameters) array('usersmanager', 'index'), array('usersmanager', ''), array('usersmanager', 'addnewtoken'), - array('usersmanager', 'usersettings'), array('usersmanager', 'deletetoken'), array('usersmanager', 'usersecurity'), - array('sitesmanager', ''), + array('sitesmanager', ''), array('sitesmanager', 'globalsettings'), array('feedback', ''), array('feedback', 'index'), @@ -397,4 +397,9 @@ public function throwNotAvailableException() throw new \Exception('This feature is not available'); } + public function getStylesheetFiles(&$files) + { + $files[] = "../plugins/WordPress/stylesheets/user.css"; + } + } diff --git a/plugins/WordPress/stylesheets/user.css b/plugins/WordPress/stylesheets/user.css new file mode 100644 index 000000000..3a888a861 --- /dev/null +++ b/plugins/WordPress/stylesheets/user.css @@ -0,0 +1,3 @@ +div.siteSelector, #defaultReportSiteSelector, div[name="username"],div[name="language"], div[name="timeformat"],div[name="defaultReport"], #newsletterSignup { + display: none; +} diff --git a/readme.txt b/readme.txt index 0a294d5f1..f8a3abe72 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_i Tags: matomo,piwik,analytics,statistics,stats,tracking,ecommerce Requires at least: 4.8 Tested up to: 5.8 -Stable tag: 4.4.1 +Stable tag: 4.4.2 Requires PHP: 7.2.5 License: GPLv3 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html diff --git a/scripts/deploy_marketplace_only.sh b/scripts/deploy_marketplace_only.sh index c12677285..708ee0c18 100755 --- a/scripts/deploy_marketplace_only.sh +++ b/scripts/deploy_marketplace_only.sh @@ -77,6 +77,11 @@ rsync -rc "$TMP_DIR/" trunk/ --delete --delete-excluded # Copy dotorg assets to /assets rsync -rc "$GITHUB_WORKSPACE/$ASSETS_DIR/" assets/ --delete --delete-excluded +# Fix screenshots getting force downloaded when clicking them +# https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/ +svn propset svn:mime-type image/png assets/*.png || true +svn propset svn:mime-type image/jpeg assets/*.jpg || true + echo "➤ Preparing files..." svn status @@ -94,11 +99,10 @@ fi # Readme also has to be updated in the .org tag echo "➤ Preparing stable tag..." -STABLE_TAG=$(grep -m 1 "^Stable tag:" "$TMP_DIR/$README_NAME" | tr -d '\r\n' | awk -F ' ' '{print $NF}') +STABLE_TAG=$(grep -m 1 -E "^([*+-]\s+)?Stable tag:" "$TMP_DIR/$README_NAME" | tr -d '\r\n' | awk -F ' ' '{print $NF}') if [[ -z "$STABLE_TAG" ]]; then echo "ℹ︎ Could not get stable tag from $README_NAME"; - HAS_STABLE=1 else echo "ℹ︎ STABLE_TAG is $STABLE_TAG" diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index 4086f7999..a7a1d2290 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -4,15 +4,21 @@ * * @package matomo */ +/** + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound + * + * @todo find why this warning is triggered + * phpcs:disable WordPress.Security.EscapeOutput.DeprecatedWhitelistCommentFound + */ +$tests_dir = getenv( 'WP_TESTS_DIR' ); -$_tests_dir = getenv( 'WP_TESTS_DIR' ); - -if ( ! $_tests_dir ) { - $_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib'; +if ( ! $tests_dir ) { + $tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib'; } -if ( ! file_exists( $_tests_dir . '/includes/functions.php' ) ) { - echo "Could not find $_tests_dir/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; // WPCS: XSS ok. +if ( ! file_exists( $tests_dir . '/includes/functions.php' ) ) { + echo "Could not find $tests_dir/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; // WPCS: XSS ok. exit( 1 ); } @@ -21,19 +27,19 @@ } // Give access to tests_add_filter() function. -require_once $_tests_dir . '/includes/functions.php'; +require_once $tests_dir . '/includes/functions.php'; /** * Manually load the plugin being tested. */ -function _manually_load_plugin() { +function manually_load_plugin() { require dirname( dirname( dirname( __FILE__ ) ) ) . '/matomo.php'; } -tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); +tests_add_filter( 'muplugins_loaded', 'manually_load_plugin' ); // Start up the WP testing environment. -require $_tests_dir . '/includes/bootstrap.php'; +require $tests_dir . '/includes/bootstrap.php'; require 'framework/test-case.php'; require 'framework/test-matomo-test-case.php'; diff --git a/tests/phpunit/framework/test-case.php b/tests/phpunit/framework/test-case.php index ae3fb5f23..d11a9ab1b 100644 --- a/tests/phpunit/framework/test-case.php +++ b/tests/phpunit/framework/test-case.php @@ -77,7 +77,7 @@ protected function reset_roles() { */ protected function get_type_attribute() { $type = ''; - if (function_exists( "wp_get_inline_script_tag" ) && ! is_admin() && ! current_theme_supports( 'html5', 'script' ) ) { + if ( function_exists( 'wp_get_inline_script_tag' ) && ! is_admin() && ! current_theme_supports( 'html5', 'script' ) ) { $type = 'type="text/javascript"'; } return $type; diff --git a/tests/phpunit/framework/test-local-tracker.php b/tests/phpunit/framework/test-local-tracker.php index 88a37900c..10da679a8 100644 --- a/tests/phpunit/framework/test-local-tracker.php +++ b/tests/phpunit/framework/test-local-tracker.php @@ -4,12 +4,17 @@ use Piwik\Plugin\API; use Piwik\Tracker; use Piwik\Tracker\Cache; - +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound $GLOBALS['PIWIK_TRACKER_DEBUG'] = false; require_once __DIR__ . '/../../../app/libs/PiwikTracker/PiwikTracker.php'; /** * Tracker that uses core/Tracker.php directly. + * Piwik constants + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + * inherit from Piwik + * phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */ class MatomoLocalTracker extends PiwikTracker { @@ -52,14 +57,15 @@ protected function sendRequest( $url, $method = 'GET', $data = null, $force = fa Tracker::$initTrackerMode = false; Tracker::setTestEnvironment( $test_environment_args, $method ); // set language + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $old_lang = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : ''; $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $this->acceptLanguage; // set user agent + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $old_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : ''; $_SERVER['HTTP_USER_AGENT'] = $this->userAgent; // set cookie $old_cookie = $_COOKIE; - // parse_str(parse_url($this->requestCookie, PHP_URL_QUERY), $_COOKIE); // do tracking and capture output ob_start(); $local_tracker = new Tracker(); @@ -71,6 +77,7 @@ protected function sendRequest( $url, $method = 'GET', $data = null, $force = fa $handler = Tracker\Handler\Factory::make(); $response = $local_tracker->main( $handler, $request ); if ( ! is_null( $response ) ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $response; } $output = ob_get_contents(); @@ -94,7 +101,7 @@ protected function sendRequest( $url, $method = 'GET', $data = null, $force = fa private function parseUrl( $url ) { // parse url - $query = parse_url( $url, PHP_URL_QUERY ); + $query = wp_parse_url( $url, PHP_URL_QUERY ); if ( false === $query ) { return; } diff --git a/tests/phpunit/framework/test-matomo-test-case.php b/tests/phpunit/framework/test-matomo-test-case.php index 451bfe0c7..981a76c24 100644 --- a/tests/phpunit/framework/test-matomo-test-case.php +++ b/tests/phpunit/framework/test-matomo-test-case.php @@ -27,13 +27,17 @@ use WpMatomo\Uninstaller; use WpMatomo\User; +/** + * Piwik constants + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals + */ class MatomoAnalytics_TestCase extends MatomoUnit_TestCase { /** * Disable creation of temporary tables. This may be needed when you're writing a test that is * tracking/archiving data. Problem is with temp tables many queries fail like this * - * can't really use temporary tables as we otherwise get errors like + * Can't really use temporary tables as we otherwise get errors like * : WP DB Error: Can't reopen table: 'log_action' - in plugin Actions at PluginsArchiver.php:186 * because temp tables cannot be joined * @@ -41,6 +45,12 @@ class MatomoAnalytics_TestCase extends MatomoUnit_TestCase { */ protected $disable_temp_tables = false; + /** + * @param $query + * + * @return mixed + * phpcs:disable PSR2.Methods.MethodDeclaration.Underscore + */ public function _create_temporary_tables( $query ) { if ( ! $this->disable_temp_tables ) { $query = parent::_create_temporary_tables( $query ); @@ -49,6 +59,12 @@ public function _create_temporary_tables( $query ) { return $query; } + /** + * @param $query + * + * @return mixed + * phpcs:disable PSR2.Methods.MethodDeclaration.Underscore + */ public function _drop_temporary_tables( $query ) { if ( ! $this->disable_temp_tables ) { $query = parent::_drop_temporary_tables( $query ); @@ -60,8 +76,8 @@ public function _drop_temporary_tables( $query ) { public function setUp() { parent::setUp(); - if (!defined('PIWIK_TEST_MODE')) { - define('PIWIK_TEST_MODE', true); + if ( ! defined( 'PIWIK_TEST_MODE' ) ) { + define( 'PIWIK_TEST_MODE', true ); } $uninstall = new Uninstaller(); $uninstall->uninstall( true ); @@ -100,6 +116,7 @@ function () { ArchiveTableCreator::clear(); Site::clearCache(); Archive::clearStaticCache(); + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase FrontController::$requestId = null; Date::$now = null; \Piwik\Tracker\Cache::deleteTrackerCache(); @@ -108,8 +125,9 @@ function () { Manager::getInstance()->deleteAll(); \WpMatomo\Updater::unlock(); PluginsArchiver::$archivers = array(); - $_GET = $_REQUEST = array(); - \Piwik\Container\StaticContainer::get(\Piwik\Translation\Translator::class)->reset(); + $_GET = array(); + $_REQUEST = array(); + \Piwik\Container\StaticContainer::get( \Piwik\Translation\Translator::class )->reset(); \Piwik\Log::unsetInstance(); } ); @@ -137,7 +155,8 @@ protected function assume_admin_page() { } protected function assert_tracking_response( $tracking_response ) { - $trans_gif_64 = 'R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='; + $trans_gif_64 = 'R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='; + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $expected_response = base64_decode( $trans_gif_64 ); $this->assertEquals( $expected_response, $tracking_response ); } @@ -171,7 +190,7 @@ protected function make_local_tracker( $date_time ) { $tracker->setLocalTime( '12:34:06' ); $tracker->setResolution( 1024, 768 ); $tracker->setBrowserHasCookies( true ); - $tracker->setPlugins( $flash = true, $java = true, $director = false ); + $tracker->setPlugins( true, true, false ); return $tracker; } diff --git a/tests/phpunit/wpmatomo/admin/test-dashboard.php b/tests/phpunit/wpmatomo/admin/test-dashboard.php index d8f2d5985..8fd4c7d15 100644 --- a/tests/phpunit/wpmatomo/admin/test-dashboard.php +++ b/tests/phpunit/wpmatomo/admin/test-dashboard.php @@ -9,7 +9,7 @@ class AdminDashboardTest extends MatomoAnalytics_TestCase { - const EXAMPLE_ID = 'VisitsSummary_get'; + const EXAMPLE_ID = 'VisitsSummary_get'; /** * @var Dashboard @@ -32,117 +32,143 @@ public function tearDown() { } public function test_get_widgets_is_empty_by_default() { - $this->assertSame(array(), $this->dashboard->get_widgets()); + $this->assertSame( array(), $this->dashboard->get_widgets() ); } public function test_get_widgets_toggle_widget() { - $this->dashboard->toggle_widget(self::EXAMPLE_ID, Dates::TODAY); - - $this->assertSame(array( - ['unique_id' => 'VisitsSummary_get', 'date' => 'today'] - ), $this->dashboard->get_widgets()); - - $this->dashboard->toggle_widget(self::EXAMPLE_ID, Dates::YESTERDAY); - $this->dashboard->toggle_widget('foobar', Dates::YESTERDAY); - - $this->assertSame(array( - ['unique_id' => 'VisitsSummary_get', 'date' => 'today'], - ['unique_id' => 'VisitsSummary_get', 'date' => 'yesterday'], - ['unique_id' => 'foobar', 'date' => 'yesterday'] - ), $this->dashboard->get_widgets()); + $this->dashboard->toggle_widget( self::EXAMPLE_ID, Dates::TODAY ); + + $this->assertSame( + array( + array( + 'unique_id' => 'VisitsSummary_get', + 'date' => 'today', + ), + ), + $this->dashboard->get_widgets() + ); + + $this->dashboard->toggle_widget( self::EXAMPLE_ID, Dates::YESTERDAY ); + $this->dashboard->toggle_widget( 'foobar', Dates::YESTERDAY ); + + $this->assertSame( + array( + array( + 'unique_id' => 'VisitsSummary_get', + 'date' => 'today', + ), + array( + 'unique_id' => 'VisitsSummary_get', + 'date' => 'yesterday', + ), + array( + 'unique_id' => 'foobar', + 'date' => 'yesterday', + ), + ), + $this->dashboard->get_widgets() + ); } public function test_toggle_widget_removes_widgt_if_already_exists() { - $this->dashboard->toggle_widget(self::EXAMPLE_ID, Dates::TODAY); - $this->dashboard->toggle_widget(self::EXAMPLE_ID, Dates::YESTERDAY); - $this->dashboard->toggle_widget('foobar', Dates::YESTERDAY); - - $this->assertCount(3, $this->dashboard->get_widgets()); - $this->assertTrue($this->dashboard->has_widget(self::EXAMPLE_ID, Dates::YESTERDAY)); - - $this->dashboard->toggle_widget(self::EXAMPLE_ID, Dates::YESTERDAY); - - $this->assertFalse($this->dashboard->has_widget(self::EXAMPLE_ID, Dates::YESTERDAY)); - $this->assertSame(array( - ['unique_id' => 'VisitsSummary_get', 'date' => 'today'], - ['unique_id' => 'foobar', 'date' => 'yesterday'] - ), $this->dashboard->get_widgets()); + $this->dashboard->toggle_widget( self::EXAMPLE_ID, Dates::TODAY ); + $this->dashboard->toggle_widget( self::EXAMPLE_ID, Dates::YESTERDAY ); + $this->dashboard->toggle_widget( 'foobar', Dates::YESTERDAY ); + + $this->assertCount( 3, $this->dashboard->get_widgets() ); + $this->assertTrue( $this->dashboard->has_widget( self::EXAMPLE_ID, Dates::YESTERDAY ) ); + + $this->dashboard->toggle_widget( self::EXAMPLE_ID, Dates::YESTERDAY ); + + $this->assertFalse( $this->dashboard->has_widget( self::EXAMPLE_ID, Dates::YESTERDAY ) ); + $this->assertSame( + array( + array( + 'unique_id' => 'VisitsSummary_get', + 'date' => 'today', + ), + array( + 'unique_id' => 'foobar', + 'date' => 'yesterday', + ), + ), + $this->dashboard->get_widgets() + ); } - public function test_add_dashboard_widgets_when_no_widgets_defined() { - global $wp_meta_boxes; - $this->dashboard->add_dashboard_widgets(); - $this->assertEmpty($wp_meta_boxes); - } + public function test_add_dashboard_widgets_when_no_widgets_defined() { + global $wp_meta_boxes; + $this->dashboard->add_dashboard_widgets(); + $this->assertEmpty( $wp_meta_boxes ); + } - public function test_add_dashboard_widgets_only_adds_valid_widgets() { - if (!function_exists('wp_add_dashboard_widget')) { - include_once ABSPATH . '/wp-admin/includes/dashboard.php'; - } - global $wp_meta_boxes; + public function test_add_dashboard_widgets_only_adds_valid_widgets() { + if ( ! function_exists( 'wp_add_dashboard_widget' ) ) { + include_once ABSPATH . '/wp-admin/includes/dashboard.php'; + } + global $wp_meta_boxes; - $this->dashboard->toggle_widget(self::EXAMPLE_ID, Dates::TODAY); - $this->dashboard->toggle_widget(self::EXAMPLE_ID, Dates::YESTERDAY); - $this->dashboard->toggle_widget('foobar', Dates::YESTERDAY); + $this->dashboard->toggle_widget( self::EXAMPLE_ID, Dates::TODAY ); + $this->dashboard->toggle_widget( self::EXAMPLE_ID, Dates::YESTERDAY ); + $this->dashboard->toggle_widget( 'foobar', Dates::YESTERDAY ); - $this->dashboard->add_dashboard_widgets(); + $this->dashboard->add_dashboard_widgets(); - $this->assertCount(2, $wp_meta_boxes['edit-post']['normal']['core']); - $this->assertTrue(isset($wp_meta_boxes['edit-post']['normal']['core']['matomo_dashboard_widget_VisitsSummary_get_today'])); - $this->assertTrue(isset($wp_meta_boxes['edit-post']['normal']['core']['matomo_dashboard_widget_VisitsSummary_get_yesterday'])); - } + $this->assertCount( 2, $wp_meta_boxes['edit-post']['normal']['core'] ); + $this->assertTrue( isset( $wp_meta_boxes['edit-post']['normal']['core']['matomo_dashboard_widget_VisitsSummary_get_today'] ) ); + $this->assertTrue( isset( $wp_meta_boxes['edit-post']['normal']['core']['matomo_dashboard_widget_VisitsSummary_get_yesterday'] ) ); + } public function test_has_widget() { - $this->assertFalse($this->dashboard->has_widget('foo', 'bar')); - $this->assertFalse($this->dashboard->has_widget(self::EXAMPLE_ID, Dates::TODAY)); + $this->assertFalse( $this->dashboard->has_widget( 'foo', 'bar' ) ); + $this->assertFalse( $this->dashboard->has_widget( self::EXAMPLE_ID, Dates::TODAY ) ); - $this->dashboard->toggle_widget(self::EXAMPLE_ID, Dates::TODAY); + $this->dashboard->toggle_widget( self::EXAMPLE_ID, Dates::TODAY ); - $this->assertFalse($this->dashboard->has_widget('foo', 'bar')); - $this->assertTrue($this->dashboard->has_widget(self::EXAMPLE_ID, Dates::TODAY)); - $this->assertFalse($this->dashboard->has_widget(self::EXAMPLE_ID, Dates::YESTERDAY)); - $this->assertFalse($this->dashboard->has_widget(self::EXAMPLE_ID, Dates::THIS_MONTH)); + $this->assertFalse( $this->dashboard->has_widget( 'foo', 'bar' ) ); + $this->assertTrue( $this->dashboard->has_widget( self::EXAMPLE_ID, Dates::TODAY ) ); + $this->assertFalse( $this->dashboard->has_widget( self::EXAMPLE_ID, Dates::YESTERDAY ) ); + $this->assertFalse( $this->dashboard->has_widget( self::EXAMPLE_ID, Dates::THIS_MONTH ) ); - $this->dashboard->toggle_widget(self::EXAMPLE_ID, Dates::YESTERDAY); + $this->dashboard->toggle_widget( self::EXAMPLE_ID, Dates::YESTERDAY ); - $this->assertTrue($this->dashboard->has_widget(self::EXAMPLE_ID, Dates::TODAY)); - $this->assertTrue($this->dashboard->has_widget(self::EXAMPLE_ID, Dates::YESTERDAY)); - $this->assertFalse($this->dashboard->has_widget(self::EXAMPLE_ID, Dates::THIS_MONTH)); + $this->assertTrue( $this->dashboard->has_widget( self::EXAMPLE_ID, Dates::TODAY ) ); + $this->assertTrue( $this->dashboard->has_widget( self::EXAMPLE_ID, Dates::YESTERDAY ) ); + $this->assertFalse( $this->dashboard->has_widget( self::EXAMPLE_ID, Dates::THIS_MONTH ) ); } public function test_uninstall() { - $this->dashboard->toggle_widget(self::EXAMPLE_ID, Dates::TODAY); + $this->dashboard->toggle_widget( self::EXAMPLE_ID, Dates::TODAY ); - $this->assertNotEmpty($this->dashboard->get_widgets()); + $this->assertNotEmpty( $this->dashboard->get_widgets() ); $this->dashboard->uninstall(); - $this->assertSame(array(), $this->dashboard->get_widgets()); + $this->assertSame( array(), $this->dashboard->get_widgets() ); } - /** - * @dataProvider getValidWidgetProvider - */ - public function test_is_valid_widget($expected, $unique_id, $date) { - $widget = $this->dashboard->is_valid_widget($unique_id, $date); - $this->assertSame($expected, !empty($widget)); - if ($expected) { - $this->assertSame($widget['report']['uniqueId'], $unique_id); - $this->assertNotEmpty($widget['date']); - } + /** + * @dataProvider getValidWidgetProvider + */ + public function test_is_valid_widget( $expected, $unique_id, $date ) { + $widget = $this->dashboard->is_valid_widget( $unique_id, $date ); + $this->assertSame( $expected, ! empty( $widget ) ); + if ( $expected ) { + $this->assertSame( $widget['report']['uniqueId'], $unique_id ); + $this->assertNotEmpty( $widget['date'] ); + } } - public function getValidWidgetProvider() - { - return array( - [true, Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, Dates::TODAY], - [true, Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, Dates::LAST_WEEK], - [false, Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, 'foobar'], - - [true, self::EXAMPLE_ID, Dates::TODAY], - [true, 'DevicesDetection_getBrowsers', Dates::LAST_WEEK], - [false, 'foobar_baz', Dates::LAST_WEEK], - ); - } + public function getValidWidgetProvider() { + return array( + array( true, Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, Dates::TODAY ), + array( true, Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, Dates::LAST_WEEK ), + array( false, Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, 'foobar' ), + + array( true, self::EXAMPLE_ID, Dates::TODAY ), + array( true, 'DevicesDetection_getBrowsers', Dates::LAST_WEEK ), + array( false, 'foobar_baz', Dates::LAST_WEEK ), + ); + } } diff --git a/tests/phpunit/wpmatomo/admin/test-install.php b/tests/phpunit/wpmatomo/admin/test-install.php index 28757082a..2a7d6723b 100644 --- a/tests/phpunit/wpmatomo/admin/test-install.php +++ b/tests/phpunit/wpmatomo/admin/test-install.php @@ -12,53 +12,56 @@ class AdminInstallTest extends MatomoUnit_TestCase { /** * @var \WpMatomo\RedirectOnActivation */ - private $_redirect; + private $redirect; - public function setUp() { - parent::setUp(); + public function setUp() { + parent::setUp(); - $this->_redirect = new RedirectOnActivation(); + $this->redirect = new RedirectOnActivation(); - wp_get_current_user()->add_role( Roles::ROLE_SUPERUSER ); + wp_get_current_user()->add_role( Roles::ROLE_SUPERUSER ); - $this->assume_admin_page(); - } + $this->assume_admin_page(); + } - public function tearDown() { - $this->reset_roles(); - parent::tearDown(); - } + public function tearDown() { + $this->reset_roles(); + parent::tearDown(); + } - public function test_redirect_to_getting_started() { - // load the options of the wpmatomo object. otherwise it's another instance and updating configuration will do nothing - $settings = $this->_redirect::$settings; - $original_show_get_started_page = $settings->get_global_option( Settings::SHOW_GET_STARTED_PAGE); - $original_tracking_mode = $settings->get_global_option('track_mode'); - $is_multi = isset($_GET['activate-multi']) ? $_GET['activate-multi'] : false; - unset($_GET['activate-multi']); - // show starting page is disabled - $settings->set_global_option(Settings::SHOW_GET_STARTED_PAGE, 0); - $settings->save(); - $this->assertFalse($this->_redirect->redirect_to_getting_started()); - // show starting page is enabled but track mode is disabled - $settings->set_global_option(Settings::SHOW_GET_STARTED_PAGE, 1); - $settings->set_global_option('track_mode', TrackingSettings::TRACK_MODE_DISABLED); - $settings->save(); - $this->assertTrue($this->_redirect->redirect_to_getting_started()); - // show getting started and track mode different of disabled - $settings->set_global_option('track_mode', TrackingSettings::TRACK_MODE_DEFAULT); - $settings->save(); - $this->assertFalse($this->_redirect->redirect_to_getting_started()); - $_GET['activate-multi'] = true; - $this->assertFalse($this->_redirect->redirect_to_getting_started()); - // restore initial configuration - $settings->set_global_option('track_mode', $original_tracking_mode); - $settings->set_global_option(Settings::SHOW_GET_STARTED_PAGE, $original_show_get_started_page); - if ($is_multi !== false) { - $_GET['activate-multi'] = $is_multi; - } else { - unset($_GET['activate-multi']); - } - $settings->save(); - } -} \ No newline at end of file + /** + * phpcs:disable WordPress.Security.ValidatedSanitizedInput + */ + public function testredirect_to_getting_started() { + // load the options of the wpmatomo object. otherwise it's another instance and updating configuration will do nothing + $settings = $this->redirect::$settings; + $original_show_get_started_page = $settings->get_global_option( Settings::SHOW_GET_STARTED_PAGE ); + $original_tracking_mode = $settings->get_global_option( 'track_mode' ); + $is_multi = isset( $_GET['activate-multi'] ) ? $_GET['activate-multi'] : false; + unset( $_GET['activate-multi'] ); + // show starting page is disabled + $settings->set_global_option( Settings::SHOW_GET_STARTED_PAGE, 0 ); + $settings->save(); + $this->assertFalse( $this->redirect->redirect_to_getting_started() ); + // show starting page is enabled but track mode is disabled + $settings->set_global_option( Settings::SHOW_GET_STARTED_PAGE, 1 ); + $settings->set_global_option( 'track_mode', TrackingSettings::TRACK_MODE_DISABLED ); + $settings->save(); + $this->assertTrue( $this->redirect->redirect_to_getting_started() ); + // show getting started and track mode different of disabled + $settings->set_global_option( 'track_mode', TrackingSettings::TRACK_MODE_DEFAULT ); + $settings->save(); + $this->assertFalse( $this->redirect->redirect_to_getting_started() ); + $_GET['activate-multi'] = true; + $this->assertFalse( $this->redirect->redirect_to_getting_started() ); + // restore initial configuration + $settings->set_global_option( 'track_mode', $original_tracking_mode ); + $settings->set_global_option( Settings::SHOW_GET_STARTED_PAGE, $original_show_get_started_page ); + if ( false !== $is_multi ) { + $_GET['activate-multi'] = $is_multi; + } else { + unset( $_GET['activate-multi'] ); + } + $settings->save(); + } +} diff --git a/tests/phpunit/wpmatomo/admin/test-summary.php b/tests/phpunit/wpmatomo/admin/test-summary.php index 0268c42bc..7d8eeff18 100644 --- a/tests/phpunit/wpmatomo/admin/test-summary.php +++ b/tests/phpunit/wpmatomo/admin/test-summary.php @@ -55,49 +55,53 @@ public function test_show_renders_ui_when_tracking_enabled() { $this->assertNotContains( 'is not enabled', $output ); } - public function test_show_pin_widget() - { - $dashboard = new \WpMatomo\Admin\Dashboard(); - $this->assertSame( [], $dashboard->get_widgets() ); - - $_GET = array( - 'pin' => "1", - 'report_date' => Dates::YESTERDAY, - 'report_uniqueid' => Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, - ); - $_REQUEST['_wpnonce'] = wp_create_nonce( Summary::NONCE_DASHBOARD ); - $_SERVER['REQUEST_URI'] = home_url(); - - ob_start(); - $this->summary->show(); - $output = ob_get_clean(); - - $this->assertSame( [ - ['unique_id' => 'visits_over_time','date' => 'yesterday'] - ], $dashboard->get_widgets() ); - - $this->assertContains('Dashboard updated.', $output); - } - - public function test_show_wont_pin_widget_when_invalid_report() - { - $dashboard = new \WpMatomo\Admin\Dashboard(); - $this->assertSame( [], $dashboard->get_widgets() ); - - $_GET = array( - 'pin' => "1", - 'report_date' => 'foo', - 'report_uniqueid' => Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, - ); - $_REQUEST['_wpnonce'] = wp_create_nonce( Summary::NONCE_DASHBOARD ); - $_SERVER['REQUEST_URI'] = home_url(); - - ob_start(); - $this->summary->show(); - $output = ob_get_clean(); - - $this->assertSame( [], $dashboard->get_widgets() ); - - $this->assertNotContains('Dashboard updated.', $output); - } + public function test_show_pin_widget() { + $dashboard = new \WpMatomo\Admin\Dashboard(); + $this->assertSame( array(), $dashboard->get_widgets() ); + + $_GET = array( + 'pin' => '1', + 'report_date' => Dates::YESTERDAY, + 'report_uniqueid' => Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, + ); + $_REQUEST['_wpnonce'] = wp_create_nonce( Summary::NONCE_DASHBOARD ); + $_SERVER['REQUEST_URI'] = home_url(); + + ob_start(); + $this->summary->show(); + $output = ob_get_clean(); + + $this->assertSame( + array( + array( + 'unique_id' => 'visits_over_time', + 'date' => 'yesterday', + ), + ), + $dashboard->get_widgets() + ); + + $this->assertContains( 'Dashboard updated.', $output ); + } + + public function test_show_wont_pin_widget_when_invalid_report() { + $dashboard = new \WpMatomo\Admin\Dashboard(); + $this->assertSame( array(), $dashboard->get_widgets() ); + + $_GET = array( + 'pin' => '1', + 'report_date' => 'foo', + 'report_uniqueid' => Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME, + ); + $_REQUEST['_wpnonce'] = wp_create_nonce( Summary::NONCE_DASHBOARD ); + $_SERVER['REQUEST_URI'] = home_url(); + + ob_start(); + $this->summary->show(); + $output = ob_get_clean(); + + $this->assertSame( array(), $dashboard->get_widgets() ); + + $this->assertNotContains( 'Dashboard updated.', $output ); + } } diff --git a/tests/phpunit/wpmatomo/admin/test-systemreport.php b/tests/phpunit/wpmatomo/admin/test-systemreport.php index 64bcde833..e80040e58 100644 --- a/tests/phpunit/wpmatomo/admin/test-systemreport.php +++ b/tests/phpunit/wpmatomo/admin/test-systemreport.php @@ -6,7 +6,16 @@ use WpMatomo\Admin\SystemReport; use WpMatomo\Roles; use WpMatomo\Settings; - +/** + * We want a real data, not something coming from cache + * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + * + * We cannot use parameters of statements as this is the table names we build + * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + * phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + * phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + * phpcs:disable WordPress.DB.DirectDatabaseQuery.SchemaChange + */ class AdminSystemReportTest extends MatomoAnalytics_TestCase { /** @@ -20,6 +29,7 @@ class AdminSystemReportTest extends MatomoAnalytics_TestCase { private $settings; /** * Required for test_get_missing_tables + * * @see AdminSystemReportTest::test_get_missing_tables() * @var bool */ @@ -82,6 +92,7 @@ public function get_trouble_shooting_data() { } public function test_not_compatible_plugins_are_mentioned_in_faq() { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $contents = file_get_contents( 'https://matomo.org/faq/wordpress/which-plugins-is-matomo-for-wordpress-known-to-be-not-compatible-with/' ); foreach ( $this->report->get_not_compatible_plugins() as $not_compatible_plugin ) { @@ -96,20 +107,20 @@ private function fake_request( $field ) { } public function test_get_missing_tables_should_return_empty_array_when_all_tables_exist() { - $this->assertSame( [], $this->report->get_missing_tables() ); + $this->assertSame( array(), $this->report->get_missing_tables() ); } public function test_get_missing_tables_should_return_the_missing_tables() { global $wpdb; - $old_table_name = $this->report->dbSettings->prefix_table_name('site'); + $old_table_name = $this->report->db_settings->prefix_table_name( 'site' ); $new_table_name = $old_table_name . '_bkp'; - $wpdb->query("ALTER TABLE $old_table_name RENAME $new_table_name" ); + $wpdb->query( "ALTER TABLE $old_table_name RENAME $new_table_name" ); $missing_tables = $this->report->get_missing_tables(); $this->assertCount( 1, $missing_tables ); - $this->assertSame( [$old_table_name], array_values( $missing_tables ) ); + $this->assertSame( array( $old_table_name ), array_values( $missing_tables ) ); - $wpdb->query("ALTER TABLE $new_table_name RENAME $old_table_name" ); + $wpdb->query( "ALTER TABLE $new_table_name RENAME $old_table_name" ); } } diff --git a/tests/phpunit/wpmatomo/db/test-wordpress.php b/tests/phpunit/wpmatomo/db/test-wordpress.php index 8c4c672ea..bd8924d3b 100644 --- a/tests/phpunit/wpmatomo/db/test-wordpress.php +++ b/tests/phpunit/wpmatomo/db/test-wordpress.php @@ -28,20 +28,18 @@ public function setUp() { $this->insert_many_values(); } - public function test_listTables() - { - // we needed to overwrite this method as Zend uses by default getConnection() which we don't support. + public function test_listTables() { + // we needed to overwrite this method as Zend uses by default getConnection() which we don't support. $tables = $this->db->listTables(); - $this->assertTrue(is_array($tables)); + $this->assertTrue( is_array( $tables ) ); } - public function test_describeTable() - { + public function test_describeTable() { $tables = $this->db->listTables(); // we needed to overwrite this method as Zend uses by default getConnection() which we don't support. - $tables = $this->db->describeTable($tables[0]); - $this->assertTrue(is_array($tables)); - $this->assertNotEmpty($tables); + $tables = $this->db->describeTable( $tables[0] ); + $this->assertTrue( is_array( $tables ) ); + $this->assertNotEmpty( $tables ); } /** @@ -53,23 +51,26 @@ public function test_query_triggers_error_when_wrong_sql() { } public function test_query_handles_null_values() { - $table = Common::prefixTable( 'log_action' ); + $table = Common::prefixTable( 'log_action' ); $this->db->query( - 'INSERT INTO '.$table.' (name, hash, type, url_prefix) VALUES (?,CRC32(?),?,?)', - array('myname', 'myname', 2, null) + 'INSERT INTO ' . $table . ' (name, hash, type, url_prefix) VALUES (?,CRC32(?),?,?)', + array( 'myname', 'myname', 2, null ) ); - $all = $this->db->fetchAll('select * from ' . $table); + $all = $this->db->fetchAll( 'select * from ' . $table ); - $this->assertSame(array( + $this->assertSame( array( - 'idaction' => '1', - 'name' => 'myname', - 'hash' => '2383257219', - 'type' => '2', - 'url_prefix' => null - ) - ), $all); + array( + 'idaction' => '1', + 'name' => 'myname', + 'hash' => '2383257219', + 'type' => '2', + 'url_prefix' => null, + ), + ), + $all + ); } public function test_query_detects_error_code() { @@ -77,25 +78,24 @@ public function test_query_detects_error_code() { $this->db->query( 'SELECT * from foobarbaz;' ); - $this->fail('Expected exception not thrown'); - } catch (Zend_Db_Exception $e) { - $this->assertContains('[1146]', $e->getMessage()); - $this->assertTrue($this->db->isErrNo($e, 1146)); - $this->assertFalse($this->db->isErrNo($e, 1145)); - $this->assertFalse($this->db->isErrNo($e, 1147)); + $this->fail( 'Expected exception not thrown' ); + } catch ( Zend_Db_Exception $e ) { + $this->assertContains( '[1146]', $e->getMessage() ); + $this->assertTrue( $this->db->isErrNo( $e, 1146 ) ); + $this->assertFalse( $this->db->isErrNo( $e, 1145 ) ); + $this->assertFalse( $this->db->isErrNo( $e, 1147 ) ); } - // make sure when there are two errors on same connection the correct error code is used... try { - $table = Common::prefixTable( 'user' ); + $table = Common::prefixTable( 'user' ); $this->db->query( 'SELECT bar from ' . $table ); - $this->fail('Expected exception not thrown 2'); - } catch (Zend_Db_Exception $e) { - $this->assertContains('[1054]', $e->getMessage()); - $this->assertTrue($this->db->isErrNo($e, 1054)); + $this->fail( 'Expected exception not thrown 2' ); + } catch ( Zend_Db_Exception $e ) { + $this->assertContains( '[1054]', $e->getMessage() ); + $this->assertTrue( $this->db->isErrNo( $e, 1054 ) ); } } diff --git a/tests/phpunit/wpmatomo/db/test-wordpresstracker.php b/tests/phpunit/wpmatomo/db/test-wordpresstracker.php index be43ed01b..40c64eb25 100644 --- a/tests/phpunit/wpmatomo/db/test-wordpresstracker.php +++ b/tests/phpunit/wpmatomo/db/test-wordpresstracker.php @@ -39,23 +39,26 @@ public function test_query_triggers_error_when_wrong_sql() { } public function test_query_handles_null_values() { - $table = Common::prefixTable( 'log_action' ); + $table = Common::prefixTable( 'log_action' ); $this->db->query( - 'INSERT INTO '.$table.' (name, hash, type, url_prefix) VALUES (?,CRC32(?),?,?)', - array('myname', 'myname', 2, null) + 'INSERT INTO ' . $table . ' (name, hash, type, url_prefix) VALUES (?,CRC32(?),?,?)', + array( 'myname', 'myname', 2, null ) ); - $all = $this->db->fetchAll('select * from ' . $table); + $all = $this->db->fetchAll( 'select * from ' . $table ); - $this->assertSame(array( + $this->assertSame( array( - 'idaction' => '1', - 'name' => 'myname', - 'hash' => '2383257219', - 'type' => '2', - 'url_prefix' => null - ) - ), $all); + array( + 'idaction' => '1', + 'name' => 'myname', + 'hash' => '2383257219', + 'type' => '2', + 'url_prefix' => null, + ), + ), + $all + ); } public function test_query_can_execute_select_queries() { @@ -104,22 +107,22 @@ public function test_query_detects_error_code() { $this->db->query( 'SELECT * from foobarbaz;' ); - $this->fail('Expected exception not thrown'); - } catch (Zend_Db_Exception $e) { - $this->assertContains('[1146]', $e->getMessage()); - $this->assertTrue($this->db->isErrNo($e, 1146)); - $this->assertFalse($this->db->isErrNo($e, 1145)); - $this->assertFalse($this->db->isErrNo($e, 1147)); + $this->fail( 'Expected exception not thrown' ); + } catch ( Zend_Db_Exception $e ) { + $this->assertContains( '[1146]', $e->getMessage() ); + $this->assertTrue( $this->db->isErrNo( $e, 1146 ) ); + $this->assertFalse( $this->db->isErrNo( $e, 1145 ) ); + $this->assertFalse( $this->db->isErrNo( $e, 1147 ) ); } // make sure when there are two errors on same connection the correct error code is used... try { - $table = Common::prefixTable( 'user' ); + $table = Common::prefixTable( 'user' ); $this->db->query( 'SELECT bar from ' . $table ); - $this->fail('Expected exception not thrown 2'); - } catch (Zend_Db_Exception $e) { + $this->fail( 'Expected exception not thrown 2' ); + } catch ( Zend_Db_Exception $e ) { $this->assertContains( '[1054]', $e->getMessage() ); $this->assertTrue( $this->db->isErrNo( $e, 1054 ) ); } diff --git a/tests/phpunit/wpmatomo/ecommerce/test-base.php b/tests/phpunit/wpmatomo/ecommerce/test-base.php new file mode 100644 index 000000000..1667c7a5a --- /dev/null +++ b/tests/phpunit/wpmatomo/ecommerce/test-base.php @@ -0,0 +1,51 @@ +settings = new Settings(); + + /* + * use a custom object which provide public methods of the Base class + */ + $this->base = new MatomoTestEcommerce( new AjaxTracker( $this->settings ) ); + } + + public function test_wrap_script_on_set_ecommerce_view() { + $this->settings->apply_tracking_related_changes( + array( + 'track_mode' => TrackingSettings::TRACK_MODE_DEFAULT, + 'track_ecommerce' => true, + ) + ); + + $params = array( + 'setEcommerceView', + 'sku', + 'product-title', + array(), + 50, + ); + + $this->assertSame( + '' . PHP_EOL, + $this->base->wrap_script( $this->base->make_matomo_js_tracker_call( $params ) ) + ); + } +} diff --git a/tests/phpunit/wpmatomo/report/test-renderer.php b/tests/phpunit/wpmatomo/report/test-renderer.php index 72d677f1e..9c12525bc 100644 --- a/tests/phpunit/wpmatomo/report/test-renderer.php +++ b/tests/phpunit/wpmatomo/report/test-renderer.php @@ -48,16 +48,16 @@ public function test_render_report_with_dimension_with_data() { $this->assertContains( '

    ', $report ); } - public function test_show_visits_over_time() { - $local_tracker = $this->make_local_tracker( gmdate( 'Y-m-d H:i:s' ) ); - $this->assert_tracking_response( $local_tracker->doTrackPageView( 'test' ) ); + public function test_show_visits_over_time() { + $local_tracker = $this->make_local_tracker( gmdate( 'Y-m-d H:i:s' ) ); + $this->assert_tracking_response( $local_tracker->doTrackPageView( 'test' ) ); - $this->enable_browser_archiving(); + $this->enable_browser_archiving(); - $report = do_shortcode( '[matomo_report unique_id='.\WpMatomo\Report\Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME.' limit=18]' ); - $this->assertContains( '
    :'; $selected_container_ids = $settings->get_global_option( 'tagmanger_container_ids' ); foreach ( $containers as $container_id => $container_name ) { - echo ' ID:' . esc_html( $container_id ) . ' Name: ' . esc_html( $container_name ) . '  
    '; + echo ' ID:' . esc_html( $container_id ) . ' Name: ' . esc_html( $container_name ) . '  
    '; } - echo '

    Edit containers '; + echo '

    Edit containers '; echo '
    For Matomo to track you will need to add a Matomo Tag to the container. It otherwise won\'t track automatically.'; echo '
    :'; $matomo_filter = $settings->get_global_option( 'add_post_annotations' ); - foreach ( get_post_types( array(), 'objects' ) as $post_type ) { - echo 'name ] ) && $matomo_filter [ $post_type->name ] ? 'checked="checked" ' : '' ) . 'value="1" name="matomo[add_post_annotations][' . $post_type->name . ']" /> ' . $post_type->label . '   '; + foreach ( get_post_types( [], 'objects' ) as $object_post_type ) { + echo 'name ] ) && $matomo_filter [ $object_post_type->name ] ? 'checked="checked" ' : '' ) . 'value="1" name="matomo[add_post_annotations][' . esc_attr( $object_post_type->name ) . ']" /> ' . esc_html( $object_post_type->label ) . '   '; } echo '
    Pageviews
    assertCount(19, $parts); - } + $report = do_shortcode( '[matomo_report unique_id=' . \WpMatomo\Report\Renderer::CUSTOM_UNIQUE_ID_VISITS_OVER_TIME . ' limit=18]' ); + $this->assertContains( '
    assertCount( 19, $parts ); + } } diff --git a/tests/phpunit/wpmatomo/site/sync/test-syncconfig.php b/tests/phpunit/wpmatomo/site/sync/test-syncconfig.php index 66773af73..49ea2e081 100644 --- a/tests/phpunit/wpmatomo/site/sync/test-syncconfig.php +++ b/tests/phpunit/wpmatomo/site/sync/test-syncconfig.php @@ -1,5 +1,4 @@ set_assume_is_network_enabled_in_tests(true); + $settings = new Settings(); + if ( is_multisite() ) { + $settings->set_assume_is_network_enabled_in_tests( true ); } $this->sync_config = new Sync\SyncConfig( $settings ); } public function test_get_config_value_no_value_set() { - $val = $this->sync_config->get_config_value('Geenral', 'foo'); - $this->assertNull($val); + $val = $this->sync_config->get_config_value( 'Geenral', 'foo' ); + $this->assertNull( $val ); } public function test_set_config_value_get_config_value_string() { - $this->sync_config->set_config_value('General', 'foo', 'bar'); + $this->sync_config->set_config_value( 'General', 'foo', 'bar' ); - $val = $this->sync_config->get_config_value('General', 'foo'); - $this->assertSame('bar', $val); + $val = $this->sync_config->get_config_value( 'General', 'foo' ); + $this->assertSame( 'bar', $val ); } public function test_set_config_value_get_config_value_array() { - $this->sync_config->set_config_value('General', 'foo', array( - 'baz', - 'bar' - )); - - $val = $this->sync_config->get_config_value('General', 'foo'); - $this->assertEquals(['baz', 'bar'], $val); + $this->sync_config->set_config_value( + 'General', + 'foo', + array( + 'baz', + 'bar', + ) + ); + + $val = $this->sync_config->get_config_value( 'General', 'foo' ); + $this->assertEquals( array( 'baz', 'bar' ), $val ); } public function test_sync_config_for_current_site_when_no_config_set() { $sync = $this->sync_config->sync_config_for_current_site(); - $this->assertNull($sync); + $this->assertNull( $sync ); } /** * @group ms-required */ public function test_sync_config_for_current_site_when_config_set() { - - $this->sync_config->set_config_value('General', 'foo', array( - 'baz', - 'bar' - )); + $this->sync_config->set_config_value( + 'General', + 'foo', + array( + 'baz', + 'bar', + ) + ); $general = \Piwik\Config::getInstance()->General; - $this->assertTrue(empty($general['foo'])); + $this->assertTrue( empty( $general['foo'] ) ); $this->sync_config->sync_config_for_current_site(); $general = \Piwik\Config::getInstance()->General; - $this->assertEquals(['baz', 'bar'], $general['foo']); + $this->assertEquals( array( 'baz', 'bar' ), $general['foo'] ); } /** * @group ms-required */ public function test_sync_config_for_current_site_when_multiple_values() { - - $this->sync_config->set_config_value('General', 'foo', array('baz','bar')); - $this->sync_config->set_config_value('NewCategory', 'bar', 'baz'); - $this->sync_config->set_config_value('NewCategory', 'hello', 'world'); + $this->sync_config->set_config_value( 'General', 'foo', array( 'baz', 'bar' ) ); + $this->sync_config->set_config_value( 'NewCategory', 'bar', 'baz' ); + $this->sync_config->set_config_value( 'NewCategory', 'hello', 'world' ); $general = \Piwik\Config::getInstance()->General; - $this->assertTrue(empty($general['foo'])); - $newCategory = \Piwik\Config::getInstance()->NewCategory; - $this->assertEmpty($newCategory); + $this->assertTrue( empty( $general['foo'] ) ); + $new_category = \Piwik\Config::getInstance()->NewCategory; + $this->assertEmpty( $new_category ); $this->sync_config->sync_config_for_current_site(); $general = \Piwik\Config::getInstance()->General; - $this->assertEquals(['baz', 'bar'], $general['foo']); + $this->assertEquals( array( 'baz', 'bar' ), $general['foo'] ); - $newCategory = \Piwik\Config::getInstance()->NewCategory; - $this->assertEquals(['bar' => 'baz', 'hello' => 'world'], $newCategory); + $new_category = \Piwik\Config::getInstance()->NewCategory; + $this->assertEquals( + array( + 'bar' => 'baz', + 'hello' => 'world', + ), + $new_category + ); // now we change one key - $this->sync_config->set_config_value('NewCategory', 'bar', ''); + $this->sync_config->set_config_value( 'NewCategory', 'bar', '' ); $this->sync_config->sync_config_for_current_site(); - $newCategory = \Piwik\Config::getInstance()->NewCategory; - $this->assertEquals(['bar' => '', 'hello' => 'world'], $newCategory); - + $new_category = \Piwik\Config::getInstance()->NewCategory; + $this->assertEquals( + array( + 'bar' => '', + 'hello' => 'world', + ), + $new_category + ); } } diff --git a/tests/phpunit/wpmatomo/test-api.php b/tests/phpunit/wpmatomo/test-api.php index fcf8decd5..99568707b 100644 --- a/tests/phpunit/wpmatomo/test-api.php +++ b/tests/phpunit/wpmatomo/test-api.php @@ -61,9 +61,9 @@ public function test_dispatch_matomo_api_must_use_correct_method() { $request = new WP_REST_Request( 'POST', '/' . API::VERSION . '/api/matomo_version' ); $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); // some newer wp versions have a dot at the end - $data['message'] = trim($data['message'], '.'); + $data['message'] = trim( $data['message'], '.' ); $this->assertEquals( array( 'code' => 'rest_no_route', diff --git a/tests/phpunit/wpmatomo/test-capabilities.php b/tests/phpunit/wpmatomo/test-capabilities.php index e35aaa5af..bbb762d63 100644 --- a/tests/phpunit/wpmatomo/test-capabilities.php +++ b/tests/phpunit/wpmatomo/test-capabilities.php @@ -8,6 +8,13 @@ use WpMatomo\Settings; class TestMatomoCapabilities extends Capabilities { + /** + * @param $cap_to_find + * @param $allcaps + * + * @return bool + */ + // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found public function has_any_higher_permission( $cap_to_find, $allcaps ) { return parent::has_any_higher_permission( $cap_to_find, $allcaps ); } diff --git a/tests/phpunit/wpmatomo/test-install.php b/tests/phpunit/wpmatomo/test-install.php index 07305cae3..9762e4fe4 100644 --- a/tests/phpunit/wpmatomo/test-install.php +++ b/tests/phpunit/wpmatomo/test-install.php @@ -25,8 +25,8 @@ class InstallTest extends MatomoAnalytics_TestCase { public function setUp() { parent::setUp(); - $this->installer = $this->make_installer(); - $this->uninstaller = new Uninstaller(); + $this->installer = $this->make_installer(); + $this->uninstaller = new Uninstaller(); } private function make_installer() { @@ -50,11 +50,11 @@ public function test_install_adds_sites_and_users() { $sites_model = new SitesModel(); $all_sites = $sites_model->getAllSites(); - $install_date = get_option(Installer::OPTION_NAME_INSTALL_DATE); + $install_date = get_option( Installer::OPTION_NAME_INSTALL_DATE ); // sets install date - $this->assertTrue(time() - 600 < $install_date); - $this->assertTrue(time() >= $install_date); + $this->assertTrue( time() - 600 < $install_date ); + $this->assertTrue( time() >= $install_date ); unset( $all_sites[0]['ts_created'] ); $this->assertEquals( diff --git a/tests/phpunit/wpmatomo/test-matomo.php b/tests/phpunit/wpmatomo/test-matomo.php index 8f0669d58..684654acb 100644 --- a/tests/phpunit/wpmatomo/test-matomo.php +++ b/tests/phpunit/wpmatomo/test-matomo.php @@ -4,7 +4,6 @@ * * @package matomo */ - class MatomoTest extends MatomoUnit_TestCase { public function test_matomo_has_compatible_content_dir() { diff --git a/tests/phpunit/wpmatomo/test-optout.php b/tests/phpunit/wpmatomo/test-optout.php index e96c79bb6..c62ead9e4 100644 --- a/tests/phpunit/wpmatomo/test-optout.php +++ b/tests/phpunit/wpmatomo/test-optout.php @@ -7,28 +7,30 @@ */ class OptOutTest extends MatomoAnalytics_TestCase { - public function setUp() { - parent::setUp(); - } - public function test_matomo_opt_out_no_options() { $result = do_shortcode( PrivacySettings::EXAMPLE_MINIMAL ); - $this->assertSame( '

    You may choose to prevent this website from aggregating and analyzing the actions you take here. Doing so will protect your privacy, but will also prevent the owner from learning from your actions and creating a better experience for you and other users.

    + $this->assertSame( + '

    You may choose to prevent this website from aggregating and analyzing the actions you take here. Doing so will protect your privacy, but will also prevent the owner from learning from your actions and creating a better experience for you and other users.

    ', $result ); + ', + $result + ); } public function test_matomo_opt_out_all_options() { $result = do_shortcode( PrivacySettings::EXAMPLE_FULL ); - $this->assertSame( '

    Sie haben die Möglichkeit zu verhindern, dass von Ihnen hier getätigte Aktionen analysiert und verknüpft werden. Dies wird Ihre Privatsphäre schützen, aber wird auch den Besitzer daran hindern, aus Ihren Aktionen zu lernen und die Bedienbarkeit für Sie und andere Benutzer zu verbessern.

    + $this->assertSame( + '

    Sie haben die Möglichkeit zu verhindern, dass von Ihnen hier getätigte Aktionen analysiert und verknüpft werden. Dies wird Ihre Privatsphäre schützen, aber wird auch den Besitzer daran hindern, aus Ihren Aktionen zu lernen und die Bedienbarkeit für Sie und andere Benutzer zu verbessern.

    ', $result ); + ', + $result + ); } public function test_optOutJs_exists() { diff --git a/tests/phpunit/wpmatomo/test-referral.php b/tests/phpunit/wpmatomo/test-referral.php index 518759a43..90a8ee812 100644 --- a/tests/phpunit/wpmatomo/test-referral.php +++ b/tests/phpunit/wpmatomo/test-referral.php @@ -14,7 +14,7 @@ class ReferralTest extends MatomoUnit_TestCase { private $time; - private $oneDayInSeconds = 86400; + private $one_day_in_seconds = 86400; public function setUp() { parent::setUp(); @@ -52,10 +52,10 @@ public function test_should_show_when_90_days_back() { $this->assertFalse( $this->referral->should_show() ); - $this->referral->set_time( 1584663656 + ( $this->oneDayInSeconds * 89.5 ) ); + $this->referral->set_time( 1584663656 + ( $this->one_day_in_seconds * 89.5 ) ); $this->assertFalse( $this->referral->should_show() ); - $this->referral->set_time( 1584663656 + ( $this->oneDayInSeconds * 90.2 ) ); + $this->referral->set_time( 1584663656 + ( $this->one_day_in_seconds * 90.2 ) ); $this->assertTrue( $this->referral->should_show() ); $this->referral->dismiss(); diff --git a/tests/phpunit/wpmatomo/test-release.php b/tests/phpunit/wpmatomo/test-release.php index 30cd946c6..e0412cb52 100644 --- a/tests/phpunit/wpmatomo/test-release.php +++ b/tests/phpunit/wpmatomo/test-release.php @@ -4,7 +4,6 @@ * * @package matomo */ - class ReleaseTest extends MatomoUnit_TestCase { /** @@ -17,6 +16,7 @@ public function test_assert_needed_files_exist( $file ) { public function test_stabletag_and_matomo_version_matches() { $plugin_data = get_plugin_data( MATOMO_ANALYTICS_FILE, $markup = false, $translate = false ); $version = $plugin_data['Version']; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $version; $txt = file_get_contents( plugin_dir_path( MATOMO_ANALYTICS_FILE ) . 'readme.txt' ); diff --git a/tests/phpunit/wpmatomo/test-scheduled-tasks.php b/tests/phpunit/wpmatomo/test-scheduled-tasks.php index cabbe1c80..857dc276a 100644 --- a/tests/phpunit/wpmatomo/test-scheduled-tasks.php +++ b/tests/phpunit/wpmatomo/test-scheduled-tasks.php @@ -15,6 +15,10 @@ use WpMatomo\Settings; use WpMatomo\Uninstaller; +/** + * Don't need remote access + * phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + */ class ScheduledTasksTest extends MatomoAnalytics_TestCase { /** @@ -32,7 +36,7 @@ public function setUp() { parent::setUp(); $this->settings = new Settings(); - $this->tasks = new ScheduledTasks( $this->settings ); + $this->tasks = new ScheduledTasks( $this->settings ); $this->tasks->schedule(); } @@ -54,24 +58,25 @@ public function test_sync_does_not_fail() { } public function test_disable_add_handler_wontfail_when_addhandler_enabled() { - $this->assertFalse($this->settings->should_disable_addhandler()); + $this->assertFalse( $this->settings->should_disable_addhandler() ); $this->tasks->disable_add_handler(); } public function test_disable_add_handler_wontfail_when_addhandler_disabled() { - $this->assertFalse($this->settings->should_disable_addhandler()); + $this->assertFalse( $this->settings->should_disable_addhandler() ); $this->settings->force_disable_addhandler = true; $this->tasks->disable_add_handler(); - $filename_to_check = dirname(MATOMO_ANALYTICS_FILE) . '/.htaccess'; - $this->assertContains('# AddHandler', file_get_contents($filename_to_check)); - $this->tasks->disable_add_handler($undo = true); - $this->assertNotContains('# AddHandler', file_get_contents($filename_to_check)); - $this->assertContains('AddHandler', file_get_contents($filename_to_check)); + $filename_to_check = dirname( MATOMO_ANALYTICS_FILE ) . '/.htaccess'; + $this->assertContains( '# AddHandler', file_get_contents( $filename_to_check ) ); + $undo = true; + $this->tasks->disable_add_handler( $undo ); + $this->assertNotContains( '# AddHandler', file_get_contents( $filename_to_check ) ); + $this->assertContains( 'AddHandler', file_get_contents( $filename_to_check ) ); $this->settings->force_disable_addhandler = false; } public function test_archive_does_not_fail() { - $this->assertEquals(array(), $this->tasks->archive()); + $this->assertEquals( array(), $this->tasks->archive() ); } public function test_set_last_time_before_cron() { diff --git a/tests/phpunit/wpmatomo/test-settings.php b/tests/phpunit/wpmatomo/test-settings.php index 4f10615e2..d4556335b 100644 --- a/tests/phpunit/wpmatomo/test-settings.php +++ b/tests/phpunit/wpmatomo/test-settings.php @@ -29,7 +29,7 @@ public function test_should_disable_addhandler() { public function test_should_disable_addhandler_forced() { $this->settings->force_disable_addhandler = true; - $disabled = $this->settings->should_disable_addhandler(); + $disabled = $this->settings->should_disable_addhandler(); $this->settings->force_disable_addhandler = false; $this->assertTrue( $disabled ); } diff --git a/tests/phpunit/wpmatomo/test-trackingcode.php b/tests/phpunit/wpmatomo/test-trackingcode.php index 752d4111a..8cc3670c2 100644 --- a/tests/phpunit/wpmatomo/test-trackingcode.php +++ b/tests/phpunit/wpmatomo/test-trackingcode.php @@ -8,6 +8,9 @@ use WpMatomo\Site; use WpMatomo\TrackingCode; +/** + * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + */ class TrackingCodeTest extends MatomoUnit_TestCase { /** @@ -91,7 +94,7 @@ public function test_tracking_enabled_adds_by_default_to_footer() { $footer = ob_get_clean(); $this->assertNotContains( 'idsite', $header ); - $this->assertContains( '\n", +g.type=\'text/javascript\'; g.async=true; g.src="\/\/example.org\/wp-content\/plugins\/matomo\/app\/matomo.js"; s.parentNode.insertBefore(g,s);' . "\n\n", $this->get_tracking_code() ); } @@ -76,14 +76,14 @@ public function test_get_tracking_code_when_using_default_tracking_code_using_re ); $this->assertSame( - '\n", +g.type=\'text/javascript\'; g.async=true; g.src="\/\/example.org\/index.php?rest_route=\/matomo\/v1\/hit\/"; s.parentNode.insertBefore(g,s);' . "\n\n", $this->get_tracking_code() ); } @@ -219,8 +219,8 @@ public function test_cookie_consent_tagmanager() { 'cookie_consent' => CookieConsent::REQUIRE_COOKIE_CONSENT, ) ); - $this->assertNotContains( "requireCookieConsent", $this->get_tracking_code() ); - $this->assertNotContains( "requireConsent", $this->get_tracking_code() ); + $this->assertNotContains( 'requireCookieConsent', $this->get_tracking_code() ); + $this->assertNotContains( 'requireConsent', $this->get_tracking_code() ); } public function test_cookie_consent_manually() { @@ -230,8 +230,8 @@ public function test_cookie_consent_manually() { 'cookie_consent' => CookieConsent::REQUIRE_COOKIE_CONSENT, ) ); - $this->assertNotContains( "requireCookieConsent", $this->get_tracking_code() ); - $this->assertNotContains( "requireConsent", $this->get_tracking_code() ); + $this->assertNotContains( 'requireCookieConsent', $this->get_tracking_code() ); + $this->assertNotContains( 'requireConsent', $this->get_tracking_code() ); } public function test_cookie_consent_none() { @@ -241,8 +241,8 @@ public function test_cookie_consent_none() { 'cookie_consent' => CookieConsent::REQUIRE_NONE, ) ); - $this->assertNotContains( "requireCookieConsent", $this->get_tracking_code() ); - $this->assertNotContains( "requireConsent", $this->get_tracking_code() ); + $this->assertNotContains( 'requireCookieConsent', $this->get_tracking_code() ); + $this->assertNotContains( 'requireConsent', $this->get_tracking_code() ); } public function test_cookie_consent_cookie() { diff --git a/tests/phpunit/wpmatomo/user/test-sync.php b/tests/phpunit/wpmatomo/user/test-sync.php index 249f08a43..8da0d088b 100644 --- a/tests/phpunit/wpmatomo/user/test-sync.php +++ b/tests/phpunit/wpmatomo/user/test-sync.php @@ -26,7 +26,7 @@ public function sync_users( $users, $idsite ) { parent::sync_users( $users, $idsite ); } } - + // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found public function ensure_user_exists( $wp_user ) { return parent::ensure_user_exists( $wp_user ); } @@ -88,23 +88,23 @@ public function test_sync_all_passes_correct_values_to_sync_site_when_there_are_ $idsite = $this->get_current_site_id(); - switch_to_blog($blogid1); - $user2 = get_user_by('login', 'admin'); + switch_to_blog( $blogid1 ); + $user2 = get_user_by( 'login', 'admin' ); restore_current_blog(); - switch_to_blog($blogid2); - $user3 = get_user_by('login', 'admin'); + switch_to_blog( $blogid2 ); + $user3 = get_user_by( 'login', 'admin' ); restore_current_blog(); $this->assertCount( 1, $this->mock->synced_users[0]['users'] ); unset( $this->mock->synced_users[0]['users'] ); - $this->assertCount( 1, $this->mock->synced_users[1]['users'] ); - $this->assertEquals( $user2->ID, $this->mock->synced_users[1]['users'][0]->ID ); - unset( $this->mock->synced_users[1]['users'] ); + $this->assertCount( 1, $this->mock->synced_users[1]['users'] ); + $this->assertEquals( $user2->ID, $this->mock->synced_users[1]['users'][0]->ID ); + unset( $this->mock->synced_users[1]['users'] ); - $this->assertCount( 1, $this->mock->synced_users[2]['users'] ); - $this->assertEquals( $user3->ID, $this->mock->synced_users[2]['users'][0]->ID ); - unset( $this->mock->synced_users[2]['users'] ); + $this->assertCount( 1, $this->mock->synced_users[2]['users'] ); + $this->assertEquals( $user3->ID, $this->mock->synced_users[2]['users'][0]->ID ); + unset( $this->mock->synced_users[2]['users'] ); $this->assertEquals( array(