Draft script: importing a JSON file as a nested diagram

A very draft and illustrative script which:

  • Throws up a file choosing dialog (for choosing a .json text)
  • Creates a new OG6 Professional document, using the hierarchical template
  • Makes a basic nested diagram

For example, opening a file containing:

{
    "alpha": {
        "delta": 1,
        "epsilon": 2,
        "zeta": 3
    },
    "beta": {
        "delta": 1,
        "epsilon": 2,
        "zeta": 3
    },
    "gamma": {
        "delta": 1,
        "epsilon": 2,
        "zeta": 3
    }
}

Generates a diagram like:

(which could be restyled with a different template, reorganised with Automatic Layout, etc)

Draft source (JavaScript for Automation)

//Ver 0.06c

// Extended range of JSON that can be diagrammed
// complete now ? 
//Import a JSON file to a nested OG6 diagram
// (JavaScript for Automation)
// Run from Script editor with language selected at top left set to JavaScript
// 1. Choose a .json from the file dialog which appears
// 2. Confirm

// Draws a nested diagram using a specified:
// - Template
// - Width and height for node shapes

// Select alternative template name and/or shape size in last line of script
(function (strTemplate, lstSize) {
    'use strict';


    // CONVERT ARBITRARY JSON TO A SIMPLE {text:String, nest:Array} tree
    // a -> [s, [b, c]]
    function jsonTree(a) {
        
        return isAtom(a) ? {
                text: a !== null ? a.toString() : 'null'
            } : Object.keys(a)
            .reverse()
            .map(function (k) {
                var subTree = jsonTree(a[k]);
                return {
                    text: k,
                    nest: (isArray(subTree) ? subTree : [subTree])
                };
            });
    }

    // [tree] -> tree
    function rootedTree(lstTree) {
        return lstTree.length > 1 ? [{
            text: "",
            nest: lstTree
        }] : lstTree;
    }

    // a -> Bool
    function isAtom(a) {
        return -1 !== [
			'boolean', 'undefined', 'number', 'string'
		].indexOf(typeof a) || null === a;
    }


    // a -> Bool
    function isArray(a) {
        return a instanceof Array;
    }

    // for type strings see: Apple's 'System-Declared Uniform Type Identifiers'
    // if strType is omitted, files of all types will be selectable
    // String -> String -> String
    function pathChoice(strPrompt, strType) {
        var a = Application.currentApplication();

        return (a.includeStandardAdditions = true, a)
            .chooseFile({
                withPrompt: strPrompt,
                ofType: strType
            })
            .toString();
    }

    // String -> String
    function textFileContents(strPath) {
        return $.NSString.stringWithContentsOfFile(strPath);
    }


    // [{}] -> String -> (Integer, Integer) -> OmniGraffle6Doc
    function jsoToOG6(lstJSO, strTemplate, lstSize) {
        function drawTree(graphics, shp, idShape, lstChiln) {

            lstChiln.forEach(function (oNode) {
                var shpChild = og.Shape({
                        text: oNode.text,
                        size: lstSize //,
                            //notes: oNode.note
                    }),
                    lstNest = oNode.nest || []; //
                //dctTags = oNode.tags;

                // add shape to canvas graphics
                graphics.push(shpChild);

                var idChild = shpChild.id();

                // record link for later creation through AppleScript kludge
                // (see connect bug below)
                lstLinks.push([idShape, idChild]);

                // BUG: It seems that the connect command may be broken in the JS interface
                //      (trips a type error)
                // og.connect(shp(), {
                //  to: child()
                // });

                if (lstNest && lstNest.length) {
                    drawTree(graphics, shpChild, idChild,
                        lstNest);
                }

                //if (dctTags) addData(shpChild, dctTags);

            });
        };

        //         function addData(shp, dctUser) {
        //             var userData = shp.userDataItems;
        // 
        //             Object.keys(dctUser).forEach(function (k) {
        //                 userData[k].value = dctUser[k];
        //             });
        //         }



        if (lstJSO.length > 0) {

            var og = Application("OmniGraffle"),
                a = Application.currentApplication(),
                sa = (a.includeStandardAdditions = true, a),
                blnTemplate = og.availableTemplates()
                .map(function (x) {
                    return x.split("/")[1];
                })
                .indexOf(strTemplate) !== -1;


            // Template found ?
            if (!blnTemplate) {
                sa.activate();
                sa.displayDialog('Template not found:\n\n\t"' +
                    strTemplate +
                    '\"', {
                        withTitle: "Import JSON into OmniGraffle"
                    });
                return;
            }

            var ds = og.documents,
                d = ((function () {
                    return ds.push(
                        og.Document({
                            template: strTemplate
                        })
                    )
                })() && ds[0]),
                cnv = d ? d.canvases.at(0) : undefined,
                oLayout = cnv ? cnv.layoutInfo() : undefined;


            if (oLayout) {
                var graphics = cnv.graphics,
                    layer = d.layers.at(0),
                    lstLinks = [];

                // suspended during drawing
                oLayout.automaticLayout = false;
                if (graphics.length) og.delete(graphics);

                lstJSO.forEach(function (oNode) {
                    var shp = og.Shape({
                            text: oNode.text,
                            size: lstSize,
                            notes: oNode.note
                        }),
                        lstChiln = oNode.nest,
                        dctTags = oNode.tags;

                    graphics.push(shp);

                    if (lstChiln) {
                        drawTree(graphics, shp, shp.id(),
                            lstChiln);
                    }

                    if (dctTags) addData(shp, dctTags);
                });

                // FALL BACK TO APPLESCRIPT KLUDGE EMBEDDED IN JS TO CREATE PARENT-CHILD LINKS BETWEEN SHAPES
                // (The JavaScript interface to doing this appears to be broken at the moment)
                sa.doShellScript(
                    "osascript <<AS_END 2>/dev/null\ntell application \"OmniGraffle\" to tell front canvas of front document\n" +
                    "set recP to {line type: straight}\n" +
                    lstLinks.map(function (pair) {
                        return 'connect shape id ' + pair[0] +
                            ' to shape id ' + pair[1] //+
                            //' with properties recP';
                    })
                    .join('\n') + "\nend tell\nAS_END\n"
                );

                // restored
                oLayout.automaticLayout = true;
                cnv.layout(graphics);
                og.activate();

            }
        }
    }


    // MAIN

    jsoToOG6( // Create a basic diagram with specified template and node size
        rootedTree( // Supply a single virtual root if trees are multiple
            jsonTree( // Normalise JSO to diagrammable {text:strText, nest:lstChildren} tree
                JSON.parse(
                    textFileContents(
                        pathChoice(
                            'JSON file:',
                            'public.text'
                        )
                    )
                    .js
                )
            )
        ),
        strTemplate, lstSize
    );

})('Hierarchical', [80, 80]);

1 Like

Ver 0.6 above should now be able to import (and diagram) any JSON, I think …

(tell me if you find a .json file with a pattern which flummoxes it)

Scale is another matter – I haven’t tested it with huge .json files

Now diagrams arrays in the JSON by using their indices as numeric key strings, with values as child nodes. Other objects, as before, use keys as parents and values as children.

1 Like

Hi, The script doesn’t handle null values in the json array. get error Error on line 25: TypeError: null is not an object (evaluating ‘a.toString’)

Thanks !

I’ll take a look on Monday.

(in the meanwhile I’ve done a quick/naive/untested fix in the script above - ver c)

Do post some sample JSON if you have any that could be used for a test.

Yes that did the trick. Thanks!

1 Like

Will it work with OG 7?

Good question – there seems to have been a change in some aspect of template handling or location. I was able to generate a diagram just now, but only by bypassing the template component.

JS Source
//Ver 0.06c

// Extended range of JSON that can be diagrammed
// complete now ? 
//Import a JSON file to a nested OG6 diagram
// (JavaScript for Automation)
// Run from Script editor with language selected at top left set to JavaScript
// 1. Choose a .json from the file dialog which appears
// 2. Confirm

// Draws a nested diagram using a specified:
// - Template
// - Width and height for node shapes

// Select alternative template name and/or shape size in last line of script
(function (strTemplate, lstSize) {
    'use strict';


    // CONVERT ARBITRARY JSON TO A SIMPLE {text:String, nest:Array} tree
    // a -> [s, [b, c]]
    function jsonTree(a) {
        
        return isAtom(a) ? {
                text: a !== null ? a.toString() : 'null'
            } : Object.keys(a)
            .reverse()
            .map(function (k) {
                var subTree = jsonTree(a[k]);
                return {
                    text: k,
                    nest: (isArray(subTree) ? subTree : [subTree])
                };
            });
    }

    // [tree] -> tree
    function rootedTree(lstTree) {
        return lstTree.length > 1 ? [{
            text: "",
            nest: lstTree
        }] : lstTree;
    }

    // a -> Bool
    function isAtom(a) {
        return -1 !== [
			'boolean', 'undefined', 'number', 'string'
		].indexOf(typeof a) || null === a;
    }


    // a -> Bool
    function isArray(a) {
        return a instanceof Array;
    }

    // for type strings see: Apple's 'System-Declared Uniform Type Identifiers'
    // if strType is omitted, files of all types will be selectable
    // String -> String -> String
    function pathChoice(strPrompt, strType) {
        var a = Application.currentApplication();

        return (a.includeStandardAdditions = true, a)
            .chooseFile({
                withPrompt: strPrompt,
                ofType: strType
            })
            .toString();
    }

    // String -> String
    function textFileContents(strPath) {
        return $.NSString.stringWithContentsOfFile(strPath);
    }


    // [{}] -> String -> (Integer, Integer) -> OmniGraffle6Doc
    function jsoToOG6(lstJSO, strTemplate, lstSize) {
        function drawTree(graphics, shp, idShape, lstChiln) {

            lstChiln.forEach(function (oNode) {
                var shpChild = og.Shape({
                        text: oNode.text,
                        size: lstSize //,
                            //notes: oNode.note
                    }),
                    lstNest = oNode.nest || []; //
                //dctTags = oNode.tags;

                // add shape to canvas graphics
                graphics.push(shpChild);

                var idChild = shpChild.id();

                // record link for later creation through AppleScript kludge
                // (see connect bug below)
                lstLinks.push([idShape, idChild]);

                // BUG: It seems that the connect command may be broken in the JS interface
                //      (trips a type error)
                // og.connect(shp(), {
                //  to: child()
                // });

                if (lstNest && lstNest.length) {
                    drawTree(graphics, shpChild, idChild,
                        lstNest);
                }

                //if (dctTags) addData(shpChild, dctTags);

            });
        };

        //         function addData(shp, dctUser) {
        //             var userData = shp.userDataItems;
        // 
        //             Object.keys(dctUser).forEach(function (k) {
        //                 userData[k].value = dctUser[k];
        //             });
        //         }



        if (lstJSO.length > 0) {

            var og = Application("OmniGraffle"),
                a = Application.currentApplication(),
                sa = (a.includeStandardAdditions = true, a),
                blnTemplate = og.availableTemplates()
                .map(function (x) {
                    return x.split("/")[1];
                })
                .indexOf(strTemplate) !== -1;


            // Template found ?
            // if (!blnTemplate) {
            //     sa.activate();
            //     sa.displayDialog('Template not found:\n\n\t"' +
            //         strTemplate +
            //         '\"', {
            //             withTitle: "Import JSON into OmniGraffle"
            //         });
            //     return;
            // }

            var ds = og.documents,
                d = ((function () {
                    return ds.push(
                        og.Document()
                    )
                })() && ds[0]),
                cnv = d ? d.canvases.at(0) : undefined,
                oLayout = cnv ? cnv.layoutInfo() : undefined;


            if (oLayout) {
                var graphics = cnv.graphics,
                    layer = d.layers.at(0),
                    lstLinks = [];

                // suspended during drawing
                oLayout.automaticLayout = false;
                if (graphics.length) og.delete(graphics);

                lstJSO.forEach(function (oNode) {
                    var shp = og.Shape({
                            text: oNode.text,
                            size: lstSize,
                            notes: oNode.note
                        }),
                        lstChiln = oNode.nest,
                        dctTags = oNode.tags;

                    graphics.push(shp);

                    if (lstChiln) {
                        drawTree(graphics, shp, shp.id(),
                            lstChiln);
                    }

                    if (dctTags) addData(shp, dctTags);
                });

                // FALL BACK TO APPLESCRIPT KLUDGE EMBEDDED IN JS TO CREATE PARENT-CHILD LINKS BETWEEN SHAPES
                // (The JavaScript interface to doing this appears to be broken at the moment)
                sa.doShellScript(
                    "osascript <<AS_END 2>/dev/null\ntell application \"OmniGraffle\" to tell front canvas of front document\n" +
                    "set recP to {line type: straight}\n" +
                    lstLinks.map(function (pair) {
                        return 'connect shape id ' + pair[0] +
                            ' to shape id ' + pair[1] //+
                            //' with properties recP';
                    })
                    .join('\n') + "\nend tell\nAS_END\n"
                );

                // restored
                oLayout.automaticLayout = true;
                cnv.layout(graphics);
                og.activate();

            }
        }
    }


    // MAIN

    jsoToOG6( // Create a basic diagram with specified template and node size
        rootedTree( // Supply a single virtual root if trees are multiple
            jsonTree( // Normalise JSO to diagrammable {text:strText, nest:lstChildren} tree
                JSON.parse(
                    textFileContents(
                        pathChoice(
                            'JSON file:',
                            'public.text'
                        )
                    )
                    .js
                )
            )
        ),
        strTemplate, lstSize
    );

})('', [80, 80]);

I think what I might do, perhaps over the weekend, is to rewrite it in terms of the newer omniJS interface.

(If you have a sample of the kind of JSON you would like me to test it with, do post it here, or via DM)

Will it work with OG 7?

I’ve sketched a first draft of an omniJS version – what would be a typical workflow for you ?

Do you think it would make more sense as:

  • paste as OG diagram from JSON in clipboard,
  • or as open JSON file as OG diagram ?

UPDATE:

See: