// Copyright ©2016 The Gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package moreland import ( "fmt" "image/color" "math" "gonum.org/v1/plot/palette" ) // smoothDiverging is a smooth diverging color palette as described in // "Diverging Color Maps for Scientific Visualization." by Kenneth Moreland, // in Proceedings of the 5th International Symposium on Visual Computing, // December 2009. DOI 10.1007/978-3-642-10520-3_9. type smoothDiverging struct { // start and end are the beginning and ending colors start, end msh // convergeM is the MSH magnitude of the convergence point. // It is 88 by default. convergeM float64 // alpha represents the opacity of the returned // colors in the range (0,1). It is 1 by default. alpha float64 // min and max are the minimum and maximum values of the range of // scalars that can be mapped to colors using this palette. min, max float64 // convergePoint is a number between min and max where the colors // should converge. convergePoint float64 } // NewSmoothDiverging creates a new smooth diverging ColorMap as described in // "Diverging Color Maps for Scientific Visualization." by Kenneth Moreland, // in Proceedings of the 5th International Symposium on Visual Computing, // December 2009. DOI 10.1007/978-3-642-10520-3_9. // // start and end are the start- and end-point colors and // convergeM is the magnitude of the convergence point in // magnitude-saturation-hue (MSH) color space. Note that // convergeM specifies the color of the convergence point; it does not // specify the location of the convergence point. func NewSmoothDiverging(start, end color.Color, convergeM float64) palette.DivergingColorMap { return newSmoothDiverging(colorToMSH(start), colorToMSH(end), convergeM) } // newSmoothDiverging creates a new smooth diverging ColorMap // where start and end are the start and end point colors in MSH space and // convergeM is the MSH magnitude of the convergence point. Note that // convergeM specifies the color of the convergence point; it does not // specify the location of the convergence point. func newSmoothDiverging(start, end msh, convergeM float64) palette.DivergingColorMap { return &smoothDiverging{ start: start, end: end, convergeM: convergeM, convergePoint: math.NaN(), alpha: 1, } } // At implements the palette.ColorMap interface. func (p *smoothDiverging) At(v float64) (color.Color, error) { if err := checkRange(p.min, p.max, v); err != nil { return nil, err } convergePoint := (p.convergePoint - p.min) / (p.max - p.min) scalar := (v - p.min) / (p.max - p.min) o := p.interpolateMSHDiverging(scalar, convergePoint).cieLAB().cieXYZ().rgb().sRGBA(p.alpha) if !inUnitRange(o.R) || !inUnitRange(o.G) || !inUnitRange(o.B) || !inUnitRange(o.A) { return nil, fmt.Errorf("moreland: invalid color r:%g, g:%g, b:%g, a:%g", o.R, o.G, o.B, o.A) } return o, nil } func inUnitRange(v float64) bool { return 0 <= v && v <= 1 } // SetMax implements the palette.ColorMap interface. func (p *smoothDiverging) SetMax(v float64) { p.max = v p.convergePoint = (p.min + p.max) / 2 } // SetMin implements the palette.ColorMap interface. func (p *smoothDiverging) SetMin(v float64) { p.min = v p.convergePoint = (p.min + p.max) / 2 } // Max implements the palette.ColorMap interface. func (p *smoothDiverging) Max() float64 { return p.max } // Min implements the palette.ColorMap interface. func (p *smoothDiverging) Min() float64 { return p.min } // SetAlpha sets the opacity value of this color map. Zero is transparent // and one is completely opaque. // The function will panic is alpha is not between zero and one. func (p *smoothDiverging) SetAlpha(alpha float64) { if !inUnitRange(alpha) { panic(fmt.Errorf("invalid alpha: %g", alpha)) } p.alpha = alpha } // Alpha returns the opacity value of this color map. func (p *smoothDiverging) Alpha() float64 { return p.alpha } // SetConvergePoint sets the value where the diverging colors // should meet. func (p *smoothDiverging) SetConvergePoint(val float64) { if val > p.Max() || val < p.Min() { panic(fmt.Errorf("moreland: convergence point (%g) must be between min (%g) and max (%g)", val, p.Min(), p.Max())) } p.convergePoint = val } // ConvergePoint returns the value where the diverging colors meet. func (p *smoothDiverging) ConvergePoint() float64 { return p.convergePoint } // interpolateMSHDiverging performs a color interpolation through MSH space, // where scalar is a number between 0 and 1 that the // color should be evaluated at, and convergePoint is a number between 0 and // 1 where the colors should converge. func (p *smoothDiverging) interpolateMSHDiverging(scalar, convergePoint float64) msh { startHTwist := hueTwist(p.start, p.convergeM) endHTwist := hueTwist(p.end, p.convergeM) if scalar < convergePoint { // interpolation factor interp := scalar / convergePoint return msh{ M: (p.convergeM-p.start.M)*interp + p.start.M, S: p.start.S * (1 - interp), H: p.start.H + startHTwist*interp, } } // interpolation factors interp1 := (scalar - 1) / (convergePoint - 1) interp2 := (scalar/convergePoint - 1) var H float64 if scalar > convergePoint { H = p.end.H + endHTwist*interp1 } return msh{ M: (p.convergeM-p.end.M)*interp1 + p.end.M, S: p.end.S * interp2, H: H, } } // Palette returns a palette.Palette with the specified number of colors. func (p smoothDiverging) Palette(n int) palette.Palette { if p.Max() == 0 && p.Min() == 0 { p.SetMin(0) p.SetMax(1) } delta := (p.max - p.min) / float64(n-1) c := make([]color.Color, n) for i := range c { v := p.min + delta*float64(i) var err error c[i], err = p.At(v) if err != nil { panic(err) } } return plte(c) } // SmoothBlueRed is a SmoothDiverging-class ColorMap ranging from blue to red. func SmoothBlueRed() palette.DivergingColorMap { start := msh{ M: 80, S: 1.08, H: -1.1, } end := msh{ M: 80, S: 1.08, H: 0.5, } return newSmoothDiverging(start, end, 88) } // SmoothPurpleOrange is a SmoothDiverging-class ColorMap ranging from purple to orange. func SmoothPurpleOrange() palette.DivergingColorMap { start := msh{ M: 64.97539711, S: 0.899434815, H: -0.899431964, } end := msh{ M: 85.00850996, S: 0.949730284, H: 0.950636521, } return newSmoothDiverging(start, end, 88) } // SmoothGreenPurple is a SmoothDiverging-class ColorMap ranging from green to purple. func SmoothGreenPurple() palette.DivergingColorMap { start := msh{ M: 78.04105346, S: 0.885011982, H: 2.499491379, } end := msh{ M: 64.97539711, S: 0.899434815, H: -0.899431964, } return newSmoothDiverging(start, end, 88) } // SmoothBlueTan is a SmoothDiverging-class ColorMap ranging from blue to tan. func SmoothBlueTan() palette.DivergingColorMap { start := msh{ M: 79.94788321, S: 0.798754784, H: -1.401313221, } end := msh{ M: 80.07193125, S: 0.799798811, H: 1.401089787, } return newSmoothDiverging(start, end, 88) } // SmoothGreenRed is a SmoothDiverging-class ColorMap ranging from green to red. func SmoothGreenRed() palette.DivergingColorMap { start := msh{ M: 78.04105346, S: 0.885011982, H: 2.499491379, } end := msh{ M: 76.96722122, S: 0.949483656, H: 0.499492043, } return newSmoothDiverging(start, end, 88) }