Skip to content

Commit

Permalink
De-duplicate, fix, and improve color parsing
Browse files Browse the repository at this point in the history
There was a very similar color parsing implementation in `color.go` and `chartdraw/drawing/color.go`.  After benchmarking it was found that `color.go` was faster, but also incorrectly handled the alpha channel.

This commit updates the implementation in `chartdraw/drawing/color.go` to use this simpler and more flexible regex pattern.  Allowing both rgb and rgba formats to be parsed by `ColorFromRGBA` in a more performant way.  With this change the implementation in `color.go` was removed to instead defer to the improved `chartdraw/drawing/color.go` implementation.  PraseColor has been elevated to the `charts` package so this functionality can be easily found.
  • Loading branch information
jentfoo committed Dec 19, 2024
1 parent 50619c9 commit 4f84a1d
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 153 deletions.
128 changes: 49 additions & 79 deletions chartdraw/drawing/color.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,99 +44,70 @@ var (
ColorAqua = Color{R: 0, G: 255, B: 255, A: 255}
)

func parseHex(hex string) uint8 {
v, _ := strconv.ParseInt(hex, 16, 16)
return uint8(v)
}

// ParseColor parses a color from a string.
func ParseColor(rawColor string) Color {
if strings.HasPrefix(rawColor, "#") {
return ColorFromHex(rawColor)
} else if strings.HasPrefix(rawColor, "rgba") {
return ColorFromRGBA(rawColor)
} else if strings.HasPrefix(rawColor, "rgb") {
return ColorFromRGB(rawColor)
return ColorFromRGBA(rawColor)
}
return ColorFromKnown(rawColor)
}

var rgbaexpr = regexp.MustCompile(`rgba\((?P<R>.+),(?P<G>.+),(?P<B>.+),(?P<A>.+)\)`)
var rgbReg = regexp.MustCompile(`\(([^)]+)\)`)

// ColorFromRGBA returns a color from an `rgba()` css function.
func ColorFromRGBA(rgba string) (output Color) {
values := rgbaexpr.FindStringSubmatch(rgba)
for i, name := range rgbaexpr.SubexpNames() {
if i == 0 {
continue
}
if i >= len(values) {
break
}
switch name {
case "R":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.R = uint8(parsed)
case "G":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.G = uint8(parsed)
case "B":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.B = uint8(parsed)
case "A":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseFloat(value, 32)
if parsed > 1 {
parsed = 1
} else if parsed < 0 {
parsed = 0
}
output.A = uint8(parsed * 255)
}
// ColorFromRGBA returns a color from a `rgb(i,i,i)` or `rgba(i,i,i,f)` css function.
func ColorFromRGBA(color string) Color {
var c Color

// Attempt to parse rgb(...) or rgba(...)
result := rgbReg.FindAllStringSubmatch(color, 1)
if len(result) == 0 || len(result[0]) != 2 {
return c
}
arr := strings.Split(result[0][1], ",")
if len(arr) < 3 { // at a minimum we expect r,g,b to be specified
return c
}
return
}

var rgbexpr = regexp.MustCompile(`rgb\((?P<R>.+),(?P<G>.+),(?P<B>.+)\)`)
rVal, _ := strconv.ParseInt(strings.TrimSpace(arr[0]), 10, 16)
c.R = uint8(rVal)
gVal, _ := strconv.ParseInt(strings.TrimSpace(arr[1]), 10, 16)
c.G = uint8(gVal)
bVal, _ := strconv.ParseInt(strings.TrimSpace(arr[2]), 10, 16)
c.B = uint8(bVal)
if len(arr) > 3 { // if alpha channel is specified
aVal, _ := strconv.ParseFloat(strings.TrimSpace(arr[3]), 64)
if aVal < 0 {
aVal = 0
} else if aVal <= 1 {
// correctly specified decimal, convert it to an integer scale
aVal *= 255
} // else, incorrectly specified value over 1, accept the value directly
c.A = uint8(aVal)
} else {
c.A = 255 // default alpha channel to 255
}

// ColorFromRGB returns a color from an `rgb()` css function.
return c
}

// Deprecated: ColorFromRGB is deprecated, use ColorFromRGBA to get colors from RGB or RGBA format strings.
func ColorFromRGB(rgb string) (output Color) {
output.A = 255
values := rgbexpr.FindStringSubmatch(rgb)
for i, name := range rgbaexpr.SubexpNames() {
if i == 0 {
continue
}
if i >= len(values) {
break
}
switch name {
case "R":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.R = uint8(parsed)
case "G":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.G = uint8(parsed)
case "B":
value := strings.TrimSpace(values[i])
parsed, _ := strconv.ParseInt(value, 10, 16)
output.B = uint8(parsed)
}
}
return
return ColorFromRGBA(rgb)
}

func parseHex(hex string) uint8 {
v, _ := strconv.ParseInt(hex, 16, 16)
return uint8(v)
}

// ColorFromHex returns a color from a css hex code.
//
// NOTE: it will trim a leading '#' character if present.
func ColorFromHex(hex string) Color {
hex = strings.TrimPrefix(hex, "#")
var c Color
c := Color{A: 255}
if len(hex) == 3 {
c.R = parseHex(string(hex[0])) * 0x11
c.G = parseHex(string(hex[1])) * 0x11
Expand All @@ -146,7 +117,6 @@ func ColorFromHex(hex string) Color {
c.G = parseHex(hex[2:4])
c.B = parseHex(hex[4:6])
}
c.A = 255
return c
}

Expand Down Expand Up @@ -193,12 +163,12 @@ func ColorFromKnown(known string) Color {
// ColorFromAlphaMixedRGBA returns the system alpha mixed rgba values.
func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color {
fa := float64(a) / 255.0
var c Color
c.R = uint8(float64(r) / fa)
c.G = uint8(float64(g) / fa)
c.B = uint8(float64(b) / fa)
c.A = uint8(a | (a >> 8))
return c
return Color{
R: uint8(float64(r) / fa),
G: uint8(float64(g) / fa),
B: uint8(float64(b) / fa),
A: uint8(a | (a >> 8)),
}
}

// ColorChannelFromFloat returns a normalized byte from a given float value.
Expand Down
2 changes: 1 addition & 1 deletion chartdraw/drawing/color_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func Test_ColorFromRGBA(t *testing.T) {
parsed = ColorFromRGBA(value)
assert.Equal(t, ColorSilver, parsed)

value = "rgba(192,192,192,1.5)"
value = "rgba(192,192,192,255)"
parsed = ColorFromRGBA(value)
assert.Equal(t, ColorSilver, parsed)
}
Expand Down
43 changes: 4 additions & 39 deletions color.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package charts

import (
"math"
"regexp"
"strconv"
"strings"

"github.com/go-analyze/charts/chartdraw/drawing"
)
Expand All @@ -16,40 +13,8 @@ func isLightColor(c Color) bool {
return math.Sqrt(r+g+b) > 127.5
}

var rgbReg = regexp.MustCompile(`\((\S+)\)`)

// TODO - de-duplicate with chartdraw/drawing/color.go:ParseColor
func parseColor(color string) Color {
c := Color{}
if color == "" {
return c
}
if strings.HasPrefix(color, "#") {
return drawing.ColorFromHex(color[1:])
}
result := rgbReg.FindAllStringSubmatch(color, 1)
if len(result) == 0 || len(result[0]) != 2 {
return c
}
arr := strings.Split(result[0][1], ",")
if len(arr) < 3 {
return c
}
// set the default value to 255
c.A = 255
for index, v := range arr {
value, _ := strconv.ParseInt(strings.TrimSpace(v), 10, 16)
ui8 := uint8(value)
switch index {
case 0:
c.R = ui8
case 1:
c.G = ui8
case 2:
c.B = ui8
default:
c.A = ui8
}
}
return c
// ParseColor parses a color from a string. The color can be specified in hex with a `#` prefix (for example '#313233'),
// in rgb(i,i,i) or rgba(i,i,i,f) format, or as a common name (for example 'red').
func ParseColor(color string) Color {
return drawing.ParseColor(color)
}
22 changes: 17 additions & 5 deletions color_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,30 @@ func TestIsLightColor(t *testing.T) {
func TestParseColor(t *testing.T) {
t.Parallel()

c := parseColor("")
c := ParseColor("")
assert.True(t, c.IsZero())

c = parseColor("#333")
c = ParseColor("#333")
assert.Equal(t, drawing.Color{R: 51, G: 51, B: 51, A: 255}, c)

c = parseColor("#313233")
c = ParseColor("#313233")
assert.Equal(t, drawing.Color{R: 49, G: 50, B: 51, A: 255}, c)

c = parseColor("rgb(31,32,33)")
c = ParseColor("rgb(31,32,33)")
assert.Equal(t, drawing.Color{R: 31, G: 32, B: 33, A: 255}, c)

c = parseColor("rgba(50,51,52,250)")
c = ParseColor("rgba(50,51,52,.981)")
assert.Equal(t, drawing.Color{R: 50, G: 51, B: 52, A: 250}, c)

c = ParseColor("rgba(50,51,52,250)")
assert.Equal(t, drawing.Color{R: 50, G: 51, B: 52, A: 250}, c)
}

func BenchmarkParseColor(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ParseColor("#333")
_ = ParseColor("#313233")
_ = ParseColor("rgb(31,32,33)")
_ = ParseColor("rgba(50,51,52,250)")
}
}
8 changes: 4 additions & 4 deletions echarts.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
Min: item.Min,
Label: SeriesLabel{
FontStyle: FontStyle{
FontColor: parseColor(item.Label.Color),
FontColor: ParseColor(item.Label.Color),
},
Show: item.Label.Show,
Distance: item.Label.Distance,
Expand All @@ -319,7 +319,7 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
YAxisIndex: item.YAxisIndex,
Label: SeriesLabel{
FontStyle: FontStyle{
FontColor: parseColor(item.Label.Color),
FontColor: ParseColor(item.Label.Color),
},
Show: item.Label.Show,
Distance: item.Label.Distance,
Expand All @@ -342,7 +342,7 @@ func (et *EChartsTextStyle) ToStyle() chartdraw.Style {
s := chartdraw.Style{
FontStyle: chartdraw.FontStyle{
FontSize: et.FontSize,
FontColor: parseColor(et.Color),
FontColor: ParseColor(et.Color),
},
}
if et.FontFamily != "" {
Expand Down Expand Up @@ -455,7 +455,7 @@ func (eo *EChartsOption) ToOption() ChartOption {
Min: item.Min,
Max: item.Max,
Formatter: item.AxisLabel.Formatter,
AxisColor: parseColor(item.AxisLine.LineStyle.Color),
AxisColor: ParseColor(item.AxisLine.LineStyle.Color),
Data: item.Data,
}
}
Expand Down
50 changes: 25 additions & 25 deletions theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ var defaultDarkFontColor = drawing.Color{R: 238, G: 238, B: 238, A: 255}
func init() {
darkGray := Color{R: 40, G: 40, B: 40, A: 255}
echartSeriesColors := []Color{
parseColor("#5470c6"),
parseColor("#91cc75"),
parseColor("#fac858"),
parseColor("#ee6666"),
parseColor("#73c0de"),
parseColor("#3ba272"),
parseColor("#fc8452"),
parseColor("#9a60b4"),
parseColor("#ea7ccc"),
drawing.ColorFromHex("#5470c6"),
drawing.ColorFromHex("#91cc75"),
drawing.ColorFromHex("#fac858"),
drawing.ColorFromHex("#ee6666"),
drawing.ColorFromHex("#73c0de"),
drawing.ColorFromHex("#3ba272"),
drawing.ColorFromHex("#fc8452"),
drawing.ColorFromHex("#9a60b4"),
drawing.ColorFromHex("#ea7ccc"),
}
InstallTheme(
ThemeLight,
Expand Down Expand Up @@ -168,14 +168,14 @@ func init() {
BackgroundColor: drawing.ColorWhite,
TextColor: Color{R: 70, G: 70, B: 70, A: 255},
SeriesColors: []Color{
parseColor("#5b8ff9"),
parseColor("#5ad8a6"),
parseColor("#5d7092"),
parseColor("#f6bd16"),
parseColor("#6f5ef9"),
parseColor("#6dc8ec"),
parseColor("#945fb9"),
parseColor("#ff9845"),
drawing.ColorFromHex("#5b8ff9"),
drawing.ColorFromHex("#5ad8a6"),
drawing.ColorFromHex("#5d7092"),
drawing.ColorFromHex("#f6bd16"),
drawing.ColorFromHex("#6f5ef9"),
drawing.ColorFromHex("#6dc8ec"),
drawing.ColorFromHex("#945fb9"),
drawing.ColorFromHex("#ff9845"),
},
},
)
Expand All @@ -188,14 +188,14 @@ func init() {
BackgroundColor: Color{R: 31, G: 29, B: 29, A: 255},
TextColor: Color{R: 216, G: 217, B: 218, A: 255},
SeriesColors: []Color{
parseColor("#7EB26D"),
parseColor("#EAB839"),
parseColor("#6ED0E0"),
parseColor("#EF843C"),
parseColor("#E24D42"),
parseColor("#1F78C1"),
parseColor("#705DA0"),
parseColor("#508642"),
drawing.ColorFromHex("#7EB26D"),
drawing.ColorFromHex("#EAB839"),
drawing.ColorFromHex("#6ED0E0"),
drawing.ColorFromHex("#EF843C"),
drawing.ColorFromHex("#E24D42"),
drawing.ColorFromHex("#1F78C1"),
drawing.ColorFromHex("#705DA0"),
drawing.ColorFromHex("#508642"),
},
},
)
Expand Down

0 comments on commit 4f84a1d

Please sign in to comment.