Using overflow property of an SVG

We’ve got an SVG string (Base64 at this point) that I’m loading into a Picture. We allow users to change the strokewidth property (before the string is encoded and transfered to this app) which results in sometimes parts of the image getting clipped. We used the ‘overflow’ property to allow it to draw outside the bounds of the SVG and it works very well just viewing the SVG in plain HTML, but the clipping still occurs in GoJS diagrams. Is this supported or is there a better way to achieve the result we’re after?

Did you set the desiredSize (or equivalently, the width and height) of the Picture? How is the Picture declared in your template? And what is the SVG (as text)?

Here’s the template.

 const shapeNodeTemplate = go.GraphObject.make(
      go.Node,
      {
        avoidableMargin: new go.Margin(0, 0, 0, 0),
        locationSpot: go.Spot.Center,
      },
      'Auto',
      new go.Binding('location', 'location', (Location) => new go.Point(Location.item1, Location.item2)),
      go.GraphObject.make(
        go.Picture,
        { imageStretch: go.GraphObject.Fill },
        new go.Binding('flip', 'flipState', (flipState) => this.getFlipState(flipState)),      
        new go.Binding('source', 'imageString', (str) => `data:image/svg+xml;base64,${str}`)
      )
    );

Here’s a simplified version of the svg that still shows how it uses the overflow property.

<svg overflow="visible" id="Pipe180Degree" width="87.58" height="45.88" version="1.1" viewBox="0 0 87.58 45.88" xmlns="http://www.w3.org/2000/svg">
	<style id="style1" type="text/css">

		.st4{fill:none;stroke:#666666;stroke-miterlimit:10;}
		.st5{fill:none;stroke:#666666;stroke-miterlimit:10;stroke-dasharray:1,2;}        
        .svgborderstroke {stroke: #696969; stroke-width: 12.000000000000002;}
	</style>
    
    <g id="Outlines_20_" transform="translate(-6.21,-26.81)">
        <path id="path16" class="svgborderstroke st4" d="m44.71 72.69v-2.09c0-2.92 2.37-5.29 5.29-5.29s5.29 2.37 5.29 5.29v2.09" />
        <path id="path17" class="svgborderstroke st4" d="m93.29 72.69 v-2.09 c0-23.91-19.38-43.29-43.29-43.29s-43.29 19.38-43.29 43.29v2.09" />
        <line id="line17" class="svgborderstroke st5" x1="6.71" x2="44.71" y1="70.6" y2="70.6" />
        <line id="line18" class="svgborderstroke st5" x1="55.29" x2="93.29" y1="70.6" y2="70.6" />
    </g>
</svg>

If you are going to modify the SVG so that what is drawn is bigger, you also need to increase the size of the viewbox. I’ve done that in the SVG of the second node in this example:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const $ = go.GraphObject.make;

const myDiagram =
  new go.Diagram("myDiagramDiv",
    {
      initialScale: 3,
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  $(go.Node, 'Auto',
    {
      avoidableMargin: new go.Margin(0, 0, 0, 0),
      locationSpot: go.Spot.Center,
    },
    //new go.Binding('location', 'location', (Location) => new go.Point(Location.item1, Location.item2)),
    $(go.Picture,
      { imageStretch: go.GraphObject.Fill },
      //new go.Binding('flip', 'flipState', (flipState) => this.getFlipState(flipState)),      
      new go.Binding('source', 'imageString', (str) => `data:image/svg+xml;base64,${str}`)
    )
  );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, text: "Alpha", imageString: btoa(`<svg overflow="visible" id="Pipe180Degree" width="87.58" height="45.88" version="1.1" viewBox="0 0 87.58 45.88" xmlns="http://www.w3.org/2000/svg">
	<style id="style1" type="text/css">
		.st4{fill:none;stroke:#666666;stroke-miterlimit:10;}
		.st5{fill:none;stroke:#666666;stroke-miterlimit:10;stroke-dasharray:1,2;}        
        .svgborderstroke {stroke: #696969; stroke-width: 12.000000000000002;}
	</style>
    <g id="Outlines_20_" transform="translate(-6.21,-26.81)">
        <path id="path16" class="svgborderstroke st4" d="m44.71 72.69v-2.09c0-2.92 2.37-5.29 5.29-5.29s5.29 2.37 5.29 5.29v2.09" />
        <path id="path17" class="svgborderstroke st4" d="m93.29 72.69 v-2.09 c0-23.91-19.38-43.29-43.29-43.29s-43.29 19.38-43.29 43.29v2.09" />
        <line id="line17" class="svgborderstroke st5" x1="6.71" x2="44.71" y1="70.6" y2="70.6" />
        <line id="line18" class="svgborderstroke st5" x1="55.29" x2="93.29" y1="70.6" y2="70.6" />
    </g>
</svg>`)
  }, 
  { key: 2, text: "Beta", imageString: btoa(`<svg overflow="visible" id="Pipe180Degree" width="87.58" height="45.88" version="1.1" viewBox="-6 -6 99.58 57.88" xmlns="http://www.w3.org/2000/svg">
	<style id="style1" type="text/css">
		.st4{fill:none;stroke:#666666;stroke-miterlimit:10;}
		.st5{fill:none;stroke:#666666;stroke-miterlimit:10;stroke-dasharray:1,2;}        
        .svgborderstroke {stroke: #696969; stroke-width: 12.000000000000002;}
	</style>
    <g id="Outlines_20_" transform="translate(-6.21,-26.81)">
        <path id="path16" class="svgborderstroke st4" d="m44.71 72.69v-2.09c0-2.92 2.37-5.29 5.29-5.29s5.29 2.37 5.29 5.29v2.09" />
        <path id="path17" class="svgborderstroke st4" d="m93.29 72.69 v-2.09 c0-23.91-19.38-43.29-43.29-43.29s-43.29 19.38-43.29 43.29v2.09" />
        <line id="line17" class="svgborderstroke st5" x1="6.71" x2="44.71" y1="70.6" y2="70.6" />
        <line id="line18" class="svgborderstroke st5" x1="55.29" x2="93.29" y1="70.6" y2="70.6" />
    </g>
</svg>`)
  }
]);
  </script>
</body>
</html>

Yes, but that just scales down the image so it fits in the height/width. The overflow property is for deciding what to do when the content doesn’t fit.

https://developer.mozilla.org/en-US/docs/Web/CSS/overflow

or

https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/overflow

I do understand that it’s a relatively new thing so if it’s not supported, I understand that. Can you confirm one way or the other?

Scaling the viewport is an option we’ve considered, but haven’t implemented as we don’t yet have a good way to figure out on the fly exactly what that size should be, given the different geometries we have to work with.

I did see in another forum post from several years ago that you had provided some unsupported code that would translate an svg to GraphObjects. I would be interested in seeing that code or something equivalient if you still happen to have it.

So you were expecting us to add scrollbars to the image so that the user could scroll them to see all of the SVG image? Sorry, we don’t do that and have no intention of doing that. It’s just supposed to be an image, and the viewbox specifies what to show of the SVG contents. The SVG width and height are necessary in order for it to be rendered properly in a browser.

Nope. Wasn’t thinking about scrollbars. Wanted something like this, though I do get that one of the options adds a scrollbar.

OverflowExample

This is just straight html/js. Hopefully it’s obvious that the SVG goes beyond the specified height/width and it is supported in the browser. (I added a red bg to make it easier to see.)

If you don’t plan on supporting it, that’s fine. Do you by chance still happen to have that old code I mentioned?

I think having objects paint beyond their bounds is a very tricky matter. We support that to a certain extent for well-known cases. But extending it to support drawing arbitrarily far away by objects that we couldn’t control would really mess up code that depended on things being within their “bounds”.

Although writing simple SVG isn’t too hard, it’s extremely hard to read and render arbitrary SVG. You might want to read: Intro - Notes on SVG parsing

Here’s the code, which Northwoods can not support in any fashion. It’s why we didn’t finish the job – we could not do so in a reasonable manner.

It won’t fit into a single post, so I’ll split it up into two sections.

'use strict';
/*
*  Copyright (C) 1998-2019 by Northwoods Software Corporation. All Rights Reserved.
*/

/*
SvgParse #1
*/

// see ??? for current issues
// no support for text or images
// no support for clipping, masks, links, markers, some stroke properties
// no support for opacity on non-Panels or on individual brushes
// limited support for brushes, including transforms & currentColor
// no support for even-odd rule
// problems with "use"
// no support for CSS styling
// no support for non-pixel units or %
// no support for viewbox

/**
* @ignore
* This is a helper class, only called by GraphObject.fromSvg.
* @constructor
* @class
* This class is used for parsing and generating SVG.
*/
function SvgParse() {
  // be sure that all data properties are named starting with an underscore
  // an array containing all the GraphObjects generated for the SVG
  this._graphObjectArray = [];
}

/**
* @ignore
* Create a {@link GraphObject} from some SVG.
* @this {SvgParse}
* @param {string|Document} svg either an XML DOM Node or a string that is parsed into XML DOM.
* @return {GraphObject}
*/
SvgParse.prototype.fromSvg = function(svg) {
  /** @ignore @type {Document} */ var xmlDoc = null;
  // if svg is an XML DOM Node it is cloned to avoid modifying the referenced document
  if (typeof svg === 'string') {
    xmlDoc = new DOMParser().parseFromString(svg, 'text/xml');
  } else if (svg instanceof Document) {
    xmlDoc = svg.implementation.createDocument('http://www.w3.org/2000/svg', 'svg', null);
    xmlDoc.appendChild(xmlDoc.importNode(svg.documentElement, true));
  }
  if (xmlDoc === null) return null;

  var svgElts = xmlDoc.getElementsByTagName('svg'); // there could be multiple SVG elements?
  if (svgElts.length === 0) return null;
  var svgElt = svgElts[0];

  // create and save brushes created for each gradient element
  // patterns?
  var linGrads = xmlDoc.getElementsByTagName('linearGradient');
  var radGrads = xmlDoc.getElementsByTagName('radialGradient');
  for (var i = 0; i < linGrads.length; i++) {
    var grad = linGrads[i];
    // ??? Why are the start and end assumed to be Left-Right?
    var brush = GraphObject.make(Brush, Brush.Linear, { start: Spot.Left, end: Spot.Right });
    // add color stops
    var stops = grad.childNodes;
    for (var j = 0; j < stops.length; j++) {
      if (stops[j].tagName === 'stop') {
        var color = this._getInheritedProperty(stops[j], 'stop-color');
        if (color === null || color === '') continue;
        var offstr = this._getInheritedProperty(stops[j], 'offset');
        if (offstr === null || offstr === '') offstr = '0';
        var off = parseFloat(offstr);
        if (isNaN(off)) off = 0;
        brush.addColorStop((offstr.indexOf('%') !== -1 ? .01 : 1) * off, color);
      }
    }
    var id = grad.getAttribute('id');
    if (typeof id === 'string') this['_brush' + id] = brush;
  }
  for (var i = 0; i < radGrads.length; i++) {
    var grad = radGrads[i];
    // ??? Why are the start and end assumed to be Center-Center?
    var brush = GraphObject.make(Brush, Brush.Radial, { start: Spot.Center, end: Spot.Center });
    // add color stops
    var stops = grad.childNodes;
    for (var j = 0; j < stops.length; j++) {
      if (stops[j].tagName === 'stop') {
        var color = this._getInheritedProperty(stops[j], 'stop-color');
        if (color === null || color === '') continue;
        var offstr = this._getInheritedProperty(stops[j], 'offset');
        if (offstr === null || offstr === '') offstr = '0';
        var off = parseFloat(offstr);
        if (isNaN(off)) off = 0;
        brush.addColorStop((offstr.indexOf('%') !== -1 ? .01 : 1) * off, color);
      }
    }
    var id = grad.getAttribute('id');
    if (typeof id === 'string') this['_brush' + id] = brush;
  }

  // append referenced nodes to <use> nodes, the <use> will then be treated as <g>
  // --------------if <use> references a <symbol> or <svg> it is treated differently
  var usesIncomplete = true;
  while (usesIncomplete) {
    usesIncomplete = false;
    var uses = xmlDoc.getElementsByTagName('use');
    for (var i = 0; i < uses.length; i++) {
      var use = uses[i]; //  SVGUseElement
      if (use.childNodes.length !== 0) continue;
      var href = use.href;
      if (href === undefined) continue; // needed? or does (refElt === null) catch all?
      var ref = href.baseVal.substring(1);
      var refElt = xmlDoc.getElementById(ref);
      if (refElt === null) continue;
      var newElt = refElt.cloneNode(true);
      newElt.removeAttribute('id');
      var x = parseFloat(use.getAttribute('x'));
      if (isNaN(x)) x = 0;
      var y = parseFloat(use.getAttribute('y'));
      if (isNaN(y)) y = 0;
      var trans = use.getAttribute('transform');
      if (trans === null) trans = '';
      use.setAttribute('transform', trans + ' translate(' + x + ',' + y + ')');
      use.appendChild(newElt);
      if (newElt.tagName === 'use') usesIncomplete = true;
    }
  }

  this._createShapes(svgElt, null);

  var imagePanel = new go.Panel();
  if (this._graphObjectArray.length === 0) return imagePanel;
  else if (this._graphObjectArray.length === 1) return this._graphObjectArray[0];
  for (var i = 0; i < this._graphObjectArray.length; i++) imagePanel.add(this._graphObjectArray[i]);
  return imagePanel;
};

/**
* @ignore
* @this {SvgParse}
* @param {*} xmlElt can't type-check, should be of type "Node" but we modified the externs.
* @param {string} property name
* @return {?string}
*/
SvgParse.prototype._getLocalProperty = function(xmlElt, property) {
  var eltProp = xmlElt.getAttribute(property);
  if (typeof eltProp !== 'string' && xmlElt.style) eltProp = xmlElt.style[property];
  if (typeof eltProp !== 'string') return null;
  return eltProp;
};

/**
* @ignore
* @this {SvgParse}
* @param {*} xmlElt can't type-check, should be of type "Node" but we modified the externs.
* @param {string} property name
* @return {?string}
*/
SvgParse.prototype._getInheritedProperty = function(xmlElt, property) {
  var eltProp = xmlElt.getAttribute(property);
  if (typeof eltProp !== 'string' && xmlElt.style) eltProp = xmlElt.style[property];
  if (typeof eltProp !== 'string' || eltProp === '' || eltProp === 'inherit') {
    var par = xmlElt.parentNode;
    if (par.tagName === 'g' || par.tagName === 'use') return this._getInheritedProperty(par, property);
    else return null;
  }
  return eltProp;
};

// go through the <svg> and add all shapes to this._graphObjectArray
// add a Panel for a group with transforms
// brush patterns?
// svgElt is the element currently being added, panel is the panel, representing a group, that its shapes should be added to
// if panel is null they will be added to this._graphObjectArray
// this is called originally by fromSvg(svg) with an entire <svg> element as svgElt and panel = null
/**
* @ignore
* @this {SvgParse}
* @param {SVGElement} svgElt
* @param {Panel} panel
*/
SvgParse.prototype._createShapes = function(svgElt, panel) {
  var tag = svgElt.tagName;
  if (tag !== 'g' && tag !== 'svg' && tag !== 'use' && tag !== 'symbol') return;

  // don't bother creating the Shapes or Panel if display: none
  var display = this._getInheritedProperty(svgElt, 'display');
  if (display === 'none') return;

  var elts = svgElt.childNodes;
  for (var i = 0; i < elts.length; i++) {
    // will we need a case "svg"? also "text" should be handled
    var elt = elts[i];
    /** @ignore @type {GraphObject} */ var gojsObj = null;

    if (elt.getAttribute === undefined) continue;
    var trans = elt.getAttribute('transform');

    switch (elt.tagName) {
      case 'g':
        if (trans === null) this._createShapes(elt, null);
        else {
          gojsObj = new go.Panel();
          this._createShapes(elt, gojsObj);
        }
        break;
      case 'use':
        if (trans === null) this._createShapes(elt, null);
        else {
          gojsObj = new go.Panel();
          this._createShapes(elt, gojsObj);
        }
        break;
      case 'symbol':
        // symbols will currently not act differently than groups
        // getSymbolScale is not yet functional and will need to take into account viewBox and preserveAspectRatio values for the <symbol>
        // as well as the height and width provided by the containing <use>
        if (svgElt.tagName !== 'use') break;
        gojsObj = new go.Panel();
        this._createShapes(elt, gojsObj);
        gojsObj.scale = this._getSymbolScale(elt, svgElt);
        break;
      case 'path':
        gojsObj = this._handlePath(elt);
        break;
      case 'line':
        gojsObj = this._handleLine(elt);
        break;
      case 'circle':
        gojsObj = this._handleCircle(elt);
        break;
      case 'ellipse':
        gojsObj = this._handleEllipse(elt);
        break;
      case 'rect':
        gojsObj = this._handleRect(elt);
        break;
      case 'polygon':
        gojsObj = this._handlePolyline(elt);
        break;
      case 'polyline':
        gojsObj = this._handlePolyline(elt);
    }

    if (gojsObj === null) continue;

    // stroke and fill properties
    if (gojsObj instanceof go.Shape) {
      var shape = /** @type {Shape} */ (gojsObj);
      //??? missing fill-opacity
      var fill = this._getInheritedProperty(elt, 'fill');
      if (fill !== null && fill.indexOf('url') !== -1) {
        var refId = fill.substring(fill.indexOf('#') + 1, fill.length - 1); // make sure this always works
        var brush = this['_brush' + refId];
        if (brush instanceof go.Brush) shape.fill = brush;
        else shape.fill = 'black';
      } else if (fill === null) {
        shape.fill = 'black';
      } else if (fill === 'none') {
        shape.fill = null;
      } else {
        shape.fill = fill;
      }

      //??? missing stroke-opacity
      var stroke = this._getInheritedProperty(elt, 'stroke');
      if (stroke !== null && stroke.indexOf('url') !== -1) {
        var refId = stroke.substring(stroke.indexOf('#') + 1, stroke.length - 1); // make sure this always works
        var brush = this['_brush' + refId];
        if (brush instanceof go.Brush) shape.stroke = brush;
        else shape.stroke = 'black';
      } else if (stroke === 'none') {
        shape.stroke = null;
      } else {
        shape.stroke = stroke;
      }

      var strokeWidth = parseFloat(this._getInheritedProperty(elt, 'stroke-width'));
      if (!isNaN(strokeWidth)) shape.strokeWidth = strokeWidth;

      var strokeCap = this._getInheritedProperty(elt, 'stroke-linecap');
      if (strokeCap !== null) shape.strokeCap = strokeCap;

      var strokeDashArray = this._getInheritedProperty(elt, 'stroke-dasharray');
      if (strokeDashArray !== null && strokeDashArray !== '') {
        var arr = strokeDashArray.split(',');
        var sda = [];
        for (var j = 0; j < arr.length; j++) {
          var dash = parseFloat(arr[j]);
          if (!isNaN(dash) && dash > 0) sda.push(dash);
        }
        shape.strokeDashArray = sda;
      }

      var strokeDashOffset = this._getInheritedProperty(elt, 'stroke-dashoffset');
      if (strokeDashOffset !== null && strokeDashOffset !== '') {
        var sdo = parseFloat(strokeDashOffset);
        if (!isNaN(sdo)) shape.strokeDashOffset = sdo;
      }

      shape.isGeometryPositioned = true;
    }

    // transforms

    // handle panels?
    if (trans !== null) {

      // separate the transform property string into separate transforms by splitting them
      //  at the parentheses closing their parameters

      // transform commands are executed from right to left

      var transforms = trans.split(')');
      // if the parameters for any transform on an object contain errors,
      //  perform no transforms on the object
      var validParams = true;
      for (var j = 0; j < transforms.length; j++) {
        if (/\(.*[^0-9\.,\s-]/.test(transforms[j])) validParams = false;
        if (/\(.*[0-9]-[0-9]/.test(transforms[j])) validParams = false;
      }
      if (validParams) {
        for (var j = transforms.length - 1; j >= 0; j--) {
          var transString = transforms[j];
          if (transString === '') continue;
          var parenInd = transString.indexOf('(');
          var transCommand = transString.substring(0, parenInd).replace(/\s*/, '');
          var paramString = transString.substring(parenInd + 1);
          var transParams = paramString.split(/\s*[\s,]\s*/);
          switch (transCommand) {
            case 'rotate':
              this._transformRotate(gojsObj, transParams);
              break;
            case 'translate':
              this._transformTranslate(gojsObj, transParams);
              break;
            case 'scale':
              this._transformScale(gojsObj, transParams);
              break;
            case 'skewX':
              this._transformSkewX(gojsObj, transParams);
              break;
            case 'skewY':
              this._transformSkewY(gojsObj, transParams);
              break;
            case 'matrix':
              this._transformMatrix(gojsObj, transParams);
              break;
          }
        }
      }
    }

    if (gojsObj instanceof go.Panel) {
      var gojsPanel = /** @type {Panel} */ (gojsObj);
      var xmin = 0.0;
      var ymin = 0.0;
      var pPos = gojsPanel.position.copy();
      if (isNaN(pPos.x)) pPos.x = 0.0;
      if (isNaN(pPos.y)) pPos.y = 0.0;
      var panElts = gojsPanel.elements.iterator;
      while (panElts.next()) {
        var gelt = panElts.value;
        var ePos = gelt.position.copy();
        if (isNaN(ePos.x)) ePos.x = 0.0;
        if (isNaN(ePos.y)) ePos.y = 0.0;
        if (ePos.x < xmin) xmin = ePos.x;
        if (ePos.y < ymin) ymin = ePos.y;
      }
      pPos.x += xmin;
      pPos.y += ymin;
      gojsPanel.position = pPos;
    }

    if (panel === null) this._graphObjectArray.push(gojsObj);
    else panel.add(gojsObj);
  }

  if (gojsObj !== null) {
    // visibility
    var visibility = this._getLocalProperty(svgElt, 'visibility');
    if (visibility === 'hidden' || visibility === 'collapse') {
      gojsObj.visible = false;
    }

    // opacity
    var opacity = this._getLocalProperty(svgElt, 'opacity');
    if (opacity !== null && opacity !== '') {
      var opa = parseFloat(opacity);
      if (!isNaN(opa)) gojsObj.opacity = opa;  //??? not supported on GraphObject, only on Panel
    }
  }

  // also need to handle <symbol>s
  // <use> treated as group, <svg> also (?)
};

/**
* @ignore
* @this {SvgParse}
* @param {SVGElement} symbol
* @param {SVGElement} parent
* @return {number}
*/
SvgParse.prototype._getSymbolScale = function(symbol, parent) { // doesn't do anything yet
  var PAR = this._getInheritedProperty(symbol, 'preserveAspectRatio');
  var viewBox = this._getInheritedProperty(symbol, 'viewBox');
  var vbPoints = [];
  if (typeof viewBox === 'string') vbPoints = viewBox.split(/\s*[\s,]\s*/);
  if (vbPoints.length < 4) return 1; //  ??? but it returns 1 anyway
  return 1;
};

section #2:

'use strict';
/*
*  Copyright (C) 1998-2019 by Northwoods Software Corporation. All Rights Reserved.
*/

/*
SvgParse #2
*/

// gojsObj is a shape or panel
// matrixParams is an array containing the 6 number parameters for a matrix transformation as strings
// change the geometry and position of the GraphObject according to the parameters
/**
* @ignore
* @this {SvgParse}
* @param {GraphObject} gojsObj
* @param {Array.<string>} matrixParams
*/
SvgParse.prototype._transformMatrix = function(gojsObj, matrixParams) {
  var a = parseFloat(matrixParams[0]);
  var b = parseFloat(matrixParams[1]);
  var c = parseFloat(matrixParams[2]);
  var d = parseFloat(matrixParams[3]);
  var e = parseFloat(matrixParams[4]);
  var f = parseFloat(matrixParams[5]);
  // do not transform if parameters are missing
  if (isNaN(a + b + c + d + e + f)) return;

  var pos = gojsObj.position.copy();
  if (isNaN(pos.x)) pos.x = 0;
  if (isNaN(pos.y)) pos.y = 0;

  if (gojsObj instanceof go.Shape) {
    var shape = /** @type {Shape} */ (gojsObj);
    var geo = shape.geometry.copy();

    if (geo.type === go.Geometry.Rectangle) {
      geo = this._turnRectangleIntoPath(geo);
    } else if (geo.type === go.Geometry.Ellipse) {
      geo = this._turnEllipseIntoPath(geo);
    } else if (geo.type === go.Geometry.Line) {
      geo.type = go.Geometry.Path;
      var fig = new go.PathFigure(geo.startX, geo.startY);
      var seg = new go.PathSegment(go.PathSegment.Line, geo.endX, geo.endY);
      fig.segments.add(seg);
      geo.figures.add(fig);
    }

    // will need to offset geometry first and after

    geo.offset(pos.x, pos.y);
    geo.transform(a, b, c, d, e - pos.x, f - pos.y);

    // what about strokeWidth?

    var p = geo.normalize();
    shape.geometry = geo;
    pos.x -= p.x;
    pos.y -= p.y;
    shape.position = pos;
  } else if (gojsObj instanceof go.Panel) {
    var panel = /** @type {Panel} */ (gojsObj);

    // increase all elements' positions by the panel's position, then decrease them by that amount after tranforming them
    var pElts = panel.elements.iterator;
    while (pElts.next()) {
      var elt1 = pElts.value;
      var ePos = elt1.position.copy();
      ePos.x += pos.x;
      ePos.y += pos.y;
      elt1.position = ePos;
    }
    pElts.reset();
    while (pElts.next()) {
      var elt2 = pElts.value;
      this._transformMatrix(elt2, matrixParams);
    }
    pElts.reset();
    while (pElts.next()) {
      var elt3 = pElts.value;
      var ePos = elt3.position.copy();
      ePos.x -= pos.x;
      ePos.y -= pos.y;
      elt3.position = ePos;
    }
  }
};

/**
* @ignore
* @this {SvgParse}
* @param {GraphObject} gojsObj
* @param {Array.<string>} rotateParams
*/
SvgParse.prototype._transformRotate = function(gojsObj, rotateParams) {
  var angle = parseFloat(rotateParams[0]);
  if (isNaN(angle)) angle = 0;
  var cx = parseFloat(rotateParams[1]);
  if (isNaN(cx)) cx = 0;
  var cy = parseFloat(rotateParams[2]);
  if (isNaN(cy)) cy = 0;
  if (angle !== 0) {
    var radAngle = angle * Math.PI / 180;

    var pos = gojsObj.position.copy();
    if (isNaN(pos.x)) pos.x = 0;
    if (isNaN(pos.y)) pos.y = 0;

    if (gojsObj instanceof go.Shape) {
      var shape = /** @type {Shape} */ (gojsObj);
      var geo = shape.geometry.copy();

      if (geo.type === go.Geometry.Ellipse) geo = this._turnEllipseIntoPath(geo);
      else if (geo.type === go.Geometry.Rectangle) geo = this._turnRectangleIntoPath(geo);

      if (geo.type === go.Geometry.Path) {
        geo.rotate(angle, cx - pos.x, cy - pos.y);

        var p = geo.normalize();
        shape.geometry = geo;
        pos.x -= p.x;
        pos.y -= p.y;
        shape.position = pos;
      } else {
        // line geometry
        var relStartX = geo.startX - cx + pos.x;
        var relStartY = geo.startY - cy + pos.y;
        var relEndX = geo.endX - cx + pos.x;
        var relEndY = geo.endY - cy + pos.y;
        var newStartX = relStartX * Math.cos(radAngle) - relStartY * Math.sin(radAngle) + cx - pos.x;
        var newStartY = relStartY * Math.cos(radAngle) + relStartX * Math.sin(radAngle) + cy - pos.y;
        var newEndX = relEndX * Math.cos(radAngle) - relEndY * Math.sin(radAngle) + cx - pos.x;
        var newEndY = relEndY * Math.cos(radAngle) + relEndX * Math.sin(radAngle) + cy - pos.y;
        var minX = Math.min(newStartX, newEndX);
        var minY = Math.min(newStartY, newEndY);
        geo.startX = newStartX - minX;
        geo.startY = newStartY - minY;
        geo.endX = newEndX - minX;
        geo.endY = newEndY - minY;
        pos.x += minX;
        pos.y += minY;
        shape.position = pos;
        shape.geometry = geo;
      }
      if (shape.fill instanceof go.Brush) {
        var newBrush = shape.fill.copy();
        var oldAngle = Math.atan((.5 - newBrush.start.y) / (.5 - newBrush.start.x));
        if (!isNaN(oldAngle)) {
          var brushAngle = oldAngle + radAngle;
          newBrush.start = new go.Spot((1 - Math.cos(brushAngle)) / 2, (1 - Math.sin(brushAngle)) / 2);
          newBrush.end = new go.Spot((1 + Math.cos(brushAngle)) / 2, (1 + Math.sin(brushAngle)) / 2);
        }
        shape.fill = newBrush;
      }
      if (shape.stroke instanceof go.Brush) {
        var newBrush = shape.stroke.copy();
        var oldAngle = Math.atan((.5 - newBrush.start.y) / (.5 - newBrush.start.x));
        if (!isNaN(oldAngle)) {
          var brushAngle = oldAngle + radAngle;
          newBrush.start = new go.Spot((1 - Math.cos(brushAngle)) / 2, (1 - Math.sin(brushAngle)) / 2);
          newBrush.end = new go.Spot((1 + Math.cos(brushAngle)) / 2, (1 + Math.sin(brushAngle)) / 2);
        }
        shape.stroke = newBrush;
      }
    } else if (gojsObj instanceof go.Panel) {
      var panel = /** @type {Panel} */ (gojsObj);
      var pElts = panel.elements.iterator;
      while (pElts.next()) {
        var elt1 = pElts.value;
        var ePos = elt1.position.copy();
        ePos.x += pos.x;
        ePos.y += pos.y;
        elt1.position = ePos;
      }
      pElts.reset();
      while (pElts.next()) {
        var elt2 = pElts.value;
        this._transformRotate(elt2, rotateParams);
      }
      pElts.reset();
      while (pElts.next()) {
        var elt3 = pElts.value;
        var ePos = elt3.position.copy();
        ePos.x -= pos.x;
        ePos.y -= pos.y;
        elt3.position = ePos;
      }
    }
  }
};

/**
* @ignore
* @this {SvgParse}
* @param {GraphObject} gojsObj
* @param {Array.<string>} transParams
*/
SvgParse.prototype._transformTranslate = function(gojsObj, transParams) {  // should only position be used?
  // will use parseFloat to read these, though might be expressions -- SVG will treat as an incorrect value if not a number, not just take the part that is one
  var dx = parseFloat(transParams[0]);
  if (isNaN(dx)) dx = 0;
  var dy = parseFloat(transParams[1]);
  if (isNaN(dy)) dy = 0;
  if (dx !== 0 || dy !== 0) {
    var pos = gojsObj.position.copy();
    if (isNaN(pos.x)) pos.x = 0;
    if (isNaN(pos.y)) pos.y = 0;
    gojsObj.position = new go.Point(dx + pos.x, dy + pos.y);
  }
};

/**
* @ignore
* @this {SvgParse}
* @param {GraphObject} gojsObj
* @param {Array.<string>} scaleParams
*/
SvgParse.prototype._transformScale = function(gojsObj, scaleParams) {
  var xScale = parseFloat(scaleParams[0]);
  if (isNaN(xScale)) xScale = 1;
  var yScale = parseFloat(scaleParams[1]);
  if (isNaN(yScale)) yScale = xScale;
  if (xScale !== 1 || yScale !== 1) {
    var pos = gojsObj.position.copy();
    if (isNaN(pos.x)) pos.x = 0;
    if (isNaN(pos.y)) pos.y = 0;

    if (gojsObj instanceof go.Shape) {
      var shape = /** @type {Shape} */ (gojsObj);
      var geo = shape.geometry.copy();
      pos.x *= xScale;
      pos.y *= yScale;
      shape.position = pos;
      geo.scale(xScale, yScale);
      // what about strokeWidth?
      shape.geometry = geo;
    } else if (gojsObj instanceof go.Panel) {
      var panel = /** @type {Panel} */ (gojsObj);
      var pElts = panel.elements.iterator;
      while (pElts.next()) {
        var elt1 = pElts.value;
        var ePos = elt1.position.copy();
        ePos.x += pos.x;
        ePos.y += pos.y;
        elt1.position = ePos;
      }
      pElts.reset();
      while (pElts.next()) {
        var elt2 = pElts.value;
        this._transformScale(elt2, scaleParams);
      }
      pElts.reset();
      while (pElts.next()) {
        var elt3 = pElts.value;
        var ePos = elt3.position.copy();
        ePos.x -= pos.x;
        ePos.y -= pos.y;
        elt3.position = ePos;
      }
    }
  }
};

/**
* @ignore
* @this {SvgParse}
* @param {GraphObject} gojsObj
* @param {Array.<string>} params
*/
SvgParse.prototype._transformSkewX = function(gojsObj, params) {
  var angle = parseFloat(params[0]);
  if (!isNaN(angle)) {
    angle = angle * Math.PI / 180;

    var pos = gojsObj.position.copy();
    if (isNaN(pos.x)) pos.x = 0;
    if (isNaN(pos.y)) pos.y = 0;

    if (gojsObj instanceof go.Shape) {
      var shape = /** @type {Shape} */ (gojsObj);
      var geo = shape.geometry.copy();

      if (geo.type === go.Geometry.Rectangle) {
        geo = this._turnRectangleIntoPath(geo);
      } else if (geo.type === go.Geometry.Ellipse) {
        geo = this._turnEllipseIntoPath(geo);
      } else if (geo.type === go.Geometry.Line) {
        geo.type = go.Geometry.Path;
        var fig = new go.PathFigure(geo.startX, geo.startY);
        var seg = new go.PathSegment(go.PathSegment.Line, geo.endX, geo.endY);
        fig.segments.add(seg);
        geo.figures.add(fig);
      }

      geo.offset(pos.x, pos.y);
      geo.transform(1, 0, Math.tan(angle), 1, -pos.x, -pos.y);
      // what about strokeWidth?

      var p = geo.normalize();
      shape.geometry = geo;
      pos.x -= p.x;
      pos.y -= p.y;
      shape.position = pos;
    } else if (gojsObj instanceof go.Panel) {
      var panel = /** @type {Panel} */ (gojsObj);
      var pElts = panel.elements.iterator;
      while (pElts.next()) {
        var elt1 = pElts.value;
        var ePos = elt1.position.copy();
        ePos.x += pos.x;
        ePos.y += pos.y;
        pElts.value.position = ePos;
      }
      pElts.reset();
      while (pElts.next()) {
        var elt2 = pElts.value;
        this._transformSkewX(elt2, params);
      }
      pElts.reset();
      while (pElts.next()) {
        var elt3 = pElts.value;
        var ePos = elt3.position.copy();
        ePos.x -= pos.x;
        ePos.y -= pos.y;
        elt3.position = ePos;
      }
    }
  }
};

/**
* @ignore
* @this {SvgParse}
* @param {GraphObject} gojsObj
* @param {Array.<string>} params
*/
SvgParse.prototype._transformSkewY = function(gojsObj, params) {
  var angle = parseFloat(params[0]);
  if (!isNaN(angle)) {
    angle = angle * Math.PI / 180;

    var pos = gojsObj.position.copy();
    if (isNaN(pos.x)) pos.x = 0;
    if (isNaN(pos.y)) pos.y = 0;

    if (gojsObj instanceof go.Shape) {
      var shape = /** @type {Shape} */ (gojsObj);
      var geo = shape.geometry.copy();

      if (geo.type === go.Geometry.Rectangle) {
        geo = this._turnRectangleIntoPath(geo);
      } else if (geo.type === go.Geometry.Ellipse) {
        geo = this._turnEllipseIntoPath(geo);
      } else if (geo.type === go.Geometry.Line) {
        geo.type = go.Geometry.Path;
        var fig = new go.PathFigure(geo.startX, geo.startY);
        var seg = new go.PathSegment(go.PathSegment.Line, geo.endX, geo.endY);
        fig.segments.add(seg);
        geo.figures.add(fig);
      }

      geo.offset(pos.x, pos.y);
      geo.transform(1, Math.tan(angle), 0, 1, -pos.x, -pos.y);

      // what about strokeWidth?

      var p = geo.normalize();
      shape.geometry = geo;
      pos.x -= p.x;
      pos.y -= p.y;
      shape.position = pos;
    } else if (gojsObj instanceof go.Panel) {
      var panel = /** @type {Panel} */ (gojsObj);
      var pElts = panel.elements.iterator;
      while (pElts.next()) {
        var elt1 = pElts.value;
        var ePos = elt1.position.copy();
        ePos.x += pos.x;
        ePos.y += pos.y;
        elt1.position = ePos;
      }
      pElts.reset();
      while (pElts.next()) {
        var elt2 = pElts.value;
        this._transformSkewY(elt2, params);
      }
      pElts.reset();
      while (pElts.next()) {
        var elt3 = pElts.value;
        var ePos = elt3.position.copy();
        ePos.x -= pos.x;
        ePos.y -= pos.y;
        elt3.position = ePos;
      }
    }
  }
};

/**
* @ignore
* @this {SvgParse}
* @param {SVGRectElement} xmlElt
* @return {Shape}
*/
SvgParse.prototype._handleRect = function(xmlElt) {
  var shape = new go.Shape();
  var w = parseFloat(xmlElt.getAttribute('width'));
  if (isNaN(w) || w < 0) return null;
  var h = parseFloat(xmlElt.getAttribute('height'));
  if (isNaN(h) || h < 0) return null;
  var x = parseFloat(xmlElt.getAttribute('x'));
  if (isNaN(x)) x = 0;
  var y = parseFloat(xmlElt.getAttribute('y'));
  if (isNaN(y)) y = 0;
  var rxstr = xmlElt.getAttribute('rx');
  var rystr = xmlElt.getAttribute('ry');
  var rx = parseFloat(rxstr);
  if (isNaN(rx) || rx < 0) rx = 0;
  var ry = parseFloat(rystr);
  if (isNaN(ry) || ry < 0) ry = 0;
  if (!(rxstr !== null && rxstr !== '') && (rystr !== null && rystr !== '')) {  // if only one specified, the other provides the same value
    rx = ry;
  } else if ((rxstr !== null && rxstr !== '') && !(rystr !== null && rystr !== '')) {
    ry = rx;
  }
  rx = Math.min(rx, w / 2);
  ry = Math.min(ry, h / 2);
  /** @ignore @type {Geometry} */ var geo;
  if (rx === 0 && ry === 0) {
    geo = new go.Geometry(go.Geometry.Rectangle);
    geo.startX = 0;
    geo.startY = 0;
    geo.endX = w;
    geo.endY = h;
  } else {
    var k = Geo.KAPPA / 2;  //???
    var context = Util.tempStreamGeometryContext();
    context.beginFigure(rx, 0, true);
    context.lineTo(w - rx, 0);
    context.bezierTo(w - rx * k, 0,
                      w, ry * k,
                      w, ry);
    context.lineTo(w, h - ry);
    context.bezierTo(w, h - ry * k,
                      w - rx * k, h,
                      w - rx, h);
    context.lineTo(rx, h);
    context.bezierTo(rx * k, h,
                      0, h - ry * k,
                      0, h - ry);
    context.lineTo(0, ry);
    context.bezierTo(0, ry * k,
                      rx * k, 0,
                      rx, 0);
    context.closeLast();
    geo = context.geo;
    Util.freeStreamGeometryContext(context);
  }
  shape.position = new go.Point(x, y);
  shape.geometry = geo;
  return shape;
};

/**
* @ignore
* @this {SvgParse}
* @param {SVGPathElement} xmlElt
* @return {Shape}
*/
SvgParse.prototype._handlePath = function(xmlElt) {
  var shape = new go.Shape();
  var data = xmlElt.getAttribute('d');
  if (typeof data === 'string') shape.geometryString = go.Geometry.fillPath(data);
  return shape;
};

/**
* @ignore
* @this {SvgParse}
* @param {SVGLineElement} xmlElt
* @return {Shape}
*/
SvgParse.prototype._handleLine = function(xmlElt) {  // change to position rather than startx & y?
  var shape = new go.Shape();
  var x1 = parseFloat(xmlElt.getAttribute('x1'));
  if (isNaN(x1)) x1 = 0;
  var y1 = parseFloat(xmlElt.getAttribute('y1'));
  if (isNaN(y1)) y1 = 0;
  var x2 = parseFloat(xmlElt.getAttribute('x2'));
  if (isNaN(x2)) x2 = 0;
  var y2 = parseFloat(xmlElt.getAttribute('y2'));
  if (isNaN(y2)) y2 = 0;
  var geo = new go.Geometry(go.Geometry.Line);
  shape.position = new go.Point(Math.min(x1, x2), Math.min(y1, y2));  // use minimum point, do this for other shapes
  if ((x2 - x1) / (y2 - y1) > 0) {
    geo.startX = 0;
    geo.startY = 0;
    geo.endX = Math.abs(x2 - x1);
    geo.endY = Math.abs(y2 - y1);
  } else {
    geo.startX = 0;
    geo.startY = Math.abs(y2 - y1);
    geo.endX = Math.abs(x2 - x1);
    geo.endY = 0;
  }
  shape.geometry = geo;
  return shape;
};

/**
* @ignore
* @this {SvgParse}
* @param {SVGCircleElement} xmlElt
* @return {Shape}
*/
SvgParse.prototype._handleCircle = function(xmlElt) {
  var shape = new go.Shape();
  var r = parseFloat(xmlElt.getAttribute('r'));
  if (isNaN(r) || r < 0) return null;
  var cx = parseFloat(xmlElt.getAttribute('cx'));
  if (isNaN(cx)) cx = 0;
  var cy = parseFloat(xmlElt.getAttribute('cy'));
  if (isNaN(cy)) cy = 0;
  var geo = new go.Geometry(go.Geometry.Ellipse);
  geo.startX = 0;
  geo.startY = 0;
  geo.endX = 2 * r;
  geo.endY = 2 * r;
  shape.position = new go.Point(cx - r, cy - r);
  shape.geometry = geo;
  return shape;
};

/**
* @ignore
* @this {SvgParse}
* @param {SVGEllipseElement} xmlElt
* @return {Shape}
*/
SvgParse.prototype._handleEllipse = function(xmlElt) {
  var shape = new go.Shape();
  var rx = parseFloat(xmlElt.getAttribute('rx'));
  if (isNaN(rx) || rx < 0) return null;
  var ry = parseFloat(xmlElt.getAttribute('ry'));
  if (isNaN(ry) || ry < 0) return null;
  var cx = parseFloat(xmlElt.getAttribute('cx'));
  if (isNaN(cx)) cx = 0;
  var cy = parseFloat(xmlElt.getAttribute('cy'));
  if (isNaN(cy)) cy = 0;
  var geo = new go.Geometry(go.Geometry.Ellipse);
  geo.startX = 0;
  geo.startY = 0;
  geo.endX = 2 * rx;
  geo.endY = 2 * ry;
  shape.position = new go.Point(cx - rx, cy - ry);
  shape.geometry = geo;
  return shape;
};

/**
* @ignore
* @this {SvgParse}
* @param {SVGPolygonElement|SVGPolylineElement} xmlElt
* @return {Shape}
*/
SvgParse.prototype._handlePolyline = function(xmlElt) { // should this change to include a position?
  var close = false;
  if (xmlElt.tagName === 'polygon') close = true;
  else if (xmlElt.tagName !== 'polyline') return null;
  var shape = new go.Shape();
  var points = xmlElt.getAttribute('points');
  var geom = new go.Geometry;
  var figureList = new go.List(go.PathFigure);
  var pointValues = points.split(/\s*[\s,]\s*/);
  if (pointValues.length < 4) return null;
  /** @ignore @type {PathFigure} */ var figure = null;
  var segs = new go.List(go.PathSegment);
  for (var i = 1; i < pointValues.length; i += 2) {
    var xVal = parseFloat(pointValues[i - 1]);
    var yVal = parseFloat(pointValues[i]);
    if (typeof xVal !== 'number' || isNaN(xVal) || typeof yVal !== 'number' || isNaN(yVal)) return null;
    if (i === 1) figure = new go.PathFigure(xVal, yVal);
    else segs.add(new go.PathSegment(go.PathSegment.Line, xVal, yVal));
  }
  if (close) {
    var endSeg = new go.PathSegment(go.PathSegment.Line, figure.startX, figure.startY);
    endSeg.close();
    segs.add(endSeg);
  }
  figure.segments = segs;
  figureList.add(figure);
  geom.figures = figureList;
  var p = geom.normalize();
  shape.position = new go.Point(-p.x, -p.y);
  shape.geometry = geom;
  return shape;
};

/**
* @ignore
* @this {SvgParse}
* @param {Geometry} geom
* @return {Geometry}
*/
SvgParse.prototype._turnEllipseIntoPath = function(geom) {
  var x1 = geom.startX;
  var y1 = geom.startY;
  var x2 = geom.endX;
  var y2 = geom.endY;

  var rx = Math.abs(x2 - x1) / 2;
  var ry = Math.abs(y2 - y1) / 2;
  var cx = Math.min(x1, x2) + rx;
  var cy = Math.min(y1, y2) + ry;

  var temp = new go.Point();

  var f = new go.PathFigure(cx, cy - ry);

  var p1 = new go.PathSegment(go.PathSegment.Bezier);
  p1.point1X = cx + (Geo.KAPPA * rx);
  p1.point1Y = cy - ry;
  p1.point2X = cx + rx;
  p1.point2Y = cy - (Geo.KAPPA * ry);
  p1.endX = cx + rx;
  p1.endY = cy;
  f.segments.add(p1);

  var p2 = new go.PathSegment(go.PathSegment.Bezier);
  p2.point1X = cx + rx;
  p2.point1Y = cy + (Geo.KAPPA * ry);
  p2.point2X = cx + (Geo.KAPPA * rx);
  p2.point2Y = cy + ry;
  p2.endX = cx;
  p2.endY = cy + ry;
  f.segments.add(p2);

  var p3 = new go.PathSegment(go.PathSegment.Bezier);
  p3.point1X = cx - (Geo.KAPPA * rx);
  p3.point1Y = cy + ry;
  p3.point2X = cx - rx;
  p3.point2Y = cy + (Geo.KAPPA * ry);
  p3.endX = cx - rx;
  p3.endY = cy;
  f.segments.add(p3);

  var p4 = new go.PathSegment(go.PathSegment.Bezier);
  p4.point1X = cx - rx;
  p4.point1Y = cy - (Geo.KAPPA * ry);
  p4.point2X = cx - (Geo.KAPPA * rx);
  p4.point2Y = cy - ry;
  p4.endX = cx;
  p4.endY = cy - ry;
  f.segments.add(p4);

  geom.type = go.Geometry.Path;
  geom.figures.add(f);
  return geom;
};

/**
* @ignore
* @this {SvgParse}
* @param {Geometry} geom
* @return {Geometry}
*/
SvgParse.prototype._turnRectangleIntoPath = function(geom) {
  var x1 = geom.startX;
  var y1 = geom.startY;
  var x2 = geom.endX;
  var y2 = geom.endY;

  var x = Math.min(x1, x2);
  var y = Math.min(y1, y2);
  var w = Math.abs(x2 - x1);
  var h = Math.abs(y2 - y1);

  var fig = new go.PathFigure(x, y);

  fig.segments.add(new go.PathSegment(go.PathSegment.Line, x + w, y));
  fig.segments.add(new go.PathSegment(go.PathSegment.Line, x + w, y + h));
  fig.segments.add(new go.PathSegment(go.PathSegment.Line, x, y + h).close());

  geom.type = go.Geometry.Path;
  geom.figures.add(fig);
  return geom;
};

// be sure that all methods are named starting with an underscore

Thank you. I’ll take a look at that article (or “book”, as he calls it) you linked to about SVG parsing. It looks very interesting. Also, thanks for posting that parsing code. It might spark some ideas for a diffferent way we can accomplish what we need. I greatly appreciate your time, expertise, and insight!