Monday, July 1, 2013

Applescript to draw a taxonomy from a JSON source.

This script outputs the exact same chart as the one in another post, but it takes a JSON source instead of the outline source. This is useful only if you have an automated way to create JSON. If you plan to hand-type it, you'd be better off with the script in this other post instead.

Here is sample output from this script. You can customize the appearance by changing the properties at the top of the script that start with "SETTINGS_":


The script expects a JSON file where each JSON object represents a node. Each object has (up to) two properties: "itemName" which is the label of the node, and (if the node has children)  "itemChildren" which contains the children of the node. Child nodes can have children of their own.

The script expects the file to be on your Desktop, and by default it needs to be named "drawtree.json" but you can change that in the settings at the top of the script.\

Here is the JSON that produces the chart above:

[
  {
    "itemName": "Rock/Pop",
    "itemChildren":
        [
      {"itemName": "Roots",
        "itemChildren":
        [
          {"itemName": "Cajun/Zydeco"},
          {"itemName": "Tex Mex"},
          {"itemName": "Swamp Pop"}
        ]
      },
      {"itemName": "Singer-Songwriter"},
      {"itemName": "Classic Rock",
        "itemChildren":
        [
          {"itemName": "AOR"}
        ]
      },
      {"itemName": "Art & Progressive Rock",
        "itemChildren":
        [
          {"itemName": "Rock Opera"},
          {"itemName": "New Prog"},
          {"itemName": "Krautrock"}
        ]
      },
      {"itemName": "Blues & Boogie Rock",
        "itemChildren":
        [
          {"itemName": "Boogie Rock"},
          {"itemName": "Southern Rock"}
        ]
      },
      {"itemName": "Glam"},
      {"itemName": "Hard Rock",
        "itemChildren":
        [
          {"itemName": "Post-Grunge"},
          {"itemName": "Instrumental Guitar Rock"},
          {"itemName": "Acid Rock"}
        ]
      },
      {"itemName": "Instrumental Rock"},
      {"itemName": "Pop",
        "itemChildren":
        [
          {"itemName": "Dance Pop"},
          {"itemName": "Teen Beat"},
          {"itemName": "Teen Idols"}
        ]
      },
      {"itemName": "Jam Rock"},
      {"itemName": "Latin Pop"},
      {"itemName": "Country Rock"},
      {"itemName": "Funk Rock"},
      {"itemName": "Metal",
        "itemChildren":
        [
          {"itemName": "Progressive Metal"},
          {"itemName": "Thrash/Speed Metal"},
          {"itemName": "Stoner Rock"},
          {"itemName": "Grindcore"},
          {"itemName": "Doom Metal"},
          {"itemName": "Pop Metal"},
          {"itemName": "New Wave of British Heavy Metal"},
          {"itemName": "Black Metal"},
          {"itemName": "Death Metal"},
          {"itemName": "Funk Metal"},
          {"itemName": "Industrial Metal"},
          {"itemName": "Christian Metal"},
          {"itemName": "Rapcore"},
          {"itemName": "Alt Metal"},
          {"itemName": "Metalcore"}
        ]
      },
      {"itemName": "Folk-Rock",
        "itemChildren":
        [
          {"itemName": "Political Rock"},
          {"itemName": "Celtic Rock"}
        ]
      },
      {"itemName": "Adult Alternative"},
      {"itemName": "Adult Contemporary",
        "itemChildren":
        [
          {"itemName": "Blue-Eyed Soul"},
          {"itemName": "Modern Folk"},
          {"itemName": "Lite Rock"},
          {"itemName": "Jazz Rock"}
        ]
      }
    ]
  }
]

IMPORTANT: this script requires the application "JSON Helper." This application is free in the Apple App Store, available under the Apple menu. (Search for "helper" and it's one of the first results.)

Script after the jump:

-- Copyright © 2013, Joseph Brick
-- All rights reserved. 
-- Redistribution, in whole or in part, with or without modification, is permitted provided that the copyright notice is retained.

-- Information on how to use this script can be found here:
--http://omnigraffletips.blogspot.com/2013/07/applescript-to-draw-taxonomy-from-json.html


-- input file name (without path). Expects a JSON file on your Desktop where each object contains a properties called "itemName" (the label of the node) and, if the node has children, "itemChildren" (the children of the node; children can contain children) 
property SETTINGS_inputFile : "drawtree.json"

-- chart properties
property SETTINGS_nodeVSpacing : 10 -- minimum distance between nodes with the same parent
property SETTINGS_nodeGroupVSpacing : 25 -- minimum distance between nodes at the same hierarchichal level but with different parents
property SETTINGS_levelHSpacing : 600 -- horizontal distance between a parent and child
property SETTINGS_chartOrigin : {50, 50} -- x,y location of the chart as a whole

-- node circle properties
property SETTINGS_nodeCircleDiameter : 20
property SETTINGS_nodeCircleStrokeThickness : 2
property SETTINGS_nodeCircleStrokeColor : {1, 0.5, 0}
property SETTINGS_nodeCircleStrokeOpacity : 1 -- 0 is fully transparent, 0.5 is 50% transparent, 1 is fully opaque
property SETTINGS_nodeCircleFillColor : {1, 1, 1}
property SETTINGS_nodeCircleFillOpacity : 0

-- line properties
property SETTINGS_lineThickness : 2
property SETTINGS_lineColor : {0.7, 0.8, 0.9}
property SETTINGS_lineOpacity : 1
property SETTINGS_lineCurveSharpness : 0.5 --length of bezier handles in relation to length of line; a higher number yields a sharper curve

-- node label properties
property SETTINGS_labelFontSize : 24
property Settings_labelFont : "Helvetica"
property SETTINGS_labelFontColor : {0, 0, 0}
property SETTINGS_labelFontOpacity : 1
property SETTINGS_labelHOffset : 8 -- distance between node label and node circle
property SETTINGS_labelVAdjust : 0 -- vertical adjustment of the label; by default, the node circle and node label are center-aligned

----

global highYPerLevel
global textHeightDetermined
global centerOffset
global depth
global theCanvas

on run
tell application id "OGfl"
set theCanvas to (canvas of front window) as reference
end tell
set depth to 0
set textHeightDetermined to false
set centerOffset to 0
set highYPerLevel to {}
set treeData to null
set jsonText to desktopFileContents(SETTINGS_inputFile)
if jsonText is null then
return
end if
set treeData to loadTreeStructureFromJSON(jsonText)
set allNodes to drawChildren(treeData, null)
set nodeGroup to groupAllNodes(allNodes)
tell application id "OGfl"
tell theCanvas
set origin of nodeGroup to SETTINGS_chartOrigin
end tell
end tell
end run

on desktopFileContents(fName)
tell application "Finder"
set fileName to URL of desktop & fName
end tell
set fileName to text (length of "file://localhost/") thru (length of fileName) of fileName
tell application "System Events"
set fileExists to (exists file fileName)
end tell
if fileExists = false then
display dialog "Can't find file:\n\n" & fileName as text buttons {"OK"}
return null
end if
open for access fileName
set retVal to (read fileName)
close access fileName
return retVal
end desktopFileContents

on loadTreeStructureFromJSON(jsonText)
tell application "JSON Helper"
set treeData to read JSON from jsonText
end tell
end loadTreeStructureFromJSON

on drawChildren(treeData, parentNodeCircle)
set i to 0
if (count highYPerLevel) < (depth + 1) then
set end of highYPerLevel to item 2 of SETTINGS_chartOrigin
end if
set retVal to {}
tell application id "OGfl"
tell theCanvas
set ox to SETTINGS_levelHSpacing * depth + (item 1 of SETTINGS_chartOrigin)
repeat with treeObject in treeData
set oy to item (depth + 1) of highYPerLevel
set item (depth + 1) of highYPerLevel to (item (depth + 1) of highYPerLevel) + SETTINGS_nodeCircleDiameter + SETTINGS_nodeVSpacing
set nodeCircle to make new shape at end of graphics with properties {name:"Circle", size:{SETTINGS_nodeCircleDiameter, SETTINGS_nodeCircleDiameter}, origin:{ox, oy}, thickness:SETTINGS_nodeCircleStrokeThickness, draws shadow:false, magnets:{{1, 0}, {-1, 0}}, stroke color:SETTINGS_nodeCircleStrokeColor & {SETTINGS_nodeCircleStrokeOpacity}, fill color:SETTINGS_nodeCircleFillColor & {SETTINGS_nodeCircleFillOpacity}}
set nodeText to make new shape at end of graphics with properties {origin:{ox + SETTINGS_nodeCircleDiameter + SETTINGS_labelHOffset, oy + SETTINGS_labelVAdjust + centerOffset}, side padding:0, fill:no fill, draws stroke:false, draws shadow:false, autosizing:full, text:{text:itemName of treeObject, size:SETTINGS_labelFontSize, color:SETTINGS_labelFontColor & {SETTINGS_labelFontOpacity}, font:Settings_labelFont, alignment:center}, vertical padding:0}
set nodeTextHeight to item 2 of (size of nodeText as list)
if textHeightDetermined is false then
set textHeightDetermined to true
set centerOffset to (SETTINGS_nodeCircleDiameter - nodeTextHeight) / 2
set origin of nodeText to {ox + SETTINGS_nodeCircleDiameter + SETTINGS_labelHOffset, (item 2 of (origin of nodeCircle as list)) + centerOffset}
end if
if parentNodeCircle is not null then
set curLine to drawLine(parentNodeCircle, ox, oy) of me
set source of curLine to parentNodeCircle
set destination of curLine to nodeCircle
end if
set children to {}
set childNodes to {}
set hasKids to hasChildren(treeObject) of me
if hasKids then
set origin of nodeText to {ox - (item 1 of (size of nodeText as list)) - SETTINGS_labelHOffset, (item 2 of (origin of nodeText as list))}
set children to itemChildren of treeObject
set depth to depth + 1
set childNodes to drawChildren(children, nodeCircle) of me
set depth to depth - 1
set cSpan to childSpan(childNodes) of me
set proposedNodeY to (spanTop of cSpan) + ((spanBottom of cSpan) - (spanTop of cSpan)) / 2 - SETTINGS_nodeCircleDiameter / 2
if proposedNodeY ≥ oy then
slide nodeCircle by {0, proposedNodeY - oy}
slide nodeText by {0, proposedNodeY - oy}
set item (depth + 1) of highYPerLevel to (item (depth + 1) of highYPerLevel) + (proposedNodeY - oy)
get adjustLeafNodes(retVal, nodeCircle) of me
else
get moveChildren(childNodes, oy - proposedNodeY) of me
end if
end if
set nodeGroup to assemble {nodeCircle, nodeText}
set retVal to retVal & {{level:depth, node:nodeGroup, hasChildren:hasKids}} & childNodes
end repeat
end tell
end tell
set item (depth + 1) of highYPerLevel to (item (depth + 1) of highYPerLevel) + (SETTINGS_nodeGroupVSpacing - SETTINGS_nodeVSpacing)
return retVal
end drawChildren

on drawLine(parentCircle, childCircleX, childCircleY)
tell application id "OGfl"
set op to (origin of parentCircle as list)
set px to (item 1 of op) + SETTINGS_nodeCircleDiameter
set py to (item 2 of op) + SETTINGS_nodeCircleDiameter / 2
set lineLen to childCircleX - px
set curveLen to lineLen * (SETTINGS_lineCurveSharpness)
tell theCanvas
set retVal to make new line at end of graphics with properties {bezier point list:{{px, py}, {px + curveLen, py}, {childCircleX - curveLen, childCircleY + SETTINGS_nodeCircleDiameter / 2}, {childCircleX, childCircleY + SETTINGS_nodeCircleDiameter / 2}}, thickness:SETTINGS_lineThickness, line type:bezier, stroke color:SETTINGS_lineColor & {SETTINGS_lineOpacity}}
end tell
return retVal
end tell
end drawLine

on childSpan(children)
tell application id "OGfl"
set retVal to {spanTop:null, spanBottom:null}
repeat with curNode in children
if level of curNode is (depth + 1) then
set nodeCircle to item 1 of graphics of (node of curNode)
set nodeTop to item 2 of (origin of nodeCircle as list)
set nodeBottom to nodeTop + SETTINGS_nodeCircleDiameter
if spanTop of retVal is null or spanTop of retVal > nodeTop then
set spanTop of retVal to nodeTop
end if
if spanBottom of retVal is null or spanBottom of retVal < nodeBottom then
set spanBottom of retVal to nodeBottom
end if
end if
end repeat
end tell
return retVal
end childSpan

on moveChildren(children, vOffset)
tell application id "OGfl"
set levelsAffected to {}
repeat with curNode in children
slide (node of curNode) by {0, vOffset}
if level of curNode is not in levelsAffected then
set end of levelsAffected to level of curNode
end if
end repeat
repeat with lvl in levelsAffected
set item (lvl + 1) of highYPerLevel to (item (lvl + 1) of highYPerLevel) + vOffset
end repeat
end tell
end moveChildren

on adjustLeafNodes(_allNodes, bottomCircle)
tell application id "OGfl"
set nodesToMove to {}
set curNode to null
repeat with i from (count _allNodes) to 1 by -1
set curNode to item i of _allNodes
set topNode to null
if level of curNode = depth then
if hasChildren of curNode = false then
set end of nodesToMove to curNode
else
set topNode to curNode
exit repeat
end if
end if
end repeat
if (count nodesToMove) > 0 and topNode is not null then
set spanBottom to (item 2 of (origin of bottomCircle as list))
set spanTop to (item 2 of (origin of (item 1 of graphics of node of topNode) as list)) + SETTINGS_nodeCircleDiameter
set nodeCount to count nodesToMove
set nodesToMoveHeight to nodeCount * SETTINGS_nodeCircleDiameter
set gapSize to ((spanBottom - spanTop) - nodesToMoveHeight) / (nodeCount + 1)
repeat with nodeToMove in nodesToMove
set curY to item 2 of (origin of (item 1 of graphics of node of nodeToMove) as list)
set spanBottom to spanBottom - gapSize - SETTINGS_nodeCircleDiameter
slide node of nodeToMove by {0, spanBottom - curY}
end repeat
end if
end tell
end adjustLeafNodes

on hasChildren(treeObject)
set retVal to false
set children to null
try
set children to itemChildren of treeObject
on error errMsg number errNumber
return retVal
end try
if (count children) > 0 then
set retVal to true
end if
return retVal
end hasChildren

on groupAllNodes(_allNodes)
tell application id "OGfl"
set objectList to {}
repeat with curNode in _allNodes
set end of objectList to node of curNode
end repeat
return assemble objectList
end tell
end groupAllNodes


9 comments:

  1. The script is not working when there is the same value of itemName for multiple items. For example, in your JSON data, if "itemName": "Rock/Pop", appears twice then the code stops working. Please suggest what I should do in this case.

    ReplyDelete
  2. Joseph, I have a network diagram export in JSON that I want Omnigrapple to import as a map. Can you comment on the feasibility of this, and if working with your script may solve this?

    vance.turner@staples.com

    ReplyDelete
  3. No deposit bonus codes 2021 - Casino Sites
    No Deposit Bonuses 2021 슬롯머신게임 — No Deposit Bonus codes 2021, 먹튀 검증 먹튀 랭크 Best no deposit bonus codes & exclusive 크루즈 베컴 No Deposit casino bonus codes. Top No Deposit 바카라양방 Free 바카라그림 Spins

    ReplyDelete
  4. We understand that lock emergencies can happen at any time, which is why our locksmiths are available around the clock, ready to respond promptly to your calls. We prioritize your safety and security, and our locksmiths arrive equipped with the latest tools and techniques to efficiently handle any situation.<a

    ReplyDelete
  5. Transparency is at the core of our service. We offer upfront pricing and detailed estimates, ensuring that you're aware of the costs before we start any work. This commitment to honesty and fairness extends to our competitive pricing as well – we believe that top-quality locksmith services should be accessible without breaking the bank.locksmith near me

    ReplyDelete
  6. It is a multifaceted industry that encompasses residential, commercial, and industrial properties, each with its unique dynamics and potential for profit. Real estate, at its core, involves the buying, selling, and management of physical assets, making it one of the most tangible and substantial forms of investment.Sky Botania Condo

    ReplyDelete
  7. Technology has revolutionized the real estate landscape. Online platforms and virtual tours have simplified property searches and transactions, making the process more accessible. Blockchain technology is also gaining traction for property records and transactions, promising increased transparency and security. HillHaven Condo

    ReplyDelete
  8. Real estate transactions are complex, involving legal, financial, and logistical considerations. Property ownership entails a web of rights and responsibilities, and legal frameworks differ significantly across jurisdictions. The transfer of ownership, often facilitated by real estate agents and brokers, requires a meticulous process of negotiation, documentation, and due diligence to ensure a seamless transition of ownership.Kovan Jewel Showflat

    ReplyDelete
  9. Sustainability and environmental concerns have become increasingly important in the real estate sector. Green building practices, energy-efficient design, and environmentally friendly materials are gaining traction as society becomes more conscious of its ecological footprint. Many property developers are incorporating sustainable features into their projects, not only for environmental reasons but also to attract environmentally conscious buyers and tenants.Lentor Modern Showroom

    ReplyDelete