Sunday, June 23, 2013

AppleScript to create a taxonomy chart

Update: 6/29/2013. Rewrote script. Now it places the outline into a hierarchical structure before it draws the tree.

Also, I've posted a script that produces the exact same output, but using a JSON source rather than an outline.

-----

Unlike most flow charts, taxonomy charts have a purely hierarchical structure. This script creates such a chart.

Below is a taxonomy of the genre Rock/Pop (according to the music service Rhapsody) created by this script.


Although it would have been nice (and would have made the scripting much simpler) to have used OmniOutliner to provide the data for this chart, I didn't want to spend the $30 it costs, and I figured most people who use OmniGraffle don't have OmniOutliner. 

Instead, the data is drawn from a file placed on your desktop. By default, this file is called drawtree.txt, but you can change that in the SETTINGS properties at the top of the script.

The text in that file to create the chart above:

o Rock/Pop
oo Roots
ooo Cajun/Zydeco
ooo Tex Mex
ooo Swamp Pop
oo Classic Rock
ooo Adult Oriented Rock
oo Art & Progressive Rock
ooo Rock Opera
ooo New Prog
ooo Krautrock
oo Blues & Boogie Rock
ooo Boogie Rock
ooo Southern Rock
oo Glam
oo Hard Rock
ooo Post-Grunge
ooo Instrumental Guitar Rock
ooo Acid Rock
oo Instrumental Rock
oo Pop
ooo Dance Pop
ooo Teen Beat
ooo Teen Idols
oo Jam Rock
oo Latin Pop
oo Country Rock
oo Funk Rock
oo Metal
ooo Progressive Metal
ooo Thrash/Speed Metal
ooo Stoner Rock
ooo Doom Metal
ooo Pop Metal
ooo New Wave of British Heavy Metal
ooo Black Metal
ooo Death Metal
ooo Funk Metal
ooo Industrial Metal
ooo Christian Metal
ooo Rapcore
ooo Alt Metal
ooo Metalcore
oo Folk-Rock
ooo Political Rock
ooo Celtic Rock
oo Adult Alternative
oo Adult Contemporary
ooo Blue-Eyed Soul
ooo Modern Folk
ooo Lite Rock
ooo Jazz Rock

The hierarchy level of each item is indicated by the length of the string proceeding it. I'm using the letter "o" to create this string; any letter will work. It must be string of letters, however, and not - say - a string of asterisks. (I'm using the AppleScript keyword "word" to grab that first string; only a string of letters is considered a word.) A single "o" is the top level, "oo" is the next level down, etc.

Note: there must be a space between the string of o's and the label following it. Also, children directly follow their parent in this list. If you compare the list above to the chart above, you'll get the gist of it.

There are customizable options at the top of this script. These all start with the prefix "SETTINGS_".

Note that this script doesn't do a whole lot of error checking. If there's a problem with the text (e.g., no space between the string of o's and the label, skipping a hierarchy level, etc.), the script will probably fail. I've tested this on several large charts with no error, so if the script errors out, verify your data.

If you've verified your data and there's still a problem, leave your data in a comment here and I'll investigate.

The script follows the jump. Copy it, paste it into AppleScript Editor, and run it.

Sunday, June 2, 2013

Script to generate a table of contents. Supports long documents.

There are a few great scripts out there that automatically create a table of contents for your OmniGraffle document.

This script does as well, but it will spread the table of contents over multiple pages if necessary, and it will also list section headers if you have canvases within your document that serve as section header pages.

It's also extremely customizable.You can pretty much make it look exactly like you want it to look, and you have precise control over almost every aspect of it by changing the properties listed at the top of the script that start with the prefix "SETTINGS_".

Here is one example of how the table of contents for a document with section headings might look, but again, you can customize it to look any number of ways.


The script currently has these limitations.
  • It does not gracefully support wrapping long canvas names; if a canvas name is so long it wraps, it's not going to look great.
  • Sections (the section header and its child pages) have no way to alter the default widow/orphan control. A section is alway kept together unless its height is greater than the canvas height
If additional table-of-content pages are required due to the document being too long to display the TOC on a single canvas, additional canvases will be created to accommodate additional TOC pages. Note: these will be placed at the front of the document and you will have to move them manually due to a bug in OmniGraffle's AppleScript implementation related to moving canvases. (That bug is explained in a post here.)

The script is after the jump. Copy it and paste it into AppleScript editor and run it.

I'm hoping the comments in the script are sufficient to describe the various customizable options. If you have any questions or find any bugs, or you want any new features, please leave a comment.

Omnigraffle AppleScript bug: moving a canvas removes all shared layers on that canvas

Just so you AppleScripters out there are aware, this bug exists in OmniGraffle's AppleScript implementation as of version 5.4.2.

Moving a canvas through AppleScript to the beginning or end of the canvas list removes all shared layers of the moved canvas, even shared layers that are the last instance of that layer.

For example, the following script will remove all shared layers from the second canvas when it is moved to the front of the document.

-- warning: running this script will remove shared layers
tell application id "OGfl"
    tell document of front window
        set c to item 2 of canvases
        move c to beginning of canvases
    end tell
end tell

Also, if any of the shared layers is the last instance of that shared layer, you won't be able to get it back (other than using Undo).

The folks at Omni Group have added this to their bug database.

Thursday, May 23, 2013

Drawing a pie chart with Adjustable Wedge using AppleScript

OmniGraffle has a set of built-in tools and shapes that you can reference by name in AppleScript. Some of these have special AppleScript access. These blessed shapes are Adjustable Wedge, Adjustable Arc, Adjustable Star, Adjustable Arrow, and Adjustable Double Arrow. (See Shapes in OmniGraffle's AppleScript Dictionary.)

Here I'll be focusing on the Adjustable Wedge, which is not in the Tools toolbar, but you can find it in the Common stencil under "Shapes."

Adjustable Wedge turns out to be very easy to manipulate, as it has the properties startAngle and endAngle for adjusting the outer arc, each of which goes from 0 to 360. If Adjustable Wedge were a clock, 0 would be 12 o'clock, 90 would be 3 o'clock, 270 would be 9 o'clock, and so on. (Adjustable Arc has the same properties.)

This makes creating a pie chart a piece of cake, as you just take the percentage of the chart that a piece of data would occupy, multiply it by 360, and that is the span of the arc. No trigonometry needed for calculating x,y postions on the arc. Adjustable Wedge handles that for you.

The code below creates this pie chart:



Previous AppleScript posts on this blog explain much of what is going on in the following script. I'll call out various lines of interest below it. Here are all posts about AppleScript on this blog.

-----

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

tell application id "OGfl"

    -- stuff the script user might want ot adjust
    set pieLoc to {50, 50}
    set pieDiameter to 400
    set wedgeValues to {24, 55, 77, 130, 37} -- values or percentages; doesn't matter
    set wedgeColors to {{1, 0, 0}, {0, 1, 1}, {1, 0, 1}, {0, 0, 1}, {0, 1, 0}} -- same length as wedgeValues
    set wedgeStartAngle to 0 -- angle at which first wedge starts drawing (0 is 12 o'clock, 180 is 6 o'clock)
   
    -- get the sum of all wedge values
    set sumOfWedgeValues to 0
    repeat with wedgeValue in wedgeValues
        set sumOfWedgeValues to sumOfWedgeValues + wedgeValue
    end repeat

    set wedges to {}  -- initialize list of wedge shapes to an empty list

   --draw one wedge per loop
    repeat with i from 1 to count of wedgeValues
        set wedgeDegrees to ((item i of wedgeValues) / sumOfWedgeValues) * 360  
        set wedgeEndAngle to wedgeStartAngle + wedgeDegrees

        tell canvas of  front window
            set wedge to make new shape at end of graphics with properties {name:"AdjustableWedge", size:{pieDiameter, pieDiameter}, origin:pieLoc, startAngle:wedgeStartAngle, endAngle:wedgeEndAngle, draws shadow:false, draws stroke:false, fill color:item i of wedgeColors}
        end tell

        set end of wedges to wedge  -- add wedge to list of wedges
        set wedgeStartAngle to wedgeEndAngle    -- start next wedge where this wedge ended
    end repeat
    assemble wedges -- group wedges
end tell

-----

repeat with i from 1 to count of wedgeValues

This is yet another form of the repeat loop. The value of variable i increases by 1 on each run through the loop, starting at 1, and ending at the number of items in the variable wedgeValues.

make new shape

This command draws each wedge. The easy way to find out how to make a given shape in AppleScript is to select that shape in OmniGraffle, and choose Edit > Copy As > AppleScript. This puts AppleScript on the clipboard that will draw this object or objects. Then it's just a matter of using variables for various properties instead of hard-coded values.

set end of wedges to wedge

It may seem counterintuitive, but setting the beginning or end of a list to something appends that something to that list. Here we are adding the latest Adjustable Wedge object we created (held by the variable wedge), and appending it to the list wedges, which we initialized as an empty list before the loop.

assemble wedges

The command assemble groups a list of objects. The variable wedges contains all of the wedges we drew. 

Tuesday, May 21, 2013

Changing the canvas size of all canvases with AppleScript

***
Update (07/20/2013): a new version of this script that prompts for the new canvas size (rather than changing the variable in the script) appears after the jump.
***

Here's another useful AppleScript script for changing all canvases in your document to a given size.

First, some important notes about page size vs. canvas size:

Changing your canvas size does not change your page size, which is defined in Page Setup as the paper size you print to. You can have multiple pages on a canvas, and you will if you set the canvas size larger than the paper size from Page Setup

Also, the variables <%#%> (page number) and <%TotalPages%> report their values based on number of pages in the document, not the number canvases. However, if you check the "Print canvas on one printer sheet" checkbox in the Canvas > Size inspector, then these variables consider a multi-page canvas a single page.

So if you are manually setting the canvas size this script does, and you only want one page per canvas, you'll want to do two things:
  1. Make sure that the paper size that you choose in Page Setup is larger than the canvas size you pick. Pick or create a humungous paper size in Page Setup, just to be safe. 
  2. If you plan to print and want the printer to ignore the page size set in Page Setup, export to a PDF document first. Note: do NOT print to PDF, but instead go to File > Export and chose PDF document. All resulting pages in the PDF will be sized to your canvas size (assuming you followed step 1 above). Open the PDF in Preview. Preview will automatically set the zoom to fit the paper size you select in its print dialog,
With those disclaimers out of the way, on to the script:


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

set theSize to {480, 800}
tell application id "OGfl"
    tell the front document
        set allCanvases to canvases
        repeat with currentCanvas in allCanvases
            tell currentCanvas
                set adjusts pages to false --prevents creating a new page in a canvas by accidentally dragging an object beyond the paper size
                set canvasSize to theSize
            end tell
        end repeat
    end tell
end tell

A few notes on this script are below. See the post "Using AppleScript to reposition objects" for more details on using AppleScript.

-----

tell the front document / end tell

Instead of using this tell / end tell structure, we could have done the following:

    set allCanvases to canvases of the front document 

Since each canvas in allCanvases will now include a reference to the document that contains it, we don't need to refer explicitly to the document when looping through the canvases.

set theSize to {480, 800}

This variable contains the size we'll set each canvas to. This is always a pixel size, regardless of which units are set in each canvas. So if you want the size to be in a different unit, you'll have to do the conversion to pixels.

For example, if you want a 10-inch by 10-inch canvas, you could do something like this:

     set conversionMultiplier to 72
     set theSize to {10 * conversionMulitplier, 10 * conversionMultiplier}

To find the conversion multiplier from pixels to any unit, set a canvas to use the desired units in the Canvas > Size Inspector, then set the Major Grid Spacing to 1 (of those units) in the Canvas > Grid Inspector, and then (temporarily) change the canvas units to pixels. The setting in Major Grid Spacing will change to the number of pixels that make up 1 unit of the desired units.

tell currentCanvas / end tell

Instead of using this tell end tell structure, we could have put "of currentCanvas" after the properties in each of the set commands within the repeat loop. E.g.,

     set canvasSize of currentCanvas to theSize

-----

Note: the purple items in the script are class properties.

To get details on all the properties that OmniGraffle classes like document and canvas contain, open AppleScript Editor, choose File > Open Dictionary, and choose "OmniGraffle Professional 5" from the ensuing dialog. This brings up documentation on everything that OmniGraffle's AppleScript implementation supports.


Monday, May 20, 2013

Using Applescript to reposition objects

***
Update (07/20/2013): a new version of this script that prompts for the horizontal and vertical offsets  (rather than having to edit the script) appears after the jump.

***

Omnigraffle gives quite a bit of access to AppleScript for automating just about anything you can do by hand. To execute AppleScript, open the AppleScript editor, create a script in it, and run it.

The following example shows you how to reposition all objects in your document by a certain horizontal/vertical offset - in this case, 20 pixels up, and 20 pixels to the left.

While you may have no need for this particular functionality, the code below shows a general technique for accessing each canvas, each layer within a canvas, and each object on a layer. For instance, you might want to change all canvases in your document to a different size, so knowing how to walk through all canvases in a document is a good starting point.

Here is the Applescript code, and below I'll call out various lines to explain them. This post assumes that you have some familiarity with programming concepts such as variables, loops, classes, etc., but does not assume knowledge of AppleScript in particular.

Notes on the colors below:
  • Green items are variables
  • Blue italicized items are classes
  • Blue bolded items are commands particular to OmniGraffle
  • Black bolded items are AppleScript keywords
  • Black unbolded items are values
  • Purple, unbolded items are class properties (this script contains none)

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

set theOffset to {-20, -20} as point
tell application id "OGfl"
    set allCanvases to canvases of front document
    repeat with currentCanvas in allCanvases
        set allNonsharedLayers to (layers of currentCanvas where class is not shared layer)
        repeat with currentLayer in allNonsharedLayers
            set allGraphics to graphics of currentLayer
            repeat with currentGraphic in allGraphics
                slide currentGraphic by theOffset
            end repeat
        end repeat
    end repeat
end tell

-----

And now, let's break down this script:

set theOffset to {-20, -20} as point

theOffset is a variable. In Applescript, you don't need to define variables ahead of time; the act of setting one for the first time creates it. theOffset is set to a list of two items. Lists are defined by delimiting a set of values by commas within curly braces.

The variable is defined as an instance of the class point, since that is what the slide command expects. (A point must be a list of two real numbers.) Without specifying the class, however, AppleScript would figure it out, so in most cases you don't need to specify the class.

tell application id "OGfl"

The tell statement here has a corresponding end tell, which is the last line in the script. Everything between those two lines will be directed at Omnigraffle, as opposed to at another application.

set allCanvases to canvases of the front document

canvases returns a list of all canvases in the document. This is placed in the variable allCanvases.

repeat with currentCanvas in allCanvases / end repeat

The repeat with statement creates a loop for each item in a list, and at the start of each loop it places the corresponding list item into a variable. Here we are walking through all canvases in the document.

The list of canvases is held by the variable allCanvases, set above, and the variable that receives the corresponding value at the beginning of each loop is currentCanvas. So at the beginning the first loop, currentCanvas receives the first item of allCanvases; in the second loop, it gets the second item, and so on.

set allNonsharedLayers to (layers of currentCanvas where class is not shared layer)

Note: there is a bug in Omnigraffle's Applescript implementation (as of version 5.4.2) that returns an invalid form of the graphics list for shared layers, so this script is limited to non-shared layers. Sadly, you'll have to move the objects in your shared layers manually, or make a temporary non-shared layer copy.

You can filter a list of items using the keyword where. (The keywords whose and that are synonyms of where.) Here, the variable allNonsharedLayers receives a list of layers in the current canvas, but only the layer objects whose class is not shared layer. Thus, the loop nested within this loop will deal only with non-shared layers.

repeat with currentLayer in allNonsharedLayers

Another repeat with loop, this time looping through all non-shared layers in the current canvas and placing each one in the variable currentLayer.

set allGraphics to graphics of currentLayer

graphics returns a list of Omnigraffle objects regardless of type - in this case, all objects on the layer the current loop is dealing with. You could also use shapes or lines if you need to be more specific.

repeat with currentGraphic in allGraphics

Here's the innermost repeat with loop, this time looping through all graphics in the current layer and placing each one in the variable currentGraphic.

slide currentGraphic by theOffset

The slide command moves a graphic to a new location by a horizontal/vertical offset from its previous location. We previously set theOffset at the top of this script to {-20, -20}, so each item will move 20 to the left, and 20 up.

-----

So where does one find documentation on OmniGraffle's AppleScript implementation?

OmniGraffle is a fairly niche product, and AppleScripting with OmniGraffle is a fairly niche activity, so as you can imagine, there's not a ton of information out there.

But in the AppleScript Editor, you can choose File > Open Dictionary, and then choose OmniGraffle Professional from the ensuing dialog. This will bring up a window showing everything OmniGraffle includes in its AppleScript implementation. You can also Google for examples like this to work from.

If you want to learn how to create objects and groups wholesale in AppleScript, you can select any object in OmniGraffle and from the menu choose Edit > Copy As > AppleScript. This will put on the clipboard AppleScript that will create this object from scratch. 

And of course it helps to be familiar with AppleScript, which is documented very well in the Mac Developer Library.


Sunday, May 19, 2013

Using variables

OmniGraffle has a number of variables related to objects, canvases, and the current document that come in handy. (This is a fixed set of variables; you cannot define your own variables.)

The menu Edit > Insert Variable exposes a number of them, but there are others.


Here I'll describe how they are used and call out some of the more useful ones.

You can put a string representing a variable in any object's text. The variable string is shown when you are editing the object's text, and the variable's value is shown otherwise. Variables are formatted with a preceding "<%" and a terminating "%>". For example, the variable for the current date is entered as <%Date%>.

You can mix variables with static text, and include multiple variables in a single object's text. For example, "Page <%#%> of <%TotalPages%>" could display as "Page 3 of 58" in, say, a page footer. Or, you might put "Last modified by <%Modifier%> on <%ModificationDate%>" on your title page.

In addition to those found in the Insert Variable menu, there are many that are associated with the Data Inspector. Each value here can display in an object's text using a variable.


The field variables above are:
  • <%Copyright%>
  • <%Version%>
  • <%Subject%> 
  • <%Description%>
  • <%Comments%>

The variables associated with the drop-down at the bottom are listed below. If multiple values are entered for a drop-down category, these are delimited by carriage returns when displayed.
  • <%Authors%>
  • <%Organizations%>
  • <%Languages%>
  • <%Keywords%>
  • <%Projects%>

Variables useful in headers, footers, and title pages

If you don't have document templates set up for your standard work documents, you should go about doing that: it's a time saver. In such a template, you can have your predefined headers, footers, and title pages populated with variables, so you never have to touch them; instead, let the variables do the work.
  • <%Canvas%> (name of the current canvas): This serves handily as the page title.
  • <%#%> (current page number)
  • <%TotalPages%> (total number of pages) 
  • <%Subject%> (document title): That's how I use it, anyway. Define in the Data Inspector.
  • <%Projects%> (project code name(s)): Typically, a single value. Define in the Data Inspector.
  • <%Organizations%> (your organization(s) or team name(s)): Typically, a single value. Define in the Data Inspector.
  • <%Creator%> (document author): I assume Omnigraffle is picking this value up from OSX, or perhaps the product registration.
  • <%Authors%> (document author(s)): Typically a single value, and usually you. Define in the Data Inspector if you don't like the value <%Creator%> is returning.
  • <%Version%> (the document version): Define in the Data Inspector.
  • <%ModificationDate%> (the date of your most recent save): I just use this as the document date, rather than showing both creation and modification dates.

Variables useful within the document

<%Length%> can be assigned only to a line label, and suddenly that line becomes useful for documenting other objects' dimensions, since the variable updates as you change the line length:


<%Width%> and <%Height%> are useful for document the dimensions of an object. Put
"<%Width%> by <%Height%>" in an object's text to automatically document your wireframes: 



Friday, May 17, 2013

Masking and cropping images

In OmniGraffle, any shape can contain an image, either raster like BMP or PNG, or vector like PDF or EPS.

Also, every image is contained by a shape. For example, when you paste a bitmap into Omnigraffle, that image arrives contained by a rectangle shape that has no fill or stroke.

Here we'll assign a pasted bitmap (sitting in its default rectangle shape) to a different shape: in this case, a circular shape.


An easy way to assign an image (or any property) of one shape to a different shape is to use the Style Tray. At the bottom of the document window, you see this tray containing style "chits" for the selected object. In this case, the selected object is the rectangular cat image we pasted.


The chit that's off by itself on the left represents all of the styles of pasted bitmap, including the rectangle shape that contains it. Each of the other chits represents individual properties of that shape. From left to right, these properties are: Fill, Stroke, Image, Shadow, Shape, Font, and Text Position. 

To assign any of these properties from the selected object to another object, just drag the chit containing the desired property from the Style Tray to the target object. So to assign the image of the (selected) pasted bitmap to the circle shape, grab its Image chit from the Style Tray and drag it onto the circle. (You can then delete the object you pasted.)

An alternative is to go the opposite direction: assign a circle shape to the the pasted image. To do this, you'd select a circle with no image, and from the Style Tray drag the Shape chit onto the pasted bitmap.

You could also assign a circle shape to the pasted image in the Lines and Shapes Inspector:


Either way, once an object contains an image, you can adjust how it displays within the object using the Image Inspector.


The icons to the top-right of the preview image determine how the image displays in the shape. From left to right, these are Natural Size, Stretch to Fit (the default, shown selected above), and Tiled.

If you want control over the top and left offsets of the image within the shape, as well as the relative size of the image within the shape, choose Natural Size, the leftmost icon. 

With Natural Size chosen, use the two offset fields to the right of the preview image to determine how far from the left and top of the shape the image displays, and use the Scale slider directly below the preview image to determine how large or small you want the image relative to the size of the shape. Scaling up an image essentially crops it within the shape.

You can also drag the preview image around in the inspector to change its offsets, but you don't get a lot of accuracy that way.

Note: with Natural Size selected, each time you resize the shape you'll likely have to readjust the offset and zoom settings, so it's best to get the shape to the desired size before you bother to make these adjustments.

Wednesday, May 15, 2013

Making a click-through HTML or PDF prototype

OmniGraffle allows you to assign actions to objects via the Actions Inspector. A one of the interactions offered is jumping to another canvas on click.

Note that this particular action works when you export to PDF or HTML, so this allows you to make simple prototypes that navigates to different screens when clicking various objects. This works for mobile prototypes as well, though you may need to experiment with various browsers and PDF readers on the device to find the one that works best.

To assign an action, select the object that will trigger navigation, and open the Actions Inspector:


  • In the first drop-down, choose "Jumps Elsewhere"
  • In the second drop-down, make sure "Switch to a Specific Canvas" is chosen
  • In the third drop-down, choose the canvas that you'll jump to upon click
Note that you can test the interaction before exporting to PDF or HTML by selecting the Browse Tool (which looks like a hand with its index finger extended) and clicking on an object.

You can assign actions to any object, but to make these actions easy to find again, I usually put a new layer on top of each canvas called "actions," and place transparent rectangles over objects that I want to react to click.

Once you've assigned actions to the desired objects, export your document to PDF or HTML by choosing the menu File > Export.

For a PDF click-through, set these options in the Export window:


For an HTML click through, set these options in the Export window:


Note that the Scale option is useful if you art targeting a mobile device that is wider or narrower than your prototype.

Tuesday, May 14, 2013

Tips using the Geometry Inspector

The Geometry Inspector provides a good alternative to sizing and positioning objects by hand.

There are a few tips that are helpful when using this inspector:

Like all inspectors that take numeric entries, you can use the up/down arrow keys to increment/decrement the value that has keyboard focus. Shift+Up/Down Arrow increments/decrements that value by 10.

Likewise, the fields accept mathematical expressions. So if you want to half the current width of 250px, just enter 250px/2.

Also, even if the ruler units of your current canvas are set to - say - pixels, you can enter any other unit into the X, Y, Width, and Height fields. (E.g., "2 in")

Holding the Shift key while spinning the Rotation knob sets rotation to the nearest 45 degree increment.