UNPKG

react-colorscales

Version:

A React component for picking colorscales based on Chroma.js

418 lines (375 loc) 16.7 kB
import React, { Component } from 'react'; import Colorscale from './Colorscale.js'; import chroma from 'chroma-js'; import Tooltip from 'rc-tooltip'; import Slider from 'rc-slider'; import 'rc-slider/assets/index.css'; import {COLORSCALE_TYPES, COLORSCALE_DESCRIPTIONS, BREWER, CMOCEAN, CUBEHELIX, SCALES_WITHOUT_LOG, DEFAULT_SCALE, DEFAULT_SWATCHES,DEFAULT_BREAKPOINTS, DEFAULT_START, DEFAULT_LOG_BREAKPOINTS, DEFAULT_ROTATIONS, DEFAULT_GAMMA, DEFAULT_LIGHTNESS, DEFAULT_NCOLORS} from './constants.js'; import './ColorscalePicker.css'; const Handle = Slider.Handle; export default class ColorscalePicker extends Component { constructor(props) { super(props); this.state = { nSwatches: this.props.nSwatches || DEFAULT_SWATCHES, colorscale: this.props.colorscale || DEFAULT_SCALE, previousColorscale: this.props.colorscale || DEFAULT_SCALE, colorscaleType: 'sequential', log: false, logBreakpoints: DEFAULT_LOG_BREAKPOINTS, customBreakpoints: DEFAULT_BREAKPOINTS, previousCustomBreakpoints: null, cubehelix: { start: DEFAULT_START, rotations: DEFAULT_ROTATIONS, } }; this.getColorscale = this.getColorscale.bind(this); this.onClick = this.onClick.bind(this); this.setColorscaleType = this.setColorscaleType.bind(this); this.updateCubehelixStart = this.updateCubehelixStart.bind(this); this.updateCubehelixRotations = this.updateCubehelixRotations.bind(this); this.updateCubehelix = this.updateCubehelix.bind(this); this.toggleLog = this.toggleLog.bind(this); this.handle = this.handle.bind(this); } componentDidMount() { this.setState({colorscaleOnMount: this.props.colorscale}) } handle = (props) => { const { value, dragging, index, ...restProps } = props; return ( <Tooltip prefixCls="rc-slider-tooltip" overlay={value} visible={dragging} placement="top" key={index} > <Handle value={value} {...restProps} /> </Tooltip> ); }; getColorscale = (colorscale, nSwatches, logBreakpoints, log) => { /* * getColorscale() takes a scale, modifies it based on the input * parameters, and returns a new scale */ // helper function repeats a categorical colorscale array N times let repeatArray = (array, n) => { let arrays = Array.apply(null, new Array(n)); arrays = arrays.map(function() { return array }); return [].concat.apply([], arrays); } let cs = chroma.scale(colorscale) .mode('lch'); if (log) { const logData = Array(nSwatches).fill().map((x,i)=>i+1); cs = cs.classes(chroma.limits(logData, 'l', logBreakpoints)); } let discreteScale = cs.colors(nSwatches); // repeat linear categorical ("qualitative") colorscales instead of repeating them if (!log && this.state.colorscaleType === 'categorical') { discreteScale = repeatArray(colorscale, nSwatches).slice(0, nSwatches); } return discreteScale; } toggleLog = () => { const cs = this.getColorscale(this.state.previousColorscale, this.state.nSwatches, this.state.logBreakpoints, !this.state.log); this.setState({log: !this.state.log, colorscale: cs}); this.props.onChange(cs); } onClick = (newColorscale, start, rot) => { const bp = this.state.customBreakpoints; const prevBp = this.state.previousCustomBreakpoints; if (bp === prevBp && this.state.colorscaleType === 'custom') { return; } const cs = this.getColorscale(newColorscale, this.state.nSwatches, this.state.logBreakpoints, this.state.log); let previousColorscale = newColorscale; if (this.state.colorscaleType === 'custom') { previousColorscale = this.state.previousColorscale; } if(!start && !rot) { this.setState({ previousColorscale: previousColorscale, colorscale: cs, previousCustomBreakpoints: this.state.colorscaleType === 'custom' ? this.state.customBreakpoints : null, }); } else { this.setState({ previousColorscale: previousColorscale, colorscale: cs, previousCustomBreakpoints: null, cubehelix: { start: start, rotations: rot } }); } this.props.onChange(cs); } updateSwatchNumber = ns => { const cs = this.getColorscale( this.state.previousColorscale, ns, this.state.logBreakpoints, this.state.log); this.setState({ nSwatches: ns, colorscale: cs, customBreakpoints: DEFAULT_BREAKPOINTS }); this.props.onChange(cs); } updateBreakpoints = e => { const bp = e.currentTarget.valueAsNumber; const cs = this.getColorscale( this.state.previousColorscale, this.state.nSwatches, bp, this.state.log); this.setState({ logBreakpoints: bp, colorscale: cs }); this.props.onChange(cs); } updateBreakpointArray = e => { const bpArr = e.currentTarget.value.replace(/,\s*$/, '').split(',').map(Number); this.setState({ customBreakpoints: bpArr }); } updateCubehelixStart = start => { const rot = this.state.cubehelix.rotations; this.updateCubehelix(start, rot); } updateCubehelixRotations = rot => { const start = this.state.cubehelix.start; this.updateCubehelix(start, rot); } updateCubehelixStartState = start => { const ch = this.state.cubehelix; ch.start = start; this.setState({cubehelix: ch}); } updateCubehelixRotState = rot => { const ch = this.state.cubehelix; ch.rotations = rot; this.setState({cubehelix: ch}); } updateCubehelix = (start, rot) => { const newColorscale = chroma.cubehelix() .start(start) .rotations(this.state.cubehelix.rotations) .gamma(DEFAULT_GAMMA) .lightness(DEFAULT_LIGHTNESS) .scale() .correctLightness() .colors(DEFAULT_NCOLORS); this.onClick(newColorscale, start, rot); } setColorscaleType = csType => { if (csType !== this.state.colorscaleType) { let isLogColorscale = this.state.log; if(SCALES_WITHOUT_LOG.indexOf(csType) >= 0) { isLogColorscale = false; } this.setState({ colorscaleType: csType, log: isLogColorscale, }); } } render() { console.warn('fixSwatches', this.props.fixSwatches); let swatchLabel = null; let swatchSlider = null; if (!this.props.fixSwatches{ swatchLabel = ( <div className='noWrap inlineBlock'> <span className='textLabel spaceRight'>SwatchTest:</span> <span className='textLabel spaceRight'>{this.state.nSwatches}</span> </div> ); swatchSlider = ( <Slider min={1} max={100} defaultValue={this.state.nSwatches} handle={this.handle} onAfterChange={this.updateSwatchNumber} /> ); } return ( <div className='colorscalePickerContainer'> <div className='colorscalePickerTopContainer'> <div className='colorscale-selected'> <Colorscale colorscale={this.state.colorscale} maxWidth={300} onClick={() => {}} /> </div> <div className='colorscaleControlPanel'> <div> {swatchLabel} {SCALES_WITHOUT_LOG.indexOf(this.state.colorscaleType) < 0 && <div className='noWrap inlineBlock alignTop'> <span className='textLabel spaceRight spaceLeft'>Log scale</span> <input type="checkbox" name="log" value="log" onChange={this.toggleLog} defaultChecked={this.state.log} className='spaceRightZeroTop alignMiddle' /> {this.state.log && <span> <span className='textLabel spaceRight spaceLeft'>Breakpoints: </span> <input type="number" step="1" min="1" max="10" value={`${this.state.logBreakpoints}`} onChange={this.updateBreakpoints} /> </span> } </div> } {swatchSlider} </div> {this.state.colorscaleType === 'cubehelix' && <div> <div className='noWrap'> <span className='textLabel'>Start: </span> <span className='textLabel'>{this.state.cubehelix.start}</span> <Slider min={0} max={300} step={1} value={this.state.cubehelix.start} onChange={this.updateCubehelixStartState} onAfterChange={this.updateCubehelixStart} handle={this.handle} /> </div> <div className='noWrap'> <span className='textLabel'>Rotations: </span> <span className='textLabel'>{this.state.cubehelix.rotations}</span> <Slider min={-1.5} max={1.5} step={0.1} value={this.state.cubehelix.rotations} onChange={this.updateCubehelixRotState} onAfterChange={this.updateCubehelixRotations} handle={this.handle} /> </div> </div> } <div className='colorscaleControlsRow'> {COLORSCALE_TYPES.map((x,i) => <a key={i} style={x === this.state.colorscaleType ? {backgroundColor: '#2a3f5f'} : null} className='colorscaleButton' onClick={() => {this.setColorscaleType(x)}} > {x} </a> )} </div> <div> {this.state.colorscaleType === 'custom' && <div className='colorscaleControlsRow'> <p className='textLabel zeroSpace'> Decimals between 0 and 1, or numbers between MIN and MAX of your data, separated by commas: </p> <input type='text' defaultValue={this.state.customBreakpoints.join(', ')} onChange={this.updateBreakpointArray} /> <p className='textLabel spaceTop'> {this.state.customBreakpoints.length-1} breakpoints: {this.state.customBreakpoints.join(' | ')} </p> </div> } </div> </div> </div> <div className='colorscalePickerBottomContainer'> <p> {COLORSCALE_DESCRIPTIONS[this.state.colorscaleType]} </p> <Colorscale colorscale={this.state.colorscaleOnMount} onClick={this.onClick} label={'RESET'} width={150} /> {BREWER.hasOwnProperty(this.state.colorscaleType) && BREWER[this.state.colorscaleType].map((x, i) => <Colorscale key={i} onClick={this.onClick} colorscale={chroma.brewer[x]} label={x} /> )} {this.state.colorscaleType === 'cubehelix' && CUBEHELIX.map((x, i) => <Colorscale key={i} onClick={this.onClick} colorscale={chroma.cubehelix() .start(x.start) .rotations(x.rotations) .gamma(DEFAULT_GAMMA) .lightness(DEFAULT_LIGHTNESS) .scale() .correctLightness() .colors(DEFAULT_NCOLORS) } label={`s${x.start} r${x.rotations}`} start={x.start} rot={x.rotations} /> )} {this.state.colorscaleType === 'cmocean' && Object.keys(CMOCEAN).map((x, i) => <Colorscale key={i} onClick={this.onClick} colorscale={CMOCEAN[x]} label={x} /> )} {this.state.colorscaleType === 'custom' && <Colorscale onClick={this.onClick} colorscale={chroma.scale(this.state.previousColorscale) .classes(this.state.customBreakpoints) .mode('lch') .colors(this.state.nSwatches) } maxWidth={200} label='Preview (click to apply)' /> } </div> </div> ); } }