Compare commits
9 Commits
master
...
funnel-cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
999d5acc74 | ||
|
|
ce67836445 | ||
|
|
2c35a2f35b | ||
|
|
f32cf4bde7 | ||
|
|
b099ffe1c9 | ||
|
|
fff25ecf4f | ||
|
|
9053b01462 | ||
|
|
2451e58df9 | ||
|
|
edf6077eb4 |
17
.babelrc
17
.babelrc
@ -1,13 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": ["last 2 versions", "safari >= 7"]
|
||||
},
|
||||
["latest", {
|
||||
"es2015": {
|
||||
"modules": false
|
||||
}
|
||||
]
|
||||
]
|
||||
}]
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": ["env"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +1,33 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -5,8 +5,6 @@
|
||||
#### Steps to Reproduce:
|
||||
*
|
||||
|
||||
|
||||
NOTE: Add a GIF/Screenshot if required.
|
||||
|
||||
Frappé Charts version:
|
||||
Codepen / Codesandbox:
|
||||
33
.github/workflows/npm-publish.yml
vendored
33
.github/workflows/npm-publish.yml
vendored
@ -1,33 +0,0 @@
|
||||
# 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
7
.gitignore
vendored
@ -60,9 +60,4 @@ typings/
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# npm build output
|
||||
dist
|
||||
docs
|
||||
docs/assets/
|
||||
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
@ -11,4 +11,4 @@ script:
|
||||
- make test
|
||||
|
||||
after_success:
|
||||
- make coveralls
|
||||
- make coveralls
|
||||
176
README.md
176
README.md
@ -1,62 +1,72 @@
|
||||
<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**
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://bundlephobia.com/result?p=frappe-charts">
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/frappe-charts">
|
||||
<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>
|
||||
<a href="https://travis-ci.org/frappe/charts">
|
||||
<img src="https://img.shields.io/travis/frappe/charts.svg?style=flat-square">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<img src=".github/example.gif">
|
||||
<p align="center">
|
||||
<a href="https://frappe.github.io/charts">
|
||||
<img src=".github/example.gif">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
### Contents
|
||||
* [Installation](#installation)
|
||||
* [Usage](#usage)
|
||||
* [Contribute](https://frappe.io/charts/docs/contributing)
|
||||
* [Updates](#updates)
|
||||
* [License](#license)
|
||||
|
||||
[Explore Demos](https://frappe.io/charts) - [Edit at CodeSandbox](https://codesandbox.io/s/frappe-charts-demo-viqud) - [Documentation](https://frappe.io/charts/docs)
|
||||
#### Installation
|
||||
* Install via [`npm`](https://www.npmjs.com/get-npm):
|
||||
|
||||
</div>
|
||||
```console
|
||||
$ npm install frappe-charts
|
||||
```
|
||||
|
||||
</div>
|
||||
and include in your project:
|
||||
```js
|
||||
import { Chart } from "frappe-charts"
|
||||
```
|
||||
|
||||
## 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.
|
||||
...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'
|
||||
```
|
||||
|
||||
### Motivation
|
||||
|
||||
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 didn’t align well with our product’s 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.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **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.
|
||||
|
||||
## Usage
|
||||
|
||||
```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>
|
||||
```
|
||||
* ...or include within your HTML
|
||||
|
||||
```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",
|
||||
@ -64,11 +74,11 @@ const data = {
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
name: "Some Data", chartType: "bar",
|
||||
name: "Some Data", type: "bar",
|
||||
values: [25, 40, 30, 35, 8, 52, 17, -4]
|
||||
},
|
||||
{
|
||||
name: "Another Set", chartType: "line",
|
||||
name: "Another Set", type: "line",
|
||||
values: [25, 50, -10, 15, 18, 32, 27, 14]
|
||||
}
|
||||
]
|
||||
@ -84,26 +94,66 @@ const chart = new frappe.Chart("#chart", { // or a DOM element,
|
||||
})
|
||||
```
|
||||
|
||||
## Contributing
|
||||
...or for es-modules:
|
||||
```js
|
||||
|
||||
//replace new frappe.Chart() with new 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:
|
||||
|
||||
1. Clone this repo.
|
||||
2. `cd` into project directory
|
||||
3. `npm install`
|
||||
4. `npm i npm-run-all -D` (*optional --> might be required for some developers*)
|
||||
5. `npm run dev`
|
||||
4. `npm run dev`
|
||||
|
||||
## Links
|
||||
#### Updates
|
||||
|
||||
- [Read the blog](https://medium.com/@pratu16x7/so-we-decided-to-create-our-own-charts-a95cb5032c97)
|
||||
##### 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.
|
||||
|
||||
##### 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>
|
||||
|
||||
4006
dist/frappe-charts.esm.js
vendored
Normal file
4006
dist/frappe-charts.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
dist/frappe-charts.min.cjs.js
vendored
Normal file
2
dist/frappe-charts.min.cjs.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/frappe-charts.min.cjs.js.map
vendored
Normal file
1
dist/frappe-charts.min.cjs.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/frappe-charts.min.css
vendored
Normal file
1
dist/frappe-charts.min.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
.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:1;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ol,.graph-svg-tip ul{padding-left:0;display:-webkit-box;display:-ms-flexbox;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;-webkit-box-flex:1;-ms-flex:1;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;border-top-color:rgba(0,0,0,.8)}.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}
|
||||
2
dist/frappe-charts.min.esm.js
vendored
Normal file
2
dist/frappe-charts.min.esm.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/frappe-charts.min.esm.js.map
vendored
Normal file
1
dist/frappe-charts.min.esm.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/frappe-charts.min.iife.js
vendored
Normal file
2
dist/frappe-charts.min.iife.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/frappe-charts.min.iife.js.map
vendored
Normal file
1
dist/frappe-charts.min.iife.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,52 +1,29 @@
|
||||
import { MONTH_NAMES_SHORT } from "../../../src/js/utils/date-utils";
|
||||
import { MONTH_NAMES_SHORT } from '../../../src/js/utils/date-utils';
|
||||
|
||||
// Composite Chart
|
||||
// ================================================================================
|
||||
const reportCountList = [
|
||||
152,
|
||||
222,
|
||||
199,
|
||||
287,
|
||||
534,
|
||||
709,
|
||||
1179,
|
||||
1256,
|
||||
1632,
|
||||
1856,
|
||||
1850,
|
||||
];
|
||||
const reportCountList = [152, 222, 199, 287, 534, 709,
|
||||
1179, 1256, 1632, 1856, 1850];
|
||||
|
||||
export const lineCompositeData = {
|
||||
labels: [
|
||||
"2007",
|
||||
"2008",
|
||||
"2009",
|
||||
"2010",
|
||||
"2011",
|
||||
"2012",
|
||||
"2013",
|
||||
"2014",
|
||||
"2015",
|
||||
"2016",
|
||||
"2017",
|
||||
],
|
||||
labels: ["2007", "2008", "2009", "2010", "2011", "2012",
|
||||
"2013", "2014", "2015", "2016", "2017"],
|
||||
|
||||
yMarkers: [
|
||||
{
|
||||
label: "Average 100 reports/month",
|
||||
value: 1200,
|
||||
options: { labelPos: "left" },
|
||||
},
|
||||
options: { labelPos: 'left' }
|
||||
}
|
||||
],
|
||||
|
||||
datasets: [
|
||||
{
|
||||
name: "Events",
|
||||
values: reportCountList,
|
||||
},
|
||||
],
|
||||
datasets: [{
|
||||
"name": "Events",
|
||||
"values": reportCountList
|
||||
}]
|
||||
};
|
||||
|
||||
|
||||
export const fireball_5_25 = [
|
||||
[4, 0, 3, 1, 1, 2, 1, 1, 1, 0, 1, 1],
|
||||
[2, 3, 3, 2, 1, 3, 0, 1, 2, 7, 10, 4],
|
||||
@ -85,7 +62,7 @@ export const fireballOver25 = [
|
||||
[5, 6, 1, 2, 5, 4, 5, 5, 16, 9, 14, 9],
|
||||
[5, 4, 7, 5, 1, 5, 3, 3, 5, 7, 22, 2],
|
||||
[5, 13, 11, 6, 1, 7, 9, 8, 14, 17, 16, 3],
|
||||
[8, 9, 8, 6, 4, 8, 5, 6, 14, 11, 21, 12],
|
||||
[8, 9, 8, 6, 4, 8, 5, 6, 14, 11, 21, 12]
|
||||
];
|
||||
|
||||
export const barCompositeData = {
|
||||
@ -101,32 +78,24 @@ export const barCompositeData = {
|
||||
},
|
||||
{
|
||||
name: "2 to 5 reports",
|
||||
values: fireball_2_5[9],
|
||||
},
|
||||
],
|
||||
values: fireball_2_5[9]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Demo Chart multitype Chart
|
||||
// ================================================================================
|
||||
export const typeData = {
|
||||
labels: [
|
||||
"12am-3am",
|
||||
"3am-6am",
|
||||
"6am-9am",
|
||||
"9am-12pm",
|
||||
"12pm-3pm",
|
||||
"3pm-6pm",
|
||||
"6pm-9pm",
|
||||
"9pm-12am",
|
||||
],
|
||||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm",
|
||||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"],
|
||||
|
||||
yMarkers: [
|
||||
{
|
||||
label: "Marker",
|
||||
value: 43,
|
||||
options: { labelPos: "left" },
|
||||
options: { labelPos: 'left' }
|
||||
// type: 'dashed'
|
||||
},
|
||||
}
|
||||
],
|
||||
|
||||
yRegions: [
|
||||
@ -134,7 +103,7 @@ export const typeData = {
|
||||
label: "Region",
|
||||
start: -10,
|
||||
end: 50,
|
||||
options: { labelPos: "right" },
|
||||
options: { labelPos: 'right' }
|
||||
},
|
||||
],
|
||||
|
||||
@ -142,137 +111,69 @@ export const typeData = {
|
||||
{
|
||||
name: "Some Data",
|
||||
values: [18, 40, 30, 35, 8, 52, 17, -4],
|
||||
axisPosition: "right",
|
||||
chartType: "bar",
|
||||
axisPosition: 'right',
|
||||
chartType: 'bar'
|
||||
},
|
||||
{
|
||||
name: "Another Set",
|
||||
values: [30, 50, -10, 15, 18, 32, 27, 14],
|
||||
axisPosition: "right",
|
||||
chartType: "bar",
|
||||
axisPosition: 'right',
|
||||
chartType: 'bar'
|
||||
},
|
||||
{
|
||||
name: "Yet Another",
|
||||
values: [15, 20, -3, -15, 58, 12, -17, 37],
|
||||
chartType: "line",
|
||||
},
|
||||
],
|
||||
chartType: 'line'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const trendsData = {
|
||||
labels: [
|
||||
1967,
|
||||
1968,
|
||||
1969,
|
||||
1970,
|
||||
1971,
|
||||
1972,
|
||||
1973,
|
||||
1974,
|
||||
1975,
|
||||
1976,
|
||||
1977,
|
||||
1978,
|
||||
1979,
|
||||
1980,
|
||||
1981,
|
||||
1982,
|
||||
1983,
|
||||
1984,
|
||||
1985,
|
||||
1986,
|
||||
1987,
|
||||
1988,
|
||||
1989,
|
||||
1990,
|
||||
1991,
|
||||
1992,
|
||||
1993,
|
||||
1994,
|
||||
1995,
|
||||
1996,
|
||||
1997,
|
||||
1998,
|
||||
1999,
|
||||
2000,
|
||||
2001,
|
||||
2002,
|
||||
2003,
|
||||
2004,
|
||||
2005,
|
||||
2006,
|
||||
2007,
|
||||
2008,
|
||||
2009,
|
||||
2010,
|
||||
2011,
|
||||
2012,
|
||||
2013,
|
||||
2014,
|
||||
2015,
|
||||
2016,
|
||||
],
|
||||
labels: [1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976,
|
||||
1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986,
|
||||
1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996,
|
||||
1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006,
|
||||
2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016] ,
|
||||
datasets: [
|
||||
{
|
||||
values: [
|
||||
132.9,
|
||||
150.0,
|
||||
149.4,
|
||||
148.0,
|
||||
94.4,
|
||||
97.6,
|
||||
54.1,
|
||||
49.2,
|
||||
22.5,
|
||||
18.4,
|
||||
39.3,
|
||||
131.0,
|
||||
220.1,
|
||||
218.9,
|
||||
198.9,
|
||||
162.4,
|
||||
91.0,
|
||||
60.5,
|
||||
20.6,
|
||||
14.8,
|
||||
33.9,
|
||||
123.0,
|
||||
211.1,
|
||||
191.8,
|
||||
203.3,
|
||||
133.0,
|
||||
76.1,
|
||||
44.9,
|
||||
25.1,
|
||||
11.6,
|
||||
28.9,
|
||||
88.3,
|
||||
136.3,
|
||||
173.9,
|
||||
170.4,
|
||||
163.6,
|
||||
99.3,
|
||||
65.3,
|
||||
45.8,
|
||||
24.7,
|
||||
12.6,
|
||||
4.2,
|
||||
4.8,
|
||||
24.9,
|
||||
80.8,
|
||||
84.5,
|
||||
94.0,
|
||||
113.3,
|
||||
69.8,
|
||||
39.8,
|
||||
],
|
||||
},
|
||||
],
|
||||
values: [132.9, 150.0, 149.4, 148.0, 94.4, 97.6, 54.1, 49.2, 22.5, 18.4,
|
||||
39.3, 131.0, 220.1, 218.9, 198.9, 162.4, 91.0, 60.5, 20.6, 14.8,
|
||||
33.9, 123.0, 211.1, 191.8, 203.3, 133.0, 76.1, 44.9, 25.1, 11.6,
|
||||
28.9, 88.3, 136.3, 173.9, 170.4, 163.6, 99.3, 65.3, 45.8, 24.7,
|
||||
12.6, 4.2, 4.8, 24.9, 80.8, 84.5, 94.0, 113.3, 69.8, 39.8]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const moonData = {
|
||||
names: ["Ganymede", "Callisto", "Io", "Europa"],
|
||||
masses: [14819000, 10759000, 8931900, 4800000],
|
||||
distances: [1070.412, 1882.709, 421.7, 671.034],
|
||||
diameters: [5262.4, 4820.6, 3637.4, 3121.6],
|
||||
distances: [1070.412, 1882.709, 421.700, 671.034],
|
||||
diameters: [5262.4, 4820.6, 3637.4, 3121.6],
|
||||
};
|
||||
|
||||
// const jupiterMoons = {
|
||||
// 'Ganymede': {
|
||||
// mass: '14819000 x 10^16 kg',
|
||||
// 'semi-major-axis': '1070412 km',
|
||||
// 'diameter': '5262.4 km'
|
||||
// },
|
||||
// 'Callisto': {
|
||||
// mass: '10759000 x 10^16 kg',
|
||||
// 'semi-major-axis': '1882709 km',
|
||||
// 'diameter': '4820.6 km'
|
||||
// },
|
||||
// 'Io': {
|
||||
// mass: '8931900 x 10^16 kg',
|
||||
// 'semi-major-axis': '421700 km',
|
||||
// 'diameter': '3637.4 km'
|
||||
// },
|
||||
// 'Europa': {
|
||||
// mass: '4800000 x 10^16 kg',
|
||||
// 'semi-major-axis': '671034 km',
|
||||
// 'diameter': '3121.6 km'
|
||||
// },
|
||||
// };
|
||||
|
||||
// ================================================================================
|
||||
|
||||
|
||||
2
docs/assets/js/frappe-charts.min.js
vendored
Normal file
2
docs/assets/js/frappe-charts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
docs/assets/js/frappe-charts.min.js.map
Normal file
1
docs/assets/js/frappe-charts.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,10 +1,8 @@
|
||||
import { shuffle, getRandomBias } from '../../../src/js/utils/helpers';
|
||||
import { HEATMAP_COLORS_YELLOW, HEATMAP_COLORS_BLUE } from '../../../src/js/utils/constants';
|
||||
import { SEC_IN_DAY, clone, timestampToMidnight, timestampSec, addDays } from '../../../src/js/utils/date-utils';
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { fireballOver25, fireball_2_5, fireball_5_25, lineCompositeData,
|
||||
barCompositeData, typeData, trendsData, moonData } from './data';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import demoConfig from './demoConfig';
|
||||
// import { lineComposite, barComposite } from './demoConfig';
|
||||
// ================================================================================
|
||||
|
||||
122
docs/assets/js/index.min.js
vendored
122
docs/assets/js/index.min.js
vendored
@ -1,11 +1,36 @@
|
||||
(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
|
||||
var ANGLE_RATIO = Math.PI / 180;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Shuffles array in place. ES6 version
|
||||
@ -26,6 +51,24 @@ 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;
|
||||
@ -37,10 +80,21 @@ function getRandomBias(min, max, bias, influence) {
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
@ -59,6 +113,21 @@ function timestampToMidnight(timestamp) {
|
||||
return midnightTs;
|
||||
}
|
||||
|
||||
// export function getMonthsBetween(startDate, endDate) {}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// mutates
|
||||
|
||||
|
||||
// mutates
|
||||
function addDays(date, numberOfDays) {
|
||||
date.setDate(date.getDate() + numberOfDays);
|
||||
@ -74,12 +143,12 @@ var lineCompositeData = {
|
||||
yMarkers: [{
|
||||
label: "Average 100 reports/month",
|
||||
value: 1200,
|
||||
options: { labelPos: "left" }
|
||||
options: { labelPos: 'left' }
|
||||
}],
|
||||
|
||||
datasets: [{
|
||||
name: "Events",
|
||||
values: reportCountList
|
||||
"name": "Events",
|
||||
"values": reportCountList
|
||||
}]
|
||||
};
|
||||
|
||||
@ -111,31 +180,31 @@ var typeData = {
|
||||
yMarkers: [{
|
||||
label: "Marker",
|
||||
value: 43,
|
||||
options: { labelPos: "left" }
|
||||
// type: 'dashed'
|
||||
}],
|
||||
options: { labelPos: 'left'
|
||||
// type: 'dashed'
|
||||
} }],
|
||||
|
||||
yRegions: [{
|
||||
label: "Region",
|
||||
start: -10,
|
||||
end: 50,
|
||||
options: { labelPos: "right" }
|
||||
options: { labelPos: 'right' }
|
||||
}],
|
||||
|
||||
datasets: [{
|
||||
name: "Some Data",
|
||||
values: [18, 40, 30, 35, 8, 52, 17, -4],
|
||||
axisPosition: "right",
|
||||
chartType: "bar"
|
||||
axisPosition: 'right',
|
||||
chartType: 'bar'
|
||||
}, {
|
||||
name: "Another Set",
|
||||
values: [30, 50, -10, 15, 18, 32, 27, 14],
|
||||
axisPosition: "right",
|
||||
chartType: "bar"
|
||||
axisPosition: 'right',
|
||||
chartType: 'bar'
|
||||
}, {
|
||||
name: "Yet Another",
|
||||
values: [15, 20, -3, -15, 58, 12, -17, 37],
|
||||
chartType: "line"
|
||||
chartType: 'line'
|
||||
}]
|
||||
};
|
||||
|
||||
@ -149,10 +218,35 @@ var trendsData = {
|
||||
var moonData = {
|
||||
names: ["Ganymede", "Callisto", "Io", "Europa"],
|
||||
masses: [14819000, 10759000, 8931900, 4800000],
|
||||
distances: [1070.412, 1882.709, 421.7, 671.034],
|
||||
distances: [1070.412, 1882.709, 421.700, 671.034],
|
||||
diameters: [5262.4, 4820.6, 3637.4, 3121.6]
|
||||
};
|
||||
|
||||
// const jupiterMoons = {
|
||||
// 'Ganymede': {
|
||||
// mass: '14819000 x 10^16 kg',
|
||||
// 'semi-major-axis': '1070412 km',
|
||||
// 'diameter': '5262.4 km'
|
||||
// },
|
||||
// 'Callisto': {
|
||||
// mass: '10759000 x 10^16 kg',
|
||||
// 'semi-major-axis': '1882709 km',
|
||||
// 'diameter': '4820.6 km'
|
||||
// },
|
||||
// 'Io': {
|
||||
// mass: '8931900 x 10^16 kg',
|
||||
// 'semi-major-axis': '421700 km',
|
||||
// 'diameter': '3637.4 km'
|
||||
// },
|
||||
// 'Europa': {
|
||||
// mass: '4800000 x 10^16 kg',
|
||||
// 'semi-major-axis': '671034 km',
|
||||
// 'diameter': '3121.6 km'
|
||||
// },
|
||||
// };
|
||||
|
||||
// ================================================================================
|
||||
|
||||
var demoConfig = {
|
||||
lineComposite: {
|
||||
elementID: "#chart-composite-1",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -83,6 +83,7 @@ redirect_to: "https://frappe.io/charts"
|
||||
<button type="button" class="btn btn-sm btn-secondary active" data-type='axis-mixed'>Mixed</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-type='pie'>Pie Chart</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-type='donut'>Donut Chart</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-type='funnel'>Funnel Chart</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button>
|
||||
</div>
|
||||
<div class="btn-group export-buttons margin-top mx-auto" role="group">
|
||||
|
||||
25092
package-lock.json
generated
25092
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
115
package.json
115
package.json
@ -1,51 +1,68 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
"name": "frappe-charts",
|
||||
"version": "1.3.0",
|
||||
"description": "https://frappe.github.io/charts",
|
||||
"main": "dist/frappe-charts.min.cjs.js",
|
||||
"module": "dist/frappe-charts.min.esm.js",
|
||||
"src": "dist/frappe-charts.esm.js",
|
||||
"browser": "dist/frappe-charts.min.iife.js",
|
||||
"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": {
|
||||
"autoprefixer": "^8.2.0",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-istanbul": "^5.1.4",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-latest": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"clean-css": "^4.1.11",
|
||||
"coveralls": "^3.0.0",
|
||||
"cross-env": "^5.1.4",
|
||||
"cssnano": "^4.1.10",
|
||||
"eslint": "^4.18.2",
|
||||
"mocha": "^5.0.5",
|
||||
"node-sass": "^4.12.0",
|
||||
"npm-run-all": "^4.1.1",
|
||||
"nyc": "^14.1.1",
|
||||
"postcss": "^6.0.21",
|
||||
"postcss-cssnext": "^3.0.2",
|
||||
"postcss-nested": "^2.1.2",
|
||||
"precss": "^3.1.2",
|
||||
"rollup": "^0.50.0",
|
||||
"rollup-plugin-babel": "^3.0.2",
|
||||
"rollup-plugin-eslint": "^6.0.0",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"rollup-plugin-postcss": "^2.0.3",
|
||||
"rollup-plugin-replace": "^2.0.0",
|
||||
"rollup-plugin-uglify": "^2.0.1",
|
||||
"rollup-plugin-uglify-es": "0.0.1",
|
||||
"rollup-watch": "^4.3.1"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
234
rollup.config.js
234
rollup.config.js
@ -1,47 +1,195 @@
|
||||
import pkg from "./package.json";
|
||||
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";
|
||||
// Rollup plugins
|
||||
import babel from 'rollup-plugin-babel';
|
||||
import { eslint } from 'rollup-plugin-eslint';
|
||||
import replace from 'rollup-plugin-replace';
|
||||
import uglify from 'rollup-plugin-uglify-es';
|
||||
import sass from 'node-sass';
|
||||
|
||||
// PostCSS plugins
|
||||
import postcssPlugin from 'rollup-plugin-postcss';
|
||||
import nested from 'postcss-nested';
|
||||
import cssnext from 'postcss-cssnext';
|
||||
import cssnano from 'cssnano';
|
||||
|
||||
import postcss from 'postcss';
|
||||
import precss from 'precss';
|
||||
import CleanCSS from 'clean-css';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
import fs from 'fs';
|
||||
|
||||
fs.readFile('src/css/charts.scss', (err, css) => {
|
||||
postcss([precss, autoprefixer])
|
||||
.process(css, { from: 'src/css/charts.scss', to: 'src/css/charts.css' })
|
||||
.then(result => {
|
||||
let options = {
|
||||
level: {
|
||||
1: {
|
||||
removeQuotes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
let output = new CleanCSS(options).minify(result.css);
|
||||
let res = JSON.stringify(output.styles).replace(/"/g, "'");
|
||||
let js = `export const CSSTEXT = "${res.slice(1, -1)}";`;
|
||||
fs.writeFile('src/css/chartsCss.js', js);
|
||||
});
|
||||
});
|
||||
|
||||
export default [
|
||||
// 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", sourcemap: true },
|
||||
{ file: pkg.module, format: "es", sourcemap: true },
|
||||
],
|
||||
plugins: [
|
||||
babel({
|
||||
exclude: ["node_modules/**"],
|
||||
}),
|
||||
terser(),
|
||||
postcss(),
|
||||
bundleSize(),
|
||||
],
|
||||
},
|
||||
{
|
||||
input: 'src/js/index.js',
|
||||
sourcemap: true,
|
||||
output: [
|
||||
{
|
||||
file: 'docs/assets/js/frappe-charts.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
{
|
||||
file: pkg.browser,
|
||||
format: 'iife',
|
||||
}
|
||||
],
|
||||
name: 'frappe',
|
||||
plugins: [
|
||||
postcssPlugin({
|
||||
preprocessor: (content, id) => new Promise((resolve, reject) => {
|
||||
const result = sass.renderSync({ file: id })
|
||||
resolve({ code: result.css.toString() })
|
||||
}),
|
||||
extensions: [ '.scss' ],
|
||||
plugins: [
|
||||
nested(),
|
||||
cssnext({ warnForDuplicates: false }),
|
||||
cssnano()
|
||||
]
|
||||
}),
|
||||
eslint({
|
||||
exclude: [
|
||||
'src/css/**'
|
||||
]
|
||||
}),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
plugins: ['external-helpers']
|
||||
}),
|
||||
replace({
|
||||
exclude: 'node_modules/**',
|
||||
ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
|
||||
}),
|
||||
uglify()
|
||||
]
|
||||
},
|
||||
{
|
||||
input: 'docs/assets/js/index.js',
|
||||
sourcemap: true,
|
||||
output: [
|
||||
{
|
||||
file: 'docs/assets/js/index.min.js',
|
||||
format: 'iife',
|
||||
}
|
||||
],
|
||||
name: 'frappe',
|
||||
plugins: [
|
||||
postcssPlugin({
|
||||
preprocessor: (content, id) => new Promise((resolve, reject) => {
|
||||
const result = sass.renderSync({ file: id })
|
||||
resolve({ code: result.css.toString() })
|
||||
}),
|
||||
extensions: [ '.scss' ],
|
||||
plugins: [
|
||||
nested(),
|
||||
cssnext({ warnForDuplicates: false }),
|
||||
cssnano()
|
||||
]
|
||||
}),
|
||||
eslint({
|
||||
exclude: [
|
||||
'src/css/**'
|
||||
]
|
||||
}),
|
||||
babel({
|
||||
exclude: 'node_modules/**'
|
||||
}),
|
||||
replace({
|
||||
exclude: 'node_modules/**',
|
||||
ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
input: 'src/js/chart.js',
|
||||
sourcemap: true,
|
||||
output: [
|
||||
{
|
||||
file: pkg.main,
|
||||
format: 'cjs',
|
||||
},
|
||||
{
|
||||
file: pkg.module,
|
||||
format: 'es',
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
postcssPlugin({
|
||||
preprocessor: (content, id) => new Promise((resolve, reject) => {
|
||||
const result = sass.renderSync({ file: id })
|
||||
resolve({ code: result.css.toString() })
|
||||
}),
|
||||
extensions: [ '.scss' ],
|
||||
plugins: [
|
||||
nested(),
|
||||
cssnext({ warnForDuplicates: false }),
|
||||
cssnano()
|
||||
]
|
||||
}),
|
||||
eslint({
|
||||
exclude: [
|
||||
'src/css/**',
|
||||
]
|
||||
}),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
}),
|
||||
replace({
|
||||
exclude: 'node_modules/**',
|
||||
ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
|
||||
}),
|
||||
uglify()
|
||||
],
|
||||
},
|
||||
{
|
||||
input: 'src/js/chart.js',
|
||||
output: [
|
||||
{
|
||||
file: pkg.src,
|
||||
format: 'es',
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
postcssPlugin({
|
||||
preprocessor: (content, id) => new Promise((resolve, reject) => {
|
||||
const result = sass.renderSync({ file: id })
|
||||
resolve({ code: result.css.toString() })
|
||||
}),
|
||||
extensions: [ '.scss' ],
|
||||
extract: 'dist/frappe-charts.min.css',
|
||||
plugins: [
|
||||
nested(),
|
||||
cssnext({ warnForDuplicates: false }),
|
||||
cssnano()
|
||||
]
|
||||
}),
|
||||
eslint({
|
||||
exclude: [
|
||||
'src/css/**',
|
||||
]
|
||||
}),
|
||||
replace({
|
||||
exclude: 'node_modules/**',
|
||||
ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
|
||||
})
|
||||
],
|
||||
}
|
||||
];
|
||||
|
||||
@ -1,192 +1,116 @@
|
||||
:root {
|
||||
--charts-label-color: #313b44;
|
||||
--charts-axis-line-color: #f4f5f6;
|
||||
|
||||
--charts-tooltip-title: var(--charts-label-color);
|
||||
--charts-tooltip-label: var(--charts-label-color);
|
||||
--charts-tooltip-value: #192734;
|
||||
--charts-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;
|
||||
/* 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;
|
||||
|
||||
.axis,
|
||||
.chart-label {
|
||||
fill: var(--charts-label-color);
|
||||
.axis, .chart-label {
|
||||
fill: #555b51;
|
||||
|
||||
line {
|
||||
stroke: var(--charts-axis-line-color);
|
||||
}
|
||||
}
|
||||
line {
|
||||
stroke: #dadada;
|
||||
}
|
||||
}
|
||||
.dataset-units {
|
||||
circle {
|
||||
stroke: #fff;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.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(--charts-stroke-width);
|
||||
}
|
||||
}
|
||||
|
||||
.dataset-path {
|
||||
stroke-width: var(--charts-stroke-width);
|
||||
}
|
||||
|
||||
.path-group {
|
||||
path {
|
||||
fill: none;
|
||||
stroke-opacity: 1;
|
||||
stroke-width: var(--charts-stroke-width);
|
||||
}
|
||||
}
|
||||
|
||||
line.dashed {
|
||||
stroke-dasharray: 5, 3;
|
||||
}
|
||||
|
||||
.axis-line {
|
||||
.specific-value {
|
||||
text-anchor: start;
|
||||
}
|
||||
|
||||
.y-line {
|
||||
text-anchor: end;
|
||||
}
|
||||
|
||||
.x-line {
|
||||
text-anchor: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.legend-dataset-label {
|
||||
fill: var(--charts-legend-label);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.legend-dataset-value {
|
||||
fill: var(--charts-legend-value);
|
||||
}
|
||||
path {
|
||||
fill: none;
|
||||
stroke-opacity: 1;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
.dataset-path {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
.path-group {
|
||||
path {
|
||||
fill: none;
|
||||
stroke-opacity: 1;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
line.dashed {
|
||||
stroke-dasharray: 5, 3;
|
||||
}
|
||||
.axis-line {
|
||||
.specific-value {
|
||||
text-anchor: start;
|
||||
}
|
||||
.y-line {
|
||||
text-anchor: end;
|
||||
}
|
||||
.x-line {
|
||||
text-anchor: middle;
|
||||
}
|
||||
}
|
||||
.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: 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;
|
||||
}
|
||||
|
||||
ol {
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
ul.data-point-list {
|
||||
li {
|
||||
min-width: 90px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
color: var(--charts-tooltip-title);
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
text-transform: uppercase;
|
||||
|
||||
strong {
|
||||
color: var(--charts-tooltip-value);
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
|
||||
&.tooltip-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
|
||||
padding: 5px 15px 15px 15px;
|
||||
|
||||
.tooltip-legend {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tooltip-label {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
max-width: 100px;
|
||||
|
||||
color: var(--fr-tooltip-label);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tooltip-value {
|
||||
color: var(--charts-tooltip-value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
position: absolute;
|
||||
z-index: 99999;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
color: #959da5;
|
||||
text-align: center;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 3px;
|
||||
ul {
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
}
|
||||
ol {
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
}
|
||||
ul.data-point-list {
|
||||
li {
|
||||
min-width: 90px;
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
strong {
|
||||
color: #dfe2e5;
|
||||
font-weight: 600;
|
||||
}
|
||||
.svg-pointer {
|
||||
position: absolute;
|
||||
height: 5px;
|
||||
margin: 0 0 0 -5px;
|
||||
content: ' ';
|
||||
border: 5px solid transparent;
|
||||
border-top-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
&.comparison {
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
.title {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,2 +1 @@
|
||||
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;border-top-color:rgba(0,0,0,.8)}.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}";
|
||||
@ -1,38 +1,42 @@
|
||||
import "../css/charts.scss";
|
||||
import '../css/charts.scss';
|
||||
|
||||
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 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 FunnelChart from './charts/FunnelChart';
|
||||
|
||||
const chartTypes = {
|
||||
bar: AxisChart,
|
||||
line: AxisChart,
|
||||
percentage: PercentageChart,
|
||||
heatmap: Heatmap,
|
||||
pie: PieChart,
|
||||
donut: DonutChart,
|
||||
bar: AxisChart,
|
||||
line: AxisChart,
|
||||
// multiaxis: MultiAxisChart,
|
||||
percentage: PercentageChart,
|
||||
heatmap: Heatmap,
|
||||
pie: PieChart,
|
||||
donut: DonutChart,
|
||||
funnel: FunnelChart,
|
||||
};
|
||||
|
||||
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 };
|
||||
@ -1,94 +1,90 @@
|
||||
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 { legendDot } from '../utils/draw';
|
||||
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.legendRowHeight = 60;
|
||||
}
|
||||
this.config.maxSlices = args.maxSlices || 20;
|
||||
this.config.maxLegendPoints = args.maxLegendPoints || 20;
|
||||
}
|
||||
|
||||
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(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);
|
||||
super.renderLegend(this.legendTotals);
|
||||
}
|
||||
renderLegend() {
|
||||
let s = this.state;
|
||||
this.legendArea.textContent = '';
|
||||
this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints);
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
let count = 0;
|
||||
let y = 0;
|
||||
this.legendTotals.map((d, i) => {
|
||||
let barWidth = 110;
|
||||
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 += 20;
|
||||
}
|
||||
let x = barWidth * count + 5;
|
||||
let dot = legendDot(
|
||||
x,
|
||||
y,
|
||||
5,
|
||||
this.colors[i],
|
||||
`${s.labels[i]}: ${d}`,
|
||||
this.config.truncateLegends
|
||||
);
|
||||
this.legendArea.appendChild(dot);
|
||||
count++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,29 +1,13 @@
|
||||
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";
|
||||
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';
|
||||
|
||||
export default class AxisChart extends BaseChart {
|
||||
constructor(parent, args) {
|
||||
@ -32,14 +16,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;
|
||||
}
|
||||
@ -47,63 +31,33 @@ 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 = 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.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.formatTooltipX = options.tooltipOptions.formatTooltipX;
|
||||
this.config.formatTooltipY = options.tooltipOptions.formatTooltipY;
|
||||
|
||||
this.config.valuesOverPoints = options.valuesOverPoints;
|
||||
this.config.legendRowHeight = 30;
|
||||
}
|
||||
|
||||
prepareData(data = this.data, config = this.config) {
|
||||
return dataPrep(data, this.type, config.continuous);
|
||||
prepareData(data=this.data) {
|
||||
return dataPrep(data, this.type);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@ -113,9 +67,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);
|
||||
@ -125,121 +79,22 @@ export default class AxisChart extends BaseChart {
|
||||
labels: labels,
|
||||
positions: labels.map((d, i) =>
|
||||
floatTwo(s.xOffset + i * s.unitWidth)
|
||||
),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
calcYAxisParameters(dataValues, withMinimum = "false") {
|
||||
let yPts,
|
||||
scaleMultiplier,
|
||||
intervalHeight,
|
||||
zeroLine,
|
||||
positions,
|
||||
yAxisConfigObject,
|
||||
yAxisAlignment,
|
||||
yKeys;
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
this.state.yAxis = {
|
||||
labels: yPts,
|
||||
positions: yPts.map(d => zeroLine - d * scaleMultiplier),
|
||||
scaleMultiplier: scaleMultiplier,
|
||||
zeroLine: zeroLine,
|
||||
};
|
||||
|
||||
// Dependent if above changes
|
||||
this.calcDatasetPoints();
|
||||
@ -249,57 +104,35 @@ export default class AxisChart extends BaseChart {
|
||||
|
||||
calcDatasetPoints() {
|
||||
let s = this.state;
|
||||
let scaleAll = (values, id) => {
|
||||
return values.map((val) => {
|
||||
let { yAxis } = s;
|
||||
let scaleAll = values => values.map(val => scale(val, s.yAxis));
|
||||
|
||||
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 &&
|
||||
d.name.replace(/<|>|&/g, (char) =>
|
||||
char == "&" ? "&" : char == "<" ? "<" : ">"
|
||||
),
|
||||
name: d.name,
|
||||
index: i,
|
||||
barIndex:
|
||||
d.chartType === "bar" ? s.barChartIndex++ : s.barChartIndex,
|
||||
chartType: d.chartType,
|
||||
|
||||
values: values,
|
||||
yPositions: scaleAll(values, d.axisID),
|
||||
id: d.axisID,
|
||||
yPositions: scaleAll(values),
|
||||
|
||||
cumulativeYs: cumulativeYs,
|
||||
cumulativeYPos: scaleAll(cumulativeYs, d.axisID),
|
||||
cumulativeYPos: scaleAll(cumulativeYs),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
@ -308,169 +141,101 @@ 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 multiAxis = this.config.yAxisConfig ? true : false;
|
||||
let allValueLists = multiAxis ? {} : [];
|
||||
let key = 'values';
|
||||
|
||||
let groupBy = (arr, property) => {
|
||||
return arr.reduce((acc, cur) => {
|
||||
acc[cur[property]] = [...(acc[cur[property]] || []), cur];
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
let generateCumulative = (arr) => {
|
||||
if(this.barOptions.stacked) {
|
||||
key = 'cumulativeYs';
|
||||
let cumulative = new Array(this.state.datasetLength).fill(0);
|
||||
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];
|
||||
this.data.datasets.map((d, i) => {
|
||||
let values = this.data.datasets[i].values;
|
||||
d[key] = cumulative = cumulative.map((c, i) => c + values[i]);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.data.yMarkers && !multiAxis) {
|
||||
allValueLists.push(this.data.yMarkers.map((d) => d.value));
|
||||
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.yRegions && !multiAxis) {
|
||||
this.data.yRegions.map((d) => {
|
||||
if(this.data.yRegions) {
|
||||
this.data.yRegions.map(d => {
|
||||
allValueLists.push([d.end, d.start]);
|
||||
});
|
||||
}
|
||||
|
||||
return multiAxis ? allValueLists : [].concat(...allValueLists); //return [].concat(...allValueLists); master
|
||||
return [].concat(...allValueLists);
|
||||
}
|
||||
|
||||
setupComponents() {
|
||||
let componentConfigs = [
|
||||
[
|
||||
"xAxis",
|
||||
'yAxis',
|
||||
{
|
||||
mode: this.config.yAxisMode,
|
||||
width: this.width,
|
||||
shortenNumbers: this.config.shortenYAxisNumbers
|
||||
// pos: 'right'
|
||||
},
|
||||
function() {
|
||||
return this.state.yAxis;
|
||||
}.bind(this)
|
||||
],
|
||||
|
||||
[
|
||||
'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)
|
||||
],
|
||||
];
|
||||
|
||||
// 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 barDatasets = this.state.datasets.filter(d => d.chartType === 'bar');
|
||||
let lineDatasets = this.state.datasets.filter(d => d.chartType === 'line');
|
||||
|
||||
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 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],
|
||||
@ -480,52 +245,32 @@ 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);
|
||||
|
||||
// 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 xPositions = s.xAxis.positions.map(x => x - barsWidth/2);
|
||||
if(!stacked) {
|
||||
xPositions = xPositions.map(p => p + barWidth * index);
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
@ -535,18 +280,18 @@ export default class AxisChart extends BaseChart {
|
||||
// values: d.values,
|
||||
labels: labels,
|
||||
|
||||
zeroLine: yAxis.zeroLine,
|
||||
zeroLine: s.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],
|
||||
@ -560,20 +305,11 @@ 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];
|
||||
|
||||
// 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;
|
||||
let minLine = s.yAxis.positions[0] < s.yAxis.zeroLine
|
||||
? s.yAxis.positions[0] : s.yAxis.zeroLine;
|
||||
|
||||
return {
|
||||
xPositions: s.xAxis.positions,
|
||||
@ -584,49 +320,37 @@ 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() {
|
||||
@ -661,16 +385,14 @@ 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();
|
||||
@ -680,83 +402,59 @@ 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) {
|
||||
let dbi = this.dataByIndex[index];
|
||||
let dbi = this.dataByIndex[index];
|
||||
|
||||
this.tip.setValues(
|
||||
dbi.xPos + this.tip.offset.x,
|
||||
dbi.yExtreme + this.tip.offset.y,
|
||||
{ name: dbi.formattedLabel, value: "" },
|
||||
dbi.values,
|
||||
index
|
||||
);
|
||||
this.tip.setValues(
|
||||
dbi.xPos + this.tip.offset.x,
|
||||
dbi.yExtreme + this.tip.offset.y,
|
||||
{name: dbi.formattedLabel, value: ''},
|
||||
dbi.values,
|
||||
index
|
||||
);
|
||||
|
||||
this.tip.showTip();
|
||||
}
|
||||
this.tip.showTip();
|
||||
}
|
||||
|
||||
renderLegend() {
|
||||
let s = this.data;
|
||||
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);
|
||||
if(s.datasets.length > 1) {
|
||||
this.legendArea.textContent = '';
|
||||
s.datasets.map((d, i) => {
|
||||
let barWidth = LEGEND_ITEM_WIDTH;
|
||||
let barWidth = AXIS_LEGEND_BAR_SIZE;
|
||||
// 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,
|
||||
@ -764,12 +462,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);
|
||||
@ -778,8 +476,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);
|
||||
});
|
||||
@ -787,30 +485,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);
|
||||
});
|
||||
@ -824,12 +522,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;
|
||||
}
|
||||
@ -837,15 +535,17 @@ 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) => {
|
||||
@ -854,19 +554,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);
|
||||
}
|
||||
@ -875,7 +575,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];
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,84 +1,46 @@
|
||||
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";
|
||||
import SvgTip from '../objects/SvgTip';
|
||||
import { $, isElementInViewport, getElementContentWidth } 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';
|
||||
|
||||
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.title = options.title || '';
|
||||
this.type = options.type || '';
|
||||
|
||||
this.realData = this.prepareData(options.data);
|
||||
this.data = this.prepareFirstData(this.realData);
|
||||
|
||||
this.colors = this.validateColors(options.colors, this.type);
|
||||
|
||||
this.config = {
|
||||
showTooltip: 1, // calculate
|
||||
showLegend:
|
||||
typeof options.showLegend !== "undefined"
|
||||
? options.showLegend
|
||||
: 1,
|
||||
showLegend: 1, // calculate
|
||||
isNavigable: options.isNavigable || 0,
|
||||
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,
|
||||
animate: (typeof options.animate !== 'undefined') ? options.animate : 1,
|
||||
truncateLegends: options.truncateLegends || 0
|
||||
};
|
||||
|
||||
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 = {};
|
||||
@ -86,7 +48,7 @@ export default class BaseChart {
|
||||
|
||||
this.initTimeout = INIT_CHART_UPDATE_TIMEOUT;
|
||||
|
||||
if (this.config.isNavigable) {
|
||||
if(this.config.isNavigable) {
|
||||
this.overlays = [];
|
||||
}
|
||||
|
||||
@ -106,7 +68,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);
|
||||
@ -127,19 +89,13 @@ export default class BaseChart {
|
||||
|
||||
// Bind window events
|
||||
this.boundDrawFn = () => this.draw(true);
|
||||
// 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);
|
||||
window.addEventListener('resize', this.boundDrawFn);
|
||||
window.addEventListener('orientationchange', this.boundDrawFn);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
//if (this.resizeObserver) this.resizeObserver.disconnect();
|
||||
window.removeEventListener("resize", this.boundDrawFn);
|
||||
window.removeEventListener("orientationchange", this.boundDrawFn);
|
||||
window.removeEventListener('resize', this.boundDrawFn);
|
||||
window.removeEventListener('orientationchange', this.boundDrawFn);
|
||||
}
|
||||
|
||||
// Has to be called manually
|
||||
@ -153,57 +109,47 @@ 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) {
|
||||
if (onlyWidthChange && isHidden(this.parent)) {
|
||||
// Don't update anything if the chart is hidden
|
||||
return;
|
||||
}
|
||||
draw(onlyWidthChange=false, init=false) {
|
||||
this.updateWidth();
|
||||
|
||||
this.calc(onlyWidthChange);
|
||||
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;
|
||||
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); */
|
||||
setTimeout(() => {this.update(this.data);}, this.initTimeout);
|
||||
}
|
||||
|
||||
if (this.config.showLegend) {
|
||||
this.renderLegend();
|
||||
}
|
||||
this.renderLegend();
|
||||
|
||||
this.setupNavigation(init);
|
||||
}
|
||||
@ -216,54 +162,50 @@ 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));
|
||||
}
|
||||
@ -271,90 +213,71 @@ 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, drawing = false, config) {
|
||||
if (!data) console.error("No data to update.");
|
||||
if (!drawing) data = deepClone(data);
|
||||
this.data = this.prepareData(data, config);
|
||||
update(data) {
|
||||
if(!data) {
|
||||
console.error('No data to update.');
|
||||
}
|
||||
this.data = this.prepareData(data);
|
||||
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(dataset) {
|
||||
this.legendArea.textContent = "";
|
||||
let count = 0;
|
||||
let y = 0;
|
||||
renderLegend() {}
|
||||
|
||||
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++;
|
||||
});
|
||||
}
|
||||
setupNavigation(init=false) {
|
||||
if(!this.config.isNavigable) return;
|
||||
|
||||
makeLegend() {}
|
||||
|
||||
setupNavigation(init = false) {
|
||||
if (!this.config.isNavigable) return;
|
||||
|
||||
if (init) {
|
||||
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]();
|
||||
}
|
||||
}
|
||||
@ -383,6 +306,6 @@ export default class BaseChart {
|
||||
|
||||
export() {
|
||||
let chartSvg = prepareForExport(this.svg);
|
||||
downloadFile(this.title || "Chart", [chartSvg]);
|
||||
downloadFile(this.title || 'Chart', [chartSvg]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,185 +1,161 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
this.init = 0;
|
||||
}
|
||||
s.sliceStrings.push(curPath);
|
||||
s.slicesProperties.push({
|
||||
startPosition,
|
||||
endPosition,
|
||||
value: total,
|
||||
total: s.grandTotal,
|
||||
startAngle,
|
||||
endAngle,
|
||||
angle: diffAngle
|
||||
});
|
||||
|
||||
setupComponents() {
|
||||
let s = this.state;
|
||||
});
|
||||
this.init = 0;
|
||||
}
|
||||
|
||||
let componentConfigs = [
|
||||
[
|
||||
"donutSlices",
|
||||
{},
|
||||
function () {
|
||||
return {
|
||||
sliceStrings: s.sliceStrings,
|
||||
colors: this.colors,
|
||||
strokeWidth: this.strokeWidth,
|
||||
};
|
||||
}.bind(this),
|
||||
],
|
||||
];
|
||||
setupComponents() {
|
||||
let s = this.state;
|
||||
|
||||
this.components = new Map(
|
||||
componentConfigs.map((args) => {
|
||||
let component = getComponent(...args);
|
||||
return [args[0], component];
|
||||
})
|
||||
);
|
||||
}
|
||||
let componentConfigs = [
|
||||
[
|
||||
'donutSlices',
|
||||
{ },
|
||||
function() {
|
||||
return {
|
||||
sliceStrings: s.sliceStrings,
|
||||
colors: this.colors,
|
||||
strokeWidth: this.strokeWidth,
|
||||
};
|
||||
}.bind(this)
|
||||
]
|
||||
];
|
||||
|
||||
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)`;
|
||||
}
|
||||
this.components = new Map(componentConfigs
|
||||
.map(args => {
|
||||
let component = getComponent(...args);
|
||||
return [args[0], component];
|
||||
}));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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)`;
|
||||
}
|
||||
|
||||
bindTooltip() {
|
||||
this.container.addEventListener("mousemove", this.mouseMove);
|
||||
this.container.addEventListener("mouseleave", this.mouseLeave);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
bindTooltip() {
|
||||
this.container.addEventListener('mousemove', this.mouseMove);
|
||||
this.container.addEventListener('mouseleave', this.mouseLeave);
|
||||
}
|
||||
|
||||
mouseLeave() {
|
||||
this.hoverSlice(this.curActiveSlice, this.curActiveSliceIndex, false);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
85
src/js/charts/FunnelChart.js
Normal file
85
src/js/charts/FunnelChart.js
Normal file
@ -0,0 +1,85 @@
|
||||
import AggregationChart from './AggregationChart';
|
||||
import { getOffset } from '../utils/dom';
|
||||
import { getComponent } from '../objects/ChartComponents';
|
||||
import { getEndpointsForTrapezoid } from '../utils/draw-utils';
|
||||
|
||||
export default class FunnelChart extends AggregationChart {
|
||||
constructor(parent, args) {
|
||||
super(parent, args);
|
||||
this.type = 'funnel';
|
||||
this.setup();
|
||||
}
|
||||
|
||||
calc() {
|
||||
super.calc();
|
||||
let s = this.state;
|
||||
|
||||
// calculate width and height options
|
||||
const totalheight = this.height * 0.9;
|
||||
const baseWidth = (2 * totalheight) / Math.sqrt(3);
|
||||
|
||||
|
||||
const reducer = (accumulator, currentValue) => accumulator + currentValue;
|
||||
const weightage = s.sliceTotals.reduce(reducer, 0.0);
|
||||
|
||||
const center_x_offset = this.center.x - baseWidth / 2;
|
||||
const center_y_offset = this.center.y - totalheight / 2;
|
||||
|
||||
let slicePoints = [];
|
||||
let startPoint = [[center_x_offset, center_y_offset], [center_x_offset + baseWidth, center_y_offset]];
|
||||
s.sliceTotals.forEach(d => {
|
||||
let height = totalheight * d / weightage;
|
||||
let endPoint = getEndpointsForTrapezoid(startPoint, height);
|
||||
slicePoints.push([startPoint, endPoint]);
|
||||
startPoint = endPoint;
|
||||
});
|
||||
s.slicePoints = slicePoints;
|
||||
}
|
||||
|
||||
setupComponents() {
|
||||
let s = this.state;
|
||||
|
||||
let componentConfigs = [
|
||||
[
|
||||
'funnelSlices',
|
||||
{ },
|
||||
function() {
|
||||
return {
|
||||
slicePoints: s.slicePoints,
|
||||
colors: this.colors
|
||||
};
|
||||
}.bind(this)
|
||||
]
|
||||
];
|
||||
|
||||
this.components = new Map(componentConfigs
|
||||
.map(args => {
|
||||
let component = getComponent(...args);
|
||||
return [args[0], component];
|
||||
}));
|
||||
}
|
||||
|
||||
bindTooltip() {
|
||||
function getPolygonWidth(slice) {
|
||||
const points = slice.points;
|
||||
return points[1].x - points[0].x;
|
||||
}
|
||||
|
||||
this.container.addEventListener('mousemove', (e) => {
|
||||
let slices = this.components.get('funnelSlices').store;
|
||||
let slice = e.target;
|
||||
if(slices.includes(slice)) {
|
||||
let i = slices.indexOf(slice);
|
||||
|
||||
let gOff = getOffset(this.container), pOff = getOffset(slice);
|
||||
let x = pOff.left - gOff.left + getPolygonWidth(slice)/2;
|
||||
let y = pOff.top - gOff.top;
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,337 +1,294 @@
|
||||
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";
|
||||
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';
|
||||
|
||||
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);
|
||||
}
|
||||
data.start = toMidnightUTC(data.start);
|
||||
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.end) {
|
||||
data.end = new Date();
|
||||
}
|
||||
data.end = toMidnightUTC(data.end);
|
||||
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;
|
||||
}
|
||||
|
||||
data.dataPoints = data.dataPoints || {};
|
||||
return data;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
calc() {
|
||||
let s = this.state;
|
||||
|
||||
return data;
|
||||
}
|
||||
s.start = clone(this.data.start);
|
||||
s.end = clone(this.data.end);
|
||||
|
||||
calc() {
|
||||
let s = this.state;
|
||||
s.firstWeekStart = clone(s.start);
|
||||
s.noOfWeeks = getWeeksBetween(s.start, s.end);
|
||||
s.distribution = calcDistribution(
|
||||
Object.values(this.data.dataPoints), HEATMAP_DISTRIBUTION_SIZE);
|
||||
|
||||
s.start = clone(this.data.start);
|
||||
s.end = clone(this.data.end);
|
||||
s.domainConfigs = this.getDomains();
|
||||
}
|
||||
|
||||
s.firstWeekStart = clone(s.start);
|
||||
s.noOfWeeks = getWeeksBetween(s.start, s.end);
|
||||
s.distribution = calcDistribution(
|
||||
Object.values(this.data.dataPoints),
|
||||
HEATMAP_DISTRIBUTION_SIZE
|
||||
);
|
||||
setupComponents() {
|
||||
let s = this.state;
|
||||
let lessCol = this.discreteDomains ? 0 : 1;
|
||||
|
||||
s.domainConfigs = this.getDomains();
|
||||
}
|
||||
let componentConfigs = s.domainConfigs.map((config, i) => [
|
||||
'heatDomain',
|
||||
{
|
||||
index: config.index,
|
||||
colWidth: COL_WIDTH,
|
||||
rowHeight: ROW_HEIGHT,
|
||||
squareSize: HEATMAP_SQUARE_SIZE,
|
||||
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)
|
||||
|
||||
setupComponents() {
|
||||
let s = this.state;
|
||||
let lessCol = this.discreteDomains ? 0 : 1;
|
||||
]);
|
||||
|
||||
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),
|
||||
]);
|
||||
this.components = new Map(componentConfigs
|
||||
.map((args, i) => {
|
||||
let component = getComponent(...args);
|
||||
return [args[0] + '-' + i, component];
|
||||
})
|
||||
);
|
||||
|
||||
this.components = new Map(
|
||||
componentConfigs.map((args, i) => {
|
||||
let component = getComponent(...args);
|
||||
return [args[0] + "-" + i, component];
|
||||
})
|
||||
);
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
update(data) {
|
||||
if(!data) {
|
||||
console.error('No data to update.');
|
||||
}
|
||||
|
||||
update(data) {
|
||||
if (!data) {
|
||||
console.error("No data to update.");
|
||||
}
|
||||
this.data = this.prepareData(data);
|
||||
this.draw();
|
||||
this.bindTooltip();
|
||||
}
|
||||
|
||||
this.data = this.prepareData(data);
|
||||
this.draw();
|
||||
this.bindTooltip();
|
||||
}
|
||||
bindTooltip() {
|
||||
this.container.addEventListener('mousemove', (e) => {
|
||||
this.components.forEach(comp => {
|
||||
let daySquares = comp.store;
|
||||
let daySquare = e.target;
|
||||
if(daySquares.includes(daySquare)) {
|
||||
|
||||
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 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 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, 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 = endDate
|
||||
? clone(endDate)
|
||||
: toMidnightUTC(getLastDateInMonth(month, year));
|
||||
getDomainConfig(startDate, endDate='') {
|
||||
let [month, year] = [startDate.getMonth(), startDate.getFullYear()];
|
||||
let startOfWeek = setDayToSunday(startDate); // TODO: Monday as well
|
||||
endDate = clone(endDate) || 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 = toMidnightUTC(
|
||||
new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd)
|
||||
);
|
||||
addDays(startOfWeek, 1);
|
||||
}
|
||||
startOfWeek = 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;
|
||||
}
|
||||
}
|
||||
|
||||
173
src/js/charts/MultiAxisChart.js
Normal file
173
src/js/charts/MultiAxisChart.js
Normal file
@ -0,0 +1,173 @@
|
||||
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
|
||||
));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,96 +1,92 @@
|
||||
import AggregationChart from "./AggregationChart";
|
||||
import { getOffset } from "../utils/dom";
|
||||
import { getComponent } from "../objects/ChartComponents";
|
||||
import { PERCENTAGE_BAR_DEFAULT_HEIGHT } from "../utils/constants";
|
||||
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';
|
||||
|
||||
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;
|
||||
let b = this.barOptions;
|
||||
b.height = b.height || PERCENTAGE_BAR_DEFAULT_HEIGHT;
|
||||
b.depth = b.depth || PERCENTAGE_BAR_DEFAULT_DEPTH;
|
||||
|
||||
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,
|
||||
},
|
||||
function () {
|
||||
return {
|
||||
xPositions: s.xPositions,
|
||||
widths: s.widths,
|
||||
colors: this.colors,
|
||||
};
|
||||
}.bind(this),
|
||||
],
|
||||
];
|
||||
let componentConfigs = [
|
||||
[
|
||||
'percentageBars',
|
||||
{
|
||||
barHeight: this.barOptions.height,
|
||||
barDepth: this.barOptions.depth,
|
||||
},
|
||||
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)) {
|
||||
let i = bars.indexOf(bar);
|
||||
let gOff = getOffset(this.container),
|
||||
pOff = getOffset(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 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 i = bars.indexOf(bar);
|
||||
let gOff = getOffset(this.container), pOff = getOffset(bar);
|
||||
|
||||
this.tip.setValues(x, y, {
|
||||
name: title,
|
||||
value: (fraction * 100).toFixed(1) + "%",
|
||||
});
|
||||
this.tip.showTip();
|
||||
}
|
||||
});
|
||||
}
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,207 +1,155 @@
|
||||
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";
|
||||
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';
|
||||
|
||||
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;
|
||||
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
|
||||
? makeCircleStr(curStart, curEnd, this.center, this.radius, clockWise, largeArc)
|
||||
: makeArcPathStr(curStart, curEnd, this.center, this.radius, 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
|
||||
? makeCircleStr(
|
||||
curStart,
|
||||
curEnd,
|
||||
this.center,
|
||||
this.radius,
|
||||
clockWise,
|
||||
largeArc
|
||||
)
|
||||
: makeArcPathStr(
|
||||
curStart,
|
||||
curEnd,
|
||||
this.center,
|
||||
this.radius,
|
||||
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 = [
|
||||
[
|
||||
"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);
|
||||
}
|
||||
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());
|
||||
}
|
||||
bindTooltip() {
|
||||
this.container.addEventListener('mousemove', this.mouseMove);
|
||||
this.container.addEventListener('mouseleave', 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();
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.6.2";
|
||||
frappe.NAME = 'Frappe Charts';
|
||||
frappe.VERSION = '1.3.0';
|
||||
|
||||
frappe = Object.assign({}, frappe, Charts);
|
||||
frappe = Object.assign({ }, frappe, Charts);
|
||||
|
||||
export default frappe;
|
||||
export default frappe;
|
||||
@ -1,39 +1,19 @@
|
||||
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";
|
||||
import { makeSVGGroup } from '../utils/draw';
|
||||
import { makeText, makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, percentageBar, getPaths, heatSquare, funnelSlice } 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;
|
||||
@ -47,10 +27,8 @@ 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();
|
||||
}
|
||||
@ -71,15 +49,11 @@ class ChartComponent {
|
||||
render(data) {
|
||||
this.store = this.makeElements(data);
|
||||
|
||||
this.layer.textContent = "";
|
||||
this.store.forEach((element) => {
|
||||
element.length
|
||||
? element.forEach((el) => {
|
||||
this.layer.appendChild(el);
|
||||
})
|
||||
: this.layer.appendChild(element);
|
||||
this.layer.textContent = '';
|
||||
this.store.forEach(element => {
|
||||
this.layer.appendChild(element);
|
||||
});
|
||||
this.labels.forEach((element) => {
|
||||
this.labels.forEach(element => {
|
||||
this.layer.appendChild(element);
|
||||
});
|
||||
}
|
||||
@ -87,7 +61,7 @@ class ChartComponent {
|
||||
update(animate = true) {
|
||||
this.refresh();
|
||||
let animateElements = [];
|
||||
if (animate) {
|
||||
if(animate) {
|
||||
animateElements = this.animateElements(this.data) || [];
|
||||
}
|
||||
return animateElements;
|
||||
@ -96,33 +70,25 @@ 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;";
|
||||
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])
|
||||
);
|
||||
return this.store.map((slice, i) => animatePathStr(slice, newData.sliceStrings[i]));
|
||||
},
|
||||
},
|
||||
pieSlices: {
|
||||
layerClass: "pie-slices",
|
||||
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 data.sliceStrings.map((s, i) =>{
|
||||
let slice = makePath(s, 'pie-path', 'none', data.colors[i]);
|
||||
slice.style.transition = 'transform .3s;';
|
||||
return slice;
|
||||
});
|
||||
},
|
||||
@ -131,129 +97,45 @@ let componentConfigs = {
|
||||
return this.store.map((slice, i) =>
|
||||
animatePathStr(slice, newData.sliceStrings[i])
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
percentageBars: {
|
||||
layerClass: "percentage-bars",
|
||||
layerClass: 'percentage-bars',
|
||||
makeElements(data) {
|
||||
const numberOfPoints = data.xPositions.length;
|
||||
return data.xPositions.map((x, i) => {
|
||||
return data.xPositions.map((x, i) =>{
|
||||
let y = 0;
|
||||
|
||||
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]
|
||||
);
|
||||
let bar = percentageBar(x, y, data.widths[i],
|
||||
this.constants.barHeight, this.constants.barDepth, data.colors[i]);
|
||||
return bar;
|
||||
});
|
||||
},
|
||||
|
||||
animateElements(newData) {
|
||||
if (newData) return [];
|
||||
},
|
||||
if(newData) return [];
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
layerClass: "y axis",
|
||||
funnelSlices: {
|
||||
layerClass: 'funnel-slices',
|
||||
makeElements(data) {
|
||||
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,
|
||||
})
|
||||
);
|
||||
return data.slicePoints.map((p, i) => {
|
||||
return funnelSlice('funnel-slice', p[0], p[1], data.colors[i]);
|
||||
});
|
||||
|
||||
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]);
|
||||
});
|
||||
}
|
||||
if(newData) return [];
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
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})
|
||||
);
|
||||
},
|
||||
|
||||
animateElements(newData) {
|
||||
let newPos = newData.positions;
|
||||
let newLabels = newData.labels;
|
||||
let oldPos = this.oldData.positions;
|
||||
@ -264,23 +146,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})
|
||||
);
|
||||
},
|
||||
|
||||
@ -295,144 +177,117 @@ 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,
|
||||
stroke: m.options.stroke,
|
||||
mode: "span",
|
||||
lineType: m.options.lineType,
|
||||
})
|
||||
return data.map(m =>
|
||||
yMarker(m.position, m.label, this.constants.width,
|
||||
{labelPos: m.options.labelPos, mode: 'span', lineType: 'dashed'})
|
||||
);
|
||||
},
|
||||
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, 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, day.fill, data);
|
||||
this.serializedSubDomains.push(square);
|
||||
}
|
||||
y += rowHeight;
|
||||
@ -445,17 +300,15 @@ 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],
|
||||
@ -468,7 +321,7 @@ let componentConfigs = {
|
||||
{
|
||||
zeroLine: data.zeroLine,
|
||||
barsWidth: data.barsWidth,
|
||||
minHeight: c.minHeight,
|
||||
minHeight: c.minHeight
|
||||
}
|
||||
);
|
||||
});
|
||||
@ -487,10 +340,7 @@ 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({
|
||||
@ -507,31 +357,23 @@ 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,
|
||||
@ -539,24 +381,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
|
||||
);
|
||||
});
|
||||
@ -588,37 +430,29 @@ 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);
|
||||
}
|
||||
|
||||
@ -1,132 +1,127 @@
|
||||
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.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>`;
|
||||
}
|
||||
this.title.innerHTML = title;
|
||||
this.dataPointList.innerHTML = '';
|
||||
|
||||
if (this.listValues.length > 4) {
|
||||
this.list.classList.add("tooltip-grid");
|
||||
} else {
|
||||
this.list.classList.remove("tooltip-grid");
|
||||
}
|
||||
this.listValues.map((set, i) => {
|
||||
const color = this.colors[i] || 'black';
|
||||
let value = set.formatted === 0 || set.formatted ? set.formatted : set.value;
|
||||
|
||||
this.title.innerHTML = title;
|
||||
this.dataPointList.innerHTML = "";
|
||||
let li = $.create('li', {
|
||||
styles: {
|
||||
'border-top': `3px solid ${color}`
|
||||
},
|
||||
innerHTML: `<strong style="display: block;">${ value === 0 || value ? value : '' }</strong>
|
||||
${set.title ? set.title : '' }`
|
||||
});
|
||||
|
||||
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>`,
|
||||
});
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,121 +1,105 @@
|
||||
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];
|
||||
}
|
||||
|
||||
@ -1,133 +1,116 @@
|
||||
// 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 = {}
|
||||
) {
|
||||
let animElement = element.cloneNode(true);
|
||||
let newElement = element.cloneNode(true);
|
||||
function animateSVGElement(element, props, dur, easingType="linear", type=undefined, oldValues={}) {
|
||||
|
||||
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 animElement = element.cloneNode(true);
|
||||
let newElement = element.cloneNode(true);
|
||||
|
||||
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",
|
||||
};
|
||||
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];
|
||||
|
||||
if (type) {
|
||||
animAttr["type"] = type;
|
||||
}
|
||||
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'
|
||||
};
|
||||
|
||||
for (var i in animAttr) {
|
||||
animateElement.setAttribute(i, animAttr[i]);
|
||||
}
|
||||
if(type) {
|
||||
animAttr["type"] = type;
|
||||
}
|
||||
|
||||
animElement.appendChild(animateElement);
|
||||
for (var i in animAttr) {
|
||||
animateElement.setAttribute(i, animAttr[i]);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
newElement.setAttribute(attributeName, `translate(${value})`);
|
||||
} else {
|
||||
newElement.setAttribute(attributeName, value);
|
||||
}
|
||||
}
|
||||
animElement.appendChild(animateElement);
|
||||
|
||||
return [animElement, newElement];
|
||||
if(type) {
|
||||
newElement.setAttribute(attributeName, `translate(${value})`);
|
||||
} else {
|
||||
newElement.setAttribute(attributeName, value);
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
if (parent) {
|
||||
parent.replaceChild(animElement, unit);
|
||||
}
|
||||
});
|
||||
parent.replaceChild(animElement, unit);
|
||||
});
|
||||
|
||||
let animSvg = svgContainer.cloneNode(true);
|
||||
let animSvg = svgContainer.cloneNode(true);
|
||||
|
||||
animElements.map((animElement, i) => {
|
||||
if (animElement[1]) {
|
||||
animElement[1].replaceChild(newElements[i], animElement[0]);
|
||||
elements[i][0] = newElements[i];
|
||||
}
|
||||
});
|
||||
animElements.map((animElement, i) => {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
import { fillArray } from "../utils/helpers";
|
||||
import {
|
||||
DEFAULT_AXIS_CHART_TYPE,
|
||||
AXIS_DATASET_CHART_TYPES,
|
||||
DEFAULT_CHAR_WIDTH,
|
||||
SERIES_LABEL_SPACE_RATIO,
|
||||
} from "../utils/constants";
|
||||
import { fillArray } from '../utils/helpers';
|
||||
import { DEFAULT_AXIS_CHART_TYPE, AXIS_DATASET_CHART_TYPES, DEFAULT_CHAR_WIDTH } from '../utils/constants';
|
||||
|
||||
export function dataPrep(data, type, config) {
|
||||
export function dataPrep(data, type) {
|
||||
data.labels = data.labels || [];
|
||||
|
||||
let datasetLength = data.labels.length;
|
||||
@ -14,51 +9,48 @@ export function dataPrep(data, type, config) {
|
||||
// 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];
|
||||
}
|
||||
});
|
||||
@ -73,62 +65,55 @@ export function zeroDataPrep(realData) {
|
||||
|
||||
let zeroData = {
|
||||
labels: realData.labels.slice(0, -1),
|
||||
datasets: realData.datasets.map((d) => {
|
||||
const { axisID } = d;
|
||||
datasets: realData.datasets.map(d => {
|
||||
return {
|
||||
axisID,
|
||||
name: "",
|
||||
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) * SERIES_LABEL_SPACE_RATIO;
|
||||
if (allowedSpace <= 0) allowedSpace = 1;
|
||||
export function getShortenedLabels(chartWidth, labels=[], isSeries=true) {
|
||||
let allowedSpace = chartWidth / labels.length;
|
||||
if(allowedSpace <= 0) allowedSpace = 1;
|
||||
let allowedLetters = allowedSpace / DEFAULT_CHAR_WIDTH;
|
||||
|
||||
let seriesMultiple;
|
||||
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 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 && i !== labels.length - 1) {
|
||||
let multiple = Math.ceil(label.length/allowedLetters);
|
||||
if(i % multiple !== 0) {
|
||||
label = "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,61 +1,45 @@
|
||||
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",
|
||||
'light-blue': '#7cd6fd',
|
||||
'blue': '#5e64ff',
|
||||
'violet': '#743ee2',
|
||||
'red': '#ff5858',
|
||||
'orange': '#ffa00a',
|
||||
'yellow': '#feef72',
|
||||
'green': '#28a745',
|
||||
'light-green': '#98d85b',
|
||||
'purple': '#b554ff',
|
||||
'magenta': '#ffa3ef',
|
||||
'black': '#36114C',
|
||||
'grey': '#bdd3e6',
|
||||
'light-grey': '#f0f4f7',
|
||||
'dark-grey': '#b8c2cc'
|
||||
};
|
||||
|
||||
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/8027444/6495043
|
||||
return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.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;
|
||||
return PRESET_COLOR_MAP[color] || color;
|
||||
};
|
||||
|
||||
@ -1,91 +1,80 @@
|
||||
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 LEGEND_ITEM_WIDTH = 150;
|
||||
export const SERIES_LABEL_SPACE_RATIO = 0.6;
|
||||
export const AXIS_LEGEND_BAR_SIZE = 100;
|
||||
|
||||
export const BAR_CHART_SPACE_RATIO = 0.5;
|
||||
export const MIN_BAR_PERCENT_HEIGHT = 0.0;
|
||||
export const MIN_BAR_PERCENT_HEIGHT = 0.00;
|
||||
|
||||
export const LINE_CHART_DOT_SIZE = 4;
|
||||
export const DOT_OVERLAY_SIZE_INCR = 4;
|
||||
|
||||
export const PERCENTAGE_BAR_DEFAULT_HEIGHT = 16;
|
||||
export const PERCENTAGE_BAR_DEFAULT_HEIGHT = 20;
|
||||
export const PERCENTAGE_BAR_DEFAULT_DEPTH = 2;
|
||||
|
||||
// Fixed 5-color theme,
|
||||
// More colors are difficult to parse visually
|
||||
@ -96,48 +85,22 @@ 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",
|
||||
];
|
||||
export const TOOLTIP_POINTER_TRIANGLE_HEIGHT = 5;
|
||||
|
||||
const DEFAULT_CHART_COLORS = ['light-blue', 'blue', 'violet', 'red', 'orange',
|
||||
'yellow', 'green', 'light-green', 'purple', 'magenta', 'light-grey', 'dark-grey'];
|
||||
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,
|
||||
funnel: DEFAULT_CHART_COLORS,
|
||||
};
|
||||
|
||||
// Universal constants
|
||||
|
||||
@ -6,132 +6,85 @@ 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;
|
||||
}
|
||||
|
||||
export function toMidnightUTC(date) {
|
||||
let result = new Date(date);
|
||||
result.setUTCHours(0, result.getTimezoneOffset(), 0, 0);
|
||||
return result;
|
||||
let result = new Date(date);
|
||||
result.setMinutes(result.getMinutes() - result.getTimezoneOffset());
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1,149 +1,130 @@
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
if (i === "inside") {
|
||||
$(val).appendChild(element);
|
||||
}
|
||||
else if (i === "around") {
|
||||
var ref = $(val);
|
||||
ref.parentNode.insertBefore(element, ref);
|
||||
element.appendChild(ref);
|
||||
|
||||
return element;
|
||||
} 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;
|
||||
};
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
// 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;
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1,103 +1,127 @@
|
||||
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 getEndpointsForTrapezoid(startPositions, height) {
|
||||
const endPosition = [];
|
||||
let [point_a, point_b] = startPositions;
|
||||
|
||||
// For an equilateral triangle, the angles are always 60 deg.
|
||||
// The end points on the polygons can be created using the following formula
|
||||
//
|
||||
// end_point_x = start_x +/- height * 1/√3
|
||||
// end_point_y = start_y + height
|
||||
//
|
||||
// b
|
||||
// _______________________________
|
||||
// \ |_| /
|
||||
// \ | /
|
||||
// \ | h /
|
||||
// \ | /
|
||||
// \|____________________/
|
||||
//
|
||||
// b = h * tan(30 deg)
|
||||
//
|
||||
|
||||
let multiplicationFactor = 1.0/Math.sqrt(3);
|
||||
endPosition[0] = [point_a[0] + height * multiplicationFactor, point_a[1] + height];
|
||||
endPosition[1] = [point_b[0] - height * multiplicationFactor, point_b[1] + height];
|
||||
|
||||
return endPosition;
|
||||
}
|
||||
|
||||
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 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 points=[];
|
||||
for(let i=0;i<xList.length;i++){
|
||||
points.push([xList[i], yList[i]]);
|
||||
}
|
||||
|
||||
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 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 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 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 pointStr = (points, command) => {
|
||||
return points.reduce(
|
||||
(acc, point, i, a) =>
|
||||
i === 0 ? `${point[0]},${point[1]}` : `${acc} ${command(point, i, a)}`,
|
||||
""
|
||||
);
|
||||
};
|
||||
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]}`;
|
||||
};
|
||||
|
||||
return pointStr(points, bezierCommand);
|
||||
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
@ -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;
|
||||
}
|
||||
|
||||
@ -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 == undefined) {
|
||||
export function fillArray(array, count, element, start=false) {
|
||||
if(!element) {
|
||||
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) {
|
||||
@ -92,52 +92,3 @@ export function getPositionByAngle(angle, radius) {
|
||||
y: Math.cos(angle * ANGLE_RATIO) * radius,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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;
|
||||
else if (nonNegative && candidate < 0) return false;
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Round a number to the closes precision, max max precision 4
|
||||
* @param {Number} d Any Number
|
||||
*/
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
@ -1,255 +1,239 @@
|
||||
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) => {
|
||||
// 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;
|
||||
let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue);
|
||||
intervals = intervals.map(value => value * Math.pow(10, exponent));
|
||||
return intervals;
|
||||
}
|
||||
|
||||
export function calcChartIntervals(values, withMinimum = true, overrideCeiling=false, overrideFloor=false) {
|
||||
//*** Where the magic happens ***
|
||||
export function calcChartIntervals(values, withMinimum=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);
|
||||
|
||||
if (overrideCeiling) {
|
||||
maxValue = overrideCeiling
|
||||
}
|
||||
// Exponent to be used for pretty print
|
||||
let exponent = 0, intervals = []; // eslint-disable-line no-unused-vars
|
||||
|
||||
if (overrideFloor) {
|
||||
minValue = overrideFloor
|
||||
}
|
||||
function getPositiveFirstIntervals(maxValue, absMinValue) {
|
||||
let intervals = getChartIntervals(maxValue);
|
||||
|
||||
// Exponent to be used for pretty print
|
||||
let exponent = 0,
|
||||
intervals = []; // eslint-disable-line no-unused-vars
|
||||
let intervalSize = intervals[1] - intervals[0];
|
||||
|
||||
function getPositiveFirstIntervals(maxValue, absMinValue) {
|
||||
let intervals = getChartIntervals(maxValue);
|
||||
// Then unshift the negative values
|
||||
let value = 0;
|
||||
for(var i = 1; value < absMinValue; i++) {
|
||||
value += intervalSize;
|
||||
intervals.unshift((-1) * value);
|
||||
}
|
||||
return intervals;
|
||||
}
|
||||
|
||||
let intervalSize = intervals[1] - intervals[0];
|
||||
// CASE I: Both non-negative
|
||||
|
||||
// Then unshift the negative values
|
||||
let value = 0;
|
||||
for (var i = 1; value < absMinValue; i++) {
|
||||
value += intervalSize;
|
||||
intervals.unshift(-1 * value);
|
||||
}
|
||||
return intervals;
|
||||
}
|
||||
if(maxValue >= 0 && minValue >= 0) {
|
||||
exponent = normalize(maxValue)[1];
|
||||
if(!withMinimum) {
|
||||
intervals = getChartIntervals(maxValue);
|
||||
} else {
|
||||
intervals = getChartIntervals(maxValue, minValue);
|
||||
}
|
||||
}
|
||||
|
||||
// CASE I: Both non-negative
|
||||
// CASE II: Only minValue negative
|
||||
|
||||
if (maxValue >= 0 && minValue >= 0) {
|
||||
exponent = normalize(maxValue)[1];
|
||||
if (!withMinimum) {
|
||||
intervals = getChartIntervals(maxValue);
|
||||
} else {
|
||||
intervals = getChartIntervals(maxValue, minValue);
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
// 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
|
||||
let absMinValue = Math.abs(minValue);
|
||||
|
||||
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.map(d => d * (-1));
|
||||
}
|
||||
|
||||
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
|
||||
else if (maxValue <= 0 && minValue <= 0) {
|
||||
// Mirrored Case I:
|
||||
// Work with positives, then reverse the sign and array
|
||||
// CASE III: Both non-positive
|
||||
|
||||
let pseudoMaxValue = Math.abs(minValue);
|
||||
let pseudoMinValue = Math.abs(maxValue);
|
||||
else if(maxValue <= 0 && minValue <= 0) {
|
||||
// Mirrored Case I:
|
||||
// Work with positives, then reverse the sign and array
|
||||
|
||||
exponent = normalize(pseudoMaxValue)[1];
|
||||
if (!withMinimum) {
|
||||
intervals = getChartIntervals(pseudoMaxValue);
|
||||
} else {
|
||||
intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue);
|
||||
}
|
||||
let pseudoMaxValue = Math.abs(minValue);
|
||||
let pseudoMinValue = Math.abs(maxValue);
|
||||
|
||||
intervals = intervals.reverse().map((d) => d * -1);
|
||||
}
|
||||
exponent = normalize(pseudoMaxValue)[1];
|
||||
if(!withMinimum) {
|
||||
intervals = getChartIntervals(pseudoMaxValue);
|
||||
} else {
|
||||
intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue);
|
||||
}
|
||||
|
||||
return intervals.sort((a, b) => a - b);
|
||||
intervals = intervals.reverse().map(d => d * (-1));
|
||||
}
|
||||
|
||||
return intervals;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
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");
|
||||
});
|
||||
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");
|
||||
});
|
||||
});
|
||||
@ -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", () => {
|
||||
assert.equal(helpers.floatTwo(1.234), 1.23);
|
||||
assert.equal(helpers.floatTwo(1.456), 1.46);
|
||||
assert.equal(helpers.floatTwo(1), 1.0);
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user