Skip to content

Commit

Permalink
Add support for text annotation on an existing plot (#171)
Browse files Browse the repository at this point in the history
* Add support for text annotation on an existing plot

* Change default text color from auto to normal

* Add halign = (:left, :center, :right) and valign = (:top, :center, :bottom) to align the text around the specified point. Use :center by default.

* Fix bug when specified point falls outside of the plot limits

* Rename annotate! -> label!

* Add unit test

* Fix halign bug (unit offset)

* Add `annotate!` support for :AsciiCanvas, :DotCanvas, :BlockCanvas

* Grow lookup tables ascii -> unicode subset

* Rework char position computation

* Add deprecation warning (`annotate!` -> `label!`)

* Cleanup / indent

* Add :hcenter and :vcenter aliases for Plots

* Move deprecated functions tests to test/tst_deprecated.jl

Co-authored-by: t-bltg <[email protected]>
  • Loading branch information
mtfishman and t-bltg authored Sep 14, 2021
1 parent b608635 commit 79b8f13
Show file tree
Hide file tree
Showing 36 changed files with 649 additions and 255 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/Manifest.toml
/test/Manifest.toml
.DS_Store
*.swp
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,19 +282,22 @@ _Note_: If you want to print the plot into a file but have monospace issues with

- `ylabel` the string to display on the far left of the plot window.

The method `annotate!` is responsible for the setting all the textual decorations of a plot. It has two functions:
The method `label!` is responsible for the setting all the textual decorations of a plot. It has two functions:

- `annotate!(plot::Plot, where::Symbol, value::String)`
- `label!(plot::Plot, where::Symbol, value::String)`

- `where` can be any of: `:tl` (top-left), `:t` (top-center), `:tr` (top-right), `:bl` (bottom-left), `:b` (bottom-center), `:br` (bottom-right), `:l` (left), `:r` (right)

- `annotate!(plot::Plot, where::Symbol, row::Int, value::String)`
- `label!(plot::Plot, where::Symbol, row::Int, value::String)`

- `where` can be any of: `:l` (left), `:r` (right)

- `row` can be between 1 and the number of character rows of the canvas

![Annotate Screenshot](https://github.com/JuliaPlots/UnicodePlots.jl/raw/unicodeplots-docs/doc/imgs/1.x/annotate.png)
![Label Screenshot](https://github.com/JuliaPlots/UnicodePlots.jl/raw/unicodeplots-docs/doc/imgs/1.x/annotate.png)

- `annotate!(plot::Plot, x::Number, y::Number, text::AbstractString; kwargs...)`
- `text` arbitrary annotation at position (x, y)

## Know Issues

Expand Down
1 change: 1 addition & 0 deletions src/UnicodePlots.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export
xlabel, xlabel!,
ylabel, ylabel!,
zlabel, zlabel!,
label!,
annotate!,

barplot, barplot!,
Expand Down
71 changes: 63 additions & 8 deletions src/canvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ 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))

pixel!(c::Canvas, pixel_x::Integer, pixel_y::Integer; color::UserColorType = :normal) = pixel!(c, pixel_x, pixel_y, color)
pixel!(
c::Canvas, pixel_x::Integer, pixel_y::Integer; color::UserColorType = :normal
) = pixel!(c, pixel_x, pixel_y, color)

function points!(c::Canvas, x::Number, y::Number, color::UserColorType)
origin_x(c) <= (xs = fscale(x, c.xscale)) <= origin_x(c) + width(c) || return c
Expand All @@ -25,14 +27,18 @@ function points!(c::Canvas, X::AbstractVector, Y::AbstractVector, color::UserCol
end

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"))
(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

points!(c::Canvas, X::AbstractVector, Y::AbstractVector; color::UserColorType = :normal) = points!(c, X, Y, color)
points!(
c::Canvas, X::AbstractVector, Y::AbstractVector; color::UserColorType = :normal
) = points!(c, X, Y, color)

# Implementation of the digital differential analyser (DDA)
function lines!(c::Canvas, x1::Number, y1::Number, x2::Number, y2::Number, color::UserColorType)
Expand Down Expand Up @@ -80,7 +86,9 @@ function lines!(c::Canvas, x1::Number, y1::Number, x2::Number, y2::Number, color
c
end

lines!(c::Canvas, x1::Number, y1::Number, x2::Number, y2::Number; color::UserColorType = :normal) = lines!(c, x1, y1, x2, y2, color)
lines!(
c::Canvas, x1::Number, y1::Number, x2::Number, y2::Number; color::UserColorType = :normal
) = lines!(c, x1, y1, x2, y2, color)

function lines!(c::Canvas, X::AbstractVector, Y::AbstractVector, color::UserColorType)
length(X) == length(Y) || throw(DimensionMismatch("X and Y must be the same length"))
Expand All @@ -95,9 +103,9 @@ end

lines!(c::Canvas, X::AbstractVector, Y::AbstractVector; color::UserColorType = :normal) = lines!(c, X, Y, color)


function get_canvas_dimensions_for_matrix(
canvas::Type{T}, nrow::Int, ncol::Int, maxwidth::Int, maxheight::Int, width::Int, height::Int, margin::Int, padding::Int, out_stream::Union{Nothing,IO};
canvas::Type{T}, nrow::Int, ncol::Int, maxwidth::Int, maxheight::Int,
width::Int, height::Int, margin::Int, padding::Int, out_stream::Union{Nothing,IO};
extra_rows::Int = 0, extra_cols::Int = 0
) where {T <: Canvas}
min_canvheight = ceil(Int, nrow / y_pixel_per_char(T))
Expand All @@ -111,7 +119,7 @@ function get_canvas_dimensions_for_matrix(
maxwidth = maxwidth > 0 ? maxwidth : term_width - width_diff

if nrow == 0 && ncol == 0
return (0, 0, maxwidth, maxheight)
return 0, 0, maxwidth, maxheight
end

# Check if the size of the plot should be derived from the matrix
Expand Down Expand Up @@ -155,5 +163,52 @@ function get_canvas_dimensions_for_matrix(
width = round(Int, width)
height = round(Int, height)

(width, height, maxwidth, maxheight)
width, height, maxwidth, maxheight
end


function align_char_point(text::AbstractString, char_x::Integer, char_y::Integer, halign::Symbol, valign::Symbol)
nchar = length(text)
char_x = if halign in (:center, :hcenter)
char_x - nchar ÷ 2
elseif halign == :left
char_x
elseif halign == :right
char_x - (nchar - 1)
else
error("Argument `halign=$halign` not supported.")
end
char_y = if valign in (:center, :vcenter)
char_y
elseif valign == :top
char_y + 1
elseif valign == :bottom
char_y - 1
else
error("Argument `valign=$valign` not supported.")
end
char_x, char_y
end

function annotate!(
c::Canvas,
x::Number,
y::Number,
text::AbstractString,
color::UserColorType;
halign = :center,
valign = :center,
)
xs = fscale(x, c.xscale)
ys = fscale(y, c.yscale)
pixel_x = (xs - origin_x(c)) / width(c) * pixel_width(c)
pixel_y = pixel_height(c) - (ys - origin_y(c)) / height(c) * pixel_height(c)

char_x, char_y = pixel_to_char_point(c, pixel_x, pixel_y)
char_x, char_y = align_char_point(text, char_x, char_y, halign, valign)
for char in text
char_point!(c, char_x, char_y, char, color)
char_x += 1
end
c
end
23 changes: 16 additions & 7 deletions src/canvas/asciicanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const ascii_signs = [0b100_000_000 0b000_100_000 0b000_000_100;
0b001_000_000 0b000_001_000 0b000_000_001]

const ascii_lookup = Dict{UInt16,Char}()
const ascii_decode = Vector{Char}(undef, 512)
ascii_lookup[0b101_000_000] = '"'
ascii_lookup[0b111_111_111] = '@'
#ascii_lookup[0b011_110_011] = '$'
Expand Down Expand Up @@ -75,20 +74,24 @@ ascii_lookup[0b100_100_100] = '|'
ascii_lookup[0b001_001_001] = '|'
ascii_lookup[0b110_011_110] = '}'

ascii_decode[0b1] = ' '
for i in 1:511
const n_ascii = 512
const ascii_decode = Vector{Char}(undef, typemax(UInt16))
ascii_decode[1] = ' '
for i in 2:n_ascii
min_dist = typemax(Int)
min_char = ' '
for (k, v) in sort_by_keys(ascii_lookup)
cur_dist = count_ones(xor(UInt16(i), k))
cur_dist = count_ones(xor(UInt16(i - 1), k))
if cur_dist < min_dist
min_dist = cur_dist
min_char = v
end
end
ascii_decode[i + 1] = min_char
ascii_decode[i] = min_char
end

ascii_decode[(n_ascii+1):typemax(UInt16)] = unicode_table[1:(typemax(UInt16)-n_ascii)]

"""
As the name suggests the `AsciiCanvas` only uses
ASCII characters to draw it's content. Naturally,
Expand Down Expand Up @@ -121,6 +124,12 @@ end
@inline lookup_encode(c::AsciiCanvas) = ascii_signs
@inline lookup_decode(c::AsciiCanvas) = ascii_decode

function AsciiCanvas(args...; nargs...)
CreateLookupCanvas(AsciiCanvas, args...; nargs...)
AsciiCanvas(args...; nargs...) = CreateLookupCanvas(AsciiCanvas, args...; nargs...)

function char_point!(c::AsciiCanvas, char_x::Int, char_y::Int, char::Char, color::UserColorType)
if checkbounds(Bool, c.grid, char_x, char_y)
c.grid[char_x,char_y] = n_ascii + char
set_color!(c.colors, char_x, char_y, crayon_256_color(color))
end
c
end
16 changes: 12 additions & 4 deletions src/canvas/blockcanvas.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const block_signs = [0b1000 0b0010;
0b0100 0b0001]

const block_decode = Vector{Char}(undef, 16)
const n_block = 16
const block_decode = Vector{Char}(undef, typemax(UInt16))
block_decode[0b0000 + 1] = ' '
block_decode[0b0001 + 1] = ''
block_decode[0b0010 + 1] = ''
Expand All @@ -18,6 +19,7 @@ block_decode[0b1100 + 1] = '▀'
block_decode[0b1101 + 1] = ''
block_decode[0b1110 + 1] = ''
block_decode[0b1111 + 1] = ''
block_decode[(n_block+1):typemax(UInt16)] = unicode_table[1:(typemax(UInt16)-n_block)]

"""
The `BlockCanvas` is also Unicode-based.
Expand All @@ -29,7 +31,7 @@ into 4 pixels that can individually be manipulated
using binary operations.
"""
struct BlockCanvas <: LookupCanvas
grid::Array{UInt8,2}
grid::Array{UInt16,2}
colors::Array{ColorType,2}
pixel_width::Int
pixel_height::Int
Expand All @@ -47,6 +49,12 @@ end
@inline lookup_encode(c::BlockCanvas) = block_signs
@inline lookup_decode(c::BlockCanvas) = block_decode

function BlockCanvas(args...; nargs...)
CreateLookupCanvas(BlockCanvas, args...; nargs...)
BlockCanvas(args...; nargs...) = CreateLookupCanvas(BlockCanvas, args...; nargs...)

function char_point!(c::BlockCanvas, char_x::Int, char_y::Int, char::Char, color::UserColorType)
if checkbounds(Bool, c.grid, char_x, char_y)
c.grid[char_x,char_y] = n_block + char
set_color!(c.colors, char_x, char_y, crayon_256_color(color))
end
c
end
20 changes: 17 additions & 3 deletions src/canvas/braillecanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ function BrailleCanvas(char_width::Int, char_height::Int;
Float64(width), Float64(height), xscale, yscale)
end

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
function pixel_to_char_point(c::BrailleCanvas, pixel_x::Number, pixel_y::Number)
pixel_x = pixel_x < c.pixel_width ? pixel_x : pixel_x - 1
pixel_y = pixel_y < c.pixel_height ? pixel_y : pixel_y - 1
cw, ch = size(c.grid)
Expand All @@ -69,15 +67,31 @@ function pixel!(c::BrailleCanvas, pixel_x::Int, pixel_y::Int, color::UserColorTy
end
char_y = floor(Int, pixel_y / c.pixel_height * ch) + 1
char_y_off = (pixel_y % 4) + 1
char_x, char_y, char_x_off, char_y_off
end

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
char_x, char_y, char_x_off, char_y_off = pixel_to_char_point(c, pixel_x, pixel_y)
c.grid[char_x,char_y] = Char(UInt64(c.grid[char_x,char_y]) | UInt64(braille_signs[char_x_off, char_y_off]))
set_color!(c.colors, char_x, char_y, crayon_256_color(color))
c
end

function char_point!(c::BrailleCanvas, char_x::Int, char_y::Int, char::Char, color::UserColorType)
if checkbounds(Bool, c.grid, char_x, char_y)
c.grid[char_x,char_y] = char
set_color!(c.colors, char_x, char_y, crayon_256_color(color))
end
c
end

function printrow(io::IO, c::BrailleCanvas, row::Int)
0 < row <= nrows(c) || throw(ArgumentError("Argument row out of bounds: $row"))
y = row
for x in 1:ncols(c)
print_color(c.colors[x,y], io, c.grid[x,y])
end
nothing
end
15 changes: 10 additions & 5 deletions src/canvas/densitycanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,22 @@ function DensityCanvas(char_width::Int, char_height::Int;
pixel_width, pixel_height,
Float64(origin_x), Float64(origin_y),
Float64(width), Float64(height),
xscale, yscale, 1
)
xscale, yscale, 1)
end

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
function pixel_to_char_point(c::DensityCanvas, pixel_x::Number, pixel_y::Number)
pixel_x = pixel_x < c.pixel_width ? pixel_x : pixel_x - 1
pixel_y = pixel_y < c.pixel_height ? pixel_y : pixel_y - 1
cw, ch = size(c.grid)
char_x = floor(Int, pixel_x / c.pixel_width * cw) + 1
char_y = floor(Int, pixel_y / c.pixel_height * ch) + 1
char_x, char_y
end

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
char_x, char_y = pixel_to_char_point(c, pixel_x, pixel_y)
c.grid[char_x,char_y] += 1
c.max_density = max(c.max_density, c.grid[char_x,char_y])
set_color!(c.colors, char_x, char_y, crayon_256_color(color))
Expand All @@ -82,4 +86,5 @@ function printrow(io::IO, c::DensityCanvas, row::Int)
den_index = round(Int, c.grid[x,y] * val_scale, RoundNearestTiesUp) + 1
print_color(c.colors[x,y], io, signs[den_index])
end
nothing
end
16 changes: 12 additions & 4 deletions src/canvas/dotcanvas.jl
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const dot_signs = [0b10 0b01]

const dot_decode = Array{Char}(undef, 5)
const n_dot = 4
const dot_decode = Array{Char}(undef, typemax(UInt16))
dot_decode[0b00 + 1] = ' '
dot_decode[0b01 + 1] = '.'
dot_decode[0b10 + 1] = '\''
dot_decode[0b11 + 1] = ':'
dot_decode[(n_dot+1):typemax(UInt16)] = unicode_table[1:(typemax(UInt16)-n_dot)]

"""
Similar to the `AsciiCanvas`, the `DotCanvas` only uses
Expand All @@ -20,7 +22,7 @@ For `lineplot` we suggest to use the `AsciiCanvas`
instead.
"""
struct DotCanvas <: LookupCanvas
grid::Array{UInt8,2}
grid::Array{UInt16,2}
colors::Array{ColorType,2}
pixel_width::Int
pixel_height::Int
Expand All @@ -38,6 +40,12 @@ end
@inline lookup_encode(c::DotCanvas) = dot_signs
@inline lookup_decode(c::DotCanvas) = dot_decode

function DotCanvas(args...; nargs...)
CreateLookupCanvas(DotCanvas, args...; nargs...)
DotCanvas(args...; nargs...) = CreateLookupCanvas(DotCanvas, args...; nargs...)

function char_point!(c::DotCanvas, char_x::Int, char_y::Int, char::Char, color::UserColorType)
if checkbounds(Bool, c.grid, char_x, char_y)
c.grid[char_x,char_y] = n_dot + char
set_color!(c.colors, char_x, char_y, crayon_256_color(color))
end
c
end
8 changes: 5 additions & 3 deletions src/canvas/heatmapcanvas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ const HALF_BLOCK = '▄'

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

function HeatmapCanvas(args...; kwargs...)
CreateLookupCanvas(HeatmapCanvas, args...; min_char_width=1, min_char_height=1, kwargs...)
end
HeatmapCanvas(args...; kwargs...) = CreateLookupCanvas(
HeatmapCanvas, args...; min_char_width=1, min_char_height=1, kwargs...
)

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

Expand Down Expand Up @@ -57,6 +57,7 @@ function printrow(io::IO, c::HeatmapCanvas, row::Int)
if iscolor
print(io, Crayon(reset=true))
end
nothing
end

function printcolorbarrow(
Expand Down Expand Up @@ -104,5 +105,6 @@ function printcolorbarrow(
end
end
print(io, repeat(blank, max_len - length(label)))
nothing
end

Loading

0 comments on commit 79b8f13

Please sign in to comment.