├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── bower.json
├── docs
├── bundle.js
├── bundle.js.map
└── index.html
├── examples
├── .stylintrc
├── index.html
├── index.jsx
└── webpack.config.js
├── package.json
├── scripts
└── bowersync
├── src
├── Gradient.jsx
├── hashid.js
├── index.jsx
└── transition-polyfill.js
└── test
└── index.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"],
3 | "plugins": [
4 | "transform-decorators-legacy"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "trendmicro",
3 | "parser": "babel-eslint",
4 | "env": {
5 | "browser": true,
6 | "node": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | /.nyc_output
4 | /coverage
5 | /lib
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | /.nyc_output
3 | /coverage
4 | /releases
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 | group: edge
4 |
5 | language: node_js
6 |
7 | os:
8 | - linux
9 |
10 | node_js:
11 | - '6'
12 | - '5'
13 | - '4'
14 |
15 | before_install:
16 | - npm install -g npm
17 | - npm --version
18 |
19 | after_success:
20 | - npm run coveralls
21 | - npm run coverage-clean
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Trend Micro Inc.
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 | # react-liquid-gauge [](https://travis-ci.org/trendmicro-frontend/react-liquid-gauge) [](https://coveralls.io/github/trendmicro-frontend/react-liquid-gauge?branch=master)
2 |
3 | [](https://www.npmjs.com/package/react-liquid-gauge)
4 |
5 | React Liquid Gauge component. It's heavily inspired by [D3 Liquid Fill Gauge](http://bl.ocks.org/brattonc/5e5ce9beee483220e2f6) and [react-liquidchart](https://github.com/arnthor3/react-liquidchart).
6 |
7 | [](http://trendmicro-frontend.github.io/react-liquid-gauge)
8 |
9 | Demo: http://trendmicro-frontend.github.io/react-liquid-gauge
10 |
11 | The [sample code](https://github.com/trendmicro-frontend/react-liquid-gauge/blob/master/examples/index.jsx) can be found in the [examples](https://github.com/trendmicro-frontend/react-liquid-gauge/tree/master/examples) directory.
12 |
13 | ## Installation
14 |
15 | ```
16 | npm install --save react react-dom react-liquid-gauge
17 | ```
18 |
19 | ### Usage
20 |
21 | ```js
22 | import { color } from 'd3-color';
23 | import { interpolateRgb } from 'd3-interpolate';
24 | import React, { Component } from 'react';
25 | import ReactDOM from 'react-dom';
26 | import LiquidFillGauge from 'react-liquid-gauge';
27 |
28 | class App extends Component {
29 | state = {
30 | value: 50
31 | };
32 | startColor = '#6495ed'; // cornflowerblue
33 | endColor = '#dc143c'; // crimson
34 |
35 | render() {
36 | const radius = 200;
37 | const interpolate = interpolateRgb(this.startColor, this.endColor);
38 | const fillColor = interpolate(this.state.value / 100);
39 | const gradientStops = [
40 | {
41 | key: '0%',
42 | stopColor: color(fillColor).darker(0.5).toString(),
43 | stopOpacity: 1,
44 | offset: '0%'
45 | },
46 | {
47 | key: '50%',
48 | stopColor: fillColor,
49 | stopOpacity: 0.75,
50 | offset: '50%'
51 | },
52 | {
53 | key: '100%',
54 | stopColor: color(fillColor).brighter(0.5).toString(),
55 | stopOpacity: 0.5,
56 | offset: '100%'
57 | }
58 | ];
59 |
60 | return (
61 |
62 |
{
72 | const value = Math.round(props.value);
73 | const radius = Math.min(props.height / 2, props.width / 2);
74 | const textPixels = (props.textSize * radius / 2);
75 | const valueStyle = {
76 | fontSize: textPixels
77 | };
78 | const percentStyle = {
79 | fontSize: textPixels * 0.6
80 | };
81 |
82 | return (
83 |
84 | {value}
85 | {props.percent}
86 |
87 | );
88 | }}
89 | riseAnimation
90 | waveAnimation
91 | waveFrequency={2}
92 | waveAmplitude={1}
93 | gradient
94 | gradientStops={gradientStops}
95 | circleStyle={{
96 | fill: fillColor
97 | }}
98 | waveStyle={{
99 | fill: fillColor
100 | }}
101 | textStyle={{
102 | fill: color('#444').toString(),
103 | fontFamily: 'Arial'
104 | }}
105 | waveTextStyle={{
106 | fill: color('#fff').toString(),
107 | fontFamily: 'Arial'
108 | }}
109 | onClick={() => {
110 | this.setState({ value: Math.random() * 100 });
111 | }}
112 | />
113 |
119 | {
123 | this.setState({ value: Math.random() * 100 });
124 | }}
125 | >
126 | Refresh
127 |
128 |
129 |
130 | );
131 | }
132 | }
133 |
134 | ReactDOM.render(
135 | ,
136 | document.getElementById('container')
137 | );
138 | ```
139 |
140 | ## API
141 |
142 | ### Properties
143 |
144 |
145 |
146 |
147 | Name
148 | Type
149 | Default
150 | Description
151 |
152 |
153 |
154 |
155 | id
156 | String
157 |
158 | A unique identifier (ID) to identify the element. Defaults to a unique random string.
159 |
160 |
161 | width
162 | Number
163 | 400
164 | The width of the component.
165 |
166 |
167 | height
168 | Number
169 | 400
170 | The height of the component.
171 |
172 |
173 | value
174 | Number
175 | 0
176 | The percent value (0-100).
177 |
178 |
179 | percent
180 | String|Node
181 | '%'
182 | The percent string (%) or SVG text element.
183 |
184 |
185 | textSize
186 | Number
187 | 1
188 | The relative height of the text to display in the wave circle. A value of 1 equals 50% of the radius of the outer circle.
189 |
190 |
191 | textOffsetX
192 | Number
193 | 0
194 |
195 |
196 |
197 | textOffsetY
198 | Number
199 | 0
200 |
201 |
202 |
203 | textRenderer
204 | Function(props)
205 |
206 | Specifies a custom text renderer for rendering a percent value.
207 |
208 |
209 | riseAnimation
210 | Boolean
211 | false
212 | Controls if the wave should rise from 0 to it's full height, or start at it's full height.
213 |
214 |
215 | riseAnimationTime
216 | Number
217 | 2000
218 | The amount of time in milliseconds for the wave to rise from 0 to it's final height.
219 |
220 |
221 | riseAnimationEasing
222 | String
223 | 'cubicInOut'
224 | d3-ease options. See the easing explorer for a visual demostration.
225 |
226 |
227 | riseAnimationOnProgress
228 | Function({ value, container })
229 |
230 | Progress callback function.
231 |
232 |
233 | riseAnimationOnComplete
234 | Function({ value, container })
235 |
236 | Complete callback function.
237 |
238 |
239 | waveAnimation
240 | Boolean
241 | false
242 | Controls if the wave scrolls or is static.
243 |
244 |
245 | waveAnimationTime
246 | Number
247 | 2000
248 | The amount of time in milliseconds for a full wave to enter the wave circle.
249 |
250 |
251 | waveAnimationEasing
252 | String
253 | 'linear'
254 | d3-ease options. See the easing explorer for a visual demostration.
255 |
256 |
257 | waveAmplitude
258 | Number
259 | 1
260 | The wave height as a percentage of the radius of the wave circle.
261 |
262 |
263 | waveFrequency
264 | Number
265 | 2
266 | The number of full waves per width of the wave circle.
267 |
268 |
269 | gradient
270 | Boolean
271 | false
272 | Whether to apply linear gradients to fill the wave circle.
273 |
274 |
275 | gradientStops
276 | Node|Array
277 |
278 | An array of the <stop> SVG element defines the ramp of colors to use on a gradient, which is a child element to either the <linearGradient> or the <radialGradient> element.
279 |
280 |
281 | onClick
282 | Function(event)
283 |
284 | onClick event handler.
285 |
286 |
287 | innerRadius
288 | Number
289 | 0.9
290 | The radius of the inner circle. A value of 0.9 equals 90% of the radius of the outer circle.
291 |
292 |
293 | outerRadius
294 | Number
295 | 1.0
296 | The radius of the outer circle. A value of 1 equals 100% of the radius of the outer circle.
297 |
298 |
299 | margin
300 | Number
301 | 0.025
302 | The size of the gap between the outer circle and wave circle as a percentage of the radius of the outer circle. A value of 0.025 equals 2.5% of the radius of the outer circle.
303 |
304 |
305 | circleStyle
306 | Object
307 |
308 | {
309 | fill: 'rgb(23, 139, 202)'
310 | }
311 |
312 | The style of the outer circle.
313 |
314 |
315 | waveStyle
316 | Object
317 |
318 | {
319 | fill: 'rgb(23, 139, 202)'
320 | }
321 |
322 | The style of the fill wave.
323 |
324 |
325 | textStyle
326 | Object
327 |
328 | {
329 | fill: 'rgb(0, 0, 0)'
330 | }
331 |
332 | The style of the text when the wave does not overlap it.
333 |
334 |
335 | waveTextStyle
336 | Object
337 |
338 | {
339 | fill: 'rgb(255, 255, 255)'
340 | }
341 |
342 | The style of the text when the wave overlaps it.
343 |
344 |
345 |
346 |
347 | ## License
348 |
349 | MIT
350 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-liquid-gauge",
3 | "description": "React Liquid Gauge component",
4 | "homepage": "https://github.com/trendmicro-frontend/react-liquid-gauge",
5 | "snapshots": [],
6 | "keywords": [
7 | "react",
8 | "react-component",
9 | "d3",
10 | "liquid",
11 | "gauge",
12 | "chart"
13 | ],
14 | "license": "MIT"
15 | }
16 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Liquid Gauge
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/examples/.stylintrc:
--------------------------------------------------------------------------------
1 | //
2 | // https://github.com/rossPatton/stylint
3 | //
4 | {
5 | "blocks": false,
6 | "brackets": "always",
7 | "colons": "always",
8 | "colors": false,
9 | "commaSpace": "always",
10 | "commentSpace": false,
11 | "cssLiteral": "never",
12 | "depthLimit": false,
13 | "duplicates": false,
14 | "efficient": "always",
15 | "extendPref": false,
16 | "globalDupe": false,
17 | "indentPref": false,
18 | "leadingZero": "never",
19 | "maxErrors": false,
20 | "maxWarnings": false,
21 | "mixed": false,
22 | "namingConvention": false,
23 | "namingConventionStrict": false,
24 | "none": "never",
25 | "noImportant": false,
26 | "parenSpace": false,
27 | "placeholders": "always",
28 | "prefixVarsWithDollar": "always",
29 | "quotePref": false,
30 | "semicolons": "always",
31 | "sortOrder": false,
32 | "stackedProperties": "never",
33 | "trailingWhitespace": "never",
34 | "universal": false,
35 | "valid": true,
36 | "zeroUnits": "never",
37 | "zIndexNormalize": false
38 | }
39 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Liquid Gauge
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/examples/index.jsx:
--------------------------------------------------------------------------------
1 | import { color } from 'd3-color';
2 | import { interpolateRgb } from 'd3-interpolate';
3 | import React, { Component } from 'react';
4 | import ReactDOM from 'react-dom';
5 | import LiquidFillGauge from '../src';
6 |
7 | const Gauge = ({ radius = 200, value = 0, ...props }) => {
8 | const startColor = '#6495ed'; // cornflowerblue
9 | const endColor = '#dc143c'; // crimson
10 | const interpolate = interpolateRgb(startColor, endColor);
11 | const fillColor = interpolate(value / 100);
12 | const gradientStops = [
13 | {
14 | key: '0%',
15 | stopColor: color(fillColor).darker(0.5).toString(),
16 | stopOpacity: 1,
17 | offset: '0%'
18 | },
19 | {
20 | key: '50%',
21 | stopColor: fillColor,
22 | stopOpacity: 0.75,
23 | offset: '50%'
24 | },
25 | {
26 | key: '100%',
27 | stopColor: color(fillColor).brighter(0.5).toString(),
28 | stopOpacity: 0.5,
29 | offset: '100%'
30 | }
31 | ];
32 |
33 | return (
34 | {
44 | value = Math.round(value);
45 | const radius = Math.min(height / 2, width / 2);
46 | const textPixels = (textSize * radius / 2);
47 | const valueStyle = {
48 | fontSize: textPixels
49 | };
50 | const percentStyle = {
51 | fontSize: textPixels * 0.6
52 | };
53 |
54 | return (
55 |
56 | {value}
57 | {percent}
58 |
59 | );
60 | }}
61 | riseAnimation
62 | waveAnimation
63 | waveFrequency={2}
64 | waveAmplitude={1}
65 | gradient
66 | gradientStops={gradientStops}
67 | circleStyle={{
68 | fill: fillColor
69 | }}
70 | waveStyle={{
71 | fill: fillColor
72 | }}
73 | textStyle={{
74 | fill: color('#444').toString(),
75 | fontFamily: 'Arial'
76 | }}
77 | waveTextStyle={{
78 | fill: color('#fff').toString(),
79 | fontFamily: 'Arial'
80 | }}
81 | />
82 | );
83 | };
84 |
85 | class App extends Component {
86 | state = {
87 | value1: Math.random() * 100,
88 | value2: Math.random() * 100
89 | };
90 |
91 | render() {
92 | return (
93 |
94 |
95 |
96 | {
101 | this.setState({ value1: Math.random() * 100 });
102 | }}
103 | />
104 |
105 |
106 | {
111 | this.setState({ value2: Math.random() * 100 });
112 | }}
113 | />
114 |
115 |
116 |
117 |
123 | {
127 | this.setState({
128 | value1: Math.random() * 100,
129 | value2: Math.random() * 100
130 | });
131 | }}
132 | >
133 | Refresh
134 |
135 |
136 |
137 |
138 | );
139 | }
140 | }
141 |
142 | ReactDOM.render(
143 | ,
144 | document.getElementById('container')
145 | );
146 |
--------------------------------------------------------------------------------
/examples/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var HtmlWebpackPlugin = require('html-webpack-plugin');
4 | var stylusLoader = require('stylus-loader');
5 | var nib = require('nib');
6 |
7 | module.exports = {
8 | devtool: 'source-map',
9 | entry: path.resolve(__dirname, 'index.jsx'),
10 | output: {
11 | path: path.join(__dirname, '../docs'),
12 | filename: 'bundle.js?[hash]'
13 | },
14 | module: {
15 | rules: [
16 | // http://survivejs.com/webpack_react/linting_in_webpack/
17 | {
18 | test: /\.jsx?$/,
19 | loader: 'eslint-loader',
20 | enforce: 'pre',
21 | exclude: /node_modules/
22 | },
23 | {
24 | test: /\.styl$/,
25 | loader: 'stylint-loader',
26 | enforce: 'pre'
27 | },
28 | {
29 | test: /\.jsx?$/,
30 | loader: 'babel-loader',
31 | exclude: /(node_modules|bower_components)/
32 | },
33 | {
34 | test: /\.styl$/,
35 | use: [
36 | 'style-loader',
37 | 'css-loader?camelCase&modules&importLoaders=1&localIdentName=[local]---[hash:base64:5]',
38 | 'stylus-loader'
39 | ]
40 | },
41 | {
42 | test: /\.css$/,
43 | loader: 'style-loader!css-loader'
44 | },
45 | {
46 | test: /\.(png|jpg)$/,
47 | loader: 'url-loader',
48 | options: {
49 | limit: 8192
50 | }
51 | },
52 | {
53 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
54 | loader: 'url-loader',
55 | options: {
56 | limit: 10000,
57 | mimetype: 'application/font-woff'
58 | }
59 | },
60 | {
61 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
62 | loader: 'file-loader'
63 | }
64 | ]
65 | },
66 | plugins: [
67 | new webpack.LoaderOptionsPlugin({
68 | debug: true,
69 | }),
70 | new webpack.NamedModulesPlugin(),
71 | new webpack.NoEmitOnErrorsPlugin(),
72 | new stylusLoader.OptionsPlugin({
73 | default: {
74 | // nib - CSS3 extensions for Stylus
75 | use: [nib()],
76 | // no need to have a '@import "nib"' in the stylesheet
77 | import: ['~nib/lib/nib/index.styl']
78 | }
79 | }),
80 | new HtmlWebpackPlugin({
81 | filename: '../docs/index.html',
82 | template: 'index.html'
83 | })
84 | ],
85 | resolve: {
86 | extensions: ['.js', '.json', '.jsx']
87 | },
88 | // https://webpack.github.io/docs/webpack-dev-server.html#additional-configuration-options
89 | devServer: {
90 | noInfo: false,
91 | lazy: false,
92 | // https://webpack.github.io/docs/node.js-api.html#compiler
93 | watchOptions: {
94 | poll: true, // use polling instead of native watchers
95 | ignored: /node_modules/
96 | }
97 | }
98 | };
99 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-liquid-gauge",
3 | "version": "1.2.4",
4 | "description": "React Liquid Gauge component",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "scripts": {
10 | "prepublish": "npm run lint && npm test && npm run clean && npm run bowersync && npm run build && npm run build-examples",
11 | "bowersync": "./scripts/bowersync",
12 | "build": "babel --out-dir ./lib ./src",
13 | "build-examples": "cd examples; webpack",
14 | "clean": "rm -f lib/*",
15 | "demo": "http-server -p 8000 docs/",
16 | "lint": "eslint ./src",
17 | "lint:fix": "eslint --fix ./src",
18 | "test": "tap test/*.js --node-arg=--require --node-arg=babel-register --node-arg=--require --node-arg=babel-polyfill",
19 | "coveralls": "tap test/*.js --coverage --coverage-report=text-lcov --nyc-arg=--require --nyc-arg=babel-register --nyc-arg=--require --nyc-arg=babel-polyfill | coveralls",
20 | "dev": "cd examples; webpack-dev-server --hot --inline --host 0.0.0.0 --port 8000 --content-base ../docs"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/trendmicro-frontend/react-liquid-gauge.git"
25 | },
26 | "author": "Cheton Wu",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/trendmicro-frontend/react-liquid-gauge/issues"
30 | },
31 | "homepage": "https://github.com/trendmicro-frontend/react-liquid-gauge",
32 | "keywords": [
33 | "react",
34 | "react-component",
35 | "d3",
36 | "liquid",
37 | "gauge",
38 | "chart"
39 | ],
40 | "peerDependencies": {
41 | "react": "^0.14.0 || >=15.0.0",
42 | "react-dom": "^0.14.0 || >=15.0.0"
43 | },
44 | "dependencies": {
45 | "d3-color": "^1.0.2",
46 | "d3-ease": "^1.0.2",
47 | "d3-interpolate": "^1.1.5",
48 | "d3-scale": "^1.0.6",
49 | "d3-selection": "^1.1.0",
50 | "d3-shape": "^1.2.0",
51 | "d3-timer": "^1.0.3",
52 | "d3-transition": "^1.1.0",
53 | "prop-types": "^15.5.8"
54 | },
55 | "devDependencies": {
56 | "@trendmicro/react-buttons": "~1.0.4",
57 | "babel-cli": "~6.24.0",
58 | "babel-core": "~6.24.0",
59 | "babel-eslint": "~7.2.2",
60 | "babel-loader": "~7.0.0",
61 | "babel-plugin-transform-decorators-legacy": "~1.3.4",
62 | "babel-preset-es2015": "~6.24.0",
63 | "babel-preset-react": "~6.24.1",
64 | "babel-preset-stage-0": "~6.24.1",
65 | "coveralls": "~2.13.1",
66 | "css-loader": "~0.28.4",
67 | "eslint": "~3.19.0",
68 | "eslint-config-trendmicro": "~0.5.1",
69 | "eslint-loader": "~1.7.1",
70 | "eslint-plugin-import": "~2.2.0",
71 | "eslint-plugin-jsx-a11y": "~2.2.3",
72 | "eslint-plugin-react": "~6.10.0",
73 | "file-loader": "~0.11.1",
74 | "html-webpack-plugin": "~2.28.0",
75 | "http-server": "~0.10.0",
76 | "nib": "~1.1.2",
77 | "react": "^0.14.0 || >=15.0.0",
78 | "react-dom": "^0.14.0 || >=15.0.0",
79 | "style-loader": "~0.18.1",
80 | "stylint": "~1.5.9",
81 | "stylint-loader": "~1.0.0",
82 | "stylus-loader": "~3.0.1",
83 | "tap": "~10.3.0",
84 | "trendmicro-ui": "~0.4.3",
85 | "url-loader": "~0.5.8",
86 | "webpack": "~2.6.1",
87 | "webpack-dev-server": "~2.4.5",
88 | "which": "~1.2.12"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/scripts/bowersync:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var fs = require('fs');
4 | var path = require('path');
5 | var pkg = require('../package.json');
6 | var bower = require('../bower.json');
7 |
8 | // Update bower.json
9 | Object.keys(bower).forEach((key) => {
10 | bower[key] = pkg[key] || bower[key];
11 | });
12 | bower.authors = pkg.contributors.map(author => {
13 | return {
14 | name: author.name,
15 | email: author.email,
16 | homepage: author.url
17 | };
18 | });
19 |
20 | var content = JSON.stringify(bower, null, 2);
21 | fs.writeFileSync(path.join(__dirname, '../bower.json'), content + '\n', 'utf8');
22 |
--------------------------------------------------------------------------------
/src/Gradient.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 |
4 | const Gradient = (props) => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
12 | Gradient.propTypes = {
13 | x1: PropTypes.string,
14 | x2: PropTypes.string,
15 | y1: PropTypes.string,
16 | y2: PropTypes.string,
17 | children: PropTypes.oneOfType([
18 | PropTypes.arrayOf(PropTypes.object),
19 | PropTypes.arrayOf(PropTypes.node),
20 | PropTypes.node
21 | ])
22 | };
23 |
24 | Gradient.defaultProps = {
25 | x1: '0%',
26 | x2: '0%',
27 | y1: '100%',
28 | y2: '0%'
29 | };
30 |
31 | export default Gradient;
32 |
--------------------------------------------------------------------------------
/src/hashid.js:
--------------------------------------------------------------------------------
1 | // Short ID Generation in JavaScript
2 | // http://fiznool.com/blog/2014/11/16/short-id-generation-in-javascript/
3 |
4 | /**
5 | * The default alphabet is 25 numbers and lowercase letters.
6 | * Any numbers that look like letters and vice versa are removed:
7 | * 1 l, 0 o.
8 | * Also the following letters are not present, to prevent any
9 | * expletives: cfhistu
10 | */
11 | const DEFAULT_ALPHABET = '23456789abdegjkmnpqrvwxyz';
12 |
13 | // Governs the length of the ID.
14 | // With an alphabet of 25 chars,
15 | // a length of 8 gives us 25^8 or
16 | // 152,587,890,625 possibilities.
17 | // Should be enough...
18 | const DEFAULT_ID_LENGTH = 5;
19 |
20 | /**
21 | * Governs the number of times we should try to find
22 | * a unique value before giving up.
23 | * @type {Number}
24 | */
25 | const UNIQUE_RETRIES = 9999;
26 |
27 | /**
28 | * Returns a randomly-generated friendly ID.
29 | * Note that the friendly ID is not guaranteed to be
30 | * unique to any other ID generated by this same method,
31 | * so it is up to you to check for uniqueness.
32 | * @return {String} friendly ID.
33 | */
34 | export const generate = (options) => {
35 | const {
36 | alphabet = DEFAULT_ALPHABET,
37 | idLength = DEFAULT_ID_LENGTH
38 | } = { ...options };
39 |
40 | let rtn = '';
41 | for (let i = 0; i < idLength; i++) {
42 | rtn += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
43 | }
44 | return rtn;
45 | };
46 |
47 | /**
48 | * Tries to generate a unique ID that is not defined in the
49 | * `previous` array.
50 | * @param {Array} previous The list of previous ids to avoid.
51 | * @return {String} A unique ID, or `null` if one could not be generated.
52 | */
53 | export const generateUnique = (previous) => {
54 | previous = previous || [];
55 | let retries = 0;
56 | let id;
57 |
58 | // Try to generate a unique ID,
59 | // i.e. one that isn't in the previous.
60 | while (!id && retries < UNIQUE_RETRIES) {
61 | id = generate();
62 | if (previous.indexOf(id) !== -1) {
63 | id = null;
64 | retries++;
65 | }
66 | }
67 |
68 | return id;
69 | };
70 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { PureComponent } from 'react';
3 | import { renderToString } from 'react-dom/server';
4 | import { color } from 'd3-color';
5 | import * as ease from 'd3-ease';
6 | import { interpolateNumber } from 'd3-interpolate';
7 | import { scaleLinear } from 'd3-scale';
8 | import { select } from 'd3-selection';
9 | import { arc, area } from 'd3-shape';
10 | import { timer } from 'd3-timer';
11 | import './transition-polyfill';
12 | import { generate } from './hashid';
13 | import Gradient from './Gradient';
14 |
15 | const ucfirst = (s) => {
16 | return s && s[0].toUpperCase() + s.slice(1);
17 | };
18 |
19 | class LiquidFillGauge extends PureComponent {
20 | static propTypes = {
21 | // A unique identifier (ID) to identify the element.
22 | id: PropTypes.string,
23 | // The width of the component.
24 | width: PropTypes.number,
25 | // The height of the component.
26 | height: PropTypes.number,
27 |
28 | // The percent value (0-100).
29 | value: PropTypes.number,
30 | // The percent string (%) or SVG text element.
31 | percent: PropTypes.oneOfType([
32 | PropTypes.string,
33 | PropTypes.node
34 | ]),
35 |
36 | // The relative height of the text to display in the wave circle. A value of 1 equals 50% of the radius of the outer circle.
37 | textSize: PropTypes.number,
38 | textOffsetX: PropTypes.number,
39 | textOffsetY: PropTypes.number,
40 |
41 | // Specifies a custom text renderer for rendering a percent value.
42 | textRenderer: PropTypes.func,
43 |
44 | // Controls if the wave should rise from 0 to it's full height, or start at it's full height.
45 | riseAnimation: PropTypes.bool,
46 | // The amount of time in milliseconds for the wave to rise from 0 to it's final height.
47 | riseAnimationTime: PropTypes.number,
48 | // [d3-ease](https://github.com/d3/d3-ease) options:
49 | // See the [easing explorer](http://bl.ocks.org/mbostock/248bac3b8e354a9103c4) for a visual demostration.
50 | riseAnimationEasing: PropTypes.string,
51 | // Progress callback function.
52 | riseAnimationOnProgress: PropTypes.func,
53 | // Complete callback function.
54 | riseAnimationOnComplete: PropTypes.func,
55 |
56 | // Controls if the wave scrolls or is static.
57 | waveAnimation: PropTypes.bool,
58 | // The amount of time in milliseconds for a full wave to enter the wave circle.
59 | waveAnimationTime: PropTypes.number,
60 | // [d3-ease](https://github.com/d3/d3-ease) options:
61 | // See the [easing explorer](http://bl.ocks.org/mbostock/248bac3b8e354a9103c4) for a visual demostration.
62 | waveAnimationEasing: PropTypes.string,
63 |
64 | // The wave amplitude.
65 | waveAmplitude: PropTypes.number,
66 | // The number of full waves per width of the wave circle.
67 | waveFrequency: PropTypes.number,
68 |
69 | // Whether to apply linear gradients to fill the wave circle.
70 | gradient: PropTypes.bool,
71 | // An array of the SVG element defines the ramp of colors to use on a gradient, which is a child element to either the or the element.
72 | gradientStops: PropTypes.oneOfType([
73 | PropTypes.arrayOf(PropTypes.object),
74 | PropTypes.arrayOf(PropTypes.node),
75 | PropTypes.node
76 | ]),
77 |
78 | // onClick event handler.
79 | onClick: PropTypes.func,
80 |
81 | // The radius of the inner circle.
82 | innerRadius: PropTypes.number,
83 | // The radius of the outer circle.
84 | outerRadius: PropTypes.number,
85 | // The size of the gap between the outer circle and wave circle as a percentage of the outer circle's radius.
86 | margin: PropTypes.number,
87 |
88 | // The fill and stroke of the outer circle.
89 | circleStyle: PropTypes.object,
90 | // The fill and stroke of the fill wave.
91 | waveStyle: PropTypes.object,
92 | // The fill and stroke of the value text when the wave does not overlap it.
93 | textStyle: PropTypes.object,
94 | // The fill and stroke of the value text when the wave overlaps it.
95 | waveTextStyle: PropTypes.object
96 | };
97 |
98 | static defaultProps = {
99 | width: 400,
100 | height: 400,
101 | value: 0,
102 | percent: '%',
103 | textSize: 1,
104 | textOffsetX: 0,
105 | textOffsetY: 0,
106 | textRenderer: (props) => {
107 | const value = Math.round(props.value);
108 | const radius = Math.min(props.height / 2, props.width / 2);
109 | const textPixels = (props.textSize * radius / 2);
110 | const valueStyle = {
111 | fontSize: textPixels
112 | };
113 | const percentStyle = {
114 | fontSize: textPixels * 0.6
115 | };
116 |
117 | return (
118 |
119 | {value}
120 | {(typeof props.percent !== 'string')
121 | ? props.percent
122 | : {props.percent}
123 | }
124 |
125 | );
126 | },
127 | riseAnimation: false,
128 | riseAnimationTime: 2000,
129 | riseAnimationEasing: 'cubicInOut',
130 | riseAnimationOnProgress: () => {},
131 | riseAnimationOnComplete: () => {},
132 | waveAnimation: false,
133 | waveAnimationTime: 2000,
134 | waveAnimationEasing: 'linear',
135 | waveAmplitude: 1,
136 | waveFrequency: 2,
137 | gradient: false,
138 | gradientStops: null,
139 | onClick: () => {},
140 | innerRadius: 0.9,
141 | outerRadius: 1.0,
142 | margin: 0.025,
143 | circleStyle: {
144 | fill: 'rgb(23, 139, 202)'
145 | },
146 | waveStyle: {
147 | fill: 'rgb(23, 139, 202)'
148 | },
149 | textStyle: {
150 | fill: 'rgb(0, 0, 0)'
151 | },
152 | waveTextStyle: {
153 | fill: 'rgb(255, 255, 255)'
154 | }
155 | };
156 |
157 | componentDidMount() {
158 | this.draw();
159 | }
160 | componentDidUpdate(prevProps, prevState) {
161 | this.draw();
162 | }
163 | draw() {
164 | const data = [];
165 | const samplePoints = 40;
166 | for (let i = 0; i <= samplePoints * this.props.waveFrequency; ++i) {
167 | data.push({
168 | x: i / (samplePoints * this.props.waveFrequency),
169 | y: i / samplePoints
170 | });
171 | }
172 |
173 | this.wave = select(this.clipPath)
174 | .datum(data)
175 | .attr('T', '0');
176 |
177 | const waveHeightScale = scaleLinear()
178 | .range([0, this.props.waveAmplitude, 0])
179 | .domain([0, 50, 100]);
180 |
181 | const fillWidth = (this.props.width * (this.props.innerRadius - this.props.margin));
182 | const waveScaleX = scaleLinear()
183 | .range([-fillWidth, fillWidth])
184 | .domain([0, 1]);
185 |
186 | const fillHeight = (this.props.height * (this.props.innerRadius - this.props.margin));
187 | const waveScaleY = scaleLinear()
188 | .range([fillHeight / 2, -fillHeight / 2])
189 | .domain([0, 100]);
190 |
191 | if (this.props.waveAnimation) {
192 | this.animateWave();
193 | }
194 |
195 | if (this.props.riseAnimation) {
196 | const clipArea = area()
197 | .x((d, i) => waveScaleX(d.x))
198 | .y1(d => (this.props.height / 2));
199 | const timeScale = scaleLinear()
200 | .range([0, 1])
201 | .domain([0, this.props.riseAnimationTime]);
202 | // Use the old value if available
203 | const interpolate = interpolateNumber(this.wave.node().oldValue || 0, this.props.value);
204 | const easing = `ease${ucfirst(this.props.riseAnimationEasing)}`;
205 | const easingFn = ease[easing] ? ease[easing] : ease.easeCubicInOut;
206 | const riseAnimationTimer = timer((t) => {
207 | const value = interpolate(easingFn(timeScale(t)));
208 | clipArea.y0((d, i) => {
209 | const radians = Math.PI * 2 * (d.y * 2); // double width
210 | return waveScaleY(waveHeightScale(value) * Math.sin(radians) + value);
211 | });
212 |
213 | this.wave.attr('d', clipArea);
214 |
215 | const renderedElement = this.props.textRenderer({
216 | ...this.props,
217 | value: value
218 | });
219 | select(this.container)
220 | .selectAll('.text, .waveText')
221 | .html(renderToString(renderedElement));
222 |
223 | this.props.riseAnimationOnProgress({
224 | value: value,
225 | container: select(this.container)
226 | });
227 |
228 | if (t >= this.props.riseAnimationTime) {
229 | riseAnimationTimer.stop();
230 |
231 | const value = interpolate(1);
232 | clipArea.y0((d, i) => {
233 | const radians = Math.PI * 2 * (d.y * 2); // double width
234 | return waveScaleY(waveHeightScale(value) * Math.sin(radians) + value);
235 | });
236 |
237 | this.wave.attr('d', clipArea);
238 |
239 | const renderedElement = this.props.textRenderer({
240 | ...this.props,
241 | value: value
242 | });
243 | select(this.container)
244 | .selectAll('.text, .waveText')
245 | .html(renderToString(renderedElement));
246 |
247 | this.props.riseAnimationOnComplete({
248 | value: value,
249 | container: select(this.container)
250 | });
251 | }
252 | });
253 |
254 | // Store the old value that can be used for the next animation
255 | this.wave.node().oldValue = this.props.value;
256 | } else {
257 | const value = this.props.value;
258 | const clipArea = area()
259 | .x((d, i) => waveScaleX(d.x))
260 | .y0((d, i) => {
261 | const radians = Math.PI * 2 * (d.y * 2); // double width
262 | return waveScaleY(waveHeightScale(value) * Math.sin(radians) + value);
263 | })
264 | .y1(d => (this.props.height / 2));
265 |
266 | this.wave.attr('d', clipArea);
267 | }
268 | }
269 | animateWave() {
270 | const width = (this.props.width * (this.props.innerRadius - this.props.margin)) / 2;
271 | const waveAnimationScale = scaleLinear()
272 | .range([-width, width])
273 | .domain([0, 1]);
274 | const easing = `ease${ucfirst(this.props.waveAnimationEasing)}`;
275 | const easingFn = ease[easing] ? ease[easing] : ease.easeLinear;
276 |
277 | this.wave
278 | .attr('transform', 'translate(' + waveAnimationScale(this.wave.attr('T')) + ', 0)')
279 | .transition()
280 | .duration(this.props.waveAnimationTime * (1 - this.wave.attr('T')))
281 | .ease(easingFn)
282 | .attr('transform', 'translate(' + waveAnimationScale(1) + ', 0)')
283 | .attr('T', '1')
284 | .on('end', () => {
285 | this.wave.attr('T', '0');
286 | if (this.props.waveAnimation) {
287 | this.animateWave();
288 | }
289 | });
290 | }
291 | render() {
292 | const {
293 | id = generate(),
294 | style
295 | } = this.props;
296 | const radius = Math.min(this.props.height / 2, this.props.width / 2);
297 | const fillCircleRadius = radius * (this.props.innerRadius - this.props.margin);
298 | const circle = arc()
299 | .outerRadius(this.props.outerRadius * radius)
300 | .innerRadius(this.props.innerRadius * radius)
301 | .startAngle(0)
302 | .endAngle(Math.PI * 2);
303 | const cX = (this.props.width / 2);
304 | const cY = (this.props.height / 2);
305 | const fillColor = this.props.waveStyle.fill;
306 | const gradientStops = this.props.gradientStops || [
307 | {
308 | key: '0%',
309 | stopColor: color(fillColor).darker(0.5).toString(),
310 | stopOpacity: 1,
311 | offset: '0%'
312 | },
313 | {
314 | key: '50%',
315 | stopColor: fillColor,
316 | stopOpacity: 0.75,
317 | offset: '50%'
318 | },
319 | {
320 | key: '100%',
321 | stopColor: color(fillColor).brighter(0.5).toString(),
322 | stopOpacity: 0.5,
323 | offset: '100%'
324 | }
325 | ];
326 |
327 | return (
328 |
335 |
336 | {
338 | this.container = c;
339 | }}
340 | transform={`translate(${cX},${cY})`}
341 | >
342 |
343 |
344 | {
346 | this.clipPath = c;
347 | }}
348 | />
349 |
350 |
351 |
359 | {this.props.textRenderer(this.props)}
360 |
361 |
362 |
368 |
376 | {this.props.textRenderer(this.props)}
377 |
378 |
379 |
384 |
391 |
392 |
393 | {gradientStops.map((stop, index) => {
394 | if (!React.isValidElement(stop)) {
395 | const key = stop.key || index;
396 | return (
397 |
398 | );
399 | }
400 | return stop;
401 | })}
402 |
403 |
404 |
405 | );
406 | }
407 | }
408 |
409 | module.exports = LiquidFillGauge;
410 |
--------------------------------------------------------------------------------
/src/transition-polyfill.js:
--------------------------------------------------------------------------------
1 | // https://github.com/d3/d3-transition/blob/master/src/selection/index.js
2 | import { selection } from 'd3-selection';
3 | import { transition, interrupt } from 'd3-transition';
4 |
5 | selection.prototype.transition = selection.prototype.transition || transition;
6 | selection.prototype.interrupt = selection.prototype.interrupt || interrupt;
7 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import { test } from 'tap';
2 |
3 | test('noop', (t) => {
4 | t.end();
5 | });
6 |
--------------------------------------------------------------------------------