Skip to content

Instantly share code, notes, and snippets.

@praveenuics
Last active June 24, 2020 14:29
Show Gist options
  • Select an option

  • Save praveenuics/1961da999ff3cc24814fa08efa4256b6 to your computer and use it in GitHub Desktop.

Select an option

Save praveenuics/1961da999ff3cc24814fa08efa4256b6 to your computer and use it in GitHub Desktop.
fresh block
license: mit
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
html {
font-family: 'Arial', sans-serif;
}
.header {
margin-top: 60px;
margin-left: 60px;
text-align: left;
}
input {
margin-right: 10px;
}
.form-check-label {
font-size: 0.8em;
margin-right: 20px;
}
</style>
</head>
<body>
<div class ='container-fluid'>
<div class='row'>
<div id="chart">
<div class='header'>
<h3>COE Prices from 2010-2018</h3>
<p>There are 2 COE Open Bidding exercises each month (Bidding Exercise 1 starts on the first Monday while Bidding Exercise 2 starts on the third Monday of the month)</p>
<div class="form-check-inline">
<label class="form-check-label">
<input style="display: inline" type="radio" name="bidding_no" value="1" checked>Bidding Exercise 1
</label>
</div>
<div class="form-check-inline">
<label class="form-check-label">
<input type="radio" name="bidding_no" value="2">Bidding Exercise 2
</label>
</div>
</div>
</div>
</div>
</div>
<script>
var glines
var mouseG
var tooltip
var parseDate = d3.timeParse("%Y-%m")
var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"]
var margin = {top: 80, right: 200, bottom: 40, left: 80}
var width = 1400 - margin.left - margin.right
var height = 500 - margin.top - margin.bottom
var lineOpacity = 1
var lineStroke = "2px"
var axisPad = 6 // axis formatting
var R = 6 //legend marker
var category = ["Category A", "Category B", "Category C", "Category D", "Category E"]
// since Category B and E are really close to each other, assign them diverging colors
var color = d3.scaleOrdinal()
.domain(category)
.range(["#2D4057", "#7C8DA4", "#B7433D", "#2E7576", "#EE811D"]);
var data = [
{date: 2010-01, serverName: 'Server1', count: 70 },
{date: 2010-02, serverName: 'Server3', count: 60 },
{date: 2010-03, serverName: 'Server2', count: 40 },
{date: 2010-05, serverName: 'Server2', count: 20 },
{date: 2010-05, serverName: 'Server5', count: 10 }
];
var res = data.map((d,i) => {
return {
date : parseDate(d.month),
serverName: d.serverName,
count : d.count
}
})
var xScale = d3.scaleTime()
.domain(d3.extent(res, d=>d.date))
.range([0, width])
function roundToNearest10K(x) {
return Math.round(x / 10000) * 10000
}
var yScale = d3.scaleLinear()
.domain([0, roundToNearest10K(d3.max(res, d => d.count))])
.range([height, 0]);
var svg = d3.select("#chart").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 + ")");
// CREATE AXES //
// render axis first before lines so that lines will overlay the horizontal ticks
var xAxis = d3.axisBottom(xScale).ticks(d3.timeYear.every(1)).tickSizeOuter(axisPad*2).tickSizeInner(axisPad*2)
var yAxis = d3.axisLeft(yScale).ticks(10, "s").tickSize(-width) //horizontal ticks across svg width
svg.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${height})`)
.call(xAxis)
.call(g => {
var years = xScale.ticks(d3.timeYear.every(1))
var xshift = (width/(years.length))/2
g.selectAll("text").attr("transform", `translate(${xshift}, 0)`) //shift tick labels to middle of interval
.style("text-anchor", "middle")
.attr("y", axisPad)
.attr('fill', '#A9A9A9')
g.selectAll("line")
.attr('stroke', '#A9A9A9')
g.select(".domain")
.attr('stroke', '#A9A9A9')
})
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.call(g => {
g.selectAll("text")
.style("text-anchor", "middle")
.attr("x", -axisPad*2)
.attr('fill', '#A9A9A9')
g.selectAll("line")
.attr('stroke', '#A9A9A9')
.attr('stroke-width', 0.7) // make horizontal tick thinner and lighter so that line paths can stand out
.attr('opacity', 0.3)
g.select(".domain").remove()
})
.append('text')
.attr('x', 50)
.attr("y", -10)
.attr("fill", "#A9A9A9")
.text("Singapore Dollars")
// CREATE LEGEND //
var svgLegend = svg.append('g')
.attr('class', 'gLegend')
.attr("transform", "translate(" + (width + 20) + "," + 0 + ")")
var legend = svgLegend.selectAll('.legend')
.data(category)
.enter().append('g')
.attr("class", "legend")
.attr("transform", function (d, i) {return "translate(0," + i * 20 + ")"})
legend.append("circle")
.attr("class", "legend-node")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", R)
.style("fill", d=>color(d))
legend.append("text")
.attr("class", "legend-text")
.attr("x", R*2)
.attr("y", R/2)
.style("fill", "#A9A9A9")
.style("font-size", 12)
.text(d=>d)
// line generator
var line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.count))
renderChart(1) // inital chart render (set default to Bidding Exercise 1 data)
// Update chart when radio button is selected
// d3.selectAll(("input[name='bidding_no']")).on('change', function(){
// updateChart(this.value)
// })
function updateChart() {
var res_nested = d3.nest()
.key(d=>d.count)
.entries(res)
glines.select('.line') //select line path within line-group (which represents a vehicle category), then bind new data
.data(res_nested)
.transition().duration(750)
.attr('d', function(d) {
return line(d.values)
})
mouseG.selectAll('.mouse-per-line')
.data(res_nested)
mouseG.on('mousemove', function () {
var mouse = d3.mouse(this)
updateTooltipContent(mouse, res_nested)
})
}
function renderChart() {
var res_nested = d3.nest() // necessary to nest data so that keys represent each vehicle category
.key(d=>d.count)
.entries(res)
// APPEND MULTIPLE LINES //
var lines = svg.append('g')
.attr('class', 'lines')
glines = lines.selectAll('.line-group')
.data(res_nested).enter()
.append('g')
.attr('class', 'line-group')
glines
.append('path')
.attr('class', 'line')
.attr('d', d => line(d.values))
.style('stroke', (d, i) => color(i))
.style('fill', 'none')
.style('opacity', lineOpacity)
.style('stroke-width', lineStroke)
// APPEND CIRCLE MARKERS //
//var gcircle = lines.selectAll("circle-group")
//.data(res_nested).enter()
//.append("g")
//.attr('class', 'circle-group')
//gcircle.selectAll("circle")
//.data(d => d.values).enter()
//.append("g")
//.attr("class", "circle")
//.append("circle")
//.attr("cx", d => xScale(d.date))
//.attr("cy", d => yScale(d.premium))
//.attr("r", 2)
// CREATE HOVER TOOLTIP WITH VERTICAL LINE //
tooltip = d3.select("#chart").append("div")
.attr('id', 'tooltip')
.style('position', 'absolute')
.style("background-color", "#D3D3D3")
.style('padding', 6)
.style('display', 'none')
mouseG = svg.append("g")
.attr("class", "mouse-over-effects");
mouseG.append("path") // create vertical line to follow mouse
.attr("class", "mouse-line")
.style("stroke", "#A9A9A9")
.style("stroke-width", lineStroke)
.style("opacity", "0");
var lines = document.getElementsByClassName('line');
var mousePerLine = mouseG.selectAll('.mouse-per-line')
.data(res_nested)
.enter()
.append("g")
.attr("class", "mouse-per-line");
mousePerLine.append("circle")
.attr("r", 4)
.style("stroke", function (d) {
return color(d.key)
})
.style("fill", "none")
.style("stroke-width", lineStroke)
.style("opacity", "0");
mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas
.attr('width', width)
.attr('height', height)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.on('mouseout', function () { // on mouse out hide line, circles and text
d3.select(".mouse-line")
.style("opacity", "0");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "0");
d3.selectAll(".mouse-per-line text")
.style("opacity", "0");
d3.selectAll("#tooltip")
.style('display', 'none')
})
.on('mouseover', function () { // on mouse in show line, circles and text
d3.select(".mouse-line")
.style("opacity", "1");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "1");
d3.selectAll("#tooltip")
.style('display', 'block')
})
.on('mousemove', function () { // update tooltip content, line, circles and text when mouse moves
var mouse = d3.mouse(this)
d3.selectAll(".mouse-per-line")
.attr("transform", function (d, i) {
var xDate = xScale.invert(mouse[0]) // use 'invert' to get date corresponding to distance from mouse position relative to svg
var bisect = d3.bisector(function (d) { return d.date; }).left // retrieve row index of date on parsed csv
var idx = bisect(d.values, xDate);
d3.select(".mouse-line")
.attr("d", function () {
var data = "M" + xScale(d.values[idx].date) + "," + (height);
data += " " + xScale(d.values[idx].date) + "," + 0;
return data;
});
return "translate(" + xScale(d.values[idx].date) + "," + yScale(d.values[idx].count) + ")";
});
updateTooltipContent(mouse, res_nested)
})
}
function updateTooltipContent(mouse, res_nested) {
sortingObj = []
res_nested.map(d => {
var xDate = xScale.invert(mouse[0])
var bisect = d3.bisector(function (d) { return d.date; }).left
var idx = bisect(d.values, xDate)
sortingObj.push({key: d.values[idx].count, count: d.values[idx].count, year: d.values[idx].date.getFullYear(), month: monthNames[d.values[idx].date.getMonth()]})
})
sortingObj.sort(function(x, y){
return d3.descending(x.count, y.count);
})
var sortingArr = sortingObj.map(d=> d.key)
var res_nested1 = res_nested.slice().sort(function(a, b){
return sortingArr.indexOf(a.key) - sortingArr.indexOf(b.key) // rank vehicle category based on price of count
})
tooltip.html(sortingObj[0].month + "-" + sortingObj[0].year)
.style('display', 'block')
.style('left', d3.event.pageX + 20)
.style('top', d3.event.pageY - 20)
.style('font-size', 11.5)
.selectAll()
.data(res_nested1).enter() // for each vehicle category, list out name and price of count
.append('div')
.style('color', d => {
return color(d.key)
})
.style('font-size', 10)
.html(d => {
var xDate = xScale.invert(mouse[0])
var bisect = d3.bisector(function (d) { return d.date; }).left
var idx = bisect(d.values, xDate)
return d.key.substring(0, 3) + " " + d.key.slice(-1) + d.values[idx].count
})
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment