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

filled contourplot #361

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions src/UnicodePlots.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export Plot,
BrailleCanvas,
DensityCanvas,
HeatmapCanvas,
FilledCanvas,
BlockCanvas,
AsciiCanvas,
DotCanvas,
Expand Down Expand Up @@ -89,6 +90,7 @@ include("canvas/blockcanvas.jl")
include("canvas/asciicanvas.jl")
include("canvas/dotcanvas.jl")
include("canvas/heatmapcanvas.jl")
include("canvas/filledcanvas.jl")

include("description.jl")
include("volume.jl")
Expand Down
4 changes: 2 additions & 2 deletions src/canvas/asciicanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct AsciiCanvas{YS<:Function,XS<:Function} <: LookupCanvas
end

const N_ASCII = grid_type(AsciiCanvas)(512)
const ASCII_SIGNS = [
const ASCII_ENCODE = [
0b100_000_000 0b010_000_000 0b001_000_000
0b000_100_000 0b000_010_000 0b000_001_000
0b000_000_100 0b000_000_010 0b000_000_001
Expand Down Expand Up @@ -123,7 +123,7 @@ ASCII_DECODE[(N_ASCII + 1):typemax(N_ASCII)] = UNICODE_TABLE[1:(typemax(N_ASCII)
@inline y_pixel_per_char(::Type{<:AsciiCanvas}) = 3
@inline x_pixel_per_char(::Type{<:AsciiCanvas}) = 3

@inline lookup_encode(::AsciiCanvas) = ASCII_SIGNS
@inline lookup_encode(::AsciiCanvas) = ASCII_ENCODE
@inline lookup_decode(::AsciiCanvas) = ASCII_DECODE
@inline lookup_offset(::AsciiCanvas) = N_ASCII

Expand Down
7 changes: 3 additions & 4 deletions src/canvas/blockcanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ struct BlockCanvas{YS<:Function,XS<:Function} <: LookupCanvas
xscale::XS
end

const BLOCK_SIGNS = [
const N_BLOCK = grid_type(BlockCanvas)(16)
const BLOCK_ENCODE = [
0b1000 0b0100
0b0010 0b0001
]

const N_BLOCK = grid_type(BlockCanvas)(16)
const BLOCK_DECODE = Vector{Char}(undef, typemax(N_BLOCK))

BLOCK_DECODE[1] = ' '
Expand All @@ -51,7 +50,7 @@ BLOCK_DECODE[(N_BLOCK + 1):typemax(N_BLOCK)] = UNICODE_TABLE[1:(typemax(N_BLOCK)
@inline x_pixel_per_char(::Type{<:BlockCanvas}) = 2
@inline y_pixel_per_char(::Type{<:BlockCanvas}) = 2

@inline lookup_encode(::BlockCanvas) = BLOCK_SIGNS
@inline lookup_encode(::BlockCanvas) = BLOCK_ENCODE
@inline lookup_decode(::BlockCanvas) = BLOCK_DECODE
@inline lookup_offset(::BlockCanvas) = N_BLOCK

Expand Down
18 changes: 9 additions & 9 deletions src/canvas/braillecanvas.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
# braille dots composing ⣿
const BRAILLE_SIGNS = UnicodeType.([
'⠁' '⠈'
'⠂' '⠐'
'⠄' '⠠'
'⡀' '⢀'
])

"""
The type of canvas with the highest resolution for Unicode-based plotting.
It uses the Unicode characters for the Braille symbols to represent individual pixel.
Expand All @@ -28,6 +20,14 @@ struct BrailleCanvas{YS<:Function,XS<:Function} <: Canvas
xscale::XS
end

# braille dots composing ⣿
const BRAILLE_ENCODE = UnicodeType.([
'⠁' '⠈'
'⠂' '⠐'
'⠄' '⠠'
'⡀' '⢀'
])

@inline blank(c::BrailleCanvas) = Char(BLANK_BRAILLE)

@inline y_pixel_per_char(::Type{<:BrailleCanvas}) = 4
Expand Down Expand Up @@ -85,7 +85,7 @@ function pixel!(
char_x, char_y, char_x_off, char_y_off = pixel_to_char_point_off(c, pixel_x, pixel_y)
if checkbounds(Bool, c.grid, char_y, char_x)
if BLANK_BRAILLE ≤ (val = c.grid[char_y, char_x]) ≤ FULL_BRAILLE
c.grid[char_y, char_x] = val | BRAILLE_SIGNS[char_y_off, char_x_off]
c.grid[char_y, char_x] = val | BRAILLE_ENCODE[char_y_off, char_x_off]
end
set_color!(c, char_x, char_y, color, blend)
end
Expand Down
10 changes: 5 additions & 5 deletions src/canvas/densitycanvas.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const DEN_SIGNS = Ref((' ', '░', '▒', '▓', '█'))

"""
Unlike the `BrailleCanvas`, the density canvas does not simply mark a "pixel" as set.
Instead it increments a counter per character that keeps track of the frequency of pixels drawn in that character.
Expand All @@ -24,6 +22,8 @@ struct DensityCanvas{YS<:Function,XS<:Function,DS<:Function} <: Canvas
dscale::DS
end

const DEN_DECODE = Ref((' ', '░', '▒', '▓', '█'))

@inline y_pixel_per_char(::Type{<:DensityCanvas}) = 2
@inline x_pixel_per_char(::Type{<:DensityCanvas}) = 1

Expand Down Expand Up @@ -94,11 +94,11 @@ end

function print_row(io::IO, _, print_color, c::DensityCanvas, row::Integer)
1 ≤ row ≤ nrows(c) || throw(ArgumentError("`row` out of bounds: $row"))
signs = DEN_SIGNS[]
fact = (length(signs) - 1) / c.max_density[]
decoder = DEN_DECODE[]
fact = (length(decoder) - 1) / c.max_density[]
for col ∈ 1:ncols(c)
val = fact * c.dscale(c.grid[row, col])
print_color(io, c.colors[row, col], signs[round(Int, val, RoundNearestTiesUp) + 1])
print_color(io, c.colors[row, col], decoder[round(Int, val, RoundNearestTiesUp) + 1])
end
nothing
end
5 changes: 3 additions & 2 deletions src/canvas/dotcanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ struct DotCanvas{YS<:Function,XS<:Function} <: LookupCanvas
end

const N_DOT = grid_type(DotCanvas)(4)
const DOT_SIGNS = [0b10; 0b01]
const DOT_ENCODE = [0b10; 0b01]
const DOT_DECODE = Array{Char}(undef, typemax(N_DOT))

DOT_DECODE[1] = ' '
DOT_DECODE[2] = '.'
DOT_DECODE[3] = '\''
Expand All @@ -37,7 +38,7 @@ DOT_DECODE[(N_DOT + 1):typemax(N_DOT)] = UNICODE_TABLE[1:(typemax(N_DOT) - N_DOT
@inline y_pixel_per_char(::Type{<:DotCanvas}) = 2
@inline x_pixel_per_char(::Type{<:DotCanvas}) = 1

@inline lookup_encode(::DotCanvas) = DOT_SIGNS
@inline lookup_encode(::DotCanvas) = DOT_ENCODE
@inline lookup_decode(::DotCanvas) = DOT_DECODE
@inline lookup_offset(::DotCanvas) = N_DOT

Expand Down
99 changes: 99 additions & 0 deletions src/canvas/filledcanvas.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
FilledCanvas uses the Unicode [Symbols for Legacy Computing](https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing)
to draw boundary segments. This is used for filled contour plots.
"""

struct FilledCanvas{YS<:Function,XS<:Function} <: LookupCanvas
grid::Transpose{UInt16,Matrix{UInt16}}
colors::Transpose{ColorType,Matrix{ColorType}}
visible::Bool
blend::Bool
yflip::Bool
xflip::Bool
pixel_height::Int
pixel_width::Int
origin_y::Float64
origin_x::Float64
height::Float64
width::Float64
min_max::NTuple{2,UnicodeType}
yscale::YS
xscale::XS
end

const FILLED_LOOKUP = Dict{NTuple{2, Int},Char}()

FILLED_LOOKUP[(1, 4)]= '🭍'
FILLED_LOOKUP[(1, 5)]= '🭏'
FILLED_LOOKUP[(1, 6)]= '◣'
FILLED_LOOKUP[(1, 7)]= '🭀'
FILLED_LOOKUP[(2, 4)]= '🭌'
FILLED_LOOKUP[(2, 5)]= '🭎'
FILLED_LOOKUP[(2, 6)]= '🭐'
FILLED_LOOKUP[(2, 7)]= '▌'
FILLED_LOOKUP[(2, 8)]= '🭛'
FILLED_LOOKUP[(2, 9)]= '🭙'
FILLED_LOOKUP[(2, 10)]= '🭗'
FILLED_LOOKUP[(3, 7)]= '🭡'
FILLED_LOOKUP[(3, 8)]= '◤'
FILLED_LOOKUP[(3, 9)]= '🭚'
FILLED_LOOKUP[(3, 10)]= '🭘'
FILLED_LOOKUP[(4, 1)]= '🭣'
FILLED_LOOKUP[(4, 2)]= '🭢'
FILLED_LOOKUP[(4, 7)]= '🭟'
FILLED_LOOKUP[(4, 8)]= '🭠'
FILLED_LOOKUP[(4, 9)]= '🭜'
FILLED_LOOKUP[(4, 10)]= '🬂'
FILLED_LOOKUP[(5, 1)]= '🭥'
FILLED_LOOKUP[(5, 2)]= '🭤'
FILLED_LOOKUP[(5, 7)]= '🭝'
FILLED_LOOKUP[(5, 8)]= '🭞'
FILLED_LOOKUP[(5, 9)]= '🬎'
FILLED_LOOKUP[(5, 10)]= '🭧'
FILLED_LOOKUP[(6, 1)]= '◥'
FILLED_LOOKUP[(6, 2)]= '🭦'
FILLED_LOOKUP[(6, 9)]= '🭓'
FILLED_LOOKUP[(6, 10)]= '🭕'
FILLED_LOOKUP[(7, 1)]= '🭖'
FILLED_LOOKUP[(7, 2)]= '▐'
FILLED_LOOKUP[(7, 3)]= '🭋'
FILLED_LOOKUP[(7, 4)]= '🭉'
FILLED_LOOKUP[(7, 5)]= '🭇'
FILLED_LOOKUP[(7, 9)]= '🭒'
FILLED_LOOKUP[(7, 10)]= '🭔'
FILLED_LOOKUP[(8, 2)]= '🭅'
FILLED_LOOKUP[(8, 3)]= '◢'
FILLED_LOOKUP[(8, 4)]= '🭊'
FILLED_LOOKUP[(8, 5)]= '🭈'
FILLED_LOOKUP[(9, 2)]= '🭃'
FILLED_LOOKUP[(9, 3)]= '🭄'
FILLED_LOOKUP[(9, 4)]= '🭆'
FILLED_LOOKUP[(9, 5)]= '🬭'
FILLED_LOOKUP[(9, 6)]= '🬽'
FILLED_LOOKUP[(9, 7)]= '🬼'
FILLED_LOOKUP[(10, 2)]= '🭁'
FILLED_LOOKUP[(10, 3)]= '🭂'
FILLED_LOOKUP[(10, 4)]= '🬹'
FILLED_LOOKUP[(10, 5)]= '🭑'
FILLED_LOOKUP[(10, 6)]= '🬿'
FILLED_LOOKUP[(10, 7)]= '🬾'

const N_FILLED = grid_type(FilledCanvas)(56)
const FILLED_DECODE = Vector{Char}(undef, typemax(N_FILLED))

FILLED_DECODE[1] = EMPTY_BLOCK # replaced during rendering, not to overdraw lower layers
FILLED_DECODE[2] = FULL_BLOCK
let n = 0
for (_, v) ∈ sort(collect(FILLED_LOOKUP))
FILLED_DECODE[(n += 1)+2] = v
end
end

@inline x_pixel_per_char(::Type{<:FilledCanvas}) = 1
@inline y_pixel_per_char(::Type{<:FilledCanvas}) = 1

@inline lookup_encode(::FilledCanvas) = FILLED_ENCODE
@inline lookup_decode(::FilledCanvas) = FILLED_DECODE
@inline lookup_offset(::FilledCanvas) = N_FILLED

FilledCanvas(args...; kw...) = CreateLookupCanvas(FilledCanvas, (0, 56), args...; kw...)
8 changes: 4 additions & 4 deletions src/canvas/heatmapcanvas.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
const HEATMAP_SIGNS = [0 1; 0 1]
const HEATMAP_DECODE = [HALF_BLOCK; HALF_BLOCK]

"""
The `HeatmapCanvas` is also Unicode-based.
It has a half the resolution of the `BlockCanvas`.
Expand All @@ -24,12 +21,15 @@ struct HeatmapCanvas{YS<:Function,XS<:Function} <: LookupCanvas
xscale::XS
end

const HEATMAP_ENCODE = [0 1; 0 1]
const HEATMAP_DECODE = [HALF_BLOCK; HALF_BLOCK]

@inline nrows(c::HeatmapCanvas) = div(size(c.grid, 1) + 1, 2)

@inline y_pixel_per_char(::Type{<:HeatmapCanvas}) = 2
@inline x_pixel_per_char(::Type{<:HeatmapCanvas}) = 1

@inline lookup_encode(::HeatmapCanvas) = HEATMAP_SIGNS
@inline lookup_encode(::HeatmapCanvas) = HEATMAP_ENCODE
@inline lookup_decode(::HeatmapCanvas) = HEATMAP_DECODE

function HeatmapCanvas(args...; kw...)
Expand Down
1 change: 1 addition & 0 deletions src/common.jl
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ const BASES = (identity = nothing, ln = "ℯ", log2 = "2", log10 = "10")

const FULL_BLOCK = '█'
const HALF_BLOCK = '▄'
const EMPTY_BLOCK = '\0'

# standard terminals seem to respect a 4:3 aspect ratio
# unix.stackexchange.com/questions/148569/standard-terminal-font-aspect-ratio
Expand Down
23 changes: 19 additions & 4 deletions src/interface/contourplot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Draws a contour plot on a new canvas.
$(arguments(
(
A = "`Matrix` of interest for which contours are extracted, or `Function` evaluated as `f(x, y)`",
levels = "the number of contour levels",
levels = "the number of contour levels or a vector of levels",
); add = (Z_DESCRIPTION..., :x, :y, :canvas), remove = (:blend, :grid)
))

Expand Down Expand Up @@ -70,7 +70,22 @@ function contourplot(
pkw...,
)
A isa Function && (A = A.(x', y))
contourplot!(plot, x, y, A; colormap, okw...)
if canvas === FilledCanvas
# TODO: process levels, and fill the canvas

# Note that displaying the characters is not done here (delayed to using `show`, `display` or `savefig` on the plot)
# and that is handled in `src/show.jl`: https://github.com/JuliaPlots/UnicodePlots.jl/blob/master/src/show.jl#L301

# There, each canvas row is printed: since `FilledCanvas` is a subtype of the abstract type `LookupCanvas`,
# `lookup_decode` must be implemented such that decoding can occur in here:
# https://github.com/JuliaPlots/UnicodePlots.jl/blob/master/src/canvas/lookupcanvas.jl#L75

# I would suggest taking inspiration from `src/interface/heatmap.jl` for filling the grid and colors:
# https://github.com/JuliaPlots/UnicodePlots.jl/blob/master/src/interface/heatmap.jl#L192-L196
println("unimplemented")
else
contourplot!(plot, x, y, A; colormap, okw...)
end
end

@doc (@doc contourplot) function contourplot!(
Expand All @@ -81,11 +96,11 @@ end
name::AbstractString = KEYWORDS.name,
colormap = KEYWORDS.colormap,
zlim = KEYWORDS.zlim,
levels::Integer = 3,
levels::Union{Integer,AbstractVector{<:Real}} = 3,
)
isempty(name) || label!(plot, :r, string(name))

mA, MA = nanless_extrema(A)
mA, MA = nanless_extrema(levels isa Integer ? A : levels)
plot.cmap.lim = (mh, Mh) = is_auto(zlim) ? (mA, MA) : zlim
plot.cmap.callback = callback = colormap_callback(colormap)

Expand Down
12 changes: 12 additions & 0 deletions test/tst_contourplot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ end
test_ref("contourplot/gauss_$(levels)levels.txt", @show_col(p))
end

@testset "custom levels" begin
p = @binf contourplot(gaussian_2d()...; levels = [0.2, 0.5, 0.6, 0.8])
test_ref("contourplot/gauss_customlevels.txt", @show_col(p))
end

@testset "update contourplot" begin
p = @binf contourplot(gaussian_2d()...; levels = 2)
# mutate the colormap & number of levels
Expand All @@ -53,3 +58,10 @@ end
p = contourplot(x, y, z; levels = 10)
test_ref("contourplot/consistency.txt", @show_col(p))
end

@testset "filled `contourplot`" begin
himmelblau(x, y) = (x^2 + y - 11)^2 + (x + y^2 - 7)^2
x = y = range(-5, 5; length = 25)
p = contourplot(x, y, himmelblau; levels = [30, 80, 150, 400], canvas = FilledCanvas)
test_ref("contourplot/filled_himmelblau.txt", @show_col(p))
end