chore: init 3.0.0-beta.1

This commit is contained in:
hustcc 2021-02-07 18:03:59 +08:00
commit 469acda80b
37 changed files with 1979 additions and 0 deletions

10
.commitlintrc.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
extends: ['@commitlint/config-angular'],
rules: {
'type-enum': [
2,
'always',
['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'wip'],
],
},
};

20
.editorconfig Normal file
View File

@ -0,0 +1,20 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[Makefile]
indent_style = tab
indent_size = 1
[*.md]
trim_trailing_whitespace = false

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist/
test
build/

40
.eslintrc Normal file
View File

@ -0,0 +1,40 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"plugins": ["prettier", "@typescript-eslint", "import"],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"impliedStrict": true
},
"rules": {
"no-sparse-arrays": 0,
"no-self-assign": 0,
"no-unused-vars": 0, // @typescript-eslint/no-unused-vars
"no-inner-declarations": 0,
"prettier/prettier": 2,
"@typescript-eslint/no-unused-vars": 1,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-use-before-define": [2, { "functions": false }],
"@typescript-eslint/ban-ts-ignore": 0,
"@typescript-eslint/interface-name-prefix": 0,
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/no-inferrable-types": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/type-annotation-spacing": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/ban-types": "off",
"no-undef": 0,
"@typescript-eslint/no-var-requires": 0,
"import/order": 0,
"import/no-default-export": 0
}
}

25
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: build
on: ["push", "pull_request"]
jobs:
build:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v1
- name: Use Node.js 12
uses: actions/setup-node@v1
with:
node-version: 12.10.0
- name: npm install
run: |
npm install
- name: build
run: |
npm run build
env:
CI: true
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

83
.gitignore vendored Normal file
View File

@ -0,0 +1,83 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# lock
package-lock.json
# 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 (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
yarn.lock
# 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
public
build
dist
temp
.DS_Store
.idea
.cache
demos/assets/screenshots
/lib
/esm
/dist
*.sw*
*.un~
.vscode
/stats.json
.umi
.umi-production

5
.lintmdrc Normal file
View File

@ -0,0 +1,5 @@
{
"rules": {
"no-long-code": 1
}
}

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
stats.json
.cache
dist
.umi

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"printWidth": 120,
"arrowParens": "always",
"endOfLine": "auto"
}

23
.umirc.js Normal file
View File

@ -0,0 +1,23 @@
import { defineConfig } from 'dumi';
export default defineConfig({
mode: 'site',
title: '\b',
base: '/',
publicPath: process.env.NODE_ENV === 'production' ? '/echarts-for-react/' : '/',
exportStatic: {},
logo: 'https://cdn.jsdelivr.net/gh/apache/echarts-website@asf-site/zh/images/logo.png?_v_=20200710_1',
styles: [
'.__dumi-default-navbar-logo:not([data-plaintext]) { padding-left: 200px!important; }',
'.echarts-for-react.class_1 { height: 400px!important; }',
'.echarts-for-react.class_2 { height: 500px!important; }',
],
navs: [
null,
{ title: '在线文档', path: 'https://github.com/hustcc/echarts-for-react' },
{ title: 'GitHub', path: 'https://github.com/hustcc/echarts-for-react' },
],
analytics: {
},
// more config: https://d.umijs.org/config
});

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 AntV team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do { so, subject } to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

306
README.md Normal file
View File

@ -0,0 +1,306 @@
# echarts-for-react
A very simple React wrapper for [Apache ECharts (incubating)](https://github.com/apache/incubator-echarts).
[![Build Status](https://github.com/hustcc/echarts-for-react/workflows/build/badge.svg?branch=master)](https://github.com/hustcc/echarts-for-react/actions?query=workflow%3Abuild)
[![Coverage](https://img.shields.io/coveralls/hustcc/echarts-for-react/master.svg)](https://coveralls.io/github/hustcc/echarts-for-react)
[![npm](https://img.shields.io/npm/v/echarts-for-react.svg)](https://www.npmjs.com/package/echarts-for-react)
[![NPM downloads](https://img.shields.io/npm/dm/echarts-for-react.svg)](https://www.npmjs.com/package/echarts-for-react)
[![License](https://img.shields.io/npm/l/echarts-for-react.svg)](https://www.npmjs.com/package/echarts-for-react)
![ECharts Ver](https://img.shields.io/badge/echarts-%5E3.0.0%20%7C%7C%20%5E4.0.0%20%7C%7C%20%5E5.0.0-blue.svg)
![React Ver](https://img.shields.io/badge/React-%20%5E15.0.0%20%7C%7C%20%20%5E16.0.0%20%7C%7C%20%20%5E17.0.0-blue.svg)
# 1. install
```sh
npm install --save echarts-for-react
# `echarts` is the peerDependence of `echarts-for-react`, you can install echarts with your own version.
npm install --save echarts
```
Then use it.
```js
import ReactEcharts from 'echarts-for-react';
// render echarts option.
<ReactEcharts option={this.getOption()} />
```
You can run demo by:
```sh
git clone git@github.com:hustcc/echarts-for-react.git
npm install
npm start
```
then open [http://127.0.0.1:8080/](http://127.0.0.1:8080/) in your browser. or see [http://git.hust.cc/echarts-for-react/](http://git.hust.cc/echarts-for-react/)
## GL
Install and import [`echarts-gl`](https://www.npmjs.com/package/echarts-gl) module when you want to create a [GL instance](https://www.echartsjs.com/examples/zh/index.html#chart-type-globe)
- **`Install`**
```sh
npm install --save echarts-gl
```
- **`Import`**
```js
import 'echarts-gl'
import ReactEcharts from "echarts-for-react";
<ReactEcharts
option={GL_OPTION}
/>
```
# 2. usage
Code of a simple demo code showed below. For more example can see: [http://git.hust.cc/echarts-for-react/](http://git.hust.cc/echarts-for-react/)
### javascript
```js
import React from 'react';
import ReactEcharts from 'echarts-for-react'; // or var ReactEcharts = require('echarts-for-react');
<ReactEcharts
option={this.getOption()}
notMerge={true}
lazyUpdate={true}
theme={"theme_name"}
onChartReady={this.onChartReadyCallback}
onEvents={EventsDict}
opts={} />
```
### typescript
```js
import * as React from "react";
import ReactEcharts from "echarts-for-react";
<ReactEcharts
option={this.getOption()}
notMerge={true}
lazyUpdate={true}
theme={"theme_name"}
onChartReady={this.onChartReadyCallback}
onEvents={EventsDict}
opts={} />
```
Import ECharts.js modules manually to reduce bundle size
```js
import React from 'react';
// import the core library.
import ReactEchartsCore from 'echarts-for-react/lib/core';
// then import echarts modules those you have used manually.
import echarts from 'echarts/lib/echarts';
// import 'echarts/lib/chart/line';
import 'echarts/lib/chart/bar';
// import 'echarts/lib/chart/pie';
// import 'echarts/lib/chart/scatter';
// import 'echarts/lib/chart/radar';
// import 'echarts/lib/chart/map';
// import 'echarts/lib/chart/treemap';
// import 'echarts/lib/chart/graph';
// import 'echarts/lib/chart/gauge';
// import 'echarts/lib/chart/funnel';
// import 'echarts/lib/chart/parallel';
// import 'echarts/lib/chart/sankey';
// import 'echarts/lib/chart/boxplot';
// import 'echarts/lib/chart/candlestick';
// import 'echarts/lib/chart/effectScatter';
// import 'echarts/lib/chart/lines';
// import 'echarts/lib/chart/heatmap';
// import 'echarts/lib/component/graphic';
// import 'echarts/lib/component/grid';
// import 'echarts/lib/component/legend';
import 'echarts/lib/component/tooltip';
// import 'echarts/lib/component/polar';
// import 'echarts/lib/component/geo';
// import 'echarts/lib/component/parallel';
// import 'echarts/lib/component/singleAxis';
// import 'echarts/lib/component/brush';
import 'echarts/lib/component/title';
// import 'echarts/lib/component/dataZoom';
// import 'echarts/lib/component/visualMap';
// import 'echarts/lib/component/markPoint';
// import 'echarts/lib/component/markLine';
// import 'echarts/lib/component/markArea';
// import 'echarts/lib/component/timeline';
// import 'echarts/lib/component/toolbox';
// import 'zrender/lib/vml/vml';
// The usage of ReactEchartsCore are same with above.
<ReactEchartsCore
echarts={echarts}
option={this.getOption()}
notMerge={true}
lazyUpdate={true}
theme={"theme_name"}
onChartReady={this.onChartReadyCallback}
onEvents={EventsDict}
opts={} />
```
# 3. component props
- **`option`** (required, object)
the echarts option config, can see [https://echarts.apache.org/option.html#title](https://echarts.apache.org/option.html#title).
- **`notMerge`** (optional, object)
when `setOption`, not merge the data, default is `false`. See [https://echarts.apache.org/api.html#echartsInstance.setOption](https://echarts.apache.org/api.html#echartsInstance.setOption).
- **`lazyUpdate`** (optional, object)
when `setOption`, lazy update the data, default is `false`. See [https://echarts.apache.org/api.html#echartsInstance.setOption](https://echarts.apache.org/api.html#echartsInstance.setOption).
- **`style`** (optional, object)
the `style` of echarts div. `object`, default is {height: '300px'}.
- **`className`** (optional, string)
the `class` of echarts div. you can setting the css style of charts by class name.
- **`theme`** (optional, string)
the `theme` of echarts. `string`, should `registerTheme` before use it (theme object format: [https://github.com/ecomfe/echarts/blob/master/theme/dark.js](https://github.com/ecomfe/echarts/blob/master/theme/dark.js)). e.g.
```js
// import echarts
import echarts from 'echarts';
...
// register theme object
echarts.registerTheme('my_theme', {
backgroundColor: '#f4cccc'
});
...
// render the echarts use option `theme`
<ReactEcharts
option={this.getOption()}
style={{height: '300px', width: '100%'}}
className='echarts-for-echarts'
theme='my_theme' />
```
- **`onChartReady`** (optional, function)
when the chart is ready, will callback the function with the `echarts object` as it's paramter.
- **`loadingOption`** (optional, object)
the echarts loading option config, can see [https://echarts.apache.org/api.html#echartsInstance.showLoading](https://echarts.apache.org/api.html#echartsInstance.showLoading).
- **`showLoading`** (optional, bool, default: false)
`bool`, when the chart is rendering, show the loading mask.
- **`onEvents`** (optional, array(string=>function) )
binding the echarts event, will callback with the `echarts event object`, and `the echart object` as it's paramters. e.g:
```js
let onEvents = {
'click': this.onChartClick,
'legendselectchanged': this.onChartLegendselectchanged
}
...
<ReactEcharts
option={this.getOption()}
style={{height: '300px', width: '100%'}}
onEvents={onEvents} />
```
for more event key name, see: [https://echarts.apache.org/api.html#events](https://echarts.apache.org/api.html#events)
- **`opts`** (optional, object)
the `opts` of echarts. `object`, will be used when initial echarts instance by `echarts.init`. Document [here](https://echarts.apache.org/api.html#echarts.init).
```js
<ReactEcharts
option={this.getOption()}
style={{height: '300px'}}
opts={{renderer: 'svg'}} // use svg to render the chart.
/>
```
# 4. Component API & Echarts API
the Component only has `one API` named `getEchartsInstance`.
- **`getEchartsInstance()`** : get the echarts instance object, then you can use any `API of echarts`.
for example:
```js
// render the echarts component below with rel
<ReactEcharts ref={(e) => { this.echarts_react = e; }}
option={this.getOption()} />
// then get the `ReactEcharts` use this.echarts_react
let echarts_instance = this.echarts_react.getEchartsInstance();
// then you can use any API of echarts.
let base64 = echarts_instance.getDataURL();
```
**About API of echarts, can see** [https://echarts.apache.org/api.html#echartsInstance](https://echarts.apache.org/api.html#echartsInstance).
You can use the API to do:
1. `binding / unbinding` event.
2. `dynamic charts` with dynamic data.
3. get the echarts dom / dataURL / base64, save the chart to png.
4. `release` the charts.
# 5. Q & A
- How to render the chart with svg when using echarts 4.x
Use the props `opts` of component with `renderer = 'svg'`. For example:
```js
<ReactEcharts
option={this.getOption()}
style={{height: '300px'}}
opts={{renderer: 'svg'}} // use svg to render the chart.
/>
```
- How to resolve Error `Component series.scatter3D not exists. Load it first.`
[Install and import `echarts-gl` first](#GL)
# 6. LICENSE
MIT@[hustcc](https://github.com/hustcc).

View File

@ -0,0 +1,37 @@
import React from 'react';
import ReactECharts from '../../src/';
import { render, destroy, createDiv, removeDom } from '../utils';
const options = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
},
series: [
{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
smooth: true,
},
],
};
describe('chart', () => {
it('simple', () => {
let instance;
const div = createDiv();
const Comp = <ReactECharts ref={(e) => (instance = e)} option={options} />;
render(Comp, div);
expect(instance).toBeDefined();
expect(instance.getEchartsInstance()).toBeDefined();
destroy(div);
expect(div.querySelector('*')).toBe(null);
removeDom(div);
});
});

View File

@ -0,0 +1,7 @@
import { isEqual } from '../../src/helper/is-equal';
describe('is-equal', () => {
it('isEqual', () => {
expect(isEqual({}, {})).toBe(true);
});
});

View File

@ -0,0 +1,10 @@
import { isFunction } from '../../src/helper/is-function';
describe('is-function', () => {
it('isFunction', () => {
expect(isFunction(1)).toBe(false);
expect(isFunction('')).toBe(false);
expect(isFunction(true)).toBe(false);
expect(isFunction(() => {})).toBe(true);
});
});

View File

@ -0,0 +1,10 @@
import { isString } from '../../src/helper/is-string';
describe('is-function', () => {
it('isFunction', () => {
expect(isString(1)).toBe(false);
expect(isString('')).toBe(true);
expect(isString(true)).toBe(false);
expect(isString(() => {})).toBe(false);
});
});

View File

@ -0,0 +1,10 @@
import { pick } from '../../src/helper/pick';
describe('pick', () => {
it('pick', () => {
expect(pick({ a: 1 }, [])).toEqual({});
expect(pick({ a: 1 }, ['b'])).toEqual({});
expect(pick({ a: 1 }, ['a'])).toEqual({ a: 1 });
expect(pick({ a: 1 }, ['a', 'b'])).toEqual({ a: 1 });
});
});

44
__tests__/utils/index.ts Normal file
View File

@ -0,0 +1,44 @@
import ReactDOM from 'react-dom';
/**
*
* @param comp
* @param container
*/
export function render(comp: any, container: HTMLElement) {
ReactDOM.render(comp, container);
}
/**
*
* @param container
*/
export function destroy(container: HTMLElement) {
ReactDOM.unmountComponentAtNode(container);
}
/**
* div container body
* @param title
* @param container
* @param id id
*/
export function createDiv(container: HTMLElement = document.body): HTMLElement {
const div = document.createElement('div');
container.appendChild(div);
return div;
}
/**
* dom
* @param dom
*/
export function removeDom(dom: HTMLElement) {
const parent = dom.parentNode;
if (parent) {
parent.removeChild(dom);
}
}

128
docs/examples/api.md Normal file
View File

@ -0,0 +1,128 @@
---
title: ECharts API
order: 2
---
## ECharts API
```tsx
import React, { useRef } from 'react';
import ReactECharts from 'echarts-for-react';
const Page: React.FC = () => {
const option = {
title: {
text: '漏斗图',
subtext: '纯属虚构'
},
tooltip: {
trigger: 'item',
formatter: "{a} <br/>{b} : {c}%"
},
toolbox: {
feature: {
dataView: {readOnly: false},
restore: {},
saveAsImage: {}
}
},
legend: {
data: ['展现','点击','访问','咨询','订单']
},
series: [
{
name: '预期',
type: 'funnel',
left: '10%',
width: '80%',
label: {
normal: {
formatter: '{b}预期'
},
emphasis: {
position:'inside',
formatter: '{b}预期: {c}%'
}
},
labelLine: {
normal: {
show: false
}
},
itemStyle: {
normal: {
opacity: 0.7
}
},
data: [
{value: 60, name: '访问'},
{value: 40, name: '咨询'},
{value: 20, name: '订单'},
{value: 80, name: '点击'},
{value: 100, name: '展现'}
]
},
{
name: '实际',
type: 'funnel',
left: '10%',
width: '80%',
maxSize: '80%',
label: {
normal: {
position: 'inside',
formatter: '{c}%',
textStyle: {
color: '#fff'
}
},
emphasis: {
position:'inside',
formatter: '{b}实际: {c}%'
}
},
itemStyle: {
normal: {
opacity: 0.5,
borderColor: '#fff',
borderWidth: 2
}
},
data: [
{value: 30, name: '访问'},
{value: 10, name: '咨询'},
{value: 5, name: '订单'},
{value: 50, name: '点击'},
{value: 80, name: '展现'}
]
}
]
};
const instance = useRef(null);
function clickBtn() {
const base64 = instance.current.getEchartsInstance().getDataURL();
const img = new Image();
img.src = base64;
const newWin = window.open('', '_blank');
newWin.document.write(img.outerHTML);
}
return (
<>
<ReactECharts
ref={instance}
option={option}
style={{ height: 400 }}
/>
<div>
<button onClick={clickBtn}>click here to get the DataURL of chart.</button>
</div>
</>
);
};
export default Page;
```

178
docs/examples/dynamic.md Normal file
View File

@ -0,0 +1,178 @@
---
title: Dynamic
order: 6
---
## Dynamic
```tsx
import React, { useState, useEffect } from 'react';
import ReactECharts from 'echarts-for-react';
import cloneDeep from 'lodash.clonedeep';
const Page: React.FC = () => {
const DEFAULT_OPTION = {
title: {
text:'Hello Echarts-for-react.',
},
tooltip: {
trigger: 'axis'
},
legend: {
data:['最新成交价', '预购队列']
},
toolbox: {
show: true,
feature: {
dataView: {readOnly: false},
restore: {},
saveAsImage: {}
}
},
grid: {
top: 60,
left: 30,
right: 60,
bottom:30
},
dataZoom: {
show: false,
start: 0,
end: 100
},
visualMap: {
show: false,
min: 0,
max: 1000,
color: ['#BE002F', '#F20C00', '#F00056', '#FF2D51', '#FF2121', '#FF4C00', '#FF7500',
'#FF8936', '#FFA400', '#F0C239', '#FFF143', '#FAFF72', '#C9DD22', '#AFDD22',
'#9ED900', '#00E500', '#0EB83A', '#0AA344', '#0C8918', '#057748', '#177CB0']
},
xAxis: [
{
type: 'category',
boundaryGap: true,
data: (function (){
let now = new Date();
let res = [];
let len = 50;
while (len--) {
res.unshift(now.toLocaleTimeString().replace(/^\D*/,''));
now = new Date(now - 2000);
}
return res;
})()
},
{
type: 'category',
boundaryGap: true,
data: (function (){
let res = [];
let len = 50;
while (len--) {
res.push(50 - len + 1);
}
return res;
})()
}
],
yAxis: [
{
type: 'value',
scale: true,
name: '价格',
max: 20,
min: 0,
boundaryGap: [0.2, 0.2]
},
{
type: 'value',
scale: true,
name: '预购量',
max: 1200,
min: 0,
boundaryGap: [0.2, 0.2]
}
],
series: [
{
name:'预购队列',
type:'bar',
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
normal: {
barBorderRadius: 4,
}
},
animationEasing: 'elasticOut',
animationDelay: function (idx) {
return idx * 10;
},
animationDelayUpdate: function (idx) {
return idx * 10;
},
data:(function (){
let res = [];
let len = 50;
while (len--) {
res.push(Math.round(Math.random() * 1000));
}
return res;
})()
},
{
name:'最新成交价',
type:'line',
data:(function (){
let res = [];
let len = 0;
while (len < 50) {
res.push((Math.random()*10 + 5).toFixed(1) - 0);
len++;
}
return res;
})()
}
]
};
let count;
const [option, setOption] = useState(DEFAULT_OPTION);
function fetchNewData() {
const axisData = (new Date()).toLocaleTimeString().replace(/^\D*/,'');
const newOption = cloneDeep(option); // immutable
newOption.title.text = 'Hello Echarts-for-react.' + new Date().getSeconds();
const data0 = newOption.series[0].data;
const data1 = newOption.series[1].data;
data0.shift();
data0.push(Math.round(Math.random() * 1000));
data1.shift();
data1.push((Math.random() * 10 + 5).toFixed(1) - 0);
newOption.xAxis[0].data.shift();
newOption.xAxis[0].data.push(axisData);
newOption.xAxis[1].data.shift();
newOption.xAxis[1].data.push(count++);
setOption(newOption);
}
useEffect(() => {
const timer = setInterval(() => {
fetchNewData();
}, 1000);
return () => clearInterval(timer);
});
return <ReactECharts
option={option}
style={{ height: 400 }}
/>;
};
export default Page;
```

85
docs/examples/event.md Normal file
View File

@ -0,0 +1,85 @@
---
title: Event
order: 4
---
## Event
```tsx
import React, { useState } from 'react';
import ReactECharts from 'echarts-for-react';
const Page: React.FC = () => {
const option = {
title : {
text: '某站点用户访问来源',
subtext: '纯属虚构',
x:'center'
},
tooltip : {
trigger: 'item',
formatter: "{a} <br/>{b} : {c} ({d}%)"
},
legend: {
orient: 'vertical',
left: 'left',
data: ['直接访问','邮件营销','联盟广告','视频广告','搜索引擎']
},
series : [
{
name: '访问来源',
type: 'pie',
radius : '55%',
center: ['50%', '60%'],
data:[
{value:335, name:'直接访问'},
{value:310, name:'邮件营销'},
{value:234, name:'联盟广告'},
{value:135, name:'视频广告'},
{value:1548, name:'搜索引擎'}
],
itemStyle: {
emphasis: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
const [count, setCount] = useState(0);
function onChartReady(echarts) {
console.log('echarts is ready', echarts);
}
function onChartClick(param, echarts) {
console.log(param, echarts);
setCount(count + 1);
};
function onChartLegendselectchanged(param, echarts) {
console.log(param, echarts);
};
return (
<>
<ReactECharts
option={option}
style={{ height: 400 }}
onChartReady={onChartReady}
onEvents={{
'click': onChartClick,
'legendselectchanged': onChartLegendselectchanged
}}
/>
<div>Click Count: {count}</div>
<div>Open console, see the log detail.</div>
</>
);
};
export default Page;
```

36
docs/examples/gl.md Normal file
View File

@ -0,0 +1,36 @@
---
title: Web GL
order: 7
---
## Web GL
```tsx
import React from 'react';
import ReactECharts from 'echarts-for-react';
import 'echarts-gl';
const Page: React.FC = () => {
const option = {
grid3D: {},
xAxis3D: {},
yAxis3D: {},
zAxis3D: {},
series: [{
type: 'scatter3D',
symbolSize: 50,
data: [[-1, -1, -1], [0, 0, 0], [1, 1, 1]],
itemStyle: {
opacity: 1
}
}]
};
return <ReactECharts
option={option}
style={{ height: 400 }}
/>;
};
export default Page;
```

52
docs/examples/graph.md Normal file

File diff suppressed because one or more lines are too long

79
docs/examples/loading.md Normal file
View File

@ -0,0 +1,79 @@
---
title: Loading
order: 5
---
## Loading
```tsx
import React, { useState, useEffect } from 'react';
import ReactECharts from 'echarts-for-react';
const Page: React.FC = () => {
const option = {
title: {
text: '基础雷达图'
},
tooltip: {},
legend: {
data: ['预算分配Allocated Budget', '实际开销Actual Spending']
},
radar: {
// shape: 'circle',
indicator: [
{ name: '销售sales', max: 6500},
{ name: '管理Administration', max: 16000},
{ name: '信息技术Information Techology', max: 30000},
{ name: '客服Customer Support', max: 38000},
{ name: '研发Development', max: 52000},
{ name: '市场Marketing', max: 25000}
]
},
series: [{
name: '预算 vs 开销Budget vs spending',
type: 'radar',
// areaStyle: {normal: {}},
data : [
{
value : [4300, 10000, 28000, 35000, 50000, 19000],
name : '预算分配Allocated Budget'
},
{
value : [5000, 14000, 28000, 31000, 42000, 21000],
name : '实际开销Actual Spending'
}
]
}]
};
let timer;
useEffect(() => {
return () => clearTimeout(timer);
});
const loadingOption = {
text: '加载中...',
color: '#4413c2',
textColor: '#270240',
maskColor: 'rgba(194, 88, 86, 0.3)',
zlevel: 0
};
function onChartReady(echarts) {
timer = setTimeout(function() {
echarts.hideLoading();
}, 3000);
}
return <ReactECharts
option={option}
style={{ height: 400 }}
onChartReady={onChartReady}
loadingOption={loadingOption}
showLoading={true}
/>;
};
export default Page;
```

78
docs/examples/simple.md Normal file
View File

@ -0,0 +1,78 @@
---
title: Simple
order: 1
---
## 简单堆积面积图
```tsx
import React from 'react';
import ReactECharts from 'echarts-for-react';
const Page: React.FC = () => {
const option = {
title: {
text: '堆叠区域图'
},
tooltip : {
trigger: 'axis'
},
legend: {
data:['邮件营销','联盟广告','视频广告']
},
toolbox: {
feature: {
saveAsImage: {}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis : [
{
type : 'category',
boundaryGap : false,
data : ['周一','周二','周三','周四','周五','周六','周日']
}
],
yAxis : [
{
type : 'value'
}
],
series : [
{
name:'邮件营销',
type:'line',
stack: '总量',
areaStyle: {normal: {}},
data:[120, 132, 101, 134, 90, 230, 210]
},
{
name:'联盟广告',
type:'line',
stack: '总量',
areaStyle: {normal: {}},
data:[220, 182, 191, 234, 290, 330, 310]
},
{
name:'视频广告',
type:'line',
stack: '总量',
areaStyle: {normal: {}},
data:[150, 232, 201, 154, 190, 330, 410]
}
]
};
return <ReactECharts
option={option}
style={{ height: 400 }}
/>;
};
export default Page;
```

40
docs/examples/svg.md Normal file
View File

@ -0,0 +1,40 @@
---
title: SVG
order: 8
---
## SVG
```tsx
import React from 'react';
import ReactECharts from 'echarts-for-react';
const Page: React.FC = () => {
const option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data:['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}]
};
return <ReactECharts
option={option}
style={{ height: 400 }}
opts={{ renderer: 'svg' }}
/>;
};
export default Page;
```

123
docs/examples/theme.md Normal file
View File

@ -0,0 +1,123 @@
---
title: Theme
order: 3
---
## Theme
```tsx
import React, { useState } from 'react';
import * as echarts from 'echarts';
import ReactECharts from 'echarts-for-react';
echarts.registerTheme('my_theme', {
backgroundColor: '#f4cccc'
});
echarts.registerTheme('another_theme', {
backgroundColor: '#eee'
});
const Page: React.FC = () => {
const option = {
title: {
text: '阶梯瀑布图',
subtext: 'From ExcelHome',
sublink: 'http://e.weibo.com/1341556070/Aj1J2x5a5'
},
tooltip : {
trigger: 'axis',
axisPointer : { // 坐标轴指示器,坐标轴触发有效
type : 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
legend: {
data:['支出','收入']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type : 'category',
splitLine: {show:false},
data : ["11月1日", "11月2日", "11月3日", "11月4日", "11月5日", "11月6日", "11月7日", "11月8日", "11月9日", "11月10日", "11月11日"]
},
yAxis: {
type : 'value'
},
series: [
{
name: '辅助',
type: 'bar',
stack: '总量',
itemStyle: {
normal: {
barBorderColor: 'rgba(0,0,0,0)',
color: 'rgba(0,0,0,0)'
},
emphasis: {
barBorderColor: 'rgba(0,0,0,0)',
color: 'rgba(0,0,0,0)'
}
},
data: [0, 900, 1245, 1530, 1376, 1376, 1511, 1689, 1856, 1495, 1292]
},
{
name: '收入',
type: 'bar',
stack: '总量',
label: {
normal: {
show: true,
position: 'top'
}
},
data: [900, 345, 393, '-', '-', 135, 178, 286, '-', '-', '-']
},
{
name: '支出',
type: 'bar',
stack: '总量',
label: {
normal: {
show: true,
position: 'bottom'
}
},
data: ['-', '-', '-', 108, 154, '-', '-', '-', 119, 361, 203]
}
]
};
const [theme, setTheme] = useState();
const [className, setClassName] = useState('class_1');
function toggleTheme() {
setTheme(theme === 'my_theme' ? 'another_theme' : 'my_theme');
}
function toggleClassName() {
setClassName(className === 'class_1' ? 'class_2' : 'class_1');
}
return (
<>
<ReactECharts
option={option}
className={className}
theme={theme}
style={{ height: 400 }}
/>
<br />
<div>
<button onClick={toggleTheme}>Click to Toggle theme.</button>
<button onClick={toggleClassName}>Click to Toggle className.</button>
</div>
</>
);
};
export default Page;
```

86
docs/index.md Normal file
View File

@ -0,0 +1,86 @@
---
title: ECharts for React - 全网最好用的 ECharts 的 React 组件封装
order: 10
hero:
title: ECharts for React
desc: 全网最好用的 ECharts 的 React 组件封装
actions:
- text: 查看在线 DEMO
link: /examples/dynamic
footer: Open-source MIT Licensed | Copyright © 2021-present
---
## 安装
```bash
$ npm install echarts-for-react
```
## 使用
```tsx | pure
import React from 'react';
import ReactECharts from 'echarts-for-react';
const Page: React.FC = () => {
const options = {
grid: { top: 8, right: 8, bottom: 24, left: 36 },
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
},
series: [
{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
smooth: true,
},
],
};
return <ReactECharts option={options} />;
};
export default Page;
```
最终结果:
```tsx
import React from 'react';
import ReactECharts from 'echarts-for-react';
const Page: React.FC = () => {
const options = {
grid: { top: 8, right: 8, bottom: 24, left: 36 },
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
},
series: [
{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
smooth: true,
},
],
};
return <ReactECharts option={options} />;
};
export default Page;
```
## 反馈
请访问 [GitHub](https://github.com/hustcc/echarts-for-react)。

116
package.json Normal file
View File

@ -0,0 +1,116 @@
{
"name": "echarts-for-react",
"version": "3.0.0-beta.1",
"description": " Apache Echarts(v5) components for React.",
"main": "lib/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts",
"files": [
"lib",
"esm",
"src"
],
"scripts": {
"docs:dev": "dumi dev",
"docs:build": "dumi build",
"docs:deploy": "rimraf dist && npm run docs:build && gh-pages -d dist",
"start": "webpack-dev-server --watch",
"lint": "eslint src __tests__ && prettier src __tests__ --check",
"fix": "eslint src __tests__ --fix && prettier src __tests__ --write",
"clean": "rimraf lib esm dist",
"test": "jest",
"lib": "run-p lib:*",
"lib:cjs": "tsc -p tsconfig.json --target ES5 --module commonjs --outDir lib",
"lib:esm": "tsc -p tsconfig.json --target ES5 --module ESNext --outDir esm",
"build": "npm run clean && npm run test && npm run lib"
},
"repository": {
"type": "git",
"url": "https://github.com/hustcc/echarts-for-react.git"
},
"keywords": [
"react",
"component",
"echarts-react",
"echarts",
"react-echarts",
"chart",
"charts",
"graph",
"react-component"
],
"author": "hustcc (http://github.com/hustcc)",
"license": "MIT",
"bugs": {
"url": "https://github.com/hustcc/echarts-for-react/issues"
},
"homepage": "https://github.com/hustcc/echarts-for-react",
"devDependencies": {
"@commitlint/cli": "^11.0.0",
"@commitlint/config-angular": "^11.0.0",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.14",
"@types/react": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.11.0",
"cross-env": "^5.1.3",
"dumi": "^1.1.4",
"dumi-theme-default": "^1.0.4",
"echarts": "^5.0.2",
"echarts-gl": "^2.0.1",
"eslint": "^7.19.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.3.1",
"gh-pages": "^3.1.0",
"husky": "^4.3.8",
"jest": "^26.6.3",
"jest-canvas-mock": "^2.3.0",
"jest-electron": "^0.1.11",
"lint-md-cli": "^0.1.2",
"lint-staged": "^10.5.3",
"lodash.clonedeep": "^4.5.0",
"miz": "^1.0.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.2.1",
"rimraf": "^2.6.2",
"ts-jest": "^26.5.0",
"ts-loader": "^8.0.15",
"typescript": "^4.1.3"
},
"dependencies": {
"size-sensor": "^1.0.0",
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"react": ">=16.8.0",
"echarts": "^5.0.0"
},
"jest": {
"runner": "jest-electron/runner",
"testEnvironment": "jest-electron/environment",
"preset": "ts-jest",
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*.ts",
"!**/node_modules/**",
"!**/vendor/**"
],
"testRegex": "/__tests__/.*-spec\\.tsx?$"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.ts": [
"eslint --fix",
"prettier --write"
],
"*.md": [
"lint-md --fix"
]
}
}

185
src/core.tsx Normal file
View File

@ -0,0 +1,185 @@
import React, { PureComponent } from 'react';
import { bind, clear } from 'size-sensor';
import { pick } from './helper/pick';
import { isFunction } from './helper/is-function';
import { isString } from './helper/is-string';
import { isEqual } from './helper/is-equal';
import { EChartsReactProps, EChartsInstance } from './types';
/**
* core component for echarts binding
*/
export default class EChartsReactCore extends PureComponent<EChartsReactProps> {
/**
* echarts render container
*/
public ele: HTMLElement;
/**
* echarts library entry
*/
protected echarts: any;
constructor(props: EChartsReactProps) {
super(props);
this.echarts = props.echarts;
this.ele = null;
}
componentDidMount() {
this.renderNewEcharts();
}
// update
componentDidUpdate(prevProps: EChartsReactProps) {
/**
* if shouldSetOption return false, then return, not update echarts options
* default is true
*/
const { shouldSetOption } = this.props;
if (isFunction(shouldSetOption) && !shouldSetOption(prevProps, this.props)) {
return;
}
// 以下属性修改的时候,需要 dispose 之后再新建
// 1. 切换 theme 的时候
// 2. 修改 opts 的时候
// 3. 修改 onEvents 的时候,这样可以取消所有之前绑定的事件 issue #151
if (
!isEqual(prevProps.theme, this.props.theme) ||
!isEqual(prevProps.opts, this.props.opts) ||
!isEqual(prevProps.onEvents, this.props.onEvents)
) {
this.dispose();
this.renderNewEcharts(); // 重建
return;
}
// when thoes props isEqual, do not update echarts
const pickKeys = ['option', 'notMerge', 'lazyUpdate', 'showLoading', 'loadingOption'];
if (isEqual(pick(this.props, pickKeys), pick(prevProps, pickKeys))) {
return;
}
const echartsInstance = this.updateEChartsOption();
/**
* when style or class name updated, change size.
*/
if (!isEqual(prevProps.style, this.props.style) || !isEqual(prevProps.className, this.props.className)) {
try {
echartsInstance.resize();
} catch (e) {
console.warn(e);
}
}
}
componentWillUnmount() {
this.dispose();
}
/**
* return the echart object
* 1. if exist, return the existed instance
* 2. or new one instance
*/
public getEchartsInstance() {
return this.echarts.getInstanceByDom(this.ele) || this.echarts.init(this.ele, this.props.theme, this.props.opts);
}
/**
* dispose echarts and clear size-sensor
*/
private dispose() {
if (this.ele) {
try {
clear(this.ele);
} catch (e) {
console.warn(e);
}
// dispose echarts instance
this.echarts.dispose(this.ele);
}
}
/**
* render a new echarts instance
*/
private renderNewEcharts() {
const { onEvents, onChartReady } = this.props;
// 1. new echarts instance
const echartsInstance = this.updateEChartsOption();
// 2. bind events
this.bindEvents(echartsInstance, onEvents || {});
// 3. on chart ready
if (isFunction(onChartReady)) onChartReady(echartsInstance);
// 4. on resize
if (this.ele) {
bind(this.ele, () => {
try {
echartsInstance.resize();
} catch (e) {
console.warn(e);
}
});
}
}
// bind the events
private bindEvents(instance, events: EChartsReactProps['onEvents']) {
function _bindEvent(eventName: string, func: Function) {
// ignore the event config which not satisfy
if (isString(eventName) && isFunction(func)) {
// binding event
instance.on(eventName, (param) => {
func(param, instance);
});
}
}
// loop and bind
for (const eventName in events) {
if (Object.prototype.hasOwnProperty.call(events, eventName)) {
_bindEvent(eventName, events[eventName]);
}
}
}
/**
* render the echarts
*/
private updateEChartsOption(): EChartsInstance {
const { option, notMerge = false, lazyUpdate = false, showLoading, loadingOption = null } = this.props;
// 1. get or initial the echarts object
const echartInstance = this.getEchartsInstance();
// 2. set the echarts option
echartInstance.setOption(option, notMerge, lazyUpdate);
// 3. set loading mask
if (showLoading) echartInstance.showLoading(loadingOption);
else echartInstance.hideLoading();
return echartInstance;
}
render(): JSX.Element {
const { style, className = '' } = this.props;
// default height = 300
const newStyle = { height: 300, ...style };
return (
<div
ref={(e: HTMLElement) => {
this.ele = e;
}}
style={newStyle}
className={`echarts-for-react ${className}`}
/>
);
}
}

3
src/helper/is-equal.ts Normal file
View File

@ -0,0 +1,3 @@
import isEqual from 'fast-deep-equal';
export { isEqual };

View File

@ -0,0 +1,3 @@
export function isFunction(v: any): boolean {
return typeof v === 'function';
}

3
src/helper/is-string.ts Normal file
View File

@ -0,0 +1,3 @@
export function isString(v: any): boolean {
return typeof v === 'string';
}

12
src/helper/pick.ts Normal file
View File

@ -0,0 +1,12 @@
/**
* object
* @param obj
* @param keys
*/
export function pick(obj: Record<string, unknown>, keys: string[]): Record<string, unknown> {
const r = {};
keys.forEach((key) => {
r[key] = obj[key];
});
return r;
}

15
src/index.ts Normal file
View File

@ -0,0 +1,15 @@
import * as echarts from 'echarts';
import { EChartsReactProps, EChartsOption, EChartsInstance } from './types';
import EChartsReactCore from './core';
export { EChartsReactProps, EChartsOption, EChartsInstance };
// export the Component the echarts Object.
export default class EChartsReact extends EChartsReactCore {
constructor(props: EChartsReactProps) {
super(props);
// 初始化为 echarts 整个包
this.echarts = echarts;
}
}

69
src/types.ts Normal file
View File

@ -0,0 +1,69 @@
import { CSSProperties } from 'react';
export type EChartsOption = any;
export type EChartsInstance = any;
export type Opts = {
readonly devicePixelRatio?: number;
readonly renderer?: 'canvas' | 'svg';
readonly width?: number | null | undefined | 'auto';
readonly height?: number | null | undefined | 'auto';
};
export type EChartsReactProps = {
/**
* echarts library entry, use it for import necessary.
*/
readonly echarts?: any;
/**
* `className` for container
*/
readonly className?: string;
/**
* `style` for container
*/
readonly style?: CSSProperties;
/**
* echarts option
*/
readonly option: EChartsOption;
/**
* echarts theme config, can be:
* 1. theme name string
* 2. theme object
*/
readonly theme?: string | Record<string, any>;
/**
* notMerge config for echarts, default is `false`
*/
readonly notMerge?: boolean;
/**
* lazyUpdate config for echarts, default is `false`
*/
readonly lazyUpdate?: boolean;
/**
* showLoading config for echarts, default is `false`
*/
readonly showLoading?: boolean;
/**
* loadingOption config for echarts, default is `null`
*/
readonly loadingOption?: any;
/**
* echarts opts config, default is `{}`
*/
readonly opts?: Opts;
/**
* when after chart reander, do the callback widht echarts instance
*/
readonly onChartReady?: (instance: EChartsInstance) => void;
/**
* bind events, default is `{}`
*/
readonly onEvents?: Record<string, Function>;
/**
* should update echarts options
*/
readonly shouldSetOption?: (prevProps: EChartsReactProps, props: EChartsReactProps) => boolean;
};

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"jsx": "react",
"module": "commonjs",
"sourceMap": true,
"inlineSources": true,
"target": "es5",
"outDir": "lib",
"declaration": true,
"importHelpers": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"lib": ["esnext", "dom"],
"types": ["jest", "react", "node"]
},
"include": ["src"]
}