Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend color for tuple of RGB components #145

Merged
merged 11 commits into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/UnicodePlots.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module UnicodePlots

using Dates
using Crayons
using StatsBase: Histogram, fit, percentile
using SparseArrays: AbstractSparseMatrix, findnz

Expand Down
20 changes: 10 additions & 10 deletions src/canvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ origin(c::Canvas) = (origin_x(c), origin_y(c))
Base.size(c::Canvas) = (width(c), height(c))
pixel_size(c::Canvas) = (pixel_width(c), pixel_height(c))

function pixel!(c::Canvas, pixel_x::Integer, pixel_y::Integer; color::Union{Int, Symbol} = :normal)
function pixel!(c::Canvas, pixel_x::Integer, pixel_y::Integer; color::UserColorType = :normal)
pixel!(c, pixel_x, pixel_y, color)
end

function points!(c::Canvas, x::Number, y::Number, color::Union{Int, Symbol})
function points!(c::Canvas, x::Number, y::Number, color::UserColorType)
origin_x(c) <= x <= origin_x(c) + width(c) || return c
origin_y(c) <= y <= origin_y(c) + height(c) || return c
plot_offset_x = x - origin_x(c)
Expand All @@ -18,32 +18,32 @@ function points!(c::Canvas, x::Number, y::Number, color::Union{Int, Symbol})
pixel!(c, floor(Int, pixel_x), floor(Int, pixel_y), color)
end

function points!(c::Canvas, x::Number, y::Number; color::Union{Int, Symbol} = :normal)
function points!(c::Canvas, x::Number, y::Number; color::UserColorType = :normal)
points!(c, x, y, color)
end

function points!(c::Canvas, X::AbstractVector, Y::AbstractVector, color::Union{Int, Symbol})
function points!(c::Canvas, X::AbstractVector, Y::AbstractVector, color::UserColorType)
length(X) == length(Y) || throw(DimensionMismatch("X and Y must be the same length"))
for I in eachindex(X, Y)
points!(c, X[I], Y[I], color)
end
c
end

function points!(c::Canvas, X::AbstractVector, Y::AbstractVector, color::AbstractVector{T}) where {T <: Union{Int, Symbol}}
function points!(c::Canvas, X::AbstractVector, Y::AbstractVector, color::AbstractVector{T}) where {T <: UserColorType}
(length(X) == length(color) && length(X) == length(Y)) || throw(DimensionMismatch("X, Y, and color must be the same length"))
for i in 1:length(X)
points!(c, X[i], Y[i], color[i])
end
c
end

function points!(c::Canvas, X::AbstractVector, Y::AbstractVector; color::Union{Int, Symbol} = :normal)
function points!(c::Canvas, X::AbstractVector, Y::AbstractVector; color::UserColorType = :normal)
points!(c, X, Y, color)
end

# Implementation of the digital differential analyser (DDA)
function lines!(c::Canvas, x1::Number, y1::Number, x2::Number, y2::Number, color::Union{Int, Symbol})
function lines!(c::Canvas, x1::Number, y1::Number, x2::Number, y2::Number, color::UserColorType)
if (x1 < origin_x(c) && x2 < origin_x(c)) ||
(x1 > origin_x(c) + width(c) && x2 > origin_x(c) + width(c))
return c
Expand Down Expand Up @@ -78,11 +78,11 @@ function lines!(c::Canvas, x1::Number, y1::Number, x2::Number, y2::Number, color
c
end

function lines!(c::Canvas, x1::Number, y1::Number, x2::Number, y2::Number; color::Union{Int, Symbol} = :normal)
function lines!(c::Canvas, x1::Number, y1::Number, x2::Number, y2::Number; color::UserColorType = :normal)
lines!(c, x1, y1, x2, y2, color)
end

function lines!(c::Canvas, X::AbstractVector, Y::AbstractVector, color::Union{Int, Symbol})
function lines!(c::Canvas, X::AbstractVector, Y::AbstractVector, color::UserColorType)
length(X) == length(Y) || throw(DimensionMismatch("X and Y must be the same length"))
for i in 1:(length(X)-1)
if isnan(X[i]) || isnan(X[i+1]) || isnan(Y[i]) || isnan(Y[i+1])
Expand All @@ -93,7 +93,7 @@ function lines!(c::Canvas, X::AbstractVector, Y::AbstractVector, color::Union{In
c
end

function lines!(c::Canvas, X::AbstractVector, Y::AbstractVector; color::Union{Int, Symbol} = :normal)
function lines!(c::Canvas, X::AbstractVector, Y::AbstractVector; color::UserColorType = :normal)
lines!(c, X, Y, color)
end

Expand Down
2 changes: 1 addition & 1 deletion src/canvas/asciicanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ instead.
"""
struct AsciiCanvas <: LookupCanvas
grid::Array{UInt16,2}
colors::Array{UInt8,2}
colors::Array{ColorType,2}
pixel_width::Int
pixel_height::Int
origin_x::Float64
Expand Down
2 changes: 1 addition & 1 deletion src/canvas/blockcanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ using binary operations.
"""
struct BlockCanvas <: LookupCanvas
grid::Array{UInt8,2}
colors::Array{UInt8,2}
colors::Array{ColorType,2}
pixel_width::Int
pixel_height::Int
origin_x::Float64
Expand Down
8 changes: 4 additions & 4 deletions src/canvas/braillecanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ that can individually be manipulated using binary operations.
"""
struct BrailleCanvas <: Canvas
grid::Array{Char,2}
colors::Array{UInt8,2}
colors::Array{ColorType,2}
pixel_width::Int
pixel_height::Int
origin_x::Float64
Expand Down Expand Up @@ -44,14 +44,14 @@ function BrailleCanvas(char_width::Int, char_height::Int;
pixel_width = char_width * x_pixel_per_char(BrailleCanvas)
pixel_height = char_height * y_pixel_per_char(BrailleCanvas)
grid = fill(Char(0x2800), char_width, char_height)
colors = fill(0x00, char_width, char_height)
colors = fill(nothing, char_width, char_height)
BrailleCanvas(grid, colors,
pixel_width, pixel_height,
Float64(origin_x), Float64(origin_y),
Float64(width), Float64(height))
end

function pixel!(c::BrailleCanvas, pixel_x::Int, pixel_y::Int, color::Symbol)
function pixel!(c::BrailleCanvas, pixel_x::Int, pixel_y::Int, color::UserColorType)
0 <= pixel_x <= c.pixel_width || return c
0 <= pixel_y <= c.pixel_height || return c
pixel_x = pixel_x < c.pixel_width ? pixel_x : pixel_x - 1
Expand All @@ -66,7 +66,7 @@ function pixel!(c::BrailleCanvas, pixel_x::Int, pixel_y::Int, color::Symbol)
char_y = floor(Int, pixel_y / c.pixel_height * ch) + 1
char_y_off = (pixel_y % 4) + 1
c.grid[char_x,char_y] = Char(UInt64(c.grid[char_x,char_y]) | UInt64(braille_signs[char_x_off, char_y_off]))
c.colors[char_x,char_y] = c.colors[char_x,char_y] | color_encode[color]
set_color!(c.colors, char_x, char_y, crayon_256_color(color))
c
end

Expand Down
8 changes: 4 additions & 4 deletions src/canvas/densitycanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ the canvas can thus draw the density of datapoints.
"""
mutable struct DensityCanvas <: Canvas
grid::Array{UInt,2}
colors::Array{UInt8,2}
colors::Array{ColorType,2}
pixel_width::Int
pixel_height::Int
origin_x::Float64
Expand Down Expand Up @@ -45,15 +45,15 @@ function DensityCanvas(char_width::Int, char_height::Int;
pixel_width = char_width * x_pixel_per_char(DensityCanvas)
pixel_height = char_height * y_pixel_per_char(DensityCanvas)
grid = fill(0, char_width, char_height)
colors = fill(0x00, char_width, char_height)
colors = fill(nothing, char_width, char_height)
DensityCanvas(grid, colors,
pixel_width, pixel_height,
Float64(origin_x), Float64(origin_y),
Float64(width), Float64(height),
1)
end

function pixel!(c::DensityCanvas, pixel_x::Int, pixel_y::Int, color::Symbol)
function pixel!(c::DensityCanvas, pixel_x::Int, pixel_y::Int, color::UserColorType)
0 <= pixel_x <= c.pixel_width || return c
0 <= pixel_y <= c.pixel_height || return c
pixel_x = pixel_x < c.pixel_width ? pixel_x : pixel_x - 1
Expand All @@ -63,7 +63,7 @@ function pixel!(c::DensityCanvas, pixel_x::Int, pixel_y::Int, color::Symbol)
char_y = floor(Int, pixel_y / c.pixel_height * ch) + 1
c.grid[char_x,char_y] += 1
c.max_density = max(c.max_density, c.grid[char_x,char_y])
c.colors[char_x,char_y] = c.colors[char_x,char_y] | color_encode[color]
set_color!(c.colors, char_x, char_y, crayon_256_color(color))
c
end

Expand Down
2 changes: 1 addition & 1 deletion src/canvas/dotcanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ instead.
"""
struct DotCanvas <: LookupCanvas
grid::Array{UInt8,2}
colors::Array{UInt8,2}
colors::Array{ColorType,2}
pixel_width::Int
pixel_height::Int
origin_x::Float64
Expand Down
28 changes: 11 additions & 17 deletions src/canvas/heatmapcanvas.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using Crayons

"""
The `HeatmapCanvas` is also Unicode-based.
It has a half the resolution of the `BlockCanvas`.
Expand All @@ -8,7 +6,7 @@ into two pixels (top and bottom).
"""
struct HeatmapCanvas <: LookupCanvas
grid::Array{UInt8,2}
colors::Array{UInt8,2}
colors::Array{ColorType,2}
pixel_width::Int
pixel_height::Int
origin_x::Float64
Expand All @@ -30,6 +28,8 @@ function HeatmapCanvas(args...; kwargs...)
CreateLookupCanvas(HeatmapCanvas, args...; min_char_width=1, min_char_height=1, kwargs...)
end

_toCrayon(c) = c === nothing ? 0 : (c isa Unsigned ? Int(c) : c)

function printrow(io::IO, c::HeatmapCanvas, row::Int)
0 < row <= nrows(c) || throw(ArgumentError("Argument row out of bounds: $row"))
y = 2*row
Expand All @@ -40,9 +40,9 @@ function printrow(io::IO, c::HeatmapCanvas, row::Int)
iscolor = get(io, :color, false)
for x in 1:ncols(c)
if iscolor
fgcol = Int(colors(c)[x, y])
fgcol = _toCrayon(c.colors[x, y])
if (y - 1) > 0
bgcol = Int(colors(c)[x, y - 1])
bgcol = _toCrayon(c.colors[x, y - 1])
print(io, Crayon(foreground=fgcol, background=bgcol), HALF_BLOCK)
# for odd numbers of rows, only print the foreground for the top row
else
Expand All @@ -63,25 +63,19 @@ function printcolorbarrow(io::IO, c::HeatmapCanvas, row::Int, colormap::Any, bor
max_z = lim[2]
if row == 1
# print top border and maximum z value
printstyled(io, b[:tl]; color = :light_black)
printstyled(io, b[:t]; color = :light_black)
printstyled(io, b[:t]; color = :light_black)
printstyled(io, b[:tr]; color = :light_black)
print_color(:light_black, io, b[:tl], b[:t], b[:t], b[:tr])
max_z_str = isinteger(max_z) ? max_z : float_round_log10(max_z)
print(io, plot_padding)
printstyled(io, max_z_str; color = :light_black)
print_color(:light_black, io, max_z_str)
elseif row == nrows(c)
# print bottom border and minimum z value
printstyled(io, b[:bl]; color = :light_black)
printstyled(io, b[:b]; color = :light_black)
printstyled(io, b[:b]; color = :light_black)
printstyled(io, b[:br]; color = :light_black)
print_color(:light_black, io, b[:bl], b[:b], b[:b], b[:br])
min_z_str = isinteger(min_z) ? min_z : float_round_log10(min_z)
print(io, plot_padding)
printstyled(io, min_z_str; color = :light_black)
print_color(:light_black, io, min_z_str)
else
# print gradient
printstyled(io, b[:l]; color = :light_black)
print_color(:light_black, io, b[:l])
# if min and max are the same, single color
if min_z == max_z
bgcol = colormap(1, 1, 1)
Expand All @@ -96,7 +90,7 @@ function printcolorbarrow(io::IO, c::HeatmapCanvas, row::Int, colormap::Any, bor
print(io, Crayon(foreground=fgcol, background=bgcol), HALF_BLOCK)
print(io, HALF_BLOCK)
print(io, Crayon(reset=true))
printstyled(io, b[:r]; color = :light_black)
print_color(:light_black, io, b[:r])

# print z label
if row == div(nrows(c), 2) + 1
Expand Down
14 changes: 5 additions & 9 deletions src/canvas/lookupcanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ function CreateLookupCanvas(
pixel_width = char_width * x_pixel_per_char(T)
pixel_height = char_height * y_pixel_per_char(T)
grid = fill(0x00, char_width, char_height)
colors = fill(0x00, char_width, char_height)
colors = fill(nothing, char_width, char_height)
T(grid, colors, pixel_width, pixel_height,
Float64(origin_x), Float64(origin_y),
Float64(width), Float64(height))
end

function pixel!(c::T, pixel_x::Int, pixel_y::Int, color::Union{Int, Symbol}) where {T <: LookupCanvas}
function pixel!(c::T, pixel_x::Int, pixel_y::Int, color::UserColorType) where {T <: LookupCanvas}
0 <= pixel_x <= pixel_width(c) || return c
0 <= pixel_y <= pixel_height(c) || return c
pixel_x = pixel_x < pixel_width(c) ? pixel_x : pixel_x - 1
Expand All @@ -51,13 +51,9 @@ function pixel!(c::T, pixel_x::Int, pixel_y::Int, color::Union{Int, Symbol}) whe
end
char_y = floor(Int, pixel_y / pixel_height(c) * ch) + 1
char_y_off = (pixel_y % y_pixel_per_char(T)) + 1
grid(c)[char_x, char_y] = grid(c)[char_x,char_y] | lookup_encode(c)[char_x_off, char_y_off]
if color isa Symbol
colors(c)[char_x, char_y] = colors(c)[char_x,char_y] | color_encode[color]
else
# don't attempt to blend colors if they have been explicitly specified
colors(c)[char_x, char_y] = color
end
grid(c)[char_x, char_y] |= lookup_encode(c)[char_x_off, char_y_off]
force = !(color isa Symbol) # don't attempt to blend colors if they have been explicitly specified
set_color!(c.colors, char_x, char_y, crayon_256_color(color); force=force)
c
end

Expand Down
58 changes: 39 additions & 19 deletions src/common.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const DOC_PLOT_PARAMS = """

- **`color`** : Color of the drawing.
Can be any of `:green`, `:blue`, `:red`, `:yellow`, `:cyan`,
`:magenta`, `:white`, `:normal`
`:magenta`, `:white`, `:normal`, an integer in the range `0`-`255` for Color256 palette,
or a tuple of three integers as RGB components.

- **`width`** : Number of characters per row that should be used
for plotting.
Expand Down Expand Up @@ -166,25 +167,44 @@ bordermap[:dashed] = border_dashed
bordermap[:dotted] = border_dotted
bordermap[:ascii] = border_ascii

const UserColorType = Union{Integer,Symbol,NTuple{3,Integer},Nothing} # allowed color type
const JuliaColorType = Union{Symbol,Int} # color type for printstyled (defined in base/util.jl)
const ColorType = Union{Nothing,UInt8} # internal UnicodePlots color type

const color_cycle = [:green, :blue, :red, :magenta, :yellow, :cyan]
const color_encode = Dict{Symbol,UInt8}()
const color_decode = Dict{UInt8,Symbol}()
color_encode[:normal] = 0b000
color_encode[:blue] = 0b001
color_encode[:red] = 0b010
color_encode[:magenta] = 0b011
color_encode[:green] = 0b100
color_encode[:cyan] = 0b101
color_encode[:yellow] = 0b110
for k in keys(color_encode)
v = color_encode[k]
color_decode[v] = k

function print_color(color::UserColorType, io::IO, args...)
printstyled(io, string(args...); color = julia_color(color))
end

function crayon_256_color(color::UserColorType)::ColorType
Copy link
Member Author

@t-bltg t-bltg Aug 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnnychen94, this seems a bit hacky, should I PR to Crayons.jl instead to have stable API conversions to 256 color ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proposing a stable API in Crayons sounds okay to me but I know very little about this. I don't know if it's useful, there are some discussions on JuliaGraphics/Colors.jl#473

Copy link
Member Author

@t-bltg t-bltg Aug 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the link. What we need in UnicodePlots is something generic working on all terminals. Since we use printstyled(...) I think the implementation in this PR is ok using 0-255 integers.

@KristofferC, do you have any advice for this ?

color in (:normal, :default, :nothing, nothing) && return nothing
ansicolor = Crayons._parse_color(color)
if ansicolor.style == Crayons.COLORS_16
return Crayons.val(ansicolor) % 60
elseif ansicolor.style == Crayons.COLORS_24BIT
return Crayons.val(Crayons.to_256_colors(ansicolor))
end
Crayons.val(ansicolor)
end
color_encode[:white] = 0b111
color_decode[0b111] = :white

function print_color(color::UInt8, io::IO, args...)
col = color in keys(color_decode) ? color_decode[color] : Int(color)
str = string(args...)
printstyled(io, str; color = col)
function julia_color(color::UserColorType)::JuliaColorType
if color isa Nothing
:normal
elseif color isa Symbol
color
elseif color isa Integer
Int(color)
else
julia_color(crayon_256_color(color))
end
end

@inline function set_color!(colors::Array{ColorType,2}, x::Int, y::Int, color::ColorType; force::Bool=false)
if color === nothing || colors[x, y] === nothing || force
colors[x, y] = color
else
colors[x, y] |= color
end
nothing
end
4 changes: 2 additions & 2 deletions src/graphics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ function Base.show(io::IO, c::GraphicsArea)
print_border_top(io, "", border_length, :solid, :light_black)
print(io, '\n')
for row in 1:nrows(c)
printstyled(io, b[:l]; color = :light_black)
print_color(:light_black, io, b[:l])
printrow(io, c, row)
printstyled(io, b[:r]; color = :light_black)
print_color(:light_black, io, b[:r])
print(io, '\n')
end
print_border_bottom(io, "", border_length, :solid, :light_black)
Expand Down
Loading