Create a D3 axis without tick labels - d3.js

How can I create a D3 axis that does not have any labels at its tick markers?
Here's an example that shows what I'm after, from Mike Bostock no less. There are several Axis objects rotated around the centre, and only the first one has tick labels.
In this case, he's achieved the result using CSS to hide all but the first axis's labels:
.axis + .axis g text {
display: none;
}
However this still results in the creation of SVG text elements in the DOM. Is there a way to avoid their generation altogether?

I'm just going to leave this here since people are likely to end up on this question. Here are the different ways you can easily manipulate a D3 axis.
Without any ticks or tick labels:
d3.svg.axis().tickValues([]);
No line or text elements are created this way.
Without ticks and with tick labels:
d3.svg.axis().tickSize(0);
The line elements are still created this way.
You can increase the distance between the tick labels and the axis with .tickPadding(10), for example.
With ticks and without tick labels:
d3.svg.axis().tickFormat("");
The text elements are still created this way.

You can't avoid the generation of the text elements without modifying the source. You can however remove those elements after they have been generated:
var axisElements = svg.append("g").call(axis);
axisElements.selectAll("text").remove();
Overall, this is probably the most flexible way to approach this as you can also remove labels selectively. You can get the data used to generated them from the scale you're using (by calling scale.ticks()), which would allow you to easily do things like remove all the odd labels.

Create a D3 axis without tick labels
A tick mark without a label can be created by using a function that returns an empty string. This works in both the Javascript and Typescript versions of D3.
d3.svg.axis().tickFormat(() => "");
Further explained on github #types/d3-axis
https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24716#issuecomment-381427458

I know this is an old question but if you are using D3 version 7 probably this approach will help you.
There are 4 axes: top, left, bottom and right. We will define the left and bottom as main axes, and top and right as tick-less axes.
There are two way to render tick-less axes either declaring empty array for the tickValues function like this:
xAxisTop = d3.axisTop().scale(xScale).tickSize(0).tickValues([])
Or declaring an empty label for the tick using tickFormat function like this:
const yAxisRight = d3.axisRight().scale(yScale).tickSize(0).tickFormat('')
However, both require that we define the tickSize function to 0 to avoid the default ticks at the start and the end of the axis.
This is working snippet:
const element = document.getElementById('plot')
const margin = {
top: 20,
right: 20,
bottom: 30,
left: 50,
}
const width = 600, height = 180
const data = [10, 15, 20, 25, 30]
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', [0, 0, width, height])
.attr('style', 'max-width: 100%; height: auto; height: intrinsic;')
const xScale = d3.scaleLinear()
.domain([d3.min(data), d3.max(data)])
.range([margin.left, width - margin.right])
const yScale = d3.scaleLinear()
.domain([d3.min(data), d3.max(data)])
.range([height - margin.bottom, margin.top])
const xAxis = d3.axisBottom().scale(xScale)
const xAxisTop = d3.axisTop().scale(xScale).tickSize(0).tickValues([])
const yAxis = d3.axisLeft().scale(yScale)
const yAxisRight = d3.axisRight().scale(yScale).tickSize(0).tickFormat('')
svg.append('g')
.attr('transform', `translate(${margin.left},0)`)
.call(yAxis)
svg.append('g')
.attr('transform', `translate(${width - margin.right}, 0)`)
.call(yAxisRight)
svg.append('g')
.attr('transform', `translate(0,${height - margin.bottom})`)
.call(xAxis)
svg.append('g')
.attr('transform', `translate(0, ${margin.top})`)
.call(xAxisTop)
element.appendChild(svg.node())
.center {
display: flex;
justify-content: center;
align-items: center;
width: 98vw;
height: 190px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id='plot' class='center'></div>

Related

d3 - y-axis with log scale - ticks getting overlapped

I am trying to use log scale for y-axis on my line chart.
Here is my code:
var yScale_for_axis = d3.scaleLog().domain([1,d3.max(vals)]).range ([height,0]);
g.append("g")
.call(d3.axisLeft(yScale_for_axis).tickFormat( d3.format(".1e"));
The ticks are getting overlapped with each other. Heres how it looks:
What should I do to make it look like this?
Look at the snippet - it seems to be working. Maybe it's tick formatting which makes ticks overlap:
const width = 400, height = 500;
// Append SVG
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
// Create scale
const scale = d3.scaleLog()
.domain([1, 5000])
.range([20, height - 20]);
// Add scales to axis
const yAxis = d3.axisLeft()
.scale(scale);
//Append group and insert axis
svg.append("g")
.attr('transform', 'translate(150, 0)')
.call(yAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

svg - Aligning text outside circular arc d3js

Can someone help me to create below image using d3js. I able to create pie chart as required but stuck to render outer text with arrows and all.
Wheel with outer text
As of know I have achieved circle creation using below code.
var svg = d3.select("svg");
var margin = {top: 40, right: 45, bottom: 30, left: 40};
console.log(svg);
var width = svg.attr('width');
var height = svg.attr('height');
var radius = Math.min(width, height)/2;
var g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var hoverStyle = {
zindex: '2px'
};
var hoverExitStyle = {
zindex: "0px"
}
var animateSpeed = 500;
// Define a Pie
var pie = d3.pie()
.sort(null)
.value(function(d) {return d.number});
// define pie section
var path = d3.arc()
.outerRadius(radius - 10)
.innerRadius(0);
//
var label = d3.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40);
// Get pie sections based on the data.
var pieSections = pie(data);
var arc = g.selectAll('.arc')
.data(pieSections)
.enter().append("g")
.attr("class", "arc")
.append('a')
.attr("href", function(d) { return d.data.url; });
arc.append("path")
.attr("d", path).transition()
.attr("fill", function(d) { return d.data.color; });
arc.append("text")
.attr("transform", function(d) { return "translate(" + label.centroid(d) + ")"; })
.attr("dy", "0.35em")
.text(function(d) { return d.data.title; });
The text and the arrows are two separate concerns that probably merit their own questions.
Curved text
To do text on a path with d3, you might want to look at the textpath documentation. It's going to be a little tricky; basically, you'll want to create a second d3.arc() generator with a slightly longer outer radius. Use the longer one to set the d attribute of path elements (that you need to create) inside the SVG's defs object, and reference those path elements' ids from textpath elements (that you also need to create).
Curved arrows
To accomplish this exactly like the image, you're probably going to need to some manual construction (including figuring out the math!) of the d attribute yourself to add appropriate arrowheads (see the SVG path syntax). If you're doing a static image, it might be faster to just create the lines (again, using a longer-radius d3.arc() generator), and export the SVG with something like SVG crowbar to a drawing program like Illustrator or Inkscape, and add the arrowheads there.

Why axis labels are not removed from the DOM in d3?

I'm implementing a chart using d3 that has a sliding x axis. Demo
I noticed that the amount of ticks (i.e. the amount of axis labels) keeps growing, meaning that the labels that slide out of the chart are not removed from the DOM.
Why are the old labels stay in the DOM, and how could I fix that?
const timeWindow = 10000;
const transitionDuration = 3000;
const xScaleDomain = (now = new Date()) =>
[now - timeWindow, now];
const totalWidth = 500;
const totalHeight = 200;
const margin = {
top: 30,
right: 50,
bottom: 30,
left: 50
};
const width = totalWidth - margin.left - margin.right;
const height = totalHeight - margin.top - margin.bottom;
const svg = d3.select('.chart')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
svg
.append('rect')
.attr('width', width)
.attr('height', height);
// Add x axis
const xScale = d3.scaleTime()
.domain(xScaleDomain(new Date() - transitionDuration))
.range([0, width]);
const xAxis = d3.axisBottom(xScale);
const xAxisSelection = svg
.append('g')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
// Animate
const animate = () => {
console.log(d3.selectAll('.tick').size()); // DOM keeps growing!!!
xScale.domain(xScaleDomain());
xAxisSelection
.transition()
.duration(transitionDuration)
.ease(d3.easeLinear)
.call(xAxis)
.on('end', animate);
};
animate();
svg {
margin: 30px;
background-color: #ccc;
}
rect {
fill: #fff;
outline: 1px dashed #ddd;
}
<script src="https://unpkg.com/d3#4.4.1/build/d3.js"></script>
<div class="chart"></div>
Analysis
The axis component will actually try to remove the ticks, which are no longer visible. Examining the source code brings up the line:
tickExit.remove();
Debugging to this line shows, that the exit selection is correctly calculated, i.e. all exiting nodes are contained in tickExit. But the nodes will not be removed as expected, because you have an active transition running on them. The documentation has it:
# transition.remove() <>
For each selected element, removes the element when the transition ends, as long as the element has no other active or pending transitions. If the element has other active or pending transitions, does nothing.
Workaround
One—admittely hacky—workaround could make use of the way D3 fades the ticks, which are no longer visible. This is not very nice, though, because it relies on the inner workings of D3 and might break in the future, should this behavior be altered.
Because selection.remove() is not that faint hearted, it can be used to take care of the removal instead of using transition.remove(). Personally, I would use something along the following lines in your animate() function:
d3.selectAll(".tick")
.filter(function() {
return +d3.select(this).attr("opacity") === 1e-6;
})
.remove();
Because the axis component will eventually fade all non-visible ticks to an opacity of 1e-6 this can be used to discard those elements. Note, however, that the tick count will at first come up to some value other than the starting value, because the transition to the final opacity will take some time to complete. But, the excess tick count is small and can safely be ignored.
Have a look at the following working demo. In this example, the tick count will increase from the initial 10 to 19 and subsequently stay at this value.
const timeWindow = 10000;
const transitionDuration = 3000;
const xScaleDomain = (now = new Date()) =>
[now - timeWindow, now];
const totalWidth = 500;
const totalHeight = 200;
const margin = {
top: 30,
right: 50,
bottom: 30,
left: 50
};
const width = totalWidth - margin.left - margin.right;
const height = totalHeight - margin.top - margin.bottom;
const svg = d3.select('.chart')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
svg
.append('rect')
.attr('width', width)
.attr('height', height);
// Add x axis
const xScale = d3.scaleTime()
.domain(xScaleDomain(new Date() - transitionDuration))
.range([0, width]);
const xAxis = d3.axisBottom(xScale);
const xAxisSelection = svg
.append('g')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
// Animate
const animate = () => {
console.log(d3.selectAll('.tick').size()); // DOM keeps growing!!!
d3.selectAll(".tick")
.filter(function() {
return +d3.select(this).attr("opacity") === 1e-6;
})
.remove();
xScale.domain(xScaleDomain());
xAxisSelection
.transition()
.duration(transitionDuration)
.ease(d3.easeLinear)
.call(xAxis)
.on('end', animate);
};
animate();
svg {
margin: 30px;
background-color: #ccc;
}
rect {
fill: #fff;
outline: 1px dashed #ddd;
}
<script src="https://d3js.org/d3.v4.js"></script>
<div class="chart"></div>
Anything below is my take on the comments to issue #23 "Axis labels are not removed from the DOM" opened by OP for the d3-axis module, which contains some really good points.
The comment by Mike Bostock provides a more in-depth look at the concurring transitions on the same element, which will eventually prevent the removal of the ticks:
The problem is that when the end event for the parent G element is dispatched, the axis has not yet removed the old ticks. The ticks are removed by transition.remove, which listens for the end event on the tick elements. The end event for the G element is dispatched prior to the end event for the tick elements, so you are starting a new transition that interrupts the old one before the axis has a chance to remove the old ticks.
The real gem whatsoever is to be found in the comment by #curran, who suggested to use setTimeout(animate). This is brilliant and, as far as I know, the only non-intrusive, non-hacky solution to this problem! By pushing the animate function to the end of the event loop, this will defer the creation of the next transition until after the actual transition has had the chance to clean up after itself.
And, to wrap up this theoretical discussion, the probably best conclusion to your actual problem seems to be Mike Bostock's:
If you want a real-time axis, you probably don’t want transitions. Instead, use d3.timer and redraw the axis with every tick.

How to fix replaying transitions in d3 v4?

I'm implementing a chart using d3 that has a sliding x axis. Demo
The problem is, when I change to another tab, and then go back (say after 10 seconds), d3 seems to try to replay the missing transitions, which results in a very awkward behavior of the axis. See here.
Mike Bostock mentions that:
D3 4.0 fixes this problem by changing the definition of time. Transitions don’t typically need to be synchronized with absolute time; transitions are primarily perceptual aids for tracking objects across views. D3 4.0 therefore runs on perceived time, which only advances when the page is in the foreground. When a tab is backgrounded and returned to the foreground, it simply picks up as if nothing had happened.
Is this really fixed? Am I doing anything wrong?
const timeWindow = 10000;
const transitionDuration = 3000;
const xScaleDomain = (now = new Date()) =>
[now - timeWindow, now];
const totalWidth = 500;
const totalHeight = 200;
const margin = {
top: 30,
right: 50,
bottom: 30,
left: 50
};
const width = totalWidth - margin.left - margin.right;
const height = totalHeight - margin.top - margin.bottom;
const svg = d3.select('.chart')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
svg
.append('rect')
.attr('width', width)
.attr('height', height);
// Add x axis
const xScale = d3.scaleTime()
.domain(xScaleDomain(new Date() - transitionDuration))
.range([0, width]);
const xAxis = d3.axisBottom(xScale);
const xAxisSelection = svg
.append('g')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
// Animate
const animate = () => {
xScale.domain(xScaleDomain());
xAxisSelection
.transition()
.duration(transitionDuration)
.ease(d3.easeLinear)
.call(xAxis)
.on('end', animate);
};
animate();
svg {
margin: 30px;
background-color: #ccc;
}
rect {
fill: #fff;
outline: 1px dashed #ddd;
}
<script src="https://unpkg.com/d3#4.4.1/build/d3.js"></script>
<div class="chart"></div>
The problem is not D3 transitions. The problem here is new Date().
Every time you go to another tab, the transition pauses. So far, so good. But when you come back to the chart, let's say, after 20 seconds, you get a new date that's the current date... however your timeWindow is the same, as well as your transitionDuration:
const timeWindow = 10000;
const transitionDuration = 3000;
const xScaleDomain = (now = new Date()) => [now - timeWindow, now];
That makes the axis jump ahead faster, because the difference between the old and new values at any point in the domain is not 3 seconds anymore.
Here is a very simple solution, too crude and requiring improvements, just to show you that the problem is new Date(). In this solution (again, far from perfect), I manually set the date in each animation to jump 10 seconds, no matter how long you stay in another tab:
var t = xScale.domain()[1];
t.setSeconds(t.getSeconds() + 10);
xScale.domain([xScale.domain()[1], t]);
Here is the CodePen: http://codepen.io/anon/pen/GrjMxy?editors=0010
A better solution, using your code, would be changing timeWindow and transitionDuration to take into consideration the difference between the new new Date() and the old new Date() (that is, how long the user has been in another tab).

D3 image transition of height only

I am trying to work with D3 transitions and an image and I seem to be having trouble.
I am trying to cause the image to disappear by having the top and bottom of the image close together until it gets to the middle, like a shutter effect, or erasing lines from the top and bottom until it is all gone. I don't want the image to scale at all, I just want it to close.
So far I have gotten the image to scale down to 0, but that is not what I want.
Also on the first transition it drops the image to the middle of the box before it starts the transition, whats up with that?
http://jsfiddle.net/Qda6B/
var svgContainer = d3.select("#box").append("svg")
.style("width", '100%')
.style("height", '100%')
.style("background-color","blue");
var imgs = svgContainer.append("svg:image")
.attr("xlink:href", "http://guiaavare.com/img/upload/images/Aishwarya-Rai-face.jpg")
.attr("width", "400")
.attr("height", "400");
d3.select("#inbutton").on("click", function () {
imgs
.attr("height",400)
.transition()
.attr({
height: 0,
y: 200
})
.duration(500);
});
d3.select("#outbutton").on("click", function () {
imgs
.transition()
.attr({
height: 400,
y: 0
})
.duration(500);
});
Thanks in advance
You can achieve the effect you are after using clipPath: http://jsfiddle.net/Qda6B/5/
var clipRect = svgContainer.append('svg:defs')
.append('svg:clipPath')
.attr('id', 'shutter-clip')
.append('rect')
.attr({
x: 0,
y: 0,
height: 400,
width: 400
})
var imgs = svgContainer.append("svg:image")
// ...
.attr("clip-path", "url(#shutter-clip)");
And then doing the transitions on the height of the rect inside the clipPath.
As an aside, though this is possible to do with d3, this very likely belongs in the domain of CSS3 transitions or jQuery or GASP animations.

Resources