diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 00000000..c5239b8c --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,59 @@ +name: CI +on: + pull_request: + branches: + - "master" + push: + branches: + - "master" + tags: + - "*" +defaults: + run: + shell: bash +env: + JULIA_NUM_THREADS: auto +jobs: + test: + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + os: + - windows-latest + - ubuntu-latest + - macos-latest + julia-version: + - "min" + - "lts" + - "1" + - "pre" + - "nightly" + exclude: + - os: windows-latest + julia-version: "min" + - os: macos-latest + julia-version: "min" + include: + - os: ubuntu-latest + julia-prefix: xvfb-run + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: julia-actions/setup-julia@v2 + with: + show-versioninfo: true + version: ${{ matrix.julia-version }} + - uses: julia-actions/julia-buildpkg@v1 + with: + ignore-no-cache: true + localregistry: https://github.com/0h7z/0hjl.git + - uses: julia-actions/julia-runtest@v1 + with: + prefix: ${{ matrix.julia-prefix }} + - uses: heptazhou/julia-codecov@v1 + - uses: codecov/codecov-action@v3.1.5 + with: + file: lcov.info diff --git a/Project.toml b/Project.toml index a489f539..f7d8f351 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Plots" -uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" -author = ["Tom Breloff (@tbreloff)"] +uuid = "90851b43-1319-4523-8ba0-be51ac244080" +authors = ["Heptazhou "] version = "1.40.8" [deps] @@ -12,12 +12,13 @@ FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" JLFzf = "1019f520-868f-41f5-a6de-eb00f4b6a39c" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JSON5 = "275fdaeb-5887-4102-8704-6b08b28f797b" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PlotThemes = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" @@ -66,18 +67,19 @@ Gaston = "1" HDF5 = "0.16 - 0.17" InspectDR = "0.5" JLFzf = "0.1" -JSON = "0.21, 1" +JSON5 = "≥ 0.21.1" LaTeXStrings = "1" Latexify = "0.14 - 0.16" Measures = "0.3" NaNMath = "0.3, 1" +OrderedCollections = "1.5" PGFPlots = "3" PGFPlotsX = "1" PlotThemes = "2, 3" PlotUtils = "1" PlotlyBase = "0.7 - 0.8" PlotlyJS = "0.18" -PlotlyKaleido = "1,2" +PlotlyKaleido = "1, 2" PrecompileTools = "1" PyPlot = "2" PythonPlot = "1" @@ -94,7 +96,7 @@ UnicodeFun = "0.4" UnicodePlots = "3.4" UnitfulLatexify = "1" Unzip = "0.1 - 0.2" -julia = "1.6" +julia = "1.7" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" @@ -131,4 +133,4 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" [targets] -test = ["Aqua", "Colors", "Distributions", "FileIO", "FilePathsBase", "FreeType", "Gaston", "GeometryBasics", "Gtk", "ImageMagick", "Images", "LibGit2", "OffsetArrays", "PGFPlotsX", "PlotlyJS", "PlotlyBase", "PyPlot", "PythonPlot", "PlotlyKaleido", "HDF5", "RDatasets", "SentinelArrays", "StableRNGs", "StaticArrays", "StatsPlots", "Test", "TestImages", "UnicodePlots", "Unitful", "VisualRegressionTests"] +test = ["Aqua", "Colors", "Distributions", "FileIO", "FilePathsBase", "FreeType", "Gaston", "GeometryBasics", "Gtk", "ImageMagick", "Images", "LibGit2", "OffsetArrays", "PGFPlotsX", "PlotlyJS", "PlotlyBase", "PythonPlot", "PlotlyKaleido", "HDF5", "RDatasets", "SentinelArrays", "StableRNGs", "StaticArrays", "Test", "TestImages", "UnicodePlots", "Unitful", "VisualRegressionTests"] diff --git a/src/Plots.jl b/src/Plots.jl index 0bef4a1b..bbbdc836 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -7,9 +7,11 @@ if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@max_m @eval Base.Experimental.@max_methods 1 end +using OrderedCollections: OrderedDict using Dates, Printf, Statistics, Base64, LinearAlgebra, SparseArrays, Random, TOML using PrecompileTools, Reexport, RelocatableFolders using Base.Meta +@reexport using LaTeXStrings @reexport using RecipesBase @reexport using PlotThemes @reexport using PlotUtils @@ -42,7 +44,7 @@ import Downloads import Showoff import Unzip import JLFzf -import JSON +import JSON5: JSON #! format: off export @@ -54,6 +56,7 @@ export wrap, theme, + Plot, plot, plot!, attr!, @@ -82,6 +85,8 @@ export inline, closeall, + GRBackend, + PlotlyBackend, backend, backends, backend_name, diff --git a/src/backends.jl b/src/backends.jl index e88b25ae..442c36ce 100644 --- a/src/backends.jl +++ b/src/backends.jl @@ -574,7 +574,7 @@ function _initialize_backend(pkg::PlotlyBackend) _runtime_init(pkg) catch err if err isa ArgumentError - @warn "Failed to load integration with PlotlyBase & PlotlyKaleido." exception = + @debug "Failed to load integration with PlotlyBase & PlotlyKaleido." exception = (err, catch_backtrace()) else rethrow(err) diff --git a/src/backends/gr.jl b/src/backends/gr.jl index 64b61562..98b6f39b 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -250,7 +250,7 @@ end gr_inqtext(x, y, s) = gr_inqtext(x, y, string(s)) gr_inqtext(x, y, s::AbstractString) = - if (occursin('\\', s) || occursin("10^{", s)) && + if (occursin('\\', s) || occursin(r"(?:2|e|10)\^\{", s)) && match(r".*\$[^\$]+?\$.*", String(s)) === nothing GR.inqtextext(x, y, s) else @@ -259,7 +259,7 @@ gr_inqtext(x, y, s::AbstractString) = gr_text(x, y, s) = gr_text(x, y, string(s)) gr_text(x, y, s::AbstractString) = - if (occursin('\\', s) || occursin("10^{", s)) && + if (occursin('\\', s) || occursin(r"(?:2|e|10)\^\{", s)) && match(r".*\$[^\$]+?\$.*", String(s)) === nothing GR.textext(x, y, s) else @@ -2086,7 +2086,16 @@ for (mime, fmt) in ( gr_display(plt, dpi_factor) end GR.emergencyclosegks() - write(io, read(filepath, String)) + let str = read(filepath, String) + if $fmt == "svg" + rex = r"^\t*\K {2}"m + while contains(str, rex) + str = replace(str, rex => "\t") + end + str = replace(str, r"[^\n]\K()\s*$"s => s"\n\1\n") + end + write(io, str) + end rm(filepath) end end diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index 12f81ad6..778411c1 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -27,7 +27,7 @@ end plotly_font(font::Font, color = font.color) = KW( :family => font.family, - :size => round(Int, 1.4font.pointsize), + :size => round(Int, 1.5font.pointsize), :color => rgba_string(color), ) @@ -191,13 +191,16 @@ function plotly_polaraxis(sp::Subplot, axis::Axis) ax end -function plotly_layout(plt::Plot) +function plotly_layout(plt::Plot; responsive::Bool = true, _...) plotattributes_out = KW() w, h = plt[:size] - plotattributes_out[:width], plotattributes_out[:height] = w, h + if !responsive + plotattributes_out[:width], plotattributes_out[:height] = w, h + end plotattributes_out[:paper_bgcolor] = rgba_string(plt[:background_color_outside]) plotattributes_out[:margin] = KW(:l => 0, :b => 20, :r => 0, :t => 20) + plotattributes_out[:hoverlabel] = KW(:namelength => -1) plotattributes_out[:annotations] = KW[] @@ -1050,79 +1053,7 @@ plotly_series_json(plt::Plot) = JSON.json(plotly_series(plt), 4) # ---------------------------------------------------------------- -html_head(plt::Plot{PlotlyBackend}) = plotly_html_head(plt) -html_body(plt::Plot{PlotlyBackend}) = plotly_html_body(plt) - -plotly_url() = - if _use_local_dependencies[] - _plotly_data_url() - else - "https://cdn.plot.ly/$_plotly_min_js_filename" - end - -function plotly_html_head(plt::Plot) - plotly = plotly_url() - - include_mathjax = get(plt[:extra_plot_kwargs], :include_mathjax, "") - - mathjax_file = if include_mathjax != "cdn" - "file://" * include_mathjax - else - "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML" - end - - mathjax_head = if isempty(include_mathjax) - "" - else - "\n\t\t" - end - - if isijulia() - mathjax_head - else - "$mathjax_head" - end -end - -function plotly_html_body(plt, style = nothing) - if style === nothing - w, h = plt[:size] - style = "width:$(w)px;height:$(h)px;" - end - - requirejs_prefix = requirejs_suffix = "" - if isijulia() - # require.js adds .js automatically - plotly_no_ext = plotly_url() |> splitext |> first - - requirejs_prefix = """ - requirejs.config({ - paths: { - plotly: '$(plotly_no_ext)' - } - }); - require(['plotly'], function (Plotly) { - """ - requirejs_suffix = "});" - end - - uuid = UUIDs.uuid4() - html = """ -
- - """ - html -end - -js_body(plt::Plot, uuid) = - "Plotly.newPlot('$(uuid)', $(plotly_series_json(plt)), $(plotly_layout_json(plt)));" - -plotly_show_js(io::IO, plot::Plot) = - JSON.print(io, Dict(:data => plotly_series(plot), :layout => plotly_layout(plot))) +include("plotly_html.jl") # ---------------------------------------------------------------- @@ -1131,6 +1062,6 @@ Base.showable(::MIME"application/prs.juno.plotpane+html", plt::Plot{PlotlyBacken _show(io::IO, ::MIME"application/vnd.plotly.v1+json", plot::Plot{PlotlyBackend}) = plotly_show_js(io, plot) -_show(io::IO, ::MIME"text/html", plt::Plot{PlotlyBackend}) = write(io, embeddable_html(plt)) +_show(io::IO, ::MIME"text/html", plt::Plot{PlotlyBackend}) = write(io, standalone_html(plt)) _display(plt::Plot{PlotlyBackend}) = standalone_html_window(plt) diff --git a/src/backends/plotly_html.jl b/src/backends/plotly_html.jl new file mode 100644 index 00000000..ee10930b --- /dev/null +++ b/src/backends/plotly_html.jl @@ -0,0 +1,70 @@ +# Copyright (C) 2024 Heptazhou +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +html_head(plt::Plot{PlotlyBackend}; kw...) = plotly_html_head(plt; kw...) +html_body(plt::Plot{PlotlyBackend}; kw...) = plotly_html_body(plt; kw...) + +# https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_config.js +function plotly_config(plt::Plot; responsive::Bool = true, _...) + KW([:responsive => responsive, :showTips => false]) +end + +function plotly_html_head(plt::Plot; _...) + # https://cdnjs.com/libraries/mathjax + # https://cdnjs.com/libraries/plotly.js + mathjax = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-svg-full.min.js" + plotly_js = "https://cdnjs.cloudflare.com/ajax/libs/plotly.js/2.35.0/plotly.min.js" + + script(src::String) = """\t\n""" + script(mathjax) * script(plotly_js) +end + +function plotly_html_body(plt::Plot; kw...) + id = uuid4() + """ +
+
+
+ + + + """ +end + +function js_body(plt::Plot, id::Base.UUID; kw...) + f = sort! ∘ OrderedDict + s = JSON.json(["$id", + plotly_series(plt) .|> f, + plotly_layout(plt; kw...) |> f, + plotly_config(plt; kw...) |> f, + ], 4) + """ + Plotly.newPlot( + $(strip(∈("[\t\n]"), s)) + ) + """ +end + +plotly_show_js(io::IO, plot::Plot) = + JSON.print(io, OrderedDict( + :data => plotly_series(plot), + :layout => plotly_layout(plot), + :config => plotly_config(plot), + )) + diff --git a/src/backends/web.jl b/src/backends/web.jl index cfa7e454..753b350f 100644 --- a/src/backends/web.jl +++ b/src/backends/web.jl @@ -3,24 +3,7 @@ # CREDIT: parts of this implementation were inspired by @joshday's PlotlyLocal.jl -standalone_html( - plt::AbstractPlot; - title::AbstractString = get(plt.attr, :window_title, "Plots.jl"), -) = """ - - - - $title - - $(html_head(plt)) - - - $(html_body(plt)) - - - """ - -embeddable_html(plt::AbstractPlot) = html_head(plt) * html_body(plt) +include("web_html.jl") function open_browser_window(filename::AbstractString) @static if Sys.isapple() diff --git a/src/backends/web_html.jl b/src/backends/web_html.jl new file mode 100644 index 00000000..3e109889 --- /dev/null +++ b/src/backends/web_html.jl @@ -0,0 +1,41 @@ +# Copyright (C) 2024 Heptazhou +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +standalone_html(plt::AbstractPlot; + title::AbstractString = get(plt.attr, :window_title, "Plots.jl"), kw...) = + """ + + + + + + + + + $title + + $(html_head(plt; kw...) |> strip) + + + $(html_body(plt; kw...) |> strip) + + + """ + +embeddable_html(plt::AbstractPlot; kw...) = html_head(plt; kw...) * html_body(plt; kw...) + diff --git a/src/examples.jl b/src/examples.jl index 2fe65b1d..d4192e6e 100644 --- a/src/examples.jl +++ b/src/examples.jl @@ -1359,7 +1359,7 @@ function test_examples( ) @info "Testing plot: $pkgname:$i:$(_examples[i].header)" - m = Module(Symbol(:PlotsExamples, pkgname)) + m = Module(Symbol(:PlotsExamples_, pkgname)) # prevent leaking variables (esp. functions) directly into Plots namespace Base.eval(m, quote diff --git a/src/init.jl b/src/init.jl index a3f955be..6b91db3c 100644 --- a/src/init.jl +++ b/src/init.jl @@ -22,7 +22,7 @@ _plotly_data_url() = """ use fixed version of Plotly instead of the latest one for stable dependency """ -const _plotly_min_js_filename = "plotly-2.6.3.min.js" +const _plotly_min_js_filename = "plotly-2.35.0.min.js" """ Whether to use local embedded or local dependencies instead of CDN. diff --git a/src/output.jl b/src/output.jl index ce7a3f24..359d89ee 100644 --- a/src/output.jl +++ b/src/output.jl @@ -153,6 +153,10 @@ function savefig(plt::Plot, fn) # fn might be an `AbstractString` or an `Abstrac end savefig(fn) = savefig(current(), fn) +function Base.write(filename::AbstractString, p::AbstractPlot) + filesize(savefig(p, filename)) +end + # --------------------------------------------------------- """ diff --git a/test/runtests.jl b/test/runtests.jl index 72b7b4e7..2310ae86 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,7 +7,6 @@ import ImageMagick import FreeType # for `unicodeplots` import LibGit2 import Aqua -import JSON using VisualRegressionTests using RecipesPipeline @@ -32,6 +31,9 @@ plotlyjs() hdf5() gr() +# https://github.com/JuliaPlots/PlotReferenceImages.jl +# ENV["VISUAL_REGRESSION_TESTS_AUTO"] = true + is_auto() = Plots.bool_env("VISUAL_REGRESSION_TESTS_AUTO", "false") is_pkgeval() = Plots.bool_env("JULIA_PKGEVAL", "false") is_ci() = Plots.bool_env("CI", "false") diff --git a/test/test_backends.jl b/test/test_backends.jl index e335cd6c..dc1426a5 100644 --- a/test/test_backends.jl +++ b/test/test_backends.jl @@ -8,7 +8,7 @@ ci_tol() = end const TESTS_MODULE = Module(:PlotsTestsModule) -const PLOTS_IMG_TOL = parse(Float64, get(ENV, "PLOTS_IMG_TOL", is_ci() ? ci_tol() : "1e-5")) +const PLOTS_IMG_TOL = parse(Float64, get(ENV, "PLOTS_IMG_TOL", Sys.isapple() ? "1e-2" : "1e-4")) Base.eval(TESTS_MODULE, :(using Random, StableRNGs, Plots)) @@ -59,6 +59,7 @@ function reference_file(backend, version, i) refdir = reference_dir("Plots", string(backend)) fn = ref_name(i) * ".png" reffn = joinpath(refdir, string(version), fn) + i in [42, 50] && (version = v"2") for ver in sort(VersionNumber.(readdir(refdir)), rev = true) ver > version && continue if (tmpfn = joinpath(refdir, string(ver), fn)) |> isfile @@ -73,7 +74,7 @@ function image_comparison_tests( pkg::Symbol, idx::Int; debug = false, - popup = !is_ci(), + popup = false, sigma = [1, 1], tol = 1e-2, ) @@ -184,11 +185,11 @@ end end const blacklist = if VERSION.major == 1 && VERSION.minor ∈ (9, 10) - [41] # FIXME: github.com/JuliaLang/julia/issues/47261 + [] else [] end -push!(blacklist, 50) # NOTE: remove when github.com/jheinen/GR.jl/issues/507 is resolved +push!(blacklist, 25, 30) # StatsPlots depends on the wrong Plots @testset "GR - reference images" begin Plots.with(:gr) do @@ -223,7 +224,7 @@ is_pkgeval() || @testset "Examples" begin ) @test filesize(fn) > 1_000 end - Sys.islinux() && for be in TEST_BACKENDS + Sys.islinux() && for be in setdiff(TEST_BACKENDS, [:pgfplotsx]) skip = vcat(Plots._backend_skips[be], blacklist) Plots.test_examples(be; skip, callback, disp = is_ci(), strict = true) # `ci` display for coverage closeall() diff --git a/test/test_misc.jl b/test/test_misc.jl index 172aee38..b8fa7668 100644 --- a/test/test_misc.jl +++ b/test/test_misc.jl @@ -1,11 +1,5 @@ # miscellaneous tests (not fitting into other test files) -@testset "Infrastructure" begin - @test_nowarn JSON.Parser.parse( - String(read(joinpath(dirname(pathof(Plots)), "..", ".zenodo.json"))), - ) -end - @testset "Plotly standalone" begin @test Plots._plotly_local_file_path[] ≡ nothing temp = Plots._use_local_dependencies[] diff --git a/test/test_quality.jl b/test/test_quality.jl index d38c2b33..8a1e16ef 100644 --- a/test/test_quality.jl +++ b/test/test_quality.jl @@ -11,6 +11,7 @@ :Contour, :Latexify, :LaTeXStrings, + :Pkg, :Requires, :UnitfulLatexify, ]