GoJs- SVG content separation into gojs shapes or gojs model

We have a JSON-formatted file containing SVG content. During the import and editing process, I want to split the SVG elements into separate GoJS shapes or models. This will allow me to perform individual actions, such as resizing, repositioning, changing the color, or deleting each layer node separately. Once the editing is done, I need to export the content back in the same SVG format.

For example, the SVG files below contain paths and circles:

<svg width="210" height="215" viewBox`="0 0 210 215" fill="none" xmlns="http://www.w3.org/2000/svg">`
  <path d="M130.461 164.079C130.461 179.16 118.541 191.283 103.964 191.283C89.3867 191.283 77.4673 179.16 77.4673 164.079C77.4673 148.997 89.3867 136.875 103.964 136.875C118.541 136.875 130.461 148.997 130.461 164.079Z" stroke="black" stroke-width="5"/>
  <path d="M83.611 123.806L77.7959 154.178L88.263 141.354L103.964 135.955L119.083 141.354L130.131 152.153L104.545 21.2173L83.611 123.806Z" fill="black" stroke="black"/>
  <path d="M206.842 107.5C206.842 165.553 161.036 212.5 104.671 212.5C48.306 212.5 2.5 165.553 2.5 107.5C2.5 49.4468 48.306 2.5 104.671 2.5C161.036 2.5 206.842 49.4468 206.842 107.5Z" stroke="black" stroke-width="5"/>
</svg>
<svg width="82" height="82" viewBox="0 0 82 82" fill="none" xmlns="http://www.w3.org/2000/svg">
  <circle cx="41" cy="41" r="39" stroke="black" stroke-opacity="0.87" stroke-width="4"/>
  <path d="M10 41.4983C15.1667 34.6649 25.7534 26.2502 41 41.4983C59 59.5 70 41.4983 71.5 41.4983" stroke="black" stroke-width="4"/>
</svg>

or these SVGs, I need to separate each <circle> and <path> as individual GoJS shapes so that I can manipulate them independently and export them in the same format. Since GoJS does not support this type of export directly, is there an alternative solution or workaround?

I tried something, I separated the layer but not able to fit in orginal position.

Could you please edit your post so that we can see the original JSON file and all of the SVG?

Hi Walter,

Thanks for the quick response…

I have edited the post… Hope now you can see the svg content. In that post I have given 2 example svg… There are more svg which is dynamic but all svgs are same format only.

Cheers,
MukeshKannan

OK, try this. EDIT: fixed rounded rectangles – cannot use the “RoundedRectangle” figure generator.

<!DOCTYPE html>
<html>
<head>
  <title>Simple SVG editor</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  <button id="mySaveButton">Show and Save SVG</button><button id="myLoadButton">Load SVG</button>
  <div id="mySvg"></div>
  <textarea id="mySavedSvg" style="width:100%;height:250px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const myDiagram =
  new go.Diagram("myDiagramDiv", {
      "animationManager.isInitial": false,
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  new go.Part({
      locationSpot: go.Spot.Center,
      resizable: true, resizeObjectName: "SHAPE",
      rotatable: true, rotationSpot: go.Spot.Center
    })
    .bindTwoWay("location", "location", go.Point.parse, go.Point.stringifyFixed(1))
    .bindTwoWay("angle")
    .add(
      new go.Shape({ name: "SHAPE" })
        .bind("stroke")
        .bind("strokeWidth")
        .bind("fill")
        .bind("figure")  // only for rectangles or ellipses: "Rectangle" or "RR" or "Ellipse" or "Circle"
        .bind("parameter1")  // only for rounded rectangles
        .bind("parameter2")  // only for rounded rectangles
        .bind("geometryString", "d")  // for all other geometries
        .bindTwoWay("desiredSize", "size", go.Size.parse, go.Size.stringifyFixed(3))
        )

// Cannot use the "RoundedRectangle" predefined figure, because an SVG <rect> with corners is different
function svgRoundedRectGen(shp, w, h) {
  let param1 = shp ? shp.parameter1 : 10;
  if (isNaN(param1)) param1 = 10;
  let param2 = shp ? shp.parameter2 : 10;
  if (isNaN(param2)) param2 = 10;
  const rx = Math.min(param1, w/2);  // limit corner
  const ry = Math.min(param2, h/2);  // limit corner
  const offx = rx * (1 - 4 * ((Math.sqrt(2) - 1) / 3));
  const offy = ry * (1 - 4 * ((Math.sqrt(2) - 1) / 3));
  const fig = new go.PathFigure(rx, 0, true);
  if (rx < w/2) fig.add(new go.PathSegment(go.SegmentType.Line, w - rx, 0));
  fig.add(new go.PathSegment(go.SegmentType.Bezier, w, ry, w - offx, 0, w, offy));
  if (ry < h/2) fig.add(new go.PathSegment(go.SegmentType.Line, w, h - ry));
  fig.add(new go.PathSegment(go.SegmentType.Bezier, w - rx, h, w, h - offy, w - offx, h));
  if (rx < w/2) fig.add(new go.PathSegment(go.SegmentType.Line, rx, h));
  fig.add(new go.PathSegment(go.SegmentType.Bezier, 0, h - ry, offx, h, 0, h - offy));
  if (ry < h/2) fig.add(new go.PathSegment(go.SegmentType.Line, 0, ry));
  fig.add(new go.PathSegment(go.SegmentType.Bezier, rx, 0, 0, offy, offx, 0).close());
  return new go.Geometry().add(fig);
}
go.Shape.defineFigureGenerator("RR", svgRoundedRectGen);

// This only handles simple SVG shapes as immediate children of the <svg> element.
// It ignores <g> elements and any transforms.
function load(svgstr) {
  const safeParse = (str, def) => {
    const val = parseFloat(str);
    if (isNaN(val)) return def !== undefined ? def : 0;
    return val;
  };

  // add each SVG shape as a separate GoJS Part holding a GoJS Shape
  const xmldoc = new DOMParser().parseFromString(svgstr, 'text/xml');
  const nodeDataArray = [];
  const roundedShape = new go.Shape("RR");
  for (const elt of xmldoc.children[0].children) {
    // represent each SVG path by a Shape with Geometry type Path with its own fill and stroke
    const nodedata = {};

    const id = elt.getAttribute('id');
    if (typeof id === 'string') nodedata.key = id;

    const tag = elt.tagName.toLowerCase();
    nodedata.tag = tag;

    let stroke = elt.getAttribute('stroke');
    if (typeof stroke !== 'string' || stroke === 'none') stroke = null;
    nodedata.stroke = stroke;

    const sw = safeParse(elt.getAttribute('stroke-width'), stroke ? 1 : 0);
    nodedata.strokeWidth = sw;

    let fill = elt.getAttribute('fill');
    if (typeof fill !== 'string' || fill === 'none') fill = null;
    nodedata.fill = fill;

    let geo = null;
    let filled = true;
    switch (tag) {
      case "path": {
        // convert the path data string into a go.Geometry
        const pdata = elt.getAttribute('d');
        if (typeof pdata === 'string') {
          geo = go.Geometry.parse(pdata, true);  // filled
        }
        break;
      }
      case "rect": {
        const x = safeParse(elt.getAttribute('x'), 0);
        const y = safeParse(elt.getAttribute('y'), 0);
        const w = safeParse(elt.getAttribute('width'), 10);
        const h = safeParse(elt.getAttribute('height'), 10);
        let rx = safeParse(elt.getAttribute('rx'), 0);
        let ry = safeParse(elt.getAttribute('ry'), 0);
        if (rx > 0 || ry > 0) {
          if (rx === 0) rx = ry;
          else if (ry === 0) ry = rx;
          nodedata.location = go.Point.stringify(new go.Point(x + w/2, y + h/2));
          nodedata.size = go.Size.stringify(new go.Size(w, h));
          nodedata.figure = "RR";
          nodedata.parameter1 = rx;
          nodedata.parameter2 = ry;
        } else {
          nodedata.location = go.Point.stringify(new go.Point(x + w/2, y + h/2));
          nodedata.size = go.Size.stringify(new go.Size(w, h));
          nodedata.figure = "Rectangle";
        }
        break;
      }
      case "ellipse": {
        const cx = safeParse(elt.getAttribute('cx'), 0);
        const cy = safeParse(elt.getAttribute('cy'), 0);
        const rx = safeParse(elt.getAttribute('rx'), 10);
        const ry = safeParse(elt.getAttribute('ry'), 10);
        nodedata.location = go.Point.stringify(new go.Point(cx - rx, cy - ry));
        nodedata.size = go.Size.stringify(new go.Size(2*rx, 2*ry));
        nodedata.figure = "Ellipse";
        break;
      }
      case "circle": {
        const cx = safeParse(elt.getAttribute('cx'), 0);
        const cy = safeParse(elt.getAttribute('cy'), 0);
        const r = safeParse(elt.getAttribute('r'), 10);
        nodedata.location = go.Point.stringify(new go.Point(cx - r, cy - r));
        nodedata.size = go.Size.stringify(new go.Size(2*r, 2*r));
        nodedata.figure = "Circle";
        break;
      }
      case "line": {
        filled = false;
        const x1 = safeParse(elt.getAttribute('x1'), 0);
        const y1 = safeParse(elt.getAttribute('y1'), 0);
        const x2 = safeParse(elt.getAttribute('x2'), 10);
        const y2 = safeParse(elt.getAttribute('y2'), 10);
        geo = new go.Geometry(go.GeometryType.Line);
        geo.startX = x1;
        geo.startY = y1;
        geo.endX = x2;
        geo.endY = y2;
        break;
      }
      case "polyline":
      case "polygon": {
        const pdata = elt.getAttribute('points');
        if (typeof pdata === 'string') {
          const a = pdata.split(/[\s,]/).filter(s => !!s);  //??? is this parsing incomplete?
          const fig = new go.PathFigure();
          if (a.length >= 2) {
            fig.startX = safeParse(a[0], 0);
            fig.startY = safeParse(a[1], 0);
            if (a.length > 3) {
              for (let i = 2; i < a.length; i += 2) {
                const seg = new go.PathSegment(go.SegmentType.Line, safeParse(a[i], 0), safeParse(a[i+1], 0));
                fig.add(seg);
              }
            }
            geo = new go.Geometry().add(fig);
          }
          if (tag === "polygon") {
            const seg = fig.segments.last();
            if (seg) seg.close();
          } else {
            filled = false;
          }
        }
        break;
      }
      default:  // ignore any elements that are not SVG shapes, especially <g>
        break;
    }
    if (geo) {
      const offset = geo.normalize();
      nodedata.location = go.Point.stringify(new go.Point(-offset.x + geo.bounds.width/2, -offset.y + geo.bounds.height/2));
      const pathstr = go.Geometry.stringifyFixed(3)(geo);
      nodedata.d = filled ? go.Geometry.fillPath(pathstr) : pathstr;
    }

    nodeDataArray.push(nodedata);
  }
  myDiagram.model = new go.Model(nodeDataArray);
}

function save() {
  const NS = "http://www.w3.org/2000/svg";
  const svgnode = document.createElementNS(NS, "svg");
  const db = myDiagram.documentBounds;
  svgnode.setAttribute("width", db.width);
  svgnode.setAttribute("height", db.height);
  svgnode.setAttribute("viewBox", `${db.x} ${db.y} ${db.width} ${db.height}`);
  svgnode.setAttribute("fill", "none");  // default no fill
  myDiagram.parts.each(part => {
    const data = part.data;
    const elt = document.createElementNS(NS, "path");
    if (data.stroke !== undefined && data.stroke !== null) elt.setAttribute("stroke", data.stroke);
    elt.setAttribute("stroke-width", data.strokeWidth || 0);
    if (data.fill !== undefined && data.fill !== null) elt.setAttribute("fill", data.fill);
    const shape = part.findObject("SHAPE");
    const geo = shape.geometry.copy();
    const gb = geo.bounds.copy();
    geo.rotate(part.angle, gb.width/2, gb.height/2);
    geo.offset(part.location.x - gb.width/2, part.location.y - gb.height/2);
    let pathstr = geo.toString();
    if (pathstr[0] === "F") pathstr = pathstr.substring(2);
    elt.setAttribute("d", pathstr);
    svgnode.appendChild(elt);
  });
  const str = new XMLSerializer().serializeToString(svgnode);
  document.getElementById("mySvg").innerHTML = str;
  document.getElementById("mySavedSvg").value = str;
  return str;
}

load(`<svg width="473" height="295" viewBox="-5 -75 473 295" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M130.461 164.079C130.461 179.16 118.541 191.283 103.964 191.283C89.3867 191.283 77.4673 179.16 77.4673 164.079C77.4673 148.997 89.3867 136.875 103.964 136.875C118.541 136.875 130.461 148.997 130.461 164.079Z" stroke="black" stroke-width="5"/>
  <path d="M83.611 123.806L77.7959 154.178L88.263 141.354L103.964 135.955L119.083 141.354L130.131 152.153L104.545 21.2173L83.611 123.806Z" fill="black" stroke="black"/>
  <path d="M206.842 107.5C206.842 165.553 161.036 212.5 104.671 212.5C48.306 212.5 2.5 165.553 2.5 107.5C2.5 49.4468 48.306 2.5 104.671 2.5C161.036 2.5 206.842 49.4468 206.842 107.5Z" stroke="black" stroke-width="5"/>
  <rect x="23" y="14" width="40" height="60" fill="green" />
  <ellipse cx="200" cy="0" rx="25" ry="35" fill="lightblue" />
  <circle cx="300" cy="0" r="25" stroke="orange" stroke-width="4" />
  <line x1="170" y1="75" x2="260" y2="105" stroke="blue" stroke-width="3" />
  <polyline points="250 150 280 160 250 170 280 180" stroke="green" stroke-width="3" />
  <polygon points="350,150 380,160 350,170 380,180" stroke="green" fill="lightgreen" />
  <rect x="350" y="0" width="20" height="20" stroke="black" stroke-width="1" />
  <rect x="380" y="0" width="20" height="20" stroke="black" stroke-width="6" />
  <rect x="410" y="0" width="20" rx="10" height="20" stroke="black" stroke-width="1" />
  <rect x="440" y="0" width="20" rx="10" height="20" stroke="black" stroke-width="6" />
  <rect x="280" y="50" width="120" height="80" rx="40" ry="20" stroke="blue" stroke-width="3" fill="transparent" />
</svg>`)

document.getElementById("mySaveButton").addEventListener("click", save);

document.getElementById("myLoadButton").addEventListener("click", () => {
  const svgstr = document.getElementById("mySavedSvg").value;
  if (svgstr) load(svgstr);
});
  </script>
</body>
</html>

I was trying really hard to resolve since one week and thanks for sharing the code. The prompt working fine. I’ll redefine the code for anglur and let you know if any problems pops up.

Thanks a million and I appreciate your help.

MukeshKannan

Hi Walter,

I have downloaded the svg diagram and extracted the svg content from downloaded svg. I replaced the svg element in html file’s svg… But svg is not getting load.

could you please try from your side?

Thanks

Mukeshkannan

Just use the show function. I have updated the code above.

Thanks Walter… I want export functionality to export svg in the particular format which I have give in above… I’ll try to write a custom export.

I tried show function… There is small postion change as you can see in the below screenshot…

image

Thanks

MukeshKannan

I’ve fixed that error dealing with strokeWidth and had to reimplement rounded rectangles, since the SVG <rect> when there are rounded corners is quite different from the “RoundedRectangle” figure in GoJS.

I have updated the code above.

Thanks Walter… It is working fine…

Mukeshkannan