axis scale shift in d3.js - animation

I am fairly new to d3.js
I am looking for a way to animate both x and y axises based on the new data. So it is more of a real time animation where the x axis is moving and the new data pops out from the right and y axis get updated dynamically as well and after a while the old data dissapear because I have so many data points.
I have this chart already made. https://jsfiddle.net/elvalencian/mfLjovx9/4/
// set the dimensions and margins of the graph
const margin = {
top: 40,
right: 80,
bottom: 60,
left: 50
},
width = 600 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3
.select("#root")
.append("svg")
.attr(
"viewBox",
`0 0 ${width + margin.left + margin.right} ${
height + margin.top + margin.bottom}`)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Read the data
d3.csv("https://raw.githubusercontent.com/sultanmalki/d3js/main/saudi_fdi.csv",
// When reading the csv, I must format variables:
function(d) {
return {
date: d3.timeParse("%Y")(d.date),
value: d.value
}
},
// Now I can use this dataset:
function(data) {
// Add X axis --> it is a date format
var x = d3.scaleTime()
.domain(d3.extent(data, function(d) {
return d.date;
}))
.range([0, width]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "axis")
.transition().duration(5000)
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return +d.value;
})])
.range([height, 0]);
svg.append("g")
.attr("class", "axis")
.transition()
.ease(d3.easeLinear)
.duration(5000)
.call(d3.axisLeft(y));
// Add the line
const linePath = svg
.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#00B0F1")
.attr("stroke-width", 1.5)
.attr("d", d3.line().curve(d3.curveCardinal)
.x(function(d) {
return x(d.date)
})
.y(function(d) {
return y(d.value)
})
)
const pathLength = linePath.node().getTotalLength();
linePath
.attr("stroke-dasharray", pathLength)
.attr("stroke-dashoffset", pathLength)
.attr("stroke-width", 3)
.transition()
.attr("transform", "translate(" + ")")
.duration(5000)
.attr("stroke-width", 3)
.attr("stroke-dashoffset", 0);
})
I would really appreciate any help.
thank you in advance

Lines are rather difficult to animate, since they are only one path object instead of multiple objects as e. g. in a bar or scatter plot. You are already using the stroke-dasharray attribute for the animation of the static data. When there is new data, you need to
Rescale the axes:
To achieve this, compute the domain for x and y and set it via the domain method. Then re-render the axes with call(AxisObject) using a transition. Use the same transition t for both x and y.
Rescale the existing line
With the rescaled axes, also the existing line path must be rescaled. This works smoothly by transitioning the d attribute using the transition t before binding the new data.
Add new data
Wait till the end of transition t to bind the new data to the line path. Before doing that, calculate getTotalLength in order to set stroke-dasharray such that the new data is initially hidden. Then transition stroke-dasharray to the new path length. As the second value for stroke-dasharray I used 9999 which must be chosen longer than the maximal expected path length of the new data.
// Some stuff to generate random time series
// Standard Normal variate using Box-Muller transform.
function randn() {
let u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
// Simulate geometric brownian motion
const mu = 0.8;
const sigma = 0.5;
function simulate() {
const prev = data[data.length - 1];
const x = prev.x + 0.01;
const bm = prev.bm + Math.sqrt(0.01) * randn();
data.push({
x: x,
bm: bm,
y: Math.exp((mu - sigma * sigma / 2) * x + sigma * bm)
});
}
// Initial data
let data = [{
x: 0,
bm: 0,
y: 1,
}];
// Add data to chart in chunks
const blockSize = 20;
let blockCounter = 0;
function addData() {
simulate();
blockCounter += 1;
if (blockCounter === blockSize) {
render(data.slice());
blockCounter = 0;
}
}
// Chart definitions
const width = 500,
height = 180,
marginLeft = 30,
marginRight = 10,
marginBottom = 30,
marginTop = 10;
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
const xSlidingWindow = 2;
const x = d3.scaleLinear()
.range([marginLeft, width - marginRight]);
const y = d3.scaleLinear()
.range([height - marginBottom, marginTop]);
const xAxis = d3.axisBottom(x);
const yAxis = d3.axisLeft(y).ticks(3);
const line = d3.line()
.x(d => x(d.x))
.y(d => y(d.y));
const gx = svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`);
const gy = svg.append("g")
.attr("transform", `translate(${marginLeft},0)`);
// Clip path to only show lines inside the axes
const clipPath = svg.append("clipPath")
.attr("id", "clip-rect")
.append("rect")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom);
const path = svg.append("path")
.datum(data.slice())
.attr("clip-path", "url(#clip-rect)")
.attr("fill", "none")
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "0, 9999");
function render(arr) {
// compute domain
const xMax = d3.max(arr, d => d.x);
x.domain([Math.max(xMax - xSlidingWindow, 0), Math.max(xSlidingWindow, xMax)]);
y.domain(d3.extent(arr, d => d.y));
// First, transition the axes
const t = d3.transition().duration(interval * blockSize / 2);
gx.transition(t).call(xAxis);
gy.transition(t).call(yAxis);
path.transition(t).attr("d", line);
t.on("end", () => {
// Then add new data
let pathLength = path.node().getTotalLength();
path.datum(arr)
.attr("stroke-dasharray", `${pathLength}, 9999`)
.attr("d", line);
pathLength = path.node().getTotalLength();
path.transition().duration(interval * blockSize / 2)
.attr("stroke-dasharray", `${pathLength}, 9999`)
.attr("d", line);
});
}
// Interval for data simulation
let intervalId;
const interval = 50;
function startStream() {
if (!intervalId) {
intervalId = setInterval(addData, interval);
}
}
function stopStream() {
clearInterval(intervalId);
intervalId = null;
}
function reset() {
clearInterval(intervalId);
data = [{
x: 0,
bm: 0,
y: 1,
}];
intervalId = setInterval(addData, interval);
}
d3.select("#start").on("click", startStream);
d3.select("#stop").on("click", stopStream);
d3.select("#reset").on("click", reset);
render(data.slice());
startStream();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.3.0/d3.min.js"></script>
<div>
<button id="start">Start</button>
<button id="stop">Stop</button>
<button id="reset">Reset</button>
</div>
<svg></svg>

Related

semicircle bar graphs with extended edges using d3.js

I'm trying to draw a d3 chart with extended edges like in the image, "this is the link to the design"
I was able to achieve a semi circle in the same fashion, but I'm a little confused how to do the extended edge, this is the code for what I have done so far, link to codepen
JS:
var width = 300,
height = 300;
var twoPi = Math.PI; // Full circle
var formatPercent = d3.format(".0%");
const color = [
"#F9C969",
"#FB8798",
"#51D6D8",
"#B192FD",
"#509FFD",
"#5B65B7"
];
console.log(d3.schemeCategory10);
var data = [
{ count: 1000 },
{ count: 800 },
{ count: 800 },
{ count: 700 },
{ count: 900 },
{ count: 600 }
];
var percent = d3.max(data, function (d) {
return +d.count / 10;
});
var max = d3.max(data, function (d) {
return +d.count;
});
var baseRad = 0.25,
cgap = 12,
maxVal = max + percent;
var cx1 = width / 2.5;
var cy1 = height / 2.5;
var cl = "c0";
var ind = 0;
var rad;
var rad2;
rad = baseRad;
rad2 = baseRad;
var svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 10 + "," + height / 10 + ")");
var svg2 = d3
.select("svg")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 10 + "," + height / 10 + ")");
svg2
.selectAll("path")
.data(data)
.enter()
.append("path")
// .each(drawBackArc)
.each(drawArc)
.style("fill", function (d, i) {
return color[i % 6];
});
svg
.selectAll("path")
.data(data)
.enter()
.append("path")
// .each(drawBackArc)
.each(drawBackArc)
.style("fill", "#F1F1F1");
// .attr("ax", "-100px")
// .attr("ay", "-100px");
function drawArc(d, i) {
console.log(d, i);
var ratio = d.count / maxVal;
var arc = d3.svg
.arc()
.startAngle(3.14159)
// .(true)
.endAngle(6.28319 * ratio)
.innerRadius(72 + cgap * rad)
.outerRadius(80 + cgap * rad);
d3.select(this)
.attr("transform", "translate(" + cx1 + "," + cy1 + ")")
.attr("d", arc)
.style("fill", function (d, i) {
return color[i % 6];
});
rad++;
}
function drawBackArc(d, i) {
var ratio = d.count / maxVal;
var arc = d3.svg
.arc()
.startAngle(twoPi)
// .(true)
.endAngle(twoPi * 2)
.innerRadius(72 + cgap * rad2)
.outerRadius(80 + cgap * rad2);
d3.select(this)
.attr("transform", "translate(" + cx1 + "," + cy1 + ")")
.attr("d", arc)
.style("fill", "#F1F1F1");
rad2++;
}
HTML:
<script src="https://d3js.org/d3.v3.min.js"></script>
<body></body>
CSS:
body{background-color: #fff;margin: 1.5rem 6rem}
I have seen tutorial explaining how to draw different shapes in d3.js and I can think of drawing a rectangle shape at one end to achieve the design, but even then the issue is how to get the data in both the separate shapes, is it possible in d3? if not please suggest any other possible ways if any.
Thanks
Since you know your center point, you added 2 translations (30,30) and (120,120), so your center point is 150,150
Now you can get the end points of all the arcs, x value be same as centerpoint and y after adjusting radius.
Added below changes to your code Please adjust your graph for length and width of the line. Also add the length of the line to the lenght of arc to get correct percantage and overlap with filled line same as below with desired length if percentage increase the length of an arc
var centerPoint = [150, 150] //added for translation
var radius = 72 + cgap * rad2;
gLines.append("line")
.attr("x1", centerPoint[0])
.attr("x2", centerPoint[0] + 140) // Add length of the bar
.attr("y1", centerPoint[0] - radius + 16)
.attr("y2", centerPoint[0] - radius + 16) // This will adjust line width and inner and outer radius
.style("stroke", "#F2F2F2")
.style("stroke-width", "8");
var width = 300,
height = 300;
var twoPi = Math.PI; // Full circle
var formatPercent = d3.format(".0%");
const color = [
"#F9C969",
"#FB8798",
"#51D6D8",
"#B192FD",
"#509FFD",
"#5B65B7"
];
console.log(d3.schemeCategory10);
var data = [{
count: 500,
color: "#F9C969"
},
{
count: 800,
color: "#FB8798"
},
{
count: 800,
color: "#51D6D8"
},
{
count: 700,
color: "#B192FD"
},
{
count: 900,
color: "#509FFD"
},
{
count: 600,
color: "#5B65B7"
}
];
var percent = d3.max(data, function(d) {
return +d.count / 10;
});
var max = d3.max(data, function(d) {
return +d.count;
});
var baseRad = 0.25,
cgap = 12,
maxVal = max + percent;
var cx1 = width / 2.5;
var cy1 = height / 2.5;
var cl = "c0";
var ind = 0;
var rad;
var rad2;
rad = baseRad;
rad2 = baseRad;
var svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 10 + "," + height / 10 + ")");
var svg2 = d3
.select("svg")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 10 + "," + height / 10 + ")");
var gLines = d3.select("svg").append("g");
svg2
.selectAll("path")
.data(data)
.enter()
.append("path")
// .each(drawBackArc)
.each(drawArc)
.style("fill", function(d, i) {
return color[i % 6];
});
svg
.selectAll("path")
.data(data)
.enter()
.append("path")
// .each(drawBackArc)
.each(drawBackArc)
.style("fill", "#F1F1F1");
// .attr("ax", "-100px")
// .attr("ay", "-100px");
function drawArc(d, i) {
console.log(d, i);
var ratio = (d.count * 2) / maxVal;
console.log(ratio);
var arc = d3.svg
.arc()
.startAngle(twoPi)
// .(true)
.endAngle(twoPi * ratio)
.innerRadius(72 + cgap * rad)
.outerRadius(80 + cgap * rad);
d3.select(this)
.attr("transform", "translate(" + cx1 + "," + cy1 + ")")
.attr("d", arc)
.style("fill", function(d, i) {
return color[i % 6];
});
rad++;
}
function drawBackArc(d, i) {
var ratio = d.count / maxVal;
var arc = d3.svg
.arc()
.startAngle(twoPi)
// .(true)
.endAngle(twoPi * 2)
.innerRadius(72 + cgap * rad2 - 20)
.outerRadius(80 + cgap * rad2 - 20);
d3.select(this)
.attr("transform", "translate(" + cx1 + "," + cy1 + ")")
.attr("d", arc)
.style("fill", "#F1F1F1");
var centerPoint = [150, 150] //added for translation
var radius = 72 + cgap * rad2;
gLines.append("line")
.attr("x1", centerPoint[0])
.attr("x2", centerPoint[0] + 140) // Add Width of the
.attr("y1", centerPoint[0] - radius + 16)
.attr("y2", centerPoint[0] - radius + 16)
.style("stroke", "#F2F2F2")
.style("stroke-width", "8");
rad2++;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<body></body>

d3.js geoGraticule doesn't follow the sphere shape

I am trying to add standard geoGraticule lines to my globe, which spins and has ripple effects. I've gone wrong somewhere but can't get to the bottom of it: live demo
The meridian lines seem to create a sort of diamond shape, rather than curving around the whole sphere.
How can I change my code so the graticule is spherical?
// margin calculations
const margin = {top: 50, right: 0, bottom: 50, left: 0};
const width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
const config = {
speed: 0.005,
verticalTilt: -30,
horizontalTilt: 0
}
// add an SVG to the body and a g group
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var g = svg.append("g");
// draw a globe I guess
d3.json("https://unpkg.com/world-atlas#1.1.4/world/110m.json", (error, world) => {
// draw the land using TOPOJSON package
var land = topojson.feature(world, world.objects.land);
// d3 3d globe projection
var projection = d3.geoOrthographic()
.fitSize([width, height], land)
.clipAngle(90)
.precision(0)
.scale(200);
// create projection of earth sphere
var path = d3.geoPath()
.projection(projection);
// load the ripple circle
var geoCircle = d3.geoCircle();
// load the meridian lines
var graticule = d3.geoGraticule();
// draw a grey circle to look like water
var ocean = g.append("circle")
.attr("class", "ocean")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("r", height / 2);
// draw countries
var earth = g.append("path")
.datum(land)
.attr("d", path);
// draw graticules
var lines = g.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path)
.style("fill-opacity", 0)
.style("stroke", "#ccc")
.style("stroke-opacity", 0.7);
// list of locations - reverse the lat / long numbers!
var locale = [
[37.5999, 14.0153],
[10.1278, 50.5074],
[0.1278, 51.5074],
[25.653906, 4.453784],
[0, -10],
[151.2012775, -33.8844644]
];
// draw the circles
function drawMarkers() {
for (var i = 0; i < locale.length; i++) {
// console.log("location " + (i+1) + ": " + locale[i]);
center = locale[i];
var circ = g.append("path")
.datum({endAngle: 0})
.attr("class", "geoCircle")
.attr("fill", "#000")
.attr("fill-opacity", 1)
.attr("d", d => path(geoCircle.center(center).radius(1)()));
}
}
// draw the ripples
setInterval(() => {
for (var i = 0; i < locale.length; i++) {
center = locale[i];
var circ = g.append("path")
.datum({endAngle: 0})
.attr("class", "ripple ripple" + i)
.attr("fill", "#000")
.attr("fill-opacity", 0.2)
.attr("opacity", 1)
.attr("d", d => path(geoCircle.center(center).radius(2)()))
.transition()
.delay(0)
.duration(2800)
.ease(d3.easeLinear)
.attr("opacity", 0)
.attrTween("d", geoCircleTween(12, center))
.remove();
function geoCircleTween(newAngle, loca) {
return function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return path(geoCircle.center(loca).radius(d.endAngle)());
};
};
}
}
}, 500)
function enableRotation() {
d3.timer(function (elapsed) {
projection.rotate([config.speed * elapsed - 120, config.verticalTilt, config.horizontalTilt]);
svg.selectAll("path").attr("d", path);
drawMarkers();
});
}
enableRotation();
});

Dynamic update to y-axis of a zoomable area chart

On zoom and pan the y-axis isn't updated to the maximum value in the visible zoomed dataset e.g. when the max value is 3500 the y axis still has ticks for 3500, 4000, 4500, 5000 & 5500 which restricts the display. Can the new max value for the filtered data be more accurately updated?
const height = 400,
width = 800;
const margin = {
top: 20,
right: 20,
bottom: 30,
left: 50
};
const parser = d3.timeParse("%Y-%m-%d");
const url = "https://static.observableusercontent.com/files/4e532df03705fa504e8f95c1ab1c114ca9e89546bf14d697c73a10f72028aafd9eb3d6ea2d87bb6b421d9707781b8ac70c2bf905ccd60664f9e452a775fe50ed?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27Book1%25401.csv";
d3.csv(url, function(d) {
return {
date: parser(d.date),
value: +d.value
};
}).then(function(data) {
const x = d3.scaleUtc()
.domain(d3.extent(data, d => d.date))
.range([margin.left, width - margin.right]),
y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height - margin.bottom, margin.top]),
xAxis = (g, x) => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0)),
yAxis = (g, y) => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).ticks(5))
.call(g => g.select(".domain").remove())
.call(g => g.select(".tick:last-of-type text").clone()
.attr("x", 3)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text(data.y)),
area = (data, x) => d3.area()
.curve(d3.curveStepAfter)
.x(d => x(d.date))
.y0(y(0))
.y1(d => y(d.value))
(data)
const zoom = d3.zoom()
.scaleExtent([1, 32])
.extent([
[margin.left, 0],
[width - margin.right, height]
])
.translateExtent([
[margin.left, -Infinity],
[width - margin.right, Infinity]
])
.on("zoom", zoomed);
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
svg.append("clipPath")
.attr("id", "myclip")
.append("rect")
.attr("x", margin.left)
.attr("y", margin.top)
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
const path = svg.append("path")
.attr("clip-path", "url('#myclip')")
.attr("fill", "steelblue")
.attr("d", area(data, x));
const gx = svg.append("g")
.call(xAxis, x);
const gy = svg.append("g")
.call(yAxis, y);
svg.call(zoom)
.transition()
.duration(750)
.call(zoom.scaleTo, 1, [x(Date.UTC(2020, 8, 1)), 0]);
function zoomed(event) {
const xz = event.transform.rescaleX(x);
const yz = event.transform.rescaleY(y);
path.attr("d", area(data, xz));
gx.call(xAxis, xz);
gy.call(yAxis, yz);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.js"></script>
<svg></svg>
TLDR
Y scale's domain contains still max value from whole dataset. Update the domain of the y scale to the max value of the currently visible dataset, by filtering it first by the rescaled x scales' domain.
Long version
I guess, your chart should only zoom in the X direction. Under this assumption, you need to implement an auto scaling for the y axis yourself. The current problem is, that your y scale's domain contains the min and max values of your whole data set. Now that you have zoomed the max value might be smaller.
So, what you need to do is, get the domain of the rescaled x scale domain. Use that domain to filter your dataset for that time range and then pick the max value out of that time range filtered subset. Then you update the domain for your y scale with that new max value and rerender. By the way, rescaling the y scale is not necessary, if you only want to do zoom on the x axis.
// global cache for the data
const data;
function zoomed(event) {
const xz = event.transform.rescaleX(x);
const [minX, maxX] = xz.domain();
const filteredData = data.filter((item) => item.date >= minX && item.date <= maxX);
y.domain([0, d3.max(filteredData, d => d.value)]);
const yz = event.transform.rescaleY(y);
path.attr("d", area(data, xz));
gx.call(xAxis, xz);
gy.call(yAxis, yz);
}
Thanks for guidance: https://observablehq.com/#steve-pegg/zoomable-area-chart
Changes below have worked.
function zoomed(event) {
var xz = event.transform.rescaleX(x);
var startDate = xz.domain()[0];
var endDate = xz.domain()[1];
var fData = data.filter(function (d) {
var date = new Date(d.date);
return (date >= startDate && date <= endDate);});
y.domain([0, d3.max(fData, function (d) { return d.value; })]);
path.attr("d", area(data, xz));
gx.call(xAxis, xz);
gy.call(yAxis, y);
}

D3 line graph show positive and negative numbers

I'm working with D3 to create a line-graph. This graph is available here jsfiddle.
I'm trying to draw lines manually to represent certain data-point-values. I've tried to add comments to most of the lines in the code, so hopefully you can follow along.
My problem is that I cannot seem to draw negative numbers in a good way, if i do, then the graph-data-lines are misaligned. So my question is: How can i scale my graph so that I can show both negative and positive numbers? In this case, the graph should go from 2 to -2 based on the max/min values i've set.
currently. I'm scaling my graph like this
//
// Setup y scale
//
var y = d3.scale.linear()
.domain([0, max])
.range([height, 0]);
//
// Setup x scale
//
var x = d3.time.scale()
.domain(d3.extent(data, dateFn))
.range([0, width]);
In my mind, doing .domain([-2,max]) would be sufficient, but that seems to make things worse.
Also, my lines do not seem to match what the data-lines are saying. In the jsfiddle, the green line is set at 1. But the data-lines whose value are 1, are not on that green line.
So, this is pretty much a scale question i guess.
Visual (picasso-esc) representation of what the graph should look like if it worked.
As you want your y domain to be [-2, 2] as opposed to be driven by the data, you can remove a lot of setup and helper functions from your drawGraph function.
After drawing your graph, you can simply loop through the yLines array, and draw a line for each with the specified color, at the specified val according to your yScale.
Update: EDITED: As you will be supplied the values of nominal, upperTolerance, lowerTolerance, innerUpperTolerance, innerLowerTolerance from your endpoint (and they don't need to be calculated from the data on the client side), just feed those values into your data-driven yScale to draw the coloured lines.
Below I have just used the values 1, 1.8, -1.8, but you will receive values that will be more meaningfully tied to your data.
// Setup
const yLines = [{
val: 1,
color: 'green'
},
{
val: 1.8,
color: 'yellow'
},
{
val: -1.8,
color: 'red'
}
]
const margin = {
top: 10,
right: 80,
bottom: 60,
left: 20
};
const strokeWidth = 3;
const pointRadius = 4;
const svgWidth = 600;
const svgHeight = 600;
const width = svgWidth - margin.left - margin.right;
const height = svgHeight - margin.top - margin.bottom;
const stroke = '#2990ea'; // blue
const areaFill = 'rgba(41,144,234,0.1)'; // lighter blue
const format = d3.time.format("%b %e %Y");
const valueFn = function(d) {
return d.value
};
const dateFn = function(d) {
return format.parse(d.name)
};
// select the div and append svg to it
const graph = d3.select('#chart').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.style('overflow', 'visible');
const transformGroup = graph.append('g')
.attr('tranform', `translate(${margin.left}, ${margin.right})`)
// Make a group for yLines
const extraLines = transformGroup.append('g')
.attr('class', 'extra-lines')
// Generate some dummy data
const getData = function() {
let JSONData = [];
for (var i = 0; i < 30; i++) {
JSONData.push({
"name": moment().add(i, 'days').format('MMM D YYYY'),
"value": Math.floor(Math.random() * (Math.floor(Math.random() * 20))) - 10
})
}
return JSONData.slice()
}
const drawGraph = function(data) {
console.log(data)
// Setup y scale
const y = d3.scale.linear()
.domain(d3.extent(data.map((d) => d.value)))
.range([height, 0]);
// Setup y axis
const yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10)
.tickSize(0, 0, 0)
// append group & call yAxis
transformGroup.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + margin.left + ",0)")
.call(yAxis);
// Draw extra coloured lines from yLines array
extraLines.selectAll('.extra-line')
.data(yLines)
.enter()
.append('line')
.attr('class', 'extra-line')
.attr('x1', margin.left)
.attr('x2', svgWidth - margin.right)
.attr('stroke', d => d.color)
.attr('y1', d => y(+d.val))
.attr('y2', d => y(+d.val))
.attr('stroke-width', strokeWidth)
.attr('opacity', 0.5)
// Setup x scale
const x = d3.time.scale()
.domain(d3.extent(data, dateFn))
.range([0, width])
// function for filling area under chart
const area = d3.svg.area()
.x(d => x(format.parse(d.name)))
.y0(height)
.y1(d => y(d.value))
// function for drawing line
const line = d3.svg.line()
.x(d => x(format.parse(d.name)))
.y(d => y(d.value))
const lineStart = d3.svg.line()
.x(d => x(format.parse(d.name)))
.y(d => y(0))
// make the line
transformGroup.append('path')
.attr('stroke', stroke)
.attr('stroke-width', strokeWidth)
.attr('fill', 'none')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
.attr('d', lineStart(data))
.attr('d', line(data))
// fill area under the graph
transformGroup.append("path")
.datum(data)
.attr("class", "area")
.attr('fill', areaFill)
.attr('transform', `translate(${margin.left}, ${margin.top})`)
.attr('d', lineStart(data))
.attr("d", area)
}
drawGraph(getData())
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<div id="chart" style="margin: 0 auto;"></div>

D3.JS: Zig zag line on y-axis between zero and start value

I've modified the y.domain of my D3 bar chart so it starts at a value above zero. However, I want to add a little "zig zag line" to indicate this, as in the picture below.
How could I do this in D3? Many thanks!
I'd just hack this on by adding another path to the y axis:
// define how much space you'd like to create the axis "break" in
var axisBreakSpace = 50;
// Add the X Axis, with the space
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height + axisBreakSpace) + ")")
.call(xAxis);
// Add the Y Axis, normally
var yG = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// add the zigzags path
yG.append("path")
.attr("d", function(){
var numZags = 10, // number of zigzags
zagDist = (axisBreakSpace - 5) / numZags; // y distance on each zig or zag, -5 is a bit of space to finish it off
// build the path at
var curZig = height,
d = "M0," + curZig;
for (var i = 0; i < numZags; i++){
curZig += zagDist;
d += (i % 2 === 0) ? " L10," + curZig : " L-10," + curZig;
}
// finish it off to the x-axis
d += " L0," + (height + axisBreakSpace);
return d;
});
Full working code sample:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/* set the CSS */
body {
font: 12px Arial;
}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
// Set the dimensions of the canvas / graph
var margin = {
top: 30,
right: 20,
bottom: 100,
left: 50
},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom,
axisBreakSpace = 50;
// Set the ranges
var x = d3.scale.linear().range([0, width])
.domain([0, 10]);
var y = d3.scale.linear().range([height, 0])
.domain([200,1000]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom")
var yAxis = d3.svg.axis().scale(y)
.orient("left");
// Define the line
var line = d3.svg.line()
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return y(d.y);
});
// Adds the svg canvas
var svg = d3.select("body")
.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 + ")");
var data = d3.range(10).map(function(d){
return {
x: d,
y: (Math.random() * 800) + 200
}
});
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", line(data));
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height + axisBreakSpace) + ")")
.call(xAxis);
// Add the Y Axis
var yG = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
yG.append("path")
.attr("d", function(){
var numZags = 10,
zagDist = (axisBreakSpace - 5) / numZags;
var curZig = height,
d = "M0," + curZig;
for (var i = 0; i < numZags; i++){
curZig += zagDist;
d += (i % 2 === 0) ? " L10," + curZig : " L-10," + curZig;
}
d += " L0," + (height + axisBreakSpace);
return d;
});
</script>
</body>
I would create some data and pass it to the D3 library. Something similar to this :
var data = [{
x1: xAxisSTARTPOINTX, //start
y1; xAxisSTARTPOINTY,
x2: firstXPointOnZigZag,
y2; firstYPointOnZigZag},{
.
.
. },{
x1: lastXPointOnZigZag, //end
y1; lastYPointOnZigZag,
x2: yAxisSTARTPOINTX,
y2; yAxisSTARTPOINTY}
}]
The values you put between will be the points on the zig zag which you can make up/generate.
Then pass this to this :
d3.select(container).data(data).enter().append('path')
.attr('x1', function(d){ return d.x1})
.attr('y1', function(d){ return d.y1})
.attr('x2', function(d){ return d.x2})
.attr('y2', function(d){ return d.y2})
.style('stroke','black');
You could generate the points yourself so you can change how many 'zigzags' you want by changing 'i' in the for loop.
A function to create points, something similar to this :
function createPoints(xAxisStartPoint, yAxisStartPoint){ //pass two arrays
var xAxisStartX = xAxisStartPoint[0], //xAxisStartPointX
xAxisStartY = xAxisStartPoint[1], //xAxisStartPointY
yAxisStartX = yAxisStartPoint[0], //xAxisStartPointX
yAxisStartY = yAxisStartPoint[1]; //yAxisStartPointY
var difference = xAxisStartY-yAxisStartY; //gets the difference between xAxis and yAxis to make sure the points are equal distance apart.
var allPoints = []; //array to populate with points
var numberOfPoints = 4; //number of zigzags
var movement = 20; //movement left and right
for(var i=0;i<=numberOfPoints;i++){
var thisPoint = [];
if(i===0){ //push xAxisStartPoint
thisPoint.push({
x:xAxisStartX,
y:xAxisStartY
})
} else if(i===4){ //push yAxisStartPoint
thisPoint.push({
x:yAxisStartX,
y:yAxisStartY
})
} else {
thisCalcPointX;
if(i%2 > 0){ //if i is odd move left
thisCalcPointX = xAxisStartX-movement; //move point to the left
} else { //if it's even move right
thisCalcPointX = xAxisStartX+movement; //move point to the right
}
thisCalcPointY = xAxisStartY + difference/i; //move point up from xAxis start point at equal distance between xAxis and yAxis
thisPoint.push({
x: xAxisStartX,
y: thisCalcPointY
})
}
allPoints.push(thisPoint); //push this point to array of points
}
return allPoints; //return the points
}
//then pass this to create the path
var xAxisStart = [ xAxisStartX, xAxisStartY];
var yAxisStart= [ yAxisStartX, yAxisStartY];
var dataPoints = createPoints([xAxisStart, yAxisStart])
d3.select(container).data(dataPoints).enter().append('path')
.attr('x1', function(d){ return d.x1})
.attr('y1', function(d){ return d.y1})
.attr('x2', function(d){ return d.x2})
.attr('y2', function(d){ return d.y2})
.style('stroke','black');
Above code is not tested and just done on the fly, may need playing with, but the logic should work to create random points either side between both axis.

Resources