I'm trying to set the x-axis labels to date strings I create. I tried using scaleTime() but found that to be a nightmare and not really required for what I want to do.
I've tried scaleLinear() and scaleOrdinal() with 'almost' success, but didn't get there totally
In the examples below 'xlabels' is an array of about 23 strings, like:
["02 JAN 2020", "03 JAN 2020" ... etc]
First trying scaleLinear:
function draw_Xscale() {
var xScale1 = d3.scaleLinear()
.domain([0, xlabels.length])
.range([0, width]);
const g = svg.append("g")
.attr("class", "axis")
.attr("transform", `translate(0, ${height - 10})`);
let x_axis = d3.axisBottom(xScale1).ticks(tlabels.length);
g.call(x_axis).selectAll("line,path").style("stroke", "brown");
}
This draws an axis but the tick labels are numbers [0 .. 22] - not what I want. I tried stuff from some online examples something like the following tacked on:
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)");
but simply couldnt get it to work - probably some subtlety of the syntax I don't understand.
I also tried scaleOrdinal:
function draw_OrdScale() {
let arr = Array.from({length: xlabels.length}, (e, i) => i);
var ordinalScale = d3.scaleOrdinal()
.domain(xlabels)
.range([0, width]);
const g = svg.append("g")
.attr("class", "axis")
.attr("transform", `translate(0, ${height-100})`);
let x_axis = d3.axisBottom(ordinalScale).ticks(tlabels.length);
g.call(x_axis).selectAll("line,path").style("stroke", "brown");
}
This drew an axis with the labels I specified, but half of them were piled up on top of each other on the left and the other half on the right. Obviously I have the range wrong but not sure what it wants to be. I tried setting the range to:
.range([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]);
But this just had them a scrunched up into a black smear on the left.
I spent most of the day looking for examples of what I want to do but the few that looked like they should work, didn't.
TIA for any insights
We're going to try to make the scale a bit more generic, in case you have more data.
Suppose that data is an array of objects, with a date property. The first step is to get the date to an usable state:
function toTitleCase(str) {
return str.replace(/[^-'\s]+/g, function(word) {
return word.replace(/^./, function(first) {
return first.toUpperCase();
});
});
}
var data = data.map(d => toTitleCase(d.date.toLowerCase()))
This will convert every date from "02 JAN 2020" to "02 Jan 2020".
Afterwards, we need to use a date parser. This will convert every date to a real date object:
var parseTime = d3.timeParse("%d %b %Y");
data = data.map(d => parseTime(d.date);
Finally, we can now use a time scale, using d3.extent() to find the min and max value in your dataset. d3.extent() will return an array with two values, the min and the max, and it works on dates, like the rest of D3:
d3
.scaleTime()
.domain(d3.extent(d => d.date))
.nice() // Make the scale look prettier.
That'll give you a scale that works automatically with any date and will work as you add more values.
Related
I created x axis with the values and the labels and applied a style in grid lines. The code is below:
let xScale = d3.scalePoint().domain(axisXValues).range([0, width]);
let xAxisGenerator = axisXLabels.length > 0
? d3.axisBottom(xScale).ticks(axisXValues.length).tickFormat((d,i) => axisXLabels[i])
: d3.axisBottom(xScale).ticks(axisXValues.length);
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxisGenerator.tickSize(-height));
I would like to include more ticks between (green) the values and apply a different style as in the example below:
Does anyone have an idea?
Thanks!
I would add a new linear scale with the same range and domain in the original point scale. (Keeping the original point scale and its rendered axis, of course)
Then calculate the values in between and plot the axis using axisBottom.tickValues()
Color of the tick line can be changed by accessing .tick line and applying the stroke attribute.
Do note that this would only work if the point scale's domain is equally spaced, i.e. linear.
Adding the following code to yours should work. Attaching a working codepen too.
const xScaleInBetween = d3.scaleLinear()
.range(xScale.range())
.domain(d3.extent(axisXValues));
let xTicks = xScale.domain();
let xTicksInBetween = Array.from({length: xTicks.length - 1},
(_, j) => ((xTicks[j] + xTicks[j+1]) /2));
chart.append("g")
.attr("class", "x-axis-between")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScaleInBetween)
.tickValues(xTicksInBetween)
.tickSize(-height))
.call(g => g.selectAll('.tick line').attr('stroke', 'green'))
.call(g => g.selectAll('.tick text').remove())
Drawing a histogram in d3 (vertical bar chart), works fine except the y-axis scale is reversed. Here is the code that (almost) works:
let maxFrequency = d3.max(histData, d=> d.length);
let yScale = d3.scaleLinear()
.range([0,config.height - config.margin.top - config.margin.bottom])
.domain([0,maxFrequency]); //<-- this is wrong, right? It should be [maxFrequency,0]
let xScale = d3.scaleBand()
.range([0, (config.width - config.margin.left - config.margin.right)])
.domain(histData.map(d => d.Name))
.padding(0.2);
//Draw the histogram bars
let bars = config.svgContainer.selectAll("rect")
.data(histData);
bars.enter()
.append("rect")
.attr("width", xScale.bandwidth())
.attr("height", (d)=> yScale(d.length))
.attr("x", (d) => xScale(d.Name) + config.margin.left)
.attr("y", (d)=> config.height - config.margin.bottom - yScale(d.length))
.attr("fill", "red");//"#2a5599");
let axisX = d3.axisBottom(xScale)
config.svgContainer.append("g")
.style("transform", `translate(${config.margin.left}px,${config.height -
config.margin.bottom}px)`)
.call(axisX)
let axisY = d3.axisLeft(yScale)
config.svgContainer.append("g")
.style('transform',`translate(${config.margin.left}px,${config.margin.top}px)`)
.call(axisY);
This code produces this graph.
enter image description here
Normally you need to reverse the order of the domain attribute for the y-axis. Here is the graph when I use the ".domain([maxFrequency,0])" code with no other changes.
enter image description here
You can see that the y-axis now is correct but the bars look wonky. On closer inspection you can see that the data (1,9,40,80) is still being represented, but the bars are a now reversed. The bars are now height 79 (80-1), 71 (80-9), 40 (80-40), and 0 (80-80).
I hope this is something simple I'm not seeing. I can't find any posted solutions for something that sounds like this.
I am trying to create a chart with X axis that shows timely progress of a trade with %H:%M:%S format.
I have tried following code but it shows years only. The seconds are very important to show on the x axis. I know timeParse needs to be used but I am not sure how to leverage it. I have searched a lot online but no examples.
// Add X axis
var parseDate=d3.timeParse("%H:%M:%S")
var x = d3.scaleTime()
.domain([new Date(0,0,0), new Date (12,59,59)])
.range([ 0, width ]);
You would need the following:
The desired scale.
var x = d3.scaleTime()
.domain([new Date(1554236172000), new Date(1554754572000)])
.range([0, width]);
The axis with the correct format and the number of desired ticks, you may want to use .ticks(d3.timeMinute.every(desiredMinutesToHandle)); for a better display of your data.
var xAxis = d3.axisBottom(x)
.tickFormat(d3.timeFormat("%H:%M:%S"))
.ticks(50);
Finally append your axis to your svg with certain transformations to have a nice look
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.attr("y", -5)
.attr("x", 30)
.attr("transform", "rotate(90)")
Plunkr with working code
In the d3.v4 documentation the following is stated:
To generate ticks every fifteen minutes with a time scale, say:
axis.tickArguments([d3.timeMinute.every(15)]);
Is there a similar approach that can be used with values other than time? I am plotting sine and cosine curves, so I'd like the ticks to begin at -2*Math.PI, end at 2*Math.PI, and between these values I'd like a tick to occur every Math.PI/2. I could, of course, explicitly compute the tick values and supply them to the tickValue method; however, if there is a simpler way to accomplish this, as in the time-related example quoted above, I'd prefer to use that.
Setting the end ticks and specifying the precise space of the ticks in a linear scale is a pain in the neck. The reason is that D3 axis generator was created in such a way that the ticks are automatically generated and spaced. So, what is handy for someone who doesn't care too much for customisation can be a nuisance for those that want a precise customisation.
My solution here is a hack: create two scales, one linear scale that you'll use to plot your data, and a second scale, that you'll use only to make the axis and whose values you can set at your will. Here, I choose a scalePoint() for the ordinal scale.
Something like this:
var realScale = d3.scaleLinear()
.range([10,width-10])
.domain([-2*Math.PI, 2*Math.PI]);
var axisScale = d3.scalePoint()
.range([10,width-10])
.domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0",
"0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);
Don't mind the \u03c0, that's just π (pi) in Unicode.
Check this demo, hover over the circles to see their positions:
var width = 500,
height = 150;
var data = [-2, -1, 0, 0.5, 1.5];
var realScale = d3.scaleLinear()
.range([10, width - 10])
.domain([-2 * Math.PI, 2 * Math.PI]);
var axisScale = d3.scalePoint()
.range([10, width - 10])
.domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0", "0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var circles = svg.selectAll("circle").data(data)
.enter()
.append("circle")
.attr("r", 8)
.attr("fill", "teal")
.attr("cy", 50)
.attr("cx", function(d) {
return realScale(d * Math.PI)
})
.append("title")
.text(function(d) {
return "this circle is at " + d + " \u03c0"
});
var axis = d3.axisBottom(axisScale);
var gX = svg.append("g")
.attr("transform", "translate(0,100)")
.call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
I was able to implement an x axis in units of PI/2, under program control (not manually laid out), by targetting the D3 tickValues and tickFormat methods. The call to tickValues sets the ticks at intervals of PI/2. The call to tickFormat generates appropriate tick labels. You can view the complete code on GitHub:
https://github.com/quantbo/sine_cosine
My solution is to customise tickValues and tickFormat. Only 1 scale is needed, and delegate d3.ticks function to give me the new tickValues that are proportional to Math.PI.
const piChar = String.fromCharCode(960);
const tickFormat = val => {
const piVal = val / Math.PI;
return piVal + piChar;
};
const convertSIToTrig = siDomain => {
const trigMin = siDomain[0] / Math.PI;
const trigMax = siDomain[1] / Math.PI;
return d3.ticks(trigMin, trigMax, 10).map(v => v * Math.PI);
};
const xScale = d3.scaleLinear().domain([-Math.PI * 2, Math.PI * 2]).range([0, 600]);
const xAxis = d3.axisBottom(xScale)
.tickValues(convertSIToTrig(xScale.domain()))
.tickFormat(tickFormat);
This way if your xScale's domain were changed via zoom/pan, the new tickValues are nicely generated with smaller/bigger interval
In my d3 line chart, I only want ticks for the plotted data. This proves to be a issue with time stamps though as I get:
d3.js:7651 Error: <g> attribute transform: Expected number, "translate(NaN,0)"..
I thought to convert the strings to numbers in the tickValues array but I can not since it's got a colon. Any ideas?
// Hard coded data
scope.data = [
{date: '12:00', glucoseLevel: 400},
{date: '15:00', glucoseLevel: 200},
{date: '18:00', glucoseLevel: 300},
{date: '23:00', glucoseLevel: 400}
];
var parseDate = d3.timeParse('%I:%M');
scope.data.forEach(function(d) {
d.date = parseDate(d.date);
d.glucoseLevel = +d.glucoseLevel;
});
var x = d3.scaleTime()
.range([0, width]);
var xAxis = d3.axisBottom(x)
.tickValues(['12:00', '15:00', '18:00', '23:00']);
// Add the X Axis
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
You are specifying X values as times, so you must also specify the X-axis tick values as times.
As you already have the X values in the correct format, you can just write
var xAxis = d3.axisBottom(x)
.tickValues(scope.data.map(function (d) { return d.date; }));
.tickValues() isn't for setting the tick labels, it's for setting where on the axis the ticks appear. If you want the tick labels formatted in some way, specify a formatter using tickFormat, for example:
var xAxis = d3.axisBottom(x)
.tickValues(scope.data.map(function (d) { return d.date; }))
.tickFormat(d3.timeFormat("%H:%M"));
I've used the format string %H:%M instead of %I:%M as %I is hours in the range 01-12 whereas %H uses the 24-hour clock. For consistency I'd recommend changing your time parsing function to d3.timeParse('%H:%M'), although parsing a time with the hours greater than 12 using %I seems to work.
Finally, you'll also need to set the domain of your scale object x, for example:
var x = d3.scaleTime()
.domain([parseDate('12:00'), parseDate('23:00')])
.range([0, width]);
The two values passed to domain are the minimum and maximum X values to use for the axis. I've used the minimum and maximum values of your data, but I could have chosen a different time range (e.g. 00:00 to 24:00) as long as it contained all of your data points.