├── .env.sample
├── .eslintrc
├── .gitignore
├── .nvmrc
├── .prettierrc
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── actions
├── appStatus.js
├── chartData.js
└── sheetData.js
├── charts
├── bar.jsx
├── barReverse.jsx
├── horizontalBar.jsx
├── horizontalBarReverse.jsx
├── line.jsx
├── lineReverse.jsx
├── pie.jsx
└── pieReverse.jsx
├── components
├── App.jsx
├── Chart.jsx
├── ChartEditor.jsx
├── ChartView.jsx
├── Footer.jsx
├── InfoView.jsx
├── SheetPicker.jsx
└── SpreadsheetPicker.jsx
├── constants.jsx
├── index.jsx
├── reducers
├── appStatus.js
├── chartData.js
├── index.js
└── sheetData.js
├── screens
└── Home.jsx
├── static
└── images
│ ├── robo-chart.gif
│ ├── robo-chart.png
│ └── spreadsheet-format.png
├── styles
├── App.scss
├── _Colors.scss
├── _Screens.scss
└── _Variables.scss
└── utils
├── handleOptions.jsx
├── horizontalBarOptions.jsx
├── lineOptions.jsx
├── pieOptions.jsx
└── processSpreadsheet.jsx
/.env.sample:
--------------------------------------------------------------------------------
1 | REACT_APP_CHART_TOKEN = ''
2 | REACT_APP_GSHEETS_API = 'https://sheets.googleapis.com/v4/spreadsheets/'
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["airbnb", "prettier", "prettier/react"],
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "jest": true
8 | },
9 | "rules": {
10 | "react/prop-types": 0
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v10.17.0
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2019 Postlight
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Postlight
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Robo Chart
2 |
3 | Transform Google sheets to pretty charts!
4 |
5 | 
6 |
7 | ## How to Install
8 |
9 | To install the React Component, please check [this link](https://github.com/postlight/react-google-sheet-chart).
10 |
11 | To setup this repository, follow these steps:
12 |
13 | 1. Clone the project:
14 |
15 | > git clone https://github.com/postlight/robo-chart-web.git
16 |
17 | 2. Generate a Google API Key: https://console.cloud.google.com/apis/credentials
18 | 3. Rename `.env.sample` to `.env` and paste the generated API Key in the `REACT_APP_CHART_TOKEN` field
19 | 4. run the following commands:
20 | ```sh
21 | npm install
22 | npm start
23 | ```
24 |
25 | Once setup is done, the app will be available on http://localhost:3000
26 |
27 | ## How to use
28 |
29 | Paste your Google Sheet URL in the Spreadsheet field, Robo Chart will fetch the data and process it.
30 |
31 | You can switch between different sheets, pick chart type, modify the Rows and Columns to process, flip Rows & Columns, modify colors and other..
32 |
33 | 
34 |
35 | ## Spreadsheet format
36 |
37 | In order to successfuly generate a chart, the Spreadsheet should have Row titles, Column titles and Values, example:
38 |
39 | 
40 |
41 | ## License
42 |
43 | Licensed under either of the below, at your preference:
44 |
45 | - Apache License, Version 2.0
46 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
47 | - MIT license
48 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
49 |
50 | ## Contributing
51 |
52 | Unless it is explicitly stated otherwise, any contribution intentionally submitted for inclusion in the work, as defined in the Apache-2.0 license, shall be dual licensed as above without any additional terms or conditions.
53 |
54 | ---
55 |
56 | 🔬 A Labs project from your friends at [Postlight](https://postlight.com/labs)
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "robo-chart-web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.21.1",
7 | "bootstrap": "^4.3.1",
8 | "chart.js": "^2.7.3",
9 | "chartjs-plugin-annotation": "^0.5.7",
10 | "dom-to-image": "^2.6.0",
11 | "eslint-config-airbnb": "^17.1.0",
12 | "eslint-config-prettier": "^4.1.0",
13 | "eslint-plugin-import": "^2.16.0",
14 | "eslint-plugin-jsx-a11y": "^6.2.1",
15 | "eslint-plugin-prettier": "^3.0.1",
16 | "eslint-plugin-react": "^7.12.4",
17 | "lodash": "^4.17.20",
18 | "node-sass": "^4.14.1",
19 | "prettier": "^1.16.4",
20 | "prop-types": "^15.7.2",
21 | "randomcolor": "^0.5.4",
22 | "react": "^16.8.4",
23 | "react-bootstrap": "^1.0.0-beta.5",
24 | "react-chartjs-2": "^2.7.4",
25 | "react-color": "^2.17.0",
26 | "react-dom": "^16.8.4",
27 | "react-redux": "^6.0.1",
28 | "react-router-bootstrap": "^0.24.4",
29 | "react-router-dom": "^4.3.1",
30 | "react-scripts": "^3.4.3",
31 | "react-spinners": "^0.5.3",
32 | "redux": "^4.0.1"
33 | },
34 | "scripts": {
35 | "deploy": "gcloud app deploy",
36 | "start": "react-scripts start",
37 | "build": "react-scripts build",
38 | "test": "react-scripts test",
39 | "eject": "react-scripts eject"
40 | },
41 | "eslintConfig": {
42 | "extends": "react-app"
43 | },
44 | "browserslist": [
45 | ">0.2%",
46 | "not dead",
47 | "not ie <= 11",
48 | "not op_mini all"
49 | ]
50 | }
51 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/postlight/robo-chart-web/b0b6f8e4306469b816020757e94c6254c82128e3/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
25 |
31 |
32 | Robo Chart
33 |
34 |
35 | You need to enable JavaScript to run this app.
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Robo Chart",
3 | "name": "Transform ugly sheets to pretty charts!",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/actions/appStatus.js:
--------------------------------------------------------------------------------
1 | /**
2 | * App Status redux actions
3 | */
4 |
5 | export const SET_FETCHING_DATA = 'SET_FETCHING_DATA';
6 | export const SET_AUTH_ERROR = 'SET_AUTH_ERROR';
7 | export const SET_ERROR = 'SET_ERROR';
8 |
9 | /**
10 | * Flag an authentication error
11 | * @param {boolean} authError
12 | */
13 | export function setAuthError(authError) {
14 | return { type: SET_AUTH_ERROR, authError };
15 | }
16 |
17 | /**
18 | * Flag a query error
19 | * @param {boolean} error
20 | */
21 | export function setError(error) {
22 | return { type: SET_ERROR, error };
23 | }
24 |
25 | /**
26 | * Flag that data is being fetched, or is done fetching
27 | * @param {boolean} fetchingData
28 | */
29 | export function setFetchingData(fetchingData) {
30 | return { type: SET_FETCHING_DATA, fetchingData };
31 | }
32 |
--------------------------------------------------------------------------------
/src/actions/chartData.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Chart data redux actions
3 | */
4 |
5 | export const SET_CHART_DATA = 'SET_CHART_DATA';
6 | export const SET_CHART_TYPE = 'SET_CHART_TYPE';
7 | export const SET_CHART_COLORS = 'SET_CHART_COLORS';
8 | export const SET_ACTIVE_COLOR = ' SET_ACTIVE_COLOR';
9 | export const SET_CHART_TITLE = 'SET_CHART_TITLE';
10 | export const SET_XSUFFIX = 'SET_XSUFFIX';
11 | export const SET_YSUFFIX = 'SET_YSUFFIX';
12 | export const SET_START_FROM = 'SET_START_FROM';
13 | export const SET_FLIP_AXIS = 'SET_FLIP_AXIS';
14 | export const RESET_CHART_DATA = 'RESET_CHART_DATA';
15 |
16 | /**
17 | * Set processed chart data
18 | * @param {object} data
19 | */
20 | export function setChartData(data) {
21 | return { type: SET_CHART_DATA, data };
22 | }
23 |
24 | /**
25 | * Reset chart data, example: while changing active sheet
26 | * @param {object} data
27 | */
28 | export function resetChartData() {
29 | return { type: RESET_CHART_DATA };
30 | }
31 |
32 | /**
33 | * Set chart colors, includes an array of colors
34 | * @param {object} data
35 | */
36 | export function setChartColors(data) {
37 | return { type: SET_CHART_COLORS, data };
38 | }
39 |
40 | /**
41 | * Set active color (color currently being edited)
42 | * @param {string} color
43 | */
44 | export function setActiveColor(color) {
45 | return { type: SET_ACTIVE_COLOR, color };
46 | }
47 |
48 | /**
49 | * Set chart type, one of:
50 | * line
51 | * bar
52 | * horizontalBar
53 | * stacked
54 | * pie
55 | * semi-pie
56 | * doughnut
57 | * semi-doughnut
58 | * @param {string} chartType
59 | */
60 | export function setChartType(chartType) {
61 | return { type: SET_CHART_TYPE, chartType };
62 | }
63 |
64 | /**
65 | * Set chart title
66 | * @param {string} title
67 | */
68 | export function setChartTitle(title) {
69 | return { type: SET_CHART_TITLE, title };
70 | }
71 |
72 | /**
73 | * Set x axis labels suffix
74 | * @param {string} xsuffix
75 | */
76 | export function setxSuffix(xsuffix) {
77 | return { type: SET_XSUFFIX, xsuffix };
78 | }
79 |
80 | /**
81 | * Set y axis labels suffix
82 | * @param {string} ysuffix
83 | */
84 | export function setySuffix(ysuffix) {
85 | return { type: SET_YSUFFIX, ysuffix };
86 | }
87 |
88 | /**
89 | * Set start from for the values axis.
90 | * example: if minimum value is 10000, user might choose to start the Y Axis from 9000 instead of 0
91 | * @param {number} startFrom
92 | */
93 | export function setStartFrom(startFrom) {
94 | return { type: SET_START_FROM, startFrom };
95 | }
96 |
97 | /**
98 | * Flip axis, process the sheet's columns as rows and vice versa
99 | * @param {string} flipAxis
100 | */
101 | export function setFlipAxis(flipAxis) {
102 | return { type: SET_FLIP_AXIS, flipAxis };
103 | }
104 |
--------------------------------------------------------------------------------
/src/actions/sheetData.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sheet data redux actions
3 | */
4 |
5 | export const SET_SHEET_ID = 'SET_SHEET_ID';
6 | export const SET_SHEET_DATA = 'SET_SHEET_DATA';
7 | export const SET_ACTIVE_SHEET = 'SET_ACTIVE_SHEET';
8 | export const SET_START_AND_END = 'SET_START_AND_END';
9 | export const RESET_SHEET_DATA = 'RESET_SHEET_DATA';
10 |
11 | /**
12 | * Set raw sheet data
13 | * @param {object} data
14 | */
15 | export function setSheetData(data) {
16 | return { type: SET_SHEET_DATA, data };
17 | }
18 |
19 | /**
20 | * Set start and end cells
21 | * e.g.: A5, E15
22 | * @param {object} data
23 | */
24 | export function setStartAndEnd(data) {
25 | return { type: SET_START_AND_END, data };
26 | }
27 |
28 | /**
29 | * Set sheet id, found in Google sheet urls
30 | * e.g.: 1RE_JYUCXBXY2LNV5Tp5GegLnMue-CpfTVMxjdudZ8Js
31 | * @param {string} sheetId
32 | */
33 | export function setSheetId(sheetId) {
34 | return { type: SET_SHEET_ID, sheetId };
35 | }
36 |
37 | /**
38 | * Set active sheet
39 | * @param {string} activeSheet
40 | */
41 | export function setActiveSheet(activeSheet) {
42 | return { type: SET_ACTIVE_SHEET, activeSheet };
43 | }
44 |
45 | /**
46 | * Reset sheet data
47 | * @param {string} activeSheet
48 | */
49 | export function resetSheetData() {
50 | return { type: RESET_SHEET_DATA };
51 | }
52 |
--------------------------------------------------------------------------------
/src/charts/bar.jsx:
--------------------------------------------------------------------------------
1 | import { randomColor } from 'randomcolor';
2 | import options from '../utils/lineOptions';
3 |
4 | /**
5 | * Returns chart data specific for Bar chart type
6 | *
7 | * @param {object} data chart data
8 | * @param {boolean} stacked true or false
9 | * @param {array} colors array of colors used to present data
10 | */
11 | const getBarChartData = (data, stacked, colors) => {
12 | const chartData = {
13 | labels: [],
14 | datasets: [],
15 | info: [],
16 | annotations: [],
17 | options,
18 | colors: [],
19 | };
20 |
21 | let columnCount = 0;
22 | let colorIndex = 0;
23 | data.forEach((element, rowindex) => {
24 | if (columnCount === 0) {
25 | columnCount = element.length;
26 | }
27 | element.forEach((value, colindex) => {
28 | const numericalValue = value.replace(/[^\d.-]/g, '');
29 |
30 | if (rowindex === 0) {
31 | if (value && value.length > 0) {
32 | const object = { data: [] };
33 | let color = colors[colorIndex];
34 | if (!colors || colorIndex >= colors.length) {
35 | color = randomColor();
36 | }
37 | colorIndex += 1;
38 | object.borderColor = color;
39 | object.backgroundColor = color;
40 | object.pointBorderColor = color;
41 | object.pointBackgroundColor = color;
42 | object.pointHoverBackgroundColor = color;
43 | object.pointHoverBorderColor = color;
44 | object.label = value;
45 | chartData.datasets.push(object);
46 |
47 | chartData.colors.push(color);
48 | }
49 | } else if (colindex === 0) {
50 | chartData.labels.push(value);
51 | } else if (chartData.datasets[colindex - 1]) {
52 | chartData.datasets[colindex - 1].data.push(numericalValue);
53 | }
54 | });
55 | let i = element.length;
56 | for (; i < columnCount; ) {
57 | if (chartData.datasets[i - 1]) {
58 | chartData.datasets[i - 1].data.push(0);
59 | }
60 | i += 1;
61 | }
62 | });
63 |
64 | chartData.options.scales.xAxes[0].labels = chartData.labels;
65 | chartData.options.scales.xAxes[0].stacked = stacked;
66 | chartData.options.scales.yAxes[0].ticks = {
67 | beginAtZero: true,
68 | };
69 | chartData.options.scales.xAxes[0].ticks = {
70 | beginAtZero: true,
71 | };
72 |
73 | return chartData;
74 | };
75 |
76 | export default getBarChartData;
77 |
--------------------------------------------------------------------------------
/src/charts/barReverse.jsx:
--------------------------------------------------------------------------------
1 | import { randomColor } from 'randomcolor';
2 | import options from '../utils/lineOptions';
3 |
4 | /**
5 | * Returns chart data specific for Bar chart type with reversed axis processing
6 | *
7 | * @param {object} data chart data
8 | * @param {boolean} stacked true or false
9 | * @param {array} colors array of colors used to present data
10 | */
11 | const getBarReverseChartData = (data, stacked, colors) => {
12 | const chartData = {
13 | labels: [],
14 | datasets: [],
15 | info: [],
16 | annotations: [],
17 | options,
18 | colors: [],
19 | };
20 |
21 | let columnCount = 0;
22 | let colorIndex = 0;
23 | data.forEach((element, rowindex) => {
24 | if (columnCount === 0) {
25 | columnCount = element.length;
26 | }
27 | element.forEach((value, colindex) => {
28 | const numericalValue = value.replace(/[^\d.-]/g, '');
29 |
30 | if (rowindex === 0) {
31 | if (value && value.length > 0) {
32 | chartData.labels.push(value);
33 | }
34 | } else if (colindex === 0) {
35 | if (value && value.length > 0) {
36 | const object = { data: [] };
37 | let color = colors[colorIndex];
38 | if (!colors || colorIndex >= colors.length) {
39 | color = randomColor();
40 | }
41 | colorIndex += 1;
42 | object.borderColor = color;
43 | object.backgroundColor = color;
44 | object.pointBorderColor = color;
45 | object.pointBackgroundColor = color;
46 | object.pointHoverBackgroundColor = color;
47 | object.pointHoverBorderColor = color;
48 | object.label = value;
49 | chartData.datasets.push(object);
50 |
51 | chartData.colors.push(color);
52 | }
53 | } else if (chartData.datasets[rowindex - 1]) {
54 | chartData.datasets[rowindex - 1].data.push(numericalValue);
55 | }
56 | });
57 | });
58 |
59 | chartData.options.scales.xAxes[0].labels = chartData.labels;
60 | chartData.options.scales.xAxes[0].stacked = stacked;
61 | chartData.options.scales.yAxes[0].ticks = {
62 | beginAtZero: true,
63 | };
64 | chartData.options.scales.xAxes[0].ticks = {
65 | beginAtZero: true,
66 | };
67 | return chartData;
68 | };
69 |
70 | export default getBarReverseChartData;
71 |
--------------------------------------------------------------------------------
/src/charts/horizontalBar.jsx:
--------------------------------------------------------------------------------
1 | import { randomColor } from 'randomcolor';
2 | import options from '../utils/horizontalBarOptions';
3 |
4 | /**
5 | * Returns chart data specific for Horizontal Bar chart type
6 | *
7 | * @param {object} data chart data
8 | * @param {array} colors array of colors used to present data
9 | */
10 | const getHorizontalBarChartData = (data, colors) => {
11 | const chartData = {
12 | labels: [],
13 | datasets: [],
14 | options,
15 | colors: [],
16 | };
17 |
18 | let columnCount = 0;
19 | let colorIndex = 0;
20 | data.forEach((element, rowindex) => {
21 | if (columnCount === 0) {
22 | columnCount = element.length;
23 | }
24 | element.forEach((value, colindex) => {
25 | const numericalValue = value.replace(/[^\d.-]/g, '');
26 | if (rowindex === 0) {
27 | if (value && value.length > 0) {
28 | chartData.labels.push(value);
29 | }
30 | } else if (colindex === 0) {
31 | if (value && value.length > 0) {
32 | const object = { data: [] };
33 | let color = colors[colorIndex];
34 | if (!colors || colorIndex >= colors.length) {
35 | color = randomColor();
36 | }
37 | colorIndex += 1;
38 | object.borderColor = color;
39 | object.backgroundColor = color;
40 | object.borderWidth = 1;
41 | object.label = value;
42 | object.hoverBackgroundColor = color;
43 | object.hoverBorderColor = color;
44 | chartData.datasets.push(object);
45 |
46 | chartData.colors.push(color);
47 | }
48 | } else if (chartData.datasets[rowindex - 1]) {
49 | chartData.datasets[rowindex - 1].data.push(numericalValue);
50 | }
51 | });
52 | });
53 | return chartData;
54 | };
55 |
56 | export default getHorizontalBarChartData;
57 |
--------------------------------------------------------------------------------
/src/charts/horizontalBarReverse.jsx:
--------------------------------------------------------------------------------
1 | import { randomColor } from 'randomcolor';
2 | import options from '../utils/horizontalBarOptions';
3 |
4 | /**
5 | * Returns chart data specific for Horizontal Bar chart type with reversed axis processing
6 | *
7 | * @param {object} data chart data
8 | * @param {array} colors array of colors used to present data
9 | */
10 | const getHorizontalBarReverseChartData = (data, colors) => {
11 | const chartData = {
12 | labels: [],
13 | datasets: [],
14 | options,
15 | colors: [],
16 | };
17 |
18 | let columnCount = 0;
19 | let colorIndex = 0;
20 | data.forEach((element, rowindex) => {
21 | if (columnCount === 0) {
22 | columnCount = element.length;
23 | }
24 | element.forEach((value, colindex) => {
25 | const numericalValue = value.replace(/[^\d.-]/g, '');
26 | if (rowindex === 0) {
27 | if (value && value.length > 0) {
28 | const object = { data: [] };
29 | let color = colors[colorIndex];
30 | if (!colors || colorIndex >= colors.length) {
31 | color = randomColor();
32 | }
33 | colorIndex += 1;
34 | object.borderColor = color;
35 | object.backgroundColor = color;
36 | object.borderWidth = 1;
37 | object.label = value;
38 | object.hoverBackgroundColor = color;
39 | object.hoverBorderColor = color;
40 | chartData.datasets.push(object);
41 |
42 | chartData.colors.push(color);
43 | }
44 | } else if (colindex === 0) {
45 | if (value && value.length > 0) {
46 | chartData.labels.push(value);
47 | }
48 | } else if (chartData.datasets[colindex - 1]) {
49 | chartData.datasets[colindex - 1].data.push(numericalValue);
50 | }
51 | });
52 | });
53 | return chartData;
54 | };
55 |
56 | export default getHorizontalBarReverseChartData;
57 |
--------------------------------------------------------------------------------
/src/charts/line.jsx:
--------------------------------------------------------------------------------
1 | import { randomColor } from 'randomcolor';
2 | import options from '../utils/lineOptions';
3 |
4 | /**
5 | * Returns chart data specific for Line chart type
6 | *
7 | * @param {object} data chart data
8 | * @param {array} colors array of colors used to present data
9 | */
10 | const getLineChartData = (data, colors) => {
11 | const chartData = {
12 | labels: [],
13 | datasets: [],
14 | info: [],
15 | annotations: [],
16 | options,
17 | colors: [],
18 | };
19 |
20 | let columnCount = 0;
21 | let colorIndex = 0;
22 | data.forEach((element, rowindex) => {
23 | if (columnCount === 0) {
24 | columnCount = element.length;
25 | }
26 | element.forEach((value, colindex) => {
27 | const numericalValue = value.replace(/[^\d.-]/g, '');
28 | if (rowindex === 0) {
29 | if (value && value.length > 0) {
30 | const object = { data: [] };
31 | let color = colors[colorIndex];
32 | if (!colors || colorIndex >= colors.length) {
33 | color = randomColor();
34 | }
35 | colorIndex += 1;
36 | object.borderColor = color;
37 | object.backgroundColor = color;
38 | object.pointBorderColor = color;
39 | object.pointBackgroundColor = color;
40 | object.pointHoverBackgroundColor = color;
41 | object.pointHoverBorderColor = color;
42 | object.fill = false;
43 | object.label = value;
44 | chartData.datasets.push(object);
45 |
46 | chartData.colors.push(color);
47 | }
48 | } else if (colindex === 0) {
49 | chartData.labels.push(value);
50 | } else if (chartData.datasets[colindex - 1]) {
51 | chartData.datasets[colindex - 1].data.push(numericalValue);
52 | }
53 | });
54 | let i = element.length;
55 | for (; i < columnCount; ) {
56 | if (chartData.datasets[i - 1]) {
57 | chartData.datasets[i - 1].data.push(0);
58 | }
59 | i += 1;
60 | }
61 | });
62 |
63 | chartData.options.scales.xAxes[0].labels = chartData.labels;
64 | return chartData;
65 | };
66 |
67 | export default getLineChartData;
68 |
--------------------------------------------------------------------------------
/src/charts/lineReverse.jsx:
--------------------------------------------------------------------------------
1 | import randomColor from 'randomcolor';
2 | import options from '../utils/lineOptions';
3 |
4 | /**
5 | * Returns chart data specific for Line chart type with reversed axis processing
6 | *
7 | * @param {object} data chart data
8 | * @param {array} colors array of colors used to present data
9 | */
10 | const getLineReverseChartData = (data, colors) => {
11 | const chartData = {
12 | labels: [],
13 | datasets: [],
14 | info: [],
15 | annotations: [],
16 | options,
17 | colors: [],
18 | };
19 |
20 | let columnCount = 0;
21 | let colorIndex = 0;
22 | data.forEach((element, rowindex) => {
23 | if (columnCount === 0) {
24 | columnCount = element.length;
25 | }
26 | element.forEach((value, colindex) => {
27 | const numericalValue = value.replace(/[^\d.-]/g, '');
28 | if (rowindex === 0) {
29 | if (value && value.length > 0) {
30 | chartData.labels.push(value);
31 | }
32 | } else if (colindex === 0) {
33 | if (value && value.length > 0) {
34 | const object = { data: [] };
35 | let color = colors[colorIndex];
36 | if (!colors || colorIndex >= colors.length) {
37 | color = randomColor();
38 | }
39 | colorIndex += 1;
40 | object.borderColor = color;
41 | object.backgroundColor = color;
42 | object.pointBorderColor = color;
43 | object.pointBackgroundColor = color;
44 | object.pointHoverBackgroundColor = color;
45 | object.pointHoverBorderColor = color;
46 | object.fill = false;
47 | object.label = value;
48 | chartData.datasets.push(object);
49 |
50 | chartData.colors.push(color);
51 | }
52 | } else if (chartData.datasets[rowindex - 1]) {
53 | chartData.datasets[rowindex - 1].data.push(numericalValue);
54 | }
55 | });
56 | });
57 |
58 | chartData.options.scales.xAxes[0].labels = chartData.labels;
59 | return chartData;
60 | };
61 |
62 | export default getLineReverseChartData;
63 |
--------------------------------------------------------------------------------
/src/charts/pie.jsx:
--------------------------------------------------------------------------------
1 | import { randomColor } from 'randomcolor';
2 | import options from '../utils/pieOptions';
3 |
4 | /**
5 | * Returns chart data specific for Pie chart type
6 | *
7 | * @param {object} data chart data
8 | * @param {boolean} semi true or false
9 | * @param {array} colors array of colors used to present data
10 | */
11 | const getPieChartData = (data, semi, colors) => {
12 | const chartData = {
13 | data: {
14 | datasets: [],
15 | labels: [],
16 | },
17 | options,
18 | colors: [],
19 | };
20 |
21 | if (semi) {
22 | chartData.options.circumference = Math.PI;
23 | chartData.options.rotation = -Math.PI;
24 | } else {
25 | chartData.options.circumference = 2 * Math.PI;
26 | chartData.options.rotation = 0;
27 | }
28 |
29 | let colorIndex = 0;
30 | data.forEach((element, rowindex) => {
31 | element.forEach((value, colindex) => {
32 | const numericalValue = value.replace(/[^\d.-]/g, '');
33 | if (rowindex === 0) {
34 | if (value && value.length > 0) {
35 | const object = { data: [], backgroundColor: [], label: '' };
36 | object.label = value;
37 | chartData.data.datasets.push(object);
38 | }
39 | } else if (colindex === 0) {
40 | chartData.data.labels.push(value);
41 | } else {
42 | let color = colors[colorIndex];
43 | if (!colors || colorIndex >= colors.length) {
44 | color = randomColor();
45 | }
46 | colorIndex += 1;
47 | if (chartData.data.datasets[colindex - 1]) {
48 | chartData.data.datasets[colindex - 1].backgroundColor.push(color);
49 | chartData.data.datasets[colindex - 1].data.push(numericalValue);
50 | }
51 | chartData.colors.push(color);
52 | }
53 | });
54 | });
55 |
56 | const finalDatasets = [];
57 | chartData.data.datasets.forEach(dataset => {
58 | if (dataset.data.length > 0) {
59 | finalDatasets.push(dataset);
60 | }
61 | });
62 |
63 | chartData.data.datasets = finalDatasets;
64 |
65 | return chartData;
66 | };
67 |
68 | export default getPieChartData;
69 |
--------------------------------------------------------------------------------
/src/charts/pieReverse.jsx:
--------------------------------------------------------------------------------
1 | import { randomColor } from 'randomcolor';
2 | import options from '../utils/pieOptions';
3 |
4 | /**
5 | * Returns chart data specific for Pie chart type with reversed axis processing
6 | *
7 | * @param {object} data chart data
8 | * @param {boolean} semi true or false
9 | * @param {array} colors array of colors used to present data
10 | */
11 | const getPieReverseChartData = (data, semi, colors) => {
12 | const chartData = {
13 | data: {
14 | datasets: [],
15 | labels: [],
16 | },
17 | options,
18 | colors: [],
19 | };
20 |
21 | if (semi) {
22 | chartData.options.circumference = Math.PI;
23 | chartData.options.rotation = -Math.PI;
24 | } else {
25 | chartData.options.circumference = 2 * Math.PI;
26 | chartData.options.rotation = 0;
27 | }
28 |
29 | let colorIndex = 0;
30 | data.forEach((element, rowindex) => {
31 | element.forEach((value, colindex) => {
32 | const numericalValue = value.replace(/[^\d.-]/g, '');
33 | if (colindex === 0) {
34 | if (value && value.length > 0) {
35 | const object = { data: [], backgroundColor: [], label: '' };
36 | object.label = value;
37 | chartData.data.datasets.push(object);
38 | if (chartData.data.labels.length === 0) {
39 | chartData.data.labels.push(value);
40 | }
41 | }
42 | } else if (rowindex === 0) {
43 | chartData.data.labels.push(value);
44 | } else {
45 | let color = colors[colorIndex];
46 | if (!colors || colorIndex >= colors.length) {
47 | color = randomColor();
48 | }
49 | colorIndex += 1;
50 | if (chartData.data.datasets[rowindex - 1]) {
51 | chartData.data.datasets[rowindex - 1].backgroundColor.push(color);
52 | chartData.data.datasets[rowindex - 1].data.push(numericalValue);
53 | }
54 | chartData.colors.push(color);
55 | }
56 | });
57 | });
58 |
59 | const finalDatasets = [];
60 | chartData.data.datasets.forEach(dataset => {
61 | if (dataset.data.length > 0) {
62 | finalDatasets.push(dataset);
63 | }
64 | });
65 |
66 | chartData.data.datasets = finalDatasets;
67 | return chartData;
68 | };
69 |
70 | export default getPieReverseChartData;
71 |
--------------------------------------------------------------------------------
/src/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch, Route, Redirect } from 'react-router-dom';
3 | import Footer from './Footer';
4 | import Home from '../screens/Home';
5 | import '../styles/App.scss';
6 |
7 | const App = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/src/components/Chart.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Line, Bar, HorizontalBar, Pie, Doughnut } from 'react-chartjs-2';
4 | import { Button, Alert } from 'react-bootstrap';
5 | import domtoimage from 'dom-to-image';
6 | import getLineChartData from '../charts/line';
7 | import getLineReverseChartData from '../charts/lineReverse';
8 | import getHorizontalBarChartData from '../charts/horizontalBar';
9 | import getHorizontalBarReverseChartData from '../charts/horizontalBarReverse';
10 | import getBarChartData from '../charts/bar';
11 | import getBarReverseChartData from '../charts/barReverse';
12 | import getPieChartData from '../charts/pie';
13 | import getPieReverseChartData from '../charts/pieReverse';
14 | import { DEMO_SHEETID } from '../constants';
15 | import handleOptions from '../utils/handleOptions';
16 |
17 | /**
18 | * Chart component
19 | */
20 | const Chart = ({ cdata, activeSheet, sheetId }) => {
21 | /**
22 | * Export image to PNG with transparency
23 | */
24 | const saveImagePng = () => {
25 | domtoimage
26 | .toPng(document.getElementsByClassName('chartjs-render-monitor')[0])
27 | .then(dataUrl => {
28 | const link = document.createElement('a');
29 | link.download = 'my-chart.png';
30 | link.href = dataUrl;
31 | link.click();
32 | });
33 | };
34 |
35 | /**
36 | * Support small screens
37 | *
38 | * @param {number} screenWidth screen width
39 | */
40 | const getSmallScreenChartDimensions = screenWidth => {
41 | const width = (100 * screenWidth) / 100;
42 | const height = (95 * width) / 100;
43 | return [width, height];
44 | };
45 |
46 | const {
47 | xsuffix,
48 | ysuffix,
49 | startFrom,
50 | title,
51 | flipAxis,
52 | type,
53 | colors,
54 | stacked,
55 | data,
56 | } = cdata;
57 |
58 | let chartData = {};
59 | let datasets = {};
60 | const columnCount = data[0].length;
61 | const rowCount = data.length;
62 |
63 | let dimensions = [];
64 | let maintainAspectRatio = true;
65 | if (window.innerWidth < 900) {
66 | maintainAspectRatio = false;
67 | dimensions = getSmallScreenChartDimensions(window.innerWidth);
68 | }
69 |
70 | let chartTitle = activeSheet;
71 | if (title && title.length > 0) {
72 | chartTitle = title;
73 | }
74 |
75 | const flip =
76 | (rowCount > columnCount && flipAxis) ||
77 | (rowCount <= columnCount && !flipAxis);
78 | const chartKey = `${type} ${chartTitle} ${startFrom} ${flipAxis} ${stacked} ${xsuffix} ${ysuffix}`;
79 | let chart;
80 | switch (type) {
81 | case 'line':
82 | if (flip) {
83 | chartData = getLineReverseChartData(data, colors);
84 | } else {
85 | chartData = getLineChartData(data, colors);
86 | }
87 |
88 | handleOptions(
89 | chartData.options,
90 | maintainAspectRatio,
91 | chartTitle,
92 | startFrom,
93 | xsuffix,
94 | ysuffix,
95 | );
96 |
97 | datasets = { datasets: chartData.datasets, labels: chartData.labels };
98 | chart = (
99 |
100 | );
101 | break;
102 | case 'bar':
103 | if (flip) {
104 | chartData = getBarReverseChartData(data, stacked, colors);
105 | } else {
106 | chartData = getBarChartData(data, stacked, colors);
107 | }
108 |
109 | handleOptions(
110 | chartData.options,
111 | maintainAspectRatio,
112 | chartTitle,
113 | startFrom,
114 | xsuffix,
115 | ysuffix,
116 | );
117 |
118 | datasets = { datasets: chartData.datasets, labels: chartData.labels };
119 | chart = (
120 |
121 | );
122 | break;
123 | case 'horizontalBar':
124 | if (flip) {
125 | chartData = getHorizontalBarReverseChartData(data, colors);
126 | } else {
127 | chartData = getHorizontalBarChartData(data, colors);
128 | }
129 |
130 | handleOptions(
131 | chartData.options,
132 | maintainAspectRatio,
133 | chartTitle,
134 | startFrom,
135 | xsuffix,
136 | ysuffix,
137 | );
138 |
139 | datasets = { datasets: chartData.datasets, labels: chartData.labels };
140 | chart = (
141 |
146 | );
147 | break;
148 | case 'pie':
149 | if (flip) {
150 | chartData = getPieReverseChartData(data, false, colors);
151 | } else {
152 | chartData = getPieChartData(data, false, colors);
153 | }
154 |
155 | chartData.options.maintainAspectRatio = maintainAspectRatio;
156 | chartData.options.title.text = chartTitle;
157 | chart = (
158 |
159 | );
160 | break;
161 | case 'semi-pie':
162 | if (flip) {
163 | chartData = getPieReverseChartData(data, true, colors);
164 | } else {
165 | chartData = getPieChartData(data, true, colors);
166 | }
167 |
168 | chartData.options.maintainAspectRatio = maintainAspectRatio;
169 | chartData.options.title.text = chartTitle;
170 | chart = (
171 |
172 | );
173 | break;
174 | case 'doughnut':
175 | if (flip) {
176 | chartData = getPieReverseChartData(data, false, colors);
177 | } else {
178 | chartData = getPieChartData(data, false, colors);
179 | }
180 |
181 | chartData.options.maintainAspectRatio = maintainAspectRatio;
182 | chartData.options.title.text = chartTitle;
183 | chart = (
184 |
189 | );
190 | break;
191 | case 'semi-doughnut':
192 | if (flip) {
193 | chartData = getPieReverseChartData(data, true, colors);
194 | } else {
195 | chartData = getPieChartData(data, true, colors);
196 | }
197 |
198 | chartData.options.maintainAspectRatio = maintainAspectRatio;
199 | chartData.options.title.text = chartTitle;
200 | chart = (
201 |
206 | );
207 | break;
208 |
209 | default:
210 | break;
211 | }
212 |
213 | let style = {};
214 | if (dimensions.length > 0) {
215 | style = {
216 | width: dimensions[0],
217 | height: dimensions[1],
218 | marginBottom: '30px',
219 | };
220 | }
221 |
222 | let sheeturl;
223 | if (sheetId === DEMO_SHEETID) {
224 | sheeturl = `https://docs.google.com/spreadsheets/d/${DEMO_SHEETID}/edit`;
225 | }
226 |
227 | return (
228 |
229 | {sheeturl && (
230 |
235 | Voila!
236 |
237 | This chart was generated using this{' '}
238 |
239 | Google Spreadsheet
240 |
241 |
242 | Click the green Save button or scroll down and edit this chart!
243 |
244 | )}
245 |
246 |
247 | {
249 | saveImagePng();
250 | }}
251 | variant="outline-secondary"
252 | >
253 | SAVE
254 |
255 |
256 | {chart}
257 |
258 |
259 | );
260 | };
261 |
262 | const mapStateToProps = state => ({
263 | cdata: state.chartData,
264 | activeSheet: state.sheetData.activeSheet,
265 | sheetId: state.sheetData.sheetId,
266 | });
267 |
268 | export default connect(mapStateToProps)(Chart);
269 |
--------------------------------------------------------------------------------
/src/components/ChartEditor.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Button, Card } from 'react-bootstrap';
4 | import { SketchPicker } from 'react-color';
5 | import { cloneDeep } from 'lodash';
6 | import SheetPicker from './SheetPicker';
7 | import { setStartAndEnd } from '../actions/sheetData';
8 | import {
9 | setChartType,
10 | setActiveColor,
11 | setChartColors,
12 | setStartFrom,
13 | setChartTitle,
14 | setFlipAxis,
15 | setxSuffix,
16 | setySuffix,
17 | } from '../actions/chartData';
18 |
19 | let start = '';
20 | let end = '';
21 | let activeInputIndex = -1;
22 | let gtitle = '';
23 | let gstartFrom;
24 | let gxsuffix = '';
25 | let gysuffix = '';
26 |
27 | /**
28 | * Chart editor component
29 | */
30 | const ChartEditor = ({ chartData, activeSheet, dispatch }) => {
31 | if (chartData.data.length === 0 || activeSheet === '') {
32 | return '';
33 | }
34 |
35 | /**
36 | * Check if string is a hexadecimal color
37 | * example of accepted values: #f0f #f1f1f1
38 | *
39 | * @param {string} hex
40 | */
41 | const isHex = hex => {
42 | return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
43 | };
44 |
45 | /**
46 | * Set start from for the values axis.
47 | * example: if minimum value is 10000, user might choose to start the Y Axis from 9000 instead of 0
48 | * Handles both dispatch and updating the attribute
49 | *
50 | * @param {number} val
51 | */
52 | const updateAxisStartFrom = val => {
53 | if (!val) {
54 | if (!Number.isNaN(gstartFrom)) {
55 | dispatch(setStartFrom(gstartFrom));
56 | }
57 | } else {
58 | gstartFrom = val;
59 | }
60 | };
61 |
62 | /**
63 | * Update chart title
64 | * Handles both dispatch and updating the attribute
65 | *
66 | * @param {string} val
67 | */
68 | const updateTitle = val => {
69 | if (!val) {
70 | dispatch(setChartTitle(gtitle));
71 | } else {
72 | gtitle = val;
73 | }
74 | };
75 |
76 | /**
77 | * Update y axis labels suffix
78 | * Handles both dispatch and updating the attribute
79 | *
80 | * @param {string} val
81 | */
82 | const updateySuffix = val => {
83 | if (!val && val !== '') {
84 | dispatch(setySuffix(gysuffix));
85 | } else {
86 | gysuffix = val;
87 | }
88 | };
89 |
90 | /**
91 | * Update x axis labels suffix
92 | * Handles both dispatch and updating the attribute
93 | *
94 | * @param {string} val
95 | */
96 | const updatexSuffix = val => {
97 | if (!val && val !== '') {
98 | dispatch(setxSuffix(gxsuffix));
99 | } else {
100 | gxsuffix = val;
101 | }
102 | };
103 |
104 | /**
105 | * Flip axis, rows and cols
106 | */
107 | const flipAxis = () => {
108 | dispatch(setFlipAxis(!chartData.flipAxis));
109 | };
110 |
111 | /**
112 | * Change active color
113 | *
114 | * @param {string} color
115 | * @param {number} index
116 | */
117 | const handleColorFocus = (color, index) => {
118 | activeInputIndex = index;
119 | if (isHex(color)) {
120 | dispatch(setActiveColor(color));
121 | }
122 | };
123 |
124 | /**
125 | * Update sheet start cell, e.g.: A5
126 | *
127 | * @param {string} val
128 | */
129 | const updateStart = val => {
130 | start = val.toUpperCase();
131 | };
132 |
133 | /**
134 | * Update sheet end cell, e.g.: E15
135 | *
136 | * @param {string} val
137 | */
138 | const updateEnd = val => {
139 | end = val.toUpperCase();
140 | };
141 |
142 | /**
143 | * Handle color change
144 | * Used for both manual input field and color picker
145 | *
146 | * @param {string} color
147 | */
148 | const handleColorChange = color => {
149 | let hex = color;
150 | if (color && color.hex) {
151 | ({ hex } = color);
152 | }
153 | if (isHex(hex) && activeInputIndex > -1) {
154 | const colors = cloneDeep(chartData.colors);
155 | colors[activeInputIndex] = hex;
156 | const data = {
157 | color: hex,
158 | colors,
159 | };
160 | dispatch(setChartColors(data));
161 | }
162 | };
163 |
164 | /**
165 | * Change start and end cells
166 | */
167 | const reload = () => {
168 | const regex = /([a-zA-Z0-9]+)/gi;
169 | const startmatches = start.match(regex);
170 | const endmatches = end.match(regex);
171 | if (startmatches && endmatches) {
172 | const data = {
173 | start,
174 | end,
175 | };
176 | dispatch(setStartAndEnd(data));
177 | }
178 | };
179 |
180 | // Generate enough color fields for available datasets
181 | const colorPickers = [];
182 | let max = chartData.data.length;
183 | chartData.data.forEach(dataset => {
184 | if (dataset.length > max) {
185 | max = dataset.length;
186 | }
187 | });
188 | for (let i = 0; i < max; i += 1) {
189 | const color = chartData.colors[i];
190 | const style = { background: color };
191 | colorPickers.push(
192 |
193 |
194 |
195 |
196 |
handleColorFocus(evt.target.value, i, dispatch)}
201 | onChange={evt => handleColorChange(evt.target.value, dispatch)}
202 | />
203 |
,
204 | );
205 | }
206 |
207 | if (start.length === 0) {
208 | ({ start } = chartData);
209 | }
210 |
211 | if (end.length === 0) {
212 | ({ end } = chartData);
213 | }
214 |
215 | // set chart title to sheet title, unless user provided a title
216 | let title = activeSheet;
217 | if (chartData.title && chartData.title.length > 0) {
218 | ({ title } = chartData);
219 | }
220 |
221 | return (
222 |
223 |
224 |
225 | Chart Type
226 |
227 | dispatch(setChartType('line'))}
231 | >
232 | Line
233 |
234 | dispatch(setChartType('bar'))}
238 | >
239 | Bar
240 |
241 | dispatch(setChartType('horizontalBar'))}
245 | >
246 | Horizontal Bar
247 |
248 | dispatch(setChartType('stacked'))}
252 | >
253 | Stacked
254 |
255 | dispatch(setChartType('pie'))}
259 | >
260 | Pie
261 |
262 | dispatch(setChartType('semi-pie'))}
266 | >
267 | Semi Pie
268 |
269 | dispatch(setChartType('doughnut'))}
273 | >
274 | Doughnut
275 |
276 | dispatch(setChartType('semi-doughnut'))}
279 | >
280 | Semi Doughnot
281 |
282 |
283 |
284 |
285 |
286 | Options
287 |
288 |
289 |
290 | Start From
291 |
292 |
updateAxisStartFrom(evt.target.value)}
299 | />
300 |
updateAxisStartFrom()}
304 | >
305 | Apply
306 |
307 |
308 |
309 |
310 | Start from {chartData.startFrom} while displaying values{' '}
311 | {chartData.startFrom !== 0 && ', default: 0'}
312 |
313 |
314 |
315 |
316 | flipAxis()}>
317 | Flip Rows & Cols
318 |
319 |
320 | Rows become Cols and Cols become Rows, all is one.
321 |
322 |
323 |
324 |
325 |
326 |
327 | Chart Title
328 |
329 |
updateTitle(evt.target.value)}
336 | />
337 |
updateTitle()}
341 | >
342 | Apply
343 |
344 |
345 |
346 | Change the title above the chart
347 |
348 |
349 |
350 |
351 |
352 | Y Suffix
353 |
354 |
updateySuffix(evt.target.value)}
362 | />
363 |
updateySuffix()}
367 | >
368 | Apply
369 |
370 |
371 |
372 |
373 |
374 | X Suffix
375 |
376 |
updatexSuffix(evt.target.value)}
384 | />
385 |
updatexSuffix()}
389 | >
390 | Apply
391 |
392 |
393 |
394 | Add a Suffix to X-axis or Y-axis labels
395 |
396 |
397 |
398 |
399 |
400 | Grid
401 |
402 |
403 |
404 | From
405 |
406 |
updateStart(evt.target.value)}
413 | />
414 |
415 |
416 |
417 |
418 | To
419 |
420 |
updateEnd(evt.target.value)}
427 | />
428 |
429 |
430 | reload(dispatch)}>
431 | Apply
432 |
433 |
434 |
435 |
436 |
437 | Colors
438 |
439 |
440 | {colorPickers}
441 |
442 |
443 |
444 | );
445 | };
446 |
447 | const mapStateToProps = state => ({
448 | chartData: state.chartData,
449 | activeSheet: state.sheetData.activeSheet,
450 | });
451 |
452 | export default connect(mapStateToProps)(ChartEditor);
453 |
--------------------------------------------------------------------------------
/src/components/ChartView.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import InfoView from './InfoView';
4 | import Chart from './Chart';
5 |
6 | /**
7 | * Chart View component, displays chart if data is available; Info View otherwise
8 | */
9 | const ChartView = ({ cdata, appStatus }) => {
10 | const { data } = cdata;
11 |
12 | if (
13 | data &&
14 | data.length > 0 &&
15 | !appStatus.fetchingData &&
16 | !appStatus.authError &&
17 | !appStatus.error
18 | ) {
19 | return ;
20 | }
21 |
22 | return ;
23 | };
24 |
25 | const mapStateToProps = state => ({
26 | cdata: state.chartData,
27 | appStatus: state.appStatus,
28 | });
29 |
30 | export default connect(mapStateToProps)(ChartView);
31 |
--------------------------------------------------------------------------------
/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () => (
4 |
5 |
6 |
7 | 🔬 A Labs project from your friends at
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/src/components/InfoView.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Alert, Button } from 'react-bootstrap';
4 | import { PulseLoader } from 'react-spinners';
5 | import { setSheetId } from '../actions/sheetData';
6 | import { DEMO_SHEETID } from '../constants';
7 |
8 | /**
9 | * InfoView component, displayed when there's no chart yet
10 | */
11 | const InfoView = ({ appStatus, dispatch }) => {
12 | return (
13 |
14 | {appStatus.authError && (
15 |
16 | Oh snap!
17 |
18 | It looks like your Spreadsheet is private, please change its access
19 | to Anyone with the link and then try again
20 |
21 |
22 | )}
23 | {appStatus.error && (
24 |
25 | Oh snap!
26 | It looks like there is a connection issue, please try again.
27 |
28 | )}
29 | {!appStatus.fetchingData && (
30 |
31 | Hey, you wanna create some charts ?
32 |
33 | After you paste in the URL to a public Google spreadsheet, you’ll be
34 | able to change the grid, colors, labels, legends, and other cool
35 | stuff
36 |
37 |
38 | Make sure your spreadsheet follows the{' '}
39 |
44 | correct format!
45 |
46 |
47 |
48 |
49 | dispatch(setSheetId(DEMO_SHEETID))}
51 | variant="outline-success"
52 | >
53 | No Spreadsheet yet? Click me!
54 |
55 |
56 |
57 | )}
58 | {appStatus.fetchingData && (
59 |
68 | )}
69 |
70 | );
71 | };
72 |
73 | const mapStateToProps = state => ({
74 | appStatus: state.appStatus,
75 | });
76 |
77 | export default connect(mapStateToProps)(InfoView);
78 |
--------------------------------------------------------------------------------
/src/components/SheetPicker.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { setActiveSheet } from '../actions/sheetData';
4 | import { resetChartData } from '../actions/chartData';
5 |
6 | /**
7 | * A radio button Sheet Picker having all available sheets
8 | */
9 | const SheetPicker = ({ sheetData, activeSheet, dispatch }) => {
10 | const activateSheet = val => {
11 | dispatch(resetChartData());
12 | dispatch(setActiveSheet(val));
13 | };
14 |
15 | const availableSheets = [];
16 | if (sheetData && sheetData.sheets) {
17 | sheetData.sheets.sheets.forEach(sheet => {
18 | let classname = 'btn btn-secondary';
19 | if (activeSheet === sheet.properties.title) {
20 | classname += ' active';
21 | }
22 | availableSheets.push(
23 |
28 | activateSheet(sheet.properties.title)}
34 | />
35 | {sheet.properties.title}
36 | ,
37 | );
38 | });
39 | }
40 | if (availableSheets && availableSheets.length > 0) {
41 | return (
42 |
43 |
44 | {availableSheets}
45 |
46 |
47 | );
48 | }
49 | return '';
50 | };
51 |
52 | const mapStateToProps = state => ({
53 | sheetData: state.sheetData,
54 | activeSheet: state.sheetData.activeSheet,
55 | });
56 |
57 | export default connect(mapStateToProps)(SheetPicker);
58 |
--------------------------------------------------------------------------------
/src/components/SpreadsheetPicker.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { setSheetId, resetSheetData } from '../actions/sheetData';
4 |
5 | /**
6 | * Spreadsheet Picker, an input field that triggers data fetching from Google sheets once it has valid URL
7 | */
8 | const SpreadsheetPicker = ({ dispatch }) => {
9 | /**
10 | * Google sheets URL validator, if successful, fetch data and plot chart
11 | *
12 | * @param {string} url spreadsheet url
13 | */
14 | const validateAndExecute = url => {
15 | const regex = /\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/gi;
16 | const matches = url.match(regex);
17 | if (matches && matches.length > 0) {
18 | const parts = matches[0].split('/');
19 | const sheetId = parts[parts.length - 1];
20 |
21 | dispatch(resetSheetData());
22 | dispatch(setSheetId(sheetId));
23 | }
24 | };
25 |
26 | return (
27 |
28 |
29 |
30 | Spreadsheet
31 |
32 |
33 |
validateAndExecute(evt.target.value, dispatch)}
40 | onKeyPress={evt => validateAndExecute(evt.target.value, dispatch)}
41 | />
42 |
43 | );
44 | };
45 |
46 | export default connect(null)(SpreadsheetPicker);
47 |
--------------------------------------------------------------------------------
/src/constants.jsx:
--------------------------------------------------------------------------------
1 | export const DEMO_SHEETID = '1RE_JYUCXBXY2LNV5Tp5GegLnMue-CpfTVMxjdudZ8Js';
2 | export const COLORS = 'colors';
3 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter as Router } from 'react-router-dom';
4 | import { createStore } from 'redux';
5 | import { Provider } from 'react-redux';
6 | import { randomColor } from 'randomcolor';
7 | import App from './components/App';
8 | import rootReducer from './reducers';
9 | import { COLORS } from './constants';
10 |
11 | /**
12 | * Generate and persist colors only once
13 | * Users may edit colors later on, changes are also persisted
14 | *
15 | * @param {number} num
16 | */
17 | const generateColors = num => {
18 | let colors = JSON.parse(localStorage.getItem(COLORS));
19 | if (!colors || colors.length < 300) {
20 | colors = [];
21 | for (let i = 0; i < num; i += 1) {
22 | colors.push(randomColor());
23 | }
24 | localStorage.setItem(COLORS, JSON.stringify(colors));
25 | }
26 |
27 | return colors;
28 | };
29 |
30 | const initialState = {
31 | chartData: {
32 | data: [],
33 | sheets: [],
34 | start: '',
35 | end: '',
36 | type: 'line',
37 | stacked: false,
38 | colors: generateColors(300),
39 | color: '',
40 | title: '',
41 | xsuffix: '',
42 | ysuffix: '',
43 | startFrom: 0,
44 | flipAxis: false,
45 | },
46 | sheetData: {
47 | sheetId: '',
48 | data: {},
49 | activeSheet: '',
50 | start: '',
51 | end: '',
52 | },
53 | appStatus: {
54 | fetchingData: false,
55 | authError: false,
56 | error: false,
57 | },
58 | };
59 | const store = createStore(rootReducer, initialState);
60 |
61 | ReactDOM.render(
62 |
63 |
64 |
65 |
66 | ,
67 | document.getElementById('root'),
68 | );
69 |
--------------------------------------------------------------------------------
/src/reducers/appStatus.js:
--------------------------------------------------------------------------------
1 | import {
2 | SET_FETCHING_DATA,
3 | SET_AUTH_ERROR,
4 | SET_ERROR,
5 | } from '../actions/appStatus';
6 |
7 | /**
8 | * Check appStatus actions for docs
9 | */
10 | const appStatus = (state = [], action) => {
11 | switch (action.type) {
12 | case SET_FETCHING_DATA: {
13 | return {
14 | ...state,
15 | authError: false,
16 | error: false,
17 | fetchingData: action.fetchingData,
18 | };
19 | }
20 | case SET_AUTH_ERROR: {
21 | return {
22 | ...state,
23 | fetchingData: false,
24 | authError: action.authError,
25 | };
26 | }
27 | case SET_ERROR: {
28 | return {
29 | ...state,
30 | fetchingData: false,
31 | error: action.error,
32 | };
33 | }
34 | default:
35 | return state;
36 | }
37 | };
38 |
39 | export default appStatus;
40 |
--------------------------------------------------------------------------------
/src/reducers/chartData.js:
--------------------------------------------------------------------------------
1 | import {
2 | RESET_CHART_DATA,
3 | SET_CHART_DATA,
4 | SET_CHART_TYPE,
5 | SET_CHART_COLORS,
6 | SET_ACTIVE_COLOR,
7 | SET_CHART_TITLE,
8 | SET_XSUFFIX,
9 | SET_YSUFFIX,
10 | SET_START_FROM,
11 | SET_FLIP_AXIS,
12 | } from '../actions/chartData';
13 |
14 | import { COLORS } from '../constants';
15 |
16 | /**
17 | * Check chartData actions for docs
18 | */
19 | const chartData = (state = [], action) => {
20 | switch (action.type) {
21 | case RESET_CHART_DATA: {
22 | return {
23 | ...state,
24 | data: [],
25 | start: '',
26 | end: '',
27 | type: 'line',
28 | stacked: false,
29 | colors: JSON.parse(localStorage.getItem(COLORS)),
30 | color: '',
31 | title: '',
32 | startFrom: 0,
33 | flipAxis: false,
34 | };
35 | }
36 | case SET_CHART_DATA: {
37 | return {
38 | ...state,
39 | data: action.data.data,
40 | start: action.data.start,
41 | end: action.data.end,
42 | };
43 | }
44 | case SET_CHART_COLORS: {
45 | localStorage.setItem(COLORS, JSON.stringify(action.data.colors));
46 | return {
47 | ...state,
48 | colors: action.data.colors,
49 | color: action.data.color,
50 | };
51 | }
52 | case SET_ACTIVE_COLOR: {
53 | return {
54 | ...state,
55 | color: action.color,
56 | };
57 | }
58 | case SET_CHART_TITLE: {
59 | return {
60 | ...state,
61 | title: action.title,
62 | };
63 | }
64 | case SET_XSUFFIX: {
65 | return {
66 | ...state,
67 | xsuffix: action.xsuffix,
68 | };
69 | }
70 | case SET_YSUFFIX: {
71 | return {
72 | ...state,
73 | ysuffix: action.ysuffix,
74 | };
75 | }
76 | case SET_START_FROM: {
77 | return {
78 | ...state,
79 | startFrom: action.startFrom,
80 | };
81 | }
82 | case SET_FLIP_AXIS: {
83 | return {
84 | ...state,
85 | flipAxis: action.flipAxis,
86 | };
87 | }
88 | case SET_CHART_TYPE: {
89 | let type = action.chartType;
90 | let stacked = false;
91 | if (action.chartType === 'stacked') {
92 | type = 'bar';
93 | stacked = true;
94 | }
95 | return {
96 | ...state,
97 | type,
98 | stacked,
99 | };
100 | }
101 | default:
102 | return state;
103 | }
104 | };
105 |
106 | export default chartData;
107 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import chartData from './chartData';
3 | import sheetData from './sheetData';
4 | import appStatus from './appStatus';
5 |
6 | export default combineReducers({
7 | chartData,
8 | sheetData,
9 | appStatus,
10 | });
11 |
--------------------------------------------------------------------------------
/src/reducers/sheetData.js:
--------------------------------------------------------------------------------
1 | import {
2 | SET_SHEET_DATA,
3 | RESET_SHEET_DATA,
4 | SET_SHEET_ID,
5 | SET_ACTIVE_SHEET,
6 | SET_START_AND_END,
7 | } from '../actions/sheetData';
8 |
9 | /**
10 | * Check sheetData actions for docs
11 | */
12 | const chartData = (state = [], action) => {
13 | switch (action.type) {
14 | case SET_SHEET_DATA: {
15 | const sheetTitle = action.data.sheets[0].properties.title;
16 | return {
17 | ...state,
18 | sheets: action.data,
19 | activeSheet: sheetTitle,
20 | start: '',
21 | end: '',
22 | };
23 | }
24 | case RESET_SHEET_DATA: {
25 | return {
26 | ...state,
27 | sheetId: '',
28 | data: {},
29 | activeSheet: '',
30 | start: '',
31 | end: '',
32 | };
33 | }
34 | case SET_ACTIVE_SHEET: {
35 | return {
36 | ...state,
37 | activeSheet: action.activeSheet,
38 | start: '',
39 | end: '',
40 | };
41 | }
42 | case SET_START_AND_END: {
43 | return {
44 | ...state,
45 | start: action.data.start,
46 | end: action.data.end,
47 | };
48 | }
49 | case SET_SHEET_ID: {
50 | return {
51 | ...state,
52 | sheetId: action.sheetId,
53 | };
54 | }
55 | default:
56 | return state;
57 | }
58 | };
59 |
60 | export default chartData;
61 |
--------------------------------------------------------------------------------
/src/screens/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import axios from 'axios';
4 | import ChartEditor from '../components/ChartEditor';
5 | import { setSheetData, setSheetId } from '../actions/sheetData';
6 | import { setFetchingData, setAuthError, setError } from '../actions/appStatus';
7 | import { setChartData } from '../actions/chartData';
8 | import SpreadsheetPicker from '../components/SpreadsheetPicker';
9 | import ChartView from '../components/ChartView';
10 | import processSpreadsheet from '../utils/processSpreadsheet';
11 |
12 | /**
13 | * Main screen
14 | */
15 | class Home extends Component {
16 | /**
17 | * Query Google sheets once the component mounts
18 | */
19 | componentDidMount() {
20 | this.runQuery();
21 | }
22 |
23 | /**
24 | * Check if app should run a new query on componentWillReceiveProps
25 | */
26 | componentWillReceiveProps(nextProps) {
27 | const { sheetId, activeSheet, start, end } = this.props;
28 | const {
29 | sheetId: nextSheetId,
30 | activeSheet: nextActiveSheet,
31 | start: nextStart,
32 | end: nextEnd,
33 | } = nextProps;
34 | if (
35 | sheetId !== nextSheetId ||
36 | activeSheet !== nextActiveSheet ||
37 | start !== nextStart ||
38 | end !== nextEnd
39 | ) {
40 | this.props = nextProps;
41 | this.runQuery();
42 | }
43 | }
44 |
45 | /**
46 | * Compose and run query using app state
47 | */
48 | runQuery() {
49 | const { sheetId, data, dispatch } = this.props;
50 | const { REACT_APP_CHART_TOKEN, REACT_APP_GSHEETS_API } = process.env;
51 | if (sheetId && sheetId.length > 5) {
52 | let url = `${REACT_APP_GSHEETS_API}${sheetId}?key=${REACT_APP_CHART_TOKEN}`;
53 | if (data.activeSheet.length > 0) {
54 | url = `${REACT_APP_GSHEETS_API}${sheetId}/values/${
55 | data.activeSheet
56 | }?key=${REACT_APP_CHART_TOKEN}`;
57 | }
58 | if (data.start.length > 0 && data.end.length > 0) {
59 | const grid = `!${data.start}:${data.end}`;
60 | url = `${REACT_APP_GSHEETS_API}${sheetId}/values/${
61 | data.activeSheet
62 | }${grid}?key=${REACT_APP_CHART_TOKEN}`;
63 | }
64 |
65 | dispatch(setFetchingData(true));
66 | axios
67 | .get(url)
68 | .then(res => {
69 | dispatch(setFetchingData(false));
70 | this.process(res);
71 | })
72 | .catch(error => {
73 | if (error.response && error.response.status === 403) {
74 | dispatch(setAuthError(true));
75 | } else {
76 | dispatch(setError(true));
77 | }
78 | dispatch(setSheetId(''));
79 | });
80 | }
81 | }
82 |
83 | /**
84 | * Process fetched data, try to find best data to plot
85 | * @param {object} res query result
86 | */
87 | process(res) {
88 | const { dispatch } = this.props;
89 |
90 | if (res.data && res.data.sheets) {
91 | dispatch(setSheetData(res.data));
92 | }
93 |
94 | if (res.data && res.data.values) {
95 | let processedData = processSpreadsheet(res.data.values);
96 | let done = false;
97 | while (processedData && processedData.data.length > 0 && !done) {
98 | const tempProcessedData = processSpreadsheet(
99 | res.data.values,
100 | processedData.startr + 1,
101 | processedData.startc + 1,
102 | );
103 | if (
104 | tempProcessedData &&
105 | tempProcessedData.data.length > processedData.data.length
106 | ) {
107 | processedData = tempProcessedData;
108 | } else {
109 | done = true;
110 | }
111 | }
112 |
113 | dispatch(setChartData(processedData));
114 | }
115 | }
116 |
117 | render() {
118 | return (
119 |
120 |
121 |
122 |
123 |
124 | );
125 | }
126 | }
127 |
128 | const mapStateToProps = state => ({
129 | sheetId: state.sheetData.sheetId,
130 | data: state.sheetData,
131 | activeSheet: state.sheetData.activeSheet,
132 | start: state.sheetData.start,
133 | end: state.sheetData.end,
134 | });
135 |
136 | export default connect(mapStateToProps)(Home);
137 |
--------------------------------------------------------------------------------
/src/static/images/robo-chart.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/postlight/robo-chart-web/b0b6f8e4306469b816020757e94c6254c82128e3/src/static/images/robo-chart.gif
--------------------------------------------------------------------------------
/src/static/images/robo-chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/postlight/robo-chart-web/b0b6f8e4306469b816020757e94c6254c82128e3/src/static/images/robo-chart.png
--------------------------------------------------------------------------------
/src/static/images/spreadsheet-format.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/postlight/robo-chart-web/b0b6f8e4306469b816020757e94c6254c82128e3/src/static/images/spreadsheet-format.png
--------------------------------------------------------------------------------
/src/styles/App.scss:
--------------------------------------------------------------------------------
1 | @import 'Variables';
2 |
3 | html {
4 | height: 100%;
5 | box-sizing: border-box;
6 | }
7 |
8 | *,
9 | *:before,
10 | *:after {
11 | box-sizing: inherit;
12 | }
13 |
14 | body {
15 | position: relative;
16 | margin: 0;
17 | padding: 0;
18 | background-color: $backgroud;
19 | min-height: 100%;
20 | font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI',
21 | Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
22 | 'Segoe UI Symbol';
23 | color: $font;
24 | -webkit-font-smoothing: antialiased;
25 | -moz-osx-font-smoothing: grayscale;
26 | }
27 |
28 | a {
29 | font-weight: bold;
30 | color: inherit !important;
31 | text-decoration: none !important;
32 | :hover {
33 | color: inherit;
34 | }
35 | }
36 |
37 | #root {
38 | min-height: 100%;
39 | }
40 |
41 | .form-inline {
42 | input {
43 | width: 100% !important;
44 | }
45 | }
46 |
47 | .sketch-picker {
48 | float: right;
49 | min-height: 300px;
50 | margin-bottom: 40px;
51 | }
52 |
53 | .color-input {
54 | float: left;
55 | }
56 |
57 | .grid-coord {
58 | max-width: 150px;
59 | float: left;
60 | padding-right: 20px;
61 | }
62 |
63 | .container {
64 | padding: $hpadding 0px;
65 | }
66 |
67 | .chart {
68 | max-width: 100%;
69 | max-height: 100%;
70 | margin: 0 auto;
71 | }
72 |
73 | .chart-editor {
74 | margin-top: $hpadding;
75 | margin-bottom: $hpadding;
76 | .grid-coord {
77 | max-width: 250px;
78 | float: unset;
79 | }
80 | .start-from {
81 | max-width: 320px;
82 | }
83 | .apply-button {
84 | margin-left: 10px;
85 | margin-bottom: 0px;
86 | }
87 | .apply-subtitle {
88 | margin-top: 20px;
89 | }
90 | }
91 |
92 | .footer-container {
93 | padding-top: 250px;
94 | footer {
95 | position: absolute;
96 | width: 100%;
97 | bottom: 0;
98 | color: $footer-color;
99 | padding-top: 80px;
100 | padding-bottom: 80px;
101 | background: $footer-background;
102 | text-align: center;
103 | span {
104 | display: inline-block;
105 | font-size: 17px;
106 | }
107 | .footer-gif {
108 | display: block;
109 | padding-top: 10px;
110 | width: 204px;
111 | margin: 0 auto;
112 | }
113 | }
114 | }
115 |
116 | .save-chart {
117 | position: relative;
118 | float: right;
119 | margin-right: 10px;
120 | .btn {
121 | padding: 5px 10px;
122 | background-color: $green;
123 | border: 0;
124 | color: white;
125 | }
126 | }
127 |
128 | .chartjs-render-monitor {
129 | background-color: #ffffff00;
130 | }
131 |
132 | .nav-pills {
133 | .nav-link.active {
134 | background: $active;
135 | color: $font;
136 | }
137 | .nav-link {
138 | color: $font;
139 | }
140 | }
141 |
142 | .loader {
143 | padding-left: 50%;
144 | margin-left: -28px;
145 | padding-top: 50px;
146 | padding-bottom: 50px;
147 | }
148 |
149 | .input-group-lg {
150 | margin-top: 0px !important;
151 | padding-top: $vpadding;
152 | }
153 |
154 | .sheets-container {
155 | width: 95%;
156 | margin: 0 auto;
157 | margin-top: $vpadding;
158 | margin-bottom: $vpadding;
159 | @include lg {
160 | width: $maxw;
161 | }
162 | }
163 |
164 | .card-body {
165 | .type-button {
166 | margin-right: 10px;
167 | }
168 | padding-right: 6px;
169 | padding-left: 6px;
170 | button {
171 | margin-bottom: 20px;
172 | }
173 |
174 | .grid-coord {
175 | margin-bottom: 20px;
176 | }
177 | padding-bottom: 0px;
178 | @include lg {
179 | padding-right: 20px;
180 | padding-left: 20px;
181 | }
182 | }
183 |
184 | .sheet-picker {
185 | overflow: auto;
186 | .btn-group {
187 | width: 100%;
188 | }
189 | }
190 |
191 | .shadow {
192 | box-shadow: 0 2px 8px 0 $shadow;
193 | border-radius: 5px;
194 | }
195 |
196 | .in-container {
197 | padding: $vpadding $hpadding;
198 | }
199 |
200 | .chart-header {
201 | margin-left: 28px;
202 | margin-top: $vpadding;
203 | margin-right: $hpadding;
204 | font-size: 25px;
205 | line-height: 22px;
206 | }
207 |
208 | .chart-info {
209 | font-size: 15px;
210 | }
211 |
212 | .flex {
213 | display: flex;
214 | }
215 |
216 | .wrap {
217 | flex-wrap: wrap;
218 | }
219 |
220 | .standard-charts {
221 | padding-bottom: $vpadding;
222 | }
223 |
224 | .horizontal-separator {
225 | border-left: 1px solid $seperator;
226 | margin-left: 10px;
227 | padding-left: 10px;
228 | }
229 |
230 | .navbar {
231 | .menu-item {
232 | margin-left: 20px;
233 | padding: 15px 10px;
234 | cursor: pointer;
235 | }
236 | .nav-item {
237 | cursor: pointer;
238 | }
239 | .logo {
240 | font-weight: bold;
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/src/styles/_Colors.scss:
--------------------------------------------------------------------------------
1 | $backgroud: white;
2 | $font: #40474d;
3 | $shadow: rgba(209, 198, 193, 0.7);
4 | $seperator: #eeeeee;
5 | $active: #e6e9ec;
6 | $green: #129f31;
7 | $footer-background: #262534;
8 | $footer-color: #828282;
9 |
--------------------------------------------------------------------------------
/src/styles/_Screens.scss:
--------------------------------------------------------------------------------
1 | // Small tablets and large smartphones (landscape view)
2 | $screen-sm-min: 576px;
3 |
4 | // Small tablets (portrait view)
5 | $screen-md-min: 768px;
6 |
7 | // Tablets and small desktops
8 | $screen-lg-min: 1140px;
9 |
10 | // Large tablets and desktops
11 | $screen-xl-min: 1800px;
12 |
13 | // Small devices
14 | @mixin sm {
15 | @media (min-width: #{$screen-sm-min}) {
16 | @content;
17 | }
18 | }
19 |
20 | // Medium devices
21 | @mixin md {
22 | @media (min-width: #{$screen-md-min}) {
23 | @content;
24 | }
25 | }
26 |
27 | // Large devices
28 | @mixin lg {
29 | @media (min-width: #{$screen-lg-min}) {
30 | @content;
31 | }
32 | }
33 |
34 | // Extra large devices
35 | @mixin xl {
36 | @media (min-width: #{$screen-xl-min}) {
37 | @content;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/styles/_Variables.scss:
--------------------------------------------------------------------------------
1 | @import 'Colors';
2 | @import 'Screens';
3 |
4 | $hpadding: 5px;
5 | $vpadding: 10px;
6 | @include lg {
7 | $hpadding: 20px;
8 | $vpadding: 30px;
9 | }
10 | $maxw: 1100px;
11 |
--------------------------------------------------------------------------------
/src/utils/handleOptions.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | /**
3 | * Update chartjs options object with the following arguments
4 | *
5 | * @param {object} options
6 | * @param {boolean} maintainAspectRatio
7 | * @param {string} chartTitle
8 | * @param {number} startFrom
9 | * @param {string} xsuffix
10 | * @param {string} ysuffix
11 | */
12 | const handleOptions = (
13 | options,
14 | maintainAspectRatio,
15 | chartTitle,
16 | startFrom,
17 | xsuffix,
18 | ysuffix,
19 | ) => {
20 | options.maintainAspectRatio = maintainAspectRatio;
21 | options.title.text = chartTitle;
22 | if (startFrom !== 0) {
23 | options.scales.xAxes[0].ticks.beginAtZero = false;
24 | options.scales.yAxes[0].ticks.beginAtZero = false;
25 | options.scales.xAxes[0].ticks.min = parseFloat(startFrom);
26 | options.scales.yAxes[0].ticks.min = parseFloat(startFrom);
27 | } else {
28 | options.scales.xAxes[0].ticks = { beginAtZero: true };
29 | options.scales.yAxes[0].ticks = { beginAtZero: true };
30 | }
31 |
32 | options.scales.xAxes[0].ticks.callback = value => {
33 | return `${value}${xsuffix}`;
34 | };
35 | options.scales.yAxes[0].ticks.callback = value => {
36 | return `${value}${ysuffix}`;
37 | };
38 | };
39 |
40 | export default handleOptions;
41 |
--------------------------------------------------------------------------------
/src/utils/horizontalBarOptions.jsx:
--------------------------------------------------------------------------------
1 | // predefined chart options for horizontal bar charts
2 | const options = {
3 | responsive: true,
4 | maintainAspectRatio: false,
5 | layout: {
6 | padding: {
7 | left: 10,
8 | right: 10,
9 | top: 10,
10 | bottom: 10,
11 | },
12 | },
13 | title: {
14 | display: false,
15 | text: '',
16 | fontSize: 20,
17 | padding: 20,
18 | },
19 | legend: {
20 | position: 'bottom',
21 | },
22 | tooltips: {
23 | mode: 'x',
24 | intersect: false,
25 | callbacks: {},
26 | },
27 | scales: {
28 | xAxes: [
29 | {
30 | display: true,
31 | gridLines: { display: false },
32 | labels: [],
33 | id: 'x-axis-1',
34 | ticks: {
35 | beginAtZero: true,
36 | },
37 | },
38 | ],
39 | yAxes: [
40 | {
41 | display: true,
42 | position: 'left',
43 | id: 'y-axis-1',
44 | ticks: {
45 | beginAtZero: true,
46 | },
47 | },
48 | ],
49 | },
50 | };
51 |
52 | export default options;
53 |
--------------------------------------------------------------------------------
/src/utils/lineOptions.jsx:
--------------------------------------------------------------------------------
1 | // predefined chart options for line charts
2 | const options = {
3 | responsive: true,
4 | maintainAspectRatio: false,
5 | layout: {
6 | padding: {
7 | left: 10,
8 | right: 10,
9 | top: 10,
10 | bottom: 10,
11 | },
12 | },
13 | hover: {
14 | mode: 'x',
15 | intersect: false,
16 | },
17 | title: {
18 | display: true,
19 | text: '',
20 | fontSize: 20,
21 | padding: 20,
22 | },
23 | legend: {
24 | position: 'bottom',
25 | },
26 | tooltips: {
27 | mode: 'x',
28 | intersect: false,
29 | callbacks: {},
30 | },
31 | scales: {
32 | xAxes: [
33 | {
34 | stacked: false,
35 | display: true,
36 | gridLines: { display: false },
37 | labels: [],
38 | id: 'x-axis-1',
39 | ticks: {
40 | beginAtZero: true,
41 | min: 0,
42 | },
43 | },
44 | ],
45 | yAxes: [
46 | {
47 | stacked: false,
48 | type: 'linear',
49 | display: true,
50 | position: 'left',
51 | id: 'y-axis-1',
52 | ticks: {
53 | beginAtZero: true,
54 | },
55 | gridLines: {
56 | zeroLineColor: '#888',
57 | zeroLineWidth: 2,
58 | display: true,
59 | },
60 | labels: {
61 | show: true,
62 | },
63 | },
64 | ],
65 | },
66 | };
67 |
68 | export default options;
69 |
--------------------------------------------------------------------------------
/src/utils/pieOptions.jsx:
--------------------------------------------------------------------------------
1 | // predefined chart options for pie charts
2 | const options = {
3 | responsive: true,
4 | maintainAspectRatio: false,
5 | circumference: 2 * Math.PI,
6 | rotation: -Math.PI / 2,
7 | title: {
8 | display: true,
9 | text: '',
10 | fontSize: 20,
11 | padding: 20,
12 | },
13 | };
14 |
15 | export default options;
16 |
--------------------------------------------------------------------------------
/src/utils/processSpreadsheet.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Spreadsheet processor brain
3 | *
4 | * Process values and stores them in an array
5 | * Use startr and startc to recursively check for better data to plot
6 | *
7 | * @param {array} values
8 | * @param {number} startr
9 | * @param {number} startc
10 | */
11 | const processSpreadsheet = (values, startr = 0, startc = 0) => {
12 | const processedData = {
13 | data: [],
14 | start: '',
15 | end: '',
16 | startr: 0,
17 | startc: 0,
18 | };
19 | let rowstart = -1;
20 | let colstart = 999999999;
21 | let colend = -1;
22 | let donerows = false;
23 | values.forEach((element, rowindex) => {
24 | if (rowindex >= startr) {
25 | if (element.length > 0 && !donerows) {
26 | if (rowstart < 0) {
27 | rowstart = rowindex + 1;
28 | }
29 | let donecols = false;
30 | const elements = [];
31 | element.forEach((value, colindex) => {
32 | if (colindex >= startc) {
33 | const trimmedValue = value.trim();
34 | if (!donecols) {
35 | if (trimmedValue.length > 0) {
36 | elements.push(trimmedValue);
37 | if (colindex < colstart) {
38 | colstart = colindex;
39 | }
40 | if (colindex > colend) {
41 | colend = colindex;
42 | }
43 | } else if (elements.length > 0) {
44 | donecols = true;
45 | }
46 | }
47 | }
48 | });
49 | processedData.data.push(elements);
50 | } else if (rowstart > 0) {
51 | donerows = true;
52 | }
53 | }
54 | });
55 |
56 | const alphabet = 'abcdefghijklmnopqrstuvwxyz'.toUpperCase().split('');
57 | const rowend = rowstart + processedData.data.length - 1;
58 | const gridStart = alphabet[colstart] + rowstart;
59 | const gridEnd = alphabet[colend] + rowend;
60 | processedData.startr = rowstart;
61 | processedData.startc = colstart;
62 |
63 | processedData.start = gridStart;
64 | processedData.end = gridEnd;
65 |
66 | return processedData;
67 | };
68 |
69 | export default processSpreadsheet;
70 |
--------------------------------------------------------------------------------