Compare commits

...

98 Commits

Author SHA1 Message Date
Faris Ansari
7b15424c3a
chore: simplify usage section 2024-12-12 20:36:01 +05:30
Faris Ansari
6d05a89021
Update README.md 2024-12-12 16:39:46 +05:30
Arjun Choudhary
f333096012 feat: experimental! allow overrriding yAxisLabels 2023-09-27 16:08:01 +05:30
Arjun Choudhary
3a16d6c30c fix: force deprecate unmaintained animate 2023-09-27 13:34:25 +05:30
Arjun
8f69be05c1
Merge pull request #410 from Sorizz/master
Changing README file
2023-04-19 17:50:49 +05:30
Souradip Sen
b9551c1fa8 Changing README file 2023-04-17 21:26:54 +05:30
Arjun Choudhary
34436a951d fix & chore: fix makeText(), cleaned unused imports 2022-12-26 10:59:34 +05:30
Arjun Choudhary
4dabb9d577 Merge commit '9f68f1ac5f7e8eb7a089d13b27d44fa0702d7931' 2022-12-20 13:16:17 +05:30
Arjun Choudhary
ae2ae01ed7 feat: pr 366 initial merge 2022-12-19 11:34:00 +05:30
Arjun Choudhary
0781eb015d fix: Aggregation chart data.labels 2022-12-16 15:31:58 +05:30
Arjun Choudhary
1b955a9f19 feat/fix: Fixes #404 maintaining backwards compat 2022-12-15 20:10:35 +05:30
Arjun Choudhary
256649fbcc fix: Partial fix for #404 needs more work.
Better null value handling. Add option for filling zero/null values to
maintain backwards compat.
2022-12-14 18:15:51 +05:30
Arjun Choudhary
9dce5cf5a8 chore: clone in update 2022-11-28 15:56:38 +05:30
Arjun Choudhary
2bb46a0c60 feat: Allow lineType from Y-marker 2022-11-28 14:57:18 +05:30
Arjun Choudhary
0a4a3a6530 chore: default to CSS vars in makeText() 2022-11-26 11:49:17 +05:30
Arjun Choudhary
c17e2c6c6f chore: remove font-fill 2022-11-26 11:47:23 +05:30
Arjun Choudhary
652913675c fix: vaidate y 2022-11-26 11:40:05 +05:30
Arjun Choudhary
6a853ca29d feat: Allow y-region colours 2022-11-26 11:34:31 +05:30
Arjun Choudhary
10558f8cce fix: remove duplicate round fn 2022-11-26 11:30:01 +05:30
Arjun Choudhary
26708dc26d fix: backported fp rounding fix 2022-11-25 16:03:19 +05:30
Arjun Choudhary
9805e01d34 feat: allow y marker stroke 2022-11-25 15:55:17 +05:30
Arjun Choudhary
6db78c1c9b feat: Non breaking new legends (Backported) 2022-11-25 12:11:32 +05:30
Arjun Choudhary
1ab2ca2bd4 chore: aggregate chart label changes 2022-11-25 11:19:51 +05:30
Arjun Choudhary
e3710f0e16 chore: update tooltip styles
* max-width for tooltip label
* remove ellipsis
* use normal white-space wrapping
2022-11-24 15:53:45 +05:30
Arjun Choudhary
a04dd7f433 chore: clean deprecated 2022-11-24 14:09:00 +05:30
Arjun Choudhary
f2262fac43 chore: formatting changes 2022-11-24 12:54:27 +05:30
Arjun Choudhary
d4e78ebb05 chore: show last label if isSeries 2022-11-24 12:41:26 +05:30
Arjun Choudhary
1572ce07fb chore: remove stroke 2022-11-24 12:36:06 +05:30
Shivam Mishra
0291b8dbbe feat: add space ratio option for x-axis series labels 2022-11-23 20:03:42 +05:30
Shivam Mishra
eecb6fdb3b feat: added function for rounded top rect 2022-11-23 19:58:04 +05:30
Arjun Choudhary
fe7650f7de chore: reset bar width 2022-11-23 16:00:51 +05:30
Shivam Mishra
d5c3886301 feat: added function for rounded top rect 2022-11-23 15:57:35 +05:30
Shivam Mishra
692cdfb102 chore: cleanup rounded bar utility 2022-11-23 15:29:17 +05:30
Shivam Mishra
ec8f669385 feat: remove depth for percentage chart 2022-11-23 15:21:05 +05:30
Shivam Mishra
428a447cba feat: added helper function for rounded edges 2022-11-23 14:18:00 +05:30
Arjun Choudhary
d139d15cb7 Merge 96095ec5793a021c44cfa3bfe7f44eb389316def into master 2022-11-22 14:42:21 +05:30
Arjun Choudhary
7918a8642a Merge fbe314d9f9135ceca54cf149f64d89d7d9413035 into master 2022-11-22 14:08:00 +05:30
Arjun Choudhary
baff0ec35c chore: switch style variables from scss to css 2022-11-22 12:25:15 +05:30
Arjun Choudhary
5945cea9e0 Merge 2c560b9bcc47d74c756bfa25372e64e02d934946 into master 2022-11-22 11:23:51 +05:30
Arjun Choudhary
6d38028791 Merge f7ec6fc77951cc119463f2678ce3c027a252cda7 into master 2022-11-22 11:16:37 +05:30
Arjun Choudhary
e7a13d0e6a Merge e54427004cf1a8266d6b5b5a16bcfdbcee8bac43 into master 2022-11-22 11:15:24 +05:30
Arjun Choudhary
4aef8bf081 Merge 23bdf8a719bc6c444a7f1a9d8fcbaad346eaddb9 into master 2022-11-22 11:13:55 +05:30
Arjun Choudhary
f2b37dceef Merge 79157c318f5456fbbc3b4d7f4ad3f08c4c69d94f into master 2022-11-22 11:10:38 +05:30
Arjun Choudhary
eb1dc5d501 Merge 09c3186813da5a4e0a2a44d5124349256b4d5800 into master 2022-11-22 11:09:00 +05:30
Arjun Choudhary
aefc788932 Merge 3ec31560e9f3851ef6469b4244b643664f872bc7 into master 2022-11-22 11:00:51 +05:30
Arjun Choudhary
ac68baed9a Merge 479752c05f38de3d61fb7e934d89194b883a1647 into master 2022-11-22 10:32:53 +05:30
Arjun
030e674a0a
Merge pull request #385 from lxhyl/master
feat: pie chart add select event
2022-11-21 12:16:13 +05:30
Arjun
79a9424be5
Merge pull request #403 from uhrjun/master
Updated dependencies and build refactor
2022-11-21 11:43:46 +05:30
Arjun Choudhary
bf068ec6ea chore: fixed build rewrite (frontported)
The fix is naming the umd module frappe instead of frappe charts
2022-11-19 11:41:05 +05:30
Arjun Choudhary
62683ea32d chore: Updated & cleaned build dependencies 2022-11-19 11:38:54 +05:30
Arjun Choudhary
92c31af629 fix: reverts 12533f3b376328a8437109977910c8f2041572e5
fixes: #371 and #396
2022-11-18 15:55:01 +05:30
Arjun Choudhary
7b5af70d80 fix: showLegend and truncateLegend 2022-11-15 17:37:56 +05:30
Arjun Choudhary
ddbd30ca07 chore: formatting changes 2022-11-15 17:33:19 +05:30
Arjun
a839277732
Merge pull request #402 from uhrjun/master
Simple build fix
2022-11-15 14:04:43 +05:30
Arjun Choudhary
56d794470b fix: rollup scss fs.writeFile err handling 2022-11-15 13:33:52 +05:30
Arjun Choudhary
fca5173948 chore: formatting changes 2022-11-15 13:29:13 +05:30
lxhyl
b87061981c feat: pie chart add select event 2022-06-09 23:58:55 +08:00
Saqib Ansari
7adc904b08
chore: bump to v1.6.3 2022-04-27 16:09:50 +05:30
Rohan
d19f03ac99
fix: ignore local timezones when creating a heatmap (#377)
* [Heatmap] Adjusting grid calculation for consistent grid layout

Date math is hard!

This addresses a problem in how the heatmap lays calculates days to layout the grid. The calculations were being thrown off in some scenarios by a combination of factors including timezone offsets, time of day, and daylight savings time.

This patch should correct all this by forcing each day to be midnight UTC when laying out the grid.

Co-authored-by: Michael Bester <michael@kimili.com>
Co-authored-by: t47io <t47@alumni.stanford.edu>
Co-authored-by: Saqib Ansari <nextchamp.saqib@gmail.com>
2022-02-23 10:18:19 +05:30
Gabriel
f6e49097a0
chore: update README.md (#370) 2022-02-21 15:20:57 +05:30
Kaleb White
9f68f1ac5f Bug fix where chart labels collided with y-axis labels. 2021-11-27 18:12:40 -08:00
Kaleb White
25207622ab Adds span/tick support 2021-11-15 17:39:29 -08:00
Kaleb White
31c8c7d008 Fixes positioning of labels. 2021-11-15 17:14:06 -08:00
Kaleb White
ec86f7c39f uglyfying 2021-11-15 14:46:31 -08:00
Kaleb White
c8727014c6 Fixes issue if yAxis returns different lengths 2021-11-15 14:34:21 -08:00
Kaleb White
b4f3e77acb Bug fixes and working with wrong yAxis data 2021-11-15 10:28:30 -08:00
Kaleb White
5034c7a954 Fixing label support. 2021-11-12 19:02:04 -08:00
Kaleb White
a5a5fc051b undoing changes to draw file. 2021-11-12 18:58:41 -08:00
Kaleb White
539bc50883 Feat: Adding multi y-Axis support and configurable labes 2021-11-12 18:57:02 -08:00
David Schnurr
10de973608
fix intervals (#352) 2021-10-04 19:40:13 +05:30
小华
5a4857d6a8
chore: update readme usage code (#346) 2021-06-29 11:10:23 +05:30
Shivam Mishra
3e63b1a2b4 chore: bump to 1.6.2 2021-06-16 11:00:59 +00:00
T3cH_W1z4rD
67030024d4
Fixed XSS (#339)
* Fixed XSS

* Removed html file and Loop
2021-05-03 19:15:46 +05:30
Shivam Mishra
17aec6de95 chore: bump to 1.6.1 2021-04-18 10:42:15 +00:00
Shivam Mishra
12533f3b37 feat: add resize observer 2021-04-18 10:26:00 +00:00
Shivam Mishra
10b1010d06 chore: ignore docs assets 2021-04-11 10:42:01 +00:00
Shivam Mishra
2f459bc345 chore: update readme links 2021-04-11 10:40:05 +00:00
Shivam Mishra
d89bdb7571 chore: update readme 2021-04-11 10:38:30 +00:00
Shivam Mishra
0832c02184 chore: bump to 1.6.0 2021-04-11 10:28:50 +00:00
Shivam Mishra
c3ee7f306c chore: remove dist from git 2021-04-11 10:28:18 +00:00
Shivam Mishra
51a07a12c6 feat: ignore dist 2021-04-11 10:27:41 +00:00
Dave Lunny
8e1bd6859a
feat: use UMD instead of IIFE for browser target (#336) 2021-04-11 15:53:19 +05:30
dependabot[bot]
d075c76ee2
chore(deps): bump lodash from 4.17.11 to 4.17.21 (#335)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-12 00:26:46 +05:30
dependabot[bot]
3431257129
chore(deps): bump yargs-parser from 5.0.0 to 5.0.1 (#333)
Bumps [yargs-parser](https://github.com/yargs/yargs-parser) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/yargs/yargs-parser/releases)
- [Changelog](https://github.com/yargs/yargs-parser/blob/v5.0.1/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs-parser/compare/v5.0.0...v5.0.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-12 00:26:18 +05:30
dependabot[bot]
9fae7c62ad
chore(deps): bump dot-prop from 4.2.0 to 4.2.1 (#332)
Bumps [dot-prop](https://github.com/sindresorhus/dot-prop) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/sindresorhus/dot-prop/releases)
- [Commits](https://github.com/sindresorhus/dot-prop/compare/v4.2.0...v4.2.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-12 00:25:55 +05:30
Shivam Mishra
40ed23b102 chore: update action 2021-03-11 12:07:19 +00:00
Shivam Mishra
ce477800db
feat: add npm publish action 2021-03-11 17:31:48 +05:30
Shivam Mishra
75038eeefb chore: bump to 1.5.8 2021-03-11 11:58:27 +00:00
Raí Toffoletto
dadecb296e
Fix ReactJS TypeError, Issue #323 (#325)
* Fix ReactJS TypeError, Issue #323

* style: space after `if`

* style: space after `if`

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2021-03-10 13:14:49 +05:30
Shivam Mishra
8bd53191b0
Merge pull request #331 from frappe/dependabot/npm_and_yarn/handlebars-4.7.7
chore(deps): bump handlebars from 4.1.2 to 4.7.7
2021-03-10 13:12:45 +05:30
dependabot[bot]
d073e14467
chore(deps): bump handlebars from 4.1.2 to 4.7.7
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.1.2 to 4.7.7.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.1.2...v4.7.7)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-10 07:42:12 +00:00
Shivam Mishra
85d7e609c8
Merge pull request #330 from helge79/master
Render legend after update, fixes #329
2021-03-10 13:11:56 +05:30
Frank Felgner
7f26c18ce7 render legend after updating data 2021-03-08 14:08:14 +01:00
Frank Felgner
4a9048f59f fix missing semicolon 2021-03-08 14:07:54 +01:00
Shivam Mishra
684ecdedc2 chore: bump to 1.5.8 2021-02-22 03:25:14 +00:00
Shivam Mishra
d357370b6a feat: clone options before building 2021-02-18 10:52:02 +00:00
Shivam Mishra
d034192373 chore: bump to 1.5.6 2020-12-22 12:22:21 +05:30
Shivam Mishra
a22db61492 fix: name.replace exception 2020-12-22 12:21:40 +05:30
48 changed files with 30589 additions and 16084 deletions

13
.babelrc Normal file
View File

@ -0,0 +1,13 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
},
"modules": false
}
]
]
}

View File

@ -1,33 +1,24 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"parserOptions": {
"sourceType": "module"
},
"rules": {
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"semi": [
"error",
"always"
],
"no-console": [
"error",
{
"allow": ["warn", "error"]
}
]
},
"globals": {
"ENV": true
}
}
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"parserOptions": {
"sourceType": "module"
},
"rules": {
"indent": ["error", "tab"],
"linebreak-style": ["error", "unix"],
"semi": ["error", "always"],
"no-console": [
"error",
{
"allow": ["warn", "error"]
}
]
},
"globals": {
"ENV": true
}
}

33
.github/workflows/npm-publish.yml vendored Normal file
View File

@ -0,0 +1,33 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
name: Node.js Package
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: npm ci
- run: npm build
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}}

7
.gitignore vendored
View File

@ -60,4 +60,9 @@ typings/
# next.js build output
.next
.DS_Store
# npm build output
dist
docs
docs/assets/
.DS_Store

View File

@ -11,4 +11,4 @@ script:
- make test
after_success:
- make coveralls
- make coveralls

174
README.md
View File

@ -1,69 +1,62 @@
<div align="center">
<img src="https://github.com/frappe/design/blob/master/logos/logo-2019/frappe-charts-logo.png" height="128">
<a href="https://frappe.github.io/charts">
<h2>Frappe Charts</h2>
<div align="center" markdown="1">
<img width="80" alt="charts-logo" src="https://github.com/user-attachments/assets/37b7ffaf-8354-48f2-8b9c-fa04fae0135b" />
# Frappe Charts
**GitHub-inspired modern, intuitive and responsive charts with zero dependencies**
<p align="center">
<a href="https://bundlephobia.com/result?p=frappe-charts">
<img src="https://img.shields.io/bundlephobia/minzip/frappe-charts">
</a>
<p align="center">
<p>GitHub-inspired modern, intuitive and responsive charts with zero dependencies</p>
<a href="https://frappe.github.io/charts">
<b>Explore Demos » </b>
</a>
<a href="https://codepen.io/pratu16x7/pen/wjKBoq">
<b> Edit at CodePen »</b>
</a>
</p>
</p>
<img src=".github/example.gif">
<div>
[Explore Demos](https://frappe.io/charts) - [Edit at CodeSandbox](https://codesandbox.io/s/frappe-charts-demo-viqud) - [Documentation](https://frappe.io/charts/docs)
</div>
<p align="center">
<a href="https://travis-ci.org/frappe/charts">
<img src="https://img.shields.io/travis/frappe/charts.svg?style=flat-square">
</a>
<a href="http://github.com/frappe/charts/tree/master/dist/js/frappe-charts.min.iife.js">
<img src="http://img.badgesize.io/frappe/charts/master/dist/frappe-charts.min.iife.js.svg?compression=gzip">
</a>
</p>
</div>
<p align="center">
<a href="https://frappe.github.io/charts">
<img src=".github/example.gif">
</a>
</p>
## Frappe Charts
Frappe Charts is a simple charting library with a focus on a simple API. The design is inspired by various charts you see on GitHub.
### Contents
* [Installation](#installation)
* [Usage](#usage)
* [Contribute](https://frappe.io/charts/docs/contributing)
* [Updates](#updates)
* [License](#license)
### Motivation
#### Installation
* Install via [`npm`](https://www.npmjs.com/get-npm):
ERPNext needed a simple sales history graph for its user company master to help users track sales. While using c3.js for reports, the library didnt align well with our products classic design. Existing JS libraries were either too complex or rigid in their structure and behavior. To address this, I decided to create a library for translating value pairs into relative shapes or positions, focusing on simplicity.
```sh
$ npm install frappe-charts
```
### Key Features
and include in your project:
```js
import { Chart } from "frappe-charts"
```
- **Variety of chart types**: Frappe Charts supports various chart types, including Axis Charts, Area and Trends, Bar, Line, Pie, Percentage, Mixed Axis, and Heatmap.
- **Annotations and tooltips**: Charts can be annotated with x and y markers, regions, and tooltips for enhanced data context and clarity.
- **Dynamic data handling**: Add, remove, or update individual data points in place, or refresh the entire dataset to reflect changes.
- **Customizable configurations**: Flexible options like colors, animations, and custom titles allow for a highly personalized chart experience.
...or include following for es-modules(eg:vuejs):
```js
import { Chart } from 'frappe-charts/dist/frappe-charts.esm.js'
// import css
import 'frappe-charts/dist/frappe-charts.min.css'
```
## Usage
* ...or include within your HTML
```sh
npm install frappe-charts
```
Import in your project:
```js
import { Chart } from 'frappe-charts'
// or esm import
import { Chart } from 'frappe-charts/dist/frappe-charts.esm.js'
// import css
import 'frappe-charts/dist/frappe-charts.min.css'
```
Or directly include script in your HTML
```html
<script src="https://unpkg.com/frappe-charts@1.6.1/dist/frappe-charts.min.umd.js"></script>
```
```html
<script src="https://cdn.jsdelivr.net/npm/frappe-charts@1.1.0/dist/frappe-charts.min.iife.js"></script>
<!-- or -->
<script src="https://unpkg.com/frappe-charts@1.1.0/dist/frappe-charts.min.iife.js"></script>
```
#### Usage
```js
const data = {
labels: ["12am-3am", "3am-6pm", "6am-9am", "9am-12am",
@ -71,11 +64,11 @@ const data = {
],
datasets: [
{
name: "Some Data", type: "bar",
name: "Some Data", chartType: "bar",
values: [25, 40, 30, 35, 8, 52, 17, -4]
},
{
name: "Another Set", type: "line",
name: "Another Set", chartType: "line",
values: [25, 50, -10, 15, 18, 32, 27, 14]
}
]
@ -91,65 +84,26 @@ const chart = new frappe.Chart("#chart", { // or a DOM element,
})
```
...or for es-modules (replace `new frappe.Chart()` with `new Chart()`):
```diff
- const chart = new frappe.Chart("#chart", {
+ const chart = new Chart("#chart", { // or a DOM element,
// new Chart() in case of ES6 module with above usage
title: "My Awesome Chart",
data: data,
type: 'axis-mixed', // or 'bar', 'line', 'scatter', 'pie', 'percentage'
height: 250,
colors: ['#7cd6fd', '#743ee2']
})
```
If you want to contribute:
## Contributing
1. Clone this repo.
2. `cd` into project directory
3. `npm install`
4. `npm run dev`
4. `npm i npm-run-all -D` (*optional --> might be required for some developers*)
5. `npm run dev`
#### Updates
## Links
##### v1.0.0
- Major rewrite out. Some new features include:
- Mixed type axis datasets
- Stacked bar charts
- Value over data points
- Y Markers and regions
- Dot size, Bar space size, and other options
- Legend for axis charts
- We would be looking to incorporate existing PRs and issues in the meantime.
- [Read the blog](https://medium.com/@pratu16x7/so-we-decided-to-create-our-own-charts-a95cb5032c97)
##### Please read [#93](https://github.com/frappe/charts/issues/93) for v0.1.0 updates on rework and development.
##### v0.0.7
- [Custom color values](https://github.com/frappe/charts/pull/71) for charts as hex codes. The API now takes an array of colors for all charts instead of a color for each dataset.
- [@iamkdev's](https://github.com/iamkdev) blog on [usage with Angular](https://medium.com/@iamkdev/frappé-charts-with-angular-c9c5dd075d9f).
##### v0.0.5
- More [flexible Y values](https://github.com/frappe/charts/commit/3de049c451194dcd8e61ff91ceeb998ce131c709): independent from exponent, minimum Y axis point for line graphs.
- Customisable [Heatmap colors](https://github.com/frappe/charts/pull/53); check out the Halloween demo on the [website](https://frappe.github.io/charts) :D
- Tooltip values can be [formatted](https://github.com/frappe/charts/commit/e3d9ed0eae14b65044dca0542cdd4d12af3f2b44).
##### v0.0.4
- Build update: [Shipped](https://github.com/frappe/charts/pull/35) an ES6 module, along with the browser friendly IIFE.
##### v0.0.2
- We have an animated [Pie Chart](https://github.com/frappe/charts/issues/29)! Thanks [@sheweichun](https://github.com/sheweichun).
- [@tobiaslins](https://github.com/tobiaslins) contributed tweaks for his quest to make these easy to use with React. Check out his [repo](https://github.com/tobiaslins/frappe-charts-react-example) and updates at [#24](https://github.com/frappe/charts/issues/24) to learn more :)
- A new logo.
##### v0.0.1
- The very first version out, with animatable bars and lines, a percentage chart and a heatmap. GitHub-style.
#### License
This repository has been released under the [MIT License](LICENSE)
------------------
Project maintained by [Frappe](https://frappe.io).
Used in [ERPNext](https://erpnext.com). Read the [blog post](https://medium.com/@pratu16x7/so-we-decided-to-create-our-own-charts-a95cb5032c97).
<br>
<br>
<div align="center">
<a href="https://frappe.io" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/Frappe-white.png">
<img src="https://frappe.io/files/Frappe-black.png" alt="Frappe Technologies" height="28"/>
</picture>
</a>
</div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,104 +0,0 @@
.chart-container {
position: relative;
/* for absolutely positioned tooltip */
/* https://www.smashingmagazine.com/2015/11/using-system-ui-fonts-practical-guide/ */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; }
.chart-container .axis, .chart-container .chart-label {
fill: #313B44; }
.chart-container .axis line, .chart-container .chart-label line {
stroke: #E2E6E9; }
.chart-container .dataset-units circle {
stroke: #fff;
stroke-width: 2; }
.chart-container .dataset-units path {
fill: none;
stroke-opacity: 1;
stroke-width: 2px; }
.chart-container .dataset-path {
stroke-width: 2px; }
.chart-container .path-group path {
fill: none;
stroke-opacity: 1;
stroke-width: 2px; }
.chart-container line.dashed {
stroke-dasharray: 5, 3; }
.chart-container .axis-line .specific-value {
text-anchor: start; }
.chart-container .axis-line .y-line {
text-anchor: end; }
.chart-container .axis-line .x-line {
text-anchor: middle; }
.chart-container .legend-dataset-text {
fill: #6c7680;
font-weight: 600; }
.graph-svg-tip {
position: absolute;
z-index: 99999;
padding: 10px;
font-size: 12px;
text-align: center;
background: #FFFFFF;
box-shadow: 0px 1px 4px rgba(17, 43, 66, 0.1), 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 40px 30px -30px rgba(17, 43, 66, 0.1);
border-radius: 6px; }
.graph-svg-tip ul {
padding-left: 0;
display: flex; }
.graph-svg-tip ol {
padding-left: 0;
display: flex; }
.graph-svg-tip ul.data-point-list li {
min-width: 90px;
font-weight: 600; }
.graph-svg-tip .svg-pointer {
position: absolute;
height: 12px;
width: 12px;
border-radius: 2px;
background: white;
transform: rotate(45deg);
margin-top: -7px;
margin-left: -6px; }
.graph-svg-tip.comparison {
text-align: left;
padding: 0px;
pointer-events: none; }
.graph-svg-tip.comparison .title {
display: block;
padding: 16px;
margin: 0;
color: #313B44;
font-weight: 600;
line-height: 1;
pointer-events: none;
text-transform: uppercase; }
.graph-svg-tip.comparison ul {
margin: 0;
white-space: nowrap;
list-style: none; }
.graph-svg-tip.comparison ul.tooltip-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 5px; }
.graph-svg-tip.comparison li {
display: inline-block;
display: flex;
flex-direction: row;
font-weight: 600;
line-height: 1;
padding: 5px 15px 15px 15px; }
.graph-svg-tip.comparison li .tooltip-legend {
height: 12px;
width: 12px;
margin-right: 8px;
border-radius: 2px; }
.graph-svg-tip.comparison li .tooltip-label {
margin-top: 4px;
font-size: 11px;
max-width: 100px;
color: #313B44;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
.graph-svg-tip.comparison li .tooltip-value {
color: #192734; }

File diff suppressed because one or more lines are too long

View File

@ -275,4 +275,4 @@ export const moonData = {
masses: [14819000, 10759000, 8931900, 4800000],
distances: [1070.412, 1882.709, 421.7, 671.034],
diameters: [5262.4, 4820.6, 3637.4, 3121.6],
};
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,36 +1,11 @@
(function () {
'use strict';
// Fixed 5-color theme,
// More colors are difficult to parse visually
var HEATMAP_COLORS_BLUE = ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'];
var HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'];
// Universal constants
/**
* Returns the value of a number upto 2 decimal places.
* @param {Number} d Any number
*/
/**
* Returns whether or not two given arrays are equal.
* @param {Array} arr1 First array
* @param {Array} arr2 Second array
*/
var ANGLE_RATIO = Math.PI / 180;
/**
* Shuffles array in place. ES6 version
@ -51,24 +26,6 @@ function shuffle(array) {
return array;
}
/**
* Fill an array with extra points
* @param {Array} array Array
* @param {Number} count number of filler elements
* @param {Object} element element to fill with
* @param {Boolean} start fill at start?
*/
/**
* Returns pixel width of string.
* @param {String} string
* @param {Number} charWidth Width of single char in pixels
*/
// https://stackoverflow.com/a/29325222
function getRandomBias(min, max, bias, influence) {
var range = max - min;
@ -79,36 +36,11 @@ function getRandomBias(min, max, bias, influence) {
return rnd * (1 - mix) + biasValue * mix; // mix full range and bias
}
/**
* Check if a number is valid for svg attributes
* @param {object} candidate Candidate to test
* @param {Boolean} nonNegative flag to treat negative number as invalid
*/
/**
* Round a number to the closes precision, max max precision 4
* @param {Number} d Any Number
*/
// Playing around with dates
var NO_OF_MILLIS = 1000;
var SEC_IN_DAY = 86400;
var MONTH_NAMES_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
function clone(date) {
return new Date(date.getTime());
}
@ -127,21 +59,6 @@ function timestampToMidnight(timestamp) {
return midnightTs;
}
// export function getMonthsBetween(startDate, endDate) {}
// mutates
// mutates
function addDays(date, numberOfDays) {
date.setDate(date.getDate() + numberOfDays);
@ -294,8 +211,6 @@ var demoConfig = {
}
};
/* eslint-disable no-unused-vars */
/* eslint-enable no-unused-vars */
// import { lineComposite, barComposite } from './demoConfig';
// ================================================================================

File diff suppressed because one or more lines are too long

26921
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +1,51 @@
{
"name": "frappe-charts",
"version": "1.5.2",
"main": "dist/frappe-charts.cjs.js",
"common": "dist/frappe-charts.cjs.js",
"module": "dist/frappe-charts.esm.js",
"browser": "dist/frappe-charts.umd.js",
"unpkg": "dist/frappe-charts.umd.js",
"description": "https://frappe.github.io/charts",
"directories": {
"doc": "docs"
},
"files": [
"src",
"dist"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "rollup -c --watch",
"dev": "npm-run-all --parallel watch",
"build": "rollup -c"
},
"repository": {
"type": "git",
"url": "git+https://github.com/frappe/charts.git"
},
"keywords": [
"js",
"charts"
],
"author": "Prateeksha Singh",
"license": "MIT",
"bugs": {
"url": "https://github.com/frappe/charts/issues"
},
"homepage": "https://github.com/frappe/charts#readme",
"devDependencies": {
"@babel/core": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"rollup": "^2.21.0",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-eslint": "^7.0.0",
"rollup-plugin-postcss": "^3.1.3",
"rollup-plugin-scss": "^2.5.0",
"rollup-plugin-terser": "^6.1.0"
}
}
"name": "frappe-charts",
"version": "v1.6.3",
"type": "module",
"main": "dist/frappe-charts.esm.js",
"module": "dist/frappe-charts.esm.js",
"browser": "dist/frappe-charts.umd.js",
"common": "dist/frappe-charts.cjs.js",
"unnpkg": "dist/frappe-charts.umd.js",
"description": "https://frappe.github.io/charts",
"directories": {
"doc": "docs"
},
"files": [
"src",
"dist"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "rollup -c --watch",
"dev": "npm-run-all --parallel watch",
"build": "rollup -c"
},
"repository": {
"type": "git",
"url": "git+https://github.com/frappe/charts.git"
},
"keywords": [
"js",
"charts"
],
"author": "Prateeksha Singh",
"license": "MIT",
"bugs": {
"url": "https://github.com/frappe/charts/issues"
},
"homepage": "https://github.com/frappe/charts#readme",
"devDependencies": {
"@babel/core": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"node-sass": "^8.0.0",
"rollup": "^2.21.0",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-bundle-size": "^1.0.3",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-eslint": "^7.0.0",
"rollup-plugin-postcss": "^3.1.3",
"rollup-plugin-scss": "^2.5.0",
"rollup-plugin-terser": "^6.1.0"
}
}

View File

@ -1,43 +1,47 @@
import pkg from './package.json';
import commonjs from 'rollup-plugin-commonjs';
import babel from 'rollup-plugin-babel';
import postcss from 'rollup-plugin-postcss';
import scss from 'rollup-plugin-scss';
import { terser } from 'rollup-plugin-terser';
import pkg from "./package.json";
import commonjs from "rollup-plugin-commonjs";
import babel from "rollup-plugin-babel";
import postcss from "rollup-plugin-postcss";
import scss from "rollup-plugin-scss";
import bundleSize from "rollup-plugin-bundle-size";
import { terser } from "rollup-plugin-terser";
export default [
// browser-friendly UMD build
{
input: 'src/js/index.js',
output: {
name: 'frappe-charts',
file: pkg.browser,
format: 'umd'
},
plugins: [
commonjs(),
babel({
exclude: ['node_modules/**']
}),
terser(),
scss({ output: 'dist/frappe-charts.min.css' })
]
},
// browser-friendly UMD build
{
input: "src/js/index.js",
output: {
sourcemap: true,
name: "frappe",
file: pkg.browser,
format: "umd",
},
plugins: [
commonjs(),
babel({
exclude: ["node_modules/**"],
}),
terser(),
scss({ output: "dist/frappe-charts.min.css" }),
bundleSize(),
],
},
// CommonJS (for Node) and ES module (for bundlers) build.
{
input: 'src/js/chart.js',
output: [
{ file: pkg.common, format: 'cjs' },
{ file: pkg.module, format: 'es' }
],
plugins: [
babel({
exclude: ['node_modules/**']
}),
postcss()
]
}
];
// CommonJS (for Node) and ES module (for bundlers) build.
{
input: "src/js/chart.js",
output: [
{ file: pkg.common, format: "cjs", sourcemap: true },
{ file: pkg.module, format: "es", sourcemap: true },
],
plugins: [
babel({
exclude: ["node_modules/**"],
}),
terser(),
postcss(),
bundleSize(),
],
},
];

View File

@ -1,10 +0,0 @@
{
"presets": [
["@babel/preset-env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
},
"modules": false
}]
]
}

View File

@ -1,179 +1,192 @@
:root {
--fr-label-color: #313b44;
--fr-axis-line-color: #E2E6E9;
--charts-label-color: #313b44;
--charts-axis-line-color: #f4f5f6;
--fr-stroke-width: 2px;
--fr-dataset-circle-stroke: #FFFFFF;
--fr-dataset-circle-stroke-width: var(--fr-stroke-width);
--charts-tooltip-title: var(--charts-label-color);
--charts-tooltip-label: var(--charts-label-color);
--charts-tooltip-value: #192734;
--charts-tooltip-bg: #ffffff;
--fr-tooltip-title: var(--fr-label-color);
--fr-tooltip-label: var(--fr-label-color);
--fr-tooltip-value: #192734;
--fr-tooltip-bg: #FFFFFF;
--charts-stroke-width: 2px;
--charts-dataset-circle-stroke: #ffffff;
--charts-dataset-circle-stroke-width: var(--charts-stroke-width);
--charts-legend-label: var(--charts-label-color);
--charts-legend-value: var(--charts-label-color);
}
.chart-container {
position: relative; /* for absolutely positioned tooltip */
position: relative;
/* for absolutely positioned tooltip */
font-family: -apple-system, BlinkMacSystemFont,
'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
.axis, .chart-label {
fill: var(--fr-label-color);
.axis,
.chart-label {
fill: var(--charts-label-color);
line {
stroke: var(--fr-axis-line-color);
}
}
line {
stroke: var(--charts-axis-line-color);
}
}
.dataset-units {
circle {
stroke: var(--fr-dataset-circle-stroke);
stroke-width: var(--fr-dataset-circle-stroke-width);
}
.dataset-units {
circle {
stroke: var(--charts-dataset-circle-stroke);
stroke-width: var(--charts-dataset-circle-stroke-width);
}
path {
fill: none;
stroke-opacity: 1;
stroke-width: var(--fr-stroke-width);
}
}
path {
fill: none;
stroke-opacity: 1;
stroke-width: var(--charts-stroke-width);
}
}
.dataset-path {
stroke-width: var(--fr-stroke-width);
}
.dataset-path {
stroke-width: var(--charts-stroke-width);
}
.path-group {
path {
fill: none;
stroke-opacity: 1;
stroke-width: var(--fr-stroke-width);
}
}
.path-group {
path {
fill: none;
stroke-opacity: 1;
stroke-width: var(--charts-stroke-width);
}
}
line.dashed {
stroke-dasharray: 5, 3;
}
line.dashed {
stroke-dasharray: 5, 3;
}
.axis-line {
.specific-value {
text-anchor: start;
}
.y-line {
text-anchor: end;
}
.x-line {
text-anchor: middle;
}
}
.axis-line {
.specific-value {
text-anchor: start;
}
.legend-dataset-label {
fill: var(--fr-tooltip-label);
font-weight: 600;
}
.y-line {
text-anchor: end;
}
.legend-dataset-value {
fill: var(--fr-tooltip-value);
}
.x-line {
text-anchor: middle;
}
}
.legend-dataset-label {
fill: var(--charts-legend-label);
font-weight: 600;
}
.legend-dataset-value {
fill: var(--charts-legend-value);
}
}
.graph-svg-tip {
position: absolute;
z-index: 99999;
padding: 10px;
font-size: 12px;
text-align: center;
background: var(--fr-tooltip-bg);
box-shadow: 0px 1px 4px rgba(17, 43, 66, 0.1), 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 40px 30px -30px rgba(17, 43, 66, 0.1);
border-radius: 6px;
position: absolute;
z-index: 99999;
padding: 10px;
font-size: 12px;
text-align: center;
background: var(--charts-tooltip-bg);
box-shadow: 0px 1px 4px rgba(17, 43, 66, 0.1),
0px 2px 6px rgba(17, 43, 66, 0.08),
0px 40px 30px -30px rgba(17, 43, 66, 0.1);
border-radius: 6px;
ul {
padding-left: 0;
display: flex;
}
ul {
padding-left: 0;
display: flex;
}
ol {
padding-left: 0;
display: flex;
}
ol {
padding-left: 0;
display: flex;
}
ul.data-point-list {
li {
min-width: 90px;
font-weight: 600;
}
}
ul.data-point-list {
li {
min-width: 90px;
font-weight: 600;
}
}
.svg-pointer {
position: absolute;
height: 12px;
width: 12px;
border-radius: 2px;
background: var(--fr-tooltip-bg);
transform: rotate(45deg);
margin-top: -7px;
margin-left: -6px;
}
.svg-pointer {
position: absolute;
height: 12px;
width: 12px;
border-radius: 2px;
background: var(--charts-tooltip-bg);
transform: rotate(45deg);
margin-top: -7px;
margin-left: -6px;
}
&.comparison {
text-align: left;
padding: 0px;
pointer-events: none;
&.comparison {
text-align: left;
padding: 0px;
pointer-events: none;
.title {
display: block;
padding: 16px;
margin: 0;
color: var(--fr-tooltip-title);
font-weight: 600;
line-height: 1;
pointer-events: none;
text-transform: uppercase;
}
.title {
display: block;
padding: 16px;
margin: 0;
color: var(--charts-tooltip-title);
font-weight: 600;
line-height: 1;
pointer-events: none;
text-transform: uppercase;
ul {
margin: 0;
white-space: nowrap;
list-style: none;
strong {
color: var(--charts-tooltip-value);
}
}
&.tooltip-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 5px;
}
}
ul {
margin: 0;
white-space: nowrap;
list-style: none;
li {
display: inline-block;
display: flex;
flex-direction: row;
font-weight: 600;
line-height: 1;
&.tooltip-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 5px;
}
}
padding: 5px 15px 15px 15px;
li {
display: inline-block;
display: flex;
flex-direction: row;
font-weight: 600;
line-height: 1;
.tooltip-legend {
height: 12px;
width: 12px;
margin-right: 8px;
border-radius: 2px;
}
padding: 5px 15px 15px 15px;
.tooltip-label {
margin-top: 4px;
font-size: 11px;
max-width: 100px;
.tooltip-legend {
height: 12px;
width: 12px;
margin-right: 8px;
border-radius: 2px;
}
color: var(--fr-tooltip-label);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tooltip-label {
margin-top: 4px;
font-size: 11px;
max-width: 100px;
.tooltip-value {
color: var(--fr-tooltip-value);
}
}
}
}
color: var(--fr-tooltip-label);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tooltip-value {
color: var(--charts-tooltip-value);
}
}
}
}

View File

@ -1 +1,2 @@
export const CSSTEXT = ".chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .legend-dataset-text{fill:#6c7680;font-weight:600}.graph-svg-tip{position:absolute;z-index:99999;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ul{padding-left:0;display:flex}.graph-svg-tip ol{padding-left:0;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;flex:1;font-weight:600}.graph-svg-tip strong{color:#dfe2e5;font-weight:600}.graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:' ';border:5px solid transparent;}.graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.graph-svg-tip.comparison li{display:inline-block;padding:5px 10px}";
export const CSSTEXT =
".chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .legend-dataset-text{fill:#6c7680;font-weight:600}.graph-svg-tip{position:absolute;z-index:99999;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ul{padding-left:0;display:flex}.graph-svg-tip ol{padding-left:0;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;flex:1;font-weight:600}.graph-svg-tip strong{color:#dfe2e5;font-weight:600}.graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:' ';border:5px solid transparent;}.graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.graph-svg-tip.comparison li{display:inline-block;padding:5px 10px}";

View File

@ -1,40 +1,38 @@
import '../css/charts.scss';
import "../css/charts.scss";
// import MultiAxisChart from './charts/MultiAxisChart';
import PercentageChart from './charts/PercentageChart';
import PieChart from './charts/PieChart';
import Heatmap from './charts/Heatmap';
import AxisChart from './charts/AxisChart';
import DonutChart from './charts/DonutChart';
import PercentageChart from "./charts/PercentageChart";
import PieChart from "./charts/PieChart";
import Heatmap from "./charts/Heatmap";
import AxisChart from "./charts/AxisChart";
import DonutChart from "./charts/DonutChart";
const chartTypes = {
bar: AxisChart,
line: AxisChart,
// multiaxis: MultiAxisChart,
percentage: PercentageChart,
heatmap: Heatmap,
pie: PieChart,
donut: DonutChart,
bar: AxisChart,
line: AxisChart,
percentage: PercentageChart,
heatmap: Heatmap,
pie: PieChart,
donut: DonutChart,
};
function getChartByType(chartType = 'line', parent, options) {
if (chartType === 'axis-mixed') {
options.type = 'line';
return new AxisChart(parent, options);
}
function getChartByType(chartType = "line", parent, options) {
if (chartType === "axis-mixed") {
options.type = "line";
return new AxisChart(parent, options);
}
if (!chartTypes[chartType]) {
console.error("Undefined chart type: " + chartType);
return;
}
if (!chartTypes[chartType]) {
console.error("Undefined chart type: " + chartType);
return;
}
return new chartTypes[chartType](parent, options);
return new chartTypes[chartType](parent, options);
}
class Chart {
constructor(parent, options) {
return getChartByType(options.type, parent, options);
}
constructor(parent, options) {
return getChartByType(options.type, parent, options);
}
}
export { Chart, PercentageChart, PieChart, Heatmap, AxisChart };
export { Chart, PercentageChart, PieChart, Heatmap, AxisChart };

View File

@ -1,97 +1,94 @@
import BaseChart from './BaseChart';
import { truncateString } from '../utils/draw-utils';
import { legendDot } from '../utils/draw';
import { round } from '../utils/helpers';
import { getExtraWidth } from '../utils/constants';
import BaseChart from "./BaseChart";
import { truncateString } from "../utils/draw-utils";
import { legendDot } from "../utils/draw";
import { round } from "../utils/helpers";
import { getExtraWidth } from "../utils/constants";
export default class AggregationChart extends BaseChart {
constructor(parent, args) {
super(parent, args);
}
constructor(parent, args) {
super(parent, args);
}
configure(args) {
super.configure(args);
configure(args) {
super.configure(args);
this.config.formatTooltipY = (args.tooltipOptions || {}).formatTooltipY;
this.config.maxSlices = args.maxSlices || 20;
this.config.maxLegendPoints = args.maxLegendPoints || 20;
}
this.config.formatTooltipY = (args.tooltipOptions || {}).formatTooltipY;
this.config.maxSlices = args.maxSlices || 20;
this.config.maxLegendPoints = args.maxLegendPoints || 20;
this.config.legendRowHeight = 60;
}
calc() {
let s = this.state;
let maxSlices = this.config.maxSlices;
s.sliceTotals = [];
calc() {
let s = this.state;
let maxSlices = this.config.maxSlices;
s.sliceTotals = [];
let allTotals = this.data.labels.map((label, i) => {
let total = 0;
this.data.datasets.map(e => {
total += e.values[i];
});
return [total, label];
}).filter(d => { return d[0] >= 0; }); // keep only positive results
let allTotals = this.data.labels
.map((label, i) => {
let total = 0;
this.data.datasets.map((e) => {
total += e.values[i];
});
return [total, label];
})
.filter((d) => {
return d[0] >= 0;
}); // keep only positive results
let totals = allTotals;
if(allTotals.length > maxSlices) {
// Prune and keep a grey area for rest as per maxSlices
allTotals.sort((a, b) => { return b[0] - a[0]; });
let totals = allTotals;
if (allTotals.length > maxSlices) {
// Prune and keep a grey area for rest as per maxSlices
allTotals.sort((a, b) => {
return b[0] - a[0];
});
totals = allTotals.slice(0, maxSlices-1);
let remaining = allTotals.slice(maxSlices-1);
totals = allTotals.slice(0, maxSlices - 1);
let remaining = allTotals.slice(maxSlices - 1);
let sumOfRemaining = 0;
remaining.map(d => {sumOfRemaining += d[0];});
totals.push([sumOfRemaining, 'Rest']);
this.colors[maxSlices-1] = 'grey';
}
let sumOfRemaining = 0;
remaining.map((d) => {
sumOfRemaining += d[0];
});
totals.push([sumOfRemaining, "Rest"]);
this.colors[maxSlices - 1] = "grey";
}
s.labels = [];
totals.map(d => {
s.sliceTotals.push(round(d[0]));
s.labels.push(d[1]);
});
s.labels = [];
totals.map((d) => {
s.sliceTotals.push(round(d[0]));
s.labels.push(d[1]);
});
s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0);
s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0);
this.center = {
x: this.width / 2,
y: this.height / 2
};
}
this.center = {
x: this.width / 2,
y: this.height / 2,
};
}
renderLegend() {
let s = this.state;
this.legendArea.textContent = '';
this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints);
renderLegend() {
let s = this.state;
this.legendArea.textContent = "";
this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints);
super.renderLegend(this.legendTotals);
}
let count = 0;
let y = 0;
this.legendTotals.map((d, i) => {
let barWidth = 150;
let divisor = Math.floor(
(this.width - getExtraWidth(this.measures))/barWidth
);
if (this.legendTotals.length < divisor) {
barWidth = this.width/this.legendTotals.length;
}
if(count > divisor) {
count = 0;
y += 60;
}
let x = barWidth * count + 5;
let label = this.config.truncateLegends ? truncateString(s.labels[i], barWidth/10) : s.labels[i];
let formatted = this.config.formatTooltipY ? this.config.formatTooltipY(d) : d;
let dot = legendDot(
x,
y,
12,
3,
this.colors[i],
`${label}: ${formatted}`,
d,
false
);
this.legendArea.appendChild(dot);
count++;
});
}
makeLegend(data, index, x_pos, y_pos) {
let formatted = this.config.formatTooltipY
? this.config.formatTooltipY(data)
: data;
return legendDot(
x_pos,
y_pos,
12, // size
3, // dot radius
this.colors[index], // fill
this.state.labels[index], // label
formatted, // value
null, // base_font_size
this.config.truncateLegends // truncate_legends
);
}
}

View File

@ -1,13 +1,29 @@
import BaseChart from './BaseChart';
import { dataPrep, zeroDataPrep, getShortenedLabels } from '../utils/axis-chart-utils';
import { AXIS_LEGEND_BAR_SIZE } from '../utils/constants';
import { getComponent } from '../objects/ChartComponents';
import { getOffset, fire } from '../utils/dom';
import { calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale, getClosestInArray } from '../utils/intervals';
import { floatTwo } from '../utils/helpers';
import { makeOverlay, updateOverlay, legendBar } from '../utils/draw';
import { getTopOffset, getLeftOffset, MIN_BAR_PERCENT_HEIGHT, BAR_CHART_SPACE_RATIO,
LINE_CHART_DOT_SIZE } from '../utils/constants';
import BaseChart from "./BaseChart";
import {
dataPrep,
zeroDataPrep,
getShortenedLabels,
} from "../utils/axis-chart-utils";
import { getComponent } from "../objects/ChartComponents";
import { getOffset, fire } from "../utils/dom";
import {
calcChartIntervals,
getIntervalSize,
getValueRange,
getZeroIndex,
scale,
getClosestInArray,
} from "../utils/intervals";
import { floatTwo } from "../utils/helpers";
import { makeOverlay, updateOverlay, legendDot } from "../utils/draw";
import {
getTopOffset,
getLeftOffset,
MIN_BAR_PERCENT_HEIGHT,
BAR_CHART_SPACE_RATIO,
LINE_CHART_DOT_SIZE,
LEGEND_ITEM_WIDTH,
} from "../utils/constants";
export default class AxisChart extends BaseChart {
constructor(parent, args) {
@ -16,14 +32,14 @@ export default class AxisChart extends BaseChart {
this.barOptions = args.barOptions || {};
this.lineOptions = args.lineOptions || {};
this.type = args.type || 'line';
this.type = args.type || "line";
this.init = 1;
this.setup();
}
setMeasures() {
if(this.data.datasets.length <= 1) {
if (this.data.datasets.length <= 1) {
this.config.showLegend = 0;
this.measures.paddings.bottom = 30;
}
@ -31,33 +47,63 @@ export default class AxisChart extends BaseChart {
configure(options) {
super.configure(options);
const { axisOptions = {} } = options;
const { xAxis, yAxis } = axisOptions || {};
options.axisOptions = options.axisOptions || {};
options.tooltipOptions = options.tooltipOptions || {};
this.config.xAxisMode = options.axisOptions.xAxisMode || 'span';
this.config.yAxisMode = options.axisOptions.yAxisMode || 'span';
this.config.xIsSeries = options.axisOptions.xIsSeries || 0;
this.config.shortenYAxisNumbers = options.axisOptions.shortenYAxisNumbers || 0;
this.config.xAxisMode = xAxis
? xAxis.xAxisMode
: axisOptions.xAxisMode || "span";
// this will pass an array
// lets determine if we need two yAxis based on if there is length
// to the yAxis array
if (yAxis && yAxis.length) {
this.config.yAxisConfig = yAxis.map((item) => {
return {
yAxisMode: item.yAxisMode,
id: item.id,
position: item.position,
title: item.title,
};
});
} else {
this.config.yAxisMode = yAxis
? yAxis.yAxisMode
: axisOptions.yAxisMode || "span";
// if we have yAxis config settings lets populate a yAxis config array.
if (yAxis && yAxis.id && yAxis.position) {
this.config.yAxisConfig = [yAxis];
}
}
this.config.xIsSeries = axisOptions.xIsSeries || 0;
this.config.shortenYAxisNumbers = axisOptions.shortenYAxisNumbers || 0;
this.config.formatTooltipX = options.tooltipOptions.formatTooltipX;
this.config.formatTooltipY = options.tooltipOptions.formatTooltipY;
this.config.valuesOverPoints = options.valuesOverPoints;
this.config.legendRowHeight = 30;
}
prepareData(data=this.data) {
return dataPrep(data, this.type);
prepareData(data = this.data, config = this.config) {
return dataPrep(data, this.type, config.continuous);
}
prepareFirstData(data=this.data) {
prepareFirstData(data = this.data) {
return zeroDataPrep(data);
}
calc(onlyWidthChange = false) {
this.calcXPositions();
if(!onlyWidthChange) {
this.calcYAxisParameters(this.getAllYValues(), this.type === 'line');
if (!onlyWidthChange) {
this.calcYAxisParameters(
this.getAllYValues(),
this.type === "line"
);
}
this.makeDataByIndex();
}
@ -67,9 +113,9 @@ export default class AxisChart extends BaseChart {
let labels = this.data.labels;
s.datasetLength = labels.length;
s.unitWidth = this.width/(s.datasetLength);
s.unitWidth = this.width / s.datasetLength;
// Default, as per bar, and mixed. Only line will be a special case
s.xOffset = s.unitWidth/2;
s.xOffset = s.unitWidth / 2;
// // For a pure Line Chart
// s.unitWidth = this.width/(s.datasetLength - 1);
@ -79,22 +125,121 @@ export default class AxisChart extends BaseChart {
labels: labels,
positions: labels.map((d, i) =>
floatTwo(s.xOffset + i * s.unitWidth)
)
),
};
}
calcYAxisParameters(dataValues, withMinimum = 'false') {
const yPts = calcChartIntervals(dataValues, withMinimum);
const scaleMultiplier = this.height / getValueRange(yPts);
const intervalHeight = getIntervalSize(yPts) * scaleMultiplier;
const zeroLine = this.height - (getZeroIndex(yPts) * intervalHeight);
calcYAxisParameters(dataValues, withMinimum = "false") {
let yPts,
scaleMultiplier,
intervalHeight,
zeroLine,
positions,
yAxisConfigObject,
yAxisAlignment,
yKeys;
this.state.yAxis = {
labels: yPts,
positions: yPts.map(d => zeroLine - d * scaleMultiplier),
scaleMultiplier: scaleMultiplier,
zeroLine: zeroLine,
};
yKeys = [];
yAxisConfigObject = this.config.yAxisMode || {};
yAxisAlignment = yAxisConfigObject.position
? yAxisConfigObject.position
: "left";
// if we have an object we have multiple yAxisParameters.
if (dataValues instanceof Array) {
yPts = calcChartIntervals(dataValues, withMinimum, this.config.overrideCeiling, this.config.overrideFloor);
scaleMultiplier = this.height / getValueRange(yPts);
intervalHeight = getIntervalSize(yPts) * scaleMultiplier;
zeroLine = this.height - getZeroIndex(yPts) * intervalHeight;
this.state.yAxis = {
labels: yPts,
positions: yPts.map((d) => zeroLine - d * scaleMultiplier),
title: yAxisConfigObject.title || null,
pos: yAxisAlignment,
scaleMultiplier: scaleMultiplier,
zeroLine: zeroLine,
};
} else {
this.state.yAxis = [];
for (let key in dataValues) {
const dataValue = dataValues[key];
yAxisConfigObject =
this.config.yAxisConfig.find((item) => key === item.id) ||
[];
yAxisAlignment = yAxisConfigObject.position
? yAxisConfigObject.position
: "left";
yPts = calcChartIntervals(dataValue, withMinimum, this.config.overrideCeiling, this.config.overrideFloor);
scaleMultiplier = this.height / getValueRange(yPts);
intervalHeight = getIntervalSize(yPts) * scaleMultiplier;
zeroLine = this.height - getZeroIndex(yPts) * intervalHeight;
positions = yPts.map((d) => zeroLine - d * scaleMultiplier);
yKeys.push(key);
if (this.state.yAxis.length > 1) {
const yPtsArray = [];
const firstArr = this.state.yAxis[0];
// we need to calculate the scaleMultiplier.
// now that we have an accurate scaleMultiplier we can
// we need to loop through original positions.
scaleMultiplier = this.height / getValueRange(yPts);
firstArr.positions.forEach((pos) => {
yPtsArray.push(Math.ceil(pos / scaleMultiplier));
});
yPts = yPtsArray.reverse();
zeroLine =
this.height - getZeroIndex(yPts) * intervalHeight;
positions = firstArr.positions;
}
this.state.yAxis.push({
axisID: key || "left-axis",
labels: yPts,
title: yAxisConfigObject.title,
pos: yAxisAlignment,
scaleMultiplier,
zeroLine,
positions,
});
}
// the labels are not aligned in length between the two yAxis objects,
// we need to run some new calculations.
if (
this.state.yAxis[1] &&
this.state.yAxis[0].labels.length !==
this.state.yAxis[1].labels.length
) {
const newYptsArr = [];
// find the shorter array
const shortest = this.state.yAxis.reduce(
(p, c) => {
return p.length > c.labels.length ? c : p;
},
{ length: Infinity }
);
// return the longest
const longest = this.state.yAxis.reduce(
(p, c) => {
return p.length < c.labels.length ? p : c;
},
{ length: Infinity }
);
// we now need to populate the shortest obj with the new scale multiplier
// with the positions of the longest obj.
longest.positions.forEach((pos) => {
// calculate a new yPts
newYptsArr.push(Math.ceil(pos / shortest.scaleMultiplier));
});
shortest.labels = newYptsArr.reverse();
shortest.positions = longest.positions;
}
}
// Dependent if above changes
this.calcDatasetPoints();
@ -104,35 +249,57 @@ export default class AxisChart extends BaseChart {
calcDatasetPoints() {
let s = this.state;
let scaleAll = values => values.map(val => scale(val, s.yAxis));
let scaleAll = (values, id) => {
return values.map((val) => {
let { yAxis } = s;
if (yAxis instanceof Array) {
yAxis =
yAxis.length > 1
? yAxis.find((axis) => id === axis.axisID)
: s.yAxis[0];
}
return scale(val, yAxis);
});
};
s.barChartIndex = 1;
s.datasets = this.data.datasets.map((d, i) => {
let values = d.values;
let cumulativeYs = d.cumulativeYs || [];
return {
name: d.name.replace(/<|>|&/g, (char) => char == '&' ? '&amp;' : char == '<' ? '&lt;' : '&gt;'),
name:
d.name &&
d.name.replace(/<|>|&/g, (char) =>
char == "&" ? "&amp;" : char == "<" ? "&lt;" : "&gt;"
),
index: i,
barIndex:
d.chartType === "bar" ? s.barChartIndex++ : s.barChartIndex,
chartType: d.chartType,
values: values,
yPositions: scaleAll(values),
yPositions: scaleAll(values, d.axisID),
id: d.axisID,
cumulativeYs: cumulativeYs,
cumulativeYPos: scaleAll(cumulativeYs),
cumulativeYPos: scaleAll(cumulativeYs, d.axisID),
};
});
}
calcYExtremes() {
let s = this.state;
if(this.barOptions.stacked) {
if (this.barOptions.stacked) {
s.yExtremes = s.datasets[s.datasets.length - 1].cumulativeYPos;
return;
}
s.yExtremes = new Array(s.datasetLength).fill(9999);
s.datasets.map(d => {
s.datasets.map((d) => {
d.yPositions.map((pos, j) => {
if(pos < s.yExtremes[j]) {
if (pos < s.yExtremes[j]) {
s.yExtremes[j] = pos;
}
});
@ -141,101 +308,169 @@ export default class AxisChart extends BaseChart {
calcYRegions() {
let s = this.state;
if(this.data.yMarkers) {
this.state.yMarkers = this.data.yMarkers.map(d => {
if (this.data.yMarkers) {
this.state.yMarkers = this.data.yMarkers.map((d) => {
d.position = scale(d.value, s.yAxis);
if(!d.options) d.options = {};
if (!d.options) d.options = {};
// if(!d.label.includes(':')) {
// d.label += ': ' + d.value;
// }
return d;
});
}
if(this.data.yRegions) {
this.state.yRegions = this.data.yRegions.map(d => {
if (this.data.yRegions) {
this.state.yRegions = this.data.yRegions.map((d) => {
d.startPos = scale(d.start, s.yAxis);
d.endPos = scale(d.end, s.yAxis);
if(!d.options) d.options = {};
if (!d.options) d.options = {};
return d;
});
}
}
getAllYValues() {
let key = 'values';
let key = "values";
let multiAxis = this.config.yAxisConfig ? true : false;
let allValueLists = multiAxis ? {} : [];
if(this.barOptions.stacked) {
key = 'cumulativeYs';
let groupBy = (arr, property) => {
return arr.reduce((acc, cur) => {
acc[cur[property]] = [...(acc[cur[property]] || []), cur];
return acc;
}, {});
};
let generateCumulative = (arr) => {
let cumulative = new Array(this.state.datasetLength).fill(0);
this.data.datasets.map((d, i) => {
let values = this.data.datasets[i].values;
d[key] = cumulative = cumulative.map((c, i) => c + values[i]);
arr.forEach((d, i) => {
let values = arr[i].values;
d[key] = cumulative = cumulative.map((c, i) => {
return c + values[i];
});
});
};
if (this.barOptions.stacked) {
key = "cumulativeYs";
// we need to filter out the different yAxis ID's here.
if (multiAxis) {
const groupedDataSets = groupBy(this.data.datasets, "axisID");
// const dataSetsByAxis = this.data.dd
for (var axisID in groupedDataSets) {
generateCumulative(groupedDataSets[axisID]);
}
} else {
generateCumulative(this.data.datasets);
}
}
// this is the trouble maker, we don't want to merge all
// datasets since we are trying to run two yAxis.
if (multiAxis) {
this.data.datasets.forEach((d) => {
// if the array exists already just push more data into it.
// otherwise create a new array into the object.
allValueLists[d.axisID || key]
? allValueLists[d.axisID || key].push(...d[key])
: (allValueLists[d.axisID || key] = [...d[key]]);
});
} else {
allValueLists = this.data.datasets.map((d) => {
return d[key];
});
}
let allValueLists = this.data.datasets.map(d => d[key]);
if(this.data.yMarkers) {
allValueLists.push(this.data.yMarkers.map(d => d.value));
if (this.data.yMarkers && !multiAxis) {
allValueLists.push(this.data.yMarkers.map((d) => d.value));
}
if(this.data.yRegions) {
this.data.yRegions.map(d => {
if (this.data.yRegions && !multiAxis) {
this.data.yRegions.map((d) => {
allValueLists.push([d.end, d.start]);
});
}
return [].concat(...allValueLists);
return multiAxis ? allValueLists : [].concat(...allValueLists); //return [].concat(...allValueLists); master
}
setupComponents() {
let componentConfigs = [
[
'yAxis',
{
mode: this.config.yAxisMode,
width: this.width,
shortenNumbers: this.config.shortenYAxisNumbers
// pos: 'right'
},
function() {
return this.state.yAxis;
}.bind(this)
],
[
'xAxis',
"xAxis",
{
mode: this.config.xAxisMode,
height: this.height,
// pos: 'right'
},
function() {
function () {
let s = this.state;
s.xAxis.calcLabels = getShortenedLabels(this.width,
s.xAxis.labels, this.config.xIsSeries);
s.xAxis.calcLabels = getShortenedLabels(
this.width,
s.xAxis.labels,
this.config.xIsSeries
);
return s.xAxis;
}.bind(this)
}.bind(this),
],
[
'yRegions',
"yRegions",
{
width: this.width,
pos: 'right'
pos: "right",
},
function() {
function () {
return this.state.yRegions;
}.bind(this)
}.bind(this),
],
];
let barDatasets = this.state.datasets.filter(d => d.chartType === 'bar');
let lineDatasets = this.state.datasets.filter(d => d.chartType === 'line');
// if we have multiple yAxisConfigs we need to update the yAxisDefault
// components to multiple yAxis components.
if (this.config.yAxisConfig && this.config.yAxisConfig.length) {
this.config.yAxisConfig.forEach((yAxis) => {
componentConfigs.push([
"yAxis",
{
mode: yAxis.yAxisMode || "span",
width: this.width,
height: this.baseHeight,
shortenNumbers: this.config.shortenYAxisNumbers,
pos: yAxis.position || "left",
},
function () {
return this.state.yAxis;
}.bind(this),
]);
});
} else {
componentConfigs.push([
"yAxis",
{
mode: this.config.yAxisMode,
width: this.width,
height: this.baseHeight,
shortenNumbers: this.config.shortenYAxisNumbers,
},
function () {
return this.state.yAxis;
}.bind(this),
]);
}
let barsConfigs = barDatasets.map(d => {
let barDatasets = this.state.datasets.filter(
(d) => d.chartType === "bar"
);
let lineDatasets = this.state.datasets.filter(
(d) => d.chartType === "line"
);
let barsConfigs = barDatasets.map((d) => {
let index = d.index;
let barIndex = d.barIndex || index;
return [
'barGraph' + '-' + d.index,
"barGraph" + "-" + d.index,
{
index: index,
color: this.colors[index],
@ -245,32 +480,52 @@ export default class AxisChart extends BaseChart {
valuesOverPoints: this.config.valuesOverPoints,
minHeight: this.height * MIN_BAR_PERCENT_HEIGHT,
},
function() {
function () {
let s = this.state;
let { yAxis } = s;
let d = s.datasets[index];
let { id = "left-axis" } = d;
let stacked = this.barOptions.stacked;
let spaceRatio = this.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO;
let spaceRatio =
this.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO;
let barsWidth = s.unitWidth * (1 - spaceRatio);
let barWidth = barsWidth/(stacked ? 1 : barDatasets.length);
let barWidth =
barsWidth / (stacked ? 1 : barDatasets.length);
let xPositions = s.xAxis.positions.map(x => x - barsWidth/2);
if(!stacked) {
xPositions = xPositions.map(p => p + barWidth * index);
// if there are multiple yAxis we need to return the yAxis with the
// proper ID.
if (yAxis instanceof Array) {
// if the person only configured one yAxis in the array return the first.
yAxis =
yAxis.length > 1
? yAxis.find((axis) => id === axis.axisID)
: s.yAxis[0];
}
let labels = new Array(s.datasetLength).fill('');
if(this.config.valuesOverPoints) {
if(stacked && d.index === s.datasets.length - 1) {
let xPositions = s.xAxis.positions.map(
(x) => x - barsWidth / 2
);
if (!stacked) {
xPositions = xPositions.map(
(p) => p + barWidth * barIndex - barWidth
);
}
let labels = new Array(s.datasetLength).fill("");
if (this.config.valuesOverPoints) {
if (stacked && d.index === s.datasets.length - 1) {
labels = d.cumulativeYs;
} else {
labels = d.values;
}
}
let offsets = new Array(s.datasetLength).fill(0);
if(stacked) {
offsets = d.yPositions.map((y, j) => y - d.cumulativeYPos[j]);
if (stacked) {
offsets = d.yPositions.map(
(y, j) => y - d.cumulativeYPos[j]
);
}
return {
@ -280,18 +535,18 @@ export default class AxisChart extends BaseChart {
// values: d.values,
labels: labels,
zeroLine: s.yAxis.zeroLine,
zeroLine: yAxis.zeroLine,
barsWidth: barsWidth,
barWidth: barWidth,
};
}.bind(this)
}.bind(this),
];
});
let lineConfigs = lineDatasets.map(d => {
let lineConfigs = lineDatasets.map((d) => {
let index = d.index;
return [
'lineGraph' + '-' + d.index,
"lineGraph" + "-" + d.index,
{
index: index,
color: this.colors[index],
@ -305,11 +560,20 @@ export default class AxisChart extends BaseChart {
// same for all datasets
valuesOverPoints: this.config.valuesOverPoints,
},
function() {
function () {
let s = this.state;
let d = s.datasets[index];
let minLine = s.yAxis.positions[0] < s.yAxis.zeroLine
? s.yAxis.positions[0] : s.yAxis.zeroLine;
// if we have more than one yindex lets map the values
const yAxis = s.yAxis.length
? s.yAxis.find((axis) => d.id === axis.axisID) ||
s.yAxis[0]
: s.yAxis;
let minLine =
yAxis.positions[0] < yAxis.zeroLine
? yAxis.positions[0]
: yAxis.zeroLine;
return {
xPositions: s.xAxis.positions,
@ -320,37 +584,49 @@ export default class AxisChart extends BaseChart {
zeroLine: minLine,
radius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE,
};
}.bind(this)
}.bind(this),
];
});
let markerConfigs = [
[
'yMarkers',
"yMarkers",
{
width: this.width,
pos: 'right'
pos: "right",
},
function() {
function () {
return this.state.yMarkers;
}.bind(this)
]
}.bind(this),
],
];
componentConfigs = componentConfigs.concat(barsConfigs, lineConfigs, markerConfigs);
componentConfigs = componentConfigs.concat(
barsConfigs,
lineConfigs,
markerConfigs
);
let optionals = ['yMarkers', 'yRegions'];
let optionals = ["yMarkers", "yRegions"];
this.dataUnitComponents = [];
this.components = new Map(componentConfigs
.filter(args => !optionals.includes(args[0]) || this.state[args[0]])
.map(args => {
let component = getComponent(...args);
if(args[0].includes('lineGraph') || args[0].includes('barGraph')) {
this.dataUnitComponents.push(component);
}
return [args[0], component];
}));
this.components = new Map(
componentConfigs
.filter(
(args) =>
!optionals.includes(args[0]) || this.state[args[0]]
)
.map((args) => {
let component = getComponent(...args);
if (
args[0].includes("lineGraph") ||
args[0].includes("barGraph")
) {
this.dataUnitComponents.push(component);
}
return [args[0], component];
})
);
}
makeDataByIndex() {
@ -385,14 +661,16 @@ export default class AxisChart extends BaseChart {
bindTooltip() {
// NOTE: could be in tooltip itself, as it is a given functionality for its parent
this.container.addEventListener('mousemove', (e) => {
this.container.addEventListener("mousemove", (e) => {
let m = this.measures;
let o = getOffset(this.container);
let relX = e.pageX - o.left - getLeftOffset(m);
let relY = e.pageY - o.top;
if(relY < this.height + getTopOffset(m)
&& relY > getTopOffset(m)) {
if (
relY < this.height + getTopOffset(m) &&
relY > getTopOffset(m)
) {
this.mapTooltipXPosition(relX);
} else {
this.tip.hideTip();
@ -402,7 +680,7 @@ export default class AxisChart extends BaseChart {
mapTooltipXPosition(relX) {
let s = this.state;
if(!s.yExtremes) return;
if (!s.yExtremes) return;
let index = getClosestInArray(relX, s.xAxis.positions, true);
if (index >= 0) {
@ -411,7 +689,7 @@ export default class AxisChart extends BaseChart {
this.tip.setValues(
dbi.xPos + this.tip.offset.x,
dbi.yExtreme + this.tip.offset.y,
{name: dbi.formattedLabel, value: ''},
{ name: dbi.formattedLabel, value: "" },
dbi.values,
index
);
@ -422,41 +700,63 @@ export default class AxisChart extends BaseChart {
renderLegend() {
let s = this.data;
if(s.datasets.length > 1) {
this.legendArea.textContent = '';
if (s.datasets.length > 1) {
super.renderLegend(s.datasets);
}
}
// Legacy
/* renderLegend() {
let s = this.data;
if (s.datasets.length > 1) {
this.legendArea.textContent = "";
console.log(s.datasets);
s.datasets.map((d, i) => {
let barWidth = AXIS_LEGEND_BAR_SIZE;
let barWidth = LEGEND_ITEM_WIDTH;
// let rightEndPoint = this.baseWidth - this.measures.margins.left - this.measures.margins.right;
// let multiplier = s.datasets.length - i;
let rect = legendBar(
// rightEndPoint - multiplier * barWidth, // To right align
barWidth * i,
'0',
"0",
barWidth,
this.colors[i],
d.name,
this.config.truncateLegends);
this.config.truncateLegends
);
this.legendArea.appendChild(rect);
});
}
} */
makeLegend(data, index, x_pos, y_pos) {
return legendDot(
x_pos,
y_pos + 5, // Extra offset
12, // size
3, // dot radius
this.colors[index], // fill
data.name, //label
null, // value
8.75, // base_font_size
this.config.truncateLegends // truncate legends
);
}
// Overlay
makeOverlay() {
if(this.init) {
if (this.init) {
this.init = 0;
return;
}
if(this.overlayGuides) {
this.overlayGuides.forEach(g => {
if (this.overlayGuides) {
this.overlayGuides.forEach((g) => {
let o = g.overlay;
o.parentNode.removeChild(o);
});
}
this.overlayGuides = this.dataUnitComponents.map(c => {
this.overlayGuides = this.dataUnitComponents.map((c) => {
return {
type: c.unitType,
overlay: undefined,
@ -464,12 +764,12 @@ export default class AxisChart extends BaseChart {
};
});
if(this.state.currentIndex === undefined) {
if (this.state.currentIndex === undefined) {
this.state.currentIndex = this.state.datasetLength - 1;
}
// Render overlays
this.overlayGuides.map(d => {
this.overlayGuides.map((d) => {
let currentUnit = d.units[this.state.currentIndex];
d.overlay = makeOverlay[d.type](currentUnit);
@ -478,8 +778,8 @@ export default class AxisChart extends BaseChart {
}
updateOverlayGuides() {
if(this.overlayGuides) {
this.overlayGuides.forEach(g => {
if (this.overlayGuides) {
this.overlayGuides.forEach((g) => {
let o = g.overlay;
o.parentNode.removeChild(o);
});
@ -487,30 +787,30 @@ export default class AxisChart extends BaseChart {
}
bindOverlay() {
this.parent.addEventListener('data-select', () => {
this.parent.addEventListener("data-select", () => {
this.updateOverlay();
});
}
bindUnits() {
this.dataUnitComponents.map(c => {
c.units.map(unit => {
unit.addEventListener('click', () => {
let index = unit.getAttribute('data-point-index');
this.dataUnitComponents.map((c) => {
c.units.map((unit) => {
unit.addEventListener("click", () => {
let index = unit.getAttribute("data-point-index");
this.setCurrentDataPoint(index);
});
});
});
// Note: Doesn't work as tooltip is absolutely positioned
this.tip.container.addEventListener('click', () => {
let index = this.tip.container.getAttribute('data-point-index');
this.tip.container.addEventListener("click", () => {
let index = this.tip.container.getAttribute("data-point-index");
this.setCurrentDataPoint(index);
});
}
updateOverlay() {
this.overlayGuides.map(d => {
this.overlayGuides.map((d) => {
let currentUnit = d.units[this.state.currentIndex];
updateOverlay[d.type](currentUnit, d.overlay);
});
@ -524,12 +824,12 @@ export default class AxisChart extends BaseChart {
this.setCurrentDataPoint(this.state.currentIndex + 1);
}
getDataPoint(index=this.state.currentIndex) {
getDataPoint(index = this.state.currentIndex) {
let s = this.state;
let data_point = {
index: index,
label: s.xAxis.labels[index],
values: s.datasets.map(d => d.values[index])
values: s.datasets.map((d) => d.values[index]),
};
return data_point;
}
@ -537,17 +837,15 @@ export default class AxisChart extends BaseChart {
setCurrentDataPoint(index) {
let s = this.state;
index = parseInt(index);
if(index < 0) index = 0;
if(index >= s.xAxis.labels.length) index = s.xAxis.labels.length - 1;
if(index === s.currentIndex) return;
if (index < 0) index = 0;
if (index >= s.xAxis.labels.length) index = s.xAxis.labels.length - 1;
if (index === s.currentIndex) return;
s.currentIndex = index;
fire(this.parent, "data-select", this.getDataPoint());
}
// API
addDataPoint(label, datasetValues, index=this.state.datasetLength) {
addDataPoint(label, datasetValues, index = this.state.datasetLength) {
super.addDataPoint(label, datasetValues, index);
this.data.labels.splice(index, 0, label);
this.data.datasets.map((d, i) => {
@ -556,19 +854,19 @@ export default class AxisChart extends BaseChart {
this.update(this.data);
}
removeDataPoint(index = this.state.datasetLength-1) {
removeDataPoint(index = this.state.datasetLength - 1) {
if (this.data.labels.length <= 1) {
return;
}
super.removeDataPoint(index);
this.data.labels.splice(index, 1);
this.data.datasets.map(d => {
this.data.datasets.map((d) => {
d.values.splice(index, 1);
});
this.update(this.data);
}
updateDataset(datasetValues, index=0) {
updateDataset(datasetValues, index = 0) {
this.data.datasets[index].values = datasetValues;
this.update(this.data);
}
@ -577,7 +875,7 @@ export default class AxisChart extends BaseChart {
updateDatasets(datasets) {
this.data.datasets.map((d, i) => {
if(datasets[i]) {
if (datasets[i]) {
d.values = datasets[i];
}
});

View File

@ -1,46 +1,84 @@
import SvgTip from '../objects/SvgTip';
import { $, isElementInViewport, getElementContentWidth, isHidden } from '../utils/dom';
import { makeSVGContainer, makeSVGDefs, makeSVGGroup, makeText } from '../utils/draw';
import { BASE_MEASURES, getExtraHeight, getExtraWidth, getTopOffset, getLeftOffset,
INIT_CHART_UPDATE_TIMEOUT, CHART_POST_ANIMATE_TIMEOUT, DEFAULT_COLORS} from '../utils/constants';
import { getColor, isValidColor } from '../utils/colors';
import { runSMILAnimation } from '../utils/animation';
import { downloadFile, prepareForExport } from '../utils/export';
import SvgTip from "../objects/SvgTip";
import {
$,
isElementInViewport,
getElementContentWidth,
isHidden,
} from "../utils/dom";
import {
makeSVGContainer,
makeSVGDefs,
makeSVGGroup,
makeText,
} from "../utils/draw";
import { LEGEND_ITEM_WIDTH } from "../utils/constants";
import {
BASE_MEASURES,
getExtraHeight,
getExtraWidth,
getTopOffset,
getLeftOffset,
INIT_CHART_UPDATE_TIMEOUT,
CHART_POST_ANIMATE_TIMEOUT,
DEFAULT_COLORS,
} from "../utils/constants";
import { getColor, isValidColor } from "../utils/colors";
import { runSMILAnimation } from "../utils/animation";
import { downloadFile, prepareForExport } from "../utils/export";
import { deepClone } from "../utils/helpers";
export default class BaseChart {
constructor(parent, options) {
// deepclone options to avoid making changes to orignal object
options = deepClone(options);
this.parent = typeof parent === 'string'
? document.querySelector(parent)
: parent;
this.parent =
typeof parent === "string"
? document.querySelector(parent)
: parent;
if (!(this.parent instanceof HTMLElement)) {
throw new Error('No `parent` element to render on was provided.');
throw new Error("No `parent` element to render on was provided.");
}
this.rawChartArgs = options;
this.title = options.title || '';
this.type = options.type || '';
this.realData = this.prepareData(options.data);
this.data = this.prepareFirstData(this.realData);
this.title = options.title || "";
this.type = options.type || "";
this.colors = this.validateColors(options.colors, this.type);
this.config = {
showTooltip: 1, // calculate
showLegend: 1, // calculate
showLegend:
typeof options.showLegend !== "undefined"
? options.showLegend
: 1,
isNavigable: options.isNavigable || 0,
animate: (typeof options.animate !== 'undefined') ? options.animate : 1,
truncateLegends: options.truncateLegends || 1
animate: 0,
overrideCeiling: options.overrideCeiling || false,
overrideFloor: options.overrideFloor || false,
truncateLegends:
typeof options.truncateLegends !== "undefined"
? options.truncateLegends
: 1,
continuous:
typeof options.continuous !== "undefined"
? options.continuous
: 1,
};
this.measures = JSON.parse(JSON.stringify(BASE_MEASURES));
let m = this.measures;
this.realData = this.prepareData(options.data, this.config);
this.data = this.prepareFirstData(this.realData);
this.setMeasures(options);
if(!this.title.length) { m.titleHeight = 0; }
if(!this.config.showLegend) m.legendHeight = 0;
if (!this.title.length) {
m.titleHeight = 0;
}
if (!this.config.showLegend) m.legendHeight = 0;
this.argHeight = options.height || m.baseHeight;
this.state = {};
@ -48,7 +86,7 @@ export default class BaseChart {
this.initTimeout = INIT_CHART_UPDATE_TIMEOUT;
if(this.config.isNavigable) {
if (this.config.isNavigable) {
this.overlays = [];
}
@ -68,7 +106,7 @@ export default class BaseChart {
colors = (colors || []).concat(DEFAULT_COLORS[type]);
colors.forEach((string) => {
const color = getColor(string);
if(!isValidColor(color)) {
if (!isValidColor(color)) {
console.warn('"' + string + '" is not a valid color.');
} else {
validColors.push(color);
@ -89,13 +127,19 @@ export default class BaseChart {
// Bind window events
this.boundDrawFn = () => this.draw(true);
window.addEventListener('resize', this.boundDrawFn);
window.addEventListener('orientationchange', this.boundDrawFn);
// Look into improving responsiveness
//if (ResizeObserver) {
// this.resizeObserver = new ResizeObserver(this.boundDrawFn);
// this.resizeObserver.observe(this.parent);
//}
window.addEventListener("resize", this.boundDrawFn);
window.addEventListener("orientationchange", this.boundDrawFn);
}
destroy() {
window.removeEventListener('resize', this.boundDrawFn);
window.removeEventListener('orientationchange', this.boundDrawFn);
//if (this.resizeObserver) this.resizeObserver.disconnect();
window.removeEventListener("resize", this.boundDrawFn);
window.removeEventListener("orientationchange", this.boundDrawFn);
}
// Has to be called manually
@ -109,31 +153,31 @@ export default class BaseChart {
makeContainer() {
// Chart needs a dedicated parent element
this.parent.innerHTML = '';
this.parent.innerHTML = "";
let args = {
inside: this.parent,
className: 'chart-container'
className: "chart-container",
};
if(this.independentWidth) {
args.styles = { width: this.independentWidth + 'px' };
if (this.independentWidth) {
args.styles = { width: this.independentWidth + "px" };
}
this.container = $.create('div', args);
this.container = $.create("div", args);
}
makeTooltip() {
this.tip = new SvgTip({
parent: this.container,
colors: this.colors
colors: this.colors,
});
this.bindTooltip();
}
bindTooltip() {}
draw(onlyWidthChange=false, init=false) {
draw(onlyWidthChange = false, init = false) {
if (onlyWidthChange && isHidden(this.parent)) {
// Don't update anything if the chart is hidden
return;
@ -144,16 +188,22 @@ export default class BaseChart {
this.makeChartArea();
this.setupComponents();
this.components.forEach(c => c.setup(this.drawArea));
this.components.forEach((c) => c.setup(this.drawArea));
// this.components.forEach(c => c.make());
this.render(this.components, false);
if(init) {
if (init) {
this.data = this.realData;
setTimeout(() => {this.update(this.data);}, this.initTimeout);
this.update(this.data, true);
// Not needed anymore since animate defaults to 0 and might potentially be refactored or deprecated
/* setTimeout(() => {
this.update(this.data, true);
}, this.initTimeout); */
}
this.renderLegend();
if (this.config.showLegend) {
this.renderLegend();
}
this.setupNavigation(init);
}
@ -166,50 +216,54 @@ export default class BaseChart {
}
makeChartArea() {
if(this.svg) {
if (this.svg) {
this.container.removeChild(this.svg);
}
let m = this.measures;
this.svg = makeSVGContainer(
this.container,
'frappe-chart chart',
"frappe-chart chart",
this.baseWidth,
this.baseHeight
);
this.svgDefs = makeSVGDefs(this.svg);
if(this.title.length) {
if (this.title.length) {
this.titleEL = makeText(
'title',
"title",
m.margins.left,
m.margins.top,
this.title,
{
fontSize: m.titleFontSize,
fill: '#666666',
dy: m.titleFontSize
fill: "#666666",
dy: m.titleFontSize,
}
);
}
let top = getTopOffset(m);
this.drawArea = makeSVGGroup(
this.type + '-chart chart-draw-area',
this.type + "-chart chart-draw-area",
`translate(${getLeftOffset(m)}, ${top})`
);
if(this.config.showLegend) {
if (this.config.showLegend) {
top += this.height + m.paddings.bottom;
this.legendArea = makeSVGGroup(
'chart-legend',
"chart-legend",
`translate(${getLeftOffset(m)}, ${top})`
);
}
if(this.title.length) { this.svg.appendChild(this.titleEL); }
if (this.title.length) {
this.svg.appendChild(this.titleEL);
}
this.svg.appendChild(this.drawArea);
if(this.config.showLegend) { this.svg.appendChild(this.legendArea); }
if (this.config.showLegend) {
this.svg.appendChild(this.legendArea);
}
this.updateTipOffset(getLeftOffset(m), getTopOffset(m));
}
@ -217,71 +271,90 @@ export default class BaseChart {
updateTipOffset(x, y) {
this.tip.offset = {
x: x,
y: y
y: y,
};
}
setupComponents() { this.components = new Map(); }
setupComponents() {
this.components = new Map();
}
update(data) {
if(!data) {
console.error('No data to update.');
}
this.data = this.prepareData(data);
update(data, drawing = false, config) {
if (!data) console.error("No data to update.");
if (!drawing) data = deepClone(data);
this.data = this.prepareData(data, config);
this.calc(); // builds state
this.render(this.components, this.config.animate);
}
render(components=this.components, animate=true) {
if(this.config.isNavigable) {
render(components = this.components, animate = true) {
if (this.config.isNavigable) {
// Remove all existing overlays
this.overlays.map(o => o.parentNode.removeChild(o));
this.overlays.map((o) => o.parentNode.removeChild(o));
// ref.parentNode.insertBefore(element, ref);
}
let elementsToAnimate = [];
// Can decouple to this.refreshComponents() first to save animation timeout
components.forEach(c => {
components.forEach((c) => {
elementsToAnimate = elementsToAnimate.concat(c.update(animate));
});
if(elementsToAnimate.length > 0) {
if (elementsToAnimate.length > 0) {
runSMILAnimation(this.container, this.svg, elementsToAnimate);
setTimeout(() => {
components.forEach(c => c.make());
components.forEach((c) => c.make());
this.updateNav();
}, CHART_POST_ANIMATE_TIMEOUT);
} else {
components.forEach(c => c.make());
components.forEach((c) => c.make());
this.updateNav();
}
}
updateNav() {
if(this.config.isNavigable) {
if (this.config.isNavigable) {
this.makeOverlay();
this.bindUnits();
}
}
renderLegend() {}
renderLegend(dataset) {
this.legendArea.textContent = "";
let count = 0;
let y = 0;
setupNavigation(init=false) {
if(!this.config.isNavigable) return;
dataset.map((data, index) => {
let divisor = Math.floor(this.width / LEGEND_ITEM_WIDTH);
if (count > divisor) {
count = 0;
y += this.config.legendRowHeight;
}
let x = LEGEND_ITEM_WIDTH * count;
let dot = this.makeLegend(data, index, x, y);
this.legendArea.appendChild(dot);
count++;
});
}
if(init) {
makeLegend() {}
setupNavigation(init = false) {
if (!this.config.isNavigable) return;
if (init) {
this.bindOverlay();
this.keyActions = {
'13': this.onEnterKey.bind(this),
'37': this.onLeftArrow.bind(this),
'38': this.onUpArrow.bind(this),
'39': this.onRightArrow.bind(this),
'40': this.onDownArrow.bind(this),
13: this.onEnterKey.bind(this),
37: this.onLeftArrow.bind(this),
38: this.onUpArrow.bind(this),
39: this.onRightArrow.bind(this),
40: this.onDownArrow.bind(this),
};
document.addEventListener('keydown', (e) => {
if(isElementInViewport(this.container)) {
document.addEventListener("keydown", (e) => {
if (isElementInViewport(this.container)) {
e = e || window.event;
if(this.keyActions[e.keyCode]) {
if (this.keyActions[e.keyCode]) {
this.keyActions[e.keyCode]();
}
}
@ -310,6 +383,6 @@ export default class BaseChart {
export() {
let chartSvg = prepareForExport(this.svg);
downloadFile(this.title || 'Chart', [chartSvg]);
downloadFile(this.title || "Chart", [chartSvg]);
}
}

View File

@ -1,161 +1,185 @@
import AggregationChart from './AggregationChart';
import { getComponent } from '../objects/ChartComponents';
import { getOffset } from '../utils/dom';
import { getPositionByAngle } from '../utils/helpers';
import { makeArcStrokePathStr, makeStrokeCircleStr } from '../utils/draw';
import { lightenDarkenColor } from '../utils/colors';
import { transform } from '../utils/animation';
import { FULL_ANGLE } from '../utils/constants';
import AggregationChart from "./AggregationChart";
import { getComponent } from "../objects/ChartComponents";
import { getOffset } from "../utils/dom";
import { getPositionByAngle } from "../utils/helpers";
import { makeArcStrokePathStr, makeStrokeCircleStr } from "../utils/draw";
import { lightenDarkenColor } from "../utils/colors";
import { transform } from "../utils/animation";
import { FULL_ANGLE } from "../utils/constants";
export default class DonutChart extends AggregationChart {
constructor(parent, args) {
super(parent, args);
this.type = 'donut';
this.initTimeout = 0;
this.init = 1;
constructor(parent, args) {
super(parent, args);
this.type = "donut";
this.initTimeout = 0;
this.init = 1;
this.setup();
}
this.setup();
}
configure(args) {
super.configure(args);
this.mouseMove = this.mouseMove.bind(this);
this.mouseLeave = this.mouseLeave.bind(this);
configure(args) {
super.configure(args);
this.mouseMove = this.mouseMove.bind(this);
this.mouseLeave = this.mouseLeave.bind(this);
this.hoverRadio = args.hoverRadio || 0.1;
this.config.startAngle = args.startAngle || 0;
this.hoverRadio = args.hoverRadio || 0.1;
this.config.startAngle = args.startAngle || 0;
this.clockWise = args.clockWise || false;
this.strokeWidth = args.strokeWidth || 30;
}
this.clockWise = args.clockWise || false;
this.strokeWidth = args.strokeWidth || 30;
}
calc() {
super.calc();
let s = this.state;
this.radius =
this.height > this.width
? this.center.x - this.strokeWidth / 2
: this.center.y - this.strokeWidth / 2;
calc() {
super.calc();
let s = this.state;
this.radius =
this.height > this.width
? this.center.x - this.strokeWidth / 2
: this.center.y - this.strokeWidth / 2;
const { radius, clockWise } = this;
const { radius, clockWise } = this;
const prevSlicesProperties = s.slicesProperties || [];
s.sliceStrings = [];
s.slicesProperties = [];
let curAngle = 180 - this.config.startAngle;
const prevSlicesProperties = s.slicesProperties || [];
s.sliceStrings = [];
s.slicesProperties = [];
let curAngle = 180 - this.config.startAngle;
s.sliceTotals.map((total, i) => {
const startAngle = curAngle;
const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE;
const largeArc = originDiffAngle > 180 ? 1: 0;
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;
const endAngle = curAngle = curAngle + diffAngle;
const startPosition = getPositionByAngle(startAngle, radius);
const endPosition = getPositionByAngle(endAngle, radius);
s.sliceTotals.map((total, i) => {
const startAngle = curAngle;
const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE;
const largeArc = originDiffAngle > 180 ? 1 : 0;
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;
const endAngle = (curAngle = curAngle + diffAngle);
const startPosition = getPositionByAngle(startAngle, radius);
const endPosition = getPositionByAngle(endAngle, radius);
const prevProperty = this.init && prevSlicesProperties[i];
const prevProperty = this.init && prevSlicesProperties[i];
let curStart,curEnd;
if(this.init) {
curStart = prevProperty ? prevProperty.startPosition : startPosition;
curEnd = prevProperty ? prevProperty.endPosition : startPosition;
} else {
curStart = startPosition;
curEnd = endPosition;
}
const curPath =
originDiffAngle === 360
? makeStrokeCircleStr(curStart, curEnd, this.center, this.radius, this.clockWise, largeArc)
: makeArcStrokePathStr(curStart, curEnd, this.center, this.radius, this.clockWise, largeArc);
let curStart, curEnd;
if (this.init) {
curStart = prevProperty ? prevProperty.startPosition : startPosition;
curEnd = prevProperty ? prevProperty.endPosition : startPosition;
} else {
curStart = startPosition;
curEnd = endPosition;
}
const curPath =
originDiffAngle === 360
? makeStrokeCircleStr(
curStart,
curEnd,
this.center,
this.radius,
this.clockWise,
largeArc
)
: makeArcStrokePathStr(
curStart,
curEnd,
this.center,
this.radius,
this.clockWise,
largeArc
);
s.sliceStrings.push(curPath);
s.slicesProperties.push({
startPosition,
endPosition,
value: total,
total: s.grandTotal,
startAngle,
endAngle,
angle: diffAngle
});
s.sliceStrings.push(curPath);
s.slicesProperties.push({
startPosition,
endPosition,
value: total,
total: s.grandTotal,
startAngle,
endAngle,
angle: diffAngle,
});
});
this.init = 0;
}
});
this.init = 0;
}
setupComponents() {
let s = this.state;
setupComponents() {
let s = this.state;
let componentConfigs = [
[
"donutSlices",
{},
function () {
return {
sliceStrings: s.sliceStrings,
colors: this.colors,
strokeWidth: this.strokeWidth,
};
}.bind(this),
],
];
let componentConfigs = [
[
'donutSlices',
{ },
function() {
return {
sliceStrings: s.sliceStrings,
colors: this.colors,
strokeWidth: this.strokeWidth,
};
}.bind(this)
]
];
this.components = new Map(
componentConfigs.map((args) => {
let component = getComponent(...args);
return [args[0], component];
})
);
}
this.components = new Map(componentConfigs
.map(args => {
let component = getComponent(...args);
return [args[0], component];
}));
}
calTranslateByAngle(property) {
const { radius, hoverRadio } = this;
const position = getPositionByAngle(
property.startAngle + property.angle / 2,
radius
);
return `translate3d(${position.x * hoverRadio}px,${
position.y * hoverRadio
}px,0)`;
}
calTranslateByAngle(property){
const{ radius, hoverRadio } = this;
const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius);
return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`;
}
hoverSlice(path, i, flag, e) {
if (!path) return;
const color = this.colors[i];
if (flag) {
transform(path, this.calTranslateByAngle(this.state.slicesProperties[i]));
path.style.stroke = lightenDarkenColor(color, 50);
let g_off = getOffset(this.svg);
let x = e.pageX - g_off.left + 10;
let y = e.pageY - g_off.top - 10;
let title =
(this.formatted_labels && this.formatted_labels.length > 0
? this.formatted_labels[i]
: this.state.labels[i]) + ": ";
let percent = (
(this.state.sliceTotals[i] * 100) /
this.state.grandTotal
).toFixed(1);
this.tip.setValues(x, y, { name: title, value: percent + "%" });
this.tip.showTip();
} else {
transform(path, "translate3d(0,0,0)");
this.tip.hideTip();
path.style.stroke = color;
}
}
hoverSlice(path,i,flag,e){
if(!path) return;
const color = this.colors[i];
if(flag) {
transform(path, this.calTranslateByAngle(this.state.slicesProperties[i]));
path.style.stroke = lightenDarkenColor(color, 50);
let g_off = getOffset(this.svg);
let x = e.pageX - g_off.left + 10;
let y = e.pageY - g_off.top - 10;
let title = (this.formatted_labels && this.formatted_labels.length > 0
? this.formatted_labels[i] : this.state.labels[i]) + ': ';
let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1);
this.tip.setValues(x, y, {name: title, value: percent + "%"});
this.tip.showTip();
} else {
transform(path,'translate3d(0,0,0)');
this.tip.hideTip();
path.style.stroke = color;
}
}
bindTooltip() {
this.container.addEventListener("mousemove", this.mouseMove);
this.container.addEventListener("mouseleave", this.mouseLeave);
}
bindTooltip() {
this.container.addEventListener('mousemove', this.mouseMove);
this.container.addEventListener('mouseleave', this.mouseLeave);
}
mouseMove(e) {
const target = e.target;
let slices = this.components.get("donutSlices").store;
let prevIndex = this.curActiveSliceIndex;
let prevAcitve = this.curActiveSlice;
if (slices.includes(target)) {
let i = slices.indexOf(target);
this.hoverSlice(prevAcitve, prevIndex, false);
this.curActiveSlice = target;
this.curActiveSliceIndex = i;
this.hoverSlice(target, i, true, e);
} else {
this.mouseLeave();
}
}
mouseMove(e){
const target = e.target;
let slices = this.components.get('donutSlices').store;
let prevIndex = this.curActiveSliceIndex;
let prevAcitve = this.curActiveSlice;
if(slices.includes(target)) {
let i = slices.indexOf(target);
this.hoverSlice(prevAcitve, prevIndex,false);
this.curActiveSlice = target;
this.curActiveSliceIndex = i;
this.hoverSlice(target, i, true, e);
} else {
this.mouseLeave();
}
}
mouseLeave(){
this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false);
}
mouseLeave() {
this.hoverSlice(this.curActiveSlice, this.curActiveSliceIndex, false);
}
}

View File

@ -1,296 +1,337 @@
import BaseChart from './BaseChart';
import { getComponent } from '../objects/ChartComponents';
import { makeText, heatSquare } from '../utils/draw';
import { DAY_NAMES_SHORT, addDays, areInSameMonth, getLastDateInMonth, setDayToSunday, getYyyyMmDd, getWeeksBetween, getMonthName, clone,
NO_OF_MILLIS, NO_OF_YEAR_MONTHS, NO_OF_DAYS_IN_WEEK } from '../utils/date-utils';
import { calcDistribution, getMaxCheckpoint } from '../utils/intervals';
import { getExtraHeight, getExtraWidth, HEATMAP_DISTRIBUTION_SIZE, HEATMAP_SQUARE_SIZE,
HEATMAP_GUTTER_SIZE } from '../utils/constants';
import BaseChart from "./BaseChart";
import { getComponent } from "../objects/ChartComponents";
import { makeText, heatSquare } from "../utils/draw";
import {
DAY_NAMES_SHORT,
toMidnightUTC,
addDays,
areInSameMonth,
getLastDateInMonth,
setDayToSunday,
getYyyyMmDd,
getWeeksBetween,
getMonthName,
clone,
NO_OF_MILLIS,
NO_OF_YEAR_MONTHS,
NO_OF_DAYS_IN_WEEK,
} from "../utils/date-utils";
import { calcDistribution, getMaxCheckpoint } from "../utils/intervals";
import {
getExtraHeight,
getExtraWidth,
HEATMAP_DISTRIBUTION_SIZE,
HEATMAP_SQUARE_SIZE,
HEATMAP_GUTTER_SIZE,
} from "../utils/constants";
const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE;
const ROW_HEIGHT = COL_WIDTH;
// const DAY_INCR = 1;
export default class Heatmap extends BaseChart {
constructor(parent, options) {
super(parent, options);
this.type = 'heatmap';
constructor(parent, options) {
super(parent, options);
this.type = "heatmap";
this.countLabel = options.countLabel || '';
this.countLabel = options.countLabel || "";
let validStarts = ['Sunday', 'Monday'];
let startSubDomain = validStarts.includes(options.startSubDomain)
? options.startSubDomain : 'Sunday';
this.startSubDomainIndex = validStarts.indexOf(startSubDomain);
let validStarts = ["Sunday", "Monday"];
let startSubDomain = validStarts.includes(options.startSubDomain)
? options.startSubDomain
: "Sunday";
this.startSubDomainIndex = validStarts.indexOf(startSubDomain);
this.setup();
}
this.setup();
}
setMeasures(options) {
let m = this.measures;
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1;
setMeasures(options) {
let m = this.measures;
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1;
m.paddings.top = ROW_HEIGHT * 3;
m.paddings.bottom = 0;
m.legendHeight = ROW_HEIGHT * 2;
m.baseHeight = ROW_HEIGHT * NO_OF_DAYS_IN_WEEK
+ getExtraHeight(m);
m.paddings.top = ROW_HEIGHT * 3;
m.paddings.bottom = 0;
m.legendHeight = ROW_HEIGHT * 2;
m.baseHeight = ROW_HEIGHT * NO_OF_DAYS_IN_WEEK + getExtraHeight(m);
let d = this.data;
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
this.independentWidth = (getWeeksBetween(d.start, d.end)
+ spacing) * COL_WIDTH + getExtraWidth(m);
}
let d = this.data;
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
this.independentWidth =
(getWeeksBetween(d.start, d.end) + spacing) * COL_WIDTH +
getExtraWidth(m);
}
updateWidth() {
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
let noOfWeeks = this.state.noOfWeeks ? this.state.noOfWeeks : 52;
this.baseWidth = (noOfWeeks + spacing) * COL_WIDTH
+ getExtraWidth(this.measures);
}
updateWidth() {
let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
let noOfWeeks = this.state.noOfWeeks ? this.state.noOfWeeks : 52;
this.baseWidth =
(noOfWeeks + spacing) * COL_WIDTH + getExtraWidth(this.measures);
}
prepareData(data=this.data) {
if(data.start && data.end && data.start > data.end) {
throw new Error('Start date cannot be greater than end date.');
}
prepareData(data = this.data) {
if (data.start && data.end && data.start > data.end) {
throw new Error("Start date cannot be greater than end date.");
}
if(!data.start) {
data.start = new Date();
data.start.setFullYear( data.start.getFullYear() - 1 );
}
if(!data.end) { data.end = new Date(); }
data.dataPoints = data.dataPoints || {};
if (!data.start) {
data.start = new Date();
data.start.setFullYear(data.start.getFullYear() - 1);
}
data.start = toMidnightUTC(data.start);
if(parseInt(Object.keys(data.dataPoints)[0]) > 100000) {
let points = {};
Object.keys(data.dataPoints).forEach(timestampSec => {
let date = new Date(timestampSec * NO_OF_MILLIS);
points[getYyyyMmDd(date)] = data.dataPoints[timestampSec];
});
data.dataPoints = points;
}
if (!data.end) {
data.end = new Date();
}
data.end = toMidnightUTC(data.end);
return data;
}
data.dataPoints = data.dataPoints || {};
calc() {
let s = this.state;
if (parseInt(Object.keys(data.dataPoints)[0]) > 100000) {
let points = {};
Object.keys(data.dataPoints).forEach((timestampSec) => {
let date = new Date(timestampSec * NO_OF_MILLIS);
points[getYyyyMmDd(date)] = data.dataPoints[timestampSec];
});
data.dataPoints = points;
}
s.start = clone(this.data.start);
s.end = clone(this.data.end);
return data;
}
s.firstWeekStart = clone(s.start);
s.noOfWeeks = getWeeksBetween(s.start, s.end);
s.distribution = calcDistribution(
Object.values(this.data.dataPoints), HEATMAP_DISTRIBUTION_SIZE);
calc() {
let s = this.state;
s.domainConfigs = this.getDomains();
}
s.start = clone(this.data.start);
s.end = clone(this.data.end);
setupComponents() {
let s = this.state;
let lessCol = this.discreteDomains ? 0 : 1;
s.firstWeekStart = clone(s.start);
s.noOfWeeks = getWeeksBetween(s.start, s.end);
s.distribution = calcDistribution(
Object.values(this.data.dataPoints),
HEATMAP_DISTRIBUTION_SIZE
);
let componentConfigs = s.domainConfigs.map((config, i) => [
'heatDomain',
{
index: config.index,
colWidth: COL_WIDTH,
rowHeight: ROW_HEIGHT,
squareSize: HEATMAP_SQUARE_SIZE,
radius: this.rawChartArgs.radius || 0,
xTranslate: s.domainConfigs
.filter((config, j) => j < i)
.map(config => config.cols.length - lessCol)
.reduce((a, b) => a + b, 0)
* COL_WIDTH
},
function() {
return s.domainConfigs[i];
}.bind(this)
s.domainConfigs = this.getDomains();
}
]);
setupComponents() {
let s = this.state;
let lessCol = this.discreteDomains ? 0 : 1;
this.components = new Map(componentConfigs
.map((args, i) => {
let component = getComponent(...args);
return [args[0] + '-' + i, component];
})
);
let componentConfigs = s.domainConfigs.map((config, i) => [
"heatDomain",
{
index: config.index,
colWidth: COL_WIDTH,
rowHeight: ROW_HEIGHT,
squareSize: HEATMAP_SQUARE_SIZE,
radius: this.rawChartArgs.radius || 0,
xTranslate:
s.domainConfigs
.filter((config, j) => j < i)
.map((config) => config.cols.length - lessCol)
.reduce((a, b) => a + b, 0) * COL_WIDTH,
},
function () {
return s.domainConfigs[i];
}.bind(this),
]);
let y = 0;
DAY_NAMES_SHORT.forEach((dayName, i) => {
if([1, 3, 5].includes(i)) {
let dayText = makeText('subdomain-name', -COL_WIDTH/2, y, dayName,
{
fontSize: HEATMAP_SQUARE_SIZE,
dy: 8,
textAnchor: 'end'
}
);
this.drawArea.appendChild(dayText);
}
y += ROW_HEIGHT;
});
}
this.components = new Map(
componentConfigs.map((args, i) => {
let component = getComponent(...args);
return [args[0] + "-" + i, component];
})
);
update(data) {
if(!data) {
console.error('No data to update.');
}
let y = 0;
DAY_NAMES_SHORT.forEach((dayName, i) => {
if ([1, 3, 5].includes(i)) {
let dayText = makeText("subdomain-name", -COL_WIDTH / 2, y, dayName, {
fontSize: HEATMAP_SQUARE_SIZE,
dy: 8,
textAnchor: "end",
});
this.drawArea.appendChild(dayText);
}
y += ROW_HEIGHT;
});
}
this.data = this.prepareData(data);
this.draw();
this.bindTooltip();
}
update(data) {
if (!data) {
console.error("No data to update.");
}
bindTooltip() {
this.container.addEventListener('mousemove', (e) => {
this.components.forEach(comp => {
let daySquares = comp.store;
let daySquare = e.target;
if(daySquares.includes(daySquare)) {
this.data = this.prepareData(data);
this.draw();
this.bindTooltip();
}
let count = daySquare.getAttribute('data-value');
let dateParts = daySquare.getAttribute('data-date').split('-');
bindTooltip() {
this.container.addEventListener("mousemove", (e) => {
this.components.forEach((comp) => {
let daySquares = comp.store;
let daySquare = e.target;
if (daySquares.includes(daySquare)) {
let count = daySquare.getAttribute("data-value");
let dateParts = daySquare.getAttribute("data-date").split("-");
let month = getMonthName(parseInt(dateParts[1])-1, true);
let month = getMonthName(parseInt(dateParts[1]) - 1, true);
let gOff = this.container.getBoundingClientRect(), pOff = daySquare.getBoundingClientRect();
let gOff = this.container.getBoundingClientRect(),
pOff = daySquare.getBoundingClientRect();
let width = parseInt(e.target.getAttribute('width'));
let x = pOff.left - gOff.left + width/2;
let y = pOff.top - gOff.top;
let value = count + ' ' + this.countLabel;
let name = ' on ' + month + ' ' + dateParts[0] + ', ' + dateParts[2];
let width = parseInt(e.target.getAttribute("width"));
let x = pOff.left - gOff.left + width / 2;
let y = pOff.top - gOff.top;
let value = count + " " + this.countLabel;
let name = " on " + month + " " + dateParts[0] + ", " + dateParts[2];
this.tip.setValues(x, y, {name: name, value: value, valueFirst: 1}, []);
this.tip.showTip();
}
});
});
}
this.tip.setValues(
x,
y,
{ name: name, value: value, valueFirst: 1 },
[]
);
this.tip.showTip();
}
});
});
}
renderLegend() {
this.legendArea.textContent = '';
let x = 0;
let y = ROW_HEIGHT;
let radius = this.rawChartArgs.radius || 0;
renderLegend() {
this.legendArea.textContent = "";
let x = 0;
let y = ROW_HEIGHT;
let radius = this.rawChartArgs.radius || 0;
let lessText = makeText('subdomain-name', x, y, 'Less',
{
fontSize: HEATMAP_SQUARE_SIZE + 1,
dy: 9
}
);
x = (COL_WIDTH * 2) + COL_WIDTH/2;
this.legendArea.appendChild(lessText);
let lessText = makeText("subdomain-name", x, y, "Less", {
fontSize: HEATMAP_SQUARE_SIZE + 1,
dy: 9,
});
x = COL_WIDTH * 2 + COL_WIDTH / 2;
this.legendArea.appendChild(lessText);
this.colors.slice(0, HEATMAP_DISTRIBUTION_SIZE).map((color, i) => {
const square = heatSquare('heatmap-legend-unit', x + (COL_WIDTH + 3) * i,
y, HEATMAP_SQUARE_SIZE, radius, color);
this.legendArea.appendChild(square);
});
this.colors.slice(0, HEATMAP_DISTRIBUTION_SIZE).map((color, i) => {
const square = heatSquare(
"heatmap-legend-unit",
x + (COL_WIDTH + 3) * i,
y,
HEATMAP_SQUARE_SIZE,
radius,
color
);
this.legendArea.appendChild(square);
});
let moreTextX = x + HEATMAP_DISTRIBUTION_SIZE * (COL_WIDTH + 3) + COL_WIDTH/4;
let moreText = makeText('subdomain-name', moreTextX, y, 'More',
{
fontSize: HEATMAP_SQUARE_SIZE + 1,
dy: 9
}
);
this.legendArea.appendChild(moreText);
}
let moreTextX =
x + HEATMAP_DISTRIBUTION_SIZE * (COL_WIDTH + 3) + COL_WIDTH / 4;
let moreText = makeText("subdomain-name", moreTextX, y, "More", {
fontSize: HEATMAP_SQUARE_SIZE + 1,
dy: 9,
});
this.legendArea.appendChild(moreText);
}
getDomains() {
let s = this.state;
const [startMonth, startYear] = [s.start.getMonth(), s.start.getFullYear()];
const [endMonth, endYear] = [s.end.getMonth(), s.end.getFullYear()];
getDomains() {
let s = this.state;
const [startMonth, startYear] = [s.start.getMonth(), s.start.getFullYear()];
const [endMonth, endYear] = [s.end.getMonth(), s.end.getFullYear()];
const noOfMonths = (endMonth - startMonth + 1) + (endYear - startYear) * 12;
const noOfMonths = endMonth - startMonth + 1 + (endYear - startYear) * 12;
let domainConfigs = [];
let domainConfigs = [];
let startOfMonth = clone(s.start);
for(var i = 0; i < noOfMonths; i++) {
let endDate = s.end;
if(!areInSameMonth(startOfMonth, s.end)) {
let [month, year] = [startOfMonth.getMonth(), startOfMonth.getFullYear()];
endDate = getLastDateInMonth(month, year);
}
domainConfigs.push(this.getDomainConfig(startOfMonth, endDate));
let startOfMonth = clone(s.start);
for (var i = 0; i < noOfMonths; i++) {
let endDate = s.end;
if (!areInSameMonth(startOfMonth, s.end)) {
let [month, year] = [
startOfMonth.getMonth(),
startOfMonth.getFullYear(),
];
endDate = getLastDateInMonth(month, year);
}
domainConfigs.push(this.getDomainConfig(startOfMonth, endDate));
addDays(endDate, 1);
startOfMonth = endDate;
}
addDays(endDate, 1);
startOfMonth = endDate;
}
return domainConfigs;
}
return domainConfigs;
}
getDomainConfig(startDate, endDate='') {
let [month, year] = [startDate.getMonth(), startDate.getFullYear()];
let startOfWeek = setDayToSunday(startDate); // TODO: Monday as well
endDate = clone(endDate) || getLastDateInMonth(month, year);
getDomainConfig(startDate, endDate = "") {
let [month, year] = [startDate.getMonth(), startDate.getFullYear()];
let startOfWeek = setDayToSunday(startDate); // TODO: Monday as well
endDate = endDate
? clone(endDate)
: toMidnightUTC(getLastDateInMonth(month, year));
let domainConfig = {
index: month,
cols: []
};
let domainConfig = {
index: month,
cols: [],
};
addDays(endDate, 1);
let noOfMonthWeeks = getWeeksBetween(startOfWeek, endDate);
addDays(endDate, 1);
let noOfMonthWeeks = getWeeksBetween(startOfWeek, endDate);
let cols = [], col;
for(var i = 0; i < noOfMonthWeeks; i++) {
col = this.getCol(startOfWeek, month);
cols.push(col);
let cols = [],
col;
for (var i = 0; i < noOfMonthWeeks; i++) {
col = this.getCol(startOfWeek, month);
cols.push(col);
startOfWeek = new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd);
addDays(startOfWeek, 1);
}
startOfWeek = toMidnightUTC(
new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd)
);
addDays(startOfWeek, 1);
}
if(col[NO_OF_DAYS_IN_WEEK - 1].dataValue !== undefined) {
addDays(startOfWeek, 1);
cols.push(this.getCol(startOfWeek, month, true));
}
if (col[NO_OF_DAYS_IN_WEEK - 1].dataValue !== undefined) {
addDays(startOfWeek, 1);
cols.push(this.getCol(startOfWeek, month, true));
}
domainConfig.cols = cols;
domainConfig.cols = cols;
return domainConfig;
}
return domainConfig;
}
getCol(startDate, month, empty = false) {
let s = this.state;
getCol(startDate, month, empty = false) {
let s = this.state;
// startDate is the start of week
let currentDate = clone(startDate);
let col = [];
// startDate is the start of week
let currentDate = clone(startDate);
let col = [];
for(var i = 0; i < NO_OF_DAYS_IN_WEEK; i++, addDays(currentDate, 1)) {
let config = {};
for (var i = 0; i < NO_OF_DAYS_IN_WEEK; i++, addDays(currentDate, 1)) {
let config = {};
// Non-generic adjustment for entire heatmap, needs state
let currentDateWithinData = currentDate >= s.start && currentDate <= s.end;
// Non-generic adjustment for entire heatmap, needs state
let currentDateWithinData =
currentDate >= s.start && currentDate <= s.end;
if(empty || currentDate.getMonth() !== month || !currentDateWithinData) {
config.yyyyMmDd = getYyyyMmDd(currentDate);
} else {
config = this.getSubDomainConfig(currentDate);
}
col.push(config);
}
if (empty || currentDate.getMonth() !== month || !currentDateWithinData) {
config.yyyyMmDd = getYyyyMmDd(currentDate);
} else {
config = this.getSubDomainConfig(currentDate);
}
col.push(config);
}
return col;
}
return col;
}
getSubDomainConfig(date) {
let yyyyMmDd = getYyyyMmDd(date);
let dataValue = this.data.dataPoints[yyyyMmDd];
let config = {
yyyyMmDd: yyyyMmDd,
dataValue: dataValue || 0,
fill: this.colors[getMaxCheckpoint(dataValue, this.state.distribution)]
};
return config;
}
getSubDomainConfig(date) {
let yyyyMmDd = getYyyyMmDd(date);
let dataValue = this.data.dataPoints[yyyyMmDd];
let config = {
yyyyMmDd: yyyyMmDd,
dataValue: dataValue || 0,
fill: this.colors[getMaxCheckpoint(dataValue, this.state.distribution)],
};
return config;
}
}

View File

@ -1,173 +0,0 @@
import AxisChart from './AxisChart';
import { Y_AXIS_MARGIN } from '../utils/constants';
// import { ChartComponent } from '../objects/ChartComponents';
import { floatTwo } from '../utils/helpers';
export default class MultiAxisChart extends AxisChart {
constructor(args) {
super(args);
// this.unitType = args.unitType || 'line';
// this.setup();
}
preSetup() {
this.type = 'multiaxis';
}
setMeasures() {
super.setMeasures();
let noOfLeftAxes = this.data.datasets.filter(d => d.axisPosition === 'left').length;
this.measures.margins.left = (noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN;
this.measures.margins.right = (this.data.datasets.length - noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN;
}
prepareYAxis() { }
prepareData(data) {
super.prepareData(data);
let sets = this.state.datasets;
// let axesLeft = sets.filter(d => d.axisPosition === 'left');
// let axesRight = sets.filter(d => d.axisPosition === 'right');
// let axesNone = sets.filter(d => !d.axisPosition ||
// !['left', 'right'].includes(d.axisPosition));
let leftCount = 0, rightCount = 0;
sets.forEach((d, i) => {
d.yAxis = {
position: d.axisPosition,
index: d.axisPosition === 'left' ? leftCount++ : rightCount++
};
});
}
configure(args) {
super.configure(args);
this.config.xAxisMode = args.xAxisMode || 'tick';
this.config.yAxisMode = args.yAxisMode || 'span';
}
// setUnitWidthAndXOffset() {
// this.state.unitWidth = this.width/(this.state.datasetLength);
// this.state.xOffset = this.state.unitWidth/2;
// }
configUnits() {
this.unitArgs = {
type: 'bar',
args: {
spaceWidth: this.state.unitWidth/2,
}
};
}
setYAxis() {
this.state.datasets.map(d => {
this.calcYAxisParameters(d.yAxis, d.values, this.unitType === 'line');
});
}
calcYUnits() {
this.state.datasets.map(d => {
d.positions = d.values.map(val => floatTwo(d.yAxis.zeroLine - val * d.yAxis.scaleMultiplier));
});
}
// TODO: function doesn't exist, handle with components
renderConstants() {
this.state.datasets.map(d => {
let guidePos = d.yAxis.position === 'left'
? -1 * d.yAxis.index * Y_AXIS_MARGIN
: this.width + d.yAxis.index * Y_AXIS_MARGIN;
this.renderer.xLine(guidePos, '', {
pos:'top',
mode: 'span',
stroke: this.colors[i],
className: 'y-axis-guide'
})
});
}
getYAxesComponents() {
return this.data.datasets.map((e, i) => {
return new ChartComponent({
layerClass: 'y axis y-axis-' + i,
make: () => {
let yAxis = this.state.datasets[i].yAxis;
this.renderer.setZeroline(yAxis.zeroline);
let options = {
pos: yAxis.position,
mode: 'tick',
offset: yAxis.index * Y_AXIS_MARGIN,
stroke: this.colors[i]
};
return yAxis.positions.map((position, j) =>
this.renderer.yLine(position, yAxis.labels[j], options)
);
},
animate: () => {}
});
});
}
// TODO remove renderer zeroline from above and below
getChartComponents() {
return this.data.datasets.map((d, index) => {
return new ChartComponent({
layerClass: 'dataset-units dataset-' + index,
make: () => {
let d = this.state.datasets[index];
let unitType = this.unitArgs;
// the only difference, should be tied to datasets or default
this.renderer.setZeroline(d.yAxis.zeroLine);
return d.positions.map((y, j) => {
return this.renderer[unitType.type](
this.state.xAxisPositions[j],
y,
unitType.args,
this.colors[index],
j,
index,
this.state.datasetLength
);
});
},
animate: (svgUnits) => {
let d = this.state.datasets[index];
let unitType = this.unitArgs.type;
// have been updated in axis render;
let newX = this.state.xAxisPositions;
let newY = this.state.datasets[index].positions;
let lastUnit = svgUnits[svgUnits.length - 1];
let parentNode = lastUnit.parentNode;
if(this.oldState.xExtra > 0) {
for(var i = 0; i<this.oldState.xExtra; i++) {
let unit = lastUnit.cloneNode(true);
parentNode.appendChild(unit);
svgUnits.push(unit);
}
}
this.renderer.setZeroline(d.yAxis.zeroLine);
svgUnits.map((unit, i) => {
if(newX[i] === undefined || newY[i] === undefined) return;
this.elementsToAnimate.push(this.renderer['animate' + unitType](
unit, // unit, with info to replace where it came from in the data
newX[i],
newY[i],
index,
this.state.noOfDatasets
));
});
}
});
});
}
}

View File

@ -1,92 +1,96 @@
import AggregationChart from './AggregationChart';
import { getOffset } from '../utils/dom';
import { getComponent } from '../objects/ChartComponents';
import { PERCENTAGE_BAR_DEFAULT_HEIGHT, PERCENTAGE_BAR_DEFAULT_DEPTH } from '../utils/constants';
import AggregationChart from "./AggregationChart";
import { getOffset } from "../utils/dom";
import { getComponent } from "../objects/ChartComponents";
import { PERCENTAGE_BAR_DEFAULT_HEIGHT } from "../utils/constants";
export default class PercentageChart extends AggregationChart {
constructor(parent, args) {
super(parent, args);
this.type = 'percentage';
this.setup();
}
constructor(parent, args) {
super(parent, args);
this.type = "percentage";
this.setup();
}
setMeasures(options) {
let m = this.measures;
this.barOptions = options.barOptions || {};
setMeasures(options) {
let m = this.measures;
this.barOptions = options.barOptions || {};
let b = this.barOptions;
b.height = b.height || PERCENTAGE_BAR_DEFAULT_HEIGHT;
b.depth = b.depth || PERCENTAGE_BAR_DEFAULT_DEPTH;
let b = this.barOptions;
b.height = b.height || PERCENTAGE_BAR_DEFAULT_HEIGHT;
m.paddings.right = 30;
m.legendHeight = 60;
m.baseHeight = (b.height + b.depth * 0.5) * 8;
}
m.paddings.right = 30;
m.legendHeight = 60;
m.baseHeight = (b.height + b.depth * 0.5) * 8;
}
setupComponents() {
let s = this.state;
setupComponents() {
let s = this.state;
let componentConfigs = [
[
'percentageBars',
{
barHeight: this.barOptions.height,
barDepth: this.barOptions.depth,
},
function() {
return {
xPositions: s.xPositions,
widths: s.widths,
colors: this.colors
};
}.bind(this)
]
];
let componentConfigs = [
[
"percentageBars",
{
barHeight: this.barOptions.height,
},
function () {
return {
xPositions: s.xPositions,
widths: s.widths,
colors: this.colors,
};
}.bind(this),
],
];
this.components = new Map(componentConfigs
.map(args => {
let component = getComponent(...args);
return [args[0], component];
}));
}
this.components = new Map(
componentConfigs.map((args) => {
let component = getComponent(...args);
return [args[0], component];
})
);
}
calc() {
super.calc();
let s = this.state;
calc() {
super.calc();
let s = this.state;
s.xPositions = [];
s.widths = [];
s.xPositions = [];
s.widths = [];
let xPos = 0;
s.sliceTotals.map((value) => {
let width = this.width * value / s.grandTotal;
s.widths.push(width);
s.xPositions.push(xPos);
xPos += width;
});
}
let xPos = 0;
s.sliceTotals.map((value) => {
let width = (this.width * value) / s.grandTotal;
s.widths.push(width);
s.xPositions.push(xPos);
xPos += width;
});
}
makeDataByIndex() { }
makeDataByIndex() {}
bindTooltip() {
let s = this.state;
this.container.addEventListener('mousemove', (e) => {
let bars = this.components.get('percentageBars').store;
let bar = e.target;
if(bars.includes(bar)) {
bindTooltip() {
let s = this.state;
this.container.addEventListener("mousemove", (e) => {
let bars = this.components.get("percentageBars").store;
let bar = e.target;
if (bars.includes(bar)) {
let i = bars.indexOf(bar);
let gOff = getOffset(this.container),
pOff = getOffset(bar);
let i = bars.indexOf(bar);
let gOff = getOffset(this.container), pOff = getOffset(bar);
let x = pOff.left - gOff.left + parseInt(bar.getAttribute("width")) / 2;
let y = pOff.top - gOff.top;
let title =
(this.formattedLabels && this.formattedLabels.length > 0
? this.formattedLabels[i]
: this.state.labels[i]) + ": ";
let fraction = s.sliceTotals[i] / s.grandTotal;
let x = pOff.left - gOff.left + parseInt(bar.getAttribute('width'))/2;
let y = pOff.top - gOff.top;
let title = (this.formattedLabels && this.formattedLabels.length>0
? this.formattedLabels[i] : this.state.labels[i]) + ': ';
let fraction = s.sliceTotals[i]/s.grandTotal;
this.tip.setValues(x, y, {name: title, value: (fraction*100).toFixed(1) + "%"});
this.tip.showTip();
}
});
}
this.tip.setValues(x, y, {
name: title,
value: (fraction * 100).toFixed(1) + "%",
});
this.tip.showTip();
}
});
}
}

View File

@ -1,155 +1,207 @@
import AggregationChart from './AggregationChart';
import { getComponent } from '../objects/ChartComponents';
import { getOffset } from '../utils/dom';
import { getPositionByAngle } from '../utils/helpers';
import { makeArcPathStr, makeCircleStr } from '../utils/draw';
import { lightenDarkenColor } from '../utils/colors';
import { transform } from '../utils/animation';
import { FULL_ANGLE } from '../utils/constants';
import AggregationChart from "./AggregationChart";
import { getComponent } from "../objects/ChartComponents";
import { getOffset, fire } from "../utils/dom";
import { getPositionByAngle } from "../utils/helpers";
import { makeArcPathStr, makeCircleStr } from "../utils/draw";
import { lightenDarkenColor } from "../utils/colors";
import { transform } from "../utils/animation";
import { FULL_ANGLE } from "../utils/constants";
export default class PieChart extends AggregationChart {
constructor(parent, args) {
super(parent, args);
this.type = 'pie';
this.initTimeout = 0;
this.init = 1;
constructor(parent, args) {
super(parent, args);
this.type = "pie";
this.initTimeout = 0;
this.init = 1;
this.setup();
}
this.setup();
}
configure(args) {
super.configure(args);
this.mouseMove = this.mouseMove.bind(this);
this.mouseLeave = this.mouseLeave.bind(this);
configure(args) {
super.configure(args);
this.mouseMove = this.mouseMove.bind(this);
this.mouseLeave = this.mouseLeave.bind(this);
this.hoverRadio = args.hoverRadio || 0.1;
this.config.startAngle = args.startAngle || 0;
this.hoverRadio = args.hoverRadio || 0.1;
this.config.startAngle = args.startAngle || 0;
this.clockWise = args.clockWise || false;
}
this.clockWise = args.clockWise || false;
}
calc() {
super.calc();
let s = this.state;
this.radius = (this.height > this.width ? this.center.x : this.center.y);
calc() {
super.calc();
let s = this.state;
this.radius = this.height > this.width ? this.center.x : this.center.y;
const { radius, clockWise } = this;
const { radius, clockWise } = this;
const prevSlicesProperties = s.slicesProperties || [];
s.sliceStrings = [];
s.slicesProperties = [];
let curAngle = 180 - this.config.startAngle;
s.sliceTotals.map((total, i) => {
const startAngle = curAngle;
const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE;
const largeArc = originDiffAngle > 180 ? 1: 0;
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;
const endAngle = curAngle = curAngle + diffAngle;
const startPosition = getPositionByAngle(startAngle, radius);
const endPosition = getPositionByAngle(endAngle, radius);
const prevSlicesProperties = s.slicesProperties || [];
s.sliceStrings = [];
s.slicesProperties = [];
let curAngle = 180 - this.config.startAngle;
const prevProperty = this.init && prevSlicesProperties[i];
s.sliceTotals.map((total, i) => {
const startAngle = curAngle;
const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE;
const largeArc = originDiffAngle > 180 ? 1 : 0;
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;
const endAngle = (curAngle = curAngle + diffAngle);
const startPosition = getPositionByAngle(startAngle, radius);
const endPosition = getPositionByAngle(endAngle, radius);
let curStart,curEnd;
if(this.init) {
curStart = prevProperty ? prevProperty.startPosition : startPosition;
curEnd = prevProperty ? prevProperty.endPosition : startPosition;
} else {
curStart = startPosition;
curEnd = endPosition;
}
const curPath =
originDiffAngle === 360
? makeCircleStr(curStart, curEnd, this.center, this.radius, clockWise, largeArc)
: makeArcPathStr(curStart, curEnd, this.center, this.radius, clockWise, largeArc);
const prevProperty = this.init && prevSlicesProperties[i];
s.sliceStrings.push(curPath);
s.slicesProperties.push({
startPosition,
endPosition,
value: total,
total: s.grandTotal,
startAngle,
endAngle,
angle: diffAngle
});
let curStart, curEnd;
if (this.init) {
curStart = prevProperty ? prevProperty.startPosition : startPosition;
curEnd = prevProperty ? prevProperty.endPosition : startPosition;
} else {
curStart = startPosition;
curEnd = endPosition;
}
const curPath =
originDiffAngle === 360
? makeCircleStr(
curStart,
curEnd,
this.center,
this.radius,
clockWise,
largeArc
)
: makeArcPathStr(
curStart,
curEnd,
this.center,
this.radius,
clockWise,
largeArc
);
});
this.init = 0;
}
s.sliceStrings.push(curPath);
s.slicesProperties.push({
startPosition,
endPosition,
value: total,
total: s.grandTotal,
startAngle,
endAngle,
angle: diffAngle,
});
});
this.init = 0;
}
setupComponents() {
let s = this.state;
setupComponents() {
let s = this.state;
let componentConfigs = [
[
'pieSlices',
{ },
function() {
return {
sliceStrings: s.sliceStrings,
colors: this.colors
};
}.bind(this)
]
];
let componentConfigs = [
[
"pieSlices",
{},
function () {
return {
sliceStrings: s.sliceStrings,
colors: this.colors,
};
}.bind(this),
],
];
this.components = new Map(componentConfigs
.map(args => {
let component = getComponent(...args);
return [args[0], component];
}));
}
this.components = new Map(
componentConfigs.map((args) => {
let component = getComponent(...args);
return [args[0], component];
})
);
}
calTranslateByAngle(property){
const{radius,hoverRadio} = this;
const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius);
return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`;
}
calTranslateByAngle(property) {
const { radius, hoverRadio } = this;
const position = getPositionByAngle(
property.startAngle + property.angle / 2,
radius
);
return `translate3d(${position.x * hoverRadio}px,${
position.y * hoverRadio
}px,0)`;
}
hoverSlice(path,i,flag,e){
if(!path) return;
const color = this.colors[i];
if(flag) {
transform(path, this.calTranslateByAngle(this.state.slicesProperties[i]));
path.style.fill = lightenDarkenColor(color, 50);
let g_off = getOffset(this.svg);
let x = e.pageX - g_off.left + 10;
let y = e.pageY - g_off.top - 10;
let title = (this.formatted_labels && this.formatted_labels.length > 0
? this.formatted_labels[i] : this.state.labels[i]) + ': ';
let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1);
this.tip.setValues(x, y, {name: title, value: percent + "%"});
this.tip.showTip();
} else {
transform(path,'translate3d(0,0,0)');
this.tip.hideTip();
path.style.fill = color;
}
}
hoverSlice(path, i, flag, e) {
if (!path) return;
const color = this.colors[i];
if (flag) {
transform(path, this.calTranslateByAngle(this.state.slicesProperties[i]));
path.style.fill = lightenDarkenColor(color, 50);
let g_off = getOffset(this.svg);
let x = e.pageX - g_off.left + 10;
let y = e.pageY - g_off.top - 10;
let title =
(this.formatted_labels && this.formatted_labels.length > 0
? this.formatted_labels[i]
: this.state.labels[i]) + ": ";
let percent = (
(this.state.sliceTotals[i] * 100) /
this.state.grandTotal
).toFixed(1);
this.tip.setValues(x, y, { name: title, value: percent + "%" });
this.tip.showTip();
} else {
transform(path, "translate3d(0,0,0)");
this.tip.hideTip();
path.style.fill = color;
}
}
bindTooltip() {
this.container.addEventListener('mousemove', this.mouseMove);
this.container.addEventListener('mouseleave', this.mouseLeave);
}
bindTooltip() {
this.container.addEventListener("mousemove", this.mouseMove);
this.container.addEventListener("mouseleave", this.mouseLeave);
}
getDataPoint(index = this.state.currentIndex) {
let s = this.state;
let data_point = {
index: index,
label: s.labels[index],
values: s.sliceTotals[index],
};
return data_point;
}
setCurrentDataPoint(index) {
let s = this.state;
index = parseInt(index);
if (index < 0) index = 0;
if (index >= s.labels.length) index = s.labels.length - 1;
if (index === s.currentIndex) return;
s.currentIndex = index;
fire(this.parent, "data-select", this.getDataPoint());
}
mouseMove(e){
const target = e.target;
let slices = this.components.get('pieSlices').store;
let prevIndex = this.curActiveSliceIndex;
let prevAcitve = this.curActiveSlice;
if(slices.includes(target)) {
let i = slices.indexOf(target);
this.hoverSlice(prevAcitve, prevIndex,false);
this.curActiveSlice = target;
this.curActiveSliceIndex = i;
this.hoverSlice(target, i, true, e);
} else {
this.mouseLeave();
}
}
bindUnits() {
const units = this.components.get("pieSlices").store;
if (!units) return;
units.forEach((unit, index) => {
unit.addEventListener("click", () => {
this.setCurrentDataPoint(index);
});
});
}
mouseMove(e) {
const target = e.target;
let slices = this.components.get("pieSlices").store;
let prevIndex = this.curActiveSliceIndex;
let prevAcitve = this.curActiveSlice;
if (slices.includes(target)) {
let i = slices.indexOf(target);
this.hoverSlice(prevAcitve, prevIndex, false);
this.curActiveSlice = target;
this.curActiveSliceIndex = i;
this.hoverSlice(target, i, true, e);
} else {
this.mouseLeave();
}
}
mouseLeave(){
this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false);
}
mouseLeave() {
this.hoverSlice(this.curActiveSlice, this.curActiveSliceIndex, false);
}
}

View File

@ -1,10 +1,10 @@
import * as Charts from './chart';
import * as Charts from "./chart";
let frappe = { };
let frappe = {};
frappe.NAME = 'Frappe Charts';
frappe.VERSION = '1.5.5';
frappe.NAME = "Frappe Charts";
frappe.VERSION = "1.6.2";
frappe = Object.assign({ }, frappe, Charts);
frappe = Object.assign({}, frappe, Charts);
export default frappe;
export default frappe;

View File

@ -1,19 +1,39 @@
import { makeSVGGroup } from '../utils/draw';
import { makeText, makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, percentageBar, getPaths, heatSquare } from '../utils/draw';
import { equilizeNoOfElements } from '../utils/draw-utils';
import { translateHoriLine, translateVertLine, animateRegion, animateBar,
animateDot, animatePath, animatePathStr } from '../utils/animate';
import { getMonthName } from '../utils/date-utils';
import { makeSVGGroup } from "../utils/draw";
import {
makeText,
makePath,
xLine,
yLine,
generateAxisLabel,
yMarker,
yRegion,
datasetBar,
datasetDot,
percentageBar,
getPaths,
heatSquare,
} from "../utils/draw";
import { equilizeNoOfElements } from "../utils/draw-utils";
import {
translateHoriLine,
translateVertLine,
animateRegion,
animateBar,
animateDot,
animatePath,
animatePathStr,
} from "../utils/animate";
import { getMonthName } from "../utils/date-utils";
class ChartComponent {
constructor({
layerClass = '',
layerTransform = '',
layerClass = "",
layerTransform = "",
constants,
getData,
makeElements,
animateElements
animateElements,
}) {
this.layerTransform = layerTransform;
this.constants = constants;
@ -27,8 +47,10 @@ class ChartComponent {
this.labels = [];
this.layerClass = layerClass;
this.layerClass = typeof(this.layerClass) === 'function'
? this.layerClass() : this.layerClass;
this.layerClass =
typeof this.layerClass === "function"
? this.layerClass()
: this.layerClass;
this.refresh();
}
@ -49,11 +71,15 @@ class ChartComponent {
render(data) {
this.store = this.makeElements(data);
this.layer.textContent = '';
this.store.forEach(element => {
this.layer.appendChild(element);
this.layer.textContent = "";
this.store.forEach((element) => {
element.length
? element.forEach((el) => {
this.layer.appendChild(el);
})
: this.layer.appendChild(element);
});
this.labels.forEach(element => {
this.labels.forEach((element) => {
this.layer.appendChild(element);
});
}
@ -61,7 +87,7 @@ class ChartComponent {
update(animate = true) {
this.refresh();
let animateElements = [];
if(animate) {
if (animate) {
animateElements = this.animateElements(this.data) || [];
}
return animateElements;
@ -70,25 +96,17 @@ class ChartComponent {
let componentConfigs = {
donutSlices: {
layerClass: 'donut-slices',
layerClass: "donut-slices",
makeElements(data) {
return data.sliceStrings.map((s, i) => {
let slice = makePath(s, 'donut-path', data.colors[i], 'none', data.strokeWidth);
slice.style.transition = 'transform .3s;';
return slice;
});
},
animateElements(newData) {
return this.store.map((slice, i) => animatePathStr(slice, newData.sliceStrings[i]));
},
},
pieSlices: {
layerClass: 'pie-slices',
makeElements(data) {
return data.sliceStrings.map((s, i) =>{
let slice = makePath(s, 'pie-path', 'none', data.colors[i]);
slice.style.transition = 'transform .3s;';
let slice = makePath(
s,
"donut-path",
data.colors[i],
"none",
data.strokeWidth
);
slice.style.transition = "transform .3s;";
return slice;
});
},
@ -97,33 +115,145 @@ let componentConfigs = {
return this.store.map((slice, i) =>
animatePathStr(slice, newData.sliceStrings[i])
);
}
},
},
pieSlices: {
layerClass: "pie-slices",
makeElements(data) {
return data.sliceStrings.map((s, i) => {
let slice = makePath(s, "pie-path", "none", data.colors[i]);
slice.style.transition = "transform .3s;";
return slice;
});
},
animateElements(newData) {
return this.store.map((slice, i) =>
animatePathStr(slice, newData.sliceStrings[i])
);
},
},
percentageBars: {
layerClass: 'percentage-bars',
layerClass: "percentage-bars",
makeElements(data) {
return data.xPositions.map((x, i) =>{
const numberOfPoints = data.xPositions.length;
return data.xPositions.map((x, i) => {
let y = 0;
let bar = percentageBar(x, y, data.widths[i],
this.constants.barHeight, this.constants.barDepth, data.colors[i]);
let isLast = i == numberOfPoints - 1;
let isFirst = i == 0;
let bar = percentageBar(
x,
y,
data.widths[i],
this.constants.barHeight,
isFirst,
isLast,
data.colors[i]
);
return bar;
});
},
animateElements(newData) {
if(newData) return [];
}
if (newData) return [];
},
},
yAxis: {
layerClass: 'y axis',
layerClass: "y axis",
makeElements(data) {
return data.positions.map((position, i) =>
yLine(position, data.labels[i], this.constants.width,
{mode: this.constants.mode, pos: this.constants.pos, shortenNumbers: this.constants.shortenNumbers})
);
let elements = [];
// will loop through each yaxis dataset if it exists
if (data.length) {
data.forEach((item, i) => {
item.positions.map((position, i) => {
elements.push(
yLine(
position,
item.labels[i],
this.constants.width,
{
mode: this.constants.mode,
pos: item.pos || this.constants.pos,
shortenNumbers:
this.constants.shortenNumbers,
title: item.title,
}
)
);
});
// we need to make yAxis titles if they are defined
if (item.title) {
elements.push(
generateAxisLabel({
title: item.title,
position: item.pos,
height: this.constants.height || data.zeroLine,
width: this.constants.width,
})
);
}
});
return elements;
}
data.positions.forEach((position, i) => {
elements.push(
yLine(position, data.labels[i], this.constants.width, {
mode: this.constants.mode,
pos: data.pos || this.constants.pos,
shortenNumbers: this.constants.shortenNumbers,
})
);
});
if (data.title) {
elements.push(
generateAxisLabel({
title: data.title,
position: data.pos,
height: this.constants.height || data.zeroLine,
width: this.constants.width,
})
);
}
return elements;
},
animateElements(newData) {
const animateMultipleElements = (oldData, newData) => {
let newPos = newData.positions;
let newLabels = newData.labels;
let oldPos = oldData.positions;
let oldLabels = oldData.labels;
[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
[oldLabels, newLabels] = equilizeNoOfElements(
oldLabels,
newLabels
);
this.render({
positions: oldPos,
labels: newLabels,
});
return this.store.map((line, i) => {
return translateHoriLine(line, newPos[i], oldPos[i]);
});
};
// we will need to animate both axis if we have more than one.
// so check if the oldData is an array of values.
if (this.oldData instanceof Array) {
return this.oldData.forEach((old, i) => {
animateMultipleElements(old, newData[i]);
});
}
let newPos = newData.positions;
let newLabels = newData.labels;
let oldPos = this.oldData.positions;
@ -134,23 +264,23 @@ let componentConfigs = {
this.render({
positions: oldPos,
labels: newLabels
labels: newLabels,
});
return this.store.map((line, i) => {
return translateHoriLine(
line, newPos[i], oldPos[i]
);
return translateHoriLine(line, newPos[i], oldPos[i]);
});
}
},
},
xAxis: {
layerClass: 'x axis',
layerClass: "x axis",
makeElements(data) {
return data.positions.map((position, i) =>
xLine(position, data.calcLabels[i], this.constants.height,
{mode: this.constants.mode, pos: this.constants.pos})
xLine(position, data.calcLabels[i], this.constants.height, {
mode: this.constants.mode,
pos: this.constants.pos,
})
);
},
@ -165,117 +295,144 @@ let componentConfigs = {
this.render({
positions: oldPos,
calcLabels: newLabels
calcLabels: newLabels,
});
return this.store.map((line, i) => {
return translateVertLine(
line, newPos[i], oldPos[i]
);
return translateVertLine(line, newPos[i], oldPos[i]);
});
}
},
},
yMarkers: {
layerClass: 'y-markers',
layerClass: "y-markers",
makeElements(data) {
return data.map(m =>
yMarker(m.position, m.label, this.constants.width,
{labelPos: m.options.labelPos, mode: 'span', lineType: 'dashed'})
return data.map((m) =>
yMarker(m.position, m.label, this.constants.width, {
labelPos: m.options.labelPos,
stroke: m.options.stroke,
mode: "span",
lineType: m.options.lineType,
})
);
},
animateElements(newData) {
[this.oldData, newData] = equilizeNoOfElements(this.oldData, newData);
[this.oldData, newData] = equilizeNoOfElements(
this.oldData,
newData
);
let newPos = newData.map(d => d.position);
let newLabels = newData.map(d => d.label);
let newOptions = newData.map(d => d.options);
let newPos = newData.map((d) => d.position);
let newLabels = newData.map((d) => d.label);
let newOptions = newData.map((d) => d.options);
let oldPos = this.oldData.map(d => d.position);
let oldPos = this.oldData.map((d) => d.position);
this.render(oldPos.map((pos, i) => {
return {
position: oldPos[i],
label: newLabels[i],
options: newOptions[i]
};
}));
this.render(
oldPos.map((pos, i) => {
return {
position: oldPos[i],
label: newLabels[i],
options: newOptions[i],
};
})
);
return this.store.map((line, i) => {
return translateHoriLine(
line, newPos[i], oldPos[i]
);
return translateHoriLine(line, newPos[i], oldPos[i]);
});
}
},
},
yRegions: {
layerClass: 'y-regions',
layerClass: "y-regions",
makeElements(data) {
return data.map(r =>
yRegion(r.startPos, r.endPos, this.constants.width,
r.label, {labelPos: r.options.labelPos})
return data.map((r) =>
yRegion(r.startPos, r.endPos, this.constants.width, r.label, {
labelPos: r.options.labelPos,
})
);
},
animateElements(newData) {
[this.oldData, newData] = equilizeNoOfElements(this.oldData, newData);
[this.oldData, newData] = equilizeNoOfElements(
this.oldData,
newData
);
let newPos = newData.map(d => d.endPos);
let newLabels = newData.map(d => d.label);
let newStarts = newData.map(d => d.startPos);
let newOptions = newData.map(d => d.options);
let newPos = newData.map((d) => d.endPos);
let newLabels = newData.map((d) => d.label);
let newStarts = newData.map((d) => d.startPos);
let newOptions = newData.map((d) => d.options);
let oldPos = this.oldData.map(d => d.endPos);
let oldStarts = this.oldData.map(d => d.startPos);
let oldPos = this.oldData.map((d) => d.endPos);
let oldStarts = this.oldData.map((d) => d.startPos);
this.render(oldPos.map((pos, i) => {
return {
startPos: oldStarts[i],
endPos: oldPos[i],
label: newLabels[i],
options: newOptions[i]
};
}));
this.render(
oldPos.map((pos, i) => {
return {
startPos: oldStarts[i],
endPos: oldPos[i],
label: newLabels[i],
options: newOptions[i],
};
})
);
let animateElements = [];
this.store.map((rectGroup, i) => {
animateElements = animateElements.concat(animateRegion(
rectGroup, newStarts[i], newPos[i], oldPos[i]
));
animateElements = animateElements.concat(
animateRegion(rectGroup, newStarts[i], newPos[i], oldPos[i])
);
});
return animateElements;
}
},
},
heatDomain: {
layerClass: function() { return 'heat-domain domain-' + this.constants.index; },
layerClass: function () {
return "heat-domain domain-" + this.constants.index;
},
makeElements(data) {
let {index, colWidth, rowHeight, squareSize, radius, xTranslate} = this.constants;
let { index, colWidth, rowHeight, squareSize, radius, xTranslate } =
this.constants;
let monthNameHeight = -12;
let x = xTranslate, y = 0;
let x = xTranslate,
y = 0;
this.serializedSubDomains = [];
data.cols.map((week, weekNo) => {
if(weekNo === 1) {
if (weekNo === 1) {
this.labels.push(
makeText('domain-name', x, monthNameHeight, getMonthName(index, true).toUpperCase(),
makeText(
"domain-name",
x,
monthNameHeight,
getMonthName(index, true).toUpperCase(),
{
fontSize: 9
fontSize: 9,
}
)
);
}
week.map((day, i) => {
if(day.fill) {
if (day.fill) {
let data = {
'data-date': day.yyyyMmDd,
'data-value': day.dataValue,
'data-day': i
"data-date": day.yyyyMmDd,
"data-value": day.dataValue,
"data-day": i,
};
let square = heatSquare('day', x, y, squareSize, radius, day.fill, data);
let square = heatSquare(
"day",
x,
y,
squareSize,
radius,
day.fill,
data
);
this.serializedSubDomains.push(square);
}
y += rowHeight;
@ -288,15 +445,17 @@ let componentConfigs = {
},
animateElements(newData) {
if(newData) return [];
}
if (newData) return [];
},
},
barGraph: {
layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; },
layerClass: function () {
return "dataset-units dataset-bars dataset-" + this.constants.index;
},
makeElements(data) {
let c = this.constants;
this.unitType = 'bar';
this.unitType = "bar";
this.units = data.yPositions.map((y, j) => {
return datasetBar(
data.xPositions[j],
@ -309,7 +468,7 @@ let componentConfigs = {
{
zeroLine: data.zeroLine,
barsWidth: data.barsWidth,
minHeight: c.minHeight
minHeight: c.minHeight,
}
);
});
@ -328,7 +487,10 @@ let componentConfigs = {
[oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos);
[oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos);
[oldOffsets, newOffsets] = equilizeNoOfElements(oldOffsets, newOffsets);
[oldOffsets, newOffsets] = equilizeNoOfElements(
oldOffsets,
newOffsets
);
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);
this.render({
@ -345,23 +507,31 @@ let componentConfigs = {
let animateElements = [];
this.store.map((bar, i) => {
animateElements = animateElements.concat(animateBar(
bar, newXPos[i], newYPos[i], newData.barWidth, newOffsets[i],
{zeroLine: newData.zeroLine}
));
animateElements = animateElements.concat(
animateBar(
bar,
newXPos[i],
newYPos[i],
newData.barWidth,
newOffsets[i],
{ zeroLine: newData.zeroLine }
)
);
});
return animateElements;
}
},
},
lineGraph: {
layerClass: function() { return 'dataset-units dataset-line dataset-' + this.constants.index; },
layerClass: function () {
return "dataset-units dataset-line dataset-" + this.constants.index;
},
makeElements(data) {
let c = this.constants;
this.unitType = 'dot';
this.unitType = "dot";
this.paths = {};
if(!c.hideLine) {
if (!c.hideLine) {
this.paths = getPaths(
data.xPositions,
data.yPositions,
@ -369,24 +539,24 @@ let componentConfigs = {
{
heatline: c.heatline,
regionFill: c.regionFill,
spline: c.spline
spline: c.spline,
},
{
svgDefs: c.svgDefs,
zeroLine: data.zeroLine
zeroLine: data.zeroLine,
}
);
}
this.units = [];
if(!c.hideDots) {
if (!c.hideDots) {
this.units = data.yPositions.map((y, j) => {
return datasetDot(
data.xPositions[j],
y,
data.radius,
c.color,
(c.valuesOverPoints ? data.values[j] : ''),
c.valuesOverPoints ? data.values[j] : "",
j
);
});
@ -418,29 +588,37 @@ let componentConfigs = {
let animateElements = [];
if(Object.keys(this.paths).length) {
animateElements = animateElements.concat(animatePath(
this.paths, newXPos, newYPos, newData.zeroLine, this.constants.spline));
if (Object.keys(this.paths).length) {
animateElements = animateElements.concat(
animatePath(
this.paths,
newXPos,
newYPos,
newData.zeroLine,
this.constants.spline
)
);
}
if(this.units.length) {
if (this.units.length) {
this.units.map((dot, i) => {
animateElements = animateElements.concat(animateDot(
dot, newXPos[i], newYPos[i]));
animateElements = animateElements.concat(
animateDot(dot, newXPos[i], newYPos[i])
);
});
}
return animateElements;
}
}
},
},
};
export function getComponent(name, constants, getData) {
let keys = Object.keys(componentConfigs).filter(k => name.includes(k));
let keys = Object.keys(componentConfigs).filter((k) => name.includes(k));
let config = componentConfigs[keys[0]];
Object.assign(config, {
constants: constants,
getData: getData
getData: getData,
});
return new ChartComponent(config);
}

View File

@ -1,134 +1,132 @@
import { $ } from '../utils/dom';
import { TOOLTIP_POINTER_TRIANGLE_HEIGHT } from '../utils/constants';
import { $ } from "../utils/dom";
import { TOOLTIP_POINTER_TRIANGLE_HEIGHT } from "../utils/constants";
export default class SvgTip {
constructor({
parent = null,
colors = []
}) {
this.parent = parent;
this.colors = colors;
this.titleName = '';
this.titleValue = '';
this.listValues = [];
this.titleValueFirst = 0;
constructor({ parent = null, colors = [] }) {
this.parent = parent;
this.colors = colors;
this.titleName = "";
this.titleValue = "";
this.listValues = [];
this.titleValueFirst = 0;
this.x = 0;
this.y = 0;
this.x = 0;
this.y = 0;
this.top = 0;
this.left = 0;
this.top = 0;
this.left = 0;
this.setup();
}
this.setup();
}
setup() {
this.makeTooltip();
}
setup() {
this.makeTooltip();
}
refresh() {
this.fill();
this.calcPosition();
}
refresh() {
this.fill();
this.calcPosition();
}
makeTooltip() {
this.container = $.create('div', {
inside: this.parent,
className: 'graph-svg-tip comparison',
innerHTML: `<span class="title"></span>
makeTooltip() {
this.container = $.create("div", {
inside: this.parent,
className: "graph-svg-tip comparison",
innerHTML: `<span class="title"></span>
<ul class="data-point-list"></ul>
<div class="svg-pointer"></div>`
});
this.hideTip();
<div class="svg-pointer"></div>`,
});
this.hideTip();
this.title = this.container.querySelector('.title');
this.list = this.container.querySelector('.data-point-list');
this.dataPointList = this.container.querySelector('.data-point-list');
this.title = this.container.querySelector(".title");
this.list = this.container.querySelector(".data-point-list");
this.dataPointList = this.container.querySelector(".data-point-list");
this.parent.addEventListener('mouseleave', () => {
this.hideTip();
});
}
this.parent.addEventListener("mouseleave", () => {
this.hideTip();
});
}
fill() {
let title;
if(this.index) {
this.container.setAttribute('data-point-index', this.index);
}
if(this.titleValueFirst) {
title = `<strong>${this.titleValue}</strong>${this.titleName}`;
} else {
title = `${this.titleName}<strong>${this.titleValue}</strong>`;
}
fill() {
let title;
if (this.index) {
this.container.setAttribute("data-point-index", this.index);
}
if (this.titleValueFirst) {
title = `<strong>${this.titleValue}</strong>${this.titleName}`;
} else {
title = `${this.titleName}<strong>${this.titleValue}</strong>`;
}
if (this.listValues.length > 4) {
this.list.classList.add('tooltip-grid');
} else {
this.list.classList.remove('tooltip-grid');
}
if (this.listValues.length > 4) {
this.list.classList.add("tooltip-grid");
} else {
this.list.classList.remove("tooltip-grid");
}
this.title.innerHTML = title;
this.dataPointList.innerHTML = '';
this.title.innerHTML = title;
this.dataPointList.innerHTML = "";
this.listValues.map((set, i) => {
const color = this.colors[i] || 'black';
let value = set.formatted === 0 || set.formatted ? set.formatted : set.value;
let li = $.create('li', {
innerHTML: `<div class="tooltip-legend" style="background: ${color};"></div>
this.listValues.map((set, i) => {
const color = this.colors[i] || "black";
let value =
set.formatted === 0 || set.formatted ? set.formatted : set.value;
let li = $.create("li", {
innerHTML: `<div class="tooltip-legend" style="background: ${color};"></div>
<div>
<div class="tooltip-value">${ value === 0 || value ? value : '' }</div>
<div class="tooltip-label">${set.title ? set.title : '' }</div>
</div>`
});
<div class="tooltip-value">${value === 0 || value ? value : ""}</div>
<div class="tooltip-label">${set.title ? set.title : ""}</div>
</div>`,
});
this.dataPointList.appendChild(li);
});
}
this.dataPointList.appendChild(li);
});
}
calcPosition() {
let width = this.container.offsetWidth;
calcPosition() {
let width = this.container.offsetWidth;
this.top = this.y - this.container.offsetHeight
- TOOLTIP_POINTER_TRIANGLE_HEIGHT;
this.left = this.x - width/2;
let maxLeft = this.parent.offsetWidth - width;
this.top =
this.y - this.container.offsetHeight - TOOLTIP_POINTER_TRIANGLE_HEIGHT;
this.left = this.x - width / 2;
let maxLeft = this.parent.offsetWidth - width;
let pointer = this.container.querySelector('.svg-pointer');
let pointer = this.container.querySelector(".svg-pointer");
if(this.left < 0) {
pointer.style.left = `calc(50% - ${-1 * this.left}px)`;
this.left = 0;
} else if(this.left > maxLeft) {
let delta = this.left - maxLeft;
let pointerOffset = `calc(50% + ${delta}px)`;
pointer.style.left = pointerOffset;
if (this.left < 0) {
pointer.style.left = `calc(50% - ${-1 * this.left}px)`;
this.left = 0;
} else if (this.left > maxLeft) {
let delta = this.left - maxLeft;
let pointerOffset = `calc(50% + ${delta}px)`;
pointer.style.left = pointerOffset;
this.left = maxLeft;
} else {
pointer.style.left = `50%`;
}
}
this.left = maxLeft;
} else {
pointer.style.left = `50%`;
}
}
setValues(x, y, title = {}, listValues = [], index = -1) {
this.titleName = title.name;
this.titleValue = title.value;
this.listValues = listValues;
this.x = x;
this.y = y;
this.titleValueFirst = title.valueFirst || 0;
this.index = index;
this.refresh();
}
setValues(x, y, title = {}, listValues = [], index = -1) {
this.titleName = title.name;
this.titleValue = title.value;
this.listValues = listValues;
this.x = x;
this.y = y;
this.titleValueFirst = title.valueFirst || 0;
this.index = index;
this.refresh();
}
hideTip() {
this.container.style.top = '0px';
this.container.style.left = '0px';
this.container.style.opacity = '0';
}
hideTip() {
this.container.style.top = "0px";
this.container.style.left = "0px";
this.container.style.opacity = "0";
}
showTip() {
this.container.style.top = this.top + 'px';
this.container.style.left = this.left + 'px';
this.container.style.opacity = '1';
}
showTip() {
this.container.style.top = this.top + "px";
this.container.style.left = this.left + "px";
this.container.style.opacity = "1";
}
}

View File

@ -1,105 +1,121 @@
import { getBarHeightAndYAttr, getSplineCurvePointsStr } from './draw-utils';
import { getBarHeightAndYAttr, getSplineCurvePointsStr } from "./draw-utils";
export const UNIT_ANIM_DUR = 350;
export const PATH_ANIM_DUR = 350;
export const MARKER_LINE_ANIM_DUR = UNIT_ANIM_DUR;
export const REPLACE_ALL_NEW_DUR = 250;
export const STD_EASING = 'easein';
export const STD_EASING = "easein";
export function translate(unit, oldCoord, newCoord, duration) {
let old = typeof oldCoord === 'string' ? oldCoord : oldCoord.join(', ');
return [
unit,
{transform: newCoord.join(', ')},
duration,
STD_EASING,
"translate",
{transform: old}
];
let old = typeof oldCoord === "string" ? oldCoord : oldCoord.join(", ");
return [
unit,
{ transform: newCoord.join(", ") },
duration,
STD_EASING,
"translate",
{ transform: old },
];
}
export function translateVertLine(xLine, newX, oldX) {
return translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR);
return translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR);
}
export function translateHoriLine(yLine, newY, oldY) {
return translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR);
return translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR);
}
export function animateRegion(rectGroup, newY1, newY2, oldY2) {
let newHeight = newY1 - newY2;
let rect = rectGroup.childNodes[0];
let width = rect.getAttribute("width");
let rectAnim = [
rect,
{ height: newHeight, 'stroke-dasharray': `${width}, ${newHeight}` },
MARKER_LINE_ANIM_DUR,
STD_EASING
];
let newHeight = newY1 - newY2;
let rect = rectGroup.childNodes[0];
let width = rect.getAttribute("width");
let rectAnim = [
rect,
{ height: newHeight, "stroke-dasharray": `${width}, ${newHeight}` },
MARKER_LINE_ANIM_DUR,
STD_EASING,
];
let groupAnim = translate(rectGroup, [0, oldY2], [0, newY2], MARKER_LINE_ANIM_DUR);
return [rectAnim, groupAnim];
let groupAnim = translate(
rectGroup,
[0, oldY2],
[0, newY2],
MARKER_LINE_ANIM_DUR
);
return [rectAnim, groupAnim];
}
export function animateBar(bar, x, yTop, width, offset=0, meta={}) {
let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
y -= offset;
if(bar.nodeName !== 'rect') {
let rect = bar.childNodes[0];
let rectAnim = [
rect,
{width: width, height: height},
UNIT_ANIM_DUR,
STD_EASING
];
export function animateBar(bar, x, yTop, width, offset = 0, meta = {}) {
let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
y -= offset;
if (bar.nodeName !== "rect") {
let rect = bar.childNodes[0];
let rectAnim = [
rect,
{ width: width, height: height },
UNIT_ANIM_DUR,
STD_EASING,
];
let oldCoordStr = bar.getAttribute("transform").split("(")[1].slice(0, -1);
let groupAnim = translate(bar, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
return [rectAnim, groupAnim];
} else {
return [[bar, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING]];
}
// bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein);
let oldCoordStr = bar.getAttribute("transform").split("(")[1].slice(0, -1);
let groupAnim = translate(bar, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
return [rectAnim, groupAnim];
} else {
return [
[
bar,
{ width: width, height: height, x: x, y: y },
UNIT_ANIM_DUR,
STD_EASING,
],
];
}
// bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein);
}
export function animateDot(dot, x, y) {
if(dot.nodeName !== 'circle') {
let oldCoordStr = dot.getAttribute("transform").split("(")[1].slice(0, -1);
let groupAnim = translate(dot, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
return [groupAnim];
} else {
return [[dot, {cx: x, cy: y}, UNIT_ANIM_DUR, STD_EASING]];
}
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);
if (dot.nodeName !== "circle") {
let oldCoordStr = dot.getAttribute("transform").split("(")[1].slice(0, -1);
let groupAnim = translate(dot, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
return [groupAnim];
} else {
return [[dot, { cx: x, cy: y }, UNIT_ANIM_DUR, STD_EASING]];
}
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);
}
export function animatePath(paths, newXList, newYList, zeroLine, spline) {
let pathComponents = [];
let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y)).join("L");
let pathComponents = [];
let pointsStr = newYList.map((y, i) => newXList[i] + "," + y).join("L");
if (spline)
pointsStr = getSplineCurvePointsStr(newXList, newYList);
if (spline) pointsStr = getSplineCurvePointsStr(newXList, newYList);
const animPath = [paths.path, {d:"M" + pointsStr}, PATH_ANIM_DUR, STD_EASING];
pathComponents.push(animPath);
const animPath = [
paths.path,
{ d: "M" + pointsStr },
PATH_ANIM_DUR,
STD_EASING,
];
pathComponents.push(animPath);
if(paths.region) {
let regStartPt = `${newXList[0]},${zeroLine}L`;
let regEndPt = `L${newXList.slice(-1)[0]}, ${zeroLine}`;
if (paths.region) {
let regStartPt = `${newXList[0]},${zeroLine}L`;
let regEndPt = `L${newXList.slice(-1)[0]}, ${zeroLine}`;
const animRegion = [
paths.region,
{d:"M" + regStartPt + pointsStr + regEndPt},
PATH_ANIM_DUR,
STD_EASING
];
pathComponents.push(animRegion);
}
const animRegion = [
paths.region,
{ d: "M" + regStartPt + pointsStr + regEndPt },
PATH_ANIM_DUR,
STD_EASING,
];
pathComponents.push(animRegion);
}
return pathComponents;
return pathComponents;
}
export function animatePathStr(oldPath, pathStr) {
return [oldPath, {d: pathStr}, UNIT_ANIM_DUR, STD_EASING];
return [oldPath, { d: pathStr }, UNIT_ANIM_DUR, STD_EASING];
}

View File

@ -1,116 +1,133 @@
// Leveraging SMIL Animations
import { REPLACE_ALL_NEW_DUR } from './animate';
import { REPLACE_ALL_NEW_DUR } from "./animate";
const EASING = {
ease: "0.25 0.1 0.25 1",
linear: "0 0 1 1",
// easein: "0.42 0 1 1",
easein: "0.1 0.8 0.2 1",
easeout: "0 0 0.58 1",
easeinout: "0.42 0 0.58 1"
ease: "0.25 0.1 0.25 1",
linear: "0 0 1 1",
// easein: "0.42 0 1 1",
easein: "0.1 0.8 0.2 1",
easeout: "0 0 0.58 1",
easeinout: "0.42 0 0.58 1",
};
function animateSVGElement(element, props, dur, easingType="linear", type=undefined, oldValues={}) {
function animateSVGElement(
element,
props,
dur,
easingType = "linear",
type = undefined,
oldValues = {}
) {
let animElement = element.cloneNode(true);
let newElement = element.cloneNode(true);
let animElement = element.cloneNode(true);
let newElement = element.cloneNode(true);
for (var attributeName in props) {
let animateElement;
if (attributeName === "transform") {
animateElement = document.createElementNS(
"http://www.w3.org/2000/svg",
"animateTransform"
);
} else {
animateElement = document.createElementNS(
"http://www.w3.org/2000/svg",
"animate"
);
}
let currentValue =
oldValues[attributeName] || element.getAttribute(attributeName);
let value = props[attributeName];
for(var attributeName in props) {
let animateElement;
if(attributeName === 'transform') {
animateElement = document.createElementNS("http://www.w3.org/2000/svg", "animateTransform");
} else {
animateElement = document.createElementNS("http://www.w3.org/2000/svg", "animate");
}
let currentValue = oldValues[attributeName] || element.getAttribute(attributeName);
let value = props[attributeName];
let animAttr = {
attributeName: attributeName,
from: currentValue,
to: value,
begin: "0s",
dur: dur / 1000 + "s",
values: currentValue + ";" + value,
keySplines: EASING[easingType],
keyTimes: "0;1",
calcMode: "spline",
fill: "freeze",
};
let animAttr = {
attributeName: attributeName,
from: currentValue,
to: value,
begin: "0s",
dur: dur/1000 + "s",
values: currentValue + ";" + value,
keySplines: EASING[easingType],
keyTimes: "0;1",
calcMode: "spline",
fill: 'freeze'
};
if (type) {
animAttr["type"] = type;
}
if(type) {
animAttr["type"] = type;
}
for (var i in animAttr) {
animateElement.setAttribute(i, animAttr[i]);
}
for (var i in animAttr) {
animateElement.setAttribute(i, animAttr[i]);
}
animElement.appendChild(animateElement);
animElement.appendChild(animateElement);
if (type) {
newElement.setAttribute(attributeName, `translate(${value})`);
} else {
newElement.setAttribute(attributeName, value);
}
}
if(type) {
newElement.setAttribute(attributeName, `translate(${value})`);
} else {
newElement.setAttribute(attributeName, value);
}
}
return [animElement, newElement];
return [animElement, newElement];
}
export function transform(element, style) { // eslint-disable-line no-unused-vars
element.style.transform = style;
element.style.webkitTransform = style;
element.style.msTransform = style;
element.style.mozTransform = style;
element.style.oTransform = style;
export function transform(element, style) {
// eslint-disable-line no-unused-vars
element.style.transform = style;
element.style.webkitTransform = style;
element.style.msTransform = style;
element.style.mozTransform = style;
element.style.oTransform = style;
}
function animateSVG(svgContainer, elements) {
let newElements = [];
let animElements = [];
let newElements = [];
let animElements = [];
elements.map(element => {
let unit = element[0];
let parent = unit.parentNode;
elements.map((element) => {
let unit = element[0];
let parent = unit.parentNode;
let animElement, newElement;
let animElement, newElement;
element[0] = unit;
[animElement, newElement] = animateSVGElement(...element);
element[0] = unit;
[animElement, newElement] = animateSVGElement(...element);
newElements.push(newElement);
animElements.push([animElement, parent]);
newElements.push(newElement);
animElements.push([animElement, parent]);
parent.replaceChild(animElement, unit);
});
if (parent) {
parent.replaceChild(animElement, unit);
}
});
let animSvg = svgContainer.cloneNode(true);
let animSvg = svgContainer.cloneNode(true);
animElements.map((animElement, i) => {
animElement[1].replaceChild(newElements[i], animElement[0]);
elements[i][0] = newElements[i];
});
animElements.map((animElement, i) => {
if (animElement[1]) {
animElement[1].replaceChild(newElements[i], animElement[0]);
elements[i][0] = newElements[i];
}
});
return animSvg;
return animSvg;
}
export function runSMILAnimation(parent, svgElement, elementsToAnimate) {
if(elementsToAnimate.length === 0) return;
if (elementsToAnimate.length === 0) return;
let animSvgElement = animateSVG(svgElement, elementsToAnimate);
if(svgElement.parentNode == parent) {
parent.removeChild(svgElement);
parent.appendChild(animSvgElement);
let animSvgElement = animateSVG(svgElement, elementsToAnimate);
if (svgElement.parentNode == parent) {
parent.removeChild(svgElement);
parent.appendChild(animSvgElement);
}
}
// Replace the new svgElement (data has already been replaced)
setTimeout(() => {
if(animSvgElement.parentNode == parent) {
parent.removeChild(animSvgElement);
parent.appendChild(svgElement);
}
}, REPLACE_ALL_NEW_DUR);
// Replace the new svgElement (data has already been replaced)
setTimeout(() => {
if (animSvgElement.parentNode == parent) {
parent.removeChild(animSvgElement);
parent.appendChild(svgElement);
}
}, REPLACE_ALL_NEW_DUR);
}

View File

@ -1,7 +1,12 @@
import { fillArray } from '../utils/helpers';
import { DEFAULT_AXIS_CHART_TYPE, AXIS_DATASET_CHART_TYPES, DEFAULT_CHAR_WIDTH } from '../utils/constants';
import { fillArray } from "../utils/helpers";
import {
DEFAULT_AXIS_CHART_TYPE,
AXIS_DATASET_CHART_TYPES,
DEFAULT_CHAR_WIDTH,
SERIES_LABEL_SPACE_RATIO,
} from "../utils/constants";
export function dataPrep(data, type) {
export function dataPrep(data, type, config) {
data.labels = data.labels || [];
let datasetLength = data.labels.length;
@ -9,48 +14,51 @@ export function dataPrep(data, type) {
// Datasets
let datasets = data.datasets;
let zeroArray = new Array(datasetLength).fill(0);
if(!datasets) {
if (!datasets) {
// default
datasets = [{
values: zeroArray
}];
datasets = [
{
values: zeroArray,
},
];
}
datasets.map(d=> {
datasets.map((d) => {
// Set values
if(!d.values) {
if (!d.values) {
d.values = zeroArray;
} else {
// Check for non values
let vals = d.values;
vals = vals.map(val => (!isNaN(val) ? val : 0));
vals = vals.map((val) => (!isNaN(val) ? val : 0));
// Trim or extend
if(vals.length > datasetLength) {
if (vals.length > datasetLength) {
vals = vals.slice(0, datasetLength);
}
if (config) {
vals = fillArray(vals, datasetLength - vals.length, null);
} else {
vals = fillArray(vals, datasetLength - vals.length, 0);
}
d.values = vals;
}
// Set labels
//
// Set type
if(!d.chartType ) {
if(!AXIS_DATASET_CHART_TYPES.includes(type)) type === DEFAULT_AXIS_CHART_TYPE;
if (!d.chartType) {
if (!AXIS_DATASET_CHART_TYPES.includes(type))
type = DEFAULT_AXIS_CHART_TYPE;
d.chartType = type;
}
});
// Markers
// Regions
// data.yRegions = data.yRegions || [];
if(data.yRegions) {
data.yRegions.map(d => {
if(d.end < d.start) {
if (data.yRegions) {
data.yRegions.map((d) => {
if (d.end < d.start) {
[d.start, d.end] = [d.end, d.start];
}
});
@ -65,61 +73,62 @@ export function zeroDataPrep(realData) {
let zeroData = {
labels: realData.labels.slice(0, -1),
datasets: realData.datasets.map(d => {
datasets: realData.datasets.map((d) => {
const { axisID } = d;
return {
name: '',
axisID,
name: "",
values: zeroArray.slice(0, -1),
chartType: d.chartType
chartType: d.chartType,
};
}),
};
if(realData.yMarkers) {
if (realData.yMarkers) {
zeroData.yMarkers = [
{
value: 0,
label: ''
}
label: "",
},
];
}
if(realData.yRegions) {
if (realData.yRegions) {
zeroData.yRegions = [
{
start: 0,
end: 0,
label: ''
}
label: "",
},
];
}
return zeroData;
}
export function getShortenedLabels(chartWidth, labels=[], isSeries=true) {
let allowedSpace = chartWidth / labels.length;
if(allowedSpace <= 0) allowedSpace = 1;
export function getShortenedLabels(chartWidth, labels = [], isSeries = true) {
let allowedSpace = (chartWidth / labels.length) * SERIES_LABEL_SPACE_RATIO;
if (allowedSpace <= 0) allowedSpace = 1;
let allowedLetters = allowedSpace / DEFAULT_CHAR_WIDTH;
let seriesMultiple;
if(isSeries) {
if (isSeries) {
// Find the maximum label length for spacing calculations
let maxLabelLength = Math.max(...labels.map(label => label.length));
seriesMultiple = Math.ceil(maxLabelLength/allowedLetters);
let maxLabelLength = Math.max(...labels.map((label) => label.length));
seriesMultiple = Math.ceil(maxLabelLength / allowedLetters);
}
let calcLabels = labels.map((label, i) => {
label += "";
if(label.length > allowedLetters) {
if(!isSeries) {
if(allowedLetters-3 > 0) {
label = label.slice(0, allowedLetters-3) + " ...";
if (label.length > allowedLetters) {
if (!isSeries) {
if (allowedLetters - 3 > 0) {
label = label.slice(0, allowedLetters - 3) + " ...";
} else {
label = label.slice(0, allowedLetters) + '..';
label = label.slice(0, allowedLetters) + "..";
}
} else {
if(i % seriesMultiple !== 0) {
if (i % seriesMultiple !== 0 && i !== labels.length - 1) {
label = "";
}
}

View File

@ -1,59 +1,61 @@
const PRESET_COLOR_MAP = {
'pink': '#F683AE',
'blue': '#318AD8',
'green': '#48BB74',
'grey': '#A6B1B9',
'red': '#F56B6B',
'yellow': '#FACF7A',
'purple': '#44427B',
'teal': '#5FD8C4',
'cyan': '#15CCEF',
'orange': '#F8814F',
'light-pink': '#FED7E5',
'light-blue': '#BFDDF7',
'light-green': '#48BB74',
'light-grey': '#F4F5F6',
'light-red': '#F6DFDF',
'light-yellow': '#FEE9BF',
'light-purple': '#E8E8F7',
'light-teal': '#D3FDF6',
'light-cyan': '#DDF8FD',
'light-orange': '#FECDB8'
pink: "#F683AE",
blue: "#318AD8",
green: "#48BB74",
grey: "#A6B1B9",
red: "#F56B6B",
yellow: "#FACF7A",
purple: "#44427B",
teal: "#5FD8C4",
cyan: "#15CCEF",
orange: "#F8814F",
"light-pink": "#FED7E5",
"light-blue": "#BFDDF7",
"light-green": "#48BB74",
"light-grey": "#F4F5F6",
"light-red": "#F6DFDF",
"light-yellow": "#FEE9BF",
"light-purple": "#E8E8F7",
"light-teal": "#D3FDF6",
"light-cyan": "#DDF8FD",
"light-orange": "#FECDB8",
};
function limitColor(r){
if (r > 255) return 255;
else if (r < 0) return 0;
return r;
function limitColor(r) {
if (r > 255) return 255;
else if (r < 0) return 0;
return r;
}
export function lightenDarkenColor(color, amt) {
let col = getColor(color);
let usePound = false;
if (col[0] == "#") {
col = col.slice(1);
usePound = true;
}
let num = parseInt(col,16);
let r = limitColor((num >> 16) + amt);
let b = limitColor(((num >> 8) & 0x00FF) + amt);
let g = limitColor((num & 0x0000FF) + amt);
return (usePound?"#":"") + (g | (b << 8) | (r << 16)).toString(16);
let col = getColor(color);
let usePound = false;
if (col[0] == "#") {
col = col.slice(1);
usePound = true;
}
let num = parseInt(col, 16);
let r = limitColor((num >> 16) + amt);
let b = limitColor(((num >> 8) & 0x00ff) + amt);
let g = limitColor((num & 0x0000ff) + amt);
return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16);
}
export function isValidColor(string) {
// https://stackoverflow.com/a/32685393
let HEX_RE = /(^\s*)(#)((?:[A-Fa-f0-9]{3}){1,2})$/i;
let RGB_RE = /(^\s*)(rgb|hsl)(a?)[(]\s*([\d.]+\s*%?)\s*,\s*([\d.]+\s*%?)\s*,\s*([\d.]+\s*%?)\s*(?:,\s*([\d.]+)\s*)?[)]$/i;
return HEX_RE.test(string) || RGB_RE.test(string);
// https://stackoverflow.com/a/32685393
let HEX_RE = /(^\s*)(#)((?:[A-Fa-f0-9]{3}){1,2})$/i;
let RGB_RE =
/(^\s*)(rgb|hsl)(a?)[(]\s*([\d.]+\s*%?)\s*,\s*([\d.]+\s*%?)\s*,\s*([\d.]+\s*%?)\s*(?:,\s*([\d.]+)\s*)?[)]$/i;
return HEX_RE.test(string) || RGB_RE.test(string);
}
export const getColor = (color) => {
// When RGB color, convert to hexadecimal (alpha value is omitted)
if((/rgb[a]{0,1}\([\d, ]+\)/gim).test(color)) {
return (/\D+(\d*)\D+(\d*)\D+(\d*)/gim).exec(color)
.map((x, i) => (i !== 0 ? Number(x).toString(16) : '#'))
.reduce((c, ch) => `${c}${ch}`);
}
return PRESET_COLOR_MAP[color] || color;
// When RGB color, convert to hexadecimal (alpha value is omitted)
if (/rgb[a]{0,1}\([\d, ]+\)/gim.test(color)) {
return /\D+(\d*)\D+(\d*)\D+(\d*)/gim
.exec(color)
.map((x, i) => (i !== 0 ? Number(x).toString(16) : "#"))
.reduce((c, ch) => `${c}${ch}`);
}
return PRESET_COLOR_MAP[color] || color;
};

View File

@ -1,80 +1,91 @@
export const ALL_CHART_TYPES = ['line', 'scatter', 'bar', 'percentage', 'heatmap', 'pie'];
export const ALL_CHART_TYPES = [
"line",
"scatter",
"bar",
"percentage",
"heatmap",
"pie",
];
export const COMPATIBLE_CHARTS = {
bar: ['line', 'scatter', 'percentage', 'pie'],
line: ['scatter', 'bar', 'percentage', 'pie'],
pie: ['line', 'scatter', 'percentage', 'bar'],
percentage: ['bar', 'line', 'scatter', 'pie'],
heatmap: []
bar: ["line", "scatter", "percentage", "pie"],
line: ["scatter", "bar", "percentage", "pie"],
pie: ["line", "scatter", "percentage", "bar"],
percentage: ["bar", "line", "scatter", "pie"],
heatmap: [],
};
export const DATA_COLOR_DIVISIONS = {
bar: 'datasets',
line: 'datasets',
pie: 'labels',
percentage: 'labels',
heatmap: HEATMAP_DISTRIBUTION_SIZE
bar: "datasets",
line: "datasets",
pie: "labels",
percentage: "labels",
heatmap: HEATMAP_DISTRIBUTION_SIZE,
};
export const BASE_MEASURES = {
margins: {
top: 10,
bottom: 10,
left: 20,
right: 20
},
paddings: {
top: 20,
bottom: 40,
left: 30,
right: 10
},
margins: {
top: 10,
bottom: 10,
left: 20,
right: 20,
},
paddings: {
top: 20,
bottom: 40,
left: 30,
right: 10,
},
baseHeight: 240,
titleHeight: 20,
legendHeight: 30,
baseHeight: 240,
titleHeight: 20,
legendHeight: 30,
titleFontSize: 12,
titleFontSize: 12,
};
export function getTopOffset(m) {
return m.titleHeight + m.margins.top + m.paddings.top;
return m.titleHeight + m.margins.top + m.paddings.top;
}
export function getLeftOffset(m) {
return m.margins.left + m.paddings.left;
return m.margins.left + m.paddings.left;
}
export function getExtraHeight(m) {
let totalExtraHeight = m.margins.top + m.margins.bottom
+ m.paddings.top + m.paddings.bottom
+ m.titleHeight + m.legendHeight;
return totalExtraHeight;
let totalExtraHeight =
m.margins.top +
m.margins.bottom +
m.paddings.top +
m.paddings.bottom +
m.titleHeight +
m.legendHeight;
return totalExtraHeight;
}
export function getExtraWidth(m) {
let totalExtraWidth = m.margins.left + m.margins.right
+ m.paddings.left + m.paddings.right;
let totalExtraWidth =
m.margins.left + m.margins.right + m.paddings.left + m.paddings.right;
return totalExtraWidth;
return totalExtraWidth;
}
export const INIT_CHART_UPDATE_TIMEOUT = 700;
export const CHART_POST_ANIMATE_TIMEOUT = 400;
export const DEFAULT_AXIS_CHART_TYPE = 'line';
export const AXIS_DATASET_CHART_TYPES = ['line', 'bar'];
export const DEFAULT_AXIS_CHART_TYPE = "line";
export const AXIS_DATASET_CHART_TYPES = ["line", "bar"];
export const AXIS_LEGEND_BAR_SIZE = 100;
export const LEGEND_ITEM_WIDTH = 150;
export const SERIES_LABEL_SPACE_RATIO = 0.6;
export const BAR_CHART_SPACE_RATIO = 0.8;
export const MIN_BAR_PERCENT_HEIGHT = 0.00;
export const BAR_CHART_SPACE_RATIO = 0.5;
export const MIN_BAR_PERCENT_HEIGHT = 0.0;
export const LINE_CHART_DOT_SIZE = 4;
export const DOT_OVERLAY_SIZE_INCR = 4;
export const PERCENTAGE_BAR_DEFAULT_HEIGHT = 20;
export const PERCENTAGE_BAR_DEFAULT_DEPTH = 2;
export const PERCENTAGE_BAR_DEFAULT_HEIGHT = 16;
// Fixed 5-color theme,
// More colors are difficult to parse visually
@ -86,18 +97,47 @@ export const HEATMAP_GUTTER_SIZE = 2;
export const DEFAULT_CHAR_WIDTH = 7;
export const TOOLTIP_POINTER_TRIANGLE_HEIGHT = 7.48;
const DEFAULT_CHART_COLORS = ['pink', 'blue', 'green', 'grey', 'red', 'yellow', 'purple', 'teal', 'cyan', 'orange'];
const HEATMAP_COLORS_GREEN = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
export const HEATMAP_COLORS_BLUE = ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'];
export const HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'];
const DEFAULT_CHART_COLORS = [
"pink",
"blue",
"green",
"grey",
"red",
"yellow",
"purple",
"teal",
"cyan",
"orange",
];
const HEATMAP_COLORS_GREEN = [
"#ebedf0",
"#c6e48b",
"#7bc96f",
"#239a3b",
"#196127",
];
export const HEATMAP_COLORS_BLUE = [
"#ebedf0",
"#c0ddf9",
"#73b3f3",
"#3886e1",
"#17459e",
];
export const HEATMAP_COLORS_YELLOW = [
"#ebedf0",
"#fdf436",
"#ffc700",
"#ff9100",
"#06001c",
];
export const DEFAULT_COLORS = {
bar: DEFAULT_CHART_COLORS,
line: DEFAULT_CHART_COLORS,
pie: DEFAULT_CHART_COLORS,
percentage: DEFAULT_CHART_COLORS,
heatmap: HEATMAP_COLORS_GREEN,
donut: DEFAULT_CHART_COLORS
bar: DEFAULT_CHART_COLORS,
line: DEFAULT_CHART_COLORS,
pie: DEFAULT_CHART_COLORS,
percentage: DEFAULT_CHART_COLORS,
heatmap: HEATMAP_COLORS_GREEN,
donut: DEFAULT_CHART_COLORS,
};
// Universal constants

View File

@ -6,85 +6,132 @@ export const DAYS_IN_YEAR = 375;
export const NO_OF_MILLIS = 1000;
export const SEC_IN_DAY = 86400;
export const MONTH_NAMES = ["January", "February", "March", "April", "May",
"June", "July", "August", "September", "October", "November", "December"];
export const MONTH_NAMES_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
export const MONTH_NAMES = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
export const MONTH_NAMES_SHORT = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
export const DAY_NAMES_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
export const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
export const DAY_NAMES_SHORT = [
"Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
];
export const DAY_NAMES = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
// https://stackoverflow.com/a/11252167/6495043
function treatAsUtc(date) {
let result = new Date(date);
result.setMinutes(result.getMinutes() - result.getTimezoneOffset());
return result;
let result = new Date(date);
result.setMinutes(result.getMinutes() - result.getTimezoneOffset());
return result;
}
export function toMidnightUTC(date) {
let result = new Date(date);
result.setUTCHours(0, result.getTimezoneOffset(), 0, 0);
return result;
}
export function getYyyyMmDd(date) {
let dd = date.getDate();
let mm = date.getMonth() + 1; // getMonth() is zero-based
return [
date.getFullYear(),
(mm>9 ? '' : '0') + mm,
(dd>9 ? '' : '0') + dd
].join('-');
let dd = date.getDate();
let mm = date.getMonth() + 1; // getMonth() is zero-based
return [
date.getFullYear(),
(mm > 9 ? "" : "0") + mm,
(dd > 9 ? "" : "0") + dd,
].join("-");
}
export function clone(date) {
return new Date(date.getTime());
return new Date(date.getTime());
}
export function timestampSec(date) {
return date.getTime()/NO_OF_MILLIS;
return date.getTime() / NO_OF_MILLIS;
}
export function timestampToMidnight(timestamp, roundAhead = false) {
let midnightTs = Math.floor(timestamp - (timestamp % SEC_IN_DAY));
if(roundAhead) {
return midnightTs + SEC_IN_DAY;
}
return midnightTs;
let midnightTs = Math.floor(timestamp - (timestamp % SEC_IN_DAY));
if (roundAhead) {
return midnightTs + SEC_IN_DAY;
}
return midnightTs;
}
// export function getMonthsBetween(startDate, endDate) {}
export function getWeeksBetween(startDate, endDate) {
let weekStartDate = setDayToSunday(startDate);
return Math.ceil(getDaysBetween(weekStartDate, endDate) / NO_OF_DAYS_IN_WEEK);
let weekStartDate = setDayToSunday(startDate);
return Math.ceil(getDaysBetween(weekStartDate, endDate) / NO_OF_DAYS_IN_WEEK);
}
export function getDaysBetween(startDate, endDate) {
let millisecondsPerDay = SEC_IN_DAY * NO_OF_MILLIS;
return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay;
let millisecondsPerDay = SEC_IN_DAY * NO_OF_MILLIS;
return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay;
}
export function areInSameMonth(startDate, endDate) {
return startDate.getMonth() === endDate.getMonth()
&& startDate.getFullYear() === endDate.getFullYear();
return (
startDate.getMonth() === endDate.getMonth() &&
startDate.getFullYear() === endDate.getFullYear()
);
}
export function getMonthName(i, short=false) {
let monthName = MONTH_NAMES[i];
return short ? monthName.slice(0, 3) : monthName;
export function getMonthName(i, short = false) {
let monthName = MONTH_NAMES[i];
return short ? monthName.slice(0, 3) : monthName;
}
export function getLastDateInMonth (month, year) {
return new Date(year, month + 1, 0); // 0: last day in previous month
export function getLastDateInMonth(month, year) {
return new Date(year, month + 1, 0); // 0: last day in previous month
}
// mutates
export function setDayToSunday(date) {
let newDate = clone(date);
const day = newDate.getDay();
if(day !== 0) {
addDays(newDate, (-1) * day);
}
return newDate;
let newDate = clone(date);
const day = newDate.getDay();
if (day !== 0) {
addDays(newDate, -1 * day);
}
return newDate;
}
// mutates
export function addDays(date, numberOfDays) {
date.setDate(date.getDate() + numberOfDays);
date.setDate(date.getDate() + numberOfDays);
}

View File

@ -1,137 +1,149 @@
export function $(expr, con) {
return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
return typeof expr === "string"
? (con || document).querySelector(expr)
: expr || null;
}
export function findNodeIndex(node)
{
var i = 0;
while (node.previousSibling) {
node = node.previousSibling;
i++;
}
return i;
export function findNodeIndex(node) {
var i = 0;
while (node.previousSibling) {
node = node.previousSibling;
i++;
}
return i;
}
$.create = (tag, o) => {
var element = document.createElement(tag);
var element = document.createElement(tag);
for (var i in o) {
var val = o[i];
for (var i in o) {
var val = o[i];
if (i === "inside") {
$(val).appendChild(element);
}
else if (i === "around") {
var ref = $(val);
ref.parentNode.insertBefore(element, ref);
element.appendChild(ref);
if (i === "inside") {
$(val).appendChild(element);
} else if (i === "around") {
var ref = $(val);
ref.parentNode.insertBefore(element, ref);
element.appendChild(ref);
} else if (i === "styles") {
if (typeof val === "object") {
Object.keys(val).map((prop) => {
element.style[prop] = val[prop];
});
}
} else if (i in element) {
element[i] = val;
} else {
element.setAttribute(i, val);
}
}
} else if (i === "styles") {
if(typeof val === "object") {
Object.keys(val).map(prop => {
element.style[prop] = val[prop];
});
}
} else if (i in element ) {
element[i] = val;
}
else {
element.setAttribute(i, val);
}
}
return element;
return element;
};
export function getOffset(element) {
let rect = element.getBoundingClientRect();
return {
// https://stackoverflow.com/a/7436602/6495043
// rect.top varies with scroll, so we add whatever has been
// scrolled to it to get absolute distance from actual page top
top: rect.top + (document.documentElement.scrollTop || document.body.scrollTop),
left: rect.left + (document.documentElement.scrollLeft || document.body.scrollLeft)
};
let rect = element.getBoundingClientRect();
return {
// https://stackoverflow.com/a/7436602/6495043
// rect.top varies with scroll, so we add whatever has been
// scrolled to it to get absolute distance from actual page top
top:
rect.top +
(document.documentElement.scrollTop || document.body.scrollTop),
left:
rect.left +
(document.documentElement.scrollLeft || document.body.scrollLeft),
};
}
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
// an element's offsetParent property will return null whenever it, or any of its parents,
// is hidden via the display style property.
export function isHidden(el) {
return (el.offsetParent === null);
return el.offsetParent === null;
}
export function isElementInViewport(el) {
// Although straightforward: https://stackoverflow.com/a/7557433/6495043
var rect = el.getBoundingClientRect();
// Although straightforward: https://stackoverflow.com/a/7557433/6495043
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
);
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight ||
document.documentElement.clientHeight) /*or $(window).height() */ &&
rect.right <=
(window.innerWidth ||
document.documentElement.clientWidth) /*or $(window).width() */
);
}
export function getElementContentWidth(element) {
var styles = window.getComputedStyle(element);
var padding = parseFloat(styles.paddingLeft) +
parseFloat(styles.paddingRight);
var styles = window.getComputedStyle(element);
var padding =
parseFloat(styles.paddingLeft) + parseFloat(styles.paddingRight);
return element.clientWidth - padding;
return element.clientWidth - padding;
}
export function bind(element, o){
if (element) {
for (var event in o) {
var callback = o[event];
export function bind(element, o) {
if (element) {
for (var event in o) {
var callback = o[event];
event.split(/\s+/).forEach(function (event) {
element.addEventListener(event, callback);
});
}
}
event.split(/\s+/).forEach(function (event) {
element.addEventListener(event, callback);
});
}
}
}
export function unbind(element, o){
if (element) {
for (var event in o) {
var callback = o[event];
export function unbind(element, o) {
if (element) {
for (var event in o) {
var callback = o[event];
event.split(/\s+/).forEach(function(event) {
element.removeEventListener(event, callback);
});
}
}
event.split(/\s+/).forEach(function (event) {
element.removeEventListener(event, callback);
});
}
}
}
export function fire(target, type, properties) {
var evt = document.createEvent("HTMLEvents");
var evt = document.createEvent("HTMLEvents");
evt.initEvent(type, true, true );
evt.initEvent(type, true, true);
for (var j in properties) {
evt[j] = properties[j];
}
for (var j in properties) {
evt[j] = properties[j];
}
return target.dispatchEvent(evt);
return target.dispatchEvent(evt);
}
// https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/
export function forEachNode(nodeList, callback, scope) {
if(!nodeList) return;
for (var i = 0; i < nodeList.length; i++) {
callback.call(scope, nodeList[i], i);
}
if (!nodeList) return;
for (var i = 0; i < nodeList.length; i++) {
callback.call(scope, nodeList[i], i);
}
}
export function activate($parent, $child, commonClass, activeClass='active', index = -1) {
let $children = $parent.querySelectorAll(`.${commonClass}.${activeClass}`);
export function activate(
$parent,
$child,
commonClass,
activeClass = "active",
index = -1
) {
let $children = $parent.querySelectorAll(`.${commonClass}.${activeClass}`);
forEachNode($children, (node, i) => {
if(index >= 0 && i <= index) return;
node.classList.remove(activeClass);
});
forEachNode($children, (node, i) => {
if (index >= 0 && i <= index) return;
node.classList.remove(activeClass);
});
$child.classList.add(activeClass);
$child.classList.add(activeClass);
}

View File

@ -1,99 +1,103 @@
import { fillArray } from './helpers';
import { fillArray } from "./helpers";
export function getBarHeightAndYAttr(yTop, zeroLine) {
let height, y;
if (yTop <= zeroLine) {
height = zeroLine - yTop;
y = yTop;
} else {
height = yTop - zeroLine;
y = zeroLine;
}
let height, y;
if (yTop <= zeroLine) {
height = zeroLine - yTop;
y = yTop;
} else {
height = yTop - zeroLine;
y = zeroLine;
}
return [height, y];
return [height, y];
}
export function equilizeNoOfElements(array1, array2,
extraCount = array2.length - array1.length) {
// Doesn't work if either has zero elements.
if(extraCount > 0) {
array1 = fillArray(array1, extraCount);
} else {
array2 = fillArray(array2, extraCount);
}
return [array1, array2];
export function equilizeNoOfElements(
array1,
array2,
extraCount = array2.length - array1.length
) {
// Doesn't work if either has zero elements.
if (extraCount > 0) {
array1 = fillArray(array1, extraCount);
} else {
array2 = fillArray(array2, extraCount);
}
return [array1, array2];
}
export function truncateString(txt, len) {
if (!txt) {
return;
}
if (txt.length > len) {
return txt.slice(0, len-3) + '...';
} else {
return txt;
}
if (!txt) {
return;
}
if (txt.length > len) {
return txt.slice(0, len - 3) + "...";
} else {
return txt;
}
}
export function shortenLargeNumber(label) {
let number;
if (typeof label === 'number') number = label;
else if (typeof label === 'string') {
number = Number(label);
if (Number.isNaN(number)) return label;
}
let number;
if (typeof label === "number") number = label;
else if (typeof label === "string") {
number = Number(label);
if (Number.isNaN(number)) return label;
}
// Using absolute since log wont work for negative numbers
let p = Math.floor(Math.log10(Math.abs(number)));
if (p <= 2) return number; // Return as is for a 3 digit number of less
let l = Math.floor(p / 3);
let shortened = (Math.pow(10, p - l * 3) * +(number / Math.pow(10, p)).toFixed(1));
// Using absolute since log wont work for negative numbers
let p = Math.floor(Math.log10(Math.abs(number)));
if (p <= 2) return number; // Return as is for a 3 digit number of less
let l = Math.floor(p / 3);
let shortened =
Math.pow(10, p - l * 3) * +(number / Math.pow(10, p)).toFixed(1);
// Correct for floating point error upto 2 decimal places
return Math.round(shortened*100)/100 + ' ' + ['', 'K', 'M', 'B', 'T'][l];
// Correct for floating point error upto 2 decimal places
return Math.round(shortened * 100) / 100 + " " + ["", "K", "M", "B", "T"][l];
}
// cubic bezier curve calculation (from example by François Romain)
export function getSplineCurvePointsStr(xList, yList) {
let points = [];
for (let i = 0; i < xList.length; i++) {
points.push([xList[i], yList[i]]);
}
let points=[];
for(let i=0;i<xList.length;i++){
points.push([xList[i], yList[i]]);
}
let smoothing = 0.2;
let line = (pointA, pointB) => {
let lengthX = pointB[0] - pointA[0];
let lengthY = pointB[1] - pointA[1];
return {
length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
angle: Math.atan2(lengthY, lengthX),
};
};
let smoothing = 0.2;
let line = (pointA, pointB) => {
let lengthX = pointB[0] - pointA[0];
let lengthY = pointB[1] - pointA[1];
return {
length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
angle: Math.atan2(lengthY, lengthX)
};
};
let controlPoint = (current, previous, next, reverse) => {
let p = previous || current;
let n = next || current;
let o = line(p, n);
let angle = o.angle + (reverse ? Math.PI : 0);
let length = o.length * smoothing;
let x = current[0] + Math.cos(angle) * length;
let y = current[1] + Math.sin(angle) * length;
return [x, y];
};
let bezierCommand = (point, i, a) => {
let cps = controlPoint(a[i - 1], a[i - 2], point);
let cpe = controlPoint(point, a[i - 1], a[i + 1], true);
return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`;
};
let pointStr = (points, command) => {
return points.reduce((acc, point, i, a) => i === 0
? `${point[0]},${point[1]}`
: `${acc} ${command(point, i, a)}`, '');
};
return pointStr(points, bezierCommand);
let controlPoint = (current, previous, next, reverse) => {
let p = previous || current;
let n = next || current;
let o = line(p, n);
let angle = o.angle + (reverse ? Math.PI : 0);
let length = o.length * smoothing;
let x = current[0] + Math.cos(angle) * length;
let y = current[1] + Math.sin(angle) * length;
return [x, y];
};
let bezierCommand = (point, i, a) => {
let cps = controlPoint(a[i - 1], a[i - 2], point);
let cpe = controlPoint(point, a[i - 1], a[i + 1], true);
return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`;
};
let pointStr = (points, command) => {
return points.reduce(
(acc, point, i, a) =>
i === 0 ? `${point[0]},${point[1]}` : `${acc} ${command(point, i, a)}`,
""
);
};
return pointStr(points, bezierCommand);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,33 @@
import { $ } from '../utils/dom';
import { CSSTEXT } from '../../css/chartsCss';
import { $ } from "../utils/dom";
import { CSSTEXT } from "../../css/chartsCss";
export function downloadFile(filename, data) {
var a = document.createElement('a');
a.style = "display: none";
var blob = new Blob(data, {type: "image/svg+xml; charset=utf-8"});
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(function(){
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 300);
var a = document.createElement("a");
a.style = "display: none";
var blob = new Blob(data, { type: "image/svg+xml; charset=utf-8" });
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 300);
}
export function prepareForExport(svg) {
let clone = svg.cloneNode(true);
clone.classList.add('chart-container');
clone.setAttribute('xmlns', "http://www.w3.org/2000/svg");
clone.setAttribute('xmlns:xlink', "http://www.w3.org/1999/xlink");
let styleEl = $.create('style', {
'innerHTML': CSSTEXT
});
clone.insertBefore(styleEl, clone.firstChild);
let clone = svg.cloneNode(true);
clone.classList.add("chart-container");
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
let styleEl = $.create("style", {
innerHTML: CSSTEXT,
});
clone.insertBefore(styleEl, clone.firstChild);
let container = $.create('div');
container.appendChild(clone);
let container = $.create("div");
container.appendChild(clone);
return container.innerHTML;
return container.innerHTML;
}

View File

@ -1,4 +1,4 @@
import { ANGLE_RATIO } from './constants';
import { ANGLE_RATIO } from "./constants";
/**
* Returns the value of a number upto 2 decimal places.
@ -14,10 +14,10 @@ export function floatTwo(d) {
* @param {Array} arr2 Second array
*/
export function arraysEqual(arr1, arr2) {
if(arr1.length !== arr2.length) return false;
if (arr1.length !== arr2.length) return false;
let areEqual = true;
arr1.map((d, i) => {
if(arr2[i] !== d) areEqual = false;
if (arr2[i] !== d) areEqual = false;
});
return areEqual;
}
@ -46,8 +46,8 @@ export function shuffle(array) {
* @param {Object} element element to fill with
* @param {Boolean} start fill at start?
*/
export function fillArray(array, count, element, start=false) {
if(!element) {
export function fillArray(array, count, element, start = false) {
if (element == undefined) {
element = start ? array[0] : array[array.length - 1];
}
let fillerArray = new Array(Math.abs(count)).fill(element);
@ -61,19 +61,19 @@ export function fillArray(array, count, element, start=false) {
* @param {Number} charWidth Width of single char in pixels
*/
export function getStringWidth(string, charWidth) {
return (string+"").length * charWidth;
return (string + "").length * charWidth;
}
export function bindChange(obj, getFn, setFn) {
return new Proxy(obj, {
set: function(target, prop, value) {
set: function (target, prop, value) {
setFn();
return Reflect.set(target, prop, value);
},
get: function(target, prop) {
get: function (target, prop) {
getFn();
return Reflect.get(target, prop);
}
},
});
}
@ -81,9 +81,9 @@ export function bindChange(obj, getFn, setFn) {
export function getRandomBias(min, max, bias, influence) {
const range = max - min;
const biasValue = range * bias + min;
var rnd = Math.random() * range + min, // random in range
mix = Math.random() * influence; // random mixer
return rnd * (1 - mix) + biasValue * mix; // mix full range and bias
var rnd = Math.random() * range + min, // random in range
mix = Math.random() * influence; // random mixer
return rnd * (1 - mix) + biasValue * mix; // mix full range and bias
}
export function getPositionByAngle(angle, radius) {
@ -98,7 +98,7 @@ export function getPositionByAngle(angle, radius) {
* @param {object} candidate Candidate to test
* @param {Boolean} nonNegative flag to treat negative number as invalid
*/
export function isValidNumber(candidate, nonNegative=false) {
export function isValidNumber(candidate, nonNegative = false) {
if (Number.isNaN(candidate)) return false;
else if (candidate === undefined) return false;
else if (!Number.isFinite(candidate)) return false;
@ -113,5 +113,31 @@ export function isValidNumber(candidate, nonNegative=false) {
export function round(d) {
// https://floating-point-gui.de/
// https://www.jacklmoore.com/notes/rounding-in-javascript/
return Number(Math.round(d + 'e4') + 'e-4');
return Number(Math.round(d + "e4") + "e-4");
}
/**
* Creates a deep clone of an object
* @param {Object} candidate Any Object
*/
export function deepClone(candidate) {
let cloned, value, key;
if (candidate instanceof Date) {
return new Date(candidate.getTime());
}
if (typeof candidate !== "object" || candidate === null) {
return candidate;
}
cloned = Array.isArray(candidate) ? [] : {};
for (key in candidate) {
value = candidate[key];
cloned[key] = deepClone(value);
}
return cloned;
}

View File

@ -1,239 +1,255 @@
import { floatTwo } from './helpers';
import { floatTwo } from "./helpers";
function normalize(x) {
// Calculates mantissa and exponent of a number
// Returns normalized number and exponent
// https://stackoverflow.com/q/9383593/6495043
// Calculates mantissa and exponent of a number
// Returns normalized number and exponent
// https://stackoverflow.com/q/9383593/6495043
if(x===0) {
return [0, 0];
}
if(isNaN(x)) {
return {mantissa: -6755399441055744, exponent: 972};
}
var sig = x > 0 ? 1 : -1;
if(!isFinite(x)) {
return {mantissa: sig * 4503599627370496, exponent: 972};
}
if (x === 0) {
return [0, 0];
}
if (isNaN(x)) {
return { mantissa: -6755399441055744, exponent: 972 };
}
var sig = x > 0 ? 1 : -1;
if (!isFinite(x)) {
return { mantissa: sig * 4503599627370496, exponent: 972 };
}
x = Math.abs(x);
var exp = Math.floor(Math.log10(x));
var man = x/Math.pow(10, exp);
x = Math.abs(x);
var exp = Math.floor(Math.log10(x));
var man = x / Math.pow(10, exp);
return [sig * man, exp];
return [sig * man, exp];
}
function getChartRangeIntervals(max, min=0) {
let upperBound = Math.ceil(max);
let lowerBound = Math.floor(min);
let range = upperBound - lowerBound;
function getChartRangeIntervals(max, min = 0) {
let upperBound = Math.ceil(max);
let lowerBound = Math.floor(min);
let range = upperBound - lowerBound;
let noOfParts = range;
let partSize = 1;
let noOfParts = range;
let partSize = 1;
// To avoid too many partitions
if(range > 5) {
if(range % 2 !== 0) {
upperBound++;
// Recalc range
range = upperBound - lowerBound;
}
noOfParts = range/2;
partSize = 2;
}
// To avoid too many partitions
if (range > 5) {
if (range % 2 !== 0) {
upperBound++;
// Recalc range
range = upperBound - lowerBound;
}
noOfParts = range / 2;
partSize = 2;
}
// Special case: 1 and 2
if(range <= 2) {
noOfParts = 4;
partSize = range/noOfParts;
}
// Special case: 1 and 2
if (range <= 2) {
noOfParts = 4;
partSize = range / noOfParts;
}
// Special case: 0
if(range === 0) {
noOfParts = 5;
partSize = 1;
}
// Special case: 0
if (range === 0) {
noOfParts = 5;
partSize = 1;
}
let intervals = [];
for(var i = 0; i <= noOfParts; i++){
intervals.push(lowerBound + partSize * i);
}
return intervals;
let intervals = [];
for (var i = 0; i <= noOfParts; i++) {
intervals.push(lowerBound + partSize * i);
}
return intervals;
}
function getChartIntervals(maxValue, minValue=0) {
let [normalMaxValue, exponent] = normalize(maxValue);
let normalMinValue = minValue ? minValue/Math.pow(10, exponent): 0;
function getChartIntervals(maxValue, minValue = 0) {
let [normalMaxValue, exponent] = normalize(maxValue);
let normalMinValue = minValue ? minValue / Math.pow(10, exponent) : 0;
// Allow only 7 significant digits
normalMaxValue = normalMaxValue.toFixed(6);
// Allow only 7 significant digits
normalMaxValue = normalMaxValue.toFixed(6);
let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue);
intervals = intervals.map(value => value * Math.pow(10, exponent));
return intervals;
let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue);
intervals = intervals.map((value) => {
// For negative exponents we want to divide by 10^-exponent to avoid
// floating point arithmetic bugs. For instance, in javascript
// 6 * 10^-1 == 0.6000000000000001, we instead want 6 / 10^1 == 0.6
if (exponent < 0) {
return value / Math.pow(10, -exponent);
}
return value * Math.pow(10, exponent);
});
return intervals;
}
export function calcChartIntervals(values, withMinimum=false) {
//*** Where the magic happens ***
export function calcChartIntervals(values, withMinimum = true, overrideCeiling=false, overrideFloor=false) {
//*** Where the magic happens ***
// Calculates best-fit y intervals from given values
// and returns the interval array
// Calculates best-fit y intervals from given values
// and returns the interval array
let maxValue = Math.max(...values);
let minValue = Math.min(...values);
let maxValue = Math.max(...values);
let minValue = Math.min(...values);
// Exponent to be used for pretty print
let exponent = 0, intervals = []; // eslint-disable-line no-unused-vars
if (overrideCeiling) {
maxValue = overrideCeiling
}
function getPositiveFirstIntervals(maxValue, absMinValue) {
let intervals = getChartIntervals(maxValue);
if (overrideFloor) {
minValue = overrideFloor
}
let intervalSize = intervals[1] - intervals[0];
// Exponent to be used for pretty print
let exponent = 0,
intervals = []; // eslint-disable-line no-unused-vars
// Then unshift the negative values
let value = 0;
for(var i = 1; value < absMinValue; i++) {
value += intervalSize;
intervals.unshift((-1) * value);
}
return intervals;
}
function getPositiveFirstIntervals(maxValue, absMinValue) {
let intervals = getChartIntervals(maxValue);
// CASE I: Both non-negative
let intervalSize = intervals[1] - intervals[0];
if(maxValue >= 0 && minValue >= 0) {
exponent = normalize(maxValue)[1];
if(!withMinimum) {
intervals = getChartIntervals(maxValue);
} else {
intervals = getChartIntervals(maxValue, minValue);
}
}
// Then unshift the negative values
let value = 0;
for (var i = 1; value < absMinValue; i++) {
value += intervalSize;
intervals.unshift(-1 * value);
}
return intervals;
}
// CASE II: Only minValue negative
// CASE I: Both non-negative
else if(maxValue > 0 && minValue < 0) {
// `withMinimum` irrelevant in this case,
// We'll be handling both sides of zero separately
// (both starting from zero)
// Because ceil() and floor() behave differently
// in those two regions
if (maxValue >= 0 && minValue >= 0) {
exponent = normalize(maxValue)[1];
if (!withMinimum) {
intervals = getChartIntervals(maxValue);
} else {
intervals = getChartIntervals(maxValue, minValue);
}
}
let absMinValue = Math.abs(minValue);
// CASE II: Only minValue negative
else if (maxValue > 0 && minValue < 0) {
// `withMinimum` irrelevant in this case,
// We'll be handling both sides of zero separately
// (both starting from zero)
// Because ceil() and floor() behave differently
// in those two regions
if(maxValue >= absMinValue) {
exponent = normalize(maxValue)[1];
intervals = getPositiveFirstIntervals(maxValue, absMinValue);
} else {
// Mirror: maxValue => absMinValue, then change sign
exponent = normalize(absMinValue)[1];
let posIntervals = getPositiveFirstIntervals(absMinValue, maxValue);
intervals = posIntervals.reverse().map(d => d * (-1));
}
let absMinValue = Math.abs(minValue);
}
if (maxValue >= absMinValue) {
exponent = normalize(maxValue)[1];
intervals = getPositiveFirstIntervals(maxValue, absMinValue);
} else {
// Mirror: maxValue => absMinValue, then change sign
exponent = normalize(absMinValue)[1];
let posIntervals = getPositiveFirstIntervals(absMinValue, maxValue);
intervals = posIntervals.reverse().map((d) => d * -1);
}
}
// CASE III: Both non-positive
// CASE III: Both non-positive
else if (maxValue <= 0 && minValue <= 0) {
// Mirrored Case I:
// Work with positives, then reverse the sign and array
else if(maxValue <= 0 && minValue <= 0) {
// Mirrored Case I:
// Work with positives, then reverse the sign and array
let pseudoMaxValue = Math.abs(minValue);
let pseudoMinValue = Math.abs(maxValue);
let pseudoMaxValue = Math.abs(minValue);
let pseudoMinValue = Math.abs(maxValue);
exponent = normalize(pseudoMaxValue)[1];
if (!withMinimum) {
intervals = getChartIntervals(pseudoMaxValue);
} else {
intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue);
}
exponent = normalize(pseudoMaxValue)[1];
if(!withMinimum) {
intervals = getChartIntervals(pseudoMaxValue);
} else {
intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue);
}
intervals = intervals.reverse().map((d) => d * -1);
}
intervals = intervals.reverse().map(d => d * (-1));
}
return intervals;
return intervals.sort((a, b) => a - b);
}
export function getZeroIndex(yPts) {
let zeroIndex;
let interval = getIntervalSize(yPts);
if(yPts.indexOf(0) >= 0) {
// the range has a given zero
// zero-line on the chart
zeroIndex = yPts.indexOf(0);
} else if(yPts[0] > 0) {
// Minimum value is positive
// zero-line is off the chart: below
let min = yPts[0];
zeroIndex = (-1) * min / interval;
} else {
// Maximum value is negative
// zero-line is off the chart: above
let max = yPts[yPts.length - 1];
zeroIndex = (-1) * max / interval + (yPts.length - 1);
}
return zeroIndex;
let zeroIndex;
let interval = getIntervalSize(yPts);
if (yPts.indexOf(0) >= 0) {
// the range has a given zero
// zero-line on the chart
zeroIndex = yPts.indexOf(0);
} else if (yPts[0] > 0) {
// Minimum value is positive
// zero-line is off the chart: below
let min = yPts[0];
zeroIndex = (-1 * min) / interval;
} else {
// Maximum value is negative
// zero-line is off the chart: above
let max = yPts[yPts.length - 1];
zeroIndex = (-1 * max) / interval + (yPts.length - 1);
}
return zeroIndex;
}
export function getRealIntervals(max, noOfIntervals, min = 0, asc = 1) {
let range = max - min;
let part = range * 1.0 / noOfIntervals;
let intervals = [];
let range = max - min;
let part = (range * 1.0) / noOfIntervals;
let intervals = [];
for(var i = 0; i <= noOfIntervals; i++) {
intervals.push(min + part * i);
}
for (var i = 0; i <= noOfIntervals; i++) {
intervals.push(min + part * i);
}
return asc ? intervals : intervals.reverse();
return asc ? intervals : intervals.reverse();
}
export function getIntervalSize(orderedArray) {
return orderedArray[1] - orderedArray[0];
return orderedArray[1] - orderedArray[0];
}
export function getValueRange(orderedArray) {
return orderedArray[orderedArray.length-1] - orderedArray[0];
return orderedArray[orderedArray.length - 1] - orderedArray[0];
}
export function scale(val, yAxis) {
return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier);
return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier);
}
export function isInRange(val, min, max) {
return val > min && val < max;
return val > min && val < max;
}
export function isInRange2D(coord, minCoord, maxCoord) {
return isInRange(coord[0], minCoord[0], maxCoord[0])
&& isInRange(coord[1], minCoord[1], maxCoord[1]);
return (
isInRange(coord[0], minCoord[0], maxCoord[0]) &&
isInRange(coord[1], minCoord[1], maxCoord[1])
);
}
export function getClosestInArray(goal, arr, index = false) {
let closest = arr.reduce(function(prev, curr) {
return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev);
}, []);
let closest = arr.reduce(function (prev, curr) {
return Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev;
}, []);
return index ? arr.indexOf(closest) : closest;
return index ? arr.indexOf(closest) : closest;
}
export function calcDistribution(values, distributionSize) {
// Assume non-negative values,
// implying distribution minimum at zero
// Assume non-negative values,
// implying distribution minimum at zero
let dataMaxValue = Math.max(...values);
let dataMaxValue = Math.max(...values);
let distributionStep = 1 / (distributionSize - 1);
let distribution = [];
let distributionStep = 1 / (distributionSize - 1);
let distribution = [];
for(var i = 0; i < distributionSize; i++) {
let checkpoint = dataMaxValue * (distributionStep * i);
distribution.push(checkpoint);
}
for (var i = 0; i < distributionSize; i++) {
let checkpoint = dataMaxValue * (distributionStep * i);
distribution.push(checkpoint);
}
return distribution;
return distribution;
}
export function getMaxCheckpoint(value, distribution) {
return distribution.filter(d => d < value).length;
return distribution.filter((d) => d < value).length;
}

View File

@ -1,14 +1,14 @@
const assert = require('assert');
const colors = require('../colors');
const assert = require("assert");
const colors = require("../colors");
describe('utils.colors', () => {
it('should return #aaabac for RGB()', () => {
assert.equal(colors.getColor('rgb(170, 171, 172)'), '#aaabac');
describe("utils.colors", () => {
it("should return #aaabac for RGB()", () => {
assert.equal(colors.getColor("rgb(170, 171, 172)"), "#aaabac");
});
it('should return #ff5858 for the named color red', () => {
assert.equal(colors.getColor('red'), '#ff5858d');
it("should return #ff5858 for the named color red", () => {
assert.equal(colors.getColor("red"), "#ff5858d");
});
it('should return #1a5c29 for the hex color #1a5c29', () => {
assert.equal(colors.getColor('#1a5c29'), '#1a5c29');
it("should return #1a5c29 for the hex color #1a5c29", () => {
assert.equal(colors.getColor("#1a5c29"), "#1a5c29");
});
});
});

View File

@ -1,10 +1,10 @@
const assert = require('assert');
const helpers = require('../helpers');
const assert = require("assert");
const helpers = require("../helpers");
describe('utils.helpers', () => {
it('should return a value fixed upto 2 decimals', () => {
describe("utils.helpers", () => {
it("should return a value fixed upto 2 decimals", () => {
assert.equal(helpers.floatTwo(1.234), 1.23);
assert.equal(helpers.floatTwo(1.456), 1.46);
assert.equal(helpers.floatTwo(1), 1.00);
assert.equal(helpers.floatTwo(1), 1.0);
});
});
});

4183
yarn.lock

File diff suppressed because it is too large Load Diff