avatorl / Deneb-Vega-Help

Do you need help with Deneb custom visual for Power BI and/or Vega visualization grammar? Create an issue here to get assistance from Deneb community expert Andrzej Leszkiewicz.
3 stars 0 forks source link

Vega - Place arrow heads at the start and end of links (paths) #7

Closed DW19904 closed 3 months ago

DW19904 commented 3 months ago

Hi,

I am trying to place triangles at the end of a path mark to represent an arrow head, i require them at both ends of the path.

In the current code below i have a triangle symbol mark added but its not in the correct position and i am struggling in how to move it to the end of each path, any help in how i get triangles at each end of the path is very much appreciated, i have also attached some mock data the visual is based on.

{ "width": 1200, "height": 600, "padding": 5, "autosize": "none", "signals": [ { "name": "labels", "value": true, "bind": {"input": "checkbox"} }, { "name": "radius", "value": 250, "bind": { "input": "range", "min": 100, "max": 250 } }, { "name": "links", "value": "line", "bind": { "input": "select", "options": [ "line", "curve", "diagonal", "orthogonal" ] } }, { "name": "originX", "update": "width / 2" }, { "name": "originY", "update": "height / 2" } ], "data": [ { "name": "dataset", "transform": [ { "type": "stratify", "key": "ID", "parentKey": "Parent" }, { "type": "tree", "method": "cluster", "size": [ 1, {"signal": "radius"} ], "as": [ "alpha", "radius", "depth", "children" ] }, { "type": "formula", "expr": "(0 + 360 datum.alpha + 160) % 360", "as": "angle" }, { "type": "formula", "expr": "PI datum.angle / 180", "as": "radians" }, { "type": "formula", "expr": "inrange(datum.angle, [90, 270])", "as": "leftside" }, { "type": "formula", "expr": "originX + datum.radius cos(datum.radians)", "as": "x" }, { "type": "formula", "expr": "originY + datum.radius sin(datum.radians)", "as": "y" } ] }, { "name": "links", "source": "dataset", "transform": [ {"type": "treelinks"}, { "type": "linkpath", "shape": {"signal": "links"}, "orient": "radial", "sourceX": "source.radians", "sourceY": "source.radius", "targetX": "target.radians", "targetY": "target.radius" } ] } ], "scales": [ { "name": "color", "type": "linear", "range": {"scheme": "magma"}, "domain": { "data": "dataset", "field": "depth" }, "zero": true } ], "marks": [ { "type": "path", "from": {"data": "links"}, "encode": { "update": { "x": {"signal": "originX"}, "y": {"signal": "originY"}, "path": {"field": "path"}, "stroke": {"value": "#ccc"}, "strokeDash": { "signal": "datum.target.LinkStrength === 'Weak' ? [5,5] : null" } } } }, { "type": "symbol", "from": {"data": "dataset"}, "encode": { "enter": { "size": {"value": 5000}, "stroke": {"value": "#666666"} }, "update": { "x": {"field": "x"}, "y": {"field": "y"}, "shape": { "signal": "datum.NodeType === 'Animal' ? 'diamond' : datum.NodeType === 'Human' ? 'circle' : 'square'" }, "fill": {"field": "NodeCF"} } } }, { "type": "symbol", "from": {"data": "dataset"}, "encode": { "enter": { "shape": {"value": "triangle-down"}, "size": {"value": 500}, "fill": {"value": "black"} }, "update": { "x": {"field": "x"}, "y": {"signal": "datum.y - 15"} } } }, { "type": "text", "from": {"data": "dataset"}, "encode": { "enter": { "text": { "signal": "split(datum.Name, ' ')" }, "fontSize": {"value": 12}, "baseline": { "value": "middle" }, "align": {"value": "center"} }, "update": { "x": {"field": "x"}, "y": { "signal": "datum.y - 0" }, "opacity": { "signal": "labels ? 1 : 0" } } } } ] }

VegaVisual

vegadata.csv

avatorl commented 3 months ago

The triangles are at the end of the each path. The problem is that path ends are in the middle of your nodes (squares, diamonds, circles), not at the node edges. And taking into account that you have multiple shapes of the nodes (squares, diamonds, circles), it won't be easy to calculate a correct position of where the path intersects node edge.

As an easier workaround you can put the triangles into the middle of each node. That requires some trigonometry calculations to make sure the triangle looks into the right direction:

    {
      "type": "symbol",
      "from": {"data": "links"},
      "encode": {
        "enter": {
          "shape": {"value": "triangle"},
          "size": {"value": 80},
          "fill": {"value": "black"}
        },
        "update": {
          "x": {"signal": "(datum.source.x+datum.target.x)/2"},
          "y": {"signal": "(datum.source.y+datum.target.y)/2"},
          "angle": {
            "signal": "360*atan((datum.target.y-datum.source.y)/(datum.target.x-datum.source.x))/(2*PI)+if(datum.target.x>originX,180,0)-90"
          }
        }
      }
    },

And I've changed data source to "from": {"data": "links"} for this mark, because you need a triangle for each link

image

avatorl commented 3 months ago

If you'll really want to put the triangles to the edges of the nodes, you need to calculate where the links intersect with the node edges. But there will be some extra trigonometry (less for the circles, more for the squares and diamonds). If you understand trigonometry - you can do that. Or just keep it as is.

DW19904 commented 3 months ago

Thank you so much, the triangle in the middle of the link works perfectly.

There is 1 other requirement that i am trying to work on, and its that some links need a triangle pointing in the other direction, but it can still sit in the middle, essentially just behind the existing one, if this is possible via another symbol mark which is a triangle, and it essentially looks like the below.

vegavisual

avatorl commented 3 months ago

I think you can use either triangle or diamond within the existing mark. Just use signal with if() statement instead of value in "shape": {"value": "triangle"}.

DW19904 commented 3 months ago

Fantastic, thank you! working a treat.