Repository: frappe/charts
Branch: master
Commit: 7b15424c3af8
Files: 39
Total size: 143.9 KB
Directory structure:
gitextract_lpjitjve/
├── .babelrc
├── .eslintrc.json
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ └── npm-publish.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── package.json
├── rollup.config.js
└── src/
├── css/
│ ├── charts.scss
│ └── chartsCss.js
└── js/
├── chart.js
├── charts/
│ ├── AggregationChart.js
│ ├── AxisChart.js
│ ├── BaseChart.js
│ ├── DonutChart.js
│ ├── Heatmap.js
│ ├── PercentageChart.js
│ └── PieChart.js
├── index.js
├── objects/
│ ├── ChartComponents.js
│ └── SvgTip.js
└── utils/
├── animate.js
├── animation.js
├── axis-chart-utils.js
├── colors.js
├── constants.js
├── date-utils.js
├── dom.js
├── draw-utils.js
├── draw.js
├── export.js
├── helpers.js
├── intervals.js
└── test/
├── colors.test.js
└── helpers.test.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
},
"modules": false
}
]
]
}
================================================
FILE: .eslintrc.json
================================================
{
"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
}
}
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
#### Expected Behaviour
#### Actual Behaviour
#### Steps to Reproduce:
*
NOTE: Add a GIF/Screenshot if required.
Frappé Charts version:
Codepen / Codesandbox:
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!-- Thank you so much for contributing! We're glad to have you onboard :) -->
<!-- Please help us understand you contribution better with these details -->
###### Explanation About What Code Achieves:
<!-- Please explain why this code is necessary / what it does -->
- Explanation
###### Screenshots/GIFs:
<!-- As this is mainly a visual lib, please include a screenshot/gif if your contribution modifies on-screen components -->
- Screenshot
###### Steps To Test:
<!-- What would someone do to be able to see the effects of your code? -->
- Steps
###### TODOs:
<!-- Is there any tests or logic that isn't in the pr that you want the reviewer to know about? -->
- None
================================================
FILE: .github/workflows/npm-publish.yml
================================================
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
name: Node.js Package
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: npm ci
- run: npm build
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}}
================================================
FILE: .gitignore
================================================
# 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
docs/assets/
.DS_Store
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "6"
- "8"
before_install:
- make install
script:
- make test
after_success:
- make coveralls
================================================
FILE: LICENSE
================================================
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.
================================================
FILE: Makefile
================================================
-include .env
BASEDIR = $(realpath .)
SRCDIR = $(BASEDIR)/src
DISTDIR = $(BASEDIR)/dist
DOCSDIR = $(BASEDIR)/docs
PROJECT = frappe-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
================================================
FILE: README.md
================================================
<div align="center" markdown="1">
<img width="80" alt="charts-logo" src="https://github.com/user-attachments/assets/37b7ffaf-8354-48f2-8b9c-fa04fae0135b" />
# Frappe Charts
**GitHub-inspired modern, intuitive and responsive charts with zero dependencies**
<p align="center">
<a href="https://bundlephobia.com/result?p=frappe-charts">
<img src="https://img.shields.io/bundlephobia/minzip/frappe-charts">
</a>
</p>
<img src=".github/example.gif">
<div>
[Explore Demos](https://frappe.io/charts) - [Edit at CodeSandbox](https://codesandbox.io/s/frappe-charts-demo-viqud) - [Documentation](https://frappe.io/charts/docs)
</div>
</div>
## Frappe Charts
Frappe Charts is a simple charting library with a focus on a simple API. The design is inspired by various charts you see on GitHub.
### Motivation
ERPNext needed a simple sales history graph for its user company master to help users track sales. While using c3.js for reports, the library didn’t align well with our product’s classic design. Existing JS libraries were either too complex or rigid in their structure and behavior. To address this, I decided to create a library for translating value pairs into relative shapes or positions, focusing on simplicity.
### Key Features
- **Variety of chart types**: Frappe Charts supports various chart types, including Axis Charts, Area and Trends, Bar, Line, Pie, Percentage, Mixed Axis, and Heatmap.
- **Annotations and tooltips**: Charts can be annotated with x and y markers, regions, and tooltips for enhanced data context and clarity.
- **Dynamic data handling**: Add, remove, or update individual data points in place, or refresh the entire dataset to reflect changes.
- **Customizable configurations**: Flexible options like colors, animations, and custom titles allow for a highly personalized chart experience.
## Usage
```sh
npm install frappe-charts
```
Import in your project:
```js
import { Chart } from 'frappe-charts'
// or esm import
import { Chart } from 'frappe-charts/dist/frappe-charts.esm.js'
// import css
import 'frappe-charts/dist/frappe-charts.min.css'
```
Or directly include script in your HTML
```html
<script src="https://unpkg.com/frappe-charts@1.6.1/dist/frappe-charts.min.umd.js"></script>
```
```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 frappe.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']
})
```
## Contributing
1. Clone this repo.
2. `cd` into project directory
3. `npm install`
4. `npm i npm-run-all -D` (*optional --> might be required for some developers*)
5. `npm run dev`
## Links
- [Read the blog](https://medium.com/@pratu16x7/so-we-decided-to-create-our-own-charts-a95cb5032c97)
<br>
<br>
<div align="center">
<a href="https://frappe.io" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/Frappe-white.png">
<img src="https://frappe.io/files/Frappe-black.png" alt="Frappe Technologies" height="28"/>
</picture>
</a>
</div>
================================================
FILE: package.json
================================================
{
"name": "frappe-charts",
"version": "v1.6.3",
"type": "module",
"main": "dist/frappe-charts.esm.js",
"module": "dist/frappe-charts.esm.js",
"browser": "dist/frappe-charts.umd.js",
"common": "dist/frappe-charts.cjs.js",
"unnpkg": "dist/frappe-charts.umd.js",
"description": "https://frappe.github.io/charts",
"directories": {
"doc": "docs"
},
"files": [
"src",
"dist"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "rollup -c --watch",
"dev": "npm-run-all --parallel watch",
"build": "rollup -c"
},
"repository": {
"type": "git",
"url": "git+https://github.com/frappe/charts.git"
},
"keywords": [
"js",
"charts"
],
"author": "Prateeksha Singh",
"license": "MIT",
"bugs": {
"url": "https://github.com/frappe/charts/issues"
},
"homepage": "https://github.com/frappe/charts#readme",
"devDependencies": {
"@babel/core": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"node-sass": "^8.0.0",
"rollup": "^2.21.0",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-bundle-size": "^1.0.3",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-eslint": "^7.0.0",
"rollup-plugin-postcss": "^3.1.3",
"rollup-plugin-scss": "^2.5.0",
"rollup-plugin-terser": "^6.1.0"
}
}
================================================
FILE: rollup.config.js
================================================
import pkg from "./package.json";
import commonjs from "rollup-plugin-commonjs";
import babel from "rollup-plugin-babel";
import postcss from "rollup-plugin-postcss";
import scss from "rollup-plugin-scss";
import bundleSize from "rollup-plugin-bundle-size";
import { terser } from "rollup-plugin-terser";
export default [
// browser-friendly UMD build
{
input: "src/js/index.js",
output: {
sourcemap: true,
name: "frappe",
file: pkg.browser,
format: "umd",
},
plugins: [
commonjs(),
babel({
exclude: ["node_modules/**"],
}),
terser(),
scss({ output: "dist/frappe-charts.min.css" }),
bundleSize(),
],
},
// CommonJS (for Node) and ES module (for bundlers) build.
{
input: "src/js/chart.js",
output: [
{ file: pkg.common, format: "cjs", sourcemap: true },
{ file: pkg.module, format: "es", sourcemap: true },
],
plugins: [
babel({
exclude: ["node_modules/**"],
}),
terser(),
postcss(),
bundleSize(),
],
},
];
================================================
FILE: src/css/charts.scss
================================================
:root {
--charts-label-color: #313b44;
--charts-axis-line-color: #f4f5f6;
--charts-tooltip-title: var(--charts-label-color);
--charts-tooltip-label: var(--charts-label-color);
--charts-tooltip-value: #192734;
--charts-tooltip-bg: #ffffff;
--charts-stroke-width: 2px;
--charts-dataset-circle-stroke: #ffffff;
--charts-dataset-circle-stroke-width: var(--charts-stroke-width);
--charts-legend-label: var(--charts-label-color);
--charts-legend-value: var(--charts-label-color);
}
.chart-container {
position: relative;
/* for absolutely positioned tooltip */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
.axis,
.chart-label {
fill: var(--charts-label-color);
line {
stroke: var(--charts-axis-line-color);
}
}
.dataset-units {
circle {
stroke: var(--charts-dataset-circle-stroke);
stroke-width: var(--charts-dataset-circle-stroke-width);
}
path {
fill: none;
stroke-opacity: 1;
stroke-width: var(--charts-stroke-width);
}
}
.dataset-path {
stroke-width: var(--charts-stroke-width);
}
.path-group {
path {
fill: none;
stroke-opacity: 1;
stroke-width: var(--charts-stroke-width);
}
}
line.dashed {
stroke-dasharray: 5, 3;
}
.axis-line {
.specific-value {
text-anchor: start;
}
.y-line {
text-anchor: end;
}
.x-line {
text-anchor: middle;
}
}
.legend-dataset-label {
fill: var(--charts-legend-label);
font-weight: 600;
}
.legend-dataset-value {
fill: var(--charts-legend-value);
}
}
.graph-svg-tip {
position: absolute;
z-index: 99999;
padding: 10px;
font-size: 12px;
text-align: center;
background: var(--charts-tooltip-bg);
box-shadow: 0px 1px 4px rgba(17, 43, 66, 0.1),
0px 2px 6px rgba(17, 43, 66, 0.08),
0px 40px 30px -30px rgba(17, 43, 66, 0.1);
border-radius: 6px;
ul {
padding-left: 0;
display: flex;
}
ol {
padding-left: 0;
display: flex;
}
ul.data-point-list {
li {
min-width: 90px;
font-weight: 600;
}
}
.svg-pointer {
position: absolute;
height: 12px;
width: 12px;
border-radius: 2px;
background: var(--charts-tooltip-bg);
transform: rotate(45deg);
margin-top: -7px;
margin-left: -6px;
}
&.comparison {
text-align: left;
padding: 0px;
pointer-events: none;
.title {
display: block;
padding: 16px;
margin: 0;
color: var(--charts-tooltip-title);
font-weight: 600;
line-height: 1;
pointer-events: none;
text-transform: uppercase;
strong {
color: var(--charts-tooltip-value);
}
}
ul {
margin: 0;
white-space: nowrap;
list-style: none;
&.tooltip-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 5px;
}
}
li {
display: inline-block;
display: flex;
flex-direction: row;
font-weight: 600;
line-height: 1;
padding: 5px 15px 15px 15px;
.tooltip-legend {
height: 12px;
width: 12px;
margin-right: 8px;
border-radius: 2px;
}
.tooltip-label {
margin-top: 4px;
font-size: 11px;
max-width: 100px;
color: var(--fr-tooltip-label);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tooltip-value {
color: var(--charts-tooltip-value);
}
}
}
}
================================================
FILE: src/css/chartsCss.js
================================================
export const CSSTEXT =
".chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .legend-dataset-text{fill:#6c7680;font-weight:600}.graph-svg-tip{position:absolute;z-index:99999;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ul{padding-left:0;display:flex}.graph-svg-tip ol{padding-left:0;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;flex:1;font-weight:600}.graph-svg-tip strong{color:#dfe2e5;font-weight:600}.graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:' ';border:5px solid transparent;}.graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.graph-svg-tip.comparison li{display:inline-block;padding:5px 10px}";
================================================
FILE: src/js/chart.js
================================================
import "../css/charts.scss";
import PercentageChart from "./charts/PercentageChart";
import PieChart from "./charts/PieChart";
import Heatmap from "./charts/Heatmap";
import AxisChart from "./charts/AxisChart";
import DonutChart from "./charts/DonutChart";
const chartTypes = {
bar: AxisChart,
line: AxisChart,
percentage: PercentageChart,
heatmap: Heatmap,
pie: PieChart,
donut: DonutChart,
};
function getChartByType(chartType = "line", parent, options) {
if (chartType === "axis-mixed") {
options.type = "line";
return new AxisChart(parent, options);
}
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 };
================================================
FILE: src/js/charts/AggregationChart.js
================================================
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;
this.config.legendRowHeight = 60;
}
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);
super.renderLegend(this.legendTotals);
}
makeLegend(data, index, x_pos, y_pos) {
let formatted = this.config.formatTooltipY
? this.config.formatTooltipY(data)
: data;
return legendDot(
x_pos,
y_pos,
12, // size
3, // dot radius
this.colors[index], // fill
this.state.labels[index], // label
formatted, // value
null, // base_font_size
this.config.truncateLegends // truncate_legends
);
}
}
================================================
FILE: src/js/charts/AxisChart.js
================================================
import BaseChart from "./BaseChart";
import {
dataPrep,
zeroDataPrep,
getShortenedLabels,
} from "../utils/axis-chart-utils";
import { getComponent } from "../objects/ChartComponents";
import { getOffset, fire } from "../utils/dom";
import {
calcChartIntervals,
getIntervalSize,
getValueRange,
getZeroIndex,
scale,
getClosestInArray,
} from "../utils/intervals";
import { floatTwo } from "../utils/helpers";
import { makeOverlay, updateOverlay, legendDot } from "../utils/draw";
import {
getTopOffset,
getLeftOffset,
MIN_BAR_PERCENT_HEIGHT,
BAR_CHART_SPACE_RATIO,
LINE_CHART_DOT_SIZE,
LEGEND_ITEM_WIDTH,
} from "../utils/constants";
export default class AxisChart extends BaseChart {
constructor(parent, args) {
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);
const { axisOptions = {} } = options;
const { xAxis, yAxis } = axisOptions || {};
options.tooltipOptions = options.tooltipOptions || {};
this.config.xAxisMode = xAxis
? xAxis.xAxisMode
: axisOptions.xAxisMode || "span";
// this will pass an array
// lets determine if we need two yAxis based on if there is length
// to the yAxis array
if (yAxis && yAxis.length) {
this.config.yAxisConfig = yAxis.map((item) => {
return {
yAxisMode: item.yAxisMode,
id: item.id,
position: item.position,
title: item.title,
};
});
} else {
this.config.yAxisMode = yAxis
? yAxis.yAxisMode
: axisOptions.yAxisMode || "span";
// if we have yAxis config settings lets populate a yAxis config array.
if (yAxis && yAxis.id && yAxis.position) {
this.config.yAxisConfig = [yAxis];
}
}
this.config.xIsSeries = axisOptions.xIsSeries || 0;
this.config.shortenYAxisNumbers = axisOptions.shortenYAxisNumbers || 0;
this.config.formatTooltipX = options.tooltipOptions.formatTooltipX;
this.config.formatTooltipY = options.tooltipOptions.formatTooltipY;
this.config.valuesOverPoints = options.valuesOverPoints;
this.config.legendRowHeight = 30;
}
prepareData(data = this.data, config = this.config) {
return dataPrep(data, this.type, config.continuous);
}
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") {
let yPts,
scaleMultiplier,
intervalHeight,
zeroLine,
positions,
yAxisConfigObject,
yAxisAlignment,
yKeys;
yKeys = [];
yAxisConfigObject = this.config.yAxisMode || {};
yAxisAlignment = yAxisConfigObject.position
? yAxisConfigObject.position
: "left";
// if we have an object we have multiple yAxisParameters.
if (dataValues instanceof Array) {
yPts = calcChartIntervals(dataValues, withMinimum, this.config.overrideCeiling, this.config.overrideFloor);
scaleMultiplier = this.height / getValueRange(yPts);
intervalHeight = getIntervalSize(yPts) * scaleMultiplier;
zeroLine = this.height - getZeroIndex(yPts) * intervalHeight;
this.state.yAxis = {
labels: yPts,
positions: yPts.map((d) => zeroLine - d * scaleMultiplier),
title: yAxisConfigObject.title || null,
pos: yAxisAlignment,
scaleMultiplier: scaleMultiplier,
zeroLine: zeroLine,
};
} else {
this.state.yAxis = [];
for (let key in dataValues) {
const dataValue = dataValues[key];
yAxisConfigObject =
this.config.yAxisConfig.find((item) => key === item.id) ||
[];
yAxisAlignment = yAxisConfigObject.position
? yAxisConfigObject.position
: "left";
yPts = calcChartIntervals(dataValue, withMinimum, this.config.overrideCeiling, this.config.overrideFloor);
scaleMultiplier = this.height / getValueRange(yPts);
intervalHeight = getIntervalSize(yPts) * scaleMultiplier;
zeroLine = this.height - getZeroIndex(yPts) * intervalHeight;
positions = yPts.map((d) => zeroLine - d * scaleMultiplier);
yKeys.push(key);
if (this.state.yAxis.length > 1) {
const yPtsArray = [];
const firstArr = this.state.yAxis[0];
// we need to calculate the scaleMultiplier.
// now that we have an accurate scaleMultiplier we can
// we need to loop through original positions.
scaleMultiplier = this.height / getValueRange(yPts);
firstArr.positions.forEach((pos) => {
yPtsArray.push(Math.ceil(pos / scaleMultiplier));
});
yPts = yPtsArray.reverse();
zeroLine =
this.height - getZeroIndex(yPts) * intervalHeight;
positions = firstArr.positions;
}
this.state.yAxis.push({
axisID: key || "left-axis",
labels: yPts,
title: yAxisConfigObject.title,
pos: yAxisAlignment,
scaleMultiplier,
zeroLine,
positions,
});
}
// the labels are not aligned in length between the two yAxis objects,
// we need to run some new calculations.
if (
this.state.yAxis[1] &&
this.state.yAxis[0].labels.length !==
this.state.yAxis[1].labels.length
) {
const newYptsArr = [];
// find the shorter array
const shortest = this.state.yAxis.reduce(
(p, c) => {
return p.length > c.labels.length ? c : p;
},
{ length: Infinity }
);
// return the longest
const longest = this.state.yAxis.reduce(
(p, c) => {
return p.length < c.labels.length ? p : c;
},
{ length: Infinity }
);
// we now need to populate the shortest obj with the new scale multiplier
// with the positions of the longest obj.
longest.positions.forEach((pos) => {
// calculate a new yPts
newYptsArr.push(Math.ceil(pos / shortest.scaleMultiplier));
});
shortest.labels = newYptsArr.reverse();
shortest.positions = longest.positions;
}
}
// Dependent if above changes
this.calcDatasetPoints();
this.calcYExtremes();
this.calcYRegions();
}
calcDatasetPoints() {
let s = this.state;
let scaleAll = (values, id) => {
return values.map((val) => {
let { yAxis } = s;
if (yAxis instanceof Array) {
yAxis =
yAxis.length > 1
? yAxis.find((axis) => id === axis.axisID)
: s.yAxis[0];
}
return scale(val, yAxis);
});
};
s.barChartIndex = 1;
s.datasets = this.data.datasets.map((d, i) => {
let values = d.values;
let cumulativeYs = d.cumulativeYs || [];
return {
name:
d.name &&
d.name.replace(/<|>|&/g, (char) =>
char == "&" ? "&" : char == "<" ? "<" : ">"
),
index: i,
barIndex:
d.chartType === "bar" ? s.barChartIndex++ : s.barChartIndex,
chartType: d.chartType,
values: values,
yPositions: scaleAll(values, d.axisID),
id: d.axisID,
cumulativeYs: cumulativeYs,
cumulativeYPos: scaleAll(cumulativeYs, d.axisID),
};
});
}
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";
let multiAxis = this.config.yAxisConfig ? true : false;
let allValueLists = multiAxis ? {} : [];
let groupBy = (arr, property) => {
return arr.reduce((acc, cur) => {
acc[cur[property]] = [...(acc[cur[property]] || []), cur];
return acc;
}, {});
};
let generateCumulative = (arr) => {
let cumulative = new Array(this.state.datasetLength).fill(0);
arr.forEach((d, i) => {
let values = arr[i].values;
d[key] = cumulative = cumulative.map((c, i) => {
return c + values[i];
});
});
};
if (this.barOptions.stacked) {
key = "cumulativeYs";
// we need to filter out the different yAxis ID's here.
if (multiAxis) {
const groupedDataSets = groupBy(this.data.datasets, "axisID");
// const dataSetsByAxis = this.data.dd
for (var axisID in groupedDataSets) {
generateCumulative(groupedDataSets[axisID]);
}
} else {
generateCumulative(this.data.datasets);
}
}
// this is the trouble maker, we don't want to merge all
// datasets since we are trying to run two yAxis.
if (multiAxis) {
this.data.datasets.forEach((d) => {
// if the array exists already just push more data into it.
// otherwise create a new array into the object.
allValueLists[d.axisID || key]
? allValueLists[d.axisID || key].push(...d[key])
: (allValueLists[d.axisID || key] = [...d[key]]);
});
} else {
allValueLists = this.data.datasets.map((d) => {
return d[key];
});
}
if (this.data.yMarkers && !multiAxis) {
allValueLists.push(this.data.yMarkers.map((d) => d.value));
}
if (this.data.yRegions && !multiAxis) {
this.data.yRegions.map((d) => {
allValueLists.push([d.end, d.start]);
});
}
return multiAxis ? allValueLists : [].concat(...allValueLists); //return [].concat(...allValueLists); master
}
setupComponents() {
let componentConfigs = [
[
"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),
],
];
// if we have multiple yAxisConfigs we need to update the yAxisDefault
// components to multiple yAxis components.
if (this.config.yAxisConfig && this.config.yAxisConfig.length) {
this.config.yAxisConfig.forEach((yAxis) => {
componentConfigs.push([
"yAxis",
{
mode: yAxis.yAxisMode || "span",
width: this.width,
height: this.baseHeight,
shortenNumbers: this.config.shortenYAxisNumbers,
pos: yAxis.position || "left",
},
function () {
return this.state.yAxis;
}.bind(this),
]);
});
} else {
componentConfigs.push([
"yAxis",
{
mode: this.config.yAxisMode,
width: this.width,
height: this.baseHeight,
shortenNumbers: this.config.shortenYAxisNumbers,
},
function () {
return this.state.yAxis;
}.bind(this),
]);
}
let barDatasets = this.state.datasets.filter(
(d) => d.chartType === "bar"
);
let lineDatasets = this.state.datasets.filter(
(d) => d.chartType === "line"
);
let barsConfigs = barDatasets.map((d) => {
let index = d.index;
let barIndex = d.barIndex || 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 { yAxis } = s;
let d = s.datasets[index];
let { id = "left-axis" } = d;
let stacked = this.barOptions.stacked;
let spaceRatio =
this.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO;
let barsWidth = s.unitWidth * (1 - spaceRatio);
let barWidth =
barsWidth / (stacked ? 1 : barDatasets.length);
// if there are multiple yAxis we need to return the yAxis with the
// proper ID.
if (yAxis instanceof Array) {
// if the person only configured one yAxis in the array return the first.
yAxis =
yAxis.length > 1
? yAxis.find((axis) => id === axis.axisID)
: s.yAxis[0];
}
let xPositions = s.xAxis.positions.map(
(x) => x - barsWidth / 2
);
if (!stacked) {
xPositions = xPositions.map(
(p) => p + barWidth * barIndex - barWidth
);
}
let labels = new Array(s.datasetLength).fill("");
if (this.config.valuesOverPoints) {
if (stacked && d.index === s.datasets.length - 1) {
labels = d.cumulativeYs;
} else {
labels = d.values;
}
}
let offsets = new Array(s.datasetLength).fill(0);
if (stacked) {
offsets = d.yPositions.map(
(y, j) => y - d.cumulativeYPos[j]
);
}
return {
xPositions: xPositions,
yPositions: d.yPositions,
offsets: offsets,
// values: d.values,
labels: labels,
zeroLine: 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];
// if we have more than one yindex lets map the values
const yAxis = s.yAxis.length
? s.yAxis.find((axis) => d.id === axis.axisID) ||
s.yAxis[0]
: s.yAxis;
let minLine =
yAxis.positions[0] < yAxis.zeroLine
? yAxis.positions[0]
: yAxis.zeroLine;
return {
xPositions: s.xAxis.positions,
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) {
super.renderLegend(s.datasets);
}
}
// Legacy
/* renderLegend() {
let s = this.data;
if (s.datasets.length > 1) {
this.legendArea.textContent = "";
console.log(s.datasets);
s.datasets.map((d, i) => {
let barWidth = LEGEND_ITEM_WIDTH;
// let rightEndPoint = this.baseWidth - this.measures.margins.left - this.measures.margins.right;
// let multiplier = s.datasets.length - i;
let rect = legendBar(
// rightEndPoint - multiplier * barWidth, // To right align
barWidth * i,
"0",
barWidth,
this.colors[i],
d.name,
this.config.truncateLegends
);
this.legendArea.appendChild(rect);
});
}
} */
makeLegend(data, index, x_pos, y_pos) {
return legendDot(
x_pos,
y_pos + 5, // Extra offset
12, // size
3, // dot radius
this.colors[index], // fill
data.name, //label
null, // value
8.75, // base_font_size
this.config.truncateLegends // truncate legends
);
}
// Overlay
makeOverlay() {
if (this.init) {
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) {}
}
================================================
FILE: src/js/charts/BaseChart.js
================================================
import SvgTip from "../objects/SvgTip";
import {
$,
isElementInViewport,
getElementContentWidth,
isHidden,
} from "../utils/dom";
import {
makeSVGContainer,
makeSVGDefs,
makeSVGGroup,
makeText,
} from "../utils/draw";
import { LEGEND_ITEM_WIDTH } from "../utils/constants";
import {
BASE_MEASURES,
getExtraHeight,
getExtraWidth,
getTopOffset,
getLeftOffset,
INIT_CHART_UPDATE_TIMEOUT,
CHART_POST_ANIMATE_TIMEOUT,
DEFAULT_COLORS,
} from "../utils/constants";
import { getColor, isValidColor } from "../utils/colors";
import { runSMILAnimation } from "../utils/animation";
import { downloadFile, prepareForExport } from "../utils/export";
import { deepClone } from "../utils/helpers";
export default class BaseChart {
constructor(parent, options) {
// deepclone options to avoid making changes to orignal object
options = deepClone(options);
this.parent =
typeof parent === "string"
? document.querySelector(parent)
: parent;
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.colors = this.validateColors(options.colors, this.type);
this.config = {
showTooltip: 1, // calculate
showLegend:
typeof options.showLegend !== "undefined"
? options.showLegend
: 1,
isNavigable: options.isNavigable || 0,
animate: 0,
overrideCeiling: options.overrideCeiling || false,
overrideFloor: options.overrideFloor || false,
truncateLegends:
typeof options.truncateLegends !== "undefined"
? options.truncateLegends
: 1,
continuous:
typeof options.continuous !== "undefined"
? options.continuous
: 1,
};
this.measures = JSON.parse(JSON.stringify(BASE_MEASURES));
let m = this.measures;
this.realData = this.prepareData(options.data, this.config);
this.data = this.prepareFirstData(this.realData);
this.setMeasures(options);
if (!this.title.length) {
m.titleHeight = 0;
}
if (!this.config.showLegend) m.legendHeight = 0;
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);
// Look into improving responsiveness
//if (ResizeObserver) {
// this.resizeObserver = new ResizeObserver(this.boundDrawFn);
// this.resizeObserver.observe(this.parent);
//}
window.addEventListener("resize", this.boundDrawFn);
window.addEventListener("orientationchange", this.boundDrawFn);
}
destroy() {
//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;
this.update(this.data, true);
// Not needed anymore since animate defaults to 0 and might potentially be refactored or deprecated
/* setTimeout(() => {
this.update(this.data, true);
}, this.initTimeout); */
}
if (this.config.showLegend) {
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,
"frappe-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, drawing = false, config) {
if (!data) console.error("No data to update.");
if (!drawing) data = deepClone(data);
this.data = this.prepareData(data, config);
this.calc(); // builds state
this.render(this.components, this.config.animate);
}
render(components = this.components, animate = true) {
if (this.config.isNavigable) {
// 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(dataset) {
this.legendArea.textContent = "";
let count = 0;
let y = 0;
dataset.map((data, index) => {
let divisor = Math.floor(this.width / LEGEND_ITEM_WIDTH);
if (count > divisor) {
count = 0;
y += this.config.legendRowHeight;
}
let x = LEGEND_ITEM_WIDTH * count;
let dot = this.makeLegend(data, index, x, y);
this.legendArea.appendChild(dot);
count++;
});
}
makeLegend() {}
setupNavigation(init = false) {
if (!this.config.isNavigable) return;
if (init) {
this.bindOverlay();
this.keyActions = {
13: this.onEnterKey.bind(this),
37: this.onLeftArrow.bind(this),
38: this.onUpArrow.bind(this),
39: this.onRightArrow.bind(this),
40: this.onDownArrow.bind(this),
};
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]);
}
}
================================================
FILE: src/js/charts/DonutChart.js
================================================
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);
}
}
================================================
FILE: src/js/charts/Heatmap.js
================================================
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;
}
}
================================================
FILE: src/js/charts/PercentageChart.js
================================================
import AggregationChart from "./AggregationChart";
import { getOffset } from "../utils/dom";
import { getComponent } from "../objects/ChartComponents";
import { PERCENTAGE_BAR_DEFAULT_HEIGHT } from "../utils/constants";
export default class PercentageChart extends AggregationChart {
constructor(parent, args) {
super(parent, args);
this.type = "percentage";
this.setup();
}
setMeasures(options) {
let m = this.measures;
this.barOptions = options.barOptions || {};
let b = this.barOptions;
b.height = b.height || PERCENTAGE_BAR_DEFAULT_HEIGHT;
m.paddings.right = 30;
m.legendHeight = 60;
m.baseHeight = (b.height + b.depth * 0.5) * 8;
}
setupComponents() {
let s = this.state;
let componentConfigs = [
[
"percentageBars",
{
barHeight: this.barOptions.height,
},
function () {
return {
xPositions: s.xPositions,
widths: s.widths,
colors: this.colors,
};
}.bind(this),
],
];
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();
}
});
}
}
================================================
FILE: src/js/charts/PieChart.js
================================================
import AggregationChart from "./AggregationChart";
import { getComponent } from "../objects/ChartComponents";
import { getOffset, fire } from "../utils/dom";
import { getPositionByAngle } from "../utils/helpers";
import { makeArcPathStr, makeCircleStr } from "../utils/draw";
import { lightenDarkenColor } from "../utils/colors";
import { transform } from "../utils/animation";
import { FULL_ANGLE } from "../utils/constants";
export default class PieChart extends AggregationChart {
constructor(parent, args) {
super(parent, args);
this.type = "pie";
this.initTimeout = 0;
this.init = 1;
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);
}
getDataPoint(index = this.state.currentIndex) {
let s = this.state;
let data_point = {
index: index,
label: s.labels[index],
values: s.sliceTotals[index],
};
return data_point;
}
setCurrentDataPoint(index) {
let s = this.state;
index = parseInt(index);
if (index < 0) index = 0;
if (index >= s.labels.length) index = s.labels.length - 1;
if (index === s.currentIndex) return;
s.currentIndex = index;
fire(this.parent, "data-select", this.getDataPoint());
}
bindUnits() {
const units = this.components.get("pieSlices").store;
if (!units) return;
units.forEach((unit, index) => {
unit.addEventListener("click", () => {
this.setCurrentDataPoint(index);
});
});
}
mouseMove(e) {
const target = e.target;
let slices = this.components.get("pieSlices").store;
let prevIndex = this.curActiveSliceIndex;
let prevAcitve = this.curActiveSlice;
if (slices.includes(target)) {
let i = slices.indexOf(target);
this.hoverSlice(prevAcitve, prevIndex, false);
this.curActiveSlice = target;
this.curActiveSliceIndex = i;
this.hoverSlice(target, i, true, e);
} else {
this.mouseLeave();
}
}
mouseLeave() {
this.hoverSlice(this.curActiveSlice, this.curActiveSliceIndex, false);
}
}
================================================
FILE: src/js/index.js
================================================
import * as Charts from "./chart";
let frappe = {};
frappe.NAME = "Frappe Charts";
frappe.VERSION = "1.6.2";
frappe = Object.assign({}, frappe, Charts);
export default frappe;
================================================
FILE: src/js/objects/ChartComponents.js
================================================
import { makeSVGGroup } from "../utils/draw";
import {
makeText,
makePath,
xLine,
yLine,
generateAxisLabel,
yMarker,
yRegion,
datasetBar,
datasetDot,
percentageBar,
getPaths,
heatSquare,
} from "../utils/draw";
import { equilizeNoOfElements } from "../utils/draw-utils";
import {
translateHoriLine,
translateVertLine,
animateRegion,
animateBar,
animateDot,
animatePath,
animatePathStr,
} from "../utils/animate";
import { getMonthName } from "../utils/date-utils";
class ChartComponent {
constructor({
layerClass = "",
layerTransform = "",
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) => {
element.length
? element.forEach((el) => {
this.layer.appendChild(el);
})
: 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) {
const numberOfPoints = data.xPositions.length;
return data.xPositions.map((x, i) => {
let y = 0;
let isLast = i == numberOfPoints - 1;
let isFirst = i == 0;
let bar = percentageBar(
x,
y,
data.widths[i],
this.constants.barHeight,
isFirst,
isLast,
data.colors[i]
);
return bar;
});
},
animateElements(newData) {
if (newData) return [];
},
},
yAxis: {
layerClass: "y axis",
makeElements(data) {
let elements = [];
// will loop through each yaxis dataset if it exists
if (data.length) {
data.forEach((item, i) => {
item.positions.map((position, i) => {
elements.push(
yLine(
position,
item.labels[i],
this.constants.width,
{
mode: this.constants.mode,
pos: item.pos || this.constants.pos,
shortenNumbers:
this.constants.shortenNumbers,
title: item.title,
}
)
);
});
// we need to make yAxis titles if they are defined
if (item.title) {
elements.push(
generateAxisLabel({
title: item.title,
position: item.pos,
height: this.constants.height || data.zeroLine,
width: this.constants.width,
})
);
}
});
return elements;
}
data.positions.forEach((position, i) => {
elements.push(
yLine(position, data.labels[i], this.constants.width, {
mode: this.constants.mode,
pos: data.pos || this.constants.pos,
shortenNumbers: this.constants.shortenNumbers,
})
);
});
if (data.title) {
elements.push(
generateAxisLabel({
title: data.title,
position: data.pos,
height: this.constants.height || data.zeroLine,
width: this.constants.width,
})
);
}
return elements;
},
animateElements(newData) {
const animateMultipleElements = (oldData, newData) => {
let newPos = newData.positions;
let newLabels = newData.labels;
let oldPos = oldData.positions;
let oldLabels = oldData.labels;
[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
[oldLabels, newLabels] = equilizeNoOfElements(
oldLabels,
newLabels
);
this.render({
positions: oldPos,
labels: newLabels,
});
return this.store.map((line, i) => {
return translateHoriLine(line, newPos[i], oldPos[i]);
});
};
// we will need to animate both axis if we have more than one.
// so check if the oldData is an array of values.
if (this.oldData instanceof Array) {
return this.oldData.forEach((old, i) => {
animateMultipleElements(old, newData[i]);
});
}
let newPos = newData.positions;
let newLabels = newData.labels;
let oldPos = this.oldData.positions;
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,
stroke: m.options.stroke,
mode: "span",
lineType: m.options.lineType,
})
);
},
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);
}
================================================
FILE: src/js/objects/SvgTip.js
================================================
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.list = this.container.querySelector(".data-point-list");
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>`;
}
if (this.listValues.length > 4) {
this.list.classList.add("tooltip-grid");
} else {
this.list.classList.remove("tooltip-grid");
}
this.title.innerHTML = title;
this.dataPointList.innerHTML = "";
this.listValues.map((set, i) => {
const color = this.colors[i] || "black";
let value =
set.formatted === 0 || set.formatted ? set.formatted : set.value;
let li = $.create("li", {
innerHTML: `<div class="tooltip-legend" style="background: ${color};"></div>
<div>
<div class="tooltip-value">${value === 0 || value ? value : ""}</div>
<div class="tooltip-label">${set.title ? set.title : ""}</div>
</div>`,
});
this.dataPointList.appendChild(li);
});
}
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";
}
}
================================================
FILE: src/js/utils/animate.js
================================================
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];
}
================================================
FILE: src/js/utils/animation.js
================================================
// 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);
}
================================================
FILE: src/js/utils/axis-chart-utils.js
================================================
import { fillArray } from "../utils/helpers";
import {
DEFAULT_AXIS_CHART_TYPE,
AXIS_DATASET_CHART_TYPES,
DEFAULT_CHAR_WIDTH,
SERIES_LABEL_SPACE_RATIO,
} from "../utils/constants";
export function dataPrep(data, type, config) {
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);
}
if (config) {
vals = fillArray(vals, datasetLength - vals.length, null);
} 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) => {
const { axisID } = d;
return {
axisID,
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) * SERIES_LABEL_SPACE_RATIO;
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 && i !== labels.length - 1) {
label = "";
}
}
}
return label;
});
return calcLabels;
}
================================================
FILE: src/js/utils/colors.js
================================================
const PRESET_COLOR_MAP = {
pink: "#F683AE",
blue: "#318AD8",
green: "#48BB74",
grey: "#A6B1B9",
red: "#F56B6B",
yellow: "#FACF7A",
purple: "#44427B",
teal: "#5FD8C4",
cyan: "#15CCEF",
orange: "#F8814F",
"light-pink": "#FED7E5",
"light-blue": "#BFDDF7",
"light-green": "#48BB74",
"light-grey": "#F4F5F6",
"light-red": "#F6DFDF",
"light-yellow": "#FEE9BF",
"light-purple": "#E8E8F7",
"light-teal": "#D3FDF6",
"light-cyan": "#DDF8FD",
"light-orange": "#FECDB8",
};
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;
};
================================================
FILE: src/js/utils/constants.js
================================================
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 LEGEND_ITEM_WIDTH = 150;
export const SERIES_LABEL_SPACE_RATIO = 0.6;
export const BAR_CHART_SPACE_RATIO = 0.5;
export const MIN_BAR_PERCENT_HEIGHT = 0.0;
export const LINE_CHART_DOT_SIZE = 4;
export const DOT_OVERLAY_SIZE_INCR = 4;
export const PERCENTAGE_BAR_DEFAULT_HEIGHT = 16;
// 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 = 7.48;
const DEFAULT_CHART_COLORS = [
"pink",
"blue",
"green",
"grey",
"red",
"yellow",
"purple",
"teal",
"cyan",
"orange",
];
const HEATMAP_COLORS_GREEN = [
"#ebedf0",
"#c6e48b",
"#7bc96f",
"#239a3b",
"#196127",
];
export const HEATMAP_COLORS_BLUE = [
"#ebedf0",
"#c0ddf9",
"#73b3f3",
"#3886e1",
"#17459e",
];
export const HEATMAP_COLORS_YELLOW = [
"#ebedf0",
"#fdf436",
"#ffc700",
"#ff9100",
"#06001c",
];
export const 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;
================================================
FILE: src/js/utils/date-utils.js
================================================
// 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);
}
================================================
FILE: src/js/utils/dom.js
================================================
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);
}
================================================
FILE: src/js/utils/draw-utils.js
================================================
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);
}
================================================
FILE: src/js/utils/draw.js
================================================
import {
getBarHeightAndYAttr,
truncateString,
shortenLargeNumber,
getSplineCurvePointsStr,
} from "./draw-utils";
import { getStringWidth, isValidNumber, round } from "./helpers";
import {
DOT_OVERLAY_SIZE_INCR,
} from "./constants";
export const AXIS_TICK_LENGTH = 6;
const LABEL_MARGIN = 4;
const LABEL_WIDTH = 25;
const TOTAL_PADDING = 120;
const LABEL_MAX_CHARS = 18;
export const FONT_SIZE = 10;
const BASE_LINE_COLOR = "#E2E6E9";
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 rightRoundedBar(x, width, height) {
// https://medium.com/@dennismphil/one-side-rounded-rectangle-using-svg-fb31cf318d90
let radius = height / 2;
let xOffset = width - radius;
return `M${x},0 h${xOffset} q${radius},0 ${radius},${radius} q0,${radius} -${radius},${radius} h-${xOffset} v${height}z`;
}
export function leftRoundedBar(x, width, height) {
let radius = height / 2;
let xOffset = width - radius;
return `M${
x + radius
},0 h${xOffset} v${height} h-${xOffset} q-${radius}, 0 -${radius},-${radius} q0,-${radius} ${radius},-${radius}z`;
}
export function percentageBar(
x,
y,
width,
height,
isFirst,
isLast,
fill = "none"
) {
if (isLast) {
let pathStr = rightRoundedBar(x, width, height);
return makePath(pathStr, "percentage-bar", null, fill);
}
if (isFirst) {
let pathStr = leftRoundedBar(x, width, height);
return makePath(pathStr, "percentage-bar", null, fill);
}
let args = {
className: "percentage-bar",
x: x,
y: y,
width: width,
height: height,
fill: fill,
};
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 legendDot(
x,
y,
size,
radius,
fill = "none",
label,
value,
font_size = null,
truncate = false
) {
label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
if (!font_size) font_size = FONT_SIZE;
let args = {
className: "legend-dot",
x: 0,
y: 4 - size,
height: size,
width: size,
rx: radius,
fill: fill,
};
let textLabel = createSVG("text", {
className: "legend-dataset-label",
x: size,
y: 0,
dx: font_size + "px",
dy: font_size / 3 + "px",
"font-size": font_size * 1.6 + "px",
"text-anchor": "start",
innerHTML: label,
});
let textValue = null;
if (value) {
textValue = createSVG("text", {
className: "legend-dataset-value",
x: size,
y: FONT_SIZE + 10,
dx: FONT_SIZE + "px",
dy: FONT_SIZE / 3 + "px",
"font-size": FONT_SIZE * 1.2 + "px",
"text-anchor": "start",
innerHTML: value,
});
}
let group = createSVG("g", {
transform: `translate(${x}, ${y})`,
});
group.appendChild(createSVG("rect", args));
group.appendChild(textLabel);
if (value && textValue) {
group.appendChild(textValue);
}
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 || "var(--charts-label-color)";
let fill = options.fill || "var(--charts-label-color)";
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.alignment) options.alignment = "left";
if (options.shortenNumbers) label = shortenLargeNumber(label);
let className =
"line-horizontal " +
options.className +
(options.lineType === "dashed" ? "dashed" : "");
const textXPos =
options.alignment === "left"
? options.title
? x1 - LABEL_MARGIN + LABEL_WIDTH
: x1 - LABEL_MARGIN
: options.title
? x2 + LABEL_MARGIN * 4 - LABEL_WIDTH
: x2 + LABEL_MARGIN * 4;
const lineX1Post = options.title ? x1 + LABEL_WIDTH : x1;
const lineX2Post = options.title ? x2 - LABEL_WIDTH : x2;
let l = createSVG("line", {
className: className,
x1: lineX1Post,
x2: lineX2Post,
y1: 0,
y2: 0,
styles: {
stroke: options.stroke,
},
});
let text = createSVG("text", {
x: textXPos,
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 generateAxisLabel(options) {
if (!options.title) return;
const y =
options.position === "left"
? (options.height - TOTAL_PADDING) / 2 +
getStringWidth(options.title, 5) / 2
: (options.height - TOTAL_PADDING) / 2 -
getStringWidth(options.title, 5) / 2;
const x = options.position === "left" ? 0 : options.width;
const y2 =
options.position === "left"
? FONT_SIZE - LABEL_WIDTH
: FONT_SIZE + LABEL_WIDTH * -1;
const rotation =
options.position === "right" ? `rotate(90)` : `rotate(270)`;
const labelSvg = createSVG("text", {
className: "chart-label",
x: 0, // getStringWidth(options.title, 5) / 2,
y: 0, // y,
dy: `${y2}px`,
"font-size": `${FONT_SIZE}px`,
"text-anchor": "start",
innerHTML: `${options.title} `,
});
let wrapper = createSVG("g", {
x: 0,
y: 0,
transformBox: "fill-box",
transform: `translate(${x}, ${y}) ${rotation}`,
className: `test-${options.position}`,
});
wrapper.appendChild(labelSvg);
return wrapper;
}
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;
// pr_366
//x1 += offset;
//x2 += offset;
x1 += options.offset;
x2 += options.offset;
if (typeof label === "number") label = round(label);
return makeHoriLine(y, label, x1, x2, {
stroke: options.stroke,
className: options.className,
lineType: options.lineType,
alignment: options.pos,
title: options.title,
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 (!isValidNumber(y)) y = 0;
if (!options.labelPos) options.labelPos = "right";
if (!options.lineType) options.lineType = "dashed";
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: options.fill || `rgba(228, 234, 239, 0.49)`,
stroke: options.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;
// x y h w
// M{x},{y+r}
// q0,-{r} {r},-{r}
// q{r},0 {r},{r}
// v{h-r}
// h-{w}z
// let radius = width/2;
// let pathStr = `M${x},${y+radius} q0,-${radius} ${radius},-${radius} q${radius},0 ${radius},${radius} v${height-radius} h-${width}z`
// let rect = createSVG('path', {
// className: 'bar mini',
// d: pathStr,
// styles: { fill: color },
// x: x,
// y: y,
// 'data-point-index': index,
// });
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);
}
},
};
================================================
FILE: src/js/utils/export.js
================================================
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;
}
================================================
FILE: src/js/utils/helpers.js
================================================
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 == undefined) {
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;
}
================================================
FILE: src/js/utils/intervals.js
================================================
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 = true, overrideCeiling=false, overrideFloor=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);
if (overrideCeiling) {
maxValue = overrideCeiling
}
if (overrideFloor) {
minValue = overrideFloor
}
// 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;
}
================================================
FILE: src/js/utils/test/colors.test.js
================================================
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");
});
});
================================================
FILE: src/js/utils/test/helpers.test.js
================================================
const assert = require("assert");
const helpers = require("../helpers");
describe("utils.helpers", () => {
it("should return a value fixed upto 2 decimals", () => {
assert.equal(helpers.floatTwo(1.234), 1.23);
assert.equal(helpers.floatTwo(1.456), 1.46);
assert.equal(helpers.floatTwo(1), 1.0);
});
});
gitextract_lpjitjve/
├── .babelrc
├── .eslintrc.json
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ └── npm-publish.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── package.json
├── rollup.config.js
└── src/
├── css/
│ ├── charts.scss
│ └── chartsCss.js
└── js/
├── chart.js
├── charts/
│ ├── AggregationChart.js
│ ├── AxisChart.js
│ ├── BaseChart.js
│ ├── DonutChart.js
│ ├── Heatmap.js
│ ├── PercentageChart.js
│ └── PieChart.js
├── index.js
├── objects/
│ ├── ChartComponents.js
│ └── SvgTip.js
└── utils/
├── animate.js
├── animation.js
├── axis-chart-utils.js
├── colors.js
├── constants.js
├── date-utils.js
├── dom.js
├── draw-utils.js
├── draw.js
├── export.js
├── helpers.js
├── intervals.js
└── test/
├── colors.test.js
└── helpers.test.js
SYMBOL INDEX (323 symbols across 23 files)
FILE: src/css/chartsCss.js
constant CSSTEXT (line 1) | const CSSTEXT =
FILE: src/js/chart.js
function getChartByType (line 18) | function getChartByType(chartType = "line", parent, options) {
class Chart (line 32) | class Chart {
method constructor (line 33) | constructor(parent, options) {
FILE: src/js/charts/AggregationChart.js
class AggregationChart (line 7) | class AggregationChart extends BaseChart {
method constructor (line 8) | constructor(parent, args) {
method configure (line 12) | configure(args) {
method calc (line 21) | calc() {
method renderLegend (line 70) | renderLegend() {
method makeLegend (line 77) | makeLegend(data, index, x_pos, y_pos) {
FILE: src/js/charts/AxisChart.js
class AxisChart (line 28) | class AxisChart extends BaseChart {
method constructor (line 29) | constructor(parent, args) {
method setMeasures (line 41) | setMeasures() {
method configure (line 48) | configure(options) {
method prepareData (line 92) | prepareData(data = this.data, config = this.config) {
method prepareFirstData (line 96) | prepareFirstData(data = this.data) {
method calc (line 100) | calc(onlyWidthChange = false) {
method calcXPositions (line 111) | calcXPositions() {
method calcYAxisParameters (line 132) | calcYAxisParameters(dataValues, withMinimum = "false") {
method calcDatasetPoints (line 250) | calcDatasetPoints() {
method calcYExtremes (line 293) | calcYExtremes() {
method calcYRegions (line 309) | calcYRegions() {
method getAllYValues (line 331) | getAllYValues() {
method setupComponents (line 396) | setupComponents() {
method makeDataByIndex (line 632) | makeDataByIndex() {
method bindTooltip (line 662) | bindTooltip() {
method mapTooltipXPosition (line 681) | mapTooltipXPosition(relX) {
method renderLegend (line 701) | renderLegend() {
method makeLegend (line 732) | makeLegend(data, index, x_pos, y_pos) {
method makeOverlay (line 747) | makeOverlay() {
method updateOverlayGuides (line 780) | updateOverlayGuides() {
method bindOverlay (line 789) | bindOverlay() {
method bindUnits (line 795) | bindUnits() {
method updateOverlay (line 812) | updateOverlay() {
method onLeftArrow (line 819) | onLeftArrow() {
method onRightArrow (line 823) | onRightArrow() {
method getDataPoint (line 827) | getDataPoint(index = this.state.currentIndex) {
method setCurrentDataPoint (line 837) | setCurrentDataPoint(index) {
method addDataPoint (line 848) | addDataPoint(label, datasetValues, index = this.state.datasetLength) {
method removeDataPoint (line 857) | removeDataPoint(index = this.state.datasetLength - 1) {
method updateDataset (line 869) | updateDataset(datasetValues, index = 0) {
method updateDatasets (line 876) | updateDatasets(datasets) {
FILE: src/js/charts/BaseChart.js
class BaseChart (line 30) | class BaseChart {
method constructor (line 31) | constructor(parent, options) {
method prepareData (line 96) | prepareData(data) {
method prepareFirstData (line 100) | prepareFirstData(data) {
method validateColors (line 104) | validateColors(colors, type) {
method setMeasures (line 118) | setMeasures() {
method configure (line 123) | configure() {
method destroy (line 139) | destroy() {
method setup (line 146) | setup() {
method makeContainer (line 154) | makeContainer() {
method makeTooltip (line 170) | makeTooltip() {
method bindTooltip (line 178) | bindTooltip() {}
method draw (line 180) | draw(onlyWidthChange = false, init = false) {
method calc (line 211) | calc() {}
method updateWidth (line 213) | updateWidth() {
method makeChartArea (line 218) | makeChartArea() {
method updateTipOffset (line 271) | updateTipOffset(x, y) {
method setupComponents (line 278) | setupComponents() {
method update (line 282) | update(data, drawing = false, config) {
method render (line 290) | render(components = this.components, animate = true) {
method updateNav (line 313) | updateNav() {
method renderLegend (line 320) | renderLegend(dataset) {
method makeLegend (line 338) | makeLegend() {}
method setupNavigation (line 340) | setupNavigation(init = false) {
method makeOverlay (line 365) | makeOverlay() {}
method updateOverlay (line 366) | updateOverlay() {}
method bindOverlay (line 367) | bindOverlay() {}
method bindUnits (line 368) | bindUnits() {}
method onLeftArrow (line 370) | onLeftArrow() {}
method onRightArrow (line 371) | onRightArrow() {}
method onUpArrow (line 372) | onUpArrow() {}
method onDownArrow (line 373) | onDownArrow() {}
method onEnterKey (line 374) | onEnterKey() {}
method addDataPoint (line 376) | addDataPoint() {}
method removeDataPoint (line 377) | removeDataPoint() {}
method getDataPoint (line 379) | getDataPoint() {}
method setCurrentDataPoint (line 380) | setCurrentDataPoint() {}
method updateDataset (line 382) | updateDataset() {}
method export (line 384) | export() {
FILE: src/js/charts/DonutChart.js
class DonutChart (line 10) | class DonutChart extends AggregationChart {
method constructor (line 11) | constructor(parent, args) {
method configure (line 20) | configure(args) {
method calc (line 32) | calc() {
method setupComponents (line 99) | setupComponents() {
method calTranslateByAngle (line 124) | calTranslateByAngle(property) {
method hoverSlice (line 135) | hoverSlice(path, i, flag, e) {
method bindTooltip (line 161) | bindTooltip() {
method mouseMove (line 166) | mouseMove(e) {
method mouseLeave (line 182) | mouseLeave() {
FILE: src/js/charts/Heatmap.js
constant COL_WIDTH (line 28) | const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE;
constant ROW_HEIGHT (line 29) | const ROW_HEIGHT = COL_WIDTH;
class Heatmap (line 32) | class Heatmap extends BaseChart {
method constructor (line 33) | constructor(parent, options) {
method setMeasures (line 48) | setMeasures(options) {
method updateWidth (line 64) | updateWidth() {
method prepareData (line 71) | prepareData(data = this.data) {
method calc (line 101) | calc() {
method setupComponents (line 117) | setupComponents() {
method update (line 161) | update(data) {
method bindTooltip (line 171) | bindTooltip() {
method renderLegend (line 203) | renderLegend() {
method getDomains (line 237) | getDomains() {
method getDomainConfig (line 265) | getDomainConfig(startDate, endDate = "") {
method getCol (line 302) | getCol(startDate, month, empty = false) {
method getSubDomainConfig (line 327) | getSubDomainConfig(date) {
FILE: src/js/charts/PercentageChart.js
class PercentageChart (line 6) | class PercentageChart extends AggregationChart {
method constructor (line 7) | constructor(parent, args) {
method setMeasures (line 13) | setMeasures(options) {
method setupComponents (line 25) | setupComponents() {
method calc (line 52) | calc() {
method makeDataByIndex (line 68) | makeDataByIndex() {}
method bindTooltip (line 70) | bindTooltip() {
FILE: src/js/charts/PieChart.js
class PieChart (line 10) | class PieChart extends AggregationChart {
method constructor (line 11) | constructor(parent, args) {
method configure (line 20) | configure(args) {
method calc (line 31) | calc() {
method setupComponents (line 95) | setupComponents() {
method calTranslateByAngle (line 119) | calTranslateByAngle(property) {
method hoverSlice (line 130) | hoverSlice(path, i, flag, e) {
method bindTooltip (line 156) | bindTooltip() {
method getDataPoint (line 160) | getDataPoint(index = this.state.currentIndex) {
method setCurrentDataPoint (line 169) | setCurrentDataPoint(index) {
method bindUnits (line 179) | bindUnits() {
method mouseMove (line 188) | mouseMove(e) {
method mouseLeave (line 204) | mouseLeave() {
FILE: src/js/objects/ChartComponents.js
class ChartComponent (line 28) | class ChartComponent {
method constructor (line 29) | constructor({
method refresh (line 58) | refresh(data) {
method setup (line 62) | setup(parent) {
method make (line 66) | make() {
method render (line 71) | render(data) {
method update (line 87) | update(animate = true) {
method makeElements (line 100) | makeElements(data) {
method animateElements (line 114) | animateElements(newData) {
method makeElements (line 122) | makeElements(data) {
method animateElements (line 130) | animateElements(newData) {
method makeElements (line 138) | makeElements(data) {
method animateElements (line 159) | animateElements(newData) {
method makeElements (line 165) | makeElements(data) {
method animateElements (line 226) | animateElements(newData) {
method makeElements (line 278) | makeElements(data) {
method animateElements (line 287) | animateElements(newData) {
method makeElements (line 309) | makeElements(data) {
method animateElements (line 319) | animateElements(newData) {
method makeElements (line 349) | makeElements(data) {
method animateElements (line 356) | animateElements(newData) {
method makeElements (line 397) | makeElements(data) {
method animateElements (line 447) | animateElements(newData) {
method makeElements (line 456) | makeElements(data) {
method animateElements (line 477) | animateElements(newData) {
method makeElements (line 530) | makeElements(data) {
method animateElements (line 567) | animateElements(newData) {
function getComponent (line 616) | function getComponent(name, constants, getData) {
FILE: src/js/objects/SvgTip.js
class SvgTip (line 4) | class SvgTip {
method constructor (line 5) | constructor({ parent = null, colors = [] }) {
method setup (line 22) | setup() {
method refresh (line 26) | refresh() {
method makeTooltip (line 31) | makeTooltip() {
method fill (line 50) | fill() {
method calcPosition (line 86) | calcPosition() {
method setValues (line 110) | setValues(x, y, title = {}, listValues = [], index = -1) {
method hideTip (line 121) | hideTip() {
method showTip (line 127) | showTip() {
FILE: src/js/utils/animate.js
constant UNIT_ANIM_DUR (line 3) | const UNIT_ANIM_DUR = 350;
constant PATH_ANIM_DUR (line 4) | const PATH_ANIM_DUR = 350;
constant MARKER_LINE_ANIM_DUR (line 5) | const MARKER_LINE_ANIM_DUR = UNIT_ANIM_DUR;
constant REPLACE_ALL_NEW_DUR (line 6) | const REPLACE_ALL_NEW_DUR = 250;
constant STD_EASING (line 8) | const STD_EASING = "easein";
function translate (line 10) | function translate(unit, oldCoord, newCoord, duration) {
function translateVertLine (line 22) | function translateVertLine(xLine, newX, oldX) {
function translateHoriLine (line 26) | function translateHoriLine(yLine, newY, oldY) {
function animateRegion (line 30) | function animateRegion(rectGroup, newY1, newY2, oldY2) {
function animateBar (line 50) | function animateBar(bar, x, yTop, width, offset = 0, meta = {}) {
function animateDot (line 78) | function animateDot(dot, x, y) {
function animatePath (line 89) | function animatePath(paths, newXList, newYList, zeroLine, spline) {
function animatePathStr (line 119) | function animatePathStr(oldPath, pathStr) {
FILE: src/js/utils/animation.js
constant EASING (line 5) | const EASING = {
function animateSVGElement (line 14) | function animateSVGElement(
function transform (line 75) | function transform(element, style) {
function animateSVG (line 84) | function animateSVG(svgContainer, elements) {
function runSMILAnimation (line 117) | function runSMILAnimation(parent, svgElement, elementsToAnimate) {
FILE: src/js/utils/axis-chart-utils.js
function dataPrep (line 9) | function dataPrep(data, type, config) {
function zeroDataPrep (line 70) | function zeroDataPrep(realData) {
function getShortenedLabels (line 109) | function getShortenedLabels(chartWidth, labels = [], isSeries = true) {
FILE: src/js/utils/colors.js
constant PRESET_COLOR_MAP (line 1) | const PRESET_COLOR_MAP = {
function limitColor (line 24) | function limitColor(r) {
function lightenDarkenColor (line 30) | function lightenDarkenColor(color, amt) {
function isValidColor (line 44) | function isValidColor(string) {
FILE: src/js/utils/constants.js
constant ALL_CHART_TYPES (line 1) | const ALL_CHART_TYPES = [
constant COMPATIBLE_CHARTS (line 10) | const COMPATIBLE_CHARTS = {
constant DATA_COLOR_DIVISIONS (line 18) | const DATA_COLOR_DIVISIONS = {
constant BASE_MEASURES (line 26) | const BASE_MEASURES = {
function getTopOffset (line 47) | function getTopOffset(m) {
function getLeftOffset (line 51) | function getLeftOffset(m) {
function getExtraHeight (line 55) | function getExtraHeight(m) {
function getExtraWidth (line 66) | function getExtraWidth(m) {
constant INIT_CHART_UPDATE_TIMEOUT (line 73) | const INIT_CHART_UPDATE_TIMEOUT = 700;
constant CHART_POST_ANIMATE_TIMEOUT (line 74) | const CHART_POST_ANIMATE_TIMEOUT = 400;
constant DEFAULT_AXIS_CHART_TYPE (line 76) | const DEFAULT_AXIS_CHART_TYPE = "line";
constant AXIS_DATASET_CHART_TYPES (line 77) | const AXIS_DATASET_CHART_TYPES = ["line", "bar"];
constant LEGEND_ITEM_WIDTH (line 79) | const LEGEND_ITEM_WIDTH = 150;
constant SERIES_LABEL_SPACE_RATIO (line 80) | const SERIES_LABEL_SPACE_RATIO = 0.6;
constant BAR_CHART_SPACE_RATIO (line 82) | const BAR_CHART_SPACE_RATIO = 0.5;
constant MIN_BAR_PERCENT_HEIGHT (line 83) | const MIN_BAR_PERCENT_HEIGHT = 0.0;
constant LINE_CHART_DOT_SIZE (line 85) | const LINE_CHART_DOT_SIZE = 4;
constant DOT_OVERLAY_SIZE_INCR (line 86) | const DOT_OVERLAY_SIZE_INCR = 4;
constant PERCENTAGE_BAR_DEFAULT_HEIGHT (line 88) | const PERCENTAGE_BAR_DEFAULT_HEIGHT = 16;
constant HEATMAP_DISTRIBUTION_SIZE (line 92) | const HEATMAP_DISTRIBUTION_SIZE = 5;
constant HEATMAP_SQUARE_SIZE (line 94) | const HEATMAP_SQUARE_SIZE = 10;
constant HEATMAP_GUTTER_SIZE (line 95) | const HEATMAP_GUTTER_SIZE = 2;
constant DEFAULT_CHAR_WIDTH (line 97) | const DEFAULT_CHAR_WIDTH = 7;
constant TOOLTIP_POINTER_TRIANGLE_HEIGHT (line 99) | const TOOLTIP_POINTER_TRIANGLE_HEIGHT = 7.48;
constant DEFAULT_CHART_COLORS (line 100) | const DEFAULT_CHART_COLORS = [
constant HEATMAP_COLORS_GREEN (line 112) | const HEATMAP_COLORS_GREEN = [
constant HEATMAP_COLORS_BLUE (line 119) | const HEATMAP_COLORS_BLUE = [
constant HEATMAP_COLORS_YELLOW (line 126) | const HEATMAP_COLORS_YELLOW = [
constant DEFAULT_COLORS (line 134) | const DEFAULT_COLORS = {
constant ANGLE_RATIO (line 144) | const ANGLE_RATIO = Math.PI / 180;
constant FULL_ANGLE (line 145) | const FULL_ANGLE = 360;
FILE: src/js/utils/date-utils.js
constant NO_OF_YEAR_MONTHS (line 3) | const NO_OF_YEAR_MONTHS = 12;
constant NO_OF_DAYS_IN_WEEK (line 4) | const NO_OF_DAYS_IN_WEEK = 7;
constant DAYS_IN_YEAR (line 5) | const DAYS_IN_YEAR = 375;
constant NO_OF_MILLIS (line 6) | const NO_OF_MILLIS = 1000;
constant SEC_IN_DAY (line 7) | const SEC_IN_DAY = 86400;
constant MONTH_NAMES (line 9) | const MONTH_NAMES = [
constant MONTH_NAMES_SHORT (line 23) | const MONTH_NAMES_SHORT = [
constant DAY_NAMES_SHORT (line 38) | const DAY_NAMES_SHORT = [
constant DAY_NAMES (line 47) | const DAY_NAMES = [
function treatAsUtc (line 58) | function treatAsUtc(date) {
function toMidnightUTC (line 64) | function toMidnightUTC(date) {
function getYyyyMmDd (line 70) | function getYyyyMmDd(date) {
function clone (line 80) | function clone(date) {
function timestampSec (line 84) | function timestampSec(date) {
function timestampToMidnight (line 88) | function timestampToMidnight(timestamp, roundAhead = false) {
function getWeeksBetween (line 98) | function getWeeksBetween(startDate, endDate) {
function getDaysBetween (line 103) | function getDaysBetween(startDate, endDate) {
function areInSameMonth (line 108) | function areInSameMonth(startDate, endDate) {
function getMonthName (line 115) | function getMonthName(i, short = false) {
function getLastDateInMonth (line 120) | function getLastDateInMonth(month, year) {
function setDayToSunday (line 125) | function setDayToSunday(date) {
function addDays (line 135) | function addDays(date, numberOfDays) {
FILE: src/js/utils/dom.js
function $ (line 1) | function $(expr, con) {
function findNodeIndex (line 7) | function findNodeIndex(node) {
function getOffset (line 44) | function getOffset(element) {
function isHidden (line 62) | function isHidden(el) {
function isElementInViewport (line 66) | function isElementInViewport(el) {
function getElementContentWidth (line 82) | function getElementContentWidth(element) {
function bind (line 90) | function bind(element, o) {
function unbind (line 102) | function unbind(element, o) {
function fire (line 114) | function fire(target, type, properties) {
function forEachNode (line 127) | function forEachNode(nodeList, callback, scope) {
function activate (line 134) | function activate(
FILE: src/js/utils/draw-utils.js
function getBarHeightAndYAttr (line 3) | function getBarHeightAndYAttr(yTop, zeroLine) {
function equilizeNoOfElements (line 16) | function equilizeNoOfElements(
function truncateString (line 30) | function truncateString(txt, len) {
function shortenLargeNumber (line 41) | function shortenLargeNumber(label) {
function getSplineCurvePointsStr (line 61) | function getSplineCurvePointsStr(xList, yList) {
FILE: src/js/utils/draw.js
constant AXIS_TICK_LENGTH (line 13) | const AXIS_TICK_LENGTH = 6;
constant LABEL_MARGIN (line 14) | const LABEL_MARGIN = 4;
constant LABEL_WIDTH (line 15) | const LABEL_WIDTH = 25;
constant TOTAL_PADDING (line 16) | const TOTAL_PADDING = 120;
constant LABEL_MAX_CHARS (line 17) | const LABEL_MAX_CHARS = 18;
constant FONT_SIZE (line 18) | const FONT_SIZE = 10;
constant BASE_LINE_COLOR (line 19) | const BASE_LINE_COLOR = "#E2E6E9";
function $ (line 21) | function $(expr, con) {
function createSVG (line 27) | function createSVG(tag, o) {
function renderVerticalGradient (line 60) | function renderVerticalGradient(svgDefElem, gradientId) {
function setGradientStop (line 71) | function setGradientStop(gradElem, offset, color, opacity) {
function makeSVGContainer (line 80) | function makeSVGContainer(parent, className, width, height) {
function makeSVGDefs (line 89) | function makeSVGDefs(svgContainer) {
function makeSVGGroup (line 95) | function makeSVGGroup(className, transform = "", parent = undefined) {
function wrapInSVGGroup (line 104) | function wrapInSVGGroup(elements, className = "") {
function makePath (line 112) | function makePath(
function makeArcPathStr (line 130) | function makeArcPathStr(
function makeCircleStr (line 152) | function makeCircleStr(
function makeArcStrokePathStr (line 178) | function makeArcStrokePathStr(
function makeStrokeCircleStr (line 200) | function makeStrokeCircleStr(
function makeGradient (line 226) | function makeGradient(svgDefElem, color, lighter = false) {
function rightRoundedBar (line 246) | function rightRoundedBar(x, width, height) {
function leftRoundedBar (line 254) | function leftRoundedBar(x, width, height) {
function percentageBar (line 263) | function percentageBar(
function heatSquare (line 294) | function heatSquare(
function legendDot (line 320) | function legendDot(
function makeText (line 382) | function makeText(className, x, y, content, options = {}) {
function makeVertLine (line 400) | function makeVertLine(x, label, y1, y2, options = {}) {
function makeHoriLine (line 432) | function makeHoriLine(y, label, x1, x2, options = {}) {
function generateAxisLabel (line 489) | function generateAxisLabel(options) {
function yLine (line 530) | function yLine(y, label, width, options = {}) {
function xLine (line 567) | function xLine(x, label, height, options = {}) {
function yMarker (line 603) | function yMarker(y, label, width, options = {}) {
function yRegion (line 634) | function yRegion(y1, y2, width, label, options = {}) {
function datasetBar (line 678) | function datasetBar(
function datasetDot (line 760) | function datasetDot(x, y, radius, color, label = "", index = 0) {
function getPaths (line 798) | function getPaths(xList, yList, color, options = {}, meta = {}) {
FILE: src/js/utils/export.js
function downloadFile (line 4) | function downloadFile(filename, data) {
function prepareForExport (line 19) | function prepareForExport(svg) {
FILE: src/js/utils/helpers.js
function floatTwo (line 7) | function floatTwo(d) {
function arraysEqual (line 16) | function arraysEqual(arr1, arr2) {
function shuffle (line 29) | function shuffle(array) {
function fillArray (line 49) | function fillArray(array, count, element, start = false) {
function getStringWidth (line 63) | function getStringWidth(string, charWidth) {
function bindChange (line 67) | function bindChange(obj, getFn, setFn) {
function getRandomBias (line 81) | function getRandomBias(min, max, bias, influence) {
function getPositionByAngle (line 89) | function getPositionByAngle(angle, radius) {
function isValidNumber (line 101) | function isValidNumber(candidate, nonNegative = false) {
function round (line 113) | function round(d) {
function deepClone (line 123) | function deepClone(candidate) {
FILE: src/js/utils/intervals.js
function normalize (line 3) | function normalize(x) {
function getChartRangeIntervals (line 26) | function getChartRangeIntervals(max, min = 0) {
function getChartIntervals (line 64) | function getChartIntervals(maxValue, minValue = 0) {
function calcChartIntervals (line 84) | function calcChartIntervals(values, withMinimum = true, overrideCeiling=...
function getZeroIndex (line 172) | function getZeroIndex(yPts) {
function getRealIntervals (line 193) | function getRealIntervals(max, noOfIntervals, min = 0, asc = 1) {
function getIntervalSize (line 205) | function getIntervalSize(orderedArray) {
function getValueRange (line 209) | function getValueRange(orderedArray) {
function scale (line 213) | function scale(val, yAxis) {
function isInRange (line 217) | function isInRange(val, min, max) {
function isInRange2D (line 221) | function isInRange2D(coord, minCoord, maxCoord) {
function getClosestInArray (line 228) | function getClosestInArray(goal, arr, index = false) {
function calcDistribution (line 236) | function calcDistribution(values, distributionSize) {
function getMaxCheckpoint (line 253) | function getMaxCheckpoint(value, distribution) {
Condensed preview — 39 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (164K chars).
[
{
"path": ".babelrc",
"chars": 192,
"preview": "{\n \"presets\": [\n [\n \"@babel/preset-env\",\n {\n \"targets\": {\n \"browsers\": [\"last 2 versions\","
},
{
"path": ".eslintrc.json",
"chars": 397,
"preview": "{\n \"env\": {\n \"browser\": true,\n \"es6\": true\n },\n \"extends\": \"eslint:recommended\",\n \"parserOptions\": {\n \"sour"
},
{
"path": ".github/ISSUE_TEMPLATE.md",
"chars": 163,
"preview": "#### Expected Behaviour\n\n#### Actual Behaviour\n\n#### Steps to Reproduce:\n*\n\n\nNOTE: Add a GIF/Screenshot if required.\n\nFr"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 682,
"preview": "<!-- Thank you so much for contributing! We're glad to have you onboard :) -->\n<!-- Please help us understand you contri"
},
{
"path": ".github/workflows/npm-publish.yml",
"chars": 848,
"preview": "# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created\n# For "
},
{
"path": ".gitignore",
"chars": 968,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directo"
},
{
"path": ".travis.yml",
"chars": 136,
"preview": "language: node_js\n\nnode_js:\n - \"6\"\n - \"8\"\n\nbefore_install:\n - make install\n\nscript:\n - make test\n\nafter_success:\n -"
},
{
"path": "LICENSE",
"chars": 1073,
"preview": "MIT License\n\nCopyright (c) 2017 Prateeksha Singh\n\nPermission is hereby granted, free of charge, to any person obtaining "
},
{
"path": "Makefile",
"chars": 912,
"preview": "-include .env\n\nBASEDIR\t\t\t\t\t\t\t\t\t= $(realpath .)\n\nSRCDIR\t\t\t\t\t\t\t\t\t= $(BASEDIR)/src\nDISTDIR\t\t\t\t\t\t\t\t\t= $(BASEDIR)/dist\nDOCSDI"
},
{
"path": "README.md",
"chars": 3633,
"preview": "<div align=\"center\" markdown=\"1\">\n \n<img width=\"80\" alt=\"charts-logo\" src=\"https://github.com/user-attachments/assets"
},
{
"path": "package.json",
"chars": 1256,
"preview": "{\n\t\"name\": \"frappe-charts\",\n\t\"version\": \"v1.6.3\",\n\t\"type\": \"module\",\n\t\"main\": \"dist/frappe-charts.esm.js\",\n\t\"module\": \"d"
},
{
"path": "rollup.config.js",
"chars": 1080,
"preview": "import pkg from \"./package.json\";\n\nimport commonjs from \"rollup-plugin-commonjs\";\nimport babel from \"rollup-plugin-babel"
},
{
"path": "src/css/charts.scss",
"chars": 3679,
"preview": ":root {\n --charts-label-color: #313b44;\n --charts-axis-line-color: #f4f5f6;\n\n --charts-tooltip-title: var(--charts-la"
},
{
"path": "src/css/chartsCss.js",
"chars": 1733,
"preview": "export const CSSTEXT =\n \".chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Ro"
},
{
"path": "src/js/chart.js",
"chars": 923,
"preview": "import \"../css/charts.scss\";\n\nimport PercentageChart from \"./charts/PercentageChart\";\nimport PieChart from \"./charts/Pie"
},
{
"path": "src/js/charts/AggregationChart.js",
"chars": 2435,
"preview": "import BaseChart from \"./BaseChart\";\nimport { truncateString } from \"../utils/draw-utils\";\nimport { legendDot } from \".."
},
{
"path": "src/js/charts/AxisChart.js",
"chars": 21831,
"preview": "import BaseChart from \"./BaseChart\";\nimport {\n\tdataPrep,\n\tzeroDataPrep,\n\tgetShortenedLabels,\n} from \"../utils/axis-chart"
},
{
"path": "src/js/charts/BaseChart.js",
"chars": 8852,
"preview": "import SvgTip from \"../objects/SvgTip\";\nimport {\n\t$,\n\tisElementInViewport,\n\tgetElementContentWidth,\n\tisHidden,\n} from \"."
},
{
"path": "src/js/charts/DonutChart.js",
"chars": 5325,
"preview": "import AggregationChart from \"./AggregationChart\";\nimport { getComponent } from \"../objects/ChartComponents\";\nimport { g"
},
{
"path": "src/js/charts/Heatmap.js",
"chars": 9230,
"preview": "import BaseChart from \"./BaseChart\";\nimport { getComponent } from \"../objects/ChartComponents\";\nimport { makeText, heatS"
},
{
"path": "src/js/charts/PercentageChart.js",
"chars": 2408,
"preview": "import AggregationChart from \"./AggregationChart\";\nimport { getOffset } from \"../utils/dom\";\nimport { getComponent } fro"
},
{
"path": "src/js/charts/PieChart.js",
"chars": 5897,
"preview": "import AggregationChart from \"./AggregationChart\";\nimport { getComponent } from \"../objects/ChartComponents\";\nimport { g"
},
{
"path": "src/js/index.js",
"chars": 180,
"preview": "import * as Charts from \"./chart\";\n\nlet frappe = {};\n\nfrappe.NAME = \"Frappe Charts\";\nfrappe.VERSION = \"1.6.2\";\n\nfrappe ="
},
{
"path": "src/js/objects/ChartComponents.js",
"chars": 13603,
"preview": "import { makeSVGGroup } from \"../utils/draw\";\nimport {\n\tmakeText,\n\tmakePath,\n\txLine,\n\tyLine,\n\tgenerateAxisLabel,\n\tyMarke"
},
{
"path": "src/js/objects/SvgTip.js",
"chars": 3443,
"preview": "import { $ } from \"../utils/dom\";\nimport { TOOLTIP_POINTER_TRIANGLE_HEIGHT } from \"../utils/constants\";\n\nexport default "
},
{
"path": "src/js/utils/animate.js",
"chars": 3313,
"preview": "import { getBarHeightAndYAttr, getSplineCurvePointsStr } from \"./draw-utils\";\n\nexport const UNIT_ANIM_DUR = 350;\nexport "
},
{
"path": "src/js/utils/animation.js",
"chars": 3209,
"preview": "// Leveraging SMIL Animations\n\nimport { REPLACE_ALL_NEW_DUR } from \"./animate\";\n\nconst EASING = {\n ease: \"0.25 0.1 0.25"
},
{
"path": "src/js/utils/axis-chart-utils.js",
"chars": 2892,
"preview": "import { fillArray } from \"../utils/helpers\";\nimport {\n\tDEFAULT_AXIS_CHART_TYPE,\n\tAXIS_DATASET_CHART_TYPES,\n\tDEFAULT_CHA"
},
{
"path": "src/js/utils/colors.js",
"chars": 1706,
"preview": "const PRESET_COLOR_MAP = {\n pink: \"#F683AE\",\n blue: \"#318AD8\",\n green: \"#48BB74\",\n grey: \"#A6B1B9\",\n red: \"#F56B6B\""
},
{
"path": "src/js/utils/constants.js",
"chars": 2899,
"preview": "export const ALL_CHART_TYPES = [\n \"line\",\n \"scatter\",\n \"bar\",\n \"percentage\",\n \"heatmap\",\n \"pie\",\n];\n\nexport const "
},
{
"path": "src/js/utils/date-utils.js",
"chars": 2871,
"preview": "// Playing around with dates\n\nexport const NO_OF_YEAR_MONTHS = 12;\nexport const NO_OF_DAYS_IN_WEEK = 7;\nexport const DAY"
},
{
"path": "src/js/utils/dom.js",
"chars": 3679,
"preview": "export function $(expr, con) {\n return typeof expr === \"string\"\n ? (con || document).querySelector(expr)\n : expr "
},
{
"path": "src/js/utils/draw-utils.js",
"chars": 2785,
"preview": "import { fillArray } from \"./helpers\";\n\nexport function getBarHeightAndYAttr(yTop, zeroLine) {\n let height, y;\n if (yT"
},
{
"path": "src/js/utils/draw.js",
"chars": 20811,
"preview": "import {\n\tgetBarHeightAndYAttr,\n\ttruncateString,\n\tshortenLargeNumber,\n\tgetSplineCurvePointsStr,\n} from \"./draw-utils\";\ni"
},
{
"path": "src/js/utils/export.js",
"chars": 975,
"preview": "import { $ } from \"../utils/dom\";\nimport { CSSTEXT } from \"../../css/chartsCss\";\n\nexport function downloadFile(filename,"
},
{
"path": "src/js/utils/helpers.js",
"chars": 3756,
"preview": "import { ANGLE_RATIO } from \"./constants\";\n\n/**\n * Returns the value of a number upto 2 decimal places.\n * @param {Numbe"
},
{
"path": "src/js/utils/intervals.js",
"chars": 6828,
"preview": "import { floatTwo } from \"./helpers\";\n\nfunction normalize(x) {\n // Calculates mantissa and exponent of a number\n // Re"
},
{
"path": "src/js/utils/test/colors.test.js",
"chars": 480,
"preview": "const assert = require(\"assert\");\nconst colors = require(\"../colors\");\n\ndescribe(\"utils.colors\", () => {\n it(\"should re"
},
{
"path": "src/js/utils/test/helpers.test.js",
"chars": 320,
"preview": "const assert = require(\"assert\");\nconst helpers = require(\"../helpers\");\n\ndescribe(\"utils.helpers\", () => {\n it(\"should"
}
]
About this extraction
This page contains the full source code of the frappe/charts GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 39 files (143.9 KB), approximately 42.3k tokens, and a symbol index with 323 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.