Skip to content

Instantly share code, notes, and snippets.

@tomshanley
Last active February 1, 2018 10:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tomshanley/01a87c81b5ed86b6d55e566403c175ba to your computer and use it in GitHub Desktop.
Save tomshanley/01a87c81b5ed86b6d55e566403c175ba to your computer and use it in GitHub Desktop.
Area segment fill using gradient
license: mit

Suggested answer to an SO question on filling a segment of an area between two paths

A linearGradient is created and used as fill on the area between two lines. And then a rect with 0 opacity has mousemove event attached, which uses the mouse's position to update a linearGradient's offsets' proportions.

Built with blockbuilder.org

forked from tomshanley's block: Area segment fill using gradient, to include a line on each side of the gradient fill, that gets is y1/y2 coordinates from for loop that traverses each path.

forked from tomshanley's block: Area segment fill using gradient

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body {
margin: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
path {
stroke-width: 3px
}
</style>
</head>
<body>
<script>
const height = 500
const width = 500
const margin = { "top": 20, "bottom": 20, "left": 20, "right": 20 }
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 curve = d3.curveCatmullRom.alpha(0.5)
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 offset1 = 0.2
let offset2 = 0.4
let stopsData = [
{ "offset": 0, "stopColour": "#FFFFFF" },
{ "offset": offset1, "stopColour": "#FFFFFF" },
{ "offset": offset1, "stopColour": "#777777" },
{ "offset": offset2, "stopColour": "#777777" },
{ "offset": offset2, "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)
let areaLine1 = g.append("line")
.attr("x1", width*offset1)
.attr("x2", width*offset1)
.attr("y1", 0)
.attr("y2", height)
.style("stroke", "black")
.style("stroke-width", "3px")
.style("opacity", 0)
let areaLine2 = g.append("line")
.attr("x1", width*offset2)
.attr("x2", width*offset2)
.attr("y1", 0)
.attr("y2", height)
.style("stroke", "black")
.style("stroke-width", "3px")
.style("opacity", 0)
let path1 = g.append("path")
.datum(combinedData)
.style("stroke", "red")
.style("fill", "none")
.attr("d", line1)
let path2 = g.append("path")
.datum(combinedData)
.style("stroke", "blue")
.style("fill", "none")
.attr("d", line2)
let path1Node = path1.node()
let path2Node = path2.node()
let rect = g.append("rect")
.attr("width", width)
.attr("height", height)
.style("opacity", 0)
.on("mouseover", function () {
areaFill.style("opacity", 1)
areaLine1.style("opacity", 1)
areaLine2.style("opacity", 1)
})
.on("mouseout", function () {
areaFill.transition().style("opacity", 0)
areaLine1.transition().style("opacity", 0)
areaLine2.transition().style("opacity", 0)
})
.on("mousemove", function (d) {
let x = d3.mouse(this)[0]
let middle = x / width
offset1 = (middle - 0.1) < 0 ? 0 : (middle - 0.1)
offset2 = (middle + 0.1) > 1 ? 1 : (middle + 0.1)
o1 = width * offset1
o2 = width * offset2
line1Ys = findYs(path1Node, o1, o2)
line2Ys = findYs(path2Node, o1, o2)
areaLine1.attr("x1", width*offset1)
.attr("x2", width*offset1)
.attr("y1", line1Ys[0])
.attr("y2", line2Ys[0])
areaLine2.attr("x1", width*offset2)
.attr("x2", width*offset2)
.attr("y1", line1Ys[1])
.attr("y2", line2Ys[1])
stopsData = [
{ "offset": 0, "stopColour": "#FFFFFF" },
{ "offset": offset1, "stopColour": "#FFFFFF" },
{ "offset": offset1, "stopColour": "#777777" },
{ "offset": offset2, "stopColour": "#777777" },
{ "offset": offset2, "stopColour": "#FFFFFF" },
{ "offset": 1, "stopColour": "#FFFFFF" }
]
gradient.selectAll("stop")
.data(stopsData)
.attr("offset", function (d) { return d.offset })
.attr("stop-color", function (d) { return d.stopColour })
})
function findYs(p, x1, x2) {
const accuracy = 1 //increase for quicker, but less accurate lines
let ys = [];
let i = x1;
const l = p.getTotalLength()
for (i; i < l; i+=accuracy) {
let pos = p.getPointAtLength(i)
if (pos.x > x1) {
ys.push(pos.y)
break
}
}
for (i; i < l; i+=accuracy) {
let pos = p.getPointAtLength(i)
if (pos.x > x2) {
ys.push(pos.y)
break
}
}
return ys;
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment