Get the desired result with defineFigureGenerator + Auto panel + geom.spot1/spot2

Hello,

I’m using GoJS v1.7.1 on Linux in Firefox.

My problem is: I defined a custom shape geometry for a rectangle with two pointy (triangular) endings, and I want to use that shape around text blocks of varying size. The tricky part for me is defining which zone of the shape can be filled with text. I’m using geometry.spot1 and spot2 for that, but the results I get are strange, see the pictures below with comments.

Question: what am I doing wrong? Thanks in advance.

First defineFigureGenerator results:

Code:

  go.Shape.defineFigureGenerator('Condition', function (shape, w, h) {
    var hh = h / 2; // Half-height
    var pw = h / 4; // Width of the pointy bit
    var path = new go.PathFigure(w - pw, 0);
    path.add(new go.PathSegment(go.PathSegment.Line, w, hh));
    path.add(new go.PathSegment(go.PathSegment.Line, w - pw, h));
    path.add(new go.PathSegment(go.PathSegment.Line, pw, h));
    path.add(new go.PathSegment(go.PathSegment.Line, 0, hh));
    path.add(new go.PathSegment(go.PathSegment.Line, pw, 0).close());
    var geo = new go.Geometry();
    geo.add(path);
    geo.spot1 = new go.Spot(0, 0, pw - 5, 0);
    geo.spot2 = new go.Spot(1, 1, - pw + 5, 0);
    return geo;
  });

In the first version, I generate a shape that has outer dimensions matching the w and h provided by GoJS, but a smaller space inside, so I expect the shape to be stretched or recomputed somehow until it fits around the TextBlock.

Since it does not work, I thought maybe the w and h indicate the expected size of the inner space, so in the second version I build a shape that provides the required space inside but is larger than w and h, but it does not work correctly either:

Code:

  go.Shape.defineFigureGenerator('Condition2', function (shape, w, h) {
    var hh = h / 2; // Half-height
    var pw = h / 4; // Width of the pointy bit
    var path = new go.PathFigure(pw + w, 0);
    path.add(new go.PathSegment(go.PathSegment.Line, pw + w + pw, hh));
    path.add(new go.PathSegment(go.PathSegment.Line, pw + w, h));
    path.add(new go.PathSegment(go.PathSegment.Line, pw, h));
    path.add(new go.PathSegment(go.PathSegment.Line, 0, hh));
    path.add(new go.PathSegment(go.PathSegment.Line, pw, 0).close());
    var geo = new go.Geometry();
    geo.add(path);
    geo.spot1 = new go.Spot(0, 0, pw - 5, 0);
    geo.spot2 = new go.Spot(1, 1, - pw + 5, 0);
    return geo;
  });

Test shapes + text:

  diagram.add(
    $(go.Part, "Auto",
      { position: new go.Point(300, 0), background: "lightgray" },
      $(go.Shape, "Condition2", { fill: "lightgreen" }),
      $(go.TextBlock, "some text", { background: "yellow", opacity: 0.8 })
    ));

  diagram.add(
    $(go.Part, "Auto",
      { position: new go.Point(0, 60), background: "lightgray" },
      $(go.Shape, "Condition2", { fill: "lightgreen" }),
      $(go.TextBlock, "some text", { width: 500, background: "yellow", opacity: 0.8 })
    ));

  diagram.add(
    $(go.Part, "Auto",
      { position: new go.Point(0, 90), background: "lightgray" },
      $(go.Shape, "Condition2", { fill: "lightgreen" }),
      $(go.TextBlock, "some text", { width: 400, height: 100, background: "yellow", opacity: 0.8, textAlign: 'center' })
    ));

  diagram.add(
    $(go.Part, "Auto",
      { position: new go.Point(0, 200), background: "lightgray" },
      $(go.Shape, "Condition2", { fill: "lightgreen" }),
      $(go.TextBlock, "some text", { width: 100, height: 200, background: "yellow", opacity: 0.8, textAlign: 'center' })
    ));

  diagram.add(
    $(go.Part, "Auto",
      { position: new go.Point(300, 200), background: "lightgray" },
      $(go.Shape, "Condition2", { fill: "lightgreen" }),
      $(go.TextBlock, "some text", { width: 100, height: 400, background: "yellow", opacity: 0.8 })
    ));

You’ve done a good job with your first try. Just one little typo:

    var pw = h / 4; // Width of the pointy bit

should be:

    var pw = w / 4; // Width of the pointy bit

Here’s what I tried with your first definition of “Condition” plus the typo fix:

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        { resizable: true },
        $(go.Shape, "Condition", { fill: "gold" }),
        $(go.TextBlock, { margin: 4 }, new go.Binding("text", "key"))
      );

Some results after reshapings:

Thanks for your quick answer, unfortunately that’s not a typo, that’s really what I want: the width of the pointy side must depend on the height, so that the slope of the triangle is constant, regardless of the text box dimensions.

To be clear, here is what I want to achieve:

Ah, OK, so that’s what you mean. We’ll look into this a bit later today, after we’ve gotten to our offices.

Well, there’s an inherent chicken-and-egg problem when dealing with figure spots. Each time the figure generator is given a different width and height, the geometry and the two spots will be different. So the “Auto” Panel measurement cannot guess what the right width and height should be in order for the area within the two spots to be a given size (the size of the TextBlock in this case).

But you can still use your custom figure if you data bind the width of the Shape to the height of the TextBlock. (Note: data binding only works when there is data, so it cannot work when just calling Diagram.add.)

    myDiagram.nodeTemplate =
      $(go.Node, "Auto", // or "Spot"
        { resizable: true, resizeObjectName: "TB", background: "lightgray" },
        new go.Binding("position"),
        $(go.Shape, "Condition",
          new go.Binding("fill", "color"),
          new go.Binding("desiredSize", "", function(tb) {
            var s = tb.desiredSize.copy();
            if (isNaN(s.height)) s.height = tb.naturalBounds.height;
            return new go.Size(Math.max(0, s.width + s.height / 2 - 10), s.height);
          }).ofObject("TB")),
        $(go.TextBlock,
          { name: "TB", background: "yellow", opacity: 0.8, textAlign: 'center' },
          new go.Binding("text", "key"),
          new go.Binding("desiredSize").makeTwoWay())
      );

    myDiagram.model = new go.GraphLinksModel(
    [
      { key: "Alpha", color: "lightblue", position: new go.Point(300, 0), },
      { key: "Beta", color: "orange", position: new go.Point(0, 60), desiredSize: new go.Size(500, NaN) },
      { key: "Gamma", color: "lightgreen", position: new go.Point(0, 90), desiredSize: new go.Size(400, 100) },
      { key: "Delta", color: "pink", position: new go.Point(0, 200), desiredSize: new go.Size(100, 200) },
      { key: "Epsilon", color: "white", position: new go.Point(300, 200), desiredSize: new go.Size(100, 400) }
    ]);

This uses your original “Condition” figure.

Unfortunately again, this solution is impractical for me because I don’t know the dimensions of my text blocks in advance: they have a maximum width and I let the text wrap into as many lines as necessary. Also, I want to use the same custom shape around groups of nodes, for which I cannot predict the size either.

I see the chicken and egg problem in the general case. However, as you suggest with your approach, in my case I could “help” the Auto panel compute the shape size given the contents size. Is there a way to hook into the Auto panel size computation? I.e. a kind of “GoJS Extension” point, like makeGeometry for Links, that I could override.

Indeed, I suppose that the Auto panel behaves thusly:

  1. compute the bounds s of the inner TextBlock (or Placeholder when I use my shape around a group)
  2. make my “Condition” figure with w = s.width (or, for some reason, w = s.width + 40) and h = s.height

If I could hook somewhere between those two steps, I could modify the second with your suggestion, like this:
2. make my “Condition” figure with w = s.width + s.height / 2 - 10 and h = s.height

Is this achievable? Thanks in advance.

Have you tried this template in your app? The results certainly seem to be exactly what you are asking for.

Yes indeed the graphical result is exactly what I’m after, thank you for that; my new problem is that I won’t have the necessary information in my app to use this exact template (no a priori desiredSize).

I didn’t try it inside my app, I will do that later today when I’m at work. However I tried it by modifying a GoJS documentation sample. I modified your suggestion to be closer to what my app does:

  • I removed the desiredSize binding, because I don’t have that information in my app
  • I put longer text in the nodes, and a maximum width on the TextBlock to simulate the varying node sizes
  • Since it didn’t work, I tried to add some code to the binding, to finally realize that it was never fired (console.log never outputs anything). According to the documentation on bindings, I think in your version it works because desiredSize is a settable property, and when it is set on TB by the data binding it triggers the ofObject binding, but in my case no settable properties are modified on the object and thus its binding never fires.

Could I force the object binding to fire at the right moment? (just after the TextBlock/Placeholder has received new actualBounds?) I tried binding to actualBounds but it didn’t work, and according to the documentation it’s normal, because it’s not a settable property.

<pre data-language="javascript" id="autoPanels">
  go.Shape.defineFigureGenerator('Condition', function (shape, w, h) {
    var hh = h / 2; // Half-height
    var pw = h / 4; // Width of the pointy bit
    var path = new go.PathFigure(w - pw, 0);
    path.add(new go.PathSegment(go.PathSegment.Line, w, hh));
    path.add(new go.PathSegment(go.PathSegment.Line, w - pw, h));
    path.add(new go.PathSegment(go.PathSegment.Line, pw, h));
    path.add(new go.PathSegment(go.PathSegment.Line, 0, hh));
    path.add(new go.PathSegment(go.PathSegment.Line, pw, 0).close());
    var geo = new go.Geometry();
    geo.add(path);
    geo.spot1 = new go.Spot(0, 0, pw - 5, 0);
    geo.spot2 = new go.Spot(1, 1, - pw + 5, 0);
    return geo;
  });

    diagram.nodeTemplate =
      $(go.Node, "Auto", // or "Spot"
        { resizable: true, resizeObjectName: "TB", background: "lightgray" },
        new go.Binding("position"),
        $(go.Shape, "Condition",
          new go.Binding("fill", "color"),
          new go.Binding("desiredSize", "", function(tb) {
            console.log('binding', tb.actualBounds());
            // tb.updateTargetBindings();
            // tb.ensureBounds();
            var s = tb.actualSize.copy();
            if (isNaN(s.height)) s.height = tb.naturalBounds.height;
            return new go.Size(Math.max(0, s.width + s.height / 2 - 10), s.height);
          }).ofObject("TB")),
        $(go.TextBlock,
          { name: "TB", background: "yellow", opacity: 0.8, textAlign: 'center', maxSize: new go.Size(100,NaN) },
          new go.Binding("text", "key")
        )
      );

    diagram.model = new go.GraphLinksModel(
    [
      { key: "Alpha", color: "lightblue", position: new go.Point(300, 0), },
      { key: "Beta ksdjlfsjql ksjfl jslkfjdskjlfj skdjfkljsdkljflsjd", color: "orange", position: new go.Point(0, 60), desiredSize: new go.Size(500, NaN) },
      { key: "Gammamslqjf sqkjdflkj qskfj lkqdslflqjkfjsdkl fkjs dklfj lkdslkfjlsdjfkjslkjfkqzj mfkj dslj flj sdkjf lkdsjlkf jdskj flksjlkfjsljflksdj lkfjlskdjfkjsdlkfjlsdjfkl sdlkjf lk", color: "lightgreen", position: new go.Point(0, 90), desiredSize: new go.Size(400, 100) },
      { key: "Deltmslmslqjf sqkjdflkj qskfj lkqdslflqjkfjsdkl fkjs dklfj lkdslkfjlsdjfkjslkjfkqzj mfkj dslj flj sqjf sqkjdflkj qskfj lkqdslflqjkfjsdkl fkjs dklfj lkdslkfjlsdjfkjslkjfkqzj mfkj dslj flj sa", color: "pink", position: new go.Point(0, 200), desiredSize: new go.Size(100, 200) },
      { key: "Epsilon", color: "white", position: new go.Point(300, 200), desiredSize: new go.Size(100, 400) }
    ]);
</pre>
<script>goCode("autoPanels", 600, 700)</script>

Sorry, there was a bug in the previous javascript, (tb.actualBounds is not a function) that’s why the console.log never showed up. Also, tb.actualSize is always undefined in my tests.

Here is my latest attempt. I recorded my screen: as you can see, at first the conditions are all very small, but as soon as I start resizing one, the binding is fired with the correct bounds and the condition gets the desired shape.

I feel that I’m almost there, I just need the binding you provided to be fired automatically when the correct actualBounds are available for the TextBlock/Placeholder.

    diagram.nodeTemplate =
      $(go.Node, "Auto", // or "Spot"
        { resizable: true, resizeObjectName: "TB", background: "lightgray" },
        new go.Binding("position"),
        $(go.Shape, "Condition",
          new go.Binding("fill", "color"),
          new go.Binding("desiredSize", "", function(tb) {
            console.log('actualSize', tb.actualSize);
            console.log('actualBounds.size', tb.actualBounds.size);
            var s = tb.actualBounds.size.copy();
            if (isNaN(s.height)) s.height = tb.naturalBounds.height;
            return new go.Size(Math.max(0, s.width + s.height / 2 - 10), s.height);
          }).ofObject("TB")),
        $(go.TextBlock,
          { name: "TB", background: "yellow", opacity: 0.8, textAlign: 'center', maxSize: new go.Size(100,NaN) },
          new go.Binding("text", "key")
        )
      );
  function init() {
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
          {
            initialContentAlignment: go.Spot.Center,
            "undoManager.isEnabled": true,
            layout: $(go.GridLayout, { isRealtime: false })
          });

    go.Shape.defineFigureGenerator('Condition', function(shape, w, h) {
      var hh = h / 2; // Half-height
      var pw = h / 4; // Width of the pointy bit
      var path = new go.PathFigure(w - pw, 0);
      path.add(new go.PathSegment(go.PathSegment.Line, w, hh));
      path.add(new go.PathSegment(go.PathSegment.Line, w - pw, h));
      path.add(new go.PathSegment(go.PathSegment.Line, pw, h));
      path.add(new go.PathSegment(go.PathSegment.Line, 0, hh));
      path.add(new go.PathSegment(go.PathSegment.Line, pw, 0).close());
      var geo = new go.Geometry();
      geo.add(path);
      geo.spot1 = new go.Spot(0, 0, pw - 5, 0);
      geo.spot2 = new go.Spot(1, 1, -pw + 5, 0);
      return geo;
    });

    myDiagram.nodeTemplate =
      $(go.Node, "Auto", // or "Spot"
        { resizable: true, resizeObjectName: "TB" },
        $(go.Shape, "Condition",
          new go.Binding("fill", "color"),
          new go.Binding("desiredSize", "", function(tb) {
            tb.part.ensureBounds();  // this is not supported
            var w = tb.naturalBounds.width;
            var h = tb.naturalBounds.height;
            return new go.Size(Math.max(0, w + h / 2 - 10), h);
          }).ofObject("TB")),
        $(go.TextBlock,
          { name: "TB", textAlign: "center", maxSize: new go.Size(100, NaN) },
          new go.Binding("text"))
      );

    myDiagram.model = new go.GraphLinksModel(
    [
      { text: "Alpha", color: "lightblue" },
      { text: "Beta ksdjlfsjql ksjfl jslkfjdskjlfj skdjfkljsdkljflsjd", color: "orange" },
      { text: "Gammamslqjf sqkjdflkj qskfj lkqdslflqjkfjsdkl fkjs dklfj lkdslkfjlsdjfkjslkjfkqzj mfkj dslj flj sdkjf lkdsjlkf jdskj flksjlkfjsljflksdj lkfjlskdjfkjsdlkfjlsdjfkl sdlkjf lk", color: "lightgreen" },
      { text: "Deltmslmslqjf sqkjdflkj qskfj lkqdslflqjkfjsdkl fkjs dklfj lkdslkfjlsdjfkjslkjfkqzj mfkj dslj flj sqjf sqkjdflkj qskfj lkqdslflqjkfjsdkl fkjs dklfj lkdslkfjlsdjfkjslkjfkqzj mfkj dslj flj sa", color: "pink" },
      { text: "Epsilon", color: "white" }
    ]);
  }

I tested your sample, it works. Thank you very much for your time!

I will integrate it to my app another day, which may have more than a TextBlock.

What do you mean by // this is not supported? That it might break in a future GoJS release?

Depending on the circumstances, side-effects in Binding conversion functions can cause undefined behavior. It seems to work in this particular case, but I can imagine it would not in other cases.

I tried to integrate the latest solution in my app, but I run into various sizing and layout inconsistencies. See the following gif:

  1. When I expand the yellow condition for the first time, the yellow shape is not wide enough (some nodes from the group are outside the shape)
  2. If I click on it, it triggers the binding again and the shape takes the correct dimension. But then we can notice on the left that the layout has been computed badly (the layerSpace is not respected)
  3. If I click again on the condition to collapse it, it keeps very large margins. Again, when I click the binding works again and sets the correct margins.
  4. At the end of the gif, I do the same operation again and it works better that time (I don’t know why). But still, there is a problem because the bottom of the yellow shape is too close to the grey line, which is the bottom of the group containing the yellow shape.

I’m a bit disappointed because on the one hand, I feel that the current solution is too much of a hack and it will be hard for me to work around all the glitches, and on the other hand my initial shape sizing problem is in fact very simple since I have all the data that I need to give to GoJS to solve it. I think there should be a simple way of doing that with GoJS.

Do you have any other ideas? Thanks in advance.

So this time you are using a “Condition” figure surrounding the Placeholder of a Group? If you replace the “Condition” figure with “Rectangle”, does everything work well?

  • without the custom desiredShape binding
    • with Rectangle → no problem
    • with Condition → my original sizing problem
  • with the custom binding
    • with Rectangle → sizing/layout are not done properly
    • with Condition → sizing/layout are not done properly

and you’re binding on the Placeholder?
(I assume you meant the “desiredSize” binding.)

Yes, desiredSize.

No, I bind on a vertical panel that holds the title of the condition and the placeholder. I’ll post the complete template tomorrow if that helps.

Hello,

This is still a problem for me, and I have found no workable solution yet.

  • Walter’s proposed fix is admittedly hack-ish and indeed generates other problems
  • The solutions I can think of by myself all sound a lot like re-implementing layout/sizing code from GoJS, which would be a waste of time, would not work as well as the original, wouldn’t scale…
  • I feel like the problem is in fact simple and that a simple fix should be possible, especially with your “insider” knowledge of GoJS.

To recap, here is what should happen ideally in the sizing algorithm:

  1. GoJS sizes the inner content of the shape (Placeholder or TextBlock or in fact whatever other element, it shouldn’t matter) → Expected: the solution not dependent on my particular template
  2. GoJS asks me for the correct shape dimensions to wrap around that (I can provide them, for example with Walter’s function) → Expected: no need for GoJS to guess anything, I know what I want and I can compute it
  3. GoJS sizes the shape with my indications → Expected: the shape fits around the inner elements correctly
  4. GoJS sizes the Auto panel → Expected: other elements around the Condition-shaped node/group don’t overlap

Thanks in advance for your help,

Jany

We’ll investigate this, but it may take some time, because of an ongoing snowstorm.