Skip to content

Instantly share code, notes, and snippets.

@noblemillie
Last active December 18, 2019 07:50
Show Gist options
  • Save noblemillie/b8a4b6f1e375b6434b9213f7d63fb67e to your computer and use it in GitHub Desktop.
Save noblemillie/b8a4b6f1e375b6434b9213f7d63fb67e 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.

forked from tomshanley's block: Area segment fill using gradient to pre-calculate each coordinate on each path, so that the interaction is smoother.

Built with blockbuilder.org

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

<!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;
/* background-color: #554c69; */
/* background-color: rgba(12,12,12,0.8); */
/* background-color: #7f668a; */
/* background-color: #80698a; */
/* background-color: #defffd; */
background-color: #9ecdde;
/* background-image: url("https://www.transparenttextures.com/patterns/cubes.png"); */
/* background-image: url("https://www.transparenttextures.com/patterns/gplay.png"); */
background-image: url("https://www.transparenttextures.com/patterns/fresh-snow.png");
/* background-image: url("https://www.transparenttextures.com/patterns/hexellence.png"); */
/* background-image: url("https://www.transparenttextures.com/patterns/clean-textile.png"); */
/* background-image: url("https://www.transparenttextures.com/patterns/3px-tile.png"); */
}
path {
fill: none;
stroke-width: 5px;
}
circle {
fill: rgba(0,255,153, 0.8);
stroke-width: 2px;
/* opacity: 0.9; */
}
svg {
padding: 30px;
background-color: rgba(42,42,42,0.3);
}
.area-line {
stroke: rgba(19, 247, 205, 0.99);
stroke-width: 1px
}
</style>
</head>
<body>
<script>
const height = 500
const width = 500
const margin = { "top": 20, "bottom": 20, "left": 20, "right": 20 }
const colour1 = "dodgerblue"
const colour2 = "snow"
const grey = "powderblue"
const data1 = [2, 5, 6, 7, 3, 8, 3, 4,4,7,8,3,9,6]
const data2 = [6, 2, 1, 2, 0.5, 2, 4, 3,5,8,2,8,5,6]
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).style("stroke", "cyan")
g.append("g").call(yAxis).style("stroke", "red")
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)
.attr("transform", "translate(1,0)")
.style("stroke", "rgba(250, 250, 250, 0.909")
.style("stroke-width", 2)
.style("font-size", 25)
.style("fill", "rgba(214, 218, 214, 0.809")
dotsText = dots.append("text")
.style("text-anchor", "middle")
.attr("transform", "translate(1,-1)")
.style("font-size", 24)
.attr("x", 0)
.style("stroke-width", 2)
.style("stroke", "rgba(35, 250, 246, 0.3)")
//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": "cornflowerblue" },
{ "offset": offset1, "stopColour": "#badde3" },
{ "offset": offset1, "stopColour": "rgba(240,240,240,0.2)" },
{ "offset": offset2, "stopColour": "rgba(240,240,240,0.7)" },
{ "offset": offset2, "stopColour": "rgba(200,240,240,0.5)" },
{ "offset": 1, "stopColour": "rgba(126, 0, 126, 0.3)" }
])
.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