added simple math and charts modules

This commit is contained in:
2024-12-10 23:53:23 +01:00
parent 62a5f3c97c
commit 688ff42752
196 changed files with 20285 additions and 8 deletions

View File

@@ -0,0 +1,97 @@
################
# DrupalCI GitLabCI template
#
# Gitlab-ci.yml to replicate DrupalCI testing for Contrib
#
# With thanks to:
# * The GitLab Acceleration Initiative participants
# * DrupalSpoons
################
################
# Guidelines
#
# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification. It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
#
# However, you can modify this template if you have additional needs for your project.
################
################
# Includes
#
# Additional configuration can be provided through includes.
# One advantage of include files is that if they are updated upstream, the changes affect all pipelines using that include.
#
# Includes can be overridden by re-declaring anything provided in an include, here in gitlab-ci.yml
# https://docs.gitlab.com/ee/ci/yaml/includes.html#override-included-configuration-values
################
include:
################
# DrupalCI includes:
# As long as you include this, any future includes added by the Drupal Association will be accessible to your pipelines automatically.
# View these include files at https://git.drupalcode.org/project/gitlab_templates/
################
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
################
# Pipeline configuration variables
#
# These are the variables provided to the Run Pipeline form that a user may want to override.
#
# Docs at https://git.drupalcode.org/project/gitlab_templates/-/blob/1.0.x/includes/include.drupalci.variables.yml
################
# variables:
# SKIP_ESLINT: '1'
###################################################################################
#
# *
# /(
# ((((,
# /(((((((
# ((((((((((*
# ,(((((((((((((((
# ,(((((((((((((((((((
# ((((((((((((((((((((((((*
# *(((((((((((((((((((((((((((((
# ((((((((((((((((((((((((((((((((((*
# *(((((((((((((((((( .((((((((((((((((((
# ((((((((((((((((((. /(((((((((((((((((*
# /((((((((((((((((( .(((((((((((((((((,
# ,(((((((((((((((((( ((((((((((((((((((
# .(((((((((((((((((((( .(((((((((((((((((
# ((((((((((((((((((((((( ((((((((((((((((/
# (((((((((((((((((((((((((((/ ,(((((((((((((((*
# .((((((((((((((/ /(((((((((((((. ,(((((((((((((((
# *(((((((((((((( ,(((((((((((((/ *((((((((((((((.
# ((((((((((((((, /(((((((((((((. ((((((((((((((,
# (((((((((((((/ ,(((((((((((((* ,(((((((((((((,
# *((((((((((((( .((((((((((((((( ,(((((((((((((
# ((((((((((((/ /((((((((((((((((((. ,((((((((((((/
# ((((((((((((( *(((((((((((((((((((((((* *((((((((((((
# ((((((((((((( ,(((((((((((((..((((((((((((( *((((((((((((
# ((((((((((((, /((((((((((((* /((((((((((((/ ((((((((((((
# ((((((((((((( /((((((((((((/ (((((((((((((* ((((((((((((
# (((((((((((((/ /(((((((((((( ,((((((((((((, *((((((((((((
# (((((((((((((( *(((((((((((/ *((((((((((((. ((((((((((((/
# *((((((((((((((((((((((((((, /(((((((((((((((((((((((((
# ((((((((((((((((((((((((( ((((((((((((((((((((((((,
# .(((((((((((((((((((((((/ ,(((((((((((((((((((((((
# ((((((((((((((((((((((/ ,(((((((((((((((((((((/
# *((((((((((((((((((((( (((((((((((((((((((((,
# ,(((((((((((((((((((((, ((((((((((((((((((((/
# ,(((((((((((((((((((((* /((((((((((((((((((((
# ((((((((((((((((((((((, ,/((((((((((((((((((((,
# ,(((((((((((((((((((((((((((((((((((((((((((((((((((
# .(((((((((((((((((((((((((((((((((((((((((((((
# .((((((((((((((((((((((((((((((((((((,.
# .,(((((((((((((((((((((((((.
#
###################################################################################

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

242
web/modules/charts/README.md Executable file
View File

@@ -0,0 +1,242 @@
# Charts
Transform **data** into **information**. The Charts module for Drupal enables
you to build dynamic charts without needing to write a line of code. If you are
comfortable coding, you can use the Charts API to generate or update charts.
Instructions for creating charts are included below.
There are many charting libraries (also sometimes referred to as providers)
that you can find online. Each has their own benefits, drawbacks, and APIs. To
use one of them **without** this module, you would need to 1) be familiar with
the charting library's API, 2) add its JavaScript file(s) in your page, and 3)
write code for every chart you want to include. That's a lot of effort, and it
requires knowledge and access that many Drupal site builders will not have.
Charts is designed so that anyone who wants a chart on their site can have a
chart -- and be happy with it.
## How Does The Charts Module Work (Technically)?
The Charts module takes data and configuration from your site, and with the help
of a submodule, organizes it into a JSON object that is saved as an attribute
on an HTML element on your page; the JSON objected is handed to your selected
charting library, which renders it into a chart.
## Charting Providers / Libraries
Out of the box, you will be able to use 5 charting solutions (referred to as
"providers" or "libraries". Each of them has particular advantages and
disadvantages.
* Billboard.js: This library is a fork of the C3 charting library. It has
additional features, such as radar charts.
* C3.js: This library is a D3-based reusable chart library that makes it easy
to generate D3-based charts.
* Chart.js: This is a simple yet flexible JavaScript charting for designers &
developers.
* Google Charts: This library generates interactive charts using SVG and VML.
* Highcharts: This library is one of the premier solutions for generating
charts. Although it is very powerful and aesthetically pleasing with smooth
animations, it requires a commercial license. It's free for non-commercial
use. See http://www.highcharts.com
## Chart Types
Each charting library has its own set of chart types. The Charts module
focuses on chart types that are available in *most* of the libraries.
To avoid a confusing user interface, the Charts module no longer includes
chart types that are not supported by the selected library in the Chart
Type selection. For example, if you select the Chart.js library, you will
not see the "Gauge" chart type.
## Installing Libraries
All the Charts submodules default to using a content delivery network (CDN) to
pull in the necessary JavaScript files unless a local copy is present, or you
have disabled the CDN option on the "Charts configuration" page
(`/admin/config/content/charts`).
Below are a couple outlines for adding the libraries locally.
### Using Composer
1. Ensure that you have the `composer/installers` package installed.
2. Ensure you have an installer-paths for the drupal-library type. Such as in
the composer.json of
https://github.com/drupal-composer/drupal-project/blob/9.x/composer.json
3. In each submodule, there is a README.md file that has code to add to your
site's composer.json file.
4. After updating your project's composer.json file, run the 'composer require'
command specified in each submodule's README.md file. For example, if you are
using Google Charts, step three would mean adding:
```json
{
"type": "package",
"package": {
"name": "google/charts",
"version": "45",
"type": "drupal-library",
"extra": {
"installer-name": "google_charts"
},
"dist": {
"url": "https://www.gstatic.com/charts/loader.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
}
```
to your project's composer.json, and then running:
`composer require --prefer-dist google/charts:45`
### Using Composer and wikimedia/composer-merge-plugin:
1. Ensure that you have the `wikimedia/composer-merge-plugin` package
installed.
2. Ensure that you have the `oomphinc/composer-installers-extender` package
installed.
3. Add an "installer-types" section in the "extra" of your project
composer.json file, make sure you have "npm-asset" listed. For example:
```json
"installer-types": [
"npm-asset"
],
```
5. In the "installer-paths" section in the "extra" of your project
composer.json file, ensure you have the types drupal-library and npm-asset.
For example:
```json
"web/libraries/{$name}": [
"type:drupal-library",
"type:npm-asset"
],
```
6. Add a "merge-plugin" section in the "extra" of your project composer.json
file, so that the composer.json file of the submodules you want is included.
For example:
```json
"merge-plugin": {
"include": [
"web/modules/contrib/charts/modules/charts_highcharts/composer.json"
]
},
```
7. Run the `composer require` specified in the submodule's README.md file
## Creating Charts in the UI
This module provides a configuration page at /admin/config/content/charts.
You may set site-wide defaults on this page (for example: set the default color
scheme). It also has library-specific configuration available after saving a
default library.
There are three options for creating a chart within the UI:
1. Using Views
2. Using a Chart Field
3. Using a Chart Block
### Creating Charts with Views
1. Create a new view:
Visit admin/structure/views/add and select the display format of "Chart" for
your new page or block.
2. Add a label field:
Under the "Fields" section, add a field you would like to be used as labels
along one axis of the chart (or slices of the pie). If you are visualizing
an Event content type, you might select `title`.
3. Add a data field or fields:
Now add a second field that will be used to determine the data values. If you
are visualizing an Event content type, this field might be
`field_number_attendees`. The label you give this field will be used in the
chart's legend to represent this series. Do this again for each different
quantity you would like to chart. Note that some chart types (e.g. "pie")
only support a single data column.
4. Configure the chart display:
Click on the "Settings" link in the "Format" section to configure the chart.
Select your chart library, type, the label field, and the data provider.
There are many other options to customize your chart. Some options may not
be available to all chart types and will be adjusted based on the type
selected.
5. Save your view.
As described in the steps above - if you have a field representing a number, you
do not necessarily need to aggregate your view. If the content you want to
visualize does not include a numeric field, you may need to aggregate your view
and use the `count` function to generate a number for your data field.
**Tip:** You may find it easier to start with a "Table" display and convert it
to a chart display after setting up the data. It can be easier to visualize
what the result of the chart will be if it's been laid out in a table first.
### Creating Combo Charts
If you are creating your chart in Views and want to a chart that includes
multiple chart types (such as columns and a line - aka a "combo" chart), add a
new display of type "Chart attachment". Use the middle column of this display
to attach the "Chart attachment" to a parent display (this is required). In
this section, you can also instruct the "Chart attachment" to inherit exposed
or contextual filters and if it should use the primary y-axis or a secondary
y-axis. You still need to configure the "Settings" in the "Format" section.
## Creating Charts in the UI with a Chart Field
If you want a chart in your entity (e.g. "node"), but don't need its data to be
dynamic (like Views could generate), you can use a Chart field. Add this field
to your bundle (e.g. "content type") via the "Manage Fields" tab - like you
would any other field. When you add a new entity, you can select the charting
library, chart type, and other configurations. You can add data using a CSV or
by manually adding it into the table input. The chart will show on your entity
after saving.
## Creating Charts in the UI with a Chart Block
If you are using Layout Builder or want to place a chart on several pages, and
the chart does not need dynamic data (like Views could generate), you can use a
Chart Block. When you add a Chart Block, you can select the charting library,
chart type, and other configurations. You can add data using a CSV or by
manually adding it into the table input.
## Creating Charts Using the API
Charts are elements that can be rendered using a Drupal render array. Please
refer to the `charts.api.php` file and also look at the included submodule,
`charts_api_examples` (specifically the controller, which has many examples).
## Debugging Your Charts
There is a checkbox in the "Advanced" tab of the config form
(/admin/config/content/charts/advanced) that enabled debugging. Checking this
box will enable a collapsible "details" element that includes the JSON
object generated by the Charts module - this can often be added directly
into a code playground provided by the charting library. If you are
implementing your own Charts submodule, you can add a line like this into
your charts_MYMODULE.js file (it may need modification):
```
if (element.nextElementSibling &&
element.nextElementSibling.hasAttribute('data-charts-debug-container')) {
element.nextElementSibling.querySelector('code').innerText = JSON.stringify(
config,
null,
' '
);
}
```
## Support
For bug reports and feature requests please use the Drupal.org issue tracker:
https://www.drupal.org/project/issues/charts.
We welcome your support in improving code documentation, tests, and providing
example use-cases not addressed by the existing module.
If you are interested in creating your own submodule for a library not
currently supported, please contact
[@andileco](https://www.drupal.org/u/andileco).

View File

@@ -0,0 +1,174 @@
<?php
/**
* @file
* Documentation on hooks provided by the Charts module.
*
* Charts module provides 4 element types that can be used to construct a chart.
* In its most basic form, a chart may be created by specifying an element
* with the #type property "chart". However, for enough information to
* accurately data to an audience, you need a little more. Below is a very
* basic example of a column chart.
*
* @code
* $chart = [
* '#type' => 'chart',
* '#chart_type' => 'column',
* 'series' => [
* '#type' => 'chart_data',
* '#title' => t('Responses'),
* '#data' => [60, 40],
* ],
* 'xaxis' => [
* '#type' => 'chart_xaxis',
* '#labels' => [t('Yes'), t('No')],
* ]
* ];
* @endcode
*
* On charts that have multiple axes, you'll need to add individual sub-elements
* for each series of data. If you desire, you may also customize the axes by
* providing an axis element too.
*
* @code
* $chart = [
* '#type' => 'chart',
* '#chart_type' => 'column',
* ];
* $chart['series'] = [
* '#type' => 'chart_data',
* '#title' => t('Responses'),
* '#data' => [10, 20, 30],
* ];
* $chart['xaxis'] = [
* '#type' => 'chart_xaxis',
* '#title' => t('Month'),
* '#labels' => [t('Jan'), t('Feb'), t('Mar')],
* ];
* @endcode
*
* Once you have generated a chart object, you can run
* \Drupal::service('renderer')->render() on it to turn it into HTML:
*
* @code
* $output = \Drupal::service('renderer')->render($chart);
* @endcode
*
* There are many, many properties available for the four chart types (chart,
* chart_data, chart_xaxis, and chart_yaxis). For a full list, see the
* charts_element_info() function.
*
* This module also includes a number of examples for reference in the
* charts_api_example module, and displayed at /charts/example/display.
*
* @see Drupal\charts\Element folder
*/
/**
* Alter an individual chart before it is printed.
*
* @param array $element
* The chart renderable. Passed in by reference.
* @param string $chart_id
* The chart identifier, pulled from the $chart['#chart_id'] property
* (if any). Not all charts have a chart identifier.
*/
function hook_chart_alter(array &$element, $chart_id) {
if ($chart_id === 'view_name__display_name') {
// Individual properties may be modified.
$element['#title_font_size'] = 20;
}
}
/**
* Alter an individual chart before it's rendered.
*
* Same as hook_chart_alter(), only including the $chart_id in the function
* name instead of being passed in as an argument.
*
* @see hook_chart_alter()
*/
function hook_chart_CHART_ID_alter(array &$element) {
}
/**
* Alter an individual chart's raw library representation.
*
* This hook is called AFTER hook_chart_alter(), after Charts module has
* converted the renderable into the chart definition that will be used by the
* library. Note that the structure of $definition will differ based on the
* charting library used. Switching charting libraries may cause your code
* to break when using this hook.
*
* Even though this hook may be fragile, it may provide developers with access
* to library-specific functionality.
*
* @param array $definition
* The chart definition to be modified. The raw values are passed directly to
* the charting library.
* @param array $element
* The chart renderable. This may be used for reference (or read to add
* support for new properties), but any changes to this variable will not
* have an effect on output.
* @param string $chart_id
* The chart ID, derived from the $chart['#chart_id'] property. Note that not
* all charts may have a $chart_id.
*/
function hook_chart_definition_alter(array &$definition, array $element, $chart_id) {
if ($element['#chart_library'] === 'google') {
$definition['options']['titleTextStyle']['fontSize'] = 20;
}
elseif ($element['#chart_library'] === 'highcharts') {
$definition['title']['style']['fontSize'] = 20;
// Add a caption based on a field on your node containing a Chart field.
$node = $element['#entity'];
if ($node instanceof \Drupal\node\NodeInterface && $node->hasField('field_caption')) {
$definition['caption']['text'] = $node->field_caption->value;
}
}
}
/**
* Alter an individual chart before it's rendered.
*
* Same as hook_chart_definition_alter(), only including the $chart_id in the
* function name instead of being passed in as an argument.
*
* @see hook_chart_definition_alter()
*/
function hook_chart_definition_CHART_ID_alter(array &$definition, array $element, $chart_id) {
}
/**
* Alter the list of supported chart types for a chart plugin.
*
* To use this hook, first define the chart types you want to add in a file
* named <module>.charts_types.yml to your module's root directory. For example,
* if your module is named "charts_foo", the file should be named
* "charts_foo.charts_types.yml".
*
* The contents of this file will look something like:
*
* @code
* candlestick:
* label: 'Candlestick'
* axis: 'xy'
* axis_inverted: false
* stacking: false
* @endcode
*
* @see charts.charts_types.yml
*
* With that file in place, you can now implement this hook. See below for an
* example adding the 'candlestick' chart type to the 'highcharts' chart plugin.
*
* @param array $types
* An array of supported chart types.
* @param string $chart_plugin_id
* The chart plugin ID.
*/
function hook_charts_plugin_supported_chart_types_alter(array &$types, string $chart_plugin_id) {
if ($chart_plugin_id === 'highcharts') {
$types[] = 'candlestick';
}
}

View File

@@ -0,0 +1,50 @@
area:
label: 'Area'
axis: 'xy'
axis_inverted: false
stacking: true
bar:
label: 'Bar'
axis: 'xy'
axis_inverted: true
stacking: true
bubble:
label: 'Bubble'
axis: 'xy'
axis_inverted: false
stacking: false
column:
label: 'Column'
axis: 'xy'
axis_inverted: false
stacking: true
donut:
label: 'Donut'
axis: 'y_only'
axis_inverted: false
stacking: false
gauge:
label: 'Gauge'
axis: 'y_only'
axis_inverted: false
stacking: false
line:
label: 'Line'
axis: 'xy'
axis_inverted: false
stacking: false
pie:
label: 'Pie'
axis: 'y_only'
axis_inverted: false
stacking: false
scatter:
label: 'Scatter'
axis: 'xy'
axis_inverted: false
stacking: false
spline:
label: 'Spline'
axis: 'xy'
axis_inverted: false
stacking: false

View File

@@ -0,0 +1,5 @@
charts.settings:
title: 'Charts settings'
base_route_name: charts.settings
names:
- charts.settings

View File

@@ -0,0 +1,11 @@
name: Charts
type: module
description: 'A charting API for Drupal that provides chart elements and integration with Views.'
package: Charts
core_version_requirement: ^10.3 || ^11
configure: charts.settings
# Information added by Drupal.org packaging script on 2024-12-02
version: '5.1.1'
project: 'charts'
datestamp: 1733170633

View File

@@ -0,0 +1,15 @@
<?php
/**
* @file
* Install and update functions for the Charts module.
*/
/**
* Implements hook_requirements().
*/
function charts_requirements($phase) {
$requirements = [];
return $requirements;
}

View File

@@ -0,0 +1,7 @@
global:
version: VERSION
js:
js/charts.js: {}
dependencies:
- core/drupal

View File

@@ -0,0 +1,5 @@
charts.settings:
title: 'Charts configuration'
description: 'Set the default library, behavior, and style of charts.'
parent: system.admin_config_content
route_name: charts.settings

View File

@@ -0,0 +1,16 @@
charts.settings:
title: 'Charts Settings'
route_name: charts.settings
base_route: charts.settings
charts.settings.default:
title: 'Default'
route_name: charts.settings
parent_id: charts.settings
weight: 10
charts.settings.advanced:
title: 'Advanced'
route_name: charts.settings.advanced
parent_id: charts.settings
weight: 20

230
web/modules/charts/charts.module Executable file
View File

@@ -0,0 +1,230 @@
<?php
/**
* @file
* Charts - Module.
*/
use Drupal\views\ViewExecutable;
/**
* Implements hook_views_data().
*/
function charts_views_data() {
$data['charts_fields']['table']['group'] = t('Charts');
$data['charts_fields']['table']['join'] = [
// Exist in all views.
'#global' => [],
];
$data['charts_fields']['field_charts_fields_scatter'] = [
'title' => t('Scatter Field'),
'help' => t('Use this field for your data field in a scatter plot.'),
'field' => [
'id' => 'field_charts_fields_scatter',
],
];
$data['charts_fields']['field_charts_fields_bubble'] = [
'title' => t('Bubble Field'),
'help' => t('Use this field for your data field in a bubble chart.'),
'field' => [
'id' => 'field_charts_fields_bubble',
],
];
$data['charts_fields']['field_exposed_chart_type'] = [
'title' => t('Exposed Chart Type'),
'help' => t('Use this field for exposing chart type.'),
'field' => [
'id' => 'field_exposed_chart_type',
],
];
return $data;
}
/**
* Implements hook_theme().
*/
function charts_theme($existing, $type, $theme, $path) {
return [
'charts_chart' => [
'render element' => 'element',
],
];
}
/**
* Implements hook_preprocess_HOOK().
*/
function template_preprocess_charts_chart(&$variables) {
$element = $variables['element'];
$attributes = $element['#attributes'];
$attributes['id'] = $element['#id'];
$attributes['class'][] = 'chart';
$variables['content'] = [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => $attributes,
'#value' => $element['#chart'] ?? '',
];
$variables['content_prefix'] = $element['#content_prefix'];
$variables['content_suffix'] = $element['#content_suffix'];
$config = \Drupal::config('charts.settings');
$variables['debug'] = [];
if (!empty($config->get('advanced.debug'))) {
$variables['debug'] = [
'#type' => 'details',
'#title' => t('Chart JSON'),
'#open' => FALSE,
'#attributes' => [
'data-charts-debug-container' => TRUE,
],
'#collapsible' => TRUE,
'json' => [
'#prefix' => '<pre class="language-json">',
'#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' => '</pre>',
],
];
}
}
/**
* 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/<domain>/*.
$search_dir[] = $site_path;
foreach ($search_dir as $dir) {
if (file_exists($dir . $source) || file_exists($dir . $source)) {
return TRUE;
}
}
return FALSE;
}

View File

@@ -0,0 +1,4 @@
'access all charts':
title: 'Administer Charts'
description: 'This permission needs to be fleshed out.'
'restrict access': true

View File

@@ -0,0 +1,221 @@
<?php
/**
* @file
* Charts post-update file.
*/
use Drupal\charts\ConfigUpdater;
use Drupal\Component\Serialization\Yaml;
/**
* Initialize advanced requirements cdn config value.
*/
function charts_post_update_initialize_advanced_requirements_cdn(&$sandbox) {
$config = \Drupal::service('config.factory')
->getEditable('charts.settings');
if ($config) {
$config->set('advanced', ['requirements' => ['cdn' => TRUE]]);
$config->save();
return 'Successfully initialized the advanced.requirements.cdn option.';
}
return 'No initialization was done. Please check your current config.';
}
/**
* Update the existing default config display colors to increase from 10 to 25.
*/
function charts_post_update_existing_default_colors_to_twenty_five(&$sandbox) {
$config = \Drupal::service('config.factory')->getEditable('charts.settings');
$existing_colors = $config->get('charts_default_settings.display.colors');
if (is_countable($existing_colors) && count($existing_colors) === 10) {
$path = \Drupal::service('extension.list.module')->getPath('charts');
$default_install_settings_file = $path . '/config/install/charts.settings.yml';
if (!file_exists($default_install_settings_file)) {
return 'Failed to update the default colors to 25 because we could not load the charts settings.';
}
$default_install_settings = Yaml::decode(file_get_contents($default_install_settings_file));
$install_colors = $default_install_settings['charts_default_settings']['display']['colors'];
// We only want to add the last 15 colors to make it 25.
$colors = array_merge($existing_colors, array_slice($install_colors, -15));
$config->set('charts_default_settings.display.colors', $colors);
$config->save();
return 'Successfully increased the number of default colors to 25.';
}
return 'We were unable to load existing colors in order to increased them to 25.';
}
/**
* Initialize the config value for the debug setting.
*/
function charts_post_update_initialize_debug_option(&$sandbox) {
$config = \Drupal::service('config.factory')
->getEditable('charts.settings');
if ($config) {
$config->set('advanced.debug', FALSE);
$config->save();
return 'Successfully initialized the advanced.debug option.';
}
return 'No initialization was done. Please check your current config.';
}
/**
* Migrate library default settings to library_config key.
*/
function charts_post_update_migrate_library_default_settings_to_library_config(&$sandbox) {
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
$config_factory = \Drupal::service('config.factory');
$config = $config_factory->getEditable('charts.settings');
if ($config) {
$default_settings = $config->get('charts_default_settings');
$library_id = $default_settings['library'] ?? '';
$old_key = $library_id ? 'charts_default_settings.' . $library_id . '_settings' : '';
if ($old_key && empty($default_settings['display']) && $library_config = $config->get($old_key)) {
$config->set('charts_default_settings.library_config', $library_config);
$config->clear($old_key);
$config->save();
return 'Library default settings have been successfully migrated to library_config key.';
}
}
return 'No migration was done';
}
/**
* Clear library_config set on the wrong place. See drupal.org/i/3310145.
*/
function charts_post_update_clear_library_config_key_added_on_wrong_place(&$sandbox) {
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
$config_factory = \Drupal::service('config.factory');
$config = $config_factory->getEditable('charts.settings');
if ($config && $config->get('library_config')) {
// Not to be confused with "charts_default_settings.library_config" which is
// the proper key.
$config->clear('library_config');
$config->save();
return 'Successfully cleared the library_config key.';
}
return 'The library_config key was not cleared, probably because it was not set.';
}
/**
* Updates charts config and views with charts style of version 3 to new config.
*/
function charts_post_update_00_update_charts_version_3_to_latest_settings_structure(&$sandbox) {
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
$config_factory = \Drupal::service('config.factory');
$config = $config_factory->getEditable('charts.settings');
$default_settings = $config ? $config->get('charts_default_settings') : [];
/** @var \Drupal\charts\ConfigUpdater $config_updater */
$config_updater = \Drupal::classResolver(ConfigUpdater::class);
if ($default_settings && !isset($default_settings['display'])) {
$results = [];
$config->set('charts_default_settings', $config_updater->transformVersion3SettingsToNew($default_settings));
$config->save();
$results[] = 'Default Config: Migration to the new structure successfully done.';
$results[] = $config_updater->updateExistingViewsVersion3ToNewSettings();
return implode(' -- ', $results);
}
elseif (!$default_settings || !$config->getRawData()) {
$config->setData($config_updater->initializedCurrentDefaultSettings());
$config->save();
$results = [];
$results[] = 'Default Config: No config were found and the default one was saved.';
$results[] = $config_updater->updateExistingViewsVersion3ToNewSettings();
return implode(' -- ', $results);
}
return 'No update of the settings structure was done.';
}
/**
* Re-save views that are using the chart style to recalculate dependencies.
*/
function charts_post_update_resave_views_to_account_calculated_library_dependencies(&$sandbox) {
$view_storage = \Drupal::entityTypeManager()->getStorage('view');
$view_ids = $view_storage->getQuery()
->accessCheck(FALSE)
->condition('display.*.display_options.style.type', 'chart', '=')
->execute();
$labels = [];
if ($view_ids) {
foreach ($view_ids as $view_id) {
/** @var \Drupal\views\ViewEntityInterface $view */
$view = $view_storage->load($view_id);
// Re-save the view to update its dependencies.
$view->save();
$labels[] = $view->label();
}
}
if ($labels) {
return t('The following views have been re-saved to re-calculate their dependencies: @views', [
'@views' => implode(',', $labels),
]);
}
return t('This site did not have any chart related views.');
}
/**
* Setting charts config module dependencies based on the selected library.
*/
function charts_post_update_setting_charts_config_module_dependencies(&$sandbox) {
$config = \Drupal::service('config.factory')
->getEditable('charts.settings');
$settings = $config->get('charts_default_settings');
$library = $settings['library'] ?? '';
$type = $settings['type'] ?? '';
if ($library || $type) {
/** @var \Drupal\charts\EventSubscriber\ConfigImportSubscriber $config_import_subscriber */
$config_import_subscriber = \Drupal::service('charts.config_import_subscriber');
$config->set('dependencies', $config_import_subscriber->calculateDependencies($library, $type))
->save();
return 'Updated chart settings dependencies';
}
return 'The default config for charts did not have a library and type set. So no dependencies were set.';
}
/**
* Move color changer from fields to display.
*/
function charts_post_update_move_views_style_color_changer_from_fields_to_display(&$sandbox) {
$view_storage = \Drupal::entityTypeManager()->getStorage('view');
$view_ids = $view_storage->getQuery()
->accessCheck(FALSE)
->condition('display.*.display_options.style.type', 'chart', '=')
->execute();
$labels = [];
if ($view_ids) {
foreach ($view_ids as $view_id) {
/** @var \Drupal\views\ViewEntityInterface $view */
$view = $view_storage->load($view_id);
$changed = FALSE;
foreach (array_keys($view->get('display')) as $display_id) {
$display = &$view->getDisplay($display_id);
if (isset($display['display_options']['style']['options']['chart_settings']['fields']['color_changer'])) {
$changed = TRUE;
$display['display_options']['style']['options']['chart_settings']['display']['color_changer'] = !empty($display['display_options']['style']['options']['chart_settings']['fields']['color_changer']);
unset($display['display_options']['style']['options']['chart_settings']['fields']['color_changer']);
}
}
if ($changed) {
$view->save();
$labels[] = $view->label();
}
}
}
if ($labels) {
return t('The following views have been updated to move the color changer from fields to display: @views', [
'@views' => implode(',', $labels),
]);
}
return t('This site did not have any chart related views.');
}

View File

@@ -0,0 +1,15 @@
charts.settings:
path: /admin/config/content/charts
defaults:
_form: Drupal\charts\Form\ChartsConfigForm
_title: 'Chart configuration'
requirements:
_permission: 'administer site configuration'
charts.settings.advanced:
path: /admin/config/content/charts/advanced
defaults:
_form: Drupal\charts\Form\ChartsConfigAdvancedForm
_title: 'Chart advanced configuration form'
requirements:
_permission: 'administer site configuration'

View File

@@ -0,0 +1,21 @@
services:
plugin.manager.charts:
class: Drupal\charts\ChartManager
parent: default_plugin_manager
plugin.manager.charts_type:
class: Drupal\charts\TypeManager
arguments: ['@module_handler', '@cache.discovery', '@event_dispatcher']
charts.plugins_uninstall_validator:
class: Drupal\charts\PluginsUninstallValidator
tags:
- { name: module_install.uninstall_validator }
arguments: ['@config.factory']
lazy: true
charts.config_import_subscriber:
class: Drupal\charts\EventSubscriber\ConfigImportSubscriber
arguments: ['@plugin.manager.charts', '@plugin.manager.charts_type']
tags:
- { name: event_subscriber }

View File

View File

@@ -0,0 +1,73 @@
dependencies: []
charts_default_settings:
library: ''
type: line
display:
title: ''
subtitle: ''
colors:
- '#006fb0'
- '#f07c33'
- '#342e9c'
- '#579b17'
- '#3f067a'
- '#cbde67'
- '#7643b6'
- '#738d00'
- '#c157c7'
- '#02dab1'
- '#ed56b4'
- '#d8d981'
- '#004695'
- '#736000'
- '#a5a5ff'
- '#833a00'
- '#ff9ee9'
- '#684507'
- '#fe4f85'
- '#5d0011'
- '#ffa67b'
- '#88005c'
- '#ff9b8f'
- '#85000f'
- '#ff7581'
color_changer: false
title_position: out
tooltips: true
tooltips_use_html: false
data_markers: false
data_labels: false
legend: false
legend_position: right
background: ''
three_dimensional: 0
polar: 0
dimensions:
width: ''
width_units: '%'
height: ''
height_units: ''
gauge:
max: '100'
min: '0'
green_from: '85'
green_to: '100'
yellow_from: '50'
yellow_to: '85'
red_from: '0'
red_to: '50'
xaxis:
title: ''
labels_rotation: '0'
yaxis:
title: ''
min: ''
max: ''
prefix: ''
suffix: ''
decimal_count: ''
labels_rotation: '0'
advanced:
debug: false
requirements:
cdn: true

View File

@@ -0,0 +1,216 @@
# Basic data types for charts.
charts_dimensions:
type: mapping
mapping:
width:
type: string
label: 'Width'
width_units:
type: string
label: 'Width units'
height:
type: string
label: 'Height'
height_units:
type: string
label: 'Height units'
charts_gauge:
type: mapping
mapping:
max:
type: string
label: 'Gauge maximum value'
min:
type: string
label: 'Gauge minimum value'
green_from:
type: string
label: 'Green minimum value'
green_to:
type: string
label: 'Green maximum value'
yellow_from:
type: string
label: 'Yellow minimum value'
yellow_to:
type: string
label: 'Yellow maximum value'
red_from:
type: string
label: 'Red minimum value'
red_to:
type: string
label: 'Red maximum value'
charts_display:
type: mapping
mapping:
title:
type: label
label: 'Chart title'
title_position:
type: string
label: 'Title position'
subtitle:
type: string
label: 'Chart subtitle'
data_labels:
type: boolean
label: 'Enable data labels'
data_markers:
type: boolean
label: 'Enable data markers'
legend:
type: boolean
label: 'Legend'
legend_position:
type: string
label: 'Legend position'
background:
type: string
label: 'Background'
three_dimensional:
type: integer
label: 'Make chart three-dimensional (3D)'
colors:
type: sequence
label: 'Chart colors'
sequence:
type: string
label: 'Hexadecimal color'
polar:
type: integer
label: 'Transform cartesian charts into the polar coordinate system'
tooltips:
type: boolean
label: 'Tooltips'
tooltips_use_html:
type: boolean
label: 'Tooltips Use HTML'
dimensions:
type: charts_dimensions
label: 'Dimensions'
gauge:
type: charts_gauge
label: 'Gauge'
color_changer:
type: boolean
label: 'Enable color changer widget'
charts_xaxis:
type: mapping
mapping:
title:
type: label
label: 'X axis title'
labels_rotation:
type: string
label: 'X axis labels rotation'
charts_yaxis:
type: mapping
mapping:
title:
type: label
label: 'Y axis title'
min:
type: string
label: 'Y axis min value'
max:
type: string
label: 'Y axis max value'
prefix:
type: label
label: 'Y axis prefix'
suffix:
type: label
label: 'Y axis suffix'
decimal_count:
type: string
label: 'Y axis decimal count'
labels_rotation:
type: string
label: 'Y axis labels rotation'
charts_views_field_data_provider:
type: mapping
mapping:
enabled:
type: boolean
label: 'Enabled'
color:
type: string
label: Color
weight:
type: integer
label: 'Weight'
charts_views_field_entity_grouping_selected_method_colors:
type: mapping
mapping:
color:
type: string
label: Color
charts_config:
type: mapping
mapping:
library:
type: string
label: 'Charting library'
type:
type: string
label: 'Chart type'
fields:
type: mapping
label: 'Charts fields'
mapping:
label:
type: string
label: 'Label field'
stacking:
type: boolean
label: Stacking
data_providers:
type: sequence
label: 'Data providers'
sequence:
type: charts_views_field_data_provider
entity_grouping:
type: mapping
label: 'Entity grouping settings'
mapping:
color_selection_method:
type: string
label: 'Color selection method'
selected_method:
type: mapping
label: 'Selected method'
mapping:
color_field_name:
type: string
label: 'Color field name'
colors:
type: sequence
label: 'Colors'
sequence:
type: charts_views_field_entity_grouping_selected_method_colors
display:
type: charts_display
label: 'Display'
xaxis:
type: charts_yaxis
label: 'X axis Display'
yaxis:
type: charts_yaxis
label: 'Y axis Display'
library_config:
type: charts.settings.library_plugin.[%parent.library]
charts.settings.library_plugin.*:
type: charts_settings_library_plugin
charts_settings_library_plugin:
type: mapping

View File

@@ -0,0 +1,24 @@
charts.settings:
type: config_object
label: 'Charts settings'
mapping:
charts_default_settings:
type: charts_config
label: 'Charts default settings'
advanced:
type: mapping
label: Advanced
mapping:
debug:
type: boolean
label: 'Enable Charts Debug'
requirements:
type: mapping
label: Requirements
mapping:
cdn:
type: boolean
label: 'Check if CDN is being used for external libraries'
dependencies:
type: config_dependencies
label: 'Dependencies'

View File

@@ -0,0 +1,47 @@
views.style.chart:
type: views_style
label: 'Chart'
mapping:
chart_settings:
type: charts_config
label: 'Settings'
views.display.chart_extension:
type: views_display
label: 'Attachment display options'
views.field.field_exposed_chart_type:
type: views_field
label: 'Exposed Chart Type'
mapping:
chart_types:
type: sequence
label: 'Chart Type Options'
exposed_select_type:
type: string
label: 'Exposed Selection Type'
views.field.field_charts_fields_bubble:
type: views_field
label: 'Bubble Field'
mapping:
x_axis:
type: string
label: 'X Axis Field'
y_axis:
type: string
label: 'Y Axis Field'
z_axis:
type: string
label: 'Z Axis Field'
views.field.field_charts_fields_scatter:
type: views_field
label: 'Scatter Field'
mapping:
x_axis:
type: string
label: 'X Axis Field'
y_axis:
type: string
label: 'Y Axis Field'

View File

@@ -0,0 +1,46 @@
/**
* @file
* Charts API.
*/
((Drupal) => {
Drupal.Charts = Drupal.Charts || {};
Drupal.Charts.Configs = Drupal.Charts.Configs || [];
/**
* @typedef {class} Drupal.Charts.Contents
*/
Drupal.Charts.Contents = class {
constructor() {
const chartsElements = document.querySelectorAll('[data-chart]');
chartsElements.forEach(function (el) {
const id = el.getAttribute('id');
Drupal.Charts.Configs[id] = JSON.parse(el.getAttribute('data-chart'));
Drupal.Charts.Configs[id].drupalChartDivElement = el;
Drupal.Charts.Configs[id].drupalChartDivId = id;
});
}
initialize(id) {
const event = new CustomEvent('drupalChartsConfigsInitialization', {
detail: Drupal.Charts.Configs[id]
});
Drupal.Charts.Configs[id].drupalChartDivElement.dispatchEvent(event);
}
static update(id, data) {
if (Drupal.Charts.Configs.hasOwnProperty(id)) {
Drupal.Charts.Configs[id] = data;
}
}
getData(id) {
if (Drupal.Charts.Configs.hasOwnProperty(id)) {
this.initialize(id);
return Drupal.Charts.Configs[id];
}
return {};
}
};
})(Drupal);

View File

@@ -0,0 +1,45 @@
/**
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(Drupal => {
Drupal.Charts = Drupal.Charts || {};
Drupal.Charts.Configs = Drupal.Charts.Configs || [];
Drupal.Charts.Contents = class {
constructor() {
const charts_elements = document.querySelectorAll('[data-chart]');
charts_elements.forEach(function (el) {
const id = el.getAttribute('id');
Drupal.Charts.Configs[id] = JSON.parse(el.getAttribute('data-chart'));
Drupal.Charts.Configs[id].drupalChartDivElement = el;
Drupal.Charts.Configs[id].drupalChartDivId = id;
});
}
initialize(id) {
const event = new CustomEvent('drupalChartsConfigsInitialization', {
detail: Drupal.Charts.Configs[id]
});
Drupal.Charts.Configs[id].drupalChartDivElement.dispatchEvent(event);
}
static update(id, data) {
if (Drupal.Charts.Configs.hasOwnProperty(id)) {
Drupal.Charts.Configs[id] = data;
}
}
getData(id) {
if (Drupal.Charts.Configs.hasOwnProperty(id)) {
this.initialize(id);
return Drupal.Charts.Configs[id];
}
return {};
}
};
})(Drupal);

View File

@@ -0,0 +1,15 @@
#Charts API Example
This module shows you how to create charts using render arrays. There are
numerous examples in the `ChartsApiExample.php` controller file. In the
`charts_api_example.module` file, you can also see how some hooks described in
`charts.api.php` can be used. One hook overrides a chart using PHP. Another
overrides with JavaScript - this is necessary in some cases when the charting
library API is expecting a function rather than a string. You can read more
about overriding with JavaScript here:
https://www.drupal.org/project/charts/issues/3197574#comment-13999765
To see the examples in your site, navigate to: `/charts/example/display`
Settings for these charts are set in the Charts configuration page
(`/admin/config/content/charts`).

View File

@@ -0,0 +1,12 @@
name: 'Charts API Example'
type: module
description: 'Demonstrates how to use the Charts module without Views.'
core_version_requirement: ^10.3 || ^11
package: Examples
dependencies:
- 'charts:charts'
# Information added by Drupal.org packaging script on 2024-12-02
version: '5.1.1'
project: 'charts'
datestamp: 1733170633

View File

@@ -0,0 +1,6 @@
charts_api_example:
version: VERSION
js:
js/charts_api_example_js_override.js: { weight: -5 }
dependencies:
- charts/global

View File

@@ -0,0 +1,5 @@
charts_api_example.display:
title: 'Charts API Example'
description: 'Displays charts rendered using the Charts API.'
parent: system.admin_config_content
route_name: charts_api_example.display

View File

@@ -0,0 +1,43 @@
<?php
/**
* @file
* Charts API Example module file.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function charts_api_example_help($route_name, RouteMatchInterface $route_match) {
$output = '';
switch ($route_name) {
// Main module help for the charts_api_example module.
case 'help.page.charts_api_example':
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('A simple example on how to interact with the Charts API') . '</p>';
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';
}
}

View File

@@ -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'

View File

@@ -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": {}
}

View File

@@ -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
1 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
2 21-Aug-21 0 71 12 13458 1 4632 6 637 18817
3 14-Aug-21 0 77 13 13517 1 4518 9 608 18743
4 7-Aug-21 0 73 11 13586 1 4651 6 606 18934
5 31-Jul-21 0 76 13 13479 1 4488 7 619 18683
6 24-Jul-21 0 70 10 13771 1 4385 7 530 18774
7 17-Jul-21 0 73 12 13756 1 4430 8 536 18816
8 10-Jul-21 0 77 13 13596 1 4362 8 519 18576
9 3-Jul-21 0 75 11 13715 1 4437 8 596 18843
10 26-Jun-21 0 73 12 13925 1 4470 9 651 19141
11 19-Jun-21 0 80 13 13886 1 4583 7 499 19069
12 12-Jun-21 0 79 13 13651 1 4506 9 455 18714
13 5-Jun-21 0 84 12 13656 1 4525 8 457 18743
14 29-May-21 0 79 15 13935 1 4554 10 438 19032
15 22-May-21 0 84 14 13840 1 4511 8 500 18958
16 15-May-21 0 80 13 13790 2 4524 11 553 18973
17 8-May-21 0 85 13 13748 1 4448 9 357 18661
18 1-May-21 0 83 12 13853 0 4371 8 379 18706
19 24-Apr-21 0 82 14 13835 1 4431 6 340 18709
20 17-Apr-21 0 86 14 14080 0 4455 10 322 18967
21 10-Apr-21 0 82 12 13736 0 4326 8 280 18444
22 3-Apr-21 0 82 14 13815 0 4347 8 289 18555
23 27-Mar-21 0 78 12 13490 1 4212 7 325 18125
24 20-Mar-21 0 77 12 13667 0 4389 9 366 18520
25 13-Mar-21 0 77 12 13671 0 4293 9 269 18331
26 6-Mar-21 0 75 14 13840 0 4361 8 253 18551
27 27-Feb-21 0 81 14 14144 1 4413 9 235 18897
28 20-Feb-21 0 83 14 14151 0 4451 10 217 18926
29 13-Feb-21 0 80 13 14057 0 4294 10 247 18701
30 6-Feb-21 0 86 16 14042 0 4271 10 233 18658
31 30-Jan-21 0 78 13 14221 0 4330 7 257 18906
32 23-Jan-21 0 85 13 14109 0 4283 9 192 18691
33 16-Jan-21 0 94 16 14294 0 4227 9 162 18802
34 9-Jan-21 0 88 15 14244 0 4133 9 154 18643
35 2-Jan-21 0 81 14 14235 0 4123 15 145 18613
36 26-Dec-20 0 82 15 13746 0 3721 9 130 17703
37 19-Dec-20 0 83 15 13904 0 3704 9 128 17843
38 12-Dec-20 0 86 16 14299 0 4007 16 170 18594
39 5-Dec-20 0 85 15 14714 0 4047 12 152 19025
40 28-Nov-20 0 81 14 14852 0 3970 16 148 19081
41 21-Nov-20 0 78 16 14817 0 4347 17 108 19383
42 14-Nov-20 0 86 17 14840 0 4567 22 62 19594
43 7-Nov-20 0 87 18 14286 0 4110 18 80 18599
44 31-Oct-20 0 86 19 14599 0 4100 25 49 18878
45 24-Oct-20 0 91 16 14571 0 4157 25 3 18863
46 17-Oct-20 0 84 16 14434 0 5116 25 0 19675
47 10-Oct-20 0 87 15 14563 0 5102 33 0 19800
48 3-Oct-20 0 83 18 14593 0 4059 26 0 18779
49 26-Sep-20 0 88 17 14717 0 3971 23 0 18816
50 19-Sep-20 0 91 20 14605 0 4064 22 0 18802
51 12-Sep-20 0 87 19 14706 0 4865 24 0 19701
52 5-Sep-20 0 88 20 14661 0 4641 29 0 19439
53 29-Aug-20 0 91 19 14814 0 4557 28 0 19509
54 22-Aug-20 0 92 17 14570 0 4572 30 0 19281
55 15-Aug-20 0 84 20 14743 0 4615 32 0 19494
56 8-Aug-20 0 92 20 14800 0 4627 28 0 19567
57 1-Aug-20 0 90 19 14925 0 4433 20 0 19487
58 25-Jul-20 0 92 19 15013 0 4533 19 0 19676
59 18-Jul-20 0 89 20 14750 0 4532 22 0 19413
60 11-Jul-20 0 93 15 14901 0 4594 20 0 19623
61 4-Jul-20 0 88 21 15080 0 4467 26 0 19682
62 27-Jun-20 0 93 19 15281 0 4679 26 0 20098
63 20-Jun-20 0 86 21 15642 0 4835 25 0 20609
64 13-Jun-20 0 91 25 15979 0 4613 23 0 20731
65 6-Jun-20 0 92 23 15597 0 4564 20 0 20296
66 30-May-20 0 86 25 15812 0 4433 17 0 20373
67 23-May-20 0 94 27 15830 0 4163 15 0 20129
68 16-May-20 0 98 29 15739 0 4169 14 0 20049
69 9-May-20 0 98 23 15300 0 4336 12 0 19769
70 2-May-20 0 94 27 15370 0 4261 12 0 19764
71 25-Apr-20 0 99 28 14711 0 4043 9 0 18890
72 18-Apr-20 0 102 25 14857 1 3970 4 0 18959
73 11-Apr-20 0 107 27 14884 0 3966 4 0 18988
74 4-Apr-20 0 104 25 14829 0 3928 3 0 18889
75 28-Mar-20 0 101 25 15302 0 3937 4 0 19369
76 21-Mar-20 0 103 27 15217 1 4067 3 0 19418
77 14-Mar-20 0 107 26 15136 0 3813 1 0 19083
78 7-Mar-20 0 106 26 15202 0 3797 0 0 19131
79 29-Feb-20 0 106 29 15269 1 3795 0 0 19200
80 22-Feb-20 0 106 30 15222 0 3726 0 0 19084
81 15-Feb-20 0 111 29 15289 0 3596 1 0 19026
82 8-Feb-20 0 108 28 15328 0 3667 0 0 19131
83 1-Feb-20 0 110 28 15523 1 3673 0 0 19335
84 25-Jan-20 0 114 25 15279 0 3523 0 0 18941
85 18-Jan-20 0 108 29 15289 0 3549 0 0 18975
86 11-Jan-20 0 115 29 15540 1 3561 0 0 19246
87 4-Jan-20 0 123 26 15612 0 3492 0 0 19253
88 28-Dec-19 0 117 30 15151 0 3065 0 0 18363
89 21-Dec-19 0 122 26 15067 0 3179 0 0 18394
90 14-Dec-19 0 125 27 16078 1 3614 0 0 19845
91 7-Dec-19 0 124 25 16029 0 3634 0 0 19812
92 30-Nov-19 0 127 30 15804 0 3591 1 0 19553
93 23-Nov-19 0 119 28 15640 0 3512 2 0 19301
94 16-Nov-19 0 119 31 15755 1 3659 1 0 19566
95 9-Nov-19 0 129 29 15583 0 3449 3 0 19193
96 2-Nov-19 0 124 32 15773 0 3488 3 0 19420
97 26-Oct-19 0 138 29 15742 0 3345 0 0 19254
98 19-Oct-19 0 129 27 15927 1 3347 0 0 19431
99 12-Oct-19 0 127 31 15761 0 3414 1 0 19334
100 5-Oct-19 0 143 38 15753 0 3405 1 0 19340
101 28-Sep-19 0 139 30 15852 1 3428 1 0 19451
102 21-Sep-19 0 129 29 15753 0 3194 0 0 19105
103 14-Sep-19 0 131 30 15994 0 3107 0 0 19262
104 7-Sep-19 0 134 30 15560 0 3222 0 0 18946
105 31-Aug-19 0 133 35 15642 1 3279 0 0 19090
106 24-Aug-19 0 131 33 15477 0 3163 0 0 18804
107 17-Aug-19 0 145 34 15513 0 3095 0 0 18787
108 10-Aug-19 0 144 29 15331 1 2854 0 0 18359
109 3-Aug-19 0 150 30 15495 0 2924 0 0 18599
110 27-Jul-19 0 135 32 15766 0 2942 0 0 18875
111 20-Jul-19 0 145 35 15715 1 2864 0 0 18760
112 13-Jul-19 0 141 29 15700 0 2975 0 0 18845
113 6-Jul-19 0 140 32 15572 0 3223 0 0 18967
114 29-Jun-19 0 153 32 15719 1 3087 0 0 18992
115 22-Jun-19 0 151 33 15604 1 3045 0 0 18834
116 15-Jun-19 0 147 35 15576 0 3226 0 0 18984
117 8-Jun-19 0 150 29 15515 0 2942 0 0 18636
118 1-Jun-19 0 140 37 15912 0 2724 0 0 18813
119 25-May-19 0 145 32 15847 1 2803 0 0 18828
120 18-May-19 0 142 31 15985 0 3094 0 0 19252
121 11-May-19 0 142 31 16276 2 3083 0 0 19534
122 4-May-19 0 131 30 16204 8 3001 0 0 19374
123 27-Apr-19 0 126 35 15858 7 2788 0 0 18814
124 20-Apr-19 0 134 30 15809 4 2635 0 0 18612
125 13-Apr-19 0 136 32 15934 8 2545 0 0 18655
126 6-Apr-19 0 136 36 15967 7 2652 0 0 18798
127 30-Mar-19 0 135 37 16346 6 2608 0 0 19132
128 23-Mar-19 0 135 32 16429 6 2537 0 0 19139
129 16-Mar-19 0 131 32 16638 4 2370 0 0 19175
130 9-Mar-19 0 138 32 16351 5 2300 0 0 18826
131 2-Mar-19 0 141 34 16383 6 2436 0 0 19000
132 23-Feb-19 0 145 31 16483 7 2458 0 0 19124
133 16-Feb-19 0 143 32 16565 6 2689 0 0 19435
134 9-Feb-19 0 143 37 16169 5 2551 0 0 18905
135 2-Feb-19 0 142 32 16180 4 2437 0 0 18795
136 26-Jan-19 0 136 34 16314 5 2474 0 0 18963
137 19-Jan-19 0 138 30 16504 6 2276 0 0 18954
138 12-Jan-19 0 140 28 16585 7 2294 0 0 19054
139 5-Jan-19 0 140 28 16175 7 2122 0 0 18472
140 29-Dec-18 0 145 31 15571 7 1974 0 0 17728
141 22-Dec-18 0 144 30 15031 4 2009 0 0 17218
142 15-Dec-18 0 145 30 15884 5 2077 0 0 18141
143 8-Dec-18 0 140 35 16095 5 2193 0 0 18468
144 1-Dec-18 0 148 31 16110 3 2268 0 0 18560
145 24-Nov-18 0 145 27 15817 3 2096 0 0 18088
146 17-Nov-18 0 153 25 15964 6 2104 0 0 18252
147 10-Nov-18 0 153 33 15995 7 2074 0 0 18262
148 3-Nov-18 0 158 34 15944 5 2319 0 0 18460
149 27-Oct-18 0 153 34 15920 4 2400 0 0 18511
150 20-Oct-18 0 150 31 16208 5 2237 0 0 18631
151 13-Oct-18 0 153 30 16470 4 2200 0 0 18857
152 6-Oct-18 0 153 32 15848 4 1926 0 0 17963
153 29-Sep-18 0 155 36 15769 5 1929 0 0 17894
154 22-Sep-18 0 157 29 15572 4 1852 0 0 17614
155 15-Sep-18 0 158 29 15334 5 1998 0 0 17524
156 8-Sep-18 0 161 28 15815 4 1974 0 0 17982
157 1-Sep-18 0 162 30 15825 4 2001 0 0 18022
158 25-Aug-18 0 156 29 15769 4 1929 0 0 17887
159 18-Aug-18 0 152 29 15565 4 1775 0 0 17525
160 11-Aug-18 0 150 30 15460 4 1860 0 0 17504
161 4-Aug-18 0 157 36 15504 5 1979 0 0 17681
162 28-Jul-18 0 157 32 15243 2 1898 0 0 17332
163 21-Jul-18 0 161 31 15322 0 1850 0 0 17364
164 14-Jul-18 0 138 34 14741 0 1725 0 0 16638
165 7-Jul-18 0 158 35 15621 0 1700 0 0 17514
166 30-Jun-18 0 145 31 13791 0 1442 0 0 15409
167 23-Jun-18 0 165 36 15441 0 1594 0 0 17236
168 16-Jun-18 0 162 35 15550 0 1633 0 0 17380
169 9-Jun-18 0 157 39 15502 0 1650 0 0 17348
170 2-Jun-18 0 167 34 15757 3 1795 0 0 17756
171 26-May-18 0 176 33 15551 0 1487 0 0 17247
172 19-May-18 0 173 37 15841 0 1554 0 0 17605
173 12-May-18 0 168 30 16140 0 1400 0 0 17738
174 5-May-18 0 174 34 16049 0 1460 0 0 17717
175 28-Apr-18 0 216 35 16600 0 1485 0 0 18336
176 21-Apr-18 0 181 33 18067 0 1517 0 0 19798
177 14-Apr-18 0 181 32 16873 2 1712 0 0 18800
178 7-Apr-18 0 177 38 16800 4 1558 0 0 18577
179 31-Mar-18 0 184 32 16736 2 1457 0 0 18411
180 24-Mar-18 0 195 31 17965 3 1458 0 0 19652
181 17-Mar-18 0 184 35 16418 2 1380 0 0 18019
182 10-Mar-18 0 184 32 16795 2 1419 0 0 18432
183 3-Mar-18 0 193 35 16780 2 1520 0 0 18530
184 24-Feb-18 0 190 31 16427 2 1338 0 0 17988
185 17-Feb-18 0 204 34 16367 2 1274 0 0 17881
186 10-Feb-18 0 196 36 16275 1 1292 0 0 17800
187 3-Feb-18 0 206 34 16345 3 1527 0 0 18115
188 27-Jan-18 0 196 33 16268 5 1385 0 0 17887
189 20-Jan-18 0 202 36 15935 2 1450 0 0 17625
190 13-Jan-18 0 200 33 15856 7 1320 0 0 17416
191 6-Jan-18 0 209 38 16034 3 1285 0 0 17569
192 30-Dec-17 0 204 39 15544 2 1056 0 0 16845
193 23-Dec-17 0 206 35 15002 2 1039 0 0 16284
194 16-Dec-17 0 203 35 15809 3 1123 0 0 17173
195 9-Dec-17 0 206 32 15921 2 1477 0 0 17638
196 2-Dec-17 0 204 38 16419 2 1339 0 0 18002
197 25-Nov-17 0 210 33 16087 2 1193 0 0 17525
198 18-Nov-17 0 205 39 16106 3 1096 0 0 17449
199 11-Nov-17 0 210 39 16304 2 1081 0 0 17636
200 4-Nov-17 0 227 40 16241 3 1163 0 0 17674
201 28-Oct-17 0 217 39 15860 4 909 0 0 17029
202 21-Oct-17 0 226 39 15948 4 863 0 0 17080
203 14-Oct-17 0 227 43 16086 3 922 0 0 17281
204 7-Oct-17 0 235 45 16215 2 932 0 0 17429
205 30-Sep-17 0 237 38 16037 2 800 0 0 17114
206 23-Sep-17 0 243 42 15838 4 715 0 0 16842
207 16-Sep-17 0 239 39 15716 4 962 0 0 16960
208 9-Sep-17 0 242 44 15784 2 659 0 0 16731
209 2-Sep-17 0 254 46 15812 2 760 0 0 16874
210 26-Aug-17 0 254 43 15805 2 681 0 0 16785
211 19-Aug-17 0 265 45 15748 3 533 0 0 16594
212 12-Aug-17 0 242 38 14836 2 474 0 0 15592
213 5-Aug-17 0 264 39 15242 3 572 0 0 16120
214 29-Jul-17 0 263 41 15266 1 288 0 0 15859
215 22-Jul-17 0 259 44 15357 2 134 0 0 15796
216 15-Jul-17 0 269 38 15512 2 132 0 0 15953
217 8-Jul-17 0 261 47 15717 3 124 0 0 16152
218 1-Jul-17 0 262 43 15617 3 114 0 0 16039
219 24-Jun-17 0 261 44 15799 3 113 0 0 16220
220 17-Jun-17 0 270 42 15745 3 107 0 0 16167
221 10-Jun-17 0 279 42 15587 4 112 0 0 16024
222 3-Jun-17 0 278 49 15721 4 105 0 0 16157
223 27-May-17 0 289 50 15553 4 100 0 0 15996
224 20-May-17 0 286 45 15504 2 92 0 0 15929
225 13-May-17 0 290 46 16796 2 93 0 0 17227
226 6-May-17 0 286 45 17445 4 74 0 0 17854
227 29-Apr-17 0 297 46 17294 3 62 0 0 17702
228 22-Apr-17 0 296 46 17500 3 63 0 0 17908
229 15-Apr-17 0 290 48 17485 3 47 0 0 17873
230 8-Apr-17 0 293 46 17367 3 36 0 0 17745
231 1-Apr-17 0 298 43 17508 7 31 0 0 17887
232 25-Mar-17 0 300 47 17271 10 27 0 0 17655
233 18-Mar-17 0 304 47 17267 14 10 0 0 17642
234 11-Mar-17 0 296 43 16712 19 0 0 0 17070
235 4-Mar-17 0 303 46 16999 3 0 0 0 17351
236 25-Feb-17 0 307 48 16959 0 0 0 0 17314
237 18-Feb-17 0 320 45 16806 0 0 0 0 17171
238 11-Feb-17 0 305 40 16866 0 0 0 0 17211
239 4-Feb-17 0 294 42 16988 0 0 0 0 17324
240 28-Jan-17 0 312 43 16604 0 0 0 0 16959
241 21-Jan-17 0 291 48 16706 0 0 0 0 17045
242 14-Jan-17 0 310 46 16459 0 0 0 0 16815
243 7-Jan-17 0 318 51 16878 0 0 0 0 17247
244 31-Dec-16 0 318 45 15947 0 0 0 0 16310
245 24-Dec-16 0 311 45 15031 0 0 0 0 15387
246 17-Dec-16 0 319 46 16005 0 0 0 0 16370
247 10-Dec-16 0 318 42 16210 0 0 0 0 16570
248 3-Dec-16 0 332 52 16549 0 0 0 0 16933
249 26-Nov-16 0 334 46 16549 0 0 0 0 16929
250 19-Nov-16 0 332 51 15957 0 0 0 0 16340
251 12-Nov-16 0 342 49 16191 0 0 0 0 16582
252 5-Nov-16 0 345 56 15339 0 0 0 0 15740
253 29-Oct-16 0 349 49 14911 0 0 0 0 15309
254 22-Oct-16 0 350 53 15040 0 0 0 0 15443
255 15-Oct-16 0 348 53 15238 0 0 0 0 15639
256 8-Oct-16 0 350 52 14567 0 0 0 0 14969
257 1-Oct-16 0 352 63 15041 0 0 0 0 15456
258 24-Sep-16 0 351 60 14551 0 0 0 0 14962
259 17-Sep-16 0 347 58 14655 0 0 0 0 15060
260 10-Sep-16 0 338 62 14584 0 0 0 0 14984
261 3-Sep-16 0 355 62 14582 0 0 0 0 14999
262 27-Aug-16 0 352 67 14673 0 0 0 0 15092
263 20-Aug-16 0 359 63 14400 0 0 0 0 14822
264 13-Aug-16 0 392 61 14415 0 0 0 0 14868
265 6-Aug-16 0 373 62 14653 0 0 0 0 15088
266 30-Jul-16 0 367 60 14607 0 0 0 0 15034
267 23-Jul-16 0 374 58 14741 0 0 0 0 15173
268 16-Jul-16 0 415 57 14959 0 0 0 0 15431
269 9-Jul-16 0 439 58 13846 0 0 0 0 14343
270 2-Jul-16 0 378 59 14610 0 0 0 0 15047
271 25-Jun-16 0 392 62 14607 0 0 0 0 15061
272 18-Jun-16 0 380 65 14953 0 0 0 0 15398
273 11-Jun-16 0 398 58 14869 0 0 0 0 15325
274 4-Jun-16 0 379 63 14444 0 0 0 0 14886
275 28-May-16 0 406 68 14443 0 0 0 0 14917
276 21-May-16 0 396 68 14739 0 0 0 0 15203
277 14-May-16 0 391 58 14553 0 0 0 0 15002
278 7-May-16 0 393 72 14577 0 0 0 0 15042
279 30-Apr-16 0 391 67 14347 0 0 0 0 14805
280 23-Apr-16 0 412 65 14447 0 0 0 0 14924
281 16-Apr-16 0 392 65 14611 0 0 0 0 15068
282 9-Apr-16 0 414 68 14426 0 0 0 0 14908
283 2-Apr-16 0 410 67 14537 0 0 0 0 15014
284 26-Mar-16 0 407 74 14040 0 0 0 0 14521
285 19-Mar-16 0 418 73 13893 0 0 0 0 14384
286 12-Mar-16 0 419 76 14256 0 0 0 0 14751
287 5-Mar-16 0 440 73 14272 0 0 0 0 14785
288 27-Feb-16 0 437 78 14293 0 0 0 0 14808
289 20-Feb-16 0 448 78 14500 0 0 0 0 15026
290 13-Feb-16 0 458 74 13949 0 0 0 0 14481
291 6-Feb-16 0 437 76 13681 0 0 0 0 14194
292 30-Jan-16 0 428 76 13729 0 0 0 0 14233
293 23-Jan-16 0 435 80 13393 0 0 0 0 13908
294 16-Jan-16 0 441 79 13369 0 0 0 0 13889
295 9-Jan-16 0 443 77 13298 0 0 0 0 13818
296 2-Jan-16 0 450 77 12385 0 0 0 0 12912
297 26-Dec-15 0 446 84 11190 0 0 0 0 11720
298 19-Dec-15 0 452 92 11888 0 0 0 0 12432
299 12-Dec-15 0 468 84 12623 0 0 0 0 13175
300 5-Dec-15 0 465 87 12736 0 0 0 0 13288
301 28-Nov-15 0 460 86 12721 0 0 0 0 13267
302 21-Nov-15 0 465 88 12423 0 0 0 0 12976
303 14-Nov-15 0 494 90 12483 0 0 0 0 13067
304 7-Nov-15 0 489 89 12383 0 0 0 0 12961
305 31-Oct-15 0 509 87 12512 0 0 0 0 13108
306 24-Oct-15 0 495 88 12346 0 0 0 0 12929
307 17-Oct-15 0 505 90 12338 0 0 0 0 12933
308 10-Oct-15 0 487 96 11995 0 0 0 0 12578
309 3-Oct-15 0 489 87 11676 0 0 0 0 12252
310 26-Sep-15 0 480 93 11015 0 0 0 0 11588
311 19-Sep-15 0 474 85 10385 0 0 0 0 10944
312 12-Sep-15 0 506 94 11041 0 0 0 0 11641
313 5-Sep-15 0 519 101 11402 0 0 0 0 12022
314 29-Aug-15 0 528 106 11556 0 0 0 0 12190
315 22-Aug-15 0 596 106 11672 0 0 0 0 12374
316 15-Aug-15 0 530 105 11574 0 0 0 0 12209
317 8-Aug-15 1 520 103 11089 0 0 0 0 11713
318 1-Aug-15 1 507 106 10947 0 0 0 0 11561
319 25-Jul-15 1 593 107 10712 0 0 0 0 11413
320 18-Jul-15 1 520 104 10533 0 0 0 0 11158
321 11-Jul-15 1 538 110 10764 0 0 0 0 11413
322 4-Jul-15 1 550 108 10918 0 0 0 0 11577
323 27-Jun-15 1 537 114 10783 0 0 0 0 11435
324 20-Jun-15 1 564 109 10961 0 0 0 0 11635
325 13-Jun-15 1 550 114 10845 0 0 0 0 11510
326 6-Jun-15 1 540 112 10369 0 0 0 0 11022
327 30-May-15 1 537 115 10137 0 0 0 0 10790
328 23-May-15 1 532 116 10024 0 0 0 0 10673
329 16-May-15 1 556 115 10051 0 0 0 0 10723
330 9-May-15 1 549 112 9923 0 0 0 0 10585
331 2-May-15 1 549 115 9700 0 0 0 0 10365
332 25-Apr-15 1 555 118 9462 0 0 0 0 10136
333 18-Apr-15 1 552 121 9363 0 0 0 0 10037
334 11-Apr-15 1 549 122 9046 0 0 0 0 9718
335 4-Apr-15 1 561 130 8955 0 0 0 0 9647
336 28-Mar-15 1 570 125 9064 0 0 0 0 9760
337 21-Mar-15 1 568 134 9194 0 0 0 0 9897
338 14-Mar-15 1 561 137 9163 0 0 0 0 9862
339 7-Mar-15 1 553 127 8826 0 0 0 0 9507
340 28-Feb-15 1 587 130 8638 0 0 0 0 9356
341 21-Feb-15 1 584 124 8459 0 0 0 0 9168
342 14-Feb-15 1 588 136 8314 0 0 0 0 9039
343 7-Feb-15 1 577 142 8240 0 0 0 0 8960
344 31-Jan-15 1 583 133 8003 0 0 0 0 8720
345 24-Jan-15 1 598 130 7738 0 0 0 0 8467
346 17-Jan-15 1 593 138 7779 0 0 0 0 8511
347 10-Jan-15 1 579 137 7654 0 0 0 0 8371
348 3-Jan-15 1 581 127 7500 0 0 0 0 8209
349 27-Dec-14 1 562 131 6455 0 0 0 0 7149
350 20-Dec-14 1 582 129 6575 0 0 0 0 7287
351 13-Dec-14 1 602 134 7342 0 0 0 0 8079
352 6-Dec-14 1 602 139 7319 0 0 0 0 8061
353 29-Nov-14 1 609 134 7171 0 0 0 0 7915
354 22-Nov-14 1 630 144 7088 0 0 0 0 7863
355 15-Nov-14 1 625 160 7301 0 0 0 0 8087
356 8-Nov-14 1 617 151 7029 0 0 0 0 7798
357 1-Nov-14 2 635 166 6769 0 0 0 0 7572
358 25-Oct-14 1 614 160 6508 0 0 0 0 7283
359 18-Oct-14 1 609 159 6348 0 0 0 0 7117
360 11-Oct-14 1 627 155 6349 0 0 0 0 7132
361 4-Oct-14 1 621 165 5897 0 0 0 0 6684
362 27-Sep-14 1 622 158 5854 0 0 0 0 6635
363 20-Sep-14 1 633 164 5881 0 0 0 0 6679
364 13-Sep-14 1 654 161 5789 0 0 0 0 6605
365 6-Sep-14 1 652 166 5700 0 0 0 0 6519
366 30-Aug-14 2 643 171 5580 0 0 0 0 6396
367 23-Aug-14 2 643 171 5466 0 0 0 0 6282
368 16-Aug-14 2 633 184 5304 0 0 0 0 6123
369 9-Aug-14 2 648 185 5183 0 0 0 0 6018
370 2-Aug-14 2 641 183 5247 0 0 0 0 6073
371 26-Jul-14 2 643 185 4938 0 0 0 0 5768
372 19-Jul-14 2 640 183 4814 0 0 0 0 5639
373 12-Jul-14 2 719 175 4713 0 0 0 0 5609
374 5-Jul-14 2 652 177 4543 0 0 0 0 5374
375 28-Jun-14 2 653 182 4526 0 0 0 0 5363
376 21-Jun-14 2 655 181 4490 0 0 0 0 5328
377 14-Jun-14 2 651 166 3895 0 0 0 0 4714
378 7-Jun-14 2 674 187 3866 0 0 0 0 4729
379 31-May-14 2 665 183 3623 0 0 0 0 4473
380 24-May-14 2 669 194 3581 0 0 0 0 4446
381 17-May-14 2 669 191 3346 0 0 0 0 4208
382 10-May-14 2 671 196 3221 0 0 0 0 4090
383 3-May-14 2 661 192 3223 0 0 0 0 4078
384 26-Apr-14 2 681 194 3095 0 0 0 0 3972
385 19-Apr-14 2 687 201 2981 0 0 0 0 3871
386 12-Apr-14 2 695 203 2910 0 0 0 0 3810
387 5-Apr-14 2 694 218 2807 0 0 0 0 3721
388 29-Mar-14 2 725 222 2807 0 0 0 0 3756
389 22-Mar-14 2 709 226 2661 0 0 0 0 3598
390 15-Mar-14 2 712 235 2544 0 0 0 0 3493
391 8-Mar-14 2 747 241 2405 0 0 0 0 3395
392 1-Mar-14 2 715 255 2368 0 0 0 0 3340
393 22-Feb-14 2 748 244 2245 0 0 0 0 3239
394 15-Feb-14 2 707 248 2107 0 0 0 0 3064
395 8-Feb-14 2 759 256 2044 0 0 0 0 3061
396 1-Feb-14 2 734 258 1928 0 0 0 0 2922
397 25-Jan-14 2 753 263 1781 0 0 0 0 2799
398 18-Jan-14 2 749 260 1722 0 0 0 0 2733
399 11-Jan-14 2 728 288 1612 0 0 0 0 2630
400 4-Jan-14 2 755 276 1343 0 0 0 0 2376
401 28-Dec-13 2 736 285 1082 0 0 0 0 2105
402 21-Dec-13 2 711 276 1005 0 0 0 0 1994
403 14-Dec-13 2 769 285 1118 0 0 0 0 2174
404 7-Dec-13 2 756 281 1008 0 0 0 0 2047
405 30-Nov-13 2 799 286 920 0 0 0 0 2007
406 23-Nov-13 2 898 290 849 0 0 0 0 2039
407 16-Nov-13 2 956 313 790 0 0 0 0 2061
408 9-Nov-13 2 939 313 746 0 0 0 0 2000
409 2-Nov-13 2 963 327 662 0 0 0 0 1954
410 26-Oct-13 2 992 322 636 0 0 0 0 1952
411 19-Oct-13 2 977 309 651 0 0 0 0 1939
412 12-Oct-13 2 990 328 576 0 0 0 0 1896
413 5-Oct-13 2 887 328 560 0 0 0 0 1777
414 28-Sep-13 2 1450 331 516 0 0 0 0 2299
415 21-Sep-13 2 1510 336 449 0 0 0 0 2297
416 14-Sep-13 2 1530 335 423 0 0 0 0 2290
417 7-Sep-13 2 874 340 363 0 0 0 0 1579
418 31-Aug-13 2 887 353 317 0 0 0 0 1559
419 24-Aug-13 2 870 349 269 0 0 0 0 1490
420 17-Aug-13 2 877 352 235 0 0 0 0 1466
421 10-Aug-13 2 862 367 190 0 0 0 0 1421
422 3-Aug-13 2 841 399 149 0 0 0 0 1391
423 27-Jul-13 2 876 416 104 0 0 0 0 1398
424 20-Jul-13 2 887 457 15 0 0 0 0 1361
425 13-Jul-13 2 879 445 0 0 0 0 0 1326
426 6-Jul-13 2 876 462 0 0 0 0 0 1340
427 29-Jun-13 2 884 464 0 0 0 0 0 1350
428 22-Jun-13 2 901 452 0 0 0 0 0 1355
429 15-Jun-13 2 893 460 0 0 0 0 0 1355
430 8-Jun-13 2 956 474 0 0 0 0 0 1432
431 1-Jun-13 2 1117 473 0 0 0 0 0 1592
432 25-May-13 2 1063 441 0 0 0 0 0 1506
433 18-May-13 2 944 383 0 0 0 0 0 1329
434 11-May-13 2 1109 450 0 0 0 0 0 1561
435 4-May-13 2 1020 451 0 0 0 0 0 1473
436 27-Apr-13 2 1003 450 0 0 0 0 0 1455
437 20-Apr-13 2 967 448 0 0 0 0 0 1417
438 13-Apr-13 2 1061 469 0 0 0 0 0 1532
439 6-Apr-13 2 1022 478 0 0 0 0 0 1502
440 30-Mar-13 2 1030 440 0 0 0 0 0 1472
441 23-Mar-13 2 1101 464 0 0 0 0 0 1567
442 16-Mar-13 2 1047 451 0 0 0 0 0 1500
443 9-Mar-13 2 1046 476 0 0 0 0 0 1524
444 2-Mar-13 3 1044 469 0 0 0 0 0 1516
445 23-Feb-13 3 1077 479 0 0 0 0 0 1559
446 16-Feb-13 3 1094 458 0 0 0 0 0 1555
447 9-Feb-13 3 1091 468 0 0 0 0 0 1562
448 2-Feb-13 3 1099 447 0 0 0 0 0 1549
449 26-Jan-13 3 1132 452 0 0 0 0 0 1587
450 19-Jan-13 3 1124 455 0 0 0 0 0 1582
451 12-Jan-13 4 1164 446 0 0 0 0 0 1614
452 5-Jan-13 3 1167 443 0 0 0 0 0 1613
453 29-Dec-12 3 1085 411 0 0 0 0 0 1499
454 22-Dec-12 3 1076 407 0 0 0 0 0 1486
455 15-Dec-12 4 1173 437 0 0 0 0 0 1614
456 8-Dec-12 4 1208 441 0 0 0 0 0 1653
457 1-Dec-12 3 1171 476 0 0 0 0 0 1650
458 24-Nov-12 3 1185 474 0 0 0 0 0 1662
459 17-Nov-12 3 1163 460 0 0 0 0 0 1626
460 10-Nov-12 3 1200 451 0 0 0 0 0 1654
461 3-Nov-12 3 1247 476 0 0 0 0 0 1726
462 27-Oct-12 3 1219 456 0 0 0 0 0 1678
463 20-Oct-12 3 1276 443 0 0 0 0 0 1722

View File

@@ -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 <b>' + this.x +
'</b> is <b>' + this.y + '</b>';
};
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);
});
}
};
}());

View File

@@ -0,0 +1,400 @@
<?php
namespace Drupal\charts_api_example\Controller;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Messenger\MessengerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Charts Api Example.
*/
class ChartsApiExample extends ControllerBase {
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The UUID service.
*
* @var \Drupal\Component\Uuid\UuidInterface
*/
protected $uuidService;
/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleList;
/**
* Construct.
*
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Drupal\Component\Uuid\UuidInterface $uuidService
* The UUID service.
* @param \Drupal\Core\Extension\ModuleExtensionList $module_list
* The module list.
*/
public function __construct(MessengerInterface $messenger, UuidInterface $uuidService, ModuleExtensionList $module_list) {
$this->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;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,95 @@
<?php
/**
* @file
* Installation and uninstallation functions.
*/
/**
* Implements hook_requirements().
*/
function charts_billboard_requirements($phase) {
$requirements = [];
switch ($phase) {
case 'runtime':
$library_path = charts_billboard_find_library();
$config = \Drupal::config('charts.settings');
$cdn = $config->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/<domain>/*.
$container = \Drupal::getContainer();
$site_path = $container->getParameter('site.path');
$site_path = explode('/', $site_path);
$site_name = $site_path[1];
$searchdir[] = $site_name . '/libraries';
foreach ($searchdir as $dir) {
if (file_exists($dir . '/billboard/dist/billboard.min.js') || file_exists($dir . '/billboard/dist/billboard.js')) {
return $dir . '/billboard';
}
}
return FALSE;
}

View File

@@ -0,0 +1,36 @@
charts_billboard:
version: 1.x
js:
js/charts_billboard.js: { weight: -1 }
dependencies:
- charts/global
- core/drupal
- core/once
d3:
remote: 'https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js'
version: 7.8.5
license:
name: BSD
url: 'https://en.wikipedia.org/wiki/BSD_licenses'
gpl-compatible: false
cdn:
/libraries/d3/: https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/
js:
/libraries/d3/d3.min.js: { minified: true }
billboard:
remote: 'https://naver.github.io/billboard.js/release/3.10.3/dist/billboard.min.js'
version: 3.10.3
license:
name: MIT
url: 'https://opensource.org/licenses/MIT'
gpl-compatible: true
cdn:
/libraries/billboard/: https://naver.github.io/billboard.js/release/3.10.3/
css:
theme:
/libraries/billboard/dist/billboard.min.css: { minified: true }
js:
/libraries/billboard/dist/billboard.min.js: { minified: true }
dependencies:
- charts_billboard/charts_billboard
- charts_billboard/d3

View File

@@ -0,0 +1,17 @@
<?php
/**
* @file
* Implement module related hooks for the charts_billboard module.
*/
/**
* Implements hook_charts_version3_to_new_settings_structure_alter().
*/
function charts_billboard_charts_version3_to_new_settings_structure_alter(array &$new_settings, string $for) {
$config_factory = \Drupal::configFactory();
$billboard_config = $config_factory->getEditable('charts_billboard.settings');
if ($billboard_config) {
$billboard_config->delete();
}
}

View File

@@ -0,0 +1,49 @@
{
"name": "drupal/charts_billboard",
"description": "Charts module integration with Billboard.js Charts.",
"type": "drupal-module",
"homepage": "http://drupal.org/project/charts",
"support": {
"issues": "https://www.drupal.org/project/issues/charts"
},
"license": "GPL-2.0+",
"require": {
"billboardjs/billboard": "3.10.3",
"d3/d3": "7.8.5"
},
"repositories": [
{
"type": "package",
"package": {
"name": "billboardjs/billboard",
"version": "3.10.3",
"type": "drupal-library",
"extra": {
"installer-name": "billboard"
},
"dist": {
"url": "https://github.com/naver/billboard.js/archive/3.10.3.zip",
"type": "zip"
}
}
},
{
"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.min.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
}
]
}

View File

@@ -0,0 +1,26 @@
/**
* @file
* JavaScript integration between Billboard and Drupal.
*/
(function (Drupal, once) {
'use strict';
Drupal.behaviors.chartsBillboard = {
attach: function (context, settings) {
const contents = new Drupal.Charts.Contents();
once('charts-billboard', '.charts-billboard', context).forEach(function (element) {
const config = contents.getData(element.id);
const title = config.title.text;
// If the title contains '\\n', convert it to a line break.
if (title.indexOf('\\n') !== -1) {
config.title.text = title.replace(/\\n/g, '\n');
}
bb.generate(config);
if (element.nextElementSibling && element.nextElementSibling.hasAttribute('data-charts-debug-container')) {
element.nextElementSibling.querySelector('code').innerText = JSON.stringify(config, null, ' ');
}
});
}
};
}(Drupal, once));

View File

@@ -0,0 +1,454 @@
<?php
namespace Drupal\charts_billboard\Plugin\chart\Library;
use Drupal\charts\Plugin\chart\Library\ChartBase;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Define a concrete class for a Chart.
*
* @Chart(
* id = "billboard",
* name = @Translation("Billboard.js"),
* types = {
* "area",
* "bar",
* "bubble",
* "column",
* "donut",
* "gauge",
* "line",
* "pie",
* "scatter",
* "spline",
* },
* )
*/
class Billboard extends ChartBase implements ContainerFactoryPluginInterface {
/**
* The element info manager.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfo;
/**
* Constructs a \Drupal\views\Plugin\Block\ViewsBlockBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* The element info manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ElementInfoManagerInterface $element_info) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->elementInfo = $element_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('element_info')
);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['intro_text'] = [
'#markup' => $this->t('This is a placeholder for Billboard.js-specific library options. If you would like to help build this out, please work from <a href="@issue_link">this issue</a>.', [
'@issue_link' => Url::fromUri('https://www.drupal.org/project/charts/issues/3046983')->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-billboard');
}
$chart_definition['bindto'] = '#' . $element['#id'];
$element['#attached']['library'][] = 'charts_billboard/billboard';
$element['#attributes']['class'][] = 'charts-billboard charts-bb';
$element['#chart_definition'] = $chart_definition;
return $element;
}
/**
* Get the chart type.
*
* @param string $chart_type
* The chart type.
* @param bool $is_polar
* Whether the polar is checked.
*
* @return string
* The chart 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.
*
* @param string $type
* The chart type.
* @param array $element
* The element.
*
* @return array
* The returned options.
*/
protected function getOptionsByType($type, array $element) {
$options = $this->getOptionsByCustomProperty($element, $type);
if ($type === 'bar') {
$options['width'] = $element['#width'];
}
return $options;
}
/**
* Get the options by custom property.
*
* @param array $element
* The element.
* @param string $type
* The chart type.
*
* @return array
* The 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'], $element['#polar'] ?? FALSE);
$title = $element['#title'] ?? '';
if (!empty($element['#subtitle'])) {
$title .= '\n' . $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') {
// Do nothing.
}
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 the 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') {
// Do nothing.
}
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'], $element['#polar'] ?? FALSE);
$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'], $element['#polar'] ?? FALSE) : $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;
}
}

View File

@@ -0,0 +1,9 @@
#Charts Blocks
Use this module if you would like a chart added via Layout Builder or the
"Block layout" page. You will need to enable at least one charting provider
submodule before this module can be used. Data can be added manually or by
uploading a CSV file.
If you create your chart using a View, you do not need this module - just add
a "Block" display to your View.

View File

@@ -0,0 +1,12 @@
name: Charts Blocks
type: module
description: Create Charts blocks without the need for Views.
core_version_requirement: ^10.3 || ^11
package: Charts
dependencies:
- drupal:charts
# Information added by Drupal.org packaging script on 2024-12-02
version: '5.1.1'
project: 'charts'
datestamp: 1733170633

View File

@@ -0,0 +1,24 @@
<?php
/**
* @file
* Contains chart_block_example.module.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function charts_blocks_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the charts_blocks module.
case 'help.page.charts_blocks':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Create Charts blocks without the need for Views.') . '</p>';
return $output;
default:
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Drupal\charts_blocks\Plugin\Block;
use Drupal\charts\Element\Chart;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'ChartsBlock' block.
*
* @Block(
* id = "charts_block",
* admin_label = @Translation("Charts block"),
* )
*/
class ChartsBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The UUID service.
*
* @var \Drupal\Component\Uuid\UuidInterface
*/
protected $uuidService;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory, UuidInterface $uuidService) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->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;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,95 @@
<?php
/**
* @file
* Installation and uninstallation functions.
*/
/**
* Implements hook_requirements().
*/
function charts_c3_requirements($phase) {
$requirements = [];
switch ($phase) {
case 'runtime':
$library_path = charts_c3_find_library();
$config = \Drupal::config('charts.settings');
$cdn = $config->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/<domain>/*.
$container = \Drupal::getContainer();
$site_path = $container->getParameter('site.path');
$site_path = explode('/', $site_path);
$site_name = $site_path[1];
$searchdir[] = $site_name . '/libraries';
foreach ($searchdir as $dir) {
if (file_exists($dir . '/c3/c3.min.js') || file_exists($dir . '/c3/c3.js')) {
return $dir . '/c3';
}
}
return FALSE;
}

View File

@@ -0,0 +1,36 @@
charts_c3:
version: 1.x
js:
js/charts_c3.js: { weight: -1 }
dependencies:
- charts/global
- core/drupal
- core/once
d3:
remote: 'https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js'
version: 5.16.0
license:
name: BSD
url: 'https://en.wikipedia.org/wiki/BSD_licenses'
gpl-compatible: false
cdn:
/libraries/d3/: https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/
js:
/libraries/d3/d3.min.js: { }
c3:
remote: 'https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.20/c3.min.js'
version: 0.7.20
license:
name: MIT
url: 'https://opensource.org/licenses/MIT'
gpl-compatible: true
cdn:
/libraries/c3/: https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.20/
css:
theme:
/libraries/c3/c3.min.css: { }
js:
/libraries/c3/c3.min.js: { }
dependencies:
- charts_c3/charts_c3
- charts_c3/d3

View File

@@ -0,0 +1,17 @@
<?php
/**
* @file
* Implement module related hooks for the charts_c3 module.
*/
/**
* Implements hook_charts_version3_to_new_settings_structure_alter().
*/
function charts_c3_charts_version3_to_new_settings_structure_alter(array &$new_settings, string $for) {
$config_factory = \Drupal::configFactory();
$c3_config = $config_factory->getEditable('charts_c3.settings');
if ($c3_config) {
$c3_config->delete();
}
}

View File

@@ -0,0 +1,49 @@
{
"name": "drupal/charts_c3",
"description": "Charts module integration with C3 Charts.",
"type": "drupal-module",
"homepage": "http://drupal.org/project/charts",
"support": {
"issues": "https://www.drupal.org/project/issues/charts"
},
"license": "GPL-2.0+",
"require": {
"c3js/c3": "0.7.20",
"d3/d3": "4.9.1"
},
"repositories": [
{
"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"
}
}
}
]
}

View File

@@ -0,0 +1,21 @@
/**
* @file
* JavaScript's integration between C3 and Drupal.
*/
(function (Drupal, once) {
'use strict';
Drupal.behaviors.chartsC3 = {
attach: function (context, settings) {
const contents = new Drupal.Charts.Contents();
once('charts-c3', '.charts-c3', context).forEach(function (element) {
const config = contents.getData(element.id);
c3.generate(config);
if (element.nextElementSibling && element.nextElementSibling.hasAttribute('data-charts-debug-container')) {
element.nextElementSibling.querySelector('code').innerText = JSON.stringify(config, null, ' ');
}
});
}
};
}(Drupal, once));

View File

@@ -0,0 +1,454 @@
<?php
namespace Drupal\charts_c3\Plugin\chart\Library;
use Drupal\charts\Plugin\chart\Library\ChartBase;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Define a concrete class for a Chart.
*
* @Chart(
* id = "c3",
* name = @Translation("C3"),
* types = {
* "area",
* "bar",
* "bubble",
* "column",
* "donut",
* "gauge",
* "line",
* "pie",
* "scatter",
* "spline",
* },
* )
*/
class C3 extends ChartBase implements ContainerFactoryPluginInterface {
/**
* The element info manager.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfo;
/**
* Constructs a \Drupal\views\Plugin\Block\ViewsBlockBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* The element info manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ElementInfoManagerInterface $element_info) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->elementInfo = $element_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('element_info')
);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['intro_text'] = [
'#markup' => $this->t('<p>This is a placeholder for C3.js-specific library options. If you would like to help build this out, please work from <a href="@issue_link">this issue</a>.</p>', [
'@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;
}
}

View File

@@ -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

View File

@@ -0,0 +1,5 @@
polarArea:
label: 'Polar Area'
axis: 'y_only'
axis_inverted: false
stacking: false

View File

@@ -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

View File

@@ -0,0 +1,95 @@
<?php
/**
* @file
* Installation and uninstallation functions.
*/
/**
* Implements hook_requirements().
*/
function charts_chartjs_requirements($phase) {
$requirements = [];
switch ($phase) {
case 'runtime':
$library_path = charts_chartjs_find_library();
$config = \Drupal::config('charts.settings');
$cdn = $config->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/<domain>/*.
$container = \Drupal::getContainer();
$site_path = $container->getParameter('site.path');
$site_path = explode('/', $site_path);
$site_name = $site_path[1];
$searchdir[] = $site_name . '/libraries';
foreach ($searchdir as $dir) {
if (file_exists($dir . '/chart.js/dist/chart.js') || file_exists($dir . '/chart.js/dist/chart.umd.js')) {
return $dir . '/chartjs';
}
}
return FALSE;
}

View File

@@ -0,0 +1,26 @@
charts_chartjs:
remote: 'https://github.com/chartjs/Chart.js'
version: VERSION
license:
name: 'Apache 2.0'
url: 'https://www.apache.org/licenses/LICENSE-2.0'
gpl-compatible: false
# @todo
cdn:
/libraries/chart.js/: https://unpkg.com/chart.js@4.4.0/
/libraries/chartjs-adapter-date-fns/: https://unpkg.com/chartjs-adapter-date-fns@3.0.0/
/libraries/chartjs-plugin-datalabels/: https://unpkg.com/chartjs-plugin-datalabels@2.0.0/
js:
/libraries/chart.js/dist/chart.umd.js: { }
/libraries/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.js: { }
/libraries/chartjs-plugin-datalabels/dist/chartjs-plugin-datalabels.js: { }
dependencies:
- core/drupal
- core/once
chartjs:
version: 1.x
js:
js/charts_chartjs.js: { weight: -1 }
dependencies:
- charts/global
- charts_chartjs/charts_chartjs

View File

@@ -0,0 +1,32 @@
<?php
/**
* @file
* Implement module related hooks for the charts_chartjs module.
*/
/**
* Implements hook_charts_version3_to_new_settings_structure_alter().
*/
function charts_chartjs_charts_version3_to_new_settings_structure_alter(array &$new_settings, string $for) {
$config_factory = \Drupal::configFactory();
$is_config = $for === 'config';
$chartjs_config = $is_config ? $config_factory->getEditable('charts_chartjs.settings') : NULL;
if (!$is_config || !$chartjs_config || empty($new_settings['library']) || $new_settings['library'] !== 'chartjs') {
if ($chartjs_config) {
$chartjs_config->delete();
}
return;
}
$new_settings['library_config'] = [
'xaxis' => [
'autoskip' => TRUE,
'horizontal_axis_title_align' => 'start',
],
'yaxis' => [
'vertical_axis_title_align' => 'start',
],
];
$chartjs_config->delete();
}

View File

@@ -0,0 +1,38 @@
{
"name": "drupal/charts_chartjs",
"description": "Charts module integration with Chart.js Charts.",
"type": "drupal-module",
"homepage": "http://drupal.org/project/charts",
"support": {
"issues": "https://www.drupal.org/project/issues/charts"
},
"license": "GPL-2.0+",
"repositories": {
"asset-packagist": {
"type": "composer",
"url": "https://asset-packagist.org"
}
},
"require": {
"oomphinc/composer-installers-extender": "^2",
"npm-asset/chart.js": "^4.4",
"npm-asset/chartjs-adapter-date-fns": "^3.0",
"npm-asset/chartjs-plugin-datalabels": "^2.0"
},
"extra": {
"installer-types": [
"npm-asset"
],
"installer-paths": {
"web/libraries/chartjs": [
"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"
]
}
}
}

View File

@@ -0,0 +1,20 @@
charts.settings.library_plugin.chartjs:
type: charts_settings_library_plugin
mapping:
xaxis:
label: 'X-Axis settings'
type: mapping
mapping:
autoskip:
label: 'Enable autoskip'
type: boolean
horizontal_axis_title_align:
label: 'Horizontal axis title alignment'
type: string
yaxis:
label: 'Y-Axis settings'
type: mapping
mapping:
vertical_axis_title_align:
label: 'Vertical axis title alignment'
type: string

View File

@@ -0,0 +1,51 @@
/**
* @file
* JavaScript integration between Chart.js and Drupal.
*/
(function (Drupal, once) {
'use strict';
function copyAttributes(source, target) {
return Array.from(source.attributes).forEach(attribute => {
target.setAttribute(
attribute.nodeName === 'id' ? 'data-id' : attribute.nodeName,
attribute.nodeValue,
);
});
}
Drupal.behaviors.chartsChartjs = {
attach: function (context) {
const contents = new Drupal.Charts.Contents();
once('load-charts-chartjs', '.charts-chartjs', context).forEach(function (element) {
const chartId = element.id;
// Switching div for canvas element.
const parent = element.parentNode;
const canvas = document.createElement('canvas');
// Transferring the attributes of our source element to the canvas.
copyAttributes(element, canvas);
canvas.id = chartId;
parent.replaceChild(canvas, element);
// Initializing the chart item.
const chart = contents.getData(chartId);
const options = chart['options'];
const enabled_plugins = [];
// If options.plugins.dataLabels.display is set to true, we need to add the plugin to the chart.
if (options.plugins && options.plugins.dataLabels && options.plugins.dataLabels.display) {
enabled_plugins.push(ChartDataLabels);
}
new Chart(chartId, {
type: chart['type'],
data: chart['data'],
plugins: enabled_plugins,
options: options,
});
if (canvas.nextElementSibling && canvas.nextElementSibling.hasAttribute('data-charts-debug-container')) {
canvas.nextElementSibling.querySelector('code').innerText = JSON.stringify(chart, null, ' ');
}
});
}
};
}(Drupal, once));

View File

@@ -0,0 +1,659 @@
<?php
namespace Drupal\charts_chartjs\Plugin\chart\Library;
use Drupal\charts\Plugin\chart\Library\ChartBase;
use Drupal\Component\Utility\Color;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
/**
* Define a concrete class for a Chart.
*
* @Chart(
* id = "chartjs",
* name = @Translation("Chart.js"),
* types = {
* "area",
* "bar",
* "bubble",
* "column",
* "donut",
* "line",
* "pie",
* "polarArea",
* "scatter",
* "spline",
* },
* )
*/
class Chartjs extends ChartBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'xaxis' => [
'autoskip' => TRUE,
'horizontal_axis_title_align' => 'start',
],
'yaxis' => [
'vertical_axis_title_align' => 'start',
],
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['intro_text'] = [
'#markup' => $this->t('<p>This is a placeholder for Chart.js-specific library options. If you would like to add more chartjs specific settings, please work from <a href="@issue_link">this issue</a>.</p>', [
'@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' => '<div data-chartjs-render-wrapper style="display:inline-block;height:' . $element['#height'] . $element['#height_units'] . ';width:' . $element['#width'] . $element['#width_units'] . ';">',
];
$element['#content_suffix']['manual_sizing'] = [
'#type' => 'inline_template',
'#template' => '</div>',
];
}
$chart_definition = $this->populateOptions($element, $chart_definition);
$chart_definition = $this->populateCategories($element, $chart_definition);
$chart_definition = $this->populateDatasets($element, $chart_definition);
$chart_definition = $this->populateAxes($element, $chart_definition);
// Merge in chart raw options.
if (!empty($element['#raw_options'])) {
$chart_definition = NestedArray::mergeDeepArray([
$chart_definition,
$element['#raw_options'],
]);
}
$element['#attached']['library'][] = 'charts_chartjs/chartjs';
$element['#attributes']['class'][] = 'charts-chartjs';
$element['#chart_definition'] = $chart_definition;
return $element;
}
/**
* Populate chart axes.
*
* @param array $element
* The element.
* @param array $chart_definition
* The chart definition.
*
* @return array
* Return the chart definition.
*/
private function populateAxes(array $element, array $chart_definition) {
$stacking = !empty($element['#stacking']) && $element['#stacking'] == 1;
$chart_type = $chart_definition['type'];
$children = Element::children($element);
/*
* Setting defaults based on what Views uses. However, API users may
* have different keys for their X and Y axes.
*/
$axes_info = [];
foreach ($children as $child) {
$type = $element[$child]['#type'];
if ($type === 'chart_xaxis') {
$x_axis_key = $child;
$xaxis_configuration = $this->configuration['xaxis'] ?? [];
if (!in_array($chart_type, $this->getPieStyleTypes())) {
if ($chart_type !== 'radar') {
$chart_definition['options']['scales']['x'] = [
'stacked' => $stacking,
'ticks' => [
'autoSkip' => $xaxis_configuration['autoskip'] ?? 1,
'maxRotation' => $element[$x_axis_key]['#labels_rotation'] ?? 0,
'minRotation' => $element[$x_axis_key]['#labels_rotation'] ?? 0,
],
];
if (!empty($element[$x_axis_key]['#title'])) {
$chart_definition['options']['scales']['x']['title']['display'] = TRUE;
$chart_definition['options']['scales']['x']['title']['text'] = $element[$x_axis_key]['#title'];
$chart_definition['options']['scales']['x']['title']['align'] = $xaxis_configuration['horizontal_axis_title_align'] ?? '';
}
}
}
$axes_info['x'] = [
'element' => $element[$x_axis_key] ?? [],
'config' => $this->configuration['xaxis'] ?? [],
];
}
elseif ($type === 'chart_yaxis') {
$target_axes = array_column($chart_definition['data']['datasets'],'yAxisID');
$y_axis_key = in_array($child, $target_axes) ? $child : 'y';
$axes_info[$y_axis_key] = [
'element' => $element[$child] ?? [],
'config' => $this->configuration['yaxis'] ?? [],
];
}
}
// Build axes options in chart_definition.
if (!in_array($chart_type, $this->getPieStyleTypes())) {
if (!empty($element['#stacking']) && $element['#stacking'] == 1) {
$stacking = TRUE;
}
else {
$stacking = FALSE;
}
if ($chart_type !== 'radar') {
$chart_definition['options']['scales']['x'] = [
'stacked' => $stacking,
'ticks' => [
'autoSkip' => $axes_info['x']['config']['autoskip'] ?? 1,
'maxRotation' => $axes_info['x']['element']['#labels_rotation'] ?? 0,
'minRotation' => $axes_info['x']['element']['#labels_rotation'] ?? 0,
],
];
// Set configured values for each axis.
foreach ($axes_info as $axis_id => $axis_info) {
$axis_element = $axis_info['element'];
$axis_config = $axis_info['config'];
if ($axis_element['#type'] == 'chart_yaxis') {
$chart_definition['options']['scales'][$axis_id] = [
'ticks' => [
'beginAtZero' => NULL,
'maxRotation' => $axes_info[$axis_id]['element']['#labels_rotation'] ?? 0,
'minRotation' => $axes_info[$axis_id]['element']['#labels_rotation'] ?? 0,
],
'maxTicksLimit' => 11,
'precision' => NULL,
'stepSize' => NULL,
'suggestedMax' => NULL,
'suggestedMin' => NULL,
'stacked' => $stacking,
];
}
// Set the axis type.
if (!empty($axis_element['#axis_type'])) {
$chart_definition['options']['scales'][$axis_id]['type'] = $axis_element['#axis_type'];
}
// Set axis position.
if (!empty($axis_element['#opposite'])) {
$chart_definition['options']['scales'][$axis_id]['position'] = 'right';
}
// Set min and max values.
foreach (['min', 'max'] as $range_value) {
if (isset($axis_element["#$range_value"])) {
$chart_definition['options']['scales'][$axis_id][$range_value] = $axis_element["#$range_value"];
}
}
// Set title properties.
if (!empty($axis_element['#title'])) {
$chart_definition['options']['scales'][$axis_id]['title']['display'] = TRUE;
$chart_definition['options']['scales'][$axis_id]['title']['text'] = $axis_element['#title'];
if (!empty($axis_config['vertical_axis_title_align'])) {
$chart_definition['options']['scales'][$axis_id]['title']['align'] = $axis_config['vertical_axis_title_align'];
}
}
// Set title color.
if (!empty($axis_element['#title_color'])) {
$chart_definition['options']['scales'][$axis_id]['title']['color'] = $axis_element['#title_color'];
}
// Set title font.
foreach (['weight', 'style', 'size'] as $font_attribute) {
$config_name = "#title_font_$font_attribute";
if (!empty($axis_element[$config_name])) {
$chart_definition['options']['scales'][$axis_id]['title']['font'][$font_attribute] = $axis_element[$config_name];
}
}
// Set tick color.
foreach (['color', 'rotation'] as $tick_attribute) {
$config_name = "#labels_$tick_attribute";
if (!empty($axis_element[$config_name])) {
$chart_definition['options']['scales'][$axis_id]['ticks'][$tick_attribute] = $axis_element[$config_name];
}
}
// Set tick font.
foreach (['weight', 'style', 'size'] as $font_attribute) {
$config_name = "#labels_font_$font_attribute";
if (!empty($axis_element[$config_name])) {
$chart_definition['options']['scales'][$axis_id]['ticks']['font'][$font_attribute] = $axis_element[$config_name];
}
}
// Set grid line colors.
if (!empty($axis_element['#grid_line_color'])) {
$chart_definition['options']['scales'][$axis_id]['grid']['color'] = $axis_element['#grid_line_color'];
}
if (!empty($axis_element['#base_line_color'])) {
$chart_definition['options']['scales'][$axis_id]['grid']['borderColor'] = $axis_element['#base_line_color'];
}
}
}
}
return $chart_definition;
}
/**
* 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) {
$chart_type = $this->populateChartType($element);
$chart_definition['type'] = $chart_type;
// Horizontal bar charts are configured by changing the bar chart indexAxis.
// See https://chartjs.org/docs/latest/charts/bar.html#horizontal-bar-chart.
if ($element['#chart_type'] === 'bar') {
$chart_definition['options']['indexAxis'] = 'y';
}
$chart_definition['options']['plugins']['title'] = $this->buildTitle($element);
if (!empty($element['#subtitle'])) {
$chart_definition['options']['plugins']['subtitle'] = [
'display' => TRUE,
'text' => $element['#subtitle'],
];
}
$chart_definition['options']['plugins']['tooltip']['enabled'] = $element['#tooltips'];
if (!empty($element['#data_labels'])) {
$chart_definition['options']['plugins']['dataLabels']['display'] = TRUE;
}
$chart_definition['options']['plugins']['legend'] = $this->buildLegend($element);
return $chart_definition;
}
/**
* Populate categories.
*
* @param array $element
* The element.
* @param array $chart_definition
* The chart definition.
*
* @return array
* Return the chart definition.
*/
private function populateCategories(array $element, array $chart_definition) {
$children = Element::children($element);
$categories = [];
foreach ($children as $child) {
$type = $element[$child]['#type'];
if ($type === 'chart_xaxis' && isset($element[$child]['#labels'])) {
$categories = is_array($element[$child]['#labels']) ? array_map('strip_tags', $element[$child]['#labels']) : [];
}
if (in_array($element['#chart_type'], $this->getPieStyleTypes())
&& $type !== 'chart_xaxis') {
if ($element[$child]['#type'] === 'chart_data') {
// Get the first item in each array inside $element[$child]['#data'].
$categories = array_map(function ($item) {
if (!empty($item['color'])) {
unset($item['color']);
}
return gettype($item) === 'array' ? array_values($item) : $item;
}, $element[$child]['#data']);
}
}
// Merge in axis raw options.
if (!empty($element[$child]['#raw_options'])) {
$categories = NestedArray::mergeDeepArray([
$categories,
$element[$child]['#raw_options'],
]);
}
}
$chart_definition['data']['labels'] = $categories;
return $chart_definition;
}
/**
* Populate Dataset.
*
* @param array $element
* The element.
* @param array $chart_definition
* The chart definition.
*
* @return array
* Return the chart definition.
*/
private function populateDatasets(array $element, array $chart_definition) {
$chart_type = $chart_definition['type'];
$datasets = [];
foreach (Element::children($element) as $key) {
if ($element[$key]['#type'] === 'chart_data') {
$series_data = [];
$dataset = [];
// Populate the data.
foreach ($element[$key]['#data'] as $data_index => $data) {
if (isset($series_data[$data_index])) {
$series_data[$data_index][] = $data;
}
else {
if ($chart_type === 'scatter') {
$data = ['y' => $data[1], 'x' => $data[0]];
}
if ($chart_type === 'bubble') {
/*
* The radius is not scaled in Chart.js, so it can look very bad.
* For suggestions about how to deal with this, see:
* https://github.com/chartjs/Chart.js/issues/3355
*/
$data = ['y' => $data[1], 'x' => $data[0], 'r' => $data[2]];
}
// Convert the array from Views when using pie-type charts
// and no label field.
if (in_array($chart_type, $this->getPieStyleTypes()) && !empty($data['color'])) {
$element['#colors'][$data_index] = $data['color'];
unset($data['color']);
$data = array_values($data);
}
/*
* This is here to account for differences between Views and
* the API. Will change if someone can find a better way.
*/
if (in_array($chart_type, $this->getPieStyleTypes()) && !empty($data[1])) {
$data = $data[1];
}
$series_data[$data_index] = $data;
}
}
if (!empty($element[$key]['#target_axis'])) {
$dataset['yAxisID'] = $element[$key]['#target_axis'];
}
$dataset['label'] = $element[$key]['#title'];
$dataset['data'] = $series_data;
// Set the background and border color.
if (!empty($element[$key]['#color'])) {
if (!in_array($chart_type, $this->getPieStyleTypes())) {
$dataset['borderColor'] = $element[$key]['#color'];
}
$dataset['backgroundColor'] = $element[$key]['#color'];
}
if (in_array($chart_type, $this->getPieStyleTypes()) && !empty($element['#colors'])) {
$dataset['backgroundColor'] = $element['#colors'];
}
$series_type = isset($element[$key]['#chart_type']) ? $this->populateChartType($element[$key]) : $chart_type;
$dataset['type'] = $series_type;
if (!empty($element[$key]['#chart_type']) && $element[$key]['#chart_type'] === 'area') {
$dataset['fill'] = 'origin';
$dataset['backgroundColor'] = $this->getTranslucentColor($element[$key]['#color']);
}
elseif ($element['#chart_type'] === 'area') {
$dataset['fill'] = 'origin';
$dataset['backgroundColor'] = $this->getTranslucentColor($element[$key]['#color']);
}
else {
$dataset['fill'] = FALSE;
}
// Merge in dataset raw options.
if (!empty($element[$key]['#raw_options'])) {
$dataset = NestedArray::mergeDeepArray([
$dataset,
$element[$key]['#raw_options'],
]);
}
$datasets[] = $dataset;
}
}
$chart_definition['data']['datasets'] = $datasets;
return $chart_definition;
}
/**
* Outputs a type that can be used by Chart.js.
*
* @param array $element
* The given element.
*
* @return string
* The generated type.
*/
protected function populateChartType(array $element) {
switch ($element['#chart_type']) {
case 'bar':
case 'column':
$type = 'bar';
break;
case 'area':
case 'spline':
$type = 'line';
break;
case 'donut':
$type = 'doughnut';
break;
case 'gauge':
// Gauge is currently not supported by Chart.js.
$type = 'donut';
break;
default:
$type = $element['#chart_type'];
break;
}
if (isset($element['#polar']) && $element['#polar'] == 1) {
if ($element['#chart_type'] === 'area' || $element['#chart_type'] === 'polarArea') {
$type = 'polarArea';
}
else {
$type = 'radar';
}
}
return $type;
}
/**
* Builds legend based on element properties.
*
* @param array $element
* The element.
*
* @return array
* The legend array.
*/
protected function buildLegend(array $element) {
$legend = [];
// Configure the legend display.
$legend['display'] = (bool) $element['#legend'];
// Configure legend position.
if (!empty($element['#legend_position'])) {
$legend['position'] = $element['#legend_position'];
if (!empty($element['#legend_font_weight'])) {
$legend['labels']['font']['weight'] = $element['#legend_font_weight'];
}
if (!empty($element['#legend_font_style'])) {
$legend['labels']['font']['style'] = $element['#legend_font_style'];
}
if (!empty($element['#legend_font_size'])) {
$legend['labels']['font']['size'] = $element['#legend_font_size'];
}
}
return $legend;
}
/**
* Builds title based on element properties.
*
* @param array $element
* The element.
*
* @return array
* The title array.
*/
protected function buildTitle(array $element) {
$title = [];
if (!empty($element['#title'])) {
$title = [
'display' => TRUE,
'text' => $element['#title'],
];
if (!empty($element['#title_position'])) {
if (in_array($element['#title_position'], ['in', 'out'])) {
$title['position'] = 'top';
}
else {
$title['position'] = $element['#title_position'];
}
}
if (!empty($element['#title_color'])) {
$title['color'] = $element['#title_color'];
}
if (!empty($element['#title_font_weight'])) {
$title['font']['weight'] = $element['#title_font_weight'];
}
if (!empty($element['#title_font_style'])) {
$title['font']['style'] = $element['#title_font_style'];
}
if (!empty($element['#title_font_size'])) {
$title['font']['size'] = $element['#title_font_size'];
}
}
return $title;
}
/**
* Get translucent color.
*
* @param string $color
* The color.
*
* @return string
* The color.
*/
protected function getTranslucentColor($color) {
if (!$color) {
return '';
}
$rgb = Color::hexToRgb($color);
return 'rgba(' . implode(",", $rgb) . ',' . 0.5 . ')';
}
/**
* Returns pie-style chart types.
*
* @return array
* An array of pie-style chart types.
*/
private static function getPieStyleTypes(): array {
return [
'pie',
'doughnut',
'polarArea',
];
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\Tests\charts_chartjs\Kernel;
use Drupal\Tests\charts\Kernel\DimensionsTest as BaseDimensionsTest;
/**
* Test the dimensions element property behavior for the chartjs library.
*
* @group charts
*/
class DimensionsTest extends BaseDimensionsTest {
/**
* {@inheritdoc}
*/
protected static $modules = [
'charts',
'charts_chartjs',
];
/**
* Test that the set dimensions are correctly added on the element.
*
* @param array $element
* The element to be tested.
* @param string|null $expected_width
* The expected width to be found on the element.
* @param string|null $expected_height
* The expected height to be found on the element.
*
* @dataProvider provideChartElements
*/
public function testDimensions(array $element, string $expected_width = NULL, string $expected_height = NULL) {
$element['#chart_library'] = 'chartjs';
$styles = $this->getChartStyle($element, '[data-chartjs-render-wrapper]');
$width = $styles['width'] ?? NULL;
$height = $styles['height'] ?? NULL;
$this->assertEquals($expected_width, $width);
$this->assertEquals($expected_height, $height);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\Tests\charts_chartjs\Kernel;
use Drupal\Tests\charts\Kernel\ChartsKernelTestBase;
/**
* Tests the raw_options element property behavior.
*
* @group charts
*/
class RawOptionsTest extends ChartsKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'charts',
'charts_chartjs',
];
/**
* Test that the raw options settings can override main definition.
*/
public function testRawOptionsOverride() {
$element = [
'#type' => 'chart',
'#chart_type' => 'bar',
'#stacking' => 1,
'#raw_options' => [
'options' => [
'scales' => [
'x' => ['stacked' => FALSE],
],
],
],
];
$path = ['options', 'scales', 'x', 'stacked'];
$this->assertJsonPropertyHasValue($element, $path, FALSE);
}
}

View File

@@ -0,0 +1,42 @@
#Installation using Composer (recommended)
If you use Composer to manage dependencies, edit your site's "composer.json"
file as follows.
1. Run `composer require --prefer-dist composer/installers` 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.
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": "google/charts",
"version": "45",
"type": "drupal-library",
"extra": {
"installer-name": "google_charts"
},
"dist": {
"url": "https://www.gstatic.com/charts/loader.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
}
4. Run `composer require --prefer-dist google/charts:45` - you should find
that new directories have been created under "/libraries"
**Please note**: if you observe an SSL error when trying to download this
library, you can address this by changing the "url" in the code above to
"http://www.gstatic.com/charts/loader.js" and by adding in:
"secure-http": false

View File

@@ -0,0 +1,5 @@
table:
label: 'Table'
axis: 'xy'
axis_inverted: false
stacking: false

View File

@@ -0,0 +1,12 @@
name: 'Google Charts'
type: module
description: 'Charts module integration with Google 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

View File

@@ -0,0 +1,71 @@
<?php
/**
* @file
* Installation and uninstallation functions.
*/
/**
* Implements hook_requirements().
*/
function charts_google_requirements($phase) {
$requirements = [];
switch ($phase) {
case 'runtime':
$library_path = charts_google_find_library();
if (!$library_path) {
$requirements['charts_google_js'] = [
'title' => t('Google Charts Library'),
'value' => t('Not Installed'),
'severity' => REQUIREMENT_WARNING,
'description' => t('You are missing the Google Charts library in your Drupal installation directory. Please see the README file inside charts_google for instructions to install the library.'),
];
}
else {
$requirements['charts_google_js'] = [
'title' => t('Google Charts Library'),
'severity' => REQUIREMENT_OK,
'value' => t('Installed'),
];
}
break;
}
return $requirements;
}
/**
* Get the location of the Google Charts library.
*
* @return string
* The location of the library, or FALSE if the library isn't installed.
*/
function charts_google_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/<domain>/*.
$container = \Drupal::getContainer();
$site_path = $container->getParameter('site.path');
$site_path = explode('/', $site_path);
$site_name = $site_path[1];
$searchdir[] = $site_name . '/libraries';
foreach ($searchdir as $dir) {
if (file_exists($dir . '/google_charts/loader.js')) {
return $dir . '/google_charts';
}
}
return FALSE;
}

View File

@@ -0,0 +1,22 @@
charts_google:
version: 1.x
js:
js/charts_google.js: { weight: -1 }
dependencies:
- core/drupal
- core/drupalSettings
- charts/global
- core/once
google:
remote: 'https://www.gstatic.com/charts/loader.js'
version: VERSION
license:
name: 'Apache 2.0'
url: 'http://www.apache.org/licenses/LICENSE-2.0'
gpl-compatible: false
cdn:
/libraries/google_charts/: https://www.gstatic.com/charts/
js:
/libraries/google_charts/loader.js: { }
dependencies:
- charts_google/charts_google

View File

@@ -0,0 +1,17 @@
<?php
/**
* @file
* Implement module related hooks for the charts_google module.
*/
/**
* Implements hook_charts_version3_to_new_settings_structure_alter().
*/
function charts_google_charts_version3_to_new_settings_structure_alter(array &$new_settings, string $for) {
$config_factory = \Drupal::configFactory();
$google_config = $config_factory->getEditable('charts_google.settings');
if ($google_config) {
$google_config->delete();
}
}

View File

@@ -0,0 +1,33 @@
{
"name": "drupal/charts_google",
"description": "Charts module integration with Google Charts.",
"type": "drupal-module",
"homepage": "http://drupal.org/project/charts",
"support": {
"issues": "https://www.drupal.org/project/issues/charts"
},
"license": "GPL-2.0+",
"require": {
"google/charts": "45"
},
"repositories": {
"google_charts": {
"type": "package",
"package": {
"name": "google/charts",
"version": "45",
"type": "drupal-library",
"extra": {
"installer-name": "google_charts"
},
"dist": {
"url": "https://www.gstatic.com/charts/loader.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
}
}
}

View File

@@ -0,0 +1,6 @@
charts.settings.library_plugin.google:
type: charts_settings_library_plugin
mapping:
use_material_design:
label: 'Use Material Design'
type: boolean

View File

@@ -0,0 +1,187 @@
/**
* @file
* JavaScript integration between Google and Drupal.
*/
(function (Drupal, drupalSettings, once) {
'use strict';
Drupal.googleCharts = Drupal.googleCharts || {charts: []};
/**
* Behavior to initialize Google Charts.
*
* @type {{attach: Drupal.behaviors.chartsGooglecharts.attach}}
*/
Drupal.behaviors.chartsGooglecharts = {
attach: function (context) {
// Define a fallback value for globalOptions;
const globalOptions = drupalSettings.charts === undefined ? {
useMaterialDesign: false,
chartType: 'bar'
} : drupalSettings.charts.google.global_options;
let useMaterialDesign = globalOptions.useMaterialDesign;
let chartType = globalOptions.chartType;
const materialDesignPackages = ['bar', 'line', 'spline', 'scatter', 'column',];
const packages = ['corechart', 'gauge', 'table',];
if (useMaterialDesign === 'true' && materialDesignPackages.indexOf(chartType) !== -1) {
if (chartType === 'spline') {
chartType = 'line';
}
if (chartType === 'column') {
chartType = 'bar';
}
packages.push(chartType);
}
// Load Google Charts API.
google.charts.load('current', {packages: packages});
// Re-draw charts if viewport size has been changed.
window.addEventListener('resize', function () {
Drupal.googleCharts.waitForFinalEvent(function () {
// Re-draw Google Charts.
Drupal.googleCharts.drawCharts();
}, 200, 'reload-google-charts');
});
// Draw Google Charts.
Drupal.googleCharts.drawCharts();
}
};
/**
* Helper function to draw Google Charts.
*/
Drupal.googleCharts.drawCharts = function () {
const contents = new Drupal.Charts.Contents();
once('load-google-charts-item', '.charts-google').forEach(function (element) {
if (element.dataset.hasOwnProperty('chart')) {
const chartId = element.id;
const dataAttributes = contents.getData(chartId);
google.charts.setOnLoadCallback(Drupal.googleCharts.drawChart(chartId, dataAttributes['visualization'], dataAttributes['data'], dataAttributes['options']));
if (element.nextElementSibling && element.nextElementSibling.hasAttribute('data-charts-debug-container')) {
element.nextElementSibling.querySelector('code').innerText = JSON.stringify(dataAttributes, null, ' ');
}
}
});
};
/**
* Helper function to draw a Google Chart.
*
* @param {string} chartId - Chart Id.
* @param {string} chartType - Chart Type.
* @param {string} dataTable - Data.
* @param {string} googleChartOptions - Options.
*
* @return {function} Draw Chart.
*/
Drupal.googleCharts.drawChart = function (chartId, chartType, dataTable, googleChartOptions) {
return function () {
const data = google.visualization.arrayToDataTable(dataTable);
const options = googleChartOptions;
const googleChartTypeFormatted = chartType;
let visualizationNamespace = 'visualization';
let visualizationClass = chartType;
// Replace the 'Spline' chart type with 'Line'.
if (visualizationClass === 'SplineChart') {
visualizationClass = 'LineChart';
}
if (options.theme === 'material') {
// Material Design wants to use the 'charts' namespace.
visualizationNamespace = 'charts';
// Strip the 'Chart' suffix from the chart type.
visualizationClass = visualizationClass.replace('Chart', '');
// Replace the 'Column' chart type with 'Bar'.
if (visualizationClass === 'Column') {
visualizationClass = 'Bar';
}
}
console.log(visualizationClass);
let chart;
switch (googleChartTypeFormatted) {
case 'BarChart':
case 'ColumnChart':
case 'LineChart':
case 'SplineChart':
case 'ScatterChart':
chart = new google[visualizationNamespace][visualizationClass](document.getElementById(chartId));
break;
case 'DonutChart':
case 'PieChart':
chart = new google.visualization.PieChart(document.getElementById(chartId));
break;
case 'BubbleChart':
chart = new google.visualization.BubbleChart(document.getElementById(chartId));
break;
case 'AreaChart':
chart = new google.visualization.AreaChart(document.getElementById(chartId));
break;
case 'Gauge':
chart = new google.visualization.Gauge(document.getElementById(chartId));
break;
case 'ComboChart':
chart = new google.visualization.ComboChart(document.getElementById(chartId));
break;
case 'GeoChart':
chart = new google.visualization.GeoChart(document.getElementById(chartId));
break;
case 'TableChart':
chart = new google.visualization.Table(document.getElementById(chartId));
}
let color_regex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
if (options['colors'] && options['colors'].length > 10) {
for (const i in options['colors']) {
if (!color_regex.test(options['colors'][i])) {
options['colors'][i] = '#FFFFFF';
}
}
}
// Rewrite the colorAxis item to include the colors: key
if (typeof options['colorAxis'] != 'undefined') {
const colors = options['colorAxis'];
const num_colors = colors.length;
options['colorAxis'] = colors.splice(num_colors);
options['colorAxis'] = {colors: colors};
}
chart.draw(data, options);
};
};
/**
* Helper function to run a callback function once when triggering an event
* multiple times.
*
* Example usage:
* @code
* window.addEventListener('resize', function () {
* Drupal.googleCharts.waitForFinalEvent(function(){
* alert('Resize...');
* }, 500, "some unique string");
* });
* @endcode
*/
Drupal.googleCharts.waitForFinalEvent = (function () {
let timers = {};
return function (callback, ms, uniqueId) {
if (!uniqueId) {
uniqueId = "Don't call this twice without a uniqueId";
}
if (timers[uniqueId]) {
clearTimeout(timers[uniqueId]);
}
timers[uniqueId] = setTimeout(callback, ms);
};
})();
}(Drupal, drupalSettings, once));

View File

@@ -0,0 +1,600 @@
<?php
namespace Drupal\charts_google\Plugin\chart\Library;
use Drupal\charts\Element\Chart as ChartElement;
use Drupal\charts\Plugin\chart\Library\ChartBase;
use Drupal\charts\TypeManager;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Define a concrete class for a Chart.
*
* @Chart(
* id = "google",
* name = @Translation("Google"),
* types = {
* "area",
* "bar",
* "bubble",
* "column",
* "donut",
* "gauge",
* "line",
* "pie",
* "scatter",
* "spline",
* },
* )
*/
class Google extends ChartBase implements ContainerFactoryPluginInterface {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The element info manager.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfo;
/**
* The chart type manager.
*
* @var \Drupal\charts\TypeManager
*/
protected $chartTypeManager;
/**
* Constructs a \Drupal\views\Plugin\Block\ViewsBlockBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The element info manager.
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* The element info manager.
* @param \Drupal\charts\TypeManager $chart_type_manager
* The chart type manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler, ElementInfoManagerInterface $element_info, TypeManager $chart_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moduleHandler = $module_handler;
$this->elementInfo = $element_info;
$this->chartTypeManager = $chart_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('element_info'),
$container->get('plugin.manager.charts_type')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'use_material_design' => FALSE,
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['placeholder'] = [
'#title' => $this->t('Placeholder'),
'#type' => 'fieldset',
'#description' => $this->t(
'This is a placeholder for Google-specific library options. If you would like to help build this out, please work from <a href="@issue_link">this issue</a>.', [
'@issue_link' => Url::fromUri('https://www.drupal.org/project/charts/issues/3046980')
->toString(),
]),
];
$form['use_material_design'] = [
'#title' => $this->t('Use Material Design'),
'#type' => 'checkbox',
'#default_value' => $this->configuration['use_material_design'] ?? FALSE,
'#description' => $this->t('Use Material Design for charts.'),
];
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['use_material_design'] = $values['use_material_design'];
}
}
/**
* {@inheritdoc}
*/
public function preRender(array $element) {
$chart_definition = [];
// Convert the chart renderable to a proper definition.
$chart_definition['visualization'] = $this->chartsGoogleVisualizationType($element['#chart_type']);
$chart_definition = $this->chartsGooglePopulateChartOptions($element, $chart_definition);
$chart_definition = $this->chartsGooglePopulateChartAxes($element, $chart_definition);
$chart_definition = $this->chartsGooglePopulateChartData($element, $chart_definition);
if (!isset($element['#id'])) {
$element['#id'] = Html::getUniqueId('google-chart-render');
}
if (!empty($element['#height']) || !empty($element['#width'])) {
$element['#attributes']['style'] = 'height:' . $element['#height'] . $element['#height_units'] . ';width:' . $element['#width'] . $element['#width_units'] . ';';
}
// Trim out empty options.
ChartElement::trimArray($chart_definition['options']);
$element['#attached']['library'][] = 'charts_google/google';
$element['#attributes']['class'][] = 'charts-google';
$element['#chart_definition'] = $chart_definition;
// Setting global options.
$element['#attached']['drupalSettings']['charts']['google']['global_options'] = [
'useMaterialDesign' => !empty($this->configuration['use_material_design']) ? 'true' : 'false',
'chartType' => $element['#chart_type'],
];
return $element;
}
/**
* Utility to convert a Drupal renderable type to a Google visualization type.
*/
public function chartsGoogleVisualizationType($renderable_type) {
$types = [
'area' => 'AreaChart',
'bar' => 'BarChart',
'column' => 'ColumnChart',
'line' => 'LineChart',
'spline' => 'SplineChart',
'pie' => 'PieChart',
'donut' => 'DonutChart',
'gauge' => 'Gauge',
'scatter' => 'ScatterChart',
'bubble' => 'BubbleChart',
'geo' => 'GeoChart',
'table' => 'TableChart',
];
$this->moduleHandler->alter('charts_google_visualization_types', $types);
return $types[$renderable_type] ?? FALSE;
}
/**
* Utility to populate main chart options.
*
* @param array $element
* The element.
* @param array $chart_definition
* The chart definition.
*
* @return array
* The returned chart definition.
*/
public function chartsGooglePopulateChartOptions(array $element, array $chart_definition) {
if (!empty($this->configuration['use_material_design'])) {
$chart_definition['options']['theme'] = 'material';
$chart_definition['options']['chart']['title'] = $element['#title'] ?? NULL;
$chart_definition['options']['chart']['subtitle'] = $element['#subtitle'] ?? NULL;
if ($element['#chart_type'] === 'bar') {
$chart_definition['options']['bars'] = 'horizontal';
}
}
else {
$title = $element['#title'] ?? NULL;
if ($title && !empty($element['#subtitle'])) {
$title .= ': ' . $element['#subtitle'];
}
$chart_definition['options']['title'] = $title;
}
$chart_definition['options']['titleTextStyle']['color'] = $element['#title_color'];
$chart_definition['options']['titleTextStyle']['bold'] = $element['#title_font_weight'] === 'bold';
$chart_definition['options']['titleTextStyle']['italic'] = $element['#title_font_style'] === 'italic';
$chart_definition['options']['titleTextStyle']['fontSize'] = $element['#title_font_size'];
$chart_definition['options']['titlePosition'] = $element['#title_position'];
$chart_definition['options']['colors'] = $element['#colors'];
$chart_definition['options']['fontName'] = $element['#font'];
$chart_definition['options']['fontSize'] = $element['#font_size'];
$chart_definition['options']['backgroundColor']['fill'] = $element['#background'];
$chart_definition['options']['isStacked'] = (bool) $element['#stacking'];
$chart_definition['options']['tooltip']['trigger'] = $element['#tooltips'] ? 'focus' : 'none';
$chart_definition['options']['tooltip']['isHtml'] = (bool) $element['#tooltips_use_html'];
$chart_definition['options']['pieSliceText'] = $element['#data_labels'] ? NULL : 'none';
$chart_definition['options']['legend']['position'] = $element['#legend_position'] ?? 'none';
$chart_definition['options']['legend']['alignment'] = 'center';
$chart_definition['options']['interpolateNulls'] = TRUE;
if ($element['#chart_type'] === 'gauge') {
$chart_definition['options']['redFrom'] = $element['#gauge']['red_from'];
$chart_definition['options']['redTo'] = $element['#gauge']['red_to'];
$chart_definition['options']['yellowFrom'] = $element['#gauge']['yellow_from'];
$chart_definition['options']['yellowTo'] = $element['#gauge']['yellow_to'];
$chart_definition['options']['GreenFrom'] = $element['#gauge']['green_from'];
$chart_definition['options']['greenTo'] = $element['#gauge']['green_to'];
$chart_definition['options']['min'] = (int) $element['#gauge']['min'];
$chart_definition['options']['max'] = (int) $element['#gauge']['max'];
}
if ($element['#chart_type'] === 'donut') {
$chart_definition['options']['pieHole'] = 0.4;
}
if ($element['#chart_type'] === 'bubble') {
$chart_definition['options']['bubble']['textStyle']['fontSize'] = 11;
}
// @todo Legend title (and thus these properties) not supported by Google.
$chart_definition['options']['legend']['title'] = $element['#legend_title'];
$chart_definition['options']['legend']['titleTextStyle']['bold'] = $element['#legend_title_font_weight'] === 'bold';
$chart_definition['options']['legend']['titleTextStyle']['italic'] = $element['#legend_title_font_style'] === 'italic';
$chart_definition['options']['legend']['titleTextStyle']['fontSize'] = $element['#legend_title_font_size'];
$chart_definition['options']['legend']['textStyle']['bold'] = $element['#legend_font_weight'] === 'bold';
$chart_definition['options']['legend']['textStyle']['italic'] = $element['#legend_font_style'] === 'italic';
$chart_definition['options']['legend']['textStyle']['fontSize'] = $element['#legend_font_size'];
// If your labels are truncated, you may need to add (try adjusting the %).
// $chart_definition['options']['chartArea']['height'] = '50%'.
$chart_definition['options']['animation']['duration'] = 10000;
$chart_definition['options']['animation']['easing'] = 'out';
// Merge in chart raw options.
if (!empty($element['#raw_options'])) {
$chart_definition = NestedArray::mergeDeepArray([
$chart_definition,
$element['#raw_options'],
]);
}
return $chart_definition;
}
/**
* Utility to populate chart axes.
*
* @param array $element
* The element.
* @param array $chart_definition
* The chart definition.
*
* @return array
* Return the chart definition.
*/
public function chartsGooglePopulateChartAxes(array $element, array $chart_definition) {
foreach (Element::children($element) as $key) {
if ($element[$key]['#type'] === 'chart_xaxis' || $element[$key]['#type'] === 'chart_yaxis') {
// Make sure defaults are loaded.
if (empty($element[$key]['#defaults_loaded'])) {
$element[$key] += $this->elementInfo->getInfo($element[$key]['#type']);
}
// Populate the chart data.
$axis = [];
$axis['title'] = $element[$key]['#title'] ?? '';
$axis['titleTextStyle']['color'] = $element[$key]['#title_color'];
$axis['titleTextStyle']['bold'] = $element[$key]['#title_font_weight'] === 'bold';
$axis['titleTextStyle']['italic'] = $element[$key]['#title_font_style'] === 'italic';
$axis['titleTextStyle']['fontSize'] = $element[$key]['#title_font_size'];
// In Google, the row column of data is used as labels.
if ($element[$key]['#labels'] && $element[$key]['#type'] === 'chart_xaxis') {
foreach ($element[$key]['#labels'] as $label_key => $label) {
$chart_definition['data'][$label_key + 1][0] = $label;
}
}
$axis['textStyle']['color'] = $element[$key]['#labels_color'];
$axis['textStyle']['bold'] = $element[$key]['#labels_font_weight'] === 'bold';
$axis['textStyle']['italic'] = $element[$key]['#labels_font_style'] === 'italic';
$axis['textStyle']['fontSize'] = $element[$key]['#labels_font_size'];
$axis['slantedText'] = !empty($element[$key]['#labels_rotation']) ? TRUE : NULL;
$axis['slantedTextAngle'] = $element[$key]['#labels_rotation'];
$axis['gridlines']['color'] = $element[$key]['#grid_line_color'];
$axis['baselineColor'] = $element[$key]['#base_line_color'];
$axis['minorGridlines']['color'] = $element[$key]['#minor_grid_line_color'];
$axis['viewWindowMode'] = isset($element[$key]['#max']) ? 'explicit' : NULL;
$axis['viewWindow']['max'] = !empty($element[$key]['#max']) ? (int) $element[$key]['#max'] : NULL;
$axis['viewWindow']['min'] = !empty($element[$key]['#min']) ? (int) $element[$key]['#min'] : NULL;
// Merge in axis raw options.
if (!empty($element[$key]['#raw_options'])) {
$axis = NestedArray::mergeDeepArray([
$axis,
$element[$key]['#raw_options'],
]);
}
// Multi-axis support only applies to the major axis in Google charts.
$chart_type = $this->chartTypeManager->getDefinition($element['#chart_type']);
$axis_index = $element[$key]['#opposite'] ? 1 : 0;
if ($element[$key]['#type'] === 'chart_xaxis') {
$axis_keys = !$chart_type['axis_inverted'] ? ['hAxis'] : [
'vAxes',
$axis_index,
];
}
else {
$axis_keys = !$chart_type['axis_inverted'] ? ['vAxes', $axis_index] : ['hAxis'];
}
$axis_drilldown = &$chart_definition['options'];
foreach ($axis_keys as $axis_key) {
$axis_drilldown = &$axis_drilldown[$axis_key];
}
$axis_drilldown = $axis;
}
}
return $chart_definition;
}
/**
* Utility to populate chart data.
*
* @param array $element
* The element.
* @param array $chart_definition
* The chart definition.
*
* @return array
* Return the chart definition.
*/
public function chartsGooglePopulateChartData(array &$element, array $chart_definition) {
$chart_definition['options']['series'] = [];
$chart_type = $this->chartTypeManager->getDefinition($element['#chart_type']);
$series_number = 0;
foreach (Element::children($element) as $key) {
if ($element[$key]['#type'] === 'chart_data') {
$series = [];
// Make sure defaults are loaded.
if (empty($element[$key]['#defaults_loaded'])) {
$element[$key] += $this->elementInfo->getInfo($element[$key]['#type']);
}
// Convert target named axis keys to integers.
$axis_index = 0;
if (isset($element[$key]['#target_axis'])) {
$axis_name = $element[$key]['#target_axis'];
foreach (Element::children($element) as $axis_key) {
$multi_axis_type = $chart_type['axis_inverted'] ? 'chart_xaxis' : 'chart_yaxis';
if ($element[$axis_key]['#type'] === $multi_axis_type) {
if ($axis_key === $axis_name) {
break;
}
$axis_index++;
}
}
$series['targetAxisIndex'] = $axis_index;
}
// Allow data to provide the labels. This will override the axis
// settings.
if ($element[$key]['#labels']) {
foreach ($element[$key]['#labels'] as $label_index => $label) {
$chart_definition['data'][$label_index + 1][0] = $label;
}
}
if ($element[$key]['#title']) {
$chart_definition['data'][0][$series_number + 1] = $element[$key]['#title'];
}
foreach ($element[$key]['#data'] as $index => $data_value) {
// Nested array values typically used for scatter charts. This weird
// approach leaves columns empty in order to make arbitrary pairings.
// See https://developers.google.com/chart/interactive/docs/gallery/scatterchart#Data_Format
if (is_array($data_value)) {
if ($chart_type['id'] === 'scatter') {
$chart_definition['data'][] = [
0 => $data_value[0],
$series_number + 1 => $data_value[1],
];
}
elseif ($chart_type['id'] === 'bubble') {
$chart_definition['data'][] = [
0 => $element[$key]['#title'],
1 => $data_value[0],
$series_number + 2 => $data_value[1],
$series_number + 3 => $data_value[2],
];
}
else {
if (!empty($data_value['color']) && (in_array($chart_type['id'], [
'pie',
'donut',
]))) {
$chart_definition['options']['slices'][$index]['color'] = $data_value['color'];
unset($data_value['color']);
$data_value = array_values($data_value);
}
$chart_definition['data'][$index + 1] = $data_value;
}
}
// Most charts provide a single-dimension array of values.
else {
if ($chart_type['id'] === 'gauge') {
$data_value = [$element[$key]['#title'], $data_value];
$chart_definition['data'][$index + 1] = $data_value;
}
else {
$chart_definition['data'][$index + 1][$series_number + 1] = $data_value;
}
}
}
$series['color'] = $element[$key]['#color'];
// Scatter charts are not visible if the data_markers are disabled.
if ($chart_type['id'] === 'scatter') {
$element['#data_markers'] = TRUE;
}
$series['pointSize'] = !empty($element['#data_markers']) ? 3 : 0;
$series['visibleInLegend'] = $element[$key]['#show_in_legend'];
// Labels only supported on pies.
$series['pieSliceText'] = $element[$key]['#show_labels'] ? 'label' : 'none';
// These properties are not real Google Charts properties. They are
// utilized by the formatter in charts_google.js.
$decimal_count = $element[$key]['#decimal_count'] ? '.' . str_repeat('0', $element[$key]['#decimal_count']) : '';
$prefix = $this->chartsGoogleEscapeIcuCharacters($element[$key]['#prefix'] ?? '');
$suffix = $this->chartsGoogleEscapeIcuCharacters($element[$key]['#suffix'] ?? '');
$format = $prefix . '#' . $decimal_count . $suffix;
$series['_format']['format'] = $format;
// @todo Convert this from PHP's date format to ICU format.
// See https://developers.google.com/chart/interactive/docs/reference#dateformatter.
// $series['_format']['dateFormat'] = $element[$key]['#date_format'];
// Conveniently only the axis that supports multiple axes is the one;
// that can receive formatting, so we know that the key will;
// always be plural.
$axis_type = $chart_type['axis_inverted'] ? 'hAxes' : 'vAxes';
$chart_definition['options'][$axis_type][$axis_index]['format'] = $format;
// Convert to a ComboChart if mixing types.
// See https://developers.google.com/chart/interactive/docs/gallery/combochart?hl=en.
if ($element[$key]['#chart_type'] && !in_array($element[$key]['#chart_type'], ['bubble', 'scatter'])) {
// Oddly Google calls a "column" chart a "bars" series.
// Using actual bar.
// charts is not supported in combo charts with Google.
$main_chart_type = $element['#chart_type'] === 'column' ? 'bars' : $element['#chart_type'];
$chart_definition['visualization'] = 'ComboChart';
$chart_definition['options']['seriesType'] = $main_chart_type;
$data_chart_type = $element[$key]['#chart_type'] === 'column' ? 'bars' : $element[$key]['#chart_type'];
$series['type'] = $data_chart_type;
}
// Merge in point raw options.
if (!empty($data_item['#raw_options'])) {
$series = NestedArray::mergeDeepArray([
$series,
$data_item['#raw_options'],
]);
}
// Add the series to the main chart definition.
ChartElement::trimArray($series);
$chart_definition['options']['series'][$series_number] = $series;
// Merge in any point-specific data points.
foreach (Element::children($element[$key]) as $sub_key) {
if ($element[$key][$sub_key]['#type'] === 'chart_data_item') {
// Make sure defaults are loaded.
if (empty($element[$key][$sub_key]['#defaults_loaded'])) {
$element[$key][$sub_key] += $this->elementInfo->getInfo($element[$key][$sub_key]['#type']);
}
$data_item = $element[$key][$sub_key];
if ($data_item['#data']) {
$chart_definition['data'][$sub_key + 1][$series_number + 1] = $data_item['#data'];
}
// These data properties are manually applied to cells in JS.
// Color role not yet supported. See https://code.google.com/p/google-visualization-api-issues/issues/detail?id=1267
$chart_definition['_data'][$sub_key + 1][$series_number + 1]['color'] = $data_item['#color'];
$chart_definition['_data'][$sub_key + 1][$series_number + 1]['tooltip'] = $data_item['#title'];
// Merge in data point raw options.
if (!empty($data_item['#raw_options'])) {
$chart_definition['_data'][$sub_key + 1][$series_number + 1] = NestedArray::mergeDeepArray([
$chart_definition['_data'][$sub_key + 1][$series_number + 1],
$data_item['#raw_options'],
]);
}
ChartElement::trimArray($chart_definition['_data'][$sub_key + 1][$series_number + 1]);
}
}
$series_number++;
}
}
// Once complete, normalize the chart data to ensure a full 2D structure.
$data = $chart_definition['data'];
if (in_array($element['#chart_type'], ['pie', 'donut', 'scatter'])) {
// Populate the 0th row with the same number of values as the 1st row.
$data[0] = array_fill(0, count($data[1]), $element[$key]['#title'] ?? '');
}
elseif ($element['#chart_type'] === 'bubble') {
$data[0] = [
$element[$key]['#title'] ?? '',
$chart_definition['options']['hAxis']['title'] ?? '',
$chart_definition['options']['vAxes'][0]['title'] ?? '',
$chart_definition['options']['zAxis']['title'] ?? '',
];
}
else {
array_unshift($data[0], '');
}
// Ensure consistent column count.
$column_count = count($data[0]);
$new_data = [];
foreach ($data as $row => $values) {
for ($n = 0; $n < $column_count; $n++) {
if (!empty(array_values($values))) {
$temp = array_values($values);
$new_data[$row][$n] = $temp[$n] ?? NULL;
}
}
ksort($new_data[$row]);
}
ksort($new_data);
$chart_definition['data'] = $new_data;
return $chart_definition;
}
/**
* Utility to escape special characters in ICU number formats.
*
* Google will use the ICU format to auto-adjust numbers based on special
* characters that are used in the format. This function escapes these special
* characters so they just show up as the character specified.
*
* The format string is a subset of the ICU pattern set. For instance,
* {pattern:'#,###%'} will result in output values "1,000%", "750%", and "50%"
* for values 10, 7.5, and 0.5.
*/
public function chartsGoogleEscapeIcuCharacters($string) {
return preg_replace('/([0-9@#\.\-,E\+;%\'\*])/', "'$1'", $string);
}
}

View File

@@ -0,0 +1,130 @@
#Installation using Composer (recommended)
If you use Composer to manage dependencies, edit your site's "composer.json"
file as follows.
1. Run `composer require --prefer-dist composer/installers` 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.
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": "highcharts/highcharts",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/highcharts.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
},
{
"type": "package",
"package": {
"name": "highcharts/more",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts_more"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/highcharts-more.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
},
{
"type": "package",
"package": {
"name": "highcharts/exporting",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts_exporting"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/modules/exporting.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
},
{
"type": "package",
"package": {
"name": "highcharts/export-data",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts_export-data"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/modules/export-data.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
},
{
"type": "package",
"package": {
"name": "highcharts/accessibility",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts_accessibility"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/modules/accessibility.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
},
{
"type": "package",
"package": {
"name": "highcharts/3d",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts_3d"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/highcharts-3d.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
}
4. Run `composer require --prefer-dist highcharts/highcharts:11.1.0
highcharts/more:11.1.0 highcharts/exporting:11.1.0
highcharts/export-data:11.1.0 highcharts/accessibility:11.1.0
highcharts/3d:11.1.0` - you should find that new directories have been
created under "/libraries"

View File

@@ -0,0 +1,12 @@
name: Highcharts
type: module
description: 'Charts module integration with Highcharts library.'
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

View File

@@ -0,0 +1,95 @@
<?php
/**
* @file
* Installation and update hooks for the Charts Highcharts module.
*/
/**
* Implements hook_requirements().
*/
function charts_highcharts_requirements($phase) {
$requirements = [];
switch ($phase) {
case 'runtime':
$library_path = charts_highcharts_find_library();
$config = \Drupal::config('charts.settings');
$cdn = $config->get('advanced.requirements.cdn') ?? FALSE;
if (!$library_path) {
if ($cdn) {
$requirements['charts_highcharts_js'] = [
'title' => t('Highcharts Library'),
'value' => t('Available through a CDN'),
'severity' => REQUIREMENT_WARNING,
'description' => t('You are using the Highcharts 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_highcharts for instructions to install the library.'),
];
}
else {
$requirements['charts_highcharts_js'] = [
'title' => t('Highcharts Library'),
'value' => t('Not Installed'),
'severity' => REQUIREMENT_ERROR,
'description' => t('You are missing the Highcharts 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_highcharts for instructions to install the library.'),
];
}
}
else {
$requirements['charts_highcharts_js'] = [
'title' => t('Highcharts Library'),
'value' => t('Installed'),
'severity' => REQUIREMENT_OK,
'description' => t('You have installed the Highcharts.js library in your Drupal installation directory.'),
];
}
break;
}
if (function_exists('libraries_detect') && $highcharts_info = libraries_detect('highcharts')) {
if (is_dir($highcharts_info['library path'] . '/js/exporting-server')) {
$requirements['charts_highcharts_js'] = [
'title' => t('Highcharts vulnerability'),
'severity' => REQUIREMENT_ERROR,
'value' => t('Dangerous sample code present'),
'description' => t('Your installation of the Highcharts library at "@path" contains a directory named "exporting-server". This directory contains dangerous sample files that may compromise the security of your site. You must delete this directory before you may use the Charts Highcharts module.', ['@path' => $highcharts_info['library path']]),
];
}
}
return $requirements;
}
/**
* Get the location of the Highcharts library.
*
* @return string
* The location of the library, or FALSE if the library isn't installed.
*/
function charts_highcharts_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/<domain>/*.
$container = \Drupal::getContainer();
$site_path = $container->getParameter('site.path');
$site_path = explode('/', $site_path);
$site_name = $site_path[1];
$searchdir[] = $site_name . '/libraries';
foreach ($searchdir as $dir) {
if (file_exists($dir . '/highcharts/highcharts.js')) {
return $dir . '/highcharts';
}
}
return FALSE;
}

View File

@@ -0,0 +1,73 @@
charts_highcharts:
version: VERSION
js:
js/charts_highcharts.js: { weight: -1 }
dependencies:
- core/once
- core/drupalSettings
- charts/global
highcharts:
remote: https://code.highcharts.com/highcharts.js
version: VERSION
license:
name: Non-commercial
url: https://creativecommons.org/licenses/by-nc/3.0/
gpl-compatible: false
cdn:
/libraries/highcharts/highcharts.js: https://code.highcharts.com/11.1.0/highcharts.js
/libraries/highcharts_more/highcharts-more.js: https://code.highcharts.com/11.1.0/highcharts-more.js
/libraries/highcharts_accessibility/accessibility.js: https://code.highcharts.com/11.1.0/modules/accessibility.js
/libraries/highcharts_3d/highcharts-3d.js: https://code.highcharts.com/11.1.0/highcharts-3d.js
js:
/libraries/highcharts/highcharts.js: { minified: true }
/libraries/highcharts_more/highcharts-more.js: { minified: true }
/libraries/highcharts_accessibility/accessibility.js: { minified: true }
/libraries/highcharts_3d/highcharts-3d.js: { minified: true }
dependencies:
- charts_highcharts/charts_highcharts
highcharts_exporting:
remote: https://code.highcharts.com/highcharts.js
version: VERSION
license:
name: Non-commercial
url: https://creativecommons.org/licenses/by-nc/3.0/
gpl-compatible: false
cdn:
/libraries/highcharts_exporting/exporting.js: https://code.highcharts.com/11.1.0/modules/exporting.js
/libraries/highcharts_export-data/export-data.js: https://code.highcharts.com/11.1.0/modules/export-data.js
js:
/libraries/highcharts_exporting/exporting.js: { minified: true }
/libraries/highcharts_export-data/export-data.js: { minified: true }
dependencies:
- charts_highcharts/charts_highcharts
- charts_highcharts/highcharts
texture:
remote: https://code.highcharts.com/highcharts.js
version: VERSION
license:
name: Non-commercial
url: https://creativecommons.org/licenses/by-nc/3.0/
gpl-compatible: false
cdn:
/libraries/highcharts_pattern-fill/pattern-fill.js: https://code.highcharts.com/11.1.0/modules/pattern-fill.js
/libraries/highcharts_high-contrast-light/high-contrast-light.js: https://code.highcharts.com/11.1.0/themes/high-contrast-light.js
js:
/libraries/highcharts_pattern-fill/pattern-fill.js: { minified: true }
/libraries/highcharts_high-contrast-light/high-contrast-light.js: { minified: true }
js/textures.js: { weight: -5 }
dependencies:
- charts/global
- core/once
- charts_highcharts/charts_highcharts
color_changer:
version: VERSION
js:
js/color_changer.js: { }
dependencies:
- charts/global
- core/once
- charts_highcharts/charts_highcharts

View File

@@ -0,0 +1,86 @@
<?php
/**
* @file
* Implement module related hooks for the charts_highcharts module.
*/
use Drupal\charts\ConfigUpdater;
use Drupal\charts_highcharts\Plugin\chart\Library\Highcharts;
/**
* Implements hook_charts_version3_to_new_settings_structure_alter().
*/
function charts_highcharts_charts_version3_to_new_settings_structure_alter(array &$new_settings, string $for, ConfigUpdater $config_updater) {
$config_factory = \Drupal::configFactory();
$is_config = $for === 'config';
$highcharts_config = $is_config ? $config_factory->getEditable('charts_highcharts.settings') : NULL;
if (!$is_config || !$highcharts_config || empty($new_settings['library']) || $new_settings['library'] !== 'highcharts') {
if ($highcharts_config) {
$highcharts_config->delete();
}
return;
}
$mapping_keys = [
'legend_layout' => 'legend_layout',
'legend_background_color' => 'legend_background_color',
'legend_border_width' => 'legend_border_width',
'legend_shadow' => 'legend_shadow',
'item_style_color' => 'legend_item_style_color',
'text_overflow' => 'legend_item_style_overflow',
];
$library_config = [
'legend' => [
'layout' => NULL,
'background_color' => '',
'border_width' => 0,
'shadow' => FALSE,
'item_style' => [
'color' => '',
'overflow' => '',
],
],
'exporting_library' => TRUE,
'texture_library' => TRUE,
'global_options' => Highcharts::defaultGlobalOptions(),
];
foreach ($highcharts_config->getRawData() as $config_key => $config_value) {
$setting_key_map = $mapping_keys[$config_key] ?? '';
if (!$setting_key_map || substr($setting_key_map, 0, 6) !== 'legend') {
continue;
}
// Stripping the 'legend_' in front of the mapping key.
$setting_key_map = substr($setting_key_map, 7, strlen($setting_key_map));
$value = $config_updater->transformBoolStringValueToBool($config_value);
switch ($setting_key_map) {
case 'layout':
$library_config['legend']['layout'] = $value;
break;
case 'background_color':
$library_config['legend']['background_color'] = $value;
break;
case 'border_width':
$library_config['legend']['border_width'] = $value;
break;
case 'shadow':
$library_config['legend']['shadow'] = $value;
break;
case 'item_style_color':
$library_config['legend']['item_style']['color'] = $value;
break;
default:
$library_config['legend']['item_style']['overflow'] = $value;
}
$new_settings['library_config'] = $library_config;
$highcharts_config->delete();
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* @file
* Charts highcharts post-update file.
*/
use Drupal\charts_highcharts\Plugin\chart\Library\Highcharts;
/**
* @file
* Charts highcharts post-update file.
*/
/**
* Set default global options.
*/
function charts_highcharts_post_update_set_default_global_options(&$sandbox) {
/** @var \Drupal\Core\Config\Config $config */
$config = \Drupal::service('config.factory')->getEditable('charts.settings');
$library_plugin_id = $config->get('charts_default_settings.library');
$library_is_highcharts = $library_plugin_id === 'highcharts';
$global_options_config_key = 'charts_default_settings.library_config.global_options';
// Getting the current config key to ensure that we don't overwrite the
// existing configurations in case the user saved the config before running
// this update.
$global_options = $library_is_highcharts ? $config->get($global_options_config_key) : [];
if ($library_is_highcharts && !$global_options) {
$config->set($global_options_config_key, Highcharts::defaultGlobalOptions());
return 'Global options config were updated.';
}
return 'Global options update were not set because the default library is not highcharts or existing global options were saved before running post update.';
}

View File

@@ -0,0 +1,128 @@
{
"name": "drupal/charts_highcharts",
"description": "Charts module integration with Highcharts library.",
"type": "drupal-module",
"homepage": "http://drupal.org/project/charts",
"support": {
"issues": "https://www.drupal.org/project/issues/charts"
},
"license": "GPL-2.0+",
"require": {
"highcharts/3d": "11.1.0",
"highcharts/accessibility": "11.1.0",
"highcharts/export-data": "11.1.0",
"highcharts/exporting": "11.1.0",
"highcharts/highcharts": "11.1.0",
"highcharts/more": "11.1.0"
},
"repositories": {
"highcharts": {
"type": "package",
"package": {
"name": "highcharts/highcharts",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/highcharts.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
},
"highcharts_3d": {
"type": "package",
"package": {
"name": "highcharts/3d",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts_3d"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/highcharts-3d.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
},
"highcharts_accessibility": {
"type": "package",
"package": {
"name": "highcharts/accessibility",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts_accessibility"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/modules/accessibility.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
},
"highcharts_export_data": {
"type": "package",
"package": {
"name": "highcharts/export-data",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts_export-data"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/modules/export-data.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
},
"highcharts_exporting": {
"type": "package",
"package": {
"name": "highcharts/exporting",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts_exporting"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/modules/exporting.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
},
"highcharts_more": {
"type": "package",
"package": {
"name": "highcharts/more",
"version": "11.1.0",
"type": "drupal-library",
"extra": {
"installer-name": "highcharts_more"
},
"dist": {
"url": "https://code.highcharts.com/11.1.0/highcharts-more.js",
"type": "file"
},
"require": {
"composer/installers": "^1.0 || ^2.0"
}
}
}
}
}

View File

@@ -0,0 +1,149 @@
charts.settings.library_plugin.highcharts:
type: charts_settings_library_plugin
mapping:
legend:
label: Legend
type: mapping
mapping:
layout:
label: Layout
type: string
background_color:
label: 'Background color'
type: string
border_width:
label: 'Border width'
type: string
item_style:
label: 'Border width'
type: mapping
mapping:
color:
label: Color
type: string
overflow:
label: 'Text overflow'
type: string
shadow:
label: 'Enable legend shadow'
type: boolean
exporting_library:
label: 'Enable exporting library'
type: boolean
texture_library:
label: 'Enable texture library'
type: boolean
global_options:
label: 'Global options'
type: mapping
mapping:
lang:
type: mapping
label: Language
mapping:
download_CSV:
type: label
label: 'Download CSV'
download_JPEG:
type: label
label: 'Download JPEG'
download_PDF:
type: label
label: 'Download PDF'
download_PNG:
type: label
label: 'Download PNG'
download_SVG:
type: label
label: 'Download SVG'
download_XLS:
type: label
label: 'Download XLS'
exit_fullscreen:
type: label
label: 'Exit fullscreen'
hide_data:
type: label
label: 'Hide data'
loading:
type: label
label: 'Loading'
main_breadcrumb:
type: label
label: 'Main breadcrumb'
thousands_sep:
type: label
label: 'Number formatting: Thousand separator'
decimal_point:
type: label
label: 'Number formatting: Decimal point'
no_data:
type: label
label: 'No data'
print_chart:
type: label
label: 'Print chart'
reset_zoom:
type: label
label: 'Reset zoom'
reset_zoom_title:
type: label
label: 'Reset zoom title'
view_data:
type: label
label: 'View data'
view_fullscreen:
type: label
label: 'View fullscreen'
context_button_title:
type: label
label: 'Context button title'
drill_up_text:
type: label
label: 'Drill up text'
invalid_date:
type: label
label: 'Invalid date'
months:
type: sequence
label: Months
sequence:
type: label
label: Month
short_months:
type: sequence
label: 'Short Months'
sequence:
type: label
label: 'Short Month'
weekdays:
type: sequence
label: Weekdays
sequence:
type: label
label: Weekday
short_weekdays:
type: sequence
label: 'Short Weekdays'
sequence:
type: label
label: 'Short Weekday'
export_data:
type: mapping
label: 'Export data'
mapping:
annotation_header:
type: label
label: 'The annotation column title'
category_datetime_header:
type: label
label: 'The category column title when axis type set to "datetime"'
category_header:
type: label
label: 'The category column title'
numeric_symbols:
type: sequence
label: 'Numeric symbols'
sequence:
type: label
label: 'Numeric symbol'

View File

@@ -0,0 +1,40 @@
/**
* @file
* JavaScript's integration between Highcharts and Drupal.
*/
(function (Drupal, once, drupalSettings) {
'use strict';
Drupal.behaviors.chartsHighcharts = {
attach: function (context) {
const contents = new Drupal.Charts.Contents();
if (drupalSettings.hasOwnProperty('charts') && drupalSettings.charts.highcharts.global_options) {
Highcharts.setOptions(drupalSettings.charts.highcharts.global_options);
}
once('charts-highchart', '.charts-highchart', context).forEach(function (element) {
const id = element.id;
let config = contents.getData(id);
if (!config) {
return;
}
config.chart.renderTo = id;
new Highcharts.Chart(config);
if (element.nextElementSibling && element.nextElementSibling.hasAttribute('data-charts-debug-container')) {
element.nextElementSibling.querySelector('code').innerText = JSON.stringify(config, null, ' ');
}
});
},
detach: function (context, settings, trigger) {
if (trigger === 'unload') {
once('charts-highchart-detach', '.charts-highchart', context).forEach(function (element) {
if (!element.dataset.hasOwnProperty('highchartsChart')) {
return;
}
Highcharts.charts[element.dataset.highchartsChart].destroy();
});
}
}
};
}(Drupal, once, drupalSettings));

View File

@@ -0,0 +1,42 @@
/**
* @file
* JavaScript's integration between Highcharts and Drupal.
*/
((Drupal, once) => {
'use strict';
Drupal.behaviors.chartsHighchartsColorChanger = {
attach: function (context) {
const colorChangerHandler = function (event) {
const chartMetadata = JSON.parse(this.dataset.chartsHighchartsColorInfo);
const chartsElement = document.getElementById(chartMetadata.chart_id);
const chart = Highcharts.charts[chartsElement.dataset.highchartsChart];
switch (chartMetadata.chart_type) {
case 'pie':
chart.series[0].data[chartMetadata.series_index].color = '';
chart.series[0].data[chartMetadata.series_index].update({
color: event.target.value
});
break;
case 'gauge':
chart.yAxis[0].plotLinesAndBands[0].options.color = event.target.value;
chart.yAxis[0].update();
break;
default:
chart.series[chartMetadata.series_index].update({
color: event.target.value
});
}
};
once('charts-color-changer', '.charts-color-changer', context).forEach(function (element) {
element.querySelectorAll('input[type="color"]').forEach(
(colorChanger) => {
colorChanger.addEventListener('change', colorChangerHandler);
}
);
});
}
};
})(Drupal, once);

View File

@@ -0,0 +1,69 @@
/**
* @file
* Applies textures to Highcharts charts.
*/
(function (Drupal, once) {
'use strict';
Drupal.charts_highcharts = Drupal.charts_highcharts || {};
Drupal.behaviors.chartsHighchartsAddTexture = {
attach: function (context) {
once('charts-highchart-texture', '.charts-highchart', context).forEach(function (element) {
element.addEventListener('drupalChartsConfigsInitialization', function (e) {
let data = e.detail;
const id = data.drupalChartDivId;
// Add textures to series.
if ('series' in data && data.series[0].color !== undefined && typeof data.series[0].data[0] === "number") {
for (let i = 0; i < data.series.length; i++) {
data.series[i].color = Drupal.charts_highcharts.getPattern(Drupal.charts_highcharts.getUnderTenIndex(i), data.series[i].color);
}
} else if ('series' in data && typeof data.series[0].data[0] === "object" && data.series[0].data[0].color !== undefined) {
for (let i = 0; i < data.series[0].data.length; i++) {
data.series[0].data[i].color = Drupal.charts_highcharts.getPattern(Drupal.charts_highcharts.getUnderTenIndex(i), data.series[0].data[i].color);
}
}
if ('series' in data && typeof data.series[0].data[0] === "object" && data.series[0].data[0].color === undefined) {
for (let i = 0; i < data.colors.length; i++) {
data.colors[i] = Drupal.charts_highcharts.getPattern(Drupal.charts_highcharts.getUnderTenIndex(i), data.colors[i]);
}
}
Drupal.Charts.Contents.update(id, data);
});
});
}
};
/**
* Get under ten index in case there are more than ten series because
* Highcharts patterns array has ten patterns
* 0 - 9 index.
*/
Drupal.charts_highcharts.getUnderTenIndex = function (k) {
if (k < 10) {
return k;
} else {
while (k >= 10) {
// Sum the digits of k.
k = k.toString().split('').reduce(function (a, b) {
return parseInt(a) + parseInt(b);
});
}
return k;
}
}
/**
* Get a default pattern, but using the series color.
* The index-argument refers to which default pattern to use
*/
Drupal.charts_highcharts.getPattern = function (index, color) {
return {
pattern: Highcharts.merge(Highcharts.patterns[index], {
color: color
})
};
}
}(Drupal, once));

View File

@@ -0,0 +1,112 @@
<?php
namespace Drupal\charts_highcharts\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Implements a color-changer form.
*/
class ColorChanger extends FormBase {
/**
* {@inheritDoc}
*/
public function getFormId() {
return 'charts_highcharts_color_changer';
}
/**
* {@inheritDoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$chart_id = $form_state->get('chart_id');
$series = $form_state->get('chart_series');
$chart_type = $form_state->get('chart_type');
$y_axis = $form_state->get('y_axis');
if (!$series || !$chart_id) {
return $form;
}
$form['#attributes']['class'][] = 'charts-color-changer';
$form['color_changer_wrapper'] = [
'#title' => $this->t('Change the colors'),
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => FALSE,
];
foreach ($series as $index => $item) {
if ($chart_type === 'pie') {
foreach ($item['data'] as $datum_index => $datum_value) {
$form['color_changer_wrapper']['color_' . $datum_index] = [
'#type' => 'textfield',
'#title' => $this->t('@label Color', ['@label' => $datum_value['name'] ?? '']),
'#attributes' => [
'TYPE' => 'color',
'style' => 'min-width:50px;',
'data-charts-highcharts-color-info' => json_encode([
'series_index' => $datum_index,
'chart_id' => $chart_id,
'chart_type' => $chart_type,
]),
],
'#size' => 10,
'#maxlength' => 7,
'#default_value' => $datum_value['color'] ?? '#000',
];
}
}
elseif ($chart_type === 'gauge') {
$gauge_colors = [
'red' => '#ff0000',
'yellow' => '#ffff00',
'green' => '#008000',
];
foreach ($y_axis['plotBands'] as $datum_index => $datum_value) {
$form['color_changer_wrapper']['color_' . $datum_index] = [
'#type' => 'textfield',
'#title' => $this->t('@label Color', ['@label' => $item['name'] . ' (' . $datum_value['color'] . ')']),
'#attributes' => [
'TYPE' => 'color',
'style' => 'min-width:50px;',
'data-charts-highcharts-color-info' => json_encode([
'series_index' => $datum_index,
'chart_id' => $chart_id,
'chart_type' => $chart_type,
]),
],
'#size' => 10,
'#maxlength' => 7,
'#default_value' => $gauge_colors[$datum_value['color']] ?? '#000',
];
}
}
else {
$form['color_changer_wrapper']['color_' . $index] = [
'#type' => 'textfield',
'#title' => $this->t('@label Color', ['@label' => $item['name']]),
'#attributes' => [
'TYPE' => 'color',
'style' => 'min-width:50px;',
'data-charts-highcharts-color-info' => json_encode([
'series_index' => $index,
'chart_id' => $chart_id,
]),
],
'#size' => 10,
'#maxlength' => 7,
'#default_value' => $item['color'] ?? '#000',
];
}
}
return $form;
}
/**
* {@inheritDoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
<?php
namespace Drupal\Tests\charts_highcharts\Kernel;
use Drupal\Tests\charts\Kernel\ChartsKernelTestBase;
/**
* Test the raw_options element property behavior.
*
* @group charts
*/
class RawOptionsTest extends ChartsKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'charts',
'charts_highcharts',
];
/**
* Test raw options set on the main chart element.
*/
public function testMergeDeepArrayInPopulateOptions() {
$series = [
'#type' => 'chart_data',
'#title' => '5.0.x',
'#data' => [257, 235, 325, 340],
'#color' => '#1d84c3',
];
$element = [
'#type' => 'chart',
'#chart_type' => 'column',
'series' => $series,
'#raw_options' => [
'plotOptions' => [
'series' => ['grouping' => TRUE],
],
],
];
// Testing raw options when there is NO data already set in the definition.
$path = ['plotOptions', 'series', 'grouping'];
$this->assertJsonPropertyHasValue($element, $path, TRUE);
// Testing raw options when there is data already set in the definition.
$element['#data_labels'] = TRUE;
$element['#raw_options'] = [
'plotOptions' => [
'series' => [
'dataLabels' => [
'enabled' => FALSE,
],
],
],
];
$path = ['plotOptions', 'series', 'dataLabels', 'enabled'];
$this->assertJsonPropertyHasValue($element, $path, FALSE);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\charts\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Chart annotation object.
*
* @Annotation
*/
class Chart extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin name.
*
* @var string
*/
public $name;
/**
* An array of chart types the chart library supports.
*
* @var array
*/
public $types = [];
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Drupal\charts;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Chart Manager.
*
* Provides the Chart plugin manager and manages discovery and instantiation of
* chart plugins.
*/
class ChartManager extends DefaultPluginManager {
/**
* Constructor for ChartManager objects.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/chart/Library', $namespaces, $module_handler, 'Drupal\charts\Plugin\chart\Library\ChartInterface', 'Drupal\charts\Annotation\Chart');
$this->alterInfo('charts_chart_library');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Drupal\charts;
/**
* Defines the interface for the chart views field data type.
*/
interface ChartViewsFieldInterface {
/**
* Get the chart field data type.
*
* @return string
* The chart field data type.
*/
public function getChartFieldDataType(): string;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\charts;
/**
* Contains helper method to help with the generation of random color.
*/
trait ColorHelperTrait {
/**
* Provide a random color.
*
* @return string
* A random color.
*/
public static function randomColor(): string {
return sprintf('#%06X', mt_rand(0, 0xFFFFFF));
}
}

View File

@@ -0,0 +1,346 @@
<?php
namespace Drupal\charts;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\Color;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Manage the updates and upgrades of settings.
*/
class ConfigUpdater implements ContainerInjectionInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleExtensionList;
/**
* ConfigEntityUpdater constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
* The module extension list.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, ModuleExtensionList $extension_list_module) {
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
$this->moduleExtensionList = $extension_list_module;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('extension.list.module')
);
}
/**
* Transforms legacy settings to newer setting architecture.
*
* @param array $old_settings
* The old settings.
* @param string $for
* The configuration for which the transformation is being done.
* Regular config or view.
*
* @return array
* The new format settings.
*/
public function transformVersion3SettingsToNew(array &$old_settings, string $for = 'config') {
$new_settings = [];
$new_settings['fields']['stacking'] = !empty($old_settings['grouping']);
$old_config_keys = $this->getLegacySettingsMappingKeys();
foreach ($old_settings as $setting_id => $setting_value) {
if (empty($old_config_keys[$setting_id])) {
continue;
}
$setting_key_map = $old_config_keys[$setting_id];
$value = $this->transformBoolStringValueToBool($setting_value);
// When a block setting belongs to the chart blocks we save it in a
// new setting.
if (substr($setting_key_map, 0, 7) === 'display') {
// Stripping the 'display_' in front of the mapping key.
$setting_key_map = substr($setting_key_map, 8, strlen($setting_key_map));
if (substr($setting_key_map, 0, 10) === 'dimensions') {
// Stripping dimensions_.
$setting_key_map = substr($setting_key_map, 11, strlen($setting_key_map));
$new_settings['display']['dimensions'][$setting_key_map] = $value;
}
elseif (substr($setting_key_map, 0, 5) === 'gauge') {
// Stripping gauge_.
$setting_key_map = substr($setting_key_map, 6, strlen($setting_key_map));
$new_settings['display']['gauge'][$setting_key_map] = $value;
}
else {
$new_settings['display'][$setting_key_map] = $value;
}
}
elseif (substr($setting_key_map, 0, 5) === 'xaxis') {
// Stripping xaxis_.
$setting_key_map = substr($setting_key_map, 6, strlen($setting_key_map));
$new_settings['xaxis'][$setting_key_map] = $value;
}
elseif (substr($setting_key_map, 0, 5) === 'yaxis') {
// Stripping yaxis_.
$setting_key_map = substr($setting_key_map, 6, strlen($setting_key_map));
if (substr($setting_key_map, 0, 9) === 'secondary') {
// Stripping gauge_.
$setting_key_map = substr($setting_key_map, 10, strlen($setting_key_map));
$new_settings['yaxis']['secondary'][$setting_key_map] = $value;
}
else {
$new_settings['yaxis'][$setting_key_map] = $value;
}
}
elseif (substr($setting_key_map, 0, 6) === 'fields') {
// Stripping fields_.
$setting_key_map = substr($setting_key_map, 7, strlen($setting_key_map));
if ($setting_key_map === 'data_providers' && is_array($value)) {
$data_providers = $new_settings['fields']['data_providers'] ?? [];
if ($setting_id === 'data_fields' || $setting_id == 'field_colors') {
$new_settings['fields']['data_providers'] = $this->transformLegacyFieldsDataProvidersToNew($data_providers, $value);
}
}
else {
$new_settings['fields'][$setting_key_map] = $value;
}
}
elseif ($setting_key_map === 'grouping' && $new_settings['fields']['stacking']) {
$new_settings[$setting_key_map] = [];
}
else {
// We make sure that we handle the color unneeded array.
$new_settings[$setting_key_map] = $setting_key_map !== 'color' ? $value : $value[0];
}
// Then we remove it from the main old settings tree.
unset($old_settings[$setting_id]);
}
// Allow other modules to alter the new settings.
$this->moduleHandler->alter('charts_version3_to_new_settings_structure', $new_settings, $for, $this);
return $new_settings;
}
/**
* Initialize the current default settings.
*/
public function initializedCurrentDefaultSettings() {
$path = $this->moduleExtensionList->getPath('charts');
$default_install_settings_file = $path . '/config/install/charts.settings.yml';
$default_install_settings = Yaml::decode(file_get_contents($default_install_settings_file));
$new_settings = &$default_install_settings['charts_default_settings'];
// Allow other modules to alter the new settings.
$for = 'config';
$this->moduleHandler->alter('charts_version3_to_new_settings_structure', $new_settings, $for, $this);
return $default_install_settings;
}
/**
* Updates settings from version 3 of views.
*/
public function updateExistingViewsVersion3ToNewSettings() {
$view_storage = $this->entityTypeManager->getStorage('view');
$view_ids = $view_storage->getQuery()
->accessCheck(FALSE)
->condition('display.*.display_options.style.type', 'chart', '=')
->execute();
if (!$view_ids) {
return 'Views: No views had a display set to a charts style.';
}
$updated_views = [];
foreach ($view_ids as $view_id) {
/** @var \Drupal\views\ViewEntityInterface $view */
if (!($view = $view_storage->load($view_id))) {
continue;
}
$changed = FALSE;
$displays = $view->get('display');
foreach ($displays as &$display) {
$style = &$display['display_options']['style'];
if ($style['type'] !== 'chart' || !isset($style['options']['field_colors']) || !isset($style['options']['fields']['table'])) {
continue;
}
$changed = TRUE;
// Removing this because it was set in version 3 but was not used for
// anything.
unset($style['options']['fields']);
$options = &$style['options'];
$options = $this->transformVersion3SettingsToNew($options, 'view');
$chart_settings_elements = [
'library',
'type',
'fields',
'display',
'xaxis',
'yaxis',
];
foreach ($options as $option_key => $option) {
if (in_array($option_key, $chart_settings_elements)) {
$options['chart_settings'][$option_key] = $option;
unset($options[$option_key]);
}
}
}
if ($changed) {
$view->set('display', $displays);
$view->save();
$updated_views[] = $view_id;
}
}
if ($updated_views) {
return sprintf('Views: The following views were updated: %s', implode(', ', $updated_views));
}
return sprintf('Views: The following views(%s) with at least one display of charts style were loaded but not updated!', implode(', ', array_values($view_ids)));
}
/**
* Transforms boolean string value to real boolean.
*
* @param mixed $value
* The value to be transformed.
*
* @return bool|mixed
* The boolean value or the original passed value.
*/
public function transformBoolStringValueToBool($value) {
if ($value === 'FALSE' || $value === 'false') {
return FALSE;
}
elseif ($value === 'TRUE' || $value === 'true') {
return TRUE;
}
return $value;
}
/**
* Transforms legacy fields data providers to new.
*
* @param array $data_providers
* Data providers.
* @param array $legacy_value
* Legacy value.
*
* @return mixed
* Data providers returned
*/
private function transformLegacyFieldsDataProvidersToNew(array $data_providers, array $legacy_value) {
$default_weight = 0;
foreach ($legacy_value as $field_id => $value) {
if (Color::validateHex($value)) {
$data_providers[$field_id]['color'] = $value;
}
else {
$data_providers[$field_id]['enabled'] = !empty($value);
}
$data_providers[$field_id]['weight'] = $default_weight;
$default_weight++;
}
return $data_providers;
}
/**
* Gets legacy settings mapping keys.
*
* @return array
* Legacy settings keys to newer ones mapping.
*/
private function getLegacySettingsMappingKeys() {
return [
'library' => 'library',
'chart_library' => 'library',
'type' => 'type',
'chart_type' => 'type',
'grouping' => 'grouping',
'title' => 'display_title',
'title_position' => 'display_title_position',
'data_labels' => 'display_data_labels',
'data_markers' => 'display_data_markers',
'legend' => 'display_legend',
'legend_position' => 'display_legend_position',
'background' => 'display_background',
'three_dimensional' => 'display_three_dimensional',
'polar' => 'display_polar',
'series' => 'series',
'data' => 'data',
'color' => 'color',
'data_series' => 'data_series',
'series_label' => 'series_label',
'categories' => 'categories',
'field_colors' => 'fields_data_providers',
'tooltips' => 'display_tooltips',
'tooltips_use_html' => 'display_tooltips_use_html',
'width' => 'display_dimensions_width',
'height' => 'display_dimensions_height',
'width_units' => 'display_dimensions_width_units',
'height_units' => 'display_dimensions_height_units',
'colors' => 'display_colors',
'xaxis_title' => 'xaxis_title',
'xaxis_labels_rotation' => 'xaxis_labels_rotation',
'yaxis_title' => 'yaxis_title',
'yaxis_min' => 'yaxis_min',
'yaxis_max' => 'yaxis_max',
'yaxis_prefix' => 'yaxis_prefix',
'yaxis_suffix' => 'yaxis_suffix',
'yaxis_decimal_count' => 'yaxis_decimal_count',
'yaxis_labels_rotation' => 'yaxis_labels_rotation',
'inherit_yaxis' => 'yaxis_inherit',
'secondary_yaxis_title' => 'yaxis_secondary_title',
'secondary_yaxis_min' => 'yaxis_secondary_min',
'secondary_yaxis_max' => 'yaxis_secondary_min',
'secondary_yaxis_prefix' => 'yaxis_secondary_prefix',
'secondary_yaxis_suffix' => 'yaxis_secondary_suffix',
'secondary_yaxis_decimal_count' => 'yaxis_secondary_decimal_count',
'secondary_yaxis_labels_rotation' => 'yaxis_secondary_labels_rotation',
'green_from' => 'display_gauge_green_from',
'green_to' => 'display_gauge_green_to',
'red_from' => 'display_gauge_red_from',
'red_to' => 'display_gauge_red_to',
'yellow_from' => 'display_gauge_yellow_from',
'yellow_to' => 'display_gauge_yellow_to',
'max' => 'display_gauge_max',
'min' => 'display_gauge_min',
'allow_advanced_rendering' => 'fields_allow_advanced_rendering',
'label_field' => 'fields_label',
'data_fields' => 'fields_data_providers',
];
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Drupal\charts;
/**
* Provide method to calculate dependencies for charts config.
*/
trait DependenciesCalculatorTrait {
/**
* The chart library plugin manager.
*
* @var \Drupal\charts\ChartManager
*/
protected $chartPluginManager;
/**
* The chart type plugin library manager.
*
* @var \Drupal\charts\TypeManager
*/
protected $chartTypePluginManager;
/**
* Calculates the dependencies given the chart library and chart type.
*
* @param string $chart_library_id
* The chart library plugin id.
* @param string $chart_type_id
* The chart type plugin id.
*
* @return array
* The calculated dependencies.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function calculateDependencies(string $chart_library_id, string $chart_type_id):array {
$calculated_dependencies = [];
$dependent_modules = [];
if ($chart_library_id) {
// Getting charting library provider and set it as one of the
// dependencies for the chart settings.
$plugin_definition = $this->chartPluginManager()->getDefinition($chart_library_id);
$dependent_modules = [$plugin_definition['provider']];
}
if ($chart_type_id) {
// Do the same thing for the chart type unless it was added by the main
// "charts" module.
$plugin_definition = $this->chartTypePluginManager()->getDefinition($chart_type_id);
$provider = $plugin_definition['provider'];
if ($provider !== 'charts' && !in_array($provider, $dependent_modules)) {
$dependent_modules[] = $provider;
}
}
if ($dependent_modules) {
$calculated_dependencies = ['module' => $dependent_modules];
}
return $calculated_dependencies;
}
/**
* Initialize the chart plugin manager if needed.
*
* @return \Drupal\charts\ChartManager
* The chart manager plugin.
*/
private function chartPluginManager(): ChartManager {
if (!isset($this->chartPluginManager)) {
$this->chartPluginManager = \Drupal::service('plugin.manager.charts');
}
return $this->chartPluginManager;
}
/**
* Initialize the chart type plugin manager if needed.
*
* @return \Drupal\charts\TypeManager
* The chart type manager plugin.
*/
private function chartTypePluginManager(): TypeManager {
if (!isset($this->chartTypePluginManager)) {
$this->chartTypePluginManager = \Drupal::service('plugin.manager.charts_type');
}
return $this->chartTypePluginManager;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,449 @@
<?php
namespace Drupal\charts\Element;
use Drupal\charts\ChartManager;
use Drupal\charts\Plugin\chart\Library\ChartBase;
use Drupal\charts\Plugin\chart\Library\ChartInterface;
use Drupal\charts\TypeManager;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\RenderElementBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a chart render element.
*
* @RenderElement("chart")
*/
class Chart extends RenderElementBase implements ContainerFactoryPluginInterface {
use StringTranslationTrait;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The chart plugin manager.
*
* @var \Drupal\charts\ChartManager
*/
protected $chartsManager;
/**
* The chart type info service.
*
* @var \Drupal\charts\Plugin\chart\Type\TypeInterface
*/
protected $chartsTypeManager;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a Chart object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\charts\ChartManager $chart_manager
* The chart plugin manager.
* @param \Drupal\charts\TypeManager $type_manager
* The chart type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory, ChartManager $chart_manager, TypeManager $type_manager, ModuleHandlerInterface $module_handler) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->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;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\charts\Element;
use Drupal\Core\Render\Element\RenderElementBase;
/**
* Provides a chart render element.
*/
abstract class ChartAxisBase extends RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo(): array {
return [
// Options: linear, logarithmic, datetime, labels.
'#axis_type' => '',
'#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,
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\charts\Element;
use Drupal\Core\Render\Element\RenderElementBase;
/**
* Provides a chart data render element.
*
* @RenderElement("chart_data")
*/
class ChartData extends RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo(): array {
return [
'#title' => 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,
];
}
}

View File

@@ -0,0 +1,909 @@
<?php
namespace Drupal\charts\Element;
use Drupal\charts\ColorHelperTrait;
use Drupal\charts\Plugin\chart\Library\ChartInterface;
use Drupal\Component\Utility\Environment;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\FormElementBase;
/**
* Provides a chart data collector table form element.
*
* @FormElement("chart_data_collector_table")
*/
class ChartDataCollectorTable extends FormElementBase {
use ColorHelperTrait;
use ElementFormStateTrait;
const FIRST_COLUMN = 'first_column';
const FIRST_ROW = 'first_row';
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => 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' => '<div id="' . $wrapper_id . '">',
'#suffix' => '</div>',
// Pass the id along to other methods.
'#wrapper_id' => $wrapper_id,
] + $element;
$element['table_categories_identifier'] = [
'#type' => 'radios',
'#title' => t('Categories are identified by'),
'#options' => [
self::FIRST_COLUMN => t('First column'),
self::FIRST_ROW => t('First row'),
],
'#description' => t('Select whether the first row or column hold the categories data'),
'#required' => $required,
'#default_value' => $element_state['table_categories_identifier'],
'#ajax' => [
'callback' => [get_called_class(), 'ajaxRefresh'],
'progress' => ['type' => 'throbber'],
'wrapper' => $wrapper_id,
'effect' => 'fade',
],
];
$table = [
'#type' => 'table',
'#tree' => TRUE,
'#header' => [],
'#responsive' => FALSE,
'#attributes' => [
'class' => ['data-collector-table'],
],
];
$table_drag = $element['#table_drag'];
$table_drag_group = Html::cleanCssIdentifier($id_prefix . '-order-weight');
if ($table_drag) {
$table['#tabledrag'] = [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => $table_drag_group,
],
];
}
if ($element['#table_wrapper'] === 'container') {
$element['table_wrapper'] = [
'#type' => 'container',
'#attributes' => $element['#table_wrapper_attributes'],
'#tree' => FALSE,
];
$element['table_wrapper']['data_collector_table'] = &$table;
}
else {
$element['data_collector_table'] = &$table;
}
$rows = count($element_state['data_collector_table']);
// Make the weight list always reflect the current number of values.
$max_weight = count($element_state['data_collector_table']);
$max_row = max(array_keys($element_state['data_collector_table']));
// The first column need to be for colors.
$is_first_column = $element_state['table_categories_identifier'] === self::FIRST_COLUMN;
$first_row_key = NULL;
foreach ($element_state['data_collector_table'] as $i => $row) {
$first_row_key = $first_row_key ?? $i;
$table_first_row = $i === $first_row_key;
$add_color_first_row = ($is_first_column && $table_first_row);
$first_col_key = NULL;
$row_form = &$table[$i];
$row_form['#attributes']['class'][] = 'data-collector-table--row';
// Adding the row textfield cells.
foreach ($row as $j => $column) {
if ($j === 'weight') {
continue;
}
$first_col_key = $first_col_key ?? $j;
$table_first_col = $j === $first_col_key;
// To be used to skip color input on cell[0][0].
$is_category_cell = $table_first_col && $table_first_row;
$row_form[$j]['data'] = [
'#type' => 'textfield',
'#title' => t('Data for column @col - Row @row', [
'@row' => $i,
'@col' => $j,
]),
'#title_display' => 'invisible',
'#size' => 10,
'#default_value' => is_array($column) ? $column['data'] : $column,
'#wrapper_attributes' => [
'class' => ['data-collector-table--row--cell'],
],
];
if (!$is_category_cell && ($add_color_first_row || (!$is_first_column && $j === $first_col_key))) {
if (empty($column['color'])) {
$color_index = $is_first_column ? $j : $i;
$column['color'] = $element['#default_colors'][$color_index - 1] ?? self::randomColor();
}
$row_form[$j]['#wrapper_attributes'] = [
'class' => ['container-inline'],
];
$row_form[$j]['color'] = [
'#type' => 'textfield',
'#title' => t('Color'),
'#title_display' => 'invisible',
'#attributes' => [
'TYPE' => 'color',
'style' => 'min-width:50px;',
],
'#size' => 10,
'#maxlength' => 7,
'#default_value' => $column['color'],
];
}
}
// Adding weight if table drag enabled.
if ($table_drag) {
$row_form['#attributes']['class'][] = 'draggable';
if (($i + 1) === $rows) {
$default_weight = $max_weight;
}
else {
$default_weight = $max_row + 1;
}
$row_form['weight'] = [
'#type' => 'weight',
'#title' => t('Weight'),
'#title_display' => 'invisible',
'#delta' => $max_weight,
'#default_value' => $element_state['data_collector_table'][$i]['weight'] ?? $default_weight,
'#attributes' => [
'class' => [$table_drag_group],
],
];
// Used by SortArray::sortByWeightProperty to sort the rows.
if (isset($user_input['data_collector_table'][$i])) {
$input_weight = $user_input['data_collector_table'][$i]['weight'];
// Make sure the weight is not out of bounds due to removals.
if ($user_input['data_collector_table'][$i]['weight'] > $max_weight) {
$input_weight = $max_weight;
}
// Reflect the updated user input on the element.
$row_form['weight']['#value'] = $input_weight;
$row_form['#weight'] = $input_weight;
}
else {
$row_form['#weight'] = $default_weight;
}
}
// Row delete button.
$row_form['delete'] = self::buildOperationButton('delete', 'row', $id_prefix, $wrapper_id, $i, [], [
'class' => ['data-collector-table--row--delete'],
]);
}
$colspan = 1;
if ($table_drag) {
// Sort the values by weight. Ensures weight is preserved on ajax refresh.
uasort($table, [
'\Drupal\Component\Utility\SortArray',
'sortByWeightProperty',
]);
// Increasing colspan when weight column is added.
$colspan = 2;
}
// Building the column delete button.
$table['_delete_column_buttons'] = [
'#attributes' => ['class' => ['data-collector-table--column-deletes-row']],
];
// Using first row to get the count of columns.
$first_row = current($element_state['data_collector_table']);
// Using array filter to exclude weight key when grabbing the row columns.
$columns = self::excludeWeightColumnFromRow($first_row);
$max_column = max(array_keys($first_row));
foreach ($columns as $column) {
$table['_delete_column_buttons'][$column] = self::buildOperationButton('delete', 'column', $id_prefix, $wrapper_id, $column, [], [
'class' => ['data-collector-table--column--delete'],
]);
if ($column === $max_column) {
$table['_delete_column_buttons'][$column]['#wrapper_attributes']['colspan'] = $colspan;
}
}
// Empty Column under delete operation placeholder.
$table['_delete_column_buttons'][$max_column + 1] = [
'#markup' => '',
];
// Footer operations.
$table['_operations'] = [
'#attributes' => ['class' => ['data-collector-table--operations-row']],
];
$table['_operations']['wrapper'] = [
'#type' => 'container',
'#wrapper_attributes' => [
'colspan' => count($columns) + $colspan,
],
];
$table['_operations']['wrapper']['add_column'] = self::buildOperationButton('add', 'column', $id_prefix, $wrapper_id, NULL);
$table['_operations']['wrapper']['add_row'] = self::buildOperationButton('add', 'row', $id_prefix, $wrapper_id, NULL);
if ($element['#import_csv']) {
$element['import'] = [
'#type' => 'details',
'#title' => t('Import Data from CSV'),
'#description' => t('Note importing data from CSV will overwrite all the current data entry in the table.'),
'#open' => FALSE,
];
$element['import']['csv_separator'] = [
'#type' => 'textfield',
'#title' => t('CSV separator'),
'#default_value' => $element['#import_csv_separator'] ?? ',',
'#size' => 1,
'#required' => TRUE,
];
$element['import']['csv'] = [
'#name' => 'files[' . $id_prefix . ']',
'#title' => t('File upload'),
'#title_display' => 'invisible',
'#type' => 'file',
'#upload_validators' => [
'file_validate_extensions' => ['csv'],
'file_validate_size' => [Environment::getUploadMaxSize()],
],
];
$element['import']['upload'] = [
'#type' => 'submit',
'#value' => t('Upload CSV'),
'#name' => $id_prefix . '-import-csv',
'#attributes' => [
'class' => [Html::cleanCssIdentifier($id_prefix . '--import-csv')],
],
'#submit' => [[get_called_class(), 'importCsvToTableSubmit']],
'#limit_validation_errors' => [
array_merge($parents, ['import', 'csv']),
array_merge($parents, ['import', 'upload']),
],
'#ajax' => [
'callback' => [get_called_class(), 'ajaxRefresh'],
'progress' => ['type' => 'throbber'],
'wrapper' => $wrapper_id,
'effect' => 'fade',
],
'#operation' => 'csv',
'#csv_separator' => $element['#import_csv_separator'] ?? ',',
];
}
$element['#attributes']['style'] = 'overflow: auto;';
return $element;
}
/**
* Validates the data collected.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function validateDataCollectorTable(array $element, FormStateInterface $form_state) {
$parents = $element['#parents'];
$value = $form_state->getValue($parents);
// Remove empty rows and unneeded keys.
foreach ($value['data_collector_table'] as $row_key => $row) {
if (!is_numeric($row_key)) {
unset($value['data_collector_table'][$row_key]);
continue;
}
foreach ($row as $column_key => $column) {
if (!is_numeric($column_key)) {
unset($value['data_collector_table'][$row_key][$column_key]);
}
}
}
unset($value['import']);
$form_state->setValue($parents, $value);
if ($element['#required'] && empty($value['table_categories_identifier'])) {
$form_state->setError($element['table_categories_identifier'], t('Please select how categories should be identified.'));
}
}
/**
* Ajax callback.
*/
public static function ajaxRefresh(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$operation = $triggering_element['#operation'] ?? '';
if ($operation === 'csv' || (!$operation && $triggering_element['#type'] === 'radio')) {
$length = -2;
}
else {
$length = $operation === 'add' ? -4 : -3;
}
$element_parents = array_slice($triggering_element['#array_parents'], 0, $length);
return NestedArray::getValue($form, $element_parents);
}
/**
* Submit callback for table add and delete operations.
*/
public static function tableOperationSubmit(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$operation_on = $triggering_element['#operation_on'];
$operation = $triggering_element['#operation'];
$length = $operation === 'add' ? -4 : -3;
$element_parents = array_slice($triggering_element['#parents'], 0, $length);
if (!$element_parents) {
$length = $operation == 'add' ? -4 : -3;
$element_parents = array_slice($triggering_element['#array_parents'], 0, $length);
}
$element_state = self::getElementState($element_parents, $form_state);
$index = $triggering_element['#' . $operation_on . '_index'] ?? NULL;
if ($operation_on === 'row') {
$element_state = self::tableRowOperation($element_state, $form_state, $operation, $index);
}
else {
$element_state = self::tableColumnOperation($element_state, $form_state, $operation, $element_parents, $index);
}
self::setElementState($element_parents, $form_state, $element_state);
$form_state->setRebuild();
}
/**
* Submit callback for table csv import operations.
*/
public static function importCsvToTableSubmit(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$element_parents = array_slice($triggering_element['#parents'], 0, -2);
$id_prefix = implode('-', $element_parents);
$files = \Drupal::request()->files->get('files');
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $file_upload */
$file_upload = $files[$id_prefix];
$handle = $file_upload ? fopen($file_upload->getPathname(), 'r') : NULL;
if ($handle) {
// Checking the encoding of the CSV file to be UTF-8.
$encoding = 'UTF-8';
if (function_exists('mb_detect_encoding')) {
$file_contents = file_get_contents($file_upload->getPathname());
$encodings = ['UTF-8', 'ISO-8859-1', 'WINDOWS-1251'];
$encodings_list = implode(',', $encodings);
$encoding = mb_detect_encoding($file_contents, $encodings_list);
}
// Populate CSV values.
$rows_count = 0;
$element_state = [];
$user_inputs = $form_state->getUserInput();
$series = NestedArray::getValue($user_inputs, $element_parents);
$separator = $series['import']['csv_separator'];
while ($row = fgetcsv($handle, 0, $separator)) {
foreach ($row as $column_value) {
$element_state['data_collector_table'][$rows_count][] = [
'data' => self::convertEncoding($column_value, $encoding),
];
}
$rows_count++;
}
fclose($handle);
\Drupal::messenger()->addMessage(t('Successfully imported @file', [
'@file' => $file_upload->getClientOriginalName(),
]));
// Updating form state storage.
self::setElementState($element_parents, $form_state, $element_state);
// Making sure that the user input is updated as well.
$input = $form_state->getUserInput();
NestedArray::setValue($input, $element_parents, $element_state);
$form_state->setUserInput($input);
}
else {
\Drupal::messenger()
->addError(t('There was a problem importing the provided file data.'));
}
$form_state->setRebuild();
}
/**
* Utility method to build a button render array for the various data table.
*
* Operation.
*/
private static function buildOperationButton($operation, $on, $id_prefix, $wrapper_id, $index = NULL, $attributes = [], $wrapper_attributes = []) {
$name = $id_prefix . '_' . $operation . '_' . $on;
$submit = [];
if (!is_null($index)) {
$name .= '_' . $index;
$submit['#' . $on . '_index'] = $index;
}
if ($attributes) {
$submit['#attributes'] = $attributes;
}
if ($wrapper_attributes) {
$submit['#wrapper_attributes'] = $wrapper_attributes;
}
$value = [];
$value['add']['row'] = t('Add row');
$value['add']['column'] = t('Add column');
$value['delete']['row'] = t('Delete row');
$value['delete']['column'] = t('Delete column');
$submit += [
'#type' => 'submit',
'#name' => $name,
'#value' => $value[$operation][$on],
'#limit_validation_errors' => [],
'#submit' => [[get_called_class(), 'tableOperationSubmit']],
'#operation' => $operation,
'#operation_on' => $on,
'#ajax' => [
'callback' => [get_called_class(), 'ajaxRefresh'],
'wrapper' => $wrapper_id,
'effect' => 'fade',
],
];
return $submit;
}
/**
* Initializes an empty table.
*
* @param array $element
* The element.
* @param string $identifier_value
* The identifier value.
*
* @return array
* The element state storage.
*/
private static function initializeEmptyTable(array $element, string $identifier_value) {
$is_first_column = $identifier_value === self::FIRST_COLUMN;
$columns = $element['#initial_columns'];
$columns_arr = range(0, $columns - 1);
$rows = $element['#initial_rows'];
$rows_arr = range(0, $rows - 1);
$data = [];
$first_row_key = NULL;
$counter_default_used_color_index = 0;
$max_default_colors = count($element['#default_colors']);
foreach ($rows_arr as $i) {
$first_row_key = $first_row_key ?? $i;
$table_first_row = $i === $first_row_key;
$first_col_key = NULL;
foreach ($columns_arr as $j) {
$first_col_key = $first_col_key ?? $j;
$table_first_col = $j === $first_col_key;
// Used to skip category cell.
$is_category_cell = $table_first_col && $table_first_row;
$data[$i][$j]['data'] = '';
if (!$is_category_cell && (($is_first_column && $i === $first_row_key) || (!$is_first_column && $j === $first_col_key))) {
if ($counter_default_used_color_index === $max_default_colors) {
$counter_default_used_color_index = 0;
}
$data[$i][$j]['color'] = $element['#default_colors'][$counter_default_used_color_index] ?? self::randomColor();
$counter_default_used_color_index++;
}
}
}
return $data;
}
/**
* Performs add or delete operation on the table row.
*
* @param array $element_state
* The element state storage.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param string $op
* The operation.
* @param null|int $index
* The row index.
*
* @return array
* The updated element state storage.
*/
private static function tableRowOperation(array $element_state, FormStateInterface $form_state, $op, $index = NULL) {
if ($op === 'delete') {
// When only one row left we just empty it's columns.
if (count($element_state['data_collector_table']) === 1) {
$row = $element_state['data_collector_table'][$index];
$element_state['data_collector_table'][$index][] = self::emptyRowColumns($row);
return $element_state;
}
unset($element_state['data_collector_table'][$index]);
}
else {
$first_row = current($element_state['data_collector_table']);
$element_state['data_collector_table'][] = self::emptyRowColumns($first_row);
}
return $element_state;
}
/**
* Performs add or delete operation on the table column.
*
* @param array $element_state
* The element state storage.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param string $op
* The operation.
* @param array $element_parents
* The element parents.
* @param null|int $index
* The column index.
*
* @return array
* The updated element state storage.
*/
private static function tableColumnOperation(array $element_state, FormStateInterface $form_state, $op, array $element_parents, $index = NULL) {
if ($op === 'delete') {
foreach ($element_state['data_collector_table'] as $row_key => $columns) {
$row = $element_state['data_collector_table'][$row_key];
if (count(self::excludeWeightColumnFromRow($row)) === 1) {
$element_state['data_collector_table'][$row_key][$index]['data'] = '';
}
else {
array_splice($element_state['data_collector_table'][$row_key], $index, 1);
// Making sure that the user input is updated as well.
$user_input = $form_state->getUserInput();
$values = NestedArray::getValue($form_state->getUserInput(), $element_parents);
if (!empty($values['data_collector_table'][$row_key][$index])) {
array_splice($values['data_collector_table'][$row_key], $index, 1);
}
NestedArray::setValue($user_input, $element_parents, $values);
$form_state->setUserInput($user_input);
}
}
}
else {
foreach ($element_state['data_collector_table'] as $row_key => $columns) {
$element_state['data_collector_table'][$row_key][]['data'] = '';
}
}
return $element_state;
}
/**
* Excludes weight column from row.
*
* @param array $row
* The row.
*
* @return array
* The columns.
*/
private static function excludeWeightColumnFromRow(array $row) {
return array_filter(array_keys($row), function ($key) {
return is_int($key);
});
}
/**
* Empty row columns.
*
* @param array $row
* The row.
*
* @return array
* The empty row column.
*/
private static function emptyRowColumns(array $row) {
$columns = self::excludeWeightColumnFromRow($row);
$empty_row_columns = [];
foreach ($columns as $key => $column) {
$empty_row_columns[$key]['data'] = '';
}
return $empty_row_columns;
}
/**
* Helper function to detect and convert strings not in UTF-8 to UTF-8.
*
* @param string $data
* The string which needs converting.
* @param string $encoding
* The encoding of the CSV file.
*
* @return string
* UTF encoded string.
*/
private static function convertEncoding($data, $encoding) {
// Converting UTF-8 to UTF-8 will not work.
if ($encoding == 'UTF-8') {
return $data;
}
// Try to convert the data to UTF-8.
if ($encoded_data = Unicode::convertToUtf8($data, $encoding)) {
return $encoded_data;
}
// Fallback on the input data.
return $data;
}
/**
* Gets the categories from the data collected by this element.
*
* @param array $data
* The data.
* @param string $type
* The chart type.
*
* @return array
* The category label and data.
*/
public static function getCategoriesFromCollectedTable(array $data, string $type) {
$categories_identifier = $data['table_categories_identifier'] ?? '';
$table = $data['data_collector_table'];
$categories = [];
$is_first_column = $categories_identifier === self::FIRST_COLUMN;
$first_row = current($table);
$category_col_key = key($first_row);
$categories['label'] = $first_row[$category_col_key];
$data = [];
if ($is_first_column) {
if (!in_array($type, ['pie', 'donut'])) {
// Extracting the categories data.
$col_cells = array_column($table, $category_col_key);
foreach ($col_cells as $cell) {
$data[] = is_array($cell) ? $cell['data'] : $cell;
}
}
else {
$col_cells = array_values($first_row);
foreach ($col_cells as $cell) {
$data[] = is_array($cell) ? $cell['data'] : $cell;
}
}
}
else {
$col_cells = array_values($first_row);
foreach ($col_cells as $cell) {
$data[] = is_array($cell) ? $cell['data'] : $cell;
}
}
$categories['data'] = $data;
// Removing the category label from categories.
$categories_data = $categories['data'];
array_shift($categories_data);
$categories['data'] = $categories_data;
return $categories;
}
/**
* Gets the series from the data collected by this element.
*
* @param array $data
* The data.
* @param string $type
* The type of chart.
*
* @return array
* The series.
*/
public static function getSeriesFromCollectedTable(array $data, string $type) {
$table = $data['data_collector_table'];
$categories_identifier = $data['table_categories_identifier'] ?? '';
/** @var \Drupal\charts\TypeManager $chart_type_plugin_manager */
$chart_type_plugin_manager = \Drupal::service('plugin.manager.charts_type');
$chart_type = $chart_type_plugin_manager->getDefinition($type);
$is_single_axis = $chart_type['axis'] === ChartInterface::SINGLE_AXIS;
$is_first_column = $categories_identifier === self::FIRST_COLUMN;
$first_row = current($table);
$category_col_key = key($first_row);
// Skip the first row if it's considered as the holding categories data.
if (!$is_first_column) {
array_shift($table);
}
$series = [];
$i = 0;
foreach ($table as $row) {
if (!$is_first_column) {
$name_key = key($row);
$series[$i]['name'] = $row[$name_key]['data'] ?? [];
$series[$i]['color'] = $row[$name_key]['color'] ?? '';
// Removing the name from data array.
unset($row[$name_key]);
foreach ($row as $column) {
// Get all the data in this column and break out of this loop.
if ($is_single_axis) {
if (is_numeric($column) || is_string($column)) {
$series[$i]['data'][] = [
$series[$i]['name'],
self::castValueToNumeric($column),
];
}
elseif (is_array($column) && isset($column['data'])) {
$series[$i]['data'][] = [
$series[$i]['name'],
self::castValueToNumeric($column['data']),
];
}
}
else {
if (is_numeric($column) || is_string($column)) {
$series[$i]['data'][] = self::castValueToNumeric($column);
}
elseif (is_array($column) && isset($column['data'])) {
$series[$i]['data'][] = self::castValueToNumeric($column['data']);
}
}
}
// Adding a couple types not currently supported but hopefully soon.
if (in_array($type, [
'scatter',
'bubble',
'candlestick',
'boxplot',
])) {
// Enclose the data value in an array.
$series[$i]['data'] = [$series[$i]['data']];
}
}
else {
$j = 0;
foreach ($row as $column_key => $column) {
// Skipping the category label and it's data.
if ($column_key === $category_col_key || !is_numeric($column_key)) {
continue;
}
elseif ($i === 0) {
// This is the first column which holds the data names and colors.
$series[$j]['name'] = $column['data'] ?? $column;
$series[$j]['color'] = $column['color'] ?? self::randomColor();
}
else {
// Get all the data in this column and break out of this loop.
$cell_value = is_array($column) && isset($column['data']) ? $column['data'] : $column;
$cell_value = self::castValueToNumeric($cell_value);
if ($is_single_axis) {
$series[$j]['data'][] = [$series[$j]['name'], $cell_value];
$series[$j]['title'][] = $row[0]['data'];
}
elseif (in_array($type, [
'scatter',
'bubble',
'candlestick',
'boxplot',
])) {
$series[$j]['data'][0][] = $cell_value;
}
else {
$series[$j]['data'][] = $cell_value;
}
}
$j++;
}
}
$i++;
}
return $series;
}
/**
* Casts string value to numeric.
*
* @param string $value
* The value.
*
* @return float|int
* The numeric value.
*/
private static function castValueToNumeric($value) {
if (is_numeric($value)) {
$value = is_int($value) ? (integer) $value : (float) $value;
}
elseif ($value === '') {
$value = NULL;
}
else {
$value = 0;
}
return $value;
}
}

Some files were not shown because too many files have changed in this diff Show More