Make the voronoi implementation extensible. #440

Closed
wants to merge 1 commit into
from

Conversation

4 participants
@bbroeksema

This is a relative small change that makes d3.geom.voronoi more extensible. The current implementation assumes that users are only interested in the cell boundaries. However, for some code I'm working on I also need the neighbors of each cell.

In order to make this possible with as little changes as possible I propose this patch. In stead of calling d3.geom.voronoi only with a set of vertices it now also takes an optional callback parameter. This parameter should be an object that provides two methods:

  • handle(e) // Handle edge addition
  • result() // finalize and return results.

In my patch the default behavior of d3.geom.voronoi is unchanged as I moved the original anonymous function into a default edgehandler which is used when the handler parameter is undefined.

This patch allows me to install my own edgehandler which records the required information.

Comments, questions and suggestions for improvements welcome of course.

Cheers,

Bertjan

@bbroeksema

This comment has been minimized.

Show comment
Hide comment
@bbroeksema

bbroeksema Jan 23, 2012

Bump.

Any comments on possible acceptation of this patch (or required improvements for acceptation)?

Bump.

Any comments on possible acceptation of this patch (or required improvements for acceptation)?

@mbostock

This comment has been minimized.

Show comment
Hide comment
@mbostock

mbostock Jan 23, 2012

Member

Sorry, I haven't had time to think about this.

Not directly related to your needs, but I was thinking the other day that perhaps the Voronoi method should be more like a layout, taking accessor functions to be flexible about the structure of the input. Currently, it assumes that the input is an array of two-element arrays (vertices: [[x1, y1], [x2, y2], …]). Most of the other layouts allow you to define the data in a more flexible way. For example, you might say something like:

var voronoi = d3.geom.voronoi()
    .x(function(d) { return d.x; })
    .y(function(d) { return d.y; });

And now you'd have a function voronoi that you could pass an array of nodes to (say, from a force-directed layout). Potentially the returned polygons could have meta data associated with them. For example, polygon[i].data could point back to the corresponding input vertex (d) that the polygon contains.

That might allow you to annotate the polygon with an array of neighbors, too. polygon[i].neighbors could be an array of input vertices. Or perhaps it should be the neighboring polygons? Or the neighboring indexes? I'm not sure.

Anyway, I'm pretty sure I don't want to pass in callback functions, because that's more white-box inheritance. Just my quick thoughts. Thanks for the pull request! Let me know what you think.

Member

mbostock commented Jan 23, 2012

Sorry, I haven't had time to think about this.

Not directly related to your needs, but I was thinking the other day that perhaps the Voronoi method should be more like a layout, taking accessor functions to be flexible about the structure of the input. Currently, it assumes that the input is an array of two-element arrays (vertices: [[x1, y1], [x2, y2], …]). Most of the other layouts allow you to define the data in a more flexible way. For example, you might say something like:

var voronoi = d3.geom.voronoi()
    .x(function(d) { return d.x; })
    .y(function(d) { return d.y; });

And now you'd have a function voronoi that you could pass an array of nodes to (say, from a force-directed layout). Potentially the returned polygons could have meta data associated with them. For example, polygon[i].data could point back to the corresponding input vertex (d) that the polygon contains.

That might allow you to annotate the polygon with an array of neighbors, too. polygon[i].neighbors could be an array of input vertices. Or perhaps it should be the neighboring polygons? Or the neighboring indexes? I'm not sure.

Anyway, I'm pretty sure I don't want to pass in callback functions, because that's more white-box inheritance. Just my quick thoughts. Thanks for the pull request! Let me know what you think.

@bbroeksema

This comment has been minimized.

Show comment
Hide comment
@bbroeksema

bbroeksema Jan 24, 2012

I do agree that my approach indeed smells like white-box inheritance, actually it is. For my project I namely just copied the edgehandling code and added the bits and pieces I needed. This is related to your above comments, namely I do so because determining the neighbors of a voronoi site is done during voronoi tessellation. At the point of adding an edge to a cell, the algorithm has information about the start and end points of the edge, the cell to which the edge belongs and to which other cell the edge is connected.

So the point is that I don't necessary myself want to annotate the polygons with neighbors, it is information I expect to be available after I did a voronoi layout (in order to prevent the costly calculation as a separate step). I might misunderstand your current proposal, but I don't see how this would deliver me the information (or give me the possibility to get it during the layout process).

An alternative might be to just store the neighbors in the .data section of vertices as part of the layout algorithm. But then a next requirement could become (not something I need), that one would be able to tell which neighbor reside at which edge. Something you don't solve in a comfortable way when annotating the neighbor at the vertices as each vertex will have two neighbors (except for those at the borders of the image).

Yet another alternative would be to return a list of "Cell" objects. Similar to the approach Raymond Hill has in his implementation (http://www.raymondhill.net/voronoi/rhill-voronoi-core.js). But that might be too heavy for d3.

I do agree that my approach indeed smells like white-box inheritance, actually it is. For my project I namely just copied the edgehandling code and added the bits and pieces I needed. This is related to your above comments, namely I do so because determining the neighbors of a voronoi site is done during voronoi tessellation. At the point of adding an edge to a cell, the algorithm has information about the start and end points of the edge, the cell to which the edge belongs and to which other cell the edge is connected.

So the point is that I don't necessary myself want to annotate the polygons with neighbors, it is information I expect to be available after I did a voronoi layout (in order to prevent the costly calculation as a separate step). I might misunderstand your current proposal, but I don't see how this would deliver me the information (or give me the possibility to get it during the layout process).

An alternative might be to just store the neighbors in the .data section of vertices as part of the layout algorithm. But then a next requirement could become (not something I need), that one would be able to tell which neighbor reside at which edge. Something you don't solve in a comfortable way when annotating the neighbor at the vertices as each vertex will have two neighbors (except for those at the borders of the image).

Yet another alternative would be to return a list of "Cell" objects. Similar to the approach Raymond Hill has in his implementation (http://www.raymondhill.net/voronoi/rhill-voronoi-core.js). But that might be too heavy for d3.

@bbroeksema

This comment has been minimized.

Show comment
Hide comment
@bbroeksema

bbroeksema Jan 24, 2012

Quick note: I do like the idea to make the algorithm more flexible with respect to the data it expects. Actually, one additional extension of your proposal would solve my problem:

var voronoi = d3.geom.voronoi()
    .x(function(d) { return d.x; })
    .y(function(d) { return d.y; });
    .edge(function(e) { e.d1.neighbor = e.d2; e.d2.neighbor = e.d1; })

The edge function would be called for every edge added to the layout and would allow me to register neighbors (or build whatever structure one needs). The argument passed to edge would contain the two data objects the edge is separating. Besides that it should of course contain the start and end vertex of the edge.

Quick note: I do like the idea to make the algorithm more flexible with respect to the data it expects. Actually, one additional extension of your proposal would solve my problem:

var voronoi = d3.geom.voronoi()
    .x(function(d) { return d.x; })
    .y(function(d) { return d.y; });
    .edge(function(e) { e.d1.neighbor = e.d2; e.d2.neighbor = e.d1; })

The edge function would be called for every edge added to the layout and would allow me to register neighbors (or build whatever structure one needs). The argument passed to edge would contain the two data objects the edge is separating. Besides that it should of course contain the start and end vertex of the edge.

@mbostock

This comment has been minimized.

Show comment
Hide comment
@mbostock

mbostock Jan 24, 2012

Member

Ah, I see; I misunderstood which data you wanted. Could you elaborate on why you want this data? Perhaps I could suggest a solution better if I understood the application. Thanks!

Member

mbostock commented Jan 24, 2012

Ah, I see; I misunderstood which data you wanted. Could you elaborate on why you want this data? Perhaps I could suggest a solution better if I understood the application. Thanks!

@bbroeksema

This comment has been minimized.

Show comment
Hide comment
@bbroeksema

bbroeksema Jan 24, 2012

I implemented a spatial merging algorithm that works as follows:

  • Take the voronoi tessellation of a set of points.
  • Sort the cells by size
  • Start from the smallest and merge it with all direct neighbors for which the site lies within merging distance D.
    repeat until no further merges occur.

being able to get the neighbors of a given cell is the crucial point here. Of course I keep track which original points fall in a merged cell. It has various applications, can't go into much detail further as I'm working on a paper.

Hope this explains enough what I need and why.

I implemented a spatial merging algorithm that works as follows:

  • Take the voronoi tessellation of a set of points.
  • Sort the cells by size
  • Start from the smallest and merge it with all direct neighbors for which the site lies within merging distance D.
    repeat until no further merges occur.

being able to get the neighbors of a given cell is the crucial point here. Of course I keep track which original points fall in a merged cell. It has various applications, can't go into much detail further as I'm working on a paper.

Hope this explains enough what I need and why.

@christophermanning

This comment has been minimized.

Show comment
Hide comment
@christophermanning

christophermanning Feb 4, 2012

I like the idea of adding x and y methods on the Voronoi object. Now I have to transform the data like this: .data(d3.geom.voronoi(vertices.map(function(o){return [o.x, o.y]})))

I was just researching how to get neighboring Voronoi cells and it doesn't seem possible since the neighbors aren't exposed from the voronoi object. I ended up using a Delaunay triangulation to connect nodes in cells that neighbored each other: http://bl.ocks.org/1734663 but that requires a Delaunay calculation during each update. If the edges were exposed, this could probably be simplified.

I like the idea of adding x and y methods on the Voronoi object. Now I have to transform the data like this: .data(d3.geom.voronoi(vertices.map(function(o){return [o.x, o.y]})))

I was just researching how to get neighboring Voronoi cells and it doesn't seem possible since the neighbors aren't exposed from the voronoi object. I ended up using a Delaunay triangulation to connect nodes in cells that neighbored each other: http://bl.ocks.org/1734663 but that requires a Delaunay calculation during each update. If the edges were exposed, this could probably be simplified.

@bbroeksema

This comment has been minimized.

Show comment
Hide comment
@bbroeksema

bbroeksema Jun 7, 2012

I'd still really like to have a solution for this problem. Currently I'm using my own version of the voronoi implementation with the extension as attached to this report.

So, why did I choose to export the internals? The current voronoi implementation calculates the diagram and forces the user to clip the cells if necessary. For this it is not needed to know the bounding box on forehand. However, when calculating the neighbors, it can happen that two cells are connected outside the boundingbox. Obviously, when I know the boundingbox on forehand I don't want these neighbours to be added. So by making some of the internals public I was able to implement my own EdgeHandler, which looks like this:

function EdgeHandler(vertices, w, h) {
    var polygons   = vertices.map(function() { return []; });
    var neighbours = vertices.map(function() { return []; });

    this.handle = function (e) {
      var s1,
          s2,
          x1,
          x2,
          y1,
          y2;

      if (e.a === 1 && e.b >= 0) {
        s1 = e.ep.r;
        s2 = e.ep.l;
      } else {
        s1 = e.ep.l;
        s2 = e.ep.r;
      }
      if (e.a === 1) {
        y1 = s1 ? s1.y : -1e6;
        x1 = e.c - e.b * y1;
        y2 = s2 ? s2.y : 1e6;
        x2 = e.c - e.b * y2;
      } else {
        x1 = s1 ? s1.x : -1e6;
        y1 = e.c - e.a * x1;
        x2 = s2 ? s2.x : 1e6;
        y2 = e.c - e.a * x2;
      }

      var v1 = [x1, y1],
          v2 = [x2, y2];

      polygons[e.region.l.index].push(v1, v2);
      polygons[e.region.r.index].push(v1, v2);

      var v1Inside = !(x1 < 0 || x1 > w || y1 < 0 || y1 > h);
      var v2Inside = !(x2 < 0 || x2 > w || y2 < 0 || y2 > h);

      if (v1Inside || v2Inside) {
        // Only record a neighbour when one of the vertices is within the 
        // bounding box: [0,0,w,h].
        neighbours[e.region.l.index].push(e.region.r.index);
        neighbours[e.region.r.index].push(e.region.l.index);
      }
    };

Obviously, this does some extra work (and needs additional information) compared to the original implementation. I don't see how I could reach the same goal though. If you have suggestions to take a different approach I'm more than happy to implement and try it out.

I'd still really like to have a solution for this problem. Currently I'm using my own version of the voronoi implementation with the extension as attached to this report.

So, why did I choose to export the internals? The current voronoi implementation calculates the diagram and forces the user to clip the cells if necessary. For this it is not needed to know the bounding box on forehand. However, when calculating the neighbors, it can happen that two cells are connected outside the boundingbox. Obviously, when I know the boundingbox on forehand I don't want these neighbours to be added. So by making some of the internals public I was able to implement my own EdgeHandler, which looks like this:

function EdgeHandler(vertices, w, h) {
    var polygons   = vertices.map(function() { return []; });
    var neighbours = vertices.map(function() { return []; });

    this.handle = function (e) {
      var s1,
          s2,
          x1,
          x2,
          y1,
          y2;

      if (e.a === 1 && e.b >= 0) {
        s1 = e.ep.r;
        s2 = e.ep.l;
      } else {
        s1 = e.ep.l;
        s2 = e.ep.r;
      }
      if (e.a === 1) {
        y1 = s1 ? s1.y : -1e6;
        x1 = e.c - e.b * y1;
        y2 = s2 ? s2.y : 1e6;
        x2 = e.c - e.b * y2;
      } else {
        x1 = s1 ? s1.x : -1e6;
        y1 = e.c - e.a * x1;
        x2 = s2 ? s2.x : 1e6;
        y2 = e.c - e.a * x2;
      }

      var v1 = [x1, y1],
          v2 = [x2, y2];

      polygons[e.region.l.index].push(v1, v2);
      polygons[e.region.r.index].push(v1, v2);

      var v1Inside = !(x1 < 0 || x1 > w || y1 < 0 || y1 > h);
      var v2Inside = !(x2 < 0 || x2 > w || y2 < 0 || y2 > h);

      if (v1Inside || v2Inside) {
        // Only record a neighbour when one of the vertices is within the 
        // bounding box: [0,0,w,h].
        neighbours[e.region.l.index].push(e.region.r.index);
        neighbours[e.region.r.index].push(e.region.l.index);
      }
    };

Obviously, this does some extra work (and needs additional information) compared to the original implementation. I don't see how I could reach the same goal though. If you have suggestions to take a different approach I'm more than happy to implement and try it out.

@bbroeksema

This comment has been minimized.

Show comment
Hide comment
@bbroeksema

bbroeksema Jan 18, 2013

Okay, retry. I still added a(n optional) callback. Differences: this time it is optional, that is, d3.geom.voronoi can still be called with just one argument and will return the same result as before. When the callback is set, this callback will be called for each edge that is encountered and nothing will be returned once the tessellation is finished. This allows for creating a custom data structure during tessellation (e.g. in my case I want to keep track of neighbours of a particular cell).

please let me know what you think of this (improved?!) approach.

Okay, retry. I still added a(n optional) callback. Differences: this time it is optional, that is, d3.geom.voronoi can still be called with just one argument and will return the same result as before. When the callback is set, this callback will be called for each edge that is encountered and nothing will be returned once the tessellation is finished. This allows for creating a custom data structure during tessellation (e.g. in my case I want to keep track of neighbours of a particular cell).

please let me know what you think of this (improved?!) approach.

Allow a callback to be set on the voronoi tessellation.
The default tessellation just returns a polygon for each cell of the
resulting tessellation. In some cases one might also want to keep
track of the neighbours of a cell.

This change lets one implement its own callback which will be called
for each edge that is found during the tessellation. The callback
will be given the two indices of the sites at both sides of the edge
as well as the two vertices that form the edge
@ryanthejuggler

This comment has been minimized.

Show comment
Hide comment
@ryanthejuggler

ryanthejuggler Jul 18, 2014

This issue could be closed; the .links() method exposes this information now.

This issue could be closed; the .links() method exposes this information now.

@mbostock mbostock closed this May 14, 2016

@mbostock mbostock modified the milestones: 4.0, Icebox May 14, 2016

@mbostock

This comment has been minimized.

Show comment
Hide comment
@mbostock

mbostock May 14, 2016

Member

The new d3-voronoi module for 4.0 returns the full Voronoi diagram, so it should be possible to track neighbors and other related operations. See the Voronoi Topology example.

Member

mbostock commented May 14, 2016

The new d3-voronoi module for 4.0 returns the full Voronoi diagram, so it should be possible to track neighbors and other related operations. See the Voronoi Topology example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment