Skip to content

Instantly share code, notes, and snippets.

@denisemauldin
Created March 29, 2018 01:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save denisemauldin/ca1a500b81d52db38ba3ea1f34dcb88b to your computer and use it in GitHub Desktop.
Save denisemauldin/ca1a500b81d52db38ba3ea1f34dcb88b to your computer and use it in GitHub Desktop.
FlowGraph Prototype - debugging node resize
license: mit
const levelOne = {
// Once we have data for these things: id and horizontal position are determined by time since start
// See if force layout handles vertical positioning
nodes: [
{
title: 'Early Assistance Session',
dayMarker: -1,
id: '0.0',
shortDesc: "Optional paid service to discuss code and development questions with staff",
longDesc: "The Early Assistance program provides a non-mandatory flexible review session for prospective business owners, developers, and designers to receive expert technical advice from staff during the preliminary phase of a project.",
infoLinks: [
{
text: 'schedule an early assistance meeting',
url: 'https://develop.early-assistance.ashevillenc.gov/'
}
]
},
{
title: 'Commercial Review Application',
dayMarker: 0,
id: '1.0',
shortDesc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
infoLinks: [
{
text: 'Submit plans for review on the development portal',
url: 'https://develop.plans.ashevillenc.gov/'
}
]
},
{
title: 'Staff Reviews Development Plan',
dayMarker: 3,
id: '2.0',
shortDesc: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
infoLinks: [
{
text: 'Track your site development application status with Accela Citizen Access',
url: 'https://services.ashevillenc.gov/citizenaccess/'
}
]
},
{
title: 'Staff Transmits Review Comments',
dayMarker: 5,
id: '3.0',
shortDesc: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
},
{
// 4 AND 4.1 BECAUSE THEY HAPPEN AT SAME TIME-- like version numbers, not decimals (4.1, 2, 3 ... 11, etc)
title: 'Zoning Plan Approved',
dayMarker: 6,
id: '4.0',
shortDesc: "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
},
{
title: 'Revision and Resubmission Required',
dayMarker: null,
id: '4.1',
shortDesc: "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam..."
},
],
links: [
{
source: '0.0',
target: '1.0'
},
{
source: '1.0',
target: '2.0'
},
{
source: '2.0',
target: '3.0'
},
{
source: '3.0',
target: '4.0'
},
{
source: '3.0',
target: '4.1'
},
{
source: '4.1',
target: '1.0'
},
]
}
class FlowGraph{
constructor(parentElement, inputData = levelOne) {
this.parentElement = d3.select(parentElement);
this.width = this.parentElement.style('width').replace('px', '');
this.height = this.parentElement.style('height').replace('px', '');
this.verticalMargins = this.height * 0.015;
this.horizontalMargins = this.width * 0.015;
this.data = inputData;
this.nodePadding = {
x: 5,
y: 10,
};
const dayValues = this.data.nodes.map(d => d.dayMarker);
this.dayValMin = d3.min(dayValues)
const daySpan = d3.max(dayValues) + Math.abs(this.dayValMin);
this.xBase = (this.width - this.horizontalMargins * 2 - this.nodePadding.x * daySpan) / (daySpan + 1);
this.nodeWidth = this.xBase - this.nodePadding.x * 2;
// This is necessary because it applies to text height test nodes and real nodes
this.parentElement.append('style').html(`
.flowGraph-node {
width: ${this.nodeWidth}px;
text-align: center;
}
p {
padding: 3px;
}
.flowGraph-node p.title {
font-weight: bolder;
font-size: 1.15em;
}
`)
// Append titles and text to a div
// Also, base positioning and stuff on this rather than the other way around
const testNodes = this.parentElement.append('div')
.attr('id', 'test-nodes')
.selectAll('div')
.data(this.data.nodes)
.enter().append('div')
.attr('class', 'flowGraph-node')
.style('display', 'inline-block')
testNodes.append('p')
.attr('class', 'title')
.html(d => d.title)
testNodes.append('p')
.html(d => d.shortDesc)
// To deal with weird div height issues
window.addEventListener("load", () => this.render())
}
render() {
this.nodeHeight = document.getElementById('test-nodes').offsetHeight
const yBase = this.nodeHeight + this.nodePadding.y // TODO: FIX D.FY SO THAT THIS ISN'T A THING
d3.select('#test-nodes').remove()
this.data.nodes = this.data.nodes.map(d => {
d.numRepeats = this.data.nodes.filter(node => node.dayMarker === d.dayMarker || node.dayMarker === null).length
return d;
})
.sort((a, b) => a.numRepeats < b.numRepeats)
const maxNodesForOneDay = this.data.nodes[0].numRepeats;
// const yBase = (this.height - this.verticalMargins * 2 - this.nodePadding.y * maxNodesForOneDay) / (maxNodesForOneDay);
// let this.nodeHeight = yBase - this.nodePadding.y * 2;
this.data.nodes.map(d => {
const nodeLevel = d.id.split('.')[1]
// Top aligned:
// d.fy = this.verticalMargins + (nodeLevel * yBase)
// Middle of the page horizontally aligned:
d.fy = Math.max((this.height / 2) - (yBase * (maxNodesForOneDay / 2.0)), this.verticalMargins) + (nodeLevel * yBase)
if (d.dayMarker === null) { return d; }
const dayIndex = this.dayValMin < 0 ? d.dayMarker + Math.abs(this.dayValMin) : d.dayMarker;
d.fx = this.horizontalMargins + (dayIndex * this.xBase) + ((dayIndex + 1) * this.nodePadding.x);
return d;
})
const svg = this.parentElement.append('svg')
.attr('width', this.width)
.attr('height', this.height)
.attr('tabindex', 0)
svg.append('defs')
.append('marker')
.attr('id', 'arrowhead')
.attr('markerWidth', 10)
.attr('markerHeight', 7)
.attr('refX', 0)
.attr('refY', 3.5)
.attr('orient', 'auto')
.append('polygon')
.attr('points', '0 0, 10 3.5, 0 7')
const simulation = d3.forceSimulation()
.force('link', d3.forceLink()
.id(d => d.id)
)
.force('charge', d3.forceCollide())
.force('center', d3.forceCenter(this.width / 2, this.height / (maxNodesForOneDay + 1)))
const link = svg.append('g')
.attr('class', 'links')
.selectAll('path')
.data(this.data.links)
.enter().append('path')
.style('stroke', '#003366')
.style('stroke-width', '2px')
.attr('id', d => `linkPath-${d.source}-${d.target}`)
svg.append('text')
.style('dominant-baseline', 'central')
.selectAll('textPath')
.data(this.data.links)
.enter().append('textPath')
.attr('xlink:href', d => `#linkPath-${d.source}-${d.target}`)
.attr('startOffset', '47%')
.html('&#x27A4;')
const node = svg.append('g')
.attr('class', 'nodes')
.selectAll('g')
.data(this.data.nodes)
.enter().append('g')
.style('cursor', 'pointer')
.on('mouseover', function() {
d3.select(this).select('.nodeShape').style('fill', '#cce5ff')
})
.on('mouseout', function() {
d3.select(this).select('.nodeShape').style('fill', '#e6f2ff')
})
.on('click', d => this.renderModal(d))
// .call(d3.drag()
// .on('start', dragstarted)
// .on('drag', dragged))
node.append('rect')
.attr('class', 'nodeShape')
.attr('width', this.nodeWidth)
.attr('height', this.nodeHeight)
.attr('rx', '15')
.attr('ry', '15')
.style('stroke', '#003366')
.style('stroke-width', '0.1')
.style('fill', '#e6f2ff')
const nodeContent = node.append('foreignObject')
.attr('x', d => d.x)
.attr('y', d => d.y)
.attr('width', this.nodeWidth)
.attr('height', this.nodeHeight)
.style('color', '#003366')
.append('xhtml:div')
.attr('class', 'flowGraph-node')
nodeContent.append('p')
.attr('class', 'title')
.html(d => d.title)
nodeContent.append('p')
.html(d => d.shortDesc)
simulation
.nodes(this.data.nodes)
.on('tick', ticked);
simulation.force('link')
.links(this.data.links);
const self = this;
function ticked() {
link.attr('d', function(d) {
const x1 = d.source.x + self.nodeWidth / 2;
const y1 = d.source.y + self.nodeHeight / 2;
const x2 = d.target.x + self.nodeWidth / 2;
const y2 = d.target.y + self.nodeHeight / 2;
return `M ${x1} ${y1} L ${x2} ${y2}`;
})
node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
}
simulation.alphaTarget(1).restart()
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
}
renderModal(d) {
// Given the datum, pop up a modal showing details
// Modal should have little x in corner that removes it when clicked
this.parentElement.select('svg')
.style('opacity', 0.25)
.selectAll('*')
.attr('pointer-events', 'none')
const modalContainer = d3.select('body').append('div')
.attr('display', 'block')
.style('width', '60%')
.style('height', '60%')
.style('position', 'absolute')
.style('top', '20%')
.style('left', '20%')
.style('background-color', '#e6f2ff')
.style('border-radius', '15px')
.style('border', '1px solid #003366')
.style('font-size', '1.25rem')
modalContainer.append('h2')
.html(`${d.title} Details`)
.style('text-align', 'center')
modalContainer.append('p')
.html(d.longDesc)
.style('padding', '2% 6%')
modalContainer.append('div')
.html('X')
.style('font-weight', 'bolder')
.style('position', 'absolute')
.style('top', '2%')
.style('right', '2%')
.style('cursor', 'pointer')
.on('click', () => {
modalContainer.remove()
this.parentElement.select('svg')
.style('opacity', 1)
.selectAll('*')
.attr('pointer-events', null)
})
}
}
<!DOCTYPE html>
<html>
<head>
<title>Flow Graph Prototype for DSD</title>
<script src="https://d3js.org/d3.v4.js"></script>
<script type="text/javascript" src="./exampleData.js"></script>
<script type="text/javascript" src="./FlowGraph.js"></script>
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
<style type="text/css">
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: 'Open Sans', sans-serif;
font-size: 14px;
}
div#parent {
width: 99%;
height: 99%;
}
</style>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
const chart = new FlowGraph(document.getElementById('parent'))
})
</script>
</head>
<body>
<div id="parent"></div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment