diff --git a/src/UnicodePlots.jl b/src/UnicodePlots.jl index 2ebdeed3..6c423ee0 100644 --- a/src/UnicodePlots.jl +++ b/src/UnicodePlots.jl @@ -25,6 +25,7 @@ export Plot, BrailleCanvas, DensityCanvas, HeatmapCanvas, + FilledCanvas, BlockCanvas, AsciiCanvas, DotCanvas, @@ -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") diff --git a/src/canvas/asciicanvas.jl b/src/canvas/asciicanvas.jl index 2d7313e5..6f61d0a5 100644 --- a/src/canvas/asciicanvas.jl +++ b/src/canvas/asciicanvas.jl @@ -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 @@ -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 diff --git a/src/canvas/blockcanvas.jl b/src/canvas/blockcanvas.jl index 6e4259d1..f092620c 100644 --- a/src/canvas/blockcanvas.jl +++ b/src/canvas/blockcanvas.jl @@ -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] = ' ' @@ -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 diff --git a/src/canvas/braillecanvas.jl b/src/canvas/braillecanvas.jl index a9a8616d..7636eb7e 100644 --- a/src/canvas/braillecanvas.jl +++ b/src/canvas/braillecanvas.jl @@ -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. @@ -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 @@ -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 diff --git a/src/canvas/densitycanvas.jl b/src/canvas/densitycanvas.jl index b1d400a5..416ba218 100644 --- a/src/canvas/densitycanvas.jl +++ b/src/canvas/densitycanvas.jl @@ -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. @@ -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 @@ -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 diff --git a/src/canvas/dotcanvas.jl b/src/canvas/dotcanvas.jl index 843f5dd8..90abf7e7 100644 --- a/src/canvas/dotcanvas.jl +++ b/src/canvas/dotcanvas.jl @@ -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] = '\'' @@ -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 diff --git a/src/canvas/filledcanvas.jl b/src/canvas/filledcanvas.jl new file mode 100644 index 00000000..c03a2f45 --- /dev/null +++ b/src/canvas/filledcanvas.jl @@ -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...) diff --git a/src/canvas/heatmapcanvas.jl b/src/canvas/heatmapcanvas.jl index 0946c310..d8b07fc5 100644 --- a/src/canvas/heatmapcanvas.jl +++ b/src/canvas/heatmapcanvas.jl @@ -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`. @@ -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...) diff --git a/src/common.jl b/src/common.jl index 1535122f..102cde55 100644 --- a/src/common.jl +++ b/src/common.jl @@ -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 diff --git a/src/interface/contourplot.jl b/src/interface/contourplot.jl index 8860e2b7..63223013 100644 --- a/src/interface/contourplot.jl +++ b/src/interface/contourplot.jl @@ -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) )) @@ -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!( @@ -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) diff --git a/test/tst_contourplot.jl b/test/tst_contourplot.jl index 674ee26b..b3bd85d8 100644 --- a/test/tst_contourplot.jl +++ b/test/tst_contourplot.jl @@ -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 @@ -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