Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to plot a partial annulus? #163

Closed
diegozea opened this issue Mar 14, 2016 · 42 comments
Closed

How to plot a partial annulus? #163

diegozea opened this issue Mar 14, 2016 · 42 comments

Comments

@diegozea
Copy link
Contributor

Hi!
I can plot the lower and upper partial circles, but I can't fill it.
A single wide line looks ugly, doesn't it?

image

I want to do a chord diagram/circos plot.

Best.

@pkofod
Copy link
Contributor

pkofod commented Mar 14, 2016

Maybe related to #143 ?

@tbreloff
Copy link
Member

I think creating custom shapes might be better (for the backends that support it anyways). Here's something to get you started:

using Plots; gadfly(size=(300,300),leg=false)

arcshape(θ1, θ2) = Shape(vcat(Plots.partialcircle(θ1, θ2, 10, 1),
                              reverse(Plots.partialcircle(θ1, θ2, 10, 0.8))))

plot([0],[0],marker = (100, arcshape(0,0.25π))) 

tmp

@diegozea
Copy link
Contributor Author

Will Plotly support custom markets in the future?

@tbreloff
Copy link
Member

I don't know... I certainly want that to happen.

@sglyon
Copy link
Member

sglyon commented Mar 14, 2016

FWIW I recently added support for Shapes in PlotlyJS. See here for more info.

The code there lets you construct lines, rectangles, or circles (really more like ellipse), and arbitrary svg paths.

This could definitely be a building plot for custom markers

EDIT: examples here

@tbreloff
Copy link
Member

This is cool @spencerlyon2! However I don't think these can be used as "markers" within Plotly, correct? They are just for drawing on the figure?

@sglyon
Copy link
Member

sglyon commented Mar 14, 2016

Ahh that is true. You could kinda get a working version by constructing many of these shapes, but it wouldn't come with all the same niceties that actual trace markers have

@tbreloff
Copy link
Member

See: plotly/plotly.js#330

@diegozea
Copy link
Contributor Author

@tbreloff Do you know why are these markers unaligned?

image

@diegozea
Copy link
Contributor Author

What is the relation between the marker size (100 in the above example) and the radius?

@tbreloff
Copy link
Member

That's weird. Does it do that for gadfly?

On Mar 15, 2016, at 4:59 PM, Diego Javier Zea notifications@github.com wrote:

@tbreloff Do you know why are these markers unaligned?


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@diegozea
Copy link
Contributor Author

Yes, It works fine with Gadfly

image

The code is:

using Plots; gadfly()

arcshape(θ1, θ2) = Shape(vcat(Plots.partialcircle(θ1, θ2, 10, 1),
                              reverse(Plots.partialcircle(θ1, θ2, 10, 0.8))))

plt = plot(xlim=(-1,1), ylim=(-1,1))

A  = 1.5π # Filled space
B  = 0.5π # White space

N = 10 # Number of nodes

Δα = A / N
Δβ = B / N

δ = Δα  + Δβ

n = 1

α = 0.0

while n <= N
    plot!(plt, x=[0], y=[0], linetype=:scatter, marker=(100, arcshape(α, α + Δα)), legend=false)
    α += δ
    n += 1
end

plt

@diegozea
Copy link
Contributor Author

How do I determine the radius using the first element of the marker tuple?

@tbreloff
Copy link
Member

Ok my guess is that PyPlot is normalizing the shape coordinates based on some calculation of approximate area. When all the shapes are the same you won't notice, but in this case it takes precise coordinates and perturbs them. I'll try to dig into the PyPlot docs to see if there's an argument to turn that behavior off.

On Mar 15, 2016, at 8:26 PM, Diego Javier Zea notifications@github.com wrote:

How do I determine the radius using the first element of the marker tuple?


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@tbreloff
Copy link
Member

The order of that marker tuple doesn't matter, but Plots "figures out" that you intend it to refer to the marker size. This happens during argument preprocessing. If you look at the implementations in MLPlots.jl, you'll see an alternative approach to defining Plots recipes, which can use the preprocessed args.

On Mar 15, 2016, at 8:26 PM, Diego Javier Zea notifications@github.com wrote:

How do I determine the radius using the first element of the marker tuple?


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@tbreloff
Copy link
Member

From the docs:

verts a list of (x, y) pairs used for Path vertices. The center of the marker is located at (0,0) and the size is normalized.

On Mar 15, 2016, at 8:26 PM, Diego Javier Zea notifications@github.com wrote:

How do I determine the radius using the first element of the marker tuple?


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@tbreloff
Copy link
Member

For reference, probably related: matplotlib/matplotlib#1980

On Mar 15, 2016, at 8:26 PM, Diego Javier Zea notifications@github.com wrote:

How do I determine the radius using the first element of the marker tuple?


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@diegozea
Copy link
Contributor Author

I couldn't make it... :/ The markers don't respect the ratio between edges...

image

The following is my code:

arcshape(θ1, θ2) = Shape(vcat(Plots.partialcircle(θ1, θ2, 10, 1.1),
                            reverse(Plots.partialcircle(θ1, θ2, 10, 0.9))))

function chorddiagram(source, destiny, weight, grad=ColorGradient(:bluesreds))

    if length(source) == length(destiny) == length(weight)

        vertices = unique(vcat(source, destiny))

        wmin, wmax = extrema(weight)

        plt = plot(xlim=(-2,2), ylim=(-2,2), legend=false)

        A  = 1.5π # Filled space
        B  = 0.5π # White space

        N = length(vertices) # Number of nodes

        Δα = A / N
        Δβ = B / N

        δ = Δα  + Δβ

        n = 1

        α = 0.0

        for i in 1:length(source)
            curve = BezierCurve(P2[ (cos((source[i ]-1)*δ - 0.5Δα), sin((source[i ]-1)*δ - 0.5Δα)), (0,0), 
                                    (cos((destiny[i]-1)*δ - 0.5Δα), sin((destiny[i]-1)*δ - 0.5Δα)) ])
            plot!(plt, curve_points(curve), line = (Plots.curvecolor(weight[i], wmin, wmax, grad), 0.5, 2))
        end

        while n <= N
            plot!(plt, x=[ 0 ], y=[ 0 ], linetype=:scatter, color=:lightblue, marker=(55, arcshape(α, α + Δα)), legend=false)
            α += δ
            n += 1
        end

        return plt

    else
        throw(ArgumentError("source, destiny and weight should have the same length"))
    end
end

@diegozea
Copy link
Contributor Author

The next is using marker size 100 (instead of 55) and equal ratio between axis (size=(400,400)):

image

@tbreloff
Copy link
Member

Yes you figured it out... You need an aspect ratio with equal sides.

I would also add the arguments: (grid=false, xticks=nothing, yticks=nothing, xlim=(-1.2,1.2), ylim=(-1.2,1.2)) to the first plot command

On Mar 16, 2016, at 7:32 AM, Diego Javier Zea notifications@github.com wrote:

The next is using marker size 100 (instead of 55) and equal ratio between axis:


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@diegozea
Copy link
Contributor Author

Outside the chord diagram:

  • Should does plot to have an argument to force equal ratio of the axis?
  • Should do custom markers to respect the axes aspect ratio?

@diegozea
Copy link
Contributor Author

@tbreloff I change the code, because I would like to use zcolor and group with the nodes. I'm having two problems:

  1. Some Bézier are plotted over the markers.
  2. I don't know how to past kargs arguments to the plot functions... ;kargs... doesn't work.

Code:

using Plots; gadfly()

arcshape(θ1, θ2) = Shape(vcat(Plots.partialcircle(θ1, θ2, 15, 1.1),
                            reverse(Plots.partialcircle(θ1, θ2, 15, 0.9))))

function chorddiagram(source, destiny, weight; grad=ColorGradient(:bluesreds), size=(400,400))

    # Needs an aspect ratio with equal sides.
    ms = minimum(size) 
    size_plot = (ms, ms)

    # Empirical marker size
    size_marker = ms < 150 ? ms/4 : 0.375ms 

    if length(source) == length(destiny) == length(weight)

        plt = plot(xlim=(-2,2), ylim=(-2,2), legend=false, grid=false, 
        xticks=nothing, yticks=nothing, xlim=(-1.2,1.2), ylim=(-1.2,1.2), size=size_plot)

        nodemin, nodemax = extrema(vcat(source, destiny))

        weightmin, weightmax = extrema(weight)       

        A  = 1.5π # Filled space
        B  = 0.5π # White space (empirical)

        Δα = A / nodemax
        Δβ = B / nodemax

        δ = Δα  + Δβ

        for i in 1:length(source)
            curve = BezierCurve(P2[ (cos((source[i ]-1)*δ - 0.5Δα), sin((source[i ]-1)*δ - 0.5Δα)), (0,0), 
                                    (cos((destiny[i]-1)*δ - 0.5Δα), sin((destiny[i]-1)*δ - 0.5Δα)) ])
            plot!(curve_points(curve), line = (Plots.curvecolor(weight[i], weightmin, weightmax, grad), 0.5, 2))
        end

        plot!(x=zeros(Int, nodemax), y=zeros(Int, nodemax), linetype=:scatter,
        marker = (size_marker, [arcshape(n*δ, n*δ + Δα) for n in 0:(nodemax-1)]))

        return plt

    else
        throw(ArgumentError("source, destiny and weight should have the same length"))
    end
end

image

@tbreloff
Copy link
Member

Some Bézier are plotted over the markers.

I suspect this is an issue with Gadfly... I've noticed that it doesn't always respect z-order of the layers.

I don't know how to past kargs arguments to the plot functions... ;kargs... doesn't work.

One method you could consider... collect the keywords in a dictionary, remove the ones specific to chorddiagram, then splat the dictionary into the plot commands. This is trickier if you want different args for each command. You'll have to explain exactly what you want to do (and there might be a better way altogether)

Should do custom markers to respect the axes aspect ratio?

The better solution is to allow plotting Shape directly using "plot coordinates". This will have varying levels of support, but it should be possible, at a minimum, with Gadfly and Plotly. This will be nicer, in many cases, than trying to squeeze arbitrary drawing into the "scatter" series type. I'll look into this option (which would change your code a little)

@tbreloff
Copy link
Member

I just pushed up some changes to the dev branch, adding a new linetype :shape with aliases shapes, poly, polygon. This puts a polygon into the plot using "plot coordinates", so you don't have to worry about marker sizes or anything like that. If you pass a Shape (or vector/matrix of Shape) to the plot function, it'll extract the coordinates and set the linetype automatically, so no need for you to do it.

In your current method, you can make the following change to use the new functionality (only implemented in Gadfly currently!)

#         plot!(x=zeros(Int, nodemax), y=zeros(Int, nodemax), linetype=:scatter,
#               marker = (size_marker, [arcshape(n*δ, n*δ + Δα) for n in 0:(nodemax-1)]))
        plot!([arcshape(n*δ, n*δ + Δα) for n in 0:(nodemax-1)]')

Your graph should work now, even with an uneven aspect ratio.

@tbreloff
Copy link
Member

One extra note... I have shapes still being treated partially like markers, so you can do something like:

tmp

@diegozea
Copy link
Contributor Author

Thanks! 👍

Do you know what the following error means?

LoadError: MethodError: `convertToAnyVector` has no method matching
convertToAnyVector(::Array{Plots.Shape,2}, ::Dict{Any,Any})

I'm doing:

function chorddiagram(source, destiny, weight; kargs...)

    args=Dict(kargs)

    grad=pop!(args, :grad, ColorGradient(:bluesreds))
    size=pop!(args, :size, (400,400))

    ...

    plot!([arcshape(n*δ, n*δ + Δα) for n in 0:(nodemax-1)]', args...)

@diegozea
Copy link
Contributor Author

I want to be able to color nodes/markers/shapes using a vector of values (zcolor) or a vector of clusters (groups).

@tbreloff
Copy link
Member

Are you sure you checked out latest Plots dev branch and restarted julia?

On Mar 16, 2016, at 1:34 PM, Diego Javier Zea notifications@github.com wrote:

Thanks!
Do you know what the following error means?

LoadError: MethodError: convertToAnyVector has no method matching
convertToAnyVector(::Array{Plots.Shape,2}, ::Dict{Any,Any})
I'm doing:

function chorddiagram(source, destiny, weight; kargs...)

args=Dict(kargs)

grad=pop!(args, :grad, ColorGradient(:bluesreds))
size=pop!(args, :size, (400,400))

plot!([arcshape(n_δ, n_δ + Δα) for n in 0:(nodemax-1)]', args...)
....


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@diegozea
Copy link
Contributor Author

Yes, I did it again, but I'm still getting that error using the Dict args... in that way.

@diegozea
Copy link
Contributor Author

image

@tbreloff
Copy link
Member

Oh I'm sorry. You just need a semicolon before splatting the args... Otherwise they won't be treated as keyword args.

On Mar 16, 2016, at 1:34 PM, Diego Javier Zea notifications@github.com wrote:

Thanks!
Do you know what the following error means?

LoadError: MethodError: convertToAnyVector has no method matching
convertToAnyVector(::Array{Plots.Shape,2}, ::Dict{Any,Any})
I'm doing:

function chorddiagram(source, destiny, weight; kargs...)

args=Dict(kargs)

grad=pop!(args, :grad, ColorGradient(:bluesreds))
size=pop!(args, :size, (400,400))

plot!([arcshape(n_δ, n_δ + Δα) for n in 0:(nodemax-1)]', args...)
....


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@diegozea
Copy link
Contributor Author

Great, the semicolon solve my mistake ;) But using zcolor or group generates strange shapes...

image

@tbreloff
Copy link
Member

Ok... I'll need to review what happens with zcolor/group. For now you could always do that yourself and plot each shape individually with its own color. You can use getColorZ(grad, val)

On Mar 16, 2016, at 2:01 PM, Diego Javier Zea notifications@github.com wrote:

Great, the semicolon solve my mistake ;) But using zcolor or group generates strange shapes...


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@tbreloff
Copy link
Member

I don't think you should pass zcolor into the plot function... I think you should handle it something like:

colorlist(grad, ::Void) = :lightblue
function colorlist(grad, z)
    zmin, zmax = extrema(z)
    RGBA{Float64}[getColorZ(grad, (zi-zmin)/(zmax-zmin)) for zi in z]'
end


function chorddiagram(source, destiny, weight; kw...)
    ...

        c = colorlist(grad, pop!(args, :zcolor, nothing))
        @show c
        plot!([arcshape(n*δ, n*δ + Δα) for n in 0:(nodemax-1)]', mc=c)
    ...
end

For the group arg... I'm a little confused what you expect the plot to look like, which makes it hard to suggest something. Currently, you're plotting N different series. The group mechanic is used to break up a single series into several. So it doesn't make sense to use group in this case.

@diegozea
Copy link
Contributor Author

I want group to generate a discrete color palette, and assign a color and each node depending on its group. Maybe the nodes should be a single serie (It will be more natural).

Hard-coded desired output for group:
image

@tbreloff
Copy link
Member

I think ideally I want to support this, but I'll have to change how I handle shapes. Lets put it on the todo list.

@diegozea
Copy link
Contributor Author

@tbreloff How can I select colors (like getColorZ) but from a discrete palette? I'm trying to implement group similar to zcolor.

@diegozea
Copy link
Contributor Author

I solved indexing _allColors ;)

@tbreloff
Copy link
Member

Can't you just pass in the colors directly? Do you want the colors to be automatically generated in a specific way?

On Mar 16, 2016, at 8:33 PM, Diego Javier Zea notifications@github.com wrote:

@tbreloff How can I select colors (like getColorZ) but from a discrete palette? I'm trying to implement group similar to zcolor.


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@diegozea
Copy link
Contributor Author

I'm using

c = group === nothing ? colorlist(grad, zcolor) : [ Plots._allColors[66 - i%64] for i in group ]'
plot!([arcshape(n*δ, n*δ + Δα) for n in 0:(nodemax-1)]'; mc=c, args...)

to pass colors, is that fine?

@tbreloff
Copy link
Member

Well that depends... Do you just want the colors to be "sufficiently different"? If so then I think you should plot one series per group and don't set the color.

On Mar 16, 2016, at 8:48 PM, Diego Javier Zea notifications@github.com wrote:

I'm using

c = group === nothing ? colorlist(grad, zcolor) : [ Plots._allColors[66 - i%64] for i in group ]'
plot!([arcshape(n_δ, n_δ + Δα) for n in 0:(nodemax-1)]'; mc=c, args...)
to pass colors, is that fine?


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@tbreloff
Copy link
Member

Ok I took the liberty to make a few changes to your method which are more in line with what I'm thinking... it's still not as clean as I'd like, but I suppose it could be cleaned up later.

using Plots; gadfly()

arcshape(θ1, θ2) = Shape(vcat(Plots.partialcircle(θ1, θ2, 15, 1.1),
                            reverse(Plots.partialcircle(θ1, θ2, 15, 0.9))))
colorlist(grad, ::Void) = :lightblue
function colorlist(grad, z)
    zmin, zmax = extrema(z)
    RGBA{Float64}[getColorZ(grad, (zi-zmin)/(zmax-zmin)) for zi in z]'
end

function chorddiagram(source, destiny, weight; kw...)
    if !(length(source) == length(destiny) == length(weight))
        throw(ArgumentError("source, destiny and weight should have the same length"))
    end

    args=Dict(kw)
    grad = pop!(args, :grad, ColorGradient(:bluesreds))
    size = pop!(args, :size, (400,400))
    group = pop!(args, :group, nothing)

    # setup plot
    plt = plot(grid=false, xticks=nothing, yticks=nothing, xlim=(-1.2,1.2), ylim=(-1.2,1.2))

    # ?? do we really want to have `nodemax` shapes??  what if the user enters something really strange?
    nodemin, nodemax = extrema(vcat(source, destiny))

    weightmin, weightmax = extrema(weight)       
    A  = 1.5π # Filled space
    B  = 0.5π # White space (empirical)
    Δα = A / nodemax
    Δβ = B / nodemax
    δ = Δα  + Δβ

    # plot the arcs
    for i in 1:length(source)
        curve = BezierCurve(P2[ (cos((source[i ]-1)*δ - 0.5Δα), sin((source[i ]-1)*δ - 0.5Δα)), (0,0), 
                                (cos((destiny[i]-1)*δ - 0.5Δα), sin((destiny[i]-1)*δ - 0.5Δα)) ])
        c = Plots.curvecolor(weight[i], weightmin, weightmax, grad)
        plot!(curve_points(curve), line = (c, 0.5, 2), lab = "")
    end

    # plot the shapes
    shapes = [arcshape(n*δ, n*δ + Δα) for n in 0:(nodemax-1)]
    if group != nothing
        for g in sort(unique(group))
            # plot only those shapes in group g
            plot!(shapes[group .== g], lab = "Group $g")
        end
    else
        # plot the shapes the way we were originally... each as its own series,
        # with colors optionally selected using a gradient
        plot!(shapes', mc = colorlist(grad, pop!(args, :zcolor, nothing)), lab = "")
    end

    plt
end

n = 5
chorddiagram(1:n, (1:n)+1, 1:n, group = [1,1,2,2,2,3])

screen shot 2016-03-16 at 10 01 07 pm

Note to self: Compose doesn't handle NaNs for polygons when drawing PNGs... need to fix and submit a PR similar to GiovineItalia/Compose.jl#189

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants