Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active September 30, 2016 04:56
Show Gist options
  • Save nitaku/7517984 to your computer and use it in GitHub Desktop.
Save nitaku/7517984 to your computer and use it in GitHub Desktop.

Revisions

  1. nitaku revised this gist May 28, 2015. 1 changed file with 0 additions and 0 deletions.
    Binary file modified thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  2. nitaku revised this gist May 27, 2015. 1 changed file with 0 additions and 0 deletions.
    Binary file modified thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  3. nitaku revised this gist Nov 18, 2013. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    Same as [the previous example](http://bl.ocks.org/nitaku/7512487), but with client-side persistence, thanks to the IndexedDB APIs (see [this other example](http://bl.ocks.org/nitaku/6890861)).

    Try to modify the graph, then reload the page to load it again.
    Try to modify the graph, then reload the page to load it again.

    The graph is automatically saved on each modification of its structure, and also every second with a [setInterval](http://www.w3schools.com/js/js_timing.asp), to store even the changes made to nodes' position by the force layout.
  4. nitaku revised this gist Nov 18, 2013. 2 changed files with 2 additions and 2 deletions.
    2 changes: 1 addition & 1 deletion index.coffee
    Original file line number Diff line number Diff line change
    @@ -236,7 +236,7 @@ window.main = (() ->
    .on('mousemove.add_link', ((d) ->
    ### check if there is a new link in creation ###
    if global.new_link_source?
    ### create the draggable link representation ###
    ### update the draggable link representation ###
    p = d3.mouse(global.vis.node())
    global.drag_link
    .attr('x1', global.new_link_source.x) # source node can be in movement
    2 changes: 1 addition & 1 deletion index.js
    Original file line number Diff line number Diff line change
    @@ -208,7 +208,7 @@
    */
    var p;
    if (global.new_link_source != null) {
    /* create the draggable link representation
    /* update the draggable link representation
    */
    p = d3.mouse(global.vis.node());
    return global.drag_link.attr('x1', global.new_link_source.x).attr('y1', global.new_link_source.y).attr('x2', p[0]).attr('y2', p[1]);
  5. nitaku revised this gist Nov 17, 2013. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    Same as [the previous example](http://bl.ocks.org/nitaku/7512487), but with client-side persistence, thanks to the IndexedDB APIs (see [this other example](http://bl.ocks.org/nitaku/6890861)).

    Try to modify the graph, then reload the page to load it again.
    Try to modify the graph, then reload the page to load it again.

    The graph is automatically saved on each modification of its structure, and also every second with a [setInterval](http://www.w3schools.com/js/js_timing.asp), to store even the changes made to nodes' position by the force layout.
  6. nitaku revised this gist Nov 17, 2013. 9 changed files with 1201 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions d3.v3.min.js
    4 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
    111 changes: 111 additions & 0 deletions db.coffee
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,111 @@
    window.db = {}

    window.db.get_or_create = (callback) ->
    request = indexedDB.open('graph', 2)

    request.onupgradeneeded = () ->
    ### called whenever the DB changes version. triggers when the DB is created ###
    db = request.result
    store = db.createObjectStore('graph', {keyPath: 'id'})

    ### initial fake data ###
    store.put {
    id: 0,
    data: {
    nodes: [
    {id: 'A', x: 469, y: 410, type: 'X'},
    {id: 'B', x: 493, y: 364, type: 'X'},
    {id: 'C', x: 442, y: 365, type: 'X'},
    {id: 'D', x: 467, y: 314, type: 'X'},
    {id: 'E', x: 477, y: 248, type: 'Y'},
    {id: 'F', x: 425, y: 207, type: 'Y'},
    {id: 'G', x: 402, y: 155, type: 'Y'},
    {id: 'H', x: 369, y: 196, type: 'Y'},
    {id: 'I', x: 350, y: 148, type: 'Z'},
    {id: 'J', x: 539, y: 222, type: 'Z'},
    {id: 'K', x: 594, y: 235, type: 'Z'},
    {id: 'L', x: 582, y: 185, type: 'Z'},
    {id: 'M', x: 633, y: 200, type: 'Z'}
    ],
    links: [
    {source: 'A', target: 'B'},
    {source: 'B', target: 'C'},
    {source: 'C', target: 'A'},
    {source: 'B', target: 'D'},
    {source: 'D', target: 'C'},
    {source: 'D', target: 'E'},
    {source: 'E', target: 'F'},
    {source: 'F', target: 'G'},
    {source: 'F', target: 'H'},
    {source: 'G', target: 'H'},
    {source: 'G', target: 'I'},
    {source: 'H', target: 'I'},
    {source: 'J', target: 'E'},
    {source: 'J', target: 'L'},
    {source: 'J', target: 'K'},
    {source: 'K', target: 'L'},
    {source: 'L', target: 'M'},
    {source: 'M', target: 'K'}
    ],
    last_index: 0
    }
    }

    console.log('Database created or upgraded.')

    request.onsuccess = () ->
    ### called when the connection with the DB is opened successfully ###
    db = request.result
    console.log('Database connection opened.')

    ### open a transaction ###
    tx = db.transaction('graph', 'readwrite')
    store = tx.objectStore('graph')

    ### get everything in the store ###
    keyRange = IDBKeyRange.lowerBound(0)
    cursorRequest = store.openCursor(keyRange)

    cursorRequest.onsuccess = (e) ->
    ### called when the cursor request succeeds ###
    result = e.target.result
    if not result?
    return

    ### pass the result to the caller's callback ###
    callback(result.value.data)

    result.continue()

    tx.oncomplete = () ->
    ### called when the transaction ends ###
    console.log('Transaction complete.')

    ### close the connection to the DB ###
    db.close()

    window.db.store = (graph) ->
    request = indexedDB.open('graph', 2)

    request.onsuccess = () ->
    ### called when the connection with the DB is opened successfully ###
    db = request.result
    console.log('Database connection opened.')

    ### open a transaction ###
    tx = db.transaction('graph', 'readwrite')
    store = tx.objectStore('graph')

    ### store the given graph ###
    store.put {
    id: 0,
    data: graph
    }

    tx.oncomplete = () ->
    ### called when the transaction ends ###
    console.log('Transaction complete.')

    ### close the connection to the DB ###
    db.close()

    215 changes: 215 additions & 0 deletions db.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,215 @@
    (function() {

    window.db = {};

    window.db.get_or_create = function(callback) {
    var request;
    request = indexedDB.open('graph', 2);
    request.onupgradeneeded = function() {
    /* called whenever the DB changes version. triggers when the DB is created
    */
    var db, store;
    db = request.result;
    store = db.createObjectStore('graph', {
    keyPath: 'id'
    });
    /* initial fake data
    */
    store.put({
    id: 0,
    data: {
    nodes: [
    {
    id: 'A',
    x: 469,
    y: 410,
    type: 'X'
    }, {
    id: 'B',
    x: 493,
    y: 364,
    type: 'X'
    }, {
    id: 'C',
    x: 442,
    y: 365,
    type: 'X'
    }, {
    id: 'D',
    x: 467,
    y: 314,
    type: 'X'
    }, {
    id: 'E',
    x: 477,
    y: 248,
    type: 'Y'
    }, {
    id: 'F',
    x: 425,
    y: 207,
    type: 'Y'
    }, {
    id: 'G',
    x: 402,
    y: 155,
    type: 'Y'
    }, {
    id: 'H',
    x: 369,
    y: 196,
    type: 'Y'
    }, {
    id: 'I',
    x: 350,
    y: 148,
    type: 'Z'
    }, {
    id: 'J',
    x: 539,
    y: 222,
    type: 'Z'
    }, {
    id: 'K',
    x: 594,
    y: 235,
    type: 'Z'
    }, {
    id: 'L',
    x: 582,
    y: 185,
    type: 'Z'
    }, {
    id: 'M',
    x: 633,
    y: 200,
    type: 'Z'
    }
    ],
    links: [
    {
    source: 'A',
    target: 'B'
    }, {
    source: 'B',
    target: 'C'
    }, {
    source: 'C',
    target: 'A'
    }, {
    source: 'B',
    target: 'D'
    }, {
    source: 'D',
    target: 'C'
    }, {
    source: 'D',
    target: 'E'
    }, {
    source: 'E',
    target: 'F'
    }, {
    source: 'F',
    target: 'G'
    }, {
    source: 'F',
    target: 'H'
    }, {
    source: 'G',
    target: 'H'
    }, {
    source: 'G',
    target: 'I'
    }, {
    source: 'H',
    target: 'I'
    }, {
    source: 'J',
    target: 'E'
    }, {
    source: 'J',
    target: 'L'
    }, {
    source: 'J',
    target: 'K'
    }, {
    source: 'K',
    target: 'L'
    }, {
    source: 'L',
    target: 'M'
    }, {
    source: 'M',
    target: 'K'
    }
    ],
    last_index: 0
    }
    });
    return console.log('Database created or upgraded.');
    };
    return request.onsuccess = function() {
    /* called when the connection with the DB is opened successfully
    */
    var cursorRequest, db, keyRange, store, tx;
    db = request.result;
    console.log('Database connection opened.');
    /* open a transaction
    */
    tx = db.transaction('graph', 'readwrite');
    store = tx.objectStore('graph');
    /* get everything in the store
    */
    keyRange = IDBKeyRange.lowerBound(0);
    cursorRequest = store.openCursor(keyRange);
    cursorRequest.onsuccess = function(e) {
    /* called when the cursor request succeeds
    */
    var result;
    result = e.target.result;
    if (!(result != null)) return;
    /* pass the result to the caller's callback
    */
    callback(result.value.data);
    return result["continue"]();
    };
    tx.oncomplete = function() {
    /* called when the transaction ends
    */ return console.log('Transaction complete.');
    };
    /* close the connection to the DB
    */
    return db.close();
    };
    };

    window.db.store = function(graph) {
    var request;
    request = indexedDB.open('graph', 2);
    return request.onsuccess = function() {
    /* called when the connection with the DB is opened successfully
    */
    var db, store, tx;
    db = request.result;
    console.log('Database connection opened.');
    /* open a transaction
    */
    tx = db.transaction('graph', 'readwrite');
    store = tx.objectStore('graph');
    /* store the given graph
    */
    store.put({
    id: 0,
    data: graph
    });
    tx.oncomplete = function() {
    /* called when the transaction ends
    */ return console.log('Transaction complete.');
    };
    /* close the connection to the DB
    */
    return db.close();
    };
    };

    }).call(this);
    379 changes: 379 additions & 0 deletions index.coffee
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,379 @@
    width = 960
    height = 500

    ### SELECTION - store the selected node ###
    ### EDITING - store the drag mode (either 'drag' or 'add_link') ###
    global = {
    selection: null
    }

    window.main = (() ->
    ### get data from the DB ###
    db.get_or_create (graph) ->
    global.graph = graph

    global.graph.objectify = () ->
    ### resolve node IDs (not optimized at all!) ###
    for l in global.graph.links
    for n in global.graph.nodes
    if l.source is n.id
    l.source = n
    continue

    if l.target is n.id
    l.target = n
    continue

    global.graph.remove = (condemned) ->
    ### remove the given node or link from the graph, also deleting dangling links if a node is removed ###
    if condemned in global.graph.nodes
    global.graph.nodes = global.graph.nodes.filter (n) -> n isnt condemned
    global.graph.links = global.graph.links.filter (l) -> l.source.id isnt condemned.id and l.target.id isnt condemned.id
    else if condemned in global.graph.links
    global.graph.links = global.graph.links.filter (l) -> l isnt condemned

    global.graph.add_node = (type) ->
    n = {
    id: global.graph.last_index++,
    x: width/2,
    y: height/2,
    type: type
    }
    global.graph.nodes.push(n)

    return n

    global.graph.add_link = (source, target) ->
    ### avoid links to self ###
    return null if source is target

    ### avoid link duplicates ###
    for link in global.graph.links
    return null if link.source is source and link.target is target

    l = {
    source: source,
    target: target
    }
    global.graph.links.push(l)

    return l

    global.graph.serialize = () ->
    ### PERSISTENCE - return a copy of the graph, with redundancies (whole nodes in links pointers) removed. also include the last_index property, to persist it also ###
    return {
    nodes: global.graph.nodes,
    links: ({source: l.source.id, target: l.target.id} for l in global.graph.links),
    last_index: global.graph.last_index
    }

    global.graph.objectify()

    ### create the SVG ###
    svg = d3.select('body').append('svg')
    .attr('width', width)
    .attr('height', height)

    ### ZOOM and PAN ###
    ### create container elements ###
    container = svg.append('g')

    container.call(d3.behavior.zoom().scaleExtent([0.5, 8]).on('zoom', (() -> global.vis.attr('transform', "translate(#{d3.event.translate})scale(#{d3.event.scale})"))))

    global.vis = container.append('g')

    ### create a rectangular overlay to catch events ###
    ### WARNING rect size is huge but not infinite. this is a dirty hack ###
    global.vis.append('rect')
    .attr('class', 'overlay')
    .attr('x', -500000)
    .attr('y', -500000)
    .attr('width', 1000000)
    .attr('height', 1000000)
    .on('click', ((d) ->
    ### SELECTION ###
    global.selection = null
    d3.selectAll('.node').classed('selected', false)
    d3.selectAll('.link').classed('selected', false)
    ))

    ### END ZOOM and PAN ###

    global.colorify = d3.scale.category10()

    ### initialize the force layout ###
    global.force = d3.layout.force()
    .size([width, height])
    .charge(-400)
    .linkDistance(60)
    .on('tick', (() ->
    ### update nodes and links ###
    global.vis.selectAll('.node')
    .attr('transform', (d) -> "translate(#{d.x},#{d.y})")

    global.vis.selectAll('.link')
    .attr('x1', (d) -> d.source.x)
    .attr('y1', (d) -> d.source.y)
    .attr('x2', (d) -> d.target.x)
    .attr('y2', (d) -> d.target.y)
    ))

    ### DRAG ###
    global.drag = global.force.drag()
    .on('dragstart', (d) -> d.fixed = true)
    .on('dragend', () -> db.store global.graph.serialize() ) # PERSISTENCE

    ### DELETION - pressing DEL deletes the selection ###
    d3.select(window)
    .on('keydown', () ->
    if d3.event.keyCode is 46 # DEL
    if global.selection?
    global.graph.remove global.selection
    global.selection = null
    update()
    db.store(global.graph.serialize()) # PERSISTENCE
    )

    update()

    ### TOOLBAR ###
    toolbar = $("<div class='toolbar'></div>")
    $('body').append(toolbar)

    toolbar.append($("""
    <svg
    class='active tool'
    data-tool='pointer'
    xmlns='http://www.w3.org/2000/svg'
    version='1.1'
    width='32'
    height='32'
    viewBox='0 0 128 128'>
    <g transform='translate(881.10358,-356.22543)'>
    <g transform='matrix(0.8660254,-0.5,0.5,0.8660254,-266.51112,-215.31898)'>
    <path
    d='m -797.14902,212.29589 a 5.6610848,8.6573169 0 0 0 -4.61823,4.3125 l -28.3428,75.0625 a 5.6610848,8.6573169 0 0 0 4.90431,13 l 56.68561,0 a 5.6610848,8.6573169 0 0 0 4.9043,-13 l -28.3428,-75.0625 a 5.6610848,8.6573169 0 0 0 -5.19039,-4.3125 z m 0.28608,25.96875 18.53419,49.09375 -37.06838,0 18.53419,-49.09375 z'
    />
    <path
    d='m -801.84375,290.40625 c -2.09434,2.1e-4 -3.99979,1.90566 -4,4 l 0,35.25 c 2.1e-4,2.09434 1.90566,3.99979 4,4 l 10,0 c 2.09434,-2.1e-4 3.99979,-1.90566 4,-4 l 0,-35.25 c -2.1e-4,-2.09434 -1.90566,-3.99979 -4,-4 z'
    />
    </g>
    </g>
    </svg>
    """))
    toolbar.append($("""
    <svg
    class='tool'
    data-tool='add_node'
    xmlns='http://www.w3.org/2000/svg'
    version='1.1'
    width='32'
    height='32'
    viewBox='0 0 128 128'>
    <g transform='translate(720.71649,-356.22543)'>
    <g transform='translate(-3.8571429,146.42857)'>
    <path
    d='m -658.27638,248.37149 c -1.95543,0.19978 -3.60373,2.03442 -3.59375,4 l 0,12.40625 -12.40625,0 c -2.09434,2.1e-4 -3.99979,1.90566 -4,4 l 0,10 c -0.007,0.1353 -0.007,0.27095 0,0.40625 0.19978,1.95543 2.03442,3.60373 4,3.59375 l 12.40625,0 0,12.4375 c 2.1e-4,2.09434 1.90566,3.99979 4,4 l 10,0 c 2.09434,-2.1e-4 3.99979,-1.90566 4,-4 l 0,-12.4375 12.4375,0 c 2.09434,-2.1e-4 3.99979,-1.90566 4,-4 l 0,-10 c -2.1e-4,-2.09434 -1.90566,-3.99979 -4,-4 l -12.4375,0 0,-12.40625 c -2.1e-4,-2.09434 -1.90566,-3.99979 -4,-4 l -10,0 c -0.1353,-0.007 -0.27095,-0.007 -0.40625,0 z'
    />
    <path
    d='m -652.84375,213.9375 c -32.97528,0 -59.875,26.86847 -59.875,59.84375 0,32.97528 26.89972,59.875 59.875,59.875 32.97528,0 59.84375,-26.89972 59.84375,-59.875 0,-32.97528 -26.86847,-59.84375 -59.84375,-59.84375 z m 0,14 c 25.40911,0 45.84375,20.43464 45.84375,45.84375 0,25.40911 -20.43464,45.875 -45.84375,45.875 -25.40911,0 -45.875,-20.46589 -45.875,-45.875 0,-25.40911 20.46589,-45.84375 45.875,-45.84375 z'
    />
    </g>
    </g>
    </svg>
    """))
    toolbar.append($("""
    <svg
    class='tool'
    data-tool='add_link'
    xmlns='http://www.w3.org/2000/svg'
    version='1.1'
    width='32'
    height='32'
    viewBox='0 0 128 128'>
    <g transform='translate(557.53125,-356.22543)'>
    <g transform='translate(20,0)'>
    <path
    d='m -480.84375,360 c -15.02602,0 -27.375,12.31773 -27.375,27.34375 0,4.24084 1.00221,8.28018 2.75,11.875 l -28.875,28.875 c -3.59505,-1.74807 -7.6338,-2.75 -11.875,-2.75 -15.02602,0 -27.34375,12.34898 -27.34375,27.375 0,15.02602 12.31773,27.34375 27.34375,27.34375 15.02602,0 27.375,-12.31773 27.375,-27.34375 0,-4.26067 -0.98685,-8.29868 -2.75,-11.90625 L -492.75,411.96875 c 3.60156,1.75589 7.65494,2.75 11.90625,2.75 15.02602,0 27.34375,-12.34898 27.34375,-27.375 C -453.5,372.31773 -465.81773,360 -480.84375,360 z m 0,14 c 7.45986,0 13.34375,5.88389 13.34375,13.34375 0,7.45986 -5.88389,13.375 -13.34375,13.375 -7.45986,0 -13.375,-5.91514 -13.375,-13.375 0,-7.45986 5.91514,-13.34375 13.375,-13.34375 z m -65.375,65.34375 c 7.45986,0 13.34375,5.91514 13.34375,13.375 0,7.45986 -5.88389,13.34375 -13.34375,13.34375 -7.45986,0 -13.34375,-5.88389 -13.34375,-13.34375 0,-7.45986 5.88389,-13.375 13.34375,-13.375 z'
    />
    <path
    d='m -484.34375,429.25 c -1.95543,0.19978 -3.60373,2.03442 -3.59375,4 l 0,12.40625 -12.40625,0 c -2.09434,2.1e-4 -3.99979,1.90566 -4,4 l 0,10 c -0.007,0.1353 -0.007,0.27095 0,0.40625 0.19978,1.95543 2.03442,3.60373 4,3.59375 l 12.40625,0 0,12.4375 c 2.1e-4,2.09434 1.90566,3.99979 4,4 l 10,0 c 2.09434,-2.1e-4 3.99979,-1.90566 4,-4 l 0,-12.4375 12.4375,0 c 2.09434,-2.1e-4 3.99979,-1.90566 4,-4 l 0,-10 c -2.1e-4,-2.09434 -1.90566,-3.99979 -4,-4 l -12.4375,0 0,-12.40625 c -2.1e-4,-2.09434 -1.90566,-3.99979 -4,-4 l -10,0 c -0.1353,-0.007 -0.27095,-0.007 -0.40625,0 z'
    />
    </g>
    </g>
    </svg>
    """))

    library = $("<div class='library'></div></div>")
    toolbar.append(library)

    ['X','Y','Z','W'].forEach (type) ->
    new_btn = $("""
    <svg width='42' height='42'>
    <g class='node'>
    <circle
    cx='21'
    cy='21'
    r='18'
    stroke='#{global.colorify(type)}'
    fill='#{d3.hcl(global.colorify(type)).brighter(3)}'
    />
    </g>
    </svg>
    """)
    new_btn.bind 'click', () ->
    global.graph.add_node(type)
    update()
    db.store(global.graph.serialize()) # PERSISTENCE

    library.append(new_btn)
    library.hide()

    global.tool = 'pointer'

    global.new_link_source = null
    global.vis
    .on('mousemove.add_link', ((d) ->
    ### check if there is a new link in creation ###
    if global.new_link_source?
    ### create the draggable link representation ###
    p = d3.mouse(global.vis.node())
    global.drag_link
    .attr('x1', global.new_link_source.x) # source node can be in movement
    .attr('y1', global.new_link_source.y)
    .attr('x2', p[0])
    .attr('y2', p[1])
    ))
    .on('mouseup.add_link', ((d) ->
    global.new_link_source = null

    ### remove the draggable link representation, if exists ###
    global.drag_link.remove() if global.drag_link?
    ))

    d3.selectAll('.tool').on 'click', () ->
    d3.selectAll('.tool').classed('active', false)
    d3.select(this).classed('active', true)

    new_tool = $(this).data('tool')

    nodes = global.vis.selectAll('.node')

    if new_tool is 'add_link' and global.tool isnt 'add_link'
    ### remove drag handlers from nodes ###
    nodes
    .on('mousedown.drag', null)
    .on('touchstart.drag', null)

    ### add drag handlers for the add_link tool ###
    nodes
    .call(drag_add_link)

    else if new_tool isnt 'add_link' and global.tool is 'add_link'
    ### remove drag handlers for the add_link tool ###
    nodes
    .on('mousedown.add_link', null)
    .on('mouseup.add_link', null)

    ### add drag behavior to nodes ###
    nodes
    .call(global.drag)

    if new_tool is 'add_node'
    library.show()
    else
    library.hide()

    global.tool = new_tool

    ### PERSISTENCE - store the graph every second, to avoid missing the force layout updates on nodes' position ###
    setInterval (() -> db.store global.graph.serialize() ), 1000
    )

    update = () ->
    ### update the layout ###
    global.force
    .nodes(global.graph.nodes)
    .links(global.graph.links)
    .start()

    ### create nodes and links ###
    ### (links are drawn with insert to make them appear under the nodes) ###

    ### also define a drag behavior to drag nodes ###
    ### dragged nodes become fixed ###
    nodes = global.vis.selectAll('.node')
    .data(global.graph.nodes, (d) -> d.id)

    new_nodes = nodes
    .enter().append('g')
    .attr('class', 'node')
    .on('click', ((d) ->
    ### SELECTION ###
    global.selection = d
    d3.selectAll('.node').classed('selected', (d2) -> d2 is d)
    d3.selectAll('.link').classed('selected', false)
    ))

    links = global.vis.selectAll('.link')
    .data(global.graph.links, (d) -> "#{d.source.id}->#{d.target.id}")

    links
    .enter().insert('line', '.node')
    .attr('class', 'link')
    .on('click', ((d) ->
    ### SELECTION ###
    global.selection = d
    d3.selectAll('.link').classed('selected', (d2) -> d2 is d)
    d3.selectAll('.node').classed('selected', false)
    ))

    links
    .exit().remove()

    ### TOOLBAR - add link tool initialization for new nodes ###
    if global.tool is 'add_link'
    new_nodes.call(drag_add_link)
    else
    new_nodes.call(global.drag) # DRAG

    new_nodes.append('circle')
    .attr('r', 18)
    .attr('stroke', (d) -> global.colorify(d.type))
    .attr('fill', (d) -> d3.hcl(global.colorify(d.type)).brighter(3))

    ### draw the label ###
    new_nodes.append('text')
    .text((d) -> d.id)
    .attr('dy', '0.35em')
    .attr('fill', (d) -> global.colorify(d.type))

    nodes
    .exit().remove()

    drag_add_link = (selection) ->
    selection
    .on('mousedown.add_link', ((d) ->
    global.new_link_source = d

    ### create the draggable link representation ###
    p = d3.mouse(global.vis.node())
    global.drag_link = global.vis.insert('line', '.node')
    .attr('class', 'drag_link')
    .attr('x1', d.x)
    .attr('y1', d.y)
    .attr('x2', p[0])
    .attr('y2', p[1])

    ### prevent pan activation ###
    d3.event.stopPropagation()
    ### prevent text selection ###
    d3.event.preventDefault()
    ))
    .on('mouseup.add_link', ((d) ->
    ### add link and update, but only if a link is actually added ###
    if global.graph.add_link(global.new_link_source, d)?
    update()
    db.store(global.graph.serialize()) # PERSISTENCE
    ))

    72 changes: 72 additions & 0 deletions index.css
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,72 @@
    .node > circle {
    stroke-width: 4px;
    }

    .node > text {
    pointer-events: none;
    font-family: sans-serif;
    font-weight: bold;
    text-anchor: middle;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -o-user-select: none;
    user-select: none;
    }

    .link, .drag_link {
    stroke-width: 6px;
    stroke: gray;
    opacity: 0.5;
    }

    .drag_link {
    stroke-linecap: round;
    }

    .selected > circle {
    stroke-width: 8px;
    }

    .selected.link {
    stroke-width: 14px;
    }

    .overlay {
    fill: transparent;
    }

    .toolbar {
    position: absolute;
    top: 12px;
    left: 480px;
    width: 240px;
    margin-left: -120px;
    text-align: center;
    }

    .tool:not(:last-child) {
    margin-right: 10px;
    }

    .tool {
    fill: #a3a4c3;
    cursor: pointer;
    }
    .tool.active {
    fill: #b52d0c;
    }

    .library {
    border: 2px solid #a3a4c3;
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.8);
    padding: 6px;
    margin-top: 10px;
    }
    .library svg {
    cursor: pointer;
    }
    .library svg:not(:last-child) {
    padding-right: 6px;
    }
    14 changes: 14 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,14 @@
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>Graph editing with persistence</title>
    <link type="text/css" href="index.css" rel="stylesheet"/>
    <script src='//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js' type='text/javascript'></script>
    <script src="d3.v3.min.js"></script>
    <script src="index.js"></script>
    <script src="db.js"></script>
    </head>
    <body onload="main()">
    </body>
    </html>
    339 changes: 339 additions & 0 deletions index.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,339 @@
    (function() {
    var drag_add_link, global, height, update, width,
    __indexOf = Array.prototype.indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };

    width = 960;

    height = 500;

    /* SELECTION - store the selected node
    */

    /* EDITING - store the drag mode (either 'drag' or 'add_link')
    */

    global = {
    selection: null
    };

    window.main = (function() {
    /* get data from the DB
    */ return db.get_or_create(function(graph) {
    var container, library, svg, toolbar;
    global.graph = graph;
    global.graph.objectify = function() {
    /* resolve node IDs (not optimized at all!)
    */
    var l, n, _i, _len, _ref, _results;
    _ref = global.graph.links;
    _results = [];
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
    l = _ref[_i];
    _results.push((function() {
    var _j, _len2, _ref2, _results2;
    _ref2 = global.graph.nodes;
    _results2 = [];
    for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
    n = _ref2[_j];
    if (l.source === n.id) {
    l.source = n;
    continue;
    }
    if (l.target === n.id) {
    l.target = n;
    continue;
    } else {
    _results2.push(void 0);
    }
    }
    return _results2;
    })());
    }
    return _results;
    };
    global.graph.remove = function(condemned) {
    /* remove the given node or link from the graph, also deleting dangling links if a node is removed
    */ if (__indexOf.call(global.graph.nodes, condemned) >= 0) {
    global.graph.nodes = global.graph.nodes.filter(function(n) {
    return n !== condemned;
    });
    return global.graph.links = global.graph.links.filter(function(l) {
    return l.source.id !== condemned.id && l.target.id !== condemned.id;
    });
    } else if (__indexOf.call(global.graph.links, condemned) >= 0) {
    return global.graph.links = global.graph.links.filter(function(l) {
    return l !== condemned;
    });
    }
    };
    global.graph.add_node = function(type) {
    var n;
    n = {
    id: global.graph.last_index++,
    x: width / 2,
    y: height / 2,
    type: type
    };
    global.graph.nodes.push(n);
    return n;
    };
    global.graph.add_link = function(source, target) {
    /* avoid links to self
    */
    var l, link, _i, _len, _ref;
    if (source === target) return null;
    /* avoid link duplicates
    */
    _ref = global.graph.links;
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
    link = _ref[_i];
    if (link.source === source && link.target === target) return null;
    }
    l = {
    source: source,
    target: target
    };
    global.graph.links.push(l);
    return l;
    };
    global.graph.serialize = function() {
    /* PERSISTENCE - return a copy of the graph, with redundancies (whole nodes in links pointers) removed. also include the last_index property, to persist it also
    */
    var l;
    return {
    nodes: global.graph.nodes,
    links: (function() {
    var _i, _len, _ref, _results;
    _ref = global.graph.links;
    _results = [];
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
    l = _ref[_i];
    _results.push({
    source: l.source.id,
    target: l.target.id
    });
    }
    return _results;
    })(),
    last_index: global.graph.last_index
    };
    };
    global.graph.objectify();
    /* create the SVG
    */
    svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
    /* ZOOM and PAN
    */
    /* create container elements
    */
    container = svg.append('g');
    container.call(d3.behavior.zoom().scaleExtent([0.5, 8]).on('zoom', (function() {
    return global.vis.attr('transform', "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
    })));
    global.vis = container.append('g');
    /* create a rectangular overlay to catch events
    */
    /* WARNING rect size is huge but not infinite. this is a dirty hack
    */
    global.vis.append('rect').attr('class', 'overlay').attr('x', -500000).attr('y', -500000).attr('width', 1000000).attr('height', 1000000).on('click', (function(d) {
    /* SELECTION
    */ global.selection = null;
    d3.selectAll('.node').classed('selected', false);
    return d3.selectAll('.link').classed('selected', false);
    }));
    /* END ZOOM and PAN
    */
    global.colorify = d3.scale.category10();
    /* initialize the force layout
    */
    global.force = d3.layout.force().size([width, height]).charge(-400).linkDistance(60).on('tick', (function() {
    /* update nodes and links
    */ global.vis.selectAll('.node').attr('transform', function(d) {
    return "translate(" + d.x + "," + d.y + ")";
    });
    return global.vis.selectAll('.link').attr('x1', function(d) {
    return d.source.x;
    }).attr('y1', function(d) {
    return d.source.y;
    }).attr('x2', function(d) {
    return d.target.x;
    }).attr('y2', function(d) {
    return d.target.y;
    });
    }));
    /* DRAG
    */
    global.drag = global.force.drag().on('dragstart', function(d) {
    return d.fixed = true;
    }).on('dragend', function() {
    return db.store(global.graph.serialize());
    });
    /* DELETION - pressing DEL deletes the selection
    */
    d3.select(window).on('keydown', function() {
    if (d3.event.keyCode === 46) {
    if (global.selection != null) {
    global.graph.remove(global.selection);
    global.selection = null;
    update();
    return db.store(global.graph.serialize());
    }
    }
    });
    update();
    /* TOOLBAR
    */
    toolbar = $("<div class='toolbar'></div>");
    $('body').append(toolbar);
    toolbar.append($("<svg\n class='active tool'\n data-tool='pointer'\n xmlns='http://www.w3.org/2000/svg'\n version='1.1'\n width='32'\n height='32'\n viewBox='0 0 128 128'>\n <g transform='translate(881.10358,-356.22543)'>\n <g transform='matrix(0.8660254,-0.5,0.5,0.8660254,-266.51112,-215.31898)'>\n <path\n d='m -797.14902,212.29589 a 5.6610848,8.6573169 0 0 0 -4.61823,4.3125 l -28.3428,75.0625 a 5.6610848,8.6573169 0 0 0 4.90431,13 l 56.68561,0 a 5.6610848,8.6573169 0 0 0 4.9043,-13 l -28.3428,-75.0625 a 5.6610848,8.6573169 0 0 0 -5.19039,-4.3125 z m 0.28608,25.96875 18.53419,49.09375 -37.06838,0 18.53419,-49.09375 z'\n />\n <path\n d='m -801.84375,290.40625 c -2.09434,2.1e-4 -3.99979,1.90566 -4,4 l 0,35.25 c 2.1e-4,2.09434 1.90566,3.99979 4,4 l 10,0 c 2.09434,-2.1e-4 3.99979,-1.90566 4,-4 l 0,-35.25 c -2.1e-4,-2.09434 -1.90566,-3.99979 -4,-4 z'\n />\n </g>\n </g>\n</svg>"));
    toolbar.append($("<svg\n class='tool'\n data-tool='add_node'\n xmlns='http://www.w3.org/2000/svg'\n version='1.1'\n width='32'\n height='32'\n viewBox='0 0 128 128'>\n <g transform='translate(720.71649,-356.22543)'>\n <g transform='translate(-3.8571429,146.42857)'>\n <path\n d='m -658.27638,248.37149 c -1.95543,0.19978 -3.60373,2.03442 -3.59375,4 l 0,12.40625 -12.40625,0 c -2.09434,2.1e-4 -3.99979,1.90566 -4,4 l 0,10 c -0.007,0.1353 -0.007,0.27095 0,0.40625 0.19978,1.95543 2.03442,3.60373 4,3.59375 l 12.40625,0 0,12.4375 c 2.1e-4,2.09434 1.90566,3.99979 4,4 l 10,0 c 2.09434,-2.1e-4 3.99979,-1.90566 4,-4 l 0,-12.4375 12.4375,0 c 2.09434,-2.1e-4 3.99979,-1.90566 4,-4 l 0,-10 c -2.1e-4,-2.09434 -1.90566,-3.99979 -4,-4 l -12.4375,0 0,-12.40625 c -2.1e-4,-2.09434 -1.90566,-3.99979 -4,-4 l -10,0 c -0.1353,-0.007 -0.27095,-0.007 -0.40625,0 z'\n />\n <path\n d='m -652.84375,213.9375 c -32.97528,0 -59.875,26.86847 -59.875,59.84375 0,32.97528 26.89972,59.875 59.875,59.875 32.97528,0 59.84375,-26.89972 59.84375,-59.875 0,-32.97528 -26.86847,-59.84375 -59.84375,-59.84375 z m 0,14 c 25.40911,0 45.84375,20.43464 45.84375,45.84375 0,25.40911 -20.43464,45.875 -45.84375,45.875 -25.40911,0 -45.875,-20.46589 -45.875,-45.875 0,-25.40911 20.46589,-45.84375 45.875,-45.84375 z'\n />\n </g>\n </g>\n</svg>"));
    toolbar.append($("<svg\n class='tool'\n data-tool='add_link'\n xmlns='http://www.w3.org/2000/svg'\n version='1.1'\n width='32'\n height='32'\n viewBox='0 0 128 128'>\n<g transform='translate(557.53125,-356.22543)'>\n <g transform='translate(20,0)'>\n <path\n d='m -480.84375,360 c -15.02602,0 -27.375,12.31773 -27.375,27.34375 0,4.24084 1.00221,8.28018 2.75,11.875 l -28.875,28.875 c -3.59505,-1.74807 -7.6338,-2.75 -11.875,-2.75 -15.02602,0 -27.34375,12.34898 -27.34375,27.375 0,15.02602 12.31773,27.34375 27.34375,27.34375 15.02602,0 27.375,-12.31773 27.375,-27.34375 0,-4.26067 -0.98685,-8.29868 -2.75,-11.90625 L -492.75,411.96875 c 3.60156,1.75589 7.65494,2.75 11.90625,2.75 15.02602,0 27.34375,-12.34898 27.34375,-27.375 C -453.5,372.31773 -465.81773,360 -480.84375,360 z m 0,14 c 7.45986,0 13.34375,5.88389 13.34375,13.34375 0,7.45986 -5.88389,13.375 -13.34375,13.375 -7.45986,0 -13.375,-5.91514 -13.375,-13.375 0,-7.45986 5.91514,-13.34375 13.375,-13.34375 z m -65.375,65.34375 c 7.45986,0 13.34375,5.91514 13.34375,13.375 0,7.45986 -5.88389,13.34375 -13.34375,13.34375 -7.45986,0 -13.34375,-5.88389 -13.34375,-13.34375 0,-7.45986 5.88389,-13.375 13.34375,-13.375 z'\n />\n <path\n d='m -484.34375,429.25 c -1.95543,0.19978 -3.60373,2.03442 -3.59375,4 l 0,12.40625 -12.40625,0 c -2.09434,2.1e-4 -3.99979,1.90566 -4,4 l 0,10 c -0.007,0.1353 -0.007,0.27095 0,0.40625 0.19978,1.95543 2.03442,3.60373 4,3.59375 l 12.40625,0 0,12.4375 c 2.1e-4,2.09434 1.90566,3.99979 4,4 l 10,0 c 2.09434,-2.1e-4 3.99979,-1.90566 4,-4 l 0,-12.4375 12.4375,0 c 2.09434,-2.1e-4 3.99979,-1.90566 4,-4 l 0,-10 c -2.1e-4,-2.09434 -1.90566,-3.99979 -4,-4 l -12.4375,0 0,-12.40625 c -2.1e-4,-2.09434 -1.90566,-3.99979 -4,-4 l -10,0 c -0.1353,-0.007 -0.27095,-0.007 -0.40625,0 z'\n />\n </g>\n </g>\n</svg>"));
    library = $("<div class='library'></div></div>");
    toolbar.append(library);
    ['X', 'Y', 'Z', 'W'].forEach(function(type) {
    var new_btn;
    new_btn = $("<svg width='42' height='42'>\n <g class='node'>\n <circle\n cx='21'\n cy='21'\n r='18'\n stroke='" + (global.colorify(type)) + "'\n fill='" + (d3.hcl(global.colorify(type)).brighter(3)) + "'\n />\n </g>\n</svg>");
    new_btn.bind('click', function() {
    global.graph.add_node(type);
    update();
    return db.store(global.graph.serialize());
    });
    library.append(new_btn);
    return library.hide();
    });
    global.tool = 'pointer';
    global.new_link_source = null;
    global.vis.on('mousemove.add_link', (function(d) {
    /* check if there is a new link in creation
    */
    var p;
    if (global.new_link_source != null) {
    /* create the draggable link representation
    */
    p = d3.mouse(global.vis.node());
    return global.drag_link.attr('x1', global.new_link_source.x).attr('y1', global.new_link_source.y).attr('x2', p[0]).attr('y2', p[1]);
    }
    })).on('mouseup.add_link', (function(d) {
    global.new_link_source = null;
    /* remove the draggable link representation, if exists
    */
    if (global.drag_link != null) return global.drag_link.remove();
    }));
    d3.selectAll('.tool').on('click', function() {
    var new_tool, nodes;
    d3.selectAll('.tool').classed('active', false);
    d3.select(this).classed('active', true);
    new_tool = $(this).data('tool');
    nodes = global.vis.selectAll('.node');
    if (new_tool === 'add_link' && global.tool !== 'add_link') {
    /* remove drag handlers from nodes
    */
    nodes.on('mousedown.drag', null).on('touchstart.drag', null);
    /* add drag handlers for the add_link tool
    */
    nodes.call(drag_add_link);
    } else if (new_tool !== 'add_link' && global.tool === 'add_link') {
    /* remove drag handlers for the add_link tool
    */
    nodes.on('mousedown.add_link', null).on('mouseup.add_link', null);
    /* add drag behavior to nodes
    */
    nodes.call(global.drag);
    }
    if (new_tool === 'add_node') {
    library.show();
    } else {
    library.hide();
    }
    return global.tool = new_tool;
    });
    /* PERSISTENCE - store the graph every second, to avoid missing the force layout updates on nodes' position
    */
    return setInterval((function() {
    return db.store(global.graph.serialize());
    }), 1000);
    });
    });

    update = function() {
    /* update the layout
    */
    var links, new_nodes, nodes;
    global.force.nodes(global.graph.nodes).links(global.graph.links).start();
    /* create nodes and links
    */
    /* (links are drawn with insert to make them appear under the nodes)
    */
    /* also define a drag behavior to drag nodes
    */
    /* dragged nodes become fixed
    */
    nodes = global.vis.selectAll('.node').data(global.graph.nodes, function(d) {
    return d.id;
    });
    new_nodes = nodes.enter().append('g').attr('class', 'node').on('click', (function(d) {
    /* SELECTION
    */ global.selection = d;
    d3.selectAll('.node').classed('selected', function(d2) {
    return d2 === d;
    });
    return d3.selectAll('.link').classed('selected', false);
    }));
    links = global.vis.selectAll('.link').data(global.graph.links, function(d) {
    return "" + d.source.id + "->" + d.target.id;
    });
    links.enter().insert('line', '.node').attr('class', 'link').on('click', (function(d) {
    /* SELECTION
    */ global.selection = d;
    d3.selectAll('.link').classed('selected', function(d2) {
    return d2 === d;
    });
    return d3.selectAll('.node').classed('selected', false);
    }));
    links.exit().remove();
    /* TOOLBAR - add link tool initialization for new nodes
    */
    if (global.tool === 'add_link') {
    new_nodes.call(drag_add_link);
    } else {
    new_nodes.call(global.drag);
    }
    new_nodes.append('circle').attr('r', 18).attr('stroke', function(d) {
    return global.colorify(d.type);
    }).attr('fill', function(d) {
    return d3.hcl(global.colorify(d.type)).brighter(3);
    });
    /* draw the label
    */
    new_nodes.append('text').text(function(d) {
    return d.id;
    }).attr('dy', '0.35em').attr('fill', function(d) {
    return global.colorify(d.type);
    });
    return nodes.exit().remove();
    };

    drag_add_link = function(selection) {
    return selection.on('mousedown.add_link', (function(d) {
    var p;
    global.new_link_source = d;
    /* create the draggable link representation
    */
    p = d3.mouse(global.vis.node());
    global.drag_link = global.vis.insert('line', '.node').attr('class', 'drag_link').attr('x1', d.x).attr('y1', d.y).attr('x2', p[0]).attr('y2', p[1]);
    /* prevent pan activation
    */
    d3.event.stopPropagation();
    /* prevent text selection
    */
    return d3.event.preventDefault();
    })).on('mouseup.add_link', (function(d) {
    /* add link and update, but only if a link is actually added
    */ if (global.graph.add_link(global.new_link_source, d) != null) {
    update();
    return db.store(global.graph.serialize());
    }
    }));
    };

    }).call(this);
    67 changes: 67 additions & 0 deletions index.sass
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,67 @@
    .node > circle
    stroke-width: 4px

    .node > text
    pointer-events: none
    font-family: sans-serif
    font-weight: bold
    text-anchor: middle

    // prevent text selection
    -webkit-user-select: none
    -moz-user-select: none
    -ms-user-select: none
    -o-user-select: none
    user-select: none

    // editing
    .link, .drag_link
    stroke-width: 6px
    stroke: gray
    opacity: 0.5

    .drag_link
    stroke-linecap: round

    // selection
    .selected > circle
    stroke-width: 8px

    .selected.link
    stroke-width: 14px

    // zoom and pan
    .overlay
    fill: transparent

    // toolbar
    .toolbar
    position: absolute
    top: 12px
    left: 480px
    width: 240px
    margin-left: -120px
    text-align: center

    .tool:not(:last-child)
    margin-right: 10px

    .tool
    fill: #A3A4C3
    cursor: pointer
    &.active
    fill: #B52D0C

    .library
    border: 2px solid #A3A4C3
    border-radius: 6px
    background: rgba(255,255,255,0.8)
    padding: 6px
    margin-top: 10px

    svg
    cursor: pointer

    svg:not(:last-child)
    padding-right: 6px

    Binary file added thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  7. nitaku revised this gist Nov 17, 2013. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1 +1,3 @@
    Same as [the previous example](http://bl.ocks.org/nitaku/7512487), but with client-side persistence, thanks to the IndexedDB APIs (see [this other example](http://bl.ocks.org/nitaku/6890861)).
    Same as [the previous example](http://bl.ocks.org/nitaku/7512487), but with client-side persistence, thanks to the IndexedDB APIs (see [this other example](http://bl.ocks.org/nitaku/6890861)).

    Try to modify the graph, then reload the page to load it again.
  8. nitaku created this gist Nov 17, 2013.
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    Same as [the previous example](http://bl.ocks.org/nitaku/7512487), but with client-side persistence, thanks to the IndexedDB APIs (see [this other example](http://bl.ocks.org/nitaku/6890861)).