Skip to content

Commit

Permalink
feature/wglmakie-and-bonito-extensions (#171)
Browse files Browse the repository at this point in the history
* added package extension for wglmakie and bonito
* added a new json() case for returning binary data directly
* updated Plotting support docs section
  • Loading branch information
ndortega authored Mar 8, 2024
1 parent 1c9957c commit 700f421
Show file tree
Hide file tree
Showing 14 changed files with 343 additions and 15 deletions.
6 changes: 5 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

[compat]
Bonito = "^3"
CairoMakie = "0.11, 1"
DataStructures = "0.18.15, 1"
Dates = "^1"
Expand All @@ -33,16 +34,19 @@ Sockets = "^1"
Statistics = "^1"
StructTypes = "^1"
Suppressor = "0.2.6, 1"
WGLMakie = "^0.9"
julia = "^1.6"

[extras]
Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
Mustache = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70"
OteraEngine = "b2d7f28f-acd6-4007-8b26-bc27716e5513"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008"

[targets]
test = ["CairoMakie", "Mustache", "OteraEngine", "Pkg", "StructTypes", "Test", "Suppressor"]
test = ["Bonito", "CairoMakie", "Mustache", "OteraEngine", "Pkg", "StructTypes", "Test", "Suppressor", "WGLMakie"]
44 changes: 40 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,23 +539,59 @@ serveparallel()
```
## Plotting Support

Oxygen has a package extension for the CairoMakie.jl library that helps us return Figures directly from a request handler while keeping all operations in-memory. Each helper function will read the figure into an IOBuffer, set the content type, and return the binary data inside a `HTTP.Response`.
Oxygen is equipped with several package extensions that enhance its plotting capabilities. These extensions make it easy to return plots directly from request handlers. All operations are performed in-memory using an IOBuffer and return a `HTTP.Response`

Here are the currently supported helper utils: `png`, `svg`, `pdf`, `html`
Supported Packages and their helper utils:

- CairoMakie.jl: `png`, `svg`, `pdf`, `html`
- WGLMakie.jl: `html`
- Bonito.jl: `html`

#### CairoMakie.jl
```julia
using CairoMakie: heatmap
using Oxygen

# generate a random heatmap plot and return it as a png
@get "/plot/png" function()
@get "/cairo" function()
fig, ax, pl = heatmap(rand(50, 50))
png(fig)
end

serve()
```

#### WGLMakie.jl
```julia
using Bonito
using WGLMakie: heatmap
using Oxygen
using Oxygen: html # Bonito also exports html

@get "/wgl" function()
fig = heatmap(rand(50, 50))
html(fig)
end

serve()
```

#### Bonito.jl
```julia
using Bonito
using Oxygen
using Oxygen: html # Bonito also exports html

@get "/bonito" function()
app = App() do
return DOM.div(DOM.h1("hello world"))
end
html(app)
end

serve()
```


## Templating

Rather than building an internal engine for templating or adding additional dependencies, Oxygen
Expand Down
4 changes: 4 additions & 0 deletions demo/Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
[deps]
Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
Expand All @@ -16,3 +19,4 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
SwaggerMarkdown = "1b6eb727-ad4b-44eb-9669-b9596a6e760f"
WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008"
32 changes: 32 additions & 0 deletions demo/webglmakiedemo.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module WGLGLMakieDemo
using Oxygen
using Oxygen: text, html
using WGLMakie
using WGLMakie.Makie: FigureLike
using Bonito, FileIO, Colors, HTTP
WGLMakie.activate!()

get("/") do
text("home")
end

get("/plot") do
plt = heatmap(rand(50, 50))
html(plt)
end

get("/page") do
app = App() do session::Session
hue_slider = Slider(0:360)
color_swatch = DOM.div(class="h-6 w-6 p-2 m-2 rounded shadow")
onjs(session, hue_slider.value, js"""function (hue){
$(color_swatch).style.backgroundColor = "hsl(" + hue + ",60%,50%)"
}""")
return Row(hue_slider, color_swatch)
end
return html(app)
end

serve()

end
44 changes: 40 additions & 4 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,23 +539,59 @@ serveparallel()
```
## Plotting Support

Oxygen has a package extension for the CairoMakie.jl library that helps us return Figures directly from a request handler while keeping all operations in-memory. Each helper function will read the figure into an IOBuffer, set the content type, and return the binary data inside a `HTTP.Response`.
Oxygen is equipped with several package extensions that enhance its plotting capabilities. These extensions make it easy to return plots directly from request handlers. All operations are performed in-memory using an IOBuffer and return a `HTTP.Response`

Here are the currently supported helper utils: `png`, `svg`, `pdf`, `html`
Supported Packages and their helper utils:

- CairoMakie.jl: `png`, `svg`, `pdf`, `html`
- WGLMakie.jl: `html`
- Bonito.jl: `html`

#### CairoMakie.jl
```julia
using CairoMakie: heatmap
using Oxygen

# generate a random heatmap plot and return it as a png
@get "/plot/png" function()
@get "/cairo" function()
fig, ax, pl = heatmap(rand(50, 50))
png(fig)
end

serve()
```

#### WGLMakie.jl
```julia
using Bonito
using WGLMakie: heatmap
using Oxygen
using Oxygen: html # Bonito also exports html

@get "/wgl" function()
fig = heatmap(rand(50, 50))
html(fig)
end

serve()
```

#### Bonito.jl
```julia
using Bonito
using Oxygen
using Oxygen: html # Bonito also exports html

@get "/bonito" function()
app = App() do
return DOM.div(DOM.h1("hello world"))
end
html(app)
end

serve()
```


## Templating

Rather than building an internal engine for templating or adding additional dependencies, Oxygen
Expand Down
13 changes: 13 additions & 0 deletions src/extensions/load.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
using Requires

const PNG = MIME"image/png"()
const SVG = MIME"image/svg+xml"()
const PDF = MIME"application/pdf"()
const HTML = MIME"text/html"()

"""
response(content::String, status=200, headers=[]) :: HTTP.Response
Expand Down Expand Up @@ -31,4 +36,12 @@ function __init__()
# Plotting Extensions #
################################################################
@require CairoMakie="13f3f980-e62b-5c42-98c6-ff1f3baf88f0" include("plotting/cairomakie.jl")
@require Bonito="824d6782-a2ef-11e9-3a09-e5662e0c26f8" include("plotting/bonito.jl")
@require WGLMakie="276b4fcb-3e11-5398-bf8b-a0c2d153d008" begin
@require Bonito="824d6782-a2ef-11e9-3a09-e5662e0c26f8" begin
include("plotting/wglmakie.jl")
end
end


end
32 changes: 32 additions & 0 deletions src/extensions/plotting/bonito.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import HTTP
import .Core.Util: html # import the html function from util so we can override it
import .Bonito: Page, App

export html

"""
Converts a Figure object to the designated MIME type and wraps it inside an HTTP response.
"""
function response(content::App, mime_type::MIME, status::Int, headers::Vector)
# Force inlining all data & js dependencies
Page(exportable=true, offline=true)

# Convert & load the figure into an IOBuffer
io = IOBuffer()
show(io, mime_type, content)
body = take!(io)

# format the response
resp = HTTP.Response(status, headers, body)
HTTP.setheader(resp, "Content-Type" => string(mime_type))
HTTP.setheader(resp, "Content-Length" => string(sizeof(body)))
return resp
end


"""
html(app::Bonito.App) :: HTTP.Response
Convert a Bonito.App to HTML and wrap it inside an HTTP response.
"""
html(app::App, status=200, headers=[]) :: HTTP.Response = response(app, HTML, status, headers)
6 changes: 0 additions & 6 deletions src/extensions/plotting/cairomakie.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@ import .Core.Util: html # import the html function from util so we can override

export png, svg, pdf, html

# Here we list all our supported MIME types
const PNG = MIME"image/png"()
const SVG = MIME"image/svg+xml"()
const PDF = MIME"application/pdf"()
const HTML = MIME"text/html"()

"""
Converts a Figure object to the designated MIME type and wraps it inside an HTTP response.
"""
Expand Down
34 changes: 34 additions & 0 deletions src/extensions/plotting/wglmakie.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import HTTP
import .Core.Util: html # import the html function from util so we can override it
import .WGLMakie.Makie: FigureLike
import .Bonito: Page, App

export html

"""
Converts a Figure object to the designated MIME type and wraps it inside an HTTP response.
"""
function response(content::FigureLike, mime_type::MIME, status::Int, headers::Vector)
# Force inlining all data & js dependencies
Page(exportable=true, offline=true)

# Convert & load the figure into an IOBuffer
io = IOBuffer()
show(io, mime_type, content)
body = take!(io)

# format the response
resp = HTTP.Response(status, headers, body)
HTTP.setheader(resp, "Content-Type" => string(mime_type))
HTTP.setheader(resp, "Content-Length" => string(sizeof(body)))
return resp
end


"""
html(fig::Makie.FigureLike) :: HTTP.Response
Convert a Makie figure to HTML and wrap it inside an HTTP response.
"""
html(fig::FigureLike, status=200, headers=[]) :: HTTP.Response = response(fig, HTML, status, headers)

14 changes: 14 additions & 0 deletions src/utilities/render.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ function json(content::Any; status = 200, headers = []) :: HTTP.Response
return response
end

"""
json(content::Vector{UInt8}; status::Int, headers::Vector{Pair}) :: HTTP.Response
A helper function that can be passed binary data that should be interpreted as JSON.
No conversion is done on the content since it's already in binary format.
"""
function json(content::Vector{UInt8}; status = 200, headers = []) :: HTTP.Response
response = HTTP.Response(status, headers, body = content)
HTTP.setheader(response, "Content-Type" => "application/json; charset=utf-8")
HTTP.setheader(response, "Content-Length" => string(sizeof(content)))
return response
end


"""
xml(content::String; status::Int, headers::Vector{Pair}) :: HTTP.Response
Expand Down
61 changes: 61 additions & 0 deletions test/bonitotests.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module CairoMakieTests
using HTTP
using Test
using Bonito
using Oxygen; @oxidise
import Oxygen: text, html
using ..Constants

@testset "Bonito Utils tests" begin

app = App() do
return DOM.div(DOM.h1("hello world"), js"""console.log('hello world')""")
end

response = html(app)
@test response isa HTTP.Response
@test response.status == 200
@test HTTP.header(response, "Content-Type") == "text/html"
@test parse(Int, HTTP.header(response, "Content-Length")) >= 0
end

@testset "WGLMakie server tests" begin

get("/") do
text("hello world")
end

get("/html") do
html("hello world")
end

get("/plot/html") do
app = App() do
return DOM.div(DOM.h1("hello world"))
end
html(app)
end

serve(host=HOST, port=PORT, async=true, show_banner=false, access_log=nothing)

# Test overloaded text() function
r = HTTP.get("$localhost/")
@test r.status == 200
@test HTTP.header(r, "Content-Type") == "text/plain; charset=utf-8"
@test parse(Int, HTTP.header(r, "Content-Length")) >= 0

# Test overloaded html function
r = HTTP.get("$localhost/html")
@test r.status == 200
@test HTTP.header(r, "Content-Type") == "text/html; charset=utf-8"
@test parse(Int, HTTP.header(r, "Content-Length")) >= 0

# Test for /plot/html endpoint
r = HTTP.get("$localhost/plot/html")
@test r.status == 200
@test HTTP.header(r, "Content-Type") == "text/html"
@test parse(Int, HTTP.header(r, "Content-Length")) >= 0
terminate()
end

end
Loading

0 comments on commit 700f421

Please sign in to comment.