diff --git a/src/backends.jl b/src/backends.jl index 7b1bc646..bdfdea3d 100644 --- a/src/backends.jl +++ b/src/backends.jl @@ -402,6 +402,7 @@ const _gr_attr = merge_with_base_supported([ :tick_direction, :camera, :contour_labels, + :connections, ]) const _gr_seriestype = [ :path, @@ -521,6 +522,7 @@ const _plotly_attr = merge_with_base_supported([ :tick_direction, :camera, :contour_labels, + :connections, ]) const _plotly_seriestype = [ @@ -777,6 +779,7 @@ const _pyplot_attr = merge_with_base_supported([ :tick_direction, :camera, :contour_labels, + :connections, ]) const _pyplot_seriestype = [ :path, @@ -793,6 +796,7 @@ const _pyplot_seriestype = [ :contour3d, :path3d, :scatter3d, + :mesh3d, :surface, :wireframe, ] @@ -860,6 +864,7 @@ const _gaston_attr = merge_with_base_supported([ # :framestyle, # :camera, # :contour_labels, + :connections, ]) const _gaston_seriestype = [ @@ -1240,6 +1245,7 @@ const _pgfplotsx_attr = merge_with_base_supported([ :tick_direction, :camera, :contour_labels, + :connections, ]) const _pgfplotsx_seriestype = [ :path, diff --git a/src/backends/pgfplotsx.jl b/src/backends/pgfplotsx.jl index dd9dd8d9..32a75c6d 100644 --- a/src/backends/pgfplotsx.jl +++ b/src/backends/pgfplotsx.jl @@ -512,10 +512,26 @@ function pgfx_add_series!(::Val{:heatmap}, axis, series_opt, series, series_func end function pgfx_add_series!(::Val{:mesh3d}, axis, series_opt, series, series_func, opt) - ptable = join( - [string(i, " ", j, " ", k, "\\\\") for (i, j, k) in zip(opt[:connections]...)], - "\n ", - ) + if opt[:connections] isa Tuple{Array,Array,Array} + # 0-based indexing + ptable = join( + [string(i, " ", j, " ", k, "\\\\") for (i, j, k) in zip(opt[:connections]...)], + "\n ", + ) + elseif typeof(opt[:connections]) <: AbstractVector{NTuple{3, Int}} + # 1-based indexing + ptable = join( + [string(i-1, " ", j-1, " ", k-1, "\\\\") for (i, j, k) in opt[:connections]], + "\n ", + ) + else + throw( + ArgumentError( + "Argument connections has to be either a tuple of three arrays (0-based indexing) + or an AbstractVector{NTuple{3,Int}} (1-based indexing).", + ), + ) + end push!( series_opt, "patch" => nothing, diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index ab09ee80..d88c16e7 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -678,6 +678,7 @@ function plotly_series(plt::Plot, series::Series) if series[:connections] !== nothing if typeof(series[:connections]) <: Tuple{Array,Array,Array} + # 0-based indexing i, j, k = series[:connections] if !(length(i) == length(j) == length(k)) throw( @@ -689,10 +690,17 @@ function plotly_series(plt::Plot, series::Series) plotattributes_out[:i] = i plotattributes_out[:j] = j plotattributes_out[:k] = k + elseif typeof(series[:connections]) <: AbstractVector{NTuple{3, Int}} + # 1-based indexing + i, j, k = broadcast(i -> [ inds[i]-1 for inds in series[:connections]], (1, 2, 3)) + plotattributes_out[:i] = i + plotattributes_out[:j] = j + plotattributes_out[:k] = k else throw( ArgumentError( - "Argument connections has to be a tuple of three arrays.", + "Argument connections has to be either a tuple of three arrays (0-based indexing) + or an AbstractVector{NTuple{3,Int}} (1-based indexing).", ), ) end diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index 79294790..5f484d41 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -698,6 +698,43 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) end end + if st == :mesh3d + polygons = if series[:connections] isa AbstractVector{<:AbstractVector{Int}} + # Combination of any polygon types + broadcast(inds -> broadcast(i -> [x[i], y[i], z[i]], inds), series[:connections]) + elseif series[:connections] isa AbstractVector{NTuple{N, Int}} where N + # Only N-gons - connections have to be 1-based (indexing) + broadcast(inds -> broadcast(i -> [x[i], y[i], z[i]], inds), series[:connections]) + elseif series[:connections] isa NTuple{3,<:AbstractVector{Int}} + # Only triangles - connections have to be 0-based (indexing) + ci, cj, ck = series[:connections] + if !(length(ci) == length(cj) == length(ck)) + throw( + ArgumentError("Argument connections must consist of equally sized arrays."), + ) + end + broadcast(j -> broadcast(i -> [x[i], y[i], z[i]], [ci[j]+1, cj[j]+1, ck[j]+1]), eachindex(ci)) + else + throw( + ArgumentError("Unsupported `:connections` type $(typeof(series[:connections])) for seriestype=$st"), + ) + end + col = mplot3d.art3d.Poly3DCollection(polygons, + linewidths = py_thickness_scale(plt, series[:linewidth]), + edgecolor = py_color(get_linecolor(series)), + facecolor = py_color(series[:fillcolor]), + alpha = get_fillalpha(series), + zorder = series[:series_plotindex] + ) + handle = ax."add_collection3d"(col) + # Fix for handle: https://stackoverflow.com/questions/54994600/pyplot-legend-poly3dcollection-object-has-no-attribute-edgecolors2d + # It seems there aren't two different alpha values for edge and face + handle._facecolors2d = py_color(series[:fillcolor]) + handle._edgecolors2d = py_color(get_linecolor(series)) + push!(handles, handle) + end + + if st == :image xmin, xmax = ignorenan_extrema(series[:x]) ymin, ymax = ignorenan_extrema(series[:y]) diff --git a/src/examples.jl b/src/examples.jl index d1e1a474..62d1cd02 100644 --- a/src/examples.jl +++ b/src/examples.jl @@ -962,8 +962,10 @@ const _examples = PlotExample[ """ Allows to plot arbitrary 3d meshes. If only x,y,z are given the mesh is generated automatically. You can also specify the connections using the connections keyword. - The connections are specified using a tuple of vectors. Each vector contains the 0-based indices of one point of a triangle, - such that elements at the same position of these vectors form a triangle. + The connections can be specified in two ways: Either as a tuple of vectors where each vector + contains the 0-based indices of one point of a triangle, such that elements at the same + position of these vectors form a triangle. Or as a vector of NTuple{3,Ints} where each element + contains the 1-based indices of the three points of a triangle. """, [ :( @@ -979,13 +981,14 @@ const _examples = PlotExample[ i = [0, 0, 0, 1] j = [1, 2, 3, 2] k = [2, 3, 1, 3] + # Or: cns = [(1, 2, 3), (1, 3, 4), (1, 4, 2), (2, 3, 4)] (1-based indexing) # the four triangles gives above give a tetrahedron mesh3d( x, y, z; - connections = (i, j, k), + connections = (i, j, k), # connections = cns title = "triangles", xlabel = "x", ylabel = "y", @@ -1235,7 +1238,7 @@ const _examples = PlotExample[ _animation_examples = [2, 31] _backend_skips = Dict( :gr => [25, 30], - :pyplot => [2, 25, 30, 31, 47, 49, 55], + :pyplot => [2, 25, 30, 31, 49, 55], :plotlyjs => [2, 21, 24, 25, 30, 31, 49, 51, 55], :plotly => [2, 21, 24, 25, 30, 31, 49, 50, 51, 55], :pgfplotsx => [ diff --git a/src/utils.jl b/src/utils.jl index ffafde64..83aa022a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1181,16 +1181,12 @@ end _document_argument(S::AbstractString) = _fmt_paragraph("`$S`: " * _arg_desc[Symbol(S)], leadingspaces = 6 + length(S)) -function mesh3d_triangles(x, y, z, cns) - if typeof(cns) <: Tuple{Array,Array,Array} - ci, cj, ck = cns - if !(length(ci) == length(cj) == length(ck)) - throw( - ArgumentError("Argument connections must consist of equally sized arrays."), - ) - end - else - throw(ArgumentError("Argument connections has to be a tuple of three arrays.")) +function mesh3d_triangles(x, y, z, cns::Tuple{Array,Array,Array}) + ci, cj, ck = cns + if !(length(ci) == length(cj) == length(ck)) + throw( + ArgumentError("Argument connections must consist of equally sized arrays."), + ) end X = zeros(eltype(x), 4length(ci)) Y = zeros(eltype(y), 4length(cj)) @@ -1215,6 +1211,30 @@ function mesh3d_triangles(x, y, z, cns) end return X, Y, Z end +function mesh3d_triangles(x, y, z, cns::AbstractVector{NTuple{3, Int}}) + X = zeros(eltype(x), 4length(cns)) + Y = zeros(eltype(y), 4length(cns)) + Z = zeros(eltype(z), 4length(cns)) + @inbounds for I in 1:length(cns) + i = cns[I][1] # connections are 1-based + j = cns[I][2] + k = cns[I][3] + m = 4(I - 1) + 1 + n = m + 1 + o = m + 2 + p = m + 3 + X[m] = X[p] = x[i] + Y[m] = Y[p] = y[i] + Z[m] = Z[p] = z[i] + X[n] = x[j] + Y[n] = y[j] + Z[n] = z[j] + X[o] = x[k] + Y[o] = y[k] + Z[o] = z[k] + end + return X, Y, Z +end # cache joined symbols so they can be looked up instead of constructed each time const _attrsymbolcache = Dict{Symbol,Dict{Symbol,Symbol}}()