diff --git a/.gitignore b/.gitignore index 9a3a7241..f33edd0c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ deps/npm-debug.log .ipynb_checkpoints deps/build.log docs/Manifest.toml +Manifest.toml +Project.toml diff --git a/.travis.yml b/.travis.yml index 60637328..52473928 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ branches: - /v(\d+)\.(\d+)\.(\d+)/ matrix: allow_failures: - - julia: nightly + - julia: nightly addons: apt: packages: @@ -27,7 +27,7 @@ addons: script: - if [[ -a .git/shallow ]]; then git fetch --unshallow; fi - if [[ `uname` = "Linux" ]]; then TESTCMD="xvfb-run julia"; else TESTCMD="julia"; fi - - $TESTCMD --check-bounds=yes -e 'using Pkg; Pkg.clone(pwd()); Pkg.build("VegaLite"); Pkg.test("VegaLite"; coverage=true)' + - $TESTCMD --check-bounds=yes -e 'using Pkg; Pkg.clone(pwd()); Pkg.build("VegaLite"); Pkg.test("VegaLite", coverage=true)' after_success: - julia -e 'using Pkg; cd(Pkg.dir("VegaLite")); Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())' - julia -e 'using Pkg; cd(Pkg.dir("VegaLite")); Pkg.add("Coverage"); using Coverage; Coveralls.submit(process_folder())' diff --git a/docs/make.jl b/docs/make.jl index 51c9369d..5b7ce43e 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -12,11 +12,12 @@ makedocs( "User Guide" => Any[ "Vega-lite specifications" => "userguide/vlspec.md", "The @vlplot command" => "userguide/vlplotmacro.md", - "Data sources" => "userguide/data.md" + "Data sources" => "userguide/data.md", + "Using Vega" => "userguide/vega.md" ], "Examples" => Any[ "Simple Charts" => "examples/examples_simplecharts.md", - "Single-View Plots" => Any[ + "Single-View Plots" => Any[ "Bar Charts & Histograms" => "examples/examples_barchartshistograms.md", "Scatter & Strip Plots" => "examples/examples_scatter_strip_plots.md", "Line Charts" => "examples/examples_line_charts.md", diff --git a/docs/src/index.md b/docs/src/index.md index 01489ac8..c7c0b5a0 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,7 +2,7 @@ ## Overview -[VegaLite.jl](https://github.com/fredo-dedup/VegaLite.jl) is a plotting package for the [julia](https://julialang.org/) programming language. The package is based on [Vega-Lite](https://vega.github.io/vega-lite/), which extends a traditional [grammar of graphics](https://doi.org/10.1007/0-387-28695-0) API into a [grammar of interactive graphics](https://doi.org/10.1109/TVCG.2016.2599030). +[VegaLite.jl](https://github.com/fredo-dedup/VegaLite.jl) is a plotting package for the [julia](https://julialang.org/) programming language. The package is based on [Vega-Lite](https://vega.github.io/vega-lite/), which extends a traditional [grammar of graphics](https://doi.org/10.1007/0-387-28695-0) API into a [grammar of interactive graphics](https://doi.org/10.1109/TVCG.2016.2599030). Along with [Vega-Lite](https://vega.github.io/vega-lite/), there is basic support for [Vega](https://vega.github.io/vega/) graphics. [VegaLite.jl](https://github.com/fredo-dedup/VegaLite.jl) allows you to create a wide range of statistical plots. It exposes the full functionality of the underlying [Vega-Lite](https://vega.github.io/vega-lite/) and is a the same time tightly integrated into the julia ecosystem. Here is an example of a scatter plot: diff --git a/docs/src/userguide/vega.md b/docs/src/userguide/vega.md new file mode 100644 index 00000000..251dffc8 --- /dev/null +++ b/docs/src/userguide/vega.md @@ -0,0 +1,254 @@ +# Using Vega + +Basic support for Vega graphics is supported as part of VegaLite.jl. Vega specifications are more verbose than +VegaLite specifications, but with that verbosity comes more control/options - see the [Vega documentation](https://vega.github.io/vega/docs/) +for details on creating Vega plots. + +VegaLite.jl supports rendering Vega JSON specification graphics with interactivity via the REPL (launching a browser if available) +or JupyterLab. There are two methods to do this: the `vg_str` macro or directly creating a `VGSpec` with parsed JSON. + +## The `vg` string macro + +Similar to the `vl` string macro, the `vg` string macro takes the Vega spec as a JSON string and returns and renders a `VGSpec`. + +```julia +using VegaLite + +spec = vg""" + { + "$schema": "https://vega.github.io/schema/vega/v4.4.json", + "width": 400, + "height": 200, + "padding": 5, + + "data": [ + { + "name": "table", + "values": [ + {"category": "A", "amount": 28}, + {"category": "B", "amount": 55}, + {"category": "C", "amount": 43}, + {"category": "D", "amount": 91}, + {"category": "E", "amount": 81}, + {"category": "F", "amount": 53}, + {"category": "G", "amount": 19}, + {"category": "H", "amount": 87} + ] + } + ], + + "signals": [ + { + "name": "tooltip", + "value": {}, + "on": [ + {"events": "rect:mouseover", "update": "datum"}, + {"events": "rect:mouseout", "update": "{}"} + ] + } + ], + + "scales": [ + { + "name": "xscale", + "type": "band", + "domain": {"data": "table", "field": "category"}, + "range": "width", + "padding": 0.05, + "round": true + }, + { + "name": "yscale", + "domain": {"data": "table", "field": "amount"}, + "nice": true, + "range": "height" + } + ], + + "axes": [ + { "orient": "bottom", "scale": "xscale" }, + { "orient": "left", "scale": "yscale" } + ], + + "marks": [ + { + "type": "rect", + "from": {"data":"table"}, + "encode": { + "enter": { + "x": {"scale": "xscale", "field": "category"}, + "width": {"scale": "xscale", "band": 1}, + "y": {"scale": "yscale", "field": "amount"}, + "y2": {"scale": "yscale", "value": 0} + }, + "update": { + "fill": {"value": "steelblue"} + }, + "hover": { + "fill": {"value": "red"} + } + } + }, + { + "type": "text", + "encode": { + "enter": { + "align": {"value": "center"}, + "baseline": {"value": "bottom"}, + "fill": {"value": "#333"} + }, + "update": { + "x": {"scale": "xscale", "signal": "tooltip.category", "band": 0.5}, + "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -2}, + "text": {"signal": "tooltip.amount"}, + "fillOpacity": [ + {"test": "datum === tooltip", "value": 0}, + {"value": 1} + ] + } + } + } + ] + } + """ +``` + +## VGSpec + +When parameterizing a Vega spec via a function, it is often simpler to construct a `VGSpec` structure directly. + +```julia +using VegaLite +using JSON + +function bar_plot(data) + json_data = json(data) + + spec = """ + { + "$schema": "https://vega.github.io/schema/vega/v4.4.json", + "width": 400, + "height": 200, + "padding": 5, + + "data": [ + { + "name": "table", + "values": $(json_data) + } + ], + + "signals": [ + { + "name": "tooltip", + "value": {}, + "on": [ + {"events": "rect:mouseover", "update": "datum"}, + {"events": "rect:mouseout", "update": "{}"} + ] + } + ], + + "scales": [ + { + "name": "xscale", + "type": "band", + "domain": {"data": "table", "field": "category"}, + "range": "width", + "padding": 0.05, + "round": true + }, + { + "name": "yscale", + "domain": {"data": "table", "field": "amount"}, + "nice": true, + "range": "height" + } + ], + + "axes": [ + { "orient": "bottom", "scale": "xscale" }, + { "orient": "left", "scale": "yscale" } + ], + + "marks": [ + { + "type": "rect", + "from": {"data":"table"}, + "encode": { + "enter": { + "x": {"scale": "xscale", "field": "category"}, + "width": {"scale": "xscale", "band": 1}, + "y": {"scale": "yscale", "field": "amount"}, + "y2": {"scale": "yscale", "value": 0} + }, + "update": { + "fill": {"value": "steelblue"} + }, + "hover": { + "fill": {"value": "red"} + } + } + }, + { + "type": "text", + "encode": { + "enter": { + "align": {"value": "center"}, + "baseline": {"value": "bottom"}, + "fill": {"value": "#333"} + }, + "update": { + "x": {"scale": "xscale", "signal": "tooltip.category", "band": 0.5}, + "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -2}, + "text": {"signal": "tooltip.amount"}, + "fillOpacity": [ + {"test": "datum === tooltip", "value": 0}, + {"value": 1} + ] + } + } + } + ] + } + """ + + return VegaLite.VGSpec(JSON.parse(spec)) +end + +d = [(category="A", amount=28), + category="B", amount=55), + category="C", amount=43), + category="D", amount=91), + category="E", amount=81), + category="F", amount=53), + category="G", amount=19), + category="H", amount=87)] + +bar_plot(d) +``` + + +## Loading and saving vega specifications + +The `load` and `save` functions can be used to load and save vega specifications to and from disc. The following example loads a vega specification from a file named `myfigure.vega`: + +```julia +using VegaLite + +spec = loadvgspec("myfigure.vega") +``` + +To save a `VGSpec` to a file on disc, use the `save` function: + +```julia +using VegaLite + +spec = ... # Aquire a spec from somewhere + +savespec("myfigure.vega", spec) +``` + +!!! note + + Using the `load` and `save` function will be enabled in a future release. For now you should use `loadvgspec` and `savespec` instead (both of these functions will be deprecated once `load` and `save` are enabled). diff --git a/src/VegaLite.jl b/src/VegaLite.jl index 8d08f961..d9d33bca 100644 --- a/src/VegaLite.jl +++ b/src/VegaLite.jl @@ -71,6 +71,7 @@ actionlinks(b::Bool) = (global ACTIONSLINKS ; ACTIONSLINKS = b) ######################## includes ##################################### +abstract type AbstractVegaSpec end include("vgspec.jl") include("vlspec.jl") diff --git a/src/rendering/fileio.jl b/src/rendering/fileio.jl index 04ad7f06..463f8176 100644 --- a/src/rendering/fileio.jl +++ b/src/rendering/fileio.jl @@ -11,5 +11,5 @@ function fileio_load(f::FileIO.File{FileIO.format"vega"}) end function fileio_save(file::FileIO.File{FileIO.format"vega"}, data::VGSpec; include_data=true) - savevgspec(file.filename, data, include_data=include_data) + savespec(file.filename, data, include_data=include_data) end diff --git a/src/rendering/io.jl b/src/rendering/io.jl index 508db75f..2bf7103a 100644 --- a/src/rendering/io.jl +++ b/src/rendering/io.jl @@ -44,6 +44,12 @@ function loadspec(filename::AbstractString) return VLSpec{:plot}(JSON.parse(s)) end +""" + loadvgspec(filename::AbstractString) + +Load a vega specification from a file with name `filename`. Returns +a `VGSpec` object. +""" function loadvgspec(filename::AbstractString) s = read(filename, String) return VGSpec(JSON.parse(s)) @@ -56,17 +62,7 @@ Save the plot `v` as a vega-lite specification file with the name `filename`. The `include_data` argument controls whether the data should be included in the saved specification file. """ -function savespec(filename::AbstractString, v::VLSpec{:plot}; include_data=false) - output_dict = copy(v.params) - if !include_data - delete!(output_dict, "data") - end - open(filename, "w") do f - JSON.print(f, output_dict) - end -end - -function savevgspec(filename::AbstractString, v::VGSpec; include_data=false) +function savespec(filename::AbstractString, v::AbstractVegaSpec; include_data=false) output_dict = copy(v.params) if !include_data delete!(output_dict, "data") diff --git a/src/rendering/show.jl b/src/rendering/show.jl index c1dc7072..d3df9f44 100644 --- a/src/rendering/show.jl +++ b/src/rendering/show.jl @@ -1,15 +1,10 @@ -function Base.show(io::IO, m::MIME"text/plain", v::VLSpec) +function Base.show(io::IO, m::MIME"text/plain", v::AbstractVegaSpec) print(io, summary(v)) end -function Base.show(io::IO, m::MIME"text/plain", v::VGSpec) - print(io, summary(v)) -end - -function convert_to_svg(v::VLSpec{:plot}) +function convert_to_svg(v::AbstractVegaSpec, script_path::String) data = JSON.json(v.params) - script_path = joinpath(@__DIR__, "compilesvg.js") p = open(`$(nodejs_cmd()) $script_path`, "r+") writer = @async begin write(p, data) @@ -24,35 +19,28 @@ function convert_to_svg(v::VLSpec{:plot}) return res end +function convert_to_svg(v::VLSpec{:plot}) + script_path = joinpath(@__DIR__, "compilesvg.js") + return convert_to_svg(v, script_path) +end + function convert_to_svg(v::VGSpec) - data = JSON.json(v.params) script_path = joinpath(@__DIR__, "compilevg2svg.js") - p = open(`$(nodejs_cmd()) $script_path`, "r+") - writer = @async begin - write(p, data) - close(p.in) - end - reader = @async read(p, String) - wait(p) - res = fetch(reader) - if p.exitcode!=0 - throw(ArgumentError("Invalid spec")) - end - return res + return convert_to_svg(v, script_path) end Base.Multimedia.istextmime(::MIME{Symbol("application/vnd.vegalite.v2+json")}) = true -Base.Multimedia.istextmime(::MIME{Symbol("application/vnd.vega.v3+json")}) = true +Base.Multimedia.istextmime(::MIME{Symbol("application/vnd.vega.v4+json")}) = true function Base.show(io::IO, m::MIME"application/vnd.vegalite.v2+json", v::VLSpec{:plot}) print(io, JSON.json(v.params)) end -function Base.show(io::IO, m::MIME"application/vnd.vega.v3+json", v::VGSpec) +function Base.show(io::IO, m::MIME"application/vnd.vega.v4+json", v::VGSpec) print(io, JSON.json(v.params)) end -function Base.show(io::IO, m::MIME"image/svg+xml", v::VLSpec{:plot}) +function Base.show(io::IO, m::MIME"image/svg+xml", v::AbstractVegaSpec) svgHeader = """ @@ -62,29 +50,7 @@ function Base.show(io::IO, m::MIME"image/svg+xml", v::VLSpec{:plot}) print(io, convert_to_svg(v)) end -function Base.show(io::IO, m::MIME"image/svg+xml", v::VGSpec) - svgHeader = """ - - - """ - - print(io, svgHeader) - print(io, convert_to_svg(v)) - end - -function Base.show(io::IO, m::MIME"application/pdf", v::VLSpec{:plot}) - svgstring = convert_to_svg(v) - - r = Rsvg.handle_new_from_data(svgstring) - d = Rsvg.handle_get_dimensions(r) - - cs = Cairo.CairoPDFSurface(io, d.width,d.height) - c = Cairo.CairoContext(cs) - Rsvg.handle_render_cairo(c,r) - Cairo.finish(cs) -end - -function Base.show(io::IO, m::MIME"application/pdf", v::VGSpec) +function Base.show(io::IO, m::MIME"application/pdf", v::AbstractVegaSpec) svgstring = convert_to_svg(v) r = Rsvg.handle_new_from_data(svgstring) @@ -96,19 +62,7 @@ function Base.show(io::IO, m::MIME"application/pdf", v::VGSpec) Cairo.finish(cs) end -function Base.show(io::IO, m::MIME"application/eps", v::VLSpec{:plot}) - svgstring = convert_to_svg(v) - - r = Rsvg.handle_new_from_data(svgstring) - d = Rsvg.handle_get_dimensions(r) - - cs = Cairo.CairoEPSSurface(io, d.width,d.height) - c = Cairo.CairoContext(cs) - Rsvg.handle_render_cairo(c,r) - Cairo.finish(cs) -end - -function Base.show(io::IO, m::MIME"application/eps", v::VGSpec) +function Base.show(io::IO, m::MIME"application/eps", v::AbstractVegaSpec) svgstring = convert_to_svg(v) r = Rsvg.handle_new_from_data(svgstring) @@ -120,31 +74,7 @@ function Base.show(io::IO, m::MIME"application/eps", v::VGSpec) Cairo.finish(cs) end -# function Base.show(io::IO, m::MIME"application/postscript", v::VLSpec{:plot}) -# svgstring = convert_to_svg(v) - -# r = Rsvg.handle_new_from_data(svgstring) -# d = Rsvg.handle_get_dimensions(r) - -# cs = Cairo.CairoPSSurface(io, d.width,d.height) -# c = Cairo.CairoContext(cs) -# Rsvg.handle_render_cairo(c,r) -# Cairo.finish(cs) -# end - -function Base.show(io::IO, m::MIME"image/png", v::VLSpec{:plot}) - svgstring = convert_to_svg(v) - - r = Rsvg.handle_new_from_data(svgstring) - d = Rsvg.handle_get_dimensions(r) - - cs = Cairo.CairoImageSurface(d.width,d.height,Cairo.FORMAT_ARGB32) - c = Cairo.CairoContext(cs) - Rsvg.handle_render_cairo(c,r) - Cairo.write_to_png(cs,io) -end - -function Base.show(io::IO, m::MIME"image/png", v::VGSpec) +function Base.show(io::IO, m::MIME"image/png", v::AbstractVegaSpec) svgstring = convert_to_svg(v) r = Rsvg.handle_new_from_data(svgstring) diff --git a/src/vgspec.jl b/src/vgspec.jl index 9fc66057..4efe9e10 100644 --- a/src/vgspec.jl +++ b/src/vgspec.jl @@ -1,3 +1,3 @@ -struct VGSpec +struct VGSpec <: AbstractVegaSpec params::Union{Dict, Vector} end diff --git a/src/vlspec.jl b/src/vlspec.jl index a1464709..d55ede9b 100644 --- a/src/vlspec.jl +++ b/src/vlspec.jl @@ -4,7 +4,7 @@ # ############################################################################### -struct VLSpec{T} +struct VLSpec{T} <: AbstractVegaSpec params::Union{Dict, Vector} end vltype(::VLSpec{T}) where T = T @@ -49,7 +49,7 @@ function (p::VLSpec{:plot})(data) set_spec_data!(new_dict, it) detect_encoding_type!(new_dict, it) - return VLSpec{:plot}(new_dict) + return VLSpec{:plot}(new_dict) end function (p::VLSpec{:plot})(uri::URI) diff --git a/test/test_io.jl b/test/test_io.jl index 7d2fadfc..fb52ab82 100644 --- a/test/test_io.jl +++ b/test/test_io.jl @@ -73,7 +73,7 @@ Base.Filesystem.mktempdir() do folder vgpl1 = getvgplot() - VegaLite.savevgspec(joinpath(folder,"test1.vega"), vgpl1, include_data=true) + VegaLite.savespec(joinpath(folder,"test1.vega"), vgpl1, include_data=true) vgpl2 = VegaLite.loadvgspec(joinpath(folder,"test1.vega")) diff --git a/test/test_show.jl b/test/test_show.jl index 53094171..36fe5c5f 100644 --- a/test/test_show.jl +++ b/test/test_show.jl @@ -14,10 +14,10 @@ vg = VegaLite.VGSpec(Dict{String,Any}()) @test istextmime("application/vnd.vegalite.v2+json") -@test istextmime("application/vnd.vega.v3+json") +@test istextmime("application/vnd.vega.v4+json") @test sprint(show, "application/vnd.vegalite.v2+json", @vlplot(:point)) == "{\"mark\":\"point\"}" -@test sprint(show, "application/vnd.vega.v3+json", vg"{}") == "{}" +@test sprint(show, "application/vnd.vega.v4+json", vg"{}") == "{}" end