Skip to content

Instantly share code, notes, and snippets.

@mikelotis
Last active August 12, 2018 21:43
Show Gist options
  • Save mikelotis/67a924261a2c3726747bf887000c2672 to your computer and use it in GitHub Desktop.
Save mikelotis/67a924261a2c3726747bf887000c2672 to your computer and use it in GitHub Desktop.
Edmonton - 311 Requests - Steam Graph
width: 950
height: 750
border: yes
{
"data": [
{
"month": "Jan",
"Animal Control": 260,
"Community Standards": 2408,
"Roadway Operations": 22203,
"Drainage Operations": 2646,
"Forestry": 477,
"Traffic Engineering": 2488,
"Parks": 448,
"River Valley Operations": 115,
"Transit": 40,
"Pest": 19,
"Rangers/River Valley Trails": 6,
"Capitial City Clean Up - Community Services": 2,
"Waste": 4
},
{
"month": "Feb",
"Animal Control": 323,
"Community Standards": 3322,
"Roadway Operations": 13538,
"Drainage Operations": 2389,
"Forestry": 361,
"Traffic Engineering": 2074,
"Parks": 398,
"River Valley Operations": 109,
"Transit": 28,
"Pest": 28,
"Rangers/River Valley Trails": 4,
"Capitial City Clean Up - Community Services": 0,
"Waste": 10
},
{
"month": "Mar",
"Animal Control": 604,
"Community Standards": 4657,
"Roadway Operations": 16205,
"Drainage Operations": 6383,
"Forestry": 579,
"Traffic Engineering": 2335,
"Parks": 693,
"River Valley Operations": 253,
"Transit": 41,
"Pest": 43,
"Rangers/River Valley Trails": 4,
"Capitial City Clean Up - Community Services": 2,
"Waste": 7
},
{
"month": "Apr",
"Animal Control": 1139,
"Community Standards": 4847,
"Roadway Operations": 15864,
"Drainage Operations": 2687,
"Forestry": 1002,
"Traffic Engineering": 2545,
"Parks": 1900,
"River Valley Operations": 515,
"Transit": 37,
"Pest": 124,
"Rangers/River Valley Trails": 11,
"Capitial City Clean Up - Community Services": 1,
"Waste": 12
},
{
"month": "May",
"Animal Control": 1204,
"Community Standards": 5612,
"Roadway Operations": 15413,
"Drainage Operations": 2062,
"Forestry": 3681,
"Traffic Engineering": 3219,
"Parks": 3281,
"River Valley Operations": 610,
"Transit": 46,
"Pest": 243,
"Rangers/River Valley Trails": 7,
"Capitial City Clean Up - Community Services": 2,
"Waste": 19
},
{
"month": "Jun",
"Animal Control": 1244,
"Community Standards": 5389,
"Roadway Operations": 13363,
"Drainage Operations": 2380,
"Forestry": 6879,
"Traffic Engineering": 3211,
"Parks": 4927,
"River Valley Operations": 463,
"Transit": 42,
"Pest": 387,
"Rangers/River Valley Trails": 9,
"Capitial City Clean Up - Community Services": 2,
"Waste": 15
},
{
"month": "Jul",
"Animal Control": 1126,
"Community Standards": 4051,
"Roadway Operations": 11041,
"Drainage Operations": 3281,
"Forestry": 4705,
"Traffic Engineering": 2599,
"Parks": 2971,
"River Valley Operations": 394,
"Transit": 60,
"Pest": 397,
"Rangers/River Valley Trails": 10,
"Capitial City Clean Up - Community Services": 2,
"Waste": 18
},
{
"month": "Aug",
"Animal Control": 1037,
"Community Standards": 2532,
"Roadway Operations": 8291,
"Drainage Operations": 1604,
"Forestry": 2781,
"Traffic Engineering": 2076,
"Parks": 2199,
"River Valley Operations": 333,
"Transit": 43,
"Pest": 321,
"Rangers/River Valley Trails": 11,
"Capitial City Clean Up - Community Services": 1,
"Waste": 6
},
{
"month": "Sep",
"Animal Control": 1001,
"Community Standards": 2512,
"Roadway Operations": 7053,
"Drainage Operations": 1192,
"Forestry": 1537,
"Traffic Engineering": 2669,
"Parks": 1415,
"River Valley Operations": 254,
"Transit": 38,
"Pest": 169,
"Rangers/River Valley Trails": 7,
"Capitial City Clean Up - Community Services": 0,
"Waste": 7
},
{
"month": "Oct",
"Animal Control": 966,
"Community Standards": 2432,
"Roadway Operations": 5707,
"Drainage Operations": 1174,
"Forestry": 1107,
"Traffic Engineering": 2473,
"Parks": 891,
"River Valley Operations": 265,
"Transit": 40,
"Pest": 66,
"Rangers/River Valley Trails": 10,
"Capitial City Clean Up - Community Services": 1,
"Waste": 20
},
{
"month": "Nov",
"Animal Control": 508,
"Community Standards": 2208,
"Roadway Operations": 6620,
"Drainage Operations": 740,
"Forestry": 386,
"Traffic Engineering": 2297,
"Parks": 563,
"River Valley Operations": 193,
"Transit": 44,
"Pest": 43,
"Rangers/River Valley Trails": 3,
"Capitial City Clean Up - Community Services": 1,
"Waste": 12
},
{
"month": "Dec",
"Animal Control": 285,
"Community Standards": 1949,
"Roadway Operations": 10906,
"Drainage Operations": 646,
"Forestry": 290,
"Traffic Engineering": 1912,
"Parks": 315,
"River Valley Operations": 92,
"Transit": 22,
"Pest": 9,
"Rangers/River Valley Trails": 3,
"Capitial City Clean Up - Community Services": 0,
"Waste": 2
}
]
}

Note: These business units are not shown on the steamgraph:

  • Animal Control
  • Transit
  • Rangers/River Valley Trails
  • Capitial City Clean Up - Community Services
  • Waste

These particular business units don't contribute a lot on a montly basis. Also view the flattened file, you will notice compared to other business units they have minimal values.

Code for flattening Data

Original file (Open Data) is large thus loading in pre-processed data (flattened file)
311(July-21-2018).csv file has two columns, Date Created and Business Unit
Processed 311(July-21-2018) file as follows:

// loading large open data with only Date Created and Business Unit Columns
// Has 304,845 rows
d3.csv("311(July-21-2018).csv")
    .then(makeData);

// months order
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

function makeData(response) {
    // log(response);

    // formating data - Adding month attribute
    response.forEach(d => d.month = months[new Date(d["Date Created"]).getMonth()]);

    let total;
    let categories;
    let monthsBizUnitsNest = d3.nest().key(d => d.month)
        .key(d => d["Business Unit"]).entries(response);

    /* formating data - Recipe to ensure all months have the same bizunits
        this is not the case by default */ 
    monthsBizUnitsNest.forEach((d, i) => { 
        const values = d.values;
        const len = values.length;
        const keys = values.map(p => p.key);

        // 0 month (January has all categories) 
        // Thus get all the categories only from January
        if(i == 0) {
            total = len;
            categories = keys;
        };

        // Months with less categories than January add Object
        // Object will be {key: missing category, values: []}
        if(len != total) {
            const janSet = new Set(categories);
            const monthSet = new Set(keys);
            const missingCategories = [...difference(janSet, monthSet)];

            missingCategories.forEach(p => {
                const object = {};
                object.key = p;
                object.values = [];
                values.push(object);
            });
        };
    });

    // formating data - Each month and it's Stats for 311 Requests categories
    const monthsAgg = monthsBizUnitsNest.map(d => { 
            let object = {month: d.key}; 
            d.values.forEach(p => object[p.key] = p.values.length);

            return object;
        }).sort((a, b) => months.indexOf(a.month) - months.indexOf(b.month));

    // flattened-JSON 
    // 311(July-21-2018)-Flattened.json
    const flattenedJson = {data: monthsAgg};

    // finds the diffrence of setB from SetA
    function difference(setA, setB) {
        var _difference = new Set(setA);
        setB.forEach(elem => {
            _difference.delete(elem);
        });
        return _difference;
    };
};

Above code produced 311(July-21-2018)-Flattened.json (flattened data).

Acknowledgements

body {
margin-top: 30px;
}
svg, .collapsible, .content {
background-color: whitesmoke;
}
.domain {
display: none;
}
line {
stroke: gray;
}
text {
fill: gray;
}
.collapsible {
color: gray;
cursor: pointer;
padding: 0px 43px 10px 40px;
/* width: 50%; */
border: none;
text-align: left;
outline: none;
font-size: 15px;
}
.collapsible, .content, svg {
display: block;
width: 930px;
margin: 0 auto;
}
/* .collapsible:hover {
background-color: rgb(236, 233, 233);
} */
.collapsible:after {
content: '\002B';
color: gray;
font-weight: bold;
float: right;
margin-left: 5px;
}
.active:after {
content: "\2212";
}
.content {
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease-out;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>311 - Categories steamGraph</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<svg id="chart"></svg>
<button class="collapsible">Click to open 311 Requests Categories</button>
<div class="content">
<p style="margin: 0px 0px 10px 41px; max-height: 200px;">
<svg id="legend" width="910px" height="165px"><svg>
</p>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.5.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.25.6/d3-legend.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="index.js" lang="babel" type="text/babel"></script>
</body>
</html>
const log = console.log;
const margin = {top: 20, bottom: 20, left: 42, right: -29};
const width = 930 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
// define svg
const svg = d3.select("svg#chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const gChart = svg.append('g')
.attr("transform", `translate(${margin.left},${margin.top})`);
//load pre-processed data
d3.json("311(July-21-2018)-Flattened.json")
.then(steamGraph);
function steamGraph(response) {
// pre-processed data
const jsonData = response.data;
// Months, 311 Categories, and fill scale
const months = jsonData.map(d => d.month);
const bizUnits = Object.keys(jsonData[0]).filter(d => d != "month");
const colors = ["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462",
"#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5","#ffed6f", "#e5d8bd"];
const fillScale = d3.scaleOrdinal().domain(bizUnits).range(colors);
// x axis
const xScale = d3.scaleBand().domain(months).range([0, width]);
const xAxis = d3.axisBottom(xScale).tickSize(-height + 15);
// stack layout
const steam = d3.stack()
.offset(d3.stackOffsetSilhouette)
.order(d3.stackOrderInsideOut)
.keys(bizUnits)
(jsonData);
// x and y values returned by layout
const allCoords = steam.map(d => {
const arr = d.reduce((acc, curVal) => acc.concat(curVal),[]);
return arr;
}).reduce((acc, curVal) => acc.concat(curVal),[]);
const yScale = d3.scaleLinear().domain(d3.extent(allCoords))
.range([height, 0]);
// area generator
const stackArea = d3.area()
.x(d => xScale(d.data.month))
.y0(d => yScale(d[0]))
.y1(d => yScale(d[1]))
.curve(d3.curveBasis);
const coll = document.getElementsByClassName("collapsible");
const legSvg = d3.select("svg#legend");
// add bottom x axis
gChart.append('g')
.attr("transform", `translate(${margin.left - 80},${height})`)
.call(xAxis);
// add area paths
gChart.selectAll("path")
.data(steam)
.enter().append("path")
.style("fill", d => fillScale(d.key))
.attr('d', d => stackArea(d));
// add top x axis
gChart.append('g')
.attr("transform", `translate(${margin.left - 80},0)`)
.call(xAxis.tickSize(0));
//--------------W3School Helper Code--------------
let i;
for (i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function() {
this.classList.toggle("active");
const content = this.nextElementSibling;
if (content.style.maxHeight){
content.style.maxHeight = null;
} else {
content.style.maxHeight = content.scrollHeight + "px";
}
});
};
//--------------W3School Helper Code--------------
// Creating two 311 Requests Categories Legends
// Add legends in svg within p tag. P tag within div.content
[
{splice:[0, 8], transform: [0, 0]},
{splice:[8, colors.length], transform: [522, 0]}
].forEach((d, i) =>{
const spliceStart = d.splice[0];
const spliceEnd = d.splice[1];
const transformX = d.transform[0];
const transformY = d.transform[1];
const colorScale = legendScale(spliceStart, spliceEnd);
const legnd = legend(colorScale.minrng, colorScale.maxrng, colorScale.ls);
appendLegend(`legend${i}`, legnd, transformX , transformY);
});
// defines legend
function legend(num1, num2, scale) {
const legend = d3.legendColor()
.labels(d3.range(num1, num2).map(d => `${bizUnits[d]}`))
.scale(scale);
return legend;
};
// defines legend's fill
function legendScale(num1, num2) {
const rangeArr = d3.range(num1, num2);
const legendScale = d3.scaleOrdinal().domain(rangeArr.map(d => bizUnits[d]))
.range(rangeArr.map(d => colors[d]));
return {ls: legendScale, minrng: num1, maxrng:num2};
};
// appends legend
function appendLegend(id, legend, num1, num2) {
legSvg.append('g')
.attr("id", id)
.attr("transform", `translate(${num1},${num2})`)
.call(legend);
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment