Drawing a tree diagram with Raphaël and CoffeeScript
I recently had to create an interactive tree diagram displaying companies and their subsidiaries and the business requirements specified something "prettier" than I could achieve by styling a nested HTML list. Additionally I had to support Internet Explorer 8, which does not have Canvas or SVG support. Pondering the problem I remembered hearing on The Changelog of a client-side drawing library called Raphaël. Raphaël provides an abstraction over SVG on browsers that support it and on IE does the same thing using VML and thus giving me the ability to draw pictures in the browser going all the way back to IE 6.
So armed with the ability to draw I created my tree. Most of the new client-side code that I am writing today is in CoffeeScript so that is what I used here. The following is my main drawing routine:
defaults =
initialX: 50
initialY: 50
boxHeight: 100
boxWidth: 200
horizontalSpace: 0
verticalSpace: 75
indicatorSize: 40
indicatorMargin: 10
defaultNodeFillColor: '#FFF'
selectedNodeFillColor: '#989FED'
labelFont: { font: "16px Times", opacity: 1 }
nodeAfterClickHandler: (node) ->
openCloseAfterClickHandler: (node) ->
class SVGTree
constructor: (options) ->
@options = $.extend {}, defaults, options
@paper = new Raphael @options.elementId, '100%', '100%'
@backgroundColor = $('#' + @options.elementId).css("background-color")
@currX
@currY
@xInc = @options.boxWidth + @options.horizontalSpace
@yInc = @options.boxHeight + @options.verticalSpace
@currentlySelectedNode
@defaultNodeFill = { fill: @options.defaultNodeFillColor }
@selectedNodeFill = { fill: @options.selectedNodeFillColor }
#Event callbacks
self = @
@openCloseClickHandler = () ->
handleOpenClose.call(self, this)
self.options.openCloseAfterClickHandler.call(self, this)
@nodeClickHandler = () ->
handleNodeClick.call(self, this)
self.options.nodeAfterClickHandler.call(self, this)
render: ->
#this can be called over and over to refresh so reset initial state
@paper.clear()
@currX = @options.initialX
@currY = @options.initialY
@renderingTop = true #the rendering of the top node is just a little different so keep track
#define functions
renderNode = (elementData, x) =>
rect = @paper.rect x, @currY, @options.boxWidth, @options.boxHeight
rect.data('identifier', elementData.identifier)
rect.attr(if elementData.selected then @selectedNodeFill else @defaultNodeFill)
if elementData.selected then @currentlySelectedNode = rect
rect.click @nodeClickHandler
createTextLabel rect, elementData
@currY = @currY + @yInc
if elementData.children?
drawOpenCloseIndicator rect, elementData
if elementData.childrenOpen
for child in elementData.children
childNode = renderNode child, x + @xInc
drawConnector rect, childNode, child
rect
drawOpenCloseIndicator = (node, elementData) =>
startXOffset = if @renderingTop then @options.indicatorMargin else (@options.boxWidth / 2 + @options.horizontalSpace) + @options.indicatorMargin
indicatorHeight = Math.round(@options.indicatorSize * 0.866) #Aprox Math.sqrt(3) / 2
indicatorStartX = node.attr("x") - @options.indicatorSize - startXOffset
indicatorStartY = node.attr("y") + ((@options.boxHeight - indicatorHeight) / 2)
calculatedPath = if elementData.childrenOpen
"M #{indicatorStartX} #{indicatorStartY} l #{indicatorHeight} 0 l -#{indicatorHeight / 2} #{indicatorHeight} l -#{indicatorHeight / 2} -#{indicatorHeight}"
else
"M #{indicatorStartX} #{indicatorStartY} l 0 #{indicatorHeight} l #{indicatorHeight} -#{indicatorHeight / 2} l -#{indicatorHeight} -#{indicatorHeight / 2}"
#Create a rectangle with the same click handler to surround the visible indicator to make the click easier to hit
target = @paper.rect(indicatorStartX - 6, indicatorStartY - 6, @options.indicatorSize + 10, @options.indicatorSize + 10)
target.attr "stroke-opacity": 0
target.attr "fill": @backgroundColor
target.attr "fill-opacity": 0
target.data("elementData", elementData)
target.click @openCloseClickHandler
triangle = @paper.path calculatedPath
triangle.attr({fill: '#000'})
triangle.data("elementData", elementData)
triangle.click @openCloseClickHandler
@renderingTop = false
#Handle name wrapping
createTextLabel = (rect, elementData) =>
textLabel = @paper.text(rect.attr('x') + (@options.boxWidth / 2), @currY + (@options.boxHeight / 2), "")
.attr(@options.labelFont).data('identifier', elementData.identifier)
textLabel.click () => @nodeClickHandler.call(rect)
tempText = " "
for word in elementData.name.split(" ")
textLabel.attr("text", tempText + " " + word)
tempText += if textLabel.getBBox().width > @options.boxWidth then "
" + word else " " + word
textLabel.attr("text", tempText.substring 1)
return textLabel
drawConnector = (parentNode, childNode, inChildPosition) =>
parentBottomY = parentNode.attr("y") + @options.boxHeight
parentBottomX = parentNode.attr("x") + (@options.boxWidth / 2)
childSideY = childNode.attr("y") + (@options.boxHeight / 2)
childSideX = childNode.attr("x")
calculatedPath = "M #{parentBottomX} #{parentBottomY} L #{parentBottomX} #{childSideY} L #{childSideX} #{childSideY}"
connector1 = @paper.path calculatedPath
#execute
renderNode @options.nodeData, @currX
handleOpenClose = (node) ->
elementData = node.data("elementData")
elementData.childrenOpen = not elementData.childrenOpen
@render() #redraw
handleNodeClick = (node) ->
@currentlySelectedNode.attr @defaultNodeFill
node.attr @selectedNodeFill
@currentlySelectedNode = node
this.SVGTree = SVGTree
And then some code to create some sample data and call it:
$ ->
data =
name: "Bob's Car Sales"
identifier: 1
selected: false
childrenOpen: true
children: [
{
name: "Bob's Car Parts"
identifier: 2
selected: false
childrenOpen: false
children: [
{
name: "A grandchild"
identifier: 14
selected: false
},
{
name: "Another grandchild"
identifier: 15
selected: false
}
]
},
{
name: "Bob's Auto Service"
identifier: 3
selected: true
childrenOpen: true
children: [
{
name: "A grandchild"
identifier: 4
selected: false
},
{
name: "Another grandchild"
identifier: 5
selected: false
}
]
}
]
tree = new SVGTree
elementId: 'tree'
nodeData: data
boxHeight: 75
boxWidth: 150
#horizontalSpace: -50
indicatorSize: 30
#defaultNodeFillColor: '#AAA'
selectedNodeFillColor: '#A89FED'
#labelFont: { font: "20px Times", opacity: 1 }
nodeAfterClickHandler: (node) ->
console.log node
console.log node.data('identifier'), 'click'
openCloseAfterClickHandler: (node) ->
console.log node.data('identifier'), 'open/close'
tree.render()
Note from Matthew in 2026
I can't recommend using this code, or even Raphael or CoffeeScript anymore, but the were big parts of my geek journey. I had lost the original demo page for this code but a freindly LLM and I recreated it, original warts and all. The link below is a fairly faithful recreation of the original code, including a layout but that I have to stifle the impulse to fix. I'll probably do a modern rework of this soon for contrast, but for now, enjoy this little time capsule of my geeky past.