1
0
Fork 0
mirror of synced 2024-06-27 02:31:04 +12:00

Merge branch '0.15.x' of https://github.com/appwrite/appwrite into origin/feat-internal-id

This commit is contained in:
Torsten Dittmann 2022-06-19 11:43:58 +02:00
commit 5a4a9e0739
46 changed files with 2006 additions and 1327 deletions

13
.gitpod.Dockerfile vendored
View file

@ -1,13 +0,0 @@
FROM gitpod/workspace-full
RUN sudo apt update
RUN sudo add-apt-repository ppa:ondrej/php -y
# Disable current PHP installation
RUN sudo a2dismod php7.4 mpm_prefork
# Install apache2 (PHP install requires to do this first) and php8.0
RUN sudo install-packages \
-o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" \
apache2 php8.0

View file

@ -1,11 +1,8 @@
image:
file: .gitpod.Dockerfile
tasks:
- name: Run Appwrite Docker Stack
init: |
docker-compose pull
docker-compose build
docker compose pull
docker compose build
command: |
docker run --rm --interactive --tty \
--volume $PWD:/app \

View file

@ -1,3 +1,5 @@
- Start using docker compose V2 (from `docker-compose` to `docker compose`)
# Version 0.14.2
## Features

View file

@ -131,6 +131,9 @@ ARG VERSION=dev
ARG DEBUG=false
ENV DEBUG=$DEBUG
ENV DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
ENV DOCKER_COMPOSE_VERSION=v2.5.0
ENV _APP_SERVER=swoole \
_APP_ENV=production \
_APP_LOCALE=en \
@ -232,12 +235,17 @@ RUN \
libmaxminddb-dev \
certbot \
docker-cli \
docker-compose \
libgomp \
&& docker-php-ext-install sockets opcache pdo_mysql \
&& apk del .deps \
&& rm -rf /var/cache/apk/*
RUN \
mkdir -p $DOCKER_CONFIG/cli-plugins \
&& ARCH=$(uname -m) && if [ $ARCH == "armv7l" ]; then $ARCH="armv7"; fi \
&& curl -SL https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-linux-$ARCH -o $DOCKER_CONFIG/cli-plugins/docker-compose \
&& chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
RUN \
if [ "$DEBUG" == "true" ]; then \
apk add boost boost-dev; \

View file

@ -59,7 +59,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.0
```
### Windows
@ -71,7 +71,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.0
```
#### PowerShell
@ -81,7 +81,7 @@ docker run -it --rm ,
--volume /var/run/docker.sock:/var/run/docker.sock ,
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
--entrypoint="install" ,
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.0
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。

View file

@ -62,7 +62,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.0
```
### Windows
@ -74,7 +74,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.0
```
#### PowerShell
@ -84,7 +84,7 @@ docker run -it --rm ,
--volume /var/run/docker.sock:/var/run/docker.sock ,
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
--entrypoint="install" ,
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.0
```
Once the Docker installation completes, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after installation completes.

View file

@ -836,7 +836,7 @@ $collections = [
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'default' => 0,
'array' => false,
'filters' => [],
],
@ -873,6 +873,17 @@ $collections = [
'array' => false,
'filters' => ['encrypt'],
],
[
'$id' => 'expire',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
@ -967,6 +978,17 @@ $collections = [
'array' => true,
'filters' => [],
],
[
'$id' => 'signatureKey',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[

View file

@ -30,7 +30,7 @@
"emails.certificate.subject": "Certificate failure for %s",
"emails.certificate.hello": "Hello",
"emails.certificate.body": "Certificate for your domain '{{domain}}' could not be generated. This is attempt no. {{attempt}}, and the failure was caused by: {{error}}",
"emails.certificate.footer": "Your previous certificate willl be valid for 30 days since the first failure. We highly recommend investigating this case, otherwise your domain will end up without a valid SSL communication.",
"emails.certificate.footer": "Your previous certificate will be valid for 30 days since the first failure. We highly recommend investigating this case, otherwise your domain will end up without a valid SSL communication.",
"emails.certificate.thanks": "Thanks",
"emails.certificate.signature": "{{project}} team",
"locale.country.unknown": "Unknown",
@ -235,4 +235,4 @@
"continents.na": "North America",
"continents.oc": "Oceania",
"continents.sa": "South America"
}
}

View file

@ -5,8 +5,8 @@
"emails.sender": "Équipe %s",
"emails.verification.subject": "Vérification du compte",
"emails.verification.hello": "Bonjour {{name}}",
"emails.verification.body": "Suivez ce lien pour vérifier votre adresse mail.",
"emails.verification.footer": "Si vous n'avez pas demandé à vérifier cette adresse mail, vous pouvez ignorer ce message.",
"emails.verification.body": "Suivez ce lien pour vérifier votre adresse e-mail.",
"emails.verification.footer": "Si vous n'avez pas demandé à vérifier cette adresse, vous pouvez ignorer ce message.",
"emails.verification.thanks": "Merci",
"emails.verification.signature": "Équipe {{project}}",
"emails.magicSession.subject": "Connexion",
@ -14,19 +14,25 @@
"emails.magicSession.body": "Suivez ce lien pour vous connecter.",
"emails.magicSession.footer": "Si vous n'avez pas demandé à vous connecter en utilisant cet e-mail, vous pouvez ignorer ce message.",
"emails.magicSession.thanks": "Merci",
"emails.magicSession.signature": "Réinitialisation du mot de passe",
"emails.recovery.subject": "Bonjour {{name}}",
"emails.magicSession.signature": "L'équipe {{project}}",
"emails.recovery.subject": "Réinitialisation du mot de passe",
"emails.recovery.hello": "Bonjour {{name}}",
"emails.recovery.body": "Suivez ce lien pour réinitialiser votre mot de passe de {{projet}}.",
"emails.recovery.body": "Suivez ce lien pour réinitialiser votre mot de passe pour {{projet}}.",
"emails.recovery.footer": "Si vous n'avez pas demandé à réinitialiser votre mot de passe, vous pouvez ignorer ce message.",
"emails.recovery.thanks": "Merci",
"emails.recovery.signature": "Équipe {{project}}",
"emails.recovery.signature": "L'équipe {{project}}",
"emails.invitation.subject": "Invitation à l'équipe %s de %s",
"emails.invitation.hello": "Bonjour",
"emails.invitation.body": "Ce mail vous a été envoyé parce que {{owner}} voulait vous inviter à devenir membre de l'équipe {{team}} de {{project}}.",
"emails.invitation.body": "Cet e-mail vous a été envoyé parce que {{owner}} souhaite vous inviter à devenir membre de l'équipe {{team}} pour {{project}}.",
"emails.invitation.footer": "Si vous n'êtes pas intéressé, vous pouvez ignorer ce message.",
"emails.invitation.thanks": "Merci",
"emails.invitation.signature": "Équipe {{project}}",
"emails.invitation.signature": "L'équipe {{project}}",
"emails.certificate.subject": "Échec du certificat pour %s",
"emails.certificate.hello": "Bonjour",
"emails.certificate.body": "Le certificate pour votre domaine '{{domain}}' n'a pas pu être généré. Ceci est la tentative {{tentative}} et l'échec a été causé par : {{erreur}}",
"emails.certificate.footer": "Votre certificat précédent sera valide pendant 30 jours à compter de la première défaillance. Nous vous recommandons fortement d'enquêter sur ce cas, sinon votre domaine se retrouvera sans communication SSL valide.",
"emails.certificate.thanks": "Merci",
"emails.certificate.signature": "L'équipe {{project}}",
"locale.country.unknown": "Inconnu",
"countries.af": "Afghanistan",
"countries.ao": "Angola",
@ -229,4 +235,4 @@
"continents.na": "Amérique du Nord",
"continents.oc": "Océanie",
"continents.sa": "Amérique du Sud"
}
}

View file

@ -61,6 +61,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false
],
'dailymotion' => [
'name' => 'Dailymotion',
'developers' => 'https://developers.dailymotion.com/api/',
'icon' => 'icon-dailymotion',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false
],
'discord' => [
'name' => 'Discord',
'developers' => 'https://discordapp.com/developers/docs/topics/oauth2',
@ -211,6 +221,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false,
],
'stripe' => [
'name' => 'Stripe',
'developers' => 'https://stripe.com/docs/api',
'icon' => 'icon-stripe',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false
],
'tradeshift' => [
'name' => 'Tradeshift',
'developers' => 'https://developers.tradeshift.com/docs/api',
@ -241,15 +261,15 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false,
],
'zoom' => [
'name' => 'Zoom',
'developers' => 'https://marketplace.zoom.us/docs/guides/auth/oauth/',
'icon' => 'icon-zoom',
'wordpress' => [
'name' => 'WordPress',
'developers' => 'https://developer.wordpress.com/docs/oauth2/',
'icon' => 'icon-wordpress',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
'mock' => false
],
'yahoo' => [
'name' => 'Yahoo',
@ -281,6 +301,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false,
],
'zoom' => [
'name' => 'Zoom',
'developers' => 'https://marketplace.zoom.us/docs/guides/auth/oauth/',
'icon' => 'icon-zoom',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
],
// 'instagram' => [
// 'name' => 'Instagram',
// 'developers' => 'https://www.instagram.com/developer/',
@ -297,26 +327,7 @@ return [ // Ordered by ABC.
// 'beta' => false,
// 'mock' => false,
// ],
'wordpress' => [
'name' => 'WordPress',
'developers' => 'https://developer.wordpress.com/docs/oauth2/',
'icon' => 'icon-wordpress',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false
],
'stripe' => [
'name' => 'Stripe',
'developers' => 'https://stripe.com/docs/api',
'icon' => 'icon-stripe',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false
],
// Keep Last
'mock' => [
'name' => 'Mock',

View file

@ -972,7 +972,7 @@ App::post('/v1/account/jwt')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_JWT)
->label('abuse-limit', 10)
->label('abuse-limit', 100)
->label('abuse-key', 'url:{url},userId:{userId}')
->inject('response')
->inject('user')

View file

@ -26,6 +26,7 @@ use Appwrite\Extend\Exception;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@ -582,6 +583,8 @@ App::post('/v1/projects/:projectId/webhooks')
$security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN);
$webhook = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
@ -593,6 +596,7 @@ App::post('/v1/projects/:projectId/webhooks')
'security' => $security,
'httpUser' => $httpUser,
'httpPass' => $httpPass,
'signatureKey' => \bin2hex(\random_bytes(64)),
]);
$webhook = $dbForConsole->createDocument('webhooks', $webhook);
@ -686,9 +690,10 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->param('signatureKey', null, new Text(256), 'Webhook signature key. Max length: 256 chars.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $webhookId, string $name, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) {
->action(function (string $projectId, string $webhookId, string $name, array $events, string $url, bool $security, string $httpUser, string $httpPass, string $signatureKey, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@ -716,8 +721,11 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->setAttribute('httpPass', $httpPass)
;
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
if (!empty($signatureKey)) {
$webhook->setAttribute('signatureKey', $signatureKey);
}
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
@ -775,9 +783,10 @@ App::post('/v1/projects/:projectId/keys')
->param('projectId', null, new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', 0, new Integer(), 'Key expiration time in Unix timestamp. Use 0 for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $name, array $scopes, Response $response, Database $dbForConsole) {
->action(function (string $projectId, string $name, array $scopes, int $expire, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@ -792,6 +801,7 @@ App::post('/v1/projects/:projectId/keys')
'projectId' => $project->getId(),
'name' => $name,
'scopes' => $scopes,
'expire' => $expire,
'secret' => \bin2hex(\random_bytes(128)),
]);
@ -882,9 +892,10 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('keyId', null, new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', 0, new Integer(), 'Key expiration time in Unix timestamp. Use 0 for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $keyId, string $name, array $scopes, Response $response, Database $dbForConsole) {
->action(function (string $projectId, string $keyId, string $name, array $scopes, int $expire, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@ -904,6 +915,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
$key
->setAttribute('name', $name)
->setAttribute('scopes', $scopes)
->setAttribute('expire', $expire)
;
$dbForConsole->updateDocument('keys', $key->getId(), $key);

View file

@ -279,6 +279,12 @@ App::init(function (App $utopia, Request $request, Response $response, Document
$role = Auth::USER_ROLE_APP;
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
$expire = $key->getAttribute('expire', 0);
if (!empty($expire) && $expire < \time()) {
throw new AppwriteException('Project key expired', 401, AppwriteException:: PROJECT_KEY_EXPIRED);
}
Authorization::setRole('role:' . Auth::USER_ROLE_APP);
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
}

View file

@ -434,7 +434,7 @@ App::post('/v1/execution')
->desc('Create an execution')
->param('runtimeId', '', new Text(64), 'The runtimeID to execute.')
->param('vars', [], new Assoc(), 'Environment variables required for the build.')
->param('data', '{}', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true)
->param('data', '', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true)
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.')
->inject('activeRuntimes')
->inject('response')

View file

@ -80,7 +80,7 @@ const APP_LIMIT_COMPRESSION = 20000000; //20MB
const APP_LIMIT_ARRAY_PARAMS_SIZE = 100; // Default maximum of how many elements can there be in API parameter that expects array value
const APP_LIMIT_SUBQUERY = 1000;
const APP_CACHE_BUSTER = 305;
const APP_VERSION_STABLE = '0.14.2';
const APP_VERSION_STABLE = '0.15.0';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@ -132,6 +132,7 @@ const DELETE_TYPE_CERTIFICATES = 'certificates';
const DELETE_TYPE_USAGE = 'usage';
const DELETE_TYPE_REALTIME = 'realtime';
const DELETE_TYPE_BUCKETS = 'buckets';
const DELETE_TYPE_SESSIONS = 'sessions';
// Mail Types
const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';

View file

@ -214,9 +214,9 @@ $cli
}
}
Console::log("Running \"docker-compose -f {$path}/docker-compose.yml up -d --remove-orphans --renew-anon-volumes\"");
Console::log("Running \"docker compose -f {$path}/docker-compose.yml up -d --remove-orphans --renew-anon-volumes\"");
$exit = Console::execute("${env} docker-compose -f {$path}/docker-compose.yml up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr);
$exit = Console::execute("${env} docker compose -f {$path}/docker-compose.yml up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr);
if ($exit !== 0) {
$message = 'Failed to install Appwrite dockers';

View file

@ -3,6 +3,7 @@
global $cli;
global $register;
use Appwrite\Auth\Auth;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Utopia\App;
@ -93,6 +94,14 @@ $cli
->trigger();
}
function notifyDeleteExpiredSessions()
{
(new Delete())
->setType(DELETE_TYPE_SESSIONS)
->setTimestamp(time() - Auth::TOKEN_EXPIRATION_LOGIN_LONG)
->trigger();
}
function renewCertificates($dbForConsole)
{
$time = date('d-m-Y H:i:s', time());
@ -136,6 +145,7 @@ $cli
notifyDeleteAuditLogs($auditLogRetention);
notifyDeleteUsageStats($usageStatsRetention30m, $usageStatsRetention1d);
notifyDeleteConnections();
notifyDeleteExpiredSessions();
renewCertificates($database);
}, $interval);
});

View file

@ -2,271 +2,131 @@
global $cli, $register;
use Appwrite\Stats\Usage;
use Appwrite\Stats\UsageDB;
use InfluxDB\Database as InfluxDatabase;
use Utopia\App;
use Utopia\Cache\Adapter\Redis;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Registry\Registry;
use Utopia\Logger\Log;
/**
* Metrics We collect
*
* General
*
* requests
* network
* executions
*
* Database
*
* database.collections.create
* database.collections.read
* database.collections.update
* database.collections.delete
* database.documents.create
* database.documents.read
* database.documents.update
* database.documents.delete
* database.collections.{collectionId}.documents.create
* database.collections.{collectionId}.documents.read
* database.collections.{collectionId}.documents.update
* database.collections.{collectionId}.documents.delete
*
* Storage
*
* storage.buckets.create
* storage.buckets.read
* storage.buckets.update
* storage.buckets.delete
* storage.files.create
* storage.files.read
* storage.files.update
* storage.files.delete
* storage.buckets.{bucketId}.files.create
* storage.buckets.{bucketId}.files.read
* storage.buckets.{bucketId}.files.update
* storage.buckets.{bucketId}.files.delete
*
* Users
*
* users.create
* users.read
* users.update
* users.delete
* users.sessions.create
* users.sessions.{provider}.create
* users.sessions.delete
*
* Functions
*
* functions.{functionId}.executions
* functions.{functionId}.failures
* functions.{functionId}.compute
*
* Counters
*
* users.count
* storage.buckets.count
* storage.files.count
* storage.buckets.{bucketId}.files.count
* database.collections.count
* database.documents.count
* database.collections.{collectionId}.documents.count
*
* Totals
*
* storage.total
*
*/
function getDatabase(Registry &$register, string $namespace): Database
{
$attempts = 0;
do {
try {
$attempts++;
$db = $register->get('db');
$redis = $register->get('cache');
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace($namespace);
if (!$database->exists($database->getDefaultDatabase(), 'projects')) {
throw new Exception('Projects collection not ready');
}
break; // leave loop if successful
} catch (\Exception$e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep(DATABASE_RECONNECT_SLEEP);
}
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
return $database;
}
function getInfluxDB(Registry &$register): InfluxDatabase
{
/** @var InfluxDB\Client $client */
$client = $register->get('influxdb');
$attempts = 0;
$max = 10;
$sleep = 1;
do { // check if telegraf database is ready
try {
$attempts++;
$database = $client->selectDB('telegraf');
if (in_array('telegraf', $client->listDatabases())) {
break; // leave the do-while if successful
}
} catch (\Throwable$th) {
Console::warning("InfluxDB not ready. Retrying connection ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('InfluxDB database not ready yet');
}
sleep($sleep);
}
} while ($attempts < $max);
return $database;
}
$logError = function (Throwable $error, string $action = 'syncUsageStats') use ($register) {
$logger = $register->get('logger');
if ($logger) {
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace("realtime");
$log->setServer(\gethostname());
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('detailedTrace', $error->getTrace());
$log->setAction($action);
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
$responseCode = $logger->addLog($log);
Console::info('Usage stats log pushed with status code: ' . $responseCode);
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
};
$cli
->task('usage')
->desc('Schedules syncing data from influxdb to Appwrite console db')
->action(function () use ($register) {
->action(function () use ($register, $logError) {
Console::title('Usage Aggregation V1');
Console::success(APP_NAME . ' usage aggregation process v1 has started');
$interval = (int) App::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '30'); // 30 seconds (by default)
$periods = [
[
'key' => '30m',
'startTime' => '-24 hours',
],
[
'key' => '1d',
'startTime' => '-90 days',
],
];
// all the metrics that we are collecting at the moment
$globalMetrics = [
'requests' => [
'table' => 'appwrite_usage_requests_all',
],
'network' => [
'table' => 'appwrite_usage_network_all',
],
'executions' => [
'table' => 'appwrite_usage_executions_all',
],
'database.collections.create' => [
'table' => 'appwrite_usage_database_collections_create',
],
'database.collections.read' => [
'table' => 'appwrite_usage_database_collections_read',
],
'database.collections.update' => [
'table' => 'appwrite_usage_database_collections_update',
],
'database.collections.delete' => [
'table' => 'appwrite_usage_database_collections_delete',
],
'database.documents.create' => [
'table' => 'appwrite_usage_database_documents_create',
],
'database.documents.read' => [
'table' => 'appwrite_usage_database_documents_read',
],
'database.documents.update' => [
'table' => 'appwrite_usage_database_documents_update',
],
'database.documents.delete' => [
'table' => 'appwrite_usage_database_documents_delete',
],
'database.collections.collectionId.documents.create' => [
'table' => 'appwrite_usage_database_documents_create',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.read' => [
'table' => 'appwrite_usage_database_documents_read',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.update' => [
'table' => 'appwrite_usage_database_documents_update',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.delete' => [
'table' => 'appwrite_usage_database_documents_delete',
'groupBy' => 'collectionId',
],
'storage.buckets.create' => [
'table' => 'appwrite_usage_storage_buckets_create',
],
'storage.buckets.read' => [
'table' => 'appwrite_usage_storage_buckets_read',
],
'storage.buckets.update' => [
'table' => 'appwrite_usage_storage_buckets_update',
],
'storage.buckets.delete' => [
'table' => 'appwrite_usage_storage_buckets_delete',
],
'storage.files.create' => [
'table' => 'appwrite_usage_storage_files_create',
],
'storage.files.read' => [
'table' => 'appwrite_usage_storage_files_read',
],
'storage.files.update' => [
'table' => 'appwrite_usage_storage_files_update',
],
'storage.files.delete' => [
'table' => 'appwrite_usage_storage_files_delete',
],
'storage.buckets.bucketId.files.create' => [
'table' => 'appwrite_usage_storage_files_create',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.read' => [
'table' => 'appwrite_usage_storage_files_read',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.update' => [
'table' => 'appwrite_usage_storage_files_update',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.delete' => [
'table' => 'appwrite_usage_storage_files_delete',
'groupBy' => 'bucketId',
],
'users.create' => [
'table' => 'appwrite_usage_users_create',
],
'users.read' => [
'table' => 'appwrite_usage_users_read',
],
'users.update' => [
'table' => 'appwrite_usage_users_update',
],
'users.delete' => [
'table' => 'appwrite_usage_users_delete',
],
'users.sessions.create' => [
'table' => 'appwrite_usage_users_sessions_create',
],
'users.sessions.provider.create' => [
'table' => 'appwrite_usage_users_sessions_create',
'groupBy' => 'provider',
],
'users.sessions.delete' => [
'table' => 'appwrite_usage_users_sessions_delete',
],
'functions.functionId.executions' => [
'table' => 'appwrite_usage_executions_all',
'groupBy' => 'functionId',
],
'functions.functionId.compute' => [
'table' => 'appwrite_usage_executions_time',
'groupBy' => 'functionId',
],
'functions.functionId.failures' => [
'table' => 'appwrite_usage_executions_all',
'groupBy' => 'functionId',
'filters' => [
'functionStatus' => 'failed',
],
],
];
$database = getDatabase($register, '_console');
$influxDB = getInfluxDB($register);
// TODO Maybe move this to the setResource method, and reuse in the http.php file
$attempts = 0;
$max = 10;
$sleep = 1;
$usage = new Usage($database, $influxDB, $logError);
$db = null;
$redis = null;
do { // connect to db
try {
$attempts++;
$db = $register->get('db');
$redis = $register->get('cache');
break; // leave the do-while if successful
} catch (\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep($sleep);
}
} while ($attempts < $max);
// TODO use inject
$cacheAdapter = new Cache(new Redis($redis));
$dbForProject = new Database(new MariaDB($db), $cacheAdapter);
$dbForConsole = new Database(new MariaDB($db), $cacheAdapter);
$dbForProject->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$dbForConsole->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$dbForConsole->setNamespace('_console');
$latestTime = [];
$usageDB = new UsageDB($database, $logError);
Authorization::disable();
$iterations = 0;
Console::loop(function () use ($interval, $register, $dbForProject, $dbForConsole, $globalMetrics, $periods, &$latestTime, &$iterations) {
Console::loop(function () use ($interval, $usage, $usageDB, &$iterations) {
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregating usage data every {$interval} seconds");
@ -274,105 +134,10 @@ $cli
/**
* Aggregate InfluxDB every 30 seconds
* @var InfluxDB\Client $client
*/
$client = $register->get('influxdb');
$attempts = 0;
$max = 10;
$sleep = 1;
$usage->collect();
do { // check if telegraf database is ready
try {
$attempts++;
$database = $client->selectDB('telegraf');
if (in_array('telegraf', $client->listDatabases())) {
break; // leave the do-while if successful
}
} catch (\Throwable $th) {
Console::warning("InfluxDB not ready. Retrying connection ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('InfluxDB database not ready yet');
}
sleep($sleep);
}
} while ($attempts < $max);
// sync data
foreach ($globalMetrics as $metric => $options) { //for each metrics
foreach ($periods as $period) { // aggregate data for each period
$start = DateTime::createFromFormat('U', \strtotime($period['startTime']))->format(DateTime::RFC3339);
if (!empty($latestTime[$metric][$period['key']])) {
$start = DateTime::createFromFormat('U', $latestTime[$metric][$period['key']])->format(DateTime::RFC3339);
}
$end = DateTime::createFromFormat('U', \strtotime('now'))->format(DateTime::RFC3339);
$table = $options['table']; //Which influxdb table to query for this metric
$groupBy = empty($options['groupBy']) ? '' : ', "' . $options['groupBy'] . '"'; //Some sub level metrics may be grouped by other tags like collectionId, bucketId, etc
$filters = $options['filters'] ?? []; // Some metrics might have additional filters, like function's status
if (!empty($filters)) {
$filters = ' AND ' . implode(' AND ', array_map(fn ($filter, $value) => "\"{$filter}\"='{$value}'", array_keys($filters), array_values($filters)));
} else {
$filters = '';
}
$query = "SELECT sum(value) AS \"value\" FROM \"{$table}\" WHERE \"time\" > '{$start}' AND \"time\" < '{$end}' AND \"metric_type\"='counter' {$filters} GROUP BY time({$period['key']}), \"projectId\" {$groupBy} FILL(null)";
try {
$result = $database->query($query);
$points = $result->getPoints();
foreach ($points as $point) {
$projectId = $point['projectId'];
if (!empty($projectId) && $projectId !== 'console') {
$dbForProject->setNamespace('_' . $projectId);
$metricUpdated = $metric;
if (!empty($groupBy)) {
$groupedBy = $point[$options['groupBy']] ?? '';
if (empty($groupedBy)) {
continue;
}
$metricUpdated = str_replace($options['groupBy'], $groupedBy, $metric);
}
$time = \strtotime($point['time']);
$id = \md5($time . '_' . $period['key'] . '_' . $metricUpdated); //Construct unique id for each metric using time, period and metric
$value = (!empty($point['value'])) ? $point['value'] : 0;
try {
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'period' => $period['key'],
'time' => $time,
'metric' => $metricUpdated,
'value' => $value,
'type' => 0,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
);
}
$latestTime[$metric][$period['key']] = $time;
} catch (\Exception $e) { // if projects are deleted this might fail
Console::warning("Failed to save data for project {$projectId} and metric {$metricUpdated}: {$e->getMessage()}");
Console::warning($e->getTraceAsString());
}
}
}
} catch (\Exception $e) {
Console::warning("Failed to Query: {$e->getMessage()}");
Console::warning($e->getTraceAsString());
}
}
}
if ($iterations % 30 != 0) { // Aggregate aggregate number of objects in database only after 15 minutes
if ($iterations % 30 != 0) { // return if 30 iterations has not passed
$iterations++;
$loopTook = microtime(true) - $loopStart;
$now = date('d-m-Y H:i:s', time());
@ -380,6 +145,7 @@ $cli
return;
}
$iterations = 0; // Reset iterations to prevent overflow when running for long time
/**
* Aggregate MariaDB every 15 minutes
* Some of the queries here might contain full-table scans.
@ -387,435 +153,7 @@ $cli
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregating database counters.");
$latestProject = null;
do { // Loop over all the projects
$attempts = 0;
$max = 10;
$sleep = 1;
do { // list projects
try {
$attempts++;
$projects = $dbForConsole->find('projects', [], 100, cursor: $latestProject);
break; // leave the do-while if successful
} catch (\Exception $e) {
Console::warning("Console DB not ready yet. Retrying ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('Failed access console db: ' . $e->getMessage());
}
sleep($sleep);
}
} while ($attempts < $max);
if (empty($projects)) {
continue;
}
$latestProject = $projects[array_key_last($projects)];
foreach ($projects as $project) {
$projectId = $project->getId();
// Get total storage
$dbForProject->setNamespace('_' . $projectId);
$deploymentsTotal = $dbForProject->sum('deployments', 'size');
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_storage.deployments.total'); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
try {
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'period' => '30m',
'time' => $time,
'metric' => 'storage.deployments.total',
'value' => $deploymentsTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $deploymentsTotal)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_storage.deployments.total'); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'period' => '1d',
'time' => $time,
'metric' => 'storage.deployments.total',
'value' => $deploymentsTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $deploymentsTotal)
);
}
} catch (\Exception $e) {
Console::warning("Failed to save data for project {$projectId} and metric storage.deployments.total: {$e->getMessage()}");
Console::warning($e->getTraceAsString());
}
$collections = [
'users' => [
'namespace' => '',
],
'collections' => [
'metricPrefix' => 'database',
'namespace' => '',
'subCollections' => [ // Some collections, like collections and later buckets have child collections that need counting
'documents' => [
'collectionPrefix' => 'collection_',
'namespace' => '',
],
],
],
'buckets' => [
'metricPrefix' => 'storage',
'namespace' => '',
'subCollections' => [
'files' => [
'namespace' => '',
'collectionPrefix' => 'bucket_',
'total' => [
'field' => 'sizeOriginal'
]
],
]
]
];
foreach ($collections as $collection => $options) {
try {
$dbForProject->setNamespace("_{$projectId}");
$count = $dbForProject->count($collection);
$metricPrefix = $options['metricPrefix'] ?? '';
$metric = empty($metricPrefix) ? "{$collection}.count" : "{$metricPrefix}.{$collection}.count";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$subCollections = $options['subCollections'] ?? [];
if (empty($subCollections)) {
continue;
}
$latestParent = null;
$subCollectionCounts = []; //total project level count of sub collections
$subCollectionTotals = []; //total project level sum of sub collections
do { // Loop over all the parent collection document for each sub collection
$dbForProject->setNamespace("_{$projectId}");
$parents = $dbForProject->find($collection, [], 100, cursor: $latestParent); // Get all the parents for the sub collections for example for documents, this will get all the collections
if (empty($parents)) {
continue;
}
$latestParent = $parents[array_key_last($parents)];
foreach ($parents as $parent) {
foreach ($subCollections as $subCollection => $subOptions) { // Sub collection counts, like database.collections.collectionId.documents.count
$dbForProject->setNamespace("_{$projectId}");
$count = $dbForProject->count(($subOptions['collectionPrefix'] ?? '') . $parent->getInternalId());
$subCollectionCounts[$subCollection] = ($subCollectionCounts[$subCollection] ?? 0) + $count; // Project level counts for sub collections like database.documents.count
$dbForProject->setNamespace("_{$projectId}");
$metric = empty($metricPrefix) ? "{$collection}.{$parent->getId()}.{$subCollection}.count" : "{$metricPrefix}.{$collection}.{$parent->getInternalId()}.{$subCollection}.count";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
// check if sum calculation is required
$total = $subOptions['total'] ?? [];
if (empty($total)) {
continue;
}
$dbForProject->setNamespace("_{$projectId}");
$total = (int) $dbForProject->sum(($subOptions['collectionPrefix'] ?? '') . $parent->getInternalId(), $total['field']);
$subCollectionTotals[$subCollection] = ($ssubCollectionTotals[$subCollection] ?? 0) + $total; // Project level sum for sub collections like storage.total
$dbForProject->setNamespace("_{$projectId}");
$metric = empty($metricPrefix) ? "{$collection}.{$parent->getId()}.{$subCollection}.total" : "{$metricPrefix}.{$collection}.{$parent->getInternalId()}.{$subCollection}.total";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $total,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $total)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $total,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $total)
);
}
}
}
} while (!empty($parents));
/**
* Inserting project level counts for sub collections like database.documents.count
*/
foreach ($subCollectionCounts as $subCollection => $count) {
$dbForProject->setNamespace("_{$projectId}");
$metric = empty($metricPrefix) ? "{$subCollection}.count" : "{$metricPrefix}.{$subCollection}.count";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
}
/**
* Inserting project level sums for sub collections like storage.files.total
*/
foreach ($subCollectionTotals as $subCollection => $count) {
$dbForProject->setNamespace("_{$projectId}");
$metric = empty($metricPrefix) ? "{$subCollection}.total" : "{$metricPrefix}.{$subCollection}.total";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
// aggregate storage.total = storage.files.total + storage.deployments.total
if ($metricPrefix === 'storage' && $subCollection === 'files') {
$metric = 'storage.total';
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count + $deploymentsTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count + $deploymentsTotal)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count + $deploymentsTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count + $deploymentsTotal)
);
}
}
}
} catch (\Exception$e) {
Console::warning("Failed to save database counters data for project {$collection}: {$e->getMessage()}");
Console::warning($e->getTraceAsString());
}
}
}
} while (!empty($projects));
$usageDB->collect();
$iterations++;
$loopTook = microtime(true) - $loopStart;

View file

@ -552,6 +552,8 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-schedule:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>

View file

@ -100,6 +100,10 @@ class DeletesV1 extends Worker
$this->deleteRealtimeUsage($this->args['timestamp']);
break;
case DELETE_TYPE_SESSIONS:
$this->deleteExpiredSessions($this->args['timestamp']);
break;
case DELETE_TYPE_CERTIFICATES:
$document = new Document($this->args['document']);
$this->deleteCertificates($document);
@ -250,6 +254,20 @@ class DeletesV1 extends Worker
});
}
/**
* @param int $timestamp
*/
protected function deleteExpiredSessions(int $timestamp): void
{
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$dbForProject = $this->getProjectDB($projectId);
// Delete Sessions
$this->deleteByGroup('sessions', [
new Query('expire', Query::TYPE_LESSER, [$timestamp])
], $dbForProject);
});
}
/**
* @param int $timestamp
*/

View file

@ -43,9 +43,11 @@ class WebhooksV1 extends Worker
protected function execute(array $events, string $payload, Document $webhook, Document $user, Document $project): void
{
$url = \rawurldecode($webhook->getAttribute('url'));
$signatureKey = $webhook->getAttribute('signatureKey');
$signature = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$httpUser = $webhook->getAttribute('httpUser');
$httpPass = $webhook->getAttribute('httpPass');
$ch = \curl_init($webhook->getAttribute('url'));
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
@ -68,7 +70,7 @@ class WebhooksV1 extends Worker
'X-' . APP_NAME . '-Webhook-Name: ' . $webhook->getAttribute('name', ''),
'X-' . APP_NAME . '-Webhook-User-Id: ' . $user->getId(),
'X-' . APP_NAME . '-Webhook-Project-Id: ' . $project->getId(),
'X-' . APP_NAME . '-Webhook-Signature: ' . $webhook->getAttribute('signature', 'not-yet-implemented'),
'X-' . APP_NAME . '-Webhook-Signature: ' . $signature,
]
);

View file

@ -588,6 +588,8 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-schedule:
entrypoint: schedule

View file

@ -11,7 +11,7 @@ void main() async {
.setEndpoint('http://[HOSTNAME_OR_IP]/v1') // Make sure your endpoint is accessible
.setProject('5ff3379a01d25') // Your project ID
.setKey('cd868c7af8bdc893b4...93b7535db89')
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
.setSelfSigned(); // Use only on dev mode with a self-signed SSL cert
Users users = Users(client);

View file

@ -182,4 +182,4 @@ function concat() {
exports.import = series(concatDep);
exports.less = series(lessLTR, lessRTL);
exports.build = series(concatApp, concat);
exports.build = series(concatApp, concat);

14
package-lock.json generated
View file

@ -9,7 +9,7 @@
"version": "0.1.0",
"license": "BSD-3-Clause",
"dependencies": {
"chart.js": "^3.7.1",
"chart.js": "^3.8.0",
"markdown-it": "^12.3.2",
"pell": "^1.0.6",
"prismjs": "^1.28.0",
@ -549,9 +549,9 @@
}
},
"node_modules/chart.js": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.1.tgz",
"integrity": "sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA=="
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz",
"integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg=="
},
"node_modules/chokidar": {
"version": "2.1.8",
@ -5484,9 +5484,9 @@
"dev": true
},
"chart.js": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.1.tgz",
"integrity": "sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA=="
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz",
"integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg=="
},
"chokidar": {
"version": "2.1.8",

View file

@ -17,7 +17,7 @@
"gulp-less": "^5.0.0"
},
"dependencies": {
"chart.js": "^3.7.1",
"chart.js": "^3.8.0",
"markdown-it": "^12.3.2",
"pell": "^1.0.6",
"prismjs": "^1.28.0",

View file

@ -682,25 +682,23 @@ stop(chart){const anims=this._charts.get(chart);if(!anims||!anims.items.length){
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 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;}
var animator=new Animator();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 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);const isShort=v=>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;}
const alpha=(a,f)=>a<255?f(a):'';function hexString(v){var f=isShort(v)?h1:h2;return v?'#'+f(v.r)+f(v.g)+f(v.b)+alpha(v.a,f):undefined;}
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;}
function hueValue(r,g,b,d,max){if(r===max){return((g-b)/d)+(g<b?6:0);}
if(g===max){return(b-r)/d+2;}
return(r-g)/d+4;}
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=hueValue(r,g,b,d,max);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);}
@ -714,11 +712,16 @@ 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];}
const map$2={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$1={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$1);const tkeys=Object.keys(map$2);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$2[k]);}
k=parseInt(names$1[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};}
let names;function nameParse(str){if(!names){names=unpack();names.transparent=[0,0,0,0];}
const a=names[str.toLowerCase()];return a&&{r:a[0],g:a[1],b:a[2],a:a.length===4?a[3]:255};}
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=m[8]?p2b(v):lim(v*255,0,255);}
r=+m[1];g=+m[3];b=+m[5];r=255&(m[2]?p2b(r):lim(r,0,255));g=255&(m[4]?p2b(g):lim(g,0,255));b=255&(m[6]?p2b(b):lim(b,0,255));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 to=v=>v<=0.0031308?v*12.92:Math.pow(v,1.0/2.4)*1.055-0.055;const from=v=>v<=0.04045?v/12.92:Math.pow((v+0.055)/1.055,2.4);function interpolate$1(rgb1,rgb2,t){const r=from(b2n(rgb1.r));const g=from(b2n(rgb1.g));const b=from(b2n(rgb1.b));return{r:n2b(to(r+t*(from(b2n(rgb2.r))-r))),g:n2b(to(g+t*(from(b2n(rgb2.g))-g))),b:n2b(to(b+t*(from(b2n(rgb2.b))-b))),a:rgb1.a+t*(rgb2.a-rgb1.a)};}
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);}
@ -732,11 +735,13 @@ 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;}
rgbString(){return this._valid?rgbString(this._rgb):undefined;}
hexString(){return this._valid?hexString(this._rgb):undefined;}
hslString(){return this._valid?hslString(this._rgb):undefined;}
mix(color,weight){if(color){const c1=this.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;this.rgb=c1;}
return this;}
interpolate(color,t){if(color){this._rgb=interpolate$1(this._rgb,color._rgb,t);}
return this;}
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;}
@ -749,12 +754,14 @@ 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 isPatternOrGradient(value){if(value&&typeof value==='object'){const type=value.toString();return type==='[object CanvasPattern]'||type==='[object CanvasGradient]';}
return false;}
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;}
const type=Object.prototype.toString.call(value);if(type.slice(0,7)==='[object'&&type.slice(-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;}
@ -781,7 +788,7 @@ const tval=target[key];const sval=source[key];if(isObject(tval)&&isObject(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);}
let pos=0;let idx=indexOfDotOrLength(key,pos);while(obj&&idx>pos){obj=obj[key.slice(pos,idx)];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;}
@ -792,14 +799,28 @@ const keys=key.split('.');for(let i=0,n=keys.length;i<n;++i){const k=keys[i];nod
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.drawActiveElementsOnTop=true;this.describe(_descriptors);}
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,includeInvisible:false};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.drawActiveElementsOnTop=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;}
var defaults=new Defaults({_scriptable:(name)=>!name.startsWith('on'),_indexable:(name)=>name!=='events',hover:{_fallback:'interaction'},interaction:{_scriptable:false,_indexable:false,}});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);}
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;}
@ -821,6 +842,29 @@ function _angleBetween(angle,start,end,sameAngleIsFullCircle){const a=_normalize
function _limitValue(value,min,max){return Math.max(min,Math.min(max,value));}
function _int16Range(value){return _limitValue(value,-32768,32767);}
function _isBetween(value,start,end,epsilon=1e-6){return value>=Math.min(start,end)-epsilon&&value<=Math.max(start,end)+epsilon;}
function _isDomSupported(){return typeof window!=='undefined'&&typeof document!=='undefined';}
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(e,canvas){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){if('native'in evt){return evt;}
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 toFontString(font){if(!font||isNullOrUndef(font.size)||isNullOrUndef(font.family)){return null;}
return(font.style?font.style+' ':'')
+(font.weight?font.weight+' ':'')
@ -861,116 +905,7 @@ 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 _isDomSupported(){return typeof window!=='undefined'&&typeof document!=='undefined';}
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&&axis!=='r'&&_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 getNearestRadialItems(chart,position,axis,useFinalPosition){let items=[];function evaluationFunc(element,datasetIndex,index){const{startAngle,endAngle}=element.getProps(['startAngle','endAngle'],useFinalPosition);const{angle}=getAngleFromPoint(element,{x:position.x,y:position.y});if(_angleBetween(angle,startAngle,endAngle)){items.push({element,datasetIndex,index});}}
optimizedEvaluateItems(chart,axis,position,evaluationFunc);return items;}
function getNearestCartesianItems(chart,position,axis,intersect,useFinalPosition){let items=[];const distanceMetric=getDistanceMetricForAxis(axis);let minDistance=Number.POSITIVE_INFINITY;function evaluationFunc(element,datasetIndex,index){const inRange=element.inRange(position.x,position.y,useFinalPosition);if(intersect&&!inRange){return;}
const center=element.getCenterPoint(useFinalPosition);const pointInArea=_isPointInArea(center,chart.chartArea,chart._minPadding);if(!pointInArea&&!inRange){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 getNearestItems(chart,position,axis,intersect,useFinalPosition){if(!_isPointInArea(position,chart.chartArea,chart._minPadding)){return[];}
return axis==='r'&&!intersect?getNearestRadialItems(chart,position,axis,useFinalPosition):getNearestCartesianItems(chart,position,axis,intersect,useFinalPosition);}
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){return getAxisItems(chart,e,{axis:'x',intersect:options.intersect},useFinalPosition);},y(chart,e,options,useFinalPosition){return getAxisItems(chart,e,{axis:'y',intersect:options.intersect},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=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,beginAtZero){const{min,max}=minmax;const change=toDimension(grace,(max-min)/2);const keepZero=(value,add)=>beginAtZero&&value===0?0:value+add;return{min:keepZero(min,-Math.abs(change)),max:keepZero(max,change)};}
function createContext(parentContext,context){return Object.assign(Object.create(parentContext),context);}
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,pos,stack,stackWeight;for(i=0,ilen=(boxes||[]).length;i<ilen;++i){box=boxes[i];({position:pos,options:{stack,stackWeight=1}}=box);layoutBoxes.push({index:i,box,pos,horizontal:box.isHorizontal(),weight:box.weight,stack:stack&&(pos+stack),stackWeight});}
return layoutBoxes;}
function buildStacks(layouts){const stacks={};for(const wrap of layouts){const{stack,pos,stackWeight}=wrap;if(!stack||!STATIC_POSITIONS.includes(pos)){continue;}
const _stack=stacks[stack]||(stacks[stack]={count:0,placed:0,weight:0,size:0});_stack.count++;_stack.weight+=stackWeight;}
return stacks;}
function setLayoutDims(layouts,params){const stacks=buildStacks(layouts);const{vBoxMaxWidth,hBoxMaxHeight}=params;let i,ilen,layout;for(i=0,ilen=layouts.length;i<ilen;++i){layout=layouts[i];const{fullSize}=layout.box;const stack=stacks[layout.stack];const factor=stack&&layout.stackWeight/stack.weight;if(layout.horizontal){layout.width=factor?factor*vBoxMaxWidth:fullSize&&params.availableWidth;layout.height=hBoxMaxHeight;}else{layout.width=vBoxMaxWidth;layout.height=factor?factor*hBoxMaxHeight:fullSize&&params.availableHeight;}}
return stacks;}
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,stacks){const{pos,box}=layout;const maxPadding=chartArea.maxPadding;if(!isObject(pos)){if(layout.size){chartArea[pos]-=layout.size;}
const stack=stacks[layout.stack]||{size:0,count:1};stack.size=Math.max(stack.size,layout.horizontal?box.height:box.width);layout.size=stack.size/stack.count;chartArea[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,stacks){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,stacks);refit|=same&&refitBoxes.length;changed=changed||other;if(!box.fullSize){refitBoxes.push(layout);}}
return refit&&fitBoxes(refitBoxes,chartArea,params,stacks)||changed;}
function setBoxDims(box,left,top,width,height){box.top=top;box.left=left;box.right=left+width;box.bottom=top+height;box.width=width;box.height=height;}
function placeBoxes(boxes,chartArea,params,stacks){const userPadding=params.padding;let{x,y}=chartArea;for(const layout of boxes){const box=layout.box;const stack=stacks[layout.stack]||{count:1,placed:0,weight:1};const weight=(layout.stackWeight/stack.weight)||1;if(layout.horizontal){const width=chartArea.w*weight;const height=stack.size||box.height;if(defined(stack.start)){y=stack.start;}
if(box.fullSize){setBoxDims(box,userPadding.left,y,params.outerWidth-userPadding.right-userPadding.left,height);}else{setBoxDims(box,chartArea.left+stack.placed,y,width,height);}
stack.start=y;stack.placed+=width;y=box.bottom;}else{const height=chartArea.h*weight;const width=stack.size||box.width;if(defined(stack.start)){x=stack.start;}
if(box.fullSize){setBoxDims(box,x,userPadding.top,width,params.outerHeight-userPadding.bottom-userPadding.top);}else{setBoxDims(box,x,chartArea.top+stack.placed,width,height);}
stack.start=x;stack.placed+=height;x=box.right;}}
chartArea.x=x;chartArea.y=y;}
defaults.set('layout',{autoPadding:true,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);const stacks=setLayoutDims(verticalBoxes.concat(horizontalBoxes),params);fitBoxes(boxes.fullSize,chartArea,params,stacks);fitBoxes(verticalBoxes,chartArea,params,stacks);if(fitBoxes(horizontalBoxes,chartArea,params,stacks)){fitBoxes(verticalBoxes,chartArea,params,stacks);}
handleMaxPadding(chartArea);placeBoxes(boxes.leftAndTop,chartArea,params,stacks);chartArea.x+=chartArea.w;chartArea.y+=chartArea.h;placeBoxes(boxes.rightAndBottom,chartArea,params,stacks);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,{left:0,top:0,right:0,bottom:0});});}};function _createResolver(scopes,prefixes=[''],rootScopes=scopes,fallback,getTarget=()=>scopes[0]){if(!defined(fallback)){fallback=_resolve('_fallback',scopes);}
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());target[prop]=storage[prop]=value;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};}
@ -1003,6 +938,8 @@ function getKeysFromAllScopes(target){let keys=target._keys;if(!keys){keys=targe
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);}
function _parseObjectDataRadialScale(meta,data,start,count){const{iScale}=meta;const{key='r'}=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]={r:iScale.parse(resolveObjectKey(item,key),index)};}
return parsed;}
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;}
@ -1032,6 +969,24 @@ function _bezierInterpolation(p1,p2,t,mode){const cp1={x:p1.cp2x,y:p1.cp2y};cons
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 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,beginAtZero){const{min,max}=minmax;const change=toDimension(grace,(max-min)/2);const keepZero=(value,add)=>beginAtZero&&value===0?0:value+add;return{min:keepZero(min,-Math.abs(change)),max:keepZero(max,change)};}
function createContext(parentContext,context){return Object.assign(Object.create(parentContext),context);}
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;}}
@ -1078,7 +1033,59 @@ if(start<i-1){addStyle(start,i-1,segment.loop,prevStyle);}}
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=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,_isClickEvent:_isClickEvent,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,_isDomSupported:_isDomSupported,_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,createContext:createContext,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,_isBetween:_isBetween,getRtlAdapter:getRtlAdapter,overrideTextDirection:overrideTextDirection,restoreTextDirection:restoreTextDirection,_boundSegment:_boundSegment,_boundSegments:_boundSegments,_computeSegments:_computeSegments});class BasePlatform{acquireContext(canvas,aspectRatio){}
var helpers=Object.freeze({__proto__:null,easingEffects:effects,isPatternOrGradient:isPatternOrGradient,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,_isClickEvent:_isClickEvent,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,_parseObjectDataRadialScale:_parseObjectDataRadialScale,splineCurve:splineCurve,splineCurveMonotone:splineCurveMonotone,_updateBezierControlPoints:_updateBezierControlPoints,_isDomSupported:_isDomSupported,_getParentNode:_getParentNode,getStyle:getStyle,getRelativePosition:getRelativePosition,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,createContext:createContext,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,_isBetween:_isBetween,getRtlAdapter:getRtlAdapter,overrideTextDirection:overrideTextDirection,restoreTextDirection:restoreTextDirection,_boundSegment:_boundSegment,_boundSegments:_boundSegments,_computeSegments:_computeSegments});function binarySearch(metaset,axis,value,intersect){const{controller,data,_sorted}=metaset;const iScale=controller._cachedMeta.iScale;if(iScale&&axis===iScale.axis&&axis!=='r'&&_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 evaluateInteractionItems(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,includeInvisible){const items=[];if(!includeInvisible&&!chart.isPointInArea(position)){return items;}
const evaluationFunc=function(element,datasetIndex,index){if(!includeInvisible&&!_isPointInArea(element,chart.chartArea,0)){return;}
if(element.inRange(position.x,position.y,useFinalPosition)){items.push({element,datasetIndex,index});}};evaluateInteractionItems(chart,axis,position,evaluationFunc,true);return items;}
function getNearestRadialItems(chart,position,axis,useFinalPosition){let items=[];function evaluationFunc(element,datasetIndex,index){const{startAngle,endAngle}=element.getProps(['startAngle','endAngle'],useFinalPosition);const{angle}=getAngleFromPoint(element,{x:position.x,y:position.y});if(_angleBetween(angle,startAngle,endAngle)){items.push({element,datasetIndex,index});}}
evaluateInteractionItems(chart,axis,position,evaluationFunc);return items;}
function getNearestCartesianItems(chart,position,axis,intersect,useFinalPosition,includeInvisible){let items=[];const distanceMetric=getDistanceMetricForAxis(axis);let minDistance=Number.POSITIVE_INFINITY;function evaluationFunc(element,datasetIndex,index){const inRange=element.inRange(position.x,position.y,useFinalPosition);if(intersect&&!inRange){return;}
const center=element.getCenterPoint(useFinalPosition);const pointInArea=!!includeInvisible||chart.isPointInArea(center);if(!pointInArea&&!inRange){return;}
const distance=distanceMetric(position,center);if(distance<minDistance){items=[{element,datasetIndex,index}];minDistance=distance;}else if(distance===minDistance){items.push({element,datasetIndex,index});}}
evaluateInteractionItems(chart,axis,position,evaluationFunc);return items;}
function getNearestItems(chart,position,axis,intersect,useFinalPosition,includeInvisible){if(!includeInvisible&&!chart.isPointInArea(position)){return[];}
return axis==='r'&&!intersect?getNearestRadialItems(chart,position,axis,useFinalPosition):getNearestCartesianItems(chart,position,axis,intersect,useFinalPosition,includeInvisible);}
function getAxisItems(chart,position,axis,intersect,useFinalPosition){const items=[];const rangeMethod=axis==='x'?'inXRange':'inYRange';let intersectsItem=false;evaluateInteractionItems(chart,axis,position,(element,datasetIndex,index)=>{if(element[rangeMethod](position[axis],useFinalPosition)){items.push({element,datasetIndex,index});intersectsItem=intersectsItem||element.inRange(position.x,position.y,useFinalPosition);}});if(intersect&&!intersectsItem){return[];}
return items;}
var Interaction={evaluateInteractionItems,modes:{index(chart,e,options,useFinalPosition){const position=getRelativePosition(e,chart);const axis=options.axis||'x';const includeInvisible=options.includeInvisible||false;const items=options.intersect?getIntersectItems(chart,position,axis,useFinalPosition,includeInvisible):getNearestItems(chart,position,axis,false,useFinalPosition,includeInvisible);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';const includeInvisible=options.includeInvisible||false;let items=options.intersect?getIntersectItems(chart,position,axis,useFinalPosition,includeInvisible):getNearestItems(chart,position,axis,false,useFinalPosition,includeInvisible);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';const includeInvisible=options.includeInvisible||false;return getIntersectItems(chart,position,axis,useFinalPosition,includeInvisible);},nearest(chart,e,options,useFinalPosition){const position=getRelativePosition(e,chart);const axis=options.axis||'xy';const includeInvisible=options.includeInvisible||false;return getNearestItems(chart,position,axis,options.intersect,useFinalPosition,includeInvisible);},x(chart,e,options,useFinalPosition){const position=getRelativePosition(e,chart);return getAxisItems(chart,position,'x',options.intersect,useFinalPosition);},y(chart,e,options,useFinalPosition){const position=getRelativePosition(e,chart);return getAxisItems(chart,position,'y',options.intersect,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,pos,stack,stackWeight;for(i=0,ilen=(boxes||[]).length;i<ilen;++i){box=boxes[i];({position:pos,options:{stack,stackWeight=1}}=box);layoutBoxes.push({index:i,box,pos,horizontal:box.isHorizontal(),weight:box.weight,stack:stack&&(pos+stack),stackWeight});}
return layoutBoxes;}
function buildStacks(layouts){const stacks={};for(const wrap of layouts){const{stack,pos,stackWeight}=wrap;if(!stack||!STATIC_POSITIONS.includes(pos)){continue;}
const _stack=stacks[stack]||(stacks[stack]={count:0,placed:0,weight:0,size:0});_stack.count++;_stack.weight+=stackWeight;}
return stacks;}
function setLayoutDims(layouts,params){const stacks=buildStacks(layouts);const{vBoxMaxWidth,hBoxMaxHeight}=params;let i,ilen,layout;for(i=0,ilen=layouts.length;i<ilen;++i){layout=layouts[i];const{fullSize}=layout.box;const stack=stacks[layout.stack];const factor=stack&&layout.stackWeight/stack.weight;if(layout.horizontal){layout.width=factor?factor*vBoxMaxWidth:fullSize&&params.availableWidth;layout.height=hBoxMaxHeight;}else{layout.width=vBoxMaxWidth;layout.height=factor?factor*hBoxMaxHeight:fullSize&&params.availableHeight;}}
return stacks;}
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,stacks){const{pos,box}=layout;const maxPadding=chartArea.maxPadding;if(!isObject(pos)){if(layout.size){chartArea[pos]-=layout.size;}
const stack=stacks[layout.stack]||{size:0,count:1};stack.size=Math.max(stack.size,layout.horizontal?box.height:box.width);layout.size=stack.size/stack.count;chartArea[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,stacks){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,stacks);refit|=same&&refitBoxes.length;changed=changed||other;if(!box.fullSize){refitBoxes.push(layout);}}
return refit&&fitBoxes(refitBoxes,chartArea,params,stacks)||changed;}
function setBoxDims(box,left,top,width,height){box.top=top;box.left=left;box.right=left+width;box.bottom=top+height;box.width=width;box.height=height;}
function placeBoxes(boxes,chartArea,params,stacks){const userPadding=params.padding;let{x,y}=chartArea;for(const layout of boxes){const box=layout.box;const stack=stacks[layout.stack]||{count:1,placed:0,weight:1};const weight=(layout.stackWeight/stack.weight)||1;if(layout.horizontal){const width=chartArea.w*weight;const height=stack.size||box.height;if(defined(stack.start)){y=stack.start;}
if(box.fullSize){setBoxDims(box,userPadding.left,y,params.outerWidth-userPadding.right-userPadding.left,height);}else{setBoxDims(box,chartArea.left+stack.placed,y,width,height);}
stack.start=y;stack.placed+=width;y=box.bottom;}else{const height=chartArea.h*weight;const width=stack.size||box.width;if(defined(stack.start)){x=stack.start;}
if(box.fullSize){setBoxDims(box,x,userPadding.top,width,params.outerHeight-userPadding.bottom-userPadding.top);}else{setBoxDims(box,x,chartArea.top+stack.placed,width,height);}
stack.start=x;stack.placed+=height;x=box.right;}}
chartArea.x=x;chartArea.y=y;}
defaults.set('layout',{autoPadding:true,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);const stacks=setLayoutDims(verticalBoxes.concat(horizontalBoxes),params);fitBoxes(boxes.fullSize,chartArea,params,stacks);fitBoxes(verticalBoxes,chartArea,params,stacks);if(fitBoxes(horizontalBoxes,chartArea,params,stacks)){fitBoxes(verticalBoxes,chartArea,params,stacks);}
handleMaxPadding(chartArea);placeBoxes(boxes.leftAndTop,chartArea,params,stacks);chartArea.x+=chartArea.w;chartArea.y+=chartArea.h;placeBoxes(boxes.rightAndBottom,chartArea,params,stacks);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,{left:0,top:0,right:0,bottom:0});});}};class BasePlatform{acquireContext(canvas,aspectRatio){}
releaseContext(context){return false;}
addEventListener(chart,type,listener){}
removeEventListener(chart,type,listener){}
@ -1093,7 +1100,7 @@ if(isNullOrEmpty(renderHeight)){if(canvas.style.height===''){canvas.height=canva
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 fromNativeEvent(event,chart){const type=EVENT_TYPES[event.type]||event.type;const{x,y}=getRelativePosition(event,chart);return{type,chart,native:event,x:x!==undefined?x:null,y:y!==undefined?y:null,};}
function nodeListContains(nodeList,canvas){for(const node of nodeList){if(node===canvas||node.contains(canvas)){return true;}}}
function createAttachObserver(chart,type,listener){const canvas=chart.canvas;const observer=new MutationObserver(entries=>{let trigger=false;for(const entry of entries){trigger=trigger||nodeListContains(entry.addedNodes,canvas);trigger=trigger&&!nodeListContains(entry.removedNodes,canvas);}
if(trigger){listener();}});observer.observe(document,{childList:true,subtree:true});return observer;}
@ -1180,7 +1187,7 @@ function createDataContext(parent,index,element){return createContext(parent,{ac
function clearStacks(meta,items){const datasetIndex=meta.controller.index;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][datasetIndex]===undefined){return;}
delete stacks[axis][datasetIndex];}}
const isDirectUpdateMode=(mode)=>mode==='reset'||mode==='none';const cloneIfNotShared=(cached,shared)=>shared?cached:Object.assign({},cached);const createStack=(canStack,meta,chart)=>canStack&&!meta.hidden&&meta._stacked&&{keys:getSortedDatasetIndices(chart,true),values:null};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();}
const isDirectUpdateMode=(mode)=>mode==='reset'||mode==='none';const cloneIfNotShared=(cached,shared)=>shared?cached:Object.assign({},cached);const createStack=(canStack,meta,chart)=>canStack&&!meta.hidden&&meta._stacked&&{keys:getSortedDatasetIndices(chart,true),values:null};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.supportsDecimation=false;this.$context=undefined;this._syncList=[];this.initialize();}
initialize(){const meta=this._cachedMeta;this.configure();this.linkScales();meta._stacked=isStacked(meta.vScale,meta);this.addElements();}
updateIndex(datasetIndex){if(this.index!==datasetIndex){clearStacks(this._cachedMeta);}
this.index=datasetIndex;}
@ -1331,7 +1338,7 @@ getLabels(){const data=this.chart.data;return this.options.labels||(this.isHoriz
beforeLayout(){this._cache={};this._dataLimitsCached=false;}
beforeUpdate(){callback(this.options.beforeUpdate,[this]);}
update(maxWidth,maxHeight,margins){const{beginAtZero,grace,ticks:tickOpts}=this.options;const sampleSize=tickOpts.sampleSize;this.beforeUpdate();this.maxWidth=maxWidth;this.maxHeight=maxHeight;this._margins=margins=Object.assign({left:0,right:0,top:0,bottom:0},margins);this.ticks=null;this._labelSizes=null;this._gridLineItems=null;this._labelItems=null;this.beforeSetDimensions();this.setDimensions();this.afterSetDimensions();this._maxLength=this.isHorizontal()?this.width+margins.left+margins.right:this.height+margins.top+margins.bottom;if(!this._dataLimitsCached){this.beforeDataLimits();this.determineDataLimits();this.afterDataLimits();this._range=_addGrace(this,grace,beginAtZero);this._dataLimitsCached=true;}
this.beforeBuildTicks();this.ticks=this.buildTicks()||[];this.afterBuildTicks();const samplingEnabled=sampleSize<this.ticks.length;this._convertTicksToLabels(samplingEnabled?sample(this.ticks,sampleSize):this.ticks);this.configure();this.beforeCalculateLabelRotation();this.calculateLabelRotation();this.afterCalculateLabelRotation();if(tickOpts.display&&(tickOpts.autoSkip||tickOpts.source==='auto')){this.ticks=autoSkip(this,this.ticks);this._labelSizes=null;}
this.beforeBuildTicks();this.ticks=this.buildTicks()||[];this.afterBuildTicks();const samplingEnabled=sampleSize<this.ticks.length;this._convertTicksToLabels(samplingEnabled?sample(this.ticks,sampleSize):this.ticks);this.configure();this.beforeCalculateLabelRotation();this.calculateLabelRotation();this.afterCalculateLabelRotation();if(tickOpts.display&&(tickOpts.autoSkip||tickOpts.source==='auto')){this.ticks=autoSkip(this,this.ticks);this._labelSizes=null;this.afterAutoSkip();}
if(samplingEnabled){this._convertTicksToLabels(this.ticks);}
this.beforeFit();this.fit();this.afterFit();this.afterUpdate();}
configure(){let reversePixels=this.options.reverse;let startPixel,endPixel;if(this.isHorizontal()){startPixel=this.left;endPixel=this.right;}else{startPixel=this.top;endPixel=this.bottom;reversePixels=!reversePixels;}
@ -1357,12 +1364,13 @@ const labelSizes=this._getLabelSizes();const maxLabelWidth=labelSizes.widest.wid
-tickOpts.padding-getTitleHeight(options.title,this.chart.options.font);maxLabelDiagonal=Math.sqrt(maxLabelWidth*maxLabelWidth+maxLabelHeight*maxLabelHeight);labelRotation=toDegrees(Math.min(Math.asin(_limitValue((labelSizes.highest.height+6)/tickWidth,-1,1)),Math.asin(_limitValue(maxHeight/maxLabelDiagonal,-1,1))-Math.asin(_limitValue(maxLabelHeight/maxLabelDiagonal,-1,1))));labelRotation=Math.max(minRotation,Math.min(maxRotation,labelRotation));}
this.labelRotation=labelRotation;}
afterCalculateLabelRotation(){callback(this.options.afterCalculateLabelRotation,[this]);}
afterAutoSkip(){}
beforeFit(){callback(this.options.beforeFit,[this]);}
fit(){const minSize={width:0,height:0};const{chart,options:{ticks:tickOpts,title:titleOpts,grid:gridOpts}}=this;const display=this._isVisible();const isHorizontal=this.isHorizontal();if(display){const titleHeight=getTitleHeight(titleOpts,chart.options.font);if(isHorizontal){minSize.width=this.maxWidth;minSize.height=getTickMarkLength(gridOpts)+titleHeight;}else{minSize.height=this.maxHeight;minSize.width=getTickMarkLength(gridOpts)+titleHeight;}
if(tickOpts.display&&this.ticks.length){const{first,last,widest,highest}=this._getLabelSizes();const tickPadding=tickOpts.padding*2;const angleRadians=toRadians(this.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(this.maxHeight,minSize.height+labelHeight+tickPadding);}else{const labelWidth=tickOpts.mirror?0:cos*widest.width+sin*highest.height;minSize.width=Math.min(this.maxWidth,minSize.width+labelWidth+tickPadding);}
this._calculatePadding(first,last,sin,cos);}}
this._handleMargins();if(isHorizontal){this.width=this._length=chart.width-this._margins.left-this._margins.right;this.height=minSize.height;}else{this.width=minSize.width;this.height=this._length=chart.height-this._margins.top-this._margins.bottom;}}
_calculatePadding(first,last,sin,cos){const{ticks:{align,padding},position}=this.options;const isRotated=this.labelRotation!==0;const labelsBelowTicks=position!=='top'&&this.axis==='x';if(this.isHorizontal()){const offsetLeft=this.getPixelForTick(0)-this.left;const offsetRight=this.right-this.getPixelForTick(this.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;}
_calculatePadding(first,last,sin,cos){const{ticks:{align,padding},position}=this.options;const isRotated=this.labelRotation!==0;const labelsBelowTicks=position!=='top'&&this.axis==='x';if(this.isHorizontal()){const offsetLeft=this.getPixelForTick(0)-this.left;const offsetRight=this.right-this.getPixelForTick(this.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 if(align!=='inner'){paddingLeft=first.width/2;paddingRight=last.width/2;}
this.paddingLeft=Math.max((paddingLeft-offsetLeft+padding)*this.width/(this.width-offsetLeft),0);this.paddingRight=Math.max((paddingRight-offsetRight+padding)*this.width/(this.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;}
this.paddingTop=paddingTop+padding;this.paddingBottom=paddingBottom+padding;}}
_handleMargins(){if(this._margins){this._margins.left=Math.max(this.paddingLeft,this._margins.left);this._margins.top=Math.max(this.paddingTop,this._margins.top);this._margins.right=Math.max(this.paddingRight,this._margins.right);this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom);}}
@ -1403,15 +1411,16 @@ _computeLabelItems(chartArea){const axis=this.axis;const options=this.options;co
textAlign=this._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=this.chart.scales[positionAxisID].getPixelForValue(value);}
textAlign=this._getYAxisLabelAlignment(tl).textAlign;}
if(axis==='y'){if(align==='start'){textBaseline='top';}else if(align==='end'){textBaseline='bottom';}}
const labelSizes=this._getLabelSizes();for(i=0,ilen=ticks.length;i<ilen;++i){tick=ticks[i];label=tick.label;const optsAtIndex=optionTicks.setContext(this.getContext(i));pixel=this.getPixelForTick(i)+optionTicks.labelOffset;font=this._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;}}
const labelSizes=this._getLabelSizes();for(i=0,ilen=ticks.length;i<ilen;++i){tick=ticks[i];label=tick.label;const optsAtIndex=optionTicks.setContext(this.getContext(i));pixel=this.getPixelForTick(i)+optionTicks.labelOffset;font=this._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;let tickTextAlign=textAlign;if(isHorizontal){x=pixel;if(textAlign==='inner'){if(i===ilen-1){tickTextAlign=!this.options.reverse?'right':'left';}else if(i===0){tickTextAlign=!this.options.reverse?'left':'right';}else{tickTextAlign='center';}}
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,});}
items.push({rotation,label,font,color,strokeColor,strokeWidth,textOffset,textAlign:tickTextAlign,textBaseline,translation:[x,y],backdrop,});}
return items;}
_getXAxisLabelAlignment(){const{position,ticks}=this.options;const rotation=-toRadians(this.labelRotation);if(rotation){return position==='top'?'left':'right';}
let align='center';if(ticks.align==='start'){align='left';}else if(ticks.align==='end'){align='right';}
let align='center';if(ticks.align==='start'){align='left';}else if(ticks.align==='end'){align='right';}else if(ticks.align==='inner'){align='inner';}
return align;}
_getYAxisLabelAlignment(tl){const{position,ticks:{crossAlign,mirror,padding}}=this.options;const labelSizes=this._getLabelSizes();const tickAndPadding=tl+padding;const widest=labelSizes.widest.width;let textAlign;let x;if(position==='left'){if(mirror){x=this.right+padding;if(crossAlign==='near'){textAlign='left';}else if(crossAlign==='center'){textAlign='center';x+=(widest/2);}else{textAlign='right';x+=widest;}}else{x=this.right-tickAndPadding;if(crossAlign==='near'){textAlign='right';}else if(crossAlign==='center'){textAlign='center';x-=(widest/2);}else{textAlign='left';x=this.left;}}}else if(position==='right'){if(mirror){x=this.left+padding;if(crossAlign==='near'){textAlign='right';}else if(crossAlign==='center'){textAlign='center';x-=(widest/2);}else{textAlign='left';x-=widest;}}else{x=this.left+tickAndPadding;if(crossAlign==='near'){textAlign='left';}else if(crossAlign==='center'){textAlign='center';x+=widest/2;}else{textAlign='right';x=this.right;}}}else{textAlign='right';}
return{textAlign,x};}
@ -1547,7 +1556,7 @@ const cacheKey=prefixes.join();let cached=cache.get(cacheKey);if(!cached){const
return cached;}
const hasFunction=value=>isObject(value)&&Object.getOwnPropertyNames(value).reduce((acc,key)=>acc||isFunction(value[key]),false);function needContext(proxy,names){const{isScriptable,isIndexable}=_descriptors(proxy);for(const prop of names){const scriptable=isScriptable(prop);const indexable=isIndexable(prop);const value=(indexable||scriptable)&&proxy[prop];if((scriptable&&(isFunction(value)||hasFunction(value)))||(indexable&&isArray(value))){return true;}}
return false;}
var version="3.7.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');}
var version="3.8.0";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);}
@ -1629,6 +1638,7 @@ _drawDataset(meta){const ctx=this.ctx;const clip=meta._clip;const useClip=!clip.
if(useClip){clipArea(ctx,{left:clip.left===false?0:area.left-clip.left,right:clip.right===false?this.width:area.right+clip.right,top:clip.top===false?0:area.top-clip.top,bottom:clip.bottom===false?this.height:area.bottom+clip.bottom});}
meta.controller.draw();if(useClip){unclipArea(ctx);}
args.cancelable=false;this.notifyPlugins('afterDatasetDraw',args);}
isPointInArea(point){return _isPointInArea(point,this.chartArea,this._minPadding);}
getElementsAtEventForMode(e,mode,options,useFinalPosition){const method=Interaction.modes[mode];if(typeof method==='function'){return method(this,e,options,useFinalPosition);}
return[];}
getDatasetMeta(datasetIndex){const dataset=this.data.datasets[datasetIndex];const metasets=this._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);}
@ -1662,7 +1672,7 @@ return{datasetIndex,element:meta.data[index],index,};});const changed=!_elements
notifyPlugins(hook,args,filter){return this._plugins.notify(this,hook,args,filter);}
_updateHoverStyles(active,lastActive,replay){const hoverOptions=this.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){this.updateHoverStyle(deactivated,hoverOptions.mode,false);}
if(activated.length&&hoverOptions.mode){this.updateHoverStyle(activated,hoverOptions.mode,true);}}
_eventHandler(e,replay){const args={event:e,replay,cancelable:true,inChartArea:_isPointInArea(e,this.chartArea,this._minPadding)};const eventFilter=(plugin)=>(plugin.options.events||this.options.events).includes(e.native.type);if(this.notifyPlugins('beforeEvent',args,eventFilter)===false){return;}
_eventHandler(e,replay){const args={event:e,replay,cancelable:true,inChartArea:this.isPointInArea(e)};const eventFilter=(plugin)=>(plugin.options.events||this.options.events).includes(e.native.type);if(this.notifyPlugins('beforeEvent',args,eventFilter)===false){return;}
const changed=this._handleEvent(e,replay,args.inChartArea);args.cancelable=false;this.notifyPlugins('afterEvent',args,eventFilter);if(changed||args.changed){this.render();}
return this;}
_handleEvent(e,replay,inChartArea){const{_active:lastActive=[],options}=this;const useFinalPosition=replay;const active=this._getActiveElements(e,lastActive,inChartArea,useFinalPosition);const isClick=_isClickEvent(e);const lastEvent=determineLastEvent(e,this._lastEvent,inChartArea,isClick);if(inChartArea){this._lastEvent=null;callback(options.onHover,[e,active,this],this);if(isClick){callback(options.onClick,[e,active,this],this);}}
@ -1739,7 +1749,7 @@ if(floating){value=custom.barStart;length=custom.barEnd-custom.barStart;if(value
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(Math.abs(size)<minBarLength){size=barSign(size,vScale,actualBase)*minBarLength;if(value===actualBase){base-=size/2;}
head=base+size;}
const startPixel=vScale.getPixelForDecimal(0);const endPixel=vScale.getPixelForDecimal(1);const min=Math.min(startPixel,endPixel);const max=Math.max(startPixel,endPixel);base=Math.max(Math.min(base,max),min);head=base+size;}
if(base===vScale.getPixelForValue(actualBase)){const halfGrid=sign(size)*vScale.getLineWidthForValue(actualBase)/2;base+=halfGrid;size-=halfGrid;}
return{size,base,head,center:head+size/2};}
_calculateBarIndexPixels(index,ruler){const scale=ruler.scale;const options=this.options;const skipNull=options.skipNull;const maxBarThickness=valueOrDefault(options.maxBarThickness,Infinity);let center,size;if(ruler.grouped){const stackCount=skipNull?this._getStackCount(index):ruler.stackCount;const range=options.barThickness==='flex'?computeFlexCategoryTraits(index,ruler,options,stackCount):computeFitCategoryTraits(index,ruler,options,stackCount);const stackIndex=this._getStackIndex(this.index,this._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(this.getParsed(index)[scale.axis],index);size=Math.min(maxBarThickness,ruler.min*ruler.ratio);}
@ -1796,11 +1806,11 @@ _getRingWeight(datasetIndex){return Math.max(valueOrDefault(this.chart.data.data
_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();}
return dataLabel;}}}}};class LineController extends DatasetController{initialize(){this.enableOptionSharing=true;this.supportsDecimation=true;super.initialize();}
update(mode){const meta=this._cachedMeta;const{dataset:line,data:points=[],_dataset}=meta;const animationsDisabled=this.chart._animationsDisabled;let{start,count}=getStartAndCountOfVisiblePoints(meta,points,animationsDisabled);this._drawStart=start;this._drawCount=count;if(scaleRangesChanged(meta)){start=0;count=points.length;}
line._chart=this.chart;line._datasetIndex=this.index;line._decimated=!!_dataset._decimated;line.points=points;const options=this.resolveDatasetElementOptions(mode);if(!this.options.showLine){options.borderWidth=0;}
options.segment=this.options.segment;this.updateElement(line,undefined,{animated:!animationsDisabled,options},mode);this.updateElements(points,start,count,mode);}
updateElements(points,start,count,mode){const reset=mode==='reset';const{iScale,vScale,_stacked,_dataset}=this._cachedMeta;const firstOpts=this.resolveDataElementOptions(start,mode);const sharedOptions=this.getSharedOptions(firstOpts);const includeOptions=this.includeOptions(mode,sharedOptions);const iAxis=iScale.axis;const vAxis=vScale.axis;const{spanGaps,segment}=this.options;const maxGapLength=isNumber(spanGaps)?spanGaps:Number.POSITIVE_INFINITY;const directUpdate=this.chart._animationsDisabled||reset||mode==='none';let prevParsed=start>0&&this.getParsed(start-1);for(let i=start;i<start+count;++i){const point=points[i];const parsed=this.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?this.applyStack(vScale,parsed,_stacked):parsed[vAxis],i);properties.skip=isNaN(iPixel)||isNaN(vPixel)||nullData;properties.stop=i>0&&(parsed[iAxis]-prevParsed[iAxis])>maxGapLength;if(segment){properties.parsed=parsed;properties.raw=_dataset.data[i];}
updateElements(points,start,count,mode){const reset=mode==='reset';const{iScale,vScale,_stacked,_dataset}=this._cachedMeta;const firstOpts=this.resolveDataElementOptions(start,mode);const sharedOptions=this.getSharedOptions(firstOpts);const includeOptions=this.includeOptions(mode,sharedOptions);const iAxis=iScale.axis;const vAxis=vScale.axis;const{spanGaps,segment}=this.options;const maxGapLength=isNumber(spanGaps)?spanGaps:Number.POSITIVE_INFINITY;const directUpdate=this.chart._animationsDisabled||reset||mode==='none';let prevParsed=start>0&&this.getParsed(start-1);for(let i=start;i<start+count;++i){const point=points[i];const parsed=this.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?this.applyStack(vScale,parsed,_stacked):parsed[vAxis],i);properties.skip=isNaN(iPixel)||isNaN(vPixel)||nullData;properties.stop=i>0&&(Math.abs(parsed[iAxis]-prevParsed[iAxis]))>maxGapLength;if(segment){properties.parsed=parsed;properties.raw=_dataset.data[i];}
if(includeOptions){properties.options=sharedOptions||this.resolveDataElementOptions(i,point.active?'active':mode);}
if(!directUpdate){this.updateElement(point,i,properties,mode);}
prevParsed=parsed;}
@ -1815,21 +1825,25 @@ function scaleRangesChanged(meta){const{xScale,yScale,_scaleRanges}=meta;const n
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 meta=this._cachedMeta;const chart=this.chart;const labels=chart.data.labels||[];const value=formatNumber(meta._parsed[index].r,chart.options.locale);return{label:labels[index]||'',value,};}
parseObjectData(meta,data,start,count){return _parseObjectDataRadialScale.bind(this)(meta,data,start,count);}
update(mode){const arcs=this._cachedMeta.data;this._updateRadius();this.updateElements(arcs,0,arcs.length,mode);}
getMinMax(){const meta=this._cachedMeta;const range={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};meta.data.forEach((element,index)=>{const parsed=this.getParsed(index).r;if(!isNaN(parsed)&&this.chart.getDataVisibility(index)){if(parsed<range.min){range.min=parsed;}
if(parsed>range.max){range.max=parsed;}}});return range;}
_updateRadius(){const chart=this.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();this.outerRadius=outerRadius-(radiusLength*this.index);this.innerRadius=this.outerRadius-radiusLength;}
updateElements(arcs,start,count,mode){const reset=mode==='reset';const chart=this.chart;const dataset=this.getDataset();const opts=chart.options;const animationOpts=opts.animation;const scale=this._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/this.countVisibleElements();for(i=0;i<start;++i){angle+=this._computeAngle(i,mode,defaultAngle);}
for(i=start;i<start+count;i++){const arc=arcs[i];let startAngle=angle;let endAngle=angle+this._computeAngle(i,mode,defaultAngle);let outerRadius=chart.getDataVisibility(i)?scale.getDistanceFromCenterForValue(dataset.data[i]):0;angle=endAngle;if(reset){if(animationOpts.animateScale){outerRadius=0;}
updateElements(arcs,start,count,mode){const reset=mode==='reset';const chart=this.chart;const opts=chart.options;const animationOpts=opts.animation;const scale=this._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/this.countVisibleElements();for(i=0;i<start;++i){angle+=this._computeAngle(i,mode,defaultAngle);}
for(i=start;i<start+count;i++){const arc=arcs[i];let startAngle=angle;let endAngle=angle+this._computeAngle(i,mode,defaultAngle);let outerRadius=chart.getDataVisibility(i)?scale.getDistanceFromCenterForValue(this.getParsed(i).r):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:this.resolveDataElementOptions(i,arc.active?'active':mode)};this.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;}
countVisibleElements(){const meta=this._cachedMeta;let count=0;meta.data.forEach((element,index)=>{if(!isNaN(this.getParsed(index).r)&&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 vScale=this._cachedMeta.vScale;const parsed=this.getParsed(index);return{label:vScale.getLabels()[index],value:''+vScale.getLabelForValue(parsed[vScale.axis])};}
parseObjectData(meta,data,start,count){return _parseObjectDataRadialScale.bind(this)(meta,data,start,count);}
update(mode){const meta=this._cachedMeta;const line=meta.dataset;const points=meta.data||[];const labels=meta.iScale.getLabels();line.points=points;if(mode!=='resize'){const options=this.resolveDatasetElementOptions(mode);if(!this.options.showLine){options.borderWidth=0;}
const properties={_loop:true,_fullLoop:labels.length===points.length,options};this.updateElement(line,undefined,properties,mode);}
this.updateElements(points,0,points.length,mode);}
updateElements(points,start,count,mode){const dataset=this.getDataset();const scale=this._cachedMeta.rScale;const reset=mode==='reset';for(let i=start;i<start+count;i++){const point=points[i];const options=this.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};this.updateElement(point,i,properties,mode);}}}
updateElements(points,start,count,mode){const scale=this._cachedMeta.rScale;const reset=mode==='reset';for(let i=start;i<start+count;i++){const point=points[i];const options=this.resolveDataElementOptions(i,point.active?'active':mode);const pointPosition=scale.getPointPositionForValue(i,this.getParsed(i).r);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};this.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=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();}
@ -1948,38 +1962,46 @@ if(maxDefined){count=_limitValue(_lookupByKey(points,iScale.axis,max).hi+1,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;}
if(!meta.controller.supportsDecimation){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);const threshold=options.threshold||4*availableWidth;if(count<=threshold){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;}
dataset._decimated=decimated;});},destroy(chart){cleanDecimatedData(chart);}};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 _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 _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 _findSegmentEnd(start,end,points){for(;end>start;end--){const point=points[end];if(!isNaN(point.x)&&!isNaN(point.y)){break;}}
return 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 _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 _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){return decodeTargetIndex(fill[0],index,target,count);}
return['origin','start','end','stack','shape'].indexOf(fill)>=0&&fill;}
function decodeTargetIndex(firstCh,index,target,count){if(firstCh==='-'||firstCh==='+'){target=index+target;}
if(target===index||target<0||target>=count){return false;}
return target;}
function _getTargetPixel(fill,scale){let pixel=null;if(fill==='start'){pixel=scale.bottom;}else if(fill==='end'){pixel=scale.top;}else if(isObject(fill)){pixel=scale.getPixelForValue(fill.value);}else if(scale.getBasePixel){pixel=scale.getBasePixel();}
return pixel;}
function _getTargetValue(fill,scale,startValue){let value;if(fill==='start'){value=startValue;}else if(fill==='end'){value=scale.options.reverse?scale.min:scale.max;}else if(isObject(fill)){value=fill.value;}else{value=scale.getBaseValue();}
return value;}
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','shape'].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{scale,index,line}=source;const points=[];const segments=line.segments;const sourcePoints=line.points;const linesBelow=getLinesBelow(scale,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);}}
function _buildStackLine(source){const{scale,index,line}=source;const points=[];const segments=line.segments;const sourcePoints=line.points;const linesBelow=getLinesBelow(scale,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:{}});}
function getLinesBelow(scale,index){const below=[];const metas=scale.getMatchingVisibleMetas('line');for(let i=0;i<metas.length;i++){const meta=metas[i];if(meta.index===index){break;}
if(!meta.hidden){below.unshift(meta.dataset);}}
@ -1990,45 +2012,40 @@ 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(_isBetween(pointValue,firstValue,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);}
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 _getTarget(source){const{chart,fill,line}=source;if(isNumberFinite(fill)){return getLineByIndex(chart,fill);}
if(fill==='stack'){return _buildStackLine(source);}
if(fill==='shape'){return true;}
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){const{segments,points}=target;let first=true;let lineLoop=false;ctx.beginPath();for(const segment of segments){const{start,end}=segment;const firstPoint=points[start];const lastPoint=points[findSegmentEnd(start,end,points)];if(first){ctx.moveTo(firstPoint.x,firstPoint.y);first=false;}else{ctx.lineTo(firstPoint.x,clipY);ctx.lineTo(firstPoint.x,firstPoint.y);}
return _createBoundaryLine(boundary,line);}
function getLineByIndex(chart,index){const meta=chart.getDatasetMeta(index);const visible=meta&&chart.isDatasetVisible(index);return visible?meta.dataset:null;}
function computeBoundary(source){const scale=source.scale||{};if(scale.getPointPositionForValue){return computeCircularBoundary(source);}
return computeLinearBoundary(source);}
function computeLinearBoundary(source){const{scale={},fill}=source;const pixel=_getTargetPixel(fill,scale);if(isNumberFinite(pixel)){const horizontal=scale.isHorizontal();return{x:horizontal?pixel:null,y:horizontal?null:pixel};}
return null;}
function computeCircularBoundary(source){const{scale,fill}=source;const options=scale.options;const length=scale.getLabels().length;const start=options.reverse?scale.max:scale.min;const value=_getTargetValue(fill,scale,start);const target=[];if(options.grid.circular){const center=scale.getPointPositionForValue(0,start);return new simpleArc({x:center.x,y:center.y,radius:scale.getDistanceFromCenterForValue(value)});}
for(let i=0;i<length;++i){target.push(scale.getPointPositionForValue(i,value));}
return target;}
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);}}
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){clipVertical(ctx,target,area.top);fill(ctx,{line,target,color:above,scale,property});ctx.restore();ctx.save();clipVertical(ctx,target,area.bottom);}
fill(ctx,{line,target,color:below,scale,property});ctx.restore();}
function clipVertical(ctx,target,clipY){const{segments,points}=target;let first=true;let lineLoop=false;ctx.beginPath();for(const segment of segments){const{start,end}=segment;const firstPoint=points[start];const lastPoint=points[_findSegmentEnd(start,end,points)];if(first){ctx.moveTo(firstPoint.x,firstPoint.y);first=false;}else{ctx.lineTo(firstPoint.x,clipY);ctx.lineTo(firstPoint.x,firstPoint.y);}
lineLoop=!!target.pathSegment(ctx,segment,{move:lineLoop});if(lineLoop){ctx.closePath();}else{ctx.lineTo(lastPoint.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;const notShape=target!==true;ctx.save();ctx.fillStyle=backgroundColor;clipBounds(ctx,scale,notShape&&getBounds(property,start,end));ctx.beginPath();const lineLoop=!!line.pathSegment(ctx,src);let loop;if(notShape){if(lineLoop){ctx.closePath();}else{interpolatedLineTo(ctx,target,end,property);}
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;const notShape=target!==true;ctx.save();ctx.fillStyle=backgroundColor;clipBounds(ctx,scale,notShape&&_getBounds(property,start,end));ctx.beginPath();const lineLoop=!!line.pathSegment(ctx,src);let loop;if(notShape){if(lineLoop){ctx.closePath();}else{interpolatedLineTo(ctx,target,end,property);}
const targetLoop=!!target.pathSegment(ctx,tgt,{move:lineLoop,reverse:true});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,};}
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);}}
var index={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);}
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){this.maxWidth=maxWidth;this.maxHeight=maxHeight;this._margins=margins;this.setDimensions();this.buildLabels();this.fit();}
setDimensions(){if(this.isHorizontal()){this.width=this.maxWidth;this.left=this._margins.left;this.right=this.width;}else{this.height=this.maxHeight;this.top=this._margins.top;this.bottom=this.height;}}
@ -2062,9 +2079,9 @@ _computeTitleHeight(){const titleOpts=this.options.title;const titleFont=toFont(
_getLegendItemAt(x,y){let i,hitBox,lh;if(_isBetween(x,this.left,this.right)&&_isBetween(y,this.top,this.bottom)){lh=this.legendHitBoxes;for(i=0;i<lh.length;++i){hitBox=lh[i];if(_isBetween(x,hitBox.left,hitBox.left+hitBox.width)&&_isBetween(y,hitBox.top,hitBox.top+hitBox.height)){return this.legendItems[i];}}}
return null;}
handleEvent(e){const opts=this.options;if(!isListened(e.type,opts)){return;}
const hoveredItem=this._getLegendItemAt(e.x,e.y);if(e.type==='mousemove'){const previous=this._hoveredItem;const sameItem=itemsEqual(previous,hoveredItem);if(previous&&!sameItem){callback(opts.onLeave,[e,previous,this],this);}
const hoveredItem=this._getLegendItemAt(e.x,e.y);if(e.type==='mousemove'||e.type==='mouseout'){const previous=this._hoveredItem;const sameItem=itemsEqual(previous,hoveredItem);if(previous&&!sameItem){callback(opts.onLeave,[e,previous,this],this);}
this._hoveredItem=hoveredItem;if(hoveredItem&&!sameItem){callback(opts.onHover,[e,hoveredItem,this],this);}}else if(hoveredItem){callback(opts.onClick,[e,hoveredItem,this],this);}}}
function isListened(type,opts){if(type==='mousemove'&&(opts.onHover||opts.onLeave)){return true;}
function isListened(type,opts){if((type==='mousemove'||type==='mouseout')&&(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;}
@ -2154,6 +2171,7 @@ ctx.lineTo(x+bottomLeft,y+height);ctx.quadraticCurveTo(x,y+height,x,y+height-bot
ctx.lineTo(x,y+topLeft);ctx.quadraticCurveTo(x,y,x+topLeft,y);ctx.closePath();ctx.fill();if(options.borderWidth>0){ctx.stroke();}}
_updateAnimationTarget(options){const chart=this.chart;const anims=this.$animations;const animX=anims&&anims.x;const animY=anims&&anims.y;if(animX||animY){const position=positioners[options.position].call(this,this._active,this._eventPosition);if(!position){return;}
const size=this._size=getTooltipSize(this,options);const positionAndSize=Object.assign({},position,this._size);const alignment=determineAlignment(chart,options,positionAndSize);const point=getBackgroundPoint(options,positionAndSize,alignment,chart);if(animX._to!==point.x||animY._to!==point.y){this.xAlign=alignment.xAlign;this.yAlign=alignment.yAlign;this.width=size.width;this.height=size.height;this.caretX=position.x;this.caretY=position.y;this._resolveAnimations().update(this,point);}}}
_willRender(){return!!this.opacity;}
draw(ctx){const options=this.options.setContext(this.getContext());let opacity=this.opacity;if(!opacity){return;}
this._updateAnimationTarget(options);const tooltipSize={width:this.width,height:this.height};const pt={x:this.x,y:this.y};opacity=Math.abs(opacity)<1e-3?0:opacity;const padding=toPadding(options.padding);const hasTooltipContent=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;if(options.enabled&&hasTooltipContent){ctx.save();ctx.globalAlpha=opacity;this.drawBackground(pt,ctx,tooltipSize,options);overrideTextDirection(ctx,options.textDirection);pt.y+=padding.top;this.drawTitle(pt,ctx,options);this.drawBody(pt,ctx,options);this.drawFooter(pt,ctx,options);restoreTextDirection(ctx,options.textDirection);ctx.restore();}}
getActiveElements(){return this._active||[];}
@ -2167,13 +2185,12 @@ if(!inChartArea){return lastActive;}
const active=this.chart.getElementsAtEventForMode(e,options.mode,options,replay);if(options.reverse){active.reverse();}
return active;}
_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,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.inChartArea)){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,boxPadding: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: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];}}
Tooltip.positioners=positioners;var plugin_tooltip={id:'tooltip',_element:Tooltip,positioners,afterInit(chart,_args,options){if(options){chart.tooltip=new Tooltip({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;if(tooltip&&tooltip._willRender()){const args={tooltip};if(chart.notifyPlugins('beforeTooltipDraw',args)===false){return;}
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.inChartArea)){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,boxPadding: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: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=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,addedLabels)=>{if(typeof raw==='string'){index=labels.push(raw)-1;addedLabels.unshift({index,label:raw});}else if(isNaN(raw)){index=null;}
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=Object.freeze({__proto__:null,Decimation:plugin_decimation,Filler:index,Legend:plugin_legend,SubTitle:plugin_subtitle,Title:plugin_title,Tooltip:plugin_tooltip});const addIfString=(labels,raw,index,addedLabels)=>{if(typeof raw==='string'){index=labels.push(raw)-1;addedLabels.unshift({index,label:raw});}else if(isNaN(raw)){index=null;}
return index;};function findOrAddLabel(labels,raw,index,addedLabels){const first=labels.indexOf(raw);if(first===-1){return addIfString(labels,raw,index,addedLabels);}
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;this._addedLabels=[];}
@ -2268,7 +2285,7 @@ function leftForTextAlign(x,w,align){if(align==='right'){x-=w;}else if(align==='
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.getPointLabelContext(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);}
function drawPointLabels(scale,labelCount){const{ctx,options:{pointLabels}}=scale;for(let i=labelCount-1;i>=0;i--){const optsAtIndex=pointLabels.setContext(scale.getPointLabelContext(i));const plFont=toFont(optsAtIndex.font);const{x,y,textAlign,left,top,right,bottom}=scale._pointLabelItems[i];const{backdropColor}=optsAtIndex;if(!isNullOrUndef(backdropColor)){const borderRadius=toTRBLCorners(optsAtIndex.borderRadius);const padding=toPadding(optsAtIndex.backdropPadding);ctx.fillStyle=backdropColor;const backdropLeft=left-padding.left;const backdropTop=top-padding.top;const backdropWidth=right-left+padding.width;const backdropHeight=bottom-top+padding.height;if(Object.values(borderRadius).some(v=>v!==0)){ctx.beginPath();addRoundedRectPath(ctx,{x:backdropLeft,y:backdropTop,w:backdropWidth,h:backdropHeight,radius:borderRadius,});ctx.fill();}else{ctx.fillRect(backdropLeft,backdropTop,backdropWidth,backdropHeight);}}
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;}
@ -2335,6 +2352,7 @@ return{min,max};}
buildTicks(){const options=this.options;const timeOpts=options.time;const tickOpts=options.ticks;const timestamps=tickOpts.source==='labels'?this.getLabelTimestamps():this._generate();if(options.bounds==='ticks'&&timestamps.length){this.min=this._userMin||timestamps[0];this.max=this._userMax||timestamps[timestamps.length-1];}
const min=this.min;const max=this.max;const ticks=_filterBetween(timestamps,min,max);this._unit=timeOpts.unit||(tickOpts.autoSkip?determineUnitForAutoTicks(timeOpts.minUnit,this.min,this.max,this._getLabelCapacity(min)):determineUnitForFormatting(this,ticks.length,timeOpts.minUnit,this.min,this.max));this._majorUnit=!tickOpts.major.enabled||this._unit==='year'?undefined:determineMajorUnit(this._unit);this.initOffsets(timestamps);if(options.reverse){ticks.reverse();}
return ticksFromTimestamps(this,ticks,this._majorUnit);}
afterAutoSkip(){if(this.options.offsetAfterAutoskip){this.initOffsets(this.ticks.map(tick=>+tick.value));}}
initOffsets(timestamps){let start=0;let end=0;let first,last;if(this.options.offset&&timestamps.length){first=this.getDecimalForValue(timestamps[0]);if(timestamps.length===1){start=1-first;}else{start=(this.getDecimalForValue(timestamps[1])-first)/2;}
last=this.getDecimalForValue(timestamps[timestamps.length-1]);if(timestamps.length===1){end=last;}else{end=(last-this.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);this._offsets={start,end,factor:1/(start+1+end)};}

View file

@ -682,25 +682,23 @@ stop(chart){const anims=this._charts.get(chart);if(!anims||!anims.items.length){
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 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;}
var animator=new Animator();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 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);const isShort=v=>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;}
const alpha=(a,f)=>a<255?f(a):'';function hexString(v){var f=isShort(v)?h1:h2;return v?'#'+f(v.r)+f(v.g)+f(v.b)+alpha(v.a,f):undefined;}
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;}
function hueValue(r,g,b,d,max){if(r===max){return((g-b)/d)+(g<b?6:0);}
if(g===max){return(b-r)/d+2;}
return(r-g)/d+4;}
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=hueValue(r,g,b,d,max);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);}
@ -714,11 +712,16 @@ 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];}
const map$2={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$1={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$1);const tkeys=Object.keys(map$2);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$2[k]);}
k=parseInt(names$1[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};}
let names;function nameParse(str){if(!names){names=unpack();names.transparent=[0,0,0,0];}
const a=names[str.toLowerCase()];return a&&{r:a[0],g:a[1],b:a[2],a:a.length===4?a[3]:255};}
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=m[8]?p2b(v):lim(v*255,0,255);}
r=+m[1];g=+m[3];b=+m[5];r=255&(m[2]?p2b(r):lim(r,0,255));g=255&(m[4]?p2b(g):lim(g,0,255));b=255&(m[6]?p2b(b):lim(b,0,255));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 to=v=>v<=0.0031308?v*12.92:Math.pow(v,1.0/2.4)*1.055-0.055;const from=v=>v<=0.04045?v/12.92:Math.pow((v+0.055)/1.055,2.4);function interpolate$1(rgb1,rgb2,t){const r=from(b2n(rgb1.r));const g=from(b2n(rgb1.g));const b=from(b2n(rgb1.b));return{r:n2b(to(r+t*(from(b2n(rgb2.r))-r))),g:n2b(to(g+t*(from(b2n(rgb2.g))-g))),b:n2b(to(b+t*(from(b2n(rgb2.b))-b))),a:rgb1.a+t*(rgb2.a-rgb1.a)};}
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);}
@ -732,11 +735,13 @@ 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;}
rgbString(){return this._valid?rgbString(this._rgb):undefined;}
hexString(){return this._valid?hexString(this._rgb):undefined;}
hslString(){return this._valid?hslString(this._rgb):undefined;}
mix(color,weight){if(color){const c1=this.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;this.rgb=c1;}
return this;}
interpolate(color,t){if(color){this._rgb=interpolate$1(this._rgb,color._rgb,t);}
return this;}
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;}
@ -749,12 +754,14 @@ 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 isPatternOrGradient(value){if(value&&typeof value==='object'){const type=value.toString();return type==='[object CanvasPattern]'||type==='[object CanvasGradient]';}
return false;}
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;}
const type=Object.prototype.toString.call(value);if(type.slice(0,7)==='[object'&&type.slice(-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;}
@ -781,7 +788,7 @@ const tval=target[key];const sval=source[key];if(isObject(tval)&&isObject(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);}
let pos=0;let idx=indexOfDotOrLength(key,pos);while(obj&&idx>pos){obj=obj[key.slice(pos,idx)];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;}
@ -792,14 +799,28 @@ const keys=key.split('.');for(let i=0,n=keys.length;i<n;++i){const k=keys[i];nod
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.drawActiveElementsOnTop=true;this.describe(_descriptors);}
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,includeInvisible:false};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.drawActiveElementsOnTop=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;}
var defaults=new Defaults({_scriptable:(name)=>!name.startsWith('on'),_indexable:(name)=>name!=='events',hover:{_fallback:'interaction'},interaction:{_scriptable:false,_indexable:false,}});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);}
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;}
@ -821,6 +842,29 @@ function _angleBetween(angle,start,end,sameAngleIsFullCircle){const a=_normalize
function _limitValue(value,min,max){return Math.max(min,Math.min(max,value));}
function _int16Range(value){return _limitValue(value,-32768,32767);}
function _isBetween(value,start,end,epsilon=1e-6){return value>=Math.min(start,end)-epsilon&&value<=Math.max(start,end)+epsilon;}
function _isDomSupported(){return typeof window!=='undefined'&&typeof document!=='undefined';}
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(e,canvas){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){if('native'in evt){return evt;}
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 toFontString(font){if(!font||isNullOrUndef(font.size)||isNullOrUndef(font.family)){return null;}
return(font.style?font.style+' ':'')
+(font.weight?font.weight+' ':'')
@ -861,116 +905,7 @@ 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 _isDomSupported(){return typeof window!=='undefined'&&typeof document!=='undefined';}
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&&axis!=='r'&&_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 getNearestRadialItems(chart,position,axis,useFinalPosition){let items=[];function evaluationFunc(element,datasetIndex,index){const{startAngle,endAngle}=element.getProps(['startAngle','endAngle'],useFinalPosition);const{angle}=getAngleFromPoint(element,{x:position.x,y:position.y});if(_angleBetween(angle,startAngle,endAngle)){items.push({element,datasetIndex,index});}}
optimizedEvaluateItems(chart,axis,position,evaluationFunc);return items;}
function getNearestCartesianItems(chart,position,axis,intersect,useFinalPosition){let items=[];const distanceMetric=getDistanceMetricForAxis(axis);let minDistance=Number.POSITIVE_INFINITY;function evaluationFunc(element,datasetIndex,index){const inRange=element.inRange(position.x,position.y,useFinalPosition);if(intersect&&!inRange){return;}
const center=element.getCenterPoint(useFinalPosition);const pointInArea=_isPointInArea(center,chart.chartArea,chart._minPadding);if(!pointInArea&&!inRange){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 getNearestItems(chart,position,axis,intersect,useFinalPosition){if(!_isPointInArea(position,chart.chartArea,chart._minPadding)){return[];}
return axis==='r'&&!intersect?getNearestRadialItems(chart,position,axis,useFinalPosition):getNearestCartesianItems(chart,position,axis,intersect,useFinalPosition);}
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){return getAxisItems(chart,e,{axis:'x',intersect:options.intersect},useFinalPosition);},y(chart,e,options,useFinalPosition){return getAxisItems(chart,e,{axis:'y',intersect:options.intersect},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=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,beginAtZero){const{min,max}=minmax;const change=toDimension(grace,(max-min)/2);const keepZero=(value,add)=>beginAtZero&&value===0?0:value+add;return{min:keepZero(min,-Math.abs(change)),max:keepZero(max,change)};}
function createContext(parentContext,context){return Object.assign(Object.create(parentContext),context);}
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,pos,stack,stackWeight;for(i=0,ilen=(boxes||[]).length;i<ilen;++i){box=boxes[i];({position:pos,options:{stack,stackWeight=1}}=box);layoutBoxes.push({index:i,box,pos,horizontal:box.isHorizontal(),weight:box.weight,stack:stack&&(pos+stack),stackWeight});}
return layoutBoxes;}
function buildStacks(layouts){const stacks={};for(const wrap of layouts){const{stack,pos,stackWeight}=wrap;if(!stack||!STATIC_POSITIONS.includes(pos)){continue;}
const _stack=stacks[stack]||(stacks[stack]={count:0,placed:0,weight:0,size:0});_stack.count++;_stack.weight+=stackWeight;}
return stacks;}
function setLayoutDims(layouts,params){const stacks=buildStacks(layouts);const{vBoxMaxWidth,hBoxMaxHeight}=params;let i,ilen,layout;for(i=0,ilen=layouts.length;i<ilen;++i){layout=layouts[i];const{fullSize}=layout.box;const stack=stacks[layout.stack];const factor=stack&&layout.stackWeight/stack.weight;if(layout.horizontal){layout.width=factor?factor*vBoxMaxWidth:fullSize&&params.availableWidth;layout.height=hBoxMaxHeight;}else{layout.width=vBoxMaxWidth;layout.height=factor?factor*hBoxMaxHeight:fullSize&&params.availableHeight;}}
return stacks;}
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,stacks){const{pos,box}=layout;const maxPadding=chartArea.maxPadding;if(!isObject(pos)){if(layout.size){chartArea[pos]-=layout.size;}
const stack=stacks[layout.stack]||{size:0,count:1};stack.size=Math.max(stack.size,layout.horizontal?box.height:box.width);layout.size=stack.size/stack.count;chartArea[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,stacks){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,stacks);refit|=same&&refitBoxes.length;changed=changed||other;if(!box.fullSize){refitBoxes.push(layout);}}
return refit&&fitBoxes(refitBoxes,chartArea,params,stacks)||changed;}
function setBoxDims(box,left,top,width,height){box.top=top;box.left=left;box.right=left+width;box.bottom=top+height;box.width=width;box.height=height;}
function placeBoxes(boxes,chartArea,params,stacks){const userPadding=params.padding;let{x,y}=chartArea;for(const layout of boxes){const box=layout.box;const stack=stacks[layout.stack]||{count:1,placed:0,weight:1};const weight=(layout.stackWeight/stack.weight)||1;if(layout.horizontal){const width=chartArea.w*weight;const height=stack.size||box.height;if(defined(stack.start)){y=stack.start;}
if(box.fullSize){setBoxDims(box,userPadding.left,y,params.outerWidth-userPadding.right-userPadding.left,height);}else{setBoxDims(box,chartArea.left+stack.placed,y,width,height);}
stack.start=y;stack.placed+=width;y=box.bottom;}else{const height=chartArea.h*weight;const width=stack.size||box.width;if(defined(stack.start)){x=stack.start;}
if(box.fullSize){setBoxDims(box,x,userPadding.top,width,params.outerHeight-userPadding.bottom-userPadding.top);}else{setBoxDims(box,x,chartArea.top+stack.placed,width,height);}
stack.start=x;stack.placed+=height;x=box.right;}}
chartArea.x=x;chartArea.y=y;}
defaults.set('layout',{autoPadding:true,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);const stacks=setLayoutDims(verticalBoxes.concat(horizontalBoxes),params);fitBoxes(boxes.fullSize,chartArea,params,stacks);fitBoxes(verticalBoxes,chartArea,params,stacks);if(fitBoxes(horizontalBoxes,chartArea,params,stacks)){fitBoxes(verticalBoxes,chartArea,params,stacks);}
handleMaxPadding(chartArea);placeBoxes(boxes.leftAndTop,chartArea,params,stacks);chartArea.x+=chartArea.w;chartArea.y+=chartArea.h;placeBoxes(boxes.rightAndBottom,chartArea,params,stacks);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,{left:0,top:0,right:0,bottom:0});});}};function _createResolver(scopes,prefixes=[''],rootScopes=scopes,fallback,getTarget=()=>scopes[0]){if(!defined(fallback)){fallback=_resolve('_fallback',scopes);}
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());target[prop]=storage[prop]=value;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};}
@ -1003,6 +938,8 @@ function getKeysFromAllScopes(target){let keys=target._keys;if(!keys){keys=targe
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);}
function _parseObjectDataRadialScale(meta,data,start,count){const{iScale}=meta;const{key='r'}=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]={r:iScale.parse(resolveObjectKey(item,key),index)};}
return parsed;}
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;}
@ -1032,6 +969,24 @@ function _bezierInterpolation(p1,p2,t,mode){const cp1={x:p1.cp2x,y:p1.cp2y};cons
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 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,beginAtZero){const{min,max}=minmax;const change=toDimension(grace,(max-min)/2);const keepZero=(value,add)=>beginAtZero&&value===0?0:value+add;return{min:keepZero(min,-Math.abs(change)),max:keepZero(max,change)};}
function createContext(parentContext,context){return Object.assign(Object.create(parentContext),context);}
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;}}
@ -1078,7 +1033,59 @@ if(start<i-1){addStyle(start,i-1,segment.loop,prevStyle);}}
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=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,_isClickEvent:_isClickEvent,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,_isDomSupported:_isDomSupported,_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,createContext:createContext,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,_isBetween:_isBetween,getRtlAdapter:getRtlAdapter,overrideTextDirection:overrideTextDirection,restoreTextDirection:restoreTextDirection,_boundSegment:_boundSegment,_boundSegments:_boundSegments,_computeSegments:_computeSegments});class BasePlatform{acquireContext(canvas,aspectRatio){}
var helpers=Object.freeze({__proto__:null,easingEffects:effects,isPatternOrGradient:isPatternOrGradient,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,_isClickEvent:_isClickEvent,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,_parseObjectDataRadialScale:_parseObjectDataRadialScale,splineCurve:splineCurve,splineCurveMonotone:splineCurveMonotone,_updateBezierControlPoints:_updateBezierControlPoints,_isDomSupported:_isDomSupported,_getParentNode:_getParentNode,getStyle:getStyle,getRelativePosition:getRelativePosition,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,createContext:createContext,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,_isBetween:_isBetween,getRtlAdapter:getRtlAdapter,overrideTextDirection:overrideTextDirection,restoreTextDirection:restoreTextDirection,_boundSegment:_boundSegment,_boundSegments:_boundSegments,_computeSegments:_computeSegments});function binarySearch(metaset,axis,value,intersect){const{controller,data,_sorted}=metaset;const iScale=controller._cachedMeta.iScale;if(iScale&&axis===iScale.axis&&axis!=='r'&&_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 evaluateInteractionItems(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,includeInvisible){const items=[];if(!includeInvisible&&!chart.isPointInArea(position)){return items;}
const evaluationFunc=function(element,datasetIndex,index){if(!includeInvisible&&!_isPointInArea(element,chart.chartArea,0)){return;}
if(element.inRange(position.x,position.y,useFinalPosition)){items.push({element,datasetIndex,index});}};evaluateInteractionItems(chart,axis,position,evaluationFunc,true);return items;}
function getNearestRadialItems(chart,position,axis,useFinalPosition){let items=[];function evaluationFunc(element,datasetIndex,index){const{startAngle,endAngle}=element.getProps(['startAngle','endAngle'],useFinalPosition);const{angle}=getAngleFromPoint(element,{x:position.x,y:position.y});if(_angleBetween(angle,startAngle,endAngle)){items.push({element,datasetIndex,index});}}
evaluateInteractionItems(chart,axis,position,evaluationFunc);return items;}
function getNearestCartesianItems(chart,position,axis,intersect,useFinalPosition,includeInvisible){let items=[];const distanceMetric=getDistanceMetricForAxis(axis);let minDistance=Number.POSITIVE_INFINITY;function evaluationFunc(element,datasetIndex,index){const inRange=element.inRange(position.x,position.y,useFinalPosition);if(intersect&&!inRange){return;}
const center=element.getCenterPoint(useFinalPosition);const pointInArea=!!includeInvisible||chart.isPointInArea(center);if(!pointInArea&&!inRange){return;}
const distance=distanceMetric(position,center);if(distance<minDistance){items=[{element,datasetIndex,index}];minDistance=distance;}else if(distance===minDistance){items.push({element,datasetIndex,index});}}
evaluateInteractionItems(chart,axis,position,evaluationFunc);return items;}
function getNearestItems(chart,position,axis,intersect,useFinalPosition,includeInvisible){if(!includeInvisible&&!chart.isPointInArea(position)){return[];}
return axis==='r'&&!intersect?getNearestRadialItems(chart,position,axis,useFinalPosition):getNearestCartesianItems(chart,position,axis,intersect,useFinalPosition,includeInvisible);}
function getAxisItems(chart,position,axis,intersect,useFinalPosition){const items=[];const rangeMethod=axis==='x'?'inXRange':'inYRange';let intersectsItem=false;evaluateInteractionItems(chart,axis,position,(element,datasetIndex,index)=>{if(element[rangeMethod](position[axis],useFinalPosition)){items.push({element,datasetIndex,index});intersectsItem=intersectsItem||element.inRange(position.x,position.y,useFinalPosition);}});if(intersect&&!intersectsItem){return[];}
return items;}
var Interaction={evaluateInteractionItems,modes:{index(chart,e,options,useFinalPosition){const position=getRelativePosition(e,chart);const axis=options.axis||'x';const includeInvisible=options.includeInvisible||false;const items=options.intersect?getIntersectItems(chart,position,axis,useFinalPosition,includeInvisible):getNearestItems(chart,position,axis,false,useFinalPosition,includeInvisible);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';const includeInvisible=options.includeInvisible||false;let items=options.intersect?getIntersectItems(chart,position,axis,useFinalPosition,includeInvisible):getNearestItems(chart,position,axis,false,useFinalPosition,includeInvisible);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';const includeInvisible=options.includeInvisible||false;return getIntersectItems(chart,position,axis,useFinalPosition,includeInvisible);},nearest(chart,e,options,useFinalPosition){const position=getRelativePosition(e,chart);const axis=options.axis||'xy';const includeInvisible=options.includeInvisible||false;return getNearestItems(chart,position,axis,options.intersect,useFinalPosition,includeInvisible);},x(chart,e,options,useFinalPosition){const position=getRelativePosition(e,chart);return getAxisItems(chart,position,'x',options.intersect,useFinalPosition);},y(chart,e,options,useFinalPosition){const position=getRelativePosition(e,chart);return getAxisItems(chart,position,'y',options.intersect,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,pos,stack,stackWeight;for(i=0,ilen=(boxes||[]).length;i<ilen;++i){box=boxes[i];({position:pos,options:{stack,stackWeight=1}}=box);layoutBoxes.push({index:i,box,pos,horizontal:box.isHorizontal(),weight:box.weight,stack:stack&&(pos+stack),stackWeight});}
return layoutBoxes;}
function buildStacks(layouts){const stacks={};for(const wrap of layouts){const{stack,pos,stackWeight}=wrap;if(!stack||!STATIC_POSITIONS.includes(pos)){continue;}
const _stack=stacks[stack]||(stacks[stack]={count:0,placed:0,weight:0,size:0});_stack.count++;_stack.weight+=stackWeight;}
return stacks;}
function setLayoutDims(layouts,params){const stacks=buildStacks(layouts);const{vBoxMaxWidth,hBoxMaxHeight}=params;let i,ilen,layout;for(i=0,ilen=layouts.length;i<ilen;++i){layout=layouts[i];const{fullSize}=layout.box;const stack=stacks[layout.stack];const factor=stack&&layout.stackWeight/stack.weight;if(layout.horizontal){layout.width=factor?factor*vBoxMaxWidth:fullSize&&params.availableWidth;layout.height=hBoxMaxHeight;}else{layout.width=vBoxMaxWidth;layout.height=factor?factor*hBoxMaxHeight:fullSize&&params.availableHeight;}}
return stacks;}
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,stacks){const{pos,box}=layout;const maxPadding=chartArea.maxPadding;if(!isObject(pos)){if(layout.size){chartArea[pos]-=layout.size;}
const stack=stacks[layout.stack]||{size:0,count:1};stack.size=Math.max(stack.size,layout.horizontal?box.height:box.width);layout.size=stack.size/stack.count;chartArea[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,stacks){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,stacks);refit|=same&&refitBoxes.length;changed=changed||other;if(!box.fullSize){refitBoxes.push(layout);}}
return refit&&fitBoxes(refitBoxes,chartArea,params,stacks)||changed;}
function setBoxDims(box,left,top,width,height){box.top=top;box.left=left;box.right=left+width;box.bottom=top+height;box.width=width;box.height=height;}
function placeBoxes(boxes,chartArea,params,stacks){const userPadding=params.padding;let{x,y}=chartArea;for(const layout of boxes){const box=layout.box;const stack=stacks[layout.stack]||{count:1,placed:0,weight:1};const weight=(layout.stackWeight/stack.weight)||1;if(layout.horizontal){const width=chartArea.w*weight;const height=stack.size||box.height;if(defined(stack.start)){y=stack.start;}
if(box.fullSize){setBoxDims(box,userPadding.left,y,params.outerWidth-userPadding.right-userPadding.left,height);}else{setBoxDims(box,chartArea.left+stack.placed,y,width,height);}
stack.start=y;stack.placed+=width;y=box.bottom;}else{const height=chartArea.h*weight;const width=stack.size||box.width;if(defined(stack.start)){x=stack.start;}
if(box.fullSize){setBoxDims(box,x,userPadding.top,width,params.outerHeight-userPadding.bottom-userPadding.top);}else{setBoxDims(box,x,chartArea.top+stack.placed,width,height);}
stack.start=x;stack.placed+=height;x=box.right;}}
chartArea.x=x;chartArea.y=y;}
defaults.set('layout',{autoPadding:true,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);const stacks=setLayoutDims(verticalBoxes.concat(horizontalBoxes),params);fitBoxes(boxes.fullSize,chartArea,params,stacks);fitBoxes(verticalBoxes,chartArea,params,stacks);if(fitBoxes(horizontalBoxes,chartArea,params,stacks)){fitBoxes(verticalBoxes,chartArea,params,stacks);}
handleMaxPadding(chartArea);placeBoxes(boxes.leftAndTop,chartArea,params,stacks);chartArea.x+=chartArea.w;chartArea.y+=chartArea.h;placeBoxes(boxes.rightAndBottom,chartArea,params,stacks);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,{left:0,top:0,right:0,bottom:0});});}};class BasePlatform{acquireContext(canvas,aspectRatio){}
releaseContext(context){return false;}
addEventListener(chart,type,listener){}
removeEventListener(chart,type,listener){}
@ -1093,7 +1100,7 @@ if(isNullOrEmpty(renderHeight)){if(canvas.style.height===''){canvas.height=canva
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 fromNativeEvent(event,chart){const type=EVENT_TYPES[event.type]||event.type;const{x,y}=getRelativePosition(event,chart);return{type,chart,native:event,x:x!==undefined?x:null,y:y!==undefined?y:null,};}
function nodeListContains(nodeList,canvas){for(const node of nodeList){if(node===canvas||node.contains(canvas)){return true;}}}
function createAttachObserver(chart,type,listener){const canvas=chart.canvas;const observer=new MutationObserver(entries=>{let trigger=false;for(const entry of entries){trigger=trigger||nodeListContains(entry.addedNodes,canvas);trigger=trigger&&!nodeListContains(entry.removedNodes,canvas);}
if(trigger){listener();}});observer.observe(document,{childList:true,subtree:true});return observer;}
@ -1180,7 +1187,7 @@ function createDataContext(parent,index,element){return createContext(parent,{ac
function clearStacks(meta,items){const datasetIndex=meta.controller.index;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][datasetIndex]===undefined){return;}
delete stacks[axis][datasetIndex];}}
const isDirectUpdateMode=(mode)=>mode==='reset'||mode==='none';const cloneIfNotShared=(cached,shared)=>shared?cached:Object.assign({},cached);const createStack=(canStack,meta,chart)=>canStack&&!meta.hidden&&meta._stacked&&{keys:getSortedDatasetIndices(chart,true),values:null};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();}
const isDirectUpdateMode=(mode)=>mode==='reset'||mode==='none';const cloneIfNotShared=(cached,shared)=>shared?cached:Object.assign({},cached);const createStack=(canStack,meta,chart)=>canStack&&!meta.hidden&&meta._stacked&&{keys:getSortedDatasetIndices(chart,true),values:null};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.supportsDecimation=false;this.$context=undefined;this._syncList=[];this.initialize();}
initialize(){const meta=this._cachedMeta;this.configure();this.linkScales();meta._stacked=isStacked(meta.vScale,meta);this.addElements();}
updateIndex(datasetIndex){if(this.index!==datasetIndex){clearStacks(this._cachedMeta);}
this.index=datasetIndex;}
@ -1331,7 +1338,7 @@ getLabels(){const data=this.chart.data;return this.options.labels||(this.isHoriz
beforeLayout(){this._cache={};this._dataLimitsCached=false;}
beforeUpdate(){callback(this.options.beforeUpdate,[this]);}
update(maxWidth,maxHeight,margins){const{beginAtZero,grace,ticks:tickOpts}=this.options;const sampleSize=tickOpts.sampleSize;this.beforeUpdate();this.maxWidth=maxWidth;this.maxHeight=maxHeight;this._margins=margins=Object.assign({left:0,right:0,top:0,bottom:0},margins);this.ticks=null;this._labelSizes=null;this._gridLineItems=null;this._labelItems=null;this.beforeSetDimensions();this.setDimensions();this.afterSetDimensions();this._maxLength=this.isHorizontal()?this.width+margins.left+margins.right:this.height+margins.top+margins.bottom;if(!this._dataLimitsCached){this.beforeDataLimits();this.determineDataLimits();this.afterDataLimits();this._range=_addGrace(this,grace,beginAtZero);this._dataLimitsCached=true;}
this.beforeBuildTicks();this.ticks=this.buildTicks()||[];this.afterBuildTicks();const samplingEnabled=sampleSize<this.ticks.length;this._convertTicksToLabels(samplingEnabled?sample(this.ticks,sampleSize):this.ticks);this.configure();this.beforeCalculateLabelRotation();this.calculateLabelRotation();this.afterCalculateLabelRotation();if(tickOpts.display&&(tickOpts.autoSkip||tickOpts.source==='auto')){this.ticks=autoSkip(this,this.ticks);this._labelSizes=null;}
this.beforeBuildTicks();this.ticks=this.buildTicks()||[];this.afterBuildTicks();const samplingEnabled=sampleSize<this.ticks.length;this._convertTicksToLabels(samplingEnabled?sample(this.ticks,sampleSize):this.ticks);this.configure();this.beforeCalculateLabelRotation();this.calculateLabelRotation();this.afterCalculateLabelRotation();if(tickOpts.display&&(tickOpts.autoSkip||tickOpts.source==='auto')){this.ticks=autoSkip(this,this.ticks);this._labelSizes=null;this.afterAutoSkip();}
if(samplingEnabled){this._convertTicksToLabels(this.ticks);}
this.beforeFit();this.fit();this.afterFit();this.afterUpdate();}
configure(){let reversePixels=this.options.reverse;let startPixel,endPixel;if(this.isHorizontal()){startPixel=this.left;endPixel=this.right;}else{startPixel=this.top;endPixel=this.bottom;reversePixels=!reversePixels;}
@ -1357,12 +1364,13 @@ const labelSizes=this._getLabelSizes();const maxLabelWidth=labelSizes.widest.wid
-tickOpts.padding-getTitleHeight(options.title,this.chart.options.font);maxLabelDiagonal=Math.sqrt(maxLabelWidth*maxLabelWidth+maxLabelHeight*maxLabelHeight);labelRotation=toDegrees(Math.min(Math.asin(_limitValue((labelSizes.highest.height+6)/tickWidth,-1,1)),Math.asin(_limitValue(maxHeight/maxLabelDiagonal,-1,1))-Math.asin(_limitValue(maxLabelHeight/maxLabelDiagonal,-1,1))));labelRotation=Math.max(minRotation,Math.min(maxRotation,labelRotation));}
this.labelRotation=labelRotation;}
afterCalculateLabelRotation(){callback(this.options.afterCalculateLabelRotation,[this]);}
afterAutoSkip(){}
beforeFit(){callback(this.options.beforeFit,[this]);}
fit(){const minSize={width:0,height:0};const{chart,options:{ticks:tickOpts,title:titleOpts,grid:gridOpts}}=this;const display=this._isVisible();const isHorizontal=this.isHorizontal();if(display){const titleHeight=getTitleHeight(titleOpts,chart.options.font);if(isHorizontal){minSize.width=this.maxWidth;minSize.height=getTickMarkLength(gridOpts)+titleHeight;}else{minSize.height=this.maxHeight;minSize.width=getTickMarkLength(gridOpts)+titleHeight;}
if(tickOpts.display&&this.ticks.length){const{first,last,widest,highest}=this._getLabelSizes();const tickPadding=tickOpts.padding*2;const angleRadians=toRadians(this.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(this.maxHeight,minSize.height+labelHeight+tickPadding);}else{const labelWidth=tickOpts.mirror?0:cos*widest.width+sin*highest.height;minSize.width=Math.min(this.maxWidth,minSize.width+labelWidth+tickPadding);}
this._calculatePadding(first,last,sin,cos);}}
this._handleMargins();if(isHorizontal){this.width=this._length=chart.width-this._margins.left-this._margins.right;this.height=minSize.height;}else{this.width=minSize.width;this.height=this._length=chart.height-this._margins.top-this._margins.bottom;}}
_calculatePadding(first,last,sin,cos){const{ticks:{align,padding},position}=this.options;const isRotated=this.labelRotation!==0;const labelsBelowTicks=position!=='top'&&this.axis==='x';if(this.isHorizontal()){const offsetLeft=this.getPixelForTick(0)-this.left;const offsetRight=this.right-this.getPixelForTick(this.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;}
_calculatePadding(first,last,sin,cos){const{ticks:{align,padding},position}=this.options;const isRotated=this.labelRotation!==0;const labelsBelowTicks=position!=='top'&&this.axis==='x';if(this.isHorizontal()){const offsetLeft=this.getPixelForTick(0)-this.left;const offsetRight=this.right-this.getPixelForTick(this.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 if(align!=='inner'){paddingLeft=first.width/2;paddingRight=last.width/2;}
this.paddingLeft=Math.max((paddingLeft-offsetLeft+padding)*this.width/(this.width-offsetLeft),0);this.paddingRight=Math.max((paddingRight-offsetRight+padding)*this.width/(this.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;}
this.paddingTop=paddingTop+padding;this.paddingBottom=paddingBottom+padding;}}
_handleMargins(){if(this._margins){this._margins.left=Math.max(this.paddingLeft,this._margins.left);this._margins.top=Math.max(this.paddingTop,this._margins.top);this._margins.right=Math.max(this.paddingRight,this._margins.right);this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom);}}
@ -1403,15 +1411,16 @@ _computeLabelItems(chartArea){const axis=this.axis;const options=this.options;co
textAlign=this._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=this.chart.scales[positionAxisID].getPixelForValue(value);}
textAlign=this._getYAxisLabelAlignment(tl).textAlign;}
if(axis==='y'){if(align==='start'){textBaseline='top';}else if(align==='end'){textBaseline='bottom';}}
const labelSizes=this._getLabelSizes();for(i=0,ilen=ticks.length;i<ilen;++i){tick=ticks[i];label=tick.label;const optsAtIndex=optionTicks.setContext(this.getContext(i));pixel=this.getPixelForTick(i)+optionTicks.labelOffset;font=this._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;}}
const labelSizes=this._getLabelSizes();for(i=0,ilen=ticks.length;i<ilen;++i){tick=ticks[i];label=tick.label;const optsAtIndex=optionTicks.setContext(this.getContext(i));pixel=this.getPixelForTick(i)+optionTicks.labelOffset;font=this._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;let tickTextAlign=textAlign;if(isHorizontal){x=pixel;if(textAlign==='inner'){if(i===ilen-1){tickTextAlign=!this.options.reverse?'right':'left';}else if(i===0){tickTextAlign=!this.options.reverse?'left':'right';}else{tickTextAlign='center';}}
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,});}
items.push({rotation,label,font,color,strokeColor,strokeWidth,textOffset,textAlign:tickTextAlign,textBaseline,translation:[x,y],backdrop,});}
return items;}
_getXAxisLabelAlignment(){const{position,ticks}=this.options;const rotation=-toRadians(this.labelRotation);if(rotation){return position==='top'?'left':'right';}
let align='center';if(ticks.align==='start'){align='left';}else if(ticks.align==='end'){align='right';}
let align='center';if(ticks.align==='start'){align='left';}else if(ticks.align==='end'){align='right';}else if(ticks.align==='inner'){align='inner';}
return align;}
_getYAxisLabelAlignment(tl){const{position,ticks:{crossAlign,mirror,padding}}=this.options;const labelSizes=this._getLabelSizes();const tickAndPadding=tl+padding;const widest=labelSizes.widest.width;let textAlign;let x;if(position==='left'){if(mirror){x=this.right+padding;if(crossAlign==='near'){textAlign='left';}else if(crossAlign==='center'){textAlign='center';x+=(widest/2);}else{textAlign='right';x+=widest;}}else{x=this.right-tickAndPadding;if(crossAlign==='near'){textAlign='right';}else if(crossAlign==='center'){textAlign='center';x-=(widest/2);}else{textAlign='left';x=this.left;}}}else if(position==='right'){if(mirror){x=this.left+padding;if(crossAlign==='near'){textAlign='right';}else if(crossAlign==='center'){textAlign='center';x-=(widest/2);}else{textAlign='left';x-=widest;}}else{x=this.left+tickAndPadding;if(crossAlign==='near'){textAlign='left';}else if(crossAlign==='center'){textAlign='center';x+=widest/2;}else{textAlign='right';x=this.right;}}}else{textAlign='right';}
return{textAlign,x};}
@ -1547,7 +1556,7 @@ const cacheKey=prefixes.join();let cached=cache.get(cacheKey);if(!cached){const
return cached;}
const hasFunction=value=>isObject(value)&&Object.getOwnPropertyNames(value).reduce((acc,key)=>acc||isFunction(value[key]),false);function needContext(proxy,names){const{isScriptable,isIndexable}=_descriptors(proxy);for(const prop of names){const scriptable=isScriptable(prop);const indexable=isIndexable(prop);const value=(indexable||scriptable)&&proxy[prop];if((scriptable&&(isFunction(value)||hasFunction(value)))||(indexable&&isArray(value))){return true;}}
return false;}
var version="3.7.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');}
var version="3.8.0";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);}
@ -1629,6 +1638,7 @@ _drawDataset(meta){const ctx=this.ctx;const clip=meta._clip;const useClip=!clip.
if(useClip){clipArea(ctx,{left:clip.left===false?0:area.left-clip.left,right:clip.right===false?this.width:area.right+clip.right,top:clip.top===false?0:area.top-clip.top,bottom:clip.bottom===false?this.height:area.bottom+clip.bottom});}
meta.controller.draw();if(useClip){unclipArea(ctx);}
args.cancelable=false;this.notifyPlugins('afterDatasetDraw',args);}
isPointInArea(point){return _isPointInArea(point,this.chartArea,this._minPadding);}
getElementsAtEventForMode(e,mode,options,useFinalPosition){const method=Interaction.modes[mode];if(typeof method==='function'){return method(this,e,options,useFinalPosition);}
return[];}
getDatasetMeta(datasetIndex){const dataset=this.data.datasets[datasetIndex];const metasets=this._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);}
@ -1662,7 +1672,7 @@ return{datasetIndex,element:meta.data[index],index,};});const changed=!_elements
notifyPlugins(hook,args,filter){return this._plugins.notify(this,hook,args,filter);}
_updateHoverStyles(active,lastActive,replay){const hoverOptions=this.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){this.updateHoverStyle(deactivated,hoverOptions.mode,false);}
if(activated.length&&hoverOptions.mode){this.updateHoverStyle(activated,hoverOptions.mode,true);}}
_eventHandler(e,replay){const args={event:e,replay,cancelable:true,inChartArea:_isPointInArea(e,this.chartArea,this._minPadding)};const eventFilter=(plugin)=>(plugin.options.events||this.options.events).includes(e.native.type);if(this.notifyPlugins('beforeEvent',args,eventFilter)===false){return;}
_eventHandler(e,replay){const args={event:e,replay,cancelable:true,inChartArea:this.isPointInArea(e)};const eventFilter=(plugin)=>(plugin.options.events||this.options.events).includes(e.native.type);if(this.notifyPlugins('beforeEvent',args,eventFilter)===false){return;}
const changed=this._handleEvent(e,replay,args.inChartArea);args.cancelable=false;this.notifyPlugins('afterEvent',args,eventFilter);if(changed||args.changed){this.render();}
return this;}
_handleEvent(e,replay,inChartArea){const{_active:lastActive=[],options}=this;const useFinalPosition=replay;const active=this._getActiveElements(e,lastActive,inChartArea,useFinalPosition);const isClick=_isClickEvent(e);const lastEvent=determineLastEvent(e,this._lastEvent,inChartArea,isClick);if(inChartArea){this._lastEvent=null;callback(options.onHover,[e,active,this],this);if(isClick){callback(options.onClick,[e,active,this],this);}}
@ -1739,7 +1749,7 @@ if(floating){value=custom.barStart;length=custom.barEnd-custom.barStart;if(value
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(Math.abs(size)<minBarLength){size=barSign(size,vScale,actualBase)*minBarLength;if(value===actualBase){base-=size/2;}
head=base+size;}
const startPixel=vScale.getPixelForDecimal(0);const endPixel=vScale.getPixelForDecimal(1);const min=Math.min(startPixel,endPixel);const max=Math.max(startPixel,endPixel);base=Math.max(Math.min(base,max),min);head=base+size;}
if(base===vScale.getPixelForValue(actualBase)){const halfGrid=sign(size)*vScale.getLineWidthForValue(actualBase)/2;base+=halfGrid;size-=halfGrid;}
return{size,base,head,center:head+size/2};}
_calculateBarIndexPixels(index,ruler){const scale=ruler.scale;const options=this.options;const skipNull=options.skipNull;const maxBarThickness=valueOrDefault(options.maxBarThickness,Infinity);let center,size;if(ruler.grouped){const stackCount=skipNull?this._getStackCount(index):ruler.stackCount;const range=options.barThickness==='flex'?computeFlexCategoryTraits(index,ruler,options,stackCount):computeFitCategoryTraits(index,ruler,options,stackCount);const stackIndex=this._getStackIndex(this.index,this._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(this.getParsed(index)[scale.axis],index);size=Math.min(maxBarThickness,ruler.min*ruler.ratio);}
@ -1796,11 +1806,11 @@ _getRingWeight(datasetIndex){return Math.max(valueOrDefault(this.chart.data.data
_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();}
return dataLabel;}}}}};class LineController extends DatasetController{initialize(){this.enableOptionSharing=true;this.supportsDecimation=true;super.initialize();}
update(mode){const meta=this._cachedMeta;const{dataset:line,data:points=[],_dataset}=meta;const animationsDisabled=this.chart._animationsDisabled;let{start,count}=getStartAndCountOfVisiblePoints(meta,points,animationsDisabled);this._drawStart=start;this._drawCount=count;if(scaleRangesChanged(meta)){start=0;count=points.length;}
line._chart=this.chart;line._datasetIndex=this.index;line._decimated=!!_dataset._decimated;line.points=points;const options=this.resolveDatasetElementOptions(mode);if(!this.options.showLine){options.borderWidth=0;}
options.segment=this.options.segment;this.updateElement(line,undefined,{animated:!animationsDisabled,options},mode);this.updateElements(points,start,count,mode);}
updateElements(points,start,count,mode){const reset=mode==='reset';const{iScale,vScale,_stacked,_dataset}=this._cachedMeta;const firstOpts=this.resolveDataElementOptions(start,mode);const sharedOptions=this.getSharedOptions(firstOpts);const includeOptions=this.includeOptions(mode,sharedOptions);const iAxis=iScale.axis;const vAxis=vScale.axis;const{spanGaps,segment}=this.options;const maxGapLength=isNumber(spanGaps)?spanGaps:Number.POSITIVE_INFINITY;const directUpdate=this.chart._animationsDisabled||reset||mode==='none';let prevParsed=start>0&&this.getParsed(start-1);for(let i=start;i<start+count;++i){const point=points[i];const parsed=this.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?this.applyStack(vScale,parsed,_stacked):parsed[vAxis],i);properties.skip=isNaN(iPixel)||isNaN(vPixel)||nullData;properties.stop=i>0&&(parsed[iAxis]-prevParsed[iAxis])>maxGapLength;if(segment){properties.parsed=parsed;properties.raw=_dataset.data[i];}
updateElements(points,start,count,mode){const reset=mode==='reset';const{iScale,vScale,_stacked,_dataset}=this._cachedMeta;const firstOpts=this.resolveDataElementOptions(start,mode);const sharedOptions=this.getSharedOptions(firstOpts);const includeOptions=this.includeOptions(mode,sharedOptions);const iAxis=iScale.axis;const vAxis=vScale.axis;const{spanGaps,segment}=this.options;const maxGapLength=isNumber(spanGaps)?spanGaps:Number.POSITIVE_INFINITY;const directUpdate=this.chart._animationsDisabled||reset||mode==='none';let prevParsed=start>0&&this.getParsed(start-1);for(let i=start;i<start+count;++i){const point=points[i];const parsed=this.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?this.applyStack(vScale,parsed,_stacked):parsed[vAxis],i);properties.skip=isNaN(iPixel)||isNaN(vPixel)||nullData;properties.stop=i>0&&(Math.abs(parsed[iAxis]-prevParsed[iAxis]))>maxGapLength;if(segment){properties.parsed=parsed;properties.raw=_dataset.data[i];}
if(includeOptions){properties.options=sharedOptions||this.resolveDataElementOptions(i,point.active?'active':mode);}
if(!directUpdate){this.updateElement(point,i,properties,mode);}
prevParsed=parsed;}
@ -1815,21 +1825,25 @@ function scaleRangesChanged(meta){const{xScale,yScale,_scaleRanges}=meta;const n
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 meta=this._cachedMeta;const chart=this.chart;const labels=chart.data.labels||[];const value=formatNumber(meta._parsed[index].r,chart.options.locale);return{label:labels[index]||'',value,};}
parseObjectData(meta,data,start,count){return _parseObjectDataRadialScale.bind(this)(meta,data,start,count);}
update(mode){const arcs=this._cachedMeta.data;this._updateRadius();this.updateElements(arcs,0,arcs.length,mode);}
getMinMax(){const meta=this._cachedMeta;const range={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};meta.data.forEach((element,index)=>{const parsed=this.getParsed(index).r;if(!isNaN(parsed)&&this.chart.getDataVisibility(index)){if(parsed<range.min){range.min=parsed;}
if(parsed>range.max){range.max=parsed;}}});return range;}
_updateRadius(){const chart=this.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();this.outerRadius=outerRadius-(radiusLength*this.index);this.innerRadius=this.outerRadius-radiusLength;}
updateElements(arcs,start,count,mode){const reset=mode==='reset';const chart=this.chart;const dataset=this.getDataset();const opts=chart.options;const animationOpts=opts.animation;const scale=this._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/this.countVisibleElements();for(i=0;i<start;++i){angle+=this._computeAngle(i,mode,defaultAngle);}
for(i=start;i<start+count;i++){const arc=arcs[i];let startAngle=angle;let endAngle=angle+this._computeAngle(i,mode,defaultAngle);let outerRadius=chart.getDataVisibility(i)?scale.getDistanceFromCenterForValue(dataset.data[i]):0;angle=endAngle;if(reset){if(animationOpts.animateScale){outerRadius=0;}
updateElements(arcs,start,count,mode){const reset=mode==='reset';const chart=this.chart;const opts=chart.options;const animationOpts=opts.animation;const scale=this._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/this.countVisibleElements();for(i=0;i<start;++i){angle+=this._computeAngle(i,mode,defaultAngle);}
for(i=start;i<start+count;i++){const arc=arcs[i];let startAngle=angle;let endAngle=angle+this._computeAngle(i,mode,defaultAngle);let outerRadius=chart.getDataVisibility(i)?scale.getDistanceFromCenterForValue(this.getParsed(i).r):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:this.resolveDataElementOptions(i,arc.active?'active':mode)};this.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;}
countVisibleElements(){const meta=this._cachedMeta;let count=0;meta.data.forEach((element,index)=>{if(!isNaN(this.getParsed(index).r)&&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 vScale=this._cachedMeta.vScale;const parsed=this.getParsed(index);return{label:vScale.getLabels()[index],value:''+vScale.getLabelForValue(parsed[vScale.axis])};}
parseObjectData(meta,data,start,count){return _parseObjectDataRadialScale.bind(this)(meta,data,start,count);}
update(mode){const meta=this._cachedMeta;const line=meta.dataset;const points=meta.data||[];const labels=meta.iScale.getLabels();line.points=points;if(mode!=='resize'){const options=this.resolveDatasetElementOptions(mode);if(!this.options.showLine){options.borderWidth=0;}
const properties={_loop:true,_fullLoop:labels.length===points.length,options};this.updateElement(line,undefined,properties,mode);}
this.updateElements(points,0,points.length,mode);}
updateElements(points,start,count,mode){const dataset=this.getDataset();const scale=this._cachedMeta.rScale;const reset=mode==='reset';for(let i=start;i<start+count;i++){const point=points[i];const options=this.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};this.updateElement(point,i,properties,mode);}}}
updateElements(points,start,count,mode){const scale=this._cachedMeta.rScale;const reset=mode==='reset';for(let i=start;i<start+count;i++){const point=points[i];const options=this.resolveDataElementOptions(i,point.active?'active':mode);const pointPosition=scale.getPointPositionForValue(i,this.getParsed(i).r);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};this.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=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();}
@ -1948,38 +1962,46 @@ if(maxDefined){count=_limitValue(_lookupByKey(points,iScale.axis,max).hi+1,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;}
if(!meta.controller.supportsDecimation){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);const threshold=options.threshold||4*availableWidth;if(count<=threshold){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;}
dataset._decimated=decimated;});},destroy(chart){cleanDecimatedData(chart);}};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 _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 _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 _findSegmentEnd(start,end,points){for(;end>start;end--){const point=points[end];if(!isNaN(point.x)&&!isNaN(point.y)){break;}}
return 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 _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 _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){return decodeTargetIndex(fill[0],index,target,count);}
return['origin','start','end','stack','shape'].indexOf(fill)>=0&&fill;}
function decodeTargetIndex(firstCh,index,target,count){if(firstCh==='-'||firstCh==='+'){target=index+target;}
if(target===index||target<0||target>=count){return false;}
return target;}
function _getTargetPixel(fill,scale){let pixel=null;if(fill==='start'){pixel=scale.bottom;}else if(fill==='end'){pixel=scale.top;}else if(isObject(fill)){pixel=scale.getPixelForValue(fill.value);}else if(scale.getBasePixel){pixel=scale.getBasePixel();}
return pixel;}
function _getTargetValue(fill,scale,startValue){let value;if(fill==='start'){value=startValue;}else if(fill==='end'){value=scale.options.reverse?scale.min:scale.max;}else if(isObject(fill)){value=fill.value;}else{value=scale.getBaseValue();}
return value;}
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','shape'].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{scale,index,line}=source;const points=[];const segments=line.segments;const sourcePoints=line.points;const linesBelow=getLinesBelow(scale,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);}}
function _buildStackLine(source){const{scale,index,line}=source;const points=[];const segments=line.segments;const sourcePoints=line.points;const linesBelow=getLinesBelow(scale,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:{}});}
function getLinesBelow(scale,index){const below=[];const metas=scale.getMatchingVisibleMetas('line');for(let i=0;i<metas.length;i++){const meta=metas[i];if(meta.index===index){break;}
if(!meta.hidden){below.unshift(meta.dataset);}}
@ -1990,45 +2012,40 @@ 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(_isBetween(pointValue,firstValue,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);}
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 _getTarget(source){const{chart,fill,line}=source;if(isNumberFinite(fill)){return getLineByIndex(chart,fill);}
if(fill==='stack'){return _buildStackLine(source);}
if(fill==='shape'){return true;}
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){const{segments,points}=target;let first=true;let lineLoop=false;ctx.beginPath();for(const segment of segments){const{start,end}=segment;const firstPoint=points[start];const lastPoint=points[findSegmentEnd(start,end,points)];if(first){ctx.moveTo(firstPoint.x,firstPoint.y);first=false;}else{ctx.lineTo(firstPoint.x,clipY);ctx.lineTo(firstPoint.x,firstPoint.y);}
return _createBoundaryLine(boundary,line);}
function getLineByIndex(chart,index){const meta=chart.getDatasetMeta(index);const visible=meta&&chart.isDatasetVisible(index);return visible?meta.dataset:null;}
function computeBoundary(source){const scale=source.scale||{};if(scale.getPointPositionForValue){return computeCircularBoundary(source);}
return computeLinearBoundary(source);}
function computeLinearBoundary(source){const{scale={},fill}=source;const pixel=_getTargetPixel(fill,scale);if(isNumberFinite(pixel)){const horizontal=scale.isHorizontal();return{x:horizontal?pixel:null,y:horizontal?null:pixel};}
return null;}
function computeCircularBoundary(source){const{scale,fill}=source;const options=scale.options;const length=scale.getLabels().length;const start=options.reverse?scale.max:scale.min;const value=_getTargetValue(fill,scale,start);const target=[];if(options.grid.circular){const center=scale.getPointPositionForValue(0,start);return new simpleArc({x:center.x,y:center.y,radius:scale.getDistanceFromCenterForValue(value)});}
for(let i=0;i<length;++i){target.push(scale.getPointPositionForValue(i,value));}
return target;}
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);}}
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){clipVertical(ctx,target,area.top);fill(ctx,{line,target,color:above,scale,property});ctx.restore();ctx.save();clipVertical(ctx,target,area.bottom);}
fill(ctx,{line,target,color:below,scale,property});ctx.restore();}
function clipVertical(ctx,target,clipY){const{segments,points}=target;let first=true;let lineLoop=false;ctx.beginPath();for(const segment of segments){const{start,end}=segment;const firstPoint=points[start];const lastPoint=points[_findSegmentEnd(start,end,points)];if(first){ctx.moveTo(firstPoint.x,firstPoint.y);first=false;}else{ctx.lineTo(firstPoint.x,clipY);ctx.lineTo(firstPoint.x,firstPoint.y);}
lineLoop=!!target.pathSegment(ctx,segment,{move:lineLoop});if(lineLoop){ctx.closePath();}else{ctx.lineTo(lastPoint.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;const notShape=target!==true;ctx.save();ctx.fillStyle=backgroundColor;clipBounds(ctx,scale,notShape&&getBounds(property,start,end));ctx.beginPath();const lineLoop=!!line.pathSegment(ctx,src);let loop;if(notShape){if(lineLoop){ctx.closePath();}else{interpolatedLineTo(ctx,target,end,property);}
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;const notShape=target!==true;ctx.save();ctx.fillStyle=backgroundColor;clipBounds(ctx,scale,notShape&&_getBounds(property,start,end));ctx.beginPath();const lineLoop=!!line.pathSegment(ctx,src);let loop;if(notShape){if(lineLoop){ctx.closePath();}else{interpolatedLineTo(ctx,target,end,property);}
const targetLoop=!!target.pathSegment(ctx,tgt,{move:lineLoop,reverse:true});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,};}
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);}}
var index={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);}
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){this.maxWidth=maxWidth;this.maxHeight=maxHeight;this._margins=margins;this.setDimensions();this.buildLabels();this.fit();}
setDimensions(){if(this.isHorizontal()){this.width=this.maxWidth;this.left=this._margins.left;this.right=this.width;}else{this.height=this.maxHeight;this.top=this._margins.top;this.bottom=this.height;}}
@ -2062,9 +2079,9 @@ _computeTitleHeight(){const titleOpts=this.options.title;const titleFont=toFont(
_getLegendItemAt(x,y){let i,hitBox,lh;if(_isBetween(x,this.left,this.right)&&_isBetween(y,this.top,this.bottom)){lh=this.legendHitBoxes;for(i=0;i<lh.length;++i){hitBox=lh[i];if(_isBetween(x,hitBox.left,hitBox.left+hitBox.width)&&_isBetween(y,hitBox.top,hitBox.top+hitBox.height)){return this.legendItems[i];}}}
return null;}
handleEvent(e){const opts=this.options;if(!isListened(e.type,opts)){return;}
const hoveredItem=this._getLegendItemAt(e.x,e.y);if(e.type==='mousemove'){const previous=this._hoveredItem;const sameItem=itemsEqual(previous,hoveredItem);if(previous&&!sameItem){callback(opts.onLeave,[e,previous,this],this);}
const hoveredItem=this._getLegendItemAt(e.x,e.y);if(e.type==='mousemove'||e.type==='mouseout'){const previous=this._hoveredItem;const sameItem=itemsEqual(previous,hoveredItem);if(previous&&!sameItem){callback(opts.onLeave,[e,previous,this],this);}
this._hoveredItem=hoveredItem;if(hoveredItem&&!sameItem){callback(opts.onHover,[e,hoveredItem,this],this);}}else if(hoveredItem){callback(opts.onClick,[e,hoveredItem,this],this);}}}
function isListened(type,opts){if(type==='mousemove'&&(opts.onHover||opts.onLeave)){return true;}
function isListened(type,opts){if((type==='mousemove'||type==='mouseout')&&(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;}
@ -2154,6 +2171,7 @@ ctx.lineTo(x+bottomLeft,y+height);ctx.quadraticCurveTo(x,y+height,x,y+height-bot
ctx.lineTo(x,y+topLeft);ctx.quadraticCurveTo(x,y,x+topLeft,y);ctx.closePath();ctx.fill();if(options.borderWidth>0){ctx.stroke();}}
_updateAnimationTarget(options){const chart=this.chart;const anims=this.$animations;const animX=anims&&anims.x;const animY=anims&&anims.y;if(animX||animY){const position=positioners[options.position].call(this,this._active,this._eventPosition);if(!position){return;}
const size=this._size=getTooltipSize(this,options);const positionAndSize=Object.assign({},position,this._size);const alignment=determineAlignment(chart,options,positionAndSize);const point=getBackgroundPoint(options,positionAndSize,alignment,chart);if(animX._to!==point.x||animY._to!==point.y){this.xAlign=alignment.xAlign;this.yAlign=alignment.yAlign;this.width=size.width;this.height=size.height;this.caretX=position.x;this.caretY=position.y;this._resolveAnimations().update(this,point);}}}
_willRender(){return!!this.opacity;}
draw(ctx){const options=this.options.setContext(this.getContext());let opacity=this.opacity;if(!opacity){return;}
this._updateAnimationTarget(options);const tooltipSize={width:this.width,height:this.height};const pt={x:this.x,y:this.y};opacity=Math.abs(opacity)<1e-3?0:opacity;const padding=toPadding(options.padding);const hasTooltipContent=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;if(options.enabled&&hasTooltipContent){ctx.save();ctx.globalAlpha=opacity;this.drawBackground(pt,ctx,tooltipSize,options);overrideTextDirection(ctx,options.textDirection);pt.y+=padding.top;this.drawTitle(pt,ctx,options);this.drawBody(pt,ctx,options);this.drawFooter(pt,ctx,options);restoreTextDirection(ctx,options.textDirection);ctx.restore();}}
getActiveElements(){return this._active||[];}
@ -2167,13 +2185,12 @@ if(!inChartArea){return lastActive;}
const active=this.chart.getElementsAtEventForMode(e,options.mode,options,replay);if(options.reverse){active.reverse();}
return active;}
_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,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.inChartArea)){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,boxPadding: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: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];}}
Tooltip.positioners=positioners;var plugin_tooltip={id:'tooltip',_element:Tooltip,positioners,afterInit(chart,_args,options){if(options){chart.tooltip=new Tooltip({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;if(tooltip&&tooltip._willRender()){const args={tooltip};if(chart.notifyPlugins('beforeTooltipDraw',args)===false){return;}
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.inChartArea)){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,boxPadding: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: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=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,addedLabels)=>{if(typeof raw==='string'){index=labels.push(raw)-1;addedLabels.unshift({index,label:raw});}else if(isNaN(raw)){index=null;}
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=Object.freeze({__proto__:null,Decimation:plugin_decimation,Filler:index,Legend:plugin_legend,SubTitle:plugin_subtitle,Title:plugin_title,Tooltip:plugin_tooltip});const addIfString=(labels,raw,index,addedLabels)=>{if(typeof raw==='string'){index=labels.push(raw)-1;addedLabels.unshift({index,label:raw});}else if(isNaN(raw)){index=null;}
return index;};function findOrAddLabel(labels,raw,index,addedLabels){const first=labels.indexOf(raw);if(first===-1){return addIfString(labels,raw,index,addedLabels);}
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;this._addedLabels=[];}
@ -2268,7 +2285,7 @@ function leftForTextAlign(x,w,align){if(align==='right'){x-=w;}else if(align==='
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.getPointLabelContext(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);}
function drawPointLabels(scale,labelCount){const{ctx,options:{pointLabels}}=scale;for(let i=labelCount-1;i>=0;i--){const optsAtIndex=pointLabels.setContext(scale.getPointLabelContext(i));const plFont=toFont(optsAtIndex.font);const{x,y,textAlign,left,top,right,bottom}=scale._pointLabelItems[i];const{backdropColor}=optsAtIndex;if(!isNullOrUndef(backdropColor)){const borderRadius=toTRBLCorners(optsAtIndex.borderRadius);const padding=toPadding(optsAtIndex.backdropPadding);ctx.fillStyle=backdropColor;const backdropLeft=left-padding.left;const backdropTop=top-padding.top;const backdropWidth=right-left+padding.width;const backdropHeight=bottom-top+padding.height;if(Object.values(borderRadius).some(v=>v!==0)){ctx.beginPath();addRoundedRectPath(ctx,{x:backdropLeft,y:backdropTop,w:backdropWidth,h:backdropHeight,radius:borderRadius,});ctx.fill();}else{ctx.fillRect(backdropLeft,backdropTop,backdropWidth,backdropHeight);}}
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;}
@ -2335,6 +2352,7 @@ return{min,max};}
buildTicks(){const options=this.options;const timeOpts=options.time;const tickOpts=options.ticks;const timestamps=tickOpts.source==='labels'?this.getLabelTimestamps():this._generate();if(options.bounds==='ticks'&&timestamps.length){this.min=this._userMin||timestamps[0];this.max=this._userMax||timestamps[timestamps.length-1];}
const min=this.min;const max=this.max;const ticks=_filterBetween(timestamps,min,max);this._unit=timeOpts.unit||(tickOpts.autoSkip?determineUnitForAutoTicks(timeOpts.minUnit,this.min,this.max,this._getLabelCapacity(min)):determineUnitForFormatting(this,ticks.length,timeOpts.minUnit,this.min,this.max));this._majorUnit=!tickOpts.major.enabled||this._unit==='year'?undefined:determineMajorUnit(this._unit);this.initOffsets(timestamps);if(options.reverse){ticks.reverse();}
return ticksFromTimestamps(this,ticks,this._majorUnit);}
afterAutoSkip(){if(this.options.offsetAfterAutoskip){this.initOffsets(this.ticks.map(tick=>+tick.value));}}
initOffsets(timestamps){let start=0;let end=0;let first,last;if(this.options.offset&&timestamps.length){first=this.getDecimalForValue(timestamps[0]);if(timestamps.length===1){start=1-first;}else{start=(this.getDecimalForValue(timestamps[1])-first)/2;}
last=this.getDecimalForValue(timestamps[timestamps.length-1]);if(timestamps.length===1){end=last;}else{end=(last-this.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);this._offsets={start,end,factor:1/(start+1+end)};}

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

View file

@ -0,0 +1,213 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
// Reference Material
// https://developers.dailymotion.com/api/#authentication
class Dailymotion extends OAuth2
{
/**
* @var string
*/
private string $endpoint = 'https://api.dailymotion.com';
/**
* @var string
*/
private string $authEndpoint = 'https://www.dailymotion.com/oauth/authorize';
/**
* @var array
*/
protected array $scopes = [
'userinfo',
'email'
];
/**
* @var array
*/
protected array $fields = [
'email',
'id',
'fullname',
'verified'
];
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @return string
*/
public function getName(): string
{
return 'dailymotion';
}
/**
* @return array
*/
public function getFields(): array
{
return $this->fields;
}
/**
* @return string
*/
public function getLoginURL(): string
{
$url = $this->authEndpoint . '?' .
\http_build_query([
'response_type' => 'code',
'client_id' => $this->appID,
'state' => \json_encode($this->state),
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes())
]);
return $url;
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth/token',
["Content-Type: application/x-www-form-urlencoded"],
\http_build_query([
'grant_type' => 'authorization_code',
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"redirect_uri" => $this->callback,
'code' => $code,
'scope' => \implode(' ', $this->getScopes()),
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth/token',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
])
), true);
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
$user = $this->getUser($accessToken);
$userId = $user['id'] ?? '';
return $userId;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
$userEmail = $user['email'] ?? '';
return $userEmail;
}
/**
* Check if the OAuth email is verified
*
* @link https://developers.dailymotion.com/api/#user-fields
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
$user = $this->getUser($accessToken);
return $user['verified'] ?? false;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
$username = $user['fullname'] ?? '';
return $username;
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken): array
{
if (empty($this->user)) {
$user = $this->request(
'GET',
$this->endpoint . '/user/me?fields=' . \implode(',', $this->getFields()),
['Authorization: Bearer ' . \urlencode($accessToken)],
);
$this->user = \json_decode($user, true);
}
return $this->user;
}
}

135
src/Appwrite/DSN/DSN.php Normal file
View file

@ -0,0 +1,135 @@
<?php
namespace Appwrite\DSN;
class DSN
{
/**
* @var string
*/
protected string $scheme;
/**
* @var ?string
*/
protected ?string $user;
/**
* @var ?string
*/
protected ?string $password;
/**
* @var string
*/
protected string $host;
/**
* @var ?string
*/
protected ?string $port;
/**
* @var ?string
*/
protected ?string $database;
/**
* @var ?string
*/
protected ?string $query;
/**
* Construct
*
* Construct a new DSN object
*
* @param string $dsn
*/
public function __construct(string $dsn)
{
$parts = parse_url($dsn);
if (!$parts) {
throw new \InvalidArgumentException("Unable to parse DSN: $dsn");
}
$this->scheme = $parts['scheme'] ?? null;
$this->user = $parts['user'] ?? null;
$this->password = $parts['pass'] ?? null;
$this->host = $parts['host'] ?? null;
$this->port = $parts['port'] ?? null;
$this->database = $parts['path'] ?? null;
$this->query = $parts['query'] ?? null;
}
/**
* Return the scheme.
*
* @return string
*/
public function getScheme(): string
{
return $this->scheme;
}
/**
* Return the user.
*
* @return ?string
*/
public function getUser(): ?string
{
return $this->user;
}
/**
* Return the password.
*
* @return ?string
*/
public function getPassword(): ?string
{
return $this->password;
}
/**
* Return the host
*
* @return string
*/
public function getHost(): string
{
return $this->host;
}
/**
* Return the port
*
* @return ?string
*/
public function getPort(): ?string
{
return $this->port;
}
/**
* Return the database
*
* @return ?string
*/
public function getDatabase(): ?string
{
return ltrim($this->database, '/');
}
/**
* Return the query string
*
* @return ?string
*/
public function getQuery(): ?string
{
return $this->query;
}
}

View file

@ -147,6 +147,7 @@ class Exception extends \Exception
public const PROJECT_INVALID_FAILURE_URL = 'project_invalid_failure_url';
public const PROJECT_MISSING_USER_ID = 'project_missing_user_id';
public const PROJECT_RESERVED_PROJECT = 'project_reserved_project';
public const PROJECT_KEY_EXPIRED = 'project_key_expired';
/** Webhooks */
public const WEBHOOK_NOT_FOUND = 'webhook_not_found';

View file

@ -45,6 +45,7 @@ abstract class Migration
'0.14.0' => 'V13',
'0.14.1' => 'V13',
'0.14.2' => 'V13',
'0.15.0' => 'V13'
];
/**

View file

@ -0,0 +1,301 @@
<?php
namespace Appwrite\Stats;
use Utopia\Database\Database;
use Utopia\Database\Document;
use InfluxDB\Database as InfluxDatabase;
use DateTime;
class Usage
{
protected InfluxDatabase $influxDB;
protected Database $database;
protected $errorHandler;
private array $latestTime = [];
// all the mertics that we are collecting
protected array $metrics = [
'requests' => [
'table' => 'appwrite_usage_requests_all',
],
'network' => [
'table' => 'appwrite_usage_network_all',
],
'executions' => [
'table' => 'appwrite_usage_executions_all',
],
'database.collections.create' => [
'table' => 'appwrite_usage_database_collections_create',
],
'database.collections.read' => [
'table' => 'appwrite_usage_database_collections_read',
],
'database.collections.update' => [
'table' => 'appwrite_usage_database_collections_update',
],
'database.collections.delete' => [
'table' => 'appwrite_usage_database_collections_delete',
],
'database.documents.create' => [
'table' => 'appwrite_usage_database_documents_create',
],
'database.documents.read' => [
'table' => 'appwrite_usage_database_documents_read',
],
'database.documents.update' => [
'table' => 'appwrite_usage_database_documents_update',
],
'database.documents.delete' => [
'table' => 'appwrite_usage_database_documents_delete',
],
'database.collections.collectionId.documents.create' => [
'table' => 'appwrite_usage_database_documents_create',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.read' => [
'table' => 'appwrite_usage_database_documents_read',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.update' => [
'table' => 'appwrite_usage_database_documents_update',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.delete' => [
'table' => 'appwrite_usage_database_documents_delete',
'groupBy' => 'collectionId',
],
'storage.buckets.create' => [
'table' => 'appwrite_usage_storage_buckets_create',
],
'storage.buckets.read' => [
'table' => 'appwrite_usage_storage_buckets_read',
],
'storage.buckets.update' => [
'table' => 'appwrite_usage_storage_buckets_update',
],
'storage.buckets.delete' => [
'table' => 'appwrite_usage_storage_buckets_delete',
],
'storage.files.create' => [
'table' => 'appwrite_usage_storage_files_create',
],
'storage.files.read' => [
'table' => 'appwrite_usage_storage_files_read',
],
'storage.files.update' => [
'table' => 'appwrite_usage_storage_files_update',
],
'storage.files.delete' => [
'table' => 'appwrite_usage_storage_files_delete',
],
'storage.buckets.bucketId.files.create' => [
'table' => 'appwrite_usage_storage_files_create',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.read' => [
'table' => 'appwrite_usage_storage_files_read',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.update' => [
'table' => 'appwrite_usage_storage_files_update',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.delete' => [
'table' => 'appwrite_usage_storage_files_delete',
'groupBy' => 'bucketId',
],
'users.create' => [
'table' => 'appwrite_usage_users_create',
],
'users.read' => [
'table' => 'appwrite_usage_users_read',
],
'users.update' => [
'table' => 'appwrite_usage_users_update',
],
'users.delete' => [
'table' => 'appwrite_usage_users_delete',
],
'users.sessions.create' => [
'table' => 'appwrite_usage_users_sessions_create',
],
'users.sessions.provider.create' => [
'table' => 'appwrite_usage_users_sessions_create',
'groupBy' => 'provider',
],
'users.sessions.delete' => [
'table' => 'appwrite_usage_users_sessions_delete',
],
'functions.functionId.executions' => [
'table' => 'appwrite_usage_executions_all',
'groupBy' => 'functionId',
],
'functions.functionId.compute' => [
'table' => 'appwrite_usage_executions_time',
'groupBy' => 'functionId',
],
'functions.functionId.failures' => [
'table' => 'appwrite_usage_executions_all',
'groupBy' => 'functionId',
'filters' => [
'functionStatus' => 'failed',
],
],
];
protected array $periods = [
[
'key' => '30m',
'multiplier' => 1800,
'startTime' => '-24 hours',
],
[
'key' => '1d',
'multiplier' => 86400,
'startTime' => '-90 days',
],
];
public function __construct(Database $database, InfluxDatabase $influxDB, callable $errorHandler = null)
{
$this->database = $database;
$this->influxDB = $influxDB;
$this->errorHandler = $errorHandler;
}
/**
* Create or Update Mertic
* Create or update each metric in the stats collection for the given project
*
* @param string $projectId
* @param int $time
* @param string $period
* @param string $metric
* @param int $value
* @param int $type
*
* @return void
*/
private function createOrUpdateMetric(string $projectId, int $time, string $period, string $metric, int $value, int $type): void
{
$id = \md5("{$time}_{$period}_{$metric}");
$this->database->setNamespace('_' . $projectId);
try {
$document = $this->database->getDocument('stats', $id);
if ($document->isEmpty()) {
$this->database->createDocument('stats', new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
'metric' => $metric,
'value' => $value,
'type' => $type,
]));
} else {
$this->database->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
);
}
$this->latestTime[$metric][$period] = $time;
} catch (\Exception $e) { // if projects are deleted this might fail
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "sync_project_{$projectId}_metric_{$metric}");
} else {
throw $e;
}
}
}
/**
* Sync From InfluxDB
* Sync stats from influxDB to stats collection in the Appwrite database
*
* @param string $metric
* @param array $options
* @param array $period
*
* @return void
*/
private function syncFromInfluxDB(string $metric, array $options, array $period): void
{
$start = DateTime::createFromFormat('U', \strtotime($period['startTime']))->format(DateTime::RFC3339);
if (!empty($this->latestTime[$metric][$period['key']])) {
$start = DateTime::createFromFormat('U', $this->latestTime[$metric][$period['key']])->format(DateTime::RFC3339);
}
$end = DateTime::createFromFormat('U', \strtotime('now'))->format(DateTime::RFC3339);
$table = $options['table']; //Which influxdb table to query for this metric
$groupBy = empty($options['groupBy']) ? '' : ', "' . $options['groupBy'] . '"'; //Some sub level metrics may be grouped by other tags like collectionId, bucketId, etc
$filters = $options['filters'] ?? []; // Some metrics might have additional filters, like function's status
if (!empty($filters)) {
$filters = ' AND ' . implode(' AND ', array_map(fn ($filter, $value) => "\"{$filter}\"='{$value}'", array_keys($filters), array_values($filters)));
} else {
$filters = '';
}
$query = "SELECT sum(value) AS \"value\" ";
$query .= "FROM \"{$table}\" ";
$query .= "WHERE \"time\" > '{$start}' ";
$query .= "AND \"time\" < '{$end}' ";
$query .= "AND \"metric_type\"='counter' {$filters} ";
$query .= "GROUP BY time({$period['key']}), \"projectId\" {$groupBy} ";
$query .= "FILL(null)";
$result = $this->influxDB->query($query);
$points = $result->getPoints();
foreach ($points as $point) {
$projectId = $point['projectId'];
if (!empty($projectId) && $projectId !== 'console') {
$metricUpdated = $metric;
if (!empty($groupBy)) {
$groupedBy = $point[$options['groupBy']] ?? '';
if (empty($groupedBy)) {
continue;
}
$metricUpdated = str_replace($options['groupBy'], $groupedBy, $metric);
}
$time = \strtotime($point['time']);
$value = (!empty($point['value'])) ? $point['value'] : 0;
$this->createOrUpdateMetric(
$projectId,
$time,
$period['key'],
$metricUpdated,
$value,
0
);
}
}
}
/**
* Collect Stats
* Collect all the stats from Influd DB to Database
*
* @return void
*/
public function collect(): void
{
foreach ($this->metrics as $metric => $options) { //for each metrics
foreach ($this->periods as $period) { // aggregate data for each period
try {
$this->syncFromInfluxDB($metric, $options, $period);
} catch (\Exception $e) {
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e);
} else {
throw $e;
}
}
}
}
}
}

View file

@ -0,0 +1,262 @@
<?php
namespace Appwrite\Stats;
use Utopia\Database\Database;
use Utopia\Database\Document;
class UsageDB extends Usage
{
public function __construct(Database $database, callable $errorHandler = null)
{
$this->database = $database;
$this->errorHandler = $errorHandler;
}
/**
* Create or Update Mertic
* Create or update each metric in the stats collection for the given project
*
* @param string $projectId
* @param string $metric
* @param int $value
*
* @return void
*/
private function createOrUpdateMetric(string $projectId, string $metric, int $value): void
{
foreach ($this->periods as $options) {
$period = $options['key'];
$time = (int) (floor(time() / $options['multiplier']) * $options['multiplier']);
$id = \md5("{$time}_{$period}_{$metric}");
$this->database->setNamespace('_' . $projectId);
try {
$document = $this->database->getDocument('stats', $id);
if ($document->isEmpty()) {
$this->database->createDocument('stats', new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
'metric' => $metric,
'value' => $value,
'type' => 1,
]));
} else {
$this->database->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
);
}
} catch (\Exception$e) { // if projects are deleted this might fail
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "sync_project_{$projectId}_metric_{$metric}");
} else {
throw $e;
}
}
}
}
/**
* Foreach Document
* Call provided callback for each document in the collection
*
* @param string $projectId
* @param string $collection
* @param array $queries
* @param callable $callback
*
* @return void
*/
private function foreachDocument(string $projectId, string $collection, array $queries, callable $callback): void
{
$limit = 50;
$results = [];
$sum = $limit;
$latestDocument = null;
$this->database->setNamespace('_' . $projectId);
while ($sum === $limit) {
try {
$results = $this->database->find($collection, $queries, $limit, cursor:$latestDocument);
} catch (\Exception $e) {
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "fetch_documents_project_{$projectId}_collection_{$collection}");
return;
} else {
throw $e;
}
}
if (empty($results)) {
return;
}
$sum = count($results);
foreach ($results as $document) {
if (is_callable($callback)) {
$callback($document);
}
}
$latestDocument = $results[array_key_last($results)];
}
}
/**
* Sum
* Calculate sum of a attribute of documents in collection
*
* @param string $projectId
* @param string $collection
* @param string $attribute
* @param string $metric
*
* @return int
*/
private function sum(string $projectId, string $collection, string $attribute, string $metric): int
{
$this->database->setNamespace('_' . $projectId);
try {
$sum = (int) $this->database->sum($collection, $attribute);
$this->createOrUpdateMetric($projectId, $metric, $sum);
return $sum;
} catch (\Exception $e) {
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "fetch_sum_project_{$projectId}_collection_{$collection}");
} else {
throw $e;
}
}
}
/**
* Count
* Count number of documents in collection
*
* @param string $projectId
* @param string $collection
* @param string $metric
*
* @return int
*/
private function count(string $projectId, string $collection, string $metric): int
{
$this->database->setNamespace("_{$projectId}");
try {
$count = $this->database->count($collection);
$this->createOrUpdateMetric($projectId, $metric, $count);
return $count;
} catch (\Exception $e) {
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "fetch_count_project_{$projectId}_collection_{$collection}");
} else {
throw $e;
}
}
}
/**
* Deployments Total
* Total sum of storage used by deployments
*
* @param string $projectId
*
* @return int
*/
private function deploymentsTotal(string $projectId): int
{
return $this->sum($projectId, 'deployments', 'size', 'stroage.deployments.total');
}
/**
* Users Stats
* Metric: users.count
*
* @param string $projectId
*
* @return void
*/
private function usersStats(string $projectId): void
{
$this->count($projectId, 'users', 'users.count');
}
/**
* Storage Stats
* Metrics: storage.total, storage.files.total, storage.buckets.{bucketId}.files.total,
* storage.buckets.count, storage.files.count, storage.buckets.{bucketId}.files.count
*
* @param string $projectId
*
* @return void
*/
private function storageStats(string $projectId): void
{
$deploymentsTotal = $this->deploymentsTotal($projectId);
$projectFilesTotal = 0;
$projectFilesCount = 0;
$metric = 'storage.buckets.count';
$this->count($projectId, 'buckets', $metric);
$this->foreachDocument($projectId, 'buckets', [], function ($bucket) use (&$projectFilesCount, &$projectFilesTotal, $projectId,) {
$metric = "storage.buckets.{$bucket->getId()}.files.count";
$count = $this->count($projectId, 'bucket_' . $bucket->getInternalId(), $metric);
$projectFilesCount += $count;
$metric = "storage.buckets.{$bucket->getId()}.files.total";
$sum = $this->sum($projectId, 'bucket_' . $bucket->getInternalId(), 'sizeOriginal', $metric);
$projectFilesTotal += $sum;
});
$this->createOrUpdateMetric($projectId, 'storage.files.count', $projectFilesCount);
$this->createOrUpdateMetric($projectId, 'storage.files.total', $projectFilesTotal);
$this->createOrUpdateMetric($projectId, 'storage.total', $projectFilesTotal + $deploymentsTotal);
}
/**
* Database Stats
* Collect all database stats
* Metrics: database.collections.count, database.collections.{collectionId}.documents.count,
* database.documents.count
*
* @param string $projectId
*
* @return void
*/
private function databaseStats(string $projectId): void
{
$projectDocumentsCount = 0;
$metric = 'database.collections.count';
$this->count($projectId, 'collections', $metric);
$this->foreachDocument($projectId, 'collections', [], function ($collection) use (&$projectDocumentsCount, $projectId,) {
$metric = "database.collections.{$collection->getId()}.documents.count";
$count = $this->count($projectId, 'collection_' . $collection->getInternalId(), $metric);
$projectDocumentsCount += $count;
});
$this->createOrUpdateMetric($projectId, 'database.documents.count', $projectDocumentsCount);
}
/**
* Collect Stats
* Collect all database related stats
*
* @return void
*/
public function collect(): void
{
$this->foreachDocument('console', 'projects', [], function ($project) {
$projectId = $project->getId();
$this->usersStats($projectId);
$this->databaseStats($projectId);
$this->storageStats($projectId);
});
}
}

View file

@ -27,6 +27,12 @@ class Key extends Model
'default' => '',
'example' => 'My API Key',
])
->addRule('expire', [
'type' => self::TYPE_INTEGER,
'description' => 'Key expiration in Unix timestamp.',
'default' => 0,
'example' => '1653990687',
])
->addRule('scopes', [
'type' => self::TYPE_STRING,
'description' => 'Allowed permission scopes.',

View file

@ -58,6 +58,12 @@ class Webhook extends Model
'default' => '',
'example' => 'password',
])
->addRule('signatureKey', [
'type' => self::TYPE_STRING,
'description' => 'Signature key which can be used to validated incoming',
'default' => '',
'example' => 'ad3d581ca230e2b7059c545e5a',
])
;
}

View file

@ -186,10 +186,18 @@ class Executor
);
$response = $this->call(self::METHOD_POST, $route, $headers, $params, true, $requestTimeout);
$status = $response['headers']['status-code'];
if ($status < 400) {
return $response['body'];
}
break;
case $status === 406:
$response = $this->call(self::METHOD_POST, $route, $headers, $params, true, $requestTimeout);
$status = $response['headers']['status-code'];
if ($status < 400) {
return $response['body'];
}
break;
default:
throw new \Exception($response['body']['message'], $status);

View file

@ -118,6 +118,7 @@ trait ProjectCustom
'name' => $project['body']['name'],
'apiKey' => $key['body']['secret'],
'webhookId' => $webhook['body']['$id'],
'signatureKey' => $webhook['body']['signatureKey'],
];
return self::$project;

View file

@ -270,6 +270,7 @@ class AccountCustomClientTest extends Scope
]));
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['headers']['x-ratelimit-remaining'], 99);
$this->assertNotEmpty($response['body']['jwt']);
$this->assertIsString($response['body']['jwt']);

View file

@ -118,6 +118,15 @@ trait DatabaseBase
'required' => true,
]);
$duration = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/attributes/integer', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'duration',
'required' => false,
]);
$actors = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -140,6 +149,11 @@ trait DatabaseBase
$this->assertEquals($releaseYear['body']['type'], 'integer');
$this->assertEquals($releaseYear['body']['required'], true);
$this->assertEquals($duration['headers']['status-code'], 201);
$this->assertEquals($duration['body']['key'], 'duration');
$this->assertEquals($duration['body']['type'], 'integer');
$this->assertEquals($duration['body']['required'], false);
$this->assertEquals($actors['headers']['status-code'], 201);
$this->assertEquals($actors['body']['key'], 'actors');
$this->assertEquals($actors['body']['type'], 'string');
@ -157,10 +171,11 @@ trait DatabaseBase
]), []);
$this->assertIsArray($movies['body']['attributes']);
$this->assertCount(3, $movies['body']['attributes']);
$this->assertCount(4, $movies['body']['attributes']);
$this->assertEquals($movies['body']['attributes'][0]['key'], $title['body']['key']);
$this->assertEquals($movies['body']['attributes'][1]['key'], $releaseYear['body']['key']);
$this->assertEquals($movies['body']['attributes'][2]['key'], $actors['body']['key']);
$this->assertEquals($movies['body']['attributes'][2]['key'], $duration['body']['key']);
$this->assertEquals($movies['body']['attributes'][3]['key'], $actors['body']['key']);
return $data;
}
@ -748,6 +763,7 @@ trait DatabaseBase
'data' => [
'title' => 'Spider-Man: Homecoming',
'releaseYear' => 2017,
'duration' => 0,
'actors' => [
'Tom Holland',
'Zendaya Maree Stoermer',
@ -757,6 +773,7 @@ trait DatabaseBase
'write' => ['user:' . $this->getUser()['$id']],
]);
$document4 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -783,6 +800,7 @@ trait DatabaseBase
$this->assertEquals($document2['headers']['status-code'], 201);
$this->assertEquals($document2['body']['title'], 'Spider-Man: Far From Home');
$this->assertEquals($document2['body']['releaseYear'], 2019);
$this->assertEquals($document2['body']['duration'], null);
$this->assertIsArray($document2['body']['$read']);
$this->assertIsArray($document2['body']['$write']);
$this->assertCount(1, $document2['body']['$read']);
@ -795,6 +813,7 @@ trait DatabaseBase
$this->assertEquals($document3['headers']['status-code'], 201);
$this->assertEquals($document3['body']['title'], 'Spider-Man: Homecoming');
$this->assertEquals($document3['body']['releaseYear'], 2017);
$this->assertEquals($document3['body']['duration'], 0);
$this->assertIsArray($document3['body']['$read']);
$this->assertIsArray($document3['body']['$write']);
$this->assertCount(1, $document3['body']['$read']);

View file

@ -1022,6 +1022,7 @@ class ProjectsConsoleClientTest extends Scope
'security' => false,
'httpUser' => '',
'httpPass' => '',
'signatureKey' => 'My own uniq key',
]);
$this->assertEquals(200, $response['headers']['status-code']);
@ -1037,6 +1038,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(false, $response['body']['security']);
$this->assertEquals('', $response['body']['httpUser']);
$this->assertEquals('', $response['body']['httpPass']);
$this->assertEquals('My own uniq key', $response['body']['signatureKey']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([
'content-type' => 'application/json',
@ -1162,7 +1164,10 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('teams.write', $response['body']['scopes']);
$this->assertNotEmpty($response['body']['secret']);
$data = array_merge($data, ['keyId' => $response['body']['$id']]);
$data = array_merge($data, [
'keyId' => $response['body']['$id'],
'secret' => $response['body']['secret']
]);
/**
* Test for FAILURE
@ -1180,6 +1185,7 @@ class ProjectsConsoleClientTest extends Scope
return $data;
}
/**
* @depends testCreateProjectKey
*/
@ -1192,6 +1198,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
@ -1202,6 +1209,7 @@ class ProjectsConsoleClientTest extends Scope
return $data;
}
/**
* @depends testCreateProjectKey
*/
@ -1213,6 +1221,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys/' . $keyId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $keyId
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
@ -1237,6 +1246,75 @@ class ProjectsConsoleClientTest extends Scope
return $data;
}
/**
* @depends testCreateProject
*/
public function testValidateProjectKey($data): void
{
$id = $data['projectId'] ?? '';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test',
'scopes' => ['health.read'],
'expire' => time() + 3600,
]);
$response = $this->client->call(Client::METHOD_GET, '/health', [
'content-type' => 'application/json',
'x-appwrite-project' => $id,
'x-appwrite-key' => $response['body']['secret']
], []);
$this->assertEquals(200, $response['headers']['status-code']);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test',
'scopes' => ['health.read'],
'expire' => 0,
]);
$response = $this->client->call(Client::METHOD_GET, '/health', [
'content-type' => 'application/json',
'x-appwrite-project' => $id,
'x-appwrite-key' => $response['body']['secret']
], []);
$this->assertEquals(200, $response['headers']['status-code']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test',
'scopes' => ['health.read'],
'expire' => time() - 3600,
]);
$response = $this->client->call(Client::METHOD_GET, '/health', [
'content-type' => 'application/json',
'x-appwrite-project' => $id,
'x-appwrite-key' => $response['body']['secret']
], []);
$this->assertEquals(401, $response['headers']['status-code']);
}
/**
* @depends testCreateProjectKey
*/
@ -1251,6 +1329,7 @@ class ProjectsConsoleClientTest extends Scope
], $this->getHeaders()), [
'name' => 'Key Test Update',
'scopes' => ['users.read', 'users.write', 'collections.read'],
'expire' => time() + 360,
]);
$this->assertEquals(200, $response['headers']['status-code']);

View file

@ -7,6 +7,14 @@ use Tests\E2E\Client;
trait WebhooksBase
{
public static function getWebhookSignature(array $webhook, string $signatureKey): string
{
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
return base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
}
public function testCreateCollection(): array
{
/**
@ -30,6 +38,7 @@ trait WebhooksBase
$this->assertNotEmpty($actors['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -38,7 +47,7 @@ trait WebhooksBase
$this->assertStringContainsString('collections.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
@ -102,6 +111,7 @@ trait WebhooksBase
sleep(10);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -116,7 +126,7 @@ trait WebhooksBase
$this->assertStringContainsString("collections.{$actorsId}.attributes.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.attributes.{$actorsId}_{$attributeId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.attributes.{$actorsId}_{$attributeId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertNotEmpty($webhook['data']['key']);
@ -131,6 +141,7 @@ trait WebhooksBase
$this->assertEquals(204, $removed['headers']['status-code']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
// $this->assertEquals($webhook['method'], 'DELETE');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -145,7 +156,7 @@ trait WebhooksBase
$this->assertStringContainsString("collections.{$actorsId}.attributes.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.attributes.{$actorsId}_{$attributeId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.attributes.{$actorsId}_{$attributeId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertNotEmpty($webhook['data']['key']);
@ -183,6 +194,7 @@ trait WebhooksBase
$this->assertNotEmpty($document['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -197,7 +209,7 @@ trait WebhooksBase
$this->assertStringContainsString("collections.{$actorsId}.documents.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.documents.{$documentId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -242,6 +254,7 @@ trait WebhooksBase
$this->assertNotEmpty($document['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -256,7 +269,7 @@ trait WebhooksBase
$this->assertStringContainsString("collections.{$actorsId}.documents.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.documents.{$documentId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -308,6 +321,7 @@ trait WebhooksBase
$this->assertEquals($document['headers']['status-code'], 204);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -322,7 +336,7 @@ trait WebhooksBase
$this->assertStringContainsString("collections.{$actorsId}.documents.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.documents.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.documents.{$documentId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -361,6 +375,7 @@ trait WebhooksBase
$this->assertNotEmpty($bucket['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -369,7 +384,7 @@ trait WebhooksBase
$this->assertStringContainsString('buckets.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
@ -406,6 +421,7 @@ trait WebhooksBase
$this->assertNotEmpty($bucket['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -414,7 +430,7 @@ trait WebhooksBase
$this->assertStringContainsString('buckets.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
@ -466,6 +482,7 @@ trait WebhooksBase
$this->assertNotEmpty($file['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -480,7 +497,7 @@ trait WebhooksBase
$this->assertStringContainsString("buckets.{$bucketId}.files.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -521,6 +538,7 @@ trait WebhooksBase
$this->assertNotEmpty($file['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -535,7 +553,7 @@ trait WebhooksBase
$this->assertStringContainsString("buckets.{$bucketId}.files.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -571,6 +589,7 @@ trait WebhooksBase
$this->assertEmpty($file['body']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -585,7 +604,7 @@ trait WebhooksBase
$this->assertStringContainsString("buckets.{$bucketId}.files.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}.files.{$fileId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -620,6 +639,7 @@ trait WebhooksBase
$this->assertEmpty($bucket['body']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -628,7 +648,7 @@ trait WebhooksBase
$this->assertStringContainsString('buckets.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("buckets.{$bucketId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
@ -658,6 +678,7 @@ trait WebhooksBase
$this->assertNotEmpty($team['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -666,7 +687,7 @@ trait WebhooksBase
$this->assertStringContainsString('teams.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -702,6 +723,7 @@ trait WebhooksBase
$this->assertNotEmpty($team['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -710,7 +732,7 @@ trait WebhooksBase
$this->assertStringContainsString('teams.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -750,6 +772,7 @@ trait WebhooksBase
], $this->getHeaders()));
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -758,7 +781,7 @@ trait WebhooksBase
$this->assertStringContainsString('teams.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -804,6 +827,7 @@ trait WebhooksBase
$membershipId = $team['body']['$id'];
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -818,7 +842,7 @@ trait WebhooksBase
$this->assertStringContainsString("teams.{$teamId}.memberships.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -875,6 +899,7 @@ trait WebhooksBase
$this->assertEquals(204, $team['headers']['status-code']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -889,7 +914,7 @@ trait WebhooksBase
$this->assertStringContainsString("teams.{$teamId}.memberships.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamId}.memberships.{$membershipId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));

View file

@ -39,6 +39,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertNotEmpty($account['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -47,7 +51,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString('users.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id']), true);
@ -111,6 +115,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertEquals($account['headers']['status-code'], 200);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -119,7 +127,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString('users.*.update.status', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update.status", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -161,6 +169,10 @@ class WebhooksCustomClientTest extends Scope
$session = $this->client->parseCookie((string)$accountSession['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -175,7 +187,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("users.{$id}.sessions.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.sessions.{$sessionId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.sessions.{$sessionId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id']), true);
@ -246,6 +258,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertEquals($accountSession['headers']['status-code'], 204);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -260,7 +276,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("users.{$id}.sessions.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.sessions.{$sessionId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.sessions.{$sessionId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -328,6 +344,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertEquals($accountSession['headers']['status-code'], 204);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -342,7 +362,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("users.{$id}.sessions.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.sessions.{$sessionId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.sessions.{$sessionId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -414,6 +434,11 @@ class WebhooksCustomClientTest extends Scope
$this->assertIsArray($account['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -424,7 +449,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update.name", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -463,6 +488,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertIsArray($account['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -473,7 +502,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update.password", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -514,6 +543,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertIsArray($account['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -524,7 +557,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update.email", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -566,6 +599,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertIsArray($account['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -576,7 +613,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update.prefs", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -617,6 +654,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertIsArray($recovery['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -631,7 +672,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("users.{$id}.recovery.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.recovery.{$recoveryId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.recovery.{$recoveryId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-User-Id'], $id);
@ -673,6 +714,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertIsArray($recovery['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -687,7 +732,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("users.{$id}.recovery.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.recovery.{$recoveryId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.recovery.{$recoveryId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id']), true);
@ -725,6 +770,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertIsArray($verification['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -739,7 +788,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("users.{$id}.verification.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -779,6 +828,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertIsArray($verification['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -793,7 +846,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("users.{$id}.verification.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -833,6 +886,10 @@ class WebhooksCustomClientTest extends Scope
$this->assertNotEmpty($team['body']['$id']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -851,7 +908,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertStringContainsString("teams.{$teamUid}.memberships.{$membershipUid}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamUid}.memberships.{$membershipUid}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("teams.{$teamUid}.memberships.{$membershipUid}.update.status", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);

View file

@ -38,6 +38,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertNotEmpty($actors['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -46,7 +47,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString('collections.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
@ -86,6 +87,7 @@ class WebhooksCustomServerTest extends Scope
sleep(5);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -100,7 +102,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString("collections.{$actorsId}.indexes.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.indexes.{$actorsId}_{$indexKey}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.indexes.{$actorsId}_{$indexKey}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
@ -114,6 +116,7 @@ class WebhooksCustomServerTest extends Scope
// // wait for database worker to remove index
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
// $this->assertEquals($webhook['method'], 'DELETE');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -128,7 +131,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString("collections.{$actorsId}.indexes.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.indexes.{$actorsId}_{$indexKey}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$actorsId}.indexes.{$actorsId}_{$indexKey}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
@ -167,6 +170,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals($actors['headers']['status-code'], 204);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -175,7 +179,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString('collections.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("collections.{$id}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
@ -214,6 +218,7 @@ class WebhooksCustomServerTest extends Scope
$id = $user['body']['$id'];
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -222,7 +227,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString('users.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -261,6 +266,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals($user['body']['a'], 'b');
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -271,7 +277,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update.prefs", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -301,6 +307,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertNotEmpty($user['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -311,7 +318,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.update.status", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -344,6 +351,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals($user['headers']['status-code'], 204);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -352,7 +360,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString('users.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
@ -389,6 +397,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertNotEmpty($function['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -397,7 +406,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString('functions.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
@ -437,6 +446,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals($function['body']['vars'], ['key1' => 'value1']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -445,7 +455,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString('functions.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
@ -481,6 +491,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertNotEmpty($deployment['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -491,7 +502,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.deployments.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
@ -523,6 +534,7 @@ class WebhooksCustomServerTest extends Scope
sleep(5);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -537,7 +549,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString("functions.{$id}.deployments.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
@ -569,7 +581,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertNotEmpty($execution['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
@ -583,7 +595,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString("functions.{$id}.executions.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.executions.{$executionId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.executions.{$executionId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
@ -591,6 +603,7 @@ class WebhooksCustomServerTest extends Scope
sleep(10);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -605,7 +618,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString("functions.{$id}.executions.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.executions.{$executionId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.executions.{$executionId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
@ -635,6 +648,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertEmpty($deployment['body']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -649,7 +663,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString("functions.{$id}.deployments.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.deployments.{$deploymentId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
@ -679,6 +693,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertEmpty($function['body']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
@ -687,7 +702,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertStringContainsString('functions.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("functions.{$id}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);

View file

@ -0,0 +1,89 @@
<?php
namespace Appwrite\Tests;
use Appwrite\DSN\DSN;
use PHPUnit\Framework\TestCase;
class DSNTest extends TestCase
{
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function testSuccess(): void
{
$dsn = new DSN("mariadb://user:password@localhost:3306/database?charset=utf8&timezone=UTC");
$this->assertEquals("mariadb", $dsn->getScheme());
$this->assertEquals("user", $dsn->getUser());
$this->assertEquals("password", $dsn->getPassword());
$this->assertEquals("localhost", $dsn->getHost());
$this->assertEquals("3306", $dsn->getPort());
$this->assertEquals("database", $dsn->getDatabase());
$this->assertEquals("charset=utf8&timezone=UTC", $dsn->getQuery());
$dsn = new DSN("mariadb://user@localhost:3306/database?charset=utf8&timezone=UTC");
$this->assertEquals("mariadb", $dsn->getScheme());
$this->assertEquals("user", $dsn->getUser());
$this->assertNull($dsn->getPassword());
$this->assertEquals("localhost", $dsn->getHost());
$this->assertEquals("3306", $dsn->getPort());
$this->assertEquals("database", $dsn->getDatabase());
$this->assertEquals("charset=utf8&timezone=UTC", $dsn->getQuery());
$dsn = new DSN("mariadb://user@localhost/database?charset=utf8&timezone=UTC");
$this->assertEquals("mariadb", $dsn->getScheme());
$this->assertEquals("user", $dsn->getUser());
$this->assertNull($dsn->getPassword());
$this->assertEquals("localhost", $dsn->getHost());
$this->assertNull($dsn->getPort());
$this->assertEquals("database", $dsn->getDatabase());
$this->assertEquals("charset=utf8&timezone=UTC", $dsn->getQuery());
$dsn = new DSN("mariadb://user@localhost?charset=utf8&timezone=UTC");
$this->assertEquals("mariadb", $dsn->getScheme());
$this->assertEquals("user", $dsn->getUser());
$this->assertNull($dsn->getPassword());
$this->assertEquals("localhost", $dsn->getHost());
$this->assertNull($dsn->getPort());
$this->assertEmpty($dsn->getDatabase());
$this->assertEquals("charset=utf8&timezone=UTC", $dsn->getQuery());
$dsn = new DSN("mariadb://user@localhost");
$this->assertEquals("mariadb", $dsn->getScheme());
$this->assertEquals("user", $dsn->getUser());
$this->assertNull($dsn->getPassword());
$this->assertEquals("localhost", $dsn->getHost());
$this->assertNull($dsn->getPort());
$this->assertEmpty($dsn->getDatabase());
$this->assertNull($dsn->getQuery());
$dsn = new DSN("mariadb://user:@localhost");
$this->assertEquals("mariadb", $dsn->getScheme());
$this->assertEquals("user", $dsn->getUser());
$this->assertEmpty($dsn->getPassword());
$this->assertEquals("localhost", $dsn->getHost());
$this->assertNull($dsn->getPort());
$this->assertEmpty($dsn->getDatabase());
$this->assertNull($dsn->getQuery());
$dsn = new DSN("mariadb://localhost");
$this->assertEquals("mariadb", $dsn->getScheme());
$this->assertNull($dsn->getUser());
$this->assertNull($dsn->getPassword());
$this->assertEquals("localhost", $dsn->getHost());
$this->assertNull($dsn->getPort());
$this->assertEmpty($dsn->getDatabase());
$this->assertNull($dsn->getQuery());
}
public function testFail(): void
{
$this->expectException(\InvalidArgumentException::class);
$dsn = new DSN("mariadb://");
}
}