From bad8fb206e4877837772282cf94e6f2f89afefa6 Mon Sep 17 00:00:00 2001 From: chmerdon Date: Thu, 9 Apr 2026 11:05:30 +0200 Subject: [PATCH] Implementation of UnicodePlots extension --- CHANGELOG.md | 3 + Project.toml | 5 +- ext/GridVisualizeUnicodePlotsExt.jl | 380 ++++++++++++++++++++++++++++ src/dispatch.jl | 17 ++ 4 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 ext/GridVisualizeUnicodePlotsExt.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ffb3d90..b07b206f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [1.18.0] - 2026-04-15 +- new UnicodePlots.jl extension that at the moment supports gridplot and scalarplot in 1D and 2D + ## [1.17.1] - 2026-03-25 - `PyPlot/PythonPlot`: assign increasing `fignumber`s to each new context (user can see multiple plotting windows) diff --git a/Project.toml b/Project.toml index f5202d24..e8982003 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "GridVisualize" uuid = "5eed8a63-0fb0-45eb-886d-8d5a387d12b8" authors = ["Juergen Fuhrmann ", "Patrick Jaap "] -version = "1.17.1" +version = "1.18.0" [deps] ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" @@ -28,6 +28,7 @@ PlutoVista = "646e1f28-b900-46d7-9d87-d554eb38a413" PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee" PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" Triangulate = "f7e6ffb2-c36d-4f8f-a77e-16e897189344" +UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" VTKView = "955f2c64-5fd0-11e9-0ad0-3332e913311a" [extensions] @@ -37,6 +38,7 @@ GridVisualizePlotsExt = "Plots" GridVisualizePlutoVistaExt = "PlutoVista" GridVisualizePyPlotExt = "PyPlot" GridVisualizePythonPlotExt = "PythonPlot" +GridVisualizeUnicodePlotsExt = "UnicodePlots" GridVisualizeVTKViewExt = "VTKView" [compat] @@ -63,6 +65,7 @@ PyPlot = "2" PythonPlot = "1" StaticArrays = "1" Triangulate = "2, 3" +UnicodePlots = "3" VTKView = "0.1,0.2" julia = "1.9" diff --git a/ext/GridVisualizeUnicodePlotsExt.jl b/ext/GridVisualizeUnicodePlotsExt.jl new file mode 100644 index 00000000..31336cda --- /dev/null +++ b/ext/GridVisualizeUnicodePlotsExt.jl @@ -0,0 +1,380 @@ +""" + module GridVisualizeUnicodePlotsExt + +Extension module for UnicodePlots.jl +""" +module GridVisualizeUnicodePlotsExt + +import GridVisualize: initialize!, gridplot!, scalarplot!, bregion_cmap, region_cmap, reveal +using GridVisualize: UnicodePlotsType, GridVisualizer, SubVisualizer +using UnicodePlots: UnicodePlots +using ExtendableGrids: Coordinates, simplexgrid, ON_CELLS, ON_FACES, ON_EDGES, CellNodes, FaceNodes, BFaceNodes, CellGeometries, CellRegions, BFaceRegions, num_cells, num_nodes, local_celledgenodes, num_bfaceregions, num_cellregions, num_targets, interpolate! +using Colors: Colors, RGB, RGBA + +initialize!(p, ::Type{UnicodePlotsType}) = nothing + + +function reveal(p::GridVisualizer, ::Type{UnicodePlotsType}) + return p.context[:figure] +end + + +function reveal(ctx::SubVisualizer, TP::Type{UnicodePlotsType}) + if ctx[:show] || ctx[:reveal] + display(ctx[:figure]) + end + return nothing +end + + +function region_legend!(canvas, title, x, y, colors) + # legend by annotate! + for (i, char) in enumerate(title) + UnicodePlots.char_point!(canvas, x + i - 1, y, char, UInt32(0), false) + end + startx = x + length(title) + for r in 1:length(colors) + red, green, blue = UInt32.(colors[r]) + uint_color = (red << 16) | (green << 8) | blue + reg_string = "$r " + for char in reg_string + startx += 1 + UnicodePlots.char_point!(canvas, startx, y, char, uint_color, false) + end + end + return +end + +function gridplot!(ctx, TP::Type{UnicodePlotsType}, ::Type{Val{2}}, grid) + UnicodePlots = ctx[:Plotter] + + # find bounding box + coords = grid[Coordinates] + ex = extrema(view(coords, 1, :)) + ey = extrema(view(coords, 2, :)) + + # line color for interior edges + if typeof(ctx[:color]) <: RGB + color = ( + Int(round(ctx[:color].r * 255)), + Int(round(ctx[:color].g * 255)), + Int(round(ctx[:color].b * 255)), + ) + else + color = ctx[:color] + end + + # determine resolution (divided by 10, to reduce pixel count in the terminal) + resolution = ctx[:size] ./ 10 + legend_space = 4 + aspect = ctx[:aspect] * resolution[1] / (resolution[1] + legend_space) + + if (true) # auto scale feature, do we want this? + wx = ex[2] - ex[1] + wy = ey[2] - ey[1] + rescale = wx / wy * (resolution[1] / (2 * resolution[2])) + if rescale > 1 + resolution = (resolution[1] * aspect, Int(ceil(resolution[2] / rescale))) + else + resolution = (Int(ceil(resolution[1] * aspect / rescale)), resolution[2]) + end + end + + # we need an integer resolution + resolution = @. Int(round(resolution)) + + # create UnicodePlots.Canvas + padding = 0.1 * max(ex[2] - ex[1], ey[2] - ey[1]) + ex = (ex[1] - 2 * padding, ex[2] + 0.5 * padding) + ey = (ey[1] - padding, ey[2] + padding) + CanvasType = UnicodePlots.BrailleCanvas # should this be a changeable parameter ? + canvas = CanvasType( + resolution[2], resolution[1] + legend_space, # number of rows and columns (characters) + origin_y = ey[1], origin_x = ex[1], # position in virtual space + height = (ey[2] - ey[1]) / (resolution[1] / (resolution[1] + legend_space)), width = ex[2] - ex[1]; blend = false + ) + + ## plot all edges in the grid + plot_based = ctx[:cellwise] ? ON_CELLS : ON_FACES + if plot_based in [ON_FACES, ON_EDGES] + # plot all edges via FaceNodes + facenodes = grid[FaceNodes] + nfaces = size(facenodes, 2) + for j in 1:nfaces + UnicodePlots.lines!( + canvas, + coords[1, facenodes[1, j]], coords[2, facenodes[1, j]], # from + coords[1, facenodes[2, j]], coords[2, facenodes[2, j]]; # to + color = color + ) + end + elseif plot_based == ON_CELLS + # plot all edges via CellNodes and local_celledgenodes + cellnodes = grid[CellNodes] + cellgeoms = grid[CellGeometries] + ncells = num_cells(grid) + for j in 1:ncells + cen = local_celledgenodes(cellgeoms[j]) + for k in 1:size(cen, 2) + UnicodePlots.lines!( + canvas, + coords[1, cellnodes[cen[1, k], j]], coords[2, cellnodes[cen[1, k], j]], + coords[1, cellnodes[cen[2, k], j]], coords[2, cellnodes[cen[2, k], j]]; + color = color + ) + end + end + end + + # color cell midpoints with cell regions color + cellregions = grid[CellRegions] + ncellregions = num_cellregions(grid) + cmap = region_cmap(max(2, ncellregions)) + ctx[:cmap] = cmap + colors = [ + ( + Int(round(cmap[i].r * 255)), + Int(round(cmap[i].g * 255)), + Int(round(cmap[i].b * 255)), + ) for i in 1:ncellregions + ] + cellnodes = grid[CellNodes] + cellgeoms = grid[CellGeometries] + ncells = num_cells(grid) + midpoint = [0.0, 0.0] + for j in 1:ncells + fill!(midpoint, 0.0) + nvertices = num_targets(cellnodes, j) + for k in 1:nvertices + midpoint .+= coords[:, cellnodes[k, j]] + end + midpoint ./= nvertices + r = cellregions[j] + UnicodePlots.points!( + canvas, + midpoint[1], midpoint[2]; + color = colors[r] + ) + end + + # plot boundary faces with bregion_cmap colors + nbregions = num_bfaceregions(grid) + bcmap = bregion_cmap(nbregions) + ctx[:bcmap] = bcmap + bcolors = [ + ( + Int(round(bcmap[i].r * 255)), + Int(round(bcmap[i].g * 255)), + Int(round(bcmap[i].b * 255)), + ) for i in 1:nbregions + ] + bfacenodes = grid[BFaceNodes] + bfaceregions = grid[BFaceRegions] + nbfaces = size(bfacenodes, 2) + for j in 1:nbfaces + UnicodePlots.lines!( + canvas, + coords[1, bfacenodes[1, j]], coords[2, bfacenodes[1, j]], + coords[1, bfacenodes[2, j]], coords[2, bfacenodes[2, j]]; + color = bcolors[bfaceregions[j]] + ) + end + + region_legend!(canvas, "cell regions: ", 2, 1, colors) + region_legend!(canvas, "bface regions:", 2, 2, bcolors) + + # corner coordinates + ex = extrema(view(coords, 1, :)) + ey = extrema(view(coords, 2, :)) + UnicodePlots.annotate!(canvas, ex[1], ey[1], "$(ex[1])", UInt32(0), false; valign = :top) + UnicodePlots.annotate!(canvas, ex[2], ey[1], "$(ex[2])", UInt32(0), false; valign = :top, halign = :right) + UnicodePlots.annotate!(canvas, ex[1] - 1.5 * padding, ey[1], "$(ey[1])", UInt32(0), false; halign = :left) + UnicodePlots.annotate!(canvas, ex[1] - 1.5 * padding, ey[2], "$(ey[2])", UInt32(0), false; halign = :left) + + # plot + ctx[:figure] = UnicodePlots.Plot(canvas; title = ctx[:title]) + return reveal(ctx, TP) +end + + +function gridplot!(ctx, TP::Type{UnicodePlotsType}, ::Type{Val{1}}, grid) + UnicodePlots = ctx[:Plotter] + + # find bounding box + coords = grid[Coordinates] + ex = extrema(view(coords, 1, :)) + + # line color for interior edges + if typeof(ctx[:color]) <: RGB + color = ( + Int(round(ctx[:color].r * 255)), + Int(round(ctx[:color].g * 255)), + Int(round(ctx[:color].b * 255)), + ) + else + color = ctx[:color] + end + + # determine resolution (divided by 10, to reduce pixel count in the terminal) + resolution = (Int(round(ctx[:size][1] / 10)), 5) + + # create UnicodePlots.Canvas + legend_space = 5 + padding = 0.05 * (ex[2] - ex[1]) + ex = (ex[1] - padding, ex[2] + padding) + CanvasType = UnicodePlots.BrailleCanvas # should this be a changeable parameter ? + canvas = CanvasType( + resolution[2], resolution[1] + legend_space, # number of rows and columns (characters) + origin_y = 0, origin_x = ex[1], # position in virtual space + height = 1, width = ex[2] - ex[1]; blend = false + ) + + # plot all edges in the grid + cellregions = grid[CellRegions] + ncellregions = num_cellregions(grid) + cmap = region_cmap(max(2, ncellregions)) + ctx[:cmap] = cmap + colors = [ + ( + Int(round(cmap[i].r * 255)), + Int(round(cmap[i].g * 255)), + Int(round(cmap[i].b * 255)), + ) for i in 1:ncellregions + ] + cellnodes = grid[CellNodes] + cellgeoms = grid[CellGeometries] + ncells = num_cells(grid) + nnodes = num_nodes(grid) + for j in 1:ncells + cen = local_celledgenodes(cellgeoms[j]) + r = cellregions[j] + for k in 1:size(cen, 2) + UnicodePlots.lines!( + canvas, + coords[1, cellnodes[cen[1, k], j]], 0.3, + coords[1, cellnodes[cen[2, k], j]], 0.3; + color = colors[r] + ) + end + end + for j in 1:nnodes + UnicodePlots.annotate!(canvas, coords[1, j], 0.4, "•", UInt32(0), false) + end + + # plot boundary nodes with bregion_cmap colors + nbregions = num_bfaceregions(grid) + bcmap = bregion_cmap(nbregions) + ctx[:bcmap] = bcmap + bcolors = [ + ( + Int(round(bcmap[i].r * 255)), + Int(round(bcmap[i].g * 255)), + Int(round(bcmap[i].b * 255)), + ) for i in 1:nbregions + ] + bfacenodes = grid[BFaceNodes] + bfaceregions = grid[BFaceRegions] + nbfaces = size(bfacenodes, 2) + for j in 1:nbfaces + red, green, blue = UInt32.(bcolors[bfaceregions[j]]) + uint_color = (red << 16) | (green << 8) | blue + UnicodePlots.annotate!(canvas, coords[1, bfacenodes[1, j]], 0.4, "•", UInt32(uint_color), false) + end + + region_legend!(canvas, "cell regions: ", 2, 1, colors) + region_legend!(canvas, "bface regions:", 2, 2, bcolors) + + + ex = extrema(view(coords, 1, :)) + UnicodePlots.annotate!(canvas, 0, 0.1, "$(ex[1])", UInt32(0), false) + UnicodePlots.annotate!(canvas, ex[2], 0.1, "$(ex[2])", UInt32(0), false) + + # plot + ctx[:figure] = UnicodePlots.Plot(canvas; title = ctx[:title]) + + return reveal(ctx, TP) +end + + +function scalarplot!( + ctx, + TP::Type{UnicodePlotsType}, + ::Type{Val{1}}, + grids, + parentgrid, + funcs + ) + + nfuncs = length(funcs) + resolution = @. Int(round(ctx[:size] ./ 10)) # reduce pixel count in the terminal + ylim = ctx[:limits] + + if ylim[1] > ylim[2] + # try to find limits automatically + ylim = (minimum([minimum(func) for func in funcs]), maximum([maximum(func) for func in funcs])) + end + + plt = ctx[:clear] ? nothing : ctx[:figure] + for ifunc in 1:nfuncs + func = funcs[ifunc] + grid = grids[ifunc] + coord = grid[Coordinates] * ctx[:gridscale] + if ifunc == 1 + plt = UnicodePlots.lineplot(coord[1, :], func; ylim, xlabel = "x", name = ctx[:label], height = resolution[2], width = resolution[1]) + else + UnicodePlots.lineplot!(plt, coord[1, :], func; name = ctx[:label]) + end + end + ctx[:figure] = plt + + return reveal(ctx, TP) +end + + +function scalarplot!( + ctx, + TP::Type{UnicodePlotsType}, + ::Type{Val{2}}, + grids, + parentgrid, + funcs + ) + + func = funcs[1] + resolution = @. Int(round(ctx[:size] ./ 10)) # reduce pixel count in the terminal + ylim = ctx[:limits] + colormap = ctx[:colormap] + + if ylim[1] > ylim[2] + # try to find limits automatically + ylim = (minimum([minimum(func) for func in funcs]), maximum([maximum(func) for func in funcs])) + end + + coords = grids[1][Coordinates] + ex = extrema(view(coords, 1, :)) + ey = extrema(view(coords, 2, :)) + + X = LinRange(ex[1], ex[2], resolution[1]) + Y = LinRange(ey[1], ey[2], resolution[2]) + xgrid_plot = simplexgrid(X, Y) + + # interpolate data onto plot_grid + I = zeros(Float64, num_nodes(xgrid_plot)) + interpolate!(I, xgrid_plot, func, grids[1]; eps = 1.0e-14, not_in_domain_value = NaN, trybrute = true) + + ctx[:figure] = UnicodePlots.heatmap( + reshape(I, (resolution[1], resolution[2]))', + xlabel = "x", + ylabel = "y", + xfact = (ex[2] - ex[1]) / (resolution[1] - 1), + yfact = (ey[2] - ey[1]) / (resolution[2] - 1), + xoffset = ex[1], + yoffset = ey[1], + title = ctx[:title], + colormap = colormap, + ) + + return reveal(ctx, TP) +end + +end # module diff --git a/src/dispatch.jl b/src/dispatch.jl index 7eb22632..97a3dadf 100644 --- a/src/dispatch.jl +++ b/src/dispatch.jl @@ -70,6 +70,13 @@ Heuristically check if Plotter is PlutoVista """ isplutovista(Plotter) = (typeof(Plotter) == Module) && isdefined(Plotter, :PlutoVistaPlot) +""" +$(SIGNATURES) + +Heuristically check if Plotter is UnicodePlots +""" +isunicodeplots(Plotter) = (typeof(Plotter) == Module) && isdefined(Plotter, :BrailleCanvas) + """ $(TYPEDEF) @@ -129,6 +136,13 @@ Abstract type for dispatching on plotter """ abstract type PlutoVistaType <: AbstractPlotterType end +""" +$(TYPEDEF) + +Abstract type for dispatching on plotter +""" +abstract type UnicodePlotsType <: AbstractPlotterType end + """ $(SIGNATURES) @@ -149,6 +163,8 @@ function plottertype(Plotter::Union{Module, Nothing}) return MeshCatType elseif isplutovista(Plotter) return PlutoVistaType + elseif isunicodeplots(Plotter) + return UnicodePlotsType end return Nothing end @@ -158,6 +174,7 @@ plottername(::Type{PlotsType}) = "Plots" plottername(::Type{PyPlotType}) = "PyPlot" plottername(::Type{PythonPlotType}) = "PythonPlot" plottername(::Type{PlutoVistaType}) = "PlutoVista" +plottername(::Type{UnicodePlotsType}) = "UnicodePlots" plottername(::Type{VTKViewType}) = "VTKView" plottername(::Type{MeshCatType}) = "MeshCat" plottername(::Type{Nothing}) = "nothing"