@@ -6,5 +6,9 @@ | |||
} | |||
}] | |||
], | |||
"plugins": ["external-helpers"] | |||
"env": { | |||
"test": { | |||
"presets": ["env"] | |||
} | |||
} | |||
} |
@@ -1,6 +1,63 @@ | |||
# cache | |||
node_modules | |||
# Logs | |||
logs | |||
*.log | |||
npm-debug.log* | |||
yarn-debug.log* | |||
yarn-error.log* | |||
# misc | |||
.DS_Store | |||
yarn.lock | |||
# 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 | |||
.DS_Store |
@@ -0,0 +1,14 @@ | |||
language: node_js | |||
node_js: | |||
- "6" | |||
- "8" | |||
before_install: | |||
- make install | |||
script: | |||
- make test | |||
after_success: | |||
- make coveralls |
@@ -0,0 +1,45 @@ | |||
-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 |
@@ -1,6 +1,8 @@ | |||
<div align="center"> | |||
<img src="https://github.com/frappe/design/blob/master/logos/charts-logo.svg" height="128"> | |||
<h2>Frappe Charts</h2> | |||
<a href="https://frappe.github.io/charts"> | |||
<h2>Frappe Charts</h2> | |||
</a> | |||
<p align="center"> | |||
<p>GitHub-inspired modern, intuitive and responsive charts with zero dependencies</p> | |||
<a href="https://frappe.github.io/charts"> | |||
@@ -10,9 +12,15 @@ | |||
</div> | |||
<p align="center"> | |||
<a href="https://travis-ci.org/frappe/charts"> | |||
<img src="https://img.shields.io/travis/frappe/charts.svg?style=flat-square"> | |||
</a> | |||
<a href="http://github.com/frappe/charts/tree/master/dist/js/frappe-charts.min.iife.js"> | |||
<img src="http://img.badgesize.io/frappe/charts/master/dist/frappe-charts.min.iife.js.svg?compression=gzip"> | |||
</a> | |||
<a href="https://travis-ci.org/frappe/charts"> | |||
<img src="https://img.shields.io/travis/frappe/charts.svg?style=flat-square"> | |||
</a> | |||
</p> | |||
<p align="center"> | |||
@@ -42,9 +50,9 @@ | |||
* ...or include within your HTML | |||
```html | |||
<script src="https://cdn.jsdelivr.net/npm/frappe-charts@1.0.0/dist/frappe-charts.min.iife.js"></script> | |||
<script src="https://cdn.jsdelivr.net/npm/frappe-charts@1.1.0/dist/frappe-charts.min.iife.js"></script> | |||
<!-- or --> | |||
<script src="https://unpkg.com/frappe-charts@1.0.0/dist/frappe-charts.min.iife.js"></script> | |||
<script src="https://unpkg.com/frappe-charts@1.1.0/dist/frappe-charts.min.iife.js"></script> | |||
``` | |||
#### Usage | |||
@@ -1 +1 @@ | |||
.chart-container{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}.chart-container .graph-focus-margin{margin:0 5%}.chart-container>.title{margin-top:25px;margin-left:25px;text-align:left;font-weight:400;font-size:12px;color:#6c7680}.chart-container .graphics{margin-top:10px;padding-top:10px;padding-bottom:10px;position:relative}.chart-container .graph-stats-group{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around;-webkit-box-flex:1;-ms-flex:1;flex:1}.chart-container .graph-stats-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:10px}.chart-container .graph-stats-container:after,.chart-container .graph-stats-container:before{content:"";display:block}.chart-container .graph-stats-container .stats{padding-bottom:15px}.chart-container .graph-stats-container .stats-title{color:#8d99a6}.chart-container .graph-stats-container .stats-value{font-size:20px;font-weight:300}.chart-container .graph-stats-container .stats-description{font-size:12px;color:#8d99a6}.chart-container .graph-stats-container .graph-data .stats-value{color:#98d85b}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .percentage-graph .progress{margin-bottom:0}.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,.chart-container .multiaxis-chart .line-horizontal,.chart-container .multiaxis-chart .y-axis-guide{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 .progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.chart-container .progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#36414c;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.chart-container .graph-svg-tip{position:absolute;z-index:1;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.chart-container .graph-svg-tip ol,.chart-container .graph-svg-tip ul{padding-left:0;display:-webkit-box;display:-ms-flexbox;display:flex}.chart-container .graph-svg-tip ul.data-point-list li{min-width:90px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-weight:600}.chart-container .graph-svg-tip strong{color:#dfe2e5;font-weight:600}.chart-container .graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:" ";border:5px solid transparent;border-top-color:rgba(0,0,0,.8)}.chart-container .graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.chart-container .graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.chart-container .graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.chart-container .graph-svg-tip.comparison li{display:inline-block;padding:5px 10px}.chart-container .indicator,.chart-container .indicator-right{background:none;font-size:12px;vertical-align:middle;font-weight:700;color:#6c7680}.chart-container .indicator i{content:"";display:inline-block;height:8px;width:8px;border-radius:8px}.chart-container .indicator:before,.chart-container .indicator i{margin:0 4px 0 0}.chart-container .indicator-right:after{margin:0 0 0 4px} | |||
.chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .legend-dataset-text{fill:#6c7680;font-weight:600}.graph-svg-tip{position:absolute;z-index:1;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ol,.graph-svg-tip ul{padding-left:0;display:-webkit-box;display:-ms-flexbox;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-weight:600}.graph-svg-tip strong{color:#dfe2e5;font-weight:600}.graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:" ";border:5px solid transparent;border-top-color:rgba(0,0,0,.8)}.graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.graph-svg-tip.comparison li{display:inline-block;padding:5px 10px} |
@@ -0,0 +1,205 @@ | |||
import { SEC_IN_DAY, MONTH_NAMES_SHORT, clone, timestampToMidnight, timestampSec, addDays } from '../../../src/js/utils/date-utils'; | |||
import { getRandomBias } from '../../../src/js/utils/helpers'; | |||
// Composite Chart | |||
// ================================================================================ | |||
const reportCountList = [152, 222, 199, 287, 534, 709, | |||
1179, 1256, 1632, 1856, 1850]; | |||
export const lineCompositeData = { | |||
labels: ["2007", "2008", "2009", "2010", "2011", "2012", | |||
"2013", "2014", "2015", "2016", "2017"], | |||
yMarkers: [ | |||
{ | |||
label: "Average 100 reports/month", | |||
value: 1200, | |||
options: { labelPos: 'left' } | |||
} | |||
], | |||
datasets: [{ | |||
"name": "Events", | |||
"values": reportCountList | |||
}] | |||
}; | |||
export const fireball_5_25 = [ | |||
[4, 0, 3, 1, 1, 2, 1, 1, 1, 0, 1, 1], | |||
[2, 3, 3, 2, 1, 3, 0, 1, 2, 7, 10, 4], | |||
[5, 6, 2, 4, 0, 1, 4, 3, 0, 2, 0, 1], | |||
[0, 2, 6, 2, 1, 1, 2, 3, 6, 3, 7, 8], | |||
[6, 8, 7, 7, 4, 5, 6, 5, 22, 12, 10, 11], | |||
[7, 10, 11, 7, 3, 2, 7, 7, 11, 15, 22, 20], | |||
[13, 16, 21, 18, 19, 17, 12, 17, 31, 28, 25, 29], | |||
[24, 14, 21, 14, 11, 15, 19, 21, 41, 22, 32, 18], | |||
[31, 20, 30, 22, 14, 17, 21, 35, 27, 50, 117, 24], | |||
[32, 24, 21, 27, 11, 27, 43, 37, 44, 40, 48, 32], | |||
[31, 38, 36, 26, 23, 23, 25, 29, 26, 47, 61, 50], | |||
]; | |||
export const fireball_2_5 = [ | |||
[22, 6, 6, 9, 7, 8, 6, 14, 19, 10, 8, 20], | |||
[11, 13, 12, 8, 9, 11, 9, 13, 10, 22, 40, 24], | |||
[20, 13, 13, 19, 13, 10, 14, 13, 20, 18, 5, 9], | |||
[7, 13, 16, 19, 12, 11, 21, 27, 27, 24, 33, 33], | |||
[38, 25, 28, 22, 31, 21, 35, 42, 37, 32, 46, 53], | |||
[50, 33, 36, 34, 35, 28, 27, 52, 58, 59, 75, 69], | |||
[54, 67, 67, 45, 66, 51, 38, 64, 90, 113, 116, 87], | |||
[84, 52, 56, 51, 55, 46, 50, 87, 114, 83, 152, 93], | |||
[73, 58, 59, 63, 56, 51, 83, 140, 103, 115, 265, 89], | |||
[106, 95, 94, 71, 77, 75, 99, 136, 129, 154, 168, 156], | |||
[81, 102, 95, 72, 58, 91, 89, 122, 124, 135, 183, 171], | |||
]; | |||
export const fireballOver25 = [ | |||
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |||
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], | |||
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0], | |||
[1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0], | |||
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2], | |||
[3, 2, 1, 3, 2, 0, 2, 2, 2, 3, 0, 1], | |||
[2, 3, 5, 2, 1, 3, 0, 2, 3, 5, 1, 4], | |||
[7, 4, 6, 1, 9, 2, 2, 2, 20, 9, 4, 9], | |||
[5, 6, 1, 2, 5, 4, 5, 5, 16, 9, 14, 9], | |||
[5, 4, 7, 5, 1, 5, 3, 3, 5, 7, 22, 2], | |||
[5, 13, 11, 6, 1, 7, 9, 8, 14, 17, 16, 3], | |||
[8, 9, 8, 6, 4, 8, 5, 6, 14, 11, 21, 12] | |||
]; | |||
export const barCompositeData = { | |||
labels: MONTH_NAMES_SHORT, | |||
datasets: [ | |||
{ | |||
name: "Over 25 reports", | |||
values: fireballOver25[9], | |||
}, | |||
{ | |||
name: "5 to 25 reports", | |||
values: fireball_5_25[9], | |||
}, | |||
{ | |||
name: "2 to 5 reports", | |||
values: fireball_2_5[9] | |||
} | |||
] | |||
}; | |||
// Demo Chart multitype Chart | |||
// ================================================================================ | |||
export const typeData = { | |||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | |||
yMarkers: [ | |||
{ | |||
label: "Marker", | |||
value: 43, | |||
options: { labelPos: 'left' } | |||
// type: 'dashed' | |||
} | |||
], | |||
yRegions: [ | |||
{ | |||
label: "Region", | |||
start: -10, | |||
end: 50, | |||
options: { labelPos: 'right' } | |||
}, | |||
], | |||
datasets: [ | |||
{ | |||
name: "Some Data", | |||
values: [18, 40, 30, 35, 8, 52, 17, -4], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, | |||
{ | |||
name: "Another Set", | |||
values: [30, 50, -10, 15, 18, 32, 27, 14], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, | |||
{ | |||
name: "Yet Another", | |||
values: [15, 20, -3, -15, 58, 12, -17, 37], | |||
chartType: 'line' | |||
} | |||
] | |||
}; | |||
export const trendsData = { | |||
labels: [1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, | |||
1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, | |||
1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, | |||
1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, | |||
2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016] , | |||
datasets: [ | |||
{ | |||
values: [132.9, 150.0, 149.4, 148.0, 94.4, 97.6, 54.1, 49.2, 22.5, 18.4, | |||
39.3, 131.0, 220.1, 218.9, 198.9, 162.4, 91.0, 60.5, 20.6, 14.8, | |||
33.9, 123.0, 211.1, 191.8, 203.3, 133.0, 76.1, 44.9, 25.1, 11.6, | |||
28.9, 88.3, 136.3, 173.9, 170.4, 163.6, 99.3, 65.3, 45.8, 24.7, | |||
12.6, 4.2, 4.8, 24.9, 80.8, 84.5, 94.0, 113.3, 69.8, 39.8] | |||
} | |||
] | |||
}; | |||
export const moonData = { | |||
names: ["Ganymede", "Callisto", "Io", "Europa"], | |||
masses: [14819000, 10759000, 8931900, 4800000], | |||
distances: [1070.412, 1882.709, 421.700, 671.034], | |||
diameters: [5262.4, 4820.6, 3637.4, 3121.6], | |||
}; | |||
// const jupiterMoons = { | |||
// 'Ganymede': { | |||
// mass: '14819000 x 10^16 kg', | |||
// 'semi-major-axis': '1070412 km', | |||
// 'diameter': '5262.4 km' | |||
// }, | |||
// 'Callisto': { | |||
// mass: '10759000 x 10^16 kg', | |||
// 'semi-major-axis': '1882709 km', | |||
// 'diameter': '4820.6 km' | |||
// }, | |||
// 'Io': { | |||
// mass: '8931900 x 10^16 kg', | |||
// 'semi-major-axis': '421700 km', | |||
// 'diameter': '3637.4 km' | |||
// }, | |||
// 'Europa': { | |||
// mass: '4800000 x 10^16 kg', | |||
// 'semi-major-axis': '671034 km', | |||
// 'diameter': '3121.6 km' | |||
// }, | |||
// }; | |||
// ================================================================================ | |||
let today = new Date(); | |||
let start = clone(today); | |||
addDays(start, 4); | |||
let end = clone(start); | |||
start.setFullYear( start.getFullYear() - 2 ); | |||
end.setFullYear( end.getFullYear() - 1 ); | |||
export let dataPoints = {}; | |||
let startTs = timestampSec(start); | |||
let endTs = timestampSec(end); | |||
startTs = timestampToMidnight(startTs); | |||
endTs = timestampToMidnight(endTs, true); | |||
while (startTs < endTs) { | |||
dataPoints[parseInt(startTs)] = Math.floor(getRandomBias(0, 5, 0.2, 1)); | |||
startTs += SEC_IN_DAY; | |||
} | |||
export const heatmapData = { | |||
dataPoints: dataPoints, | |||
start: start, | |||
end: end | |||
}; |
@@ -1,91 +1,15 @@ | |||
// Composite Chart | |||
// ================================================================================ | |||
let reportCountList = [152, 222, 199, 287, 534, 709, | |||
1179, 1256, 1632, 1856, 1850]; | |||
let lineCompositeData = { | |||
labels: ["2007", "2008", "2009", "2010", "2011", "2012", | |||
"2013", "2014", "2015", "2016", "2017"], | |||
yMarkers: [ | |||
{ | |||
label: "Average 100 reports/month", | |||
value: 1200, | |||
} | |||
], | |||
datasets: [{ | |||
"name": "Events", | |||
"values": reportCountList | |||
}] | |||
}; | |||
import { shuffle, getRandomBias } from '../../../src/js/utils/helpers'; | |||
import { HEATMAP_COLORS_YELLOW, HEATMAP_COLORS_BLUE } from '../../../src/js/utils/constants'; | |||
import { fireballOver25, fireball_2_5, fireball_5_25, lineCompositeData, | |||
barCompositeData, typeData, trendsData, moonData, heatmapData } from './data'; | |||
let fireball_5_25 = [ | |||
[4, 0, 3, 1, 1, 2, 1, 1, 1, 0, 1, 1], | |||
[2, 3, 3, 2, 1, 3, 0, 1, 2, 7, 10, 4], | |||
[5, 6, 2, 4, 0, 1, 4, 3, 0, 2, 0, 1], | |||
[0, 2, 6, 2, 1, 1, 2, 3, 6, 3, 7, 8], | |||
[6, 8, 7, 7, 4, 5, 6, 5, 22, 12, 10, 11], | |||
[7, 10, 11, 7, 3, 2, 7, 7, 11, 15, 22, 20], | |||
[13, 16, 21, 18, 19, 17, 12, 17, 31, 28, 25, 29], | |||
[24, 14, 21, 14, 11, 15, 19, 21, 41, 22, 32, 18], | |||
[31, 20, 30, 22, 14, 17, 21, 35, 27, 50, 117, 24], | |||
[32, 24, 21, 27, 11, 27, 43, 37, 44, 40, 48, 32], | |||
[31, 38, 36, 26, 23, 23, 25, 29, 26, 47, 61, 50], | |||
]; | |||
let fireball_2_5 = [ | |||
[22, 6, 6, 9, 7, 8, 6, 14, 19, 10, 8, 20], | |||
[11, 13, 12, 8, 9, 11, 9, 13, 10, 22, 40, 24], | |||
[20, 13, 13, 19, 13, 10, 14, 13, 20, 18, 5, 9], | |||
[7, 13, 16, 19, 12, 11, 21, 27, 27, 24, 33, 33], | |||
[38, 25, 28, 22, 31, 21, 35, 42, 37, 32, 46, 53], | |||
[50, 33, 36, 34, 35, 28, 27, 52, 58, 59, 75, 69], | |||
[54, 67, 67, 45, 66, 51, 38, 64, 90, 113, 116, 87], | |||
[84, 52, 56, 51, 55, 46, 50, 87, 114, 83, 152, 93], | |||
[73, 58, 59, 63, 56, 51, 83, 140, 103, 115, 265, 89], | |||
[106, 95, 94, 71, 77, 75, 99, 136, 129, 154, 168, 156], | |||
[81, 102, 95, 72, 58, 91, 89, 122, 124, 135, 183, 171], | |||
]; | |||
let fireballOver25 = [ | |||
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |||
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], | |||
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0], | |||
[1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0], | |||
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2], | |||
[3, 2, 1, 3, 2, 0, 2, 2, 2, 3, 0, 1], | |||
[2, 3, 5, 2, 1, 3, 0, 2, 3, 5, 1, 4], | |||
[7, 4, 6, 1, 9, 2, 2, 2, 20, 9, 4, 9], | |||
[5, 6, 1, 2, 5, 4, 5, 5, 16, 9, 14, 9], | |||
[5, 4, 7, 5, 1, 5, 3, 3, 5, 7, 22, 2], | |||
[5, 13, 11, 6, 1, 7, 9, 8, 14, 17, 16, 3], | |||
[8, 9, 8, 6, 4, 8, 5, 6, 14, 11, 21, 12] | |||
]; | |||
let monthNames = ["January", "February", "March", "April", "May", "June", | |||
"July", "August", "September", "October", "November", "December"]; | |||
let barCompositeData = { | |||
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], | |||
datasets: [ | |||
{ | |||
name: "Over 25 reports", | |||
values: fireballOver25[9], | |||
}, | |||
{ | |||
name: "5 to 25 reports", | |||
values: fireball_5_25[9], | |||
}, | |||
{ | |||
name: "2 to 5 reports", | |||
values: fireball_2_5[9] | |||
} | |||
] | |||
}; | |||
// ================================================================================ | |||
let c1 = document.querySelector("#chart-composite-1"); | |||
let c2 = document.querySelector("#chart-composite-2"); | |||
let Chart = frappe.Chart; // eslint-disable-line no-undef | |||
let lineCompositeChart = new Chart (c1, { | |||
title: "Fireball/Bolide Events - Yearly (reported)", | |||
data: lineCompositeData, | |||
@@ -105,7 +29,7 @@ let lineCompositeChart = new Chart (c1, { | |||
let barCompositeChart = new Chart (c2, { | |||
data: barCompositeData, | |||
type: 'bar', | |||
height: 190, | |||
height: 210, | |||
colors: ['violet', 'light-blue', '#46a9f9'], | |||
valuesOverPoints: 1, | |||
axisOptions: { | |||
@@ -124,82 +48,26 @@ lineCompositeChart.parent.addEventListener('data-select', (e) => { | |||
]); | |||
}); | |||
// Demo Chart (bar, linepts, scatter(blobs), percentage) | |||
// ================================================================================ | |||
let typeData = { | |||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | |||
yMarkers: [ | |||
{ | |||
label: "Marker", | |||
value: 43, | |||
// type: 'dashed' | |||
} | |||
], | |||
yRegions: [ | |||
{ | |||
label: "Region", | |||
start: -10, | |||
end: 50 | |||
}, | |||
], | |||
datasets: [ | |||
{ | |||
name: "Some Data", | |||
values: [18, 40, 30, 35, 8, 52, 17, -4], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, | |||
{ | |||
name: "Another Set", | |||
values: [30, 50, -10, 15, 18, 32, 27, 14], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, | |||
{ | |||
name: "Yet Another", | |||
values: [15, 20, -3, -15, 58, 12, -17, 37], | |||
chartType: 'line' | |||
} | |||
] | |||
}; | |||
// let typeChart = new Chart("#chart-types", { | |||
// title: "My Awesome Chart", | |||
// data: typeData, | |||
// type: 'bar', | |||
// height: 250, | |||
// colors: ['purple', 'magenta', 'red'], | |||
// tooltipOptions: { | |||
// formatTooltipX: d => (d + '').toUpperCase(), | |||
// formatTooltipY: d => d + ' pts', | |||
// } | |||
// }); | |||
// Aggregation chart | |||
// ================================================================================ | |||
let args = { | |||
let customColors = ['purple', 'magenta', 'light-blue']; | |||
let typeChartArgs = { | |||
title: "My Awesome Chart", | |||
data: typeData, | |||
type: 'axis-mixed', | |||
height: 250, | |||
colors: ['purple', 'magenta', 'light-blue'], | |||
height: 300, | |||
colors: customColors, | |||
maxLegendPoints: 6, | |||
// maxLegendPoints: 6, | |||
maxSlices: 10, | |||
tooltipOptions: { | |||
formatTooltipX: d => (d + '').toUpperCase(), | |||
formatTooltipY: d => d + ' pts', | |||
} | |||
} | |||
let aggrChart = new Chart("#chart-aggr", args); | |||
}; | |||
let aggrChart = new Chart("#chart-aggr", typeChartArgs); | |||
Array.prototype.slice.call( | |||
document.querySelectorAll('.aggr-type-buttons button') | |||
@@ -207,9 +75,20 @@ Array.prototype.slice.call( | |||
el.addEventListener('click', (e) => { | |||
let btn = e.target; | |||
let type = btn.getAttribute('data-type'); | |||
args.type = type; | |||
typeChartArgs.type = type; | |||
if(type !== 'axis-mixed') { | |||
typeChartArgs.colors = undefined; | |||
} else { | |||
typeChartArgs.colors = customColors; | |||
} | |||
if(type !== 'percentage') { | |||
typeChartArgs.height = 300; | |||
} else { | |||
typeChartArgs.height = undefined; | |||
} | |||
let newChart = new Chart("#chart-aggr", args);; | |||
let newChart = new Chart("#chart-aggr", typeChartArgs); | |||
if(newChart){ | |||
aggrChart = newChart; | |||
} | |||
@@ -221,27 +100,31 @@ Array.prototype.slice.call( | |||
}); | |||
}); | |||
document.querySelector('.export-aggr').addEventListener('click', () => { | |||
aggrChart.export(); | |||
}); | |||
// Update values chart | |||
// ================================================================================ | |||
let update_data_all_labels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", | |||
let updateDataAllLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", | |||
"Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", | |||
"Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"]; | |||
let getRandom = () => Math.floor(Math.random() * 75 - 15); | |||
let update_data_all_values = Array.from({length: 30}, getRandom); | |||
let getRandom = () => Math.floor(getRandomBias(-40, 60, 0.8, 1)); | |||
let updateDataAllValues = Array.from({length: 30}, getRandom); | |||
// We're gonna be shuffling this | |||
let update_data_all_indices = update_data_all_labels.map((d,i) => i); | |||
let updateDataAllIndices = updateDataAllLabels.map((d,i) => i); | |||
let get_update_data = (source_array, length=10) => { | |||
let indices = update_data_all_indices.slice(0, length); | |||
let getUpdateData = (source_array, length=10) => { | |||
let indices = updateDataAllIndices.slice(0, length); | |||
return indices.map((index) => source_array[index]); | |||
}; | |||
let update_data = { | |||
labels: get_update_data(update_data_all_labels), | |||
let updateData = { | |||
labels: getUpdateData(updateDataAllLabels), | |||
datasets: [{ | |||
"values": get_update_data(update_data_all_values) | |||
"values": getUpdateData(updateDataAllValues) | |||
}], | |||
yMarkers: [ | |||
{ | |||
@@ -259,10 +142,10 @@ let update_data = { | |||
], | |||
}; | |||
let update_chart = new Chart("#chart-update", { | |||
data: update_data, | |||
let updateChart = new Chart("#chart-update", { | |||
data: updateData, | |||
type: 'line', | |||
height: 250, | |||
height: 300, | |||
colors: ['#ff6c03'], | |||
lineOptions: { | |||
// hideLine: 1, | |||
@@ -270,16 +153,16 @@ let update_chart = new Chart("#chart-update", { | |||
}, | |||
}); | |||
let chart_update_buttons = document.querySelector('.chart-update-buttons'); | |||
let chartUpdateButtons = document.querySelector('.chart-update-buttons'); | |||
chart_update_buttons.querySelector('[data-update="random"]').addEventListener("click", (e) => { | |||
shuffle(update_data_all_indices); | |||
chartUpdateButtons.querySelector('[data-update="random"]').addEventListener("click", () => { | |||
shuffle(updateDataAllIndices); | |||
let value = getRandom(); | |||
let start = getRandom(); | |||
let end = getRandom(); | |||
let data = { | |||
labels: update_data_all_labels.slice(0, 10), | |||
datasets: [{values: get_update_data(update_data_all_values)}], | |||
labels: updateDataAllLabels.slice(0, 10), | |||
datasets: [{values: getUpdateData(updateDataAllValues)}], | |||
yMarkers: [ | |||
{ | |||
label: "Altitude", | |||
@@ -294,46 +177,34 @@ chart_update_buttons.querySelector('[data-update="random"]').addEventListener("c | |||
end: end | |||
}, | |||
], | |||
} | |||
update_chart.update(data); | |||
}; | |||
updateChart.update(data); | |||
}); | |||
chart_update_buttons.querySelector('[data-update="add"]').addEventListener("click", (e) => { | |||
let index = update_chart.state.datasetLength; // last index to add | |||
if(index >= update_data_all_indices.length) return; | |||
update_chart.addDataPoint( | |||
update_data_all_labels[index], [update_data_all_values[index]] | |||
chartUpdateButtons.querySelector('[data-update="add"]').addEventListener("click", () => { | |||
let index = updateChart.state.datasetLength; // last index to add | |||
if(index >= updateDataAllIndices.length) return; | |||
updateChart.addDataPoint( | |||
updateDataAllLabels[index], [updateDataAllValues[index]] | |||
); | |||
}); | |||
chart_update_buttons.querySelector('[data-update="remove"]').addEventListener("click", (e) => { | |||
update_chart.removeDataPoint(); | |||
chartUpdateButtons.querySelector('[data-update="remove"]').addEventListener("click", () => { | |||
updateChart.removeDataPoint(); | |||
}); | |||
document.querySelector('.export-update').addEventListener('click', () => { | |||
updateChart.export(); | |||
}); | |||
// Trends Chart | |||
// ================================================================================ | |||
let trends_data = { | |||
labels: [1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, | |||
1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, | |||
1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, | |||
1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, | |||
2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016] , | |||
datasets: [ | |||
{ | |||
values: [132.9, 150.0, 149.4, 148.0, 94.4, 97.6, 54.1, 49.2, 22.5, 18.4, | |||
39.3, 131.0, 220.1, 218.9, 198.9, 162.4, 91.0, 60.5, 20.6, 14.8, | |||
33.9, 123.0, 211.1, 191.8, 203.3, 133.0, 76.1, 44.9, 25.1, 11.6, | |||
28.9, 88.3, 136.3, 173.9, 170.4, 163.6, 99.3, 65.3, 45.8, 24.7, | |||
12.6, 4.2, 4.8, 24.9, 80.8, 84.5, 94.0, 113.3, 69.8, 39.8] | |||
} | |||
] | |||
}; | |||
let plotChartArgs = { | |||
title: "Mean Total Sunspot Count - Yearly", | |||
data: trends_data, | |||
data: trendsData, | |||
type: 'line', | |||
height: 250, | |||
height: 300, | |||
colors: ['#238e38'], | |||
lineOptions: { | |||
hideDots: 1, | |||
@@ -346,7 +217,7 @@ let plotChartArgs = { | |||
} | |||
}; | |||
new Chart("#chart-trends", plotChartArgs); | |||
let trendsChart = new Chart("#chart-trends", plotChartArgs); | |||
Array.prototype.slice.call( | |||
document.querySelectorAll('.chart-plot-buttons button') | |||
@@ -374,89 +245,59 @@ Array.prototype.slice.call( | |||
}); | |||
}); | |||
document.querySelector('.export-trends').addEventListener('click', () => { | |||
trendsChart.export(); | |||
}); | |||
// Event chart | |||
// ================================================================================ | |||
let moon_names = ["Ganymede", "Callisto", "Io", "Europa"]; | |||
let masses = [14819000, 10759000, 8931900, 4800000]; | |||
let distances = [1070.412, 1882.709, 421.700, 671.034]; | |||
let diameters = [5262.4, 4820.6, 3637.4, 3121.6]; | |||
let jupiter_moons = { | |||
'Ganymede': { | |||
mass: '14819000 x 10^16 kg', | |||
'semi-major-axis': '1070412 km', | |||
'diameter': '5262.4 km' | |||
}, | |||
'Callisto': { | |||
mass: '10759000 x 10^16 kg', | |||
'semi-major-axis': '1882709 km', | |||
'diameter': '4820.6 km' | |||
}, | |||
'Io': { | |||
mass: '8931900 x 10^16 kg', | |||
'semi-major-axis': '421700 km', | |||
'diameter': '3637.4 km' | |||
}, | |||
'Europa': { | |||
mass: '4800000 x 10^16 kg', | |||
'semi-major-axis': '671034 km', | |||
'diameter': '3121.6 km' | |||
}, | |||
}; | |||
let events_data = { | |||
let eventsData = { | |||
labels: ["Ganymede", "Callisto", "Io", "Europa"], | |||
datasets: [ | |||
{ | |||
"values": distances, | |||
"formatted": distances.map(d => d*1000 + " km") | |||
"values": moonData.distances, | |||
"formatted": moonData.distances.map(d => d*1000 + " km") | |||
} | |||
] | |||
}; | |||
let events_chart = new Chart("#chart-events", { | |||
let eventsChart = new Chart("#chart-events", { | |||
title: "Jupiter's Moons: Semi-major Axis (1000 km)", | |||
data: events_data, | |||
data: eventsData, | |||
type: 'bar', | |||
height: 250, | |||
height: 330, | |||
colors: ['grey'], | |||
isNavigable: 1, | |||
}); | |||
let data_div = document.querySelector('.chart-events-data'); | |||
let dataDiv = document.querySelector('.chart-events-data'); | |||
events_chart.parent.addEventListener('data-select', (e) => { | |||
let name = moon_names[e.index]; | |||
data_div.querySelector('.moon-name').innerHTML = name; | |||
data_div.querySelector('.semi-major-axis').innerHTML = distances[e.index] * 1000; | |||
data_div.querySelector('.mass').innerHTML = masses[e.index]; | |||
data_div.querySelector('.diameter').innerHTML = diameters[e.index]; | |||
data_div.querySelector('img').src = "./assets/img/" + name.toLowerCase() + ".jpg"; | |||
eventsChart.parent.addEventListener('data-select', (e) => { | |||
let name = moonData.names[e.index]; | |||
dataDiv.querySelector('.moon-name').innerHTML = name; | |||
dataDiv.querySelector('.semi-major-axis').innerHTML = moonData.distances[e.index] * 1000; | |||
dataDiv.querySelector('.mass').innerHTML = moonData.masses[e.index]; | |||
dataDiv.querySelector('.diameter').innerHTML = moonData.diameters[e.index]; | |||
dataDiv.querySelector('img').src = "./assets/img/" + name.toLowerCase() + ".jpg"; | |||
}); | |||
// Heatmap | |||
// ================================================================================ | |||
let heatmapData = {}; | |||
let current_date = new Date(); | |||
let timestamp = current_date.getTime()/1000; | |||
timestamp = Math.floor(timestamp - (timestamp % 86400)).toFixed(1); // convert to midnight | |||
for (var i = 0; i< 375; i++) { | |||
heatmapData[parseInt(timestamp)] = Math.floor(Math.random() * 5); | |||
timestamp = Math.floor(timestamp - 86400).toFixed(1); | |||
} | |||
let heatmap = new Chart("#chart-heatmap", { | |||
let heatmapArgs = { | |||
title: "Monthly Distribution", | |||
data: heatmapData, | |||
type: 'heatmap', | |||
legendScale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discreteDomains: 1, | |||
legendColors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'] | |||
}); | |||
// console.log(heatmapData, heatmap); | |||
countLabel: 'Level', | |||
colors: HEATMAP_COLORS_BLUE, | |||
legendScale: [0, 1, 2, 4, 5] | |||
}; | |||
let heatmapChart = new Chart("#chart-heatmap", heatmapArgs); | |||
Array.prototype.slice.call( | |||
document.querySelectorAll('.heatmap-mode-buttons button') | |||
@@ -475,17 +316,14 @@ Array.prototype.slice.call( | |||
.querySelector('.heatmap-color-buttons .active') | |||
.getAttribute('data-color'); | |||
if(colors_mode === 'halloween') { | |||
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | |||
colors = HEATMAP_COLORS_YELLOW; | |||
} else if (colors_mode === 'blue') { | |||
colors = HEATMAP_COLORS_BLUE; | |||
} | |||
new Chart("#chart-heatmap", { | |||
data: heatmapData, | |||
type: 'heatmap', | |||
legendScale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discreteDomains: discreteDomains, | |||
legendColors: colors | |||
}); | |||
heatmapArgs.discreteDomains = discreteDomains; | |||
heatmapArgs.colors = colors; | |||
new Chart("#chart-heatmap", heatmapArgs); | |||
Array.prototype.slice.call( | |||
btn.parentNode.querySelectorAll('button')).map(el => { | |||
@@ -504,7 +342,9 @@ Array.prototype.slice.call( | |||
let colors = []; | |||
if(colors_mode === 'halloween') { | |||
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | |||
colors = HEATMAP_COLORS_YELLOW; | |||
} else if (colors_mode === 'blue') { | |||
colors = HEATMAP_COLORS_BLUE; | |||
} | |||
let discreteDomains = 1; | |||
@@ -516,14 +356,9 @@ Array.prototype.slice.call( | |||
discreteDomains = 0; | |||
} | |||
new Chart("#chart-heatmap", { | |||
data: heatmapData, | |||
type: 'heatmap', | |||
legendScale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discreteDomains: discreteDomains, | |||
legendColors: colors | |||
}); | |||
heatmapArgs.discreteDomains = discreteDomains; | |||
heatmapArgs.colors = colors; | |||
new Chart("#chart-heatmap", heatmapArgs); | |||
Array.prototype.slice.call( | |||
btn.parentNode.querySelectorAll('button')).map(el => { | |||
@@ -533,28 +368,6 @@ Array.prototype.slice.call( | |||
}); | |||
}); | |||
// Helpers | |||
// ================================================================================ | |||
function shuffle(array) { | |||
// https://stackoverflow.com/a/2450976/6495043 | |||
// Awesomeness: https://bost.ocks.org/mike/shuffle/ | |||
var currentIndex = array.length, temporaryValue, randomIndex; | |||
// While there remain elements to shuffle... | |||
while (0 !== currentIndex) { | |||
// Pick a remaining element... | |||
randomIndex = Math.floor(Math.random() * currentIndex); | |||
currentIndex -= 1; | |||
// And swap it with the current element. | |||
temporaryValue = array[currentIndex]; | |||
array[currentIndex] = array[randomIndex]; | |||
array[randomIndex] = temporaryValue; | |||
} | |||
return array; | |||
} | |||
document.querySelector('.export-heatmap').addEventListener('click', () => { | |||
heatmapChart.export(); | |||
}); |
@@ -0,0 +1,653 @@ | |||
(function () { | |||
'use strict'; | |||
function __$styleInject(css, ref) { | |||
if ( ref === void 0 ) ref = {}; | |||
var insertAt = ref.insertAt; | |||
if (!css || typeof document === 'undefined') { return; } | |||
var head = document.head || document.getElementsByTagName('head')[0]; | |||
var style = document.createElement('style'); | |||
style.type = 'text/css'; | |||
if (insertAt === 'top') { | |||
if (head.firstChild) { | |||
head.insertBefore(style, head.firstChild); | |||
} else { | |||
head.appendChild(style); | |||
} | |||
} else { | |||
head.appendChild(style); | |||
} | |||
if (style.styleSheet) { | |||
style.styleSheet.cssText = css; | |||
} else { | |||
style.appendChild(document.createTextNode(css)); | |||
} | |||
} | |||
// Fixed 5-color theme, | |||
// More colors are difficult to parse visually | |||
var HEATMAP_COLORS_BLUE = ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e']; | |||
var HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | |||
// Universal constants | |||
/** | |||
* Returns the value of a number upto 2 decimal places. | |||
* @param {Number} d Any number | |||
*/ | |||
/** | |||
* Returns whether or not two given arrays are equal. | |||
* @param {Array} arr1 First array | |||
* @param {Array} arr2 Second array | |||
*/ | |||
/** | |||
* Shuffles array in place. ES6 version | |||
* @param {Array} array An array containing the items. | |||
*/ | |||
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 (var i = array.length - 1; i > 0; i--) { | |||
var j = Math.floor(Math.random() * (i + 1)); | |||
var _ref = [array[j], array[i]]; | |||
array[i] = _ref[0]; | |||
array[j] = _ref[1]; | |||
} | |||
return array; | |||
} | |||
/** | |||
* Fill an array with extra points | |||
* @param {Array} array Array | |||
* @param {Number} count number of filler elements | |||
* @param {Object} element element to fill with | |||
* @param {Boolean} start fill at start? | |||
*/ | |||
/** | |||
* Returns pixel width of string. | |||
* @param {String} string | |||
* @param {Number} charWidth Width of single char in pixels | |||
*/ | |||
// https://stackoverflow.com/a/29325222 | |||
function getRandomBias(min, max, bias, influence) { | |||
var range = max - min; | |||
var 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 | |||
} | |||
// Playing around with dates | |||
var NO_OF_MILLIS = 1000; | |||
var SEC_IN_DAY = 86400; | |||
var MONTH_NAMES_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | |||
function clone(date) { | |||
return new Date(date.getTime()); | |||
} | |||
function timestampSec(date) { | |||
return date.getTime() / NO_OF_MILLIS; | |||
} | |||
function timestampToMidnight(timestamp) { | |||
var roundAhead = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; | |||
var midnightTs = Math.floor(timestamp - timestamp % SEC_IN_DAY); | |||
if (roundAhead) { | |||
return midnightTs + SEC_IN_DAY; | |||
} | |||
return midnightTs; | |||
} | |||
// export function getMonthsBetween(startDate, endDate) {} | |||
// mutates | |||
// mutates | |||
function addDays(date, numberOfDays) { | |||
date.setDate(date.getDate() + numberOfDays); | |||
} | |||
// Composite Chart | |||
// ================================================================================ | |||
var reportCountList = [152, 222, 199, 287, 534, 709, 1179, 1256, 1632, 1856, 1850]; | |||
var lineCompositeData = { | |||
labels: ["2007", "2008", "2009", "2010", "2011", "2012", "2013", "2014", "2015", "2016", "2017"], | |||
yMarkers: [{ | |||
label: "Average 100 reports/month", | |||
value: 1200, | |||
options: { labelPos: 'left' } | |||
}], | |||
datasets: [{ | |||
"name": "Events", | |||
"values": reportCountList | |||
}] | |||
}; | |||
var fireball_5_25 = [[4, 0, 3, 1, 1, 2, 1, 1, 1, 0, 1, 1], [2, 3, 3, 2, 1, 3, 0, 1, 2, 7, 10, 4], [5, 6, 2, 4, 0, 1, 4, 3, 0, 2, 0, 1], [0, 2, 6, 2, 1, 1, 2, 3, 6, 3, 7, 8], [6, 8, 7, 7, 4, 5, 6, 5, 22, 12, 10, 11], [7, 10, 11, 7, 3, 2, 7, 7, 11, 15, 22, 20], [13, 16, 21, 18, 19, 17, 12, 17, 31, 28, 25, 29], [24, 14, 21, 14, 11, 15, 19, 21, 41, 22, 32, 18], [31, 20, 30, 22, 14, 17, 21, 35, 27, 50, 117, 24], [32, 24, 21, 27, 11, 27, 43, 37, 44, 40, 48, 32], [31, 38, 36, 26, 23, 23, 25, 29, 26, 47, 61, 50]]; | |||
var fireball_2_5 = [[22, 6, 6, 9, 7, 8, 6, 14, 19, 10, 8, 20], [11, 13, 12, 8, 9, 11, 9, 13, 10, 22, 40, 24], [20, 13, 13, 19, 13, 10, 14, 13, 20, 18, 5, 9], [7, 13, 16, 19, 12, 11, 21, 27, 27, 24, 33, 33], [38, 25, 28, 22, 31, 21, 35, 42, 37, 32, 46, 53], [50, 33, 36, 34, 35, 28, 27, 52, 58, 59, 75, 69], [54, 67, 67, 45, 66, 51, 38, 64, 90, 113, 116, 87], [84, 52, 56, 51, 55, 46, 50, 87, 114, 83, 152, 93], [73, 58, 59, 63, 56, 51, 83, 140, 103, 115, 265, 89], [106, 95, 94, 71, 77, 75, 99, 136, 129, 154, 168, 156], [81, 102, 95, 72, 58, 91, 89, 122, 124, 135, 183, 171]]; | |||
var fireballOver25 = [ | |||
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |||
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0], [1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2], [3, 2, 1, 3, 2, 0, 2, 2, 2, 3, 0, 1], [2, 3, 5, 2, 1, 3, 0, 2, 3, 5, 1, 4], [7, 4, 6, 1, 9, 2, 2, 2, 20, 9, 4, 9], [5, 6, 1, 2, 5, 4, 5, 5, 16, 9, 14, 9], [5, 4, 7, 5, 1, 5, 3, 3, 5, 7, 22, 2], [5, 13, 11, 6, 1, 7, 9, 8, 14, 17, 16, 3], [8, 9, 8, 6, 4, 8, 5, 6, 14, 11, 21, 12]]; | |||
var barCompositeData = { | |||
labels: MONTH_NAMES_SHORT, | |||
datasets: [{ | |||
name: "Over 25 reports", | |||
values: fireballOver25[9] | |||
}, { | |||
name: "5 to 25 reports", | |||
values: fireball_5_25[9] | |||
}, { | |||
name: "2 to 5 reports", | |||
values: fireball_2_5[9] | |||
}] | |||
}; | |||
// Demo Chart multitype Chart | |||
// ================================================================================ | |||
var typeData = { | |||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", "12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | |||
yMarkers: [{ | |||
label: "Marker", | |||
value: 43, | |||
options: { labelPos: 'left' | |||
// type: 'dashed' | |||
} }], | |||
yRegions: [{ | |||
label: "Region", | |||
start: -10, | |||
end: 50, | |||
options: { labelPos: 'right' } | |||
}], | |||
datasets: [{ | |||
name: "Some Data", | |||
values: [18, 40, 30, 35, 8, 52, 17, -4], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, { | |||
name: "Another Set", | |||
values: [30, 50, -10, 15, 18, 32, 27, 14], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, { | |||
name: "Yet Another", | |||
values: [15, 20, -3, -15, 58, 12, -17, 37], | |||
chartType: 'line' | |||
}] | |||
}; | |||
var trendsData = { | |||
labels: [1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016], | |||
datasets: [{ | |||
values: [132.9, 150.0, 149.4, 148.0, 94.4, 97.6, 54.1, 49.2, 22.5, 18.4, 39.3, 131.0, 220.1, 218.9, 198.9, 162.4, 91.0, 60.5, 20.6, 14.8, 33.9, 123.0, 211.1, 191.8, 203.3, 133.0, 76.1, 44.9, 25.1, 11.6, 28.9, 88.3, 136.3, 173.9, 170.4, 163.6, 99.3, 65.3, 45.8, 24.7, 12.6, 4.2, 4.8, 24.9, 80.8, 84.5, 94.0, 113.3, 69.8, 39.8] | |||
}] | |||
}; | |||
var moonData = { | |||
names: ["Ganymede", "Callisto", "Io", "Europa"], | |||
masses: [14819000, 10759000, 8931900, 4800000], | |||
distances: [1070.412, 1882.709, 421.700, 671.034], | |||
diameters: [5262.4, 4820.6, 3637.4, 3121.6] | |||
}; | |||
// const jupiterMoons = { | |||
// 'Ganymede': { | |||
// mass: '14819000 x 10^16 kg', | |||
// 'semi-major-axis': '1070412 km', | |||
// 'diameter': '5262.4 km' | |||
// }, | |||
// 'Callisto': { | |||
// mass: '10759000 x 10^16 kg', | |||
// 'semi-major-axis': '1882709 km', | |||
// 'diameter': '4820.6 km' | |||
// }, | |||
// 'Io': { | |||
// mass: '8931900 x 10^16 kg', | |||
// 'semi-major-axis': '421700 km', | |||
// 'diameter': '3637.4 km' | |||
// }, | |||
// 'Europa': { | |||
// mass: '4800000 x 10^16 kg', | |||
// 'semi-major-axis': '671034 km', | |||
// 'diameter': '3121.6 km' | |||
// }, | |||
// }; | |||
// ================================================================================ | |||
var today = new Date(); | |||
var start = clone(today); | |||
addDays(start, 4); | |||
var end = clone(start); | |||
start.setFullYear(start.getFullYear() - 2); | |||
end.setFullYear(end.getFullYear() - 1); | |||
var dataPoints = {}; | |||
var startTs = timestampSec(start); | |||
var endTs = timestampSec(end); | |||
startTs = timestampToMidnight(startTs); | |||
endTs = timestampToMidnight(endTs, true); | |||
while (startTs < endTs) { | |||
dataPoints[parseInt(startTs)] = Math.floor(getRandomBias(0, 5, 0.2, 1)); | |||
startTs += SEC_IN_DAY; | |||
} | |||
var heatmapData = { | |||
dataPoints: dataPoints, | |||
start: start, | |||
end: end | |||
}; | |||
// ================================================================================ | |||
var c1 = document.querySelector("#chart-composite-1"); | |||
var c2 = document.querySelector("#chart-composite-2"); | |||
var Chart = frappe.Chart; // eslint-disable-line no-undef | |||
var lineCompositeChart = new Chart(c1, { | |||
title: "Fireball/Bolide Events - Yearly (reported)", | |||
data: lineCompositeData, | |||
type: 'line', | |||
height: 190, | |||
colors: ['green'], | |||
isNavigable: 1, | |||
valuesOverPoints: 1, | |||
lineOptions: { | |||
dotSize: 8 | |||
} | |||
// yAxisMode: 'tick' | |||
// regionFill: 1 | |||
}); | |||
var barCompositeChart = new Chart(c2, { | |||
data: barCompositeData, | |||
type: 'bar', | |||
height: 210, | |||
colors: ['violet', 'light-blue', '#46a9f9'], | |||
valuesOverPoints: 1, | |||
axisOptions: { | |||
xAxisMode: 'tick' | |||
}, | |||
barOptions: { | |||
stacked: 1 | |||
} | |||
}); | |||
lineCompositeChart.parent.addEventListener('data-select', function (e) { | |||
var i = e.index; | |||
barCompositeChart.updateDatasets([fireballOver25[i], fireball_5_25[i], fireball_2_5[i]]); | |||
}); | |||
// ================================================================================ | |||
var customColors = ['purple', 'magenta', 'light-blue']; | |||
var typeChartArgs = { | |||
title: "My Awesome Chart", | |||
data: typeData, | |||
type: 'axis-mixed', | |||
height: 300, | |||
colors: customColors, | |||
// maxLegendPoints: 6, | |||
maxSlices: 10, | |||
tooltipOptions: { | |||
formatTooltipX: function formatTooltipX(d) { | |||
return (d + '').toUpperCase(); | |||
}, | |||
formatTooltipY: function formatTooltipY(d) { | |||
return d + ' pts'; | |||
} | |||
} | |||
}; | |||
var aggrChart = new Chart("#chart-aggr", typeChartArgs); | |||
Array.prototype.slice.call(document.querySelectorAll('.aggr-type-buttons button')).map(function (el) { | |||
el.addEventListener('click', function (e) { | |||
var btn = e.target; | |||
var type = btn.getAttribute('data-type'); | |||
typeChartArgs.type = type; | |||
if (type !== 'axis-mixed') { | |||
typeChartArgs.colors = undefined; | |||
} else { | |||
typeChartArgs.colors = customColors; | |||
} | |||
if (type !== 'percentage') { | |||
typeChartArgs.height = 300; | |||
} else { | |||
typeChartArgs.height = undefined; | |||
} | |||
var newChart = new Chart("#chart-aggr", typeChartArgs); | |||
if (newChart) { | |||
aggrChart = newChart; | |||
} | |||
Array.prototype.slice.call(btn.parentNode.querySelectorAll('button')).map(function (el) { | |||
el.classList.remove('active'); | |||
}); | |||
btn.classList.add('active'); | |||
}); | |||
}); | |||
document.querySelector('.export-aggr').addEventListener('click', function () { | |||
aggrChart.export(); | |||
}); | |||
// Update values chart | |||
// ================================================================================ | |||
var updateDataAllLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"]; | |||
var getRandom = function getRandom() { | |||
return Math.floor(getRandomBias(-40, 60, 0.8, 1)); | |||
}; | |||
var updateDataAllValues = Array.from({ length: 30 }, getRandom); | |||
// We're gonna be shuffling this | |||
var updateDataAllIndices = updateDataAllLabels.map(function (d, i) { | |||
return i; | |||
}); | |||
var getUpdateData = function getUpdateData(source_array) { | |||
var length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10; | |||
var indices = updateDataAllIndices.slice(0, length); | |||
return indices.map(function (index) { | |||
return source_array[index]; | |||
}); | |||
}; | |||
var updateData = { | |||
labels: getUpdateData(updateDataAllLabels), | |||
datasets: [{ | |||
"values": getUpdateData(updateDataAllValues) | |||
}], | |||
yMarkers: [{ | |||
label: "Altitude", | |||
value: 25, | |||
type: 'dashed' | |||
}], | |||
yRegions: [{ | |||
label: "Range", | |||
start: 10, | |||
end: 45 | |||
}] | |||
}; | |||
var updateChart = new Chart("#chart-update", { | |||
data: updateData, | |||
type: 'line', | |||
height: 300, | |||
colors: ['#ff6c03'], | |||
lineOptions: { | |||
// hideLine: 1, | |||
regionFill: 1 | |||
} | |||
}); | |||
var chartUpdateButtons = document.querySelector('.chart-update-buttons'); | |||
chartUpdateButtons.querySelector('[data-update="random"]').addEventListener("click", function () { | |||
shuffle(updateDataAllIndices); | |||
var value = getRandom(); | |||
var start = getRandom(); | |||
var end = getRandom(); | |||
var data = { | |||
labels: updateDataAllLabels.slice(0, 10), | |||
datasets: [{ values: getUpdateData(updateDataAllValues) }], | |||
yMarkers: [{ | |||
label: "Altitude", | |||
value: value, | |||
type: 'dashed' | |||
}], | |||
yRegions: [{ | |||
label: "Range", | |||
start: start, | |||
end: end | |||
}] | |||
}; | |||
updateChart.update(data); | |||
}); | |||
chartUpdateButtons.querySelector('[data-update="add"]').addEventListener("click", function () { | |||
var index = updateChart.state.datasetLength; // last index to add | |||
if (index >= updateDataAllIndices.length) return; | |||
updateChart.addDataPoint(updateDataAllLabels[index], [updateDataAllValues[index]]); | |||
}); | |||
chartUpdateButtons.querySelector('[data-update="remove"]').addEventListener("click", function () { | |||
updateChart.removeDataPoint(); | |||
}); | |||
document.querySelector('.export-update').addEventListener('click', function () { | |||
updateChart.export(); | |||
}); | |||
// Trends Chart | |||
// ================================================================================ | |||
var plotChartArgs = { | |||
title: "Mean Total Sunspot Count - Yearly", | |||
data: trendsData, | |||
type: 'line', | |||
height: 300, | |||
colors: ['#238e38'], | |||
lineOptions: { | |||
hideDots: 1, | |||
heatline: 1 | |||
}, | |||
axisOptions: { | |||
xAxisMode: 'tick', | |||
yAxisMode: 'span', | |||
xIsSeries: 1 | |||
} | |||
}; | |||
var trendsChart = new Chart("#chart-trends", plotChartArgs); | |||
Array.prototype.slice.call(document.querySelectorAll('.chart-plot-buttons button')).map(function (el) { | |||
el.addEventListener('click', function (e) { | |||
var btn = e.target; | |||
var type = btn.getAttribute('data-type'); | |||
var config = {}; | |||
config[type] = 1; | |||
if (['regionFill', 'heatline'].includes(type)) { | |||
config.hideDots = 1; | |||
} | |||
// plotChartArgs.init = false; | |||
plotChartArgs.lineOptions = config; | |||
new Chart("#chart-trends", plotChartArgs); | |||
Array.prototype.slice.call(btn.parentNode.querySelectorAll('button')).map(function (el) { | |||
el.classList.remove('active'); | |||
}); | |||
btn.classList.add('active'); | |||
}); | |||
}); | |||
document.querySelector('.export-trends').addEventListener('click', function () { | |||
trendsChart.export(); | |||
}); | |||
// Event chart | |||
// ================================================================================ | |||
var eventsData = { | |||
labels: ["Ganymede", "Callisto", "Io", "Europa"], | |||
datasets: [{ | |||
"values": moonData.distances, | |||
"formatted": moonData.distances.map(function (d) { | |||
return d * 1000 + " km"; | |||
}) | |||
}] | |||
}; | |||
var eventsChart = new Chart("#chart-events", { | |||
title: "Jupiter's Moons: Semi-major Axis (1000 km)", | |||
data: eventsData, | |||
type: 'bar', | |||
height: 330, | |||
colors: ['grey'], | |||
isNavigable: 1 | |||
}); | |||
var dataDiv = document.querySelector('.chart-events-data'); | |||
eventsChart.parent.addEventListener('data-select', function (e) { | |||
var name = moonData.names[e.index]; | |||
dataDiv.querySelector('.moon-name').innerHTML = name; | |||
dataDiv.querySelector('.semi-major-axis').innerHTML = moonData.distances[e.index] * 1000; | |||
dataDiv.querySelector('.mass').innerHTML = moonData.masses[e.index]; | |||
dataDiv.querySelector('.diameter').innerHTML = moonData.diameters[e.index]; | |||
dataDiv.querySelector('img').src = "./assets/img/" + name.toLowerCase() + ".jpg"; | |||
}); | |||
// Heatmap | |||
// ================================================================================ | |||
var heatmapArgs = { | |||
title: "Monthly Distribution", | |||
data: heatmapData, | |||
type: 'heatmap', | |||
discreteDomains: 1, | |||
countLabel: 'Level', | |||
colors: HEATMAP_COLORS_BLUE, | |||
legendScale: [0, 1, 2, 4, 5] | |||
}; | |||
var heatmapChart = new Chart("#chart-heatmap", heatmapArgs); | |||
Array.prototype.slice.call(document.querySelectorAll('.heatmap-mode-buttons button')).map(function (el) { | |||
el.addEventListener('click', function (e) { | |||
var btn = e.target; | |||
var mode = btn.getAttribute('data-mode'); | |||
var discreteDomains = 0; | |||
if (mode === 'discrete') { | |||
discreteDomains = 1; | |||
} | |||
var colors = []; | |||
var colors_mode = document.querySelector('.heatmap-color-buttons .active').getAttribute('data-color'); | |||
if (colors_mode === 'halloween') { | |||
colors = HEATMAP_COLORS_YELLOW; | |||
} else if (colors_mode === 'blue') { | |||
colors = HEATMAP_COLORS_BLUE; | |||
} | |||
heatmapArgs.discreteDomains = discreteDomains; | |||
heatmapArgs.colors = colors; | |||
new Chart("#chart-heatmap", heatmapArgs); | |||
Array.prototype.slice.call(btn.parentNode.querySelectorAll('button')).map(function (el) { | |||
el.classList.remove('active'); | |||
}); | |||
btn.classList.add('active'); | |||
}); | |||
}); | |||
Array.prototype.slice.call(document.querySelectorAll('.heatmap-color-buttons button')).map(function (el) { | |||
el.addEventListener('click', function (e) { | |||
var btn = e.target; | |||
var colors_mode = btn.getAttribute('data-color'); | |||
var colors = []; | |||
if (colors_mode === 'halloween') { | |||
colors = HEATMAP_COLORS_YELLOW; | |||
} else if (colors_mode === 'blue') { | |||
colors = HEATMAP_COLORS_BLUE; | |||
} | |||
var discreteDomains = 1; | |||
var view_mode = document.querySelector('.heatmap-mode-buttons .active').getAttribute('data-mode'); | |||
if (view_mode === 'continuous') { | |||
discreteDomains = 0; | |||
} | |||
heatmapArgs.discreteDomains = discreteDomains; | |||
heatmapArgs.colors = colors; | |||
new Chart("#chart-heatmap", heatmapArgs); | |||
Array.prototype.slice.call(btn.parentNode.querySelectorAll('button')).map(function (el) { | |||
el.classList.remove('active'); | |||
}); | |||
btn.classList.add('active'); | |||
}); | |||
}); | |||
document.querySelector('.export-heatmap').addEventListener('click', function () { | |||
heatmapChart.export(); | |||
}); | |||
}()); | |||
//# sourceMappingURL=index.min.js.map |
@@ -1,559 +0,0 @@ | |||
// Composite Chart | |||
// ================================================================================ | |||
let report_count_list = [17, 40, 33, 44, 126, 156, | |||
324, 333, 478, 495, 176]; | |||
let bar_composite_data = { | |||
labels: ["2007", "2008", "2009", "2010", "2011", "2012", | |||
"2013", "2014", "2015", "2016", "2017"], | |||
yMarkers: [ | |||
{ | |||
label: "Marker 1", | |||
value: 420, | |||
}, | |||
{ | |||
label: "Marker 2", | |||
value: 250, | |||
} | |||
], | |||
yRegions: [ | |||
{ | |||
label: "Region Y 1", | |||
start: 100, | |||
end: 300 | |||
}, | |||
], | |||
datasets: [{ | |||
"name": "Events", | |||
"values": report_count_list, | |||
// "formatted": report_count_list.map(d => d + " reports") | |||
}] | |||
}; | |||
let line_composite_data = { | |||
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], | |||
datasets: [{ | |||
"values": [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 0, 0], | |||
// "values": [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 40, 40], | |||
// "values": [-36, -46, -45, -32, -27, -31, -30, -36, -39, -49, -40, -40], | |||
}] | |||
}; | |||
let more_line_data = [ | |||
[4, 0, 3, 1, 1, 2, 1, 2, 1, 0, 1, 1], | |||
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |||
[2, 3, 3, 2, 1, 4, 0, 1, 2, 7, 11, 4], | |||
[7, 7, 2, 4, 0, 1, 5, 3, 1, 2, 0, 1], | |||
[0, 2, 6, 2, 2, 1, 2, 3, 6, 3, 7, 10], | |||
[9, 10, 8, 10, 6, 5, 8, 8, 24, 15, 10, 13], | |||
[9, 13, 16, 9, 4, 5, 7, 10, 14, 22, 23, 24], | |||
[20, 22, 28, 19, 28, 19, 14, 19, 51, 37, 29, 38], | |||
[29, 20, 22, 16, 16, 19, 24, 26, 57, 31, 46, 27], | |||
[36, 24, 38, 27, 15, 22, 24, 38, 32, 57, 139, 26], | |||
[37, 36, 32, 33, 12, 34, 52, 45, 58, 57, 64, 35], | |||
[36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 0, 0], | |||
// [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 40, 40] | |||
// [-36, -46, -45, -32, -27, -31, -30, -36, -39, -49, -40, -40] | |||
]; | |||
let c1 = document.querySelector("#chart-composite-1"); | |||
let c2 = document.querySelector("#chart-composite-2"); | |||
let bar_composite_chart = new Chart (c1, { | |||
title: "Fireball/Bolide Events - Yearly (more than 5 reports)", | |||
data: bar_composite_data, | |||
type: 'bar', | |||
height: 180, | |||
colors: ['orange'], | |||
isNavigable: 1, | |||
isSeries: 1, | |||
valuesOverPoints: 1, | |||
yAxisMode: 'tick' | |||
// regionFill: 1 | |||
}); | |||
let line_composite_chart = new Chart (c2, { | |||
data: line_composite_data, | |||
type: 'line', | |||
lineOptions: { | |||
dotSize: 10 | |||
}, | |||
height: 180, | |||
colors: ['green'], | |||
isSeries: 1, | |||
valuesOverPoints: 1, | |||
}); | |||
bar_composite_chart.parent.addEventListener('data-select', (e) => { | |||
line_composite_chart.updateDataset(more_line_data[e.index]); | |||
}); | |||
// Demo Chart (bar, linepts, scatter(blobs), percentage) | |||
// ================================================================================ | |||
let type_data = { | |||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | |||
yMarkers: [ | |||
{ | |||
label: "Marker 1", | |||
value: 42, | |||
type: 'dashed' | |||
}, | |||
{ | |||
label: "Marker 2", | |||
value: 25, | |||
type: 'dashed' | |||
} | |||
], | |||
yRegions: [ | |||
{ | |||
label: "Region Y 1", | |||
start: -10, | |||
end: 50 | |||
}, | |||
], | |||
// will depend on series code for calculating X values | |||
// xRegions: [ | |||
// { | |||
// label: "Region X 2", | |||
// start: , | |||
// end: , | |||
// } | |||
// ], | |||
datasets: [ | |||
{ | |||
name: "Some Data", | |||
values: [18, 40, 30, 35, 8, 52, 17, -4], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, | |||
{ | |||
name: "Another Set", | |||
values: [30, 50, -10, 15, 18, 32, 27, 14], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, | |||
{ | |||
name: "Yet Another", | |||
values: [15, 20, -3, -15, 58, 12, -17, 37], | |||
chartType: 'line' | |||
} | |||
// temp : Stacked | |||
// { | |||
// name: "Some Data", | |||
// values:[25, 30, 50, 45, 18, 12, 27, 14] | |||
// }, | |||
// { | |||
// name: "Another Set", | |||
// values: [18, 20, 30, 35, 8, 7, 17, 4] | |||
// }, | |||
// { | |||
// name: "Another Set", | |||
// values: [11, 8, 19, 15, 3, 4, 10, 2] | |||
// }, | |||
] | |||
}; | |||
let type_chart = new Chart("#chart-types", { | |||
// title: "My Awesome Chart", | |||
data: type_data, | |||
type: 'bar', | |||
height: 250, | |||
colors: ['purple', 'magenta', 'light-blue'], | |||
isSeries: 1, | |||
xAxisMode: 'tick', | |||
yAxisMode: 'span', | |||
valuesOverPoints: 1, | |||
barOptions: { | |||
stacked: 1 | |||
} | |||
// formatTooltipX: d => (d + '').toUpperCase(), | |||
// formatTooltipY: d => d + ' pts' | |||
}); | |||
Array.prototype.slice.call( | |||
document.querySelectorAll('.chart-type-buttons button') | |||
).map(el => { | |||
el.addEventListener('click', (e) => { | |||
let btn = e.target; | |||
let type = btn.getAttribute('data-type'); | |||
let newChart = type_chart.getDifferentChart(type); | |||
if(newChart){ | |||
type_chart = newChart; | |||
} | |||
Array.prototype.slice.call( | |||
btn.parentNode.querySelectorAll('button')).map(el => { | |||
el.classList.remove('active'); | |||
}); | |||
btn.classList.add('active'); | |||
}); | |||
}); | |||
// Trends Chart | |||
// ================================================================================ | |||
let trends_data = { | |||
labels: [1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, | |||
1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, | |||
1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, | |||
1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, | |||
2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016] , | |||
datasets: [ | |||
{ | |||
"values": [132.9, 150.0, 149.4, 148.0, 94.4, 97.6, 54.1, 49.2, 22.5, 18.4, | |||
39.3, 131.0, 220.1, 218.9, 198.9, 162.4, 91.0, 60.5, 20.6, 14.8, | |||
33.9, 123.0, 211.1, 191.8, 203.3, 133.0, 76.1, 44.9, 25.1, 11.6, | |||
28.9, 88.3, 136.3, 173.9, 170.4, 163.6, 99.3, 65.3, 45.8, 24.7, | |||
12.6, 4.2, 4.8, 24.9, 80.8, 84.5, 94.0, 113.3, 69.8, 39.8] | |||
} | |||
] | |||
}; | |||
let plot_chart_args = { | |||
title: "Mean Total Sunspot Count - Yearly", | |||
data: trends_data, | |||
type: 'line', | |||
height: 250, | |||
colors: ['blue'], | |||
isSeries: 1, | |||
lineOptions: { | |||
hideDots: 1, | |||
heatline: 1, | |||
}, | |||
xAxisMode: 'tick', | |||
yAxisMode: 'span' | |||
}; | |||
new Chart("#chart-trends", plot_chart_args); | |||
Array.prototype.slice.call( | |||
document.querySelectorAll('.chart-plot-buttons button') | |||
).map(el => { | |||
el.addEventListener('click', (e) => { | |||
let btn = e.target; | |||
let type = btn.getAttribute('data-type'); | |||
let config = []; | |||
if(type === 'line') { | |||
config = [0, 0, 0]; | |||
} else if(type === 'region') { | |||
config = [0, 0, 1]; | |||
} else { | |||
config = [0, 1, 0]; | |||
} | |||
plot_chart_args.hideDots = config[0]; | |||
plot_chart_args.heatline = config[1]; | |||
plot_chart_args.regionFill = config[2]; | |||
plot_chart_args.init = false; | |||
new Chart("#chart-trends", plot_chart_args); | |||
Array.prototype.slice.call( | |||
btn.parentNode.querySelectorAll('button')).map(el => { | |||
el.classList.remove('active'); | |||
}); | |||
btn.classList.add('active'); | |||
}); | |||
}); | |||
// Update values chart | |||
// ================================================================================ | |||
let update_data_all_labels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", | |||
"Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", | |||
"Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"]; | |||
let update_data_all_values = Array.from({length: 30}, () => Math.floor(Math.random() * 75 - 15)); | |||
// We're gonna be shuffling this | |||
let update_data_all_indices = update_data_all_labels.map((d,i) => i); | |||
let get_update_data = (source_array, length=10) => { | |||
let indices = update_data_all_indices.slice(0, length); | |||
return indices.map((index) => source_array[index]); | |||
}; | |||
let update_data = { | |||
labels: get_update_data(update_data_all_labels), | |||
datasets: [{ | |||
"values": get_update_data(update_data_all_values) | |||
}], | |||
"specific_values": [ | |||
{ | |||
name: "Altitude", | |||
// name: "A very long text", | |||
line_type: "dashed", | |||
value: 38 | |||
}, | |||
] | |||
}; | |||
let update_chart = new Chart("#chart-update", { | |||
data: update_data, | |||
type: 'line', | |||
height: 250, | |||
colors: ['red'], | |||
isSeries: 1, | |||
lineOptions: { | |||
regionFill: 1 | |||
}, | |||
}); | |||
let chart_update_buttons = document.querySelector('.chart-update-buttons'); | |||
chart_update_buttons.querySelector('[data-update="random"]').addEventListener("click", (e) => { | |||
shuffle(update_data_all_indices); | |||
let data = { | |||
labels: update_data_all_labels.slice(0, 10), | |||
datasets: [{values: get_update_data(update_data_all_values)}], | |||
} | |||
update_chart.update(data); | |||
}); | |||
chart_update_buttons.querySelector('[data-update="add"]').addEventListener("click", (e) => { | |||
let index = update_chart.state.datasetLength; // last index to add | |||
if(index >= update_data_all_indices.length) return; | |||
update_chart.addDataPoint( | |||
update_data_all_labels[index], [update_data_all_values[index]] | |||
); | |||
}); | |||
chart_update_buttons.querySelector('[data-update="remove"]').addEventListener("click", (e) => { | |||
update_chart.removeDataPoint(); | |||
}); | |||
// Event chart | |||
// ================================================================================ | |||
let moon_names = ["Ganymede", "Callisto", "Io", "Europa"]; | |||
let masses = [14819000, 10759000, 8931900, 4800000]; | |||
let distances = [1070.412, 1882.709, 421.700, 671.034]; | |||
let diameters = [5262.4, 4820.6, 3637.4, 3121.6]; | |||
let jupiter_moons = { | |||
'Ganymede': { | |||
mass: '14819000 x 10^16 kg', | |||
'semi-major-axis': '1070412 km', | |||
'diameter': '5262.4 km' | |||
}, | |||
'Callisto': { | |||
mass: '10759000 x 10^16 kg', | |||
'semi-major-axis': '1882709 km', | |||
'diameter': '4820.6 km' | |||
}, | |||
'Io': { | |||
mass: '8931900 x 10^16 kg', | |||
'semi-major-axis': '421700 km', | |||
'diameter': '3637.4 km' | |||
}, | |||
'Europa': { | |||
mass: '4800000 x 10^16 kg', | |||
'semi-major-axis': '671034 km', | |||
'diameter': '3121.6 km' | |||
}, | |||
}; | |||
let events_data = { | |||
labels: ["Ganymede", "Callisto", "Io", "Europa"], | |||
datasets: [ | |||
{ | |||
"values": distances, | |||
"formatted": distances.map(d => d*1000 + " km") | |||
} | |||
] | |||
}; | |||
let events_chart = new Chart("#chart-events", { | |||
title: "Jupiter's Moons: Semi-major Axis (1000 km)", | |||
data: events_data, | |||
type: 'bar', | |||
height: 250, | |||
colors: ['grey'], | |||
isNavigable: 1, | |||
}); | |||
let data_div = document.querySelector('.chart-events-data'); | |||
events_chart.parent.addEventListener('data-select', (e) => { | |||
let name = moon_names[e.index]; | |||
data_div.querySelector('.moon-name').innerHTML = name; | |||
data_div.querySelector('.semi-major-axis').innerHTML = distances[e.index] * 1000; | |||
data_div.querySelector('.mass').innerHTML = masses[e.index]; | |||
data_div.querySelector('.diameter').innerHTML = diameters[e.index]; | |||
data_div.querySelector('img').src = "./assets/img/" + name.toLowerCase() + ".jpg"; | |||
}); | |||
// Aggregation chart | |||
// ================================================================================ | |||
let aggr_data = { | |||
labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], | |||
datasets: [ | |||
{ | |||
"values": [25, 40, 30, 35, 8, 52, 17] | |||
}, | |||
{ | |||
"values": [25, 50, 10, 15, 18, 32, 27], | |||
} | |||
] | |||
}; | |||
let aggr_chart = new Chart("#chart-aggr", { | |||
data: aggr_data, | |||
type: 'bar', | |||
height: 250, | |||
colors: ['light-green', 'blue'], | |||
valuesOverPoints: 1, | |||
barOptions: { | |||
// stacked: 1 | |||
} | |||
}); | |||
document.querySelector('[data-aggregation="sums"]').addEventListener("click", (e) => { | |||
if(e.target.innerHTML === "Show Sums") { | |||
aggr_chart.show_sums(); | |||
e.target.innerHTML = "Hide Sums"; | |||
} else { | |||
aggr_chart.hide_sums(); | |||
e.target.innerHTML = "Show Sums"; | |||
} | |||
}); | |||
document.querySelector('[data-aggregation="average"]').addEventListener("click", (e) => { | |||
if(e.target.innerHTML === "Show Averages") { | |||
aggr_chart.show_averages(); | |||
e.target.innerHTML = "Hide Averages"; | |||
} else { | |||
aggr_chart.hide_averages(); | |||
e.target.innerHTML = "Show Averages"; | |||
} | |||
}); | |||
// Heatmap | |||
// ================================================================================ | |||
let heatmap_data = {}; | |||
let current_date = new Date(); | |||
let timestamp = current_date.getTime()/1000; | |||
timestamp = Math.floor(timestamp - (timestamp % 86400)).toFixed(1); // convert to midnight | |||
for (var i = 0; i< 375; i++) { | |||
heatmap_data[parseInt(timestamp)] = Math.floor(Math.random() * 5); | |||
timestamp = Math.floor(timestamp - 86400).toFixed(1); | |||
} | |||
new Chart("#chart-heatmap", { | |||
data: heatmap_data, | |||
type: 'heatmap', | |||
legend_scale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discrete_domains: 1 | |||
}); | |||
Array.prototype.slice.call( | |||
document.querySelectorAll('.heatmap-mode-buttons button') | |||
).map(el => { | |||
el.addEventListener('click', (e) => { | |||
let btn = e.target; | |||
let mode = btn.getAttribute('data-mode'); | |||
let discrete_domains = 0; | |||
if(mode === 'discrete') { | |||
discrete_domains = 1; | |||
} | |||
let colors = []; | |||
let colors_mode = document | |||
.querySelector('.heatmap-color-buttons .active') | |||
.getAttribute('data-color'); | |||
if(colors_mode === 'halloween') { | |||
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | |||
} | |||
new Chart("#chart-heatmap", { | |||
data: heatmap_data, | |||
type: 'heatmap', | |||
legend_scale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discrete_domains: discrete_domains, | |||
legend_colors: colors | |||
}); | |||
Array.prototype.slice.call( | |||
btn.parentNode.querySelectorAll('button')).map(el => { | |||
el.classList.remove('active'); | |||
}); | |||
btn.classList.add('active'); | |||
}); | |||
}); | |||
Array.prototype.slice.call( | |||
document.querySelectorAll('.heatmap-color-buttons button') | |||
).map(el => { | |||
el.addEventListener('click', (e) => { | |||
let btn = e.target; | |||
let colors_mode = btn.getAttribute('data-color'); | |||
let colors = []; | |||
if(colors_mode === 'halloween') { | |||
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | |||
} | |||
let discrete_domains = 1; | |||
let view_mode = document | |||
.querySelector('.heatmap-mode-buttons .active') | |||
.getAttribute('data-mode'); | |||
if(view_mode === 'continuous') { | |||
discrete_domains = 0; | |||
} | |||
new Chart("#chart-heatmap", { | |||
data: heatmap_data, | |||
type: 'heatmap', | |||
legend_scale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discrete_domains: discrete_domains, | |||
legend_colors: colors | |||
}); | |||
Array.prototype.slice.call( | |||
btn.parentNode.querySelectorAll('button')).map(el => { | |||
el.classList.remove('active'); | |||
}); | |||
btn.classList.add('active'); | |||
}); | |||
}); | |||
// Helpers | |||
// ================================================================================ | |||
function shuffle(array) { | |||
// https://stackoverflow.com/a/2450976/6495043 | |||
// Awesomeness: https://bost.ocks.org/mike/shuffle/ | |||
var currentIndex = array.length, temporaryValue, randomIndex; | |||
// While there remain elements to shuffle... | |||
while (0 !== currentIndex) { | |||
// Pick a remaining element... | |||
randomIndex = Math.floor(Math.random() * currentIndex); | |||
currentIndex -= 1; | |||
// And swap it with the current element. | |||
temporaryValue = array[currentIndex]; | |||
array[currentIndex] = array[randomIndex]; | |||
array[randomIndex] = temporaryValue; | |||
} | |||
return array; | |||
} | |||
@@ -27,9 +27,8 @@ | |||
<div class="row hero" style="padding-top: 30px; padding-bottom: 0px;"> | |||
<div class="jumbotron" style="background: transparent;"> | |||
<h1>Frappe Charts</h1> | |||
<p class="mt-2">GitHub-inspired simple and modern charts for the web</p> | |||
<p class="mt-2">GitHub-inspired simple and modern SVG charts for the web</p> | |||
<p class="mt-2">with zero dependencies.</p> | |||
<!--<p class="mt-2">Because dumb charts are hard to come by.</p>--> | |||
</div> | |||
<div class="col-sm-10 push-sm-1 later" style="font-size: 14px;"> | |||
@@ -57,28 +56,38 @@ | |||
datasets: [ | |||
{ | |||
label: "Some Data", type: 'bar', | |||
label: "Some Data", chartType: 'bar', | |||
values: [25, 40, 30, 35, 8, 52, 17, -4] | |||
}, | |||
{ | |||
label: "Another Set", type: 'bar', | |||
label: "Another Set", chartType: 'bar', | |||
values: [25, 50, -10, 15, 18, 32, 27, 14] | |||
}, | |||
{ | |||
label: "Yet Another", type: 'line', | |||
label: "Yet Another", chartType: 'line', | |||
values: [15, 20, -3, -15, 58, 12, -17, 37] | |||
} | |||
], | |||
yMarkers: [{ label: "Marker", value: 70 }], | |||
yRegions: [{ label: "Region", start: -10, end: 50 }] | |||
yMarkers: [{ label: "Marker", value: 70, | |||
options: { labelPos: 'left' }}], | |||
yRegions: [{ label: "Region", start: -10, end: 50, | |||
options: { labelPos: 'right' }}] | |||
}, | |||
title: "My Awesome Chart", | |||
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage' | |||
height: 250, | |||
colors: ['purple', '#ffa3ef', 'red'] | |||
});</code></pre> | |||
height: 300, | |||
colors: ['purple', '#ffa3ef', 'red'], | |||
tooltipOptions: { | |||
formatTooltipX: d => (d + '').toUpperCase(), | |||
formatTooltipY: d => d + ' pts', | |||
} | |||
}); | |||
chart.export(); | |||
</code></pre> | |||
<!-- <div id="chart-types" class="border" style="margin-bottom: 15px"></div> --> | |||
<!-- <div > | |||
<div class="btn-group x-axis-buttons margin-vertical-px" role="group"> | |||
@@ -101,6 +110,9 @@ | |||
<button type="button" class="btn btn-sm btn-secondary" data-type='pie'>Pie Chart</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button> | |||
</div> | |||
<div class="btn-group export-buttons margin-vertical-px mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary export-aggr">Export ...</button> | |||
</div> | |||
<!-- <p class="text-muted"> | |||
<a target="_blank" href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts">Why Percentage?</a> | |||
</p> --> | |||
@@ -117,6 +129,7 @@ | |||
<button type="button" class="btn btn-sm btn-secondary" data-update="random">Random Data</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-update="add">Add Value</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-update="remove">Remove Value</button> | |||
<button type="button" class="btn btn-sm btn-secondary export-update">Export ...</button> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -133,6 +146,9 @@ | |||
<button type="button" class="btn btn-sm btn-secondary active" data-type="heatline">HeatLine</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type="regionFill">Region</button> | |||
</div> | |||
<div class="btn-group export-buttons mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary export-trends">Export ...</button> | |||
</div> | |||
<!-- <pre><code class="hljs javascript margin-vertical-px"> ... | |||
lineOptions: 'line', // Line Chart specific properties: | |||
@@ -182,32 +198,34 @@ | |||
And a Month-wise Heatmap | |||
</h6> | |||
<div id="chart-heatmap" class="border" | |||
style="overflow: scroll; text-align: center; padding: 20px;"></div> | |||
style="overflow: scroll;"></div> | |||
<div class="heatmap-mode-buttons btn-group mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-mode="discrete">Discrete</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-mode="continuous">Continuous</button> | |||
</div> | |||
<div class="heatmap-color-buttons btn-group mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary" data-color="default">Default green</button> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-color="halloween">GitHub's Halloween</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-color="default">Green (Default)</button> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-color="blue">Blue</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-color="halloween">GitHub's Halloween</button> | |||
</div> | |||
<div class="btn-group export-buttons mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary export-heatmap">Export ...</button> | |||
</div> | |||
<pre><code class="hljs javascript margin-vertical-px"> let heatmap = new Chart("#heatmap", { | |||
type: 'heatmap', | |||
height: 115, | |||
data: heatmapData, // object with date/timestamp-value pairs | |||
discreteDomains: 1 // default: 0 | |||
start: startDate, | |||
// A Date object; | |||
// default: today's date in past year | |||
// for an annual heatmap | |||
legendColors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'], | |||
title: "Monthly Distribution", | |||
data: { | |||
dataPoints: {'1524064033': 8, /* ... */}, | |||
// object with timestamp-value pairs | |||
start: startDate | |||
end: endDate // Date objects | |||
}, | |||
countLabel: 'Level', | |||
discreteDomains: 0 // default: 1 | |||
colors: ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'], | |||
// Set of five incremental colors, | |||
// beginning with a low-saturation color for zero data; | |||
// default: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'] | |||
// preferably with a low-saturation color for zero data; | |||
// def: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'] | |||
});</code></pre> | |||
</div> | |||
</div> | |||
@@ -237,6 +255,7 @@ | |||
isNavigable: 1, // default: 0 | |||
valuesOverPoints: 1, // default: 0 | |||
barOptions: { | |||
spaceRatio: 1 // default: 0.5 | |||
stacked: 1 // default: 0 | |||
} | |||
@@ -256,15 +275,17 @@ | |||
}, | |||
// Pie/Percentage charts | |||
maxLegendPoints: 6, // default: 20 | |||
maxSlices: 10, // default: 20 | |||
// Heatmap | |||
// Percentage chart | |||
barOptions: { | |||
height: 15 // default: 20 | |||
depth: 5 // default: 2 | |||
} | |||
// Heatmap | |||
discreteDomains: 1, // default: 1 | |||
start: startDate, // Date object | |||
legendColors: [] | |||
} | |||
... | |||
@@ -276,6 +297,12 @@ | |||
chart.removeDataPoint(index) | |||
chart.updateDataset(datasetValues, index) | |||
// Exporting | |||
chart.export(); | |||
// Unbind window-resize events | |||
chart.unbindWindowEvents(); | |||
</code></pre> | |||
</div> | |||
</div> | |||
@@ -286,9 +313,9 @@ | |||
<p class="step-explain">Install via npm</p> | |||
<pre><code class="hljs console"> npm install frappe-charts</code></pre> | |||
<p class="step-explain">And include it in your project</p> | |||
<pre><code class="hljs javascript"> import Chart from "frappe-charts/dist/frappe-charts.min.esm"</code></pre> | |||
<pre><code class="hljs javascript"> import { Chart } from "frappe-charts"</code></pre> | |||
<p class="step-explain">... or include it directly in your HTML</p> | |||
<pre><code class="hljs html"> <script src="https://unpkg.com/frappe-charts@1.0.0/dist/frappe-charts.min.iife.js"></script></code></pre> | |||
<pre><code class="hljs html"> <script src="https://unpkg.com/frappe-charts@1.0.0"></script></code></pre> | |||
</div> | |||
</div> | |||
@@ -336,6 +363,6 @@ | |||
</a> | |||
<script src="assets/js/frappe-charts.min.js"></script> | |||
<script src="assets/js/index.js"></script> | |||
<script src="assets/js/index.min.js"></script> | |||
</body> | |||
</html> |
@@ -1,312 +0,0 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<title>Frappe Charts</title> | |||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||
<meta name="keywords" content="open source javascript js charts library svg zero-dependency interactive data visualization beautiful drag resize"> | |||
<meta name="description" content="A simple, responsive, modern charts library for the web."> | |||
<link rel="stylesheet" type="text/css" href="assets/css/normalize.css" media="screen"> | |||
<link href='https://fonts.googleapis.com/css?family=Roboto:400,700' rel='stylesheet' type='text/css'> | |||
<link rel="stylesheet" type="text/css" href="assets/css/bootstrap.min.css" media="screen"> | |||
<link rel="stylesheet" type="text/css" href="assets/css/frappe_theme.css" media="screen"> | |||
<link rel="stylesheet" type="text/css" href="assets/css/index.css" media="screen"> | |||
<link rel="stylesheet" type="text/css" href="assets/css/default.css" media="screen"> | |||
<script src="assets/js/highlight.pack.js"></script> | |||
<script>hljs.initHighlightingOnLoad();</script> | |||
<link rel="shortcut icon" href="https://frappe.github.io/frappe/assets/img/favicon.png" type="image/x-icon"> | |||
<link rel="icon" href="https://frappe.github.io/frappe/assets/img/favicon.png" type="image/x-icon"> | |||
</head> | |||
<body> | |||
<div class="container"> | |||
<div class="row hero" style="padding-top: 30px; padding-bottom: 0px;"> | |||
<div class="jumbotron" style="background: transparent;"> | |||
<h1>Frappe Charts</h1> | |||
<p class="mt-2">GitHub-inspired simple and modern charts for the web</p> | |||
<p class="mt-2">with zero dependencies.</p> | |||
<!--<p class="mt-2">Because dumb charts are hard to come by.</p>--> | |||
</div> | |||
<div class="col-sm-10 push-sm-1 later" style="font-size: 14px;"> | |||
<div id="chart-composite-1" class="border"><svg height=225></svg></div> | |||
<p class="mt-1">Click or use arrow keys to navigate data points</p> | |||
</div> | |||
<div class="col-sm-10 push-sm-1 later" style="font-size: 14px;"> | |||
<div id="chart-composite-2" class="border"><svg height=225></svg></div> | |||
</div> | |||
</div> | |||
<div class="group later"> | |||
<div class="row section"> | |||
<div class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<h6 class="margin-vertical-rem"> | |||
<!--Bars, Lines or <a href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts" target="_blank">Percentages</a>--> | |||
Create a chart | |||
</h6> | |||
<p class="step-explain">Install</p> | |||
<pre><code class="hljs console"> npm install frappe-charts</code></pre> | |||
<p class="step-explain">And include it in your project</p> | |||
<pre><code class="hljs javascript"> import Chart from "frappe-charts/dist/frappe-charts.min.esm"</code></pre> | |||
<p class="step-explain">... or include it directly in your HTML</p> | |||
<pre><code class="hljs html"> <script src="https://unpkg.com/frappe-charts@1.0.0/dist/frappe-charts.min.iife.js"></script></code></pre> | |||
<p class="step-explain">Make a new Chart</p> | |||
<pre><code class="hljs html"> <!--HTML--> | |||
<div id="chart"></div></code></pre> | |||
<pre><code class="hljs javascript"> // Javascript | |||
let data = { | |||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | |||
datasets: [ | |||
{ | |||
label: "Some Data", | |||
values: [25, 40, 30, 35, 8, 52, 17, -4] | |||
}, | |||
{ | |||
label: "Another Set", | |||
values: [25, 50, -10, 15, 18, 32, 27, 14] | |||
}, | |||
{ | |||
label: "Yet Another", | |||
values: [15, 20, -3, -15, 58, 12, -17, 37] | |||
} | |||
] | |||
}; | |||
let chart = new Chart({ | |||
parent: "#chart", // or a DOM element | |||
title: "My Awesome Chart", | |||
data: data, | |||
type: 'bar', // or 'line', 'scatter', 'pie', 'percentage' | |||
height: 250, | |||
colors: ['#7cd6fd', 'violet', 'blue'], | |||
// hex-codes or these preset colors; | |||
// defaults (in order): | |||
// ['light-blue', 'blue', 'violet', 'red', | |||
// 'orange', 'yellow', 'green', 'light-green', | |||
// 'purple', 'magenta', 'grey', 'dark-grey'] | |||
format_tooltip_x: d => (d + '').toUpperCase(), | |||
format_tooltip_y: d => d + ' pts' | |||
});</code></pre> | |||
<div id="chart-types" class="border"></div> | |||
<div class="btn-group chart-type-buttons margin-vertical-px mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-type='bar'>Bar Chart</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type='line'>Line Chart</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type='pie'>Pie Chart</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button> | |||
</div> | |||
<p class="text-muted"> | |||
<a target="_blank" href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts">Why Percentage?</a> | |||
</p> | |||
</div> | |||
</div> | |||
<div class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<h6 class="margin-vertical-rem"> | |||
Update Values | |||
</h6> | |||
<pre><code class="hljs javascript"> // Update entire datasets | |||
chart.updateData( | |||
[ | |||
{values: new_dataset_1_values}, | |||
{values: new_dataset_2_values} | |||
], | |||
new_labels | |||
); | |||
// Add a new data point | |||
chart.add_data_point( | |||
[new_value_1, new_value_2], | |||
new_label, | |||
index // defaults to last index | |||
); | |||
// Remove a data point | |||
chart.remove_data_point(index);</code></pre> | |||
<div id="chart-update" class="border"></div> | |||
<div class="chart-update-buttons mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary" data-update="random">Random Data</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-update="add">Add Value</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-update="remove">Remove Value</button> | |||
</div> | |||
<pre><code class="hljs javascript margin-vertical-px"> ... | |||
// Include specific Y values in input data to be displayed as lines | |||
// (before passing data to a new chart): | |||
data.specific_values = [ | |||
{ | |||
label: "Altitude", | |||
line_type: "dashed", // or "solid" | |||
value: 38 | |||
} | |||
] | |||
...</code></pre> | |||
</div> | |||
</div> | |||
<div class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<h6 class="margin-vertical-rem"> | |||
Plot Trends | |||
</h6> | |||
<pre><code class="hljs javascript"> ... | |||
xAxisMode: 'tick', // for short label ticks | |||
// or 'span' for long spanning vertical axis lines | |||
yAxisMode: 'span', // for long horizontal lines, or 'tick' | |||
isSeries: 1, // to allow for skipping of X values | |||
...</code></pre> | |||
<div id="chart-trends" class="border"></div> | |||
<div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type="line">Line</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type="dots">Dots</button> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-type="heatline">HeatLine</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type="region">Region</button> | |||
</div> | |||
<pre><code class="hljs javascript margin-vertical-px"> ... | |||
type: 'line', // Line Chart specific properties: | |||
hideDots: 1, // Hide data points on the line; default 0 | |||
heatline: 1, // Show a value-wise line gradient; default 0 | |||
regionFill: 1, // Fill the area under the graph; default 0 | |||
...</code></pre> | |||
</div> | |||
</div> | |||
<div class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<h6 class="margin-vertical-rem"> | |||
Listen to state change | |||
</h6> | |||
<div class="row"> | |||
<div class="col-sm-8"> | |||
<div id="chart-events" class="border"></div> | |||
</div> | |||
<div class="col-sm-4"> | |||
<div class="chart-events-data" class="border data-container"> | |||
<div class="image-container border"> | |||
<img class="moon-image" src="./assets/img/europa.jpg"> | |||
</div> | |||
<div class="data margin-vertical-px"> | |||
<h6 class="moon-name">Europa</h6> | |||
<p>Semi-major-axis: <span class="semi-major-axis">671034</span> km</p> | |||
<p>Mass: <span class="mass">4800000</span> x 10^16 kg</p> | |||
<p>Diameter: <span class="diameter">3121.6</span> km</p> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<pre><code class="hljs javascript margin-vertical-px"> ... | |||
type: 'bar', // Bar Chart specific properties: | |||
isNavigable: 1, // Navigate across bars; default 0 | |||
... | |||
chart.parent.addEventListener('data-select', (e) => { | |||
update_moon_data(e.index); // e contains index and value of current datapoint | |||
});</code></pre> | |||
</div> | |||
</div> | |||
<div class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<h6 class="margin-vertical-rem"> | |||
Simple Aggregations | |||
</h6> | |||
<div id="chart-aggr" class="border"></div> | |||
<div class="chart-aggr-buttons mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary" data-aggregation="sums">Show Sums</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-aggregation="average">Show Averages</button> | |||
</div> | |||
<pre><code class="hljs javascript margin-vertical-px"> chart.show_sums(); // and `hide_sums()` | |||
chart.show_averages(); // and `hide_averages()`</code></pre> | |||
</div> | |||
</div> | |||
<div class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<h6 class="margin-vertical-rem"> | |||
And a Month-wise Heatmap | |||
</h6> | |||
<div id="chart-heatmap" class="border" | |||
style="overflow: scroll; text-align: center; padding: 20px;"></div> | |||
<div class="heatmap-mode-buttons btn-group mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-mode="discrete">Discrete</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-mode="continuous">Continuous</button> | |||
</div> | |||
<div class="heatmap-color-buttons btn-group mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-color="default">Default green</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-color="halloween">GitHub's Halloween</button> | |||
</div> | |||
<pre><code class="hljs javascript margin-vertical-px"> let heatmap = new Chart({ | |||
parent: "#heatmap", | |||
type: 'heatmap', | |||
height: 115, | |||
data: heatmap_data, // object with date/timestamp-value pairs | |||
discrete_domains: 1 // default: 0 | |||
start: start_date, | |||
// A Date object; | |||
// default: today's date in past year | |||
// for an annual heatmap | |||
legend_colors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'], | |||
// Set of five incremental colors, | |||
// beginning with a low-saturation color for zero data; | |||
// default: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'] | |||
});</code></pre> | |||
</div> | |||
</div> | |||
<div class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<!-- Closing --> | |||
<div class="text-center" style="margin-top: 70px"> | |||
<a href="https://github.com/frappe/charts/archive/master.zip"><button class="large blue button">Download</button></a> | |||
<p style="margin-top: 3rem;margin-bottom: 1.5rem;"><a href="https://github.com/frappe/charts" target="_blank">View on GitHub</a></p> | |||
<p style="margin-top: 1rem;"><iframe src="https://ghbtns.com/github-btn.html?user=frappe&repo=charts&type=star&count=true" frameborder="0" scrolling="0" width="94px" height="20px"></iframe></p> | |||
<p>License: MIT</p> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="built-with-frappe text-center" style="margin-top: -20px"> | |||
<img style="padding: 5px; width: 40px; background: #fff" class="frappe-bird" src="https://frappe.github.io/frappe/assets/img/frappe-bird-grey.svg"> | |||
<p style="margin: 24px 0 0px 0; font-size: 15px"> | |||
Project maintained by <a href="https://frappe.io" target="_blank">Frappe</a>. | |||
Used in <a href="https://erpnext.com" target="_blank">ERPNext</a>. | |||
Read the <a href="https://medium.com/@pratu16x7/so-we-decided-to-create-our-own-charts-a95cb5032c97" target="_blank">blog post</a>. | |||
</p> | |||
<p style="margin: 24px 0 80px 0; font-size: 12px"> | |||
Data from the <a href="https://www.amsmeteors.org" target="_blank">American Meteor Society</a>, | |||
<a href="http://www.sidc.be/silso" target="_blank">SILSO</a> and | |||
<a href="https://api.nasa.gov/index.html" target="_blank">NASA Open APIs</a> | |||
</p> | |||
</div> | |||
<a href="https://github.com/frappe/charts" target="_blank" class="github-corner" aria-label="View source on Github"> | |||
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#9a9a9a; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"> | |||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path> | |||
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path> | |||
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path> | |||
</svg> | |||
</a> | |||
<script src="assets/js/frappe-charts.min.js"></script> | |||
<script src="assets/js/old_index.js"></script> | |||
</body> | |||
</html> |
@@ -1,6 +1,6 @@ | |||
{ | |||
"name": "frappe-charts", | |||
"version": "1.0.0", | |||
"version": "1.1.0", | |||
"description": "https://frappe.github.io/charts", | |||
"main": "dist/frappe-charts.min.cjs.js", | |||
"module": "dist/frappe-charts.min.esm.js", | |||
@@ -33,16 +33,28 @@ | |||
}, | |||
"homepage": "https://github.com/frappe/charts#readme", | |||
"devDependencies": { | |||
"autoprefixer": "^8.2.0", | |||
"babel-core": "^6.26.0", | |||
"babel-plugin-external-helpers": "^6.22.0", | |||
"babel-plugin-istanbul": "^4.1.5", | |||
"babel-preset-env": "^1.6.1", | |||
"babel-preset-latest": "^6.24.1", | |||
"clean-css": "^4.1.11", | |||
"babel-register": "^6.26.0", | |||
"coveralls": "^3.0.0", | |||
"cross-env": "^5.1.4", | |||
"cssnano": "^3.10.0", | |||
"eslint": "^4.18.2", | |||
"fs": "0.0.1-security", | |||
"livereload": "^0.6.3", | |||
"mocha": "^5.0.5", | |||
"node-sass": "^4.7.2", | |||
"npm-run-all": "^4.1.1", | |||
"postcss": "^6.0.21", | |||
"nyc": "^11.6.0", | |||
"postcss-cssnext": "^3.0.2", | |||
"postcss-nested": "^2.1.2", | |||
"precss": "^3.1.2", | |||
"rollup": "^0.50.0", | |||
"rollup-plugin-babel": "^3.0.2", | |||
"rollup-plugin-eslint": "^4.0.0", | |||
@@ -54,6 +66,5 @@ | |||
"rollup-watch": "^4.3.1" | |||
}, | |||
"dependencies": { | |||
"eslint": "^4.18.2" | |||
} | |||
} |
@@ -1,21 +1,45 @@ | |||
import pkg from './package.json'; | |||
// Rollup plugins | |||
import babel from 'rollup-plugin-babel'; | |||
import eslint from 'rollup-plugin-eslint'; | |||
import replace from 'rollup-plugin-replace'; | |||
import uglify from 'rollup-plugin-uglify-es'; | |||
import sass from 'node-sass'; | |||
import postcss from 'rollup-plugin-postcss'; | |||
// PostCSS plugins | |||
import postcssPlugin from 'rollup-plugin-postcss'; | |||
import nested from 'postcss-nested'; | |||
import cssnext from 'postcss-cssnext'; | |||
import cssnano from 'cssnano'; | |||
import pkg from './package.json'; | |||
import postcss from 'postcss'; | |||
import precss from 'precss'; | |||
import CleanCSS from 'clean-css'; | |||
import autoprefixer from 'autoprefixer'; | |||
import fs from 'fs'; | |||
fs.readFile('src/css/charts.scss', (err, css) => { | |||
postcss([precss, autoprefixer]) | |||
.process(css, { from: 'src/css/charts.scss', to: 'src/css/charts.css' }) | |||
.then(result => { | |||
let options = { | |||
level: { | |||
1: { | |||
removeQuotes: false, | |||
} | |||
} | |||
} | |||
let output = new CleanCSS(options).minify(result.css); | |||
let res = JSON.stringify(output.styles).replace(/"/g, "'"); | |||
let js = `export const CSSTEXT = "${res.slice(1, -1)}";`; | |||
fs.writeFile('src/css/chartsCss.js', js); | |||
}); | |||
}); | |||
export default [ | |||
{ | |||
input: 'src/js/chart.js', | |||
input: 'src/js/index.js', | |||
sourcemap: true, | |||
output: [ | |||
{ | |||
@@ -27,9 +51,9 @@ export default [ | |||
format: 'iife', | |||
} | |||
], | |||
name: 'Chart', | |||
name: 'frappe', | |||
plugins: [ | |||
postcss({ | |||
postcssPlugin({ | |||
preprocessor: (content, id) => new Promise((resolve, reject) => { | |||
const result = sass.renderSync({ file: id }) | |||
resolve({ code: result.css.toString() }) | |||
@@ -43,11 +67,12 @@ export default [ | |||
}), | |||
eslint({ | |||
exclude: [ | |||
'src/scss/**' | |||
'src/css/**' | |||
] | |||
}), | |||
babel({ | |||
exclude: 'node_modules/**' | |||
exclude: 'node_modules/**', | |||
plugins: ['external-helpers'] | |||
}), | |||
replace({ | |||
exclude: 'node_modules/**', | |||
@@ -56,8 +81,46 @@ export default [ | |||
uglify() | |||
] | |||
}, | |||
{ | |||
input: 'docs/assets/js/index.js', | |||
sourcemap: true, | |||
output: [ | |||
{ | |||
file: 'docs/assets/js/index.min.js', | |||
format: 'iife', | |||
} | |||
], | |||
name: 'frappe', | |||
plugins: [ | |||
postcssPlugin({ | |||
preprocessor: (content, id) => new Promise((resolve, reject) => { | |||
const result = sass.renderSync({ file: id }) | |||
resolve({ code: result.css.toString() }) | |||
}), | |||
extensions: [ '.scss' ], | |||
plugins: [ | |||
nested(), | |||
cssnext({ warnForDuplicates: false }), | |||
cssnano() | |||
] | |||
}), | |||
eslint({ | |||
exclude: [ | |||
'src/css/**' | |||
] | |||
}), | |||
babel({ | |||
exclude: 'node_modules/**' | |||
}), | |||
replace({ | |||
exclude: 'node_modules/**', | |||
ENV: JSON.stringify(process.env.NODE_ENV || 'development'), | |||
}) | |||
] | |||
}, | |||
{ | |||
input: 'src/js/chart.js', | |||
sourcemap: true, | |||
output: [ | |||
{ | |||
file: pkg.main, | |||
@@ -69,7 +132,7 @@ export default [ | |||
} | |||
], | |||
plugins: [ | |||
postcss({ | |||
postcssPlugin({ | |||
preprocessor: (content, id) => new Promise((resolve, reject) => { | |||
const result = sass.renderSync({ file: id }) | |||
resolve({ code: result.css.toString() }) | |||
@@ -83,7 +146,7 @@ export default [ | |||
}), | |||
eslint({ | |||
exclude: [ | |||
'src/scss/**', | |||
'src/css/**', | |||
] | |||
}), | |||
babel({ | |||
@@ -105,7 +168,7 @@ export default [ | |||
} | |||
], | |||
plugins: [ | |||
postcss({ | |||
postcssPlugin({ | |||
preprocessor: (content, id) => new Promise((resolve, reject) => { | |||
const result = sass.renderSync({ file: id }) | |||
resolve({ code: result.css.toString() }) | |||
@@ -120,7 +183,7 @@ export default [ | |||
}), | |||
eslint({ | |||
exclude: [ | |||
'src/scss/**', | |||
'src/css/**', | |||
] | |||
}), | |||
replace({ | |||
@@ -0,0 +1,116 @@ | |||
.chart-container { | |||
position: relative; /* for absolutely positioned tooltip */ | |||
/* https://www.smashingmagazine.com/2015/11/using-system-ui-fonts-practical-guide/ */ | |||
font-family: -apple-system, BlinkMacSystemFont, | |||
'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', | |||
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; | |||
.axis, .chart-label { | |||
fill: #555b51; | |||
line { | |||
stroke: #dadada; | |||
} | |||
} | |||
.dataset-units { | |||
circle { | |||
stroke: #fff; | |||
stroke-width: 2; | |||
} | |||
path { | |||
fill: none; | |||
stroke-opacity: 1; | |||
stroke-width: 2px; | |||
} | |||
} | |||
.dataset-path { | |||
stroke-width: 2px; | |||
} | |||
.path-group { | |||
path { | |||
fill: none; | |||
stroke-opacity: 1; | |||
stroke-width: 2px; | |||
} | |||
} | |||
line.dashed { | |||
stroke-dasharray: 5, 3; | |||
} | |||
.axis-line { | |||
.specific-value { | |||
text-anchor: start; | |||
} | |||
.y-line { | |||
text-anchor: end; | |||
} | |||
.x-line { | |||
text-anchor: middle; | |||
} | |||
} | |||
.legend-dataset-text { | |||
fill: #6c7680; | |||
font-weight: 600; | |||
} | |||
} | |||
.graph-svg-tip { | |||
position: absolute; | |||
z-index: 99999; | |||
padding: 10px; | |||
font-size: 12px; | |||
color: #959da5; | |||
text-align: center; | |||
background: rgba(0, 0, 0, 0.8); | |||
border-radius: 3px; | |||
ul { | |||
padding-left: 0; | |||
display: flex; | |||
} | |||
ol { | |||
padding-left: 0; | |||
display: flex; | |||
} | |||
ul.data-point-list { | |||
li { | |||
min-width: 90px; | |||
flex: 1; | |||
font-weight: 600; | |||
} | |||
} | |||
strong { | |||
color: #dfe2e5; | |||
font-weight: 600; | |||
} | |||
.svg-pointer { | |||
position: absolute; | |||
height: 5px; | |||
margin: 0 0 0 -5px; | |||
content: ' '; | |||
border: 5px solid transparent; | |||
border-top-color: rgba(0, 0, 0, 0.8); | |||
} | |||
&.comparison { | |||
padding: 0; | |||
text-align: left; | |||
pointer-events: none; | |||
.title { | |||
display: block; | |||
padding: 10px; | |||
margin: 0; | |||
font-weight: 600; | |||
line-height: 1; | |||
pointer-events: none; | |||
} | |||
ul { | |||
margin: 0; | |||
white-space: nowrap; | |||
list-style: none; | |||
} | |||
li { | |||
display: inline-block; | |||
padding: 5px 10px; | |||
} | |||
} | |||
} |
@@ -0,0 +1 @@ | |||
export const CSSTEXT = ".chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .legend-dataset-text{fill:#6c7680;font-weight:600}.graph-svg-tip{position:absolute;z-index:99999;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ul{padding-left:0;display:flex}.graph-svg-tip ol{padding-left:0;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;flex:1;font-weight:600}.graph-svg-tip strong{color:#dfe2e5;font-weight:600}.graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:' ';border:5px solid transparent;border-top-color:rgba(0,0,0,.8)}.graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.graph-svg-tip.comparison li{display:inline-block;padding:5px 10px}"; |
@@ -1,4 +1,4 @@ | |||
import '../scss/charts.scss'; | |||
import '../css/charts.scss'; | |||
// import MultiAxisChart from './charts/MultiAxisChart'; | |||
import PercentageChart from './charts/PercentageChart'; | |||
@@ -7,6 +7,8 @@ import Heatmap from './charts/Heatmap'; | |||
import AxisChart from './charts/AxisChart'; | |||
const chartTypes = { | |||
bar: AxisChart, | |||
line: AxisChart, | |||
// multiaxis: MultiAxisChart, | |||
percentage: PercentageChart, | |||
heatmap: Heatmap, | |||
@@ -14,13 +16,7 @@ const chartTypes = { | |||
}; | |||
function getChartByType(chartType = 'line', parent, options) { | |||
if(chartType === 'line') { | |||
options.type = 'line'; | |||
return new AxisChart(parent, options); | |||
} else if (chartType === 'bar') { | |||
options.type = 'bar'; | |||
return new AxisChart(parent, options); | |||
} else if (chartType === 'axis-mixed') { | |||
if (chartType === 'axis-mixed') { | |||
options.type = 'line'; | |||
return new AxisChart(parent, options); | |||
} | |||
@@ -33,8 +29,10 @@ function getChartByType(chartType = 'line', parent, options) { | |||
return new chartTypes[chartType](parent, options); | |||
} | |||
export default class Chart { | |||
class Chart { | |||
constructor(parent, options) { | |||
return getChartByType(options.type, parent, options); | |||
} | |||
} | |||
export { Chart, PercentageChart, PieChart, Heatmap, AxisChart }; |
@@ -1,5 +1,6 @@ | |||
import BaseChart from './BaseChart'; | |||
import { $ } from '../utils/dom'; | |||
import { legendDot } from '../utils/draw'; | |||
import { getExtraWidth } from '../utils/constants'; | |||
export default class AggregationChart extends BaseChart { | |||
constructor(parent, args) { | |||
@@ -24,7 +25,7 @@ export default class AggregationChart extends BaseChart { | |||
total += e.values[i]; | |||
}); | |||
return [total, label]; | |||
}).filter(d => { return d[0] > 0; }); // keep only positive results | |||
}).filter(d => { return d[0] >= 0; }); // keep only positive results | |||
let totals = allTotals; | |||
if(allTotals.length > maxSlices) { | |||
@@ -45,28 +46,41 @@ export default class AggregationChart extends BaseChart { | |||
s.sliceTotals.push(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.statsWrapper.textContent = ''; | |||
this.legendArea.textContent = ''; | |||
this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints); | |||
let xValues = s.labels; | |||
let count = 0; | |||
let y = 0; | |||
this.legendTotals.map((d, i) => { | |||
if(d) { | |||
let stats = $.create('div', { | |||
className: 'stats', | |||
inside: this.statsWrapper | |||
}); | |||
stats.innerHTML = `<span class="indicator"> | |||
<i style="background: ${this.colors[i]}"></i> | |||
<span class="text-muted">${xValues[i]}:</span> | |||
${d} | |||
</span>`; | |||
let barWidth = 110; | |||
let divisor = Math.floor( | |||
(this.width - getExtraWidth(this.measures))/barWidth | |||
); | |||
if(count > divisor) { | |||
count = 0; | |||
y += 20; | |||
} | |||
let x = barWidth * count + 5; | |||
let dot = legendDot( | |||
x, | |||
y, | |||
5, | |||
this.colors[i], | |||
`${s.labels[i]}: ${d}` | |||
); | |||
this.legendArea.appendChild(dot); | |||
count++; | |||
}); | |||
} | |||
} |
@@ -1,12 +1,13 @@ | |||
import BaseChart from './BaseChart'; | |||
import { dataPrep, zeroDataPrep, getShortenedLabels } from '../utils/axis-chart-utils'; | |||
import { Y_AXIS_MARGIN } from '../utils/constants'; | |||
import { AXIS_LEGEND_BAR_SIZE } from '../utils/constants'; | |||
import { getComponent } from '../objects/ChartComponents'; | |||
import { $, getOffset, fire } from '../utils/dom'; | |||
import { calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale } from '../utils/intervals'; | |||
import { getOffset, fire } from '../utils/dom'; | |||
import { calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale, getClosestInArray } from '../utils/intervals'; | |||
import { floatTwo } from '../utils/helpers'; | |||
import { makeOverlay, updateOverlay } from '../utils/draw'; | |||
import { MIN_BAR_PERCENT_HEIGHT, BAR_CHART_SPACE_RATIO, LINE_CHART_DOT_SIZE } from '../utils/constants'; | |||
import { makeOverlay, updateOverlay, legendBar } from '../utils/draw'; | |||
import { getTopOffset, getLeftOffset, MIN_BAR_PERCENT_HEIGHT, BAR_CHART_SPACE_RATIO, | |||
LINE_CHART_DOT_SIZE } from '../utils/constants'; | |||
export default class AxisChart extends BaseChart { | |||
constructor(parent, args) { | |||
@@ -21,26 +22,27 @@ export default class AxisChart extends BaseChart { | |||
this.setup(); | |||
} | |||
configure(args) { | |||
super.configure(); | |||
setMeasures() { | |||
if(this.data.datasets.length <= 1) { | |||
this.config.showLegend = 0; | |||
this.measures.paddings.bottom = 30; | |||
} | |||
} | |||
args.axisOptions = args.axisOptions || {}; | |||
args.tooltipOptions = args.tooltipOptions || {}; | |||
configure(options) { | |||
super.configure(options); | |||
this.config.xAxisMode = args.axisOptions.xAxisMode || 'span'; | |||
this.config.yAxisMode = args.axisOptions.yAxisMode || 'span'; | |||
this.config.xIsSeries = args.axisOptions.xIsSeries || 0; | |||
options.axisOptions = options.axisOptions || {}; | |||
options.tooltipOptions = options.tooltipOptions || {}; | |||
this.config.formatTooltipX = args.tooltipOptions.formatTooltipX; | |||
this.config.formatTooltipY = args.tooltipOptions.formatTooltipY; | |||
this.config.xAxisMode = options.axisOptions.xAxisMode || 'span'; | |||
this.config.yAxisMode = options.axisOptions.yAxisMode || 'span'; | |||
this.config.xIsSeries = options.axisOptions.xIsSeries || 0; | |||
this.config.valuesOverPoints = args.valuesOverPoints; | |||
} | |||
this.config.formatTooltipX = options.tooltipOptions.formatTooltipX; | |||
this.config.formatTooltipY = options.tooltipOptions.formatTooltipY; | |||
setMargins() { | |||
super.setMargins(); | |||
this.leftMargin = Y_AXIS_MARGIN; | |||
this.rightMargin = Y_AXIS_MARGIN; | |||
this.config.valuesOverPoints = options.valuesOverPoints; | |||
} | |||
prepareData(data=this.data) { | |||
@@ -53,8 +55,10 @@ export default class AxisChart extends BaseChart { | |||
calc(onlyWidthChange = false) { | |||
this.calcXPositions(); | |||
if(onlyWidthChange) return; | |||
this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); | |||
if(!onlyWidthChange) { | |||
this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); | |||
} | |||
this.makeDataByIndex(); | |||
} | |||
calcXPositions() { | |||
@@ -139,6 +143,7 @@ export default class AxisChart extends BaseChart { | |||
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; | |||
// } | |||
@@ -149,13 +154,13 @@ export default class AxisChart extends BaseChart { | |||
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() { | |||
// TODO: yMarkers, regions, sums, every Y value ever | |||
let key = 'values'; | |||
if(this.barOptions.stacked) { | |||
@@ -300,6 +305,8 @@ export default class AxisChart extends BaseChart { | |||
function() { | |||
let s = this.state; | |||
let d = s.datasets[index]; | |||
let minLine = s.yAxis.positions[0] < s.yAxis.zeroLine | |||
? s.yAxis.positions[0] : s.yAxis.zeroLine; | |||
return { | |||
xPositions: s.xAxis.positions, | |||
@@ -307,7 +314,7 @@ export default class AxisChart extends BaseChart { | |||
values: d.values, | |||
zeroLine: s.yAxis.zeroLine, | |||
zeroLine: minLine, | |||
radius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE, | |||
}; | |||
}.bind(this) | |||
@@ -343,14 +350,46 @@ export default class AxisChart extends BaseChart { | |||
})); | |||
} | |||
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.chartWrapper.addEventListener('mousemove', (e) => { | |||
let o = getOffset(this.chartWrapper); | |||
let relX = e.pageX - o.left - this.leftMargin; | |||
let relY = e.pageY - o.top - this.translateY; | |||
if(relY < this.height + this.translateY * 2) { | |||
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(); | |||
@@ -362,56 +401,43 @@ export default class AxisChart extends BaseChart { | |||
let s = this.state; | |||
if(!s.yExtremes) return; | |||
let formatY = this.config.formatTooltipY; | |||
let formatX = this.config.formatTooltipX; | |||
let titles = s.xAxis.labels; | |||
if(formatX && formatX(titles[0])) { | |||
titles = titles.map(d=>formatX(d)); | |||
} | |||
let index = getClosestInArray(relX, s.xAxis.positions, true); | |||
let dbi = this.dataByIndex[index]; | |||
formatY = formatY && formatY(s.yAxis.labels[0]) ? formatY : 0; | |||
for(var i=s.datasetLength - 1; i >= 0 ; i--) { | |||
let xVal = s.xAxis.positions[i]; | |||
// let delta = i === 0 ? s.unitWidth : xVal - s.xAxis.positions[i-1]; | |||
if(relX > xVal - s.unitWidth/2) { | |||
let x = xVal + this.leftMargin; | |||
let y = s.yExtremes[i] + this.translateY; | |||
let values = this.data.datasets.map((set, j) => { | |||
return { | |||
title: set.name, | |||
value: formatY ? formatY(set.values[i]) : set.values[i], | |||
color: this.colors[j], | |||
}; | |||
}); | |||
this.tip.setValues( | |||
dbi.xPos + this.tip.offset.x, | |||
dbi.yExtreme + this.tip.offset.y, | |||
{name: dbi.formattedLabel, value: ''}, | |||
dbi.values, | |||
index | |||
); | |||
this.tip.setValues(x, y, {name: titles[i], value: ''}, values, i); | |||
this.tip.showTip(); | |||
break; | |||
} | |||
} | |||
this.tip.showTip(); | |||
} | |||
renderLegend() { | |||
let s = this.data; | |||
this.statsWrapper.textContent = ''; | |||
if(s.datasets.length > 1) { | |||
this.legendArea.textContent = ''; | |||
s.datasets.map((d, i) => { | |||
let stats = $.create('div', { | |||
className: 'stats', | |||
inside: this.statsWrapper | |||
}); | |||
stats.innerHTML = `<span class="indicator"> | |||
<i style="background: ${this.colors[i]}"></i> | |||
${d.name} | |||
</span>`; | |||
let barWidth = AXIS_LEGEND_BAR_SIZE; | |||
// let rightEndPoint = this.baseWidth - this.measures.margins.left - this.measures.margins.right; | |||
// let multiplier = s.datasets.length - i; | |||
let rect = legendBar( | |||
// rightEndPoint - multiplier * barWidth, // To right align | |||
barWidth * i, | |||
'0', | |||
barWidth, | |||
this.colors[i], | |||
d.name); | |||
this.legendArea.appendChild(rect); | |||
}); | |||
} | |||
} | |||
// Overlay | |||
makeOverlay() { | |||
if(this.init) { | |||
this.init = 0; | |||
@@ -512,6 +538,8 @@ export default class AxisChart extends BaseChart { | |||
fire(this.parent, "data-select", this.getDataPoint()); | |||
} | |||
// API | |||
addDataPoint(label, datasetValues, index=this.state.datasetLength) { | |||
super.addDataPoint(label, datasetValues, index); | |||
@@ -1,35 +1,49 @@ | |||
import SvgTip from '../objects/SvgTip'; | |||
import { $, isElementInViewport, getElementContentWidth } from '../utils/dom'; | |||
import { makeSVGContainer, makeSVGDefs, makeSVGGroup } from '../utils/draw'; | |||
import { VERT_SPACE_OUTSIDE_BASE_CHART, TRANSLATE_Y_BASE_CHART, LEFT_MARGIN_BASE_CHART, | |||
RIGHT_MARGIN_BASE_CHART, INIT_CHART_UPDATE_TIMEOUT, CHART_POST_ANIMATE_TIMEOUT } from '../utils/constants'; | |||
import { getColor, DEFAULT_COLORS } from '../utils/colors'; | |||
import { getDifferentChart } from '../config'; | |||
import { makeSVGContainer, makeSVGDefs, makeSVGGroup, makeText } from '../utils/draw'; | |||
import { BASE_MEASURES, getExtraHeight, getExtraWidth, getTopOffset, getLeftOffset, | |||
INIT_CHART_UPDATE_TIMEOUT, CHART_POST_ANIMATE_TIMEOUT, DEFAULT_COLORS} from '../utils/constants'; | |||
import { getColor, isValidColor } from '../utils/colors'; | |||
import { runSMILAnimation } from '../utils/animation'; | |||
import { downloadFile, prepareForExport } from '../utils/export'; | |||
let BOUND_DRAW_FN; | |||
export default class BaseChart { | |||
constructor(parent, options) { | |||
this.rawChartArgs = options; | |||
this.parent = typeof parent === 'string' ? document.querySelector(parent) : parent; | |||
this.parent = typeof parent === 'string' | |||
? document.querySelector(parent) | |||
: parent; | |||
if (!(this.parent instanceof HTMLElement)) { | |||
throw new Error('No `parent` element to render on was provided.'); | |||
} | |||
this.rawChartArgs = options; | |||
this.title = options.title || ''; | |||
this.subtitle = options.subtitle || ''; | |||
this.argHeight = options.height || 240; | |||
this.type = options.type || ''; | |||
this.realData = this.prepareData(options.data); | |||
this.data = this.prepareFirstData(this.realData); | |||
this.colors = []; | |||
this.colors = this.validateColors(options.colors, this.type); | |||
this.config = { | |||
showTooltip: 1, // calculate | |||
showLegend: options.showLegend || 1, | |||
showLegend: 1, // calculate | |||
isNavigable: options.isNavigable || 0, | |||
animate: 1 | |||
}; | |||
this.measures = JSON.parse(JSON.stringify(BASE_MEASURES)); | |||
let m = this.measures; | |||
this.setMeasures(options); | |||
if(!this.title.length) { m.titleHeight = 0; } | |||
if(!this.config.showLegend) m.legendHeight = 0; | |||
this.argHeight = options.height || m.baseHeight; | |||
this.state = {}; | |||
this.options = {}; | |||
@@ -42,84 +56,81 @@ export default class BaseChart { | |||
this.configure(options); | |||
} | |||
configure(args) { | |||
this.setColors(args); | |||
this.setMargins(); | |||
// Bind window events | |||
window.addEventListener('resize', () => this.draw(true)); | |||
window.addEventListener('orientationchange', () => this.draw(true)); | |||
prepareData(data) { | |||
return data; | |||
} | |||
setColors() { | |||
let args = this.rawChartArgs; | |||
// Needs structure as per only labels/datasets, from config | |||
const list = args.type === 'percentage' || args.type === 'pie' | |||
? args.data.labels | |||
: args.data.datasets; | |||
prepareFirstData(data) { | |||
return data; | |||
} | |||
if(!args.colors || (list && args.colors.length < list.length)) { | |||
this.colors = DEFAULT_COLORS; | |||
} else { | |||
this.colors = args.colors; | |||
} | |||
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; | |||
} | |||
this.colors = this.colors.map(color => getColor(color)); | |||
setMeasures() { | |||
// Override measures, including those for title and legend | |||
// set config for legend and title | |||
} | |||
setMargins() { | |||
configure() { | |||
let height = this.argHeight; | |||
this.baseHeight = height; | |||
this.height = height - VERT_SPACE_OUTSIDE_BASE_CHART; | |||
this.translateY = TRANSLATE_Y_BASE_CHART; | |||
this.height = height - getExtraHeight(this.measures); | |||
// Horizontal margins | |||
this.leftMargin = LEFT_MARGIN_BASE_CHART; | |||
this.rightMargin = RIGHT_MARGIN_BASE_CHART; | |||
// Bind window events | |||
BOUND_DRAW_FN = this.boundDrawFn.bind(this); | |||
window.addEventListener('resize', BOUND_DRAW_FN); | |||
window.addEventListener('orientationchange', this.boundDrawFn.bind(this)); | |||
} | |||
validate() { | |||
return true; | |||
boundDrawFn() { | |||
this.draw(true); | |||
} | |||
setup() { | |||
if(this.validate()) { | |||
this._setup(); | |||
} | |||
unbindWindowEvents() { | |||
window.removeEventListener('resize', BOUND_DRAW_FN); | |||
window.removeEventListener('orientationchange', this.boundDrawFn.bind(this)); | |||
} | |||
_setup() { | |||
// Has to be called manually | |||
setup() { | |||
this.makeContainer(); | |||
this.updateWidth(); | |||
this.makeTooltip(); | |||
this.draw(false, true); | |||
} | |||
setupComponents() { | |||
this.components = new Map(); | |||
} | |||
makeContainer() { | |||
this.container = $.create('div', { | |||
className: 'chart-container', | |||
innerHTML: `<h6 class="title">${this.title}</h6> | |||
<h6 class="sub-title uppercase">${this.subtitle}</h6> | |||
<div class="frappe-chart graphics"></div> | |||
<div class="graph-stats-container"></div>` | |||
}); | |||
// Chart needs a dedicated parent element | |||
this.parent.innerHTML = ''; | |||
this.parent.appendChild(this.container); | |||
this.chartWrapper = this.container.querySelector('.frappe-chart'); | |||
this.statsWrapper = this.container.querySelector('.graph-stats-container'); | |||
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.chartWrapper, | |||
parent: this.container, | |||
colors: this.colors | |||
}); | |||
this.bindTooltip(); | |||
@@ -128,7 +139,8 @@ export default class BaseChart { | |||
bindTooltip() {} | |||
draw(onlyWidthChange=false, init=false) { | |||
this.calcWidth(); | |||
this.updateWidth(); | |||
this.calc(onlyWidthChange); | |||
this.makeChartArea(); | |||
this.setupComponents(); | |||
@@ -139,36 +151,87 @@ export default class BaseChart { | |||
if(init) { | |||
this.data = this.realData; | |||
setTimeout(() => {this.update();}, this.initTimeout); | |||
setTimeout(() => {this.update(this.data);}, this.initTimeout); | |||
} | |||
if(!onlyWidthChange) { | |||
this.renderLegend(); | |||
} | |||
this.renderLegend(); | |||
this.setupNavigation(init); | |||
} | |||
calcWidth() { | |||
calc() {} // builds state | |||
updateWidth() { | |||
this.baseWidth = getElementContentWidth(this.parent); | |||
this.width = this.baseWidth - (this.leftMargin + this.rightMargin); | |||
this.width = this.baseWidth - getExtraWidth(this.measures); | |||
} | |||
update(data=this.data) { | |||
this.data = this.prepareData(data); | |||
this.calc(); // builds state | |||
this.render(); | |||
} | |||
makeChartArea() { | |||
if(this.svg) { | |||
this.container.removeChild(this.svg); | |||
} | |||
let m = this.measures; | |||
prepareData(data=this.data) { | |||
return data; | |||
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)); | |||
} | |||
prepareFirstData(data=this.data) { | |||
return data; | |||
updateTipOffset(x, y) { | |||
this.tip.offset = { | |||
x: x, | |||
y: y | |||
}; | |||
} | |||
calc() {} // builds state | |||
setupComponents() { this.components = new Map(); } | |||
update(data) { | |||
if(!data) { | |||
console.error('No data to update.'); | |||
} | |||
this.data = this.prepareData(data); | |||
this.calc(); // builds state | |||
this.render(); | |||
} | |||
render(components=this.components, animate=true) { | |||
if(this.config.isNavigable) { | |||
@@ -182,7 +245,7 @@ export default class BaseChart { | |||
elementsToAnimate = elementsToAnimate.concat(c.update(animate)); | |||
}); | |||
if(elementsToAnimate.length > 0) { | |||
runSMILAnimation(this.chartWrapper, this.svg, elementsToAnimate); | |||
runSMILAnimation(this.container, this.svg, elementsToAnimate); | |||
setTimeout(() => { | |||
components.forEach(c => c.make()); | |||
this.updateNav(); | |||
@@ -195,39 +258,9 @@ export default class BaseChart { | |||
updateNav() { | |||
if(this.config.isNavigable) { | |||
// if(!this.overlayGuides){ | |||
this.makeOverlay(); | |||
this.bindUnits(); | |||
// } else { | |||
// this.updateOverlay(); | |||
// } | |||
} | |||
} | |||
makeChartArea() { | |||
if(this.svg) { | |||
this.chartWrapper.removeChild(this.svg); | |||
} | |||
this.svg = makeSVGContainer( | |||
this.chartWrapper, | |||
'chart', | |||
this.baseWidth, | |||
this.baseHeight | |||
); | |||
this.svgDefs = makeSVGDefs(this.svg); | |||
// I WISH !!! | |||
// this.svg = makeSVGGroup( | |||
// svgContainer, | |||
// 'flipped-coord-system', | |||
// `translate(0, ${this.baseHeight}) scale(1, -1)` | |||
// ); | |||
this.drawArea = makeSVGGroup( | |||
this.svg, | |||
this.type + '-chart', | |||
`translate(${this.leftMargin}, ${this.translateY})` | |||
); | |||
} | |||
renderLegend() {} | |||
@@ -247,7 +280,7 @@ export default class BaseChart { | |||
}; | |||
document.addEventListener('keydown', (e) => { | |||
if(isElementInViewport(this.chartWrapper)) { | |||
if(isElementInViewport(this.container)) { | |||
e = e || window.event; | |||
if(this.keyActions[e.keyCode]) { | |||
this.keyActions[e.keyCode](); | |||
@@ -276,7 +309,8 @@ export default class BaseChart { | |||
updateDataset() {} | |||
getDifferentChart(type) { | |||
return getDifferentChart(type, this.type, this.parent, this.rawChartArgs); | |||
export() { | |||
let chartSvg = prepareForExport(this.svg); | |||
downloadFile(this.title || 'Chart', [chartSvg]); | |||
} | |||
} |
@@ -1,263 +1,294 @@ | |||
import BaseChart from './BaseChart'; | |||
import { makeSVGGroup, makeHeatSquare, makeText } from '../utils/draw'; | |||
import { addDays, getDdMmYyyy, getWeeksBetween } from '../utils/date-utils'; | |||
import { getComponent } from '../objects/ChartComponents'; | |||
import { makeText, heatSquare } from '../utils/draw'; | |||
import { DAY_NAMES_SHORT, addDays, areInSameMonth, getLastDateInMonth, setDayToSunday, getYyyyMmDd, getWeeksBetween, getMonthName, clone, | |||
NO_OF_MILLIS, NO_OF_YEAR_MONTHS, NO_OF_DAYS_IN_WEEK } from '../utils/date-utils'; | |||
import { calcDistribution, getMaxCheckpoint } from '../utils/intervals'; | |||
import { isValidColor } from '../utils/colors'; | |||
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.domain = options.domain || ''; | |||
this.subdomain = options.subdomain || ''; | |||
this.data = options.data || {}; | |||
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | |||
this.countLabel = options.countLabel || ''; | |||
let today = new Date(); | |||
this.start = options.start || addDays(today, 365); | |||
let legendColors = (options.legendColors || []).slice(0, 5); | |||
this.legendColors = this.validate_colors(legendColors) | |||
? legendColors | |||
: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']; | |||
let validStarts = ['Sunday', 'Monday']; | |||
let startSubDomain = validStarts.includes(options.startSubDomain) | |||
? options.startSubDomain : 'Sunday'; | |||
this.startSubDomainIndex = validStarts.indexOf(startSubDomain); | |||
// Fixed 5-color theme, | |||
// More colors are difficult to parse visually | |||
this.distribution_size = 5; | |||
this.translateX = 0; | |||
this.setup(); | |||
} | |||
setMargins() { | |||
super.setMargins(); | |||
this.leftMargin = 10; | |||
this.translateY = 10; | |||
} | |||
setMeasures(options) { | |||
let m = this.measures; | |||
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | |||
validate_colors(colors) { | |||
if(colors.length < 5) return 0; | |||
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 valid = 1; | |||
colors.forEach(function(string) { | |||
if(!isValidColor(string)) { | |||
valid = 0; | |||
console.warn('"' + string + '" is not a valid color.'); | |||
} | |||
}, this); | |||
return valid; | |||
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); | |||
} | |||
configure() { | |||
super.configure(); | |||
this.today = new Date(); | |||
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); | |||
} | |||
if(!this.start) { | |||
this.start = new Date(); | |||
this.start.setFullYear( this.start.getFullYear() - 1 ); | |||
prepareData(data=this.data) { | |||
if(data.start && data.end && data.start > data.end) { | |||
throw new Error('Start date cannot be greater than end date.'); | |||
} | |||
this.firstWeekStart = new Date(this.start.toDateString()); | |||
this.lastWeekStart = new Date(this.today.toDateString()); | |||
if(this.firstWeekStart.getDay() !== 7) { | |||
addDays(this.firstWeekStart, (-1) * this.firstWeekStart.getDay()); | |||
if(!data.start) { | |||
data.start = new Date(); | |||
data.start.setFullYear( data.start.getFullYear() - 1 ); | |||
} | |||
if(this.lastWeekStart.getDay() !== 7) { | |||
addDays(this.lastWeekStart, (-1) * this.lastWeekStart.getDay()); | |||
if(!data.end) { data.end = new Date(); } | |||
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; | |||
} | |||
this.no_of_cols = getWeeksBetween(this.firstWeekStart + '', this.lastWeekStart + '') + 1; | |||
return data; | |||
} | |||
calcWidth() { | |||
this.baseWidth = (this.no_of_cols + 3) * 12 ; | |||
calc() { | |||
let s = this.state; | |||
if(this.discreteDomains) { | |||
this.baseWidth += (12 * 12); | |||
} | |||
} | |||
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); | |||
makeChartArea() { | |||
super.makeChartArea(); | |||
this.domainLabelGroup = makeSVGGroup(this.drawArea, | |||
'domain-label-group chart-label'); | |||
s.domainConfigs = this.getDomains(); | |||
} | |||
this.dataGroups = makeSVGGroup(this.drawArea, | |||
'data-groups', | |||
`translate(0, 20)` | |||
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, | |||
xTranslate: s.domainConfigs | |||
.filter((config, j) => j < i) | |||
.map(config => config.cols.length - lessCol) | |||
.reduce((a, b) => a + b, 0) | |||
* COL_WIDTH | |||
}, | |||
function() { | |||
return s.domainConfigs[i]; | |||
}.bind(this) | |||
]); | |||
this.components = new Map(componentConfigs | |||
.map((args, i) => { | |||
let component = getComponent(...args); | |||
return [args[0] + '-' + i, component]; | |||
}) | |||
); | |||
this.container.querySelector('.title').style.display = 'None'; | |||
this.container.querySelector('.sub-title').style.display = 'None'; | |||
this.container.querySelector('.graph-stats-container').style.display = 'None'; | |||
this.chartWrapper.style.marginTop = '0px'; | |||
this.chartWrapper.style.paddingTop = '0px'; | |||
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; | |||
}); | |||
} | |||
calc() { | |||
let dataValues = Object.keys(this.data).map(key => this.data[key]); | |||
this.distribution = calcDistribution(dataValues, this.distribution_size); | |||
update(data) { | |||
if(!data) { | |||
console.error('No data to update.'); | |||
} | |||
this.monthNames = ["January", "February", "March", "April", "May", "June", | |||
"July", "August", "September", "October", "November", "December" | |||
]; | |||
this.data = this.prepareData(data); | |||
this.draw(); | |||
this.bindTooltip(); | |||
} | |||
render() { | |||
this.renderAllWeeksAndStoreXValues(this.no_of_cols); | |||
} | |||
bindTooltip() { | |||
this.container.addEventListener('mousemove', (e) => { | |||
this.components.forEach(comp => { | |||
let daySquares = comp.store; | |||
let daySquare = e.target; | |||
if(daySquares.includes(daySquare)) { | |||
renderAllWeeksAndStoreXValues(no_of_weeks) { | |||
// renderAllWeeksAndStoreXValues | |||
this.domainLabelGroup.textContent = ''; | |||
this.dataGroups.textContent = ''; | |||
let currentWeekSunday = new Date(this.firstWeekStart); | |||
this.weekCol = 0; | |||
this.currentMonth = currentWeekSunday.getMonth(); | |||
this.months = [this.currentMonth + '']; | |||
this.monthWeeks = {}, this.monthStartPoints = []; | |||
this.monthWeeks[this.currentMonth] = 0; | |||
this.monthStartPoints.push(13); | |||
for(var i = 0; i < no_of_weeks; i++) { | |||
let dataGroup, monthChange = 0; | |||
let day = new Date(currentWeekSunday); | |||
[dataGroup, monthChange] = this.get_week_squares_group(day, this.weekCol); | |||
this.dataGroups.appendChild(dataGroup); | |||
this.weekCol += 1 + parseInt(this.discreteDomains && monthChange); | |||
this.monthWeeks[this.currentMonth]++; | |||
if(monthChange) { | |||
this.currentMonth = (this.currentMonth + 1) % 12; | |||
this.months.push(this.currentMonth + ''); | |||
this.monthWeeks[this.currentMonth] = 1; | |||
} | |||
addDays(currentWeekSunday, 7); | |||
} | |||
this.render_month_labels(); | |||
} | |||
let count = daySquare.getAttribute('data-value'); | |||
let dateParts = daySquare.getAttribute('data-date').split('-'); | |||
get_week_squares_group(currentDate, index) { | |||
const noOfWeekdays = 7; | |||
const squareSide = 10; | |||
const cellPadding = 2; | |||
const step = 1; | |||
const todayTime = this.today.getTime(); | |||
let month = getMonthName(parseInt(dateParts[1])-1, true); | |||
let monthChange = 0; | |||
let weekColChange = 0; | |||
let gOff = this.container.getBoundingClientRect(), pOff = daySquare.getBoundingClientRect(); | |||
let dataGroup = makeSVGGroup(this.dataGroups, 'data-group'); | |||
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]; | |||
for(var y = 0, i = 0; i < noOfWeekdays; i += step, y += (squareSide + cellPadding)) { | |||
let dataValue = 0; | |||
let colorIndex = 0; | |||
this.tip.setValues(x, y, {name: name, value: value, valueFirst: 1}, []); | |||
this.tip.showTip(); | |||
} | |||
}); | |||
}); | |||
} | |||
let currentTimestamp = currentDate.getTime()/1000; | |||
let timestamp = Math.floor(currentTimestamp - (currentTimestamp % 86400)).toFixed(1); | |||
renderLegend() { | |||
this.legendArea.textContent = ''; | |||
let x = 0; | |||
let y = ROW_HEIGHT; | |||
if(this.data[timestamp]) { | |||
dataValue = this.data[timestamp]; | |||
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, color); | |||
this.legendArea.appendChild(square); | |||
}); | |||
if(this.data[Math.round(timestamp)]) { | |||
dataValue = this.data[Math.round(timestamp)]; | |||
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()]; | |||
if(dataValue) { | |||
colorIndex = getMaxCheckpoint(dataValue, this.distribution); | |||
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)); | |||
let x = 13 + (index + weekColChange) * 12; | |||
addDays(endDate, 1); | |||
startOfMonth = endDate; | |||
} | |||
let dataAttr = { | |||
'data-date': getDdMmYyyy(currentDate), | |||
'data-value': dataValue, | |||
'data-day': currentDate.getDay() | |||
}; | |||
return domainConfigs; | |||
} | |||
let heatSquare = makeHeatSquare('day', x, y, squareSide, | |||
this.legendColors[colorIndex], dataAttr); | |||
getDomainConfig(startDate, endDate='') { | |||
let [month, year] = [startDate.getMonth(), startDate.getFullYear()]; | |||
let startOfWeek = setDayToSunday(startDate); // TODO: Monday as well | |||
endDate = clone(endDate) || getLastDateInMonth(month, year); | |||
dataGroup.appendChild(heatSquare); | |||
let domainConfig = { | |||
index: month, | |||
cols: [] | |||
}; | |||
let nextDate = new Date(currentDate); | |||
addDays(nextDate, 1); | |||
if(nextDate.getTime() > todayTime) break; | |||
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); | |||
if(nextDate.getMonth() - currentDate.getMonth()) { | |||
monthChange = 1; | |||
if(this.discreteDomains) { | |||
weekColChange = 1; | |||
} | |||
startOfWeek = new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd); | |||
addDays(startOfWeek, 1); | |||
} | |||
this.monthStartPoints.push(13 + (index + weekColChange) * 12); | |||
} | |||
currentDate = nextDate; | |||
if(col[NO_OF_DAYS_IN_WEEK - 1].dataValue !== undefined) { | |||
addDays(startOfWeek, 1); | |||
cols.push(this.getCol(startOfWeek, month, true)); | |||
} | |||
return [dataGroup, monthChange]; | |||
} | |||
domainConfig.cols = cols; | |||
render_month_labels() { | |||
// this.first_month_label = 1; | |||
// if (this.firstWeekStart.getDate() > 8) { | |||
// this.first_month_label = 0; | |||
// } | |||
// this.last_month_label = 1; | |||
// let first_month = this.months.shift(); | |||
// let first_month_start = this.monthStartPoints.shift(); | |||
// render first month if | |||
// let last_month = this.months.pop(); | |||
// let last_month_start = this.monthStartPoints.pop(); | |||
// render last month if | |||
this.months.shift(); | |||
this.monthStartPoints.shift(); | |||
this.months.pop(); | |||
this.monthStartPoints.pop(); | |||
this.monthStartPoints.map((start, i) => { | |||
let month_name = this.monthNames[this.months[i]].substring(0, 3); | |||
let text = makeText('y-value-text', start+12, 10, month_name); | |||
this.domainLabelGroup.appendChild(text); | |||
}); | |||
return domainConfig; | |||
} | |||
bindTooltip() { | |||
Array.prototype.slice.call( | |||
document.querySelectorAll(".data-group .day") | |||
).map(el => { | |||
el.addEventListener('mouseenter', (e) => { | |||
let count = e.target.getAttribute('data-value'); | |||
let dateParts = e.target.getAttribute('data-date').split('-'); | |||
getCol(startDate, month, empty = false) { | |||
let s = this.state; | |||
let month = this.monthNames[parseInt(dateParts[1])-1].substring(0, 3); | |||
// startDate is the start of week | |||
let currentDate = clone(startDate); | |||
let col = []; | |||
let gOff = this.chartWrapper.getBoundingClientRect(), pOff = e.target.getBoundingClientRect(); | |||
for(var i = 0; i < NO_OF_DAYS_IN_WEEK; i++, addDays(currentDate, 1)) { | |||
let config = {}; | |||
let width = parseInt(e.target.getAttribute('width')); | |||
let x = pOff.left - gOff.left + (width+2)/2; | |||
let y = pOff.top - gOff.top - (width+2)/2; | |||
let value = count + ' ' + this.countLabel; | |||
let name = ' on ' + month + ' ' + dateParts[0] + ', ' + dateParts[2]; | |||
// Non-generic adjustment for entire heatmap, needs state | |||
let currentDateWithinData = currentDate >= s.start && currentDate <= s.end; | |||
this.tip.setValues(x, y, {name: name, value: value, valueFirst: 1}, []); | |||
this.tip.showTip(); | |||
}); | |||
}); | |||
if(empty || currentDate.getMonth() !== month || !currentDateWithinData) { | |||
config.yyyyMmDd = getYyyyMmDd(currentDate); | |||
} else { | |||
config = this.getSubDomainConfig(currentDate); | |||
} | |||
col.push(config); | |||
} | |||
return col; | |||
} | |||
update(data) { | |||
super.update(data); | |||
this.bindTooltip(); | |||
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; | |||
} | |||
} |
@@ -14,11 +14,11 @@ export default class MultiAxisChart extends AxisChart { | |||
this.type = 'multiaxis'; | |||
} | |||
setMargins() { | |||
super.setMargins(); | |||
setMeasures() { | |||
super.setMeasures(); | |||
let noOfLeftAxes = this.data.datasets.filter(d => d.axisPosition === 'left').length; | |||
this.leftMargin = (noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN; | |||
this.rightMargin = (this.data.datasets.length - noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN; | |||
this.measures.margins.left = (noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN; | |||
this.measures.margins.right = (this.data.datasets.length - noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN; | |||
} | |||
prepareYAxis() { } | |||
@@ -1,73 +1,90 @@ | |||
import AggregationChart from './AggregationChart'; | |||
import { $, getOffset } from '../utils/dom'; | |||
import { getOffset } from '../utils/dom'; | |||
import { getComponent } from '../objects/ChartComponents'; | |||
import { PERCENTAGE_BAR_DEFAULT_HEIGHT, PERCENTAGE_BAR_DEFAULT_DEPTH } from '../utils/constants'; | |||
export default class PercentageChart extends AggregationChart { | |||
constructor(parent, args) { | |||
super(parent, args); | |||
this.type = 'percentage'; | |||
this.setup(); | |||
} | |||
makeChartArea() { | |||
this.chartWrapper.className += ' ' + 'graph-focus-margin'; | |||
this.chartWrapper.style.marginTop = '45px'; | |||
setMeasures(options) { | |||
let m = this.measures; | |||
this.barOptions = options.barOptions || {}; | |||
this.statsWrapper.className += ' ' + 'graph-focus-margin'; | |||
this.statsWrapper.style.marginBottom = '30px'; | |||
this.statsWrapper.style.paddingTop = '0px'; | |||
let b = this.barOptions; | |||
b.height = b.height || PERCENTAGE_BAR_DEFAULT_HEIGHT; | |||
b.depth = b.depth || PERCENTAGE_BAR_DEFAULT_DEPTH; | |||
this.svg = $.create('div', { | |||
className: 'div', | |||
inside: this.chartWrapper | |||
}); | |||
m.paddings.right = 30; | |||
m.legendHeight = 80; | |||
m.baseHeight = (b.height + b.depth * 0.5) * 8; | |||
} | |||
this.chart = $.create('div', { | |||
className: 'progress-chart', | |||
inside: this.svg | |||
}); | |||
setupComponents() { | |||
let s = this.state; | |||
this.percentageBar = $.create('div', { | |||
className: 'progress', | |||
inside: this.chart | |||
}); | |||
let componentConfigs = [ | |||
[ | |||
'percentageBars', | |||
{ | |||
barHeight: this.barOptions.height, | |||
barDepth: this.barOptions.depth, | |||
}, | |||
function() { | |||
return { | |||
xPositions: s.xPositions, | |||
widths: s.widths, | |||
colors: this.colors | |||
}; | |||
}.bind(this) | |||
] | |||
]; | |||
this.components = new Map(componentConfigs | |||
.map(args => { | |||
let component = getComponent(...args); | |||
return [args[0], component]; | |||
})); | |||
} | |||
render() { | |||
calc() { | |||
super.calc(); | |||
let s = this.state; | |||
this.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0); | |||
s.slices = []; | |||
s.sliceTotals.map((total, i) => { | |||
let slice = $.create('div', { | |||
className: `progress-bar`, | |||
'data-index': i, | |||
inside: this.percentageBar, | |||
styles: { | |||
background: this.colors[i], | |||
width: total*100/this.grandTotal + "%" | |||
} | |||
}); | |||
s.slices.push(slice); | |||
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)) { | |||
this.chartWrapper.addEventListener('mousemove', (e) => { | |||
let slice = e.target; | |||
if(slice.classList.contains('progress-bar')) { | |||
let i = slice.getAttribute('data-index'); | |||
let gOff = getOffset(this.chartWrapper), pOff = getOffset(slice); | |||
let i = bars.indexOf(bar); | |||
let gOff = getOffset(this.container), pOff = getOffset(bar); | |||
let x = pOff.left - gOff.left + slice.offsetWidth/2; | |||
let y = pOff.top - gOff.top - 6; | |||
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 percent = (s.sliceTotals[i]*100/this.grandTotal).toFixed(1); | |||
let fraction = s.sliceTotals[i]/s.grandTotal; | |||
this.tip.setValues(x, y, {name: title, value: percent + "%"}); | |||
this.tip.setValues(x, y, {name: title, value: (fraction*100).toFixed(1) + "%"}); | |||
this.tip.showTip(); | |||
} | |||
}); | |||
@@ -12,6 +12,7 @@ export default class PieChart extends AggregationChart { | |||
super(parent, args); | |||
this.type = 'pie'; | |||
this.initTimeout = 0; | |||
this.init = 1; | |||
this.setup(); | |||
} | |||
@@ -27,28 +28,11 @@ export default class PieChart extends AggregationChart { | |||
this.clockWise = args.clockWise || false; | |||
} | |||
prepareFirstData(data=this.data) { | |||
this.init = 1; | |||
return data; | |||
} | |||
calc() { | |||
super.calc(); | |||
let s = this.state; | |||
this.center = { | |||
x: this.width / 2, | |||
y: this.height / 2 | |||
}; | |||
this.radius = (this.height > this.width ? this.center.x : this.center.y); | |||
s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0); | |||
this.calcSlices(); | |||
} | |||
calcSlices() { | |||
let s = this.state; | |||
const { radius, clockWise } = this; | |||
const prevSlicesProperties = s.slicesProperties || []; | |||
@@ -142,8 +126,8 @@ export default class PieChart extends AggregationChart { | |||
} | |||
bindTooltip() { | |||
this.chartWrapper.addEventListener('mousemove', this.mouseMove); | |||
this.chartWrapper.addEventListener('mouseleave', this.mouseLeave); | |||
this.container.addEventListener('mousemove', this.mouseMove); | |||
this.container.addEventListener('mouseleave', this.mouseLeave); | |||
} | |||
mouseMove(e){ | |||
@@ -1,46 +0,0 @@ | |||
import Chart from './chart'; | |||
const ALL_CHART_TYPES = ['line', 'scatter', 'bar', 'percentage', 'heatmap', 'pie']; | |||
const COMPATIBLE_CHARTS = { | |||
bar: ['line', 'scatter', 'percentage', 'pie'], | |||
line: ['scatter', 'bar', 'percentage', 'pie'], | |||
pie: ['line', 'scatter', 'percentage', 'bar'], | |||
scatter: ['line', 'bar', 'percentage', 'pie'], | |||
percentage: ['bar', 'line', 'scatter', 'pie'], | |||
heatmap: [] | |||
}; | |||
// Needs structure as per only labels/datasets | |||
const COLOR_COMPATIBLE_CHARTS = { | |||
bar: ['line', 'scatter'], | |||
line: ['scatter', 'bar'], | |||
pie: ['percentage'], | |||
scatter: ['line', 'bar'], | |||
percentage: ['pie'], | |||
heatmap: [] | |||
}; | |||
export function getDifferentChart(type, current_type, parent, args) { | |||
if(type === current_type) return; | |||
if(!ALL_CHART_TYPES.includes(type)) { | |||
console.error(`'${type}' is not a valid chart type.`); | |||
} | |||
if(!COMPATIBLE_CHARTS[current_type].includes(type)) { | |||
console.error(`'${current_type}' chart cannot be converted to a '${type}' chart.`); | |||
} | |||
// whether the new chart can use the existing colors | |||
const useColor = COLOR_COMPATIBLE_CHARTS[current_type].includes(type); | |||
// Okay, this is anticlimactic | |||
// this function will need to actually be 'changeChartType(type)' | |||
// that will update only the required elements, but for now ... | |||
args.type = type; | |||
args.colors = useColor ? args.colors : undefined; | |||
return new Chart(parent, args); | |||
} |
@@ -0,0 +1,10 @@ | |||
import * as Charts from './chart'; | |||
let frappe = { }; | |||
frappe.NAME = 'Frappe Charts'; | |||
frappe.VERSION = '1.1.0'; | |||
frappe = Object.assign({ }, frappe, Charts); | |||
export default frappe; |
@@ -1,8 +1,9 @@ | |||
import { makeSVGGroup } from '../utils/draw'; | |||
import { makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, getPaths } from '../utils/draw'; | |||
import { makeText, makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, percentageBar, getPaths, heatSquare } from '../utils/draw'; | |||
import { equilizeNoOfElements } from '../utils/draw-utils'; | |||
import { translateHoriLine, translateVertLine, animateRegion, animateBar, | |||
animateDot, animatePath, animatePathStr } from '../utils/animate'; | |||
import { getMonthName } from '../utils/date-utils'; | |||
class ChartComponent { | |||
constructor({ | |||
@@ -23,6 +24,7 @@ class ChartComponent { | |||
this.animateElements = animateElements; | |||
this.store = []; | |||
this.labels = []; | |||
this.layerClass = layerClass; | |||
this.layerClass = typeof(this.layerClass) === 'function' | |||
@@ -36,7 +38,7 @@ class ChartComponent { | |||
} | |||
setup(parent) { | |||
this.layer = makeSVGGroup(parent, this.layerClass, this.layerTransform); | |||
this.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent); | |||
} | |||
make() { | |||
@@ -51,13 +53,16 @@ class ChartComponent { | |||
this.store.forEach(element => { | |||
this.layer.appendChild(element); | |||
}); | |||
this.labels.forEach(element => { | |||
this.layer.appendChild(element); | |||
}); | |||
} | |||
update(animate = true) { | |||
this.refresh(); | |||
let animateElements = []; | |||
if(animate) { | |||
animateElements = this.animateElements(this.data); | |||
animateElements = this.animateElements(this.data) || []; | |||
} | |||
return animateElements; | |||
} | |||
@@ -80,6 +85,21 @@ let componentConfigs = { | |||
); | |||
} | |||
}, | |||
percentageBars: { | |||
layerClass: 'percentage-bars', | |||
makeElements(data) { | |||
return data.xPositions.map((x, i) =>{ | |||
let y = 0; | |||
let bar = percentageBar(x, y, data.widths[i], | |||
this.constants.barHeight, this.constants.barDepth, data.colors[i]); | |||
return bar; | |||
}); | |||
}, | |||
animateElements(newData) { | |||
if(newData) return []; | |||
} | |||
}, | |||
yAxis: { | |||
layerClass: 'y axis', | |||
makeElements(data) { | |||
@@ -145,9 +165,9 @@ let componentConfigs = { | |||
yMarkers: { | |||
layerClass: 'y-markers', | |||
makeElements(data) { | |||
return data.map(marker => | |||
yMarker(marker.position, marker.label, this.constants.width, | |||
{pos:'right', mode: 'span', lineType: 'dashed'}) | |||
return data.map(m => | |||
yMarker(m.position, m.label, this.constants.width, | |||
{labelPos: m.options.labelPos, mode: 'span', lineType: 'dashed'}) | |||
); | |||
}, | |||
animateElements(newData) { | |||
@@ -155,13 +175,15 @@ let componentConfigs = { | |||
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] | |||
label: newLabels[i], | |||
options: newOptions[i] | |||
}; | |||
})); | |||
@@ -176,9 +198,9 @@ let componentConfigs = { | |||
yRegions: { | |||
layerClass: 'y-regions', | |||
makeElements(data) { | |||
return data.map(region => | |||
yRegion(region.startPos, region.endPos, this.constants.width, | |||
region.label) | |||
return data.map(r => | |||
yRegion(r.startPos, r.endPos, this.constants.width, | |||
r.label, {labelPos: r.options.labelPos}) | |||
); | |||
}, | |||
animateElements(newData) { | |||
@@ -187,6 +209,7 @@ let componentConfigs = { | |||
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); | |||
@@ -195,7 +218,8 @@ let componentConfigs = { | |||
return { | |||
startPos: oldStarts[i], | |||
endPos: oldPos[i], | |||
label: newLabels[i] | |||
label: newLabels[i], | |||
options: newOptions[i] | |||
}; | |||
})); | |||
@@ -211,6 +235,49 @@ let componentConfigs = { | |||
} | |||
}, | |||
heatDomain: { | |||
layerClass: function() { return 'heat-domain domain-' + this.constants.index; }, | |||
makeElements(data) { | |||
let {index, colWidth, rowHeight, squareSize, 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, 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) { | |||
@@ -1,4 +1,5 @@ | |||
import { $ } from '../utils/dom'; | |||
import { TOOLTIP_POINTER_TRIANGLE_HEIGHT } from '../utils/constants'; | |||
export default class SvgTip { | |||
constructor({ | |||
@@ -28,7 +29,6 @@ export default class SvgTip { | |||
refresh() { | |||
this.fill(); | |||
this.calcPosition(); | |||
// this.showTip(); | |||
} | |||
makeTooltip() { | |||
@@ -64,12 +64,13 @@ export default class SvgTip { | |||
this.listValues.map((set, i) => { | |||
const color = this.colors[i] || 'black'; | |||
let value = set.formatted === 0 || set.formatted ? set.formatted : set.value; | |||
let li = $.create('li', { | |||
styles: { | |||
'border-top': `3px solid ${color}` | |||
}, | |||
innerHTML: `<strong style="display: block;">${ set.value === 0 || set.value ? set.value : '' }</strong> | |||
innerHTML: `<strong style="display: block;">${ value === 0 || value ? value : '' }</strong> | |||
${set.title ? set.title : '' }` | |||
}); | |||
@@ -80,7 +81,8 @@ export default class SvgTip { | |||
calcPosition() { | |||
let width = this.container.offsetWidth; | |||
this.top = this.y - this.container.offsetHeight; | |||
this.top = this.y - this.container.offsetHeight | |||
- TOOLTIP_POINTER_TRIANGLE_HEIGHT; | |||
this.left = this.x - width/2; | |||
let maxLeft = this.parent.offsetWidth - width; | |||
@@ -102,4 +102,3 @@ export function animatePath(paths, newXList, newYList, zeroLine) { | |||
export function animatePathStr(oldPath, pathStr) { | |||
return [oldPath, {d: pathStr}, UNIT_ANIM_DUR, STD_EASING]; | |||
} | |||
@@ -98,6 +98,7 @@ export function zeroDataPrep(realData) { | |||
export function getShortenedLabels(chartWidth, labels=[], isSeries=true) { | |||
let allowedSpace = chartWidth / labels.length; | |||
if(allowedSpace <= 0) allowedSpace = 1; | |||
let allowedLetters = allowedSpace / DEFAULT_CHAR_WIDTH; | |||
let calcLabels = labels.map((label, i) => { | |||
@@ -121,4 +122,4 @@ export function getShortenedLabels(chartWidth, labels=[], isSeries=true) { | |||
}); | |||
return calcLabels; | |||
} | |||
} |
@@ -15,9 +15,6 @@ const PRESET_COLOR_MAP = { | |||
'dark-grey': '#b8c2cc' | |||
}; | |||
export const DEFAULT_COLORS = ['light-blue', 'blue', 'violet', 'red', 'orange', | |||
'yellow', 'green', 'light-green', 'purple', 'magenta', 'light-grey', 'dark-grey']; | |||
function limitColor(r){ | |||
if (r > 255) return 255; | |||
else if (r < 0) return 0; | |||
@@ -1,8 +1,63 @@ | |||
export const VERT_SPACE_OUTSIDE_BASE_CHART = 50; | |||
export const TRANSLATE_Y_BASE_CHART = 20; | |||
export const LEFT_MARGIN_BASE_CHART = 60; | |||
export const RIGHT_MARGIN_BASE_CHART = 40; | |||
export const Y_AXIS_MARGIN = 60; | |||
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; | |||
@@ -10,14 +65,42 @@ export const CHART_POST_ANIMATE_TIMEOUT = 400; | |||
export const DEFAULT_AXIS_CHART_TYPE = 'line'; | |||
export const AXIS_DATASET_CHART_TYPES = ['line', 'bar']; | |||
export const AXIS_LEGEND_BAR_SIZE = 100; | |||
export const BAR_CHART_SPACE_RATIO = 0.5; | |||
export const MIN_BAR_PERCENT_HEIGHT = 0.01; | |||
export const LINE_CHART_DOT_SIZE = 4; | |||
export const DOT_OVERLAY_SIZE_INCR = 4; | |||
export const PERCENTAGE_BAR_DEFAULT_HEIGHT = 20; | |||
export const PERCENTAGE_BAR_DEFAULT_DEPTH = 2; | |||
// Fixed 5-color theme, | |||
// More colors are difficult to parse visually | |||
export const HEATMAP_DISTRIBUTION_SIZE = 5; | |||
export const HEATMAP_SQUARE_SIZE = 10; | |||
export const HEATMAP_GUTTER_SIZE = 2; | |||
export const DEFAULT_CHAR_WIDTH = 7; | |||
export const TOOLTIP_POINTER_TRIANGLE_HEIGHT = 5; | |||
const DEFAULT_CHART_COLORS = ['light-blue', 'blue', 'violet', 'red', 'orange', | |||
'yellow', 'green', 'light-green', 'purple', 'magenta', 'light-grey', 'dark-grey']; | |||
const HEATMAP_COLORS_GREEN = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']; | |||
export const HEATMAP_COLORS_BLUE = ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e']; | |||
export const HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | |||
export const DEFAULT_COLORS = { | |||
bar: DEFAULT_CHART_COLORS, | |||
line: DEFAULT_CHART_COLORS, | |||
pie: DEFAULT_CHART_COLORS, | |||
percentage: DEFAULT_CHART_COLORS, | |||
heatmap: HEATMAP_COLORS_GREEN | |||
}; | |||
// Universal constants | |||
export const ANGLE_RATIO = Math.PI / 180; | |||
export const FULL_ANGLE = 360; | |||
export const FULL_ANGLE = 360; |
@@ -1,39 +1,90 @@ | |||
// 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(dateStr) { | |||
let result = new Date(dateStr); | |||
function treatAsUtc(date) { | |||
let result = new Date(date); | |||
result.setMinutes(result.getMinutes() - result.getTimezoneOffset()); | |||
return result; | |||
} | |||
export function getDdMmYyyy(date) { | |||
export function getYyyyMmDd(date) { | |||
let dd = date.getDate(); | |||
let mm = date.getMonth() + 1; // getMonth() is zero-based | |||
return [ | |||
(dd>9 ? '' : '0') + dd, | |||
date.getFullYear(), | |||
(mm>9 ? '' : '0') + mm, | |||
date.getFullYear() | |||
(dd>9 ? '' : '0') + dd | |||
].join('-'); | |||
} | |||
export function getWeeksBetween(startDateStr, endDateStr) { | |||
return Math.ceil(getDaysBetween(startDateStr, endDateStr) / 7); | |||
export function clone(date) { | |||
return new Date(date.getTime()); | |||
} | |||
export function getDaysBetween(startDateStr, endDateStr) { | |||
let millisecondsPerDay = 24 * 60 * 60 * 1000; | |||
return (treatAsUtc(endDateStr) - treatAsUtc(startDateStr)) / millisecondsPerDay; | |||
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 addDays(date, numberOfDays) { | |||
date.setDate(date.getDate() + numberOfDays); | |||
export function setDayToSunday(date) { | |||
let newDate = clone(date); | |||
const day = newDate.getDay(); | |||
if(day !== 0) { | |||
addDays(newDate, (-1) * day); | |||
} | |||
return newDate; | |||
} | |||
export function getMonthName(i) { | |||
let monthNames = ["January", "February", "March", "April", "May", "June", | |||
"July", "August", "September", "October", "November", "December" | |||
]; | |||
return monthNames[i]; | |||
// mutates | |||
export function addDays(date, numberOfDays) { | |||
date.setDate(date.getDate() + numberOfDays); | |||
} |
@@ -109,3 +109,22 @@ export function fire(target, type, properties) { | |||
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); | |||
} |
@@ -1,11 +1,13 @@ | |||
import { getBarHeightAndYAttr } from './draw-utils'; | |||
import { getStringWidth } from './helpers'; | |||
import { DOT_OVERLAY_SIZE_INCR } from './constants'; | |||
import { DOT_OVERLAY_SIZE_INCR, PERCENTAGE_BAR_DEFAULT_DEPTH } from './constants'; | |||
import { lightenDarkenColor } from './colors'; | |||
const AXIS_TICK_LENGTH = 6; | |||
export const AXIS_TICK_LENGTH = 6; | |||
const LABEL_MARGIN = 4; | |||
export const FONT_SIZE = 10; | |||
const BASE_LINE_COLOR = '#dadada'; | |||
const FONT_FILL = '#555b51'; | |||
function $(expr, con) { | |||
return typeof expr === "string"? (con || document).querySelector(expr) : expr || null; | |||
@@ -79,12 +81,13 @@ export function makeSVGDefs(svgContainer) { | |||
}); | |||
} | |||
export function makeSVGGroup(parent, className, transform='') { | |||
return createSVG('g', { | |||
export function makeSVGGroup(className, transform='', parent=undefined) { | |||
let args = { | |||
className: className, | |||
inside: parent, | |||
transform: transform | |||
}); | |||
}; | |||
if(parent) args.inside = parent; | |||
return createSVG('g', args); | |||
} | |||
export function wrapInSVGGroup(elements, className='') { | |||
@@ -131,7 +134,29 @@ export function makeGradient(svgDefElem, color, lighter = false) { | |||
return gradientId; | |||
} | |||
export function makeHeatSquare(className, x, y, size, fill='none', data={}) { | |||
export function percentageBar(x, y, width, height, | |||
depth=PERCENTAGE_BAR_DEFAULT_DEPTH, fill='none') { | |||
let args = { | |||
className: 'percentage-bar', | |||
x: x, | |||
y: y, | |||
width: width, | |||
height: height, | |||
fill: fill, | |||
styles: { | |||
'stroke': lightenDarkenColor(fill, -25), | |||
// Diabolically good: https://stackoverflow.com/a/9000859 | |||
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray | |||
'stroke-dasharray': `0, ${height + width}, ${width}, ${height}`, | |||
'stroke-width': depth | |||
}, | |||
}; | |||
return createSVG("rect", args); | |||
} | |||
export function heatSquare(className, x, y, size, fill='none', data={}) { | |||
let args = { | |||
className: className, | |||
x: x, | |||
@@ -148,13 +173,77 @@ export function makeHeatSquare(className, x, y, size, fill='none', data={}) { | |||
return createSVG("rect", args); | |||
} | |||
export function makeText(className, x, y, content) { | |||
export function legendBar(x, y, size, fill='none', label) { | |||
let args = { | |||
className: 'legend-bar', | |||
x: 0, | |||
y: 0, | |||
width: size, | |||
height: '2px', | |||
fill: fill | |||
}; | |||
let text = createSVG('text', { | |||
className: 'legend-dataset-text', | |||
x: 0, | |||
y: 0, | |||
dy: (FONT_SIZE * 2) + 'px', | |||
'font-size': (FONT_SIZE * 1.2) + 'px', | |||
'text-anchor': 'start', | |||
fill: FONT_FILL, | |||
innerHTML: label | |||
}); | |||
let group = createSVG('g', { | |||
transform: `translate(${x}, ${y})` | |||
}); | |||
group.appendChild(createSVG("rect", args)); | |||
group.appendChild(text); | |||
return group; | |||
} | |||
export function legendDot(x, y, size, fill='none', label) { | |||
let args = { | |||
className: 'legend-dot', | |||
cx: 0, | |||
cy: 0, | |||
r: size, | |||
fill: fill | |||
}; | |||
let text = createSVG('text', { | |||
className: 'legend-dataset-text', | |||
x: 0, | |||
y: 0, | |||
dx: (FONT_SIZE) + 'px', | |||
dy: (FONT_SIZE/3) + 'px', | |||
'font-size': (FONT_SIZE * 1.2) + 'px', | |||
'text-anchor': 'start', | |||
fill: FONT_FILL, | |||
innerHTML: label | |||
}); | |||
let group = createSVG('g', { | |||
transform: `translate(${x}, ${y})` | |||
}); | |||
group.appendChild(createSVG("circle", args)); | |||
group.appendChild(text); | |||
return group; | |||
} | |||
export function makeText(className, x, y, content, options = {}) { | |||
let fontSize = options.fontSize || FONT_SIZE; | |||
let dy = options.dy !== undefined ? options.dy : (fontSize / 2); | |||
let fill = options.fill || FONT_FILL; | |||
let textAnchor = options.textAnchor || 'start'; | |||
return createSVG('text', { | |||
className: className, | |||
x: x, | |||
y: y, | |||
dy: (FONT_SIZE / 2) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
dy: dy + 'px', | |||
'font-size': fontSize + 'px', | |||
fill: fill, | |||
'text-anchor': textAnchor, | |||
innerHTML: content | |||
}); | |||
} | |||
@@ -294,9 +383,13 @@ export function xLine(x, label, height, options={}) { | |||
} | |||
export function yMarker(y, label, width, options={}) { | |||
if(!options.labelPos) options.labelPos = 'right'; | |||
let x = options.labelPos === 'left' ? LABEL_MARGIN | |||
: width - getStringWidth(label, 5) - LABEL_MARGIN; | |||
let labelSvg = createSVG('text', { | |||
className: 'chart-label', | |||
x: width - getStringWidth(label, 5) - LABEL_MARGIN, | |||
x: x, | |||
y: 0, | |||
dy: (FONT_SIZE / -2) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
@@ -315,7 +408,7 @@ export function yMarker(y, label, width, options={}) { | |||
return line; | |||
} | |||
export function yRegion(y1, y2, width, label) { | |||
export function yRegion(y1, y2, width, label, options={}) { | |||
// return a group | |||
let height = y1 - y2; | |||
@@ -333,9 +426,13 @@ export function yRegion(y1, y2, width, label) { | |||
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: width - getStringWidth(label+"", 4.5) - LABEL_MARGIN, | |||
x: x, | |||
y: 0, | |||
dy: (FONT_SIZE / -2) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
@@ -357,6 +454,11 @@ export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, m | |||
let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine); | |||
y -= offset; | |||
if(height === 0) { | |||
height = meta.minHeight; | |||
y -= meta.minHeight; | |||
} | |||
let rect = createSVG('rect', { | |||
className: `bar mini`, | |||
style: `fill: ${color}`, | |||
@@ -364,7 +466,7 @@ export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, m | |||
x: x, | |||
y: y, | |||
width: width, | |||
height: height || meta.minHeight // TODO: correct y for positive min height | |||
height: height | |||
}); | |||
label += ""; | |||
@@ -452,7 +554,6 @@ export function getPaths(xList, yList, color, options={}, meta={}) { | |||
if(options.regionFill) { | |||
let gradient_id_region = makeGradient(meta.svgDefs, color, true); | |||
// TODO: use zeroLine OR minimum | |||
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})`); | |||
} | |||
@@ -490,6 +591,25 @@ export let makeOverlay = { | |||
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); | |||
} | |||
@@ -532,5 +652,23 @@ export let updateOverlay = { | |||
if(transformValue) { | |||
overlay.setAttribute('transform', transformValue); | |||
} | |||
} | |||
}, | |||
'heat_square': (unit, overlay) => { | |||
let transformValue; | |||
if(unit.nodeName !== 'circle') { | |||
transformValue = unit.getAttribute('transform'); | |||
unit = unit.childNodes[0]; | |||
} | |||
let attributes = ['cx', 'cy']; | |||
Object.values(unit.attributes) | |||
.filter(attr => attributes.includes(attr.name) && attr.specified) | |||
.map(attr => { | |||
overlay.setAttribute(attr.name, attr.nodeValue); | |||
}); | |||
if(transformValue) { | |||
overlay.setAttribute('transform', transformValue); | |||
} | |||
}, | |||
}; |
@@ -0,0 +1,33 @@ | |||
import { $ } from '../utils/dom'; | |||
import { CSSTEXT } from '../../css/chartsCss'; | |||
export function downloadFile(filename, data) { | |||
var a = document.createElement('a'); | |||
a.style = "display: none"; | |||
var blob = new Blob(data, {type: "image/svg+xml; charset=utf-8"}); | |||
var url = window.URL.createObjectURL(blob); | |||
a.href = url; | |||
a.download = filename; | |||
document.body.appendChild(a); | |||
a.click(); | |||
setTimeout(function(){ | |||
document.body.removeChild(a); | |||
window.URL.revokeObjectURL(url); | |||
}, 300); | |||
} | |||
export function prepareForExport(svg) { | |||
let clone = svg.cloneNode(true); | |||
clone.classList.add('chart-container'); | |||
clone.setAttribute('xmlns', "http://www.w3.org/2000/svg"); | |||
clone.setAttribute('xmlns:xlink', "http://www.w3.org/1999/xlink"); | |||
let styleEl = $.create('style', { | |||
'innerHTML': CSSTEXT | |||
}); | |||
clone.insertBefore(styleEl, clone.firstChild); | |||
let container = $.create('div'); | |||
container.appendChild(clone); | |||
return container.innerHTML; | |||
} |
@@ -77,9 +77,18 @@ export function bindChange(obj, getFn, setFn) { | |||
}); | |||
} | |||
// 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, | |||
x: Math.sin(angle * ANGLE_RATIO) * radius, | |||
y: Math.cos(angle * ANGLE_RATIO) * radius, | |||
}; | |||
} |
@@ -200,6 +200,23 @@ 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 | |||
@@ -0,0 +1,10 @@ | |||
const assert = require('assert') | |||
const helpers = require('../helpers') | |||
describe('utils.helpers', () => { | |||
it('should return a value fixed upto 2 decimals', () => { | |||
assert.equal(helpers.floatTwo(1.234), 1.23); | |||
assert.equal(helpers.floatTwo(1.456), 1.46); | |||
assert.equal(helpers.floatTwo(1), 1.00); | |||
}); | |||
}); |
@@ -1,225 +0,0 @@ | |||
.chart-container { | |||
// https://www.smashingmagazine.com/2015/11/using-system-ui-fonts-practical-guide/ | |||
font-family: -apple-system, BlinkMacSystemFont, | |||
"Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", | |||
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; | |||
.graph-focus-margin { | |||
margin: 0px 5%; | |||
} | |||
&>.title { | |||
margin-top: 25px; | |||
margin-left: 25px; | |||
text-align: left; | |||
font-weight: normal; | |||
font-size: 12px; | |||
color: #6c7680; | |||
} | |||
.graphics { | |||
margin-top: 10px; | |||
padding-top: 10px; | |||
padding-bottom: 10px; | |||
position: relative; | |||
} | |||
.graph-stats-group { | |||
display: flex; | |||
justify-content: space-around; | |||
flex: 1; | |||
} | |||
.graph-stats-container { | |||
display: flex; | |||
justify-content: space-between; | |||
padding: 10px; | |||
&:before, | |||
&:after { | |||
content: ''; | |||
display: block; | |||
} | |||
.stats { | |||
padding-bottom: 15px; | |||
} | |||
.stats-title { | |||
color: #8D99A6; | |||
} | |||
.stats-value { | |||
font-size: 20px; | |||
font-weight: 300; | |||
} | |||
.stats-description { | |||
font-size: 12px; | |||
color: #8D99A6; | |||
} | |||
.graph-data { | |||
.stats-value { | |||
color: #98d85b; | |||
} | |||
} | |||
} | |||
.axis, .chart-label { | |||
fill: #555b51; | |||
// temp commented | |||
line { | |||
stroke: #dadada; | |||
} | |||
} | |||
.percentage-graph { | |||
.progress { | |||
margin-bottom: 0px; | |||
} | |||
} | |||
.dataset-units { | |||
circle { | |||
stroke: #fff; | |||
stroke-width: 2; | |||
} | |||
// temp | |||
path { | |||
fill: none; | |||
stroke-opacity: 1; | |||
stroke-width: 2px; | |||
} | |||
} | |||
.multiaxis-chart { | |||
.line-horizontal, .y-axis-guide { | |||
stroke-width: 2px; | |||
} | |||
} | |||
.dataset-path { | |||
stroke-width: 2px; | |||
} | |||
.path-group { | |||
path { | |||
fill: none; | |||
stroke-opacity: 1; | |||
stroke-width: 2px; | |||
} | |||
} | |||
line.dashed { | |||
stroke-dasharray: 5,3; | |||
} | |||
.axis-line { | |||
// &.x-axis-label { | |||
// display: block; | |||
// } | |||
// TODO: hack dy attr to be settable via styles | |||
.specific-value { | |||
text-anchor: start; | |||
} | |||
.y-line { | |||
text-anchor: end; | |||
} | |||
.x-line { | |||
text-anchor: middle; | |||
} | |||
} | |||
.progress { | |||
height: 20px; | |||
margin-bottom: 20px; | |||
overflow: hidden; | |||
background-color: #f5f5f5; | |||
border-radius: 4px; | |||
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); | |||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); | |||
} | |||
.progress-bar { | |||
float: left; | |||
width: 0; | |||
height: 100%; | |||
font-size: 12px; | |||
line-height: 20px; | |||
color: #fff; | |||
text-align: center; | |||
background-color: #36414c; | |||
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); | |||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); | |||
-webkit-transition: width .6s ease; | |||
-o-transition: width .6s ease; | |||
transition: width .6s ease; | |||
} | |||
.graph-svg-tip { | |||
position: absolute; | |||
z-index: 99999; | |||
padding: 10px; | |||
font-size: 12px; | |||
color: #959da5; | |||
text-align: center; | |||
background: rgba(0, 0, 0, 0.8); | |||
border-radius: 3px; | |||
ul { | |||
padding-left: 0; | |||
display: flex; | |||
} | |||
ol { | |||
padding-left: 0; | |||
display: flex; | |||
} | |||
ul.data-point-list { | |||
li { | |||
min-width: 90px; | |||
flex: 1; | |||
font-weight: 600; | |||
} | |||
} | |||
strong { | |||
color: #dfe2e5; | |||
font-weight: 600; | |||
} | |||
.svg-pointer { | |||
position: absolute; | |||
height: 5px; | |||
margin: 0 0 0 -5px; | |||
content: " "; | |||
border: 5px solid transparent; | |||
border-top-color: rgba(0, 0, 0, 0.8); | |||
} | |||
&.comparison { | |||
padding: 0; | |||
text-align: left; | |||
pointer-events: none; | |||
.title { | |||
display: block; | |||
padding: 10px; | |||
margin: 0; | |||
font-weight: 600; | |||
line-height: 1; | |||
pointer-events: none; | |||
} | |||
ul { | |||
margin: 0; | |||
white-space: nowrap; | |||
list-style: none; | |||
} | |||
li { | |||
display: inline-block; | |||
padding: 5px 10px; | |||
} | |||
} | |||
} | |||
/*Indicators*/ | |||
.indicator, | |||
.indicator-right { | |||
background: none; | |||
font-size: 12px; | |||
vertical-align: middle; | |||
font-weight: bold; | |||
color: #6c7680; | |||
} | |||
.indicator i { | |||
content: ''; | |||
display: inline-block; | |||
height: 8px; | |||
width: 8px; | |||
border-radius: 8px; | |||
} | |||
.indicator::before,.indicator i { | |||
margin: 0 4px 0 0px; | |||
} | |||
.indicator-right::after { | |||
margin: 0 0 0 4px; | |||
} | |||
} | |||