├── .browserslistrc
├── .editorconfig
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── stale.yml
└── workflows
│ ├── build-and-deploy.yml
│ └── test-and-build.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .prettierrc
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── angular.json
├── commitlint.config.js
├── package-lock.json
├── package.json
├── projects
├── angular-google-charts
│ ├── README.md
│ ├── jest.config.js
│ ├── ng-package.json
│ ├── ng-package.prod.json
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ └── lib
│ │ │ ├── components
│ │ │ ├── chart-base
│ │ │ │ └── chart-base.component.ts
│ │ │ ├── chart-editor
│ │ │ │ ├── chart-editor-ref.spec.ts
│ │ │ │ ├── chart-editor-ref.ts
│ │ │ │ ├── chart-editor.component.spec.ts
│ │ │ │ ├── chart-editor.component.ts
│ │ │ │ └── chart-editor.ts
│ │ │ ├── chart-wrapper
│ │ │ │ ├── chart-wrapper.component.spec.ts
│ │ │ │ └── chart-wrapper.component.ts
│ │ │ ├── control-wrapper
│ │ │ │ ├── control-wrapper.component.spec.ts
│ │ │ │ └── control-wrapper.component.ts
│ │ │ ├── dashboard
│ │ │ │ ├── dashboard.component.spec.ts
│ │ │ │ └── dashboard.component.ts
│ │ │ └── google-chart
│ │ │ │ ├── google-chart.component.spec.ts
│ │ │ │ └── google-chart.component.ts
│ │ │ ├── google-charts.module.spec.ts
│ │ │ ├── google-charts.module.ts
│ │ │ ├── helpers
│ │ │ ├── chart.helper.ts
│ │ │ └── id.helper.ts
│ │ │ ├── services
│ │ │ ├── data-table.service.ts
│ │ │ ├── script-loader.service.spec.ts
│ │ │ └── script-loader.service.ts
│ │ │ └── types
│ │ │ ├── chart-type.ts
│ │ │ ├── control-type.ts
│ │ │ ├── events.ts
│ │ │ ├── formatter.ts
│ │ │ └── google-charts-config.ts
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ ├── tsconfig.spec.json
│ └── tslint.json
└── playground
│ ├── .browserslistrc
│ ├── src
│ ├── app
│ │ ├── app-routing.module.ts
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ ├── main
│ │ │ ├── main.component.html
│ │ │ └── main.component.ts
│ │ └── test
│ │ │ ├── test.component.html
│ │ │ └── test.component.ts
│ ├── assets
│ │ └── .gitkeep
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ └── styles.scss
│ ├── tsconfig.app.json
│ └── tslint.json
├── tsconfig.json
└── tslint.json
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed
5 | > 0.5%
6 | last 2 versions
7 | Firefox ESR
8 | not dead
9 | # IE 9-11
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report a bug in angular-google-charts
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 | ---
8 |
9 | # Bug Report
10 |
11 | ## Prerequisites
12 |
13 | Please answer the following questions for yourself before submitting an issue.
14 | **YOU MAY DELETE THE PREREQUISITES SECTION.**
15 |
16 | - [ ] I am running the latest version
17 | - [ ] I checked the Readme and found no answer
18 | - [ ] I checked to make sure that this issue has not already been filed
19 | - [ ] This is related to angular-google-charts and not to Google Charts directly (if it's a Google Charts issue, [their forum](https://groups.google.com/forum/#!forum/google-visualization-api) may help)
20 |
21 | ## Description
22 |
23 | A clear and concise description of what the bug is.
24 |
25 | ## To reproduce
26 |
27 | Steps to reproduce the behaviour:
28 |
29 | 1. Go to '...'
30 | 2. Click on '....'
31 | 3. Scroll down to '....'
32 | 4. See error
33 |
34 | ## Expected behaviour
35 |
36 | A clear and concise description of what you expected to happen.
37 |
38 | ## Exception or Error
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | ## Your environment
47 |
48 |
49 |
50 |
51 |
52 | ## Anything else?
53 |
54 | Add any other context about the problem here.
55 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest a new feature for this project
4 | title: ''
5 | labels: feature
6 | assignees: ''
7 | ---
8 |
9 | # Feature request
10 |
11 | ## Describe the solution you'd like
12 |
13 | A clear and concise description of what you want to happen. If this is related to a feature in the Google Charts library, please include links to the documentation.
14 |
15 | ## Describe alternatives you've considered
16 |
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | ## Additional context
20 |
21 | Add any other context or screenshots about the feature request here.
22 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 7
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | # Label to use when marking an issue as stale
10 | staleLabel: stale
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | This issue has been automatically marked as stale because it has not had
14 | recent activity. It will be closed if no further activity occurs. Thank you
15 | for your contributions.
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-deploy.yml:
--------------------------------------------------------------------------------
1 | name: Build and deploy
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*.*.*'
7 |
8 | jobs:
9 | build-and-deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: '18.10'
16 | registry-url: 'https://registry.npmjs.org'
17 | cache: 'npm'
18 | - run: npm ci
19 | - run: npm run build
20 | - run: npm publish ./dist/angular-google-charts
21 | env:
22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
23 |
--------------------------------------------------------------------------------
/.github/workflows/test-and-build.yml:
--------------------------------------------------------------------------------
1 | name: Test and build
2 |
3 | on:
4 | push:
5 | branches: ['master']
6 | pull_request:
7 | branches: ['master']
8 |
9 | jobs:
10 | test-and-build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: '18.10'
17 | cache: 'npm'
18 | - run: npm ci
19 | - run: npm run build
20 | - name: Run tests
21 | run: npm run test -- --ci --coverage --reporters default --reporters jest-junit
22 | - name: Upload test results
23 | uses: actions/upload-artifact@v4
24 | with:
25 | name: test-results
26 | path: test-results
27 | - name: Upload coverage report
28 | uses: actions/upload-artifact@v4
29 | with:
30 | name: coverage
31 | path: coverage
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # test results
28 | /coverage
29 | /test-results
30 |
31 | # misc
32 | /.angular/cache
33 | /.sass-cache
34 | /connect.lock
35 | /libpeerconnection.log
36 | npm-debug.log
37 | yarn-error.log
38 | testem.log
39 | /typings
40 |
41 | # System Files
42 | .DS_Store
43 | Thumbs.db
44 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | commitlint --edit $1
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | lint-staged
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "semi": true,
4 | "singleQuote": true,
5 | "printWidth": 120,
6 | "quoteProps": "as-needed",
7 | "trailingComma": "none",
8 | "arrowParens": "avoid"
9 | }
10 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
4 | // List of extensions which should be recommended for users of this workspace.
5 | "recommendations": [
6 | "esbenp.prettier-vscode",
7 | "ms-vscode.vscode-typescript-tslint-plugin",
8 | "msjsdiag.debugger-for-chrome",
9 | "angular.ng-template",
10 | "orta.vscode-jest"
11 | ],
12 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
13 | "unwantedRecommendations": []
14 | }
15 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "chrome",
9 | "request": "attach",
10 | "name": "Attach to Chrome",
11 | "port": 9222,
12 | "webRoot": "${workspaceFolder}"
13 | },
14 | {
15 | "type": "node",
16 | "request": "launch",
17 | "name": "Debug Current Test File",
18 | "program": "${workspaceFolder}/node_modules/@angular/cli/bin/ng",
19 | "args": [
20 | "test",
21 | "angular-google-charts",
22 | "--runInBand=true",
23 | "--coverage=false",
24 | "--testPathPattern=${fileBasename}"
25 | ],
26 | "cwd": "${workspaceFolder}",
27 | "console": "integratedTerminal",
28 | "trace": "all"
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "[typescript]": {
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll.tslint": "explicit"
6 | }
7 | },
8 | "git.rebaseWhenSync": true
9 | }
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [16.0.2](https://github.com/FERNman/angular-google-charts/compare/v16.0.1...v16.0.2) (2025-01-17)
6 |
7 | ### [16.0.1](https://github.com/FERNman/angular-google-charts/compare/v16.0.0...v16.0.1) (2024-09-12)
8 |
9 | ### Bug Fixes
10 |
11 | - include typings in production build ([39cf41b](https://github.com/FERNman/angular-google-charts/commit/39cf41bb5ed4b0c57bbbc77d315fee8ddf8338b2))
12 |
13 | ## [16.0.0](https://github.com/FERNman/angular-google-charts/compare/v12.0.0...v16.0.0) (2024-09-12)
14 |
15 | ### ⚠ BREAKING CHANGES
16 |
17 | - Angular 16 (#321)
18 |
19 | - Angular 16 ([#321](https://github.com/FERNman/angular-google-charts/issues/321)) ([d236480](https://github.com/FERNman/angular-google-charts/commit/d236480e92f1436b5539a4e8b67a86f2d1f183d3))
20 |
21 | ## [12.0.0](https://github.com/FERNman/angular-google-charts/compare/v2.2.4...v12.0.0) (2024-09-12)
22 |
23 | ### Features
24 |
25 | - expand `row` type ([#313](https://github.com/FERNman/angular-google-charts/issues/313)) ([ea6456b](https://github.com/FERNman/angular-google-charts/commit/ea6456b0747a3e17e364faa04a06492af0a1ce59))
26 |
27 | ### [2.2.4](https://github.com/FERNman/angular-google-charts/compare/v2.2.3...v2.2.4) (2024-09-02)
28 |
29 | ### Bug Fixes
30 |
31 | - [#248](https://github.com/FERNman/angular-google-charts/issues/248) fixed WordTree in chart-type.ts, added sample WordTree ([#277](https://github.com/FERNman/angular-google-charts/issues/277)) ([34575a1](https://github.com/FERNman/angular-google-charts/commit/34575a1f757cc451a0f055280172c5686610c816))
32 |
33 | ### [2.2.3](https://github.com/FERNman/angular-google-charts/compare/v2.2.2...v2.2.3) (2022-07-18)
34 |
35 | ### [2.2.2](https://github.com/FERNman/angular-google-charts/compare/v2.2.1...v2.2.2) (2021-06-12)
36 |
37 | ### [2.2.1](https://github.com/FERNman/angular-google-charts/compare/v2.2.0...v2.2.1) (2021-05-03)
38 |
39 | ### Bug Fixes
40 |
41 | - resize subscription memory leak ([#226](https://github.com/FERNman/angular-google-charts/issues/226)) ([c1f49a0](https://github.com/FERNman/angular-google-charts/commit/c1f49a05a6351ba9f5ef949baf47fa6cad191be4)), closes [#225](https://github.com/FERNman/angular-google-charts/issues/225)
42 |
43 | ## [2.2.0](https://github.com/FERNman/angular-google-charts/compare/v2.1.1...v2.2.0) (2021-04-19)
44 |
45 | ### Features
46 |
47 | - add lazy loading for config ([#221](https://github.com/FERNman/angular-google-charts/issues/221)) ([f04d689](https://github.com/FERNman/angular-google-charts/commit/f04d689ac2a496b85ae51fa982f070edcb3dbe0c))
48 |
49 | ### [2.1.1](https://github.com/FERNman/angular-google-charts/compare/v2.1.0...v2.1.1) (2021-04-15)
50 |
51 | ### Bug Fixes
52 |
53 | - loading correct package based on chart type ([#219](https://github.com/FERNman/angular-google-charts/issues/219)) ([d6e6922](https://github.com/FERNman/angular-google-charts/commit/d6e69226f80af3f7cd78cd925c562a99120f4ca5))
54 |
55 | ## [2.1.0](https://github.com/FERNman/angular-google-charts/compare/v2.0.1...v2.1.0) (2021-04-10)
56 |
57 | ### Features
58 |
59 | - added support for add/remove custom event listeners ([65c6836](https://github.com/FERNman/angular-google-charts/commit/65c6836ce85f8db9be1f213491ea3a1c646bcaf9))
60 | - add dashboard formatters ([9fbfefd](https://github.com/FERNman/angular-google-charts/commit/9fbfefd870f3e4b685433034e8ceed4f6ef9a3be))
61 |
62 | ### Bug Fixes
63 |
64 | - get/set chartWrapper ([bf3a194](https://github.com/FERNman/angular-google-charts/commit/bf3a194e49a55fbbda2bef188e5bc02df44e89eb))
65 |
66 | ### [2.0.1](https://github.com/FERNman/angular-google-charts/compare/v2.0.0...v2.0.1) (2021-01-27)
67 |
68 | ## [2.0.0](https://github.com/FERNman/angular-google-charts/compare/v1.1.6...v2.0.0) (2021-01-16)
69 |
70 | ### ⚠ BREAKING CHANGES
71 |
72 | - compile in strict mode
73 |
74 | ### Features
75 |
76 | - **error-dash-events:** add error and ready listeners ([853dac4](https://github.com/FERNman/angular-google-charts/commit/853dac4c81408bcdb46819b5522196cf36f09755))
77 |
78 | ### Bug Fixes
79 |
80 | - dataTable contains stale values ([4c9f7c6](https://github.com/FERNman/angular-google-charts/commit/4c9f7c69e7f4c7fd829b54687590d7858eb913d0))
81 | - provide script-loader service in module ([a9dd652](https://github.com/FERNman/angular-google-charts/commit/a9dd65215b1f3e0eb2cf4d10350747cd84412a40))
82 |
83 | - compile in strict mode ([d387928](https://github.com/FERNman/angular-google-charts/commit/d38792859231cabf4cf16d62da669fea5dbe7e32))
84 |
85 | ### [1.1.6](https://github.com/FERNman/angular-google-charts/compare/v1.1.5...v1.1.6) (2020-09-12)
86 |
87 | ### Bug Fixes
88 |
89 | - compilation errors because of types ([24a5d6c](https://github.com/FERNman/angular-google-charts/commit/24a5d6c0b8c8e6e403e2ac4b9e0ab39196e76641)), closes [#167](https://github.com/FERNman/angular-google-charts/issues/167)
90 |
91 | ### [1.1.5](https://github.com/FERNman/angular-google-charts/compare/v1.1.4...v1.1.5) (2020-09-07)
92 |
93 | ### Bug Fixes
94 |
95 | - incorrect documentation links ([eaba673](https://github.com/FERNman/angular-google-charts/commit/eaba67300582e8a49a03fcf698f5233d18271891))
96 |
97 | ### [1.1.4](https://github.com/FERNman/angular-google-charts/compare/v1.1.3...v1.1.4) (2020-04-26)
98 |
99 | ### Bug Fixes
100 |
101 | - compile error when using typescript < 3.7 ([7e9ff39](https://github.com/FERNman/angular-google-charts/commit/7e9ff396ce7a92e4d23d6737f43f6fc050b07cd5)), closes [microsoft/typescript#33939](https://github.com/microsoft/typescript/issues/33939) [#140](https://github.com/FERNman/angular-google-charts/issues/140)
102 |
103 | ### [1.1.3](https://github.com/FERNman/angular-google-charts/compare/v1.1.2...v1.1.3) (2020-04-23)
104 |
105 | ### Bug Fixes
106 |
107 | - no selector for directive error ([95e594b](https://github.com/FERNman/angular-google-charts/commit/95e594b38256ff88dd5d18313d3f478f4afdb8a5))
108 |
109 | ### [1.1.2](https://github.com/FERNman/angular-google-charts/compare/v1.1.1...v1.1.2) (2020-04-22)
110 |
111 | ### Bug Fixes
112 |
113 | - select event not firing ([624b080](https://github.com/FERNman/angular-google-charts/commit/624b080d443e696b38c222b07f540bc52b8993bb))
114 |
115 | ### [1.1.1](https://github.com/FERNman/angular-google-charts/compare/v1.1.0...v1.1.1) (2020-04-22)
116 |
117 | ## [1.1.0](https://github.com/FERNman/angular-google-charts/compare/v0.1.6...v1.1.0) (2020-04-19)
118 |
119 | ### ⚠ BREAKING CHANGES
120 |
121 | - always load the `google.visualization` namespace
122 | - rename raw chart to chart wrapper
123 | - raw chart component
124 | - google chart component
125 | - script loader service public interface
126 |
127 | ### Features
128 |
129 | - add safe mode to config ([e11974c](https://github.com/FERNman/angular-google-charts/commit/e11974c9ae8a851329d99b00251051cb3f29059b))
130 | - always load the `google.visualization` namespace ([1a9d892](https://github.com/FERNman/angular-google-charts/commit/1a9d892ff721693d6636b24670f325b91a533c05))
131 | - controls and dashboards ([3c7c497](https://github.com/FERNman/angular-google-charts/commit/3c7c497edcfd9d11db61eafd1ed251349b6fa55f))
132 | - editing charts ([c6eda2d](https://github.com/FERNman/angular-google-charts/commit/c6eda2db8b270f7289c911a789ba65aac1cb0d4e))
133 | - provide config as object ([2d5953f](https://github.com/FERNman/angular-google-charts/commit/2d5953fb62401890e81d6d6cc170eb05ac797597))
134 |
135 | ### Bug Fixes
136 |
137 | - mouse events emitting multiple times if chart is redrawn ([20e6ad1](https://github.com/FERNman/angular-google-charts/commit/20e6ad1e27018ad5c300b23c4a374c2d43b02466)), closes [#83](https://github.com/FERNman/angular-google-charts/issues/83)
138 | - run callbacks after loading scripts in angular zone ([b567030](https://github.com/FERNman/angular-google-charts/commit/b567030fa7821549eef4ecde135c5431755a5271))
139 |
140 | ### improvement
141 |
142 | - script loader service public interface ([8f1f36b](https://github.com/FERNman/angular-google-charts/commit/8f1f36b0254d6444cf5bc9da556176bac85713f3))
143 |
144 | * google chart component ([013f978](https://github.com/FERNman/angular-google-charts/commit/013f978dae88cceb963983ae353574344c41726d))
145 | * raw chart component ([ed88549](https://github.com/FERNman/angular-google-charts/commit/ed885493882d9c7266c28a44416cb406eccdafed))
146 | * rename raw chart to chart wrapper ([875e71e](https://github.com/FERNman/angular-google-charts/commit/875e71e6eacaf119314d2b3e4d32d64cca35665d))
147 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # License
2 |
3 | MIT License
4 |
5 | Copyright (c) 2024 Gabriel Sperrer
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8 |
9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular-Google-Charts
2 |
3 |   [](https://opensource.org/licenses/MIT) [](https://github.com/prettier/prettier)
4 |
5 | > A wrapper for the [Google Charts library](https://developers.google.com/chart/) written in Angular.
6 |
7 | ## Setup
8 |
9 | ### Installation
10 |
11 | To use Angular-Google-Charts in your project, install the package with npm by calling
12 |
13 | ```bash
14 | npm install angular-google-charts
15 | ```
16 |
17 | This will add the package to your package.json and install the required dependencies.
18 |
19 | ### Importing
20 |
21 | Import the `GoogleChartsModule` in your `app.module.ts`:
22 |
23 | ```typescript
24 | import { GoogleChartsModule } from 'angular-google-charts';
25 |
26 | @NgModule({
27 | ...
28 | imports: [
29 | ...
30 | GoogleChartsModule,
31 | ...
32 | ],
33 | ...
34 | })
35 | export class AppModule {}
36 | ```
37 |
38 | This will allow you to use all of the features provided by this library.
39 |
40 | #### Configuring
41 |
42 | For some use cases, it might be necessary to use some different config options than the default values.
43 |
44 | All config options for Angular Google Charts are provided through a config object, which
45 | can be passed to the library by importing the `GoogleChartsModule` using its `forRoot` method
46 | or by providing the `GOOGLE_CHARTS_LAZY_CONFIG` injection token with an `Observable` value.
47 |
48 | ##### Using forRoot
49 |
50 | Here you will pass the options that are passed to the `google.charts.load` method in the normal JavaScript library.
51 | For instance, to change the [version](https://developers.google.com/chart/interactive/docs/basic_load_libs#load-version-name-or-number)
52 |
53 | ```typescript
54 | GoogleChartsModule.forRoot({ version: 'chart-version' }),
55 | ```
56 |
57 | Another example, to specify the Google Maps API key, or any other [Settings](https://developers.google.com/chart/interactive/docs/basic_load_libs#load-settings):
58 |
59 | ```typescript
60 | GoogleChartsModule.forRoot({ mapsApiKey: '' }),
61 | ```
62 |
63 | ##### Using lazy loading
64 |
65 | ###### Option #1
66 |
67 | ```typescript
68 | // Provide an observable through a service that fetches your chart configurations
69 |
70 | @Injectable()
71 | export class GoogleChartsConfigService {
72 | private configSubject = new ReplaySubject(1);
73 | readonly config$ = this.configSubject.asObservable();
74 |
75 | constructor(private http: HttpClient) {}
76 |
77 | loadLazyConfigValues(): void {
78 | this.http.post('https://special.config.api.com/getchartsconfig', {})
79 | .pipe(take(1))
80 | .subscribe(config => this.configSubject.next(config));
81 | }
82 | }
83 |
84 | // Factory function that provides the config$ observable from your GoogleChartsConfigService
85 | export function googleChartsConfigFactory(configService: GoogleChartsConfigService): Observable {
86 | return configService.config$;
87 | }
88 |
89 | @NgModule({
90 | ...
91 | providers: [
92 | GoogleChartsConfigService,
93 | {provide: GOOGLE_CHARTS_LAZY_CONFIG, useFactory: googleChartsConfigFactory, deps: [GoogleChartsConfigService]}
94 | ]
95 | })
96 | export class AppModule {}
97 |
98 | ```
99 |
100 | ###### Option #2
101 |
102 | ```typescript
103 | // Use a global subject (whether this violates best practices in your case is up to you).
104 | // This is just to point out a more simple way of achieving a lazy-loaded config.
105 | export const googleChartsConfigSubject = new ReplaySubject(1);
106 |
107 | // Call this from anywhere you want
108 | googleChartsConfigSubject.next(config);
109 |
110 | // Your app.module
111 | @NgModule({
112 | ...
113 | providers: [
114 | {provide: GOOGLE_CHARTS_LAZY_CONFIG, useValue: googleChartsConfigSubject.asObservable()}
115 | ]
116 | })
117 | export class AppModule {}
118 | ```
119 |
120 | #### NOTE
121 |
122 | - You can provide options through the `forRoot` function **OR** the `GOOGLE_CHARTS_LAZY_CONFIG` token. You cannot use them interchangeably.
123 | - If you provide a lazy-loaded config object then the charts will not render until the observable has a value for the subscriber.
124 |
125 | ## Charts
126 |
127 | The easiest way to create a chart is using the `GoogleChartComponent`.
128 |
129 | ```html
130 |
131 | ```
132 |
133 | Using the component, it is possible to create every chart in the Google Charts library.
134 | It has a few important input properties, which are explained below.
135 |
136 | ### Type
137 |
138 | ```html
139 |
140 | ```
141 |
142 | The type of chart you want to create. Must be of type `ChartType`. Check [this file](https://github.com/FERNman/angular-google-charts/blob/master/projects/angular-google-charts/src/lib/types/chart-type.ts) for a list of the supported types
143 |
144 | To see examples for all chart types and more information, visit the [google chart gallery](https://developers.google.com/chart/interactive/docs/gallery).
145 |
146 | ### Data
147 |
148 | ```html
149 |
150 | ```
151 |
152 | The data property expects an array of a certain shape, which depends on the chart type. Some chart types even support different data formats depending on the mode.
153 |
154 | Example with a chart that expects two-dimensional arrays:
155 |
156 | ```typescript
157 | myData = [
158 | ['London', 8136000],
159 | ['New York', 8538000],
160 | ['Paris', 2244000],
161 | ['Berlin', 3470000],
162 | ['Kairo', 19500000],
163 | ...
164 | ];
165 | ```
166 |
167 | The data object can also include formatters for the given data. To use these, pass an object of type `{ v: any, f: string }` as the data values in the inner array. The property `v` should contain the real value, and the property `f` the formatted value.
168 |
169 | Formatters can also be passed as a separate input property, see [Formatters](#formatters);
170 |
171 | ```typescript
172 | myData = [
173 | ['London', {v: 8136000, f: '8,1360'}],
174 | ['New York', {v: 8538000, f: '8,530'}],
175 | ...
176 | ];
177 | ```
178 |
179 | For further information, please see the official [documentation](https://developers.google.com/chart/interactive/docs/reference#arraytodatatable) on `ArrayToDataTable`, which is the function used internally.
180 |
181 | ### Columns
182 |
183 | ```html
184 |
185 | ```
186 |
187 | The `columns` property expects an array describing the columns chart data array. The number of entries must match the length of the inner array passed in the `data` property.
188 | Some charts don't require columns. Whether your chart requires it can be check in the official documentation.
189 |
190 | Continuing with the simple two-dimensional example:
191 |
192 | ```typescript
193 | chartColumns = ['City', 'Inhabitants'];
194 | ```
195 |
196 | For more complex formats an array of objects can be passed. For instance, the GeoChart in markers mode expects 4 columns of type number:
197 |
198 | ```typescript
199 | chartColumns = [
200 | { type: 'number', role: 'latitude' },
201 | { type: 'number', role: 'longitude' },
202 | { type: 'number', role: 'markerColor' },
203 | { type: 'number', role: 'markerSize' }
204 | ];
205 | ```
206 |
207 | ### Title
208 |
209 | ```html
210 |
211 | ```
212 |
213 | The `title` property is optional and provided for convenience. It can also be included in the `options` property.
214 |
215 | ### Width
216 |
217 | ```html
218 |
219 | ```
220 |
221 | The `width` property is optional and allows to set the width of the chart. The number provided will be converted to a pixel value. The default is `undefined`, which makes the chart figure out its width by itself.
222 | You can also set the width using CSS, which has the advantage of allowing `%` values instead of only pixels. For more information on that, see [dynamic resize](#dynamic-resize).
223 |
224 | ### Height
225 |
226 | ```html
227 |
228 | ```
229 |
230 | The `height` property is optional and allows to set the height of the chart. The number provided will be converted to a pixel value. The default is `undefined`, which makes the chart figure out its height by itself.
231 | You can also set the height using CSS, which has the advantage of allowing `%` values instead of only pixels. For more information on that, see [dynamic resize](#dynamic-resize).
232 |
233 | ### Options
234 |
235 | ```html
236 |
237 | ```
238 |
239 | The `options` property is optional and allows to customize the chart to a great extent. How and what you can customize depends on the type of chart. For more information, please see the [google documentation](https://developers.google.com/chart/interactive/docs/customizing_charts).
240 |
241 | ```typescript
242 | // example
243 | myOptions = {
244 | colors: ['#e0440e', '#e6693e', '#ec8f6e', '#f3b49f', '#f6c7b6'],
245 | is3D: true
246 | };
247 | ```
248 |
249 | ### Formatters
250 |
251 | ```html
252 |
253 | ```
254 |
255 | The `formatter` property is optional and allows to format the chart data. It requires an array of objects containing a formatter and an index.
256 |
257 | For more information and all formatter types, please refer to the [documentation](https://developers.google.com/chart/interactive/docs/reference#formatters).
258 |
259 | ```typescript
260 | // Formats the column with the index 1 and 3 to Date(long)
261 | myFormatters = [
262 | {
263 | formatter: new google.visualization.DateFormat({ formatType: 'long' }),
264 | colIndex: 1
265 | },
266 | {
267 | formatter: new google.visualization.DateFormat({ formatType: 'long' }),
268 | colIndex: 3
269 | }
270 | ];
271 | ```
272 |
273 | _Note: When you get the error "google is not defined" whilst using the formatter in your component, you probably didn't load the google charts script. Please read the chapter on using the [ScriptLoaderService](#using-the-scriptloaderservice)._
274 |
275 | ### Dynamic Resize
276 |
277 | ```html
278 |
279 | ```
280 |
281 | The `dynamicResize` property is optional and makes your chart redraw every time the window is resized.
282 | Defaults to `false` and should only be used when setting the width or height of the chart to a percentage value.
283 | Otherwise, the chart gets redrawn unnecessary and therefore slows down the site.
284 |
285 | ### Styling
286 |
287 | ```html
288 |
289 | ```
290 |
291 | Most CSS properties should work exactly as you would expect them to.
292 | If you want to have the chart full-width for example, set the width to `100%`.
293 |
294 | ## Events
295 |
296 | The `GoogleChartComponent` provides bindings for the most common Google Chart events.
297 |
298 | ### Ready
299 |
300 | The [`ready` event](https://developers.google.com/chart/interactive/docs/events#the-ready-event) is emitted as soon as the chart got drawn and after every subsequent redraw.
301 |
302 | ```html
303 |
304 | ```
305 |
306 | The event is of type `ChartReadyEvent`.
307 |
308 | ### Error
309 |
310 | The [`error` event](https://developers.google.com/chart/interactive/docs/events#the-error-event) is emitted when an internal error occurs. However, since the newer versions of google-charts, most errors are displayed in the chart HTML as well. It can be bound to like this:
311 |
312 | ```html
313 |
314 | ```
315 |
316 | The event is of type `ChartErrorEvent`.
317 |
318 | ### Select
319 |
320 | The [`select` event](https://developers.google.com/chart/interactive/docs/events#the-select-event) is emitted when an element in the chart gets selected.
321 |
322 | ```html
323 |
324 | ```
325 |
326 | The event of type `ChartSelectionChangedEvent` containing an array of selected values.
327 |
328 | ### Mouseover
329 |
330 | The `mouseover` event fires when the mouse hovers over one of the charts elements (i. e. a bar in a bar chart or a segment in a pie chart).
331 |
332 | ```html
333 |
334 | ```
335 |
336 | The event is of type `ChartMouseOverEvent`, where `column` is the index of the hovered column and `row` is the index of the hovered row.
337 |
338 | ### Mouseleave
339 |
340 | The `mouseleave` event fires when the mouse stops hovering one of the charts elements (i. e. a bar in a bar chart or a segment in a pie chart).
341 |
342 | ```html
343 |
344 | ```
345 |
346 | The event is of type `ChartMouseLeaveEvent`, where `column` is the index of the no-longer hovered column and `row` is the index of the no-longer hovered row.
347 |
348 | ## Controls and Dashboards
349 |
350 | Google Charts supports combining multiple charts into dashboards and giving users controls to manipulate what data they show, see [their documentation](https://developers.google.com/chart/interactive/docs/gallery/controls). Using this library, dashboards can be created easily.
351 |
352 | A dashboard component can be instantiated, which can contain child controls and charts. Every control must specify one or more charts they are controlling via their `for` property. It accepts a single chart as well as an array of charts, and one chart can be controlled by multiple controls.
353 |
354 | ```html
355 |
356 |
357 |
358 |
359 | ```
360 |
361 | When creating dashboards, the charts themselves are not responsible for drawing, which means their `columns`, `data`, and (optional) `formatter` properties are unused. Instead, the dashboard is responsible for drawing. It therefore accepts data in the same format as charts do through the `columns`, `data`, and `formatter` properties.
362 |
363 | Note that charts in a dashboard will not be visible if they are not referenced in at least one control.
364 |
365 | ## Editing Charts
366 |
367 | Google Charts comes with a full-fledged [chart editor](https://developers.google.com/chart/interactive/docs/reference#google_visualization_charteditor),
368 | allowing users to configure charts the way they want.
369 |
370 | Angular-Google-Charts includes a component wrapping the native `ChartEditor`, the `ChartEditorComponent`.
371 | It has to be instantiated in HTML and can be used to edit charts by calling its `editChart` method.
372 |
373 | ```html
374 |
375 |
376 |
377 |
378 |
379 | ```
380 |
381 | ```typescript
382 | // my.component.ts
383 | class MyComp {
384 | @ViewChild(ChartEditorComponent)
385 | public readonly editor: ChartEditorComponent;
386 |
387 | public editChart(chart: ChartBase) {
388 | this.editor
389 | .editChart(chart)
390 | .afterClosed()
391 | .subscribe(result => {
392 | if (result) {
393 | // Saved
394 | } else {
395 | // Cancelled
396 | }
397 | });
398 | }
399 | }
400 | ```
401 |
402 | `editChart` returns a handle to the open dialog which can be used to close the edit dialog.
403 |
404 | Note that only one chart can be edited by a chart editor at a time.
405 |
406 | ## Advanced
407 |
408 | ### Accessing the chart wrapper directly
409 |
410 | I case you don't need any of the special features the `GoogleChartsComponent` provides, the `ChartWrapperComponent` can be used.
411 | It is a direct wrapper of the [`ChartWrapper`](https://developers.google.com/chart/interactive/docs/reference#chartwrapper-class)..
412 |
413 | ```html
414 |
415 | ```
416 |
417 | The `ChartWrapperComponent` should be used if you need fine-grained control over the data you are providing or you want to use e.g.
418 | the query feature that Google Charts provides, which is not supported using the `GoogleChartComponent`.
419 |
420 | ### Using the `ScriptLoaderService`
421 |
422 | If a specific chart is created a lot in your application, you may want to create custom components.
423 |
424 | When doing so, you need to load the chart packages by yourself.
425 | The `ScriptLoaderService` provides a few methods helping with this.
426 |
427 | ```typescript
428 | class MyComponent {
429 | private readonly chartPackage = getPackageForChart(ChartType.BarChart);
430 |
431 | @ViewChild('container', { read: ElementRef })
432 | private containerEl: ElementRef;
433 |
434 | constructor(private loaderService: ScriptLoaderService) {}
435 |
436 | ngOnInit() {
437 | this.loaderService.loadChartPackages(this.chartPackage).subscribe(() => {
438 | // Start creating your chart now
439 | const char = new google.visualization.BarChart(this.containerEl.nativeElement);
440 | });
441 | }
442 | }
443 | ```
444 |
445 | The `loadChartPackages` method can also be called without any parameters. This way, only the default
446 | google charts packages will be loaded. These include the namespaces `google.charts` and `google.visualization`, but no charts.
447 |
448 | ### Preloading the Google Charts script
449 |
450 | If the existence of charts is crucial to your application, you may want to decrease the time it takes until the first chart becomes visible.
451 | This can be achieved by loading the Google Charts script concurrently with the rest of the application.
452 | In the playground application, this reduces the time until the first chart appears by roughly 20%, which means for
453 | example about 4 seconds when using the "Slow 3G" profile in Chrome DevTools.
454 |
455 | To achieve this, two scripts have to be added to the `index.html` file in your apps' root folder.
456 | The first one loads the generic Google Charts script, the second one the version-specific parts of the library needed to load charts.
457 |
458 | In the code below, `` has to be replaced with the **exact** of the Google Charts library that you want to use and must match the version you use when importing the `GoogleChartsModule`.
459 |
460 | The only exception to this is version `46`. All minor versions of Google Charts v46 require the loader to be of version `46.2`.
461 |
462 | ```html
463 |
464 |
465 | ```
466 |
467 | Please note that this can increase the time it takes until Angular is fully loaded.
468 | I suggest doing some benchmarks with your specific application before deploying this to production.
469 |
470 | ## License
471 |
472 | This project is provided under the [MIT license](https://github.com/FERNman/angular-google-charts/blob/master/LICENSE.md).
473 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-google-charts": {
7 | "root": "projects/angular-google-charts",
8 | "sourceRoot": "projects/angular-google-charts/src",
9 | "projectType": "library",
10 | "prefix": "lib",
11 | "architect": {
12 | "build": {
13 | "builder": "@angular-devkit/build-angular:ng-packagr",
14 | "options": {
15 | "tsConfig": "projects/angular-google-charts/tsconfig.lib.json",
16 | "project": "projects/angular-google-charts/ng-package.json"
17 | },
18 | "configurations": {
19 | "production": {
20 | "project": "projects/angular-google-charts/ng-package.prod.json",
21 | "tsConfig": "projects/angular-google-charts/tsconfig.lib.prod.json"
22 | }
23 | }
24 | },
25 | "test": {
26 | "builder": "@angular-builders/jest:run",
27 | "options": {
28 | "configPath": "jest.config.js",
29 | "projects": ["projects/angular-google-charts"],
30 | "coverageDirectory": "../../coverage"
31 | }
32 | }
33 | }
34 | },
35 | "playground": {
36 | "projectType": "application",
37 | "schematics": {},
38 | "root": "projects/playground",
39 | "sourceRoot": "projects/playground/src",
40 | "prefix": "app",
41 | "architect": {
42 | "build": {
43 | "builder": "@angular-devkit/build-angular:browser",
44 | "options": {
45 | "aot": true,
46 | "outputPath": "dist/playground",
47 | "index": "projects/playground/src/index.html",
48 | "main": "projects/playground/src/main.ts",
49 | "polyfills": "projects/playground/src/polyfills.ts",
50 | "tsConfig": "projects/playground/tsconfig.app.json",
51 | "assets": ["projects/playground/src/favicon.ico", "projects/playground/src/assets"],
52 | "styles": ["projects/playground/src/styles.scss"],
53 | "scripts": []
54 | },
55 | "configurations": {
56 | "production": {
57 | "fileReplacements": [
58 | {
59 | "replace": "projects/playground/src/environments/environment.ts",
60 | "with": "projects/playground/src/environments/environment.prod.ts"
61 | }
62 | ],
63 | "optimization": true,
64 | "outputHashing": "all",
65 | "sourceMap": false,
66 | "namedChunks": false,
67 | "aot": true,
68 | "extractLicenses": true,
69 | "vendorChunk": false,
70 | "buildOptimizer": true,
71 | "budgets": [
72 | {
73 | "type": "initial",
74 | "maximumWarning": "2mb",
75 | "maximumError": "5mb"
76 | },
77 | {
78 | "type": "anyComponentStyle",
79 | "maximumWarning": "6kb"
80 | }
81 | ]
82 | }
83 | }
84 | },
85 | "serve": {
86 | "builder": "@angular-devkit/build-angular:dev-server",
87 | "options": {
88 | "browserTarget": "playground:build"
89 | },
90 | "configurations": {
91 | "production": {
92 | "browserTarget": "playground:build:production"
93 | }
94 | }
95 | }
96 | }
97 | }
98 | },
99 | "cli": {
100 | "analytics": "d81f7890-056e-4fbd-90be-bc8fb5b6dfaf"
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-google-charts",
3 | "description": "A wrapper for the Google Charts library written in Angular",
4 | "version": "0.0.0",
5 | "private": false,
6 | "scripts": {
7 | "ng": "ng",
8 | "start": "ng serve playground -o",
9 | "build": "ng build angular-google-charts --configuration production",
10 | "test": "ng test angular-google-charts --coverage",
11 | "lint": "ng lint playground",
12 | "release": "standard-version",
13 | "prepare": "husky"
14 | },
15 | "lint-staged": {
16 | "*.{json,html,ts,js,scss,css,yml,md}": "prettier --write"
17 | },
18 | "jest-junit": {
19 | "outputDirectory": "test-results"
20 | },
21 | "standard-version": {
22 | "bumpFiles": "libs/angular-google-charts/package.json",
23 | "packageFiles": "libs/angular-google-charts/package.json"
24 | },
25 | "engines": {
26 | "npm": ">=8.0.0"
27 | },
28 | "dependencies": {
29 | "@angular/common": "^16.2.12",
30 | "@angular/compiler": "^16.2.12",
31 | "@angular/core": "^16.2.12",
32 | "@angular/platform-browser": "^16.2.12",
33 | "@angular/platform-browser-dynamic": "^16.2.12",
34 | "@angular/router": "^16.2.12",
35 | "core-js": "^3.10.1",
36 | "rxjs": "^7.8.1",
37 | "tslib": "^2.2.0",
38 | "zone.js": "^0.13.3"
39 | },
40 | "devDependencies": {
41 | "@angular-builders/jest": "^16.0.1",
42 | "@angular-devkit/build-angular": "^16.2.15",
43 | "@angular/cli": "^16.2.15",
44 | "@angular/compiler-cli": "^16.2.12",
45 | "@angular/language-service": "^16.2.12",
46 | "@commitlint/cli": "^19.4.1",
47 | "@commitlint/config-conventional": "^19.4.1",
48 | "@types/google.visualization": "0.0.74",
49 | "@types/jest": "^29.5.12",
50 | "@types/node": "^16.18.106",
51 | "husky": "^9.1.5",
52 | "jest": "^29.7.0",
53 | "jest-junit": "^16.0.0",
54 | "lint-staged": "^15.2.10",
55 | "ng-packagr": "^16.2.3",
56 | "prettier": "^3.3.3",
57 | "standard-version": "^9.5.0",
58 | "ts-jest": "^29.2.5",
59 | "typescript": "^5.1.6"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/README.md:
--------------------------------------------------------------------------------
1 | # angular-google-charts
2 |
3 | > A wrapper for the [Google Charts library](https://google-developers.appspot.com/chart/) written in Angular.
4 |
5 | ## Install
6 |
7 | With [npm](https://npmjs.org/) installed, run
8 |
9 | ```bash
10 | npm install angular-google-charts
11 | ```
12 |
13 | ## Usage
14 |
15 | Import the `GoogleChartsModule` in your `app.module.ts`:
16 |
17 | ```typescript
18 | import { GoogleChartsModule } from 'angular-google-charts';
19 |
20 | @NgModule({
21 | ...
22 | imports: [
23 | ...
24 | GoogleChartsModule,
25 | ...
26 | ],
27 | ...
28 | })
29 | export class AppModule {}
30 | ```
31 |
32 | And create a `google-chart` component somewhere in your application:
33 |
34 | ```html
35 |
42 |
43 | ```
44 |
45 | ## Detailed Instructions
46 |
47 | Find the full readme at [GitHub](https://github.com/FERNman/angular-google-charts).
48 |
49 | ## License
50 |
51 | MIT
52 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | module.exports = {
3 | resetMocks: true,
4 | transform: {
5 | '^.+\\.ts?$': ['ts-jest', { diagnostics: { ignoreCodes: ['TS151001'] } }]
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/angular-google-charts",
4 | "deleteDestPath": false,
5 | "lib": {
6 | "entryFile": "src/index.ts"
7 | },
8 | "allowedNonPeerDependencies": ["."]
9 | }
10 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/ng-package.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/angular-google-charts",
4 | "lib": {
5 | "entryFile": "src/index.ts"
6 | },
7 | "allowedNonPeerDependencies": ["."]
8 | }
9 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-google-charts",
3 | "version": "16.0.2",
4 | "description": "A wrapper for the Google Charts library written with Angular",
5 | "keywords": [
6 | "Angular",
7 | "Angular 2",
8 | "Charts",
9 | "Google",
10 | "Chart"
11 | ],
12 | "homepage": "https://github.com/FERNman/angular-google-charts",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/FERNman/angular-google-charts"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/FERNman/angular-google-charts",
19 | "email": "gabriel.sperrer@gmail.com"
20 | },
21 | "license": "MIT",
22 | "author": {
23 | "name": "Gabriel Sperrer",
24 | "email": "gabriel.sperrer@gmail.com"
25 | },
26 | "peerDependencies": {
27 | "@angular/common": ">=16.0.0",
28 | "@angular/core": ">=16.0.0"
29 | },
30 | "dependencies": {
31 | "@types/google.visualization": "0.0.74",
32 | "tslib": "^2.2.0"
33 | },
34 | "main": "src/index.ts"
35 | }
36 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Public API Surface of angular-google-charts
3 | */
4 |
5 | export * from './lib/components/chart-editor/chart-editor-ref';
6 | export * from './lib/components/chart-editor/chart-editor.component';
7 | export * from './lib/components/google-chart/google-chart.component';
8 | export * from './lib/components/chart-wrapper/chart-wrapper.component';
9 | export * from './lib/components/dashboard/dashboard.component';
10 | export * from './lib/components/control-wrapper/control-wrapper.component';
11 | export * from './lib/components/chart-base/chart-base.component';
12 |
13 | export * from './lib/helpers/chart.helper';
14 |
15 | export * from './lib/types/chart-type';
16 | export * from './lib/types/control-type';
17 | export * from './lib/types/events';
18 | export * from './lib/types/formatter';
19 | export * from './lib/types/google-charts-config';
20 |
21 | export * from './lib/services/script-loader.service';
22 |
23 | export * from './lib/google-charts.module';
24 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/chart-base/chart-base.component.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from '@angular/core';
2 | import { Observable } from 'rxjs';
3 |
4 | import { ChartErrorEvent, ChartReadyEvent, ChartSelectionChangedEvent } from '../../types/events';
5 |
6 | export type Column = string | google.visualization.ColumnSpec;
7 | export type Row = (string | number | Date | boolean | google.visualization.DataObjectCell | null)[];
8 |
9 | export interface ChartBase {
10 | /**
11 | * The chart is ready for external method calls.
12 | *
13 | * Emits *after* the chart was drawn for the first time every time the chart gets redrawn.
14 | */
15 | ready: EventEmitter;
16 |
17 | /**
18 | * Emits when an error occurs when attempting to render the chart.
19 | */
20 | error: EventEmitter;
21 |
22 | /**
23 | * Emits when the user clicks a bar or legend.
24 | *
25 | * When a chart element is selected, the corresponding cell
26 | * in the data table is selected; when a legend is selected,
27 | * the corresponding column in the data table is selected.
28 | */
29 | select: EventEmitter;
30 |
31 | /**
32 | * The drawn chart or `null`.
33 | */
34 | chart: google.visualization.ChartBase | null;
35 |
36 | /**
37 | * The underlying chart wrapper.
38 | *
39 | * This will throw an exception when trying to access the chart wrapper before `wrapperReady$` emits.
40 | */
41 | chartWrapper: google.visualization.ChartWrapper;
42 |
43 | /**
44 | * Emits after the `ChartWrapper` is created, but before the chart is drawn for the first time.
45 | */
46 | wrapperReady$: Observable;
47 | }
48 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/chart-editor/chart-editor-ref.spec.ts:
--------------------------------------------------------------------------------
1 | import { ChartEditorRef } from './chart-editor-ref';
2 |
3 | const visualizationMock = {
4 | events: {
5 | addOneTimeListener: jest.fn(),
6 | removeAllListeners: jest.fn()
7 | }
8 | };
9 |
10 | const editorMock = {
11 | openDialog: jest.fn(),
12 | closeDialog: jest.fn(),
13 | setChartWrapper: jest.fn(),
14 | getChartWrapper: jest.fn()
15 | } as jest.Mocked;
16 |
17 | describe('ChartEditorRef', () => {
18 | let editor: ChartEditorRef;
19 |
20 | beforeEach(() => {
21 | globalThis.google = { visualization: visualizationMock } as any;
22 | editor = new ChartEditorRef(editorMock);
23 | });
24 |
25 | it('should create', () => {
26 | expect(editor).toBeTruthy();
27 | });
28 |
29 | it('should register event listeners on create', () => {
30 | expect(visualizationMock.events.addOneTimeListener).toHaveBeenCalledWith(editorMock, 'ok', expect.any(Function));
31 | expect(visualizationMock.events.addOneTimeListener).toHaveBeenCalledWith(
32 | editorMock,
33 | 'cancel',
34 | expect.any(Function)
35 | );
36 | });
37 |
38 | describe('afterClosed', () => {
39 | it('should emit update wrapper if dialog was saved', () => {
40 | const okCallback = visualizationMock.events.addOneTimeListener.mock.calls[0][2];
41 |
42 | const editResult = { draw: jest.fn() };
43 | editorMock.getChartWrapper.mockReturnValueOnce(editResult as any);
44 |
45 | const closedSpy = jest.fn();
46 | editor.afterClosed().subscribe(result => closedSpy(result));
47 |
48 | okCallback();
49 |
50 | expect(google.visualization.events.removeAllListeners).toHaveBeenCalled();
51 | expect(closedSpy).toHaveBeenCalledWith(editResult);
52 | });
53 |
54 | it('should emit `null` if dialog was cancelled', () => {
55 | const cancelCallback = visualizationMock.events.addOneTimeListener.mock.calls[1][2];
56 |
57 | const closedSpy = jest.fn();
58 | editor.afterClosed().subscribe(result => closedSpy(result));
59 |
60 | cancelCallback();
61 |
62 | expect(google.visualization.events.removeAllListeners).toHaveBeenCalled();
63 | expect(closedSpy).toHaveBeenCalledWith(null);
64 | });
65 | });
66 |
67 | describe('cancel', () => {
68 | it('should close the dialog', () => {
69 | editor.cancel();
70 |
71 | expect(editorMock.closeDialog).toHaveBeenCalled();
72 | });
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/chart-editor/chart-editor-ref.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { Observable, Subject } from 'rxjs';
4 |
5 | export type EditChartResult = google.visualization.ChartWrapper | null;
6 |
7 | export class ChartEditorRef {
8 | private readonly doneSubject = new Subject();
9 |
10 | constructor(private readonly editor: google.visualization.ChartEditor) {
11 | this.addEventListeners();
12 | }
13 |
14 | /**
15 | * Gets an observable that is notified when the dialog is saved.
16 | * Emits either the result if the dialog was saved or `null` if editing was cancelled.
17 | */
18 | public afterClosed(): Observable {
19 | return this.doneSubject.asObservable();
20 | }
21 |
22 | /**
23 | * Stops editing the chart and closes the dialog.
24 | */
25 | public cancel() {
26 | this.editor.closeDialog();
27 | }
28 |
29 | private addEventListeners() {
30 | google.visualization.events.addOneTimeListener(this.editor, 'ok', () => {
31 | google.visualization.events.removeAllListeners(this.editor);
32 |
33 | const updatedChartWrapper = this.editor.getChartWrapper();
34 |
35 | this.doneSubject.next(updatedChartWrapper);
36 | this.doneSubject.complete();
37 | });
38 |
39 | google.visualization.events.addOneTimeListener(this.editor, 'cancel', () => {
40 | google.visualization.events.removeAllListeners(this.editor);
41 |
42 | this.doneSubject.next(null);
43 | this.doneSubject.complete();
44 | });
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/chart-editor/chart-editor.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 | import { EMPTY, of } from 'rxjs';
3 |
4 | import { ScriptLoaderService } from '../../services/script-loader.service';
5 | import { ChartBase } from '../chart-base/chart-base.component';
6 |
7 | import { ChartEditorRef } from './chart-editor-ref';
8 | import { ChartEditorComponent } from './chart-editor.component';
9 |
10 | jest.mock('../../services/script-loader.service');
11 | jest.mock('./chart-editor-ref');
12 |
13 | const editorRefMock = {
14 | afterClosed: jest.fn()
15 | };
16 |
17 | const visualizationMock = {
18 | ChartEditor: jest.fn()
19 | };
20 |
21 | const editorMock = {
22 | openDialog: jest.fn(),
23 | closeDialog: jest.fn(),
24 | setChartWrapper: jest.fn(),
25 | getChartWrapper: jest.fn()
26 | } as jest.Mocked;
27 |
28 | describe('ChartEditorComponent', () => {
29 | let component: ChartEditorComponent;
30 | let fixture: ComponentFixture;
31 |
32 | beforeEach(() => {
33 | visualizationMock.ChartEditor.mockReturnValue(editorMock);
34 | globalThis.google = { visualization: visualizationMock } as any;
35 | });
36 |
37 | beforeEach(async () => {
38 | await TestBed.configureTestingModule({
39 | declarations: [ChartEditorComponent],
40 | providers: [ScriptLoaderService]
41 | }).compileComponents();
42 | });
43 |
44 | beforeEach(() => {
45 | fixture = TestBed.createComponent(ChartEditorComponent);
46 | component = fixture.componentInstance;
47 | });
48 |
49 | it('should create', () => {
50 | expect(component).toBeTruthy();
51 | });
52 |
53 | describe('ngOnInit', () => {
54 | it('should load editor package', () => {
55 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
56 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(EMPTY);
57 |
58 | component.ngOnInit();
59 |
60 | expect(scriptLoaderService.loadChartPackages).toHaveBeenCalledWith('charteditor');
61 | });
62 |
63 | it('should create chart editor', () => {
64 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
65 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
66 |
67 | component.ngOnInit();
68 |
69 | expect(visualizationMock.ChartEditor).toHaveBeenCalled();
70 | });
71 |
72 | it('should emit initialized event', () => {
73 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
74 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
75 |
76 | const initializedSpy = jest.fn();
77 | component.initialized$.subscribe(event => initializedSpy(event));
78 |
79 | component.ngOnInit();
80 |
81 | expect(initializedSpy).toHaveBeenCalledWith(editorMock);
82 | });
83 | });
84 |
85 | describe('editChart', () => {
86 | const chartWrapper = { draw: jest.fn() } as any;
87 |
88 | beforeEach(() => {
89 | component['editor'] = editorMock;
90 | (ChartEditorRef as any as jest.SpyInstance).mockReturnValue(editorRefMock);
91 | editorRefMock.afterClosed.mockReturnValue(EMPTY);
92 | });
93 |
94 | it('should open the edit dialog', () => {
95 | const chartComponent = { chartWrapper } as ChartBase;
96 |
97 | component.editChart(chartComponent);
98 |
99 | expect(editorMock.openDialog).toHaveBeenCalledWith(chartComponent.chartWrapper, {});
100 | });
101 |
102 | it('should pass the provided options', () => {
103 | const chartComponent = { chartWrapper } as ChartBase;
104 |
105 | const options = {
106 | dataSourceInput: 'urlbox'
107 | } as google.visualization.ChartEditorOptions;
108 |
109 | component.editChart(chartComponent, options);
110 |
111 | expect(editorMock.openDialog).toHaveBeenCalledWith(chartComponent.chartWrapper, options);
112 | });
113 |
114 | it('should create an editor ref and return it', () => {
115 | const chartComponent = { chartWrapper } as ChartBase;
116 | const handle = component.editChart(chartComponent);
117 |
118 | expect(ChartEditorRef).toHaveBeenCalledWith(editorMock);
119 | expect(handle).toBe(editorRefMock);
120 | });
121 |
122 | it('should update the components chart wrapper with the edit result', async () => {
123 | const chartComponent = { chartWrapper } as ChartBase;
124 | const updatedWrapper = { draw: jest.fn() };
125 |
126 | editorRefMock.afterClosed.mockReturnValue(of(updatedWrapper));
127 |
128 | component.editChart(chartComponent);
129 |
130 | expect(chartComponent.chartWrapper).toBe(updatedWrapper);
131 | });
132 |
133 | it('should not update the components wrapper if editing was cancelled', () => {
134 | const chartComponent = { chartWrapper } as ChartBase;
135 |
136 | editorRefMock.afterClosed.mockReturnValue(of(null));
137 |
138 | component.editChart(chartComponent);
139 |
140 | expect(chartComponent.chartWrapper).toBe(chartWrapper);
141 | });
142 |
143 | it("should throw if the component' chart wrapper is undefined", () => {
144 | const chartComponent = {} as ChartBase;
145 | expect(() => component.editChart(chartComponent)).toThrow();
146 | });
147 |
148 | it("should throw if the component' editor is undefined", () => {
149 | const chartComponent = { chartWrapper } as ChartBase;
150 |
151 | component['editor'] = undefined;
152 |
153 | expect(() => component.editChart(chartComponent)).toThrow();
154 | });
155 | });
156 | });
157 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/chart-editor/chart-editor.component.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
4 | import { Subject } from 'rxjs';
5 |
6 | import { ScriptLoaderService } from '../../services/script-loader.service';
7 | import { ChartBase } from '../chart-base/chart-base.component';
8 |
9 | import { ChartEditorRef } from './chart-editor-ref';
10 |
11 | @Component({
12 | selector: 'chart-editor',
13 | template: ``,
14 | host: { class: 'chart-editor' },
15 | changeDetection: ChangeDetectionStrategy.OnPush
16 | })
17 | export class ChartEditorComponent implements OnInit {
18 | private editor: google.visualization.ChartEditor | undefined;
19 | private initializedSubject = new Subject();
20 |
21 | constructor(private scriptLoaderService: ScriptLoaderService) {}
22 |
23 | /**
24 | * Emits as soon as the chart editor is fully initialized.
25 | */
26 | public get initialized$() {
27 | return this.initializedSubject.asObservable();
28 | }
29 |
30 | public ngOnInit() {
31 | this.scriptLoaderService.loadChartPackages('charteditor').subscribe(() => {
32 | this.editor = new google.visualization.ChartEditor();
33 | this.initializedSubject.next(this.editor);
34 | this.initializedSubject.complete();
35 | });
36 | }
37 |
38 | /**
39 | * Opens the chart editor as an embedded dialog box on the page.
40 | * If the editor gets saved, the components' chart will be updated with the result.
41 | *
42 | * @param component The chart to be edited.
43 | * @returns A reference to the open editor.
44 | */
45 | public editChart(component: ChartBase): ChartEditorRef;
46 | public editChart(component: ChartBase, options: google.visualization.ChartEditorOptions): ChartEditorRef;
47 | public editChart(component: ChartBase, options?: google.visualization.ChartEditorOptions) {
48 | if (!component.chartWrapper) {
49 | throw new Error(
50 | 'Chart wrapper is `undefined`. Please wait for the `initialized$` observable before trying to edit a chart.'
51 | );
52 | }
53 | if (!this.editor) {
54 | throw new Error(
55 | 'Chart editor is `undefined`. Please wait for the `initialized$` observable before trying to edit a chart.'
56 | );
57 | }
58 |
59 | const handle = new ChartEditorRef(this.editor);
60 | this.editor.openDialog(component.chartWrapper, options || {});
61 |
62 | handle.afterClosed().subscribe(result => {
63 | if (result) {
64 | component.chartWrapper = result;
65 | }
66 | });
67 |
68 | return handle;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/chart-editor/chart-editor.ts:
--------------------------------------------------------------------------------
1 | declare namespace google {
2 | namespace visualization {
3 | export interface ChartEditorOptions {
4 | dataSourceInput?: HTMLElement | 'urlbox';
5 | }
6 |
7 | export class ChartEditor {
8 | public openDialog(chartWrapper: google.visualization.ChartWrapper, options: ChartEditorOptions): void;
9 | public getChartWrapper(): google.visualization.ChartWrapper;
10 | public setChartWrapper(chartWrapper: google.visualization.ChartWrapper): void;
11 | public closeDialog(): void;
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/chart-wrapper/chart-wrapper.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { SimpleChange } from '@angular/core';
2 | import { ComponentFixture, TestBed } from '@angular/core/testing';
3 | import { EMPTY, of } from 'rxjs';
4 |
5 | import { ScriptLoaderService } from '../../services/script-loader.service';
6 | import { ChartType } from '../../types/chart-type';
7 | import { ChartErrorEvent, ChartReadyEvent, ChartSelectionChangedEvent } from '../../types/events';
8 |
9 | import { ChartWrapperComponent } from './chart-wrapper.component';
10 |
11 | jest.mock('../../services/script-loader.service');
12 |
13 | const chartWrapperMock = {
14 | setChartType: jest.fn(),
15 | setDataTable: jest.fn(),
16 | setOptions: jest.fn(),
17 | setContainerId: jest.fn(),
18 | setDataSourceUrl: jest.fn(),
19 | setQuery: jest.fn(),
20 | setRefreshInterval: jest.fn(),
21 | setView: jest.fn(),
22 | draw: jest.fn(),
23 | getChart: jest.fn()
24 | };
25 |
26 | const visualizationMock = {
27 | ChartWrapper: jest.fn(),
28 | arrayToDataTable: jest.fn(),
29 | events: {
30 | addListener: jest.fn(),
31 | removeAllListeners: jest.fn()
32 | }
33 | };
34 |
35 | describe('ChartWrapperComponent', () => {
36 | let component: ChartWrapperComponent;
37 | let fixture: ComponentFixture;
38 |
39 | beforeEach(() => {
40 | visualizationMock.ChartWrapper.mockReturnValue(chartWrapperMock);
41 | globalThis.google = { visualization: visualizationMock } as any;
42 | });
43 |
44 | beforeEach(async () => {
45 | await TestBed.configureTestingModule({
46 | declarations: [ChartWrapperComponent],
47 | providers: [ScriptLoaderService]
48 | }).compileComponents();
49 | });
50 |
51 | beforeEach(() => {
52 | fixture = TestBed.createComponent(ChartWrapperComponent);
53 | component = fixture.componentInstance;
54 | // No change detection here, we want to invoke the
55 | // lifecycle methods in the unit tests
56 | });
57 |
58 | it('should be created', () => {
59 | expect(component).toBeTruthy();
60 | });
61 |
62 | describe('ngOnInit', () => {
63 | it('should load the google charts package', () => {
64 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
65 | scriptLoaderService.loadChartPackages.mockReturnValue(EMPTY);
66 |
67 | component.ngOnInit();
68 |
69 | expect(scriptLoaderService.loadChartPackages).toHaveBeenCalled();
70 | });
71 |
72 | it('should create the chart wrapper using the provided specs', () => {
73 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
74 | scriptLoaderService.loadChartPackages.mockReturnValue(of(null));
75 |
76 | const specs = { chartType: ChartType.AreaChart, dataTable: [] };
77 | component.specs = specs;
78 | component.ngOnInit();
79 |
80 | expect(visualizationMock.ChartWrapper).toHaveBeenCalledWith(expect.objectContaining(specs));
81 | });
82 |
83 | it('should not throw if the specs are `null`', () => {
84 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
85 | scriptLoaderService.loadChartPackages.mockReturnValue(of(null));
86 | component.specs = undefined;
87 |
88 | expect(() => component.ngOnInit()).not.toThrow();
89 | });
90 |
91 | it('should not use container or containerId if present in the chart specs', () => {
92 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
93 | scriptLoaderService.loadChartPackages.mockReturnValue(of(null));
94 |
95 | const specs = {
96 | chartType: ChartType.AreaChart,
97 | container: { innerHTML: '' } as HTMLElement,
98 | containerId: 'test'
99 | };
100 | component.specs = specs;
101 | component.ngOnInit();
102 |
103 | expect(visualizationMock.ChartWrapper).not.toHaveBeenCalledWith(
104 | expect.objectContaining({ container: specs.container })
105 | );
106 | expect(visualizationMock.ChartWrapper).not.toHaveBeenCalledWith(
107 | expect.objectContaining({ containerId: specs.containerId })
108 | );
109 | });
110 |
111 | it('should register chart wrapper event handlers', () => {
112 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
113 | scriptLoaderService.loadChartPackages.mockReturnValue(of(null));
114 |
115 | component.specs = { chartType: ChartType.AreaChart };
116 |
117 | component.ngOnInit();
118 |
119 | expect(visualizationMock.events.removeAllListeners).toHaveBeenCalled();
120 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(
121 | chartWrapperMock,
122 | 'ready',
123 | expect.any(Function)
124 | );
125 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(
126 | chartWrapperMock,
127 | 'error',
128 | expect.any(Function)
129 | );
130 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(
131 | chartWrapperMock,
132 | 'select',
133 | expect.any(Function)
134 | );
135 | });
136 |
137 | it('should emit ready event', () => {
138 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
139 | scriptLoaderService.loadChartPackages.mockReturnValue(of(null));
140 |
141 | component.specs = { chartType: ChartType.AreaChart };
142 |
143 | const readySpy = jest.fn();
144 | component.wrapperReady$.subscribe(event => readySpy(event));
145 |
146 | component.ngOnInit();
147 |
148 | expect(readySpy).toHaveBeenCalledWith(chartWrapperMock);
149 | });
150 |
151 | it('should draw the chart', () => {
152 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
153 | scriptLoaderService.loadChartPackages.mockReturnValue(of(null));
154 |
155 | component.specs = { chartType: ChartType.AreaChart };
156 |
157 | component.ngOnInit();
158 |
159 | expect(chartWrapperMock.draw).toHaveBeenCalled();
160 | });
161 | });
162 |
163 | describe('ngOnChanges', () => {
164 | beforeEach(() => {
165 | component['wrapper'] = chartWrapperMock as any;
166 | component['initialized'] = true;
167 | });
168 |
169 | it('should not throw if the wrapper is not yet initialized', () => {
170 | component['wrapper'] = undefined;
171 | component['initialized'] = false;
172 |
173 | const specs = {
174 | chartType: ChartType.AreaChart
175 | };
176 |
177 | expect(() => changeSpecs(specs)).not.toThrow();
178 | });
179 |
180 | it('should update the chart wrapper with the provided specs', () => {
181 | const specs = {
182 | chartType: ChartType.AreaChart,
183 | dataSourceUrl: 'www.data.de',
184 | dataTable: [],
185 | options: { test: 'any' },
186 | query: 'query',
187 | refreshInterval: 100,
188 | view: 'testview'
189 | } as google.visualization.ChartSpecs;
190 |
191 | changeSpecs(specs);
192 |
193 | expect(chartWrapperMock.setChartType).toHaveBeenCalledWith(specs.chartType);
194 | expect(chartWrapperMock.setDataSourceUrl).toHaveBeenCalledWith(specs.dataSourceUrl);
195 | expect(chartWrapperMock.setDataTable).toHaveBeenCalledWith(specs.dataTable);
196 | expect(chartWrapperMock.setOptions).toHaveBeenCalledWith(specs.options);
197 | expect(chartWrapperMock.setQuery).toHaveBeenCalledWith(specs.query);
198 | expect(chartWrapperMock.setRefreshInterval).toHaveBeenCalledWith(specs.refreshInterval);
199 | expect(chartWrapperMock.setView).toHaveBeenCalledWith(specs.view);
200 | });
201 |
202 | it('should ignore `container` and `containerId` if given', () => {
203 | const specs = { containerId: 'test', container: {} } as google.visualization.ChartSpecs;
204 | component.specs = specs;
205 |
206 | expect(chartWrapperMock.setContainerId).not.toHaveBeenCalled();
207 | });
208 |
209 | it('should not throw if the specs are `undefined`', () => {
210 | expect(() => changeSpecs(undefined)).not.toThrow();
211 |
212 | expect(chartWrapperMock.draw).toHaveBeenCalled();
213 | });
214 |
215 | it('should redraw the chart if the specs change', () => {
216 | const specs = { chartType: ChartType.AreaChart } as google.visualization.ChartSpecs;
217 | component.specs = specs;
218 |
219 | const newSpecs = { ...specs, chartType: ChartType.GeoChart };
220 | changeSpecs(newSpecs);
221 |
222 | expect(chartWrapperMock.draw).toHaveBeenCalled();
223 | });
224 |
225 | it("should not redraw the chart if the specs didn't change", () => {
226 | const specs = { chartType: ChartType.AreaChart } as google.visualization.ChartSpecs;
227 | component.specs = specs;
228 |
229 | component.ngOnChanges({});
230 |
231 | expect(chartWrapperMock.draw).not.toHaveBeenCalled();
232 | });
233 | });
234 |
235 | describe('chart', () => {
236 | it('should not throw when trying to access chart if its not yet drawn', () => {
237 | component['wrapper'] = chartWrapperMock as any;
238 | expect(() => component.chart).not.toThrow();
239 | });
240 | });
241 |
242 | describe('chartWrapper', () => {
243 | it('should throw if the chart wrapper is `undefined`', () => {
244 | expect(() => component.chartWrapper).toThrow();
245 | });
246 |
247 | it('should return the chart wrapper', () => {
248 | component['wrapper'] = chartWrapperMock as any;
249 |
250 | const wrapper = component.chartWrapper;
251 | expect(wrapper).toBe(chartWrapperMock);
252 | });
253 |
254 | it('should redraw if changed', () => {
255 | component.chartWrapper = chartWrapperMock as any;
256 |
257 | expect(chartWrapperMock.draw).toHaveBeenCalled();
258 | });
259 | });
260 |
261 | describe('events', () => {
262 | beforeEach(() => {
263 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
264 | service.loadChartPackages.mockReturnValueOnce(of(null));
265 | });
266 |
267 | it('should emit ready event after the chart is ready', () => {
268 | const chartMock = { draw: jest.fn() };
269 | chartWrapperMock.getChart.mockReturnValue(chartMock);
270 |
271 | component.ngOnInit();
272 |
273 | const readySpy = jest.fn();
274 | component.ready.subscribe((event: ChartReadyEvent) => readySpy(event));
275 |
276 | const readyCallback = visualizationMock.events.addListener.mock.calls[0][2];
277 | readyCallback();
278 |
279 | expect(readySpy).toHaveBeenCalledWith({ chart: chartMock });
280 | });
281 |
282 | it('should emit error event if the chart caused an error', () => {
283 | component.ngOnInit();
284 |
285 | const errorSpy = jest.fn();
286 | component.error.subscribe((event: ChartErrorEvent) => errorSpy(event));
287 |
288 | const errorCallback = visualizationMock.events.addListener.mock.calls[1][2];
289 |
290 | const error = 'someerror';
291 | errorCallback(error);
292 |
293 | expect(errorSpy).toHaveBeenCalledWith(error);
294 | });
295 |
296 | it('should emit select event if a value was selected', () => {
297 | const selection = [{ column: 1, row: 2 }] as google.visualization.ChartSelection[];
298 |
299 | const chartMock = { getSelection: jest.fn(() => selection) };
300 | chartWrapperMock.getChart.mockReturnValue(chartMock);
301 |
302 | const selectSpy = jest.fn();
303 | component.select.subscribe((event: ChartSelectionChangedEvent) => selectSpy(event));
304 |
305 | component.ngOnInit();
306 |
307 | expect(selectSpy).not.toHaveBeenCalled();
308 |
309 | const selectCallback = visualizationMock.events.addListener.mock.calls[2][2];
310 |
311 | selectCallback();
312 |
313 | expect(selectSpy).toHaveBeenCalledWith({ selection });
314 | });
315 | });
316 |
317 | function changeSpecs(newValue?: google.visualization.ChartSpecs) {
318 | const oldValue = component.specs;
319 | component.specs = newValue;
320 | component.ngOnChanges({ specs: new SimpleChange(oldValue, newValue, oldValue == null) });
321 | }
322 | });
323 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/chart-wrapper/chart-wrapper.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | ElementRef,
5 | EventEmitter,
6 | Input,
7 | OnChanges,
8 | OnInit,
9 | Output,
10 | SimpleChanges
11 | } from '@angular/core';
12 | import { ReplaySubject } from 'rxjs';
13 |
14 | import { ScriptLoaderService } from '../../services/script-loader.service';
15 | import { ChartErrorEvent, ChartReadyEvent, ChartSelectionChangedEvent } from '../../types/events';
16 | import { ChartBase } from '../chart-base/chart-base.component';
17 |
18 | @Component({
19 | selector: 'chart-wrapper',
20 | template: '',
21 | styles: [':host { width: fit-content; display: block; }'],
22 | host: { class: 'chart-wrapper' },
23 | exportAs: 'chartWrapper',
24 | changeDetection: ChangeDetectionStrategy.OnPush
25 | })
26 | export class ChartWrapperComponent implements ChartBase, OnChanges, OnInit {
27 | /**
28 | * Either a JSON object defining the chart, or a serialized string version of that object.
29 | * The format of this object is shown in the
30 | * {@link https://developers.google.com/chart/interactive/docs/reference#google.visualization.drawchart `drawChart()`} documentation.
31 | *
32 | * The `container` and `containerId` will be overwritten by this component to allow
33 | * rendering the chart into the components' template.
34 | */
35 | @Input()
36 | public specs?: google.visualization.ChartSpecs;
37 |
38 | @Output()
39 | public error = new EventEmitter();
40 |
41 | @Output()
42 | public ready = new EventEmitter();
43 |
44 | @Output()
45 | public select = new EventEmitter();
46 |
47 | private wrapper: google.visualization.ChartWrapper | undefined;
48 | private wrapperReadySubject = new ReplaySubject(1);
49 | private initialized = false;
50 |
51 | constructor(private element: ElementRef, private scriptLoaderService: ScriptLoaderService) {}
52 |
53 | public get chart(): google.visualization.ChartBase | null {
54 | return this.chartWrapper.getChart();
55 | }
56 |
57 | public get wrapperReady$() {
58 | return this.wrapperReadySubject.asObservable();
59 | }
60 |
61 | public get chartWrapper(): google.visualization.ChartWrapper {
62 | if (!this.wrapper) {
63 | throw new Error('Cannot access the chart wrapper before initialization.');
64 | }
65 |
66 | return this.wrapper;
67 | }
68 |
69 | public set chartWrapper(wrapper: google.visualization.ChartWrapper) {
70 | this.wrapper = wrapper;
71 | this.drawChart();
72 | }
73 |
74 | public ngOnInit() {
75 | // We don't need to load any chart packages, the chart wrapper will handle this else for us
76 | this.scriptLoaderService.loadChartPackages().subscribe(() => {
77 | if (!this.specs) {
78 | this.specs = {} as google.visualization.ChartSpecs;
79 | }
80 |
81 | const { containerId, container, ...specs } = this.specs;
82 |
83 | // Only ever create the wrapper once to allow animations to happen if something changes.
84 | this.wrapper = new google.visualization.ChartWrapper({
85 | ...specs,
86 | container: this.element.nativeElement
87 | });
88 | this.registerChartEvents();
89 |
90 | this.wrapperReadySubject.next(this.wrapper);
91 |
92 | this.drawChart();
93 | this.initialized = true;
94 | });
95 | }
96 |
97 | public ngOnChanges(changes: SimpleChanges) {
98 | if (!this.initialized) {
99 | return;
100 | }
101 |
102 | if (changes.specs) {
103 | this.updateChart();
104 | this.drawChart();
105 | }
106 | }
107 |
108 | private updateChart() {
109 | if (!this.specs) {
110 | // When creating the wrapper with empty specs, the google charts library will show an error
111 | // If we don't do this, a javascript error will be thrown, which is not as visible to the user
112 | this.specs = {} as google.visualization.ChartSpecs;
113 | }
114 |
115 | // The typing here are not correct. These methods accept `undefined` as well.
116 | // That's why we have to cast to `any`
117 |
118 | this.wrapper!.setChartType(this.specs.chartType);
119 | this.wrapper!.setDataTable(this.specs.dataTable as any);
120 | this.wrapper!.setDataSourceUrl(this.specs.dataSourceUrl as any);
121 | this.wrapper!.setDataSourceUrl(this.specs.dataSourceUrl as any);
122 | this.wrapper!.setQuery(this.specs.query as any);
123 | this.wrapper!.setOptions(this.specs.options as any);
124 | this.wrapper!.setRefreshInterval(this.specs.refreshInterval as any);
125 | this.wrapper!.setView(this.specs.view);
126 | }
127 |
128 | private drawChart() {
129 | if (this.wrapper) {
130 | this.wrapper.draw();
131 | }
132 | }
133 |
134 | private registerChartEvents() {
135 | google.visualization.events.removeAllListeners(this.wrapper);
136 |
137 | const registerChartEvent = (object: any, eventName: string, callback: Function) => {
138 | google.visualization.events.addListener(object, eventName, callback);
139 | };
140 |
141 | registerChartEvent(this.wrapper, 'ready', () => this.ready.emit({ chart: this.chart! }));
142 | registerChartEvent(this.wrapper, 'error', (error: ChartErrorEvent) => this.error.emit(error));
143 | registerChartEvent(this.wrapper, 'select', () => {
144 | const selection = this.chart!.getSelection();
145 | this.select.emit({ selection });
146 | });
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/control-wrapper/control-wrapper.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { SimpleChange } from '@angular/core';
2 | import { ComponentFixture, TestBed } from '@angular/core/testing';
3 | import { EMPTY, of } from 'rxjs';
4 |
5 | import { ScriptLoaderService } from '../../services/script-loader.service';
6 | import { FilterType } from '../../types/control-type';
7 | import { ChartErrorEvent, ChartReadyEvent } from '../../types/events';
8 |
9 | import { ControlWrapperComponent } from './control-wrapper.component';
10 |
11 | jest.mock('../../services/script-loader.service');
12 |
13 | const visualizationMock = {
14 | ControlWrapper: jest.fn(),
15 | events: {
16 | addListener: jest.fn(),
17 | removeAllListeners: jest.fn()
18 | }
19 | };
20 |
21 | describe('ControlWrapperComponent', () => {
22 | let component: ControlWrapperComponent;
23 | let fixture: ComponentFixture;
24 |
25 | beforeEach(async () => {
26 | await TestBed.configureTestingModule({
27 | declarations: [ControlWrapperComponent],
28 | providers: [ScriptLoaderService]
29 | }).compileComponents();
30 | });
31 |
32 | beforeEach(() => {
33 | fixture = TestBed.createComponent(ControlWrapperComponent);
34 | component = fixture.componentInstance;
35 | // No change detection here, we want to invoke the
36 | // lifecycle methods in the unit tests
37 | });
38 |
39 | it('should create', () => {
40 | expect(component).toBeTruthy();
41 | });
42 |
43 | describe('ngOnInit', () => {
44 | it('should load the `controls` package', () => {
45 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
46 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(EMPTY);
47 |
48 | component.ngOnInit();
49 |
50 | expect(scriptLoaderService.loadChartPackages).toHaveBeenCalledWith('controls');
51 | });
52 |
53 | it('should create the control wrapper after the packages loaded', () => {
54 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
55 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
56 |
57 | globalThis.google = { visualization: visualizationMock } as any;
58 |
59 | const options = {
60 | containerId: 'someid',
61 | controlType: FilterType.ChartRange,
62 | state: { test: 1 },
63 | options: { key: 'value' }
64 | };
65 |
66 | // @ts-ignore
67 | component.id = options.containerId;
68 | component.type = options.controlType;
69 | component.state = options.state;
70 | component.options = options.options;
71 |
72 | component.ngOnInit();
73 |
74 | expect(visualizationMock.ControlWrapper).toHaveBeenCalledWith(options);
75 | expect(component.controlWrapper).toBeTruthy();
76 | });
77 |
78 | it('should throw if the control wrapper is accessed before being initialized', () => {
79 | expect(() => component.controlWrapper).toThrow();
80 | });
81 |
82 | it('should add event listeners', () => {
83 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
84 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
85 |
86 | const controlWrapperMock = { setControlType: jest.fn() };
87 | visualizationMock.ControlWrapper.mockReturnValue(controlWrapperMock);
88 |
89 | globalThis.google = { visualization: visualizationMock } as any;
90 |
91 | component.ngOnInit();
92 |
93 | expect(visualizationMock.events.removeAllListeners).toHaveBeenCalledWith(controlWrapperMock);
94 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(
95 | controlWrapperMock,
96 | 'ready',
97 | expect.any(Function)
98 | );
99 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(
100 | controlWrapperMock,
101 | 'error',
102 | expect.any(Function)
103 | );
104 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(
105 | controlWrapperMock,
106 | 'statechange',
107 | expect.any(Function)
108 | );
109 | });
110 |
111 | it('should emit wrapper ready event', () => {
112 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
113 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
114 |
115 | const controlWrapperMock = { setControlType: jest.fn() };
116 | visualizationMock.ControlWrapper.mockReturnValue(controlWrapperMock);
117 |
118 | globalThis.google = { visualization: visualizationMock } as any;
119 |
120 | const wrapperReadySpy = jest.fn();
121 | component.wrapperReady$.subscribe(event => wrapperReadySpy(event));
122 |
123 | component.ngOnInit();
124 |
125 | expect(wrapperReadySpy).toHaveBeenCalledTimes(1);
126 | expect(wrapperReadySpy).toHaveBeenCalledWith(controlWrapperMock);
127 | });
128 | });
129 |
130 | describe('ngOnChanges', () => {
131 | function changeInput(property: K, newValue: ControlWrapperComponent[K]) {
132 | const oldValue = component[property];
133 | component[property] = newValue;
134 | component.ngOnChanges({ [property]: new SimpleChange(oldValue, newValue, oldValue == null) });
135 | }
136 |
137 | it('should not throw if component is not yet initialized', () => {
138 | expect(() => component.ngOnChanges({ type: new SimpleChange(null, null, true) })).not.toThrow();
139 | });
140 |
141 | it('should update the control type if it changed', () => {
142 | const controlWrapperMock = { setControlType: jest.fn() };
143 | component['_controlWrapper'] = controlWrapperMock as any;
144 |
145 | const type = FilterType.Category;
146 | changeInput('type', type);
147 |
148 | expect(controlWrapperMock.setControlType).toHaveBeenCalledWith(type);
149 | });
150 |
151 | it('should update the options if they changed', () => {
152 | const controlWrapperMock = { setOptions: jest.fn() };
153 | component['_controlWrapper'] = controlWrapperMock as any;
154 |
155 | const options = { key: 'value' };
156 | changeInput('options', options);
157 |
158 | expect(controlWrapperMock.setOptions).toHaveBeenCalledWith(options);
159 | });
160 |
161 | it('should update the state if it changed', () => {
162 | const controlWrapperMock = { setState: jest.fn() };
163 | component['_controlWrapper'] = controlWrapperMock as any;
164 |
165 | const state = { from: 'to' };
166 | changeInput('state', state);
167 |
168 | expect(controlWrapperMock.setState).toHaveBeenCalledWith(state);
169 | });
170 | });
171 |
172 | describe('events', () => {
173 | beforeEach(() => {
174 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
175 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
176 |
177 | const controlWrapperMock = { setControlType: jest.fn() };
178 | visualizationMock.ControlWrapper.mockReturnValue(controlWrapperMock);
179 |
180 | globalThis.google = { visualization: visualizationMock } as any;
181 | });
182 |
183 | it('should emit ready event', () => {
184 | const readySpy = jest.fn();
185 | component.ready.subscribe((event: ChartReadyEvent) => readySpy(event));
186 |
187 | // This leads to the component subscribing to all events
188 | component.ngOnInit();
189 |
190 | const readyCallback: Function = visualizationMock.events.addListener.mock.calls[0][2];
191 |
192 | const eventMock = 'event';
193 | readyCallback(eventMock);
194 |
195 | expect(readySpy).toHaveBeenCalledWith(eventMock);
196 | });
197 |
198 | it('should emit error event', () => {
199 | const errorSpy = jest.fn();
200 | component.error.subscribe((event: ChartErrorEvent) => errorSpy(event));
201 |
202 | // This leads to the component subscribing to all events
203 | component.ngOnInit();
204 |
205 | const errorCallback: Function = visualizationMock.events.addListener.mock.calls[1][2];
206 |
207 | const eventMock = 'event';
208 | errorCallback(eventMock);
209 |
210 | expect(errorSpy).toHaveBeenCalledWith(eventMock);
211 | });
212 |
213 | it('should emit statechange event', () => {
214 | const stateChangeSpy = jest.fn();
215 | component.stateChange.subscribe((event: unknown) => stateChangeSpy(event));
216 |
217 | // This leads to the component subscribing to all events
218 | component.ngOnInit();
219 |
220 | const stateChangeCallback: Function = visualizationMock.events.addListener.mock.calls[2][2];
221 |
222 | const eventMock = 'event';
223 | stateChangeCallback(eventMock);
224 |
225 | expect(stateChangeSpy).toHaveBeenCalledWith(eventMock);
226 | });
227 | });
228 | });
229 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/control-wrapper/control-wrapper.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | EventEmitter,
5 | HostBinding,
6 | Input,
7 | OnChanges,
8 | OnInit,
9 | Output,
10 | SimpleChanges
11 | } from '@angular/core';
12 | import { ReplaySubject } from 'rxjs';
13 |
14 | import { generateRandomId } from '../../helpers/id.helper';
15 | import { ScriptLoaderService } from '../../services/script-loader.service';
16 | import { FilterType } from '../../types/control-type';
17 | import { ChartErrorEvent, ChartReadyEvent } from '../../types/events';
18 | import { ChartBase } from '../chart-base/chart-base.component';
19 |
20 | @Component({
21 | selector: 'control-wrapper',
22 | template: '',
23 | host: { class: 'control-wrapper' },
24 | exportAs: 'controlWrapper',
25 | changeDetection: ChangeDetectionStrategy.OnPush
26 | })
27 | export class ControlWrapperComponent implements OnInit, OnChanges {
28 | /**
29 | * Charts controlled by this control wrapper. Can be a single chart or an array of charts.
30 | */
31 | @Input()
32 | public for!: ChartBase | ChartBase[];
33 |
34 | /**
35 | * The class name of the control.
36 | * The `google.visualization` package name can be omitted for Google controls.
37 | *
38 | * @example
39 | *
40 | * ```html
41 | *
42 | * ```
43 | */
44 | @Input()
45 | public type!: FilterType;
46 |
47 | /**
48 | * An object describing the options for the control.
49 | * You can use either JavaScript literal notation, or provide a handle to the object.
50 | *
51 | * @example
52 | *
53 | * ```html
54 | *
55 | * ```
56 | */
57 | @Input()
58 | public options?: object;
59 |
60 | /**
61 | * An object describing the state of the control.
62 | * The state collects all the variables that the user operating the control can affect.
63 | *
64 | * For example, a range slider state can be described in term of the positions that the low and high thumb
65 | * of the slider occupy.
66 | * You can use either Javascript literal notation, or provide a handle to the object.
67 | *
68 | * @example
69 | *
70 | * ```html
71 | *
72 | * ```
73 | */
74 | @Input()
75 | public state?: object;
76 |
77 | /**
78 | * Emits when an error occurs when attempting to render the control.
79 | */
80 | @Output()
81 | public error = new EventEmitter();
82 |
83 | /**
84 | * The control is ready to accept user interaction and for external method calls.
85 | *
86 | * Alternatively, you can listen for a ready event on the dashboard holding the control
87 | * and call control methods only after the event was fired.
88 | */
89 | @Output()
90 | public ready = new EventEmitter();
91 |
92 | /**
93 | * Emits when the user interacts with the control, affecting its state.
94 | * For example, a `stateChange` event will be emitted whenever you move the thumbs of a range slider control.
95 | *
96 | * To retrieve an updated control state after the event fired, call `ControlWrapper.getState()`.
97 | */
98 | @Output()
99 | public stateChange = new EventEmitter();
100 |
101 | /**
102 | * A generated id assigned to this components DOM element.
103 | */
104 | @HostBinding('id')
105 | public readonly id = generateRandomId();
106 |
107 | private _controlWrapper?: google.visualization.ControlWrapper;
108 | private wrapperReadySubject = new ReplaySubject(1);
109 |
110 | constructor(private loaderService: ScriptLoaderService) {}
111 |
112 | /**
113 | * Emits after the `ControlWrapper` was created.
114 | */
115 | public get wrapperReady$() {
116 | return this.wrapperReadySubject.asObservable();
117 | }
118 |
119 | public get controlWrapper(): google.visualization.ControlWrapper {
120 | if (!this._controlWrapper) {
121 | throw new Error(`Cannot access the control wrapper before it being initialized.`);
122 | }
123 |
124 | return this._controlWrapper;
125 | }
126 |
127 | public ngOnInit() {
128 | this.loaderService.loadChartPackages('controls').subscribe(() => {
129 | this.createControlWrapper();
130 | });
131 | }
132 |
133 | public ngOnChanges(changes: SimpleChanges): void {
134 | if (!this._controlWrapper) {
135 | return;
136 | }
137 |
138 | if (changes.type) {
139 | this._controlWrapper.setControlType(this.type);
140 | }
141 |
142 | if (changes.options) {
143 | this._controlWrapper.setOptions(this.options || {});
144 | }
145 |
146 | if (changes.state) {
147 | this._controlWrapper.setState(this.state || {});
148 | }
149 | }
150 |
151 | private createControlWrapper() {
152 | this._controlWrapper = new google.visualization.ControlWrapper({
153 | containerId: this.id,
154 | controlType: this.type,
155 | state: this.state,
156 | options: this.options
157 | });
158 |
159 | this.addEventListeners();
160 | this.wrapperReadySubject.next(this._controlWrapper);
161 | }
162 |
163 | private addEventListeners() {
164 | google.visualization.events.removeAllListeners(this._controlWrapper);
165 |
166 | google.visualization.events.addListener(this._controlWrapper, 'ready', (event: ChartReadyEvent) =>
167 | this.ready.emit(event)
168 | );
169 | google.visualization.events.addListener(this._controlWrapper, 'error', (event: ChartErrorEvent) =>
170 | this.error.emit(event)
171 | );
172 | google.visualization.events.addListener(this._controlWrapper, 'statechange', (event: unknown) =>
173 | this.stateChange.emit(event)
174 | );
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/dashboard/dashboard.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { SimpleChange } from '@angular/core';
2 | import { ComponentFixture, TestBed } from '@angular/core/testing';
3 | import { EMPTY, of, Subject } from 'rxjs';
4 |
5 | import { DataTableService } from '../../services/data-table.service';
6 | import { ScriptLoaderService } from '../../services/script-loader.service';
7 |
8 | import { DashboardComponent } from './dashboard.component';
9 |
10 | jest.mock('../../services/script-loader.service');
11 |
12 | const visualizationMock = {
13 | Dashboard: jest.fn(),
14 | arrayToDataTable: jest.fn(),
15 | events: {
16 | addListener: jest.fn(),
17 | removeAllListeners: jest.fn()
18 | }
19 | };
20 |
21 | describe('DashboardComponent', () => {
22 | let component: DashboardComponent;
23 | let fixture: ComponentFixture;
24 |
25 | beforeEach(async () => {
26 | await TestBed.configureTestingModule({
27 | declarations: [DashboardComponent],
28 | providers: [ScriptLoaderService, DataTableService]
29 | }).compileComponents();
30 | });
31 |
32 | beforeEach(() => {
33 | fixture = TestBed.createComponent(DashboardComponent);
34 | component = fixture.componentInstance;
35 | // No change detection here, we want to invoke the
36 | // lifecycle methods in the unit tests
37 | });
38 |
39 | it('should create', () => {
40 | expect(component).toBeTruthy();
41 | });
42 |
43 | describe('ngOnInit', () => {
44 | it('should load the controls package', () => {
45 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
46 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(EMPTY);
47 |
48 | component.ngOnInit();
49 |
50 | expect(scriptLoaderService.loadChartPackages).toHaveBeenCalledWith('controls');
51 | });
52 |
53 | it('should create the data table', () => {
54 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
55 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
56 |
57 | globalThis.google = { visualization: visualizationMock } as any;
58 |
59 | const columns = ['test', 'test2'];
60 | component.columns = columns;
61 |
62 | const data = [
63 | ['row 1', 10],
64 | ['row 2', 12]
65 | ];
66 | component.data = data;
67 |
68 | component['controlWrappers'] = [] as any;
69 |
70 | component.ngOnInit();
71 |
72 | expect(visualizationMock.arrayToDataTable).toHaveBeenCalledWith([columns, ...data], false);
73 | });
74 |
75 | it('should create the data table without columns if none are provided', () => {
76 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
77 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
78 |
79 | globalThis.google = { visualization: visualizationMock } as any;
80 | const data = [
81 | ['row 1', 10],
82 | ['row 2', 12]
83 | ];
84 | component.data = data;
85 |
86 | component['controlWrappers'] = [] as any;
87 |
88 | component.ngOnInit();
89 |
90 | expect(visualizationMock.arrayToDataTable).toHaveBeenCalledWith(data, true);
91 | });
92 |
93 | it('should wait for all controls and their charts until creating the dashboard', () => {
94 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
95 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
96 |
97 | const dashboardMock = { bind: jest.fn(), draw: jest.fn() };
98 | visualizationMock.Dashboard.mockReturnValueOnce(dashboardMock);
99 |
100 | globalThis.google = { visualization: visualizationMock } as any;
101 |
102 | const chartOne = { wrapperReady$: new Subject() };
103 | const chartTwo = { wrapperReady$: new Subject() };
104 | const controlOne = { wrapperReady$: new Subject(), for: chartOne };
105 | const controlTwo = { wrapperReady$: new Subject(), for: [chartOne, chartTwo] };
106 |
107 | component['controlWrappers'] = [controlOne, controlTwo] as any;
108 |
109 | component.ngOnInit();
110 |
111 | expect(visualizationMock.Dashboard).not.toHaveBeenCalled();
112 |
113 | controlOne.wrapperReady$.next();
114 | controlTwo.wrapperReady$.next();
115 | expect(visualizationMock.Dashboard).not.toHaveBeenCalled();
116 |
117 | chartOne.wrapperReady$.next();
118 | expect(visualizationMock.Dashboard).not.toHaveBeenCalled();
119 | chartTwo.wrapperReady$.next();
120 |
121 | expect(visualizationMock.Dashboard).toHaveBeenCalled();
122 | });
123 |
124 | it('should bind all controls and dashboards', () => {
125 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
126 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
127 |
128 | const dashboardMock = { bind: jest.fn(), draw: jest.fn() };
129 | visualizationMock.Dashboard.mockReturnValueOnce(dashboardMock);
130 |
131 | globalThis.google = { visualization: visualizationMock } as any;
132 |
133 | const chartOne = { wrapperReady$: of(null), chartWrapper: {} };
134 | const chartTwo = { wrapperReady$: of(null), chartWrapper: {} };
135 | const controlOne = { wrapperReady$: of(null), for: chartOne, controlWrapper: {} };
136 | const controlTwo = { wrapperReady$: of(null), for: [chartOne, chartTwo], controlWrapper: {} };
137 |
138 | component['controlWrappers'] = [controlOne, controlTwo] as any;
139 |
140 | component.ngOnInit();
141 |
142 | expect(dashboardMock.bind).toHaveBeenCalledWith(controlOne.controlWrapper, chartOne.chartWrapper);
143 | expect(dashboardMock.bind).toHaveBeenCalledWith(controlTwo.controlWrapper, [
144 | chartOne.chartWrapper,
145 | chartTwo.chartWrapper
146 | ]);
147 | });
148 |
149 | it('should register dashboard wrapper event handlers', () => {
150 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
151 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
152 |
153 | const dashboardMock = { bind: jest.fn(), draw: jest.fn() };
154 | visualizationMock.Dashboard.mockReturnValueOnce(dashboardMock);
155 |
156 | const dataTableMock = {};
157 | visualizationMock.arrayToDataTable.mockReturnValueOnce(dataTableMock);
158 |
159 | globalThis.google = { visualization: visualizationMock } as any;
160 |
161 | // At least one control wrapper is needed to start the drawing
162 | const chart = { wrapperReady$: of(null), chartWrapper: {} };
163 | const control = { wrapperReady$: of(null), for: chart, controlWrapper: {} };
164 | component['controlWrappers'] = [control] as any;
165 |
166 | component.data = [];
167 |
168 | component.ngOnInit();
169 |
170 | expect(visualizationMock.events.removeAllListeners).toHaveBeenCalled();
171 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(dashboardMock, 'ready', expect.any(Function));
172 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(dashboardMock, 'error', expect.any(Function));
173 | });
174 |
175 | it('should apply the provided formatters', () => {
176 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
177 | service.loadChartPackages.mockReturnValueOnce(of(null));
178 |
179 | const formatter = { formatter: { format: jest.fn() }, colIndex: 1 };
180 | component.formatters = [formatter];
181 | component.data = [];
182 |
183 | const dataTableMock = {};
184 | visualizationMock.arrayToDataTable.mockReturnValueOnce(dataTableMock);
185 |
186 | component.ngOnInit();
187 |
188 | expect(formatter.formatter.format).toHaveBeenCalledWith(dataTableMock, formatter.colIndex);
189 | });
190 |
191 | it('should draw the dashboard using the provided data', () => {
192 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
193 | scriptLoaderService.loadChartPackages.mockReturnValueOnce(of(null));
194 |
195 | const dashboardMock = { bind: jest.fn(), draw: jest.fn() };
196 | visualizationMock.Dashboard.mockReturnValueOnce(dashboardMock);
197 |
198 | const dataTableMock = {};
199 | visualizationMock.arrayToDataTable.mockReturnValueOnce(dataTableMock);
200 |
201 | globalThis.google = { visualization: visualizationMock } as any;
202 |
203 | // At least one control wrapper is needed to start the drawing
204 | const chart = { wrapperReady$: of(null), chartWrapper: {} };
205 | const control = { wrapperReady$: of(null), for: chart, controlWrapper: {} };
206 | component['controlWrappers'] = [control] as any;
207 |
208 | component.data = [];
209 |
210 | component.ngOnInit();
211 |
212 | expect(dashboardMock.draw).toHaveBeenCalledWith(dataTableMock);
213 | });
214 | });
215 |
216 | describe('ngOnChanges', () => {
217 | function changeInput(property: K, newValue: DashboardComponent[K]) {
218 | const oldValue = component[property];
219 | component[property] = newValue;
220 | component.ngOnChanges({ [property]: new SimpleChange(oldValue, newValue, oldValue == null) });
221 | }
222 |
223 | it('should not throw if called before initialization', () => {
224 | expect(() => component.ngOnChanges({})).not.toThrow();
225 | });
226 |
227 | it('should redraw the chart if the data changed', () => {
228 | const dashboardMock = { draw: jest.fn() };
229 | component['dashboard'] = dashboardMock as any;
230 | component['initialized'] = true;
231 |
232 | globalThis.google = { visualization: visualizationMock } as any;
233 |
234 | const data = [['row 1', 12]];
235 | changeInput('data', data);
236 |
237 | expect(visualizationMock.arrayToDataTable).toHaveBeenCalled();
238 | expect(dashboardMock.draw).toHaveBeenCalled();
239 | });
240 |
241 | it('should redraw the chart if the columns changed', () => {
242 | const dashboardMock = { draw: jest.fn() };
243 | component['dashboard'] = dashboardMock as any;
244 | component['initialized'] = true;
245 |
246 | globalThis.google = { visualization: visualizationMock } as any;
247 |
248 | component.data = [];
249 |
250 | const columns = ['test'];
251 | changeInput('columns', columns);
252 |
253 | expect(visualizationMock.arrayToDataTable).toHaveBeenCalled();
254 | expect(dashboardMock.draw).toHaveBeenCalled();
255 | });
256 |
257 | it('should redraw the chart if `formatters` changed', () => {
258 | const dashboardMock = { draw: jest.fn() };
259 | component['dashboard'] = dashboardMock as any;
260 | component['initialized'] = true;
261 |
262 | globalThis.google = { visualization: visualizationMock } as any;
263 |
264 | const data = [
265 | ['First Row', 10],
266 | ['Second Row', 11]
267 | ];
268 | component.data = data;
269 |
270 | const columns = ['Some label', 'Some values'];
271 | component.columns = columns;
272 |
273 | const dataTableMock = {};
274 | visualizationMock.arrayToDataTable.mockReturnValueOnce(dataTableMock);
275 |
276 | const formatter = { formatter: { format: jest.fn() }, colIndex: 1 };
277 | changeInput('formatters', [formatter]);
278 |
279 | expect(formatter.formatter.format).toHaveBeenCalled();
280 | expect(visualizationMock.arrayToDataTable).toHaveBeenCalled();
281 | expect(dashboardMock.draw).toHaveBeenCalled();
282 | });
283 |
284 | it('should not redraw if nothing changed', () => {
285 | const dashboardMock = { draw: jest.fn() };
286 | component['dashboard'] = dashboardMock as any;
287 | component['initialized'] = true;
288 |
289 | globalThis.google = { visualization: visualizationMock } as any;
290 |
291 | component.data = [['row 1', 12]];
292 | component.columns = ['test'];
293 |
294 | component.ngOnChanges({});
295 |
296 | expect(visualizationMock.arrayToDataTable).not.toHaveBeenCalled();
297 | expect(dashboardMock.draw).not.toHaveBeenCalled();
298 | });
299 | });
300 | });
301 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/dashboard/dashboard.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | ContentChildren,
5 | ElementRef,
6 | EventEmitter,
7 | Input,
8 | OnChanges,
9 | OnInit,
10 | Output,
11 | QueryList,
12 | SimpleChanges
13 | } from '@angular/core';
14 | import { combineLatest } from 'rxjs';
15 |
16 | import { DataTableService } from '../../services/data-table.service';
17 | import { ScriptLoaderService } from '../../services/script-loader.service';
18 | import { ChartErrorEvent } from '../../types/events';
19 | import { Formatter } from '../../types/formatter';
20 | import { Column, Row } from '../chart-base/chart-base.component';
21 | import { ControlWrapperComponent } from '../control-wrapper/control-wrapper.component';
22 |
23 | @Component({
24 | selector: 'dashboard',
25 | template: '',
26 | changeDetection: ChangeDetectionStrategy.OnPush,
27 | exportAs: 'dashboard',
28 | host: { class: 'dashboard' }
29 | })
30 | export class DashboardComponent implements OnInit, OnChanges {
31 | /**
32 | * Data used to initialize the table.
33 | *
34 | * This must also contain all roles that are set in the `columns` property.
35 | */
36 | @Input()
37 | public data!: Row[];
38 |
39 | /**
40 | * The columns the `data` consists of.
41 | * The length of this array must match the length of each row in the `data` object.
42 | *
43 | * If {@link https://developers.google.com/chart/interactive/docs/roles roles} should be applied, they must be included in this array as well.
44 | */
45 | @Input()
46 | public columns?: Column[];
47 |
48 | /**
49 | * Used to change the displayed value of the specified column in all rows.
50 | *
51 | * Each array element must consist of an instance of a [`formatter`](https://developers.google.com/chart/interactive/docs/reference#formatters)
52 | * and the index of the column you want the formatter to get applied to.
53 | */
54 | @Input()
55 | public formatters?: Formatter[];
56 |
57 | /**
58 | * The dashboard has completed drawing and is ready to accept changes.
59 | *
60 | * The ready event will also fire:
61 | * - after the completion of a dashboard refresh triggered by a user or programmatic interaction with one of the controls,
62 | * - after redrawing any chart on the dashboard.
63 | */
64 | @Output()
65 | public ready = new EventEmitter();
66 |
67 | /**
68 | * Emits when an error occurs when attempting to render the dashboard.
69 | * One or more of the controls and charts that are part of the dashboard may have failed rendering.
70 | */
71 | @Output()
72 | public error = new EventEmitter();
73 |
74 | @ContentChildren(ControlWrapperComponent)
75 | private controlWrappers!: QueryList;
76 |
77 | private dashboard?: google.visualization.Dashboard;
78 | private dataTable?: google.visualization.DataTable;
79 | private initialized = false;
80 |
81 | constructor(
82 | private element: ElementRef,
83 | private loaderService: ScriptLoaderService,
84 | private dataTableService: DataTableService
85 | ) {}
86 |
87 | public ngOnInit() {
88 | this.loaderService.loadChartPackages('controls').subscribe(() => {
89 | this.dataTable = this.dataTableService.create(this.data, this.columns, this.formatters);
90 | this.createDashboard();
91 | this.initialized = true;
92 | });
93 | }
94 |
95 | public ngOnChanges(changes: SimpleChanges): void {
96 | if (!this.initialized) {
97 | return;
98 | }
99 |
100 | if (changes.data || changes.columns || changes.formatters) {
101 | this.dataTable = this.dataTableService.create(this.data, this.columns, this.formatters);
102 | this.dashboard!.draw(this.dataTable!);
103 | }
104 | }
105 |
106 | private createDashboard(): void {
107 | // TODO: This should happen in the control wrapper
108 | // However, I don't yet know how to do this because then `bind()` would get called multiple times
109 | // for the same control if something changes. This is not supported by google charts as far as I can tell
110 | // from their source code.
111 | const controlWrappersReady$ = this.controlWrappers.map(control => control.wrapperReady$);
112 | const chartsReady$ = this.controlWrappers
113 | .map(control => control.for)
114 | .map(charts => {
115 | if (Array.isArray(charts)) {
116 | // CombineLatest waits for all observables
117 | return combineLatest(charts.map(chart => chart.wrapperReady$));
118 | } else {
119 | return charts.wrapperReady$;
120 | }
121 | });
122 |
123 | // We have to wait for all chart wrappers and control wrappers to be initialized
124 | // before we can compose them together to create the dashboard
125 | combineLatest([...controlWrappersReady$, ...chartsReady$]).subscribe(() => {
126 | this.dashboard = new google.visualization.Dashboard(this.element.nativeElement);
127 | this.initializeBindings();
128 | this.registerEvents();
129 | this.dashboard.draw(this.dataTable!);
130 | });
131 | }
132 |
133 | private registerEvents(): void {
134 | google.visualization.events.removeAllListeners(this.dashboard);
135 |
136 | const registerDashEvent = (object: any, eventName: string, callback: Function) => {
137 | google.visualization.events.addListener(object, eventName, callback);
138 | };
139 |
140 | registerDashEvent(this.dashboard, 'ready', () => this.ready.emit());
141 | registerDashEvent(this.dashboard, 'error', (error: ChartErrorEvent) => this.error.emit(error));
142 | }
143 |
144 | private initializeBindings(): void {
145 | this.controlWrappers.forEach(control => {
146 | if (Array.isArray(control.for)) {
147 | const chartWrappers = control.for.map(chart => chart.chartWrapper);
148 | this.dashboard!.bind(control.controlWrapper, chartWrappers);
149 | } else {
150 | this.dashboard!.bind(control.controlWrapper, control.for.chartWrapper);
151 | }
152 | });
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/google-chart/google-chart.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { SimpleChange } from '@angular/core';
2 | import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
3 | import { EMPTY, of } from 'rxjs';
4 |
5 | import { ScriptLoaderService } from '../../services/script-loader.service';
6 | import { ChartType } from '../../types/chart-type';
7 | import { ChartReadyEvent } from '../../types/events';
8 |
9 | import { GoogleChartComponent } from './google-chart.component';
10 |
11 | jest.mock('../../services/script-loader.service');
12 |
13 | const chartWrapperMock = {
14 | setChartType: jest.fn(),
15 | setDataTable: jest.fn(),
16 | setOptions: jest.fn(),
17 | draw: jest.fn(),
18 | getChart: jest.fn()
19 | };
20 |
21 | const visualizationMock = {
22 | ChartWrapper: jest.fn(),
23 | arrayToDataTable: jest.fn(),
24 | events: {
25 | addListener: jest.fn(),
26 | removeListener: jest.fn(),
27 | removeAllListeners: jest.fn()
28 | }
29 | };
30 |
31 | describe('GoogleChartComponent', () => {
32 | let component: GoogleChartComponent;
33 | let fixture: ComponentFixture;
34 |
35 | beforeEach(() => {
36 | visualizationMock.ChartWrapper.mockReturnValue(chartWrapperMock);
37 | globalThis.google = { visualization: visualizationMock } as any;
38 | });
39 |
40 | beforeEach(async () => {
41 | await TestBed.configureTestingModule({
42 | declarations: [GoogleChartComponent],
43 | providers: [ScriptLoaderService]
44 | }).compileComponents();
45 | });
46 |
47 | beforeEach(() => {
48 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
49 | scriptLoaderService.loadChartPackages.mockReturnValue(EMPTY);
50 | });
51 |
52 | beforeEach(() => {
53 | fixture = TestBed.createComponent(GoogleChartComponent);
54 | component = fixture.componentInstance;
55 | // No change detection here, we want to invoke the
56 | // lifecycle methods in the unit tests
57 | });
58 |
59 | it('should be created', () => {
60 | expect(component).toBeTruthy();
61 | });
62 |
63 | it('should not throw when trying to access chart if its not yet drawn', () => {
64 | component['wrapper'] = chartWrapperMock as any;
65 | expect(() => component.chart).not.toThrow();
66 | });
67 |
68 | describe('chartWrapper', () => {
69 | it('should throw if the chart wrapper is `undefined`', () => {
70 | expect(() => component.chartWrapper).toThrow();
71 | });
72 |
73 | it('should return the chart wrapper', () => {
74 | component['wrapper'] = chartWrapperMock as any;
75 |
76 | const wrapper = component.chartWrapper;
77 | expect(wrapper).toBe(chartWrapperMock);
78 | });
79 |
80 | it('should redraw if changed', () => {
81 | component.chartWrapper = chartWrapperMock as any;
82 |
83 | expect(chartWrapperMock.draw).toHaveBeenCalled();
84 | });
85 | });
86 |
87 | describe('ngOnInit', () => {
88 | it('should load the google chart library', () => {
89 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
90 | service.loadChartPackages.mockReturnValueOnce(EMPTY);
91 |
92 | component.ngOnInit();
93 |
94 | expect(service.loadChartPackages).toHaveBeenCalled();
95 | });
96 |
97 | it('should not throw if only the type, but no data is provided', waitForAsync(() => {
98 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
99 | service.loadChartPackages.mockReturnValueOnce(of(null));
100 |
101 | component.ngOnInit();
102 |
103 | expect(component['wrapper']).toBeDefined();
104 | expect(chartWrapperMock.draw).toHaveBeenCalledTimes(1);
105 | }));
106 |
107 | it('should create the data table', () => {
108 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
109 | service.loadChartPackages.mockReturnValueOnce(of(null));
110 |
111 | const data = [
112 | ['First Row', 10],
113 | ['Second Row', 11]
114 | ];
115 | component.data = data;
116 |
117 | const columns = ['Some data', 'Some values'];
118 | component.columns = columns;
119 |
120 | component.ngOnInit();
121 |
122 | expect(visualizationMock.arrayToDataTable).toHaveBeenCalledWith([columns, ...data], false);
123 | });
124 |
125 | it('should assume the first row is data if no columns are provided', () => {
126 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
127 | service.loadChartPackages.mockReturnValueOnce(of(null));
128 |
129 | const data = [
130 | ['First Row', 10],
131 | ['Second Row', 11]
132 | ];
133 | component.data = data;
134 |
135 | component.ngOnInit();
136 |
137 | expect(visualizationMock.arrayToDataTable).toHaveBeenCalledWith(data, true);
138 | });
139 |
140 | it('should create the chart', () => {
141 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
142 | service.loadChartPackages.mockReturnValueOnce(of(null));
143 |
144 | const dataTableMock = { data: [] };
145 | visualizationMock.arrayToDataTable.mockReturnValueOnce(dataTableMock);
146 |
147 | const data = [
148 | ['First Row', 10],
149 | ['Second Row', 11]
150 | ];
151 | component.data = data;
152 |
153 | const columns = ['Some data', 'Some values'];
154 | component.columns = columns;
155 |
156 | const options = { test: 'test' };
157 | component.options = options;
158 |
159 | const chartType = ChartType.BarChart;
160 | component.type = chartType;
161 |
162 | component.ngOnInit();
163 |
164 | expect(visualizationMock.ChartWrapper).toHaveBeenCalledWith({
165 | container: expect.any(Object),
166 | dataTable: dataTableMock,
167 | chartType,
168 | options
169 | });
170 | });
171 |
172 | it('should set the title, width and height', () => {
173 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
174 | service.loadChartPackages.mockReturnValueOnce(of(null));
175 |
176 | const dataTableMock = { data: [] };
177 | visualizationMock.arrayToDataTable.mockReturnValueOnce(dataTableMock);
178 |
179 | const data = [
180 | ['First Row', 10],
181 | ['Second Row', 11]
182 | ];
183 | component.data = data;
184 |
185 | const columns = ['Some data', 'Some values'];
186 | component.columns = columns;
187 |
188 | const options = { test: 'test' };
189 | component.options = options;
190 |
191 | const title = 'chart';
192 | component.title = title;
193 |
194 | const width = 120;
195 | component.width = width;
196 |
197 | const height = 150;
198 | component.height = height;
199 |
200 | const chartType = ChartType.BarChart;
201 | component.type = chartType;
202 |
203 | component.ngOnInit();
204 |
205 | expect(visualizationMock.ChartWrapper).toHaveBeenCalledWith({
206 | container: expect.any(Object),
207 | dataTable: dataTableMock,
208 | options: { ...options, width, height, title },
209 | chartType
210 | });
211 | });
212 |
213 | it('should apply the provided formatters', () => {
214 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
215 | service.loadChartPackages.mockReturnValueOnce(of(null));
216 |
217 | const formatter = { formatter: { format: jest.fn() }, colIndex: 1 };
218 | component.formatters = [formatter];
219 | component.data = [];
220 |
221 | const dataTableMock = {};
222 | visualizationMock.arrayToDataTable.mockReturnValueOnce(dataTableMock);
223 |
224 | component.ngOnInit();
225 |
226 | expect(formatter.formatter.format).toHaveBeenCalledWith(dataTableMock, formatter.colIndex);
227 | });
228 |
229 | it('should draw the chart', () => {
230 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
231 | service.loadChartPackages.mockReturnValueOnce(of(null));
232 |
233 | component.ngOnInit();
234 |
235 | expect(chartWrapperMock.draw).toHaveBeenCalled();
236 | });
237 |
238 | it('should not draw the chart if the chart is part of a dashboard', () => {
239 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
240 | service.loadChartPackages.mockReturnValueOnce(of(null));
241 |
242 | component['dashboard'] = {} as any;
243 |
244 | component.ngOnInit();
245 |
246 | expect(chartWrapperMock.draw).not.toHaveBeenCalled();
247 | });
248 |
249 | it('should emit wrapper ready event', () => {
250 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
251 | service.loadChartPackages.mockReturnValueOnce(of(null));
252 |
253 | const readySpy = jest.fn();
254 | component.wrapperReady$.subscribe(event => readySpy(event));
255 |
256 | component.ngOnInit();
257 |
258 | expect(readySpy).toHaveBeenCalledWith(chartWrapperMock);
259 | });
260 |
261 | it('should register chart wrapper event handlers', () => {
262 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
263 | service.loadChartPackages.mockReturnValueOnce(of(null));
264 |
265 | component.ngOnInit();
266 |
267 | expect(visualizationMock.events.removeAllListeners).toHaveBeenCalled();
268 |
269 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(
270 | chartWrapperMock,
271 | 'ready',
272 | expect.any(Function)
273 | );
274 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(
275 | chartWrapperMock,
276 | 'error',
277 | expect.any(Function)
278 | );
279 | });
280 |
281 | it('should create the chart with correct package', () => {
282 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
283 | service.loadChartPackages.mockReturnValueOnce(of(null));
284 |
285 | const chartType = ChartType.Map;
286 | component.type = chartType;
287 |
288 | component.ngOnInit();
289 |
290 | expect(service.loadChartPackages).toHaveBeenCalledWith('map');
291 | });
292 | });
293 |
294 | describe('ngOnChanges', () => {
295 | beforeEach(() => {
296 | component['wrapper'] = chartWrapperMock as any;
297 | component['initialized'] = true;
298 | });
299 |
300 | it('should not throw if anything changed but the chart wrapper was not yet initialized', () => {
301 | component['wrapper'] = undefined;
302 | component['initialized'] = false;
303 |
304 | expect(() => {
305 | component.ngOnChanges({
306 | data: new SimpleChange(null, [], true),
307 | columns: new SimpleChange(null, [], true)
308 | });
309 | }).not.toThrow();
310 | });
311 |
312 | it('should draw the chart only once if `type`, `data` and `columns` changed all at once', () => {
313 | const data = [
314 | ['First Row', 10],
315 | ['Second Row', 11]
316 | ];
317 | component.data = data;
318 |
319 | const columns = ['Some data', 'Some values'];
320 | component.columns = columns;
321 |
322 | const chartType = ChartType.BarChart;
323 | component.type = chartType;
324 | component.ngOnChanges({
325 | type: new SimpleChange(null, chartType, true),
326 | data: new SimpleChange(null, data, true),
327 | columns: new SimpleChange(null, columns, true)
328 | });
329 |
330 | expect(chartWrapperMock.draw).toHaveBeenCalledTimes(1);
331 | });
332 |
333 | it('should not redraw the chart if nothing changed', () => {
334 | component.ngOnChanges({});
335 |
336 | expect(chartWrapperMock.draw).not.toBeCalled();
337 | });
338 |
339 | it('should redraw the chart if `data` changed', () => {
340 | const data = [
341 | ['First Row', 10],
342 | ['Second Row', 11]
343 | ];
344 | component.data = data;
345 |
346 | const columns = ['Some label', 'Some values'];
347 | component.columns = columns;
348 |
349 | const dataTableMock = {};
350 | visualizationMock.arrayToDataTable.mockReturnValueOnce(dataTableMock);
351 |
352 | const newData = [...data, ['Third Row', 12]];
353 | changeInput('data', newData);
354 |
355 | expect(visualizationMock.arrayToDataTable).toHaveBeenCalledWith([columns, ...newData], false);
356 | expect(chartWrapperMock.setDataTable).toHaveBeenCalledWith(dataTableMock);
357 | expect(chartWrapperMock.draw).toHaveBeenCalled();
358 | });
359 |
360 | it('should redraw the chart if `columns` changed', () => {
361 | const data = [
362 | ['First Row', 10],
363 | ['Second Row', 11]
364 | ];
365 | component.data = data;
366 |
367 | const columns = ['Some label', 'Some values'];
368 | component.columns = columns;
369 |
370 | const dataTableMock = {};
371 | visualizationMock.arrayToDataTable.mockReturnValueOnce(dataTableMock);
372 |
373 | const newColumns = ['New label', 'Some values'];
374 | changeInput('columns', newColumns);
375 |
376 | expect(visualizationMock.arrayToDataTable).toHaveBeenCalledWith([newColumns, ...data], false);
377 | expect(chartWrapperMock.setDataTable).toHaveBeenCalledWith(dataTableMock);
378 | expect(chartWrapperMock.draw).toHaveBeenCalled();
379 | });
380 |
381 | it('should redraw the chart if `formatters` changed', () => {
382 | const data = [
383 | ['First Row', 10],
384 | ['Second Row', 11]
385 | ];
386 | component.data = data;
387 |
388 | const columns = ['Some label', 'Some values'];
389 | component.columns = columns;
390 |
391 | const dataTableMock = {};
392 | visualizationMock.arrayToDataTable.mockReturnValueOnce(dataTableMock);
393 |
394 | const formatter = { formatter: { format: jest.fn() }, colIndex: 1 };
395 | changeInput('formatters', [formatter]);
396 |
397 | expect(formatter.formatter.format).toHaveBeenCalled();
398 | expect(chartWrapperMock.setDataTable).toHaveBeenCalledWith(dataTableMock);
399 | expect(chartWrapperMock.draw).toHaveBeenCalled();
400 | });
401 |
402 | it('should redraw the chart if `options` changed', () => {
403 | const options = { test: 'test' };
404 | changeInput('options', options);
405 |
406 | expect(chartWrapperMock.setOptions).toHaveBeenCalledWith(options);
407 | expect(chartWrapperMock.draw).toHaveBeenCalled();
408 | });
409 |
410 | it('should redraw the chart if `title` changed', () => {
411 | const title = 'some title';
412 | changeInput('title', title);
413 |
414 | expect(chartWrapperMock.setOptions).toHaveBeenCalledWith({ title });
415 | expect(chartWrapperMock.draw).toHaveBeenCalled();
416 | });
417 |
418 | it('should redraw the chart if `width` changed', () => {
419 | const width = 100;
420 | changeInput('width', width);
421 |
422 | expect(chartWrapperMock.setOptions).toHaveBeenCalledWith({ width });
423 | expect(chartWrapperMock.draw).toHaveBeenCalled();
424 | });
425 |
426 | it('should redraw the chart if `height` changed', () => {
427 | const height = 100;
428 | changeInput('height', height);
429 |
430 | expect(chartWrapperMock.setOptions).toHaveBeenCalledWith({ height });
431 | expect(chartWrapperMock.draw).toHaveBeenCalled();
432 | });
433 | });
434 |
435 | describe('dynamicResize', () => {
436 | it('should subscribe to window resize event if set to true', () => {
437 | changeInput('dynamicResize', true);
438 |
439 | expect(component['resizeSubscription']).toBeTruthy();
440 | });
441 |
442 | it('should unsubscribe existing subscription if changed', () => {
443 | const subscriptionMock = { unsubscribe: jest.fn() };
444 | component['resizeSubscription'] = subscriptionMock as any;
445 |
446 | changeInput('dynamicResize', false);
447 |
448 | expect(subscriptionMock.unsubscribe).toHaveBeenCalled();
449 | expect(component['resizeSubscription']).toBeUndefined();
450 | });
451 |
452 | it('should do nothing if the window was resized, but the chart is not yet initialized', fakeAsync(() => {
453 | changeInput('dynamicResize', true);
454 |
455 | expect(() => {
456 | // This would cause an error if the wrapper would somehow be called
457 | window.dispatchEvent(new Event('resize'));
458 | tick(100);
459 | }).not.toThrow();
460 | }));
461 |
462 | it('should redraw the chart if the window was resized', fakeAsync(() => {
463 | const scriptLoaderService = TestBed.inject(ScriptLoaderService) as jest.Mocked;
464 | scriptLoaderService.loadChartPackages.mockReturnValue(of(null));
465 |
466 | component.ngOnInit();
467 |
468 | changeInput('dynamicResize', true);
469 |
470 | window.dispatchEvent(new Event('resize'));
471 | tick(100);
472 |
473 | expect(chartWrapperMock.draw).toHaveBeenCalled();
474 | }));
475 | });
476 |
477 | describe('events', () => {
478 | beforeEach(() => {
479 | const service = TestBed.inject(ScriptLoaderService) as jest.Mocked;
480 | service.loadChartPackages.mockReturnValueOnce(of(null));
481 | });
482 |
483 | it('should register chart event handlers after the chart got drawn', () => {
484 | const chartMock = { draw: jest.fn() };
485 | chartWrapperMock.getChart.mockReturnValue(chartMock);
486 |
487 | component.ngOnInit();
488 |
489 | expect(visualizationMock.events.addListener).not.toHaveBeenCalledWith(
490 | chartWrapperMock,
491 | 'onmouseover',
492 | expect.any(Function)
493 | );
494 | expect(visualizationMock.events.addListener).not.toHaveBeenCalledWith(
495 | chartWrapperMock,
496 | 'onmouseout',
497 | expect.any(Function)
498 | );
499 | expect(visualizationMock.events.addListener).not.toHaveBeenCalledWith(
500 | chartWrapperMock,
501 | 'select',
502 | expect.any(Function)
503 | );
504 |
505 | const readyCallback = visualizationMock.events.addListener.mock.calls[0][2];
506 | readyCallback();
507 |
508 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(chartMock, 'onmouseover', expect.any(Function));
509 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(chartMock, 'onmouseout', expect.any(Function));
510 | expect(visualizationMock.events.addListener).toHaveBeenCalledWith(chartMock, 'select', expect.any(Function));
511 | });
512 |
513 | it('should remove all listeners from the chart before subscribing again', () => {
514 | const chartMock = { draw: jest.fn() };
515 | chartWrapperMock.getChart.mockReturnValue(chartMock);
516 |
517 | component.ngOnInit();
518 |
519 | expect(visualizationMock.events.removeAllListeners).toHaveBeenCalledWith(chartWrapperMock);
520 |
521 | const readyCallback = visualizationMock.events.addListener.mock.calls[0][2];
522 | readyCallback();
523 |
524 | expect(visualizationMock.events.removeAllListeners).toHaveBeenCalledWith(chartMock);
525 | });
526 |
527 | it('should emit ready event after the chart is ready', () => {
528 | const chartMock = { draw: jest.fn() };
529 | chartWrapperMock.getChart.mockReturnValue(chartMock);
530 |
531 | component.ngOnInit();
532 |
533 | const readySpy = jest.fn();
534 | component.ready.subscribe((event: ChartReadyEvent) => readySpy(event));
535 |
536 | const readyCallback = visualizationMock.events.addListener.mock.calls[0][2];
537 | readyCallback();
538 |
539 | expect(readySpy).toHaveBeenCalledWith({ chart: chartMock });
540 | });
541 |
542 | it('should emit error event if the chart caused an error', () => {
543 | component.ngOnInit();
544 |
545 | const errorSpy = jest.fn();
546 | component.error.subscribe((event: ChartReadyEvent) => errorSpy(event));
547 |
548 | const errorCallback = visualizationMock.events.addListener.mock.calls[1][2];
549 |
550 | const error = 'someerror';
551 | errorCallback(error);
552 |
553 | expect(errorSpy).toHaveBeenCalledWith(error);
554 | });
555 |
556 | it('should emit select event if a value was selected', () => {
557 | const selection = [{ column: 1, row: 2 }] as google.visualization.ChartSelection[];
558 |
559 | const chartMock = { getSelection: jest.fn(() => selection) };
560 | chartWrapperMock.getChart.mockReturnValue(chartMock);
561 |
562 | const selectSpy = jest.fn();
563 | component.select.subscribe((event: ChartReadyEvent) => selectSpy(event));
564 |
565 | component.ngOnInit();
566 |
567 | const readyCallback = visualizationMock.events.addListener.mock.calls[0][2];
568 | readyCallback();
569 |
570 | expect(selectSpy).not.toHaveBeenCalled();
571 |
572 | const selectCallback = visualizationMock.events.addListener.mock.calls[4][2];
573 | selectCallback();
574 |
575 | expect(selectSpy).toHaveBeenCalledWith({ selection });
576 | });
577 |
578 | it('should add and remove custom event listeners', () => {
579 | const chartMock = { draw: jest.fn() };
580 | chartWrapperMock.getChart.mockReturnValue(chartMock);
581 |
582 | component.ngOnInit();
583 |
584 | visualizationMock.events.addListener.mockReturnValue('handle1');
585 | const rollupCallback = () => {};
586 | let handle = component.addEventListener('rollup', rollupCallback);
587 | expect(handle).toBe('handle1');
588 | expect(visualizationMock.events.addListener).lastCalledWith(chartMock, 'rollup', rollupCallback);
589 |
590 | component.removeEventListener(handle);
591 | expect(visualizationMock.events.removeListener).lastCalledWith(handle);
592 |
593 | handle = component.addEventListener('rollup', rollupCallback);
594 | visualizationMock.events.addListener.mockReturnValue('handle2');
595 | const readyCallback = visualizationMock.events.addListener.mock.calls[0][2];
596 | readyCallback();
597 |
598 | component.removeEventListener(handle);
599 | expect(visualizationMock.events.removeListener).not.lastCalledWith(handle);
600 | expect(visualizationMock.events.removeListener).lastCalledWith('handle2');
601 | });
602 | });
603 |
604 | function changeInput(property: K, newValue: GoogleChartComponent[K]) {
605 | const oldValue = component[property];
606 | component[property] = newValue;
607 | component.ngOnChanges({ [property]: new SimpleChange(oldValue, newValue, oldValue == null) });
608 | }
609 | });
610 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/components/google-chart/google-chart.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | ElementRef,
5 | EventEmitter,
6 | Input,
7 | OnChanges,
8 | OnDestroy,
9 | OnInit,
10 | Optional,
11 | Output,
12 | SimpleChanges
13 | } from '@angular/core';
14 | import { fromEvent, Observable, ReplaySubject, Subscription } from 'rxjs';
15 | import { debounceTime } from 'rxjs/operators';
16 |
17 | import { getPackageForChart } from '../../helpers/chart.helper';
18 | import { DataTableService } from '../../services/data-table.service';
19 | import { ScriptLoaderService } from '../../services/script-loader.service';
20 | import { ChartType } from '../../types/chart-type';
21 | import {
22 | ChartErrorEvent,
23 | ChartMouseLeaveEvent,
24 | ChartMouseOverEvent,
25 | ChartReadyEvent,
26 | ChartSelectionChangedEvent
27 | } from '../../types/events';
28 | import { Formatter } from '../../types/formatter';
29 | import { ChartBase, Column, Row } from '../chart-base/chart-base.component';
30 | import { DashboardComponent } from '../dashboard/dashboard.component';
31 |
32 | @Component({
33 | selector: 'google-chart',
34 | template: '',
35 | styles: [':host { width: fit-content; display: block; }'],
36 | host: { class: 'google-chart' },
37 | exportAs: 'googleChart',
38 | changeDetection: ChangeDetectionStrategy.OnPush
39 | })
40 | export class GoogleChartComponent implements ChartBase, OnInit, OnChanges, OnDestroy {
41 | /**
42 | * The type of the chart to create.
43 | */
44 | @Input()
45 | public type!: ChartType;
46 |
47 | /**
48 | * Data used to initialize the table.
49 | *
50 | * This must also contain all roles that are set in the `columns` property.
51 | */
52 | @Input()
53 | public data!: Row[];
54 |
55 | /**
56 | * The columns the `data` consists of.
57 | * The length of this array must match the length of each row in the `data` object.
58 | *
59 | * If {@link https://developers.google.com/chart/interactive/docs/roles roles} should be applied, they must be included in this array as well.
60 | */
61 | @Input()
62 | public columns?: Column[];
63 |
64 | /**
65 | * A convenience property used to set the title of the chart.
66 | *
67 | * This can also be set using `options.title`, which, if existant, will overwrite this value.
68 | */
69 | @Input()
70 | public title?: string;
71 |
72 | /**
73 | * A convenience property used to set the width of the chart in pixels.
74 | *
75 | * This can also be set using `options.width`, which, if existant, will overwrite this value.
76 | */
77 | @Input()
78 | public width?: number;
79 |
80 | /**
81 | * A convenience property used to set the height of the chart in pixels.
82 | *
83 | * This can also be set using `options.height`, which, if existant, will overwrite this value.
84 | */
85 | @Input()
86 | public height?: number;
87 |
88 | /**
89 | * The chart-specific options. All options listen in the Google Charts documentation applying
90 | * to the chart type specified can be used here.
91 | */
92 | @Input()
93 | public options: object = {};
94 |
95 | /**
96 | * Used to change the displayed value of the specified column in all rows.
97 | *
98 | * Each array element must consist of an instance of a [`formatter`](https://developers.google.com/chart/interactive/docs/reference#formatters)
99 | * and the index of the column you want the formatter to get applied to.
100 | */
101 | @Input()
102 | public formatters?: Formatter[];
103 |
104 | /**
105 | * If this is set to `true`, the chart will be redrawn if the browser window is resized.
106 | * Defaults to `false` and should only be used when specifying the width or height of the chart
107 | * in percent.
108 | *
109 | * Note that this can impact performance.
110 | */
111 | @Input()
112 | public dynamicResize = false;
113 |
114 | @Output()
115 | public ready = new EventEmitter();
116 |
117 | @Output()
118 | public error = new EventEmitter();
119 |
120 | @Output()
121 | public select = new EventEmitter();
122 |
123 | @Output()
124 | public mouseover = new EventEmitter();
125 |
126 | @Output()
127 | public mouseleave = new EventEmitter();
128 |
129 | private resizeSubscription?: Subscription;
130 |
131 | private dataTable: google.visualization.DataTable | undefined;
132 | private wrapper: google.visualization.ChartWrapper | undefined;
133 | private wrapperReadySubject = new ReplaySubject(1);
134 | private initialized = false;
135 | private eventListeners = new Map();
136 |
137 | constructor(
138 | private element: ElementRef,
139 | private scriptLoaderService: ScriptLoaderService,
140 | private dataTableService: DataTableService,
141 | @Optional() private dashboard?: DashboardComponent
142 | ) {}
143 |
144 | public get chart(): google.visualization.ChartBase | null {
145 | return this.chartWrapper.getChart();
146 | }
147 |
148 | public get wrapperReady$(): Observable {
149 | return this.wrapperReadySubject.asObservable();
150 | }
151 |
152 | public get chartWrapper(): google.visualization.ChartWrapper {
153 | if (!this.wrapper) {
154 | throw new Error('Trying to access the chart wrapper before it was fully initialized');
155 | }
156 |
157 | return this.wrapper;
158 | }
159 |
160 | public set chartWrapper(wrapper: google.visualization.ChartWrapper) {
161 | this.wrapper = wrapper;
162 | this.drawChart();
163 | }
164 |
165 | public ngOnInit() {
166 | // We don't need to load any chart packages, the chart wrapper will handle this for us
167 | this.scriptLoaderService.loadChartPackages(getPackageForChart(this.type)).subscribe(() => {
168 | this.dataTable = this.dataTableService.create(this.data, this.columns, this.formatters);
169 |
170 | // Only ever create the wrapper once to allow animations to happen when something changes.
171 | this.wrapper = new google.visualization.ChartWrapper({
172 | container: this.element.nativeElement,
173 | chartType: this.type,
174 | dataTable: this.dataTable,
175 | options: this.mergeOptions()
176 | });
177 |
178 | this.registerChartEvents();
179 |
180 | this.wrapperReadySubject.next(this.wrapper);
181 | this.initialized = true;
182 |
183 | this.drawChart();
184 | });
185 | }
186 |
187 | public ngOnChanges(changes: SimpleChanges) {
188 | if (changes.dynamicResize) {
189 | this.updateResizeListener();
190 | }
191 |
192 | if (this.initialized) {
193 | let shouldRedraw = false;
194 | if (changes.data || changes.columns || changes.formatters) {
195 | this.dataTable = this.dataTableService.create(this.data, this.columns, this.formatters);
196 | this.wrapper!.setDataTable(this.dataTable!);
197 | shouldRedraw = true;
198 | }
199 |
200 | if (changes.type) {
201 | this.wrapper!.setChartType(this.type);
202 | shouldRedraw = true;
203 | }
204 |
205 | if (changes.options || changes.width || changes.height || changes.title) {
206 | this.wrapper!.setOptions(this.mergeOptions());
207 | shouldRedraw = true;
208 | }
209 |
210 | if (shouldRedraw) {
211 | this.drawChart();
212 | }
213 | }
214 | }
215 |
216 | public ngOnDestroy(): void {
217 | this.unsubscribeToResizeIfSubscribed();
218 | }
219 |
220 | /**
221 | * For listening to events other than the most common ones (available via Output properties).
222 | *
223 | * Can be called after the chart emits that it's "ready".
224 | *
225 | * Returns a handle that can be used for `removeEventListener`.
226 | */
227 | public addEventListener(eventName: string, callback: Function): any {
228 | const handle = this.registerChartEvent(this.chart, eventName, callback);
229 | this.eventListeners.set(handle, { eventName, callback, handle });
230 | return handle;
231 | }
232 |
233 | public removeEventListener(handle: any): void {
234 | const entry = this.eventListeners.get(handle);
235 | if (entry) {
236 | google.visualization.events.removeListener(entry.handle);
237 | this.eventListeners.delete(handle);
238 | }
239 | }
240 |
241 | private updateResizeListener() {
242 | this.unsubscribeToResizeIfSubscribed();
243 |
244 | if (this.dynamicResize) {
245 | this.resizeSubscription = fromEvent(window, 'resize', { passive: true })
246 | .pipe(debounceTime(100))
247 | .subscribe(() => {
248 | if (this.initialized) {
249 | this.drawChart();
250 | }
251 | });
252 | }
253 | }
254 |
255 | private unsubscribeToResizeIfSubscribed() {
256 | if (this.resizeSubscription != null) {
257 | this.resizeSubscription.unsubscribe();
258 | this.resizeSubscription = undefined;
259 | }
260 | }
261 |
262 | private mergeOptions(): object {
263 | return {
264 | title: this.title,
265 | width: this.width,
266 | height: this.height,
267 | ...this.options
268 | };
269 | }
270 |
271 | private registerChartEvents() {
272 | google.visualization.events.removeAllListeners(this.wrapper);
273 |
274 | this.registerChartEvent(this.wrapper, 'ready', () => {
275 | // This could also be done by checking if we already subscribed to the events
276 | google.visualization.events.removeAllListeners(this.chart);
277 | this.registerChartEvent(this.chart, 'onmouseover', (event: ChartMouseOverEvent) => this.mouseover.emit(event));
278 | this.registerChartEvent(this.chart, 'onmouseout', (event: ChartMouseLeaveEvent) => this.mouseleave.emit(event));
279 | this.registerChartEvent(this.chart, 'select', () => {
280 | const selection = this.chart!.getSelection();
281 | this.select.emit({ selection });
282 | });
283 | this.eventListeners.forEach(x => (x.handle = this.registerChartEvent(this.chart, x.eventName, x.callback)));
284 |
285 | this.ready.emit({ chart: this.chart! });
286 | });
287 |
288 | this.registerChartEvent(this.wrapper, 'error', (error: ChartErrorEvent) => this.error.emit(error));
289 | }
290 |
291 | private registerChartEvent(object: any, eventName: string, callback: Function): any {
292 | return google.visualization.events.addListener(object, eventName, callback);
293 | }
294 |
295 | private drawChart() {
296 | if (this.dashboard != null) {
297 | // If this chart is part of a dashboard, the dashboard takes care of drawing
298 | return;
299 | }
300 |
301 | this.wrapper!.draw();
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/google-charts.module.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { GoogleChartsModule } from './google-charts.module';
4 | import { ScriptLoaderService } from './services/script-loader.service';
5 | import { GoogleChartsConfig, GOOGLE_CHARTS_CONFIG } from './types/google-charts-config';
6 |
7 | describe('GoogleChartsModule', () => {
8 | let service: ScriptLoaderService;
9 |
10 | describe('direct import', () => {
11 | beforeEach(() => {
12 | TestBed.configureTestingModule({
13 | imports: [GoogleChartsModule]
14 | });
15 | });
16 |
17 | it('should allow instantiation of `ScriptLoaderService`', () => {
18 | service = TestBed.inject(ScriptLoaderService);
19 | expect(service).toBeTruthy();
20 | });
21 | });
22 |
23 | describe('config via forRoot', () => {
24 | const mapsApiKey = 'myMapsApiKey';
25 | const version = '13.5';
26 | const safeMode = false;
27 |
28 | it('should provide the given config values', () => {
29 | const config: GoogleChartsConfig = { mapsApiKey, version, safeMode };
30 |
31 | TestBed.configureTestingModule({
32 | imports: [GoogleChartsModule.forRoot(config)]
33 | });
34 |
35 | expect(TestBed.inject(GOOGLE_CHARTS_CONFIG)).toEqual(config);
36 | });
37 |
38 | it('should accept empty config', () => {
39 | TestBed.configureTestingModule({
40 | imports: [GoogleChartsModule.forRoot()]
41 | });
42 |
43 | expect(TestBed.inject(GOOGLE_CHARTS_CONFIG)).toEqual({});
44 | });
45 |
46 | it('should accept a partial config', () => {
47 | TestBed.configureTestingModule({
48 | imports: [GoogleChartsModule.forRoot({ mapsApiKey })]
49 | });
50 |
51 | expect(TestBed.inject(GOOGLE_CHARTS_CONFIG)).toMatchObject({ mapsApiKey });
52 | });
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/google-charts.module.ts:
--------------------------------------------------------------------------------
1 | import { ModuleWithProviders, NgModule } from '@angular/core';
2 |
3 | import { ChartEditorComponent } from './components/chart-editor/chart-editor.component';
4 | import { ChartWrapperComponent } from './components/chart-wrapper/chart-wrapper.component';
5 | import { ControlWrapperComponent } from './components/control-wrapper/control-wrapper.component';
6 | import { DashboardComponent } from './components/dashboard/dashboard.component';
7 | import { GoogleChartComponent } from './components/google-chart/google-chart.component';
8 | import { ScriptLoaderService } from './services/script-loader.service';
9 | import { GoogleChartsConfig, GOOGLE_CHARTS_CONFIG } from './types/google-charts-config';
10 |
11 | @NgModule({
12 | declarations: [
13 | GoogleChartComponent,
14 | ChartWrapperComponent,
15 | DashboardComponent,
16 | ControlWrapperComponent,
17 | ChartEditorComponent
18 | ],
19 | providers: [ScriptLoaderService],
20 | exports: [
21 | GoogleChartComponent,
22 | ChartWrapperComponent,
23 | DashboardComponent,
24 | ControlWrapperComponent,
25 | ChartEditorComponent
26 | ]
27 | })
28 | export class GoogleChartsModule {
29 | public static forRoot(config: GoogleChartsConfig = {}): ModuleWithProviders {
30 | return {
31 | ngModule: GoogleChartsModule,
32 | providers: [{ provide: GOOGLE_CHARTS_CONFIG, useValue: config }]
33 | };
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/helpers/chart.helper.ts:
--------------------------------------------------------------------------------
1 | import { ChartType } from '../types/chart-type';
2 | import { GoogleChartsConfig } from '../types/google-charts-config';
3 |
4 | const ChartTypesToPackages = {
5 | [ChartType.AnnotationChart]: 'annotationchart',
6 | [ChartType.AreaChart]: 'corechart',
7 | [ChartType.Bar]: 'bar',
8 | [ChartType.BarChart]: 'corechart',
9 | [ChartType.BubbleChart]: 'corechart',
10 | [ChartType.Calendar]: 'calendar',
11 | [ChartType.CandlestickChart]: 'corechart',
12 | [ChartType.ColumnChart]: 'corechart',
13 | [ChartType.ComboChart]: 'corechart',
14 | [ChartType.PieChart]: 'corechart',
15 | [ChartType.Gantt]: 'gantt',
16 | [ChartType.Gauge]: 'gauge',
17 | [ChartType.GeoChart]: 'geochart',
18 | [ChartType.Histogram]: 'corechart',
19 | [ChartType.Line]: 'line',
20 | [ChartType.LineChart]: 'corechart',
21 | [ChartType.Map]: 'map',
22 | [ChartType.OrgChart]: 'orgchart',
23 | [ChartType.Sankey]: 'sankey',
24 | [ChartType.Scatter]: 'scatter',
25 | [ChartType.ScatterChart]: 'corechart',
26 | [ChartType.SteppedAreaChart]: 'corechart',
27 | [ChartType.Table]: 'table',
28 | [ChartType.Timeline]: 'timeline',
29 | [ChartType.TreeMap]: 'treemap',
30 | [ChartType.WordTree]: 'wordtree'
31 | };
32 |
33 | export function getPackageForChart(type: ChartType): string {
34 | return ChartTypesToPackages[type];
35 | }
36 |
37 | export function getDefaultConfig(): GoogleChartsConfig {
38 | return {
39 | version: 'current',
40 | safeMode: false
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/helpers/id.helper.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Generates a random ID which can be used to uniquely identify an element.
3 | */
4 | export function generateRandomId() {
5 | // Math.random should be unique because of its seeding algorithm.
6 | // Convert it to base 36 (numbers + letters), and grab the first 9 characters
7 | // after the decimal.
8 | return '_' + Math.random().toString(36).substr(2, 9);
9 | }
10 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/services/data-table.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import { Column, Row } from '../components/chart-base/chart-base.component';
4 | import { Formatter } from '../types/formatter';
5 |
6 | @Injectable({ providedIn: 'root' })
7 | export class DataTableService {
8 | public create(
9 | data: Row[] | undefined,
10 | columns?: Column[],
11 | formatters?: Formatter[]
12 | ): google.visualization.DataTable | undefined {
13 | if (data == null) {
14 | return undefined;
15 | }
16 |
17 | let firstRowIsData = true;
18 | if (columns != null) {
19 | firstRowIsData = false;
20 | }
21 |
22 | const dataTable = google.visualization.arrayToDataTable(this.getDataAsTable(data, columns), firstRowIsData);
23 | if (formatters) {
24 | this.applyFormatters(dataTable, formatters);
25 | }
26 |
27 | return dataTable;
28 | }
29 |
30 | private getDataAsTable(data: Row[], columns: Column[] | undefined): (Row | Column[])[] {
31 | if (columns) {
32 | return [columns, ...data];
33 | } else {
34 | return data;
35 | }
36 | }
37 |
38 | private applyFormatters(dataTable: google.visualization.DataTable, formatters: Formatter[]): void {
39 | for (const val of formatters) {
40 | val.formatter.format(dataTable, val.colIndex);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/services/script-loader.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { LOCALE_ID } from '@angular/core';
2 | import { TestBed } from '@angular/core/testing';
3 | import { Subject, throwError } from 'rxjs';
4 | import { catchError } from 'rxjs/operators';
5 |
6 | import { getDefaultConfig } from '../helpers/chart.helper';
7 | import { GoogleChartsConfig, GOOGLE_CHARTS_CONFIG, GOOGLE_CHARTS_LAZY_CONFIG } from '../types/google-charts-config';
8 |
9 | import { ScriptLoaderService } from './script-loader.service';
10 |
11 | describe('ScriptLoaderService', () => {
12 | let service: ScriptLoaderService;
13 |
14 | beforeEach(() => {
15 | TestBed.configureTestingModule({
16 | providers: [ScriptLoaderService]
17 | });
18 | });
19 |
20 | beforeEach(() => {
21 | service = TestBed.inject(ScriptLoaderService);
22 | (globalThis as any).google = undefined;
23 | });
24 |
25 | describe('isGoogleChartsAvailable', () => {
26 | it('should be false if `google` is not available', () => {
27 | expect(service.isGoogleChartsAvailable()).toBeFalsy();
28 | });
29 |
30 | it('should be false if another google package is loaded, but `google.charts` is not available', () => {
31 | globalThis.google = { someOtherPackage: {} } as any;
32 |
33 | expect(service.isGoogleChartsAvailable()).toBeFalsy();
34 | });
35 |
36 | it('should be true if `google.charts` and `google.visulization` is available', () => {
37 | globalThis.google = {
38 | charts: { load: () => {} },
39 | visulization: { arrayToDataTable: () => {} }
40 | } as any;
41 |
42 | expect(service.isGoogleChartsAvailable()).toBeTruthy();
43 | });
44 | });
45 |
46 | describe('loadChartPackages', () => {
47 | it('should load the google charts script before trying to load packages', () => {
48 | const createElementSpy = jest.spyOn(document, 'createElement').mockReturnValue({} as any);
49 |
50 | const headMock = { appendChild: jest.fn() };
51 | jest
52 | .spyOn(document, 'getElementsByTagName')
53 | .mockReturnValueOnce([] as any)
54 | .mockReturnValueOnce([headMock] as any);
55 |
56 | service.loadChartPackages().subscribe();
57 |
58 | expect(createElementSpy).toHaveBeenCalledWith('script');
59 | expect(headMock.appendChild).toHaveBeenCalledWith(
60 | expect.objectContaining({
61 | type: 'text/javascript',
62 | src: service['scriptSource'],
63 | async: true
64 | })
65 | );
66 | });
67 |
68 | describe('loading the charts script', () => {
69 | let createElementSpy: jest.SpyInstance;
70 | let getElementsByTagNameSpy: jest.SpyInstance;
71 |
72 | beforeEach(() => {
73 | createElementSpy = jest.spyOn(document, 'createElement').mockImplementation();
74 | getElementsByTagNameSpy = jest.spyOn(document, 'getElementsByTagName').mockImplementation();
75 | });
76 |
77 | it('should not load the script if it is already loaded', () => {
78 | globalThis.google = {
79 | charts: { load: () => {} }
80 | } as any;
81 |
82 | service.loadChartPackages().subscribe();
83 |
84 | expect(createElementSpy).not.toHaveBeenCalled();
85 | });
86 |
87 | it('should not load the script if it is currently being loaded', () => {
88 | getElementsByTagNameSpy.mockReturnValue([{ src: service['scriptSource'] }]);
89 |
90 | service.loadChartPackages().subscribe();
91 |
92 | expect(createElementSpy).not.toHaveBeenCalled();
93 | });
94 |
95 | it('should load the google charts script', () => {
96 | createElementSpy.mockReturnValue({});
97 |
98 | const headMock = { appendChild: jest.fn() };
99 | getElementsByTagNameSpy.mockReturnValueOnce([]).mockReturnValueOnce([headMock]);
100 |
101 | service.loadChartPackages().subscribe();
102 |
103 | expect(createElementSpy).toHaveBeenCalledWith('script');
104 | expect(headMock.appendChild).toHaveBeenCalledWith(
105 | expect.objectContaining({
106 | type: 'text/javascript',
107 | src: service['scriptSource'],
108 | async: true
109 | })
110 | );
111 | });
112 |
113 | it('should emit an error if the script fails to load', () => {
114 | const scriptMock = { onerror: () => void 0 };
115 | createElementSpy.mockReturnValue(scriptMock);
116 |
117 | const headMock = { appendChild: jest.fn() };
118 | getElementsByTagNameSpy.mockReturnValueOnce([]).mockReturnValueOnce([headMock]);
119 |
120 | const errorSpy = jest.fn();
121 | service
122 | .loadChartPackages()
123 | .pipe(
124 | catchError(error => {
125 | errorSpy();
126 | return throwError(error);
127 | })
128 | )
129 | .subscribe();
130 |
131 | expect(errorSpy).not.toHaveBeenCalled();
132 |
133 | const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
134 | scriptMock.onerror();
135 | consoleSpy.mockReset();
136 |
137 | expect(errorSpy).toHaveBeenCalled();
138 | });
139 | });
140 |
141 | describe('loading chart packages', () => {
142 | const chartsMock = {
143 | load: jest.fn(),
144 | setOnLoadCallback: jest.fn()
145 | };
146 |
147 | it('should load the chart packages after loading the google charts script', () => {
148 | const scriptMock = { onload: () => {} };
149 |
150 | jest.spyOn(document, 'createElement').mockReturnValue(scriptMock as any);
151 |
152 | const headMock = { appendChild: jest.fn() };
153 | jest
154 | .spyOn(document, 'getElementsByTagName')
155 | .mockReturnValueOnce([] as any)
156 | .mockReturnValueOnce([headMock] as any);
157 |
158 | service.loadChartPackages().subscribe();
159 |
160 | globalThis.google = { charts: chartsMock } as any;
161 | scriptMock.onload();
162 |
163 | expect(chartsMock.load).toHaveBeenCalledWith('current', {
164 | packages: [],
165 | language: 'en-US',
166 | mapsApiKey: undefined,
167 | safeMode: false
168 | });
169 | });
170 |
171 | it('should immediately load the chart packages if the google charts script is already loaded', () => {
172 | globalThis.google = { charts: chartsMock } as any;
173 |
174 | const chart = 'corechart';
175 |
176 | service.loadChartPackages(chart).subscribe();
177 |
178 | expect(chartsMock.load).toHaveBeenCalledWith('current', {
179 | packages: [chart],
180 | language: 'en-US',
181 | mapsApiKey: undefined,
182 | safeMode: false
183 | });
184 | });
185 |
186 | it('should emit after loading the charts', () => {
187 | globalThis.google = { charts: chartsMock } as any;
188 |
189 | const chart = 'corechart';
190 | let loadCallback: Function;
191 |
192 | chartsMock.setOnLoadCallback.mockImplementation(callback => (loadCallback = callback));
193 |
194 | const loadedSpy = jest.fn();
195 | service.loadChartPackages(chart).subscribe(() => loadedSpy());
196 |
197 | expect(loadedSpy).not.toHaveBeenCalled();
198 |
199 | expect(loadCallback!).toBeTruthy();
200 | loadCallback!();
201 | expect(loadedSpy).toHaveBeenCalled();
202 | });
203 |
204 | it('should use injected config values', () => {
205 | globalThis.google = { charts: chartsMock } as any;
206 |
207 | TestBed.resetTestingModule();
208 |
209 | const version = 'current';
210 | const mapsApiKey = 'mapsApiKey';
211 | const safeMode = true;
212 | const locale = 'de-DE';
213 |
214 | TestBed.configureTestingModule({
215 | providers: [
216 | ScriptLoaderService,
217 | { provide: LOCALE_ID, useValue: locale },
218 | { provide: GOOGLE_CHARTS_CONFIG, useValue: { version, mapsApiKey, safeMode } }
219 | ]
220 | });
221 | service = TestBed.inject(ScriptLoaderService);
222 |
223 | const chart = 'corechart';
224 |
225 | service.loadChartPackages(chart).subscribe();
226 |
227 | expect(chartsMock.load).toHaveBeenCalledWith(version, {
228 | packages: [chart],
229 | language: locale,
230 | mapsApiKey,
231 | safeMode
232 | });
233 | });
234 |
235 | it('should use injected lazy config values', () => {
236 | globalThis.google = { charts: chartsMock } as any;
237 |
238 | TestBed.resetTestingModule();
239 |
240 | const mapsApiKey = 'mapsApiKey';
241 | const safeMode = false;
242 | const locale = 'en-US';
243 | const lazyConfigSubject = new Subject();
244 |
245 | TestBed.configureTestingModule({
246 | providers: [
247 | ScriptLoaderService,
248 | { provide: LOCALE_ID, useValue: locale },
249 | { provide: GOOGLE_CHARTS_LAZY_CONFIG, useValue: lazyConfigSubject.asObservable() }
250 | ]
251 | });
252 | service = TestBed.inject(ScriptLoaderService);
253 |
254 | const chart = 'corechart';
255 |
256 | service.loadChartPackages(chart).subscribe();
257 |
258 | expect(chartsMock.load).not.toHaveBeenCalled();
259 |
260 | lazyConfigSubject.next({ mapsApiKey, safeMode });
261 |
262 | expect(chartsMock.load).toHaveBeenCalledWith(getDefaultConfig().version, {
263 | packages: [chart],
264 | language: locale,
265 | mapsApiKey,
266 | safeMode
267 | });
268 | });
269 | });
270 | });
271 | });
272 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/services/script-loader.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core';
2 | import { Observable, of, Subject } from 'rxjs';
3 | import { map, mergeMap, switchMap } from 'rxjs/operators';
4 |
5 | import { getDefaultConfig } from '../helpers/chart.helper';
6 | import { GoogleChartsConfig, GOOGLE_CHARTS_LAZY_CONFIG } from '../types/google-charts-config';
7 |
8 | @Injectable()
9 | export class ScriptLoaderService {
10 | private readonly scriptSource = 'https://www.gstatic.com/charts/loader.js';
11 | private readonly scriptLoadSubject = new Subject();
12 |
13 | constructor(
14 | private zone: NgZone,
15 | @Inject(LOCALE_ID) private localeId: string,
16 | @Inject(GOOGLE_CHARTS_LAZY_CONFIG) private readonly config$: Observable
17 | ) {}
18 |
19 | /**
20 | * Checks whether `google.charts` is available.
21 | *
22 | * If not, it can be loaded by calling `loadChartPackages`.
23 | *
24 | * @returns `true` if `google.charts` is available, `false` otherwise.
25 | */
26 | public isGoogleChartsAvailable(): boolean {
27 | if (typeof google === 'undefined' || typeof google.charts === 'undefined') {
28 | return false;
29 | }
30 |
31 | return true;
32 | }
33 |
34 | /**
35 | * Loads the Google Chart script and the provided chart packages.
36 | * Can be called multiple times to load more packages.
37 | *
38 | * When called without any arguments, this will just load the default package
39 | * containing the namespaces `google.charts` and `google.visualization` without any charts.
40 | *
41 | * @param packages The packages to load.
42 | * @returns A stream emitting as soon as the chart packages are loaded.
43 | */
44 | public loadChartPackages(...packages: string[]): Observable {
45 | return this.loadGoogleCharts().pipe(
46 | mergeMap(() => this.config$),
47 | map(config => {
48 | return { ...getDefaultConfig(), ...(config || {}) };
49 | }),
50 | switchMap((googleChartsConfig: GoogleChartsConfig) => {
51 | return new Observable(observer => {
52 | const config = {
53 | packages,
54 | language: this.localeId,
55 | mapsApiKey: googleChartsConfig.mapsApiKey,
56 | safeMode: googleChartsConfig.safeMode
57 | };
58 |
59 | google.charts.load(googleChartsConfig.version!, config);
60 | google.charts.setOnLoadCallback(() => {
61 | this.zone.run(() => {
62 | observer.next();
63 | observer.complete();
64 | });
65 | });
66 | });
67 | })
68 | );
69 | }
70 |
71 | /**
72 | * Loads the Google Charts script. After the script is loaded, `google.charts` is defined.
73 | *
74 | * @returns A stream emitting as soon as loading has completed.
75 | * If the google charts script is already loaded, the stream emits immediately.
76 | */
77 | private loadGoogleCharts(): Observable {
78 | if (this.isGoogleChartsAvailable()) {
79 | return of(undefined);
80 | } else if (!this.isLoadingGoogleCharts()) {
81 | const script = this.createGoogleChartsScript();
82 | script.onload = () => {
83 | this.zone.run(() => {
84 | this.scriptLoadSubject.next();
85 | this.scriptLoadSubject.complete();
86 | });
87 | };
88 |
89 | script.onerror = () => {
90 | this.zone.run(() => {
91 | console.error('Failed to load the google charts script!');
92 | this.scriptLoadSubject.error(new Error('Failed to load the google charts script!'));
93 | });
94 | };
95 | }
96 |
97 | return this.scriptLoadSubject.asObservable();
98 | }
99 |
100 | private isLoadingGoogleCharts() {
101 | return this.getGoogleChartsScript() != null;
102 | }
103 |
104 | private getGoogleChartsScript(): HTMLScriptElement | undefined {
105 | const pageScripts = Array.from(document.getElementsByTagName('script'));
106 | return pageScripts.find(script => script.src === this.scriptSource);
107 | }
108 |
109 | private createGoogleChartsScript(): HTMLScriptElement {
110 | const script = document.createElement('script');
111 | script.type = 'text/javascript';
112 | script.src = this.scriptSource;
113 | script.async = true;
114 | document.getElementsByTagName('head')[0].appendChild(script);
115 | return script;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/types/chart-type.ts:
--------------------------------------------------------------------------------
1 | export enum ChartType {
2 | AnnotationChart = 'AnnotationChart',
3 | AreaChart = 'AreaChart',
4 | Bar = 'Bar',
5 | BarChart = 'BarChart',
6 | BubbleChart = 'BubbleChart',
7 | Calendar = 'Calendar',
8 | CandlestickChart = 'CandlestickChart',
9 | ColumnChart = 'ColumnChart',
10 | ComboChart = 'ComboChart',
11 | PieChart = 'PieChart',
12 | Gantt = 'Gantt',
13 | Gauge = 'Gauge',
14 | GeoChart = 'GeoChart',
15 | Histogram = 'Histogram',
16 | Line = 'Line',
17 | LineChart = 'LineChart',
18 | Map = 'Map',
19 | OrgChart = 'OrgChart',
20 | Sankey = 'Sankey',
21 | Scatter = 'Scatter',
22 | ScatterChart = 'ScatterChart',
23 | SteppedAreaChart = 'SteppedAreaChart',
24 | Table = 'Table',
25 | Timeline = 'Timeline',
26 | TreeMap = 'TreeMap',
27 | WordTree = 'WordTree'
28 | }
29 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/types/control-type.ts:
--------------------------------------------------------------------------------
1 | export enum FilterType {
2 | Category = 'CategoryFilter',
3 | ChartRange = 'ChartRangeFilter',
4 | DateRange = 'DateRangeFilter',
5 | NumberRange = 'NumberRangeFilter',
6 | String = 'StringFilter'
7 | }
8 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/types/events.ts:
--------------------------------------------------------------------------------
1 | export interface ChartReadyEvent {
2 | /**
3 | * The newly instantiated chart.
4 | */
5 | chart: T;
6 | }
7 |
8 | export interface ChartSelectionChangedEvent {
9 | /**
10 | * An array of objects describing the selected data elements.
11 | *
12 | * These objects have the properties `row` and `column`.
13 | * `row` and `column` are the row and column indexes of the selected item in the charts' data.
14 | *
15 | * If both `row` and `column` are specified, the selected element is a cell.
16 | * If only `row` is specified, the selected element is a row.
17 | * If only `column` is specified, the selected element is a column.
18 | */
19 | selection: google.visualization.ChartSelection[];
20 | }
21 |
22 | export interface ChartMouseOverEvent {
23 | /**
24 | * The index of the column of the hovered item in the chart data.
25 | */
26 | column: number;
27 |
28 | /**
29 | * The index of the row of the hovered item in the chart data.
30 | */
31 | row: number;
32 | }
33 |
34 | export interface ChartMouseLeaveEvent {
35 | column: number;
36 | row: number;
37 | }
38 |
39 | export interface ChartErrorEvent {
40 | /**
41 | * The ID of the DOM element containing the chart, or
42 | * an error message displayed instead of the chart if it cannot be rendered.
43 | */
44 | id: string;
45 |
46 | /**
47 | * A short message string describing the error.
48 | */
49 | message: string;
50 |
51 | /**
52 | * A detailed explanation of the error.
53 | */
54 | detailedMessage?: string;
55 |
56 | /**
57 | * An object containing custom parameters appropriate to this error and chart type.
58 | */
59 | options?: object;
60 | }
61 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/types/formatter.ts:
--------------------------------------------------------------------------------
1 | export interface Formatter {
2 | formatter: google.visualization.DefaultFormatter;
3 | colIndex: number;
4 | }
5 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/src/lib/types/google-charts-config.ts:
--------------------------------------------------------------------------------
1 | import { inject, InjectFlags, InjectionToken } from '@angular/core';
2 | import { Observable, of } from 'rxjs';
3 |
4 | import { getDefaultConfig } from '../helpers/chart.helper';
5 |
6 | export interface GoogleChartsConfig {
7 | /**
8 | * This setting lets you specify a key that you may use with Geochart and Map Chart.
9 | * You may want to do this rather than using the default behavior which may result in
10 | * occasional throttling of service for your users.
11 | *
12 | * Only available when using Google Charts 45 or higher.
13 | *
14 | * {@link https://developers.google.com/chart/interactive/docs/basic_load_libs#load-settings Parameter documentation }
15 | * {@link https://developers.google.com/chart/interactive/docs/gallery/geochart GeoChart Documentation}
16 | */
17 | mapsApiKey?: string;
18 |
19 | /**
20 | * Which version of Google Charts to use.
21 | *
22 | * Please note that this library does only work with Google Charts 45 or higher.
23 | *
24 | * @description
25 | * Can be either a number specifying a
26 | * {@link https://developers.google.com/chart/interactive/docs/release_notes#current:-january-6,-2020 frozen version } of Google Charts
27 | * or one of the special versions `current` and `upcoming`.
28 | *
29 | * Defaults to `current`.
30 | *
31 | * {@link https://developers.google.com/chart/interactive/docs/basic_load_libs#basic-library-loading Offical Documentation}
32 | */
33 | version?: string;
34 |
35 | /**
36 | * When set to true, all charts and tooltips that generate HTML from user-supplied data will sanitize it
37 | * by stripping out unsafe elements and attributes.
38 | *
39 | * Only available when using GoogleCharts 47 or higher.
40 | *
41 | * {@link https://developers.google.com/chart/interactive/docs/basic_load_libs#load-settings Parameter documentation }
42 | */
43 | safeMode?: boolean;
44 | }
45 |
46 | export const GOOGLE_CHARTS_CONFIG = new InjectionToken>('GOOGLE_CHARTS_CONFIG');
47 | export const GOOGLE_CHARTS_LAZY_CONFIG = new InjectionToken>(
48 | 'GOOGLE_CHARTS_LAZY_CONFIG',
49 | {
50 | providedIn: 'root',
51 | factory: () => {
52 | const configFromModule = inject(GOOGLE_CHARTS_CONFIG, InjectFlags.Optional);
53 | return of({ ...getDefaultConfig(), ...(configFromModule || {}) });
54 | }
55 | }
56 | );
57 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/lib",
5 | "declarationMap": true,
6 | "module": "es2015",
7 | "moduleResolution": "node",
8 | "declaration": true,
9 | "sourceMap": true,
10 | "inlineSources": true,
11 | "emitDecoratorMetadata": true,
12 | "experimentalDecorators": true,
13 | "importHelpers": true,
14 | "types": ["google.visualization"],
15 | "lib": ["dom", "es2015"]
16 | },
17 | "angularCompilerOptions": {
18 | "skipTemplateCodegen": true,
19 | "strictMetadataEmit": true,
20 | "fullTemplateTypeCheck": true,
21 | "strictInjectionParameters": true,
22 | "flatModuleId": "AUTOGENERATED",
23 | "flatModuleOutFile": "AUTOGENERATED"
24 | },
25 | "exclude": ["src/setup-tests.ts", "**/*.spec.ts"]
26 | }
27 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.lib.json",
3 | "compilerOptions": {
4 | "declarationMap": false
5 | },
6 | "angularCompilerOptions": {
7 | "compilationMode": "partial"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/spec",
5 | "types": ["jest", "node", "google.visualization"]
6 | },
7 | "include": ["**/*.spec.ts", "**/*.d.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/projects/angular-google-charts/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json",
3 | "rules": {
4 | "directive-selector": [true, "attribute", "camelCase"],
5 | "component-selector": [true, "element", "kebab-case"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/projects/playground/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https:// github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | > 0.5%
9 | last 2 versions
10 | Firefox ESR
11 | not dead
12 | not IE 9-11 # For IE 9-11 support, remove 'not'.
--------------------------------------------------------------------------------
/projects/playground/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 |
4 | import { MainComponent } from './main/main.component';
5 | import { TestComponent } from './test/test.component';
6 |
7 | const routes: Routes = [
8 | { path: 'app', component: MainComponent },
9 | { path: 'test', component: TestComponent },
10 | { path: '', redirectTo: '/app', pathMatch: 'full' }
11 | ];
12 |
13 | @NgModule({
14 | imports: [RouterModule.forRoot(routes)],
15 | exports: [RouterModule]
16 | })
17 | export class AppRoutingModule {}
18 |
--------------------------------------------------------------------------------
/projects/playground/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | template: ''
6 | })
7 | export class AppComponent {}
8 |
--------------------------------------------------------------------------------
/projects/playground/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { GoogleChartsModule } from 'angular-google-charts';
4 |
5 | import { AppRoutingModule } from './app-routing.module';
6 | import { AppComponent } from './app.component';
7 | import { MainComponent } from './main/main.component';
8 | import { TestComponent } from './test/test.component';
9 |
10 | @NgModule({
11 | declarations: [AppComponent, TestComponent, MainComponent],
12 | imports: [
13 | BrowserModule,
14 | AppRoutingModule,
15 | GoogleChartsModule.forRoot({ mapsApiKey: 'AIzaSyD-9tSrke72PouQMnMX-a7eZSW0jkFMBWY' })
16 | ],
17 | bootstrap: [AppComponent]
18 | })
19 | export class AppModule {}
20 |
--------------------------------------------------------------------------------
/projects/playground/src/app/main/main.component.html:
--------------------------------------------------------------------------------
1 | Angular Google-Charts Demo
2 |
3 |
16 |
17 |
18 |
19 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/projects/playground/src/app/main/main.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, ViewChild } from '@angular/core';
2 | import { Router } from '@angular/router';
3 | import {
4 | ChartErrorEvent,
5 | ChartMouseLeaveEvent,
6 | ChartMouseOverEvent,
7 | ChartSelectionChangedEvent,
8 | ChartType,
9 | Column,
10 | GoogleChartComponent
11 | } from 'angular-google-charts';
12 |
13 | @Component({
14 | selector: 'app-main',
15 | templateUrl: './main.component.html',
16 | styles: [':host > *:not(h1) { display: inline-block !important; }']
17 | })
18 | export class MainComponent implements OnInit {
19 | public charts: {
20 | title: string;
21 | type: ChartType;
22 | data: any[][];
23 | columns?: Column[];
24 | options?: {};
25 | }[] = [];
26 |
27 | public changingChart = {
28 | title: 'Changing Chart',
29 | type: ChartType.BarChart,
30 | data: [
31 | ['Copper', 8.94],
32 | ['Silver', 10.49],
33 | ['Gold', 19.3],
34 | ['Platinum', 21.45]
35 | ],
36 | columns: ['Element', 'Density'],
37 | options: {
38 | animation: {
39 | duration: 250,
40 | easing: 'ease-in-out',
41 | startup: true
42 | }
43 | }
44 | };
45 |
46 | @ViewChild('chart', { static: true })
47 | public chart!: GoogleChartComponent;
48 |
49 | constructor(private router: Router) {
50 | this.charts.push({
51 | title: 'Pie Chart',
52 | type: ChartType.PieChart,
53 | columns: ['Task', 'Hours per Day'],
54 | data: [
55 | ['Work', 11],
56 | ['Eat', 2],
57 | ['Commute', 2],
58 | ['Watch TV', 2],
59 | ['Sleep', 7]
60 | ]
61 | });
62 |
63 | this.charts.push({
64 | title: 'Bar Chart',
65 | type: ChartType.BarChart,
66 | columns: ['Element', 'Density', { role: 'style', type: 'string' }],
67 | data: [
68 | ['Copper', 8.94, '#b87333'],
69 | ['Silver', 10.49, 'silver'],
70 | ['Gold', 19.3, 'gold'],
71 | ['Platinum', 21.45, 'color: #e5e4e2']
72 | ]
73 | });
74 |
75 | this.charts.push({
76 | title: 'Bar Chart',
77 | type: ChartType.BarChart,
78 | columns: [
79 | 'City',
80 | '2010 Population',
81 | { role: 'annotation', type: 'string' },
82 | '2000 Population',
83 | { role: 'annotation', type: 'string' }
84 | ],
85 | data: [
86 | ['New York City, NY', 8175000, '8.1M', 8008000, '8M'],
87 | ['Los Angeles, CA', 3792000, '3.8M', 3694000, '3.7M'],
88 | ['Chicago, IL', 2695000, '2.7M', 2896000, '2.9M'],
89 | ['Houston, TX', 2099000, '2.1M', 1953000, '2.0M'],
90 | ['Philadelphia, PA', 1526000, '1.5M', 1517000, '1.5M']
91 | ],
92 | options: {
93 | annotations: {
94 | alwaysOutside: true,
95 | textStyle: {
96 | fontSize: 12,
97 | auraColor: 'none',
98 | color: '#555'
99 | },
100 | boxStyle: {
101 | stroke: '#ccc',
102 | strokeWidth: 1,
103 | gradient: {
104 | color1: '#f3e5f5',
105 | color2: '#f3e5f5',
106 | x1: '0%',
107 | y1: '0%',
108 | x2: '100%',
109 | y2: '100%'
110 | }
111 | }
112 | },
113 | hAxis: {
114 | title: 'Total Population',
115 | minValue: 0
116 | },
117 | vAxis: {
118 | title: 'City'
119 | }
120 | }
121 | });
122 |
123 | this.charts.push({
124 | title: 'Styled Line Chart',
125 | type: ChartType.LineChart,
126 | columns: [
127 | 'Element',
128 | 'Density',
129 | { type: 'number', role: 'interval' },
130 | { type: 'number', role: 'interval' },
131 | { type: 'string', role: 'annotation' },
132 | { type: 'string', role: 'annotationText' },
133 | { type: 'boolean', role: 'certainty' }
134 | ],
135 | data: [
136 | ['April', 1000, 900, 1100, 'A', 'Stolen data', true],
137 | ['May', 1170, 1000, 1200, 'B', 'Coffee spill', true],
138 | ['June', 660, 550, 800, 'C', 'Wumpus attack', true],
139 | ['July', 1030, null, null, null, null, false]
140 | ]
141 | });
142 |
143 | this.charts.push({
144 | title: 'Material Bar Chart',
145 | type: ChartType.Bar,
146 | columns: ['Year', 'Sales', 'Expenses', 'Profit'],
147 | data: [
148 | ['2014', 1000, 400, 200],
149 | ['2015', 1170, 460, 250],
150 | ['2016', 660, 1120, 300],
151 | ['2017', 1030, 540, 350]
152 | ],
153 | options: {
154 | chart: {
155 | title: 'Material Bar Chart',
156 | subtitle: 'Sales, Expenses, and Profit: 2014-2017'
157 | },
158 | bars: 'horizontal' // Required for Material Bar Charts.
159 | }
160 | });
161 |
162 | this.charts.push({
163 | title: 'Area Chart',
164 | type: ChartType.AreaChart,
165 | columns: ['Year', 'Sales', 'Expenses'],
166 | data: [
167 | ['2013', 1000, 400],
168 | ['2014', 1170, 460],
169 | ['2015', 660, 1120],
170 | ['2016', 1030, 540]
171 | ]
172 | });
173 |
174 | this.charts.push({
175 | title: 'Bubble Chart',
176 | type: ChartType.BubbleChart,
177 | columns: ['ID', 'X', 'Y'],
178 | data: [
179 | ['Hallo', 80, 167],
180 | ['', 79, 136],
181 | ['', 78, 184],
182 | ['', 72, 278],
183 | ['', 81, 200],
184 | ['', 72, 170],
185 | ['', 68, 477]
186 | ]
187 | });
188 |
189 | this.charts.push({
190 | title: 'Candlestick Chart',
191 | type: ChartType.CandlestickChart,
192 | columns: undefined,
193 | data: [
194 | ['Mon', 20, 28, 38, 45],
195 | ['Tue', 31, 38, 55, 66],
196 | ['Wed', 50, 55, 77, 80],
197 | ['Thu', 77, 77, 66, 50],
198 | ['Fri', 68, 66, 22, 15]
199 | ]
200 | });
201 |
202 | this.charts.push({
203 | title: 'Combo Chart',
204 | type: ChartType.ComboChart,
205 | columns: ['Month', 'Bolivia', 'Ecuador', 'Madagascar', 'Papua New Guinea', 'Rwanda', 'Average'],
206 | data: [
207 | ['2004/05', 165, 938, 522, 998, 450, 614.6],
208 | ['2005/06', 135, 1120, 599, 1268, 288, 682],
209 | ['2006/07', 157, 1167, 587, 807, 397, 623],
210 | ['2007/08', 139, 1110, 615, 968, 215, 609.4],
211 | ['2008/09', 136, 691, 629, 1026, 366, 569.6]
212 | ],
213 | options: {
214 | vAxis: { title: 'Cups' },
215 | hAxis: { title: 'Month' },
216 | seriesType: 'bars',
217 | series: { 5: { type: 'line' } }
218 | }
219 | });
220 |
221 | this.charts.push({
222 | title: 'Histogram',
223 | type: ChartType.Histogram,
224 | columns: ['Dinosaur', 'Length'],
225 | data: [
226 | ['Acrocanthosaurus (top-spined lizard)', 12.2],
227 | ['Albertosaurus (Alberta lizard)', 9.1],
228 | ['Allosaurus (other lizard)', 12.2],
229 | ['Apatosaurus (deceptive lizard)', 22.9],
230 | ['Archaeopteryx (ancient wing)', 0.9],
231 | ['Argentinosaurus (Argentina lizard)', 36.6],
232 | ['Baryonyx (heavy claws)', 9.1],
233 | ['Brachiosaurus (arm lizard)', 30.5],
234 | ['Ceratosaurus (horned lizard)', 6.1],
235 | ['Coelophysis (hollow form)', 2.7],
236 | ['Compsognathus (elegant jaw)', 0.9],
237 | ['Deinonychus (terrible claw)', 2.7],
238 | ['Diplodocus (double beam)', 27.1],
239 | ['Dromicelomimus (emu mimic)', 3.4],
240 | ['Gallimimus (fowl mimic)', 5.5],
241 | ['Mamenchisaurus (Mamenchi lizard)', 21.0],
242 | ['Megalosaurus (big lizard)', 7.9],
243 | ['Microvenator (small hunter)', 1.2],
244 | ['Ornithomimus (bird mimic)', 4.6],
245 | ['Oviraptor (egg robber)', 1.5],
246 | ['Plateosaurus (flat lizard)', 7.9],
247 | ['Sauronithoides (narrow-clawed lizard)', 2.0],
248 | ['Seismosaurus (tremor lizard)', 45.7],
249 | ['Spinosaurus (spiny lizard)', 12.2],
250 | ['Supersaurus (super lizard)', 30.5],
251 | ['Tyrannosaurus (tyrant lizard)', 15.2],
252 | ['Ultrasaurus (ultra lizard)', 30.5],
253 | ['Velociraptor (swift robber)', 1.8]
254 | ]
255 | });
256 |
257 | this.charts.push({
258 | title: 'Scatter Chart',
259 | type: ChartType.ScatterChart,
260 | columns: ['Age', 'Weight'],
261 | data: [
262 | [8, 12],
263 | [4, 5.5],
264 | [11, 14],
265 | [4, 5],
266 | [3, 3.5],
267 | [6.5, 7]
268 | ],
269 | options: {
270 | explorer: {
271 | actions: ['dragToZoom', 'rightClickToReset'],
272 | keepInBounds: true,
273 | maxZoomIn: 4,
274 | zoomDelta: 1
275 | }
276 | }
277 | });
278 |
279 | this.charts.push({
280 | title: 'WordTree',
281 | type: ChartType.WordTree,
282 | // type: 'WordTree' as ChartType,
283 | columns: ['Phrases'],
284 | data: [['This is a test'], ['This is not a test'], ['This is an actual test']],
285 | options: {
286 | wordtree: {
287 | format: 'implicit',
288 | type: 'double',
289 | word: 'test'
290 | }
291 | }
292 | });
293 | }
294 |
295 | public onReady() {
296 | console.log('Chart ready');
297 | }
298 |
299 | public onError(error: ChartErrorEvent) {
300 | console.error('Error: ' + error.message.toString());
301 | }
302 |
303 | public onSelect(event: ChartSelectionChangedEvent) {
304 | console.log('Selected: ' + event.toString());
305 | }
306 |
307 | public onMouseEnter(event: ChartMouseOverEvent) {
308 | console.log('Hovering ' + event.toString());
309 | }
310 |
311 | public onMouseLeave(event: ChartMouseLeaveEvent) {
312 | console.log('No longer hovering ' + event.toString());
313 | }
314 |
315 | public ngOnInit() {
316 | console.log(this.chart);
317 | }
318 |
319 | public changeChart() {
320 | this.changingChart.data = [
321 | ['Copper', Math.random() * 20.0],
322 | ['Silver', Math.random() * 20.0],
323 | ['Gold', Math.random() * 20.0],
324 | ['Platinum', Math.random() * 20.0]
325 | ];
326 | }
327 |
328 | public navigateToTest() {
329 | this.router.navigateByUrl('/test');
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/projects/playground/src/app/test/test.component.html:
--------------------------------------------------------------------------------
1 | Test Component
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
37 |
38 |
--------------------------------------------------------------------------------
/projects/playground/src/app/test/test.component.ts:
--------------------------------------------------------------------------------
1 | import { Location } from '@angular/common';
2 | import { Component, ViewChild } from '@angular/core';
3 | import {
4 | ChartBase,
5 | ChartEditorComponent,
6 | ChartType,
7 | FilterType,
8 | Formatter,
9 | ScriptLoaderService
10 | } from 'angular-google-charts';
11 | import { Observable } from 'rxjs';
12 | import { map, share } from 'rxjs/operators';
13 |
14 | @Component({
15 | selector: 'app-test',
16 | templateUrl: './test.component.html',
17 | styles: ['.inline > * { display: inline-block; vertical-align: top; }']
18 | })
19 | export class TestComponent {
20 | public chart = {
21 | title: 'Test Chart',
22 | type: ChartType.BarChart,
23 | data: [
24 | ['Copper', 8.94],
25 | ['Silver', 10.49],
26 | ['Gold', 19.3],
27 | ['Platinum', 21.45]
28 | ],
29 | columnNames: ['Element', 'Density'],
30 | options: {
31 | animation: {
32 | duration: 250,
33 | easing: 'ease-in-out',
34 | startup: true
35 | }
36 | }
37 | };
38 |
39 | public dashboardData = [
40 | ['Michael', 5],
41 | ['Elisa', 7],
42 | ['Robert', 3],
43 | ['John', 2],
44 | ['Jessica', 6],
45 | ['Aaron', 1],
46 | ['Margareth', 8]
47 | ];
48 | public filterType = FilterType.NumberRange;
49 |
50 | public chartWrapperSpecs: google.visualization.ChartSpecs = {
51 | chartType: ChartType.AreaChart,
52 | dataTable: [
53 | ['SMR CV', 'US Cents/KG'],
54 | [new Date(1990, 1, 1), 10],
55 | [new Date(1991, 1, 1), 20],
56 | [new Date(1992, 1, 1), 40],
57 | [new Date(1993, 1, 1), 80],
58 | [new Date(1994, 1, 1), 160],
59 | [new Date(1995, 1, 1), 320],
60 | [new Date(1996, 1, 1), 640],
61 | [new Date(1997, 1, 1), 1280]
62 | ]
63 | };
64 |
65 | public readonly formatters$: Observable = this.scriptLoaderService.loadChartPackages().pipe(
66 | share(),
67 | map(() => [
68 | { colIndex: 1, formatter: new google.visualization.NumberFormat({ fractionDigits: 0, prefix: '$', suffix: '‰' }) }
69 | ])
70 | );
71 |
72 | @ViewChild(ChartEditorComponent)
73 | public readonly editor!: ChartEditorComponent;
74 |
75 | constructor(
76 | private location: Location,
77 | private scriptLoaderService: ScriptLoaderService
78 | ) {}
79 |
80 | public edit(chart: ChartBase) {
81 | this.editor
82 | .editChart(chart)
83 | .afterClosed()
84 | .subscribe(result => {
85 | if (result) {
86 | console.log(result);
87 | } else {
88 | console.log('Editing was cancelled');
89 | }
90 | });
91 | }
92 |
93 | public goBack() {
94 | this.location.back();
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/projects/playground/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FERNman/angular-google-charts/eebda5494c9f0c6088f5fb8975e367d6b0b90ab0/projects/playground/src/assets/.gitkeep
--------------------------------------------------------------------------------
/projects/playground/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/projects/playground/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/projects/playground/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FERNman/angular-google-charts/eebda5494c9f0c6088f5fb8975e367d6b0b90ab0/projects/playground/src/favicon.ico
--------------------------------------------------------------------------------
/projects/playground/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Playground
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/projects/playground/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic()
12 | .bootstrapModule(AppModule)
13 | .catch(err => console.error(err));
14 |
--------------------------------------------------------------------------------
/projects/playground/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * By default, zone.js will patch all possible macroTask and DomEvents
23 | * user can disable parts of macroTask/DomEvents patch by setting following flags
24 | * because those flags need to be set before `zone.js` being loaded, and webpack
25 | * will put import in the top of bundle, so user need to create a separate file
26 | * in this directory (for example: zone-flags.ts), and put the following flags
27 | * into that file, and then add the following code before importing zone.js.
28 | * import './zone-flags.ts';
29 | *
30 | * The flags allowed in zone-flags.ts are listed here.
31 | *
32 | * The following flags will work for all browsers.
33 | *
34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 | *
38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
40 | *
41 | * (window as any).__Zone_enable_cross_context_check = true;
42 | *
43 | */
44 |
45 | /***************************************************************************************************
46 | * Zone JS is required by default for Angular itself.
47 | */
48 | import 'zone.js/dist/zone'; // Included with Angular CLI.
49 |
50 | /***************************************************************************************************
51 | * APPLICATION IMPORTS
52 | */
53 |
--------------------------------------------------------------------------------
/projects/playground/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/projects/playground/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/app",
5 | "types": ["google.visualization"]
6 | },
7 | "files": ["src/main.ts", "src/polyfills.ts"],
8 | "include": ["src/**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/projects/playground/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json",
3 | "rules": {
4 | "directive-selector": [true, "attribute", "app", "camelCase"],
5 | "component-selector": [true, "element", "app", "kebab-case"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "importHelpers": true,
6 | "module": "es2020",
7 | "outDir": "./dist/out-tsc",
8 | "sourceMap": true,
9 | "declaration": false,
10 | "moduleResolution": "node",
11 | "emitDecoratorMetadata": true,
12 | "experimentalDecorators": true,
13 | "strict": true,
14 | "target": "ES2022",
15 | "typeRoots": ["node_modules/@types"],
16 | "lib": ["es2017", "dom"],
17 | "noUnusedLocals": true,
18 | "paths": {
19 | "angular-google-charts": ["projects/angular-google-charts/src/index.ts"]
20 | },
21 | "useDefineForClassFields": false
22 | },
23 | "angularCompilerOptions": {
24 | "skipTemplateCodegen": true,
25 | "fullTemplateTypeCheck": true,
26 | "strictInjectionParameters": true
27 | },
28 | "exclude": ["dist/**/*"]
29 | }
30 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint-angular", "tslint-config-prettier"],
3 | "rulesDirectory": ["node_modules/codelyzer"],
4 | "rules": {
5 | "directive-selector": [true, "attribute", "camelCase"],
6 | "component-selector": [true, "element", "kebab-case"],
7 | "deprecation": {
8 | "severity": "warning"
9 | },
10 | "ordered-imports": [true, { "grouped-imports": true }],
11 | "member-ordering": [true, { "order": "fields-first" }],
12 | "member-access": true,
13 | "array-type": [true, "array"],
14 | "no-host-metadata-property": false,
15 | "unified-signatures": false,
16 | "no-non-null-assertion": false
17 | }
18 | }
19 |
--------------------------------------------------------------------------------