',
+ '#type' => 'html_tag',
+ '#tag' => 'code',
+ '#attributes' => [
+ 'class' => ['language-json'],
+ ],
+ '#value' => t('If this is blank, your library will need to implement code in your JavaScript. Please see the README.md.'),
+ '#suffix' => '',
+ ],
+ ];
+ }
+}
+
+/**
+ * Implements hook_views_pre_view().
+ */
+function charts_views_pre_view(ViewExecutable $view, $display_id, array &$args) {
+ $hasFields = array_key_exists('fields', $view->display_handler->options);
+ if ($hasFields) {
+ $fields = $view->display_handler->getOption('fields');
+ $hasViewsFieldsOnOffHandler = FALSE;
+ foreach ($fields as $field) {
+ if (array_key_exists('plugin_id', $field)) {
+
+ if ($field['plugin_id'] == 'field_exposed_chart_type') {
+ $hasViewsFieldsOnOffHandler = TRUE;
+ }
+ }
+ }
+ if ($hasViewsFieldsOnOffHandler) {
+ // Grab the type that has been submitted.
+ $params = \Drupal::request()->query->all();
+ // This is for a GET request.
+ // If the view is submitted through AJAX, like in view preview, it will be
+ // a POST request. Merge the parameter arrays and we will get our values.
+ $postParams = \Drupal::request()->request->all();
+ $params = array_merge($params, $postParams);
+ foreach ($params as $key => $value) {
+ if (strpos($key, 'ct') === 0) {
+ $view->storage->set('exposed_chart_type', $value);
+ }
+ }
+ $view->element['#cache']['contexts'][] = 'url';
+ }
+ }
+}
+
+/**
+ * Implements hook_library_info_alter().
+ *
+ * Adapted from webform_library_info_alter() to add CDN support.
+ */
+function charts_library_info_alter(&$libraries, $extension) {
+ // Only process for charts_* modules.
+ if (strpos($extension, 'charts_') !== 0) {
+ return;
+ }
+
+ // Only try to apply cdn changes if the cdn requirements is on.
+ $config = \Drupal::config('charts.settings');
+ if (!$config->get('advanced.requirements.cdn')) {
+ return;
+ }
+
+ foreach ($libraries as &$library) {
+ if (isset($library['cdn']) && is_array($library['cdn'])) {
+ _charts_library_info_alter_recursive($library, $library['cdn']);
+ }
+ }
+}
+
+/**
+ * Recursive through a charts library.
+ *
+ * @param array $library
+ * A library defined in a chart module.
+ * @param array $cdn
+ * A associative array of library paths mapped to CDN URL.
+ *
+ * @note copied from _webform_library_info_alter_recursive().
+ */
+function _charts_library_info_alter_recursive(array &$library, array $cdn) {
+ foreach ($library as $key => &$value) {
+ // CSS and JS files and listed in associative arrays keyed via string.
+ if (!is_string($key) || !is_array($value)) {
+ continue;
+ }
+
+ // Ignore the CDN's associative array.
+ if ($key === 'cdn') {
+ continue;
+ }
+
+ // Replace the CDN sources (i.e. /library/*) with the CDN URL destination
+ // (https://cdnjs.cloudflare.com/ajax/libs/*).
+ foreach ($cdn as $source => $destination) {
+ if (_charts_library_check_source_exists($source)) {
+ continue;
+ }
+ if (strpos($key, $source) === 0) {
+ $uri = str_replace($source, $destination, $key);
+ $library[$uri] = $value;
+ $library[$uri]['type'] = 'external';
+ unset($library[$key]);
+ break;
+ }
+ }
+
+ // Recurse downward to find nested libraries.
+ _charts_library_info_alter_recursive($value, $cdn);
+ }
+}
+
+/**
+ * Checks whether a library source directory or file exists locally.
+ *
+ * @param string $source
+ * The source directory or file.
+ *
+ * @return bool
+ * True when the file exist, FALSE otherwise.
+ */
+function _charts_library_check_source_exists(string $source) {
+ $source = rtrim($source, '/');
+
+ $container = \Drupal::getContainer();
+ $app_root = $container->getParameter('app.root');
+ $site_path = $container->getParameter('site.path');
+
+ // The following logic is taken from libraries_get_libraries()
+ $search_dir = [];
+ $search_dir[] = $app_root;
+ // Similar to 'modules' and 'themes' directories inside an installation
+ // profile, installation profiles may want to place libraries into a
+ // 'libraries' directory.
+ $search_dir[] = 'profiles/' . \Drupal::installProfile();
+ // Also search sites/' . t('A simple example on how to interact with the Charts API') . '
'; + break; + } + return $output; +} + +/** + * Implements hook_chart_definition_CHART_ID_alter(). + */ +function charts_api_example_chart_definition_exampleidphp_alter(&$chart) { + // Please note: this will only show when the charts_highcharts module is + // enabled and Highcharts is set as your default library. + $chart['chart']['backgroundColor'] = 'blue'; +} + +/** + * Implements hook_chart_alter(). + */ +function charts_api_example_chart_alter(array &$element, $chart_id) { + // Please note: this will only show when the charts_highcharts module is + // enabled and Highcharts is set as your default library. + if ($chart_id === 'exampleidjschart') { + $element['#attached']['library'][] = 'charts_api_example/charts_api_example'; + } +} diff --git a/web/modules/charts/modules/charts_api_example/charts_api_example.routing.yml b/web/modules/charts/modules/charts_api_example/charts_api_example.routing.yml new file mode 100755 index 00000000..5d0c2c5d --- /dev/null +++ b/web/modules/charts/modules/charts_api_example/charts_api_example.routing.yml @@ -0,0 +1,7 @@ +charts_api_example.display: + path: /charts/example/display + defaults: + _controller: '\Drupal\charts_api_example\Controller\ChartsApiExample::display' + _title: 'How to Use the Charts API' + requirements: + _permission: 'access content' diff --git a/web/modules/charts/modules/charts_api_example/composer.json b/web/modules/charts/modules/charts_api_example/composer.json new file mode 100755 index 00000000..4fda5297 --- /dev/null +++ b/web/modules/charts/modules/charts_api_example/composer.json @@ -0,0 +1,16 @@ +{ + "name": "drupal/charts_api_example", + "type": "drupal-module", + "description": "How to interact with the Charts Api", + "keywords": [ + "Drupal" + ], + "license": "GPL-2.0+", + "homepage": "https://www.drupal.org/project/charts_api_example", + "minimum-stability": "dev", + "support": { + "issues": "https://www.drupal.org/project/issues/charts_api_example", + "source": "http://cgit.drupalcode.org/charts_api_example" + }, + "require": {} +} diff --git a/web/modules/charts/modules/charts_api_example/fixtures/charts_api_example_file.csv b/web/modules/charts/modules/charts_api_example/fixtures/charts_api_example_file.csv new file mode 100644 index 00000000..177f6506 --- /dev/null +++ b/web/modules/charts/modules/charts_api_example/fixtures/charts_api_example_file.csv @@ -0,0 +1,463 @@ +Week,6.x-0.x,6.x-1.x,7.x-1.x,7.x-2.x,8.x-1.x,8.x-3.x,8.x-4.x,5.0.x,Total +21-Aug-21,0,71,12,13458,1,4632,6,637,18817 +14-Aug-21,0,77,13,13517,1,4518,9,608,18743 +7-Aug-21,0,73,11,13586,1,4651,6,606,18934 +31-Jul-21,0,76,13,13479,1,4488,7,619,18683 +24-Jul-21,0,70,10,13771,1,4385,7,530,18774 +17-Jul-21,0,73,12,13756,1,4430,8,536,18816 +10-Jul-21,0,77,13,13596,1,4362,8,519,18576 +3-Jul-21,0,75,11,13715,1,4437,8,596,18843 +26-Jun-21,0,73,12,13925,1,4470,9,651,19141 +19-Jun-21,0,80,13,13886,1,4583,7,499,19069 +12-Jun-21,0,79,13,13651,1,4506,9,455,18714 +5-Jun-21,0,84,12,13656,1,4525,8,457,18743 +29-May-21,0,79,15,13935,1,4554,10,438,19032 +22-May-21,0,84,14,13840,1,4511,8,500,18958 +15-May-21,0,80,13,13790,2,4524,11,553,18973 +8-May-21,0,85,13,13748,1,4448,9,357,18661 +1-May-21,0,83,12,13853,0,4371,8,379,18706 +24-Apr-21,0,82,14,13835,1,4431,6,340,18709 +17-Apr-21,0,86,14,14080,0,4455,10,322,18967 +10-Apr-21,0,82,12,13736,0,4326,8,280,18444 +3-Apr-21,0,82,14,13815,0,4347,8,289,18555 +27-Mar-21,0,78,12,13490,1,4212,7,325,18125 +20-Mar-21,0,77,12,13667,0,4389,9,366,18520 +13-Mar-21,0,77,12,13671,0,4293,9,269,18331 +6-Mar-21,0,75,14,13840,0,4361,8,253,18551 +27-Feb-21,0,81,14,14144,1,4413,9,235,18897 +20-Feb-21,0,83,14,14151,0,4451,10,217,18926 +13-Feb-21,0,80,13,14057,0,4294,10,247,18701 +6-Feb-21,0,86,16,14042,0,4271,10,233,18658 +30-Jan-21,0,78,13,14221,0,4330,7,257,18906 +23-Jan-21,0,85,13,14109,0,4283,9,192,18691 +16-Jan-21,0,94,16,14294,0,4227,9,162,18802 +9-Jan-21,0,88,15,14244,0,4133,9,154,18643 +2-Jan-21,0,81,14,14235,0,4123,15,145,18613 +26-Dec-20,0,82,15,13746,0,3721,9,130,17703 +19-Dec-20,0,83,15,13904,0,3704,9,128,17843 +12-Dec-20,0,86,16,14299,0,4007,16,170,18594 +5-Dec-20,0,85,15,14714,0,4047,12,152,19025 +28-Nov-20,0,81,14,14852,0,3970,16,148,19081 +21-Nov-20,0,78,16,14817,0,4347,17,108,19383 +14-Nov-20,0,86,17,14840,0,4567,22,62,19594 +7-Nov-20,0,87,18,14286,0,4110,18,80,18599 +31-Oct-20,0,86,19,14599,0,4100,25,49,18878 +24-Oct-20,0,91,16,14571,0,4157,25,3,18863 +17-Oct-20,0,84,16,14434,0,5116,25,0,19675 +10-Oct-20,0,87,15,14563,0,5102,33,0,19800 +3-Oct-20,0,83,18,14593,0,4059,26,0,18779 +26-Sep-20,0,88,17,14717,0,3971,23,0,18816 +19-Sep-20,0,91,20,14605,0,4064,22,0,18802 +12-Sep-20,0,87,19,14706,0,4865,24,0,19701 +5-Sep-20,0,88,20,14661,0,4641,29,0,19439 +29-Aug-20,0,91,19,14814,0,4557,28,0,19509 +22-Aug-20,0,92,17,14570,0,4572,30,0,19281 +15-Aug-20,0,84,20,14743,0,4615,32,0,19494 +8-Aug-20,0,92,20,14800,0,4627,28,0,19567 +1-Aug-20,0,90,19,14925,0,4433,20,0,19487 +25-Jul-20,0,92,19,15013,0,4533,19,0,19676 +18-Jul-20,0,89,20,14750,0,4532,22,0,19413 +11-Jul-20,0,93,15,14901,0,4594,20,0,19623 +4-Jul-20,0,88,21,15080,0,4467,26,0,19682 +27-Jun-20,0,93,19,15281,0,4679,26,0,20098 +20-Jun-20,0,86,21,15642,0,4835,25,0,20609 +13-Jun-20,0,91,25,15979,0,4613,23,0,20731 +6-Jun-20,0,92,23,15597,0,4564,20,0,20296 +30-May-20,0,86,25,15812,0,4433,17,0,20373 +23-May-20,0,94,27,15830,0,4163,15,0,20129 +16-May-20,0,98,29,15739,0,4169,14,0,20049 +9-May-20,0,98,23,15300,0,4336,12,0,19769 +2-May-20,0,94,27,15370,0,4261,12,0,19764 +25-Apr-20,0,99,28,14711,0,4043,9,0,18890 +18-Apr-20,0,102,25,14857,1,3970,4,0,18959 +11-Apr-20,0,107,27,14884,0,3966,4,0,18988 +4-Apr-20,0,104,25,14829,0,3928,3,0,18889 +28-Mar-20,0,101,25,15302,0,3937,4,0,19369 +21-Mar-20,0,103,27,15217,1,4067,3,0,19418 +14-Mar-20,0,107,26,15136,0,3813,1,0,19083 +7-Mar-20,0,106,26,15202,0,3797,0,0,19131 +29-Feb-20,0,106,29,15269,1,3795,0,0,19200 +22-Feb-20,0,106,30,15222,0,3726,0,0,19084 +15-Feb-20,0,111,29,15289,0,3596,1,0,19026 +8-Feb-20,0,108,28,15328,0,3667,0,0,19131 +1-Feb-20,0,110,28,15523,1,3673,0,0,19335 +25-Jan-20,0,114,25,15279,0,3523,0,0,18941 +18-Jan-20,0,108,29,15289,0,3549,0,0,18975 +11-Jan-20,0,115,29,15540,1,3561,0,0,19246 +4-Jan-20,0,123,26,15612,0,3492,0,0,19253 +28-Dec-19,0,117,30,15151,0,3065,0,0,18363 +21-Dec-19,0,122,26,15067,0,3179,0,0,18394 +14-Dec-19,0,125,27,16078,1,3614,0,0,19845 +7-Dec-19,0,124,25,16029,0,3634,0,0,19812 +30-Nov-19,0,127,30,15804,0,3591,1,0,19553 +23-Nov-19,0,119,28,15640,0,3512,2,0,19301 +16-Nov-19,0,119,31,15755,1,3659,1,0,19566 +9-Nov-19,0,129,29,15583,0,3449,3,0,19193 +2-Nov-19,0,124,32,15773,0,3488,3,0,19420 +26-Oct-19,0,138,29,15742,0,3345,0,0,19254 +19-Oct-19,0,129,27,15927,1,3347,0,0,19431 +12-Oct-19,0,127,31,15761,0,3414,1,0,19334 +5-Oct-19,0,143,38,15753,0,3405,1,0,19340 +28-Sep-19,0,139,30,15852,1,3428,1,0,19451 +21-Sep-19,0,129,29,15753,0,3194,0,0,19105 +14-Sep-19,0,131,30,15994,0,3107,0,0,19262 +7-Sep-19,0,134,30,15560,0,3222,0,0,18946 +31-Aug-19,0,133,35,15642,1,3279,0,0,19090 +24-Aug-19,0,131,33,15477,0,3163,0,0,18804 +17-Aug-19,0,145,34,15513,0,3095,0,0,18787 +10-Aug-19,0,144,29,15331,1,2854,0,0,18359 +3-Aug-19,0,150,30,15495,0,2924,0,0,18599 +27-Jul-19,0,135,32,15766,0,2942,0,0,18875 +20-Jul-19,0,145,35,15715,1,2864,0,0,18760 +13-Jul-19,0,141,29,15700,0,2975,0,0,18845 +6-Jul-19,0,140,32,15572,0,3223,0,0,18967 +29-Jun-19,0,153,32,15719,1,3087,0,0,18992 +22-Jun-19,0,151,33,15604,1,3045,0,0,18834 +15-Jun-19,0,147,35,15576,0,3226,0,0,18984 +8-Jun-19,0,150,29,15515,0,2942,0,0,18636 +1-Jun-19,0,140,37,15912,0,2724,0,0,18813 +25-May-19,0,145,32,15847,1,2803,0,0,18828 +18-May-19,0,142,31,15985,0,3094,0,0,19252 +11-May-19,0,142,31,16276,2,3083,0,0,19534 +4-May-19,0,131,30,16204,8,3001,0,0,19374 +27-Apr-19,0,126,35,15858,7,2788,0,0,18814 +20-Apr-19,0,134,30,15809,4,2635,0,0,18612 +13-Apr-19,0,136,32,15934,8,2545,0,0,18655 +6-Apr-19,0,136,36,15967,7,2652,0,0,18798 +30-Mar-19,0,135,37,16346,6,2608,0,0,19132 +23-Mar-19,0,135,32,16429,6,2537,0,0,19139 +16-Mar-19,0,131,32,16638,4,2370,0,0,19175 +9-Mar-19,0,138,32,16351,5,2300,0,0,18826 +2-Mar-19,0,141,34,16383,6,2436,0,0,19000 +23-Feb-19,0,145,31,16483,7,2458,0,0,19124 +16-Feb-19,0,143,32,16565,6,2689,0,0,19435 +9-Feb-19,0,143,37,16169,5,2551,0,0,18905 +2-Feb-19,0,142,32,16180,4,2437,0,0,18795 +26-Jan-19,0,136,34,16314,5,2474,0,0,18963 +19-Jan-19,0,138,30,16504,6,2276,0,0,18954 +12-Jan-19,0,140,28,16585,7,2294,0,0,19054 +5-Jan-19,0,140,28,16175,7,2122,0,0,18472 +29-Dec-18,0,145,31,15571,7,1974,0,0,17728 +22-Dec-18,0,144,30,15031,4,2009,0,0,17218 +15-Dec-18,0,145,30,15884,5,2077,0,0,18141 +8-Dec-18,0,140,35,16095,5,2193,0,0,18468 +1-Dec-18,0,148,31,16110,3,2268,0,0,18560 +24-Nov-18,0,145,27,15817,3,2096,0,0,18088 +17-Nov-18,0,153,25,15964,6,2104,0,0,18252 +10-Nov-18,0,153,33,15995,7,2074,0,0,18262 +3-Nov-18,0,158,34,15944,5,2319,0,0,18460 +27-Oct-18,0,153,34,15920,4,2400,0,0,18511 +20-Oct-18,0,150,31,16208,5,2237,0,0,18631 +13-Oct-18,0,153,30,16470,4,2200,0,0,18857 +6-Oct-18,0,153,32,15848,4,1926,0,0,17963 +29-Sep-18,0,155,36,15769,5,1929,0,0,17894 +22-Sep-18,0,157,29,15572,4,1852,0,0,17614 +15-Sep-18,0,158,29,15334,5,1998,0,0,17524 +8-Sep-18,0,161,28,15815,4,1974,0,0,17982 +1-Sep-18,0,162,30,15825,4,2001,0,0,18022 +25-Aug-18,0,156,29,15769,4,1929,0,0,17887 +18-Aug-18,0,152,29,15565,4,1775,0,0,17525 +11-Aug-18,0,150,30,15460,4,1860,0,0,17504 +4-Aug-18,0,157,36,15504,5,1979,0,0,17681 +28-Jul-18,0,157,32,15243,2,1898,0,0,17332 +21-Jul-18,0,161,31,15322,0,1850,0,0,17364 +14-Jul-18,0,138,34,14741,0,1725,0,0,16638 +7-Jul-18,0,158,35,15621,0,1700,0,0,17514 +30-Jun-18,0,145,31,13791,0,1442,0,0,15409 +23-Jun-18,0,165,36,15441,0,1594,0,0,17236 +16-Jun-18,0,162,35,15550,0,1633,0,0,17380 +9-Jun-18,0,157,39,15502,0,1650,0,0,17348 +2-Jun-18,0,167,34,15757,3,1795,0,0,17756 +26-May-18,0,176,33,15551,0,1487,0,0,17247 +19-May-18,0,173,37,15841,0,1554,0,0,17605 +12-May-18,0,168,30,16140,0,1400,0,0,17738 +5-May-18,0,174,34,16049,0,1460,0,0,17717 +28-Apr-18,0,216,35,16600,0,1485,0,0,18336 +21-Apr-18,0,181,33,18067,0,1517,0,0,19798 +14-Apr-18,0,181,32,16873,2,1712,0,0,18800 +7-Apr-18,0,177,38,16800,4,1558,0,0,18577 +31-Mar-18,0,184,32,16736,2,1457,0,0,18411 +24-Mar-18,0,195,31,17965,3,1458,0,0,19652 +17-Mar-18,0,184,35,16418,2,1380,0,0,18019 +10-Mar-18,0,184,32,16795,2,1419,0,0,18432 +3-Mar-18,0,193,35,16780,2,1520,0,0,18530 +24-Feb-18,0,190,31,16427,2,1338,0,0,17988 +17-Feb-18,0,204,34,16367,2,1274,0,0,17881 +10-Feb-18,0,196,36,16275,1,1292,0,0,17800 +3-Feb-18,0,206,34,16345,3,1527,0,0,18115 +27-Jan-18,0,196,33,16268,5,1385,0,0,17887 +20-Jan-18,0,202,36,15935,2,1450,0,0,17625 +13-Jan-18,0,200,33,15856,7,1320,0,0,17416 +6-Jan-18,0,209,38,16034,3,1285,0,0,17569 +30-Dec-17,0,204,39,15544,2,1056,0,0,16845 +23-Dec-17,0,206,35,15002,2,1039,0,0,16284 +16-Dec-17,0,203,35,15809,3,1123,0,0,17173 +9-Dec-17,0,206,32,15921,2,1477,0,0,17638 +2-Dec-17,0,204,38,16419,2,1339,0,0,18002 +25-Nov-17,0,210,33,16087,2,1193,0,0,17525 +18-Nov-17,0,205,39,16106,3,1096,0,0,17449 +11-Nov-17,0,210,39,16304,2,1081,0,0,17636 +4-Nov-17,0,227,40,16241,3,1163,0,0,17674 +28-Oct-17,0,217,39,15860,4,909,0,0,17029 +21-Oct-17,0,226,39,15948,4,863,0,0,17080 +14-Oct-17,0,227,43,16086,3,922,0,0,17281 +7-Oct-17,0,235,45,16215,2,932,0,0,17429 +30-Sep-17,0,237,38,16037,2,800,0,0,17114 +23-Sep-17,0,243,42,15838,4,715,0,0,16842 +16-Sep-17,0,239,39,15716,4,962,0,0,16960 +9-Sep-17,0,242,44,15784,2,659,0,0,16731 +2-Sep-17,0,254,46,15812,2,760,0,0,16874 +26-Aug-17,0,254,43,15805,2,681,0,0,16785 +19-Aug-17,0,265,45,15748,3,533,0,0,16594 +12-Aug-17,0,242,38,14836,2,474,0,0,15592 +5-Aug-17,0,264,39,15242,3,572,0,0,16120 +29-Jul-17,0,263,41,15266,1,288,0,0,15859 +22-Jul-17,0,259,44,15357,2,134,0,0,15796 +15-Jul-17,0,269,38,15512,2,132,0,0,15953 +8-Jul-17,0,261,47,15717,3,124,0,0,16152 +1-Jul-17,0,262,43,15617,3,114,0,0,16039 +24-Jun-17,0,261,44,15799,3,113,0,0,16220 +17-Jun-17,0,270,42,15745,3,107,0,0,16167 +10-Jun-17,0,279,42,15587,4,112,0,0,16024 +3-Jun-17,0,278,49,15721,4,105,0,0,16157 +27-May-17,0,289,50,15553,4,100,0,0,15996 +20-May-17,0,286,45,15504,2,92,0,0,15929 +13-May-17,0,290,46,16796,2,93,0,0,17227 +6-May-17,0,286,45,17445,4,74,0,0,17854 +29-Apr-17,0,297,46,17294,3,62,0,0,17702 +22-Apr-17,0,296,46,17500,3,63,0,0,17908 +15-Apr-17,0,290,48,17485,3,47,0,0,17873 +8-Apr-17,0,293,46,17367,3,36,0,0,17745 +1-Apr-17,0,298,43,17508,7,31,0,0,17887 +25-Mar-17,0,300,47,17271,10,27,0,0,17655 +18-Mar-17,0,304,47,17267,14,10,0,0,17642 +11-Mar-17,0,296,43,16712,19,0,0,0,17070 +4-Mar-17,0,303,46,16999,3,0,0,0,17351 +25-Feb-17,0,307,48,16959,0,0,0,0,17314 +18-Feb-17,0,320,45,16806,0,0,0,0,17171 +11-Feb-17,0,305,40,16866,0,0,0,0,17211 +4-Feb-17,0,294,42,16988,0,0,0,0,17324 +28-Jan-17,0,312,43,16604,0,0,0,0,16959 +21-Jan-17,0,291,48,16706,0,0,0,0,17045 +14-Jan-17,0,310,46,16459,0,0,0,0,16815 +7-Jan-17,0,318,51,16878,0,0,0,0,17247 +31-Dec-16,0,318,45,15947,0,0,0,0,16310 +24-Dec-16,0,311,45,15031,0,0,0,0,15387 +17-Dec-16,0,319,46,16005,0,0,0,0,16370 +10-Dec-16,0,318,42,16210,0,0,0,0,16570 +3-Dec-16,0,332,52,16549,0,0,0,0,16933 +26-Nov-16,0,334,46,16549,0,0,0,0,16929 +19-Nov-16,0,332,51,15957,0,0,0,0,16340 +12-Nov-16,0,342,49,16191,0,0,0,0,16582 +5-Nov-16,0,345,56,15339,0,0,0,0,15740 +29-Oct-16,0,349,49,14911,0,0,0,0,15309 +22-Oct-16,0,350,53,15040,0,0,0,0,15443 +15-Oct-16,0,348,53,15238,0,0,0,0,15639 +8-Oct-16,0,350,52,14567,0,0,0,0,14969 +1-Oct-16,0,352,63,15041,0,0,0,0,15456 +24-Sep-16,0,351,60,14551,0,0,0,0,14962 +17-Sep-16,0,347,58,14655,0,0,0,0,15060 +10-Sep-16,0,338,62,14584,0,0,0,0,14984 +3-Sep-16,0,355,62,14582,0,0,0,0,14999 +27-Aug-16,0,352,67,14673,0,0,0,0,15092 +20-Aug-16,0,359,63,14400,0,0,0,0,14822 +13-Aug-16,0,392,61,14415,0,0,0,0,14868 +6-Aug-16,0,373,62,14653,0,0,0,0,15088 +30-Jul-16,0,367,60,14607,0,0,0,0,15034 +23-Jul-16,0,374,58,14741,0,0,0,0,15173 +16-Jul-16,0,415,57,14959,0,0,0,0,15431 +9-Jul-16,0,439,58,13846,0,0,0,0,14343 +2-Jul-16,0,378,59,14610,0,0,0,0,15047 +25-Jun-16,0,392,62,14607,0,0,0,0,15061 +18-Jun-16,0,380,65,14953,0,0,0,0,15398 +11-Jun-16,0,398,58,14869,0,0,0,0,15325 +4-Jun-16,0,379,63,14444,0,0,0,0,14886 +28-May-16,0,406,68,14443,0,0,0,0,14917 +21-May-16,0,396,68,14739,0,0,0,0,15203 +14-May-16,0,391,58,14553,0,0,0,0,15002 +7-May-16,0,393,72,14577,0,0,0,0,15042 +30-Apr-16,0,391,67,14347,0,0,0,0,14805 +23-Apr-16,0,412,65,14447,0,0,0,0,14924 +16-Apr-16,0,392,65,14611,0,0,0,0,15068 +9-Apr-16,0,414,68,14426,0,0,0,0,14908 +2-Apr-16,0,410,67,14537,0,0,0,0,15014 +26-Mar-16,0,407,74,14040,0,0,0,0,14521 +19-Mar-16,0,418,73,13893,0,0,0,0,14384 +12-Mar-16,0,419,76,14256,0,0,0,0,14751 +5-Mar-16,0,440,73,14272,0,0,0,0,14785 +27-Feb-16,0,437,78,14293,0,0,0,0,14808 +20-Feb-16,0,448,78,14500,0,0,0,0,15026 +13-Feb-16,0,458,74,13949,0,0,0,0,14481 +6-Feb-16,0,437,76,13681,0,0,0,0,14194 +30-Jan-16,0,428,76,13729,0,0,0,0,14233 +23-Jan-16,0,435,80,13393,0,0,0,0,13908 +16-Jan-16,0,441,79,13369,0,0,0,0,13889 +9-Jan-16,0,443,77,13298,0,0,0,0,13818 +2-Jan-16,0,450,77,12385,0,0,0,0,12912 +26-Dec-15,0,446,84,11190,0,0,0,0,11720 +19-Dec-15,0,452,92,11888,0,0,0,0,12432 +12-Dec-15,0,468,84,12623,0,0,0,0,13175 +5-Dec-15,0,465,87,12736,0,0,0,0,13288 +28-Nov-15,0,460,86,12721,0,0,0,0,13267 +21-Nov-15,0,465,88,12423,0,0,0,0,12976 +14-Nov-15,0,494,90,12483,0,0,0,0,13067 +7-Nov-15,0,489,89,12383,0,0,0,0,12961 +31-Oct-15,0,509,87,12512,0,0,0,0,13108 +24-Oct-15,0,495,88,12346,0,0,0,0,12929 +17-Oct-15,0,505,90,12338,0,0,0,0,12933 +10-Oct-15,0,487,96,11995,0,0,0,0,12578 +3-Oct-15,0,489,87,11676,0,0,0,0,12252 +26-Sep-15,0,480,93,11015,0,0,0,0,11588 +19-Sep-15,0,474,85,10385,0,0,0,0,10944 +12-Sep-15,0,506,94,11041,0,0,0,0,11641 +5-Sep-15,0,519,101,11402,0,0,0,0,12022 +29-Aug-15,0,528,106,11556,0,0,0,0,12190 +22-Aug-15,0,596,106,11672,0,0,0,0,12374 +15-Aug-15,0,530,105,11574,0,0,0,0,12209 +8-Aug-15,1,520,103,11089,0,0,0,0,11713 +1-Aug-15,1,507,106,10947,0,0,0,0,11561 +25-Jul-15,1,593,107,10712,0,0,0,0,11413 +18-Jul-15,1,520,104,10533,0,0,0,0,11158 +11-Jul-15,1,538,110,10764,0,0,0,0,11413 +4-Jul-15,1,550,108,10918,0,0,0,0,11577 +27-Jun-15,1,537,114,10783,0,0,0,0,11435 +20-Jun-15,1,564,109,10961,0,0,0,0,11635 +13-Jun-15,1,550,114,10845,0,0,0,0,11510 +6-Jun-15,1,540,112,10369,0,0,0,0,11022 +30-May-15,1,537,115,10137,0,0,0,0,10790 +23-May-15,1,532,116,10024,0,0,0,0,10673 +16-May-15,1,556,115,10051,0,0,0,0,10723 +9-May-15,1,549,112,9923,0,0,0,0,10585 +2-May-15,1,549,115,9700,0,0,0,0,10365 +25-Apr-15,1,555,118,9462,0,0,0,0,10136 +18-Apr-15,1,552,121,9363,0,0,0,0,10037 +11-Apr-15,1,549,122,9046,0,0,0,0,9718 +4-Apr-15,1,561,130,8955,0,0,0,0,9647 +28-Mar-15,1,570,125,9064,0,0,0,0,9760 +21-Mar-15,1,568,134,9194,0,0,0,0,9897 +14-Mar-15,1,561,137,9163,0,0,0,0,9862 +7-Mar-15,1,553,127,8826,0,0,0,0,9507 +28-Feb-15,1,587,130,8638,0,0,0,0,9356 +21-Feb-15,1,584,124,8459,0,0,0,0,9168 +14-Feb-15,1,588,136,8314,0,0,0,0,9039 +7-Feb-15,1,577,142,8240,0,0,0,0,8960 +31-Jan-15,1,583,133,8003,0,0,0,0,8720 +24-Jan-15,1,598,130,7738,0,0,0,0,8467 +17-Jan-15,1,593,138,7779,0,0,0,0,8511 +10-Jan-15,1,579,137,7654,0,0,0,0,8371 +3-Jan-15,1,581,127,7500,0,0,0,0,8209 +27-Dec-14,1,562,131,6455,0,0,0,0,7149 +20-Dec-14,1,582,129,6575,0,0,0,0,7287 +13-Dec-14,1,602,134,7342,0,0,0,0,8079 +6-Dec-14,1,602,139,7319,0,0,0,0,8061 +29-Nov-14,1,609,134,7171,0,0,0,0,7915 +22-Nov-14,1,630,144,7088,0,0,0,0,7863 +15-Nov-14,1,625,160,7301,0,0,0,0,8087 +8-Nov-14,1,617,151,7029,0,0,0,0,7798 +1-Nov-14,2,635,166,6769,0,0,0,0,7572 +25-Oct-14,1,614,160,6508,0,0,0,0,7283 +18-Oct-14,1,609,159,6348,0,0,0,0,7117 +11-Oct-14,1,627,155,6349,0,0,0,0,7132 +4-Oct-14,1,621,165,5897,0,0,0,0,6684 +27-Sep-14,1,622,158,5854,0,0,0,0,6635 +20-Sep-14,1,633,164,5881,0,0,0,0,6679 +13-Sep-14,1,654,161,5789,0,0,0,0,6605 +6-Sep-14,1,652,166,5700,0,0,0,0,6519 +30-Aug-14,2,643,171,5580,0,0,0,0,6396 +23-Aug-14,2,643,171,5466,0,0,0,0,6282 +16-Aug-14,2,633,184,5304,0,0,0,0,6123 +9-Aug-14,2,648,185,5183,0,0,0,0,6018 +2-Aug-14,2,641,183,5247,0,0,0,0,6073 +26-Jul-14,2,643,185,4938,0,0,0,0,5768 +19-Jul-14,2,640,183,4814,0,0,0,0,5639 +12-Jul-14,2,719,175,4713,0,0,0,0,5609 +5-Jul-14,2,652,177,4543,0,0,0,0,5374 +28-Jun-14,2,653,182,4526,0,0,0,0,5363 +21-Jun-14,2,655,181,4490,0,0,0,0,5328 +14-Jun-14,2,651,166,3895,0,0,0,0,4714 +7-Jun-14,2,674,187,3866,0,0,0,0,4729 +31-May-14,2,665,183,3623,0,0,0,0,4473 +24-May-14,2,669,194,3581,0,0,0,0,4446 +17-May-14,2,669,191,3346,0,0,0,0,4208 +10-May-14,2,671,196,3221,0,0,0,0,4090 +3-May-14,2,661,192,3223,0,0,0,0,4078 +26-Apr-14,2,681,194,3095,0,0,0,0,3972 +19-Apr-14,2,687,201,2981,0,0,0,0,3871 +12-Apr-14,2,695,203,2910,0,0,0,0,3810 +5-Apr-14,2,694,218,2807,0,0,0,0,3721 +29-Mar-14,2,725,222,2807,0,0,0,0,3756 +22-Mar-14,2,709,226,2661,0,0,0,0,3598 +15-Mar-14,2,712,235,2544,0,0,0,0,3493 +8-Mar-14,2,747,241,2405,0,0,0,0,3395 +1-Mar-14,2,715,255,2368,0,0,0,0,3340 +22-Feb-14,2,748,244,2245,0,0,0,0,3239 +15-Feb-14,2,707,248,2107,0,0,0,0,3064 +8-Feb-14,2,759,256,2044,0,0,0,0,3061 +1-Feb-14,2,734,258,1928,0,0,0,0,2922 +25-Jan-14,2,753,263,1781,0,0,0,0,2799 +18-Jan-14,2,749,260,1722,0,0,0,0,2733 +11-Jan-14,2,728,288,1612,0,0,0,0,2630 +4-Jan-14,2,755,276,1343,0,0,0,0,2376 +28-Dec-13,2,736,285,1082,0,0,0,0,2105 +21-Dec-13,2,711,276,1005,0,0,0,0,1994 +14-Dec-13,2,769,285,1118,0,0,0,0,2174 +7-Dec-13,2,756,281,1008,0,0,0,0,2047 +30-Nov-13,2,799,286,920,0,0,0,0,2007 +23-Nov-13,2,898,290,849,0,0,0,0,2039 +16-Nov-13,2,956,313,790,0,0,0,0,2061 +9-Nov-13,2,939,313,746,0,0,0,0,2000 +2-Nov-13,2,963,327,662,0,0,0,0,1954 +26-Oct-13,2,992,322,636,0,0,0,0,1952 +19-Oct-13,2,977,309,651,0,0,0,0,1939 +12-Oct-13,2,990,328,576,0,0,0,0,1896 +5-Oct-13,2,887,328,560,0,0,0,0,1777 +28-Sep-13,2,1450,331,516,0,0,0,0,2299 +21-Sep-13,2,1510,336,449,0,0,0,0,2297 +14-Sep-13,2,1530,335,423,0,0,0,0,2290 +7-Sep-13,2,874,340,363,0,0,0,0,1579 +31-Aug-13,2,887,353,317,0,0,0,0,1559 +24-Aug-13,2,870,349,269,0,0,0,0,1490 +17-Aug-13,2,877,352,235,0,0,0,0,1466 +10-Aug-13,2,862,367,190,0,0,0,0,1421 +3-Aug-13,2,841,399,149,0,0,0,0,1391 +27-Jul-13,2,876,416,104,0,0,0,0,1398 +20-Jul-13,2,887,457,15,0,0,0,0,1361 +13-Jul-13,2,879,445,0,0,0,0,0,1326 +6-Jul-13,2,876,462,0,0,0,0,0,1340 +29-Jun-13,2,884,464,0,0,0,0,0,1350 +22-Jun-13,2,901,452,0,0,0,0,0,1355 +15-Jun-13,2,893,460,0,0,0,0,0,1355 +8-Jun-13,2,956,474,0,0,0,0,0,1432 +1-Jun-13,2,1117,473,0,0,0,0,0,1592 +25-May-13,2,1063,441,0,0,0,0,0,1506 +18-May-13,2,944,383,0,0,0,0,0,1329 +11-May-13,2,1109,450,0,0,0,0,0,1561 +4-May-13,2,1020,451,0,0,0,0,0,1473 +27-Apr-13,2,1003,450,0,0,0,0,0,1455 +20-Apr-13,2,967,448,0,0,0,0,0,1417 +13-Apr-13,2,1061,469,0,0,0,0,0,1532 +6-Apr-13,2,1022,478,0,0,0,0,0,1502 +30-Mar-13,2,1030,440,0,0,0,0,0,1472 +23-Mar-13,2,1101,464,0,0,0,0,0,1567 +16-Mar-13,2,1047,451,0,0,0,0,0,1500 +9-Mar-13,2,1046,476,0,0,0,0,0,1524 +2-Mar-13,3,1044,469,0,0,0,0,0,1516 +23-Feb-13,3,1077,479,0,0,0,0,0,1559 +16-Feb-13,3,1094,458,0,0,0,0,0,1555 +9-Feb-13,3,1091,468,0,0,0,0,0,1562 +2-Feb-13,3,1099,447,0,0,0,0,0,1549 +26-Jan-13,3,1132,452,0,0,0,0,0,1587 +19-Jan-13,3,1124,455,0,0,0,0,0,1582 +12-Jan-13,4,1164,446,0,0,0,0,0,1614 +5-Jan-13,3,1167,443,0,0,0,0,0,1613 +29-Dec-12,3,1085,411,0,0,0,0,0,1499 +22-Dec-12,3,1076,407,0,0,0,0,0,1486 +15-Dec-12,4,1173,437,0,0,0,0,0,1614 +8-Dec-12,4,1208,441,0,0,0,0,0,1653 +1-Dec-12,3,1171,476,0,0,0,0,0,1650 +24-Nov-12,3,1185,474,0,0,0,0,0,1662 +17-Nov-12,3,1163,460,0,0,0,0,0,1626 +10-Nov-12,3,1200,451,0,0,0,0,0,1654 +3-Nov-12,3,1247,476,0,0,0,0,0,1726 +27-Oct-12,3,1219,456,0,0,0,0,0,1678 +20-Oct-12,3,1276,443,0,0,0,0,0,1722 diff --git a/web/modules/charts/modules/charts_api_example/js/charts_api_example_js_override.js b/web/modules/charts/modules/charts_api_example/js/charts_api_example_js_override.js new file mode 100644 index 00000000..93dcfa62 --- /dev/null +++ b/web/modules/charts/modules/charts_api_example/js/charts_api_example_js_override.js @@ -0,0 +1,24 @@ +(function () { + 'use strict'; + Drupal.charts_api_example = Drupal.charts_api_example || {}; + // PLEASE NOTE: this example is structured to work for Highcharts, but you + // do the same thing for any of the charting libraries. + Drupal.charts_api_example.highchartsTooltipFormatter = function () { + return 'The value for ' + this.x + + ' is ' + this.y + ''; + }; + Drupal.behaviors.chartsApiExampleCharts = { + attach: function (context, settings) { + let chartcontainer = document.getElementById('exampleidjs'); + chartcontainer.addEventListener('drupalChartsConfigsInitialization', function (e) { + let data = e.detail; + const id = data.drupalChartDivId; + // Change the background of the chart to green. + data.chart.backgroundColor = 'green'; + // Replace the tooltip formatter. + data.tooltip.formatter = Drupal.charts_api_example.highchartsTooltipFormatter; + Drupal.Charts.Contents.update(id, data); + }); + } + }; +}()); diff --git a/web/modules/charts/modules/charts_api_example/src/Controller/ChartsApiExample.php b/web/modules/charts/modules/charts_api_example/src/Controller/ChartsApiExample.php new file mode 100755 index 00000000..15085449 --- /dev/null +++ b/web/modules/charts/modules/charts_api_example/src/Controller/ChartsApiExample.php @@ -0,0 +1,400 @@ +messenger = $messenger; + $this->uuidService = $uuidService; + $this->moduleList = $module_list; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('messenger'), + $container->get('uuid'), + $container->get('extension.list.module') + ); + } + + /** + * Display the dashboard of charts. + * + * @return array + * Array to render. + */ + public function display() { + + /* + * If you do not set a library specifically in your render array, Charts + * will use your default. But you do need to set your default library. + */ + $charts_settings = $this->config('charts.settings'); + $library = $charts_settings->get('charts_default_settings.library'); + if (empty($library)) { + $this->messenger->addError($this->t('You need to first configure Charts default settings')); + return []; + } + + // This is a container for all the charts below. + $charts_container = [ + '#type' => 'container', + 'content' => [], + ]; + + /* Listing all the default chart types except 'gauge' and 'scatter' + * (they come later). + */ + $chart_types = ['area', 'bar', 'column', 'line', 'spline', 'pie', 'donut']; + + // Define a series to be used in multiple examples. + $series = [ + '#type' => 'chart_data', + '#title' => $this->t('5.0.x'), + '#data' => [257, 235, 325, 340], + '#color' => '#1d84c3', + ]; + + // Define an x-axis to be used in multiple examples. + $xaxis = [ + '#type' => 'chart_xaxis', + '#title' => $this->t('Months'), + '#labels' => [ + $this->t('January 2021'), + $this->t('February 2021'), + $this->t('March 2021'), + $this->t('April 2021'), + ], + ]; + + // Define a y-axis to be used in multiple examples. + $yaxis = [ + '#type' => 'chart_yaxis', + '#title' => $this->t('Number of Installs'), + ]; + + // Iterate through the chart types and build a chart for each. + foreach ($chart_types as $type) { + $charts_container['content'][$type] = [ + '#type' => 'chart', + '#tooltips' => $charts_settings->get('charts_default_settings.display.tooltips'), + '#title' => $this->t('@library @type Chart', [ + '@library' => ucfirst($library), + '@type' => ucfirst($type), + ]), + '#chart_type' => $type, + 'series' => $series, + 'x_axis' => $xaxis, + 'y_axis' => $yaxis, + '#raw_options' => [], + // e.g. ['chart' => ['backgroundColor' => '#000000']]. + ]; + } + + // Column chart with two series. + $charts_container['content']['two_series_column'] = [ + '#type' => 'chart', + '#tooltips' => $charts_settings->get('charts_default_settings.display.tooltips'), + '#title' => $this->t('@library Column Chart (Two Series)', ['@library' => ucfirst($library)]), + '#chart_type' => 'column', + 'series_one' => $series, + 'series_two' => [ + '#type' => 'chart_data', + '#title' => $this->t('8.x-3.x'), + '#data' => [4330, 4413, 4212, 4431], + '#color' => '#77b259', + ], + 'x_axis' => $xaxis, + 'y_axis' => $yaxis, + '#raw_options' => [], + ]; + + // Stacked Area Chart from a local CSV file. + $file_contents = $this->getCsvContents(); + $charts_container['content']['from_csv_file'] = [ + '#type' => 'chart', + '#tooltips' => $charts_settings->get('charts_default_settings.display.tooltips'), + '#title' => $this->t('@library Stacked Area Chart from CSV File', ['@library' => ucfirst($library)]), + '#chart_type' => 'area', + 'series_seven_2' => [ + '#type' => 'chart_data', + '#title' => $this->t('7.x-2.x'), + // Using array_reverse because my file is desc rather than asc. + '#data' => array_reverse($file_contents['7.x-2.x']), + '#color' => '#76b7b2', + ], + 'series_eight_three' => [ + '#type' => 'chart_data', + '#title' => $this->t('8.x-3.x'), + '#data' => array_reverse($file_contents['8.x-3.x']), + '#color' => '#edc949', + ], + 'series_five_zero' => [ + '#type' => 'chart_data', + '#title' => $this->t('5.0.x'), + '#data' => array_reverse($file_contents['5.0.x']), + '#color' => '#ff9da7', + ], + 'x_axis' => [ + '#type' => 'chart_xaxis', + '#title' => $this->t('Week'), + '#labels' => array_reverse($file_contents['Week']), + ], + 'y_axis' => [ + '#type' => 'chart_yaxis', + '#title' => $this->t('Number of Installs'), + ], + '#stacking' => TRUE, + ]; + if ($library === 'chartjs') { + // A real-life #raw_options use-case. + $charts_container['content']['from_csv_file']['#raw_options'] = [ + 'options' => [ + 'scales' => [ + 'x' => [ + 'ticks' => [ + 'autoSkip' => TRUE, + ], + ], + ], + ], + ]; + } + elseif ($library === 'billboard' || $library === 'c3') { + $charts_container['content']['from_csv_file']['#raw_options'] = [ + 'axis' => [ + 'x' => [ + 'tick' => [ + 'culling' => TRUE, + ], + ], + ], + ]; + } + + // Stacked column chart with two series. + $charts_container['content']['stacked_two_series_column'] = [ + '#type' => 'chart', + '#tooltips' => $charts_settings->get('charts_default_settings.display.tooltips'), + '#title' => $this->t('@library Stacked Column Chart (Two Series)', ['@library' => ucfirst($library)]), + '#chart_type' => 'column', + 'series_one' => $series, + 'series_two' => [ + '#type' => 'chart_data', + '#title' => $this->t('8.x-3.x'), + '#data' => [4330, 4413, 4212, 4431], + '#color' => '#77b259', + ], + 'x_axis' => $xaxis, + 'y_axis' => $yaxis, + '#stacking' => TRUE, + '#raw_options' => [], + ]; + + // Combination chart (column and line). + $charts_container['content']['combo'] = [ + '#type' => 'chart', + '#tooltips' => $charts_settings->get('charts_default_settings.display.tooltips'), + '#title' => $this->t('@library Combination Chart', ['@library' => ucfirst($library)]), + '#chart_type' => 'column', + 'series_one' => $series, + 'series_two' => [ + '#type' => 'chart_data', + '#chart_type' => 'line', + '#title' => $this->t('8.x-3.x'), + '#data' => [4330, 4413, 4212, 4431], + '#color' => '#77b259', + ], + 'x_axis' => $xaxis, + 'y_axis' => $yaxis, + '#raw_options' => [], + ]; + + // Combination chart (column and line) with secondary Y-Axis. + $charts_container['content']['combo_dual_yaxes'] = [ + '#type' => 'chart', + '#tooltips' => $charts_settings->get('charts_default_settings.display.tooltips'), + '#title' => $this->t('@library Combination Chart with Secondary Y-Axis', ['@library' => ucfirst($library)]), + '#chart_type' => 'column', + 'series_one' => $series, + 'series_two' => [ + '#type' => 'chart_data', + '#chart_type' => 'line', + '#title' => $this->t('8.x-3.x'), + '#data' => [4330, 4413, 4212, 4431], + '#color' => '#77b259', + '#target_axis' => 'y_axis_secondary', + ], + 'x_axis' => $xaxis, + 'y_axis' => $yaxis, + 'y_axis_secondary' => [ + '#type' => 'chart_yaxis', + '#title' => $this->t('Secondary Y-Axis'), + '#opposite' => TRUE, + ], + '#raw_options' => [], + ]; + + // Radar chart. Not supported by C3.js or Google Charts (natively). + if (!in_array($library, ['c3', 'google'])) { + $charts_container['content']['radar'] = [ + '#type' => 'chart', + '#tooltips' => $charts_settings->get('charts_default_settings.display.tooltips'), + '#title' => $this->t('@library Radar Chart', ['@library' => ucfirst($library)]), + '#chart_type' => 'line', + 'series' => $series, + 'x_axis' => $xaxis, + 'y_axis' => $yaxis, + '#polar' => TRUE, + '#raw_options' => [], + ]; + } + + // Gauge chart. Not yet supported by Chart.js (as of 3.5.1). + if ($library !== 'chartjs') { + $charts_container['content']['gauge'] = [ + '#type' => 'chart', + '#title' => $this->t('@library Gauge Chart', ['@library' => ucfirst($library)]), + '#chart_type' => 'gauge', + '#gauge' => [ + 'green_to' => 100, + 'green_from' => 75, + 'yellow_to' => 74, + 'yellow_from' => 50, + 'red_to' => 49, + 'red_from' => 0, + 'max' => 100, + 'min' => 0, + ], + 'series' => [ + '#type' => 'chart_data', + '#title' => $this->t('Speed'), + '#data' => [65], + ], + '#raw_options' => [], + ]; + } + + // Scatter chart. + $charts_container['content']['scatter'] = [ + '#type' => 'chart', + '#tooltips' => $charts_settings->get('charts_default_settings.display.tooltips'), + '#title' => $this->t('@library Scatter Chart', ['@library' => ucfirst($library)]), + '#data_markers' => TRUE, + '#chart_type' => 'scatter', + 'series' => [ + '#type' => 'chart_data', + '#title' => $this->t('Group 1'), + '#data' => [[162.2, 51.8], [164.5, 58.0], [160.5, 49.6], [154.0, 65.0]], + ], + 'x_axis' => [ + '#type' => 'chart_xaxis', + '#title' => $this->t('Height'), + '#labels' => [], + ], + 'y_axis' => [ + '#type' => 'chart_yaxis', + '#title' => $this->t('Weight'), + ], + '#stacking' => TRUE, + '#raw_options' => [], + ]; + + if ($library === 'highcharts') { + $charts_container['content']['php_override'] = [ + '#chart_id' => 'exampleidphp', + '#type' => 'chart', + '#tooltips' => $charts_settings->get('charts_default_settings.display.tooltips'), + '#title' => $this->t('@library Chart, Overridden By PHP Hook', ['@library' => ucfirst($library)]), + '#chart_type' => 'column', + 'series' => $series, + 'x_axis' => $xaxis, + 'y_axis' => $yaxis, + '#color_changer' => TRUE, + '#raw_options' => [], + ]; + + $charts_container['content']['js_override'] = [ + '#id' => 'exampleidjs', + '#chart_id' => 'exampleidjschart', + '#type' => 'chart', + '#tooltips' => $charts_settings->get('charts_default_settings.display.tooltips'), + '#title' => $this->t('@library Chart, Overridden By JS Function', ['@library' => ucfirst($library)]), + '#chart_type' => 'column', + 'series' => $series, + 'x_axis' => $xaxis, + 'y_axis' => $yaxis, + '#raw_options' => [], + ]; + } + + return $charts_container; + } + + /** + * Returns the CSV contents in an array with data organized by column. + * + * @return array + * The array of rows. + */ + private function getCsvContents() { + $file_path = $this->moduleList->getPath('charts_api_example'); + $file_name = $file_path . '/fixtures/charts_api_example_file.csv'; + $handle = fopen($file_name, 'r'); + $all_rows = []; + while ($row = fgetcsv($handle)) { + $all_rows['Week'][] = $row[0]; + $all_rows['7.x-2.x'][] = (int) $row[4]; + $all_rows['8.x-3.x'][] = (int) $row[6]; + $all_rows['5.0.x'][] = (int) $row[8]; + } + fclose($handle); + + return $all_rows; + } + +} diff --git a/web/modules/charts/modules/charts_billboard/README.md b/web/modules/charts/modules/charts_billboard/README.md new file mode 100755 index 00000000..5f3d07e9 --- /dev/null +++ b/web/modules/charts/modules/charts_billboard/README.md @@ -0,0 +1,117 @@ +#Installation Using Composer (recommended) + +If you use Composer to manage dependencies, edit your site's "composer.json" +file as follows. + +1. Run the following command to ensure that you have the "composer/installers" +package installed. This package facilitates the installation of packages into +directories other than "/vendor" (e.g. "/libraries") using Composer. + + composer require --prefer-dist composer/installers + +2. Add the following to the "installer-paths" section of "composer.json": + + "libraries/{$name}": ["type:drupal-library"], + +3. Add the following to the "repositories" section of "composer.json": + + { + "type": "package", + "package": { + "name": "billboardjs/billboard", + "version": "3.10.3", + "type": "drupal-library", + "extra": { + "installer-name": "billboard" + }, + "dist": { + "url": "https://registry.npmjs.org/billboard.js/-/billboard.js-3.10.3.tgz", + "type": "tar" + } + } + }, + { + "type": "package", + "package": { + "name": "d3/d3", + "version": "7.8.5", + "type": "drupal-library", + "extra": { + "installer-name": "d3" + }, + "dist": { + "url": "https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.js", + "type": "file" + }, + "require": { + "composer/installers": "^1.0 || ^2.0" + } + } + } + +4. This and the following step is optional but recommended. The reason for +them is that when installing the Billboard.js package with Composer, +additional files are added into the library directory. These files are not +necessary and can be potentially harmful to your site, so it's best to remove +them. So: create a new directory in your project root called "scripts". +5. Inside that directory, create a new file called "clean-billboardjs.sh" and +paste the following into it: + + #!/usr/bin/env bash + set -eu + declare -a directories=( + "web/libraries/billboard/dist-esm" + "web/libraries/billboard/src" + "web/libraries/billboard/types" + "web/libraries/billboard/dist/plugin" + "web/libraries/billboard/dist/theme" + ) + counter=0 + echo "Deleting unneeded directories inside web/libraries/billboard" + for directory in "${directories[@]}" + do + if [ -d $directory ]; then + echo "Deleting $directory" + rm -rf $directory + counter=$((counter+1)) + fi + done + echo "$counter folders were deleted" + declare -a files=( + "web/libraries/billboard/CONTRIBUTING.md" + "web/libraries/billboard/README.md" + "web/libraries/billboard/LICENSE" + "web/libraries/billboard/package.json" + "web/libraries/billboard/dist/package.json" + ) + counter=0 + echo "Deleting unneeded files inside web/libraries/billboard" + for file in "${files[@]}" + do + if [[ -f $file ]]; then + echo "Deleting $file" + rm $file + counter=$((counter+1)) + fi + done + echo "$counter files were deleted" + +6. Add a "scripts" entry to your "composer.json" file as shown below. If +"scripts" already exists, you will need to do a little more to incorporate +the code below. + + "scripts": { + "clean-billboardjs": "chmod +x scripts/clean-billboardjs.sh && + ./scripts/clean-billboardjs.sh", + "post-install-cmd": [ + "@clean-billboardjs" + ], + "post-update-cmd": [ + "@clean-billboardjs" + ] + } + +7. Run the following command; you should find that new directories have been +created under "/libraries". + + composer require --prefer-dist billboardjs/billboard:3.10.3 d3/d3:7.8.5 diff --git a/web/modules/charts/modules/charts_billboard/charts_billboard.info.yml b/web/modules/charts/modules/charts_billboard/charts_billboard.info.yml new file mode 100644 index 00000000..38c22dab --- /dev/null +++ b/web/modules/charts/modules/charts_billboard/charts_billboard.info.yml @@ -0,0 +1,12 @@ +name: 'Billboard.js Charts' +type: module +description: 'Charts module integration with Billboard.js Charts.' +package: Charts +core_version_requirement: ^10.3 || ^11 +dependencies: + - 'charts:charts' + +# Information added by Drupal.org packaging script on 2024-12-02 +version: '5.1.1' +project: 'charts' +datestamp: 1733170633 diff --git a/web/modules/charts/modules/charts_billboard/charts_billboard.install b/web/modules/charts/modules/charts_billboard/charts_billboard.install new file mode 100644 index 00000000..b7cddf34 --- /dev/null +++ b/web/modules/charts/modules/charts_billboard/charts_billboard.install @@ -0,0 +1,95 @@ +get('advanced.requirements.cdn') ?? FALSE; + + if (!$library_path) { + if ($cdn) { + $requirements['charts_billboard_js'] = [ + 'title' => t('Billboard.js Library'), + 'value' => t('Available through a CDN'), + 'severity' => REQUIREMENT_WARNING, + 'description' => t('You are using the Billboard.js library via a content delivery network. It is generally considered better practice to install the library files locally. Please see the README file inside charts_billboard for instructions to install the library.'), + ]; + } + else { + $requirements['charts_billboard_js'] = [ + 'title' => t('Billboard.js Library'), + 'value' => t('Not Installed'), + 'severity' => REQUIREMENT_ERROR, + 'description' => t('You are missing the Billboard.js library in your Drupal installation directory and you have opted not to use a CDN. Please either enable use of the CDN in the Chart Settings under the Advanced tab or see the README file inside charts_billboard for instructions to install the library.'), + ]; + } + } + else { + // If the library directory contains a demo directory, warn. + if (file_exists($library_path . '/src')) { + $requirements['charts_billboard_js'] = [ + 'title' => t('Billboard.js Library'), + 'value' => t('Installed'), + 'severity' => REQUIREMENT_WARNING, + 'description' => t('You have installed the Billboard.js library in your Drupal installation directory, but it contains additional files that are not required and may be harmful. Please delete everything except the files listed in charts_billboard.libraries.yml. We have included a suggested post-install/post-update script in the README file.'), + ]; + } + else { + $requirements['charts_billboard_js'] = [ + 'title' => t('Billboard.js Library'), + 'value' => t('Installed'), + 'severity' => REQUIREMENT_OK, + 'description' => t('You have installed the Billboard.js library in your Drupal installation directory.'), + ]; + } + } + break; + } + + return $requirements; +} + +/** + * Get the location of the Billboard.js library. + * + * @return string + * The location of the library, or FALSE if the library isn't installed. + */ +function charts_billboard_find_library() { + // The following logic is taken from libraries_get_libraries() + $searchdir = []; + + // Similar to 'modules' and 'themes' directories inside an installation + // profile, installation profiles may want to place libraries into a + // 'libraries' directory. + $searchdir[] = 'profiles/' . \Drupal::installProfile() . '/libraries'; + + // Always search libraries. + $searchdir[] = 'libraries'; + + // Also search sites/' . t('Create Charts blocks without the need for Views.') . '
'; + return $output; + + default: + } +} diff --git a/web/modules/charts/modules/charts_blocks/src/Plugin/Block/ChartsBlock.php b/web/modules/charts/modules/charts_blocks/src/Plugin/Block/ChartsBlock.php new file mode 100755 index 00000000..a0d3e043 --- /dev/null +++ b/web/modules/charts/modules/charts_blocks/src/Plugin/Block/ChartsBlock.php @@ -0,0 +1,114 @@ +setConfiguration($configuration); + $this->configFactory = $config_factory; + $this->uuidService = $uuidService; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('config.factory'), + $container->get('uuid') + ); + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state) { + parent::blockForm($form, $form_state); + + $chart_block_configurations = !empty($this->configuration['chart']) ? $this->configuration['chart'] : []; + + // Merge the charts default settings with this block's configuration. + $charts_settings = $this->configFactory->get('charts.settings'); + $charts_default_settings = $charts_settings->get('charts_default_settings') ?? []; + $defaults = NestedArray::mergeDeep($charts_default_settings, $chart_block_configurations); + + $form['chart'] = [ + '#type' => 'details', + '#title' => $this->t('Chart configurations'), + '#open' => TRUE, + ]; + + $form['chart']['settings'] = [ + '#type' => 'charts_settings', + '#used_in' => 'basic_form', + '#required' => TRUE, + '#series' => TRUE, + '#default_value' => $defaults, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + $this->configuration['chart'] = $form_state->getValue(['chart', 'settings']); + } + + /** + * {@inheritdoc} + */ + public function build() { + $chart_settings = !empty($this->configuration['chart']) ? $this->configuration['chart'] : []; + + // Creates a UUID for the chart ID. + $chart_id = 'charts_block__' . $this->configuration['id']; + $id = 'chart-' . $this->uuidService->generate(); + $build = Chart::buildElement($chart_settings, $chart_id); + $build['#id'] = $id; + $build['#chart_id'] = $chart_id; + + return $build; + } + +} diff --git a/web/modules/charts/modules/charts_c3/README.md b/web/modules/charts/modules/charts_c3/README.md new file mode 100755 index 00000000..a10e64e1 --- /dev/null +++ b/web/modules/charts/modules/charts_c3/README.md @@ -0,0 +1,137 @@ +#Installation Using Composer (recommended) + +If you use Composer to manage dependencies, edit your site's "composer.json" +file as follows. + +1. Run the following command to ensure that you have the "composer/installers" + package installed. This package facilitates the installation of packages into + directories other than "/vendor" (e.g. "/libraries") using Composer. + + composer require --prefer-dist composer/installers + +2. Add the following to the "installer-paths" section of "composer.json": + + "libraries/{$name}": ["type:drupal-library"], + +3. Add the following to the "repositories" section of "composer.json": + + { + "type": "package", + "package": { + "name": "c3js/c3", + "version": "v0.7.20", + "type": "drupal-library", + "extra": { + "installer-name": "c3" + }, + "dist": { + "url": "https://github.com/c3js/c3/archive/v0.7.20.zip", + "type": "zip" + } + } + }, + { + "type": "package", + "package": { + "name": "d3/d3", + "version": "v4.9.1", + "type": "drupal-library", + "extra": { + "installer-name": "d3" + }, + "dist": { + "url": "https://github.com/d3/d3/archive/v4.9.1.zip", + "type": "zip" + }, + "require": { + "composer/installers": "^1.0 || ^2.0" + } + } + } + +4. This and the following step is optional but recommended. The reason for + them is that when installing the C3.js package with Composer, + additional files are added into the library directory. These files are not + necessary and can be potentially harmful to your site, so it's best to remove + them. So: create a new directory in your project root called "scripts". +5. Inside that directory, create a new file called "clean-c3js.sh" and + paste the following into it: + + #!/usr/bin/env bash + set -eu + declare -a directories=( + "web/libraries/c3/.circleci" + "web/libraries/c3/.github" + "web/libraries/c3/data" + "web/libraries/c3/design" + "web/libraries/c3/docs" + "web/libraries/c3/extensions" + "web/libraries/c3/htdocs" + "web/libraries/c3/spec" + "web/libraries/c3/src" + ) + counter=0 + echo "Deleting unneeded directories inside web/libraries/c3" + for directory in "${directories[@]}" + do + if [ -d $directory ]; then + echo "Deleting $directory" + rm -rf $directory + counter=$((counter+1)) + fi + done + echo "$counter folders were deleted" + declare -a files=( + "web/libraries/c3/CONTRIBUTING.md" + "web/libraries/c3/README.md" + "web/libraries/c3/LICENSE" + "web/libraries/c3/package.json" + "web/libraries/c3/.bmp.yml" + "web/libraries/c3/.editorconfig" + "web/libraries/c3/.gitignore" + "web/libraries/c3/.jshintrc" + "web/libraries/c3/.prettierrc.json" + "web/libraries/c3/.travis.yml" + "web/libraries/c3/bower.json" + "web/libraries/c3/codecov.yml" + "web/libraries/c3/component.json" + "web/libraries/c3/config.rb" + "web/libraries/c3/Gemfile" + "web/libraries/c3/Gemfile.lock" + "web/libraries/c3/karma.conf.js" + "web/libraries/c3/MAINTAINANCE.md" + "web/libraries/c3/rollup.config.js" + "web/libraries/c3/tsconfig.json" + "web/libraries/c3/yarn.lock" + ) + counter=0 + echo "Deleting unneeded files inside web/libraries/c3" + for file in "${files[@]}" + do + if [[ -f $file ]]; then + echo "Deleting $file" + rm $file + counter=$((counter+1)) + fi + done + echo "$counter files were deleted" + +6. Add a "scripts" entry to your "composer.json" file as shown below. If + "scripts" already exists, you will need to do a little more to incorporate + the code below. + + "scripts": { + "clean-c3js": "chmod +x scripts/clean-c3js.sh && + ./scripts/clean-c3js.sh", + "post-install-cmd": [ + "@clean-c3js" + ], + "post-update-cmd": [ + "@clean-c3js" + ] + } + +7. Run the following command; you should find that new directories have been + created under "/libraries". + + composer require --prefer-dist c3js/c3:0.7.20 d3/d3:4.9.1 diff --git a/web/modules/charts/modules/charts_c3/charts_c3.info.yml b/web/modules/charts/modules/charts_c3/charts_c3.info.yml new file mode 100644 index 00000000..4f318cab --- /dev/null +++ b/web/modules/charts/modules/charts_c3/charts_c3.info.yml @@ -0,0 +1,12 @@ +name: 'C3.js Charts' +type: module +description: 'Charts module integration with C3.js Charts.' +package: Charts +core_version_requirement: ^10.3 || ^11 +dependencies: + - 'charts:charts' + +# Information added by Drupal.org packaging script on 2024-12-02 +version: '5.1.1' +project: 'charts' +datestamp: 1733170633 diff --git a/web/modules/charts/modules/charts_c3/charts_c3.install b/web/modules/charts/modules/charts_c3/charts_c3.install new file mode 100644 index 00000000..5d8f03ba --- /dev/null +++ b/web/modules/charts/modules/charts_c3/charts_c3.install @@ -0,0 +1,95 @@ +get('advanced.requirements.cdn') ?? FALSE; + + if (!$library_path) { + if ($cdn) { + $requirements['charts_c3_js'] = [ + 'title' => t('C3.js Library'), + 'value' => t('Available through a CDN'), + 'severity' => REQUIREMENT_WARNING, + 'description' => t('You are using the C3.js library via a content delivery network. It is generally considered better practice to install the library files locally. Please see the README file inside charts_c3 for instructions to install the library.'), + ]; + } + else { + $requirements['charts_c3_js'] = [ + 'title' => t('C3.js Library'), + 'value' => t('Not Installed'), + 'severity' => REQUIREMENT_ERROR, + 'description' => t('You are missing the C3.js library in your Drupal installation directory and you have opted not to use a CDN. Please either enable use of the CDN in the Chart Settings under the Advanced tab or see the README file inside charts_c3 for instructions to install the library.'), + ]; + } + } + else { + // If the library directory contains a demo directory, warn. + if (file_exists($library_path . '/src')) { + $requirements['charts_c3_js'] = [ + 'title' => t('C3.js Library'), + 'value' => t('Installed'), + 'severity' => REQUIREMENT_WARNING, + 'description' => t('You have installed the C3.js library in your Drupal installation directory, but it contains additional files that are not required and may be harmful. Please delete everything except the files listed in charts_c3.libraries.yml. We have included a suggested post-install/post-update script in the README file.'), + ]; + } + else { + $requirements['charts_c3_js'] = [ + 'title' => t('C3.js Library'), + 'value' => t('Installed'), + 'severity' => REQUIREMENT_OK, + 'description' => t('You have installed the C3.js library in your Drupal installation directory.'), + ]; + } + } + break; + } + + return $requirements; +} + +/** + * Get the location of the C3.js library. + * + * @return string + * The location of the library, or FALSE if the library isn't installed. + */ +function charts_c3_find_library() { + // The following logic is taken from libraries_get_libraries() + $searchdir = []; + + // Similar to 'modules' and 'themes' directories inside an installation + // profile, installation profiles may want to place libraries into a + // 'libraries' directory. + $searchdir[] = 'profiles/' . \Drupal::installProfile() . '/libraries'; + + // Always search libraries. + $searchdir[] = 'libraries'; + + // Also search sites/This is a placeholder for C3.js-specific library options. If you would like to help build this out, please work from this issue.
', [ + '@issue_link' => Url::fromUri('https://www.drupal.org/project/charts/issues/3046982')->toString(), + ]), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function preRender(array $element) { + // Populate chart settings. + $chart_definition = []; + + $chart_definition = $this->populateOptions($element, $chart_definition); + $chart_definition = $this->populateData($element, $chart_definition); + $chart_definition = $this->populateAxes($element, $chart_definition); + + if (!empty($element['#height']) || !empty($element['#width'])) { + $element['#attributes']['style'] = 'height:' . $element['#height'] . $element['#height_units'] . ';width:' . $element['#width'] . $element['#width_units'] . ';'; + } + + if (!isset($element['#id'])) { + $element['#id'] = Html::getUniqueId('chart-c3'); + } + $chart_definition['bindto'] = '#' . $element['#id']; + + $element['#attached']['library'][] = 'charts_c3/c3'; + $element['#attributes']['class'][] = 'charts-c3'; + $element['#chart_definition'] = $chart_definition; + + return $element; + } + + /** + * The chart type. + * + * @param string $chart_type + * The chart type. + * @param bool $is_polar + * Whether polar is checked. + * + * @return string + * Return the type. + */ + protected function getType($chart_type, $is_polar = FALSE) { + // If Polar is checked, then convert to Radar chart type. + if ($is_polar) { + $type = 'radar'; + } + else { + $type = $chart_type == 'column' ? 'bar' : $chart_type; + } + return $type; + } + + /** + * Get options by type. + * + * @param string $type + * The chart type. + * @param array $element + * The element. + * + * @return array + * Return options. + */ + protected function getOptionsByType($type, array $element) { + $options = $this->getOptionsByCustomProperty($element, $type); + if ($type === 'bar') { + $options['width'] = $element['#width']; + } + + return $options; + } + + /** + * Get options by custom property. + * + * @param array $element + * The element. + * @param string $type + * The chart type. + * + * @return array + * Return options. + */ + protected function getOptionsByCustomProperty(array $element, $type) { + $options = []; + $properties = Element::properties($element); + // Remove properties which are not related to this chart type. + $properties = array_filter($properties, function ($property) use ($type) { + $query = '#chart_' . $type . '_'; + return substr($property, 0, strlen($query)) === $query; + }); + foreach ($properties as $property) { + $query = '#chart_' . $type . '_'; + $option_key = substr($property, strlen($query), strlen($property)); + $options[$option_key] = $element[$property]; + } + return $options; + } + + /** + * Populate options. + * + * @param array $element + * The element. + * @param array $chart_definition + * The chart definition. + * + * @return array + * Return the chart definition. + */ + private function populateOptions(array $element, array $chart_definition) { + $type = $this->getType($element['#chart_type']); + $title = $element['#title'] ?? ''; + if (!empty($element['#subtitle'])) { + $title .= ': ' . $element['#subtitle']; + } + $chart_definition['title']['text'] = $title; + $chart_definition['legend']['show'] = !empty($element['#legend_position']); + if (!in_array($type, ['scatter', 'bubble'])) { + $chart_definition['axis']['x']['type'] = 'category'; + } + $chart_definition['data']['labels'] = (bool) $element['#data_labels']; + + if ($type === 'pie' || $type === 'donut') { + + } + elseif ($type === 'gauge') { + $chart_definition['gauge']['min'] = $element['#gauge']['min']; + $chart_definition['gauge']['max'] = $element['#gauge']['max']; + $chart_definition['color']['pattern'] = [ + 'red', + 'yellow', + 'green', + ]; + $chart_definition['color']['threshold']['values'] = [ + $element['#gauge']['red_from'], + $element['#gauge']['yellow_from'], + $element['#gauge']['green_from'], + ]; + } + elseif ($type === 'line' || $type === 'spline') { + $chart_definition['point']['show'] = !empty($element['#data_markers']); + } + else { + /* + * Billboard does not use bar, so column must be used. Since 'column' + * is changed + * to 'bar' in getType(), we need to use the value from the element. + */ + if ($element['#chart_type'] === 'bar') { + $chart_definition['axis']['rotated'] = TRUE; + } + elseif ($element['#chart_type'] === 'column') { + $type = 'bar'; + $chart_definition['axis']['rotated'] = FALSE; + } + } + $chart_definition['data']['type'] = $type; + // Merge in chart raw options. + if (!empty($element['#raw_options'])) { + $chart_definition = NestedArray::mergeDeepArray([ + $chart_definition, + $element['#raw_options'], + ]); + } + + return $chart_definition; + } + + /** + * Populate axes. + * + * @param array $element + * The element. + * @param array $chart_definition + * The chart definition. + * + * @return array + * Return chart definition. + */ + private function populateAxes(array $element, array $chart_definition) { + $children = Element::children($element); + foreach ($children as $child) { + $type = $element[$child]['#type']; + if ($type === 'chart_xaxis') { + $chart_definition['axis']['x']['label'] = $element[$child]['#title'] ?? ''; + $chart_type = $this->getType($element['#chart_type']); + $categories = $this->stripLabelTags($element[$child]['#labels']); + if (!in_array($chart_type, ['pie', 'donut'])) { + if ($chart_type === 'scatter' || $chart_type === 'bubble') { + // Scatter is not supported: https://github.com/c3js/c3/issues/1902 + } + else { + $chart_definition['data']['columns'][] = ['x']; + $chart_definition['data']['x'] = 'x'; + $categories_keys = array_keys($chart_definition['data']['columns']); + $categories_key = end($categories_keys); + foreach ($categories as $category) { + $chart_definition['data']['columns'][$categories_key][] = $category; + } + } + } + else { + $chart_definition['data']['columns'] = array_map(NULL, $categories, $chart_definition['data']['columns']); + } + } + if ($type === 'chart_yaxis') { + if (!empty($element[$child]['#opposite']) && $element[$child]['#opposite'] === TRUE) { + $chart_definition['axis']['y2']['show'] = TRUE; + $this->setLabelMinMax($chart_definition, 'y2', $element[$child]); + } + else { + $this->setLabelMinMax($chart_definition, 'y', $element[$child]); + } + } + } + + return $chart_definition; + } + + /** + * Set the label, min, and max. + * + * @param array $chart_definition + * The chart definition. + * @param string $axis + * The axis. + * @param array $element + * The element. + */ + private function setLabelMinMax(array &$chart_definition, string $axis, array $element): void { + $chart_definition['axis'][$axis]['label'] = $element['#title'] ?? ''; + if (!empty($element['#min'])) { + $chart_definition['axis'][$axis]['min'] = $element['#min']; + } + if (!empty($element['#max'])) { + $chart_definition['axis'][$axis]['max'] = $element['#max']; + } + } + + /** + * Populate data. + * + * @param array $element + * The element. + * @param array $chart_definition + * The chart definition. + * + * @return array + * Return the chart definition. + */ + private function populateData(array &$element, array $chart_definition) { + $type = $this->getType($element['#chart_type']); + $types = []; + $children = Element::children($element); + $y_axes = []; + foreach ($children as $child) { + $element_type = $element[$child]['#type']; + if ($element_type === 'chart_yaxis') { + $y_axes[] = $child; + } + } + $data_elements = array_filter($children, function ($child) use ($element) { + return $element[$child]['#type'] === 'chart_data'; + }); + + $columns = $chart_definition['data']['columns'] ?? []; + $column_keys = array_keys($columns); + $columns_key_start = $columns ? end($column_keys) + 1 : 0; + foreach ($data_elements as $key) { + $child_element = $element[$key]; + // Make sure defaults are loaded. + if (empty($child_element['#defaults_loaded'])) { + $child_element += $this->elementInfo->getInfo($child_element['#type']); + } + if ($child_element['#color'] && $type !== 'gauge') { + $chart_definition['color']['pattern'][] = $child_element['#color']; + } + if (!in_array($type, ['pie', 'donut'])) { + $series_title = isset($child_element['#title']) ? strip_tags($child_element['#title']) : ''; + $types[$series_title] = $child_element['#chart_type'] ? $this->getType($child_element['#chart_type']) : $type; + if (!in_array($type, ['scatter', 'bubble'])) { + $columns[$columns_key_start][] = $series_title; + foreach ($child_element['#data'] as $datum) { + if (gettype($datum) === 'array') { + if ($type === 'gauge') { + array_shift($datum); + } + $columns[$columns_key_start][] = array_map(function ($item) { + return isset($item) ? strip_tags($item) : NULL; + }, $datum); + } + else { + $columns[$columns_key_start][] = isset($datum) ? strip_tags($datum) : NULL; + } + } + } + else { + $row = []; + $row[$series_title][0] = $series_title; + $row[$series_title . '_x'][0] = $series_title . '_x'; + foreach ($child_element['#data'] as $datum) { + $row[$series_title][] = $datum[0]; + $row[$series_title . '_x'][] = $datum[1]; + } + $chart_definition['data']['xs'][$series_title] = $series_title . '_x'; + foreach ($row as $value) { + $columns[] = $value; + } + $columns = array_values($columns); + } + } + else { + foreach ($child_element['#data'] as $datum_index => $datum) { + if (!empty($datum['color'])) { + $chart_definition['color']['pattern'][$datum_index] = $datum['color']; + unset($datum['color']); + $datum = array_values($datum); + } + $columns[] = $datum; + } + + // Add colors for each segment. + foreach ($child_element['#grouping_colors'] ?? [] as $key => $colors_array) { + $color = reset($colors_array); + $chart_definition['color']['pattern'][$key] = $color; + } + } + + $columns_key_start++; + } + if ($element['#stacking']) { + $chart_definition['data']['groups'] = [array_keys($types)]; + } + $chart_definition['data']['types'] = $types; + $chart_definition['data']['columns'] = $columns; + + if (count($y_axes) >= 2) { + foreach ($columns as $index => $column) { + if ($index <= 1) { + $axis = ($index + 1) === 2 ? 2 : ''; + $chart_definition['data']['axes'][$column[0]] = 'y' . $axis; + } + } + } + + return $chart_definition; + } + + /** + * Strip tags from each item in an array. + * + * @param array $items + * The array. + * + * @return array + * Return the cleaned array. + */ + private function stripLabelTags(array $items): array { + if (empty($items)) { + return []; + } + $categories = []; + foreach ($items as $item) { + $categories[] = isset($item) ? strip_tags($item) : NULL; + } + + return $categories; + } + +} diff --git a/web/modules/charts/modules/charts_chartjs/README.md b/web/modules/charts/modules/charts_chartjs/README.md new file mode 100755 index 00000000..b4f05cc6 --- /dev/null +++ b/web/modules/charts/modules/charts_chartjs/README.md @@ -0,0 +1,158 @@ +#Installation Using Composer (recommended) + +If you use Composer to manage dependencies, edit your site's "composer.json" +file as follows. + +1. Add the asset-packagist composer repository to "composer.json". +This allows installing packages (like Chart.js) that are published on NPM. + + "asset-packagist": { + "type": "composer", + "url": "https://asset-packagist.org" + }, + +You may need to add it in your composer.json file like this (second item in +the array): + + "repositories": [ + { + "type": "composer", + "url": "https://packages.drupal.org/8" + }, + { + "type": "composer", + "url": "https://asset-packagist.org" + }, + ], + +2. Run the following command to ensure that you have the +"oomphinc/composer-installers-extender" package installed. This package +facilitates the installation of any package into directories other than the +default "/vendor" (e.g. "/libraries") using Composer. + + composer require --prefer-dist oomphinc/composer-installers-extender + +3. Configure composer to install the Chart.js dependencies into "/libraries" +by adding the following "installer-types" and "installer-paths" to the "extra" +section of "composer.json". If you are not using the "web" directory, then +remove "web/" from the lines below: + +"extra": { + "installer-types": ["npm-asset"], + "installer-paths": { + "web/libraries/chart.js": ["npm-asset/chart.js"], + "web/libraries/chartjs-adapter-date-fns": [ + "npm-asset/chartjs-adapter-date-fns" + ], + "web/libraries/chartjs-plugin-datalabels": [ + "npm-asset/chartjs-plugin-datalabels" + ], + }, +} + +NOTE: If this isn't working, try instead adding: +"extra": { + "installer-types": ["npm-asset"], + "installer-paths": { + ... + "web/libraries/{$name}": ["type:drupal-library", "vendor:npm-asset"] + }, +} + +4. This and the following step is optional but recommended. The reason for +them is that when installing the Chart.js package with Composer, +additional files are added into the library directory. These files are not +necessary and can be potentially harmful to your site, so it's best to remove +them. So: create a new directory in your project root called "scripts". + +5. Inside that directory, create a new file called "clean-chartjs.sh" and + paste the following into it: + + #!/usr/bin/env bash + set -eu + declare -a directories=( + "web/libraries/chart.js/auto" + "web/libraries/chart.js/helpers" + "web/libraries/chart.js/types" + "web/libraries/chart.js/dist/chunks" + "web/libraries/chart.js/dist/docs" + "web/libraries/chart.js/dist/controllers" + "web/libraries/chart.js/dist/core" + "web/libraries/chart.js/dist/elements" + "web/libraries/chart.js/dist/helpers" + "web/libraries/chart.js/dist/platform" + "web/libraries/chart.js/dist/plugins" + "web/libraries/chart.js/dist/scales" + "web/libraries/chart.js/dist/types" + "web/libraries/chartjs-plugin-datalabels/types" + ) + counter=0 + echo "Deleting unneeded directories inside web/libraries/chartjs" + for directory in "${directories[@]}" + do + if [ -d $directory ]; then + echo "Deleting $directory" + rm -rf $directory + counter=$((counter+1)) + fi + done + echo "$counter folders were deleted" + declare -a files=( + "web/libraries/chart.js/README.md" + "web/libraries/chart.js/LICENSE.md" + "web/libraries/chart.js/package.json" + "web/libraries/chart.js/dist/helpers.esm.js" + "web/libraries/chart.js/dist/helpers.mjs" + "web/libraries/chart.js/dist/chart.mjs" + "web/libraries/chart.js/dist/chart.esm.js" + "web/libraries/chart.js/dist/chart.cjs" + "web/libraries/chart.js/dist/chart.cjs.map" + "web/libraries/chart.js/dist/helpers.js" + "web/libraries/chart.js/dist/helpers.js.map" + "web/libraries/chart.js/dist/helpers.cjs" + "web/libraries/chart.js/dist/helpers.cjs.map" + "web/libraries/chart.js/dist/index.d.ts" + "web/libraries/chart.js/dist/index.umd.d.ts" + "web/libraries/chart.js/dist/types.d.ts" + "web/libraries/chartjs-adapter-date-fns/README.md" + "web/libraries/chartjs-adapter-date-fns/LICENSE.md" + "web/libraries/chartjs-adapter-date-fns/package.json" + "web/libraries/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.esm.js" + "web/libraries/chartjs-plugin-datalabels/README.md" + "web/libraries/chartjs-plugin-datalabels/LICENSE.md" + "web/libraries/chartjs-plugin-datalabels/package.json" + "web/libraries/chartjs-plugin-datalabels/bower.json" + ) + counter=0 + echo "Deleting unneeded files inside web/libraries/chartjs" + for file in "${files[@]}" + do + if [[ -f $file ]]; then + echo "Deleting $file" + rm $file + counter=$((counter+1)) + fi + done + echo "$counter files were deleted" + +6. Add a "scripts" entry to your "composer.json" file as shown below. If + "scripts" already exists, you will need to do a little more to incorporate + the code below. + + "scripts": { + "clean-chartjs": "chmod +x scripts/clean-chartjs.sh && + ./scripts/clean-chartjs.sh", + "post-install-cmd": [ + "@clean-chartjs" + ], + "post-update-cmd": [ + "@clean-chartjs" + ] + } + +7. Run the following command; you should find that new directories have been + created under "/libraries". + + composer require --prefer-dist npm-asset/chart.js:^4.4 + npm-asset/chartjs-adapter-date-fns:^3.0 + npm-asset/chartjs-plugin-datalabels:^2.0 diff --git a/web/modules/charts/modules/charts_chartjs/charts_chartjs.charts_types.yml b/web/modules/charts/modules/charts_chartjs/charts_chartjs.charts_types.yml new file mode 100644 index 00000000..68f79621 --- /dev/null +++ b/web/modules/charts/modules/charts_chartjs/charts_chartjs.charts_types.yml @@ -0,0 +1,5 @@ +polarArea: + label: 'Polar Area' + axis: 'y_only' + axis_inverted: false + stacking: false diff --git a/web/modules/charts/modules/charts_chartjs/charts_chartjs.info.yml b/web/modules/charts/modules/charts_chartjs/charts_chartjs.info.yml new file mode 100644 index 00000000..7f816aef --- /dev/null +++ b/web/modules/charts/modules/charts_chartjs/charts_chartjs.info.yml @@ -0,0 +1,12 @@ +name: 'Chart.js Charts' +type: module +description: 'Charts module integration with Chart.js.' +package: Charts +core_version_requirement: ^10.3 || ^11 +dependencies: + - 'charts:charts' + +# Information added by Drupal.org packaging script on 2024-12-02 +version: '5.1.1' +project: 'charts' +datestamp: 1733170633 diff --git a/web/modules/charts/modules/charts_chartjs/charts_chartjs.install b/web/modules/charts/modules/charts_chartjs/charts_chartjs.install new file mode 100644 index 00000000..d339c068 --- /dev/null +++ b/web/modules/charts/modules/charts_chartjs/charts_chartjs.install @@ -0,0 +1,95 @@ +get('advanced.requirements.cdn') ?? FALSE; + + if (!$library_path) { + if ($cdn) { + $requirements['charts_chartjs_js'] = [ + 'title' => t('Chart.js Library'), + 'value' => t('Available through a CDN'), + 'severity' => REQUIREMENT_WARNING, + 'description' => t('You are using the Chart.js library via a content delivery network. It is generally considered better practice to install the library files locally. Please see the README file inside charts_chartjs for instructions to install the library.'), + ]; + } + else { + $requirements['charts_chartjs_js'] = [ + 'title' => t('Chart.js Library'), + 'value' => t('Not Installed'), + 'severity' => REQUIREMENT_ERROR, + 'description' => t('You are missing the Chart.js library in your Drupal installation directory and you have opted not to use a CDN. Please either enable use of the CDN in the Chart Settings under the Advanced tab or see the README file inside charts_chartjs for instructions to install the library.'), + ]; + } + } + else { + // If the library directory contains "auto", provide a warning. + if (file_exists($library_path . '/auto')) { + $requirements['charts_chartjs_js'] = [ + 'title' => t('Chart.js Library'), + 'value' => t('Installed'), + 'severity' => REQUIREMENT_WARNING, + 'description' => t('You have installed the Chart.js library in your Drupal installation directory, but it contains additional files that are not required and may be harmful. Please delete everything except the files listed in charts_chartjs.libraries.yml. We have included a suggested post-install/post-update script in the README file.'), + ]; + } + else { + $requirements['charts_chartjs_js'] = [ + 'title' => t('Chart.js Library'), + 'value' => t('Installed'), + 'severity' => REQUIREMENT_OK, + 'description' => t('You have installed the Chart.js library in your Drupal installation directory.'), + ]; + } + } + break; + } + + return $requirements; +} + +/** + * Get the location of the Chart.js library. + * + * @return string + * The location of the library, or FALSE if the library isn't installed. + */ +function charts_chartjs_find_library() { + // The following logic is taken from libraries_get_libraries() + $searchdir = []; + + // Similar to 'modules' and 'themes' directories inside an installation + // profile, installation profiles may want to place libraries into a + // 'libraries' directory. + $searchdir[] = 'profiles/' . \Drupal::installProfile() . '/libraries'; + + // Always search libraries. + $searchdir[] = 'libraries'; + + // Also search sites/This is a placeholder for Chart.js-specific library options. If you would like to add more chartjs specific settings, please work from this issue.
', [ + '@issue_link' => Url::fromUri('https://www.drupal.org/project/charts/issues/3046984')->toString(), + ]), + ]; + + $xaxis_configuration = $this->configuration['xaxis'] ?? []; + $form['xaxis'] = [ + '#title' => $this->t('X-Axis Settings'), + '#type' => 'fieldset', + '#tree' => TRUE, + ]; + $form['xaxis']['autoskip'] = [ + '#title' => $this->t('Enable autoskip'), + '#type' => 'checkbox', + '#default_value' => !empty($xaxis_configuration['autoskip']), + ]; + $form['xaxis']['horizontal_axis_title_align'] = [ + '#title' => $this->t('Align horizontal axis title'), + '#type' => 'select', + '#options' => [ + 'start' => $this->t('Start'), + 'center' => $this->t('Center'), + 'end' => $this->t('End'), + ], + '#default_value' => $xaxis_configuration['horizontal_axis_title_align'] ?? 'center', + ]; + $form['yaxis'] = [ + '#title' => $this->t('Y-Axis Settings'), + '#type' => 'fieldset', + '#tree' => TRUE, + ]; + $form['yaxis']['vertical_axis_title_align'] = [ + '#title' => $this->t('Align vertical axis title'), + '#type' => 'select', + '#options' => [ + 'start' => $this->t('Start'), + 'center' => $this->t('Center'), + 'end' => $this->t('End'), + ], + '#default_value' => $this->configuration['yaxis']['vertical_axis_title_align'] ?? 'center', + ]; + + return $form; + } + + /** + * Build configurations. + * + * @param array $form + * The form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + + if (!$form_state->getErrors()) { + $values = $form_state->getValue($form['#parents']); + $this->configuration['xaxis'] = $values['xaxis']; + $this->configuration['yaxis'] = $values['yaxis']; + } + } + + /** + * {@inheritdoc} + */ + public function preRender(array $element) { + $chart_definition = []; + + if (!isset($element['#id'])) { + $element['#id'] = Html::getUniqueId('chartjs-render'); + } + + if (!empty($element['#height']) || !empty($element['#width'])) { + $element['#content_prefix']['manual_sizing'] = [ + '#type' => 'inline_template', + '#template' => 'Charts is designed to be generic enough to work with multiple charting libraries. If you would like settings that apply to all Highcharts charts, you can submit a ticket to have a setting added here, in the Highcharts-specific settings.
'), + ]; + + $form['exporting_library'] = [ + '#title' => $this->t('Enable Highcharts\' "Exporting" library'), + '#type' => 'checkbox', + '#default_value' => !empty($this->configuration['exporting_library']), + ]; + + $form['texture_library'] = [ + '#title' => $this->t('Enable Highcharts\' "Texture" library'), + '#type' => 'checkbox', + '#default_value' => !empty($this->configuration['texture_library']), + ]; + + $legend_configuration = $this->configuration['legend'] ?? []; + $form['legend'] = [ + '#title' => $this->t('Legend Settings'), + '#type' => 'fieldset', + ]; + $form['legend']['layout'] = [ + '#title' => $this->t('Legend layout'), + '#type' => 'select', + '#options' => [ + 'vertical' => $this->t('Vertical'), + 'horizontal' => $this->t('Horizontal'), + ], + '#default_value' => $legend_configuration['layout'] ?? NULL, + ]; + $form['legend']['background_color'] = [ + '#title' => $this->t('Legend background color'), + '#type' => 'textfield', + '#size' => 10, + '#maxlength' => 7, + '#attributes' => ['placeholder' => $this->t('transparent')], + '#description' => $this->t('Leave blank for a transparent background.'), + '#default_value' => $legend_configuration['background_color'] ?? '', + ]; + $form['legend']['border_width'] = [ + '#title' => $this->t('Legend border width'), + '#type' => 'select', + '#options' => [ + 0 => $this->t('None'), + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + ], + '#default_value' => $legend_configuration['border_width'] ?? 0, + ]; + $form['legend']['shadow'] = [ + '#title' => $this->t('Enable legend shadow'), + '#type' => 'checkbox', + '#default_value' => !empty($legend_configuration['shadow']), + ]; + $form['legend']['item_style'] = [ + '#title' => $this->t('Item Style'), + '#type' => 'fieldset', + ]; + $form['legend']['item_style']['color'] = [ + '#title' => $this->t('Item style color'), + '#type' => 'textfield', + '#size' => 10, + '#maxlength' => 7, + '#attributes' => ['placeholder' => '#333333'], + '#description' => $this->t('Leave blank for a dark gray font.'), + '#default_value' => $legend_configuration['item_style']['color'] ?? '', + ]; + $form['legend']['item_style']['overflow'] = [ + '#title' => $this->t('Text overflow'), + '#type' => 'select', + '#options' => [ + '' => $this->t('No'), + 'ellipsis' => $this->t('Ellipsis'), + ], + '#default_value' => $legend_configuration['item_style']['overflow'] ?? '', + ]; + + $form['global_options'] = [ + '#title' => $this->t('Global options'), + '#type' => 'details', + '#collapsible' => TRUE, + '#tree' => TRUE, + ]; + $form['global_options']['lang'] = [ + '#title' => $this->t('Language'), + '#type' => 'details', + '#collapsible' => TRUE, + '#tree' => TRUE, + ]; + // Download menu item. + $lang_config = $this->defaultConfiguration()['global_options']['lang']; + foreach (array_keys($lang_config) as $property) { + if (strpos($property, 'download_') !== 0) { + continue; + } + + [, $format] = explode('_', $property); + $form['global_options']['lang'][$property] = [ + '#title' => $this->t('Download @format', ['@format' => $format]), + '#type' => 'textfield', + '#description' => $this->t('The text for the @format download menu item.', ['@format' => $format]), + '#default_value' => $this->configuration['global_options']['lang'][$property] ?? $lang_config[$property], + '#required' => TRUE, + ]; + } + // Other simple string configs. + $form['global_options']['lang']['exit_fullscreen'] = [ + '#title' => $this->t('Exit fullscreen'), + '#type' => 'textfield', + '#description' => $this->t('Exporting module only. The text for the menu item to exit the chart from full screen.'), + '#default_value' => $this->configuration['global_options']['lang']['exit_fullscreen'] ?? $lang_config['exit_fullscreen'], + '#required' => TRUE, + ]; + $form['global_options']['lang']['hide_data'] = [ + '#title' => $this->t('Hide data'), + '#type' => 'textfield', + '#description' => $this->t('The text for the menu item.'), + '#default_value' => $this->configuration['global_options']['lang']['hide_data'] ?? $lang_config['hide_data'], + '#required' => TRUE, + ]; + $form['global_options']['lang']['loading'] = [ + '#title' => $this->t('Loading'), + '#type' => 'textfield', + '#description' => $this->t('The loading text that appears when the chart is set into the loading state following a call tochart.showLoading.'),
+ '#default_value' => $this->configuration['global_options']['lang']['loading'] ?? $lang_config['loading'],
+ '#required' => TRUE,
+ ];
+ $form['global_options']['lang']['main_breadcrumb'] = [
+ '#title' => $this->t('Main breadcrumb'),
+ '#type' => 'textfield',
+ '#default_value' => $this->configuration['global_options']['lang']['main_breadcrumb'] ?? $lang_config['main_breadcrumb'],
+ '#required' => TRUE,
+ ];
+ $form['global_options']['lang']['thousands_sep'] = [
+ '#title' => $this->t('Number formatting: Thousand separator'),
+ '#type' => 'textfield',
+ '#default_value' => $this->configuration['global_options']['lang']['thousands_sep'] ?? $lang_config['thousands_sep'],
+ ];
+ $form['global_options']['lang']['decimal_point'] = [
+ '#title' => $this->t('Number formatting: Decimal point'),
+ '#type' => 'textfield',
+ '#default_value' => $this->configuration['global_options']['lang']['decimal_point'] ?? $lang_config['decimal_point'],
+ '#required' => TRUE,
+ ];
+ $form['global_options']['lang']['no_data'] = [
+ '#title' => $this->t('No data'),
+ '#type' => 'textfield',
+ '#description' => $this->t('The text to display when the chart contains no data.'),
+ '#default_value' => $this->configuration['global_options']['lang']['no_data'] ?? $lang_config['no_data'],
+ '#required' => TRUE,
+ ];
+ $form['global_options']['lang']['print_chart'] = [
+ '#title' => $this->t('Print chart'),
+ '#type' => 'textfield',
+ '#description' => $this->t('Exporting module only. The text for the menu item to print the chart.'),
+ '#default_value' => $this->configuration['global_options']['lang']['print_chart'] ?? $lang_config['print_chart'],
+ '#required' => TRUE,
+ ];
+ $form['global_options']['lang']['reset_zoom'] = [
+ '#title' => $this->t('Reset zoom'),
+ '#type' => 'textfield',
+ '#description' => $this->t('The text for the label appearing when a chart is zoomed.'),
+ '#default_value' => $this->configuration['global_options']['lang']['reset_zoom'] ?? $lang_config['reset_zoom'],
+ '#required' => TRUE,
+ ];
+ $form['global_options']['lang']['reset_zoom_title'] = [
+ '#title' => $this->t('Reset zoom title'),
+ '#type' => 'textfield',
+ '#description' => $this->t('The tooltip title for the label appearing when a chart is zoomed.'),
+ '#default_value' => $this->configuration['global_options']['lang']['reset_zoom_title'] ?? $lang_config['reset_zoom_title'],
+ '#required' => TRUE,
+ ];
+ $form['global_options']['lang']['view_data'] = [
+ '#title' => $this->t('View data'),
+ '#type' => 'textfield',
+ '#description' => $this->t('The text for the menu item.'),
+ '#default_value' => $this->configuration['global_options']['lang']['view_data'] ?? $lang_config['view_data'],
+ '#required' => TRUE,
+ ];
+ $form['global_options']['lang']['view_fullscreen'] = [
+ '#title' => $this->t('View fullscreen'),
+ '#type' => 'textfield',
+ '#description' => $this->t('Exporting module only. The text for the menu item to view the chart in full screen.'),
+ '#default_value' => $this->configuration['global_options']['lang']['view_fullscreen'] ?? $lang_config['view_fullscreen'],
+ '#required' => TRUE,
+ ];
+ $form['global_options']['lang']['context_button_title'] = [
+ '#title' => $this->t('Context button title'),
+ '#type' => 'textfield',
+ '#description' => $this->t('Exporting module menu. The tooltip title for the context menu holding print and export menu items.'),
+ '#default_value' => $this->configuration['global_options']['lang']['context_button_title'] ?? $lang_config['context_button_title'],
+ '#required' => TRUE,
+ ];
+ $form['global_options']['lang']['drill_up_text'] = [
+ '#title' => $this->t('Drill up text'),
+ '#type' => 'textfield',
+ '#description' => $this->t('The text for the button that appears when drilling down, linking back to the parent series.'),
+ '#default_value' => $this->configuration['global_options']['lang']['drill_up_text'] ?? $lang_config['drill_up_text'],
+ '#required' => TRUE,
+ ];
+ $form['global_options']['lang']['invalid_date'] = [
+ '#title' => $this->t('Invalid date'),
+ '#type' => 'textfield',
+ '#description' => $this->t('What to show in a date field for invalid dates.'),
+ '#default_value' => $this->configuration['global_options']['lang']['invalid_date'] ?? $lang_config['invalid_date'],
+ '#required' => TRUE,
+ ];
+ // Dates related global options.
+ foreach ($this->datesDataForConfigForm() as $dates_data_key => $data) {
+ $form['global_options']['lang'][$dates_data_key] = [
+ '#type' => 'details',
+ '#title' => $data['label_plural'],
+ '#description' => $data['description'],
+ '#collapsible' => TRUE,
+ '#tree' => TRUE,
+ ];
+ foreach (range(0, $data['range_end']) as $counter) {
+ $form['global_options']['lang'][$dates_data_key][$counter] = [
+ '#title' => $this->t('@label_singular', [
+ '@label_singular' => $data['label_singular'],
+ ]) . ' ' . ($counter + 1),
+ '#type' => 'textfield',
+ '#default_value' => $this->configuration['global_options']['lang'][$dates_data_key][$counter] ?? $lang_config[$dates_data_key][$counter],
+ '#required' => TRUE,
+ ];
+ }
+ }
+ // Export data related global options.
+ $form['global_options']['lang']['export_data'] = [
+ '#title' => $this->t('Export data'),
+ '#type' => 'details',
+ '#collapsible' => TRUE,
+ '#tree' => TRUE,
+ ];
+ foreach ($this->exportDataForConfigForm() as $export_data_key => $data) {
+ $form['global_options']['lang']['export_data'][$export_data_key] = [
+ '#title' => $this->t('@label', [
+ '@label' => $data['label'],
+ ]),
+ '#type' => 'textfield',
+ '#description' => $this->t('@description', [
+ '@description' => $data['description'],
+ ]),
+ '#default_value' => $this->configuration['global_options']['lang']['export_data'][$export_data_key] ?? $lang_config['export_data'][$export_data_key],
+ '#required' => TRUE,
+ ];
+ }
+ // Numeric symbols related global options.
+ $form['global_options']['lang']['numeric_symbols'] = [
+ '#type' => 'details',
+ '#title' => 'Numeric symbols',
+ '#description' => 'The numeric symbols.',
+ '#collapsible' => TRUE,
+ '#tree' => TRUE,
+ ];
+ foreach (range(0, 5) as $counter) {
+ $form['global_options']['lang']['numeric_symbols'][$counter] = [
+ '#title' => $this->t('@label_singular', [
+ '@label_singular' => 'Numeric symbol',
+ ]) . ' ' . ($counter + 1),
+ '#type' => 'textfield',
+ '#default_value' => $this->configuration['global_options']['lang']['numeric_symbols'][$counter] ?? $lang_config['numeric_symbols'][$counter],
+ '#required' => TRUE,
+ ];
+ }
+
+ return $form;
+ }
+
+ /**
+ * Submit configurations.
+ *
+ * @param array $form
+ * The form element.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ */
+ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+ parent::submitConfigurationForm($form, $form_state);
+
+ if (!$form_state->getErrors()) {
+ $values = $form_state->getValue($form['#parents']);
+ $this->configuration['legend'] = $values['legend'];
+ $this->configuration['exporting_library'] = $values['exporting_library'];
+ $this->configuration['texture_library'] = $values['texture_library'];
+ $this->configuration['global_options'] = $values['global_options'];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preRender(array $element) {
+ // Populate chart settings.
+ $chart_definition = [];
+
+ $chart_definition = $this->populateOptions($element, $chart_definition);
+ $chart_definition = $this->populateAxes($element, $chart_definition);
+ $chart_definition = $this->populateData($element, $chart_definition);
+
+ if (!empty($element['#height']) || !empty($element['#width'])) {
+ $element['#attributes']['style'] = 'height:' . $element['#height'] . $element['#height_units'] . ';width:' . $element['#width'] . $element['#width_units'] . ';';
+ }
+
+ // Remove machine names from series. Highcharts series must be an array.
+ $series = !empty($chart_definition['series']) ? array_values($chart_definition['series']) : [];
+ unset($chart_definition['series']);
+
+ // Trim out empty options (excluding "series" for efficiency).
+ ChartElement::trimArray($chart_definition);
+
+ // Put back the data.
+ $chart_definition['series'] = $series;
+
+ if (!isset($element['#id'])) {
+ $element['#id'] = Html::getUniqueId('highchart-render');
+ }
+
+ $element['#attached']['library'][] = 'charts_highcharts/highcharts';
+ if (!empty($this->configuration['exporting_library'])) {
+ $element['#attached']['library'][] = 'charts_highcharts/highcharts_exporting';
+ }
+ if (!empty($this->configuration['texture_library'])) {
+ $element['#attached']['library'][] = 'charts_highcharts/texture';
+ }
+ $element['#attributes']['class'][] = 'charts-highchart';
+ $element['#chart_definition'] = $chart_definition;
+ // Show a form on the front-end so users can change chart colors.
+ if (!empty($element['#color_changer']) && empty($element['#in_preview_mode'])) {
+ $form_state = new FormState();
+ $form_state->set('chart_series', $series);
+ $form_state->set('chart_id', $element['#id']);
+ $form_state->set('chart_type', $chart_definition['chart']['type']);
+ if (!empty($chart_definition['yAxis'])) {
+ $form_state->set('y_axis', $chart_definition['yAxis']);
+ }
+ $element['#attached']['library'][] = 'charts_highcharts/color_changer';
+ $element['#content_suffix']['color_changer'] = $this->formBuilder->buildForm(ColorChanger::class, $form_state);
+ }
+
+ // Setting global options.
+ $element['#attached']['drupalSettings']['charts']['highcharts']['global_options'] = $this->processedGlobalOptions();
+
+ return $element;
+ }
+
+ /**
+ * Defines the default global options.
+ */
+ public static function defaultGlobalOptions() {
+ return [
+ 'lang' => [
+ 'download_CSV' => 'Download CSV',
+ 'download_JPEG' => 'Download JPEG image',
+ 'download_PDF' => 'Download PDF document',
+ 'download_PNG' => 'Download PNG image ',
+ 'download_SVG' => 'Download SVG vector image',
+ 'download_XLS' => 'Download XLS',
+ 'exit_fullscreen' => 'Exit from full screen',
+ 'hide_data' => 'Hide data table',
+ 'loading' => 'Loading...',
+ 'main_breadcrumb' => 'Main',
+ 'thousands_sep' => ' ',
+ 'decimal_point' => '.',
+ 'no_data' => 'No data to display',
+ 'print_chart' => 'Print chart',
+ 'reset_zoom' => 'Reset zoom',
+ 'reset_zoom_title' => 'Reset zoom level 1:1',
+ 'view_data' => 'View data table',
+ 'view_fullscreen' => 'View in full screen',
+ 'context_button_title' => 'Chart context menu',
+ 'drill_up_text' => 'Back to {series.name}',
+ 'invalid_date' => 'Invalid date',
+ 'months' => [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+ ],
+ 'short_months' => [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sept',
+ 'Oct',
+ 'Nov',
+ 'Dec',
+ ],
+ 'weekdays' => [
+ 'Sunday',
+ 'Monday',
+ 'Tuesday',
+ 'Wednesday',
+ 'Thursday',
+ 'Friday',
+ 'Saturday',
+ ],
+ 'short_weekdays' => [
+ 'Sun',
+ 'Mon',
+ 'Tue',
+ 'Wed',
+ 'Thurs',
+ 'Frid',
+ 'Sat',
+ ],
+ 'export_data' => [
+ 'annotation_header' => 'Annotations',
+ 'category_datetime_header' => 'DateTime',
+ 'category_header' => 'Category',
+ ],
+ 'numeric_symbols' => [
+ 'k',
+ 'M',
+ 'G',
+ 'T',
+ 'P',
+ 'E',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Populate options.
+ *
+ * @param array $element
+ * The element.
+ * @param array $chart_definition
+ * The chart definition.
+ *
+ * @return array
+ * Return the chart definition.
+ */
+ protected function populateOptions(array $element, array $chart_definition) {
+ $chart_type = $this->getType($element['#chart_type']);
+ $chart_definition['chart']['type'] = $chart_type;
+ $chart_definition['chart']['backgroundColor'] = $element['#background'];
+ $chart_definition['chart']['polar'] = $element['#polar'] ?? NULL;
+ $chart_definition['chart']['options3d']['enabled'] = $element['#three_dimensional'] ?? NULL;
+ $chart_definition['credits']['enabled'] = FALSE;
+ $chart_definition['title']['text'] = $element['#title'] ?? '';
+ $chart_definition['title']['style']['color'] = $element['#title_color'];
+ $title_position = $element['#title_position'] ?? '';
+ $chart_definition['title']['align'] = in_array($title_position, [
+ 'left',
+ 'center',
+ 'right',
+ ]) ? $title_position : NULL;
+ $chart_definition['title']['verticalAlign'] = in_array($title_position, [
+ 'top',
+ 'bottom',
+ ]) ? $title_position : NULL;
+ $chart_definition['title']['y'] = $title_position === 'in' ? 24 : NULL;
+ $chart_definition['subtitle']['text'] = $element['#subtitle'] ?? '';
+ $chart_definition['subtitle']['align'] = in_array($title_position, [
+ 'left',
+ 'center',
+ 'right',
+ ]) ? $title_position : NULL;
+ $chart_definition['subtitle']['verticalAlign'] = in_array($title_position, [
+ 'top',
+ 'bottom',
+ ]) ? $title_position : NULL;
+ if (!empty($element['#subtitle']) && !empty($title_position)) {
+ if ($title_position === 'in') {
+ $chart_definition['subtitle']['y'] = 42;
+ }
+ elseif ($title_position === 'bottom') {
+ $chart_definition['subtitle']['y'] = 26;
+ }
+ }
+ $chart_definition['colors'] = $element['#colors'];
+ $chart_definition['tooltip']['enabled'] = (bool) $element['#tooltips'];
+ $chart_definition['tooltip']['useHTML'] = (bool) $element['#tooltips_use_html'];
+ $chart_definition['plotOptions']['series']['stacking'] = $element['#stacking'] ?? '';
+ $chart_definition['plotOptions']['series']['dataLabels']['enabled'] = (bool) $element['#data_labels'];
+ $chart_definition['plotOptions']['series']['marker']['enabled'] = (bool) $element['#data_markers'];
+ if ($element['#chart_type'] === 'gauge') {
+ $chart_definition['yAxis']['plotBands'][] = [
+ 'from' => $element['#gauge']['red_from'],
+ 'to' => $element['#gauge']['red_to'],
+ 'color' => 'red',
+ ];
+ $chart_definition['yAxis']['plotBands'][] = [
+ 'from' => $element['#gauge']['yellow_from'],
+ 'to' => $element['#gauge']['yellow_to'],
+ 'color' => 'yellow',
+ ];
+ $chart_definition['yAxis']['plotBands'][] = [
+ 'from' => $element['#gauge']['green_from'],
+ 'to' => $element['#gauge']['green_to'],
+ 'color' => 'green',
+ ];
+ $chart_definition['yAxis']['min'] = (int) $element['#gauge']['min'];
+ $chart_definition['yAxis']['max'] = (int) $element['#gauge']['max'];
+ }
+
+ // These changes are for consistency with Google. Perhaps too specific?
+ if ($element['#chart_type'] === 'pie') {
+ $chart_definition['plotOptions']['pie']['dataLabels']['distance'] = -30;
+ $chart_definition['plotOptions']['pie']['dataLabels']['color'] = 'white';
+ $chart_definition['plotOptions']['pie']['dataLabels']['format'] = '{percentage:.1f}%';
+
+ $chart_definition['tooltip']['pointFormat'] = '{point.y} ({point.percentage:.1f}%)Please install at least one module that implements a chart library plugin.
'), + ], + ]; + return $element; + } + + if (!empty($element['#library']) && isset($library_options[$element['#library']])) { + $element['library'] = [ + '#type' => 'value', + '#value' => $element['#library'], + ]; + $selected_library = $element['#library']; + } + else { + $element['library'] = [ + '#title' => new TranslatableMarkup('Charting library'), + '#type' => 'select', + '#options' => $library_options, + '#default_value' => $options['library'], + '#required' => $required, + '#access' => count($library_options) > 0, + '#attributes' => ['class' => ['chart-library-select']], + '#ajax' => [ + 'callback' => [get_called_class(), 'ajaxRefresh'], + 'wrapper' => $wrapper_id, + ], + ]; + $selected_library = $options['library'] ?: array_key_first($library_options); + } + + // Making sure that the selected chart type is part of chart type options + // associated with the library. + $chart_type_options = self::getChartTypes($selected_library); + $selected_chart_type = $options['type'] ?? ''; + if (!isset($chart_type_options[$selected_chart_type])) { + $selected_chart_type = NULL; + } + $element['type'] = [ + '#title' => new TranslatableMarkup('Chart type'), + '#type' => 'radios', + '#default_value' => $selected_chart_type, + '#options' => $chart_type_options, + '#access' => !empty($chart_type_options), + '#required' => $required, + '#attributes' => [ + 'class' => [ + 'chart-type-radios', + 'container-inline', + ], + ], + ]; + + if (!$chart_type_options) { + $error_message = $selected_library !== 'site_default' ? + new TranslatableMarkup('The selected chart library does not have valid chart type options. Please select another charting library or install a module that have chart type options.
') : + new TranslatableMarkup('The site default charting library has not been set yet, or it does not support chart type options. Please ensure that you have correctly configured the chart module.
'); + $element['no_chart_type_plugin_error'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['messages', 'messages--error']], + 'content' => ['#markup' => $error_message], + ]; + return $element; + } + + if (!empty($element['#series'])) { + $element = self::processSeriesForm($element, $options, $form_state); + } + + $element['display'] = [ + '#title' => new TranslatableMarkup('Display'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ]; + + $element['display']['title'] = [ + '#title' => new TranslatableMarkup('Chart title'), + '#type' => 'textfield', + '#default_value' => $options['display']['title'], + ]; + + $element['display']['subtitle'] = [ + '#title' => new TranslatableMarkup('Chart subtitle'), + '#type' => 'textfield', + '#default_value' => $options['display']['subtitle'] ?? '', + '#description' => new TranslatableMarkup('Not all charting libraries support this option. Disabled until there is a title value.'), + '#states' => [ + 'disabled' => [ + ':input[name="' . $element['#name'] . '[display][title]"]' => ['value' => ''], + ], + ], + ]; + + $element['xaxis'] = [ + '#title' => new TranslatableMarkup('Horizontal axis'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#attributes' => ['class' => ['chart-xaxis']], + ]; + + $element['yaxis'] = [ + '#title' => new TranslatableMarkup('Vertical axis'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#attributes' => ['class' => ['chart-yaxis']], + ]; + + if ($used_in === 'view_form') { + $element = self::processViewForm($element, $options, $complete_form, $form_state); + } + elseif ($used_in === 'config_form') { + $element = self::processConfigForm($element, $options, $form_state); + } + + $element['display']['title_position'] = [ + '#title' => new TranslatableMarkup('Title position'), + '#type' => 'select', + '#options' => [ + '' => new TranslatableMarkup('None'), + 'out' => new TranslatableMarkup('Outside'), + 'in' => new TranslatableMarkup('Inside'), + 'top' => new TranslatableMarkup('Top'), + 'right' => new TranslatableMarkup('Right'), + 'bottom' => new TranslatableMarkup('Bottom'), + 'left' => new TranslatableMarkup('Left'), + ], + '#description' => new TranslatableMarkup('Not all of these will apply to your selected library.'), + '#default_value' => $options['display']['title_position'] ?? '', + ]; + + $element['display']['tooltips'] = [ + '#title' => new TranslatableMarkup('Enable tooltips'), + '#type' => 'checkbox', + '#description' => new TranslatableMarkup('Show data details on mouse over? Note: unavailable for print or on mobile devices.'), + '#default_value' => !empty($options['display']['tooltips']), + ]; + + $element['display']['data_labels'] = [ + '#title' => new TranslatableMarkup('Enable data labels'), + '#type' => 'checkbox', + '#default_value' => !empty($options['display']['data_labels']), + '#description' => new TranslatableMarkup('Show data details as labels on chart? Note: recommended for print or on mobile devices.'), + ]; + + $element['display']['data_markers'] = [ + '#title' => new TranslatableMarkup('Enable data markers'), + '#type' => 'checkbox', + '#default_value' => !empty($options['display']['data_markers']), + '#description' => new TranslatableMarkup('Show data markers (points) on line charts?'), + ]; + + $element['display']['legend_position'] = [ + '#title' => new TranslatableMarkup('Legend position'), + '#type' => 'select', + '#options' => [ + '' => new TranslatableMarkup('None'), + 'top' => new TranslatableMarkup('Top'), + 'right' => new TranslatableMarkup('Right'), + 'bottom' => new TranslatableMarkup('Bottom'), + 'left' => new TranslatableMarkup('Left'), + ], + '#default_value' => $options['display']['legend_position'] ?? '', + ]; + + $element['display']['background'] = [ + '#title' => new TranslatableMarkup('Background color'), + '#type' => 'textfield', + '#size' => 10, + '#maxlength' => 7, + '#attributes' => ['placeholder' => new TranslatableMarkup('transparent')], + '#description' => new TranslatableMarkup('Leave blank for a transparent background.'), + '#default_value' => $options['display']['background'] ?? '', + ]; + + $element['display']['three_dimensional'] = [ + '#title' => new TranslatableMarkup('Make chart three-dimensional (3D)'), + '#type' => 'checkbox', + '#default_value' => $options['display']['three_dimensional'] ?? FALSE, + '#attributes' => [ + 'class' => [ + 'chart-type-checkbox', + 'container-inline', + ], + ], + ]; + + $element['display']['polar'] = [ + '#title' => new TranslatableMarkup('Transform cartesian charts into the polar coordinate system'), + '#type' => 'checkbox', + '#default_value' => $options['display']['polar'] ?? FALSE, + '#attributes' => [ + 'class' => [ + 'chart-type-checkbox', + 'container-inline', + ], + ], + ]; + + $element['display']['dimensions'] = [ + '#title' => new TranslatableMarkup('Dimensions'), + '#theme_wrappers' => ['form_element'], + '#description' => new TranslatableMarkup('If dimensions are left empty, the chart will fill its containing element.'), + ]; + + $element['display']['dimensions']['width'] = [ + '#type' => 'number', + '#attributes' => [ + 'placeholder' => new TranslatableMarkup('auto'), + ], + '#min' => 0, + '#max' => 9999, + '#default_value' => $options['display']['dimensions']['width'] ?? '', + '#size' => 8, + '#theme_wrappers' => [], + ]; + $element['display']['dimensions']['width_units'] = [ + '#type' => 'textfield', + '#attributes' => [ + 'placeholder' => new TranslatableMarkup('%'), + ], + '#default_value' => $options['display']['dimensions']['width_units'] ?? '', + '#suffix' => ' x ', + '#size' => 2, + '#theme_wrappers' => [], + ]; + + $element['display']['dimensions']['height'] = [ + '#type' => 'number', + '#attributes' => [ + 'placeholder' => new TranslatableMarkup('auto'), + ], + '#min' => 0, + '#max' => 9999, + '#default_value' => $options['display']['dimensions']['height'] ?? '', + '#size' => 8, + '#theme_wrappers' => [], + ]; + $element['display']['dimensions']['height_units'] = [ + '#type' => 'textfield', + '#attributes' => [ + 'placeholder' => new TranslatableMarkup('px'), + ], + '#default_value' => $options['display']['dimensions']['height_units'] ?? '', + '#size' => 2, + '#theme_wrappers' => [], + ]; + + $element['xaxis']['title'] = [ + '#title' => new TranslatableMarkup('Custom title'), + '#type' => 'textfield', + '#default_value' => $options['xaxis']['title'] ?? '', + ]; + + $element['xaxis']['labels_rotation'] = [ + '#title' => new TranslatableMarkup('Labels rotation'), + '#type' => 'select', + '#options' => [ + 0 => new TranslatableMarkup('0°'), + 15 => new TranslatableMarkup('15°'), + 30 => new TranslatableMarkup('30°'), + 45 => new TranslatableMarkup('45°'), + 60 => new TranslatableMarkup('60°'), + 90 => new TranslatableMarkup('90°'), + ], + // This is only shown on non-inverted charts. + '#attributes' => ['class' => ['axis-inverted-hide']], + '#default_value' => $options['xaxis']['labels_rotation'] ?? '', + ]; + + $element['yaxis']['title'] = [ + '#title' => new TranslatableMarkup('Custom title'), + '#type' => 'textfield', + '#default_value' => $options['yaxis']['title'] ?? '', + ]; + + $element['yaxis']['min_max_label'] = [ + '#type' => 'html_tag', + '#tag' => 'label', + '#value' => new TranslatableMarkup('Value range'), + ]; + $element['yaxis']['min'] = [ + '#type' => 'number', + '#title' => new TranslatableMarkup('Value range minimum'), + '#title_display' => 'invisible', + '#attributes' => [ + 'placeholder' => new TranslatableMarkup('Minimum'), + ], + '#max' => 999999999, + '#default_value' => $options['yaxis']['min'] ?? '', + '#size' => 12, + '#suffix' => ' ', + ]; + $element['yaxis']['max'] = [ + '#type' => 'number', + '#attributes' => [ + 'placeholder' => new TranslatableMarkup('Maximum'), + ], + '#max' => 999999999, + '#default_value' => $options['yaxis']['max'] ?? '', + '#size' => 12, + ]; + + $element['yaxis']['prefix'] = [ + '#title' => new TranslatableMarkup('Value prefix'), + '#type' => 'textfield', + '#default_value' => $options['yaxis']['prefix'] ?? '', + '#size' => 12, + ]; + $element['yaxis']['suffix'] = [ + '#title' => new TranslatableMarkup('Value suffix'), + '#type' => 'textfield', + '#default_value' => $options['yaxis']['suffix'] ?? '', + '#size' => 12, + ]; + + $element['yaxis']['decimal_count'] = [ + '#title' => new TranslatableMarkup('Decimal count'), + '#type' => 'number', + '#attributes' => [ + 'placeholder' => new TranslatableMarkup('auto'), + ], + '#min' => 0, + '#max' => 20, + '#default_value' => $options['yaxis']['decimal_count'] ?? '', + '#size' => 5, + '#description' => new TranslatableMarkup('Enforce a certain number of decimal-place digits in displayed values.'), + ]; + + $element['yaxis']['labels_rotation'] = [ + '#title' => new TranslatableMarkup('Labels rotation'), + '#type' => 'select', + '#options' => [ + 0 => new TranslatableMarkup('0°'), + 15 => new TranslatableMarkup('15°'), + 30 => new TranslatableMarkup('30°'), + 45 => new TranslatableMarkup('45°'), + 60 => new TranslatableMarkup('60°'), + 90 => new TranslatableMarkup('90°'), + ], + // This is only shown on inverted charts. + '#attributes' => ['class' => ['axis-inverted-show']], + '#default_value' => $options['yaxis']['labels_rotation'] ?? '', + ]; + + // Adding basic form yaxis other fields. + if ($used_in === 'basic_form') { + $element = self::processBasicForm($element, $options); + } + if ($used_in === 'view_form' || $used_in === 'basic_form') { + $element['display']['color_changer'] = [ + '#title' => new TranslatableMarkup('Enable color changer widget'), + '#type' => 'checkbox', + '#description' => new TranslatableMarkup('Display a widget that enables users to switch the chart colors.'), + '#default_value' => !empty($options['display']['color_changer']), + ]; + } + + // Settings for gauges. + $element['display']['gauge'] = [ + '#title' => new TranslatableMarkup('Gauge settings'), + '#type' => 'fieldset', + '#collapsible' => FALSE, + '#states' => [ + 'visible' => [ + ':input[class*=chart-type-radios]' => ['value' => 'gauge'], + ], + ], + 'max' => [ + '#title' => new TranslatableMarkup('Gauge maximum value'), + '#type' => 'number', + '#default_value' => $options['display']['gauge']['max'] ?? '', + ], + 'min' => [ + '#title' => new TranslatableMarkup('Gauge minimum value'), + '#type' => 'number', + '#default_value' => $options['display']['gauge']['min'] ?? '', + ], + 'green_from' => [ + '#title' => new TranslatableMarkup('Green minimum value'), + '#type' => 'number', + '#default_value' => $options['display']['gauge']['green_from'] ?? '', + ], + 'green_to' => [ + '#title' => new TranslatableMarkup('Green maximum value'), + '#type' => 'number', + '#default_value' => $options['display']['gauge']['green_to'] ?? '', + ], + 'yellow_from' => [ + '#title' => new TranslatableMarkup('Yellow minimum value'), + '#type' => 'number', + '#default_value' => $options['display']['gauge']['yellow_from'] ?? '', + ], + 'yellow_to' => [ + '#title' => new TranslatableMarkup('Yellow maximum value'), + '#type' => 'number', + '#default_value' => $options['display']['gauge']['yellow_to'] ?? '', + ], + 'red_from' => [ + '#title' => new TranslatableMarkup('Red minimum value'), + '#type' => 'number', + '#default_value' => $options['display']['gauge']['red_from'] ?? '', + ], + 'red_to' => [ + '#title' => new TranslatableMarkup('Red maximum value'), + '#type' => 'number', + '#default_value' => $options['display']['gauge']['red_to'] ?? '', + ], + ]; + + if ($used_in === 'config_form' && $selected_library) { + $element = self::buildLibraryConfigurationForm($element, $form_state, $selected_library); + } + + return $element; + } + + /** + * Validates the chart library plugin configuration. + * + * @param array $element + * The chart base settings element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @param array $complete_form + * The complete form. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public static function validateLibraryPluginConfiguration(array &$element, FormStateInterface $form_state, array &$complete_form) { + $used_in = $element['#used_in']; + if ($used_in === 'config_form') { + $settings = $form_state->getValue($element['#parents']); + // Adding validate callback for the chart library settings. + if (!empty($settings['library'])) { + /** @var \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager */ + $plugin_manager = \Drupal::service('plugin.manager.charts'); + /** @var \Drupal\charts\Plugin\chart\Library\ChartInterface $plugin */ + $plugin = $plugin_manager->createInstance($settings['library']); + $plugin->validateConfigurationForm($element['library_config'], $form_state); + } + } + } + + /** + * Submits the plugin configuration. + * + * @param array $element + * An associative array containing the properties of the element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public static function submitLibraryPluginConfiguration(array &$element, FormStateInterface $form_state) { + $used_in = $element['#used_in']; + if ($used_in === 'config_form') { + $settings = $form_state->getValue($element['#parents']); + if (!empty($settings['library'])) { + /** @var \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager */ + $plugin_manager = \Drupal::service('plugin.manager.charts'); + /** @var \Drupal\charts\Plugin\chart\Library\ChartInterface $plugin */ + $plugin = $plugin_manager->createInstance($settings['library']); + $plugin->submitConfigurationForm($element['library_config'], $form_state); + $form_state->setValueForElement($element['library_config'], $plugin->getConfiguration()); + } + } + } + + /** + * Ajax callback. + */ + public static function ajaxRefresh(array $form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + return NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -1)); + } + + /** + * Get the libraries. + * + * @return array + * The library options. + */ + public static function getLibraries() { + // Using plugins to get the available installed libraries. + $plugin_manager = \Drupal::service('plugin.manager.charts'); + $plugin_definitions = $plugin_manager->getDefinitions(); + $library_options = []; + + foreach ($plugin_definitions as $plugin_definition) { + $library_options[$plugin_definition['id']] = $plugin_definition['name']; + } + + return $library_options; + } + + /** + * Gets chart types by chart library. + * + * @param string $library_plugin_id + * The library. + * + * @return array + * The type options. + */ + public static function getChartTypes(string $library_plugin_id = ''): array { + if ($library_plugin_id === 'site_default') { + $library_plugin_id = static::getConfiguredSiteDefaultLibraryId(); + } + if (!$library_plugin_id) { + \Drupal::logger('charts')->error('Update your custom code to pass the library in getChartTypes() or install a module that implement a charting library plugin.'); + return []; + } + + $chart_type_plugin_manager = \Drupal::service('plugin.manager.charts_type'); + $chart_plugin_manager = \Drupal::service('plugin.manager.charts'); + + /** @var \Drupal\charts\Plugin\chart\Library\ChartInterface $chart_plugin */ + $chart_plugin = $chart_plugin_manager->createInstance($library_plugin_id, []); + $chart_type_plugin_definitions = $chart_type_plugin_manager->getDefinitions(); + $types_options = []; + + foreach ($chart_type_plugin_definitions as $plugin_definition) { + $chart_type_id = $plugin_definition['id']; + if ($chart_plugin->isSupportedChartType($chart_type_id)) { + $types_options[$chart_type_id] = $plugin_definition['label']; + } + } + return $types_options; + } + + /** + * Gets the configured site default library chart plugin id. + * + * @return string + * The plugin id or an empty string if nothing has been configured. + */ + public static function getConfiguredSiteDefaultLibraryId(): string { + $chart_config = \Drupal::config('charts.settings'); + return $chart_config->get('charts_default_settings.library') ?? ''; + } + + /** + * Attaches the #charts_library_settings_element_submit functionality. + * + * @param array $element + * The form element being processed. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + * + * @return array + * The processed form element. + */ + public static function attachLibraryElementSubmit(array $element, FormStateInterface $form_state, array &$complete_form) { + if (isset($complete_form['#charts_library_settings_element_submit_attached'])) { + return $element; + } + // The #validate callbacks of the complete form run last. + // That allows executeElementSubmitHandlers() to be completely certain that + // the form has passed validation before proceeding. + $complete_form['#validate'][] = [ + static::class, + 'executeLibraryElementSubmitHandlers', + ]; + $complete_form['#charts_library_settings_element_submit_attached'] = TRUE; + + return $element; + } + + /** + * Submits elements by calling their #charts_library_settings_element_submit. + * + * Callbacks. + * + * This approach was took from the commerce module to work around the fact. + * that drupal core doesn't have an element_submit property. + * + * @param array &$form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public static function executeLibraryElementSubmitHandlers(array &$form, FormStateInterface $form_state) { + if (!$form_state->isSubmitted() || $form_state->hasAnyErrors()) { + // The form wasn't submitted (#ajax in progress) or failed validation. + return; + } + $triggering_element = $form_state->getTriggeringElement(); + $button_type = $triggering_element['#button_type'] ?? ''; + if ($button_type != 'primary' && count($form_state->getButtons()) > 1) { + // The form was submitted, but not via the primary button, which + // indicates that it will probably be rebuilt. + return; + } + + self::doExecuteLibrarySubmitHandlers($form, $form_state); + } + + /** + * Calls the #charts_library_settings_element_submit callbacks recursively. + * + * @param array &$element + * The current element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public static function doExecuteLibrarySubmitHandlers(array &$element, FormStateInterface $form_state) { + // Recurse through all children. + foreach (Element::children($element) as $key) { + if (!empty($element[$key])) { + static::executeLibraryElementSubmitHandlers($element[$key], $form_state); + } + } + + // If there are callbacks on this level, run them. + if (!empty($element['#charts_library_settings_element_submit'])) { + foreach ($element['#charts_library_settings_element_submit'] as $callback) { + call_user_func_array($callback, [&$element, &$form_state]); + } + } + } + + /** + * Process form. + * + * @param array $element + * The current element. + * @param array $options + * Options. + * + * @return array + * The element. + */ + private static function processBasicForm(array $element, array $options) { + $element_name = $element['#name']; + + $element['display']['stacking'] = [ + '#title' => new TranslatableMarkup('Enable stacking'), + '#type' => 'checkbox', + '#description' => new TranslatableMarkup('Enable stacking for this chart. Will stack based on the selected label field.'), + '#default_value' => !empty($options['display']['stacking']), + ]; + + $element['yaxis']['inherit'] = [ + '#title' => new TranslatableMarkup('Add a secondary y-axis'), + '#type' => 'checkbox', + '#default_value' => $options['yaxis']['inherit'] ?? FALSE, + '#description' => new TranslatableMarkup('Only one additional (secondary) y-axis can be created.'), + ]; + + $element['yaxis']['secondary'] = [ + '#title' => new TranslatableMarkup('Secondary vertical axis'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#attributes' => ['class' => ['chart-yaxis']], + '#states' => [ + 'visible' => [ + ':input[name="' . $element_name . '[yaxis][inherit]"]' => ['checked' => TRUE], + ], + ], + ]; + + $element['yaxis']['secondary']['title'] = [ + '#title' => new TranslatableMarkup('Custom title'), + '#type' => 'textfield', + '#default_value' => $options['yaxis']['secondary']['title'] ?? '', + ]; + + $element['yaxis']['secondary']['min_max_label'] = [ + '#type' => 'html_tag', + '#tag' => 'label', + '#value' => new TranslatableMarkup('Value range'), + ]; + $element['yaxis']['secondary']['min'] = [ + '#type' => 'number', + '#title' => new TranslatableMarkup('Value range minimum'), + '#title_display' => 'invisible', + '#attributes' => [ + 'placeholder' => new TranslatableMarkup('Minimum'), + ], + '#max' => 999999999, + '#size' => 12, + '#suffix' => ' ', + '#default_value' => $options['yaxis']['secondary']['min'] ?? '', + ]; + $element['yaxis']['secondary']['max'] = [ + '#type' => 'number', + '#title' => new TranslatableMarkup('Value range maximum'), + '#title_display' => 'invisible', + '#attributes' => [ + 'placeholder' => new TranslatableMarkup('Maximum'), + ], + '#max' => 999999999, + '#size' => 12, + '#default_value' => $options['yaxis']['secondary']['max'] ?? '', + ]; + + $element['yaxis']['secondary']['prefix'] = [ + '#title' => new TranslatableMarkup('Value prefix'), + '#type' => 'textfield', + '#size' => 12, + '#default_value' => $options['yaxis']['secondary']['prefix'] ?? '', + ]; + + $element['yaxis']['secondary']['suffix'] = [ + '#title' => new TranslatableMarkup('Value suffix'), + '#type' => 'textfield', + '#size' => 12, + '#default_value' => $options['yaxis']['secondary']['suffix'] ?? '', + ]; + + $element['yaxis']['secondary']['decimal_count'] = [ + '#title' => new TranslatableMarkup('Decimal count'), + '#type' => 'number', + '#attributes' => [ + 'placeholder' => new TranslatableMarkup('auto'), + ], + '#max' => 20, + '#min' => 0, + '#size' => 5, + '#description' => new TranslatableMarkup('Enforce a certain number of decimal-place digits in displayed values.'), + '#default_value' => $options['yaxis']['secondary']['decimal_count'] ?? '', + ]; + + $element['yaxis']['secondary']['labels_rotation'] = [ + '#title' => new TranslatableMarkup('Labels rotation'), + '#type' => 'select', + '#options' => [ + 0 => new TranslatableMarkup('0°'), + 15 => new TranslatableMarkup('15°'), + 30 => new TranslatableMarkup('30°'), + 45 => new TranslatableMarkup('45°'), + 60 => new TranslatableMarkup('60°'), + 90 => new TranslatableMarkup('90°'), + ], + // This is only shown on inverted charts. + '#attributes' => ['class' => ['axis-inverted-show']], + '#default_value' => $options['yaxis']['secondary']['labels_rotation'] ?? '', + ]; + + return $element; + } + + /** + * Process view form. + * + * @param array $element + * The current element. + * @param array $options + * The options. + * @param array $complete_form + * The complete form where the element is attached to. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The element. + */ + private static function processViewForm(array $element, array $options, array &$complete_form, FormStateInterface $form_state) { + if (!is_array($element['#field_options'])) { + throw new \InvalidArgumentException('The chart_base_settings element need valid field options when used as view form.'); + } + + $element['display']['#weight'] = 2; + $element['xaxis']['#weight'] = 2; + $element['yaxis']['#weight'] = 2; + + $element_name = $element['#name']; + $field_options = $element['#field_options']; + $first_field = $field_options ? key($field_options) : ''; + + $element['fields'] = [ + '#title' => new TranslatableMarkup('Charts fields'), + '#type' => 'fieldset', + '#weight' => 1, + ]; + + // Add a views-specific chart option to allow advanced rendering. + // $element['fields']['allow_advanced_rendering'] = [ + // '#type' => 'checkbox', + // '#title' => new TranslatableMarkup('Allow advanced rendering'), + // '#description' => new TranslatableMarkup('Allow views field rewriting. + // etc. for label and data fields. This can break charts if you rewrite + // the field to a value the charting library cannot handle + // - e.g. passing a string value into a numeric data column.'), + // '#default_value' => isset($options['fields']. + // ['allow_advanced_rendering']) ? $options['fields'] + // ['allow_advanced_rendering'] : NULL,]. + $element['fields']['label'] = [ + '#type' => 'radios', + '#title' => new TranslatableMarkup('Label field'), + '#options' => $field_options + ['' => new TranslatableMarkup('No label field')], + '#default_value' => $options['fields']['label'] ?? $first_field, + ]; + + // Enable stacking. + $element['fields']['stacking'] = [ + '#type' => 'checkbox', + '#title' => new TranslatableMarkup('Stacking'), + '#description' => new TranslatableMarkup('Enable stacking for this chart. Will stack based on the selected label field.'), + '#default_value' => !empty($options['fields']['stacking']) ? $options['fields']['stacking'] : FALSE, + ]; + + $element['fields']['data_providers'] = [ + '#type' => 'table', + '#header' => [ + new TranslatableMarkup('Field Name'), + new TranslatableMarkup('Provides Data'), + new TranslatableMarkup('Color'), + new TranslatableMarkup('Weight'), + ], + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'view-chart-fields-data-providers-order-weight', + ], + ], + ]; + // Make the weight list always reflect the current number of values. + // Taken from WidgetBase::formMultipleElements(). + $max_weight = count($field_options); + + $field_options_count = 0; + $default_colors = $options['display']['colors'] ?? []; + foreach ($field_options as $field_name => $field_label) { + $field_option_element = &$element['fields']['data_providers'][$field_name]; + $default_value = $options['fields']['data_providers'][$field_name] ?? []; + $default_weight = $default_value['weight'] ?? $max_weight; + $default_color = $default_value['color'] ?? ''; + if (!$default_color) { + $default_color = $default_colors[$field_options_count] ?? '#000000'; + } + + $field_option_element['#attributes']['class'][] = 'draggable'; + // Field option label. + $field_option_element['label'] = [ + '#markup' => new TranslatableMarkup('@label', [ + '@label' => $field_label, + ]), + ]; + + $field_option_element['enabled'] = [ + '#type' => 'checkbox', + '#title' => new TranslatableMarkup('Provides data'), + '#title_display' => 'invisible', + '#default_value' => !empty($default_value['enabled']), + '#states' => [ + 'disabled' => [ + ':input[name="' . $element_name . '[fields][label]"]' => ['value' => $field_name], + ], + ], + ]; + + $field_option_element['color'] = [ + '#type' => 'textfield', + '#title' => new TranslatableMarkup('Color'), + '#attributes' => [ + 'TYPE' => 'color', + 'style' => 'min-width:50px;', + ], + '#title_display' => 'invisible', + '#size' => 10, + '#maxlength' => 7, + '#default_value' => $default_color, + ]; + + $field_option_element['weight'] = [ + '#type' => 'weight', + '#title' => new TranslatableMarkup('Weight'), + '#title_display' => 'invisible', + '#delta' => $max_weight, + '#default_value' => $default_weight, + '#attributes' => [ + 'class' => ['view-chart-fields-data-providers-order-weight'], + ], + ]; + + $field_option_element['#weight'] = $default_weight; + $field_options_count++; + } + + $element['fields']['entity_grouping'] = [ + '#title' => new TranslatableMarkup('Entity grouping settings'), + '#type' => 'fieldset', + '#collapsed' => TRUE, + '#collapsible' => TRUE, + '#weight' => 2, + '#description' => new TranslatableMarkup('When grouping by an entity reference field, you can set the colors by entities or by a color field attached to the entity type bundle in question.', [ + '@href' => 'https://drupal.org/project/color_field', + ]), + '#description_display' => 'before', + ]; + $module_handler = \Drupal::moduleHandler(); + if (empty($element['#view_charts_style_plugin'])) { + return $element; + } + + // Entity grouping settings. + // Try to get it from $form_state. + $style_options_values = $form_state->getValue(['style_options'], []); + $grouping_field = $style_options_values['grouping'][0] ?? []; + $grouping_field_name = $grouping_field['field'] ?? ''; + $grouping_field_element = $complete_form['options']['style_options']['grouping'][0]['field'] ?? []; + $triggering_element = $form_state->getTriggeringElement(); + if (!$grouping_field_name && !$triggering_element && $grouping_field_element) { + // Get the grouping field name from default value property. + $grouping_field_name = $grouping_field_element['#default_value'] ?? ''; + } + if (!$grouping_field_name) { + return $element; + } + + // Check which selection method the user want to go with. + /** @var \Drupal\charts\Plugin\views\style\ChartsPluginStyleChart $style_plugin */ + $style_plugin = $element['#view_charts_style_plugin']; + $view = $style_plugin->view; + $selection_method_wrapper_id = $view->id() . '--' . $view->current_display . '--' . $style_plugin->getPluginId() . '--fields--entity-grouping--color-selection-method'; + $selected_method = $options['fields']['entity_grouping']['color_selection_method'] ?? 'by_entities_on_entity_reference'; + $element['fields']['entity_grouping']['color_selection_method'] = [ + '#type' => 'radios', + '#title' => new TranslatableMarkup('Color selection method'), + '#required' => TRUE, + '#options' => [ + 'by_entities_on_entity_reference' => new TranslatableMarkup('Set color by entities on entity reference'), + ], + '#default_value' => $selected_method, + '#ajax' => [ + 'wrapper' => $selection_method_wrapper_id, + 'callback' => [ + get_called_class(), + 'groupingChartSettingsSelectedMethodAjaxCallback', + ], + ], + '#limit_validation_errors' => [], + ]; + if ($module_handler->moduleExists('color_field')) { + $element['fields']['entity_grouping']['color_selection_method']['#options']['by_field_on_referenced_entity'] = new TranslatableMarkup('Set color based on a color field on entity reference'); + } + $element['fields']['entity_grouping']['selected_method'] = [ + '#type' => 'container', + '#prefix' => 'Please fill up the required value below then update the preview.
'), + ]; + } + + if (!empty($element['#default_value'])) { + $options = NestedArray::mergeDeep($options, $element['#default_value']); + } + $element['series'] = [ + '#type' => 'chart_data_collector_table', + '#initial_rows' => $element['#table_initial_rows'] ?? 5, + '#initial_columns' => $element['#table_initial_columns'] ?? 2, + '#table_drag' => FALSE, + '#default_value' => $options['series'] ?? [], + '#default_colors' => $options['display']['colors'] ?? [], + ]; + + return $element; + } + + /** + * Preview refresh Ajax callback. + */ + public static function ajaxRefreshPreview(array $form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + $element = NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -2)); + return $element['preview']; + } + + /** + * Submit callback for the preview button. + */ + public static function chartPreviewSubmit(array $form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + $element_parents = array_slice($triggering_element['#parents'], 0, -2); + + // Getting the current element state. + $element_state = ChartDataCollectorTable::getElementState($element_parents, $form_state); + $element_state[$triggering_element['#open_preview_elt_state_key']] = TRUE; + // Updating form state storage. + ChartDataCollectorTable::setElementState($element_parents, $form_state, $element_state); + $form_state->setRebuild(); + } + + /** + * Grouping chart settings ajax callback. + * + * @param array $form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The render array of the chart settings. + */ + public static function groupingChartSettingsSelectedMethodAjaxCallback(array $form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + $entity_grouping_element = NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -2)); + return $entity_grouping_element['selected_method']; + } + + /** + * Submit callback for adding a new default display color item value. + */ + public static function addDefaultDisplayColorItemSubmit(array $form, FormStateInterface $form_state): void { + $triggering_element = $form_state->getTriggeringElement(); + $element_parents = array_slice($triggering_element['#array_parents'], 0, -5); + $element_state = static::getElementState($element_parents, $form_state); + + // Adding a new default color color item index. + $element_state[$triggering_element['#state_color_indexes_key']][] = '_new'; + // Updating form state storage. + static::setElementState($element_parents, $form_state, $element_state); + $form_state->setRebuild(); + } + + /** + * Submit callback for removing a value. + */ + public static function removeDefaultDisplayColorItemSubmit(array $form, FormStateInterface $form_state): void { + $triggering_element = $form_state->getTriggeringElement(); + $element_parents = array_slice($triggering_element['#array_parents'], 0, -4); + $element_state = static::getElementState($element_parents, $form_state); + $color_index = $triggering_element['#color_index']; + + // Removing the color item. + unset($element_state[$triggering_element['#state_color_indexes_key']][$color_index]); + // Updating form state storage. + static::setElementState($element_parents, $form_state, $element_state); + $form_state->setRebuild(); + } + + /** + * Ajax callback for then default display colors operations. + */ + public static function defaultDisplayColorItemsAjax(array $form, FormStateInterface $form_state): array { + $triggering_element = $form_state->getTriggeringElement(); + $slice_length = $triggering_element['#operation'] === 'add' ? -4 : -3; + $element = NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, $slice_length)); + return $element['colors']; + } + + /** + * Builds the chart library configuration form into the settings. + * + * @param array $element + * The element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @param string $library + * The chart library. + * + * @return array + * The configuration subform. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + private static function buildLibraryConfigurationForm(array $element, FormStateInterface $form_state, string $library) { + $plugin_configuration = $element['#value']['library_config'] ?? []; + // Using plugins to get the available installed libraries. + /** @var \Drupal\charts\ChartManager $plugin_manager */ + $plugin_manager = \Drupal::service('plugin.manager.charts'); + /** @var \Drupal\charts\Plugin\chart\Library\ChartInterface $plugin */ + $plugin = $plugin_manager->createInstance($library, $plugin_configuration); + $element['library_config'] = [ + '#type' => 'details', + '#title' => new TranslatableMarkup('@library settings', [ + '@library' => $plugin->getPluginDefinition()['name'], + ]), + '#group' => $element['display']['#group'], + '#weight' => 4, + ]; + $element['library_config'] = $plugin->buildConfigurationForm($element['library_config'], $form_state); + return $element; + } + + /** + * Helper method to retrieve the referenced entity type id. + * + * @param array $field_info + * The field info. + * @param string $selection_method + * The selection method. + * + * @return string + * The entity type id. + */ + private static function getReferenceEntityTypeId(array $field_info, string $selection_method): string { + if (!$selection_method || empty($field_info['type']) || $field_info['type'] !== 'entity_reference_label' || empty($field_info['field'])) { + return ''; + } + $table = Views::viewsData()->get($field_info['table']); + $field_machine_name = $field_info['field']; + return $table[$field_machine_name]['relationship']['entity type'] ?? ''; + } + + /** + * Helper method to build a color selection element by entities. + * + * @param array $metadata + * The metadata information to use to retrieve the color options. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * + * @return array + * The color selection table or a markup message when the provided metadata + * are incomplete. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + private static function buildColorsSelectionSubFormByEntities(array $metadata, EntityTypeManagerInterface $entity_type_manager): array { + $empty_entity_colors = new TranslatableMarkup("No grouping by an entity reference field was detected or the selected field didnt have any entity or color field attached."); + $colors = [ + '#markup' => '' . $empty_entity_colors . '
', + ]; + if (empty($metadata['grouping_field_name']) || empty($metadata['entity_type_id'])) { + return $colors; + } + + // Identifying the vocabulary this field could belong to. + $field_config_storage = $entity_type_manager->getStorage('field_config'); + /** @var \Drupal\field\FieldConfigInterface[] $grouping_field_configs */ + $grouping_field_configs = $field_config_storage->loadByProperties(['field_name' => $metadata['grouping_field_name']]); + $entity_type_id = $metadata['entity_type_id']; + $bundle_ids = []; + foreach ($grouping_field_configs as $grouping_field_config) { + $field_settings = $grouping_field_config->getSettings(); + if (empty($field_settings['target_type']) || $field_settings['target_type'] !== $entity_type_id) { + continue; + } + $target_bundles = $field_settings['handler_settings']['target_bundles'] ?? []; + foreach ($target_bundles as $bundle_id) { + $bundle_ids[$bundle_id] = $bundle_id; + } + } + + // Load the entities by bundle. + $entity_storage = $entity_type_manager->getStorage($entity_type_id); + $entity_type = $entity_type_manager->getDefinition($entity_type_id); + + $colors = [ + '#type' => 'table', + '#empty' => $empty_entity_colors, + '#header' => [ + new TranslatableMarkup('Entity label'), + new TranslatableMarkup('Bundle'), + new TranslatableMarkup('Color'), + ], + ]; + $bundle_key = $entity_type->getKey('bundle'); + $query = $entity_storage->getQuery()->accessCheck(FALSE) + ->condition($bundle_key, $bundle_ids, 'IN'); + + if ($entity_type instanceof EntityTypeInterface) { + $published_key = $entity_type->getKey('published'); + $query->condition($published_key, TRUE); + } + + // For now limiting to 150 entities. + $entity_ids = $query->range(0, 150) + ->execute(); + $has_uuid_key = $entity_type->hasKey('uuid'); + foreach ($entity_ids as $id) { + $entity = $entity_storage->load($id); + $color_id_key = $has_uuid_key ? $entity->get('uuid')->value : $entity->id(); + $field_option_element = &$colors[$color_id_key]; + $default_value = $metadata['colors'][$color_id_key]['color'] ?? '#000000'; + + $label = $entity->label(); + $field_option_element['label'] = [ + '#markup' => new TranslatableMarkup('@label', [ + '@label' => $label, + ]), + ]; + $field_option_element['bundle'] = [ + '#markup' => new TranslatableMarkup('@bundle', [ + '@bundle' => $entity->bundle(), + ]), + ]; + $field_option_element['color'] = [ + '#type' => 'textfield', + '#title' => new TranslatableMarkup('@label', ['@label' => $label]), + '#title_display' => 'invisible', + '#attributes' => [ + 'TYPE' => 'color', + 'style' => 'min-width:50px;', + ], + '#size' => 10, + '#maxlength' => 7, + '#default_value' => $default_value, + ]; + } + + return $colors; + } + + /** + * Method to build a color selection element by field on referenced entity. + * + * @param array $metadata + * The metadata information to use to retrieve the color options. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * + * @return array + * The select element or the status message when no color options was + * found. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + private static function buildColorsSelectionSubFormByFieldOnReferencedEntity(array $metadata, EntityTypeManagerInterface $entity_type_manager): array { + // Identifying the vocabulary this field could belong to. + $field_config_storage = $entity_type_manager->getStorage('field_config'); + /** @var \Drupal\field\FieldConfigInterface[] $grouping_field_configs */ + $grouping_field_configs = $field_config_storage->loadByProperties(['field_name' => $metadata['grouping_field_name']]); + $entity_type_id = $metadata['entity_type_id']; + $processed_bundle_ids = []; + /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */ + $entity_field_manager = \Drupal::service('entity_field.manager'); + $color_field_options = []; + foreach ($grouping_field_configs as $grouping_field_config) { + $field_settings = $grouping_field_config->getSettings(); + if (empty($field_settings['target_type']) || $field_settings['target_type'] !== $entity_type_id) { + continue; + } + + $target_bundles = $field_settings['handler_settings']['target_bundles'] ?? []; + foreach ($target_bundles as $bundle_id) { + if (in_array($bundle_id, $processed_bundle_ids)) { + continue; + } + + // Extract fields from bundle fields. + foreach ($entity_field_manager->getFieldDefinitions($entity_type_id, $bundle_id) as $field_name => $field_definition) { + if ($field_definition->getType() !== 'color_field_type' || !empty($color_field_options[$field_name])) { + continue; + } + $color_field_options[$field_name] = $field_definition->getLabel(); + } + $processed_bundle_ids[] = $bundle_id; + } + } + + if (!$color_field_options) { + $empty_entity_field_name = new TranslatableMarkup("You can't set the color using the selected method because the referenced entity doesn't have any color field type configured."); + return [ + '#theme' => 'status_messages', + '#message_list' => ['warning' => [$empty_entity_field_name]], + '#status_headings' => [ + 'warning' => new TranslatableMarkup('Warning message'), + ], + ]; + } + + return [ + '#type' => 'select', + '#title' => new TranslatableMarkup('Color field'), + '#description' => new TranslatableMarkup('The color field on which we should get the color data from.'), + '#options' => $color_field_options, + '#default_value' => $metadata['color_field_name'] ?? '', + '#required' => TRUE, + ]; + } + +} diff --git a/web/modules/charts/src/Element/Chart.php b/web/modules/charts/src/Element/Chart.php new file mode 100755 index 00000000..36f86a81 --- /dev/null +++ b/web/modules/charts/src/Element/Chart.php @@ -0,0 +1,449 @@ +configFactory = $config_factory; + $this->chartsManager = $chart_manager; + $this->chartsTypeManager = $type_manager; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('config.factory'), + $container->get('plugin.manager.charts'), + $container->get('plugin.manager.charts_type'), + $container->get('module_handler') + ); + } + + /** + * {@inheritdoc} + */ + public function getInfo() { + return [ + '#chart_type' => NULL, + '#chart_library' => NULL, + '#chart_id' => NULL, + '#title' => NULL, + '#title_color' => '#000', + '#title_font_weight' => 'normal', + '#title_font_style' => 'normal', + '#title_font_size' => 14, + '#title_position' => 'out', + '#subtitle' => NULL, + '#colors' => ChartBase::getDefaultColors(), + '#font' => 'Arial', + '#font_size' => 12, + '#gauge' => [], + '#background' => 'transparent', + '#stacking' => NULL, + '#color_changer' => FALSE, + '#pre_render' => [ + [$this, 'preRender'], + ], + '#tooltips' => TRUE, + '#tooltips_use_html' => FALSE, + '#data_labels' => FALSE, + '#data_markers' => FALSE, + '#legend' => TRUE, + '#legend_title' => '', + '#legend_title_font_weight' => 'bold', + '#legend_title_font_style' => 'normal', + '#legend_title_font_size' => '', + '#legend_position' => 'right', + '#legend_font_weight' => 'normal', + '#legend_font_style' => 'normal', + '#legend_font_size' => NULL, + '#width' => NULL, + '#height' => NULL, + '#attributes' => [], + '#chart_definition' => [], + '#raw_options' => [], + '#content_prefix' => [], + '#content_suffix' => [], + ]; + } + + /** + * Main #pre_render callback to expand a chart element. + * + * @param array $element + * The element. + * + * @return array + * The chart element. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function preRender(array $element) { + /** @var \Drupal\charts\Plugin\chart\Library\ChartInterface[] $definitions */ + $definitions = $this->chartsManager->getDefinitions(); + + if (!$definitions) { + $element['#type'] = 'markup'; + $element['#markup'] = $this->t('No charting library found. Enable a charting module such as Google Charts or Highcharts.'); + return $element; + } + + // Ensure there's an x and y axis to provide defaults. + $type_name = $element['#chart_type']; + $type = $this->chartsTypeManager->getDefinition($type_name); + if ($type && $type['axis'] === ChartInterface::DUAL_AXIS) { + $children_types = []; + foreach (Element::children($element) as $key) { + $children_types[] = $element[$key]['#type']; + } + + if (!in_array('chart_xaxis', $children_types)) { + $element['xaxis'] = ['#type' => 'chart_xaxis']; + } + if (!in_array('chart_yaxis', $children_types)) { + $element['yaxis'] = ['#type' => 'chart_yaxis']; + } + } + + self::castElementIntergerValues($element); + + // Generic theme function assuming it will be suitable for most chart types. + $element['#theme'] = 'charts_chart'; + // Allow the chart to be altered - @TODO use event dispatching if needed. + $alter_hooks = ['chart']; + $chart_id = $element['#chart_id']; + if ($chart_id) { + $alter_hooks[] = 'chart_' . $element['#chart_id']; + } + $this->moduleHandler->alter($alter_hooks, $element, $chart_id); + + // Include the library-specific render callback via their plugin manager. + // Use the first charting library if the requested library is not available. + $library = $element['#chart_library'] ?? ''; + $library = $this->getLibrary($library); + + $element['#chart_library'] = $library; + $charts_settings = $this->configFactory->get('charts.settings'); + $plugin_configuration = $charts_settings->get('charts_default_settings.library_config') ?? []; + /** @var \Drupal\charts\Plugin\chart\Library\ChartInterface $plugin */ + $plugin = $this->chartsManager->createInstance($library, $plugin_configuration); + if (!$plugin->isSupportedChartType($type_name)) { + // Chart type not supported by the library. + throw new \LogicException(sprintf('The provided chart type "%s" is not supported by "%s" chart plugin library.', $type_name, $plugin->getChartName())); + } + + $element = $plugin->preRender($element); + + if (!empty($element['#chart_definition'])) { + $chart_definition = $element['#chart_definition']; + unset($element['#chart_definition']); + + // Allow the chart definition to be altered - @TODO use event dispatching + // if needed. + $alter_hooks = ['chart_definition']; + if ($element['#chart_id']) { + $alter_hooks[] = 'chart_definition_' . $chart_id; + } + $this->moduleHandler->alter($alter_hooks, $chart_definition, $element, $chart_id); + // Set the element #chart_json property as a data-attribute. + $element['#attributes']['data-chart'] = Json::encode($chart_definition); + } + + $element['#cache']['tags'][] = 'config:charts.settings'; + + return $element; + } + + /** + * Casts recursively integer values. + * + * @param array $element + * The element. + */ + public static function castElementIntergerValues(array &$element) { + // Cast options to integers to avoid redundant library fixing problems. + $integer_options = [ + // Chart options. + '#title_font_size', + '#font_size', + '#legend_title_font_size', + '#legend_font_size', + '#width', + '#height', + // Axis options. + '#title_font_size', + '#labels_font_size', + '#labels_rotation', + '#max', + '#min', + // Data options. + '#decimal_count', + ]; + + foreach ($element as $property_name => $value) { + if (is_array($element[$property_name])) { + self::castElementIntergerValues($element[$property_name]); + } + elseif ($property_name && in_array($property_name, $integer_options)) { + $element[$property_name] = (is_null($element[$property_name]) || strlen($element[$property_name]) === 0) ? NULL : (int) $element[$property_name]; + } + } + } + + /** + * Trims out, recursively, empty options that aren't used. + * + * @param array $array + * The array to trim. + */ + public static function trimArray(array &$array) { + foreach ($array as $key => &$value) { + if (is_array($value)) { + self::trimArray($value); + } + elseif (is_null($value) || (is_array($value) && count($value) === 0)) { + unset($array[$key]); + } + } + } + + /** + * Get the library. + * + * @param string $library + * The library. + * + * @return string + * The library. + */ + private function getLibrary($library) { + $definitions = $this->chartsManager->getDefinitions(); + if (!$library || $library === 'site_default') { + $charts_settings = $this->configFactory->get('charts.settings'); + $default_settings_library = $charts_settings->get('charts_default_settings.library'); + $library = !empty($default_settings_library) ? $default_settings_library : key($definitions); + } + elseif (!isset($definitions[$library])) { + $library = key($definitions); + } + + return $library; + } + + /** + * Build the element. + * + * @param array $settings + * The settings. + * @param string $chart_id + * The chart id. + * + * @return array + * The element. + */ + public static function buildElement(array $settings, string $chart_id): array { + $type = $settings['type']; + $single_axis = in_array($type, ['pie', 'donut']); + $display_colors = $settings['display']['colors'] ?? []; + + $element = [ + '#type' => 'chart', + '#chart_type' => $type, + '#chart_library' => $settings['library'], + '#title' => $settings['display']['title'], + '#title_position' => $settings['display']['title_position'], + '#subtitle' => $settings['display']['subtitle'] ?? '', + '#tooltips' => $settings['display']['tooltips'] ?? [], + '#data_labels' => $settings['display']['data_labels'] ?? FALSE, + '#data_markers' => $settings['display']['data_markers'] ?? FALSE, + '#colors' => $display_colors, + '#background' => $settings['display']['background'] ?? 'transparent', + '#three_dimensional' => $settings['display']['three_dimensional'] ?? FALSE, + '#polar' => $settings['display']['polar'] ?? FALSE, + '#legend' => !empty($settings['display']['legend_position']), + '#legend_position' => $settings['display']['legend_position'] ?? '', + '#gauge' => $settings['display']['gauge'] ?? [], + '#stacking' => !empty($settings['display']['stacking']) ?? NULL, + '#width' => $settings['display']['dimensions']['width'], + '#height' => $settings['display']['dimensions']['height'], + '#width_units' => $settings['display']['dimensions']['width_units'], + '#height_units' => $settings['display']['dimensions']['height_units'], + '#color_changer' => $settings['display']['color_changer'] ?? FALSE, + ]; + + if (empty($settings['series'])) { + return $element; + } + + $table = $settings['series']; + // Extracting the categories. + $categories = ChartDataCollectorTable::getCategoriesFromCollectedTable($table, $type); + // Extracting the rest of the data. + $series_data = ChartDataCollectorTable::getSeriesFromCollectedTable($table, $type); + + $element['xaxis'] = [ + '#type' => 'chart_xaxis', + '#labels' => $single_axis ? '' : $categories['data'], + '#title' => $settings['xaxis']['title'] ?? FALSE, + '#labels_rotation' => $settings['xaxis']['labels_rotation'], + ]; + + if (empty($series_data)) { + return $element; + } + + $element['yaxis'] = [ + '#type' => 'chart_yaxis', + '#title' => $settings['yaxis']['title'] ?? '', + '#labels_rotation' => $settings['yaxis']['labels_rotation'], + '#max' => $settings['yaxis']['max'], + '#min' => $settings['yaxis']['min'], + '#prefix' => $settings['yaxis']['prefix'], + '#suffix' => $settings['yaxis']['suffix'], + '#decimal_count' => $settings['yaxis']['decimal_count'], + ]; + + // Create a secondary axis if needed. + $series_count = count($series_data); + if (!empty($settings['yaxis']['inherit']) && $series_count === 2) { + $element['secondary_yaxis'] = [ + '#type' => 'chart_yaxis', + '#title' => $settings['yaxis']['secondary']['title'] ?? '', + '#labels_rotation' => $settings['yaxis']['secondary']['labels_rotation'], + '#max' => $settings['yaxis']['secondary']['max'], + '#min' => $settings['yaxis']['secondary']['min'], + '#prefix' => $settings['yaxis']['secondary']['prefix'], + '#suffix' => $settings['yaxis']['secondary']['suffix'], + '#decimal_count' => $settings['yaxis']['secondary']['decimal_count'], + '#opposite' => TRUE, + ]; + } + + // Overriding element colors for pie and donut chart types when the + // settings display colors is empty. + $overrides_element_colors = !$display_colors && ($type === 'pie' || $type === 'donut'); + $series_key = $chart_id . '__series'; + if ($single_axis) { + $new_series = []; + $labels = []; + foreach ($series_data as $datum) { + $new_series[] = $datum['data'][0][1]; + $labels[] = $datum['name']; + if ($overrides_element_colors) { + $element['#colors'][] = $datum['color']; + } + } + $element['xaxis']['#labels'] = $labels; + // @todo Address more than one series. + $element[$series_key] = [ + '#type' => 'chart_data', + '#data' => $new_series, + '#title' => $series_data[0]['title'] ?? '', + ]; + } + else { + $series_counter = 0; + foreach ($series_data as $data_index => $data) { + $key = $series_key . '__' . $data_index; + $element[$key] = [ + '#type' => 'chart_data', + '#data' => $data['data'], + '#title' => $data['name'], + ]; + if (!empty($data['color'])) { + $element[$key]['#color'] = $data['color']; + } + if (isset($element['yaxis'])) { + $element[$key]['#prefix'] = $settings['yaxis']['prefix']; + $element[$key]['#suffix'] = $settings['yaxis']['suffix']; + $element[$key]['#decimal_count'] = $settings['yaxis']['decimal_count']; + } + if (isset($element['secondary_yaxis']) && $series_counter === 1) { + $element[$key]['#target_axis'] = 'secondary_yaxis'; + $element[$key]['#prefix'] = $settings['yaxis']['secondary']['prefix']; + $element[$key]['#suffix'] = $settings['yaxis']['secondary']['suffix']; + $element[$key]['#decimal_count'] = $settings['yaxis']['secondary']['decimal_count']; + } + $series_counter++; + } + } + + return $element; + } + +} diff --git a/web/modules/charts/src/Element/ChartAxisBase.php b/web/modules/charts/src/Element/ChartAxisBase.php new file mode 100755 index 00000000..05e50651 --- /dev/null +++ b/web/modules/charts/src/Element/ChartAxisBase.php @@ -0,0 +1,49 @@ + '', + '#title' => '', + '#title_color' => '#000', + // Options: normal, bold. + '#title_font_weight' => 'normal', + // Options: normal, italic. + '#title_font_style' => 'normal', + '#title_font_size' => 12, + // CSS value for font size, e.g. 1em or 12px. + '#labels' => NULL, + '#labels_color' => '#000', + // Options: normal, bold. + '#labels_font_weight' => 'normal', + // Options: normal, italic. + '#labels_font_style' => 'normal', + // CSS value for font size, e.g. 1em or 12px. + '#labels_font_size' => NULL, + // Integer rotation value, e.g. 30, -60 or 90. + '#labels_rotation' => NULL, + '#grid_line_color' => '#ccc', + '#base_line_color' => '#ccc', + '#minor_grid_line_color' => '#e0e0e0', + // Integer max value on this axis. + '#max' => NULL, + // Integer minimum value on this axis. + '#min' => NULL, + // Display axis on opposite normal side. + '#opposite' => FALSE, + ]; + } + +} diff --git a/web/modules/charts/src/Element/ChartData.php b/web/modules/charts/src/Element/ChartData.php new file mode 100755 index 00000000..a2d95506 --- /dev/null +++ b/web/modules/charts/src/Element/ChartData.php @@ -0,0 +1,44 @@ + NULL, + '#labels' => NULL, + '#data' => [], + '#color' => NULL, + '#show_in_legend' => TRUE, + // Show inline labels next to the data. + '#show_labels' => FALSE, + // If building multicharts. The chart type, e.g. pie. + '#chart_type' => NULL, + // Line chart only. + '#line_width' => 1, + // Line chart only. Size in pixels, e.g. 1, 5. + '#marker_radius' => 3, + // If using multiple axes, key for the matching y axis. + '#target_axis' => NULL, + // Formatting options. + // The number of digits after the decimal separator. e.g. 2. + '#decimal_count' => NULL, + // A custom date format, e.g. %Y-%m-%d. + '#date_format' => NULL, + '#prefix' => NULL, + '#suffix' => NULL, + ]; + } + +} diff --git a/web/modules/charts/src/Element/ChartDataCollectorTable.php b/web/modules/charts/src/Element/ChartDataCollectorTable.php new file mode 100755 index 00000000..fdedd7ab --- /dev/null +++ b/web/modules/charts/src/Element/ChartDataCollectorTable.php @@ -0,0 +1,909 @@ + TRUE, + // Either to enable csv import. + '#import_csv' => TRUE, + '#import_csv_separator' => ',', + // The initial number of rows to generate. + '#initial_rows' => 5, + // The initial number of columns to generate. + '#initial_columns' => 2, + // The optional element the table should be wrapped in. + '#table_wrapper' => '', + '#table_wrapper_attributes' => [], + '#table_attributes' => [], + // Allows to toggle on/off drupal tabledrag functionality. + '#table_drag' => TRUE, + '#default_colors' => [], + '#process' => [ + [$class, 'processDataCollectorTable'], + ], + '#element_validate' => [ + [$class, 'validateDataCollectorTable'], + ], + '#theme_wrappers' => ['container'], + ]; + } + + /** + * Processes the element to render a table to collect a data for the chart. + * + * @param array $element + * The element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @param array $complete_form + * The complete form. + * + * @return array + * The processed element. + */ + public static function processDataCollectorTable(array &$element, FormStateInterface $form_state, array &$complete_form) { + $parents = $element['#parents']; + $id_prefix = implode('-', $parents); + $wrapper_id = Html::getUniqueId($id_prefix . '-ajax-wrapper'); + $value = $element['#value']; + $required = !empty($element['#required']); + $user_input = $form_state->getUserInput(); + + $element_state = self::getElementState($parents, $form_state); + // Getting columns and rows count. + if (empty($element_state['data_collector_table']) || empty($element_state['table_categories_identifier'])) { + $identifier_value = $value['table_categories_identifier'] ?? self::FIRST_COLUMN; + $element_state['table_categories_identifier'] = $identifier_value; + $element_state['data_collector_table'] = $value['data_collector_table'] ?? []; + $element_state['data_collector_table'] = $element_state['data_collector_table'] ?: self::initializeEmptyTable($element, $identifier_value); + self::setElementState($parents, $form_state, $element_state); + } + else { + // This is hack to make ajax call return the proper identifier. + $element_state['table_categories_identifier'] = $value['table_categories_identifier']; + } + + // Enforce tree. + $element = [ + '#tree' => TRUE, + '#prefix' => '