d3 area chart filling below x asis - d3.js

I am trying to draw area graph but it is filling below x axis. In below code I have used y0(yScale(0)) as I have seen in many examples and also I tried to give y0(height) it is not giving me correct output. I want area to be filled only above x axis if y axis values are +ve and if y axis values are -ve then area is going above max tick of y axis.
const D3Node = require('d3-node');
getGraphString: (data,yAxisTickFormat) => {
const d3n = new D3Node() // initializes D3 with container element
const d3 = d3n.d3;
let margin = { top: 20, right: 30, bottom: 20, left: 40 },
width = 275,
height = 200;
let svg = d3n.createSVG(width, height+margin.top+margin.bottom);
let xScale = d3.scaleTime().range([margin.left, width - margin.right])
.domain(d3.extent(data, function (d) { return d.date })),
yScale = d3.scaleLinear().range([height - margin.top, margin.bottom])
.domain(d3.extent(data, function (d) { return d.value }));
svg.append('g').attr("class", "xAxis")
.attr("transform", "translate(0," + (height - margin.bottom) + ")") //The transforms are SVG transforms
.call(d3.axisBottom(xScale).ticks(d3.timeYear).tickFormat(d3.timeFormat('%Y')))
.selectAll("text")
.style("text-anchor","end")
.attr("dx", "-.9em")
.attr("dy", ".50em")
.attr("transform","rotate(-45)")
svg.append("g") //We create an SVG Group Element to hold all the elements that the axis function produces.
.attr("transform", "translate(" + (margin.left) + ",0)")
.attr("class","yAxis")
.call(d3.axisLeft(yScale).ticks(4).tickFormat(d3.format(yAxisTickFormat)))
.selectAll("text")
.style("text-anchor","end")
.attr("dy", "0.32em")
.attr("dx", "-0.4em")
let lineFunc = d3.line().x(function (obj) { return xScale(obj.date) })
.y(function (obj) { return yScale(obj.value) })
svg.append("path")
.attr("d", lineFunc(data))
.attr("stroke", '#002046')
.attr("stroke-width", 3)
.attr("fill", "none");
let area = d3.area()
.curve(d3.curveLinear)
.x(function (d) { return xScale(d.date); })
.y0(yScale(0))
.y1(function (d) { return yScale(d.value); });
svg.append("path")
.style("fill", "#002046")
.attr("d", area(data));
return d3n.svgString();
}
let data =[ { date: 2019-01-01T00:00:00.000Z, value: 0.6330419130189774 },
{ date: 2018-01-01T00:00:00.000Z, value: 0.6266752649582236 },
{ date: 2017-01-01T00:00:00.000Z, value: 0.6403446517126394 },
{ date: 2016-01-01T00:00:00.000Z, value: 0.6432956408788177 } ];
getGraphString(data,'.0%');

Problem
If your y axis (scale domain) starts at 0, then yScale(0) is an appropriate baseline for the area. However, your scale's domain extent does not start at 0, it is dependent on the dataset:
yScale = d3.scaleLinear().range([height - margin.top, margin.bottom])
.domain(d3.extent(data, function (d) { return d.value }));
The lowest value in your dataset is 0.6266... not zero. In using yScale(0) D3 interpolates where y(0) would be, which in your case would require extending the axis quite a bit down the page and off the SVG.
Solution
We can manually set the baseline with something like : area.y0(yScale(0.6266...)). This places the baseline at the base of your y axis. But you don't need to set it manually as you can can set it with:
area.y0(yScale.range()[0]);
yScale.range() returns an array containing the scaled extent of the yScale (and therefore the y axis), we want to have the area's base be the same as the axis.
yScale.range()[0] is the equivilant of yScale(yScale.domain()[0]); - if 0 is the minimum value of the domain (0 == yScale.domain()[0]), it's a short jump to the often used area.y0(yScale(0))
Alternative
Alternatively, if you want the axis to include zero, you could keep yScale(0) as the baseline and set 0 to be the minimum value of the scale's domain:
.domain([0,d3.max(function(d) { return d.value; })])
Either way, the value provided as the minimum value for the scale's range and area.y0 should be the same if you want the bottom of the area to be aligned to the bottom of the axis. (This value should generally also be equal to the y translate value for the x axis).

Related

axis scale shift in d3.js

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>

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);
}

How add legend to a bar-chart based on data in an object dynamically (d3)

I am really new to the realm of D3 and based on the book of 'Interactive Data visualization for the web', I managed to create a Bar chart which is mostly based on the code from the following link.
The problem is I don't manage to add a legend to my bar chart based on an object dynamically.
I have tried consulting youtube videos and other stackoverflow questions related to 'adding a legend to a bar-chart', however in my opinion I couldn't find the question concerning how one is able to retrieve keys from an array of objects and use the data to add as an legend to the bar-chart.
For now all my bars also have the same color, see the second code below.
See the code below for the formatting of my object which is embedded in an array.
The name 'key' and 'value' are fixed, while the amount of the objects and their corresponding name and value differ after an click event of the user ( which determines which variables will be included in the object).
The following example is able create a legend, however in this case the formatting of the object is somehow different than in my case and my current knowledge of D3 is limitd, so I have no idea in which ways I have to adapt the code.
2: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenTotaal", value: 490}
3: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening", value: 165}
4: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_R_uCultuur_Recreatie_OverigeDiensten", value: 120}
5: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_K_lFinancieleDiensten_OnroerendGoed", value: 15}
6: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_ALandbouw_BosbouwEnVisserij", value: 0}
7: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie", value: 85}];
Based on the code from the book and accounting for other variables, I have currently the following code for visualizing a bar chart, in which the values (see object above) are shown in the bar charts and the color of the bar are all blueish. However there is not yet an legend included in my current code. Therefore I am wondering how one is able to dynamically create a legend based on the 'keys' ( in my case)in the object and represent the corresponding color bound to the bars. I would like to achieve the lowest image which I have drawn a sketch of.
var svg = d3.select("#barchart")
.select("svg")
.remove("svg");
//Width and height
var w = 600;
var h = 250;
var padding=20;
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.rangeRound([w - padding,padding ])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function (d) {
return d.value;
})])
.range([padding,h - padding]);
console.log("yscale",yScale);
//Define key function, to be used when binding data
var key = function (d) {
console.log("key", d);
return d.key;
};
// d3.select("svg").remove();
//Create SVG element
var svg = d3.select("#barchart")
.append("svg")
.attr("width", w)
.attr("height", h);
console.log("svg", svg);
//Create bars
svg.selectAll("rect")
.data(dataset, key) //Bind data with custom key function
.enter()
.append("rect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", function (d) {
return h - yScale(d.value);
})
.attr("width", xScale.bandwidth())
.attr("height", function (d) {
return yScale(d.value);
})
// .attr("data-legend", function (d) { return d.key })
.attr("fill", function (d) {
return "rgb(0, 0, " + (d.value * 10) + ")";
});
//Create labels
svg.selectAll("text")
.data(dataset, key) //Bind data with custom key function
.enter()
.append("text")
.text(function (d) {
return d.value;
})
.attr("text-anchor", "middle")
.attr("x", function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr("y", function (d) {
return h - yScale(d.value) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
If I understood correctly this is what you should need.
Plunker with working code.
First of all I would encourage to use an margin object which will allow better flexibility when dealing with charts
var margin = {
top: 20,
right: 20,
bottom: 20,
left: 20
};
We want to display the data with an odinal scale from the data and example you provided.
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenTotaal", value: 490}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening", value: 165}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_R_uCultuur_Recreatie_OverigeDiensten", value: 120}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_K_lFinancieleDiensten_OnroerendGoed", value: 15}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_ALandbouw_BosbouwEnVisserij", value: 0}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie", value: 85}];
Taking into account that probably the first element is a sum of sorts of the dataset I think it shouldn't be included in the chart since it is an aggregation of the elements we want to display.
(In case you need to display it as an element you should be able to do it quickly after reviewing the answer)
The element structure in your dataset is the following:
{
key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie",
value: 85
}
The domain of our xScale should be all the key values in our dataset, since the key is a huge string, I created a custom property in each element called label
{
key:
"bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening",
label: "Business Services",
value: 165
}
Lets create our scale with the correct domain and range:
var xScale = d3
.scaleBand()
.domain(dataset.map(d => d.label)) // All our label properties
.rangeRound([0, w - margin.left - margin.right]) // This scale will map our values from [0, width - margin.left - margin.right]
.paddingInner(0.05);
The yScale was almost correct, we just need to change it a little to use our margin object and use the correct range
The range must start from 0, if we used padding as the starting point our values will have an offset, since our values would be mapped from [padding, h - padding]. If we wanted to display a zero the value would be mapped to the padding value, if this is way you want to show the information keep it that way. In this case we will modify the scale.
var yScale = d3
.scaleLinear()
.domain([
0,
d3.max(dataset, function(d) {
return d.value;
})
])
.range([0, h - margin.top - margin.bottom]);
Next we will create a function to get the desired value from our elements
var xKey = function(d) {
return d.label;
};
Add our svg with some visual cues to help visualizing the way the elements are layed out:
var svg = d3
.select("#barchart")
.append("svg")
.style("background", "rgb(243, 243, 243)")
.style("border", "1px dashed #b4b4b4")
.attr("width", w)
.attr("height", h);
We want to use a margin, so lets use a group tag to achieve this, we could individually set the margin in each group/element we desired but I find this way simpler and clearer
var g = svg
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
We will need the width and height of the chart with the margins taken into account, lets define them really quick:
const customWidth = w - margin.left - margin.right;
const customHeight = h - margin.top - margin.bottom;
Let us add a rect to show where will our rects will be displayed:
g.append("rect")
.attr("fill", "#e3e3e3")
.attr("width", customWidth)
.attr("height", customHeight);
Lets deal with the rect creation, in your code you had a custom fill function which modified the b value within the RGB color values. In this case since we are dealing with categorical data we will use an array of colors for the rects.
g.append("g")
.attr("class", "rect__container")
.selectAll("rect")
.data(dataset, xKey) //Bind data with custom key function
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(xKey(d)); // use our key function
})
.attr("y", function(d) {
return customHeight - yScale(d.value); // use our custom size values
})
.attr("width", xScale.bandwidth())
.attr("height", function(d) {
return yScale(d.value);
})
.attr("fill", function(d, i) {
return d3.schemeCategory10[i]; // use an array of colors and use the index to decide which color to use
});
We have two options to show the labels of the chart:
We can create an x-axis or the desired legends. We will do both since it won't affect the outcome of the chart and either one of them can be removed.
var margin = {
top: 20,
right: 300, // modifiy our margin to have space to display the legends
bottom: 50,
left: 20
};
var legendElement = g
.append("g")
.attr("class", "legend__container")
.attr("transform", `translate(${customWidth}, ${margin.top})`) // set our group position to the end of the chart
.selectAll("g.legend__element")
.data(xScale.domain()) // use the scale domain as data
.enter()
.append("g")
.attr("transform", function(d, i) {
return `translate(${10}, ${i * 30})`; // provide an offset for each element found in the domain
});
legendElement
.append("text")
.attr("x", 30)
.attr("font-size", "14px")
.text(d => d);
legendElement
.append("rect")
.attr("x", 0)
.attr("y", -15)
.attr("width", 20)
.attr("height", 20)
.attr("fill", function(d, i) {
return d3.schemeCategory10[i]; // use the same category color that we previously used in rects
});
Now lets use the axis approach:
// create axis
var x_axis = d3.axisBottom().scale(xScale);
//Append group and insert axis
g.append("g")
.attr("transform", `translate(${0}, ${customHeight})`)
.call(x_axis);
g.append("g")
.attr("transform", `translate(${customWidth / 2}, ${customHeight + 40})`)
.append("text")
.text("Activities")
.attr("font-family", "sans-serif")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.style("text-transform", "uppercase")
.attr("text-anchor", "middle");
And finally create the labels for the value in our data:
//Create labels
g.append("g")
.attr("class", "text__container")
.selectAll("text")
.data(dataset, xKey) //Bind data with custom key function
.enter()
.append("text")
.text(function(d) {
return d.value;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(xKey(d)) + xScale.bandwidth() / 2;
})
.attr("y", function(d) {
return customHeight - yScale(d.value) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");

D3.js ordinal scale only returning extremes the values

I'm new to d3.js. I'm trying to plot to get the ordinal values ​​on the x-axis, replacing the date values. I'm using the example from http://bl.ocks.org/hopelessoptimism/b8ef4734abad1c644221.
I'm having difficulty using d3.js: Ordinal scale.
I tried this link too and other alternatives with no success.
the data2 csv is:
timestamp,AJU,BEL,BPS,BSB,BVB,CGB,CGH,CGR,CNF,CWB,FLN,FOR,GIG,GRU,GYN,IGU,IMP,IOS,JOI,JPA,LDB,MAB,MAO,MCP,MCZ,NAT,NVT,PMW,POA,PVH,RAO,RBR,REC,SDU,SJP,SLZ,SSA,STM,THE,UDI,VCP,VIX
A,7,5,8,4,3,8,6,7,10,4,10,2,6,8,2,3,3,4,5,10,4,4,2,9,8,9,5,7,7,7,10,4,6,9,5,3,8,5,4,3,8,3
B,7,6,10,7,6,4,7,5,3,4,7,6,5,2,9,10,10,4,7,6,2,2,2,9,3,4,7,9,2,4,8,10,2,3,9,2,2,2,2,10,4,9
C,6,4,7,4,5,8,8,10,9,5,2,2,8,2,2,6,8,4,10,5,2,9,3,4,6,3,9,2,2,4,2,10,9,5,6,4,10,10,4,3,7,10
D,8,5,10,2,7,3,6,3,6,9,7,8,5,2,3,5,6,7,2,10,3,4,4,6,9,3,4,7,2,2,3,7,4,8,6,7,3,8,5,9,7,8
error pic
This is a part of the line code:
// maximum reviews
var max_y = d3.max(data[2], function(d) {
var max = 0;
d3.values(d).forEach(function(i) {
if (+i && (+i > max)) {
max = +i;
}
});
return max;
});
// Create y-axis scale mapping price -> pixels
var measure_scale = d3.scale.linear()
.range([height, 100])
.domain([0, max_y])
;
// Create D3 axis object from measure_scale for the y-axis
var measure_axis = d3.svg.axis()
.scale(measure_scale)
.orient("right");
// Append SVG to page corresponding to the D3 y-axis
d3.select('#chart').append('g')
.attr('class', 'y axis')
.attr("transform", "translate(" + width + " , -15)")
.call(measure_axis);
// add label to y-axis
d3.select(".y.axis")
.append("text")
.attr('class', 'label')
.text("Daily")
.attr("transform", "translate(45,110) rotate(90)");
// create a function to draw the timeseries for each neighborhood
var drawChart = function(field) {
// remove the previous chart
d3.select('#chart').select('.x.axis').remove();
d3.select('#chart').select('path').remove();
// update the title
d3.select('#heading')
.text(field);
// remove missing values
var neigh_data = data[2].filter(function(d) {
return d[field];
});
// get min/max dates
var time_extent = d3.extent(neigh_data, function(d){
return d['timestamp'];
;
});
// Create x-axis scale mapping dates -> pixels
var time_scale = d3.scale.ordinal()
.range([0, width - margin])
// .rangePoints([0, width - margin])
// .range([0, width - margin])
// .rangeBands([0, width], .1)
.domain(time_extent)
;
// Create D3 axis object from time_scale for the x-axis
var time_axis = d3.svg.axis()
.scale(time_scale)
// .ticks(10)
// .tickFormat(d3.format(""))
// .tickFormat(d3.time.format("%b '%y"))
;
// Append SVG to page corresponding to the D3 x-axis
d3.select('#chart').append('g')
.attr('class', 'x axis')
.attr('transform', "translate(" + margin + ',' + (height - 15) + ")")
.call(time_axis)
.selectAll("text")
.attr("y", 0)
.attr("x", 9)
.attr("dy", ".35em")
.attr("transform", "rotate(90)")
.style("text-anchor", "start");
// define the values to map for x and y position of the line
var line = d3.svg.line()
.x(function(d) { return time_scale(d['timestamp']); })
.y(function(d) { return measure_scale(+d[field]); });
;
// append a SVG path that corresponds to the line chart
d3.select('#chart').append("path")
.datum(neigh_data)
.attr("class", "line")
.attr("d", line)
.attr('transform', 'translate(' + margin + ', -15)');
};
drawChart(field);
// create a callback for the neighborhood hover
var mover = function(d) {
var neigh = d.iata;
d3.select('#map path.' + neigh).style('fill', '#9999ff');
drawChart(neigh);
};
Can someone tell me what am I doing wrong?

d3.js Y axis doesn't extend to value

I have a barchart which is populated by values from a JSON variable. The chart is dynamic because the values are retrieved from an SQL query by doing a count. So my data gets fed back in like :
[{"Fruit":"Apple","COUNT( Fruit )":"12"},{"Fruit":"Orange","COUNT( Fruit )":"6"},{"Fruit":"Pear","COUNT( Fruit )":"1"},{"Fruit":"Blank","COUNT( Fruit )":"1"},{"Fruit":"Pineapple","COUNT( Fruit )":"1"},{"Fruit":"Kiwi","COUNT( Fruit )":"1"}]
For the most part my graphs seem to be displaying properly. However some are returning values that exceed the Y Axis, I dont think it's the values that are causing the issues I believe its the axis that isnt calculating the right height. For instance
If Orange count is 14, sometimes the Y axis stops at a number less than this and that column extends the graph.
By viewing it in google chrome developer console, I can see the height of the bar is
<rect id="Orange" y="-520px" x="94px" height="1040px" width="162"></rect>
which far extends my SVG height of 600px - Margins(top + bottom) of 80px!
Does anyone know why my Y Axis isn't getting the right Max value?
Code here:
var canv = document.getElementById("exportCanvas");
canv.width = screen.width;
var margin ={top:40, right:0, bottom:40, left:40},
width=screen.width - 250,
height=600-margin.top-margin.bottom;
var jsplit = jdata.split('"');
var keyX = jsplit[1];
var keyY = "";
var data = JSON.parse(jdata);
for (k in data[0]) {
if (k!=keyX) keyY=k;
}
console.log("keyX = " + keyX);
console.log(keyY);
console.log(data[0]);
// scale to ordinal because x axis is not numerical
var x = d3.scale.ordinal()
.domain(['Orange','Apple','Pear']) //Added this in temporarilly. this should be calculated from the data.
.rangeRoundBands([0, width], 0.25,0.25);
//scale to numerical value by height
// var y = d3.scale.linear().range([height, 0]);
var y = d3.scale.linear()
.range([height, 0]);
console.log(data);
x.domain(data.map(function(d){ return d[keyX]}));
y.domain([0, d3.max(data, function(d){return d[keyY]})]);
var chart = d3.select("#chart")
.append("svg") //append svg element inside #chart
.attr("width", width+ margin.left+margin.right) //set width
// .attr("width", width+(2*margin.left)+margin.right) //set width
.attr("height", height+margin.top+margin.bottom); //set height
// .attr("transform", "translate(" + Math.min(width,height) / 2 + "," + Math.min(width,height) / 2 + ")");
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom"); //orient bottom because x-axis will appear below the bars
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10).tickFormat(function(d) {
if (d % 1 == 0) {
return d3.format('.f')(d)
} else {
return ""
}
});
var bar = chart.selectAll("g")
.data(data)
.enter()
.append("g");
//you're moving the group and then moving the rect below as well
//one or the other not both need to be moved.
//.attr("transform", function(d, i){
// return "translate("+x(d[keyX])+", 0)";
//});
bar.append("rect")
.attr("id", function(d) {
return d[keyX];
})
.attr("y", function(d) {
return y(d[keyY]) + "px";
})
.attr("x", function(d,i){
//AB - Adjusted this so it correcly places the bar along the X
//x.range is an array of x values for each bar
//calculated in the var x = line above , with the .rangeRoundBands([0, width], 0.25,0.25);
//setting the width of the bars (an equal division of width) with margins of 0.25 at the start
//and end of the graph and 0.25 between each bar.
return x.range()[i] + margin.left + "px";
})
.attr("height", function(d) {
return height - y(d[keyY]) +"px";
})
.attr("width", x.rangeBand()); //set width base on range on ordinal data
bar.append("text")
.attr("x",function(d,i){
//similar to above but adding half the width of the bar to the x position
//to roughly center it on the bar. only rough as doesnt take account of length of text.
return x.range()[i] + margin.left + (x.rangeBand()/2)+ "px";
})
.attr("y", function(d) { return y(d[keyY]) +20; })
.attr("dy", ".75em")
.style("fill","white")
.style("font-weight", "bold")
.text(function(d) { return d[keyY]; });
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate("+margin.left+","+ height+")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.attr("transform", "translate("+margin.left+",0)")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text(keyY);
Apologies for commented out code, I have been playing with it alot to try and suss this out.
You need to recalculate y.domain() when your dataset refreshes. So when you update your data, you can try something like:
y.domain([0, d3.max(data, function(d){return d[keyY]})]);
chart.select(".y.axis")
.call(yAxis.scale(y));

Resources