|
<!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> |