├── .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 [![build status](https://travis-ci.org/trendmicro-frontend/react-liquid-gauge.svg?branch=master)](https://travis-ci.org/trendmicro-frontend/react-liquid-gauge) [![Coverage Status](https://coveralls.io/repos/github/trendmicro-frontend/react-liquid-gauge/badge.svg?branch=master)](https://coveralls.io/github/trendmicro-frontend/react-liquid-gauge?branch=master) 2 | 3 | [![NPM](https://nodei.co/npm/react-liquid-gauge.png?downloads=true&stars=true)](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 | [![react-liquid-gauge](https://cloud.githubusercontent.com/assets/447801/21498498/f1ab231e-cc67-11e6-830c-8e5db6b81af0.png)](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 | 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 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 312 | 313 | 314 | 315 | 316 | 317 | 322 | 323 | 324 | 325 | 326 | 327 | 332 | 333 | 334 | 335 | 336 | 337 | 342 | 343 | 344 | 345 |
NameTypeDefaultDescription
idStringA unique identifier (ID) to identify the element. Defaults to a unique random string.
widthNumber400The width of the component.
heightNumber400The height of the component.
valueNumber0The percent value (0-100).
percentString|Node'%'The percent string (%) or SVG text element.
textSizeNumber1The relative height of the text to display in the wave circle. A value of 1 equals 50% of the radius of the outer circle.
textOffsetXNumber0
textOffsetYNumber0
textRendererFunction(props)Specifies a custom text renderer for rendering a percent value.
riseAnimationBooleanfalseControls if the wave should rise from 0 to it's full height, or start at it's full height.
riseAnimationTimeNumber2000The amount of time in milliseconds for the wave to rise from 0 to it's final height.
riseAnimationEasingString'cubicInOut'd3-ease options. See the easing explorer for a visual demostration.
riseAnimationOnProgressFunction({ value, container })Progress callback function.
riseAnimationOnCompleteFunction({ value, container })Complete callback function.
waveAnimationBooleanfalseControls if the wave scrolls or is static.
waveAnimationTimeNumber2000The amount of time in milliseconds for a full wave to enter the wave circle.
waveAnimationEasingString'linear'd3-ease options. See the easing explorer for a visual demostration.
waveAmplitudeNumber1The wave height as a percentage of the radius of the wave circle.
waveFrequencyNumber2The number of full waves per width of the wave circle.
gradientBooleanfalseWhether to apply linear gradients to fill the wave circle.
gradientStopsNode|ArrayAn 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.
onClickFunction(event)onClick event handler.
innerRadiusNumber0.9The radius of the inner circle. A value of 0.9 equals 90% of the radius of the outer circle.
outerRadiusNumber1.0The radius of the outer circle. A value of 1 equals 100% of the radius of the outer circle.
marginNumber0.025The 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.
circleStyleObject 308 |
{
309 |   fill: 'rgb(23, 139, 202)'
310 | }
311 |
The style of the outer circle.
waveStyleObject 318 |
{
319 |   fill: 'rgb(23, 139, 202)'
320 | }
321 |
The style of the fill wave.
textStyleObject 328 |
{
329 |   fill: 'rgb(0, 0, 0)'
330 | }
331 |
The style of the text when the wave does not overlap it.
waveTextStyleObject 338 |
{
339 |   fill: 'rgb(255, 255, 255)'
340 | }
341 |
The style of the text when the wave overlaps it.
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 | 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 | 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 | 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 | --------------------------------------------------------------------------------