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

Bubble Plot #984

Merged
merged 7 commits into from
May 2, 2021
Merged

Bubble Plot #984

merged 7 commits into from
May 2, 2021

Conversation

PeterDavidson
Copy link
Contributor

Purpose:
#960 talks about a new plot type, BubblePlot, and a simpler IPlottable Circle to just draw a single circle with the radius expressed in chart units. #973 also asks for the ability to produce a single circle. This PR implement the single circle plottable. I have named it Circle (rather than CirclePlot, which was suggested in #973) since that seems to better match the naming convention used for Polygon.

If you are happy with the overall approach then I can add a new AddCircle function to Plot and update the documentation/examples.

New Functionality:
A new plottable which allows a custom circle/ellipse to be drawn, with an optional label drawn at an offset from the center as specified in the Font.Alignment. The circle has the following data

  • X,Y : the coordinate of the center
  • Radius: the radius of the circle (specified in axis units).
  • Label: the label to be shown in the circle
  • AllowEllipse: if true then allow an ellipse. Since the scaling of the X and Y axes may be different, this will draw an ellipse that correctly shows the radius on both axes. If it is false (the default) then a circle will be drawn using whichever scale gives the largest value
  • IsVisible: if false then the circle will not be shown

It also has some fairly standard customization options - LineWidth, LineColor, FillColor, HatchStyle, HatchColor, Font, FontColor, FontName, FontSize, FontBold, Rotation.

Example of use
This is an example that effectively creates a bubble plot by creating a circle for each point in a list. Most of the customizations are applied here.

            var plt = new Plot();
            int pointCount = 100;
            Random rand = new Random(0);
            double[] xs = DataGen.Consecutive(pointCount, 0.1);
            double[] ys = DataGen.NoisySin(rand, pointCount);
            double[] radius = DataGen.Random(rand, pointCount, 0.1, 0.02);

            var sph = plt.AddScatter(xs, ys);

            var font = new ScottPlot.Drawing.Font() { Size = 12, Color = Color.Black, Alignment = Alignment.MiddleCenter };
            var colors = new[] { Color.Blue, Color.Green, Color.Cyan }.Select(c => Color.FromArgb(127, c.R, c.G, c.B)).ToList();
            for(int i=0; i < pointCount; i++) { 
                var circle = new ScottPlot.Plottable.Circle() { X = xs[i], Y = ys[i], Radius = radius[i], Label = $"{i}", 
                    LineColor = colors[i % colors.Count], FillColor = colors[i % colors.Count], 
                    Font = font, HatchStyle = ScottPlot.Drawing.HatchStyle.SmallCheckerBoard };
                plt.Add(circle);
            }

CirclePlot

Copy link
Member

@bclehmann bclehmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, most of my review is trivial things like changing comments.

The one concern I do have is whether we ought to have a plottable for creating a single circle, or should we have a plottable which plots an array of circles. The user could then plot a single circle by passing in an array with one item (perhaps helped by another method in Plot.Add).

I think the latter is preferred, because if a future CirclePlot or BubblePlot is to be added, it seems unfortunate to require they have to create lots of Circle objects which duplicates lots of fields. For example, a BubblePlot would likely have its own fields for tracking fonts, line widths and colours, which axis it's associated with, etc, etc. Having one class that draws lots of circles minimizes that duplication.

The approach described above is how AddScatter and AddPoint work, both create a scatter plot, but AddPoint creates a scatter plot with only one point in it. Let me know what you think.

src/ScottPlot/Plottable/Circle.cs Outdated Show resolved Hide resolved
src/ScottPlot/Plottable/Circle.cs Outdated Show resolved Hide resolved
src/ScottPlot/Plottable/Circle.cs Outdated Show resolved Hide resolved
src/ScottPlot/Plottable/Circle.cs Outdated Show resolved Hide resolved
@StendProg
Copy link
Contributor

StendProg commented Apr 19, 2021

Hi @PeterDavidson,

It looks like you work is based on PlottableText.
Maybe it makes sense to do this more explicitly through inheritance from PlottableText.
This will reduce code duplication. For example, you can reuse an existing PlottableText.ApplyAlignmentOffset.

On the other hand, if we look closely, then the circle has nothing in common with the text, the text does not adjust to the circles, and the circles do not adjust to the text in any way.
It may be worth making the simplest PlottableCircle that draws only filled ellipses, and display the text using a separate PlottableText. If there is a need to group them, then you could do it, for example, in the AddCircleText API method, creating 2 separate but connected by coordinates(PlottableCircle and PlottableText).

What I like the most is the idea of creating CircleDecorator decorator that can wrap any IPlottable and add a circle over or behind IPlottable.Render. But that will probably complicate things a lot.

@swharden
Copy link
Member

swharden commented May 1, 2021

Displaying Multiple Shapes

The one concern I do have is whether we ought to have a plottable for creating a single circle, or should we have a plottable which plots an array of circles

I agree with @bclehmann's suggestion - it would be nice for one plottable to be able to display many circles.

Inheritance and Plottables

Maybe it makes sense to do this more explicitly through inheritance from PlottableText

On the other hand, if we look closely, then the circle has nothing in common with the text

If I had a deep understanding of the problem domain (exactly what functionality people would find useful) it would make sense to design and implement a complex inheritance tree (e.g., an abstract base class that text and shape inherit from, interfaces for shapes like circle and square, etc.) for these shape-plotting modules. However, I think I am still exploring the problem domain here, and am inclined to avoid inheritance for experimental features like this until I understand it better. I'm often surprised by how people use this library - it's not always what I would have predicted - so instead of trying to architect the perfect solution up-front my strategy is to keep experimental modules (like bubble plots) segregated from the rest of the modules base by minimizing inheritance.

That's a long answer, but it's the reasoning behind why I tend to prefer duplicating code over inheritance for plottables.

Immediate Next Steps

I'll refine this PR today, merge it in, and release a new package shortly. I think this plot type will/should continue to evolve, but it will be nice to have have some of this functionality available immediately. Thanks again @PeterDavidson for getting the ball rolling here!

Long-Term Goal: Expand Marker Functionality

I'm not going to step in this direction right now... but some day I think markers should be smarter (size, fill, border, etc. and also IMarker so custom markers could be injectable), then we could have a MarkerPlot which replaces this plot type. This concept has been triaged (#716, #386, #784, #785) for a while...

@swharden
Copy link
Member

swharden commented May 2, 2021

I'm converging onto something like this...

I'm thinking we should keep text/font/labels/alignment out of this plot type. There's already a plot type to add text to plots, and it's easy to add to the plot at any time.

Simple Example

double[] xs = ScottPlot.DataGen.Consecutive(31);
double[] sin = ScottPlot.DataGen.Sin(31);
double[] cos = ScottPlot.DataGen.Cos(31);

var plt = new ScottPlot.Plot(600, 400);
plt.AddBubblePlot(xs, sin);
plt.AddBubblePlot(xs, cos);
plt.Title("Simple Bubble Plot");
plt.SaveFig("simple.png");

image

Advanced Example

double[] xs = ScottPlot.DataGen.Consecutive(31);
double[] ys = ScottPlot.DataGen.Sin(31);
var cmap = ScottPlot.Drawing.Colormap.Viridis;

var plt = new ScottPlot.Plot(600, 400);
var myBubblePlot = plt.AddBubblePlot();
for (int i = 0; i < xs.Length; i++)
{
    double fraction = (double)i / xs.Length;
    myBubblePlot.Add(
        x: xs[i],
        y: ys[i],
        radius: 10 + i,
        fillColor: cmap.GetColor(fraction, alpha: .8),
        edgeColor: System.Drawing.Color.Black,
        edgeWidth: 2
    );
}

var plt = new ScottPlot.Plot(600, 400);
plt.Add(myBubblePlot);
plt.Title("Bubble Plot");
plt.AxisAuto(.2, .25);
plt.SaveFig("advanced.png");

image

@swharden swharden changed the title Circle plot Bubble Plot May 2, 2021
@swharden
Copy link
Member

swharden commented May 2, 2021

Okay this landed in a good spot. A quick summary of where we are:

image

@swharden swharden merged commit 8c20464 into ScottPlot:master May 2, 2021
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

Successfully merging this pull request may close these issues.

None yet

4 participants