浏览代码

Merge pull request #154 from frappe/develop

Develop
tags/1.2.0
Prateeksha Singh 7 年前
committed by GitHub
父节点
当前提交
a47a4d0eb7
找不到此签名对应的密钥 GPG 密钥 ID: 4AEE18F83AFDEB23
共有 52 个文件被更改,包括 10669 次插入3533 次删除
  1. +5
    -1
      .babelrc
  2. +62
    -5
      .gitignore
  3. +14
    -0
      .travis.yml
  4. +45
    -0
      Makefile
  5. +11
    -3
      README.md
  6. +1057
    -635
      dist/frappe-charts.esm.js
  7. +2
    -1
      dist/frappe-charts.min.cjs.js
  8. +1
    -0
      dist/frappe-charts.min.cjs.js.map
  9. +1
    -1
      dist/frappe-charts.min.css
  10. +2
    -1
      dist/frappe-charts.min.esm.js
  11. +1
    -0
      dist/frappe-charts.min.esm.js.map
  12. +1
    -1
      dist/frappe-charts.min.iife.js
  13. +1
    -1
      dist/frappe-charts.min.iife.js.map
  14. +205
    -0
      docs/assets/js/data.js
  15. +1
    -1
      docs/assets/js/frappe-charts.min.js
  16. +1
    -1
      docs/assets/js/frappe-charts.min.js.map
  17. +109
    -296
      docs/assets/js/index.js
  18. +653
    -0
      docs/assets/js/index.min.js
  19. +1
    -0
      docs/assets/js/index.min.js.map
  20. +0
    -559
      docs/assets/js/old_index.js
  21. +61
    -34
      docs/index.html
  22. +0
    -312
      docs/old_index.html
  23. +1880
    -870
      package-lock.json
  24. +13
    -2
      package.json
  25. +74
    -11
      rollup.config.js
  26. +116
    -0
      src/css/charts.scss
  27. +1
    -0
      src/css/chartsCss.js
  28. +7
    -9
      src/js/chart.js
  29. +30
    -16
      src/js/charts/AggregationChart.js
  30. +96
    -68
      src/js/charts/AxisChart.js
  31. +145
    -111
      src/js/charts/BaseChart.js
  32. +227
    -196
      src/js/charts/Heatmap.js
  33. +4
    -4
      src/js/charts/MultiAxisChart.js
  34. +61
    -44
      src/js/charts/PercentageChart.js
  35. +3
    -19
      src/js/charts/PieChart.js
  36. +0
    -46
      src/js/config.js
  37. +10
    -0
      src/js/index.js
  38. +78
    -11
      src/js/objects/ChartComponents.js
  39. +5
    -3
      src/js/objects/SvgTip.js
  40. +0
    -1
      src/js/utils/animate.js
  41. +2
    -1
      src/js/utils/axis-chart-utils.js
  42. +0
    -3
      src/js/utils/colors.js
  43. +89
    -6
      src/js/utils/constants.js
  44. +68
    -17
      src/js/utils/date-utils.js
  45. +19
    -0
      src/js/utils/dom.js
  46. +154
    -16
      src/js/utils/draw.js
  47. +33
    -0
      src/js/utils/export.js
  48. +11
    -2
      src/js/utils/helpers.js
  49. +17
    -0
      src/js/utils/intervals.js
  50. +10
    -0
      src/js/utils/test/helpers.test.js
  51. +0
    -225
      src/scss/charts.scss
  52. +5282
    -0
      yarn.lock

+ 5
- 1
.babelrc 查看文件

@@ -6,5 +6,9 @@
}
}]
],
"plugins": ["external-helpers"]
"env": {
"test": {
"presets": ["env"]
}
}
}

+ 62
- 5
.gitignore 查看文件

@@ -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

+ 14
- 0
.travis.yml 查看文件

@@ -0,0 +1,14 @@
language: node_js

node_js:
- "6"
- "8"

before_install:
- make install

script:
- make test

after_success:
- make coveralls

+ 45
- 0
Makefile 查看文件

@@ -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

+ 11
- 3
README.md 查看文件

@@ -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


+ 1057
- 635
dist/frappe-charts.esm.js
文件差异内容过多而无法显示
查看文件


+ 2
- 1
dist/frappe-charts.min.cjs.js
文件差异内容过多而无法显示
查看文件


+ 1
- 0
dist/frappe-charts.min.cjs.js.map
文件差异内容过多而无法显示
查看文件


+ 1
- 1
dist/frappe-charts.min.css 查看文件

@@ -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}

+ 2
- 1
dist/frappe-charts.min.esm.js
文件差异内容过多而无法显示
查看文件


+ 1
- 0
dist/frappe-charts.min.esm.js.map
文件差异内容过多而无法显示
查看文件


+ 1
- 1
dist/frappe-charts.min.iife.js
文件差异内容过多而无法显示
查看文件


+ 1
- 1
dist/frappe-charts.min.iife.js.map
文件差异内容过多而无法显示
查看文件


+ 205
- 0
docs/assets/js/data.js 查看文件

@@ -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
- 1
docs/assets/js/frappe-charts.min.js
文件差异内容过多而无法显示
查看文件


+ 1
- 1
docs/assets/js/frappe-charts.min.js.map
文件差异内容过多而无法显示
查看文件


+ 109
- 296
docs/assets/js/index.js 查看文件

@@ -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();
});

+ 653
- 0
docs/assets/js/index.min.js 查看文件

@@ -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
- 0
docs/assets/js/index.min.js.map
文件差异内容过多而无法显示
查看文件


+ 0
- 559
docs/assets/js/old_index.js 查看文件

@@ -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;
}



+ 61
- 34
docs/index.html 查看文件

@@ -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"> &lt;script src="https://unpkg.com/frappe-charts@1.0.0/dist/frappe-charts.min.iife.js"&gt;&lt;/script&gt;</code></pre>
<pre><code class="hljs html"> &lt;script src="https://unpkg.com/frappe-charts@1.0.0"&gt;&lt;/script&gt;</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>

+ 0
- 312
docs/old_index.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"> &lt;script src="https://unpkg.com/frappe-charts@1.0.0/dist/frappe-charts.min.iife.js"&gt;&lt;/script&gt;</code></pre>
<p class="step-explain">Make a new Chart</p>
<pre><code class="hljs html"> &lt!--HTML--&gt;
&lt;div id="chart"&gt;&lt;/div&gt;</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>

+ 1880
- 870
package-lock.json
文件差异内容过多而无法显示
查看文件


+ 13
- 2
package.json 查看文件

@@ -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"
}
}

+ 74
- 11
rollup.config.js 查看文件

@@ -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({


+ 116
- 0
src/css/charts.scss 查看文件

@@ -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;
}
}
}

+ 1
- 0
src/css/chartsCss.js 查看文件

@@ -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}";

+ 7
- 9
src/js/chart.js 查看文件

@@ -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 };

+ 30
- 16
src/js/charts/AggregationChart.js 查看文件

@@ -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++;
});
}
}

+ 96
- 68
src/js/charts/AxisChart.js 查看文件

@@ -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);


+ 145
- 111
src/js/charts/BaseChart.js 查看文件

@@ -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]);
}
}

+ 227
- 196
src/js/charts/Heatmap.js 查看文件

@@ -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;
}
}

+ 4
- 4
src/js/charts/MultiAxisChart.js 查看文件

@@ -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() { }


+ 61
- 44
src/js/charts/PercentageChart.js 查看文件

@@ -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();
}
});


+ 3
- 19
src/js/charts/PieChart.js 查看文件

@@ -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){


+ 0
- 46
src/js/config.js 查看文件

@@ -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);
}

+ 10
- 0
src/js/index.js 查看文件

@@ -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;

+ 78
- 11
src/js/objects/ChartComponents.js 查看文件

@@ -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) {


+ 5
- 3
src/js/objects/SvgTip.js 查看文件

@@ -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;



+ 0
- 1
src/js/utils/animate.js 查看文件

@@ -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];
}


+ 2
- 1
src/js/utils/axis-chart-utils.js 查看文件

@@ -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;
}
}

+ 0
- 3
src/js/utils/colors.js 查看文件

@@ -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;


+ 89
- 6
src/js/utils/constants.js 查看文件

@@ -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;

+ 68
- 17
src/js/utils/date-utils.js 查看文件

@@ -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);
}

+ 19
- 0
src/js/utils/dom.js 查看文件

@@ -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);
}

+ 154
- 16
src/js/utils/draw.js 查看文件

@@ -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);
}
},
};

+ 33
- 0
src/js/utils/export.js 查看文件

@@ -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;
}

+ 11
- 2
src/js/utils/helpers.js 查看文件

@@ -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,
};
}

+ 17
- 0
src/js/utils/intervals.js 查看文件

@@ -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


+ 10
- 0
src/js/utils/test/helpers.test.js 查看文件

@@ -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);
});
});

+ 0
- 225
src/scss/charts.scss 查看文件

@@ -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;
}
}



+ 5282
- 0
yarn.lock
文件差异内容过多而无法显示
查看文件


正在加载...
取消
保存