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

Contour Plot #4296

Merged
merged 25 commits into from
Oct 12, 2024
Merged

Contour Plot #4296

merged 25 commits into from
Oct 12, 2024

Conversation

jon-rizzo
Copy link
Contributor

@jon-rizzo jon-rizzo commented Sep 27, 2024

Added DelaunatorSharp code with license agreement on each file, with a few minor changes: the addition of a Z attribute to Point, and changes that were necessary to support nullable references types. Added TINPlot and Coordinate3d classes to create TIN diagrams, Voronoi Diagrams, and draw contours.

A TINPlot is a class representing a Triangular Irregular Network, or TIN. A TIN is a 2d triangulation of points that will allow you to interpolate the value of a third dimension on a straight line between neighboring points. This class also allows you to draw a Voronoi diagram as well as the contours resulting from this interpolation. Each diagram has its own point and line style settings. Future work could include smoothing and could also be used to render a 3d surface, since the result is a set of 3d triangles.

#2230 #3795
Example: surface.csv

TINPlot

Data taken from this location:
https://cmgds.marine.usgs.gov/data/field-activity-data/2016-030-FA/

ScottPlot.Plot myPlot = new();
List<Coordinates3d> pts = new();
using (StreamReader sr = new(".\surface.csv")) // csv file with x,y,z points
{
    //sr.ReadLine(); // skip the header
    while (!sr.EndOfStream)
    {
        string[] cols = sr.ReadLine().Split(',');
        pts.Add(new Coordinates3d(double.Parse(cols[0]), double.Parse(cols[1]), double.Parse(cols[2])));
    }
}

TINSourceCoordinates3dArray src = new(pts.ToArray());
TINPlot tin = new(src);
myPlot.Add.Plottable(tin);
myPlot.SavePng("tin.png");

…the only change being the addition of a Z attribute to Point. Added TINPlot and Coordinate3d classes to create TIN diagrams, Voronoi Diagrams, and draw contours.
@StendProg
Copy link
Contributor

Hi @jon-rizzo,

  1. Please add screenshots of Plottables examples in the description. It will be illustrative.
  2. DelavourSharp has nuget packet. It is not clear why you need to copy the code. Is there any way to make your code modifications on top of the installed package?
  3. ScottPlot tries to have a minimum number of dependencies. Maybe it would be better to make a separate project (inside the solution, for example, as Controls are implemented) that will have this dependency and plug it in as needed. Without adding the dependency to the main ScottPlot project. (Not in a hurry to redesign just thoughts for discussion.)

@jon-rizzo
Copy link
Contributor Author

jon-rizzo commented Sep 27, 2024

  1. Please add screenshots of Plottables examples in the description. It will be illustrative.

was hoping that someone could contribute a meaningful example. my data is all proprietary and cannot be shared. random points with random elevations do not make a meaningful example.

  1. DelavourSharp [sic] has nuget packet. It is not clear why you need to copy the code. Is there any way to make your code modifications on top of the installed package?

scott expressed a desire not to make a dependency. I included the code to avoid a dependency & added the license agreement.

  1. ScottPlot tries to have a minimum number of dependencies. Maybe it would be better to make a separate project (inside the solution, for example, as Controls are implemented) that will have this dependency and plug it in as needed. Without adding the dependency to the main ScottPlot project. (Not in a hurry to redesign just thoughts for discussion.)

I had to make changes to DelaunatorSharp so that it would build against the null handling requirements.

Not sure how to resolve the build failure. it is saying that I need to install various workloads. not sure how this is done or why it is required.

@StendProg
Copy link
Contributor

Not sure how to resolve the build failure. it is saying that I need to install various workloads. not sure how this is done or why it is required.

All PR's are susceptible to this error for now. It appears to be a broken pipeline. You don't have to worry about this particular error, it is not up to you.

@swharden
Copy link
Member

swharden commented Oct 10, 2024

Hi @jon-rizzo, thanks for this PR! I'm just now starting to take a closer look at it.

However, I think @StendProg and I are having trouble evaluating this PR because it's not clear what the intended visual output is supposed to be. Proprietary sample data is not required to demonstrate how this plot type works if we can communicate the main idea using generated sample data.

I modified this PR to add a cookbook page demonstrating how this plot type works. When the test runs is generates the following output. Is this the output you would expect given the sample data generated here? Do you have a recommendation for a better way to generate sample data? I'm a bit confused because the output looks very different from what I'd expect to see (plots like the demonstration ones at the top of #2230 and #3795)

Coordinates3d[] cs = new Coordinates3d[500];
for (int i = 0; i < 500; i++)
{
    double x = Generate.RandomNumber(0, Math.PI * 2);
    double y = Generate.RandomNumber(0, Math.PI * 2);
    double z = Math.Sin(x) + Math.Cos(y);
    cs[i] = new(x, y, z);
}

TINSourceCoordinates3dArray data = new(cs.ToArray());
ScottPlot.Plottables.TINPlot contour = new(data);
myPlot.PlottableList.Add(contour);

image

Thanks for your feedback!
Scott

Additional Resources:

@swharden
Copy link
Member

I probably won't put too much more time into researching this at the moment, but highly relevant information is that Python's matplotlib has tripcolor and tricontourf functions which seem to use these data as a starting point for creating colored contour plots, so I think we are off to a good start here...

image

@swharden swharden changed the title Added DelaunatorSharp, TINPlot and Coordinate3d. Contour Plot Oct 10, 2024
@jon-rizzo
Copy link
Contributor Author

So, the contouring algorithm that I implemented is admittedly very naiive. It draws straight line contours on each triangle. As a result, large data sets (such as the one that I had originally cited) will show more "pleasing" contours than small ones. there are other contouring algorithms that will result in smoother (more visually pleasing, but technically less precise) contours. there are both contour smoothing algorithms and surface (TIN) smoothing algorithms that can do this so I don't think there is one "right answer". I think your data set did not generate contours because all of the values would be between 0 and 1. try changing the contouring interval to something smaller, like 0,1. Contours currently render in a single color. the colorized triangles that you showed (or other colorization options) should be easy enough to implement in the future, but I may have to leave that to others. Mostly, I wanted to get more eyes on this before I went too far.

@jon-rizzo
Copy link
Contributor Author

jon-rizzo commented Oct 10, 2024

Sorry for the confusion - I have the defaults set differently than I was expecting. By default, the contours are not shown. you have to set the ContourLineWidth to at least 1, and your data set works best with the MinorInterval set to 0.1 (or some other value smaller than 1). If you modify your example as follows. you should get the results below.

Coordinates3d[] cs = new Coordinates3d[500];
for (int i = 0; i < 500; i++)
{
    double x = Generate.RandomNumber(0, Math.PI * 2);
    double y = Generate.RandomNumber(0, Math.PI * 2);
    double z = Math.Sin(x) + Math.Cos(y);
    cs[i] = new(x, y, z);
}

TINSourceCoordinates3dArray data = new(cs.ToArray());
ScottPlot.Plottables.TINPlot contour = new(data);
contour.MinorInterval = 0.1;
contour.ContourLineWidth = 1;
myPlot.PlottableList.Add(contour);

Contours

or, even better, you can turn off the display of the triangles and points, add more points, and increase the contour interval slightly

Coordinates3d[] cs = new Coordinates3d[1000];
for (int i = 0; i < 1000; i++)
{
    double x = Generate.RandomNumber(0, Math.PI * 2);
    double y = Generate.RandomNumber(0, Math.PI * 2);
    double z = Math.Sin(x) + Math.Cos(y);
    cs[i] = new(x, y, z);
}

TINSourceCoordinates3dArray data = new(cs.ToArray());
ScottPlot.Plottables.TINPlot contour = new(data);
contour.MinorInterval = 0.2;
contour.ContourLineWidth = 1;
contour.LineWidth = 0;
contour.MarkerStyle = MarkerStyle.None;
myPlot.PlottableList.Add(contour);

Contours2

@StendProg
Copy link
Contributor

StendProg commented Oct 10, 2024

This looks very interesting, you should have started with something like this, at the beginning it was really unclear what it was for.

If we take a Heatmap with a regular grid, and put these isolines on top of it, it should be even more beautiful.

            int N = 100;
            Coordinates3d[] cs = new Coordinates3d[N * N];
            double[,] heatmapData = new double[N, N];
            for (int i = 0; i < N; i++)
                for (int j = 0; j < N; j++)
                {
                    double x = 2.0 * Math.PI * i / N;
                    double y = 2.0 * Math.PI * j / N;
                    double z = Math.Sin(x) + Math.Cos(y);
                    cs[i * N + j] = new(x, y, z);
                    heatmapData[j, i] = z;
                }

            var heatmap = myPlot.Add.Heatmap(heatmapData);
            heatmap.CellAlignment = Alignment.LowerLeft;
            heatmap.CellWidth = 2.0 * Math.PI / N;
            heatmap.CellHeight = 2.0 * Math.PI / N;

            TINSourceCoordinates3dArray data = new(cs.ToArray());
            ScottPlot.Plottables.TINPlot contour = new(data);
            contour.MinorInterval = 0.2;
            contour.ContourLineWidth = 1;
            contour.LineWidth = 0;
            contour.MarkerStyle = MarkerStyle.None;
            myPlot.PlottableList.Add(contour);

image

@jon-rizzo
Copy link
Contributor Author

The first example that I posted is an elevation contour map of a real place on the earth. these are rarely, if ever, described as beautiful, but this is the context with which I am most familiar. I think the triangles were making it difficult to see the contours, but can have other uses, as scott pointed out in his images. my previous attempts with random points used random elevations, and the results were far from 'beautiful' because that type of data is not very meaningful. It did not occur to me to use a trig function based on X & Y to calculate Z.

There are many more features that can be added in the future - the triangles themselves could be colored based a heatmap (like scott is showing on the left) or the space between the contours can be colored based on a heatmap (like scott is showing on the right). the contours could also be colored based on a heatmap. the most useful feature to most folks, however, would probably be adding labels to each index contour, each intermediate contour, or both. The plot control can also draw Voronoi diagrams of the resulting TIN, but I do not know the practical applications of those diagrams, or if any such applications even exist.

@swharden
Copy link
Member

swharden commented Oct 10, 2024

Hi @jon-rizzo, thanks for the clarification - that helps a lot! The path forward is now a lot easier to see.

Also @StendProg I love your example showing the contour lines over the heatmap!

I'll continue to work on this PR and create cookbook examples as I go. I'll probably work toward moving as much logic as possible out of the TINPlot plottable and put it in static classes so the same logic can be used by other plot types.

Presently a lot of math is performed in the render loop, but it seems unlikely that users will be creating plots like this from "live data", so that work could probably be done up front. I'm even thinking that, when given a collection of 3d points, users may want to get different things to create different types of plots (perhaps leveraging existing plottables):

  • A collection of lines (to show show the triangulated network) using AddLine() or AddLines()
  • A collection of triangles (to create a Voronoi diagram) using AddPolygon()
  • A collection of paths (to show contour lines) using AddPolygon or perhaps a new ContourLine plot type that has logic built in to place labels inline

I'll keep experimenting and commit changes to this PR as I go.
Thanks again everyone for your input along the way!

@jon-rizzo
Copy link
Contributor Author

but it seems unlikely that users will be creating plots like this from "live data",

unlikely, but not unreasonable or impossible. I have seen several dynamic "grading optimizers" (in the civil engineering world) that adjusts elevations of all of the points and re-renders the tin and contours continuously while the optimizer converges on a solution . There are also simple widgets like this one: https://www.geogebra.org/m/Bkn8sw7v

@swharden
Copy link
Member

but it seems unlikely that users will be creating plots like this from "live data"

unlikely, but not unreasonable or impossible

Still working on this, but I'll precalculate all the lines when the plottable is constructed, but expose an Update method to allow users to recalculate the network if/when data changes.

I greatly simplified TINPlot (and the Delaunator backend), and am continuing to work toward separating calculation logic from display logic. I may set this down soon today and pick it up again tomorrow 👍

@StendProg
Copy link
Contributor

Very strange algorithm for finding contours. In fact, it just hatch each triangle with a given step, starting from the minimum value. Hoping that the hatchings on different triangles will add up to a single path. This works in some form, but it's not hard to come up with exceptions breaking this. The only advantage is speed. In a good way I think it is necessary to traverse neighboring triangles making a single path, but this is probably a very difficult and computationally expensive task.

For example: here the vertices of the triangle are deliberately made a little less than 0.5. As a result, we have a highly irregular pitch of contour lines on the square.
IrregularContour

After some thought I came to the conclusion that if we want to build a contour on the heatmap, we do not have to triangulate it, we can do bilinear filtering directly on the rectangles that make up the heatmap.

@jon-rizzo
Copy link
Contributor Author

As you mentioned, the benefit is speed. That, and simplicity of implementation. a spatial index could easily be created if the desire is for continuous, connected paths. There are more advanced algorithms for contouring to achieve smoother contours. There are many established algorithms for doing all of these things. Delaunay Triangulation is one very common solution, particularly with points that have irregular or random distribution. as a bonus, the resulting triangles can also be rendered quickly in 3D or displayed with different colors based on the elevation or slope to perform various analyses. if the points are on a grid, other solutions might be faster and/or more elegant. For example, the CONREC algorithm. but, my need is specifically for random points. I don't think there is one "best" way to draw contours for all situations, so I don't view this as an either/or choice. more solutions = more better.

@swharden
Copy link
Member

Okay, I brought this forward to a place where I'm happy to merge. This works now:

myPlot.Add.ContourLines(coordinates);

Limitations / Improvements

The ContourLines plottable constructor has an overload which accepts Coordinates[,] for displaying contour lines from rectangular datasets. As suggested earlier, a better algorithm (bilinear filtering?) could be used to generate contour lines for this case.

Also every ContourLine presently only has a Path with two points. In other words, the contour lines are a bunch of short segments and aren't actually "connected" into polygons. This would have to be fixed to enable filled contour plots.

I don't intend to put time into these improvements right now (I wish to focus on some other open high priority issues) but if one of you wants to add this enhancement and create a PR I would be happy to review it!

New Cookbook Recipes

Contour Lines from a Irregular Points (Coordinates)

image

Contour Lines from a Rectangular Grid (Coordinates[,])

image

Contour Lines over Heatmap

image

Contour Lines with Colormap

image

@swharden swharden merged commit e942cda into ScottPlot:main Oct 12, 2024
3 checks passed
@jon-rizzo jon-rizzo deleted the tinplot branch October 12, 2024 15:33
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.

3 participants