Skip to content

Instantly share code, notes, and snippets.

@tomshanley
Last active December 6, 2017 00:35
Show Gist options
  • Save tomshanley/373640a6bc6f89293507550ae5629488 to your computer and use it in GitHub Desktop.
Save tomshanley/373640a6bc6f89293507550ae5629488 to your computer and use it in GitHub Desktop.
Area segment fill using gradient
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css?family=Cutive+Mono" rel="stylesheet">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body {
margin: 50px;
font-family: 'Cutive Mono', monospace;
}
path {
fill: none;
stroke-width: 3px
}
circle {
fill: white;
stroke-width: 3px
}
.area-line {
stroke: black;
stroke-width: 3px
}
</style>
</head>
<body>
<script>
const height = 500
const width = 500
const margin = { "top": 20, "bottom": 20, "left": 20, "right": 20 }
const colour1 = "#4169E1"
const colour2 = "#DC143C"
const grey = "#C0C0C0"
const data1 = [2, 5, 6, 7, 3, 8, 3, 4]
const data2 = [6, 2, 2, 2, 2, 2, 4, 9]
let combinedData = []
for (var i = 0; i < data1.length; i++) {
let o = {}
o.data1 = data1[i]
o.data2 = data2[i]
combinedData.push(o)
}
let xScale = d3.scaleLinear()
.domain([0, combinedData.length - 1])
.range([0, width])
let yScale = d3.scaleLinear()
.domain([0, 10])
.range([height, 0])
let xAxis = d3.axisBottom(xScale)
let yAxis = d3.axisLeft(yScale)
let curve = d3.curveCatmullRom.alpha(0.5) //check out different curves
//let curve = d3.curveMonotoneX
//let curve = d3.curveStep
//let curve = d3.curveLinear
let area = d3.area()
.x(function (d, i) { return xScale(i) })
.y0(function (d) { return yScale(d.data1) })
.y1(function (d) { return yScale(d.data2) })
.curve(curve);
let line1 = d3.line()
.x(function (d, i) { return xScale(i) })
.y(function (d) { return yScale(d.data1) })
.curve(curve);
let line2 = d3.line()
.x(function (d, i) { return xScale(i) })
.y(function (d) { return yScale(d.data2) })
.curve(curve);
let svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
let g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
var gradient = g.append("defs").append("linearGradient")
.attr("id", "area-gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "0%");
let stopsData = [
{ "offset": 0, "stopColour": "#FFFFFF" },
{ "offset": 0, "stopColour": "#FFFFFF" },
{ "offset": 0, "stopColour": grey },
{ "offset": 0, "stopColour": grey },
{ "offset": 0, "stopColour": "#FFFFFF" },
{ "offset": 1, "stopColour": "#FFFFFF" }
]
gradient.selectAll("stop")
.data(stopsData)
.enter()
.append("stop")
.attr("offset", function (d) { return d.offset })
.attr("stop-color", function (d) { return d.stopColour })
let areaFill = g.append("path")
.datum(combinedData)
.style("fill", "url(#area-gradient)")
.style("opacity", 0)
.attr("d", area)
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
g.append("g").call(yAxis)
let areaLine1 = g.append("line")
.attr("class", "area-line")
.style("opacity", 0)
let areaLine2 = g.append("line")
.attr("class", "area-line")
.style("opacity", 0)
let path1 = g.append("path")
.datum(combinedData)
.style("stroke", colour1)
.attr("d", line1)
let path2 = g.append("path")
.datum(combinedData)
.style("stroke", colour2)
.attr("d", line2)
let path1Node = path1.node()
let path2Node = path2.node()
let path1NodeLength = path1Node.getTotalLength()
let path2NodeLength = path2Node.getTotalLength()
//for every x coordinate, get the y coordinates for each line
//and store for use later on
let allCoordinates = []
let x = 0;
for (x; x < width; x++) {
let obj = {}
obj.y1 = findY(path1Node, path1NodeLength, x, width)
obj.y2 = findY(path2Node, path2NodeLength, x, width)
allCoordinates.push(obj)
}
let dots = g.selectAll(".dot")
.data([1, 1, 1, 1])
.enter()
.append("g")
.style("opacity", 0)
dots.append("circle")
.attr("r", 8)
dotsBgdText = dots.append("text")
.style("text-anchor", "middle")
.attr("x", 0)
.style("stroke", "white")
.style("stroke-width", 3)
.style("fill", "white")
dotsText = dots.append("text")
.style("text-anchor", "middle")
.attr("x", 0)
.style("stroke", "none")
//Add a rect to handle mouse events
let rect = g.append("rect")
.attr("width", width)
.attr("height", height)
.style("opacity", 0)
.on("mouseover", showArea)
.on("mouseout", hideArea)
.on("mousemove", function (d) {
let middle = d3.mouse(this)[0] / width
let offset = 0.1;
offset1 = (middle - offset) < 0 ? 0 : (middle - offset)
offset2 = (middle + offset) > 1 ? 1 : (middle + offset)
let x1 = Math.floor(width * offset1) < 0 ? 0 : Math.floor(width * offset1)
let x2 = Math.floor(width * offset2) > 499 ? 499 : Math.floor(width * offset2)
areaLine1.attr("x1", x1)
.attr("x2", x1)
.attr("y1", allCoordinates[x1].y1)
.attr("y2", allCoordinates[x1].y2)
areaLine2.attr("x1", x2)
.attr("x2", x2)
.attr("y1", allCoordinates[x2].y1)
.attr("y2", allCoordinates[x2].y2)
let dotsData = [
{ "cx": x1, "cy": allCoordinates[x1].y1, "colour": colour1 },
{ "cx": x1, "cy": allCoordinates[x1].y2, "colour": colour2 },
{ "cx": x2, "cy": allCoordinates[x2].y1, "colour": colour1 },
{ "cx": x2, "cy": allCoordinates[x2].y2, "colour": colour2 }
]
dots.data(dotsData)
.attr("transform", function (d) { return "translate(" + d.cx + "," + d.cy + ")" })
.style("stroke", function (d) { return d.colour })
dotsText.data(dotsData)
.text(function (d) {
return roundNumber(yScale.invert(d.cy));
})
.attr("y", function (d) {
let maxY = d3.max(dotsData, function (e) { return e.cx == d.cx ? e.cy : 0 })
return d.cy == maxY ? 27 : -15;
})
.style("fill", function (d) { return d.colour })
dotsBgdText.data(dotsData)
.text(function (d) {
return roundNumber(yScale.invert(d.cy));
})
.attr("y", function (d) {
let maxY = d3.max(dotsData, function (e) { return e.cx == d.cx ? e.cy : 0 })
return d.cy == maxY ? 27 : -15;
})
gradient.selectAll("stop")
.data([
{ "offset": 0, "stopColour": "#FFFFFF" },
{ "offset": offset1, "stopColour": "#FFFFFF" },
{ "offset": offset1, "stopColour": grey },
{ "offset": offset2, "stopColour": grey },
{ "offset": offset2, "stopColour": "#FFFFFF" },
{ "offset": 1, "stopColour": "#FFFFFF" }
])
.attr("offset", function (d) { return d.offset })
.attr("stop-color", function (d) { return d.stopColour })
})
function showArea() {
areaFill.style("opacity", 1)
areaLine1.style("opacity", 1)
areaLine2.style("opacity", 1)
dots.style("opacity", 1)
}
function hideArea() {
areaFill.transition().style("opacity", 0)
areaLine1.transition().style("opacity", 0)
areaLine2.transition().style("opacity", 0)
dots.transition().style("opacity", 0)
}
//iteratively search a path to get a point close to a desired x coordinate
function findY(path, pathLength, x, width) {
const accuracy = 1 //px
const iterations = Math.ceil(Math.log10(accuracy/width) / Math.log10(0.5)); //for width (w), get the # iterations to get to the desired accuracy, generally 1px
let i = 0;
let nextLengthChange = pathLength / 2;
let nextLength = pathLength / 2;
let y = 0;
for (i; i < iterations; i++) {
let pos = path.getPointAtLength(nextLength)
y = pos.y
nextLength = x < pos.x ? nextLength - nextLengthChange : nextLength + nextLengthChange
nextLengthChange = nextLengthChange / 2
}
return y
}
function roundNumber(n) {
return Math.round(n * 100) / 100
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment