Created
January 18, 2017 17:16
-
-
Save jgeryk/5b605a168c7d62310828cd8aacbf98ff to your computer and use it in GitHub Desktop.
Dates/Hours Prototype w/ Area Fill
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>Date/Hours Sculpter</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.4.0/d3.min.js" charset="utf-8"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.19.0/d3-legend.min.js" charset="utf-8"></script> | |
| <style> | |
| body { | |
| font: 13px sans-serif; | |
| position: relative; | |
| width: 960px; | |
| height: 500px; | |
| } | |
| .axis text { | |
| font: 10px sans-serif; | |
| } | |
| .axis path, | |
| .axis line { | |
| fill: none; | |
| stroke: #000; | |
| shape-rendering: crispEdges; | |
| } | |
| .area{ | |
| pointer-events: none; | |
| } | |
| path { | |
| stroke-width: 2; | |
| fill: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div style="position: absolute; margin-top: 15px; margin-left: 100px; color: #F3A174"> Project Manager </div> | |
| <div style="position: absolute; margin-top: 30px; margin-left: 100px; color: #6ba2ea"> Developer </div> | |
| <script> | |
| var now = new Date | |
| var future = new Date | |
| future = future.setDate(future.getDate() + 7) | |
| // set the dimensions and margins of the graph | |
| var margin = {top: 20, right: 20, bottom: 50, left: 70}, | |
| width = 960 - margin.left - margin.right, | |
| height = 500 - margin.top - margin.bottom; | |
| var x = d3.scaleTime() | |
| .domain([now, future]) | |
| .nice() | |
| .range([0, width]) | |
| var xAxis = d3.axisBottom(x).ticks(8) | |
| var dates = xAxis.scale().ticks(xAxis.ticks()[0]) | |
| var scaleMax = 12 | |
| var clicked = undefined | |
| var dragged = null, | |
| selected = null | |
| var project = { | |
| budgetedHours: 85, | |
| roles: [{title: 'Project Manager', percent: 60, color: '#F3A174'}, | |
| {title: 'Developer', percent: 40, color: "#6ba2ea"}] | |
| } | |
| // project.roles.forEach((r, i) => {r.dates = dates.map( (d, index) => { return { date: d, hours: ( (r.percent/100) * project.budgetedHours)/dates.length, percent: r.percent/dates.length, locked: false, roleIndex: i, index: index } } )}) | |
| project.roles.forEach( (r,i) => {r.dates = dates.map( | |
| (d, index) => { | |
| return { | |
| date: d, | |
| hours: ( (r.percent/100) * project.budgetedHours)/dates.length, | |
| percent: r.percent/dates.length, | |
| locked: false, | |
| roleIndex: i, | |
| index: index | |
| } | |
| } | |
| )}) | |
| d3.select(window) | |
| .on("mousemove", mousemove) | |
| .on("mouseup", mouseup) | |
| .on("keydown", keydown); | |
| var area = d3.area() | |
| .x(function(d) { return x(d.date); }) | |
| .y0(height) | |
| .y1(function(d) { return y(d.hours); }) | |
| .curve(d3.curveCardinal); | |
| var valueline = d3.line() | |
| .x(function(d) { return x(d.date); }) | |
| .y(function(d) { return y(d.hours); }) | |
| .curve(d3.curveCardinal); | |
| // valueline.interpolate('cardinal-open') | |
| var y = d3.scaleLinear().range([0, height]).domain([scaleMax, 0]) | |
| var svg = d3.select("body").append("svg") | |
| .attr("width", width + margin.left + margin.right) | |
| .attr("height", height + margin.top + margin.bottom) | |
| .append("g") | |
| .attr("transform", "translate(" + margin.left + "," + margin.top + ")") | |
| svg.append("g") | |
| .attr("transform", "translate(0," + height + ")") | |
| // .call(d3.axisLeft(y)) | |
| .call(xAxis) | |
| svg.append("g") | |
| .call(d3.axisLeft(y)) | |
| function redraw(){ | |
| svg.selectAll('.line').remove() | |
| svg.selectAll('.dot').remove() | |
| svg.selectAll('.area').remove() | |
| svg.selectAll('circle').remove() | |
| project.roles.forEach( r=> { | |
| svg.append("path") | |
| .data([r.dates]) | |
| .attr("class", "area") | |
| .attr("d", area) | |
| // .style("stroke-width", 0) | |
| .style("fill", (d,i) => {return project.roles[d[i].roleIndex].color}) | |
| .style("opacity", 0.3) | |
| var line = svg.append("path") | |
| .data([r.dates]) | |
| .attr("stroke", (d,i) => {return project.roles[d[i].roleIndex].color}) | |
| .attr("class", "line") | |
| .attr("d", valueline) | |
| .on("mousedown", function(d){ selected = dragged = d; }); | |
| svg.selectAll("dot") | |
| .data(r.dates) | |
| .enter().append("circle") | |
| .attr("r", 10) | |
| .attr("cx", function(d) { return x(d.date); }) | |
| .attr("cy", function(d) { return y(d.hours); }) | |
| .attr("fill", d => { return d.locked ? "#000" : project.roles[d.roleIndex].color}) | |
| .on("mousedown", function(d){ selected = dragged = d; }) | |
| .on("click", point => { | |
| if(point == clicked){ | |
| point.locked = !point.locked | |
| redraw() | |
| } else { | |
| clicked = point | |
| } | |
| }); | |
| }) | |
| } | |
| function mousedown() { | |
| console.log('mousedown') | |
| console.log(d3.event) | |
| } | |
| function mousemove() { | |
| if(!dragged || dragged.locked) return; | |
| else { | |
| var m = d3.mouse(svg.node()); | |
| // getDay(m[0]) | |
| day = dragged | |
| oldPct = day.percent | |
| day.hours = scaleHours(m[1]) | |
| day.percent = 100*day.hours/project.budgetedHours | |
| balance(project.roles[day.roleIndex].dates, dragged.index, oldPct) | |
| // dragged[1] = Math.max(0, Math.min(height, m[1])); | |
| redraw(); | |
| } | |
| } | |
| function mouseup() { | |
| console.log(d3.mouse(svg.node())) | |
| if(!dragged) return; | |
| // mousemove(); | |
| dragged = null; | |
| } | |
| function keydown(){ | |
| console.log('keydown') | |
| } | |
| redraw() | |
| function getDay(xPos){ | |
| dayLength = width / data.length | |
| return data[Math.round((xPos/dayLength) - 0.8)] | |
| } | |
| function scaleHours(yPos){ | |
| var invertedPct = 1-(yPos / height) | |
| var hours = invertedPct * scaleMax | |
| return hours < 0 ? 0 : (hours > scaleMax ? scaleMax : hours) | |
| } | |
| function balance(array, index, oldPct){ | |
| var totalPct = project.roles[array[index].roleIndex].percent | |
| var unlockedTotal = totalPct - array.reduce((a,b) => { | |
| var l = b.locked ? b.percent : 0 | |
| return a + l | |
| }, 0) | |
| var balancedOldPct = totalPct * oldPct / unlockedTotal; | |
| var div = 1 - balancedOldPct / totalPct | |
| var balancedPct = totalPct * array[index].percent / unlockedTotal | |
| for(var i=0; i<array.length; i++){ | |
| if (i != index && !array[i].locked){ | |
| array[i].percent = totalPct * (((1 - (balancedPct / totalPct)) * (array[i].percent / totalPct)) / div); | |
| array[i].hours = project.budgetedHours * array[i].percent/100 | |
| } | |
| } | |
| return array | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment