@@ -0,0 +1,14 @@ | |||||
{ | |||||
"presets": [ | |||||
["latest", { | |||||
"es2015": { | |||||
"modules": false | |||||
} | |||||
}] | |||||
], | |||||
"env": { | |||||
"test": { | |||||
"presets": ["env"] | |||||
} | |||||
} | |||||
} |
@@ -0,0 +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 | |||||
} | |||||
} |
@@ -0,0 +1,67 @@ | |||||
# Logs | |||||
logs | |||||
*.log | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
# Runtime data | |||||
pids | |||||
*.pid | |||||
*.seed | |||||
*.pid.lock | |||||
# Directory for instrumented libs generated by jscoverage/JSCover | |||||
lib-cov | |||||
# Coverage directory used by tools like istanbul | |||||
coverage | |||||
# nyc test coverage | |||||
.nyc_output | |||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | |||||
.grunt | |||||
# Bower dependency directory (https://bower.io/) | |||||
bower_components | |||||
# node-waf configuration | |||||
.lock-wscript | |||||
# Compiled binary addons (https://nodejs.org/api/addons.html) | |||||
build/Release | |||||
# Dependency directories | |||||
node_modules/ | |||||
jspm_packages/ | |||||
# Typescript v1 declaration files | |||||
typings/ | |||||
# Optional npm cache directory | |||||
.npm | |||||
# Optional eslint cache | |||||
.eslintcache | |||||
# Optional REPL history | |||||
.node_repl_history | |||||
# Output of 'npm pack' | |||||
*.tgz | |||||
# Yarn Integrity file | |||||
.yarn-integrity | |||||
# dotenv environment variables file | |||||
.env | |||||
# next.js build output | |||||
.next | |||||
# npm build output | |||||
dist | |||||
docs/assets/ | |||||
.DS_Store |
@@ -0,0 +1,8 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="WEB_MODULE" version="4"> | |||||
<component name="NewModuleRootManager"> | |||||
<content url="file://$MODULE_DIR$" /> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
</component> | |||||
</module> |
@@ -0,0 +1,49 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="PublishConfigData"> | |||||
<serverData> | |||||
<paths name="MAGDY SERVER 6"> | |||||
<serverdata> | |||||
<mappings> | |||||
<mapping local="$PROJECT_DIR$" web="/" /> | |||||
</mappings> | |||||
</serverdata> | |||||
</paths> | |||||
<paths name="MAGDY_SAPOS"> | |||||
<serverdata> | |||||
<mappings> | |||||
<mapping local="$PROJECT_DIR$" web="/" /> | |||||
</mappings> | |||||
</serverdata> | |||||
</paths> | |||||
<paths name="MEMBTECH-OFFLINE-POS"> | |||||
<serverdata> | |||||
<mappings> | |||||
<mapping local="$PROJECT_DIR$" web="/" /> | |||||
</mappings> | |||||
</serverdata> | |||||
</paths> | |||||
<paths name="MEMBTECH-STORE"> | |||||
<serverdata> | |||||
<mappings> | |||||
<mapping local="$PROJECT_DIR$" web="/" /> | |||||
</mappings> | |||||
</serverdata> | |||||
</paths> | |||||
<paths name="MEMBTECH_COM"> | |||||
<serverdata> | |||||
<mappings> | |||||
<mapping local="$PROJECT_DIR$" web="/" /> | |||||
</mappings> | |||||
</serverdata> | |||||
</paths> | |||||
<paths name="WOLF"> | |||||
<serverdata> | |||||
<mappings> | |||||
<mapping local="$PROJECT_DIR$" web="/" /> | |||||
</mappings> | |||||
</serverdata> | |||||
</paths> | |||||
</serverData> | |||||
</component> | |||||
</project> |
@@ -0,0 +1,6 @@ | |||||
<component name="InspectionProjectProfileManager"> | |||||
<profile version="1.0"> | |||||
<option name="myName" value="Project Default" /> | |||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> | |||||
</profile> | |||||
</component> |
@@ -0,0 +1,6 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="JavaScriptSettings"> | |||||
<option name="languageLevel" value="ES6" /> | |||||
</component> | |||||
</project> |
@@ -0,0 +1,8 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectModuleManager"> | |||||
<modules> | |||||
<module fileurl="file://$PROJECT_DIR$/.idea/charts.iml" filepath="$PROJECT_DIR$/.idea/charts.iml" /> | |||||
</modules> | |||||
</component> | |||||
</project> |
@@ -0,0 +1,70 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ChangeListManager"> | |||||
<list default="true" id="5e3d8f02-f7dd-4684-9ff7-f2b0b9acabbe" name="Default Changelist" comment="" /> | |||||
<option name="SHOW_DIALOG" value="false" /> | |||||
<option name="HIGHLIGHT_CONFLICTS" value="true" /> | |||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> | |||||
<option name="LAST_RESOLUTION" value="IGNORE" /> | |||||
</component> | |||||
<component name="ComposerSettings"> | |||||
<execution /> | |||||
</component> | |||||
<component name="Git.Settings"> | |||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> | |||||
</component> | |||||
<component name="ProjectId" id="2GhgTzrwO5Bp8mwG1eLiOKYJbuL" /> | |||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true" /> | |||||
<component name="ProjectViewState"> | |||||
<option name="hideEmptyMiddlePackages" value="true" /> | |||||
<option name="showLibraryContents" value="true" /> | |||||
</component> | |||||
<component name="PropertiesComponent"> | |||||
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" /> | |||||
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" /> | |||||
<property name="WebServerToolWindowFactoryState" value="true" /> | |||||
<property name="last_opened_file_path" value="$PROJECT_DIR$" /> | |||||
<property name="node.js.detected.package.eslint" value="true" /> | |||||
<property name="node.js.path.for.package.eslint" value="project" /> | |||||
<property name="node.js.selected.package.eslint" value="(autodetect)" /> | |||||
<property name="nodejs_package_manager_path" value="npm" /> | |||||
<property name="settings.editor.selected.configurable" value="project.propVCSSupport.Mappings" /> | |||||
</component> | |||||
<component name="SvnConfiguration"> | |||||
<configuration /> | |||||
</component> | |||||
<component name="TaskManager"> | |||||
<task active="true" id="Default" summary="Default task"> | |||||
<changelist id="5e3d8f02-f7dd-4684-9ff7-f2b0b9acabbe" name="Default Changelist" comment="" /> | |||||
<created>1666846241879</created> | |||||
<option name="number" value="Default" /> | |||||
<option name="presentableId" value="Default" /> | |||||
<updated>1666846241879</updated> | |||||
<workItem from="1666846242985" duration="331000" /> | |||||
<workItem from="1666846601117" duration="7000" /> | |||||
</task> | |||||
<servers /> | |||||
</component> | |||||
<component name="TypeScriptGeneratedFilesManager"> | |||||
<option name="version" value="2" /> | |||||
</component> | |||||
<component name="VcsManagerConfiguration"> | |||||
<ignored-roots> | |||||
<path value="$PROJECT_DIR$" /> | |||||
</ignored-roots> | |||||
</component> | |||||
<component name="WindowStateProjectService"> | |||||
<state x="740" y="265" key="FileChooserDialogImpl" timestamp="1666846535459"> | |||||
<screen x="0" y="0" width="1920" height="1048" /> | |||||
</state> | |||||
<state x="740" y="265" key="FileChooserDialogImpl/0.0.1920.1048@0.0.1920.1048" timestamp="1666846535459" /> | |||||
<state x="461" y="169" key="SettingsEditor" timestamp="1666846606724"> | |||||
<screen x="0" y="0" width="1920" height="1048" /> | |||||
</state> | |||||
<state x="461" y="169" key="SettingsEditor/0.0.1920.1048@0.0.1920.1048" timestamp="1666846606724" /> | |||||
<state x="667" y="257" width="1115" height="656" key="find.popup" timestamp="1666846525442"> | |||||
<screen x="0" y="0" width="1920" height="1048" /> | |||||
</state> | |||||
<state x="667" y="257" width="1115" height="656" key="find.popup/0.0.1920.1048@0.0.1920.1048" timestamp="1666846525442" /> | |||||
</component> | |||||
</project> |
@@ -0,0 +1,14 @@ | |||||
language: node_js | |||||
node_js: | |||||
- "6" | |||||
- "8" | |||||
before_install: | |||||
- make install | |||||
script: | |||||
- make test | |||||
after_success: | |||||
- make coveralls |
@@ -0,0 +1,21 @@ | |||||
MIT License | |||||
Copyright (c) 2017 Prateeksha Singh | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. |
@@ -0,0 +1,45 @@ | |||||
-include .env | |||||
BASEDIR = $(realpath .) | |||||
SRCDIR = $(BASEDIR)/src | |||||
DISTDIR = $(BASEDIR)/dist | |||||
DOCSDIR = $(BASEDIR)/docs | |||||
PROJECT = influxframework-charts | |||||
NODEMOD = $(BASEDIR)/node_modules | |||||
NODEBIN = $(NODEMOD)/.bin | |||||
build: clean install | |||||
$(NODEBIN)/rollup \ | |||||
--config $(BASEDIR)/rollup.config.js \ | |||||
--watch=$(watch) | |||||
clean: | |||||
rm -rf \ | |||||
$(BASEDIR)/.nyc_output \ | |||||
$(BASEDIR)/.yarn-error.log | |||||
clear | |||||
install.dep: | |||||
ifeq ($(shell command -v yarn),) | |||||
@echo "Installing yarn..." | |||||
npm install -g yarn | |||||
endif | |||||
install: install.dep | |||||
yarn --cwd $(BASEDIR) | |||||
test: clean | |||||
$(NODEBIN)/cross-env \ | |||||
NODE_ENV=test \ | |||||
$(NODEBIN)/nyc \ | |||||
$(NODEBIN)/mocha \ | |||||
--require $(NODEMOD)/babel-register \ | |||||
--recursive \ | |||||
$(SRCDIR)/js/**/test/*.test.js | |||||
coveralls: | |||||
$(NODEBIN)/nyc report --reporter text-lcov | $(NODEBIN)/coveralls |
@@ -0,0 +1,119 @@ | |||||
<div align="center"> | |||||
<img src="https://github.com/influxframework/design/blob/master/logos/logo-2019/influxframework-charts-logo.png" height="128"> | |||||
<a href="https://influxframework.github.io/charts"> | |||||
<h2>InfluxFramework Charts</h2> | |||||
</a> | |||||
<p align="center"> | |||||
<p>GitHub-inspired modern, intuitive and responsive charts with zero dependencies</p> | |||||
<a href="https://influxframework.com/charts"> | |||||
<b>Explore Demos » </b> | |||||
</a> | |||||
<a href="https://codesandbox.io/s/influxframework-charts-demo-viqud"> | |||||
<b> Edit at CodeSandbox »</b> | |||||
</a> | |||||
<a href="https://influxframework.com/charts/docs"> | |||||
<b>Documentation » </b> | |||||
</a> | |||||
</p> | |||||
</div> | |||||
<p align="center"> | |||||
<a href="https://bundlephobia.com/result?p=influxframework-charts"> | |||||
<img src="https://img.shields.io/bundlephobia/minzip/influxframework-charts"> | |||||
</a> | |||||
</p> | |||||
<p align="center"> | |||||
<a href="https://influxframework.github.io/charts"> | |||||
<img src=".github/example.gif"> | |||||
</a> | |||||
</p> | |||||
### Contents | |||||
* [Installation](#installation) | |||||
* [Usage](#usage) | |||||
* [Contribute](https://influxframework.com/charts/docs/contributing) | |||||
* [License](#license) | |||||
#### Installation | |||||
##### Via NPM | |||||
Install via [`npm`](https://www.npmjs.com/get-npm): | |||||
```sh | |||||
$ npm install influxframework-charts | |||||
``` | |||||
and include in your project: | |||||
```js | |||||
import { Chart } from "influxframework-charts" | |||||
``` | |||||
Or include following for es-modules(eg:vuejs): | |||||
```js | |||||
import { Chart } from 'influxframework-charts/dist/influxframework-charts.esm.js' | |||||
// import css | |||||
import 'influxframework-charts/dist/influxframework-charts.min.css' | |||||
``` | |||||
##### or include within your HTML | |||||
```html | |||||
<script src="https://cdn.jsdelivr.net/npm/influxframework-charts@1.6.1/dist/influxframework-charts.min.umd.js"></script> | |||||
<!-- or --> | |||||
<script src="https://unpkg.com/influxframework-charts@1.6.1/dist/influxframework-charts.min.umd.js"></script> | |||||
``` | |||||
#### Usage | |||||
```js | |||||
const data = { | |||||
labels: ["12am-3am", "3am-6pm", "6am-9am", "9am-12am", | |||||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9am-12am" | |||||
], | |||||
datasets: [ | |||||
{ | |||||
name: "Some Data", chartType: "bar", | |||||
values: [25, 40, 30, 35, 8, 52, 17, -4] | |||||
}, | |||||
{ | |||||
name: "Another Set", chartType: "line", | |||||
values: [25, 50, -10, 15, 18, 32, 27, 14] | |||||
} | |||||
] | |||||
} | |||||
const chart = new influxframework.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'] | |||||
}) | |||||
``` | |||||
Or for es-modules (replace `new influxframework.Chart()` with `new Chart()`): | |||||
```diff | |||||
- const chart = new influxframework.Chart("#chart", { | |||||
+ const chart = new Chart("#chart", { // or a DOM element, | |||||
// new Chart() in case of ES6 module with above usage | |||||
title: "My Awesome Chart", | |||||
data: data, | |||||
type: 'axis-mixed', // or 'bar', 'line', 'scatter', 'pie', 'percentage' | |||||
height: 250, | |||||
colors: ['#7cd6fd', '#743ee2'] | |||||
}) | |||||
``` | |||||
If you want to contribute: | |||||
1. Clone this repo. | |||||
2. `cd` into project directory | |||||
3. `npm install` | |||||
4. `npm run dev` | |||||
#### License | |||||
This repository has been released under the [MIT License](LICENSE) | |||||
------------------ |
@@ -0,0 +1,2 @@ | |||||
plugins: | |||||
- jekyll-redirect-from |
@@ -0,0 +1,321 @@ | |||||
--- | |||||
redirect_to: "https://influxframework.com/charts" | |||||
--- | |||||
<!DOCTYPE html> | |||||
<html> | |||||
<head> | |||||
<meta charset="UTF-8"> | |||||
<title>InfluxFramework Charts</title> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
<meta name="keywords" content="open source javascript js charts library svg zero-dependency interactive data visualization beautiful drag resize"> | |||||
<meta name="description" content="A simple, responsive, modern charts library for the web."> | |||||
<link rel="stylesheet" type="text/css" href="assets/css/reset.css" media="screen"> | |||||
<link rel="stylesheet" type="text/css" href="assets/css/bootstrap.min.css" media="screen"> | |||||
<link rel="stylesheet" type="text/css" href="assets/css/index.css" media="screen"> | |||||
<link rel="stylesheet" type="text/css" href="assets/css/hljs.css" media="screen"> | |||||
<script src="assets/js/highlight.pack.js"></script> | |||||
<script>hljs.initHighlightingOnLoad();</script> | |||||
<link rel="shortcut icon" href="https://influxframework.github.io/influxframework/assets/img/favicon.png" type="image/x-icon"> | |||||
<link rel="icon" href="https://influxframework.github.io/influxframework/assets/img/favicon.png" type="image/x-icon"> | |||||
<script async defer src="https://buttons.github.io/buttons.js"></script> | |||||
</head> | |||||
<body> | |||||
<header> | |||||
<h1>InfluxFramework Charts</h1> | |||||
<p class="lead-text">GitHub-inspired simple and modern SVG charts for the web<br>with zero dependencies.</p> | |||||
<div id="chart-composite-1" class="border"></div> | |||||
<p class="demo-tip">Click or use arrow keys to navigate data points</p> | |||||
<div id="chart-composite-2" class="border"></div> | |||||
</header> | |||||
<section> | |||||
<h6>Create a chart</h6> | |||||
<pre><code class="hljs html"> <!--HTML--> | |||||
<div id="chart"></div></code></pre> | |||||
<pre><code class="hljs javascript"> // Javascript | |||||
let chart = new influxframework.Chart( "#chart", { // or DOM element | |||||
data: { | |||||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | |||||
datasets: [ | |||||
{ | |||||
name: "Some Data", chartType: 'bar', | |||||
values: [25, 40, 30, 35, 8, 52, 17, -4] | |||||
}, | |||||
{ | |||||
name: "Another Set", chartType: 'bar', | |||||
values: [25, 50, -10, 15, 18, 32, 27, 14] | |||||
}, | |||||
{ | |||||
name: "Yet Another", chartType: 'line', | |||||
values: [15, 20, -3, -15, 58, 12, -17, 37] | |||||
} | |||||
], | |||||
yMarkers: [{ label: "Marker", value: 70, | |||||
options: { labelPos: 'left' }}], | |||||
yRegions: [{ label: "Region", start: -10, end: 50, | |||||
options: { labelPos: 'right' }}] | |||||
}, | |||||
title: "My Awesome Chart", | |||||
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage', 'donut' | |||||
height: 300, | |||||
colors: ['purple', '#ffa3ef', 'light-blue'], | |||||
tooltipOptions: { | |||||
formatTooltipX: d => (d + '').toUpperCase(), | |||||
formatTooltipY: d => d + ' pts', | |||||
} | |||||
}); | |||||
chart.export(); | |||||
</code></pre> | |||||
<div id="chart-aggr" class="border"></div> | |||||
<div class="btn-group aggr-type-buttons margin-top mx-auto" role="group"> | |||||
<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='percentage'>Percentage Chart</button> | |||||
</div> | |||||
<div class="btn-group export-buttons margin-top mx-auto" role="group"> | |||||
<button type="button" class="btn btn-sm btn-secondary export-aggr">Export ...</button> | |||||
</div> | |||||
</section> | |||||
<section> | |||||
<h6>Update Values</h6> | |||||
<div id="chart-update" class="border"></div> | |||||
<div class="chart-update-buttons mt-1 mx-auto" role="group"> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-update="random">Random Data</button> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-update="add">Add Value</button> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-update="remove">Remove Value</button> | |||||
<button type="button" class="btn btn-sm btn-secondary export-update">Export ...</button> | |||||
</div> | |||||
</section> | |||||
<section> | |||||
<h6>Plot Trends</h6> | |||||
<div id="chart-trends" class="border"></div> | |||||
<div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group"> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-type="hideDots">Line</button> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-type="hideLine">Dots</button> | |||||
<button type="button" class="btn btn-sm btn-secondary active" data-type="heatline">HeatLine</button> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-type="regionFill">Region</button> | |||||
</div> | |||||
<div class="btn-group export-buttons mt-1 mx-auto" role="group"> | |||||
<button type="button" class="btn btn-sm btn-secondary export-trends">Export ...</button> | |||||
</div> | |||||
</section> | |||||
<section> | |||||
<h6>Listen to state change</h6> | |||||
<div class="row"> | |||||
<div class="col-sm-8"> | |||||
<div id="chart-events" class="border"></div> | |||||
</div> | |||||
<div class="col-sm-4 chart-events-data"> | |||||
<div class="image-container border"> | |||||
<img class="moon-image" src="./assets/img/europa.jpg"> | |||||
</div> | |||||
<div class="content-data margin-top"> | |||||
<h6 class="moon-name">Europa</h6> | |||||
<p>Semi-major-axis: <span class="semi-major-axis">671034</span> km</p> | |||||
<p>Mass: <span class="mass">4800000</span> x 10^16 kg</p> | |||||
<p>Diameter: <span class="diameter">3121.6</span> km</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<pre><code class="hljs javascript margin-top"> ... | |||||
isNavigable: 1, // Navigate across data points; default 0 | |||||
... | |||||
chart.parent.addEventListener('data-select', (e) => { | |||||
update_moon_data(e.index); // e contains index and value of current datapoint | |||||
});</code></pre> | |||||
</section> | |||||
<section> | |||||
<h6> | |||||
And a Month-wise Heatmap | |||||
</h6> | |||||
<div id="chart-heatmap" class="border" | |||||
style="overflow: scroll;"></div> | |||||
<div class="heatmap-mode-buttons btn-group mt-1 mx-auto" role="group"> | |||||
<button type="button" class="btn btn-sm btn-secondary active" data-mode="discrete">Discrete</button> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-mode="continuous">Continuous</button> | |||||
</div> | |||||
<div class="heatmap-color-buttons btn-group mt-1 mx-auto" role="group"> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-color="default">Green (Default)</button> | |||||
<button type="button" class="btn btn-sm btn-secondary active" data-color="blue">Blue</button> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-color="halloween">GitHub's Halloween</button> | |||||
</div> | |||||
<div class="btn-group export-buttons mt-1 mx-auto" role="group"> | |||||
<button type="button" class="btn btn-sm btn-secondary export-heatmap">Export ...</button> | |||||
</div> | |||||
<pre><code class="hljs javascript margin-top"> let heatmap = new influxframework.Chart("#heatmap", { | |||||
type: 'heatmap', | |||||
title: "Monthly Distribution", | |||||
data: { | |||||
dataPoints: {'1524064033': 8, /* ... */}, | |||||
// object with timestamp-value pairs | |||||
start: startDate | |||||
end: endDate // Date objects | |||||
}, | |||||
countLabel: 'Level', | |||||
discreteDomains: 0 // default: 1 | |||||
colors: ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'], | |||||
// Set of five incremental colors, | |||||
// preferably with a low-saturation color for zero data; | |||||
// def: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'] | |||||
});</code></pre> | |||||
</section> | |||||
<section> | |||||
<h6>Demo</h6> | |||||
<p data-height="299" data-theme-id="light" data-slug-hash="wjKBoq" data-default-tab="js,result" | |||||
data-user="pratu16x7" data-embed-version="2" data-pen-title="InfluxFramework Charts Demo" class="codepen"> | |||||
See the Pen <a href="https://codepen.io/pratu16x7/pen/wjKBoq/">InfluxFramework Charts Demo</a> | |||||
by Prateeksha Singh (<a href="https://codepen.io/pratu16x7">@pratu16x7</a>) on | |||||
<a href="https://codepen.io">CodePen</a>. | |||||
</p> | |||||
<script async src="https://static.codepen.io/assets/embed/ei.js"></script> | |||||
</section> | |||||
<section> | |||||
<h6>Available options</h6> | |||||
<pre><code class="hljs javascript"> | |||||
... | |||||
{ | |||||
data: { | |||||
labels: [], | |||||
datasets: [], | |||||
yRegions: [], | |||||
yMarkers: [] | |||||
} | |||||
title: '', | |||||
colors: [], | |||||
height: 200, | |||||
tooltipOptions: { | |||||
formatTooltipX: d => (d + '').toUpperCase(), | |||||
formatTooltipY: d => d + ' pts', | |||||
} | |||||
// Axis charts | |||||
isNavigable: 1, // default: 0 | |||||
valuesOverPoints: 1, // default: 0 | |||||
barOptions: { | |||||
spaceRatio: 1 // default: 0.5 | |||||
stacked: 1 // default: 0 | |||||
} | |||||
lineOptions: { | |||||
dotSize: 6, // default: 4 | |||||
hideLine: 0, // default: 0 | |||||
hideDots: 1, // default: 0 | |||||
heatline: 1, // default: 0 | |||||
regionFill: 1 // default: 0 | |||||
} | |||||
axisOptions: { | |||||
yAxisMode: 'span', // Axis lines, default | |||||
xAxisMode: 'tick', // No axis lines, only short ticks | |||||
xIsSeries: 1 // Allow skipping x values for space | |||||
// default: 0 | |||||
}, | |||||
// Pie/Percentage/Donut charts | |||||
maxLegendPoints: 6, // default: 20 | |||||
maxSlices: 10, // default: 20 | |||||
// Percentage chart | |||||
barOptions: { | |||||
height: 15 // default: 20 | |||||
depth: 5 // default: 2 | |||||
} | |||||
// Heatmap | |||||
discreteDomains: 1, // default: 1 | |||||
} | |||||
... | |||||
// Updating values | |||||
chart.update(data); | |||||
// Axis charts: | |||||
chart.addDataPoint(label, valueFromEachDataset, index) | |||||
chart.removeDataPoint(index) | |||||
chart.updateDataset(datasetValues, index) | |||||
// Exporting | |||||
chart.export(); | |||||
// Unbind window-resize events | |||||
chart.destroy(); | |||||
</code></pre> | |||||
</section> | |||||
<section> | |||||
<h6>Install</h6> | |||||
<p>Install via npm</p> | |||||
<pre><code class="hljs console"> npm install influxframework-charts</code></pre> | |||||
<p>And include it in your project</p> | |||||
<pre><code class="hljs javascript"> import { Chart } from "influxframework-charts"</code></pre> | |||||
<p>(for ES6+ import the ESModule from the dist folder)</p> | |||||
<pre><code class="hljs javascript"> import { Chart } from "influxframework-charts/dist/influxframework-charts.esm.js"</code></pre> | |||||
<p>... or include it directly in your HTML</p> | |||||
<pre><code class="hljs html"> <script src="https://unpkg.com/influxframework-charts@1.1.0"></script></code></pre> | |||||
<p>Use as:</p> | |||||
<pre><code class="hljs javascript"> new Chart(); // ES6 module | |||||
// or | |||||
new influxframework.Chart(); // Browser</code></pre> | |||||
</section> | |||||
<section class="text-center"> | |||||
<!-- Closing --> | |||||
<a href="https://github.com/influxframework/charts/archive/master.zip"><button class="large blue button btn">Download</button></a> | |||||
<p style="margin-top: 3rem;margin-bottom: 1.5rem;"> | |||||
<!-- <a href="docs.html" style="margin-right: 1rem;" target="_blank">Documentation</a> --> | |||||
<a href="https://github.com/influxframework/charts" target="_blank">View on GitHub</a> | |||||
</p> | |||||
<p style="margin-top: 1rem;"> | |||||
<a class="github-button" href="https://github.com/influxframework/charts" data-icon="octicon-star" data-show-count="true" aria-label="Star influxframework/charts on GitHub">Star</a> | |||||
</p> | |||||
<p>License: MIT</p> | |||||
</section> | |||||
<footer class="built-with-influxframework text-center"> | |||||
<img style="padding: 5px; width: 40px; background: #fff" class="influxframework-bird" src="./assets/img/influxframework-bird.png"> | |||||
<p style="margin: 24px 0 0px 0; font-size: 15px"> | |||||
Project maintained by <a href="https://influxframework.com" target="_blank">InfluxFramework</a>. | |||||
</p> | |||||
<p style="margin: 24px 0 80px 0; font-size: 12px"> | |||||
Data from the <a href="https://www.amsmeteors.org" target="_blank">American Meteor Society</a>, | |||||
<a href="http://www.sidc.be/silso" target="_blank">SILSO</a> and | |||||
<a href="https://api.nasa.gov/index.html" target="_blank">NASA Open APIs</a> | |||||
</p> | |||||
</footer> | |||||
<a href="https://github.com/influxframework/charts" target="_blank" class="github-corner" aria-label="View source on Github"> | |||||
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#9a9a9a; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"> | |||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path> | |||||
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path> | |||||
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path> | |||||
</svg> | |||||
</a> | |||||
<script src="assets/js/influxframework-charts.min.js"></script> | |||||
<script src="assets/js/index.min.js"></script> | |||||
</body> | |||||
</html> |
@@ -0,0 +1,67 @@ | |||||
{ | |||||
"name": "influxframework-charts", | |||||
"version": "1.6.3", | |||||
"description": "https://influxframework.github.io/charts", | |||||
"main": "dist/influxframework-charts.min.cjs.js", | |||||
"module": "dist/influxframework-charts.min.esm.js", | |||||
"src": "dist/influxframework-charts.esm.js", | |||||
"browser": "dist/influxframework-charts.min.umd.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/influxframework/charts.git" | |||||
}, | |||||
"keywords": [ | |||||
"js", | |||||
"charts" | |||||
], | |||||
"author": "Prateeksha Singh", | |||||
"license": "MIT", | |||||
"bugs": { | |||||
"url": "https://github.com/influxframework/charts/issues" | |||||
}, | |||||
"homepage": "https://github.com/influxframework/charts#readme", | |||||
"devDependencies": { | |||||
"autoprefixer": "^8.1.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": "^8.9.0", | |||||
"mocha": "^9.2.0", | |||||
"node-sass": "^7.0.1", | |||||
"npm-run-all": "^4.1.2", | |||||
"nyc": "^15.1.0", | |||||
"postcss": "^6.0.21", | |||||
"postcss-cssnext": "^3.1.0", | |||||
"postcss-nested": "^3.0.0", | |||||
"precss": "^3.1.2", | |||||
"rollup": "^0.56.5", | |||||
"rollup-plugin-babel": "^3.0.3", | |||||
"rollup-plugin-eslint": "^6.0.0", | |||||
"rollup-plugin-node-resolve": "^3.3.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" | |||||
} | |||||
} |
@@ -0,0 +1,195 @@ | |||||
import pkg from './package.json'; | |||||
// 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 [ | |||||
{ | |||||
input: 'src/js/index.js', | |||||
sourcemap: true, | |||||
output: [ | |||||
{ | |||||
file: 'docs/assets/js/influxframework-charts.min.js', | |||||
format: 'iife', | |||||
}, | |||||
{ | |||||
file: pkg.browser, | |||||
format: 'umd', | |||||
} | |||||
], | |||||
name: 'influxframework', | |||||
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: 'influxframework', | |||||
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/influxframework-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'), | |||||
}) | |||||
], | |||||
} | |||||
]; |
@@ -0,0 +1,116 @@ | |||||
.chart-container { | |||||
position: relative; /* for absolutely positioned tooltip */ | |||||
/* https://www.smashingmagazine.com/2015/11/using-system-ui-fonts-practical-guide/ */ | |||||
font-family: -apple-system, BlinkMacSystemFont, | |||||
'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', | |||||
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; | |||||
.axis, .chart-label { | |||||
fill: #555b51; | |||||
line { | |||||
stroke: #dadada; | |||||
} | |||||
} | |||||
.dataset-units { | |||||
circle { | |||||
stroke: #fff; | |||||
stroke-width: 2; | |||||
} | |||||
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; | |||||
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; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +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;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}"; |
@@ -0,0 +1,40 @@ | |||||
import '../css/charts.scss'; | |||||
// import MultiAxisChart from './charts/MultiAxisChart'; | |||||
import PercentageChart from './charts/PercentageChart'; | |||||
import PieChart from './charts/PieChart'; | |||||
import Heatmap from './charts/Heatmap'; | |||||
import AxisChart from './charts/AxisChart'; | |||||
import DonutChart from './charts/DonutChart'; | |||||
const chartTypes = { | |||||
bar: AxisChart, | |||||
line: AxisChart, | |||||
// multiaxis: MultiAxisChart, | |||||
percentage: PercentageChart, | |||||
heatmap: Heatmap, | |||||
pie: PieChart, | |||||
donut: DonutChart, | |||||
}; | |||||
function getChartByType(chartType = 'line', parent, options) { | |||||
if (chartType === 'axis-mixed') { | |||||
options.type = 'line'; | |||||
return new AxisChart(parent, options); | |||||
} | |||||
if (!chartTypes[chartType]) { | |||||
console.error("Undefined chart type: " + chartType); | |||||
return; | |||||
} | |||||
return new chartTypes[chartType](parent, options); | |||||
} | |||||
class Chart { | |||||
constructor(parent, options) { | |||||
return getChartByType(options.type, parent, options); | |||||
} | |||||
} | |||||
export { Chart, PercentageChart, PieChart, Heatmap, AxisChart }; |
@@ -0,0 +1,95 @@ | |||||
import BaseChart from './BaseChart'; | |||||
import { truncateString } from '../utils/draw-utils'; | |||||
import { legendDot } from '../utils/draw'; | |||||
import { round } from '../utils/helpers'; | |||||
import { getExtraWidth } from '../utils/constants'; | |||||
export default class AggregationChart extends BaseChart { | |||||
constructor(parent, args) { | |||||
super(parent, args); | |||||
} | |||||
configure(args) { | |||||
super.configure(args); | |||||
this.config.formatTooltipY = (args.tooltipOptions || {}).formatTooltipY; | |||||
this.config.maxSlices = args.maxSlices || 20; | |||||
this.config.maxLegendPoints = args.maxLegendPoints || 20; | |||||
} | |||||
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 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); | |||||
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.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0); | |||||
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); | |||||
let count = 0; | |||||
let y = 0; | |||||
this.legendTotals.map((d, i) => { | |||||
let barWidth = 150; | |||||
let divisor = Math.floor( | |||||
(this.width - getExtraWidth(this.measures))/barWidth | |||||
); | |||||
if (this.legendTotals.length < divisor) { | |||||
barWidth = this.width/this.legendTotals.length; | |||||
} | |||||
if(count > divisor) { | |||||
count = 0; | |||||
y += 20; | |||||
} | |||||
let x = barWidth * count + 5; | |||||
let label = this.config.truncateLegends ? truncateString(s.labels[i], barWidth/10) : s.labels[i]; | |||||
let formatted = this.config.formatTooltipY ? this.config.formatTooltipY(d) : d; | |||||
let dot = legendDot( | |||||
x, | |||||
y, | |||||
5, | |||||
this.colors[i], | |||||
`${label}: ${formatted}`, | |||||
false | |||||
); | |||||
this.legendArea.appendChild(dot); | |||||
count++; | |||||
}); | |||||
} | |||||
} |
@@ -0,0 +1,590 @@ | |||||
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) { | |||||
super(parent, args); | |||||
this.barOptions = args.barOptions || {}; | |||||
this.lineOptions = args.lineOptions || {}; | |||||
this.type = args.type || 'line'; | |||||
this.init = 1; | |||||
this.setup(); | |||||
} | |||||
setMeasures() { | |||||
if(this.data.datasets.length <= 1) { | |||||
this.config.showLegend = 0; | |||||
this.measures.paddings.bottom = 30; | |||||
} | |||||
} | |||||
configure(options) { | |||||
super.configure(options); | |||||
options.axisOptions = options.axisOptions || {}; | |||||
options.tooltipOptions = options.tooltipOptions || {}; | |||||
this.config.xAxisMode = options.axisOptions.xAxisMode || 'span'; | |||||
this.config.yAxisMode = options.axisOptions.yAxisMode || 'span'; | |||||
this.config.xIsSeries = options.axisOptions.xIsSeries || 0; | |||||
this.config.shortenYAxisNumbers = options.axisOptions.shortenYAxisNumbers || 0; | |||||
this.config.formatTooltipX = options.tooltipOptions.formatTooltipX; | |||||
this.config.formatTooltipY = options.tooltipOptions.formatTooltipY; | |||||
this.config.valuesOverPoints = options.valuesOverPoints; | |||||
} | |||||
prepareData(data=this.data) { | |||||
return dataPrep(data, this.type); | |||||
} | |||||
prepareFirstData(data=this.data) { | |||||
return zeroDataPrep(data); | |||||
} | |||||
calc(onlyWidthChange = false) { | |||||
this.calcXPositions(); | |||||
if(!onlyWidthChange) { | |||||
this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); | |||||
} | |||||
this.makeDataByIndex(); | |||||
} | |||||
calcXPositions() { | |||||
let s = this.state; | |||||
let labels = this.data.labels; | |||||
s.datasetLength = labels.length; | |||||
s.unitWidth = this.width/(s.datasetLength); | |||||
// Default, as per bar, and mixed. Only line will be a special case | |||||
s.xOffset = s.unitWidth/2; | |||||
// // For a pure Line Chart | |||||
// s.unitWidth = this.width/(s.datasetLength - 1); | |||||
// s.xOffset = 0; | |||||
s.xAxis = { | |||||
labels: labels, | |||||
positions: labels.map((d, i) => | |||||
floatTwo(s.xOffset + i * s.unitWidth) | |||||
) | |||||
}; | |||||
} | |||||
calcYAxisParameters(dataValues, withMinimum = 'false') { | |||||
const yPts = calcChartIntervals(dataValues, withMinimum); | |||||
const scaleMultiplier = this.height / getValueRange(yPts); | |||||
const intervalHeight = getIntervalSize(yPts) * scaleMultiplier; | |||||
const zeroLine = this.height - (getZeroIndex(yPts) * intervalHeight); | |||||
this.state.yAxis = { | |||||
labels: yPts, | |||||
positions: yPts.map(d => zeroLine - d * scaleMultiplier), | |||||
scaleMultiplier: scaleMultiplier, | |||||
zeroLine: zeroLine, | |||||
}; | |||||
// Dependent if above changes | |||||
this.calcDatasetPoints(); | |||||
this.calcYExtremes(); | |||||
this.calcYRegions(); | |||||
} | |||||
calcDatasetPoints() { | |||||
let s = this.state; | |||||
let scaleAll = values => values.map(val => scale(val, s.yAxis)); | |||||
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 == '<' ? '<' : '>'), | |||||
index: i, | |||||
chartType: d.chartType, | |||||
values: values, | |||||
yPositions: scaleAll(values), | |||||
cumulativeYs: cumulativeYs, | |||||
cumulativeYPos: scaleAll(cumulativeYs), | |||||
}; | |||||
}); | |||||
} | |||||
calcYExtremes() { | |||||
let s = this.state; | |||||
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 => { | |||||
d.yPositions.map((pos, j) => { | |||||
if(pos < s.yExtremes[j]) { | |||||
s.yExtremes[j] = pos; | |||||
} | |||||
}); | |||||
}); | |||||
} | |||||
calcYRegions() { | |||||
let s = this.state; | |||||
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.label.includes(':')) { | |||||
// d.label += ': ' + d.value; | |||||
// } | |||||
return 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 = {}; | |||||
return d; | |||||
}); | |||||
} | |||||
} | |||||
getAllYValues() { | |||||
let key = 'values'; | |||||
if(this.barOptions.stacked) { | |||||
key = 'cumulativeYs'; | |||||
let cumulative = new Array(this.state.datasetLength).fill(0); | |||||
this.data.datasets.map((d, i) => { | |||||
let values = this.data.datasets[i].values; | |||||
d[key] = cumulative = cumulative.map((c, i) => c + values[i]); | |||||
}); | |||||
} | |||||
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) { | |||||
this.data.yRegions.map(d => { | |||||
allValueLists.push([d.end, d.start]); | |||||
}); | |||||
} | |||||
return [].concat(...allValueLists); | |||||
} | |||||
setupComponents() { | |||||
let componentConfigs = [ | |||||
[ | |||||
'yAxis', | |||||
{ | |||||
mode: this.config.yAxisMode, | |||||
width: this.width, | |||||
shortenNumbers: this.config.shortenYAxisNumbers | |||||
// pos: 'right' | |||||
}, | |||||
function() { | |||||
return this.state.yAxis; | |||||
}.bind(this) | |||||
], | |||||
[ | |||||
'xAxis', | |||||
{ | |||||
mode: this.config.xAxisMode, | |||||
height: this.height, | |||||
// pos: 'right' | |||||
}, | |||||
function() { | |||||
let s = this.state; | |||||
s.xAxis.calcLabels = getShortenedLabels(this.width, | |||||
s.xAxis.labels, this.config.xIsSeries); | |||||
return s.xAxis; | |||||
}.bind(this) | |||||
], | |||||
[ | |||||
'yRegions', | |||||
{ | |||||
width: this.width, | |||||
pos: 'right' | |||||
}, | |||||
function() { | |||||
return this.state.yRegions; | |||||
}.bind(this) | |||||
], | |||||
]; | |||||
let barDatasets = this.state.datasets.filter(d => d.chartType === 'bar'); | |||||
let lineDatasets = this.state.datasets.filter(d => d.chartType === 'line'); | |||||
let barsConfigs = barDatasets.map(d => { | |||||
let index = d.index; | |||||
return [ | |||||
'barGraph' + '-' + d.index, | |||||
{ | |||||
index: index, | |||||
color: this.colors[index], | |||||
stacked: this.barOptions.stacked, | |||||
// same for all datasets | |||||
valuesOverPoints: this.config.valuesOverPoints, | |||||
minHeight: this.height * MIN_BAR_PERCENT_HEIGHT, | |||||
}, | |||||
function() { | |||||
let s = this.state; | |||||
let d = s.datasets[index]; | |||||
let stacked = this.barOptions.stacked; | |||||
let spaceRatio = this.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO; | |||||
let barsWidth = s.unitWidth * (1 - spaceRatio); | |||||
let barWidth = barsWidth/(stacked ? 1 : barDatasets.length); | |||||
let xPositions = s.xAxis.positions.map(x => x - barsWidth/2); | |||||
if(!stacked) { | |||||
xPositions = xPositions.map(p => p + barWidth * index); | |||||
} | |||||
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]); | |||||
} | |||||
return { | |||||
xPositions: xPositions, | |||||
yPositions: d.yPositions, | |||||
offsets: offsets, | |||||
// values: d.values, | |||||
labels: labels, | |||||
zeroLine: s.yAxis.zeroLine, | |||||
barsWidth: barsWidth, | |||||
barWidth: barWidth, | |||||
}; | |||||
}.bind(this) | |||||
]; | |||||
}); | |||||
let lineConfigs = lineDatasets.map(d => { | |||||
let index = d.index; | |||||
return [ | |||||
'lineGraph' + '-' + d.index, | |||||
{ | |||||
index: index, | |||||
color: this.colors[index], | |||||
svgDefs: this.svgDefs, | |||||
heatline: this.lineOptions.heatline, | |||||
regionFill: this.lineOptions.regionFill, | |||||
spline: this.lineOptions.spline, | |||||
hideDots: this.lineOptions.hideDots, | |||||
hideLine: this.lineOptions.hideLine, | |||||
// same for all datasets | |||||
valuesOverPoints: this.config.valuesOverPoints, | |||||
}, | |||||
function() { | |||||
let s = this.state; | |||||
let d = s.datasets[index]; | |||||
let minLine = s.yAxis.positions[0] < s.yAxis.zeroLine | |||||
? s.yAxis.positions[0] : s.yAxis.zeroLine; | |||||
return { | |||||
xPositions: s.xAxis.positions, | |||||
yPositions: d.yPositions, | |||||
values: d.values, | |||||
zeroLine: minLine, | |||||
radius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE, | |||||
}; | |||||
}.bind(this) | |||||
]; | |||||
}); | |||||
let markerConfigs = [ | |||||
[ | |||||
'yMarkers', | |||||
{ | |||||
width: this.width, | |||||
pos: 'right' | |||||
}, | |||||
function() { | |||||
return this.state.yMarkers; | |||||
}.bind(this) | |||||
] | |||||
]; | |||||
componentConfigs = componentConfigs.concat(barsConfigs, lineConfigs, markerConfigs); | |||||
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]; | |||||
})); | |||||
} | |||||
makeDataByIndex() { | |||||
this.dataByIndex = {}; | |||||
let s = this.state; | |||||
let formatX = this.config.formatTooltipX; | |||||
let formatY = this.config.formatTooltipY; | |||||
let titles = s.xAxis.labels; | |||||
titles.map((label, index) => { | |||||
let values = this.state.datasets.map((set, i) => { | |||||
let value = set.values[index]; | |||||
return { | |||||
title: set.name, | |||||
value: value, | |||||
yPos: set.yPositions[index], | |||||
color: this.colors[i], | |||||
formatted: formatY ? formatY(value) : value, | |||||
}; | |||||
}); | |||||
this.dataByIndex[index] = { | |||||
label: label, | |||||
formattedLabel: formatX ? formatX(label) : label, | |||||
xPos: s.xAxis.positions[index], | |||||
values: values, | |||||
yExtreme: s.yExtremes[index], | |||||
}; | |||||
}); | |||||
} | |||||
bindTooltip() { | |||||
// NOTE: could be in tooltip itself, as it is a given functionality for its parent | |||||
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)) { | |||||
this.mapTooltipXPosition(relX); | |||||
} else { | |||||
this.tip.hideTip(); | |||||
} | |||||
}); | |||||
} | |||||
mapTooltipXPosition(relX) { | |||||
let s = this.state; | |||||
if(!s.yExtremes) return; | |||||
let index = getClosestInArray(relX, s.xAxis.positions, true); | |||||
if (index >= 0) { | |||||
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.showTip(); | |||||
} | |||||
} | |||||
renderLegend() { | |||||
let s = this.data; | |||||
if(s.datasets.length > 1) { | |||||
this.legendArea.textContent = ''; | |||||
s.datasets.map((d, i) => { | |||||
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', | |||||
barWidth, | |||||
this.colors[i], | |||||
d.name, | |||||
this.config.truncateLegends); | |||||
this.legendArea.appendChild(rect); | |||||
}); | |||||
} | |||||
} | |||||
// Overlay | |||||
makeOverlay() { | |||||
if(this.init) { | |||||
this.init = 0; | |||||
return; | |||||
} | |||||
if(this.overlayGuides) { | |||||
this.overlayGuides.forEach(g => { | |||||
let o = g.overlay; | |||||
o.parentNode.removeChild(o); | |||||
}); | |||||
} | |||||
this.overlayGuides = this.dataUnitComponents.map(c => { | |||||
return { | |||||
type: c.unitType, | |||||
overlay: undefined, | |||||
units: c.units, | |||||
}; | |||||
}); | |||||
if(this.state.currentIndex === undefined) { | |||||
this.state.currentIndex = this.state.datasetLength - 1; | |||||
} | |||||
// Render overlays | |||||
this.overlayGuides.map(d => { | |||||
let currentUnit = d.units[this.state.currentIndex]; | |||||
d.overlay = makeOverlay[d.type](currentUnit); | |||||
this.drawArea.appendChild(d.overlay); | |||||
}); | |||||
} | |||||
updateOverlayGuides() { | |||||
if(this.overlayGuides) { | |||||
this.overlayGuides.forEach(g => { | |||||
let o = g.overlay; | |||||
o.parentNode.removeChild(o); | |||||
}); | |||||
} | |||||
} | |||||
bindOverlay() { | |||||
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.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.setCurrentDataPoint(index); | |||||
}); | |||||
} | |||||
updateOverlay() { | |||||
this.overlayGuides.map(d => { | |||||
let currentUnit = d.units[this.state.currentIndex]; | |||||
updateOverlay[d.type](currentUnit, d.overlay); | |||||
}); | |||||
} | |||||
onLeftArrow() { | |||||
this.setCurrentDataPoint(this.state.currentIndex - 1); | |||||
} | |||||
onRightArrow() { | |||||
this.setCurrentDataPoint(this.state.currentIndex + 1); | |||||
} | |||||
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]) | |||||
}; | |||||
return data_point; | |||||
} | |||||
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; | |||||
s.currentIndex = index; | |||||
fire(this.parent, "data-select", this.getDataPoint()); | |||||
} | |||||
// API | |||||
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) => { | |||||
d.values.splice(index, 0, datasetValues[i]); | |||||
}); | |||||
this.update(this.data); | |||||
} | |||||
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 => { | |||||
d.values.splice(index, 1); | |||||
}); | |||||
this.update(this.data); | |||||
} | |||||
updateDataset(datasetValues, index=0) { | |||||
this.data.datasets[index].values = datasetValues; | |||||
this.update(this.data); | |||||
} | |||||
// addDataset(dataset, index) {} | |||||
// removeDataset(index = 0) {} | |||||
updateDatasets(datasets) { | |||||
this.data.datasets.map((d, i) => { | |||||
if(datasets[i]) { | |||||
d.values = datasets[i]; | |||||
} | |||||
}); | |||||
this.update(this.data); | |||||
} | |||||
// updateDataPoint(dataPoint, index = 0) {} | |||||
// addDataPoint(dataPoint, index = 0) {} | |||||
// removeDataPoint(index = 0) {} | |||||
} |
@@ -0,0 +1,324 @@ | |||||
import SvgTip from '../objects/SvgTip'; | |||||
import { $, isElementInViewport, getElementContentWidth, isHidden } from '../utils/dom'; | |||||
import { makeSVGContainer, makeSVGDefs, makeSVGGroup, makeText } from '../utils/draw'; | |||||
import { BASE_MEASURES, getExtraHeight, getExtraWidth, getTopOffset, getLeftOffset, | |||||
INIT_CHART_UPDATE_TIMEOUT, CHART_POST_ANIMATE_TIMEOUT, DEFAULT_COLORS} from '../utils/constants'; | |||||
import { getColor, isValidColor } from '../utils/colors'; | |||||
import { runSMILAnimation } from '../utils/animation'; | |||||
import { downloadFile, prepareForExport } from '../utils/export'; | |||||
import { deepClone } from '../utils/helpers'; | |||||
export default class BaseChart { | |||||
constructor(parent, options) { | |||||
// deepclone options to avoid making changes to orignal object | |||||
options = deepClone(options); | |||||
this.parent = typeof parent === 'string' | |||||
? document.querySelector(parent) | |||||
: parent; | |||||
if (!(this.parent instanceof HTMLElement)) { | |||||
throw new Error('No `parent` element to render on was provided.'); | |||||
} | |||||
this.rawChartArgs = options; | |||||
this.title = options.title || ''; | |||||
this.type = options.type || ''; | |||||
this.realData = this.prepareData(options.data); | |||||
this.data = this.prepareFirstData(this.realData); | |||||
this.colors = this.validateColors(options.colors, this.type); | |||||
this.config = { | |||||
showTooltip: 1, // calculate | |||||
showLegend: 1, // calculate | |||||
isNavigable: options.isNavigable || 0, | |||||
animate: (typeof options.animate !== 'undefined') ? options.animate : 1, | |||||
truncateLegends: options.truncateLegends || 1 | |||||
}; | |||||
this.measures = JSON.parse(JSON.stringify(BASE_MEASURES)); | |||||
let m = this.measures; | |||||
this.setMeasures(options); | |||||
if(!this.title.length) { m.titleHeight = 0; } | |||||
if(!this.config.showLegend) m.legendHeight = 0; | |||||
this.argHeight = options.height || m.baseHeight; | |||||
this.state = {}; | |||||
this.options = {}; | |||||
this.initTimeout = INIT_CHART_UPDATE_TIMEOUT; | |||||
if(this.config.isNavigable) { | |||||
this.overlays = []; | |||||
} | |||||
this.configure(options); | |||||
} | |||||
prepareData(data) { | |||||
return data; | |||||
} | |||||
prepareFirstData(data) { | |||||
return data; | |||||
} | |||||
validateColors(colors, type) { | |||||
const validColors = []; | |||||
colors = (colors || []).concat(DEFAULT_COLORS[type]); | |||||
colors.forEach((string) => { | |||||
const color = getColor(string); | |||||
if(!isValidColor(color)) { | |||||
console.warn('"' + string + '" is not a valid color.'); | |||||
} else { | |||||
validColors.push(color); | |||||
} | |||||
}); | |||||
return validColors; | |||||
} | |||||
setMeasures() { | |||||
// Override measures, including those for title and legend | |||||
// set config for legend and title | |||||
} | |||||
configure() { | |||||
let height = this.argHeight; | |||||
this.baseHeight = height; | |||||
this.height = height - getExtraHeight(this.measures); | |||||
// Bind window events | |||||
this.boundDrawFn = () => this.draw(true); | |||||
if (ResizeObserver) { | |||||
this.resizeObserver = new ResizeObserver(this.boundDrawFn); | |||||
this.resizeObserver.observe(this.parent); | |||||
} | |||||
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); | |||||
} | |||||
// Has to be called manually | |||||
setup() { | |||||
this.makeContainer(); | |||||
this.updateWidth(); | |||||
this.makeTooltip(); | |||||
this.draw(false, true); | |||||
} | |||||
makeContainer() { | |||||
// Chart needs a dedicated parent element | |||||
this.parent.innerHTML = ''; | |||||
let args = { | |||||
inside: this.parent, | |||||
className: 'chart-container' | |||||
}; | |||||
if(this.independentWidth) { | |||||
args.styles = { width: this.independentWidth + 'px' }; | |||||
} | |||||
this.container = $.create('div', args); | |||||
} | |||||
makeTooltip() { | |||||
this.tip = new SvgTip({ | |||||
parent: this.container, | |||||
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; | |||||
} | |||||
this.updateWidth(); | |||||
this.calc(onlyWidthChange); | |||||
this.makeChartArea(); | |||||
this.setupComponents(); | |||||
this.components.forEach(c => c.setup(this.drawArea)); | |||||
// this.components.forEach(c => c.make()); | |||||
this.render(this.components, false); | |||||
if(init) { | |||||
this.data = this.realData; | |||||
setTimeout(() => {this.update(this.data);}, this.initTimeout); | |||||
} | |||||
this.renderLegend(); | |||||
this.setupNavigation(init); | |||||
} | |||||
calc() {} // builds state | |||||
updateWidth() { | |||||
this.baseWidth = getElementContentWidth(this.parent); | |||||
this.width = this.baseWidth - getExtraWidth(this.measures); | |||||
} | |||||
makeChartArea() { | |||||
if(this.svg) { | |||||
this.container.removeChild(this.svg); | |||||
} | |||||
let m = this.measures; | |||||
this.svg = makeSVGContainer( | |||||
this.container, | |||||
'influxframework-chart chart', | |||||
this.baseWidth, | |||||
this.baseHeight | |||||
); | |||||
this.svgDefs = makeSVGDefs(this.svg); | |||||
if(this.title.length) { | |||||
this.titleEL = makeText( | |||||
'title', | |||||
m.margins.left, | |||||
m.margins.top, | |||||
this.title, | |||||
{ | |||||
fontSize: m.titleFontSize, | |||||
fill: '#666666', | |||||
dy: m.titleFontSize | |||||
} | |||||
); | |||||
} | |||||
let top = getTopOffset(m); | |||||
this.drawArea = makeSVGGroup( | |||||
this.type + '-chart chart-draw-area', | |||||
`translate(${getLeftOffset(m)}, ${top})` | |||||
); | |||||
if(this.config.showLegend) { | |||||
top += this.height + m.paddings.bottom; | |||||
this.legendArea = makeSVGGroup( | |||||
'chart-legend', | |||||
`translate(${getLeftOffset(m)}, ${top})` | |||||
); | |||||
} | |||||
if(this.title.length) { this.svg.appendChild(this.titleEL); } | |||||
this.svg.appendChild(this.drawArea); | |||||
if(this.config.showLegend) { this.svg.appendChild(this.legendArea); } | |||||
this.updateTipOffset(getLeftOffset(m), getTopOffset(m)); | |||||
} | |||||
updateTipOffset(x, y) { | |||||
this.tip.offset = { | |||||
x: x, | |||||
y: y | |||||
}; | |||||
} | |||||
setupComponents() { this.components = new Map(); } | |||||
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); | |||||
this.renderLegend(); | |||||
} | |||||
render(components=this.components, animate=true) { | |||||
if(this.config.isNavigable) { | |||||
// Remove all existing overlays | |||||
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 => { | |||||
elementsToAnimate = elementsToAnimate.concat(c.update(animate)); | |||||
}); | |||||
if(elementsToAnimate.length > 0) { | |||||
runSMILAnimation(this.container, this.svg, elementsToAnimate); | |||||
setTimeout(() => { | |||||
components.forEach(c => c.make()); | |||||
this.updateNav(); | |||||
}, CHART_POST_ANIMATE_TIMEOUT); | |||||
} else { | |||||
components.forEach(c => c.make()); | |||||
this.updateNav(); | |||||
} | |||||
} | |||||
updateNav() { | |||||
if(this.config.isNavigable) { | |||||
this.makeOverlay(); | |||||
this.bindUnits(); | |||||
} | |||||
} | |||||
renderLegend() {} | |||||
setupNavigation(init=false) { | |||||
if(!this.config.isNavigable) return; | |||||
if(init) { | |||||
this.bindOverlay(); | |||||
this.keyActions = { | |||||
'13': this.onEnterKey.bind(this), | |||||
'37': this.onLeftArrow.bind(this), | |||||
'38': this.onUpArrow.bind(this), | |||||
'39': this.onRightArrow.bind(this), | |||||
'40': this.onDownArrow.bind(this), | |||||
}; | |||||
document.addEventListener('keydown', (e) => { | |||||
if(isElementInViewport(this.container)) { | |||||
e = e || window.event; | |||||
if(this.keyActions[e.keyCode]) { | |||||
this.keyActions[e.keyCode](); | |||||
} | |||||
} | |||||
}); | |||||
} | |||||
} | |||||
makeOverlay() {} | |||||
updateOverlay() {} | |||||
bindOverlay() {} | |||||
bindUnits() {} | |||||
onLeftArrow() {} | |||||
onRightArrow() {} | |||||
onUpArrow() {} | |||||
onDownArrow() {} | |||||
onEnterKey() {} | |||||
addDataPoint() {} | |||||
removeDataPoint() {} | |||||
getDataPoint() {} | |||||
setCurrentDataPoint() {} | |||||
updateDataset() {} | |||||
export() { | |||||
let chartSvg = prepareForExport(this.svg); | |||||
downloadFile(this.title || 'Chart', [chartSvg]); | |||||
} | |||||
} |
@@ -0,0 +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'; | |||||
export default class DonutChart extends AggregationChart { | |||||
constructor(parent, args) { | |||||
super(parent, args); | |||||
this.type = 'donut'; | |||||
this.initTimeout = 0; | |||||
this.init = 1; | |||||
this.setup(); | |||||
} | |||||
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.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; | |||||
const { radius, clockWise } = this; | |||||
const prevSlicesProperties = s.slicesProperties || []; | |||||
s.sliceStrings = []; | |||||
s.slicesProperties = []; | |||||
let curAngle = 180 - this.config.startAngle; | |||||
s.sliceTotals.map((total, i) => { | |||||
const startAngle = curAngle; | |||||
const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE; | |||||
const largeArc = originDiffAngle > 180 ? 1: 0; | |||||
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | |||||
const endAngle = curAngle = curAngle + diffAngle; | |||||
const startPosition = getPositionByAngle(startAngle, radius); | |||||
const endPosition = getPositionByAngle(endAngle, radius); | |||||
const 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); | |||||
s.sliceStrings.push(curPath); | |||||
s.slicesProperties.push({ | |||||
startPosition, | |||||
endPosition, | |||||
value: total, | |||||
total: s.grandTotal, | |||||
startAngle, | |||||
endAngle, | |||||
angle: diffAngle | |||||
}); | |||||
}); | |||||
this.init = 0; | |||||
} | |||||
setupComponents() { | |||||
let s = this.state; | |||||
let componentConfigs = [ | |||||
[ | |||||
'donutSlices', | |||||
{ }, | |||||
function() { | |||||
return { | |||||
sliceStrings: s.sliceStrings, | |||||
colors: this.colors, | |||||
strokeWidth: this.strokeWidth, | |||||
}; | |||||
}.bind(this) | |||||
] | |||||
]; | |||||
this.components = new Map(componentConfigs | |||||
.map(args => { | |||||
let component = getComponent(...args); | |||||
return [args[0], component]; | |||||
})); | |||||
} | |||||
calTranslateByAngle(property){ | |||||
const{ radius, hoverRadio } = this; | |||||
const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius); | |||||
return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`; | |||||
} | |||||
hoverSlice(path,i,flag,e){ | |||||
if(!path) return; | |||||
const color = this.colors[i]; | |||||
if(flag) { | |||||
transform(path, this.calTranslateByAngle(this.state.slicesProperties[i])); | |||||
path.style.stroke = lightenDarkenColor(color, 50); | |||||
let g_off = getOffset(this.svg); | |||||
let x = e.pageX - g_off.left + 10; | |||||
let y = e.pageY - g_off.top - 10; | |||||
let title = (this.formatted_labels && this.formatted_labels.length > 0 | |||||
? this.formatted_labels[i] : this.state.labels[i]) + ': '; | |||||
let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1); | |||||
this.tip.setValues(x, y, {name: title, value: percent + "%"}); | |||||
this.tip.showTip(); | |||||
} else { | |||||
transform(path,'translate3d(0,0,0)'); | |||||
this.tip.hideTip(); | |||||
path.style.stroke = color; | |||||
} | |||||
} | |||||
bindTooltip() { | |||||
this.container.addEventListener('mousemove', this.mouseMove); | |||||
this.container.addEventListener('mouseleave', this.mouseLeave); | |||||
} | |||||
mouseMove(e){ | |||||
const target = e.target; | |||||
let slices = this.components.get('donutSlices').store; | |||||
let prevIndex = this.curActiveSliceIndex; | |||||
let prevAcitve = this.curActiveSlice; | |||||
if(slices.includes(target)) { | |||||
let i = slices.indexOf(target); | |||||
this.hoverSlice(prevAcitve, prevIndex,false); | |||||
this.curActiveSlice = target; | |||||
this.curActiveSliceIndex = i; | |||||
this.hoverSlice(target, i, true, e); | |||||
} else { | |||||
this.mouseLeave(); | |||||
} | |||||
} | |||||
mouseLeave(){ | |||||
this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false); | |||||
} | |||||
} |
@@ -0,0 +1,302 @@ | |||||
import BaseChart from './BaseChart'; | |||||
import { getComponent } from '../objects/ChartComponents'; | |||||
import { makeText, heatSquare } from '../utils/draw'; | |||||
import { DAY_NAMES_SHORT, toMidnightUTC, addDays, areInSameMonth, getLastDateInMonth, setDayToSunday, getYyyyMmDd, getWeeksBetween, getMonthName, clone, | |||||
NO_OF_MILLIS, NO_OF_YEAR_MONTHS, NO_OF_DAYS_IN_WEEK } from '../utils/date-utils'; | |||||
import { calcDistribution, getMaxCheckpoint } from '../utils/intervals'; | |||||
import { getExtraHeight, getExtraWidth, HEATMAP_DISTRIBUTION_SIZE, HEATMAP_SQUARE_SIZE, | |||||
HEATMAP_GUTTER_SIZE } from '../utils/constants'; | |||||
const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE; | |||||
const ROW_HEIGHT = COL_WIDTH; | |||||
// const DAY_INCR = 1; | |||||
export default class Heatmap extends BaseChart { | |||||
constructor(parent, options) { | |||||
super(parent, options); | |||||
this.type = 'heatmap'; | |||||
this.countLabel = options.countLabel || ''; | |||||
let validStarts = ['Sunday', 'Monday']; | |||||
let startSubDomain = validStarts.includes(options.startSubDomain) | |||||
? options.startSubDomain : 'Sunday'; | |||||
this.startSubDomainIndex = validStarts.indexOf(startSubDomain); | |||||
this.setup(); | |||||
} | |||||
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); | |||||
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); | |||||
} | |||||
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.end) { | |||||
data.end = new Date(); | |||||
} | |||||
data.end = toMidnightUTC(data.end); | |||||
data.dataPoints = data.dataPoints || {}; | |||||
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; | |||||
} | |||||
return data; | |||||
} | |||||
calc() { | |||||
let s = this.state; | |||||
s.start = clone(this.data.start); | |||||
s.end = clone(this.data.end); | |||||
s.firstWeekStart = clone(s.start); | |||||
s.noOfWeeks = getWeeksBetween(s.start, s.end); | |||||
s.distribution = calcDistribution( | |||||
Object.values(this.data.dataPoints), HEATMAP_DISTRIBUTION_SIZE); | |||||
s.domainConfigs = this.getDomains(); | |||||
} | |||||
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]; | |||||
}) | |||||
); | |||||
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.'); | |||||
} | |||||
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)) { | |||||
let count = daySquare.getAttribute('data-value'); | |||||
let dateParts = daySquare.getAttribute('data-date').split('-'); | |||||
let month = getMonthName(parseInt(dateParts[1])-1, true); | |||||
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]; | |||||
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; | |||||
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); | |||||
}); | |||||
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()]; | |||||
const noOfMonths = (endMonth - startMonth + 1) + (endYear - startYear) * 12; | |||||
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)); | |||||
addDays(endDate, 1); | |||||
startOfMonth = endDate; | |||||
} | |||||
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)); | |||||
let domainConfig = { | |||||
index: month, | |||||
cols: [] | |||||
}; | |||||
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); | |||||
startOfWeek = toMidnightUTC(new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd)); | |||||
addDays(startOfWeek, 1); | |||||
} | |||||
if(col[NO_OF_DAYS_IN_WEEK - 1].dataValue !== undefined) { | |||||
addDays(startOfWeek, 1); | |||||
cols.push(this.getCol(startOfWeek, month, true)); | |||||
} | |||||
domainConfig.cols = cols; | |||||
return domainConfig; | |||||
} | |||||
getCol(startDate, month, empty = false) { | |||||
let s = this.state; | |||||
// 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 = {}; | |||||
// 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); | |||||
} | |||||
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; | |||||
} | |||||
} |
@@ -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 | |||||
)); | |||||
}); | |||||
} | |||||
}); | |||||
}); | |||||
} | |||||
} |
@@ -0,0 +1,92 @@ | |||||
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(); | |||||
} | |||||
setMeasures(options) { | |||||
let m = this.measures; | |||||
this.barOptions = options.barOptions || {}; | |||||
let b = this.barOptions; | |||||
b.height = b.height || PERCENTAGE_BAR_DEFAULT_HEIGHT; | |||||
b.depth = b.depth || PERCENTAGE_BAR_DEFAULT_DEPTH; | |||||
m.paddings.right = 30; | |||||
m.legendHeight = 60; | |||||
m.baseHeight = (b.height + b.depth * 0.5) * 8; | |||||
} | |||||
setupComponents() { | |||||
let s = this.state; | |||||
let componentConfigs = [ | |||||
[ | |||||
'percentageBars', | |||||
{ | |||||
barHeight: this.barOptions.height, | |||||
barDepth: this.barOptions.depth, | |||||
}, | |||||
function() { | |||||
return { | |||||
xPositions: s.xPositions, | |||||
widths: s.widths, | |||||
colors: this.colors | |||||
}; | |||||
}.bind(this) | |||||
] | |||||
]; | |||||
this.components = new Map(componentConfigs | |||||
.map(args => { | |||||
let component = getComponent(...args); | |||||
return [args[0], component]; | |||||
})); | |||||
} | |||||
calc() { | |||||
super.calc(); | |||||
let s = this.state; | |||||
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; | |||||
}); | |||||
} | |||||
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); | |||||
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(); | |||||
} | |||||
}); | |||||
} | |||||
} |
@@ -0,0 +1,155 @@ | |||||
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; | |||||
this.setup(); | |||||
} | |||||
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.clockWise = args.clockWise || false; | |||||
} | |||||
calc() { | |||||
super.calc(); | |||||
let s = this.state; | |||||
this.radius = (this.height > this.width ? this.center.x : this.center.y); | |||||
const { radius, clockWise } = this; | |||||
const prevSlicesProperties = s.slicesProperties || []; | |||||
s.sliceStrings = []; | |||||
s.slicesProperties = []; | |||||
let curAngle = 180 - this.config.startAngle; | |||||
s.sliceTotals.map((total, i) => { | |||||
const startAngle = curAngle; | |||||
const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE; | |||||
const largeArc = originDiffAngle > 180 ? 1: 0; | |||||
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | |||||
const endAngle = curAngle = curAngle + diffAngle; | |||||
const startPosition = getPositionByAngle(startAngle, radius); | |||||
const endPosition = getPositionByAngle(endAngle, radius); | |||||
const 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); | |||||
s.sliceStrings.push(curPath); | |||||
s.slicesProperties.push({ | |||||
startPosition, | |||||
endPosition, | |||||
value: total, | |||||
total: s.grandTotal, | |||||
startAngle, | |||||
endAngle, | |||||
angle: diffAngle | |||||
}); | |||||
}); | |||||
this.init = 0; | |||||
} | |||||
setupComponents() { | |||||
let s = this.state; | |||||
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]; | |||||
})); | |||||
} | |||||
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; | |||||
} | |||||
} | |||||
bindTooltip() { | |||||
this.container.addEventListener('mousemove', this.mouseMove); | |||||
this.container.addEventListener('mouseleave', 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); | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
import * as Charts from './chart'; | |||||
let influxframework = { }; | |||||
influxframework.NAME = 'InfluxFramework Charts'; | |||||
influxframework.VERSION = '1.6.2'; | |||||
influxframework = Object.assign({ }, influxframework, Charts); | |||||
export default influxframework; |
@@ -0,0 +1,446 @@ | |||||
import { makeSVGGroup } from '../utils/draw'; | |||||
import { makeText, makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, percentageBar, getPaths, heatSquare } from '../utils/draw'; | |||||
import { equilizeNoOfElements } from '../utils/draw-utils'; | |||||
import { translateHoriLine, translateVertLine, animateRegion, animateBar, | |||||
animateDot, animatePath, animatePathStr } from '../utils/animate'; | |||||
import { getMonthName } from '../utils/date-utils'; | |||||
class ChartComponent { | |||||
constructor({ | |||||
layerClass = '', | |||||
layerTransform = '', | |||||
constants, | |||||
getData, | |||||
makeElements, | |||||
animateElements | |||||
}) { | |||||
this.layerTransform = layerTransform; | |||||
this.constants = constants; | |||||
this.makeElements = makeElements; | |||||
this.getData = getData; | |||||
this.animateElements = animateElements; | |||||
this.store = []; | |||||
this.labels = []; | |||||
this.layerClass = layerClass; | |||||
this.layerClass = typeof(this.layerClass) === 'function' | |||||
? this.layerClass() : this.layerClass; | |||||
this.refresh(); | |||||
} | |||||
refresh(data) { | |||||
this.data = data || this.getData(); | |||||
} | |||||
setup(parent) { | |||||
this.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent); | |||||
} | |||||
make() { | |||||
this.render(this.data); | |||||
this.oldData = this.data; | |||||
} | |||||
render(data) { | |||||
this.store = this.makeElements(data); | |||||
this.layer.textContent = ''; | |||||
this.store.forEach(element => { | |||||
this.layer.appendChild(element); | |||||
}); | |||||
this.labels.forEach(element => { | |||||
this.layer.appendChild(element); | |||||
}); | |||||
} | |||||
update(animate = true) { | |||||
this.refresh(); | |||||
let animateElements = []; | |||||
if(animate) { | |||||
animateElements = this.animateElements(this.data) || []; | |||||
} | |||||
return animateElements; | |||||
} | |||||
} | |||||
let componentConfigs = { | |||||
donutSlices: { | |||||
layerClass: 'donut-slices', | |||||
makeElements(data) { | |||||
return data.sliceStrings.map((s, i) => { | |||||
let slice = makePath(s, 'donut-path', data.colors[i], 'none', data.strokeWidth); | |||||
slice.style.transition = 'transform .3s;'; | |||||
return slice; | |||||
}); | |||||
}, | |||||
animateElements(newData) { | |||||
return this.store.map((slice, i) => animatePathStr(slice, newData.sliceStrings[i])); | |||||
}, | |||||
}, | |||||
pieSlices: { | |||||
layerClass: 'pie-slices', | |||||
makeElements(data) { | |||||
return data.sliceStrings.map((s, i) =>{ | |||||
let slice = makePath(s, 'pie-path', 'none', data.colors[i]); | |||||
slice.style.transition = 'transform .3s;'; | |||||
return slice; | |||||
}); | |||||
}, | |||||
animateElements(newData) { | |||||
return this.store.map((slice, i) => | |||||
animatePathStr(slice, newData.sliceStrings[i]) | |||||
); | |||||
} | |||||
}, | |||||
percentageBars: { | |||||
layerClass: 'percentage-bars', | |||||
makeElements(data) { | |||||
return data.xPositions.map((x, i) =>{ | |||||
let y = 0; | |||||
let bar = percentageBar(x, y, data.widths[i], | |||||
this.constants.barHeight, this.constants.barDepth, data.colors[i]); | |||||
return bar; | |||||
}); | |||||
}, | |||||
animateElements(newData) { | |||||
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; | |||||
let oldLabels = this.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] | |||||
); | |||||
}); | |||||
} | |||||
}, | |||||
xAxis: { | |||||
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}) | |||||
); | |||||
}, | |||||
animateElements(newData) { | |||||
let newPos = newData.positions; | |||||
let newLabels = newData.calcLabels; | |||||
let oldPos = this.oldData.positions; | |||||
let oldLabels = this.oldData.calcLabels; | |||||
[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos); | |||||
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels); | |||||
this.render({ | |||||
positions: oldPos, | |||||
calcLabels: newLabels | |||||
}); | |||||
return this.store.map((line, i) => { | |||||
return translateVertLine( | |||||
line, newPos[i], oldPos[i] | |||||
); | |||||
}); | |||||
} | |||||
}, | |||||
yMarkers: { | |||||
layerClass: 'y-markers', | |||||
makeElements(data) { | |||||
return data.map(m => | |||||
yMarker(m.position, m.label, this.constants.width, | |||||
{labelPos: m.options.labelPos, mode: 'span', lineType: 'dashed'}) | |||||
); | |||||
}, | |||||
animateElements(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 oldPos = this.oldData.map(d => d.position); | |||||
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] | |||||
); | |||||
}); | |||||
} | |||||
}, | |||||
yRegions: { | |||||
layerClass: 'y-regions', | |||||
makeElements(data) { | |||||
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); | |||||
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); | |||||
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] | |||||
)); | |||||
}); | |||||
return animateElements; | |||||
} | |||||
}, | |||||
heatDomain: { | |||||
layerClass: function() { return 'heat-domain domain-' + this.constants.index; }, | |||||
makeElements(data) { | |||||
let {index, colWidth, rowHeight, squareSize, radius, xTranslate} = this.constants; | |||||
let monthNameHeight = -12; | |||||
let x = xTranslate, y = 0; | |||||
this.serializedSubDomains = []; | |||||
data.cols.map((week, weekNo) => { | |||||
if(weekNo === 1) { | |||||
this.labels.push( | |||||
makeText('domain-name', x, monthNameHeight, getMonthName(index, true).toUpperCase(), | |||||
{ | |||||
fontSize: 9 | |||||
} | |||||
) | |||||
); | |||||
} | |||||
week.map((day, i) => { | |||||
if(day.fill) { | |||||
let data = { | |||||
'data-date': day.yyyyMmDd, | |||||
'data-value': day.dataValue, | |||||
'data-day': i | |||||
}; | |||||
let square = heatSquare('day', x, y, squareSize, radius, day.fill, data); | |||||
this.serializedSubDomains.push(square); | |||||
} | |||||
y += rowHeight; | |||||
}); | |||||
y = 0; | |||||
x += colWidth; | |||||
}); | |||||
return this.serializedSubDomains; | |||||
}, | |||||
animateElements(newData) { | |||||
if(newData) return []; | |||||
} | |||||
}, | |||||
barGraph: { | |||||
layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; }, | |||||
makeElements(data) { | |||||
let c = this.constants; | |||||
this.unitType = 'bar'; | |||||
this.units = data.yPositions.map((y, j) => { | |||||
return datasetBar( | |||||
data.xPositions[j], | |||||
y, | |||||
data.barWidth, | |||||
c.color, | |||||
data.labels[j], | |||||
j, | |||||
data.offsets[j], | |||||
{ | |||||
zeroLine: data.zeroLine, | |||||
barsWidth: data.barsWidth, | |||||
minHeight: c.minHeight | |||||
} | |||||
); | |||||
}); | |||||
return this.units; | |||||
}, | |||||
animateElements(newData) { | |||||
let newXPos = newData.xPositions; | |||||
let newYPos = newData.yPositions; | |||||
let newOffsets = newData.offsets; | |||||
let newLabels = newData.labels; | |||||
let oldXPos = this.oldData.xPositions; | |||||
let oldYPos = this.oldData.yPositions; | |||||
let oldOffsets = this.oldData.offsets; | |||||
let oldLabels = this.oldData.labels; | |||||
[oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos); | |||||
[oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos); | |||||
[oldOffsets, newOffsets] = equilizeNoOfElements(oldOffsets, newOffsets); | |||||
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels); | |||||
this.render({ | |||||
xPositions: oldXPos, | |||||
yPositions: oldYPos, | |||||
offsets: oldOffsets, | |||||
labels: newLabels, | |||||
zeroLine: this.oldData.zeroLine, | |||||
barsWidth: this.oldData.barsWidth, | |||||
barWidth: this.oldData.barWidth, | |||||
}); | |||||
let animateElements = []; | |||||
this.store.map((bar, i) => { | |||||
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; }, | |||||
makeElements(data) { | |||||
let c = this.constants; | |||||
this.unitType = 'dot'; | |||||
this.paths = {}; | |||||
if(!c.hideLine) { | |||||
this.paths = getPaths( | |||||
data.xPositions, | |||||
data.yPositions, | |||||
c.color, | |||||
{ | |||||
heatline: c.heatline, | |||||
regionFill: c.regionFill, | |||||
spline: c.spline | |||||
}, | |||||
{ | |||||
svgDefs: c.svgDefs, | |||||
zeroLine: data.zeroLine | |||||
} | |||||
); | |||||
} | |||||
this.units = []; | |||||
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] : ''), | |||||
j | |||||
); | |||||
}); | |||||
} | |||||
return Object.values(this.paths).concat(this.units); | |||||
}, | |||||
animateElements(newData) { | |||||
let newXPos = newData.xPositions; | |||||
let newYPos = newData.yPositions; | |||||
let newValues = newData.values; | |||||
let oldXPos = this.oldData.xPositions; | |||||
let oldYPos = this.oldData.yPositions; | |||||
let oldValues = this.oldData.values; | |||||
[oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos); | |||||
[oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos); | |||||
[oldValues, newValues] = equilizeNoOfElements(oldValues, newValues); | |||||
this.render({ | |||||
xPositions: oldXPos, | |||||
yPositions: oldYPos, | |||||
values: newValues, | |||||
zeroLine: this.oldData.zeroLine, | |||||
radius: this.oldData.radius, | |||||
}); | |||||
let animateElements = []; | |||||
if(Object.keys(this.paths).length) { | |||||
animateElements = animateElements.concat(animatePath( | |||||
this.paths, newXPos, newYPos, newData.zeroLine, this.constants.spline)); | |||||
} | |||||
if(this.units.length) { | |||||
this.units.map((dot, 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 config = componentConfigs[keys[0]]; | |||||
Object.assign(config, { | |||||
constants: constants, | |||||
getData: getData | |||||
}); | |||||
return new ChartComponent(config); | |||||
} |
@@ -0,0 +1,127 @@ | |||||
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; | |||||
this.x = 0; | |||||
this.y = 0; | |||||
this.top = 0; | |||||
this.left = 0; | |||||
this.setup(); | |||||
} | |||||
setup() { | |||||
this.makeTooltip(); | |||||
} | |||||
refresh() { | |||||
this.fill(); | |||||
this.calcPosition(); | |||||
} | |||||
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(); | |||||
this.title = this.container.querySelector('.title'); | |||||
this.dataPointList = this.container.querySelector('.data-point-list'); | |||||
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>`; | |||||
} | |||||
this.title.innerHTML = title; | |||||
this.dataPointList.innerHTML = ''; | |||||
this.listValues.map((set, i) => { | |||||
const color = this.colors[i] || 'black'; | |||||
let value = set.formatted === 0 || set.formatted ? set.formatted : set.value; | |||||
let li = $.create('li', { | |||||
styles: { | |||||
'border-top': `3px solid ${color}` | |||||
}, | |||||
innerHTML: `<strong style="display: block;">${ value === 0 || value ? value : '' }</strong> | |||||
${set.title ? set.title : '' }` | |||||
}); | |||||
this.dataPointList.appendChild(li); | |||||
}); | |||||
} | |||||
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; | |||||
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; | |||||
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(); | |||||
} | |||||
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'; | |||||
} | |||||
} |
@@ -0,0 +1,105 @@ | |||||
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 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} | |||||
]; | |||||
} | |||||
export function translateVertLine(xLine, newX, oldX) { | |||||
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); | |||||
} | |||||
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 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 | |||||
]; | |||||
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); | |||||
} | |||||
export function animatePath(paths, newXList, newYList, zeroLine, spline) { | |||||
let pathComponents = []; | |||||
let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y)).join("L"); | |||||
if (spline) | |||||
pointsStr = getSplineCurvePointsStr(newXList, newYList); | |||||
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}`; | |||||
const animRegion = [ | |||||
paths.region, | |||||
{d:"M" + regStartPt + pointsStr + regEndPt}, | |||||
PATH_ANIM_DUR, | |||||
STD_EASING | |||||
]; | |||||
pathComponents.push(animRegion); | |||||
} | |||||
return pathComponents; | |||||
} | |||||
export function animatePathStr(oldPath, pathStr) { | |||||
return [oldPath, {d: pathStr}, UNIT_ANIM_DUR, STD_EASING]; | |||||
} |
@@ -0,0 +1,120 @@ | |||||
// Leveraging SMIL Animations | |||||
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" | |||||
}; | |||||
function animateSVGElement(element, props, dur, easingType="linear", type=undefined, oldValues={}) { | |||||
let animElement = element.cloneNode(true); | |||||
let newElement = element.cloneNode(true); | |||||
for(var attributeName in props) { | |||||
let animateElement; | |||||
if(attributeName === 'transform') { | |||||
animateElement = document.createElementNS("http://www.w3.org/2000/svg", "animateTransform"); | |||||
} else { | |||||
animateElement = document.createElementNS("http://www.w3.org/2000/svg", "animate"); | |||||
} | |||||
let currentValue = oldValues[attributeName] || element.getAttribute(attributeName); | |||||
let value = props[attributeName]; | |||||
let animAttr = { | |||||
attributeName: attributeName, | |||||
from: currentValue, | |||||
to: value, | |||||
begin: "0s", | |||||
dur: dur/1000 + "s", | |||||
values: currentValue + ";" + value, | |||||
keySplines: EASING[easingType], | |||||
keyTimes: "0;1", | |||||
calcMode: "spline", | |||||
fill: 'freeze' | |||||
}; | |||||
if(type) { | |||||
animAttr["type"] = type; | |||||
} | |||||
for (var i in animAttr) { | |||||
animateElement.setAttribute(i, animAttr[i]); | |||||
} | |||||
animElement.appendChild(animateElement); | |||||
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; | |||||
} | |||||
function animateSVG(svgContainer, elements) { | |||||
let newElements = []; | |||||
let animElements = []; | |||||
elements.map(element => { | |||||
let unit = element[0]; | |||||
let parent = unit.parentNode; | |||||
let animElement, newElement; | |||||
element[0] = unit; | |||||
[animElement, newElement] = animateSVGElement(...element); | |||||
newElements.push(newElement); | |||||
animElements.push([animElement, parent]); | |||||
if (parent) { | |||||
parent.replaceChild(animElement, unit); | |||||
} | |||||
}); | |||||
let animSvg = svgContainer.cloneNode(true); | |||||
animElements.map((animElement, i) => { | |||||
if (animElement[1]) { | |||||
animElement[1].replaceChild(newElements[i], animElement[0]); | |||||
elements[i][0] = newElements[i]; | |||||
} | |||||
}); | |||||
return animSvg; | |||||
} | |||||
export function runSMILAnimation(parent, svgElement, elementsToAnimate) { | |||||
if(elementsToAnimate.length === 0) return; | |||||
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); | |||||
} |
@@ -0,0 +1,129 @@ | |||||
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) { | |||||
data.labels = data.labels || []; | |||||
let datasetLength = data.labels.length; | |||||
// Datasets | |||||
let datasets = data.datasets; | |||||
let zeroArray = new Array(datasetLength).fill(0); | |||||
if(!datasets) { | |||||
// default | |||||
datasets = [{ | |||||
values: zeroArray | |||||
}]; | |||||
} | |||||
datasets.map(d=> { | |||||
// Set values | |||||
if(!d.values) { | |||||
d.values = zeroArray; | |||||
} else { | |||||
// Check for non values | |||||
let vals = d.values; | |||||
vals = vals.map(val => (!isNaN(val) ? val : 0)); | |||||
// Trim or extend | |||||
if(vals.length > datasetLength) { | |||||
vals = vals.slice(0, datasetLength); | |||||
} else { | |||||
vals = fillArray(vals, datasetLength - vals.length, 0); | |||||
} | |||||
d.values = vals; | |||||
} | |||||
// Set 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) { | |||||
[d.start, d.end] = [d.end, d.start]; | |||||
} | |||||
}); | |||||
} | |||||
return data; | |||||
} | |||||
export function zeroDataPrep(realData) { | |||||
let datasetLength = realData.labels.length; | |||||
let zeroArray = new Array(datasetLength).fill(0); | |||||
let zeroData = { | |||||
labels: realData.labels.slice(0, -1), | |||||
datasets: realData.datasets.map(d => { | |||||
return { | |||||
name: '', | |||||
values: zeroArray.slice(0, -1), | |||||
chartType: d.chartType | |||||
}; | |||||
}), | |||||
}; | |||||
if(realData.yMarkers) { | |||||
zeroData.yMarkers = [ | |||||
{ | |||||
value: 0, | |||||
label: '' | |||||
} | |||||
]; | |||||
} | |||||
if(realData.yRegions) { | |||||
zeroData.yRegions = [ | |||||
{ | |||||
start: 0, | |||||
end: 0, | |||||
label: '' | |||||
} | |||||
]; | |||||
} | |||||
return zeroData; | |||||
} | |||||
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) + " ..."; | |||||
} else { | |||||
label = label.slice(0, allowedLetters) + '..'; | |||||
} | |||||
} else { | |||||
if(i % seriesMultiple !== 0) { | |||||
label = ""; | |||||
} | |||||
} | |||||
} | |||||
return label; | |||||
}); | |||||
return calcLabels; | |||||
} |
@@ -0,0 +1,53 @@ | |||||
const PRESET_COLOR_MAP = { | |||||
'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; | |||||
} | |||||
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); | |||||
} | |||||
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); | |||||
} | |||||
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; | |||||
}; |
@@ -0,0 +1,107 @@ | |||||
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: [] | |||||
}; | |||||
export const DATA_COLOR_DIVISIONS = { | |||||
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 | |||||
}, | |||||
baseHeight: 240, | |||||
titleHeight: 20, | |||||
legendHeight: 30, | |||||
titleFontSize: 12, | |||||
}; | |||||
export function getTopOffset(m) { | |||||
return m.titleHeight + m.margins.top + m.paddings.top; | |||||
} | |||||
export function getLeftOffset(m) { | |||||
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; | |||||
} | |||||
export function getExtraWidth(m) { | |||||
let totalExtraWidth = m.margins.left + m.margins.right | |||||
+ m.paddings.left + m.paddings.right; | |||||
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 AXIS_LEGEND_BAR_SIZE = 100; | |||||
export const BAR_CHART_SPACE_RATIO = 0.5; | |||||
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 = 20; | |||||
export const PERCENTAGE_BAR_DEFAULT_DEPTH = 2; | |||||
// Fixed 5-color theme, | |||||
// More colors are difficult to parse visually | |||||
export const HEATMAP_DISTRIBUTION_SIZE = 5; | |||||
export const HEATMAP_SQUARE_SIZE = 10; | |||||
export const HEATMAP_GUTTER_SIZE = 2; | |||||
export const DEFAULT_CHAR_WIDTH = 7; | |||||
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 | |||||
}; | |||||
// Universal constants | |||||
export const ANGLE_RATIO = Math.PI / 180; | |||||
export const FULL_ANGLE = 360; |
@@ -0,0 +1,96 @@ | |||||
// Playing around with dates | |||||
export const NO_OF_YEAR_MONTHS = 12; | |||||
export const NO_OF_DAYS_IN_WEEK = 7; | |||||
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 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; | |||||
} | |||||
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('-'); | |||||
} | |||||
export function clone(date) { | |||||
return new Date(date.getTime()); | |||||
} | |||||
export function timestampSec(date) { | |||||
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; | |||||
} | |||||
// 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); | |||||
} | |||||
export function getDaysBetween(startDate, endDate) { | |||||
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(); | |||||
} | |||||
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 | |||||
} | |||||
// mutates | |||||
export function setDayToSunday(date) { | |||||
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); | |||||
} |
@@ -0,0 +1,137 @@ | |||||
export function $(expr, con) { | |||||
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; | |||||
} | |||||
$.create = (tag, o) => { | |||||
var element = document.createElement(tag); | |||||
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); | |||||
} | |||||
} | |||||
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); | |||||
} | |||||
export function isElementInViewport(el) { | |||||
// 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() */ | |||||
); | |||||
} | |||||
export function getElementContentWidth(element) { | |||||
var styles = window.getComputedStyle(element); | |||||
var padding = parseFloat(styles.paddingLeft) + | |||||
parseFloat(styles.paddingRight); | |||||
return element.clientWidth - padding; | |||||
} | |||||
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); | |||||
}); | |||||
} | |||||
} | |||||
} | |||||
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); | |||||
}); | |||||
} | |||||
} | |||||
} | |||||
export function fire(target, type, properties) { | |||||
var evt = document.createEvent("HTMLEvents"); | |||||
evt.initEvent(type, true, true ); | |||||
for (var j in properties) { | |||||
evt[j] = properties[j]; | |||||
} | |||||
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); | |||||
} | |||||
} | |||||
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); | |||||
}); | |||||
$child.classList.add(activeClass); | |||||
} |
@@ -0,0 +1,99 @@ | |||||
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; | |||||
} | |||||
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 truncateString(txt, len) { | |||||
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; | |||||
} | |||||
// 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]; | |||||
} | |||||
// 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 controlPoint = (current, previous, next, reverse) => { | |||||
let p = previous || current; | |||||
let n = next || current; | |||||
let o = line(p, n); | |||||
let angle = o.angle + (reverse ? Math.PI : 0); | |||||
let length = o.length * smoothing; | |||||
let x = current[0] + Math.cos(angle) * length; | |||||
let y = current[1] + Math.sin(angle) * length; | |||||
return [x, y]; | |||||
}; | |||||
let bezierCommand = (point, i, a) => { | |||||
let cps = controlPoint(a[i - 1], a[i - 2], point); | |||||
let cpe = controlPoint(point, a[i - 1], a[i + 1], true); | |||||
return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`; | |||||
}; | |||||
let pointStr = (points, command) => { | |||||
return points.reduce((acc, point, i, a) => i === 0 | |||||
? `${point[0]},${point[1]}` | |||||
: `${acc} ${command(point, i, a)}`, ''); | |||||
}; | |||||
return pointStr(points, bezierCommand); | |||||
} |
@@ -0,0 +1,731 @@ | |||||
import { getBarHeightAndYAttr, truncateString, shortenLargeNumber, getSplineCurvePointsStr } from './draw-utils'; | |||||
import { getStringWidth, isValidNumber } from './helpers'; | |||||
import { DOT_OVERLAY_SIZE_INCR, PERCENTAGE_BAR_DEFAULT_DEPTH } from './constants'; | |||||
import { lightenDarkenColor } from './colors'; | |||||
export const AXIS_TICK_LENGTH = 6; | |||||
const LABEL_MARGIN = 4; | |||||
const LABEL_MAX_CHARS = 15; | |||||
export const FONT_SIZE = 10; | |||||
const BASE_LINE_COLOR = '#dadada'; | |||||
const FONT_FILL = '#555b51'; | |||||
function $(expr, con) { | |||||
return typeof expr === "string"? (con || document).querySelector(expr) : expr || null; | |||||
} | |||||
export function createSVG(tag, o) { | |||||
var element = document.createElementNS("http://www.w3.org/2000/svg", tag); | |||||
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 === "className") { i = "class"; } | |||||
if(i === "innerHTML") { | |||||
element['textContent'] = val; | |||||
} else { | |||||
element.setAttribute(i, val); | |||||
} | |||||
} | |||||
} | |||||
return element; | |||||
} | |||||
function renderVerticalGradient(svgDefElem, gradientId) { | |||||
return createSVG('linearGradient', { | |||||
inside: svgDefElem, | |||||
id: gradientId, | |||||
x1: 0, | |||||
x2: 0, | |||||
y1: 0, | |||||
y2: 1 | |||||
}); | |||||
} | |||||
function setGradientStop(gradElem, offset, color, opacity) { | |||||
return createSVG('stop', { | |||||
'inside': gradElem, | |||||
'style': `stop-color: ${color}`, | |||||
'offset': offset, | |||||
'stop-opacity': opacity | |||||
}); | |||||
} | |||||
export function makeSVGContainer(parent, className, width, height) { | |||||
return createSVG('svg', { | |||||
className: className, | |||||
inside: parent, | |||||
width: width, | |||||
height: height | |||||
}); | |||||
} | |||||
export function makeSVGDefs(svgContainer) { | |||||
return createSVG('defs', { | |||||
inside: svgContainer, | |||||
}); | |||||
} | |||||
export function makeSVGGroup(className, transform='', parent=undefined) { | |||||
let args = { | |||||
className: className, | |||||
transform: transform | |||||
}; | |||||
if(parent) args.inside = parent; | |||||
return createSVG('g', args); | |||||
} | |||||
export function wrapInSVGGroup(elements, className='') { | |||||
let g = createSVG('g', { | |||||
className: className | |||||
}); | |||||
elements.forEach(e => g.appendChild(e)); | |||||
return g; | |||||
} | |||||
export function makePath(pathStr, className='', stroke='none', fill='none', strokeWidth=2) { | |||||
return createSVG('path', { | |||||
className: className, | |||||
d: pathStr, | |||||
styles: { | |||||
stroke: stroke, | |||||
fill: fill, | |||||
'stroke-width': strokeWidth | |||||
} | |||||
}); | |||||
} | |||||
export function makeArcPathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){ | |||||
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y]; | |||||
let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y]; | |||||
return `M${center.x} ${center.y} | |||||
L${arcStartX} ${arcStartY} | |||||
A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0} | |||||
${arcEndX} ${arcEndY} z`; | |||||
} | |||||
export function makeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){ | |||||
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y]; | |||||
let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, center.y * 2, center.y + endPosition.y]; | |||||
return `M${center.x} ${center.y} | |||||
L${arcStartX} ${arcStartY} | |||||
A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0} | |||||
${arcEndX} ${midArc} z | |||||
L${arcStartX} ${midArc} | |||||
A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0} | |||||
${arcEndX} ${arcEndY} z`; | |||||
} | |||||
export function makeArcStrokePathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){ | |||||
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y]; | |||||
let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y]; | |||||
return `M${arcStartX} ${arcStartY} | |||||
A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0} | |||||
${arcEndX} ${arcEndY}`; | |||||
} | |||||
export function makeStrokeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){ | |||||
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y]; | |||||
let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, radius * 2 + arcStartY, center.y + startPosition.y]; | |||||
return `M${arcStartX} ${arcStartY} | |||||
A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0} | |||||
${arcEndX} ${midArc} | |||||
M${arcStartX} ${midArc} | |||||
A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0} | |||||
${arcEndX} ${arcEndY}`; | |||||
} | |||||
export function makeGradient(svgDefElem, color, lighter = false) { | |||||
let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default'); | |||||
let gradientDef = renderVerticalGradient(svgDefElem, gradientId); | |||||
let opacities = [1, 0.6, 0.2]; | |||||
if(lighter) { | |||||
opacities = [0.4, 0.2, 0]; | |||||
} | |||||
setGradientStop(gradientDef, "0%", color, opacities[0]); | |||||
setGradientStop(gradientDef, "50%", color, opacities[1]); | |||||
setGradientStop(gradientDef, "100%", color, opacities[2]); | |||||
return gradientId; | |||||
} | |||||
export function percentageBar(x, y, width, height, | |||||
depth=PERCENTAGE_BAR_DEFAULT_DEPTH, fill='none') { | |||||
let args = { | |||||
className: 'percentage-bar', | |||||
x: x, | |||||
y: y, | |||||
width: width, | |||||
height: height, | |||||
fill: fill, | |||||
styles: { | |||||
'stroke': lightenDarkenColor(fill, -25), | |||||
// Diabolically good: https://stackoverflow.com/a/9000859 | |||||
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray | |||||
'stroke-dasharray': `0, ${height + width}, ${width}, ${height}`, | |||||
'stroke-width': depth | |||||
}, | |||||
}; | |||||
return createSVG("rect", args); | |||||
} | |||||
export function heatSquare(className, x, y, size, radius, fill='none', data={}) { | |||||
let args = { | |||||
className: className, | |||||
x: x, | |||||
y: y, | |||||
width: size, | |||||
height: size, | |||||
rx: radius, | |||||
fill: fill | |||||
}; | |||||
Object.keys(data).map(key => { | |||||
args[key] = data[key]; | |||||
}); | |||||
return createSVG("rect", args); | |||||
} | |||||
export function legendBar(x, y, size, fill='none', label, truncate=false) { | |||||
label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label; | |||||
let args = { | |||||
className: 'legend-bar', | |||||
x: 0, | |||||
y: 0, | |||||
width: size, | |||||
height: '2px', | |||||
fill: fill | |||||
}; | |||||
let text = createSVG('text', { | |||||
className: 'legend-dataset-text', | |||||
x: 0, | |||||
y: 0, | |||||
dy: (FONT_SIZE * 2) + 'px', | |||||
'font-size': (FONT_SIZE * 1.2) + 'px', | |||||
'text-anchor': 'start', | |||||
fill: FONT_FILL, | |||||
innerHTML: label | |||||
}); | |||||
let group = createSVG('g', { | |||||
transform: `translate(${x}, ${y})` | |||||
}); | |||||
group.appendChild(createSVG("rect", args)); | |||||
group.appendChild(text); | |||||
return group; | |||||
} | |||||
export function legendDot(x, y, size, fill='none', label, truncate=false) { | |||||
label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label; | |||||
let args = { | |||||
className: 'legend-dot', | |||||
cx: 0, | |||||
cy: 0, | |||||
r: size, | |||||
fill: fill | |||||
}; | |||||
let text = createSVG('text', { | |||||
className: 'legend-dataset-text', | |||||
x: 0, | |||||
y: 0, | |||||
dx: (FONT_SIZE) + 'px', | |||||
dy: (FONT_SIZE/3) + 'px', | |||||
'font-size': (FONT_SIZE * 1.2) + 'px', | |||||
'text-anchor': 'start', | |||||
fill: FONT_FILL, | |||||
innerHTML: label | |||||
}); | |||||
let group = createSVG('g', { | |||||
transform: `translate(${x}, ${y})` | |||||
}); | |||||
group.appendChild(createSVG("circle", args)); | |||||
group.appendChild(text); | |||||
return group; | |||||
} | |||||
export function makeText(className, x, y, content, options = {}) { | |||||
let fontSize = options.fontSize || FONT_SIZE; | |||||
let dy = options.dy !== undefined ? options.dy : (fontSize / 2); | |||||
let fill = options.fill || FONT_FILL; | |||||
let textAnchor = options.textAnchor || 'start'; | |||||
return createSVG('text', { | |||||
className: className, | |||||
x: x, | |||||
y: y, | |||||
dy: dy + 'px', | |||||
'font-size': fontSize + 'px', | |||||
fill: fill, | |||||
'text-anchor': textAnchor, | |||||
innerHTML: content | |||||
}); | |||||
} | |||||
function makeVertLine(x, label, y1, y2, options={}) { | |||||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | |||||
let l = createSVG('line', { | |||||
className: 'line-vertical ' + options.className, | |||||
x1: 0, | |||||
x2: 0, | |||||
y1: y1, | |||||
y2: y2, | |||||
styles: { | |||||
stroke: options.stroke | |||||
} | |||||
}); | |||||
let text = createSVG('text', { | |||||
x: 0, | |||||
y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE, | |||||
dy: FONT_SIZE + 'px', | |||||
'font-size': FONT_SIZE + 'px', | |||||
'text-anchor': 'middle', | |||||
innerHTML: label + "" | |||||
}); | |||||
let line = createSVG('g', { | |||||
transform: `translate(${ x }, 0)` | |||||
}); | |||||
line.appendChild(l); | |||||
line.appendChild(text); | |||||
return line; | |||||
} | |||||
function makeHoriLine(y, label, x1, x2, options={}) { | |||||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | |||||
if(!options.lineType) options.lineType = ''; | |||||
if (options.shortenNumbers) label = shortenLargeNumber(label); | |||||
let className = 'line-horizontal ' + options.className + | |||||
(options.lineType === "dashed" ? "dashed": ""); | |||||
let l = createSVG('line', { | |||||
className: className, | |||||
x1: x1, | |||||
x2: x2, | |||||
y1: 0, | |||||
y2: 0, | |||||
styles: { | |||||
stroke: options.stroke | |||||
} | |||||
}); | |||||
let text = createSVG('text', { | |||||
x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN, | |||||
y: 0, | |||||
dy: (FONT_SIZE / 2 - 2) + 'px', | |||||
'font-size': FONT_SIZE + 'px', | |||||
'text-anchor': x1 < x2 ? 'end' : 'start', | |||||
innerHTML: label+"" | |||||
}); | |||||
let line = createSVG('g', { | |||||
transform: `translate(0, ${y})`, | |||||
'stroke-opacity': 1 | |||||
}); | |||||
if(text === 0 || text === '0') { | |||||
line.style.stroke = "rgba(27, 31, 35, 0.6)"; | |||||
} | |||||
line.appendChild(l); | |||||
line.appendChild(text); | |||||
return line; | |||||
} | |||||
export function yLine(y, label, width, options={}) { | |||||
if (!isValidNumber(y)) y = 0; | |||||
if(!options.pos) options.pos = 'left'; | |||||
if(!options.offset) options.offset = 0; | |||||
if(!options.mode) options.mode = 'span'; | |||||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | |||||
if(!options.className) options.className = ''; | |||||
let x1 = -1 * AXIS_TICK_LENGTH; | |||||
let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0; | |||||
if(options.mode === 'tick' && options.pos === 'right') { | |||||
x1 = width + AXIS_TICK_LENGTH; | |||||
x2 = width; | |||||
} | |||||
// let offset = options.pos === 'left' ? -1 * options.offset : options.offset; | |||||
x1 += options.offset; | |||||
x2 += options.offset; | |||||
return makeHoriLine(y, label, x1, x2, { | |||||
stroke: options.stroke, | |||||
className: options.className, | |||||
lineType: options.lineType, | |||||
shortenNumbers: options.shortenNumbers | |||||
}); | |||||
} | |||||
export function xLine(x, label, height, options={}) { | |||||
if (!isValidNumber(x)) x = 0; | |||||
if(!options.pos) options.pos = 'bottom'; | |||||
if(!options.offset) options.offset = 0; | |||||
if(!options.mode) options.mode = 'span'; | |||||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | |||||
if(!options.className) options.className = ''; | |||||
// Draw X axis line in span/tick mode with optional label | |||||
// y2(span) | |||||
// | | |||||
// | | |||||
// x line | | |||||
// | | |||||
// | | |||||
// ---------------------+-- y2(tick) | |||||
// | | |||||
// y1 | |||||
let y1 = height + AXIS_TICK_LENGTH; | |||||
let y2 = options.mode === 'span' ? -1 * AXIS_TICK_LENGTH : height; | |||||
if(options.mode === 'tick' && options.pos === 'top') { | |||||
// top axis ticks | |||||
y1 = -1 * AXIS_TICK_LENGTH; | |||||
y2 = 0; | |||||
} | |||||
return makeVertLine(x, label, y1, y2, { | |||||
stroke: options.stroke, | |||||
className: options.className, | |||||
lineType: options.lineType | |||||
}); | |||||
} | |||||
export function yMarker(y, label, width, options={}) { | |||||
if(!options.labelPos) options.labelPos = 'right'; | |||||
let x = options.labelPos === 'left' ? LABEL_MARGIN | |||||
: width - getStringWidth(label, 5) - LABEL_MARGIN; | |||||
let labelSvg = createSVG('text', { | |||||
className: 'chart-label', | |||||
x: x, | |||||
y: 0, | |||||
dy: (FONT_SIZE / -2) + 'px', | |||||
'font-size': FONT_SIZE + 'px', | |||||
'text-anchor': 'start', | |||||
innerHTML: label+"" | |||||
}); | |||||
let line = makeHoriLine(y, '', 0, width, { | |||||
stroke: options.stroke || BASE_LINE_COLOR, | |||||
className: options.className || '', | |||||
lineType: options.lineType | |||||
}); | |||||
line.appendChild(labelSvg); | |||||
return line; | |||||
} | |||||
export function yRegion(y1, y2, width, label, options={}) { | |||||
// return a group | |||||
let height = y1 - y2; | |||||
let rect = createSVG('rect', { | |||||
className: `bar mini`, // remove class | |||||
styles: { | |||||
fill: `rgba(228, 234, 239, 0.49)`, | |||||
stroke: BASE_LINE_COLOR, | |||||
'stroke-dasharray': `${width}, ${height}` | |||||
}, | |||||
// 'data-point-index': index, | |||||
x: 0, | |||||
y: 0, | |||||
width: width, | |||||
height: height | |||||
}); | |||||
if(!options.labelPos) options.labelPos = 'right'; | |||||
let x = options.labelPos === 'left' ? LABEL_MARGIN | |||||
: width - getStringWidth(label+"", 4.5) - LABEL_MARGIN; | |||||
let labelSvg = createSVG('text', { | |||||
className: 'chart-label', | |||||
x: x, | |||||
y: 0, | |||||
dy: (FONT_SIZE / -2) + 'px', | |||||
'font-size': FONT_SIZE + 'px', | |||||
'text-anchor': 'start', | |||||
innerHTML: label+"" | |||||
}); | |||||
let region = createSVG('g', { | |||||
transform: `translate(0, ${y2})` | |||||
}); | |||||
region.appendChild(rect); | |||||
region.appendChild(labelSvg); | |||||
return region; | |||||
} | |||||
export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) { | |||||
let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine); | |||||
y -= offset; | |||||
if(height === 0) { | |||||
height = meta.minHeight; | |||||
y -= meta.minHeight; | |||||
} | |||||
// Preprocess numbers to avoid svg building errors | |||||
if (!isValidNumber(x)) x = 0; | |||||
if (!isValidNumber(y)) y = 0; | |||||
if (!isValidNumber(height, true)) height = 0; | |||||
if (!isValidNumber(width, true)) width = 0; | |||||
let rect = createSVG('rect', { | |||||
className: `bar mini`, | |||||
style: `fill: ${color}`, | |||||
'data-point-index': index, | |||||
x: x, | |||||
y: y, | |||||
width: width, | |||||
height: height | |||||
}); | |||||
label += ""; | |||||
if(!label && !label.length) { | |||||
return rect; | |||||
} else { | |||||
rect.setAttribute('y', 0); | |||||
rect.setAttribute('x', 0); | |||||
let text = createSVG('text', { | |||||
className: 'data-point-value', | |||||
x: width/2, | |||||
y: 0, | |||||
dy: (FONT_SIZE / 2 * -1) + 'px', | |||||
'font-size': FONT_SIZE + 'px', | |||||
'text-anchor': 'middle', | |||||
innerHTML: label | |||||
}); | |||||
let group = createSVG('g', { | |||||
'data-point-index': index, | |||||
transform: `translate(${x}, ${y})` | |||||
}); | |||||
group.appendChild(rect); | |||||
group.appendChild(text); | |||||
return group; | |||||
} | |||||
} | |||||
export function datasetDot(x, y, radius, color, label='', index=0) { | |||||
let dot = createSVG('circle', { | |||||
style: `fill: ${color}`, | |||||
'data-point-index': index, | |||||
cx: x, | |||||
cy: y, | |||||
r: radius | |||||
}); | |||||
label += ""; | |||||
if(!label && !label.length) { | |||||
return dot; | |||||
} else { | |||||
dot.setAttribute('cy', 0); | |||||
dot.setAttribute('cx', 0); | |||||
let text = createSVG('text', { | |||||
className: 'data-point-value', | |||||
x: 0, | |||||
y: 0, | |||||
dy: (FONT_SIZE / 2 * -1 - radius) + 'px', | |||||
'font-size': FONT_SIZE + 'px', | |||||
'text-anchor': 'middle', | |||||
innerHTML: label | |||||
}); | |||||
let group = createSVG('g', { | |||||
'data-point-index': index, | |||||
transform: `translate(${x}, ${y})` | |||||
}); | |||||
group.appendChild(dot); | |||||
group.appendChild(text); | |||||
return group; | |||||
} | |||||
} | |||||
export function getPaths(xList, yList, color, options={}, meta={}) { | |||||
let pointsList = yList.map((y, i) => (xList[i] + ',' + y)); | |||||
let pointsStr = pointsList.join("L"); | |||||
// Spline | |||||
if (options.spline) | |||||
pointsStr = getSplineCurvePointsStr(xList, yList); | |||||
let path = makePath("M"+pointsStr, 'line-graph-path', color); | |||||
// HeatLine | |||||
if(options.heatline) { | |||||
let gradient_id = makeGradient(meta.svgDefs, color); | |||||
path.style.stroke = `url(#${gradient_id})`; | |||||
} | |||||
let paths = { | |||||
path: path | |||||
}; | |||||
// Region | |||||
if(options.regionFill) { | |||||
let gradient_id_region = makeGradient(meta.svgDefs, color, true); | |||||
let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`; | |||||
paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`); | |||||
} | |||||
return paths; | |||||
} | |||||
export let makeOverlay = { | |||||
'bar': (unit) => { | |||||
let transformValue; | |||||
if(unit.nodeName !== 'rect') { | |||||
transformValue = unit.getAttribute('transform'); | |||||
unit = unit.childNodes[0]; | |||||
} | |||||
let overlay = unit.cloneNode(); | |||||
overlay.style.fill = '#000000'; | |||||
overlay.style.opacity = '0.4'; | |||||
if(transformValue) { | |||||
overlay.setAttribute('transform', transformValue); | |||||
} | |||||
return overlay; | |||||
}, | |||||
'dot': (unit) => { | |||||
let transformValue; | |||||
if(unit.nodeName !== 'circle') { | |||||
transformValue = unit.getAttribute('transform'); | |||||
unit = unit.childNodes[0]; | |||||
} | |||||
let overlay = unit.cloneNode(); | |||||
let radius = unit.getAttribute('r'); | |||||
let fill = unit.getAttribute('fill'); | |||||
overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR); | |||||
overlay.setAttribute('fill', fill); | |||||
overlay.style.opacity = '0.6'; | |||||
if(transformValue) { | |||||
overlay.setAttribute('transform', transformValue); | |||||
} | |||||
return overlay; | |||||
}, | |||||
'heat_square': (unit) => { | |||||
let transformValue; | |||||
if(unit.nodeName !== 'circle') { | |||||
transformValue = unit.getAttribute('transform'); | |||||
unit = unit.childNodes[0]; | |||||
} | |||||
let overlay = unit.cloneNode(); | |||||
let radius = unit.getAttribute('r'); | |||||
let fill = unit.getAttribute('fill'); | |||||
overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR); | |||||
overlay.setAttribute('fill', fill); | |||||
overlay.style.opacity = '0.6'; | |||||
if(transformValue) { | |||||
overlay.setAttribute('transform', transformValue); | |||||
} | |||||
return overlay; | |||||
} | |||||
}; | |||||
export let updateOverlay = { | |||||
'bar': (unit, overlay) => { | |||||
let transformValue; | |||||
if(unit.nodeName !== 'rect') { | |||||
transformValue = unit.getAttribute('transform'); | |||||
unit = unit.childNodes[0]; | |||||
} | |||||
let attributes = ['x', 'y', 'width', 'height']; | |||||
Object.values(unit.attributes) | |||||
.filter(attr => attributes.includes(attr.name) && attr.specified) | |||||
.map(attr => { | |||||
overlay.setAttribute(attr.name, attr.nodeValue); | |||||
}); | |||||
if(transformValue) { | |||||
overlay.setAttribute('transform', transformValue); | |||||
} | |||||
}, | |||||
'dot': (unit, overlay) => { | |||||
let transformValue; | |||||
if(unit.nodeName !== 'circle') { | |||||
transformValue = unit.getAttribute('transform'); | |||||
unit = unit.childNodes[0]; | |||||
} | |||||
let attributes = ['cx', 'cy']; | |||||
Object.values(unit.attributes) | |||||
.filter(attr => attributes.includes(attr.name) && attr.specified) | |||||
.map(attr => { | |||||
overlay.setAttribute(attr.name, attr.nodeValue); | |||||
}); | |||||
if(transformValue) { | |||||
overlay.setAttribute('transform', transformValue); | |||||
} | |||||
}, | |||||
'heat_square': (unit, overlay) => { | |||||
let transformValue; | |||||
if(unit.nodeName !== 'circle') { | |||||
transformValue = unit.getAttribute('transform'); | |||||
unit = unit.childNodes[0]; | |||||
} | |||||
let attributes = ['cx', 'cy']; | |||||
Object.values(unit.attributes) | |||||
.filter(attr => attributes.includes(attr.name) && attr.specified) | |||||
.map(attr => { | |||||
overlay.setAttribute(attr.name, attr.nodeValue); | |||||
}); | |||||
if(transformValue) { | |||||
overlay.setAttribute('transform', transformValue); | |||||
} | |||||
}, | |||||
}; |
@@ -0,0 +1,33 @@ | |||||
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); | |||||
} | |||||
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 container = $.create('div'); | |||||
container.appendChild(clone); | |||||
return container.innerHTML; | |||||
} |
@@ -0,0 +1,143 @@ | |||||
import { ANGLE_RATIO } from './constants'; | |||||
/** | |||||
* Returns the value of a number upto 2 decimal places. | |||||
* @param {Number} d Any number | |||||
*/ | |||||
export function floatTwo(d) { | |||||
return parseFloat(d.toFixed(2)); | |||||
} | |||||
/** | |||||
* Returns whether or not two given arrays are equal. | |||||
* @param {Array} arr1 First array | |||||
* @param {Array} arr2 Second array | |||||
*/ | |||||
export function arraysEqual(arr1, arr2) { | |||||
if (arr1.length !== arr2.length) return false; | |||||
let areEqual = true; | |||||
arr1.map((d, i) => { | |||||
if (arr2[i] !== d) areEqual = false; | |||||
}); | |||||
return areEqual; | |||||
} | |||||
/** | |||||
* Shuffles array in place. ES6 version | |||||
* @param {Array} array An array containing the items. | |||||
*/ | |||||
export function shuffle(array) { | |||||
// Awesomeness: https://bost.ocks.org/mike/shuffle/ | |||||
// https://stackoverflow.com/a/2450976/6495043 | |||||
// https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array?noredirect=1&lq=1 | |||||
for (let i = array.length - 1; i > 0; i--) { | |||||
let j = Math.floor(Math.random() * (i + 1)); | |||||
[array[i], array[j]] = [array[j], array[i]]; | |||||
} | |||||
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? | |||||
*/ | |||||
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); | |||||
array = start ? fillerArray.concat(array) : array.concat(fillerArray); | |||||
return array; | |||||
} | |||||
/** | |||||
* Returns pixel width of string. | |||||
* @param {String} string | |||||
* @param {Number} charWidth Width of single char in pixels | |||||
*/ | |||||
export function getStringWidth(string, charWidth) { | |||||
return (string + "").length * charWidth; | |||||
} | |||||
export function bindChange(obj, getFn, setFn) { | |||||
return new Proxy(obj, { | |||||
set: function (target, prop, value) { | |||||
setFn(); | |||||
return Reflect.set(target, prop, value); | |||||
}, | |||||
get: function (target, prop) { | |||||
getFn(); | |||||
return Reflect.get(target, prop); | |||||
} | |||||
}); | |||||
} | |||||
// https://stackoverflow.com/a/29325222 | |||||
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 | |||||
} | |||||
export function getPositionByAngle(angle, radius) { | |||||
return { | |||||
x: Math.sin(angle * ANGLE_RATIO) * 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; | |||||
} |
@@ -0,0 +1,247 @@ | |||||
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 | |||||
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); | |||||
return [sig * man, exp]; | |||||
} | |||||
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; | |||||
// 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: 0 | |||||
if(range === 0) { | |||||
noOfParts = 5; | |||||
partSize = 1; | |||||
} | |||||
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; | |||||
// 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; | |||||
} | |||||
export function calcChartIntervals(values, withMinimum=false) { | |||||
//*** Where the magic happens *** | |||||
// Calculates best-fit y intervals from given values | |||||
// and returns the interval array | |||||
let maxValue = Math.max(...values); | |||||
let minValue = Math.min(...values); | |||||
// Exponent to be used for pretty print | |||||
let exponent = 0, intervals = []; // eslint-disable-line no-unused-vars | |||||
function getPositiveFirstIntervals(maxValue, absMinValue) { | |||||
let intervals = getChartIntervals(maxValue); | |||||
let intervalSize = intervals[1] - intervals[0]; | |||||
// Then unshift the negative values | |||||
let value = 0; | |||||
for(var i = 1; value < absMinValue; i++) { | |||||
value += intervalSize; | |||||
intervals.unshift((-1) * value); | |||||
} | |||||
return intervals; | |||||
} | |||||
// CASE I: Both non-negative | |||||
if(maxValue >= 0 && minValue >= 0) { | |||||
exponent = normalize(maxValue)[1]; | |||||
if(!withMinimum) { | |||||
intervals = getChartIntervals(maxValue); | |||||
} else { | |||||
intervals = getChartIntervals(maxValue, minValue); | |||||
} | |||||
} | |||||
// CASE II: Only minValue negative | |||||
else if(maxValue > 0 && minValue < 0) { | |||||
// `withMinimum` irrelevant in this case, | |||||
// We'll be handling both sides of zero separately | |||||
// (both starting from zero) | |||||
// Because ceil() and floor() behave differently | |||||
// in those two regions | |||||
let absMinValue = Math.abs(minValue); | |||||
if(maxValue >= absMinValue) { | |||||
exponent = normalize(maxValue)[1]; | |||||
intervals = getPositiveFirstIntervals(maxValue, absMinValue); | |||||
} else { | |||||
// Mirror: maxValue => absMinValue, then change sign | |||||
exponent = normalize(absMinValue)[1]; | |||||
let posIntervals = getPositiveFirstIntervals(absMinValue, maxValue); | |||||
intervals = posIntervals.reverse().map(d => d * (-1)); | |||||
} | |||||
} | |||||
// CASE III: Both non-positive | |||||
else if(maxValue <= 0 && minValue <= 0) { | |||||
// Mirrored Case I: | |||||
// Work with positives, then reverse the sign and array | |||||
let pseudoMaxValue = Math.abs(minValue); | |||||
let pseudoMinValue = Math.abs(maxValue); | |||||
exponent = normalize(pseudoMaxValue)[1]; | |||||
if(!withMinimum) { | |||||
intervals = getChartIntervals(pseudoMaxValue); | |||||
} else { | |||||
intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue); | |||||
} | |||||
intervals = intervals.reverse().map(d => d * (-1)); | |||||
} | |||||
return intervals.sort((a, b) => (a - b)); | |||||
} | |||||
export function getZeroIndex(yPts) { | |||||
let zeroIndex; | |||||
let interval = getIntervalSize(yPts); | |||||
if(yPts.indexOf(0) >= 0) { | |||||
// the range has a given zero | |||||
// zero-line on the chart | |||||
zeroIndex = yPts.indexOf(0); | |||||
} else if(yPts[0] > 0) { | |||||
// Minimum value is positive | |||||
// zero-line is off the chart: below | |||||
let min = yPts[0]; | |||||
zeroIndex = (-1) * min / interval; | |||||
} else { | |||||
// Maximum value is negative | |||||
// zero-line is off the chart: above | |||||
let max = yPts[yPts.length - 1]; | |||||
zeroIndex = (-1) * max / interval + (yPts.length - 1); | |||||
} | |||||
return zeroIndex; | |||||
} | |||||
export function getRealIntervals(max, noOfIntervals, min = 0, asc = 1) { | |||||
let range = max - min; | |||||
let part = range * 1.0 / noOfIntervals; | |||||
let intervals = []; | |||||
for(var i = 0; i <= noOfIntervals; i++) { | |||||
intervals.push(min + part * i); | |||||
} | |||||
return asc ? intervals : intervals.reverse(); | |||||
} | |||||
export function getIntervalSize(orderedArray) { | |||||
return orderedArray[1] - orderedArray[0]; | |||||
} | |||||
export function getValueRange(orderedArray) { | |||||
return orderedArray[orderedArray.length-1] - orderedArray[0]; | |||||
} | |||||
export function scale(val, yAxis) { | |||||
return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier); | |||||
} | |||||
export function isInRange(val, min, 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]); | |||||
} | |||||
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); | |||||
}, []); | |||||
return index ? arr.indexOf(closest) : closest; | |||||
} | |||||
export function calcDistribution(values, distributionSize) { | |||||
// Assume non-negative values, | |||||
// implying distribution minimum at zero | |||||
let dataMaxValue = Math.max(...values); | |||||
let distributionStep = 1 / (distributionSize - 1); | |||||
let distribution = []; | |||||
for(var i = 0; i < distributionSize; i++) { | |||||
let checkpoint = dataMaxValue * (distributionStep * i); | |||||
distribution.push(checkpoint); | |||||
} | |||||
return distribution; | |||||
} | |||||
export function getMaxCheckpoint(value, distribution) { | |||||
return distribution.filter(d => d < value).length; | |||||
} |
@@ -0,0 +1,14 @@ | |||||
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'); | |||||
}); | |||||
}); |
@@ -0,0 +1,10 @@ | |||||
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.00); | |||||
}); | |||||
}); |