DEV Community

Lionel Marco
Lionel Marco

Posted on

React Color Scale Interpolation

Sometimes we have to work with colors, either for labels, icons, bar charts or for a choropleth maps.

It will show a way in which given two colors, the color between them will be calculated, or a scale with many colors will be generated.

For UI components will be use MATERIAL-UI library.

Image

Table of Contents

1) Color interpolation

Here the more important function is which, given two colors, them
calculate a resulting color. It will be more similar to one or the other,
acording to a conversion ratio, that will vary between 0 and 1.


  const interpolate = (start,end,ratio)=>{    
    const r = Math.trunc(ratio*end[0] + (1-ratio)*start[0])
    const g = Math.trunc(ratio*end[1] + (1-ratio)*start[1])
    const b = Math.trunc(ratio*end[2] + (1-ratio)*start[2])    
    return [r,g,b]
  }

Enter fullscreen mode Exit fullscreen mode

For example, interpolating in the middle of Black and White:

interpolate([0,0,0],[255,255,255],0.5)

We get Gray:

[127,127,127]

2) Color Selector

The MATERIAL-UI library have a Select control, in this case we use to display a list of color and choice one of them.

There are two color selector, one for start and other for end.

          <Select  value={color1}                                    
                   name={'color1'}
                   onChange={ this.onColorChange}
                   renderValue={ showIcon } 
                 >                    
                     { colors.map( e =>  <MenuItem  key={e} value={e}><ColorIcon color={e} /></MenuItem> )}    
          </Select> 
Enter fullscreen mode Exit fullscreen mode

The colors come from a previously declared array:

const colors = ['#800080','#FF0000','#FFD700','#00FF00','#006400','#0000FF']; //purple,red,gold,darkgreen,blue
Enter fullscreen mode Exit fullscreen mode

Color Icon

On every choice of our select an icon with their respective color is displayed.
The icon receive a props 'color', basically it is a rectangule filled with the given color.

function ColorIcon({color}) {
 return (
 <SvgIcon   viewBox="0 0 50 20" style={{ width: 50, height:20  }}>     
   <rect   fill={color} x={0} y='0' width={50} height={20}  ></rect>            
 </SvgIcon>
);
};
Enter fullscreen mode Exit fullscreen mode

Show Icon

The select have a 'renderValue' props, wich give the flexibility of show another thing that only text.

renderValue={ showIcon }

function showIcon(value) {
  return (  <ColorIcon color={value}/> );
}

Enter fullscreen mode Exit fullscreen mode

3) Ratio Slider

The MATERIAL-UI library have a Slider control, the slider will control the ratio of color mix. Varying from 0 to 100, then will be re maped to 0-1.

          <Slider
            value={ratio}          
            onChange={ this.onRatioChange}
            step={10} min={0} max={100}
            valueLabelDisplay="auto"
            marks={[ {value: 0,label: '0%'}, {value: 50,label: '50%'}, {value: 100,label: '100%'}]}                                   
          />

Enter fullscreen mode Exit fullscreen mode

Every change on the slider will update the state and trigger a new render:

  onRatioChange = (event, newValue) => {    
    this.setState(prevState => ({...prevState,ratio: newValue})); 
  };
Enter fullscreen mode Exit fullscreen mode

4) Full code

Next the full code is showed, imports are ommited just for shortness.

function ColorIcon({color}) {
 return (
 <SvgIcon   viewBox="0 0 50 20" style={{ width: 50, height:20  }}>     
   <rect   fill={color} x={0} y='0' width={50} height={20}  ></rect>            
 </SvgIcon>
);
};


function showIcon(value) {
  return (  <ColorIcon color={value}/> );
}

const colors = ['#800080','#FF0000','#FFD700','#00FF00','#006400','#0000FF']; //purple,red,gold,darkgreen,blue

export default class ColorInterpolation extends React.Component {

  constructor(props) {
    super(props);
    this.state = {color1:colors[2],color2:colors[1],ratio:50}      
  };   

  onColorChange = (e) => {    
    const {name,value} = e.target;
    this.setState(prevState => ({...prevState,[name]: value}));  
  };

  onRatioChange = (event, newValue) => {    
    this.setState(prevState => ({...prevState,ratio: newValue})); 
  };  

  render() {   
    //console.log("Render");     
    const {color1,color2,ratio} = this.state;    
    const interpolatedColor=getColor(color1,color2,ratio);

    return (    

      <div style={{  
        maxWidth:'500px',         
        display: "flex" ,                    
        flexDirection: "column", 
        margin:'10px',padding:'20px',
        border: '2px solid grey',
        borderRadius:'4px'
        }}>

        <div style={{               
           display: "flex" ,                    
           flexDirection: "row",      
           alignItems: "center",
           justifyContent: "space-around" }}>

          <Typography> Source: </Typography> 
          <Select  value={color1}                                    
                   name={'color1'}
                   onChange={ this.onColorChange}
                   renderValue={ showIcon } >                    
                     { colors.map( e =>  <MenuItem  key={e} value={e}><ColorIcon color={e} /></MenuItem> )}    
          </Select>    
          <Typography> Target: </Typography>       
          <Select   value={color2}                                    
                    name={'color2'}
                    onChange={ this.onColorChange}
                    renderValue={ showIcon } >                    
                     { colors.map( e =>  <MenuItem  key={e} value={e}><ColorIcon color={e} /></MenuItem> )}
          </Select>    
          </div>  
          <Slider
            value={ratio}          
            onChange={ this.onRatioChange}
            step={10} min={0} max={100}
            valueLabelDisplay="auto"
            marks={[ {value: 0,label: '0%'}, {value: 50,label: '50%'}, {value: 100,label: '100%'}]}
          />
          <div style={{marginTop:'5px', display: "flex" , alignItems: "center", justifyContent: "center" }}>   
            <Typography > Interpolated:</Typography>        
            <Typography > {ratio}% </Typography>        
            <ColorIcon color={interpolatedColor} />
            <Typography >{interpolatedColor}</Typography>
          </div>    
          <div style={{marginTop:'5px', display: "flex" , alignItems: "center", justifyContent: "center" }}>   
            <Typography > Scale:</Typography>        
            { [0,10,20,40,60,80,100].map( (v,i)=> {
              let c =getColor(this.state.color1,this.state.color2,v)
            return <ColorIcon key={i} color={c} />})}
          </div> 
        </div>
    );
}
}

function getColor(c1,c2,ratio)
{

  const interpolate = (start,end,ratio)=>{    
    const r = Math.trunc(ratio*end[0] + (1-ratio)*start[0])
    const g = Math.trunc(ratio*end[1] + (1-ratio)*start[1])
    const b = Math.trunc(ratio*end[2] + (1-ratio)*start[2])    
    return [r,g,b]
  }

  const hexToRgb = (hex) =>   
     [ 
       parseInt(hex.substr(1,2),16),
       parseInt(hex.substr(3,2),16),
       parseInt(hex.substr(5,2),16),
     ];  

  const rgbToHex = (rgb) => '#' + rgb.map(x => {
    const hex = x.toString(16)
    return hex.length === 1 ? '0' + hex : hex
  }).join('');

  const rgbInterpolated = interpolate(hexToRgb(c1),hexToRgb(c2),ratio/100);
  return rgbToHex(rgbInterpolated);
}

Enter fullscreen mode Exit fullscreen mode

Latest comments (0)