Question

The requirement is as follows:

We need to map values to colors. So each discrete value will have a color.

We allow the user to specify a maxColor but NO minColor but allow them to specify the number of bins representing the number of shades. So if the maxColor selected is Color.GREEN and the bins= 5 ,then we would like to have 5 shades of green with the color selected as max being the darkest and the rest four will be in order of increasing lightness.

//Give me a list of 5 shades of Green with the first argument being the darkest.
List<Color> greenShades = calculateShades(Color.GREEN,5);

//Give me a list of 7 shades of RED with the first argument being the darkest.
List<Color> greenShades = calculateShades(Color.RED,7);

I tagged the question as Java as I am coding in Java. But I understand it is just an algorithm.So implementation/idea of this implementation in other languages like JavaScript will also be acceptable.

Was it helpful?

Solution

The basic concept revolves around the idea of generating a color based on a fraction of the source...

That is, if you want 5 bands, each band will 1/5 the intensity of the last...

public List<Color> getColorBands(Color color, int bands) {

    List<Color> colorBands = new ArrayList<>(bands);
    for (int index = 0; index < bands; index++) {
        colorBands.add(darken(color, (double) index / (double) bands));
    }
    return colorBands;

}

public static Color darken(Color color, double fraction) {

    int red = (int) Math.round(Math.max(0, color.getRed() - 255 * fraction));
    int green = (int) Math.round(Math.max(0, color.getGreen() - 255 * fraction));
    int blue = (int) Math.round(Math.max(0, color.getBlue() - 255 * fraction));

    int alpha = color.getAlpha();

    return new Color(red, green, blue, alpha);

}

As a quick and nasty example...

enter image description here

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class ColorBands {

    public static void main(String[] args) {
        new ColorBands();
    }

    public ColorBands() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private JPanel bandsPane;
        private JSlider slider;
        private Timer changeTimer;

        public TestPane() {
            bandsPane = new JPanel(new GridBagLayout());
            slider = new JSlider(1, 100);
            setLayout(new BorderLayout());
            add(new JScrollPane(bandsPane));
            add(slider, BorderLayout.SOUTH);
            slider.addChangeListener(new ChangeListener() {
                @Override
                public void stateChanged(ChangeEvent e) {
                    changeTimer.restart();
                }
            });

            changeTimer = new Timer(250, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    int bands = slider.getValue();
                    List<Color> bandsList = getColorBands(Color.RED, bands);
                    bandsPane.removeAll();
                    GridBagConstraints gbc = new GridBagConstraints();
                    gbc.gridwidth = GridBagConstraints.REMAINDER;
                    gbc.insets = new Insets(1, 1, 1, 1);
                    for (Color color : bandsList) {
                        bandsPane.add(new ColorBand(color), gbc);
                    }
                    gbc.weighty = 1;
                    bandsPane.add(new JPanel(), gbc);
                    revalidate();
                    repaint();
                }
            });
            changeTimer.setRepeats(false);
            slider.setValue(1);
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }
    }

    public List<Color> getColorBands(Color color, int bands) {

        List<Color> colorBands = new ArrayList<>(bands);
        for (int index = 0; index < bands; index++) {
            colorBands.add(darken(color, (double) index / (double) bands));
        }
        return colorBands;

    }

    public static Color darken(Color color, double fraction) {

        int red = (int) Math.round(Math.max(0, color.getRed() - 255 * fraction));
        int green = (int) Math.round(Math.max(0, color.getGreen() - 255 * fraction));
        int blue = (int) Math.round(Math.max(0, color.getBlue() - 255 * fraction));

        int alpha = color.getAlpha();

        return new Color(red, green, blue, alpha);

    }

    public class ColorBand extends JPanel {

        public ColorBand(Color color) {
            setBackground(color);
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(100, 20);
        }

    }

}

OTHER TIPS

Take a look at the Java sources for Color#darker(), apply the same logic with different FACTOR

public Color darker() {
    return new Color(Math.max((int)(getRed()  *FACTOR), 0), 
         Math.max((int)(getGreen()*FACTOR), 0),
         Math.max((int)(getBlue() *FACTOR), 0));
}

The basic idea is you want to walk each color component (RGB) from the color you gave to Color.WHITE (R=255, G=255, B=255) in increments of bin size. Here's some code that will do that.

public List<Color> calculateShades(Color baseColor, int numberShades)
{
  //decompose color into RGB
  int redMax  = baseColor.getRed();
  int greenMax  = baseColor.getGreen();
  int blueMax  = baseColor.getBlue();


  //Max color component in RGB
  final int MAX_COMPONENT = 255;

  //bin sizes for each color component
  int redDelta = (MAX_COMPONENT - redMax) / numberShades;
  int greenDelta = (MAX_COMPONENT - greenMax) / numberShades;
  int blueDelta = (MAX_COMPONENT - blueMax) / numberShades;

  List<Color> colors = new ArrayList<Color>();

  int redCurrent = redMax;
  int greenCurrent = greenMax;
  int blueCurrent = blueMax;

  //now step through each shade, and decrease darkness by adding color to it
  for(int i = 0; i < numberShades; i++)
  {

     //step up by the bin size, but stop at the max color component (255)
     redCurrent = (redCurrent+redDelta) < MAX_COMPONENT ? (redCurrent + redDelta ) : MAX_COMPONENT;
     greenCurrent = (greenCurrent+greenDelta) < MAX_COMPONENT ? (greenCurrent + greenDelta ) : MAX_COMPONENT;
     blueCurrent = (blueCurrent+blueDelta) < MAX_COMPONENT ? (blueCurrent + blueDelta ) : MAX_COMPONENT;

     Color nextShade = new Color(redCurrent, greenCurrent, blueCurrent);
     colors.add(nextShade);
  }

  return colors;
}

RGB color system is easy to identify the color proportions but it lacks the flexibility to manipulate colors. using averages or ratios will give you undesired results. Simply put, you cannot achieve the required results using RGB color system.

The solution would be to convert the color to HSV or HSL (HSL perfered) and manipulate value/luminosity to get the result.

Have a look at the conversion algorithms:

HSL to RGB color conversion

Mathematically, let's say you have a color R,G,B then:

Required number of bins = 5

Hue = <some value h>
Saturation = <some value s>
Luminosity = (max(R,G,B) + min (R,G,B))/2

Now for the same h,s you will have 5 values of L:

L1 = 0
L2 = ((1 * 100) / 4)
L3 = ((2 * 100) / 4)
L4 = ((3 * 100) / 4)
L5 = 100

Here since first and last bin will be black and white so we have used 4 instead of 5.

Now convert back the HSL to RGB to get the desired RGB color.

In addition to MadProgrammer answer above you may also lighten the color, so the whole thing may look something like that:

public static List<Color> getColorBands(
        Color color, 
        int bands, 
        SortDirection direction) {

    List<Color> colorBands = new ArrayList<>(bands);
    if(direction.equals(SortDirection.ASC)) {
        
        for (int index = 0; index < bands; index++)
            colorBands.add(lighten(color, (double) index / (double) bands));
    }
    if(direction.equals(SortDirection.DESC)) {
        
        for (int index = 0; index < bands; index++)
            colorBands.add(darken(color, (double) index / (double) bands));
    }       
    return colorBands;
}

public static Color darken(Color color, double fraction) {

    int red = (int) Math.round(Math.max(0, color.getRed() - 255 * fraction));
    int green = (int) Math.round(Math.max(0, color.getGreen() - 255 * fraction));
    int blue = (int) Math.round(Math.max(0, color.getBlue() - 255 * fraction));

    int alpha = color.getAlpha();

    return new Color(red, green, blue, alpha);
}

public static Color lighten(Color color, double fraction) {

    int red = (int) Math.round(Math.min(255, color.getRed() + 255 * fraction));
    int green = (int) Math.round(Math.min(255, color.getGreen() + 255 * fraction));
    int blue = (int) Math.round(Math.min(255, color.getBlue() + 255 * fraction));

    int alpha = color.getAlpha();

    return new Color(red, green, blue, alpha);
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top