├── .gitignore
├── README.md
├── demo
├── javascript
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src
│ │ ├── App.css
│ │ ├── App.js
│ │ ├── App.test.js
│ │ ├── adapters
│ │ │ ├── isaaclin.js
│ │ │ └── long2short.js
│ │ ├── data
│ │ │ └── isaaclin
│ │ │ │ ├── current.json
│ │ │ │ ├── history.json
│ │ │ │ ├── overall.json
│ │ │ │ ├── patch.json
│ │ │ │ └── update.sh
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── mock
│ │ │ └── informationMapMockData.js
│ │ ├── serviceWorker.js
│ │ └── setupTests.js
│ └── yarn.lock
└── typescript
│ ├── README.md
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ ├── src
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── adapters
│ │ ├── isaaclin.js
│ │ └── long2short.js
│ ├── data
│ │ └── isaaclin
│ │ │ ├── current.json
│ │ │ ├── history.json
│ │ │ ├── overall.json
│ │ │ ├── patch.json
│ │ │ └── update.sh
│ ├── index.css
│ ├── index.tsx
│ ├── mock
│ │ └── informationMapMockData.ts
│ ├── react-app-env.d.ts
│ ├── serviceWorker.ts
│ └── setupTests.ts
│ ├── tsconfig.json
│ └── yarn.lock
├── index.d.ts
├── lib
├── components.js
└── types
│ └── src
│ ├── adapters
│ ├── isaaclin.d.ts
│ └── long2short.d.ts
│ ├── components
│ ├── informationMap
│ │ ├── baiduMap.d.ts
│ │ ├── informationMap.d.ts
│ │ └── marker.d.ts
│ └── virusMap
│ │ ├── echartsMap.d.ts
│ │ ├── hierarchicalVirusMap.d.ts
│ │ ├── reactEcharts.d.ts
│ │ ├── virusChart.d.ts
│ │ └── virusMap.d.ts
│ └── data
│ └── map
│ ├── district.d.ts
│ └── provinces.d.ts
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.css
├── App.test.tsx
├── App.tsx
├── adapters
│ ├── isaaclin.ts
│ └── long2short.ts
├── components
│ ├── informationMap
│ │ ├── README.md
│ │ ├── baiduMap.tsx
│ │ ├── informationMap.tsx
│ │ ├── marker.ts
│ │ └── style.css
│ └── virusMap
│ │ ├── README.md
│ │ ├── echartsMap.tsx
│ │ ├── hierarchicalVirusMap.tsx
│ │ ├── reactEcharts.tsx
│ │ ├── style.css
│ │ ├── virusChart.tsx
│ │ └── virusMap.tsx
├── data
│ ├── isaaclin
│ │ ├── current.json
│ │ ├── history.json
│ │ ├── overall.json
│ │ ├── patch.json
│ │ └── update.sh
│ ├── map
│ │ ├── district.ts
│ │ ├── provinces.ts
│ │ └── readme.md
│ └── ncov_nosensor
│ │ ├── history.json
│ │ └── update.sh
├── index.css
├── index.tsx
├── mock
│ └── informationMapMockData.js
├── pages
│ ├── hierarchicalVirusMapDemo.tsx
│ └── informationMapDemo.tsx
├── react-app-env.d.ts
├── serviceWorker.ts
└── setupTests.ts
├── tsconfig.json
├── webpack.config.lib.js
├── webpack.lib.entry.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /demo/javascript/node_modules
6 | /demo/typescript/node_modules
7 |
8 | /.pnp
9 | .pnp.js
10 |
11 | # testing
12 | /coverage
13 |
14 | # production
15 | /build
16 |
17 | # misc
18 | .DS_Store
19 | .env.local
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 |
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | /dist
29 | /.cache
30 | /tmp
31 | /.vscode
32 | /.idea
33 | /.DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 武汉新型冠状病毒防疫信息收集平台-地图可视化项目
2 |
3 | 本项目负责平台的信息展示,可视化地理信息。
4 |
5 | ## 项目介绍
6 |
7 | 提供基于 ECharts 可视化库的前端组件。
8 |
9 | ### 1、提供一个完整独立的疫情地图组件
10 |
11 | - **目的&设计**:创建一个独立的疫情地图可视化,有两个主要目标
12 | 1. 地理精度:有市级地理粒度,最开始是一个全国地图的 heatmap,点击一个省重绘成省 map。([重绘参考](https://gallery.echartsjs.com/editor.html?c=xm3iS_cb0g))
13 | - optional: 可能会做成县级精度,具体见[讨论](https://github.com/wuhan2020/map-viz/issues/52)
14 | 2. 时间信息:有时间轴,点击一个地区可以画出[stacked area chart](https://echarts.apache.org/examples/en/editor.html?c=area-stack)之类的疫情发展图 (确诊/疑似/死亡为不同层),也可以根据选择的时间点重绘地图。([时间轴参考案例](https://echarts.apache.org/examples/en/editor.html?c=mix-timeline-finance))
15 |
16 | - **进度**
17 |
18 | - [x] 基础疫情地图,并用统计图(线图+ stacked area chart)显示疫情发展数据
19 | - [x] 省市层级交互
20 | - [x] 时间轴交互
21 | - [ ] 接入手动收集的省级数据
22 | - [ ] 疫情地图时间轴与统计图联动
23 |
24 | ### 2、提供一个通用地图组件
25 |
26 | - **目的**:用于可视化各种不同地理信息(例如医院位置,酒店位置,etc.)
27 | - **使用**:作为组件被前端调用,数据来自前端。
28 |
29 | ## 使用
30 |
31 | ```sh
32 | npm install wuhan2020-mapviz
33 | ```
34 |
35 | - [疫情地图组件说明文档](./src/components/virusMap/README.md)
36 | - [通用地图组件说明文档](./src/components/informationMap/README.md)
37 |
38 | - [javascript使用样例参考](./demo/javascript/README.md)
39 | - [typescript使用样例参考](./demo/typescript/README.md)
40 |
41 | ## 本地开发
42 |
43 | ### 配置
44 |
45 | 1. [安装 Node.js](https://nodejs.org/en/download/package-manager/)
46 | 2.
47 |
48 | ```sh
49 | # clone the repo
50 | git clone git@github.com:wuhan2020/map-viz.git
51 |
52 | cd map-viz
53 | # checkout react branch
54 | git checkout react
55 | # setup the npm env
56 | npm install
57 | # start the project
58 | npm start
59 | ```
60 |
61 | ### 任务拆分&参与指南
62 |
63 | [合作指南参考](https://github.com/wuhan2020/wuhan2020/blob/master/CONTRIBUTING.md)(注意将 demo script 改成我们的 repo)
64 |
65 | 1. 请在[project 面板](https://github.com/wuhan2020/map-viz/projects/1)自行认领&self-assign issues(如果不能更改 assignee,请回复 issue 表示认领,我们会后面添加 assign)
66 | 2. 对数据和设计如果有讨论请参见如下 issue:
67 |
68 | - [地图设计讨论](https://github.com/wuhan2020/map-viz/issues/2)
69 | - [数据格式设计讨论](https://github.com/wuhan2020/map-viz/issues/3)
70 | - [收集已有数据及可视化](https://github.com/wuhan2020/map-viz/issues/7)
71 |
72 | 3. 如有其它建议请开 issue
73 | 4. 参与更多讨论请加入[slack 讨论组](https://join.slack.com/t/wuhan2020/shared_invite/enQtOTQxMTU4MzgyNTYwLWIxMTMyNWI4NWE2YTk3NGRjZGJhMjUzNmJhMjg1MDQ3OTEzNDE5NGY4MWFhMjRlYWU4MmE3ZGQyOGU4N2YwMzY),我们在 channel #proj-map-visualization
74 |
75 | ## 技术栈
76 |
77 | - 可视化库: [ECharts v4][4]
78 | - 逻辑语言: [TypeScript v3][1]
79 | - 开发框架: [React.js][2]
80 | - 组件库: [Ant Design][3]
81 |
82 | ## 教程及有用链接
83 |
84 | [5 分钟上手 ECharts](https://www.echartsjs.com/zh/tutorial.html#5%20%E5%88%86%E9%92%9F%E4%B8%8A%E6%89%8B%20ECharts)
85 |
86 | [echarts example](https://gallery.echartsjs.com/explore.html#sort=rank~timeframe=all~author=all)
87 |
88 | [百度地图](http://lbsyun.baidu.com/jsdemo.htm#canvaslayer)
89 |
90 | [坐标拾取](http://api.map.baidu.com/lbsapi/getpoint/index.html)
91 |
92 | ### 例子
93 |
94 | [百度迁徙](https://qianxi.baidu.com/?from=shoubai#city=420100)
95 |
96 | [百度实时疫情数据](https://voice.baidu.com/act/newpneumonia/newpneumonia)
97 |
98 | [丁香园实时疫情数据](https://3g.dxy.cn/newh5/view/pneumonia)
99 |
100 | [qq 实时疫情数据](https://news.qq.com/zt2020/page/feiyan.htm)
101 |
102 | ### 临时接口
103 |
104 | [省市每日历史数据](http://ncov.nosensor.com:8080/api/)
105 |
106 | [百度实时疫情](https://service-nxxl1y2s-1252957949.gz.apigw.tencentcs.com/release/newpneumonia)
107 |
108 | [百度迁徙](https://huiyan.baidu.com/migration/cityrank.jsonp?dt=city&id=420100&type=move_out&date=20200128&callback=jsonp_1580257678289_5758459)
109 |
110 | [丁香园实时疫情](https://service-0gg71fu4-1252957949.gz.apigw.tencentcs.com/release/dingxiangyuan)
111 |
112 | [丁香园每分钟历史数据](http://lab.isaaclin.cn/nCoV/api/area?latest=0)
113 |
114 | [丁香园其他](http://lab.isaaclin.cn/nCoV/)
115 |
116 | [qq 实时+历史疫情](https://service-n9zsbooc-1252957949.gz.apigw.tencentcs.com/release/qq)
117 |
118 | [百度地图地址转经纬度](https://service-qf7o2c4u-1252957949.gz.apigw.tencentcs.com/release/bmap?address=华中科技大学)
119 |
120 | [新闻收集接口](http://ncov.news.dragon-yuan.me/api/news?search=&page=)
121 |
122 | [1]: https://typescriptlang.org
123 | [2]: https://react.docschina.org
124 | [3]: https://ant.design
125 | [4]: https://www.echartsjs.com/
--------------------------------------------------------------------------------
/demo/javascript/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `yarn build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/demo/javascript/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "map-viz-test-js",
3 | "version": "0.1.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "asap": {
8 | "version": "2.0.6",
9 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
10 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
11 | },
12 | "async": {
13 | "version": "2.6.3",
14 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
15 | "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
16 | "requires": {
17 | "lodash": "^4.17.14"
18 | }
19 | },
20 | "colors": {
21 | "version": "1.0.3",
22 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
23 | "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs="
24 | },
25 | "core-js": {
26 | "version": "1.2.7",
27 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
28 | "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
29 | },
30 | "corser": {
31 | "version": "2.0.1",
32 | "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
33 | "integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c="
34 | },
35 | "create-react-class": {
36 | "version": "15.6.3",
37 | "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz",
38 | "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==",
39 | "requires": {
40 | "fbjs": "^0.8.9",
41 | "loose-envify": "^1.3.1",
42 | "object-assign": "^4.1.1"
43 | }
44 | },
45 | "debug": {
46 | "version": "3.2.6",
47 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
48 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
49 | "requires": {
50 | "ms": "^2.1.1"
51 | }
52 | },
53 | "echarts": {
54 | "version": "4.6.0",
55 | "resolved": "https://registry.npmjs.org/echarts/-/echarts-4.6.0.tgz",
56 | "integrity": "sha512-xKkcr6v9UVOSF+PMuj7Ngt3bnzLwN1sSXWCvpvX+jYb3mePYsZnABq7wGkPac/m0nV653uGHXoHK8DCKCprdNg==",
57 | "requires": {
58 | "zrender": "4.2.0"
59 | }
60 | },
61 | "ecstatic": {
62 | "version": "3.3.2",
63 | "resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz",
64 | "integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==",
65 | "requires": {
66 | "he": "^1.1.1",
67 | "mime": "^1.6.0",
68 | "minimist": "^1.1.0",
69 | "url-join": "^2.0.5"
70 | }
71 | },
72 | "encoding": {
73 | "version": "0.1.12",
74 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
75 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
76 | "requires": {
77 | "iconv-lite": "~0.4.13"
78 | }
79 | },
80 | "eventemitter3": {
81 | "version": "4.0.0",
82 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz",
83 | "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg=="
84 | },
85 | "fbjs": {
86 | "version": "0.8.17",
87 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
88 | "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
89 | "requires": {
90 | "core-js": "^1.0.0",
91 | "isomorphic-fetch": "^2.1.1",
92 | "loose-envify": "^1.0.0",
93 | "object-assign": "^4.1.0",
94 | "promise": "^7.1.1",
95 | "setimmediate": "^1.0.5",
96 | "ua-parser-js": "^0.7.18"
97 | }
98 | },
99 | "follow-redirects": {
100 | "version": "1.10.0",
101 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.10.0.tgz",
102 | "integrity": "sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ==",
103 | "requires": {
104 | "debug": "^3.0.0"
105 | }
106 | },
107 | "he": {
108 | "version": "1.2.0",
109 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
110 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
111 | },
112 | "http-proxy": {
113 | "version": "1.18.0",
114 | "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz",
115 | "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==",
116 | "requires": {
117 | "eventemitter3": "^4.0.0",
118 | "follow-redirects": "^1.0.0",
119 | "requires-port": "^1.0.0"
120 | }
121 | },
122 | "http-server": {
123 | "version": "0.11.1",
124 | "resolved": "https://registry.npmjs.org/http-server/-/http-server-0.11.1.tgz",
125 | "integrity": "sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w==",
126 | "requires": {
127 | "colors": "1.0.3",
128 | "corser": "~2.0.0",
129 | "ecstatic": "^3.0.0",
130 | "http-proxy": "^1.8.1",
131 | "opener": "~1.4.0",
132 | "optimist": "0.6.x",
133 | "portfinder": "^1.0.13",
134 | "union": "~0.4.3"
135 | }
136 | },
137 | "iconv-lite": {
138 | "version": "0.4.24",
139 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
140 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
141 | "requires": {
142 | "safer-buffer": ">= 2.1.2 < 3"
143 | }
144 | },
145 | "is-stream": {
146 | "version": "1.1.0",
147 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
148 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
149 | },
150 | "isomorphic-fetch": {
151 | "version": "2.2.1",
152 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
153 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
154 | "requires": {
155 | "node-fetch": "^1.0.1",
156 | "whatwg-fetch": ">=0.10.0"
157 | }
158 | },
159 | "js-tokens": {
160 | "version": "4.0.0",
161 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
162 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
163 | },
164 | "lodash": {
165 | "version": "4.17.15",
166 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
167 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
168 | },
169 | "loose-envify": {
170 | "version": "1.4.0",
171 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
172 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
173 | "requires": {
174 | "js-tokens": "^3.0.0 || ^4.0.0"
175 | }
176 | },
177 | "mime": {
178 | "version": "1.6.0",
179 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
180 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
181 | },
182 | "minimist": {
183 | "version": "1.2.0",
184 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
185 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
186 | },
187 | "mkdirp": {
188 | "version": "0.5.1",
189 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
190 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
191 | "requires": {
192 | "minimist": "0.0.8"
193 | },
194 | "dependencies": {
195 | "minimist": {
196 | "version": "0.0.8",
197 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
198 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
199 | }
200 | }
201 | },
202 | "ms": {
203 | "version": "2.1.2",
204 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
205 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
206 | },
207 | "node-fetch": {
208 | "version": "1.7.3",
209 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
210 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
211 | "requires": {
212 | "encoding": "^0.1.11",
213 | "is-stream": "^1.0.1"
214 | }
215 | },
216 | "object-assign": {
217 | "version": "4.1.1",
218 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
219 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
220 | },
221 | "opener": {
222 | "version": "1.4.3",
223 | "resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz",
224 | "integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg="
225 | },
226 | "optimist": {
227 | "version": "0.6.1",
228 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
229 | "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
230 | "requires": {
231 | "minimist": "~0.0.1",
232 | "wordwrap": "~0.0.2"
233 | },
234 | "dependencies": {
235 | "minimist": {
236 | "version": "0.0.10",
237 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
238 | "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
239 | }
240 | }
241 | },
242 | "portfinder": {
243 | "version": "1.0.25",
244 | "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz",
245 | "integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==",
246 | "requires": {
247 | "async": "^2.6.2",
248 | "debug": "^3.1.1",
249 | "mkdirp": "^0.5.1"
250 | }
251 | },
252 | "promise": {
253 | "version": "7.3.1",
254 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
255 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
256 | "requires": {
257 | "asap": "~2.0.3"
258 | }
259 | },
260 | "prop-types": {
261 | "version": "15.7.2",
262 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
263 | "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
264 | "requires": {
265 | "loose-envify": "^1.4.0",
266 | "object-assign": "^4.1.1",
267 | "react-is": "^16.8.1"
268 | }
269 | },
270 | "qs": {
271 | "version": "2.3.3",
272 | "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
273 | "integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ="
274 | },
275 | "react-is": {
276 | "version": "16.12.0",
277 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
278 | "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
279 | },
280 | "react-slider-light": {
281 | "version": "4.2.0",
282 | "resolved": "https://registry.npmjs.org/react-slider-light/-/react-slider-light-4.2.0.tgz",
283 | "integrity": "sha512-T+R3hqhuaT0qXcbuHFiWRKmNI6UMdkzmkNEyBx6cAEXz03Ecg32kLMKlLLw1xNcc470982Pda/QNzTuHBrs7PA==",
284 | "requires": {
285 | "http-server": "^0.11.1",
286 | "react": "^15.4.0",
287 | "react-dom": "^15.4.0"
288 | },
289 | "dependencies": {
290 | "react": {
291 | "version": "15.6.2",
292 | "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz",
293 | "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=",
294 | "requires": {
295 | "create-react-class": "^15.6.0",
296 | "fbjs": "^0.8.9",
297 | "loose-envify": "^1.1.0",
298 | "object-assign": "^4.1.0",
299 | "prop-types": "^15.5.10"
300 | }
301 | },
302 | "react-dom": {
303 | "version": "15.6.2",
304 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz",
305 | "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=",
306 | "requires": {
307 | "fbjs": "^0.8.9",
308 | "loose-envify": "^1.1.0",
309 | "object-assign": "^4.1.0",
310 | "prop-types": "^15.5.10"
311 | }
312 | }
313 | }
314 | },
315 | "requires-port": {
316 | "version": "1.0.0",
317 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
318 | "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
319 | },
320 | "safer-buffer": {
321 | "version": "2.1.2",
322 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
323 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
324 | },
325 | "scheduler": {
326 | "version": "0.18.0",
327 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz",
328 | "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==",
329 | "requires": {
330 | "loose-envify": "^1.1.0",
331 | "object-assign": "^4.1.1"
332 | }
333 | },
334 | "setimmediate": {
335 | "version": "1.0.5",
336 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
337 | "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
338 | },
339 | "ua-parser-js": {
340 | "version": "0.7.21",
341 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
342 | "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ=="
343 | },
344 | "union": {
345 | "version": "0.4.6",
346 | "resolved": "https://registry.npmjs.org/union/-/union-0.4.6.tgz",
347 | "integrity": "sha1-GY+9rrolTniLDvy2MLwR8kopWeA=",
348 | "requires": {
349 | "qs": "~2.3.3"
350 | }
351 | },
352 | "url-join": {
353 | "version": "2.0.5",
354 | "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",
355 | "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg="
356 | },
357 | "whatwg-fetch": {
358 | "version": "3.0.0",
359 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
360 | "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
361 | },
362 | "wordwrap": {
363 | "version": "0.0.3",
364 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
365 | "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
366 | },
367 | "wuhan2020-mapviz": {
368 | "version": "1.0.10",
369 | "resolved": "https://registry.npmjs.org/wuhan2020-mapviz/-/wuhan2020-mapviz-1.0.10.tgz",
370 | "integrity": "sha512-y54lXHzKg0/CgbkThwYdZ6J+NeeUpDYFiOtL1V1ccPMEXzeqUqsY3hbZS1LgCLivrBboqhLE8rySFWW3Rxbnog==",
371 | "requires": {
372 | "echarts": "^4.6.0",
373 | "react": "^16.12.0",
374 | "react-dom": "^16.12.0",
375 | "react-slider-light": "^4.2.0"
376 | },
377 | "dependencies": {
378 | "react": {
379 | "version": "16.12.0",
380 | "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
381 | "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==",
382 | "requires": {
383 | "loose-envify": "^1.1.0",
384 | "object-assign": "^4.1.1",
385 | "prop-types": "^15.6.2"
386 | }
387 | },
388 | "react-dom": {
389 | "version": "16.12.0",
390 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz",
391 | "integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==",
392 | "requires": {
393 | "loose-envify": "^1.1.0",
394 | "object-assign": "^4.1.1",
395 | "prop-types": "^15.6.2",
396 | "scheduler": "^0.18.0"
397 | }
398 | }
399 | }
400 | },
401 | "zrender": {
402 | "version": "4.2.0",
403 | "resolved": "https://registry.npmjs.org/zrender/-/zrender-4.2.0.tgz",
404 | "integrity": "sha512-YJ9hxt5uFincYYU3KK31+Ce+B6PJmYYK0Q9fQ6jOUAoC/VHbe4kCKAPkxKeT7jGTxrK5wYu18R0TLGqj2zbEOA=="
405 | }
406 | }
407 | }
408 |
--------------------------------------------------------------------------------
/demo/javascript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "map-viz-test-js",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "react": "^16.12.0",
10 | "react-dom": "^16.12.0",
11 | "react-scripts": "3.3.1",
12 | "wuhan2020-mapviz": "^1.0.11"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": "react-app"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | },
35 | "devDependencies": {
36 | "@babel/polyfill": "^7.8.3"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/demo/javascript/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhan2020/map-viz/6bde1fecc8770805ea405bd16de21702ac62e31e/demo/javascript/public/favicon.ico
--------------------------------------------------------------------------------
/demo/javascript/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/demo/javascript/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhan2020/map-viz/6bde1fecc8770805ea405bd16de21702ac62e31e/demo/javascript/public/logo192.png
--------------------------------------------------------------------------------
/demo/javascript/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhan2020/map-viz/6bde1fecc8770805ea405bd16de21702ac62e31e/demo/javascript/public/logo512.png
--------------------------------------------------------------------------------
/demo/javascript/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/demo/javascript/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/demo/javascript/src/App.css:
--------------------------------------------------------------------------------
1 | .app {
2 | text-align: center;
3 | }
4 |
5 | .virus-map {
6 | width: 100vw;
7 | height: 80vh;
8 | }
9 |
10 | .information-map {
11 | margin: 50px 0 0 0;
12 | width: 100vw;
13 | height: 80vh;
14 | }
--------------------------------------------------------------------------------
/demo/javascript/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './App.css';
3 | import { HierarchicalVirusMap, InformationMap } from 'wuhan2020-mapviz';
4 | import { convertProvincesSeries, convertCountrySeries, } from './adapters/isaaclin';
5 | import patchData from './data/isaaclin/patch.json';
6 | import rawData from './data/isaaclin/history.json';
7 | import overviewData from './data/isaaclin/overall.json';
8 | import informationMockData from './mock/informationMapMockData';
9 |
10 |
11 | export default class App extends React.Component {
12 | render() {
13 | const resolution = 3600000 * 24;
14 | const data = {
15 | provincesSeries: convertProvincesSeries(
16 | [...rawData['results'], ...patchData],
17 | resolution,
18 | true
19 | ),
20 | countrySeries: convertCountrySeries(overviewData['results'], resolution)
21 | };
22 |
23 | const initPoint = informationMockData.initPoint;
24 | const zoom = informationMockData.zoom;
25 | const makerArray = informationMockData.makerArray;
26 |
27 | return (
28 |
29 |
疫情地图组件-概要视图
30 |
31 |
32 |
33 |
疫情地图组件-PC端视图
34 |
35 |
36 |
37 |
疫情地图组件-移动端视图
38 |
39 |
40 |
41 |
42 |
通用地图组件
43 |
48 |
49 |
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/demo/javascript/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/demo/javascript/src/adapters/isaaclin.js:
--------------------------------------------------------------------------------
1 | import long2short from './long2short'; // some city names are NOT short names so we also convert them here
2 |
3 | function convertStat(source) {
4 | return {
5 | confirmed: source.confirmedCount,
6 | suspected: source.suspectedCount,
7 | cured: source.curedCount,
8 | dead: source.deadCount
9 | };
10 | }
11 |
12 | function convertCountry(source) {
13 | let provinces = {};
14 |
15 | source.forEach(
16 | (p) =>
17 | (provinces[p.provinceShortName] = convertProvince(p))
18 | );
19 |
20 | // currently we only support china
21 | return {
22 | name: '中国',
23 | confirmed: 0,
24 | suspected: 0,
25 | cured: 0,
26 | dead: 0,
27 | provinces
28 | };
29 | }
30 |
31 | function convertProvince(source) {
32 | let cities = {};
33 | if (source.cities && source.cities.length > 0) {
34 | source.cities.forEach(
35 | (c) =>
36 | // 把省级的更新时间传入市级由于市级没有各自的数据更新时间
37 | (cities[long2short(c.cityName)] = convertCity(c, source.updateTime))
38 | );
39 | }
40 | return {
41 | name: source.provinceShortName,
42 | timestamp: source.updateTime,
43 | cities,
44 | ...convertStat(source)
45 | };
46 | }
47 |
48 | function convertCity(source, updateTime) {
49 | return {
50 | name: long2short(source.cityName),
51 | timestamp: updateTime, // 使用传入的省级数据更新时间
52 | ...convertStat(source)
53 | };
54 | }
55 |
56 | function roundTime(t, resolution) {
57 | const offset = resolution >= 24 * 3600000 ? 8 * 3600000 : 0; // consider locale if resolution > 1 day
58 | return Math.floor((t + offset) / resolution) * resolution - offset;
59 | }
60 |
61 | function fillForward(series) {
62 | const all_ts = Object.keys(series).sort();
63 | all_ts.forEach((t, i) => {
64 | if (i < all_ts.length - 1) {
65 | Object.keys(series[t]).forEach(name => {
66 | const next_t = parseInt(all_ts[i + 1], 10);
67 | if (series[next_t][name] === undefined) {
68 | series[next_t][name] = series[t][name];
69 | }
70 | });
71 | }
72 | });
73 | }
74 |
75 | function convertProvincesSeries(source, resolution, shouldFillForward = false) {
76 | let res = {};
77 | source
78 | .sort((item) => item.updateTime)
79 | .forEach((item) => {
80 | const t = roundTime(item.updateTime, resolution);
81 | if (res[t] === undefined) {
82 | res[t] = {};
83 | }
84 | const prov = convertProvince(item);
85 | res[t][prov.name] = prov;
86 | });
87 | if (shouldFillForward) {
88 | fillForward(res);
89 | }
90 | return res;
91 | }
92 |
93 | function extractCitiesSeries(series, name, resolution, shouldFillForward = false) {
94 | let res = {};
95 | Object.values(series).forEach((provs) => {
96 | if (provs[name] !== undefined) {
97 | res[roundTime(provs[name].timestamp, resolution)] = provs[name].cities;
98 | }
99 | });
100 | if (shouldFillForward) {
101 | fillForward(res);
102 | }
103 | return res;
104 | }
105 |
106 | function convertCountrySeries(source, resolution) {
107 | let res = {};
108 | source.forEach((item) => {
109 | const t = roundTime(item.updateTime, resolution);
110 | if (res[t] === undefined) {
111 | res[t] = {};
112 | }
113 | res[t] = item;
114 | });
115 | return res;
116 | }
117 |
118 | export {
119 | convertCountry,
120 | convertProvince,
121 | convertCity,
122 | convertProvincesSeries,
123 | convertCountrySeries,
124 | extractCitiesSeries
125 | };
126 |
--------------------------------------------------------------------------------
/demo/javascript/src/adapters/long2short.js:
--------------------------------------------------------------------------------
1 | // convert long city name to short
2 | const nations = [
3 | '仫佬族',
4 | '黎族',
5 | '土家族',
6 | '蒙古族',
7 | '羌族',
8 | '僳僳族',
9 | '哈尼族',
10 | '回族',
11 | '布朗族',
12 | '佤族',
13 | '哈萨克族',
14 | '藏族',
15 | '撒拉族',
16 | '畲族',
17 | '傣族',
18 | '维吾尔族',
19 | '毛南族',
20 | '高山族',
21 | '德昂族',
22 | '苗族',
23 | '仡佬族',
24 | '拉祜族',
25 | '保安族',
26 | '彝族',
27 | '锡伯族',
28 | '水族',
29 | '裕固族',
30 | '壮族',
31 | '阿昌族',
32 | '东乡族',
33 | '京族',
34 | '布依族',
35 | '普米族',
36 | '纳西族',
37 | '独龙族',
38 | '朝鲜族',
39 | '塔吉克族',
40 | '景颇族',
41 | '鄂伦春族',
42 | '满族',
43 | '怒族',
44 | '柯尔克孜族',
45 | '赫哲族',
46 | '侗族',
47 | '乌孜别克族',
48 | '土族',
49 | '门巴族',
50 | '瑶族',
51 | '俄罗斯族',
52 | '达斡尔族',
53 | '珞巴族',
54 | '白族',
55 | '鄂温克族',
56 | '塔塔尔族',
57 | '基诺族 '
58 | ];
59 |
60 | function long2short(name) {
61 | nations.forEach(n => (name = name.replace(n, '')));
62 | name = name.replace('自治', '');
63 | if (name.endsWith('林区')) return name;
64 | if (name.endsWith('区') || name.endsWith('市'))
65 | return name.slice(0, name.length - 1);
66 |
67 | return name;
68 | }
69 |
70 | export default long2short;
71 |
--------------------------------------------------------------------------------
/demo/javascript/src/data/isaaclin/patch.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "provinceName": "青海省",
4 | "provinceShortName": "青海",
5 | "confirmedCount": 1,
6 | "suspectedCount": 0,
7 | "curedCount": 0,
8 | "deadCount": 0,
9 | "comment": "",
10 | "cities": [
11 | {
12 | "cityName": "西宁",
13 | "confirmedCount": 1,
14 | "suspectedCount": 0,
15 | "curedCount": 0,
16 | "deadCount": 0
17 | }
18 | ],
19 | "country": "中国",
20 | "updateTime": 1580001790159
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/demo/javascript/src/data/isaaclin/update.sh:
--------------------------------------------------------------------------------
1 | curl "https://lab.isaaclin.cn/nCoV/api/area" > current.json
2 | curl "https://lab.isaaclin.cn/nCoV/api/area?latest=0" > history.json
3 | curl "https://lab.isaaclin.cn/nCoV/api/overall?latest=0" > overall.json
4 |
5 |
--------------------------------------------------------------------------------
/demo/javascript/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/demo/javascript/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/demo/javascript/src/mock/informationMapMockData.js:
--------------------------------------------------------------------------------
1 | /*
2 | InformationMap组件的mock数据
3 |
4 | 主要结构:
5 | {
6 | "initPoint": 地图初始点坐标
7 | "zoom": 地图初始缩放比例
8 | "markerArray": 地图标记点的内容
9 | },
10 | ...
11 |
12 | 其中,markerArray中每一项的内容为:
13 | export interface GeoData {
14 | type: "hospital"|"hotel"|"other";
15 | name: string;
16 | url?: string;
17 | coord: [number, number];
18 | metadata: Metadata[];
19 | }
20 | export interface Metadata {
21 | key: string;
22 | label?: string;
23 | value: string|number|InquiryMeta[],
24 | }
25 |
26 | 使用:
27 |
31 | */
32 |
33 | export default {
34 | initPoint: [110.350658, 32.938285],
35 | zoom: 6,
36 | makerArray: [
37 | {
38 | name: '华中科技大学同济医学院附属协和医院',
39 | type: 'hospital',
40 | coord: [114.281196, 30.590103],
41 | url: 'https://mp.weixin.qq.com/s/geO3CCd0_8B3L-r_xlBbZQ',
42 | metadata: [
43 | {
44 | key: 'request',
45 | label: '物资需求',
46 | value: [
47 | ['普通医用口罩', 10000],
48 | ['医用外科口罩', true],
49 | ['医用防护口罩 | N95口罩', 10000],
50 | ['防冲击眼罩/护目镜/防护眼镜', true],
51 | ['防护面罩', 25050],
52 | ['防护帽/医用帽/圆帽', 10420],
53 | ['隔离衣', true],
54 | ['防护服', 5000],
55 | ['手术衣', true],
56 | ['乳胶手套', true],
57 | ['长筒胶鞋/防污染靴', true],
58 | ['防污染鞋套', true],
59 | ['防污染靴套', true],
60 | ['84消毒液', true],
61 | ['过氧乙酸', true],
62 | ['75%酒精', true],
63 | ['手部皮肤消毒液', true],
64 | ['活力碘', true],
65 | ['床罩', true],
66 | ['医用面罩式雾化器', true],
67 | ['测体温设备', true],
68 | ['空气消毒设备', true],
69 | ['医用紫外线消毒车', true]
70 | ]
71 | },
72 | {
73 | key: 'address',
74 | value:
75 | '湖北省武汉市江汉区解放大道1277号华中科技大学同济医学院附属协和医院总务处',
76 | label: '邮寄地址'
77 | },
78 | {
79 | key: 'note',
80 | value: null,
81 | label: '备注信息'
82 | }
83 | ]
84 | },
85 | {
86 | name: '红安县人民医院',
87 | type: 'hospital',
88 | coord: [114.625222, 31.286868],
89 | url: 'https://mp.weixin.qq.com/s/geO3CCd0_8B3L-r_xlBbZQ',
90 | address: '红安县人民医院红安县城关镇陵园大道附50号',
91 | metadata: [
92 | {
93 | key: 'request',
94 | label: '物资需求',
95 | value: [
96 | ['普通医用口罩', 1000],
97 | ['医用外科口罩', 1000],
98 | ['医用防护口罩 | N95口罩', 10000],
99 | ['防冲击眼罩/护目镜/防护眼镜', 1000],
100 | ['防护面罩', true],
101 | ['防护帽/医用帽/圆帽', 1000],
102 | ['隔离衣', true],
103 | ['防护服', 100],
104 | ['手术衣', true],
105 | ['乳胶手套', 1000],
106 | ['长筒胶鞋/防污染靴', 100],
107 | ['防污染鞋套', 100],
108 | ['防污染靴套', 10000],
109 | ['84消毒液', true],
110 | ['过氧乙酸', true],
111 | ['75%酒精', true],
112 | ['手部皮肤消毒液', true],
113 | ['活力碘', true],
114 | ['床罩', true],
115 | ['医用面罩式雾化器', true],
116 | ['测体温设备', true],
117 | ['空气消毒设备', true],
118 | ['医用紫外线消毒车', true]
119 | ]
120 | },
121 | {
122 | key: 'address',
123 | value:
124 | '湖北省武汉市江汉区解放大道1277号华中科技大学同济医学院附属协和医院总务处',
125 | label: '邮寄地址'
126 | },
127 | {
128 | key: 'note',
129 | value: null,
130 | label: '备注信息'
131 | },
132 | {
133 | key: 'contact',
134 | label: '联系方式',
135 | value: [['0713-5242320'], ['设备科周主任, 13636105950']]
136 | }
137 | ]
138 | },
139 | {
140 | type: 'hotel',
141 | name: '住宿数据1',
142 | coord: [114.881337, 30.205063],
143 | metadata: [
144 | {
145 | key: 'capability',
146 | value: 100,
147 | label: '容量'
148 | },
149 | {
150 | key: 'note',
151 | value: '发布日期, 2020年1月25日',
152 | label: '备注信息'
153 | },
154 | {
155 | key: 'contact',
156 | label: '联系方式',
157 | value: [['XXX:123456789']]
158 | }
159 | ]
160 | },
161 |
162 | {
163 | type: 'others',
164 | name: '其他数据',
165 | coord: [114.681337, 30.295063],
166 | metadata: [
167 | {
168 | key: '内容',
169 | value: '我是内容'
170 | }
171 | ]
172 | }
173 | ]
174 | };
175 |
--------------------------------------------------------------------------------
/demo/javascript/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' }
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready.then(registration => {
134 | registration.unregister();
135 | });
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/demo/javascript/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/demo/typescript/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
--------------------------------------------------------------------------------
/demo/typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "map-viz-test-ts",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "@types/jest": "^24.0.0",
10 | "@types/node": "^12.0.0",
11 | "@types/react": "^16.9.0",
12 | "@types/react-dom": "^16.9.0",
13 | "react": "^16.12.0",
14 | "react-dom": "^16.12.0",
15 | "react-scripts": "3.3.1",
16 | "typescript": "~3.7.2",
17 | "wuhan2020-mapviz": "^1.0.11"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject"
24 | },
25 | "eslintConfig": {
26 | "extends": "react-app"
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/demo/typescript/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhan2020/map-viz/6bde1fecc8770805ea405bd16de21702ac62e31e/demo/typescript/public/favicon.ico
--------------------------------------------------------------------------------
/demo/typescript/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/demo/typescript/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhan2020/map-viz/6bde1fecc8770805ea405bd16de21702ac62e31e/demo/typescript/public/logo192.png
--------------------------------------------------------------------------------
/demo/typescript/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhan2020/map-viz/6bde1fecc8770805ea405bd16de21702ac62e31e/demo/typescript/public/logo512.png
--------------------------------------------------------------------------------
/demo/typescript/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/demo/typescript/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/demo/typescript/src/App.css:
--------------------------------------------------------------------------------
1 | .app {
2 | text-align: center;
3 | }
4 |
5 | .virus-map {
6 | width: 100vw;
7 | height: 80vh;
8 | }
9 |
10 | .information-map {
11 | margin: 50px 0 0 0;
12 | width: 100vw;
13 | height: 80vh;
14 | }
--------------------------------------------------------------------------------
/demo/typescript/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/demo/typescript/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './App.css';
3 | import { HierarchicalVirusMap, InformationMap } from 'wuhan2020-mapviz';
4 | import { convertProvincesSeries, convertCountrySeries } from './adapters/isaaclin';
5 | import patchData from './data/isaaclin/patch.json';
6 | import rawData from './data/isaaclin/history.json';
7 | import overviewData from './data/isaaclin/overall.json';
8 | import informationMockData from './mock/informationMapMockData';
9 |
10 |
11 | function App() {
12 | const resolution = 3600000 * 24;
13 | const data = {
14 | provincesSeries: convertProvincesSeries(
15 | [...rawData['results'], ...patchData],
16 | resolution,
17 | true
18 | ),
19 | countrySeries: convertCountrySeries(overviewData['results'], resolution)
20 | };
21 |
22 | const initPoint = informationMockData.initPoint;
23 | const zoom = informationMockData.zoom;
24 | const makerArray = informationMockData.makerArray;
25 |
26 | return (
27 |
28 |
疫情地图组件-概要视图
29 |
30 |
31 |
32 |
疫情地图组件-PC端视图
33 |
34 |
35 |
36 |
疫情地图组件-移动端视图
37 |
38 |
39 |
40 |
41 |
通用地图组件
42 |
47 |
48 |
49 | );
50 | }
51 |
52 | export default App;
53 |
--------------------------------------------------------------------------------
/demo/typescript/src/adapters/isaaclin.js:
--------------------------------------------------------------------------------
1 | import long2short from './long2short'; // some city names are NOT short names so we also convert them here
2 |
3 | function convertStat(source) {
4 | return {
5 | confirmed: source.confirmedCount,
6 | suspected: source.suspectedCount,
7 | cured: source.curedCount,
8 | dead: source.deadCount
9 | };
10 | }
11 |
12 | function convertCountry(source) {
13 | let provinces = {};
14 |
15 | source.forEach(
16 | (p) =>
17 | (provinces[p.provinceShortName] = convertProvince(p))
18 | );
19 |
20 | // currently we only support china
21 | return {
22 | name: '中国',
23 | confirmed: 0,
24 | suspected: 0,
25 | cured: 0,
26 | dead: 0,
27 | provinces
28 | };
29 | }
30 |
31 | function convertProvince(source) {
32 | let cities = {};
33 | if (source.cities && source.cities.length > 0) {
34 | source.cities.forEach(
35 | (c) =>
36 | // 把省级的更新时间传入市级由于市级没有各自的数据更新时间
37 | (cities[long2short(c.cityName)] = convertCity(c, source.updateTime))
38 | );
39 | }
40 | return {
41 | name: source.provinceShortName,
42 | timestamp: source.updateTime,
43 | cities,
44 | ...convertStat(source)
45 | };
46 | }
47 |
48 | function convertCity(source, updateTime) {
49 | return {
50 | name: long2short(source.cityName),
51 | timestamp: updateTime, // 使用传入的省级数据更新时间
52 | ...convertStat(source)
53 | };
54 | }
55 |
56 | function roundTime(t, resolution) {
57 | const offset = resolution >= 24 * 3600000 ? 8 * 3600000 : 0; // consider locale if resolution > 1 day
58 | return Math.floor((t + offset) / resolution) * resolution - offset;
59 | }
60 |
61 | function fillForward(series) {
62 | const all_ts = Object.keys(series).sort();
63 | all_ts.forEach((t, i) => {
64 | if (i < all_ts.length - 1) {
65 | Object.keys(series[t]).forEach(name => {
66 | const next_t = parseInt(all_ts[i + 1], 10);
67 | if (series[next_t][name] === undefined) {
68 | series[next_t][name] = series[t][name];
69 | }
70 | });
71 | }
72 | });
73 | }
74 |
75 | function convertProvincesSeries(source, resolution, shouldFillForward = false) {
76 | let res = {};
77 | source
78 | .sort((item) => item.updateTime)
79 | .forEach((item) => {
80 | const t = roundTime(item.updateTime, resolution);
81 | if (res[t] === undefined) {
82 | res[t] = {};
83 | }
84 | const prov = convertProvince(item);
85 | res[t][prov.name] = prov;
86 | });
87 | if (shouldFillForward) {
88 | fillForward(res);
89 | }
90 | return res;
91 | }
92 |
93 | function extractCitiesSeries(series, name, resolution, shouldFillForward = false) {
94 | let res = {};
95 | Object.values(series).forEach((provs) => {
96 | if (provs[name] !== undefined) {
97 | res[roundTime(provs[name].timestamp, resolution)] = provs[name].cities;
98 | }
99 | });
100 | if (shouldFillForward) {
101 | fillForward(res);
102 | }
103 | return res;
104 | }
105 |
106 | function convertCountrySeries(source, resolution) {
107 | let res = {};
108 | source.forEach((item) => {
109 | const t = roundTime(item.updateTime, resolution);
110 | if (res[t] === undefined) {
111 | res[t] = {};
112 | }
113 | res[t] = item;
114 | });
115 | return res;
116 | }
117 |
118 | export {
119 | convertCountry,
120 | convertProvince,
121 | convertCity,
122 | convertProvincesSeries,
123 | convertCountrySeries,
124 | extractCitiesSeries
125 | };
126 |
--------------------------------------------------------------------------------
/demo/typescript/src/adapters/long2short.js:
--------------------------------------------------------------------------------
1 | // convert long city name to short
2 | const nations = [
3 | '仫佬族',
4 | '黎族',
5 | '土家族',
6 | '蒙古族',
7 | '羌族',
8 | '僳僳族',
9 | '哈尼族',
10 | '回族',
11 | '布朗族',
12 | '佤族',
13 | '哈萨克族',
14 | '藏族',
15 | '撒拉族',
16 | '畲族',
17 | '傣族',
18 | '维吾尔族',
19 | '毛南族',
20 | '高山族',
21 | '德昂族',
22 | '苗族',
23 | '仡佬族',
24 | '拉祜族',
25 | '保安族',
26 | '彝族',
27 | '锡伯族',
28 | '水族',
29 | '裕固族',
30 | '壮族',
31 | '阿昌族',
32 | '东乡族',
33 | '京族',
34 | '布依族',
35 | '普米族',
36 | '纳西族',
37 | '独龙族',
38 | '朝鲜族',
39 | '塔吉克族',
40 | '景颇族',
41 | '鄂伦春族',
42 | '满族',
43 | '怒族',
44 | '柯尔克孜族',
45 | '赫哲族',
46 | '侗族',
47 | '乌孜别克族',
48 | '土族',
49 | '门巴族',
50 | '瑶族',
51 | '俄罗斯族',
52 | '达斡尔族',
53 | '珞巴族',
54 | '白族',
55 | '鄂温克族',
56 | '塔塔尔族',
57 | '基诺族 '
58 | ];
59 |
60 | function long2short(name) {
61 | nations.forEach(n => (name = name.replace(n, '')));
62 | name = name.replace('自治', '');
63 | if (name.endsWith('林区')) return name;
64 | if (name.endsWith('区') || name.endsWith('市'))
65 | return name.slice(0, name.length - 1);
66 |
67 | return name;
68 | }
69 |
70 | export default long2short;
71 |
--------------------------------------------------------------------------------
/demo/typescript/src/data/isaaclin/patch.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "provinceName": "青海省",
4 | "provinceShortName": "青海",
5 | "confirmedCount": 1,
6 | "suspectedCount": 0,
7 | "curedCount": 0,
8 | "deadCount": 0,
9 | "comment": "",
10 | "cities": [
11 | {
12 | "cityName": "西宁",
13 | "confirmedCount": 1,
14 | "suspectedCount": 0,
15 | "curedCount": 0,
16 | "deadCount": 0
17 | }
18 | ],
19 | "country": "中国",
20 | "updateTime": 1580001790159
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/demo/typescript/src/data/isaaclin/update.sh:
--------------------------------------------------------------------------------
1 | curl "https://lab.isaaclin.cn/nCoV/api/area" > current.json
2 | curl "https://lab.isaaclin.cn/nCoV/api/area?latest=0" > history.json
3 | curl "https://lab.isaaclin.cn/nCoV/api/overall?latest=0" > overall.json
4 |
5 |
--------------------------------------------------------------------------------
/demo/typescript/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/demo/typescript/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/demo/typescript/src/mock/informationMapMockData.ts:
--------------------------------------------------------------------------------
1 | /*
2 | InformationMap组件的mock数据
3 |
4 | 主要结构:
5 | {
6 | "initPoint": 地图初始点坐标
7 | "zoom": 地图初始缩放比例
8 | "markerArray": 地图标记点的内容
9 | },
10 | ...
11 |
12 | 其中,markerArray中每一项的内容为:
13 | export interface GeoData {
14 | type: "hospital"|"hotel"|"other";
15 | name: string;
16 | url?: string;
17 | coord: [number, number];
18 | metadata: Metadata[];
19 | }
20 | export interface Metadata {
21 | key: string;
22 | label?: string;
23 | value: string|number|InquiryMeta[],
24 | }
25 |
26 | 使用:
27 |
31 | */
32 |
33 | export default {
34 | initPoint: [110.350658, 32.938285],
35 | zoom: 6,
36 | makerArray: [
37 | {
38 | name: '华中科技大学同济医学院附属协和医院',
39 | type: 'hospital',
40 | coord: [114.281196, 30.590103],
41 | url: 'https://mp.weixin.qq.com/s/geO3CCd0_8B3L-r_xlBbZQ',
42 | metadata: [
43 | {
44 | key: 'request',
45 | label: '物资需求',
46 | value: [
47 | ['普通医用口罩', 10000],
48 | ['医用外科口罩', true],
49 | ['医用防护口罩 | N95口罩', 10000],
50 | ['防冲击眼罩/护目镜/防护眼镜', true],
51 | ['防护面罩', 25050],
52 | ['防护帽/医用帽/圆帽', 10420],
53 | ['隔离衣', true],
54 | ['防护服', 5000],
55 | ['手术衣', true],
56 | ['乳胶手套', true],
57 | ['长筒胶鞋/防污染靴', true],
58 | ['防污染鞋套', true],
59 | ['防污染靴套', true],
60 | ['84消毒液', true],
61 | ['过氧乙酸', true],
62 | ['75%酒精', true],
63 | ['手部皮肤消毒液', true],
64 | ['活力碘', true],
65 | ['床罩', true],
66 | ['医用面罩式雾化器', true],
67 | ['测体温设备', true],
68 | ['空气消毒设备', true],
69 | ['医用紫外线消毒车', true]
70 | ]
71 | },
72 | {
73 | key: 'address',
74 | value:
75 | '湖北省武汉市江汉区解放大道1277号华中科技大学同济医学院附属协和医院总务处',
76 | label: '邮寄地址'
77 | },
78 | {
79 | key: 'note',
80 | value: null,
81 | label: '备注信息'
82 | }
83 | ]
84 | },
85 | {
86 | name: '红安县人民医院',
87 | type: 'hospital',
88 | coord: [114.625222, 31.286868],
89 | url: 'https://mp.weixin.qq.com/s/geO3CCd0_8B3L-r_xlBbZQ',
90 | address: '红安县人民医院红安县城关镇陵园大道附50号',
91 | metadata: [
92 | {
93 | key: 'request',
94 | label: '物资需求',
95 | value: [
96 | ['普通医用口罩', 1000],
97 | ['医用外科口罩', 1000],
98 | ['医用防护口罩 | N95口罩', 10000],
99 | ['防冲击眼罩/护目镜/防护眼镜', 1000],
100 | ['防护面罩', true],
101 | ['防护帽/医用帽/圆帽', 1000],
102 | ['隔离衣', true],
103 | ['防护服', 100],
104 | ['手术衣', true],
105 | ['乳胶手套', 1000],
106 | ['长筒胶鞋/防污染靴', 100],
107 | ['防污染鞋套', 100],
108 | ['防污染靴套', 10000],
109 | ['84消毒液', true],
110 | ['过氧乙酸', true],
111 | ['75%酒精', true],
112 | ['手部皮肤消毒液', true],
113 | ['活力碘', true],
114 | ['床罩', true],
115 | ['医用面罩式雾化器', true],
116 | ['测体温设备', true],
117 | ['空气消毒设备', true],
118 | ['医用紫外线消毒车', true]
119 | ]
120 | },
121 | {
122 | key: 'address',
123 | value:
124 | '湖北省武汉市江汉区解放大道1277号华中科技大学同济医学院附属协和医院总务处',
125 | label: '邮寄地址'
126 | },
127 | {
128 | key: 'note',
129 | value: null,
130 | label: '备注信息'
131 | },
132 | {
133 | key: 'contact',
134 | label: '联系方式',
135 | value: [['0713-5242320'], ['设备科周主任, 13636105950']]
136 | }
137 | ]
138 | },
139 | {
140 | type: 'hotel',
141 | name: '住宿数据1',
142 | coord: [114.881337, 30.205063],
143 | metadata: [
144 | {
145 | key: 'capability',
146 | value: 100,
147 | label: '容量'
148 | },
149 | {
150 | key: 'note',
151 | value: '发布日期, 2020年1月25日',
152 | label: '备注信息'
153 | },
154 | {
155 | key: 'contact',
156 | label: '联系方式',
157 | value: [['XXX:123456789']]
158 | }
159 | ]
160 | },
161 |
162 | {
163 | type: 'others',
164 | name: '其他数据',
165 | coord: [114.681337, 30.295063],
166 | metadata: [
167 | {
168 | key: '内容',
169 | value: '我是内容'
170 | }
171 | ]
172 | }
173 | ]
174 | };
175 |
--------------------------------------------------------------------------------
/demo/typescript/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/demo/typescript/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | process.env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl, {
112 | headers: { 'Service-Worker': 'script' }
113 | })
114 | .then(response => {
115 | // Ensure service worker exists, and that we really are getting a JS file.
116 | const contentType = response.headers.get('content-type');
117 | if (
118 | response.status === 404 ||
119 | (contentType != null && contentType.indexOf('javascript') === -1)
120 | ) {
121 | // No service worker found. Probably a different app. Reload the page.
122 | navigator.serviceWorker.ready.then(registration => {
123 | registration.unregister().then(() => {
124 | window.location.reload();
125 | });
126 | });
127 | } else {
128 | // Service worker found. Proceed as normal.
129 | registerValidSW(swUrl, config);
130 | }
131 | })
132 | .catch(() => {
133 | console.log(
134 | 'No internet connection found. App is running in offline mode.'
135 | );
136 | });
137 | }
138 |
139 | export function unregister() {
140 | if ('serviceWorker' in navigator) {
141 | navigator.serviceWorker.ready.then(registration => {
142 | registration.unregister();
143 | });
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/demo/typescript/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/demo/typescript/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'wuhan2020-mapviz';
--------------------------------------------------------------------------------
/lib/components.js:
--------------------------------------------------------------------------------
1 | module.exports=function(t){var e={};function n(o){if(e[o])return e[o].exports;var a=e[o]={i:o,l:!1,exports:{}};return t[o].call(a.exports,a,a.exports,n),a.l=!0,a.exports}return n.m=t,n.c=e,n.d=function(t,e,o){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var a in t)n.d(o,a,function(e){return t[e]}.bind(null,a));return o},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=2)}([function(t,e){t.exports=require("react")},function(t,e){t.exports=require("echarts")},function(t,e,n){"use strict";n.r(e);var o=n(0),a=n.n(o),i=n(1),r=n.n(i),c=["仫佬族","黎族","土家族","蒙古族","羌族","僳僳族","哈尼族","回族","布朗族","佤族","哈萨克族","藏族","撒拉族","畲族","傣族","维吾尔族","毛南族","高山族","德昂族","苗族","仡佬族","拉祜族","保安族","彝族","锡伯族","水族","裕固族","壮族","阿昌族","东乡族","京族","布依族","普米族","纳西族","独龙族","朝鲜族","塔吉克族","景颇族","鄂伦春族","满族","怒族","柯尔克孜族","赫哲族","侗族","乌孜别克族","土族","门巴族","瑶族","俄罗斯族","达斡尔族","珞巴族","白族","鄂温克族","塔塔尔族","基诺族 "];var p,s=function(t){return c.forEach((function(e){return t=t.replace(e,"")})),(t=t.replace("自治","")).endsWith("林区")?t:t.endsWith("区")||t.endsWith("市")?t.slice(0,t.length-1):t},u=(p=function(t,e){return(p=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])})(t,e)},function(t,e){function n(){this.constructor=t}p(t,e),t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)}),h=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.chartId=e.generateChartId(),e.chart=null,e}return u(e,t),e.prototype.generateChartId=function(){var t=Math.floor(100*Math.random()),e=(new Date).getTime();return"map"+t.toString()+e.toString()},e.prototype.componentDidMount=function(){var t=this.props,e=t.mapUrl,n=t.mapName,o=t.chartOptions,a=t.chartOnClickCallBack;this.updateMap(e,n,o,a)},e.prototype.updateMap=function(t,e,n,o){var a=this;fetch(t).then((function(t){return t.json()})).then((function(t){if(document.getElementById(a.chartId)){if(t.features.forEach((function(t){return t.properties.name=s(t.properties.name)})),r.a.registerMap(e,t),!a.chart){var i={times:0,name:""};a.chart=r.a.init(document.getElementById(a.chartId)),a.chart.on("click","series",(function(t){a.props.mobile?i.name===t.name&&1===i.times?(o(t,a.chart),i.times=0,i.name=""):(i.times=1,i.name=t.name):o(t,a.chart)})),a.chart.on("click","timeline",(function(t){a.chart.dispatchAction({type:"timelineChange",currentIndex:n.baseOption.timeline.data.findIndex((function(e){return e===t.dataIndex}))})}))}a.chart.setOption(n);var c=window.onresize;window.onresize=function(){c(),a.chart.resize()}}})).catch((function(t){return console.log("加载疫情地图出现错误!",t)}))},e.prototype.render=function(){if(this.chart){var t=this.props,e=t.mapUrl,n=t.mapName,o=t.chartOptions,i=t.chartOnClickCallBack;this.updateMap(e,n,o,i)}return a.a.createElement("div",{id:this.chartId,style:{width:"100%",height:"100%"}})},e.defaultProps={mapUrl:"",chartOptions:{},mapName:"",mobile:!0,chartOnClickCallBack:function(t,e){console.log(t,e)}},e}(a.a.Component),l=function(){var t=function(e,n){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])})(e,n)};return function(e,n){function o(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(o.prototype=n.prototype,new o)}}(),d=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.chartId=e.generateChartId(),e.chart=null,e.getEchartsInstance=function(){return r.a.getInstanceByDom(document.getElementById(e.chartId))||r.a.init(document.getElementById(e.chartId))},e}return l(e,t),e.prototype.generateChartId=function(){var t=Math.floor(100*Math.random()),e=(new Date).getTime();return"map"+t.toString()+e.toString()},e.prototype.componentDidMount=function(){var t=this;setTimeout((function(){if(document.getElementById(t.chartId)){t.chart=r.a.init(document.getElementById(t.chartId)),t.chart.setOption(t.props.chartOptions);var e=window.onresize;window.onresize=function(){e&&e(),t.chart.resize()}}}),0)},e.prototype.render=function(){return this.chart&&this.chart.setOption(this.props.chartOptions,!1,!1),a.a.createElement("div",{id:this.chartId,style:{width:"100%",height:"100%"}})},e.defaultProps={chartOptions:{}},e}(a.a.Component),m=["湖北","广东","浙江","北京","上海","湖南","安徽","重庆","四川","山东","广西","福建","江苏","河南","海南","天津","江西","陕西","贵州","辽宁","香港","黑龙江","澳门","新疆","甘肃","云南","台湾","山西","吉林","河北","宁夏","内蒙古","青海"],f=function(){var t=function(e,n){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])})(e,n)};return function(e,n){function o(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(o.prototype=n.prototype,new o)}}(),g=function(t){return(window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth)>.8*(window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight)?t*(window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth)/1e3:t*(window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth)/500},y=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return f(e,t),e.prototype.getOrderedTimeData=function(t){var e=[];for(var n in t)t[n].date=parseInt(n),e.push(t[n]);return e.sort((function(t,e){return t.date-e.date})),e},e.prototype.getData=function(t,e,n,o){var a=[],i=[],r=[],c=[];if(0===o.length&&"中国"===n)for(var p=0,s=e;p.8*(window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight)?n*(window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth)/1e3:n*(window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth)/500)}},tooltip:{},visualMap:[a],series:[{name:"疫情数据",type:"map",map:O(t),mapType:"map",zoom:1,label:{show:"pc"===this.props.type,fontSize:8,textBorderColor:"#FAFAFA",textBorderWidth:1},emphasis:{label:{show:"pc"===this.props.type,fontSize:8}},data:[]}]}},e.prototype.overrides=function(t){var e=this;return{tooltip:{trigger:"item",triggerOn:"onmousemove",confine:"true",formatter:function(n){if("timeline"===n.componentType)return n.dataIndex%24*36e5==0?new Date(n.dataIndex).toLocaleDateString("zh-CN"):new Date(n.dataIndex).toLocaleDateString("zh-CN-u-hc-h24");var o=[n.name];return void 0===t[n.name]?n.name+"
暂无数据":(void 0!==t[n.name].confirmed&&o.push("确诊:"+t[n.name].confirmed),void 0!==t[n.name].suspected&&o.push("疑似:"+t[n.name].suspected),void 0!==t[n.name].cured&&o.push("治愈:"+t[n.name].cured),void 0!==t[n.name].dead&&o.push("死亡:"+t[n.name].dead),"mobile"===e.props.type&&o.push('
再次点击查看详情
'),o.join("
"))}},series:[{data:Object.keys(t).map((function(e){return{name:e,value:t[e].confirmed||0}}))}]}},e.prototype.getChartOptions=function(t,e){void 0===e&&(e=null),e||(e=this.baseOptions(this.props.name,this.props.breaks));var n=this.overrides(t);return e.series[0].data=n.series[0].data,e.tooltip=n.tooltip,e},e.prototype.getSTChartOptions=function(t,e){var n=this;void 0===e&&(e=null);var o=this.props,a=o.name,i=o.breaks,r=o.type;return e||(e=this.baseOptions(a,i)),e.timeline={axisType:"time",show:!0,tooltip:{formatter:function(t){return new Date(parseInt(t.dataIndex,10)).toLocaleDateString("zh-CN")}},playInterval:1500,currentIndex:t.timeline.length-1,left:"20px",right:"20px",symbolSize:"mobile"===r?5:10,checkpointStyle:{symbolSize:"mobile"===r?8:13},data:t.timeline,label:{fontSize:10,position:10,formatter:function(e){return e!==t.timeline[0]&&e!==t.timeline[t.timeline.length-1]?"":new Date(parseInt(e,10)).toLocaleDateString("zh-CN").substring(5)}}},{baseOption:e,options:t.timeline.sort().map((function(e){return n.overrides(t.data[e])}))}},e.prototype.isTimelineData=function(t){return void 0!==t.timeline},e.prototype.render=function(){var t=this.props,e=t.name,n=t.data,o=t.chartOnClickCallBack,i=t.chartData,r=t.chartPath,c=t.currentChartArea,p=t.type,s=v[e];return a.a.createElement("div",{style:"mobile"===p?{display:"flex",flexDirection:"column",width:"100%",height:"100%"}:{display:"flex",flexDirection:"row",width:"100%",height:"100%"}},a.a.createElement("div",{style:"mobile"!==p?"overview"!==p?{width:"65%",height:"100%"}:{width:"100%",height:"100%"}:{width:"100%",height:"65%"}},a.a.createElement(h,{mapUrl:s,mapName:O(e),chartOptions:this.isTimelineData(n)?this.getSTChartOptions(n):this.getChartOptions(n),chartOnClickCallBack:o,mobile:"mobile"===p})),a.a.createElement("div",{style:"mobile"!==p?"overview"!==p?{width:"65%",height:"100%"}:{display:"none"}:{width:"100%",height:"35%"}},a.a.createElement(y,{data:i,area:c,path:r,type:p})))},e.defaultProps={name:"",data:{},breaks:[1,10,50,100,500,1e3],chartData:{},chartPath:[],currentChartArea:"",type:"pc",chartOnClickCallBack:function(t,e){console.log(t,e)}},e}(a.a.Component);function j(t,e){var n=e>=864e5?288e5:0;return Math.floor((t+n)/e)*e-n}function D(t){var e=Object.keys(t).sort();e.forEach((function(n,o){o0&&this.setState({path:this.state.path.slice(0,this.state.path.length-1),currentChartArea:"中国"})},e.prototype.getVirusMapConfig=function(t,e,n){var o="中国",a={timeline:[],data:{}};if(0===t.length)a={timeline:Object.keys(e).map((function(t){return parseInt(t,10)})).sort(),data:e};else if(1===t.length){var i=function(t,e,n,o){void 0===o&&(o=!1);var a={};return Object.values(t).forEach((function(t){void 0!==t[e]&&(a[j(t[e].timestamp,n)]=t[e].cities)})),o&&D(a),a}(e,o=t[0],n,!0);a={timeline:Object.keys(i).map((function(t){return parseInt(t,10)})).sort(),data:i}}return{name:o,data:a,navigateDown:this.navigateDown.bind(this)}},e.prototype.autoBreaks=function(t){var e=[1,10,50,100,500,1e3],n=500*Math.floor(Math.max.apply(Math,t.filter((function(t){return void 0!==t})))/5/500)/Math.max.apply(Math,e),o=e.map((function(t){return n*t}));return o[0]=1,o},e.prototype.render=function(){var t=this.props,e=t.data,n=t.resolution,o=t.type,i=this.state,r=i.path,c=i.currentChartArea,p=this.getVirusMapConfig(r,e.provincesSeries,n),s=e.provincesSeries[Math.max.apply(Math,Object.keys(e.provincesSeries).map((function(t){return parseInt(t,10)})))];return a.a.createElement("div",{style:{position:"relative",width:"100%",height:"100%"}},a.a.createElement(C,{name:p.name,data:p.data,breaks:this.autoBreaks(Object.values(s).map((function(t){return t.confirmed}))),chartData:e,chartPath:r,currentChartArea:c,chartOnClickCallBack:"overview"===o?function(){}:p.navigateDown,type:o}),a.a.createElement("button",{style:{display:this.state.path.length>0?"block":"none",width:"30px",height:"30px",position:"absolute",top:"50px",left:"120px",padding:"5px",backgroundColor:"#86868d",border:"none",borderRadius:"3px",fontWeight:"bolder",color:"white",fontSize:"13pt"},onClick:this.navigateUp.bind(this)},a.a.createElement("span",null,"-")))},e.defaultProps={data:{provincesSeries:{},countrySeries:{}},resolution:36e5,type:"pc",detail:!0},e}(a.a.Component),I=function(){var t=function(e,n){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])})(e,n)};return function(e,n){function o(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(o.prototype=n.prototype,new o)}}(),k=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.mapId=e.generateMapId(),e}return I(e,t),e.prototype.generateMapId=function(){var t=Math.floor(100*Math.random());return"map"+(new Date).getTime().toString()+t.toString()},e.prototype.componentDidMount=function(){var t=this;setTimeout((function(){var e=document.createElement("script");e.src="http://api.map.baidu.com/api?v=2.0&ak="+t.props.baiduMapKey+"&callback=initialize",document.body.appendChild(e),window.initialize=function(){var e=new window.BMap.Map(t.mapId),n=new window.BMap.Point(t.props.mapOptions.initPoint[0],t.props.mapOptions.initPoint[1]);e.centerAndZoom(n,t.props.mapOptions.zoom),e.enableScrollWheelZoom(!0),e.addControl(new window.BMap.NavigationControl);for(var o=function(t){var n=new window.BMap.Point(t.point[0],t.point[1]),o=new window.BMap.Icon(t.icon,new window.BMap.Size(25,25),{offset:new window.BMap.Size(10,25)}),a=new window.BMap.Marker(n,{icon:o});a.addEventListener("click",(function(){var n=new window.BMap.InfoWindow(t.infoWindowContent,{title:t.infoWindowTitle});a.openInfoWindow(n,e.getCenter())})),e.addOverlay(a)},a=0,i=t.props.mapOptions.markerArray;a"+t+"\n "},e.prototype.generateTooltipContent=function(t,e){var n="";return t.forEach((function(t){var e=t.value;t.value&&"string"!=typeof t.value&&"number"!=typeof t.value&&(e="",t.value.forEach((function(t){var n=t.filter((function(t){return"string"==typeof t||"number"==typeof t})).map((function(t){return""+t}));n.length>0&&(e+=""+n.join(" ")+"
")}))),e&&(n+=""+(t.label||t.key)+""+e+"
")})),e&&(n+=""),n},e.prototype.generateOptions=function(t,e){return{point:t.coord,icon:e,infoWindowTitle:t.name,infoWindowContent:this.generateTooltipContent(t.metadata,t.url)}},e.prototype.generateMarker=function(t){for(var e=[],n=P,o=0,a=t;o;
4 | icon: string;
5 | labelText: string;
6 | labelStyle: Object;
7 | infoWindowTitle: string;
8 | infoWindowContent: string;
9 | };
10 | declare type MapOptions = {
11 | initPoint: Array;
12 | zoom: number;
13 | markerArray: Array;
14 | };
15 | declare type Props = {
16 | baiduMapKey: string;
17 | mapOptions: MapOptions;
18 | };
19 | export declare class BaiduMap extends React.Component {
20 | static defaultProps: {
21 | baiduMapKey: string;
22 | mapOptions: {};
23 | };
24 | mapId: string;
25 | generateMapId(): string;
26 | componentDidMount(): void;
27 | render(): JSX.Element;
28 | }
29 | export {};
30 |
--------------------------------------------------------------------------------
/lib/types/src/components/informationMap/informationMap.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | declare type InquiryMeta = [string, number | string | boolean];
3 | export interface Metadata {
4 | key: string;
5 | label?: string;
6 | value: string | number | InquiryMeta[];
7 | }
8 | export interface GeoData {
9 | type: 'hospital' | 'hotel' | 'other';
10 | name: string;
11 | url: string;
12 | coord: [number, number];
13 | metadata: Metadata[];
14 | }
15 | declare type Props = {
16 | initPoint: [number, number];
17 | zoom: number;
18 | markers: GeoData[];
19 | };
20 | declare class InformationMap extends React.Component {
21 | static defaultProps: {
22 | initPoint: number[];
23 | zoom: number;
24 | markers: any[];
25 | };
26 | generateDefaultInfoWindowContent(content: string): string;
27 | generateTooltipContent(metas: Metadata[], url: string): string;
28 | generateOptions(item: GeoData, iconUrl: string): {
29 | point: [number, number];
30 | icon: string;
31 | infoWindowTitle: string;
32 | infoWindowContent: string;
33 | };
34 | generateMarker(markerArray: GeoData[]): any[];
35 | getMapOptions(initPoint: [number, number], zoom: number, markers: GeoData[]): {
36 | initPoint: [number, number];
37 | zoom: number;
38 | markerArray: any[];
39 | };
40 | render(): JSX.Element;
41 | }
42 | export default InformationMap;
43 |
--------------------------------------------------------------------------------
/lib/types/src/components/informationMap/marker.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: {
2 | "hospital": string;
3 | "hotel": string;
4 | "others": string;
5 | };
6 | export default _default;
7 |
--------------------------------------------------------------------------------
/lib/types/src/components/virusMap/echartsMap.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | interface Props {
3 | mapUrl: string;
4 | chartOptions: any;
5 | mapName: string;
6 | mobile: boolean;
7 | chartOnClickCallBack: Function;
8 | }
9 | export declare class EchartsMap extends React.Component {
10 | static defaultProps: {
11 | mapUrl: string;
12 | chartOptions: {};
13 | mapName: string;
14 | mobile: boolean;
15 | chartOnClickCallBack: (param: any, chart: any) => void;
16 | };
17 | chartId: string;
18 | chart: any;
19 | generateChartId(): string;
20 | componentDidMount(): void;
21 | updateMap(mapUrl: string, mapName: string, chartOptions: any, chartOnClickCallBack: Function): void;
22 | render(): JSX.Element;
23 | }
24 | export {};
25 |
--------------------------------------------------------------------------------
/lib/types/src/components/virusMap/hierarchicalVirusMap.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | declare type MapDataType = {
3 | [name: string]: any;
4 | };
5 | declare type STMapDataType = {
6 | timeline: number[];
7 | data: {
8 | [timestamp: number]: MapDataType;
9 | };
10 | };
11 | declare type Props = {
12 | data: any;
13 | resolution: number;
14 | type: 'overview' | 'pc' | 'mobile';
15 | };
16 | declare type State = {
17 | path: string[];
18 | currentChartArea: string;
19 | };
20 | declare class HierarchicalVirusMap extends React.Component> {
21 | constructor(props: Props);
22 | static defaultProps: {
23 | data: {
24 | provincesSeries: {};
25 | countrySeries: {};
26 | };
27 | resolution: number;
28 | type: string;
29 | detail: boolean;
30 | };
31 | navigateDown(params: any): void;
32 | navigateUp(): void;
33 | getVirusMapConfig(path: any, data: any, resolution: any): {
34 | name: string;
35 | data: STMapDataType;
36 | navigateDown: any;
37 | };
38 | autoBreaks(values: number[]): number[];
39 | render(): JSX.Element;
40 | }
41 | export default HierarchicalVirusMap;
42 |
--------------------------------------------------------------------------------
/lib/types/src/components/virusMap/reactEcharts.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import echarts from 'echarts';
3 | declare type Props = {
4 | chartOptions: any;
5 | };
6 | export declare class ReactEcharts extends React.Component {
7 | static defaultProps: {
8 | chartOptions: {};
9 | };
10 | chartId: string;
11 | chart: any;
12 | generateChartId(): string;
13 | componentDidMount(): void;
14 | getEchartsInstance: () => echarts.ECharts;
15 | render(): JSX.Element;
16 | }
17 | export {};
18 |
--------------------------------------------------------------------------------
/lib/types/src/components/virusMap/virusChart.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | declare type Props = {
3 | data: any;
4 | area: string;
5 | type: string;
6 | path: Array;
7 | };
8 | declare type State = {
9 | echartOptions: any;
10 | };
11 | export declare class VirusChart extends React.Component> {
12 | getConfirmedSuspectChart: any;
13 | curedDeadChart: any;
14 | static defaultProps: {
15 | data: {
16 | provincesSeries: {};
17 | countrySeries: {};
18 | };
19 | area: string;
20 | path: string;
21 | };
22 | getOrderedTimeData(data: any): any[];
23 | getData(orderedProvinceData: Array, orderedOverviewData: Array, area: string, path: Array): {
24 | confirmedData: any[];
25 | suspectedData: any[];
26 | curedData: any[];
27 | deadData: any[];
28 | };
29 | getConfirmedSuspectChartOptions(orderedProvinceData: Array, orderedOverviewData: Array, area: string, path: Array): {
30 | title: {
31 | text: string;
32 | x: string;
33 | textStyle: {
34 | fontSize: number;
35 | };
36 | };
37 | legend: {
38 | top: number;
39 | orient: string;
40 | data: string[];
41 | itemWidth: number;
42 | itemHeight: number;
43 | textStyle: {
44 | fontSize: number;
45 | };
46 | };
47 | grid: {
48 | bottom: string;
49 | };
50 | tooltip: {
51 | trigger: string;
52 | };
53 | xAxis: {
54 | name: string;
55 | type: string;
56 | nameTextStyle: {
57 | fontSize: number;
58 | };
59 | nameGap: number;
60 | axisLabel: {
61 | textStyle: {
62 | fontSize: number;
63 | };
64 | formatter: (params: any) => string;
65 | };
66 | };
67 | yAxis: {
68 | name: string;
69 | nameTextStyle: {
70 | fontSize: number;
71 | };
72 | nameGap: number;
73 | axisLabel: {
74 | textStyle: {
75 | fontSize: number;
76 | };
77 | };
78 | };
79 | series: {
80 | name: string;
81 | data: any[];
82 | type: string;
83 | stack: string;
84 | symbolSize: number;
85 | lineStyle: {
86 | width: number;
87 | };
88 | areaStyle: {
89 | color: string;
90 | };
91 | }[];
92 | color: string[];
93 | };
94 | getCuredDeadChartOptions(orderedProvinceData: Array, orderedOverviewData: Array, area: string, path: Array): {
95 | title: {
96 | text: string;
97 | x: string;
98 | textStyle: {
99 | fontSize: number;
100 | };
101 | };
102 | legend: {
103 | top: number;
104 | orient: string;
105 | data: string[];
106 | itemWidth: number;
107 | itemHeight: number;
108 | textStyle: {
109 | fontSize: number;
110 | };
111 | };
112 | tooltip: {
113 | trigger: string;
114 | };
115 | grid: {
116 | bottom: string;
117 | };
118 | xAxis: {
119 | name: string;
120 | title: string;
121 | type: string;
122 | nameTextStyle: {
123 | fontSize: number;
124 | };
125 | nameGap: number;
126 | axisLabel: {
127 | textStyle: {
128 | fontSize: number;
129 | };
130 | formatter: (params: any) => string;
131 | };
132 | };
133 | yAxis: {
134 | name: string;
135 | nameTextStyle: {
136 | fontSize: number;
137 | };
138 | nameGap: number;
139 | axisLabel: {
140 | textStyle: {
141 | fontSize: number;
142 | };
143 | };
144 | };
145 | series: {
146 | name: string;
147 | data: any[];
148 | type: string;
149 | symbolSize: number;
150 | lineStyle: {
151 | width: number;
152 | };
153 | }[];
154 | color: string[];
155 | };
156 | getAllChartOptions(orderedProvinceData: Array, orderedOverviewData: Array, area: string, path: Array): {
157 | title: {
158 | text: string;
159 | x: string;
160 | textStyle: {
161 | fontSize: number;
162 | };
163 | };
164 | legend: {
165 | top: number;
166 | orient: string;
167 | data: string[];
168 | itemWidth: number;
169 | itemHeight: number;
170 | textStyle: {
171 | fontSize: number;
172 | };
173 | };
174 | grid: {
175 | bottom: string;
176 | };
177 | tooltip: {
178 | trigger: string;
179 | };
180 | xAxis: {
181 | name: string;
182 | type: string;
183 | nameTextStyle: {
184 | fontSize: number;
185 | };
186 | nameGap: number;
187 | axisLabel: {
188 | textStyle: {
189 | fontSize: number;
190 | };
191 | formatter: (params: any) => string;
192 | };
193 | };
194 | yAxis: {
195 | name: string;
196 | nameTextStyle: {
197 | fontSize: number;
198 | };
199 | nameGap: number;
200 | axisLabel: {
201 | textStyle: {
202 | fontSize: number;
203 | };
204 | };
205 | };
206 | series: ({
207 | name: string;
208 | data: any[];
209 | type: string;
210 | stack: string;
211 | symbolSize: number;
212 | lineStyle: {
213 | width: number;
214 | };
215 | areaStyle: {
216 | color: string;
217 | };
218 | } | {
219 | name: string;
220 | data: any[];
221 | type: string;
222 | symbolSize: number;
223 | lineStyle: {
224 | width: number;
225 | };
226 | stack?: undefined;
227 | areaStyle?: undefined;
228 | })[];
229 | color: string[];
230 | };
231 | chartsCount(): 1 | 0 | 2;
232 | componentDidMount(): void;
233 | render(): JSX.Element;
234 | }
235 | export {};
236 |
--------------------------------------------------------------------------------
/lib/types/src/components/virusMap/virusMap.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | declare type MapDataType = {
3 | [name: string]: any;
4 | };
5 | declare type STMapDataType = {
6 | timeline: number[];
7 | data: {
8 | [timestamp: number]: MapDataType;
9 | };
10 | };
11 | interface Props {
12 | name: string;
13 | data: MapDataType | STMapDataType;
14 | breaks: number[];
15 | chartData: any;
16 | chartPath: Array;
17 | currentChartArea: string;
18 | type: string;
19 | chartOnClickCallBack: Function;
20 | }
21 | export declare class VirusMap extends React.Component {
22 | constructor(props: Props);
23 | static defaultProps: {
24 | name: string;
25 | data: {};
26 | breaks: number[];
27 | chartData: {};
28 | chartPath: any[];
29 | currentChartArea: string;
30 | type: string;
31 | chartOnClickCallBack: (param: any, chart: any) => void;
32 | };
33 | state: {
34 | mapScale: number;
35 | chartArea: string;
36 | };
37 | private genBasicVisualMap;
38 | private baseOptions;
39 | private overrides;
40 | getChartOptions(data: MapDataType, options?: any): any;
41 | getSTChartOptions(data: STMapDataType, options?: any): {
42 | baseOption: any;
43 | options: {
44 | tooltip: {
45 | trigger: string;
46 | triggerOn: string;
47 | confine: string;
48 | formatter: (params: any) => string;
49 | };
50 | series: {
51 | data: {
52 | name: string;
53 | value: any;
54 | }[];
55 | }[];
56 | }[];
57 | };
58 | private isTimelineData;
59 | render(): JSX.Element;
60 | }
61 | export {};
62 |
--------------------------------------------------------------------------------
/lib/types/src/data/map/district.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: {
2 | "中国": string;
3 | "世界": string;
4 | "安徽": string;
5 | "澳门": string;
6 | "北京": string;
7 | "重庆": string;
8 | "福建": string;
9 | "甘肃": string;
10 | "广东": string;
11 | "广西": string;
12 | "贵州": string;
13 | "海南": string;
14 | "河北": string;
15 | "黑龙江": string;
16 | "河南": string;
17 | "湖北": string;
18 | "湖南": string;
19 | "江苏": string;
20 | "江西": string;
21 | "吉林": string;
22 | "辽宁": string;
23 | "内蒙古": string;
24 | "宁夏": string;
25 | "青海": string;
26 | "山东": string;
27 | "上海": string;
28 | "山西": string;
29 | "陕西": string;
30 | "四川": string;
31 | "天津": string;
32 | "香港": string;
33 | "新疆": string;
34 | "西藏": string;
35 | "云南": string;
36 | "浙江": string;
37 | };
38 | export default _default;
39 |
--------------------------------------------------------------------------------
/lib/types/src/data/map/provinces.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: string[];
2 | export default _default;
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wuhan2020-mapviz",
3 | "version": "1.0.11",
4 | "description": "map visualization components for wuhan 2020",
5 | "repository": "wuhan2020/map-viz",
6 | "license": "MIT",
7 | "main": "lib/components.js",
8 | "files": [
9 | "lib",
10 | "index.d.ts"
11 | ],
12 | "keywords": [
13 | "wuhan2020"
14 | ],
15 | "author": "shadowings-zy",
16 | "peerDependencies": {
17 | "react": "^16.12.0",
18 | "react-dom": "^16.12.0",
19 | "echarts": "^4.6.0"
20 | },
21 | "dependencies": {
22 | "echarts": "^4.6.0",
23 | "react": "^16.12.0",
24 | "react-dom": "^16.12.0",
25 | "react-slider-light": "^4.2.0"
26 | },
27 | "devDependencies": {
28 | "@babel/cli": "^7.0.0",
29 | "@babel/core": "^7.0.0",
30 | "@babel/plugin-proposal-class-properties": "^7.0.0",
31 | "@babel/plugin-proposal-decorators": "^7.0.0",
32 | "@babel/plugin-transform-modules-commonjs": "^7.0.0",
33 | "@babel/plugin-transform-runtime": "^7.0.0",
34 | "@babel/polyfill": "^7.8.3",
35 | "@babel/preset-env": "^7.0.0",
36 | "@babel/preset-react": "^7.0.0",
37 | "@testing-library/jest-dom": "^4.2.4",
38 | "@testing-library/react": "^9.3.2",
39 | "@testing-library/user-event": "^7.1.2",
40 | "@types/echarts": "^4.4.2",
41 | "@types/jest": "^24.0.0",
42 | "@types/node": "^12.0.0",
43 | "@types/react": "^16.9.19",
44 | "@types/react-dom": "^16.9.0",
45 | "awesome-typescript-loader": "^5.2.1",
46 | "babel-loader": "^8.0.0",
47 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
48 | "react-router-dom": "^5.1.2",
49 | "react-scripts": "3.3.1",
50 | "typescript": "~3.7.2",
51 | "webpack": "^4.26.0",
52 | "webpack-cli": "^3.1.2"
53 | },
54 | "scripts": {
55 | "start": "react-scripts start",
56 | "build": "react-scripts build",
57 | "test": "react-scripts test",
58 | "eject": "react-scripts eject",
59 | "lib": "webpack --config ./webpack.config.lib.js"
60 | },
61 | "eslintConfig": {
62 | "extends": "react-app"
63 | },
64 | "browserslist": {
65 | "production": [
66 | ">0.2%",
67 | "not dead",
68 | "not op_mini all"
69 | ],
70 | "development": [
71 | "last 1 chrome version",
72 | "last 1 firefox version",
73 | "last 1 safari version"
74 | ]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhan2020/map-viz/6bde1fecc8770805ea405bd16de21702ac62e31e/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhan2020/map-viz/6bde1fecc8770805ea405bd16de21702ac62e31e/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhan2020/map-viz/6bde1fecc8770805ea405bd16de21702ac62e31e/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | width: 100vw;
3 | height: 100vh;
4 | }
5 |
6 | .navigator {
7 | display: flex;
8 | flex-direction: row;
9 | }
10 |
11 | .item {
12 | margin: 10px 20px;
13 | }
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './App.css';
3 | import { HashRouter, Route, Switch } from 'react-router-dom';
4 | import { HierarchicalVirusMapDemo } from './pages/hierarchicalVirusMapDemo';
5 | import { InformationMapDemo } from './pages/informationMapDemo';
6 |
7 | export default class App extends React.Component<{}, {}> {
8 | render() {
9 | return (
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {/*
24 |
25 |
*/}
26 |
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/adapters/isaaclin.ts:
--------------------------------------------------------------------------------
1 | import long2short from './long2short'; // some city names are NOT short names so we also convert them here
2 |
3 | function convertStat(source: any): any {
4 | return {
5 | confirmed: source.confirmedCount,
6 | suspected: source.suspectedCount,
7 | cured: source.curedCount,
8 | dead: source.deadCount
9 | };
10 | }
11 |
12 | function convertCountry(source: any): any {
13 | let provinces = {};
14 |
15 | source.forEach(
16 | (p: { provinceShortName: string }) =>
17 | (provinces[p.provinceShortName] = convertProvince(p))
18 | );
19 |
20 | // currently we only support china
21 | return {
22 | name: '中国',
23 | confirmed: 0,
24 | suspected: 0,
25 | cured: 0,
26 | dead: 0,
27 | provinces
28 | };
29 | }
30 |
31 | function convertProvince(source: any): any {
32 | let cities = {};
33 | if (source.cities && source.cities.length > 0) {
34 | source.cities.forEach(
35 | (c: { cityName: string }) =>
36 | // 把省级的更新时间传入市级由于市级没有各自的数据更新时间
37 | (cities[long2short(c.cityName)] = convertCity(c, source.updateTime))
38 | );
39 | }
40 | return {
41 | name: source.provinceShortName,
42 | timestamp: source.updateTime,
43 | cities,
44 | ...convertStat(source)
45 | };
46 | }
47 |
48 | function convertCity(source: any, updateTime: any): any {
49 | return {
50 | name: long2short(source.cityName),
51 | timestamp: updateTime, // 使用传入的省级数据更新时间
52 | ...convertStat(source)
53 | };
54 | }
55 |
56 | function roundTime(t: number, resolution: number) {
57 | const offset = resolution >= 24 * 3600000 ? 8 * 3600000 : 0; // consider locale if resolution > 1 day
58 | return Math.floor((t + offset) / resolution) * resolution - offset;
59 | }
60 |
61 | function fillForward(series: any) {
62 | const all_ts = Object.keys(series).sort();
63 | all_ts.forEach((t, i) => {
64 | if (i < all_ts.length - 1) {
65 | Object.keys(series[t]).forEach(name => {
66 | const next_t = parseInt(all_ts[i + 1], 10);
67 | if (series[next_t][name] === undefined) {
68 | series[next_t][name] = series[t][name];
69 | }
70 | });
71 | }
72 | });
73 | }
74 |
75 | function convertProvincesSeries(source: any, resolution: number, shouldFillForward = false): any {
76 | let res: any = {};
77 | source
78 | .sort((item: any) => item.updateTime)
79 | .forEach((item: any) => {
80 | const t = roundTime(item.updateTime, resolution);
81 | if (res[t] === undefined) {
82 | res[t] = {};
83 | }
84 | const prov = convertProvince(item);
85 | res[t][prov.name] = prov;
86 | });
87 | if (shouldFillForward) {
88 | fillForward(res);
89 | }
90 | return res;
91 | }
92 |
93 | function extractCitiesSeries(series: any, name: string, resolution: number, shouldFillForward = false): any {
94 | let res: any = {};
95 | Object.values(series).forEach((provs: any) => {
96 | if (provs[name] !== undefined) {
97 | res[roundTime(provs[name].timestamp, resolution)] = provs[name].cities;
98 | }
99 | });
100 | if (shouldFillForward) {
101 | fillForward(res);
102 | }
103 | return res;
104 | }
105 |
106 | function convertCountrySeries(source: any, resolution: number): any {
107 | let res: any = {};
108 | source.forEach((item: any) => {
109 | const t = roundTime(item.updateTime, resolution);
110 | if (res[t] === undefined) {
111 | res[t] = {};
112 | }
113 | res[t] = item;
114 | });
115 | return res;
116 | }
117 |
118 | export {
119 | convertCountry,
120 | convertProvince,
121 | convertCity,
122 | convertProvincesSeries,
123 | convertCountrySeries,
124 | extractCitiesSeries
125 | };
126 |
--------------------------------------------------------------------------------
/src/adapters/long2short.ts:
--------------------------------------------------------------------------------
1 | // convert long city name to short
2 | const nations = [
3 | '仫佬族',
4 | '黎族',
5 | '土家族',
6 | '蒙古族',
7 | '羌族',
8 | '僳僳族',
9 | '哈尼族',
10 | '回族',
11 | '布朗族',
12 | '佤族',
13 | '哈萨克族',
14 | '藏族',
15 | '撒拉族',
16 | '畲族',
17 | '傣族',
18 | '维吾尔族',
19 | '毛南族',
20 | '高山族',
21 | '德昂族',
22 | '苗族',
23 | '仡佬族',
24 | '拉祜族',
25 | '保安族',
26 | '彝族',
27 | '锡伯族',
28 | '水族',
29 | '裕固族',
30 | '壮族',
31 | '阿昌族',
32 | '东乡族',
33 | '京族',
34 | '布依族',
35 | '普米族',
36 | '纳西族',
37 | '独龙族',
38 | '朝鲜族',
39 | '塔吉克族',
40 | '景颇族',
41 | '鄂伦春族',
42 | '满族',
43 | '怒族',
44 | '柯尔克孜族',
45 | '赫哲族',
46 | '侗族',
47 | '乌孜别克族',
48 | '土族',
49 | '门巴族',
50 | '瑶族',
51 | '俄罗斯族',
52 | '达斡尔族',
53 | '珞巴族',
54 | '白族',
55 | '鄂温克族',
56 | '塔塔尔族',
57 | '基诺族 '
58 | ];
59 |
60 | function long2short(name: string) {
61 | nations.forEach(n => (name = name.replace(n, '')));
62 | name = name.replace('自治', '');
63 | if (name.endsWith('林区')) return name;
64 | if (name.endsWith('区') || name.endsWith('市'))
65 | return name.slice(0, name.length - 1);
66 |
67 | return name;
68 | }
69 |
70 | export default long2short;
71 |
--------------------------------------------------------------------------------
/src/components/informationMap/README.md:
--------------------------------------------------------------------------------
1 | # React 地图信息可视化组件
2 |
3 | 本地图组件对百度地图API进行了封装,提供了开箱即用的百度地图接入组件。
4 | 本组件固定了地图上 Marker 的格式以及样式。
5 | @author: shadowingszy
6 |
7 | ## 传入参数及说明:
8 |
9 | mapOptions: 地图配置项,可参考百度地图 API 进行配置。(可参考/mock/information_map_general_mock_data.js)
10 |
11 | mapOptions 入参样例:
12 |
13 | ```js
14 | {
15 | initPoint: [116.350658, 39.938285], // 地图初始化时中心点坐标
16 | zoom: 6, // 地图初始化时缩放比例
17 | markers: [ // 地图上标记点数组
18 | {
19 | name: '华中科技大学同济医学院附属协和医院',
20 | //目前支持的 type 有:hospital,hotel,others 三种
21 | type: "hospital",
22 | // 坐标
23 | coord: [114.281196, 30.590103],
24 | // 有则填,无则不填
25 | url?: 'https://mp.weixin.qq.com/s/geO3CCd0_8B3L-r_xlBbZQ',
26 | // 一些属性,可能会不同
27 | metadata: []
28 | }
29 | ]
30 | }
31 | ```
32 |
33 | 其中 metadata 的 type:
34 |
35 | ```js
36 | {
37 | key: string; // 一个关键词
38 | label?: string; // 如果有label,这个作为显示词,覆盖key
39 | value: string|number|InquiryMeta[], // 具体值
40 | }
41 | // 例子
42 | metadata: [
43 | { // 酒店
44 | key: "request",
45 | label: "物资需求",
46 | value: [
47 | ['普通医用口罩', 10000],
48 | ['医用外科口罩', true]
49 | ]
50 | }, { // 地理位置
51 | key: "address",
52 | value: '湖北省武汉市江汉区解放大道1277号华中科技大学同济医学院附属协和医院总务处',
53 | label: "邮寄地址",
54 | }, { // 酒店容量
55 | key: "capability",
56 | value: 100,
57 | label: "容量",
58 | }
59 | ]
60 | ```
61 |
62 | ## 使用样例
63 |
64 | ```js
65 |
66 | ```
67 |
--------------------------------------------------------------------------------
/src/components/informationMap/baiduMap.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * React 百度地图地图可视化通用组件,接入百度地图API
3 | * 本地图组件为百度地图定制化开发提供了最高的自由度
4 | * @author: shadowingszy
5 | *
6 | * 传入props说明:
7 | * key: 百度地图API的密钥,默认为我自己申请的百度地图API密钥,建议自己申请
8 | * mapUrl: 地图json文件地址
9 | */
10 | import React from 'react';
11 |
12 | type Marker = {
13 | point: Array;
14 | icon: string;
15 | labelText: string;
16 | labelStyle: Object;
17 | infoWindowTitle: string;
18 | infoWindowContent: string;
19 | }
20 |
21 | type MapOptions = {
22 | initPoint: Array;
23 | zoom: number;
24 | markerArray: Array;
25 | }
26 |
27 | type Props = {
28 | baiduMapKey: string;
29 | mapOptions: MapOptions;
30 | }
31 |
32 | export class BaiduMap extends React.Component {
33 |
34 | static defaultProps = {
35 | baiduMapKey: '4CsWt6kSluEoQFXxh8GlqoFDrctcoAIo',
36 | mapOptions: {}
37 | }
38 |
39 | mapId = this.generateMapId();
40 |
41 | /**
42 | * 使用随机数+date生成当前组件的唯一ID
43 | */
44 | generateMapId() {
45 | const random = Math.floor(Math.random() * 100);
46 | const dateStr = new Date().getTime();
47 | return 'map' + dateStr.toString() + random.toString();
48 | }
49 |
50 | componentDidMount() {
51 | setTimeout(() => {
52 | const script = document.createElement('script');
53 | script.src =
54 | 'http://api.map.baidu.com/api?v=2.0&ak=' +
55 | this.props.baiduMapKey +
56 | '&callback=initialize';
57 | document.body.appendChild(script);
58 | (window as any).initialize = () => {
59 | const map = new (window as any).BMap.Map(this.mapId);
60 | let point = new (window as any).BMap.Point(
61 | this.props.mapOptions.initPoint[0],
62 | this.props.mapOptions.initPoint[1]
63 | );
64 | map.centerAndZoom(point, this.props.mapOptions.zoom);
65 | map.enableScrollWheelZoom(true);
66 | map.addControl(new (window as any).BMap.NavigationControl());
67 |
68 | for (const item of this.props.mapOptions.markerArray) {
69 | const position = new (window as any).BMap.Point(
70 | item.point[0],
71 | item.point[1]
72 | );
73 | const myIcon = new (window as any).BMap.Icon(
74 | item.icon,
75 | new (window as any).BMap.Size(25, 25),
76 | {
77 | offset: new (window as any).BMap.Size(10, 25)
78 | }
79 | );
80 | const marker = new (window as any).BMap.Marker(position, {
81 | icon: myIcon
82 | });
83 |
84 | marker.addEventListener('click', function() {
85 | var infoWindow = new (window as any).BMap.InfoWindow(
86 | item.infoWindowContent,
87 | {
88 | title: item.infoWindowTitle
89 | }
90 | );
91 | marker.openInfoWindow(infoWindow, map.getCenter());
92 | });
93 |
94 | map.addOverlay(marker);
95 | }
96 | };
97 | }, 0);
98 | }
99 |
100 | public render() {
101 | return (
102 |
103 | );
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/informationMap/informationMap.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * React 地理位置信息可视化组件
3 | * 在 BaiduMap 组件的基础上定制了不同种类的信息及表现方式
4 | * @author: shadowingszy
5 | *
6 | */
7 |
8 | import React from 'react';
9 | import { BaiduMap } from './baiduMap';
10 | import markerIcon from './marker';
11 |
12 | type InquiryMeta = [string, number | string | boolean];
13 |
14 | export interface Metadata {
15 | key: string;
16 | label?: string;
17 | value: string | number | InquiryMeta[];
18 | }
19 |
20 | export interface GeoData {
21 | type: 'hospital' | 'hotel' | 'other';
22 | name: string;
23 | url: string;
24 | coord: [number, number];
25 | metadata: Metadata[];
26 | }
27 |
28 | type Props = {
29 | initPoint: [number, number];
30 | zoom: number;
31 | markers: GeoData[];
32 | }
33 |
34 | class InformationMap extends React.Component {
35 |
36 | static defaultProps = {
37 | initPoint: [0, 0],
38 | zoom: 1,
39 | markers: []
40 | }
41 |
42 | generateDefaultInfoWindowContent(content: string) {
43 | return `
44 | ${content}
45 | `;
46 | }
47 |
48 | generateTooltipContent(metas: Metadata[], url: string) {
49 | let tooltip = '';
50 | metas.forEach((meta: Metadata) => {
51 | let content = meta.value;
52 |
53 | if (
54 | meta.value &&
55 | typeof meta.value !== 'string' &&
56 | typeof meta.value !== 'number'
57 | ) {
58 | content = '';
59 | (meta.value as InquiryMeta[]).forEach(item => {
60 | const filterItem = item
61 | .filter(i => typeof i === 'string' || typeof i === 'number')
62 | .map(i => `${i}`);
63 | if (filterItem.length > 0) {
64 | content += `${filterItem.join(' ')}
`;
65 | }
66 | });
67 | }
68 | if (content) {
69 | tooltip += `${meta.label ||
70 | meta.key}${content}
`;
71 | }
72 | });
73 | if (url) {
74 | tooltip += ``;
75 | }
76 | return tooltip;
77 | }
78 |
79 | generateOptions(item: GeoData, iconUrl: string) {
80 | let marker = {
81 | point: item.coord,
82 | icon: iconUrl,
83 | infoWindowTitle: item.name,
84 | infoWindowContent: this.generateTooltipContent(item.metadata, item.url)
85 | };
86 | return marker;
87 | }
88 |
89 | generateMarker(markerArray: GeoData[]) {
90 | const output = [];
91 | let iconUrl = markerIcon.others;
92 | for (const item of markerArray) {
93 | if (item.type === 'hospital') {
94 | iconUrl = markerIcon.hospital;
95 | } else if (item.type === 'hotel') {
96 | iconUrl = markerIcon.hotel;
97 | } else {
98 | iconUrl = markerIcon.others;
99 | }
100 | output.push(this.generateOptions(item, iconUrl));
101 | }
102 | return output;
103 | }
104 |
105 | getMapOptions(initPoint: [number, number], zoom: number, markers: GeoData[]) {
106 | const output = {
107 | initPoint: initPoint,
108 | zoom: zoom,
109 | markerArray: []
110 | };
111 | let markerArray: any = this.generateMarker(markers);
112 | output.markerArray = output.markerArray.concat(markerArray);
113 | return output;
114 | }
115 |
116 | public render() {
117 | return (
118 |
126 | );
127 | }
128 | }
129 |
130 | export default InformationMap;
131 |
--------------------------------------------------------------------------------
/src/components/informationMap/marker.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | "hospital": "http://i2.tiimg.com/708620/f869afffd7f03d19.png",
3 | "hotel": "http://i2.tiimg.com/708620/ad73337602f2cc9b.png",
4 | "others": "http://i2.tiimg.com/708620/15e58afc32dfc7bf.png"
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/informationMap/style.css:
--------------------------------------------------------------------------------
1 | .info-label {
2 | white-space: pre;
3 | /*margin-top: 1px!important; */
4 | /*margin-bottom: 1px!important; */
5 | display: inline-block;
6 | box-sizing: border-box;
7 | -moz-box-sizing: border-box;
8 | -webkit-box-sizing: border-box;
9 | background-color: #ffa18c;
10 | font-weight: bold;
11 | }
--------------------------------------------------------------------------------
/src/components/virusMap/README.md:
--------------------------------------------------------------------------------
1 | # React 疫情地图可视化组件
2 |
3 | 本地图组件提供了完整的全国疫情查看功能,并提供增加聚焦到省显示市级数据与回到省级的功能。
4 | @author: shadowingszy,yarray
5 |
6 | ## 传入参数:
7 |
8 | - resolution: 时间精度。
9 | - data: echarts 中每个区域的疫情数据。
10 | - type: 三种不同的charts显示方式(可选值为: overview, pc, mobile)。
11 |
12 | ## 参数说明:
13 |
14 | 传入参数的数据格式为:
15 |
16 | ```typescript
17 | interface Props {
18 | resolution: number; // 地图放大的程度
19 | type: 'overview' | 'pc' | 'mobile'; // 三种不同的显示模式,分别对应总览(不支持下钻,无折线图)、pc端(支持下钻,折线图在地图右方)、移动端(支持下钻,折线图在地图下方)
20 | data: {
21 | provinceSeries: {
22 | [time: number]: ProvinceData
23 | },
24 | countrySeries: {
25 | [time: number]: CountryData
26 | }
27 | }; //echarts 中每个区域的疫情数据。
28 | }
29 |
30 | interface ProvinceData {
31 | name: string;
32 | timestamp: number;
33 | confirmed: number;
34 | suspected: number;
35 | cured: number;
36 | dead: number;
37 | cities: {
38 | [name: string]: PatientStatData
39 | }
40 | }
41 |
42 | interface PatientStatData {
43 | name: string,
44 | timestamp: 1580699799130,
45 | confirmed: number;
46 | suspected: number;
47 | cured: number;
48 | dead: number;
49 | }
50 |
51 | interface CountryData {
52 | confirmedCount: number;
53 | suspectedCount: number;
54 | curedCount: number;
55 | deadCount: number;
56 | seriousCount: number;
57 | updateTime: number;
58 | }
59 | ```
60 |
61 | data属性数据样例:
62 |
63 | ```json
64 | {
65 | "provincesSeries":{
66 | "1580659200000": {
67 | "name": "福建",
68 | "timestamp": 1580699799130,
69 | "cities": {
70 | "福州": {
71 | "name": "福州",
72 | "timestamp": 1580699799130,
73 | "confirmed": 47,
74 | "suspected": 0,
75 | "cured": 0,
76 | "dead": 0,
77 | },
78 | "莆田": {
79 | "name": "莆田",
80 | "timestamp": 1580699799130,
81 | "confirmed": 47,
82 | "suspected": 0,
83 | "cured": 0,
84 | "dead": 0,
85 | }
86 | },
87 | "confirmed": 179,
88 | "suspected": 0,
89 | "cured": 0,
90 | "dead": 0,
91 | },
92 | "1580572800000": {
93 | "name": "福建",
94 | "timestamp": 1580699799130,
95 | "cities": {
96 | "福州": {
97 | "name": "福州",
98 | "timestamp": 1580699799130,
99 | "confirmed": 97,
100 | "suspected": 0,
101 | "cured": 0,
102 | "dead": 0,
103 | },
104 | "莆田": {
105 | "name": "莆田",
106 | "timestamp": 1580699799130,
107 | "confirmed": 107,
108 | "suspected": 0,
109 | "cured": 0,
110 | "dead": 0,
111 | }
112 | },
113 | "confirmed": 219,
114 | "suspected": 0,
115 | "cured": 0,
116 | "dead": 0,
117 | }
118 | },
119 | "countrySeries": {
120 | "1580659200000": {
121 | "confirmedCount": 14490,
122 | "suspectedCount": 19544,
123 | "curedCount": 434,
124 | "deadCount": 304,
125 | "seriousCount": 0,
126 | "updateTime": 1580659262067
127 | },
128 | "1580572800000": {
129 | "confirmedCount": 14490,
130 | "suspectedCount": 19544,
131 | "curedCount": 434,
132 | "deadCount": 304,
133 | "seriousCount": 0,
134 | "updateTime": 1580572800000
135 | },
136 | }
137 | }
138 | ```
139 |
140 | ## 使用样例
141 |
142 | ```js
143 |
144 |
145 |
146 | ```
147 |
--------------------------------------------------------------------------------
/src/components/virusMap/echartsMap.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * React 地理热力图可视化通用组件
3 | * 本地图组件为地理热力图定制化开发提供了最高的自由度
4 | * @author: shadowingszy
5 | *
6 | * 传入props说明:
7 | * mapUrl: 地图json文件地址
8 | * chartOptions: echarts中的所有options
9 | * mapName: 地图名称
10 | * mobile: 是否为手机端
11 | * chartOnClickCallBack: 点击地图后的回调函数
12 | */
13 |
14 | import React from 'react';
15 | import echarts from 'echarts';
16 | import long2short from '../../adapters/long2short';
17 |
18 | interface Props {
19 | mapUrl: string;
20 | chartOptions: any;
21 | mapName: string;
22 | mobile: boolean;
23 | chartOnClickCallBack: Function;
24 | }
25 |
26 | export class EchartsMap extends React.Component {
27 |
28 | static defaultProps = {
29 | mapUrl: '',
30 | chartOptions: {},
31 | mapName: '',
32 | mobile: true,
33 | chartOnClickCallBack: (param: any, chart: any) => {
34 | console.log(param, chart);
35 | }
36 | }
37 |
38 | chartId: string = this.generateChartId();
39 | chart: any = null;
40 |
41 | /**
42 | * 使用随机数+date生成当前组件的唯一ID
43 | */
44 | generateChartId() {
45 | const random = Math.floor(Math.random() * 100);
46 | const dateStr = new Date().getTime();
47 | return 'map' + random.toString() + dateStr.toString();
48 | }
49 |
50 | componentDidMount() {
51 | const { mapUrl, mapName, chartOptions, chartOnClickCallBack } = this.props;
52 | this.updateMap(mapUrl, mapName, chartOptions, chartOnClickCallBack);
53 | }
54 |
55 | updateMap(mapUrl: string, mapName: string, chartOptions: any, chartOnClickCallBack: Function) {
56 | fetch(mapUrl)
57 | .then(response => response.json())
58 | .then(data => {
59 | if (!document.getElementById(this.chartId)) {
60 | return;
61 | }
62 | data.features.forEach(
63 | (f: { properties: { name: string } }) =>
64 | (f.properties.name = long2short(f.properties.name))
65 | );
66 | echarts.registerMap(mapName, data);
67 | if (!this.chart) {
68 | let previousClick = {
69 | times: 0,
70 | name: ''
71 | }
72 |
73 | this.chart = echarts.init((document.getElementById(this.chartId)) as HTMLDivElement);
74 | this.chart.on('click', 'series', (params: any) => {
75 | if (this.props.mobile) {
76 | if (previousClick.name === params.name && previousClick.times === 1) {
77 | chartOnClickCallBack(params, this.chart);
78 | previousClick.times = 0;
79 | previousClick.name = '';
80 | } else {
81 | previousClick.times = 1;
82 | previousClick.name = params.name;
83 | }
84 | } else {
85 | chartOnClickCallBack(params, this.chart);
86 | }
87 | });
88 | this.chart.on('click', 'timeline', (params: { dataIndex: any; }) => {
89 | this.chart.dispatchAction({
90 | type: 'timelineChange',
91 | // index of time point
92 | currentIndex: chartOptions.baseOption.timeline.data.findIndex(
93 | (d: any) => d === params.dataIndex
94 | )
95 | });
96 | });
97 | }
98 | this.chart.setOption(chartOptions);
99 |
100 | let originFunction = (window as any).onresize;
101 | window.onresize = () => {
102 | originFunction();
103 | this.chart.resize();
104 | };
105 | })
106 | .catch(e => console.log('加载疫情地图出现错误!', e));
107 | }
108 |
109 | public render() {
110 | if (this.chart) {
111 | const { mapUrl, mapName, chartOptions, chartOnClickCallBack } = this.props;
112 | this.updateMap(mapUrl, mapName, chartOptions, chartOnClickCallBack);
113 | }
114 |
115 | return (
116 |
117 | );
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/components/virusMap/hierarchicalVirusMap.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * React 分层疫情地图组件
3 | * 在 VirusMap 基础上,增加聚焦到省显示市级数据与回到省级的功能
4 | * @author: yarray, shadowingszy
5 | *
6 | * 传入参数说明:
7 | * data: 地图数据
8 | * resolution: 时间精度
9 | * type: overview, pc, mobile 三种不同的charts显示方式
10 | */
11 |
12 | import React from 'react';
13 | import { VirusMap } from './virusMap';
14 | import { extractCitiesSeries } from '../../adapters/isaaclin';
15 |
16 | type MapDataType = {
17 | [name: string]: any;
18 | };
19 |
20 | type STMapDataType = {
21 | timeline: number[];
22 | data: { [timestamp: number]: MapDataType };
23 | }; // spatio-temporal data
24 |
25 | type Props = {
26 | data: any;
27 | resolution: number;
28 | type: 'overview' | 'pc' | 'mobile';
29 | };
30 |
31 | type State = {
32 | path: string[];
33 | currentChartArea: string;
34 | };
35 |
36 | class HierarchicalVirusMap extends React.Component> {
37 | constructor(props: Props) {
38 | super(props);
39 | this.state = {
40 | path: [],
41 | currentChartArea: '中国'
42 | };
43 | }
44 |
45 | static defaultProps = {
46 | data: {
47 | provincesSeries: {},
48 | countrySeries: {}
49 | },
50 | resolution: 3600000,
51 | type: 'pc',
52 | detail: true
53 | };
54 |
55 | navigateDown(params: any) {
56 | // enter province view
57 | this.setState({
58 | path:
59 | params.name && this.state.path.length < 1
60 | ? [...this.state.path, params.name]
61 | : this.state.path,
62 | currentChartArea: params.name
63 | });
64 | }
65 |
66 | navigateUp() {
67 | // back to country view
68 | if (this.state.path.length > 0) {
69 | this.setState({
70 | path: this.state.path.slice(0, this.state.path.length - 1),
71 | currentChartArea: '中国'
72 | });
73 | }
74 | }
75 |
76 | getVirusMapConfig(path: any, data: any, resolution: any) {
77 | let name = '中国';
78 |
79 | let dataOnMap: STMapDataType = {
80 | timeline: [],
81 | data: {}
82 | };
83 | if (path.length === 0) {
84 | dataOnMap = {
85 | timeline: Object.keys(data as any)
86 | .map(t => parseInt(t, 10))
87 | .sort(),
88 | data
89 | };
90 | } else if (path.length === 1) {
91 | name = path[0];
92 | const citiesSeries = extractCitiesSeries(data, name, resolution, true);
93 | dataOnMap = {
94 | timeline: Object.keys(citiesSeries)
95 | .map(t => parseInt(t, 10))
96 | .sort(),
97 | data: citiesSeries
98 | };
99 | }
100 | return {
101 | name,
102 | data: dataOnMap,
103 | navigateDown: this.navigateDown.bind(this)
104 | };
105 | }
106 |
107 | autoBreaks(values: number[]) {
108 | const base = [1, 10, 50, 100, 500, 1000];
109 | const k =
110 | (Math.floor(Math.max(...values.filter(v => v !== undefined)) / 5 / 500) *
111 | 500) /
112 | Math.max(...base);
113 | let res = base.map(b => k * b);
114 | res[0] = 1;
115 | return res;
116 | }
117 |
118 | public render() {
119 | const { data, resolution, type } = this.props;
120 | const { path, currentChartArea } = this.state;
121 |
122 | const config = this.getVirusMapConfig(
123 | path,
124 | data.provincesSeries,
125 | resolution
126 | );
127 |
128 | const current =
129 | data.provincesSeries[
130 | Math.max(...Object.keys(data.provincesSeries).map(t => parseInt(t, 10)))
131 | ];
132 |
133 | return (
134 |
135 | prov.confirmed)
140 | )} // use current province values to calculate viable mapping breaks
141 | chartData={data}
142 | chartPath={path}
143 | currentChartArea={currentChartArea}
144 | chartOnClickCallBack={
145 | type === 'overview' ? () => {} : config.navigateDown
146 | }
147 | type={type}
148 | // onDblClick={this.navigateUp.bind(this)}
149 | />
150 |
170 |
171 | );
172 | }
173 | }
174 |
175 | export default HierarchicalVirusMap;
176 |
--------------------------------------------------------------------------------
/src/components/virusMap/reactEcharts.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * React Echarts可视化通用组件
3 | * 本地图组件为使用echarts进行开发提供了基础组件
4 | * @author: shadowingszy
5 | *
6 | * 传入props说明:
7 | * chartOptions: echarts中的所有options。
8 | */
9 |
10 | import React from 'react';
11 | import echarts from 'echarts';
12 |
13 | type Props = {
14 | chartOptions: any;
15 | }
16 |
17 | export class ReactEcharts extends React.Component {
18 | static defaultProps = {
19 | chartOptions: {}
20 | }
21 |
22 | chartId: string = this.generateChartId();
23 | chart: any = null;
24 |
25 | /**
26 | * 使用随机数+date生成当前组件的唯一ID
27 | */
28 | generateChartId() {
29 | const random = Math.floor(Math.random() * 100);
30 | const dateStr = new Date().getTime();
31 | return 'map' + random.toString() + dateStr.toString();
32 | }
33 |
34 | componentDidMount() {
35 | setTimeout(() => {
36 | if (document.getElementById(this.chartId)) {
37 | this.chart = echarts.init((document.getElementById(this.chartId) as HTMLDivElement));
38 | // console.log(this.chart);
39 | this.chart.setOption(this.props.chartOptions);
40 | let onResizeFunction = (window as any).onresize;
41 | window.onresize = () => {
42 | if (onResizeFunction) {
43 | onResizeFunction();
44 | }
45 | this.chart.resize();
46 | };
47 | }
48 |
49 | }, 0);
50 | }
51 |
52 | getEchartsInstance = () => echarts.getInstanceByDom(document.getElementById(this.chartId) as HTMLDivElement)||
53 | echarts.init(document.getElementById(this.chartId) as HTMLDivElement);
54 |
55 | public render() {
56 | if (this.chart) {
57 | this.chart.setOption(this.props.chartOptions, false, false);
58 | }
59 | // console.log(this.chart, this.props);
60 | return (
61 |
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/virusMap/style.css:
--------------------------------------------------------------------------------
1 | .overview-virus-map {
2 | display: flex;
3 | flex-direction: row;
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
8 | .pc-virus-map {
9 | display: flex;
10 | flex-direction: row;
11 | width: 100%;
12 | height: 100%;
13 | }
14 |
15 | .mobile-virus-map {
16 | display: flex;
17 | flex-direction: column;
18 | width: 100%;
19 | height: 100%;
20 | }
21 |
22 | .overview-echarts-map {
23 | width: 100%;
24 | height: 100%;
25 | }
26 |
27 | .pc-echarts-map {
28 | width: 65%;
29 | height: 100%;
30 | }
31 |
32 | .mobile-echarts-map {
33 | width: 100%;
34 | height: 50%;
35 | }
36 |
37 | .overview-virus-chart {
38 | display: none;
39 | }
40 |
41 | .pc-virus-chart {
42 | width: 35%;
43 | height: 100%;
44 | }
45 |
46 | .mobile-virus-chart {
47 | width: 100%;
48 | height: 50%;
49 | }
50 |
51 | #tooltip-detail {
52 | font-size: 10px;
53 | }
--------------------------------------------------------------------------------
/src/components/virusMap/virusChart.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * React 疫情数据折线图可视化组件
3 | * 本组件使用stack line chart和line chart展现信息
4 | * @author: shadowingszy
5 | *
6 | * 传入props说明:
7 | * data: 各省市或国家数据
8 | * area: 当前选中的国家或省市
9 | * path: 点击地图路径
10 | */
11 |
12 | import React from 'react';
13 | import { ReactEcharts } from './reactEcharts';
14 | import provinceName from '../../data/map/provinces';
15 | import echarts from 'echarts';
16 |
17 | type Props = {
18 | data: any;
19 | area: string;
20 | type: string;
21 | path: Array;
22 | }
23 |
24 | type State = {
25 | echartOptions: any;
26 | }
27 |
28 | const TITLE_SIZE = 12;
29 | const LEGEND_TOP = 20;
30 | const ITEM_WIDTH = 18;
31 | const AXIS_SIZE = 9;
32 | const CONTENT_SIZE = 7;
33 | const LINE_WIDTH = 5;
34 | const SYMOBL_SIZE = 10;
35 |
36 | const MOBILE_TITLE_SIZE = 15;
37 | const MOBILE_LEGEND_TOP = 35;
38 | const MOBILE_ITEM_WIDTH = 30;
39 | const MOBILE_AXIS_SIZE = 9;
40 | const MOBILE_CONTENT_SIZE = 7;
41 | const MOBILE_LINE_WIDTH = 1;
42 | const MOBILE_SYMOBL_SIZE = 3;
43 |
44 | const fixChartFontSize = (baseFontSize: number) => {
45 | const isPC =
46 | (window.innerWidth ||
47 | document.documentElement.clientWidth ||
48 | document.body.clientWidth) >
49 | (window.innerHeight ||
50 | document.documentElement.clientHeight ||
51 | document.body.clientHeight) *
52 | 0.8;
53 |
54 | if (isPC) {
55 | return (
56 | (baseFontSize *
57 | (window.innerWidth ||
58 | document.documentElement.clientWidth ||
59 | document.body.clientWidth)) /
60 | 1000
61 | );
62 | } else {
63 | return (
64 | (baseFontSize *
65 | (window.innerWidth ||
66 | document.documentElement.clientWidth ||
67 | document.body.clientWidth)) /
68 | 500
69 | );
70 | }
71 | }
72 |
73 | export class VirusChart extends React.Component> {
74 | getConfirmedSuspectChart :any
75 | curedDeadChart : any
76 |
77 | static defaultProps = {
78 | data: {
79 | provincesSeries: {},
80 | countrySeries: {}
81 | },
82 | area: '',
83 | path: ''
84 | }
85 |
86 | public getOrderedTimeData(data: any) {
87 | let output = [];
88 | for (const property in data) {
89 | data[property].date = parseInt(property);
90 | output.push(data[property]);
91 | }
92 | output.sort((a, b) => {
93 | return a.date - b.date;
94 | });
95 | return output;
96 | }
97 |
98 | public getData(
99 | orderedProvinceData: Array,
100 | orderedOverviewData: Array,
101 | area: string,
102 | path: Array
103 | ) {
104 | let confirmedData = [];
105 | let suspectedData = [];
106 | let curedData = [];
107 | let deadData = [];
108 |
109 | if (path.length === 0 && area === '中国') {
110 | for (const item of orderedOverviewData) {
111 | confirmedData.push([item.date, item.confirmedCount]);
112 | suspectedData.push([item.date, item.suspectedCount]);
113 | curedData.push([item.date, item.curedCount]);
114 | deadData.push([item.date, item.deadCount]);
115 | }
116 | } else if (path.length === 1 && provinceName.indexOf(area) !== -1) {
117 | for (const item of orderedProvinceData) {
118 | confirmedData.push([item.date, item[area] ? item[area].confirmed : 0]);
119 | suspectedData.push([item.date, item[area] ? item[area].suspected : 0]);
120 | curedData.push([item.date, item[area] ? item[area].cured : 0]);
121 | deadData.push([item.date, item[area] ? item[area].dead : 0]);
122 | }
123 | } else if (path.length === 1 && provinceName.indexOf(area) === -1) {
124 | for (const item of orderedProvinceData) {
125 | confirmedData.push([
126 | item.date,
127 | item[path[0]]
128 | ? item[path[0]].cities[area]
129 | ? item[path[0]].cities[area].confirmed
130 | : 0
131 | : 0
132 | ]);
133 | suspectedData.push([
134 | item.date,
135 | item[path[0]]
136 | ? item[path[0]].cities[area]
137 | ? item[path[0]].cities[area].suspected
138 | : 0
139 | : 0
140 | ]);
141 | curedData.push([
142 | item.date,
143 | item[path[0]]
144 | ? item[path[0]].cities[area]
145 | ? item[path[0]].cities[area].cured
146 | : 0
147 | : 0
148 | ]);
149 | deadData.push([
150 | item.date,
151 | item[path[0]]
152 | ? item[path[0]].cities[area]
153 | ? item[path[0]].cities[area].dead
154 | : 0
155 | : 0
156 | ]);
157 | }
158 | }
159 |
160 | return {
161 | confirmedData,
162 | suspectedData,
163 | curedData,
164 | deadData
165 | };
166 | }
167 |
168 | public getConfirmedSuspectChartOptions(
169 | orderedProvinceData: Array,
170 | orderedOverviewData: Array,
171 | area: string,
172 | path: Array
173 | ) {
174 | const { confirmedData, suspectedData } = this.getData(
175 | orderedProvinceData,
176 | orderedOverviewData,
177 | area,
178 | path
179 | );
180 |
181 | return {
182 | title: {
183 | text: area + '疫情统计',
184 | x: 'center',
185 | textStyle: {
186 | fontSize: fixChartFontSize(TITLE_SIZE)
187 | }
188 | },
189 | legend: {
190 | top: fixChartFontSize(LEGEND_TOP),
191 | orient: 'horizontal',
192 | data: ['确诊', '疑似'],
193 | itemWidth: fixChartFontSize(ITEM_WIDTH),
194 | itemHeight: fixChartFontSize(ITEM_WIDTH / 2),
195 | textStyle: {
196 | fontSize: fixChartFontSize(AXIS_SIZE)
197 | }
198 | },
199 | grid: {
200 | bottom: '11%'
201 | },
202 | tooltip: {
203 | trigger: 'axis'
204 | },
205 | xAxis: {
206 | name: '时间',
207 | type: 'time',
208 | nameTextStyle: {
209 | fontSize: fixChartFontSize(AXIS_SIZE)
210 | },
211 | nameGap: 5,
212 | axisLabel: {
213 | textStyle: {
214 | fontSize: fixChartFontSize(CONTENT_SIZE)
215 | },
216 | formatter: function (params: any) {
217 | const date = new Date(params);
218 | return date.getMonth() + 1 + '/' + date.getDate();
219 | }
220 | }
221 | },
222 | yAxis: {
223 | name: '数量',
224 | nameTextStyle: {
225 | fontSize: fixChartFontSize(AXIS_SIZE)
226 | },
227 | nameGap: 10,
228 | axisLabel: {
229 | textStyle: {
230 | fontSize: fixChartFontSize(CONTENT_SIZE)
231 | }
232 | }
233 | },
234 | series: [
235 | {
236 | name: '确诊',
237 | data: confirmedData,
238 | type: 'line',
239 | stack: '总量',
240 | symbolSize: SYMOBL_SIZE,
241 | lineStyle: { width: LINE_WIDTH },
242 | areaStyle: { color: '#f6bdcd' }
243 | },
244 | {
245 | name: '疑似',
246 | data: suspectedData,
247 | type: 'line',
248 | stack: '总量',
249 | symbolSize: SYMOBL_SIZE,
250 | lineStyle: { width: LINE_WIDTH },
251 | areaStyle: { color: '#f9e4ba' }
252 | }
253 | ],
254 | color: ['#c22b49', '#cca42d']
255 | };
256 | }
257 |
258 | public getCuredDeadChartOptions(
259 | orderedProvinceData: Array,
260 | orderedOverviewData: Array,
261 | area: string,
262 | path: Array
263 | ) {
264 | const { curedData, deadData } = this.getData(
265 | orderedProvinceData,
266 | orderedOverviewData,
267 | area,
268 | path
269 | );
270 |
271 | return {
272 | title: {
273 | text: area + '疫情统计',
274 | x: 'center',
275 | textStyle: {
276 | fontSize: fixChartFontSize(TITLE_SIZE)
277 | }
278 | },
279 | legend: {
280 | top: fixChartFontSize(LEGEND_TOP),
281 | orient: 'horizontal',
282 | data: ['治愈', '死亡'],
283 | itemWidth: fixChartFontSize(ITEM_WIDTH),
284 | itemHeight: fixChartFontSize(ITEM_WIDTH / 2),
285 | textStyle: {
286 | fontSize: fixChartFontSize(AXIS_SIZE)
287 | }
288 | },
289 | tooltip: {
290 | trigger: 'axis'
291 | },
292 | grid: {
293 | bottom: '11%'
294 | },
295 | xAxis: {
296 | name: '时间',
297 | title: '时间',
298 | type: 'time',
299 | nameTextStyle: {
300 | fontSize: fixChartFontSize(AXIS_SIZE)
301 | },
302 | nameGap: 5,
303 | axisLabel: {
304 | textStyle: {
305 | fontSize: fixChartFontSize(CONTENT_SIZE)
306 | },
307 | formatter: function (params: any) {
308 | const date = new Date(params);
309 | return date.getMonth() + 1 + '/' + date.getDate();
310 | }
311 | }
312 | },
313 | yAxis: {
314 | name: '数量',
315 | nameTextStyle: {
316 | fontSize: fixChartFontSize(AXIS_SIZE)
317 | },
318 | nameGap: 10,
319 | axisLabel: {
320 | textStyle: {
321 | fontSize: fixChartFontSize(CONTENT_SIZE)
322 | }
323 | }
324 | },
325 | series: [
326 | {
327 | name: '治愈',
328 | data: curedData,
329 | type: 'line',
330 | symbolSize: SYMOBL_SIZE,
331 | lineStyle: { width: LINE_WIDTH }
332 | },
333 | {
334 | name: '死亡',
335 | data: deadData,
336 | type: 'line',
337 | symbolSize: SYMOBL_SIZE,
338 | lineStyle: { width: LINE_WIDTH }
339 | }
340 | ],
341 | color: ['#2dce89', '#86868d']
342 | };
343 | }
344 |
345 | public getAllChartOptions(
346 | orderedProvinceData: Array,
347 | orderedOverviewData: Array,
348 | area: string,
349 | path: Array
350 | ) {
351 | const { confirmedData, suspectedData, curedData, deadData } = this.getData(
352 | orderedProvinceData,
353 | orderedOverviewData,
354 | area,
355 | path
356 | );
357 |
358 | return {
359 | title: {
360 | text: area + '疫情统计',
361 | x: 'center',
362 | textStyle: {
363 | fontSize: fixChartFontSize(MOBILE_TITLE_SIZE)
364 | }
365 | },
366 | legend: {
367 | top: fixChartFontSize(MOBILE_LEGEND_TOP),
368 | orient: 'horizontal',
369 | data: ['确诊', '疑似', '治愈', '死亡'],
370 | itemWidth: fixChartFontSize(MOBILE_ITEM_WIDTH),
371 | itemHeight: fixChartFontSize(MOBILE_ITEM_WIDTH / 2),
372 | textStyle: {
373 | fontSize: fixChartFontSize(MOBILE_AXIS_SIZE)
374 | }
375 | },
376 | grid: {
377 | bottom: '11%'
378 | },
379 | tooltip: {
380 | trigger: 'axis'
381 | },
382 | xAxis: {
383 | name: '时间',
384 | type: 'time',
385 | nameTextStyle: {
386 | fontSize: fixChartFontSize(MOBILE_AXIS_SIZE)
387 | },
388 | nameGap: 5,
389 | axisLabel: {
390 | textStyle: {
391 | fontSize: fixChartFontSize(MOBILE_CONTENT_SIZE)
392 | },
393 | formatter: function (params: any) {
394 | const date = new Date(params);
395 | return date.getMonth() + 1 + '/' + date.getDate();
396 | }
397 | }
398 | },
399 | yAxis: {
400 | name: '数量',
401 | nameTextStyle: {
402 | fontSize: fixChartFontSize(MOBILE_AXIS_SIZE)
403 | },
404 | nameGap: 10,
405 | axisLabel: {
406 | textStyle: {
407 | fontSize: fixChartFontSize(MOBILE_CONTENT_SIZE)
408 | }
409 | }
410 | },
411 | series: [
412 | {
413 | name: '确诊',
414 | data: confirmedData,
415 | type: 'line',
416 | stack: '总量',
417 | symbolSize: MOBILE_SYMOBL_SIZE,
418 | lineStyle: { width: MOBILE_LINE_WIDTH },
419 | areaStyle: { color: '#f6bdcd' }
420 | },
421 | {
422 | name: '疑似',
423 | data: suspectedData,
424 | type: 'line',
425 | stack: '总量',
426 | symbolSize: MOBILE_SYMOBL_SIZE,
427 | lineStyle: { width: MOBILE_LINE_WIDTH },
428 | areaStyle: { color: '#f9e4ba' }
429 | },
430 | {
431 | name: '治愈',
432 | data: curedData,
433 | type: 'line',
434 | symbolSize: MOBILE_SYMOBL_SIZE,
435 | lineStyle: { width: MOBILE_LINE_WIDTH }
436 | },
437 | {
438 | name: '死亡',
439 | data: deadData,
440 | type: 'line',
441 | symbolSize: MOBILE_SYMOBL_SIZE,
442 | lineStyle: { width: MOBILE_LINE_WIDTH }
443 | }
444 | ],
445 | color: ['#c22b49', '#cca42d', '#2dce89', '#86868d']
446 | };
447 | }
448 |
449 | chartsCount(){
450 | const { type } = this.props;
451 | switch (type){
452 | case 'pc':
453 | return 2;
454 | case 'mobile':
455 | return 1;
456 | case 'overview':
457 | default:
458 | return 0;
459 | }
460 | }
461 |
462 | componentDidMount() {
463 | if (this.chartsCount() === 2) {
464 | let confirmedSuspectChart = this.getConfirmedSuspectChart.getEchartsInstance();
465 | let curedDeadChart = this.curedDeadChart.getEchartsInstance();
466 | confirmedSuspectChart.group = 'virusChart';
467 | curedDeadChart.group = 'virusChart';
468 | echarts.connect('virusChart');
469 | }
470 | }
471 |
472 | public render() {
473 | const { data, area, path } = this.props;
474 | const orderedProvincesData = this.getOrderedTimeData(data.provincesSeries);
475 | const orderedCountryData = this.getOrderedTimeData(data.countrySeries);
476 |
477 | if (this.chartsCount() === 1) {
478 | return (
479 |
480 |
488 |
489 | );
490 | } else if (this.chartsCount() === 2){
491 | return (
492 |
500 |
501 | { this.getConfirmedSuspectChart = e; }}
503 | chartOptions={this.getConfirmedSuspectChartOptions(
504 | orderedProvincesData,
505 | orderedCountryData,
506 | area,
507 | path
508 | )}
509 | />
510 |
511 |
512 |
513 | { this.curedDeadChart = e; }}
515 | chartOptions={this.getCuredDeadChartOptions(
516 | orderedProvincesData,
517 | orderedCountryData,
518 | area,
519 | path
520 | )}
521 | />
522 |
523 |
524 | );
525 | } else {
526 | return()
527 | }
528 | }
529 | }
530 |
--------------------------------------------------------------------------------
/src/components/virusMap/virusMap.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * React 疫情地图组件
3 | * 基于EchartsMap组件构建的疫情地图组件,传入地图url及各区域的具体信息后自动生成疫情地图。
4 | * @author: shadowingszy, yarray
5 | *
6 | * 传入props说明:
7 | * name: 地图对应的行政区划(简写)
8 | * data: 显示在地图中的疫情数据。
9 | * breaks: 疫情严重程度分层。
10 | * chartData: 折线图数据。
11 | * chartPath: 热力图点击路径
12 | * currentChartArea: 当前点击位置
13 | * type: overview, pc, mobile 三种不同的charts显示方式
14 | * chartOnClickCallBack: 点击地图后的回调函数。
15 | */
16 |
17 | import React from 'react';
18 | import { EchartsMap } from './echartsMap';
19 | import { VirusChart } from './virusChart';
20 | import mapUrls from '../../data/map/district';
21 |
22 | type MapDataType = {
23 | [name: string]: any;
24 | };
25 |
26 | type STMapDataType = {
27 | timeline: number[];
28 | data: { [timestamp: number]: MapDataType };
29 | }; // spatio-temporal data
30 |
31 | interface Props {
32 | name: string;
33 | data: MapDataType | STMapDataType;
34 | breaks: number[];
35 | chartData: any;
36 | chartPath: Array;
37 | currentChartArea: string;
38 | type: string;
39 | chartOnClickCallBack: Function;
40 | }
41 |
42 | const mapName = (name: string) => {
43 | return name === '中国' ? 'china' : 'map';
44 | };
45 |
46 | const TITLE_SIZE = 15;
47 | const PALETTE = [
48 | '#EEEEEE',
49 | '#FFFADD',
50 | '#FFDC90',
51 | '#FFA060',
52 | '#DD6C5C',
53 | '#AC2F13',
54 | '#3E130E'
55 | ];
56 |
57 | const fixChartFontSize = (baseFontSize: number) => {
58 | const isPC =
59 | (window.innerWidth ||
60 | document.documentElement.clientWidth ||
61 | document.body.clientWidth) >
62 | (window.innerHeight ||
63 | document.documentElement.clientHeight ||
64 | document.body.clientHeight) *
65 | 0.8;
66 |
67 | if (isPC) {
68 | return (
69 | (baseFontSize *
70 | (window.innerWidth ||
71 | document.documentElement.clientWidth ||
72 | document.body.clientWidth)) /
73 | 1000
74 | );
75 | } else {
76 | return (
77 | (baseFontSize *
78 | (window.innerWidth ||
79 | document.documentElement.clientWidth ||
80 | document.body.clientWidth)) /
81 | 500
82 | );
83 | }
84 | }
85 |
86 | const pair = (s: any[]) => {
87 | return s.slice(0, s.length - 1).map((item, i) => [item, s[i + 1]]);
88 | }
89 |
90 | const createPieces = (breaks: number[], palette: string[]) => {
91 | return [
92 | { min: 0, max: 0, color: palette[0] },
93 | ...pair(breaks).map(([b1, b2], i) => ({
94 | gte: b1,
95 | lt: b2,
96 | color: palette[i + 1]
97 | })),
98 | { gte: breaks[breaks.length - 1], color: palette[breaks.length] }
99 | ];
100 | }
101 |
102 | export class VirusMap extends React.Component {
103 | constructor(props: Props) {
104 | super(props);
105 | this.state = {
106 | mapScale: 1,
107 | chartArea: this.props.name
108 | };
109 | }
110 |
111 | static defaultProps = {
112 | name: '',
113 | data: {},
114 | breaks: [1, 10, 50, 100, 500, 1000],
115 | chartData: {},
116 | chartPath: [],
117 | currentChartArea: '',
118 | type: 'pc',
119 | chartOnClickCallBack: (param: any, chart: any) => {
120 | console.log(param, chart);
121 | }
122 | };
123 |
124 | public state = {
125 | mapScale: 1,
126 | chartArea: this.props.name
127 | };
128 |
129 | private genBasicVisualMap() {
130 | const { type } = this.props;
131 | return {
132 | show: true,
133 | type: 'piecewise',
134 | left: type === 'mobile' ? 5 : 10,
135 | top: type === 'mobile' ? 0 : 50,
136 | orient: 'vertical',
137 | itemHeight: type === 'mobile' ? 5 : 10,
138 | itemWidth: type === 'mobile' ? 8 : 14,
139 | itemGap: type === 'mobile' ? 5 : 10,
140 | itemSymbol: 'circle',
141 | padding: type === 'mobile' ? 3 : 10,
142 | textStyle: {
143 | fontSize: 10
144 | }
145 | };
146 | }
147 |
148 | private baseOptions(name: string, breaks: number[]) {
149 | const pieceDict = { pieces: createPieces(breaks, PALETTE) };
150 | const visualMap = {
151 | ...this.genBasicVisualMap(),
152 | ...pieceDict
153 | };
154 | return {
155 | title: {
156 | text: name + '疫情地图',
157 | x: 'center',
158 | textStyle: {
159 | fontSize: fixChartFontSize(TITLE_SIZE)
160 | }
161 | },
162 | tooltip: {},
163 | visualMap: [visualMap],
164 | series: [
165 | {
166 | name: '疫情数据',
167 | type: 'map',
168 | map: mapName(name),
169 | mapType: 'map',
170 | zoom: 1,
171 | label: {
172 | show: this.props.type === 'pc',
173 | fontSize: 8,
174 | textBorderColor: '#FAFAFA',
175 | textBorderWidth: 1
176 | },
177 | emphasis: {
178 | label: {
179 | show: this.props.type === 'pc',
180 | fontSize: 8
181 | }
182 | },
183 | data: []
184 | }
185 | ]
186 | };
187 | }
188 |
189 | private overrides(data: MapDataType) {
190 | return {
191 | tooltip: {
192 | trigger: 'item',
193 | triggerOn: 'onmousemove',
194 | confine: 'true',
195 | formatter: (params: any) => {
196 | if (params.componentType === 'timeline') {
197 | if ((params.dataIndex % 24) * 3600000 === 0) {
198 | return new Date(params.dataIndex).toLocaleDateString('zh-CN');
199 | } else {
200 | return new Date(params.dataIndex).toLocaleDateString(
201 | 'zh-CN-u-hc-h24'
202 | );
203 | }
204 | }
205 |
206 | const outputArray = [params.name];
207 | if (data[params.name] === undefined) {
208 | return params.name + '
暂无数据';
209 | }
210 | if (data[params.name].confirmed !== undefined) {
211 | outputArray.push('确诊:' + data[params.name].confirmed);
212 | }
213 | if (data[params.name].suspected !== undefined) {
214 | outputArray.push('疑似:' + data[params.name].suspected);
215 | }
216 | if (data[params.name].cured !== undefined) {
217 | outputArray.push('治愈:' + data[params.name].cured);
218 | }
219 | if (data[params.name].dead !== undefined) {
220 | outputArray.push('死亡:' + data[params.name].dead);
221 | }
222 | if (this.props.type === 'mobile') {
223 | outputArray.push(
224 | '
再次点击查看详情
'
225 | );
226 | }
227 | return outputArray.join('
');
228 | }
229 | },
230 | series: [
231 | {
232 | data: Object.keys(data).map(name => ({
233 | name,
234 | value: data[name].confirmed || 0
235 | }))
236 | }
237 | ]
238 | };
239 | }
240 |
241 | public getChartOptions(data: MapDataType, options: any = null) {
242 | if (!options) {
243 | options = this.baseOptions(this.props.name, this.props.breaks);
244 | }
245 | let extra = this.overrides(data);
246 | options.series[0].data = extra.series[0].data;
247 | options.tooltip = extra.tooltip;
248 | return options;
249 | }
250 |
251 | public getSTChartOptions(data: STMapDataType, options: any = null) {
252 | const { name, breaks, type } = this.props;
253 | if (!options) {
254 | options = this.baseOptions(name, breaks);
255 | }
256 | options['timeline'] = {
257 | axisType: 'time',
258 | show: true,
259 | tooltip: {
260 | formatter: function (param: any) {
261 | return new Date(parseInt(param.dataIndex, 10)).toLocaleDateString('zh-CN');
262 | }
263 | },
264 | playInterval: 1500,
265 | currentIndex: data.timeline.length - 1,
266 | left: '20px',
267 | right: '20px',
268 | symbolSize: type === 'mobile' ? 5 : 10,
269 | checkpointStyle: {
270 | symbolSize: type === 'mobile' ? 8 : 13,
271 | },
272 | data: data.timeline,
273 | label: {
274 | fontSize: 10,
275 | position: 10,
276 | formatter: function (param: any) {
277 | if (param !== data.timeline[0] && param !== data.timeline[data.timeline.length - 1]) {
278 | return '';
279 | } else {
280 | return new Date(parseInt(param, 10)).toLocaleDateString('zh-CN').substring(5);
281 | }
282 | }
283 | }
284 | };
285 | return {
286 | baseOption: options,
287 | options: data.timeline.sort().map(t => this.overrides(data.data[t]))
288 | };
289 | }
290 |
291 | private isTimelineData(data: MapDataType | STMapDataType): boolean {
292 | return (data as STMapDataType).timeline !== undefined;
293 | }
294 |
295 | public render() {
296 | const {
297 | name,
298 | data,
299 | chartOnClickCallBack,
300 | chartData,
301 | chartPath,
302 | currentChartArea,
303 | type
304 | } = this.props;
305 | const mapUrl = mapUrls[name];
306 |
307 | return (
308 |
325 |
334 |
345 |
346 |
355 |
361 |
362 |
363 | );
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/src/data/isaaclin/patch.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "provinceName": "青海省",
4 | "provinceShortName": "青海",
5 | "confirmedCount": 1,
6 | "suspectedCount": 0,
7 | "curedCount": 0,
8 | "deadCount": 0,
9 | "comment": "",
10 | "cities": [
11 | {
12 | "cityName": "西宁",
13 | "confirmedCount": 1,
14 | "suspectedCount": 0,
15 | "curedCount": 0,
16 | "deadCount": 0
17 | }
18 | ],
19 | "country": "中国",
20 | "updateTime": 1580001790159
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/src/data/isaaclin/update.sh:
--------------------------------------------------------------------------------
1 | curl "https://lab.isaaclin.cn/nCoV/api/area" > current.json
2 | curl "https://lab.isaaclin.cn/nCoV/api/area?latest=0" > history.json
3 | curl "https://lab.isaaclin.cn/nCoV/api/overall?latest=0" > overall.json
4 |
5 |
--------------------------------------------------------------------------------
/src/data/map/district.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | "中国": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china.json",
3 | "世界": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/world.json",
4 | "安徽": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/anhui.json",
5 | "澳门": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/aomen.json",
6 | "北京": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/beijing.json",
7 | "重庆": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/chongqing.json",
8 | "福建": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/fujian.json",
9 | "甘肃": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/gansu.json",
10 | "广东": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/guangdong.json",
11 | "广西": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/guangxi.json",
12 | "贵州": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/guizhou.json",
13 | "海南": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/hainan.json",
14 | "河北": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/hebei.json",
15 | "黑龙江": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/heilongjiang.json",
16 | "河南": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/henan.json",
17 | "湖北": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/hubei.json",
18 | "湖南": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/hunan.json",
19 | "江苏": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/jiangsu.json",
20 | "江西": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/jiangxi.json",
21 | "吉林": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/jilin.json",
22 | "辽宁": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/liaoning.json",
23 | "内蒙古": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/neimenggu.json",
24 | "宁夏": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/ningxia.json",
25 | "青海": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/qinghai.json",
26 | "山东": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/shangdong.json",
27 | "上海": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/shanghai.json",
28 | "山西": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/shangxi.json",
29 | "陕西": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/shanxi.json",
30 | "四川": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/sichuan.json",
31 | "天津": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/tianjin.json",
32 | "香港": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/xianggang.json",
33 | "新疆": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/xinjiang.json",
34 | "西藏": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/xizang.json",
35 | "云南": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/yunnan.json",
36 | "浙江": "https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/zhejiang.json"
37 | }
38 |
--------------------------------------------------------------------------------
/src/data/map/provinces.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | "湖北",
3 | "广东",
4 | "浙江",
5 | "北京",
6 | "上海",
7 | "湖南",
8 | "安徽",
9 | "重庆",
10 | "四川",
11 | "山东",
12 | "广西",
13 | "福建",
14 | "江苏",
15 | "河南",
16 | "海南",
17 | "天津",
18 | "江西",
19 | "陕西",
20 | "贵州",
21 | "辽宁",
22 | "香港",
23 | "黑龙江",
24 | "澳门",
25 | "新疆",
26 | "甘肃",
27 | "云南",
28 | "台湾",
29 | "山西",
30 | "吉林",
31 | "河北",
32 | "宁夏",
33 | "内蒙古",
34 | "青海"
35 | ]
--------------------------------------------------------------------------------
/src/data/map/readme.md:
--------------------------------------------------------------------------------
1 | [百度坐标拾取](http://api.map.baidu.com/lbsapi/getpoint/index.html)
2 |
3 | [省->坐标](https://map-1252957949.cos.ap-guangzhou.myqcloud.com/geo/province.json "")
4 |
5 | [市->坐标](https://map-1252957949.cos.ap-guangzhou.myqcloud.com/geo/city.json "")
6 |
7 | [县->坐标](https://map-1252957949.cos.ap-guangzhou.myqcloud.com/geo/country.json "")
8 |
9 |
10 | ### 省/直辖市/自治区格式
11 |
12 | 注意 北京/北京市 这种不要那个市字
13 |
14 | ```json
15 | {
16 | "台湾": [
17 | 121.5135,
18 | 25.0308
19 | ],
20 | "香港": [
21 | 114.157046,
22 | 22.410655
23 | ],
24 | "澳门": [
25 | 113.538941,
26 | 22.138267
27 | ],
28 | "北京": [
29 | 116.4551,
30 | 40.2539
31 | ],
32 | "新疆": [
33 | 87.9236,
34 | 43.5883
35 | ]
36 | }
37 |
38 |
39 | ```
40 |
41 | ### 直辖市/省辖市/市格式
42 |
43 | ```json
44 | {
45 | "北京": [
46 | 116.395645,
47 | 39.929986
48 | ]
49 |
50 | }
51 | ```
52 |
53 | ### 县格式
54 |
55 | 2个字的一般不带县字
56 |
57 | ```json
58 | {
59 | "徽县": [
60 | 106.11,
61 | 33.78
62 | ],
63 | "礼县": [
64 | 105.15,
65 | 34.22
66 | ],
67 | "武山": [
68 | 104.88,
69 | 34.69
70 | ],
71 | "秦安": [
72 | 105.69,
73 | 34.89
74 | ]
75 |
76 | }
77 |
78 | ```
79 |
80 | ```javascript
81 |
82 | province=[
83 | "中国",
84 | "世界",
85 | "澳门",
86 | "香港",
87 | "台湾",
88 | "北京",
89 | "上海",
90 | "重庆",
91 | "广西",
92 | "新疆",
93 | "宁夏",
94 | "西藏",
95 | "内蒙古",
96 | "云南",
97 | "吉林",
98 | "四川",
99 | "天津",
100 | "安徽",
101 | "山东",
102 | "山西",
103 | "广东",
104 | "江苏",
105 | "江西",
106 | "河北",
107 | "河南",
108 | "浙江",
109 | "海南",
110 | "湖北",
111 | "湖南",
112 | "甘肃",
113 | "福建",
114 | "贵州",
115 | "辽宁",
116 | "陕西",
117 | "青海",
118 | "黑龙江"
119 | ]
120 |
121 | ```
122 |
123 |
124 | ### 地图文件命名规则
125 |
126 |
127 | 注意 陕西和山西拼音相同
128 | 所以 山东山西的山统一改为shang
129 |
130 | 山东省的地图文件url为 [shangdong.json](https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/shangdong.json)
131 |
132 | 拼音汉字互转
133 |
134 | ```json
135 | {
136 |
137 | "中国": "china",
138 | "世界": "world",
139 | "澳门": "aomen",
140 | "香港": "xianggang",
141 | "台湾": "taiwan",
142 | "北京": "beijing",
143 | "天津": "tianjin",
144 | "上海": "shanghai",
145 | "重庆": "chongqing",
146 | "广西": "guangxi",
147 | "新疆": "xinjiang",
148 | "西藏": "xizang",
149 | "内蒙古": "neimenggu",
150 | "宁夏": "ningxia",
151 | "云南": "yunnan",
152 | "吉林": "jilin",
153 | "四川": "sichuan",
154 | "安徽": "anhui",
155 | "山东": "shangdong",
156 | "山西": "shangxi",
157 | "广东": "guangdong",
158 | "江苏": "jiangsu",
159 | "江西": "jiangxi",
160 | "河北": "hebei",
161 | "河南": "henan",
162 | "浙江": "zhejiang",
163 | "海南": "hainan",
164 | "湖北": "hubei",
165 | "湖南": "hunan",
166 | "甘肃": "gansu",
167 | "福建": "fujian",
168 | "贵州": "guizhou",
169 | "辽宁": "liaoning",
170 | "陕西": "shanxi",
171 | "青海": "qinghai",
172 | "黑龙江": "heilongjiang",
173 | "china": "中国",
174 | "world": "世界",
175 | "aomen": "澳门",
176 | "xianggang": "香港",
177 | "taiwan": "台湾",
178 | "beijing": "北京",
179 | "shanghai": "上海",
180 | "chongqing": "重庆",
181 | "guangxi": "广西",
182 | "xinjiang": "新疆",
183 | "xizang": "西藏",
184 | "neimenggu": "内蒙古",
185 | "yunnan": "云南",
186 | "jilin": "吉林",
187 | "sichuan": "四川",
188 | "tianjin": "天津",
189 | "ningxia": "宁夏",
190 | "anhui": "安徽",
191 | "shangdong": "山东",
192 | "shangxi": "山西",
193 | "guangdong": "广东",
194 | "jiangsu": "江苏",
195 | "jiangxi": "江西",
196 | "hebei": "河北",
197 | "henan": "河南",
198 | "zhejiang": "浙江",
199 | "hainan": "海南",
200 | "hubei": "湖北",
201 | "hunan": "湖南",
202 | "gansu": "甘肃",
203 | "fujian": "福建",
204 | "guizhou": "贵州",
205 | "liaoning": "辽宁",
206 | "shanxi": "陕西",
207 | "qinghai": "青海",
208 | "heilongjiang": "黑龙江"
209 | }
210 |
211 | ```
212 |
213 |
214 | ### 注册地图方式
215 |
216 | ```javascript
217 | const get=(u="/")=>fetch(u).then(x=>x.json())
218 | const MAP_SERVER="https://map-1252957949.cos.ap-guangzhou.myqcloud.com"
219 |
220 | test1=async ()=>{
221 | mapName="山东"
222 | url="https://map-1252957949.cos.ap-guangzhou.myqcloud.com/china/shangdong.json"
223 | r=await get(url)
224 | echarts.registerMap(mapName, r)
225 | }
226 |
227 | test=()=>{
228 | //echarts=...
229 | mapName="北京"
230 | //d=province2pinyin[mapName]
231 | d="beijing"
232 | url=MAP_SERVER+"/map/china/"+d+".json"
233 | r=await get(url)
234 | echarts.registerMap(mapName, r)
235 | }
236 |
237 | ```
238 |
--------------------------------------------------------------------------------
/src/data/ncov_nosensor/update.sh:
--------------------------------------------------------------------------------
1 | curl http://ncov.nosensor.com:8080/api > history.json
2 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/src/mock/informationMapMockData.js:
--------------------------------------------------------------------------------
1 | /*
2 | InformationMap组件的mock数据
3 |
4 | 主要结构:
5 | {
6 | "initPoint": 地图初始点坐标
7 | "zoom": 地图初始缩放比例
8 | "markerArray": 地图标记点的内容
9 | },
10 | ...
11 |
12 | 其中,markerArray中每一项的内容为:
13 | export interface GeoData {
14 | type: "hospital"|"hotel"|"other";
15 | name: string;
16 | url?: string;
17 | coord: [number, number];
18 | metadata: Metadata[];
19 | }
20 | export interface Metadata {
21 | key: string;
22 | label?: string;
23 | value: string|number|InquiryMeta[],
24 | }
25 |
26 | 使用:
27 |
31 | */
32 |
33 | export default {
34 | initPoint: [110.350658, 32.938285],
35 | zoom: 6,
36 | makerArray: [
37 | {
38 | name: '华中科技大学同济医学院附属协和医院',
39 | type: 'hospital',
40 | coord: [114.281196, 30.590103],
41 | url: 'https://mp.weixin.qq.com/s/geO3CCd0_8B3L-r_xlBbZQ',
42 | metadata: [
43 | {
44 | key: 'request',
45 | label: '物资需求',
46 | value: [
47 | ['普通医用口罩', 10000],
48 | ['医用外科口罩', true],
49 | ['医用防护口罩 | N95口罩', 10000],
50 | ['防冲击眼罩/护目镜/防护眼镜', true],
51 | ['防护面罩', 25050],
52 | ['防护帽/医用帽/圆帽', 10420],
53 | ['隔离衣', true],
54 | ['防护服', 5000],
55 | ['手术衣', true],
56 | ['乳胶手套', true],
57 | ['长筒胶鞋/防污染靴', true],
58 | ['防污染鞋套', true],
59 | ['防污染靴套', true],
60 | ['84消毒液', true],
61 | ['过氧乙酸', true],
62 | ['75%酒精', true],
63 | ['手部皮肤消毒液', true],
64 | ['活力碘', true],
65 | ['床罩', true],
66 | ['医用面罩式雾化器', true],
67 | ['测体温设备', true],
68 | ['空气消毒设备', true],
69 | ['医用紫外线消毒车', true]
70 | ]
71 | },
72 | {
73 | key: 'address',
74 | value:
75 | '湖北省武汉市江汉区解放大道1277号华中科技大学同济医学院附属协和医院总务处',
76 | label: '邮寄地址'
77 | },
78 | {
79 | key: 'note',
80 | value: null,
81 | label: '备注信息'
82 | }
83 | ]
84 | },
85 | {
86 | name: '红安县人民医院',
87 | type: 'hospital',
88 | coord: [114.625222, 31.286868],
89 | url: 'https://mp.weixin.qq.com/s/geO3CCd0_8B3L-r_xlBbZQ',
90 | address: '红安县人民医院红安县城关镇陵园大道附50号',
91 | metadata: [
92 | {
93 | key: 'request',
94 | label: '物资需求',
95 | value: [
96 | ['普通医用口罩', 1000],
97 | ['医用外科口罩', 1000],
98 | ['医用防护口罩 | N95口罩', 10000],
99 | ['防冲击眼罩/护目镜/防护眼镜', 1000],
100 | ['防护面罩', true],
101 | ['防护帽/医用帽/圆帽', 1000],
102 | ['隔离衣', true],
103 | ['防护服', 100],
104 | ['手术衣', true],
105 | ['乳胶手套', 1000],
106 | ['长筒胶鞋/防污染靴', 100],
107 | ['防污染鞋套', 100],
108 | ['防污染靴套', 10000],
109 | ['84消毒液', true],
110 | ['过氧乙酸', true],
111 | ['75%酒精', true],
112 | ['手部皮肤消毒液', true],
113 | ['活力碘', true],
114 | ['床罩', true],
115 | ['医用面罩式雾化器', true],
116 | ['测体温设备', true],
117 | ['空气消毒设备', true],
118 | ['医用紫外线消毒车', true]
119 | ]
120 | },
121 | {
122 | key: 'address',
123 | value:
124 | '湖北省武汉市江汉区解放大道1277号华中科技大学同济医学院附属协和医院总务处',
125 | label: '邮寄地址'
126 | },
127 | {
128 | key: 'note',
129 | value: null,
130 | label: '备注信息'
131 | },
132 | {
133 | key: 'contact',
134 | label: '联系方式',
135 | value: [['0713-5242320'], ['设备科周主任, 13636105950']]
136 | }
137 | ]
138 | },
139 | {
140 | type: 'hotel',
141 | name: '住宿数据1',
142 | coord: [114.881337, 30.205063],
143 | metadata: [
144 | {
145 | key: 'capability',
146 | value: 100,
147 | label: '容量'
148 | },
149 | {
150 | key: 'note',
151 | value: '发布日期, 2020年1月25日',
152 | label: '备注信息'
153 | },
154 | {
155 | key: 'contact',
156 | label: '联系方式',
157 | value: [['XXX:123456789']]
158 | }
159 | ]
160 | },
161 |
162 | {
163 | type: 'others',
164 | name: '其他数据',
165 | coord: [114.681337, 30.295063],
166 | metadata: [
167 | {
168 | key: '内容',
169 | value: '我是内容'
170 | }
171 | ]
172 | }
173 | ]
174 | };
175 |
--------------------------------------------------------------------------------
/src/pages/hierarchicalVirusMapDemo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HierarchicalVirusMap from '../components/virusMap/hierarchicalVirusMap';
3 | import { convertProvincesSeries, convertCountrySeries, } from '../adapters/isaaclin';
4 | import patchData from '../data/isaaclin/patch.json';
5 | import rawData from '../data/isaaclin/history.json';
6 | import overviewData from '../data/isaaclin/overall.json';
7 |
8 | export class HierarchicalVirusMapDemo extends React.Component<{}, {}> {
9 | render() {
10 | const resolution = 3600000 * 24;
11 | const data = {
12 | provincesSeries: convertProvincesSeries(
13 | [...rawData['results'], ...patchData],
14 | resolution,
15 | true
16 | ),
17 | countrySeries: convertCountrySeries(overviewData['results'], resolution)
18 | };
19 |
20 | return (
21 |
22 | {/* */}
23 |
24 | {/* */}
25 |
26 | );
27 | }
28 | }
--------------------------------------------------------------------------------
/src/pages/informationMapDemo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import InformationMap from '../components/informationMap/informationMap';
3 | import informationMockData from '../mock/informationMapMockData';
4 |
5 | export class InformationMapDemo extends React.Component<{}, {}> {
6 | render() {
7 | const initPoint: any = informationMockData.initPoint;
8 | const zoom: number = informationMockData.zoom;
9 | const makerArray: any[] = informationMockData.makerArray;
10 |
11 | return (
12 |
13 |
18 |
19 | );
20 | }
21 | }
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | process.env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl, {
112 | headers: { 'Service-Worker': 'script' }
113 | })
114 | .then(response => {
115 | // Ensure service worker exists, and that we really are getting a JS file.
116 | const contentType = response.headers.get('content-type');
117 | if (
118 | response.status === 404 ||
119 | (contentType != null && contentType.indexOf('javascript') === -1)
120 | ) {
121 | // No service worker found. Probably a different app. Reload the page.
122 | navigator.serviceWorker.ready.then(registration => {
123 | registration.unregister().then(() => {
124 | window.location.reload();
125 | });
126 | });
127 | } else {
128 | // Service worker found. Proceed as normal.
129 | registerValidSW(swUrl, config);
130 | }
131 | })
132 | .catch(() => {
133 | console.log(
134 | 'No internet connection found. App is running in offline mode.'
135 | );
136 | });
137 | }
138 |
139 | export function unregister() {
140 | if ('serviceWorker' in navigator) {
141 | navigator.serviceWorker.ready.then(registration => {
142 | registration.unregister();
143 | });
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./lib/",
5 | "declaration": true,
6 | "declarationDir": "./lib/types/",
7 | "target": "es5",
8 | "lib": [
9 | "dom",
10 | "dom.iterable",
11 | "esnext"
12 | ],
13 | "rootDir": "./",
14 | "allowJs": true,
15 | "skipLibCheck": true,
16 | "esModuleInterop": true,
17 | "allowSyntheticDefaultImports": true,
18 | "strict": false,
19 | "forceConsistentCasingInFileNames": true,
20 | "module": "esnext",
21 | "moduleResolution": "node",
22 | "resolveJsonModule": true,
23 | "isolatedModules": true,
24 | "noEmit": true,
25 | "jsx": "react",
26 | "suppressImplicitAnyIndexErrors": true,
27 | "removeComments": true
28 | },
29 | "include": [
30 | "src/**/*.ts",
31 | "src/**/*.tsx"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/webpack.config.lib.js:
--------------------------------------------------------------------------------
1 | // 此文件为webpack打包组件时的配置
2 |
3 | const path = require('path');
4 |
5 | module.exports = {
6 | entry: './webpack.lib.entry.js',
7 | mode: 'production',
8 | output: {
9 | path: path.resolve(__dirname, './lib/'),
10 | filename: 'components.js',
11 | libraryTarget: 'commonjs2',
12 | },
13 | resolve: {
14 | extensions: ['.ts', '.tsx', '.js']
15 | },
16 | module: {
17 | rules: [
18 | {
19 | test: /\.tsx?$/,
20 | use: [
21 | 'awesome-typescript-loader'
22 | ],
23 | exclude: /node_modules/
24 | },
25 | {
26 | test: /\.ts?$/,
27 | use: [
28 | 'awesome-typescript-loader'
29 | ],
30 | exclude: /node_modules/
31 | },
32 | {
33 | test: /\.js?$/,
34 | use: [
35 | 'babel-loader'
36 | ],
37 | exclude: /node_modules/
38 | }
39 | ]
40 | },
41 | externals: {
42 | react: 'react',
43 | echarts: 'echarts',
44 | }
45 | }
--------------------------------------------------------------------------------
/webpack.lib.entry.js:
--------------------------------------------------------------------------------
1 | // 此文件为webpack打包组件的入口
2 | import HierarchicalVirusMap from './src/components/virusMap/hierarchicalVirusMap';
3 | import InformationMap from './src/components/informationMap/informationMap';
4 |
5 | export {
6 | HierarchicalVirusMap,
7 | InformationMap
8 | };
--------------------------------------------------------------------------------