am plotting histogram based on Data ( which is changing dynamically ) , but the height of some bars are not fitting the svg zone ( the exceed the svg area )
this is piece of code which i have doubt on :
private drawBars(data: any[]): void {
let f = Math.min.apply(Math, this.fixedData.map(function (o) {
return o.xAxis;
}));
/** Create the X-axis band scale */
const x = d3.scaleBand()
.range([f, 240])
.domain(data.map(d => d.xAxis))
.padding(0);
/** Create the Y-axis band scale */
const y = d3.scaleLinear()
.domain([0, this.axisMax])
.range([this.height, 0])
.nice();
/** Create and fill the bars */
this.svg.selectAll("*").remove()
this.svg.selectAll("bars")
.data(data, d => d)
.enter()
.append("rect")
.attr("x", d => x(d.xAxis))
.attr("y", d => y(d.yAxis))
.attr("width", x.bandwidth())
.attr("height", (d) => this.height - y(d.yAxis))
.attr("fill", (d) => this.colorPicker(d))
}
and here is a proof of that weird behaviour :
any ideas and i would be thankful !
Your problem is caused by this.axisMax being set way too low, here:
const y = d3.scaleLinear()
.domain([0, this.axisMax])
.range([this.height, 0])
.nice();
You need to recalculate it based on the available data.
The domain of a scale holds the range of possible values it accepts, but, at least for scaleLinear, scaleTime, and some others, being outside that range doesn't throw an error or even return NaN. For these types of scales, the domain is instead used to identify a linear transformation between the input values and output pixels.
For example
Suppose you have a linear scale with domain [0, 10] and range [0, 100]. Then, x = 0 gives pixel value 0, and x = 10 gives 100. However, x = 20 gives pixel value 200, which is more than the given range.
Related
I am trying to create small multiple bar charts that have different y-axis scales using d3 v6. There are a few examples out there (https://flowingdata.com/2014/10/15/linked-small-multiples/) of small multiples for previous versions of d3, but not v6, which seems to have a good number of changes implemented.
I don't have much experience with d3, so I am probably missing something obvious, but I can't get the bars to properly generate, the axes are generating (though I think I am generating them too many times on top of each other).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Small multiple bar charts</title>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<div id='vis'></div>
<script type="text/javascript">
// Set the sizing metrics
var width = 150;
var height = 120;
var margin = {top: 15, right: 10, bottom: 40, left: 35};
// Create the axes
var xScale = d3.scaleBand()
.range([0, width])
.padding(0.1);
var yScale = d3.scaleLinear()
.range([height, 0]);
var xAxis = d3.axisBottom()
.scale(xScale);
// Load the data
d3.csv('data.csv').then(function(data) {
data.forEach(function(d) {
d.segment = d.segment;
d.parameter = d.parameter;
d.the_value = +d.the_value;
});
// set the x domain
xScale.domain(data.map(function(d) { return d.segment; }));
// group the data
metrics = Array.from(
d3.group(data, d => d.parameter), ([key, value]) => ({key, value})
);
// create a separate SVG object for each group
var svg = d3.select('#vis').selectAll('svg')
.data(metrics)
.enter()
.append('svg');
// loop over the data and create the bars
metrics.forEach(function(d, i) {
console.log(d);
console.log(metrics);
yScale.domain([0, d3.max(metrics, function(c) { return c.the_value; })]);
svg.selectAll('.bar')
.data(d)
.enter().append('rect')
.attr('class', 'bar')
.attr('x', function(c) { return xScale(c.segment); })
.attr('width', xScale.bandwidth())
.attr('y', function(c) { return yScale(c.the_value); })
.attr('height', function(c) { return height - yScale(c.the_value); })
.attr('fill', 'teal');
svg.append('g')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis)
});
});
</script>
</body>
</html>
Here is the data file:
segment,parameter,the_value
A,one,33
A,two,537723
A,three,14
A,four,5
A,five,0.093430759
B,one,76
B,two,137110
B,three,16
B,four,20
B,five,0.893868331
C,one,74
C,two,62020
C,three,25
C,four,14
C,five,0.862952872
Eventually I would also like to get the charts linked so that when series A is hovered on the first graph the value will display for each series on all of the graphs, but the first step is to get the visuals properly working.
There's a few small changes to get it working:
When you set the domain on the x scale, you just need the unique segments e.g. A, B, C and not the full list of segments from the data.
When you create the 5 SVGs you can class them so that you can refer to each separately when you loop through the values of the metrics. So the first small multiple has a class of one, the second small multiple has a class of two etc
Reset the y domain using the set of the_values from the metrics you're charting - i.e. use d not metrics
When you loop metrics first select the small multiple for that metric and then selectAll('.bar')
Pass d.value to data as this makes the references to c.the_value etc work properly
To prevent adding the x axis multiple times, again select the SVG for the specific small multiple before call(xAxis) otherwise you add as many axes as there are parameters to each small multiple.
I faked up your data to include random data.
See the example below - maybe there's a smarter way to do it:
// fake data
var data = ["A", "B", "C"].map(seg => {
return ["one", "two", "three", "four", "five"].map((par, ix) => {
return {
"segment": seg,
"parameter": par,
"the_value": (Math.floor(Math.random() * 10) + 1) * (Math.floor(Math.random() * 10 * ix) + 1)
}
});
}).flat();
// Set the sizing metrics
var width = 150;
var height = 120;
var margin = {top: 15, right: 10, bottom: 40, left: 35};
// Create the axes
var xScale = d3.scaleBand()
.range([0, width])
.padding(0.1);
var yScale = d3.scaleLinear()
.range([height, 0]);
var xAxis = d3.axisBottom()
.scale(xScale);
// set the x domain
// put unique segments into the domain e.g. A, B, C
var uniqueSegments = Array.from(new Set(data.map(function(d) {return d.segment;} )));
xScale.domain(uniqueSegments);
// group the data
var metrics = Array.from(
d3.group(data, d => d.parameter), ([key, value]) => ({key, value})
);
// create a separate SVG object for each group
// class each SVG with parameter from metrics
var svg = d3.select('#vis').selectAll('svg')
.data(metrics)
.enter()
.append('svg')
.attr("class", function(d) { return d.value[0].parameter;});
// loop over the data and create the bars
metrics.forEach(function(d, i) {
//console.log(d);
//console.log(metrics);
// reset yScale domain based on the set of the_value's for these metrics
yScale.domain([0, d3.max(d.value, function(c) { return c.the_value; })]);
// select the right svg for this set of metrics
d3.select("svg." + d.value[0].parameter)
.selectAll('.bar')
.data(d.value) // use d.value to get to the the_value
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', function(c) { return xScale(c.segment); })
.attr('width', xScale.bandwidth())
.attr('y', function(c) { return yScale(c.the_value); })
.attr('height', function(c) { return height - yScale(c.the_value); })
.attr('fill', 'teal');
// call axis just on this SVG
// otherwise calling it 5 times for 5 metrics...
d3.select("svg." + d.value[0].parameter)
.append('g')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.5.0/d3.min.js"></script>
<div id='vis'></div>
I'm attempting to set up a simple bar chart using D3 version 4.x.x However, I believe I am doing everything correctly, but can not seem to get the rects to display. I've attached a codepen to see this.
Thank you in advance for any noob issue that is causing this as I am new to D3.
http://codepen.io/PizzaPokerGuy/pen/XpoJxG?editors=0111
enter code here//Width of svg, will be used again down the road
const width = 1000;
//Height of svg, will be used again down the road
const height = 800;
//Padding so things have room to be displayed within svg
const padding = 60;
//Create our SVG container
var svg = d3.select("body")
.append('svg')
.attr("width", width)
.attr("height", height);
//JSON Enter data
var data = d3.json('https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/mast er/GDP-data.json',
(error, data) => {
var chartData = data.data;
//Stores barWidth variable, math.ciel rounds up, used to set an equal width for each rect
var barWidth = Math.ceil((width - padding) / chartData.length);
//Define xScale
const xScale = d3.scaleLinear()
.domain([0, d3.max(chartData, (d) => d[0])])
.range([padding, width - padding]);
//Define yScale
const yScale = d3.scaleLinear()
.domain([0, d3.max(chartData, (d) => d[1])])
.range([height - padding, padding]);
//Selects SVG elements and selects all rect elements within it
svg.selectAll("rect")
//Adds data to use later
.data(chartData)
//Allows us to add items to the dom if data is larger than ammoutn of rect elements selected
.enter()
//Adds rect element
.append("rect")
//Adds x attribute to x based off of d(chartData), need to create a date as a string is not enough
.attr("x", (d) => xScale(new Date(d[0])))
//Sets y attribute of rectangle
.attr("y", (d) => yScale(d[1]))
//Sets height, we minus the value from height to invert the bars
.attr("height", (d) => height - yScale(d[1]))
//sets width of rect elements
.attr("width", barWidth)
//fill in color of rects
.attr("fill", "black");
});
You are using dates for the X axis, so you'll be better to use a timescale rather than scaleLinear
const xScale = d3.scaleTime()
.domain(d3.extent(chartData, function(d) { return new Date(d[0]); }))
.range([padding, width - padding]);
Codepen: http://codepen.io/anon/pen/egbGaJ?editors=0111
You x values are strings that represent dates but you've made no attempt to treat them as such. Your current scale code is expecting them to be numbers. So you need to decide to create them as strings or dates. For example, coercing them to dates would look like this;
// a time parser
var tF = d3.timeParse("%Y-%m-%d");
// convert to dates
chartData.forEach(function(d){
d[0] = tF(d[0])
});
...
//Define xScale as a time scale
const xScale = d3.scaleTime()
.domain([0, d3.max(chartData, (d) => d[0])])
...
Updated codepen.
I have a couple of issues with a my D3 punchcard (plunk is here: https://plnkr.co/edit/vIejaTBrGrV07B8UWxOb):
It is looking crowded, I have zooming set up but I can't figure out how to set the initial view to a more "readable" scale, with the dots more spaced out. I plan to add in tooltips and varied radii for the dots.
//Create scale functions
var xScale = d3.scale.linear()
.domain([1900, 2020])
.range([0, w]);
var yScale = d3.scale.linear()
.domain([0, (deptlist.length)])
.range([h, 0]);
//console.log(deptlist.length);
//var rScale = d3.scale.linear()
//.domain([0, d3.max(dataset, function(d) { return d[1]; })])
//.range([2, 5]);
//Define X axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickFormat(function(d) {
//console.log(d);
return d;
});
//Define Y axis
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.tickFormat(function(d) {
//console.log(deptlist[d]);
return deptlist[d];
})
.ticks(deptlist.length - 1);
I can't get my deptlist[] to sort alphabetically.
It is also quite slow...
1. Better distinguish circles
You'll be better able to distinguish the dots if you make them initially smaller, and then make them bigger when the zoom level increases.
d3.selectAll('.dot')
// ...
.attr('r', 2)
Then when you zoom in, you can make the r value a product of the difference between the values in the xScale.domain. For example...
function zoomed() {
svg.selectAll(".dot")
.attr("cx", function(d) { return xScale(+d.year); })
.attr("cy", function(d) { return yScale(deptlist.indexOf(d.dept)); })
.attr('r', function(d) {
return 120 / (xScale.domain()[1] - xScale.domain()[0]);
});
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
}
There are many different ways you can scale your dots, this is just one way of going about it. Your xScale.domain() returns the domain of your xScale, e.g., [1900, 2020]. By subtracting the first value from the second, we get a value that can be used as a reference point from which can scale the dots.
I simply took the largest possible difference, 120, and divided it by the current value of the scale (which is changed on zoom). This creates a larger value the more that the zoom is increased.
2. Sort Y axis alphabetically
Using some of D3's array methods you can make your code much more declarative.
Instead of doing this:
var deptlist = [];
dataset.forEach(function(d) {
if(deptlist.indexOf(d.dept) == -1) deptlist.push(d.dept);
});
You can use d3.map, array.keys(), and d3.descending to (a) return only d.dept to your array, (b) get only the unique values from the array, and (c) use the JS native sort array method in combination with d3.ascending to sort them alphabetically.
var deptlistUnsorted = d3.map(dataset, function(d) {
return d.dept;
}).keys();
var deptlist = deptlistUnsorted.sort(d3.descending);
As a final note, your code is running slowly because you have a console.log statement inside of a forEach loop of an array of several thousand objects. This puts a lot of strain on the browser and is generally something to avoid when dealing with arrays of that size.
I updated your plunkr to reflect the code above.
I have a bar chart where I want to make the gap more pronounced between the 6th and the bar in my chart and the 12th and 13th bar in my chart. Right now I'm using .rangeRoundBands which results in even padding and there doesn't seem to be a way to override that for specific rectangles (I tried appending padding and margins to that particular rectangle with no success).
Here's a jsfiddle of the graph
And my code for generating the bands and the bars themselves:
var yScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([padding, h- padding], 0.05);
svg.selectAll("rect.bars")
.data(dataset)
.enter()
.append("rect")
.attr("class", "bars")
.attr("x", 0 + padding)
.attr("y", function(d, i){
return yScale(i);
})
.attr("width", function(d) {
return xScale(d.values[0]);
})
.attr("height", yScale.rangeBand())
You can provide a function to calculate the height based on data and index. That is, you could use something like
.attr("height", function(d,i) {
if(i == 5) {
return 5;
}
return yScale.rangeBand();
})
to make the 6th bar 5 pixels high. You can of course base this value on yScale.rangeBand(), i.e. subtract a certain number to make the gap wider.
Here's a function for D3 v6 that takes a band scale and returns a scale with gaps.
// Create a new scale from a band scale, with gaps between groups of items
//
// Parameters:
// scale: a band scale
// where: how many items should be before each gap?
// gapSize: gap size as a fraction of scale.size()
function scaleWithGaps(scale, where, gapSize) {
scale = scale.copy();
var offsets = {};
var i = 0;
var offset = -(scale.step() * gapSize * where.length) / 2;
scale.domain().forEach((d, j) => {
if (j == where[i]) {
offset += scale.step() * gapSize;
++i;
}
offsets[d] = offset;
});
var newScale = value => scale(value) + offsets[value];
// Give the new scale the methods of the original scale
for (var key in scale) {
newScale[key] = scale[key];
}
newScale.copy = function() {
return scaleWithGaps(scale, where, gapSize);
};
return newScale;
}
To use this, first create a band scale...
let y_ = d3
.scaleBand()
.domain(data.map(d => d.name))
.range([margin.left, width - margin.right])
.paddingInner(0.1)
.paddingOuter(0.5)
... then call scaleWithGaps() on it:
y = scaleWithGaps(y_, [1, 5], .5)
You can create a bar chart in the normal way with this scale.
Here is an example on Observable.
I'm building a bar plot in d3.js in which each bar represents total TB cases during a month. The data essentially consists of a date (initially strings in %Y-%m format, but parsed using d3.time.format.parse) and an integer. I'd like the axis labels to be relatively flexible (show just year boundaries, label each month, etc.), but I'd also like the bars to be evenly spaced.
I can get flexible axis labeling when I use a date scale:
var xScaleDate = d3.time.scale()
.domain(d3.extent(thisstat, function(d) { return d.date; }))
.range([0, width - margin.left - margin.right]);
... but the bars aren't evenly spaced due to varying numbers of days in each month (e.g., February and March are noticeably closer together than other months). I can get evenly-spaced bars using a linear scale:
var xScaleLinear = d3.scale.linear()
.domain([0, thisstat.length])
.range([0, width - margin.left - margin.right]);
... but I can't figure out how to then have date-based axis labels. I've tried using both scales simultaneously and only generating an axis from the xScaleDate, just to see what would happen, but the scales naturally don't align quite right.
Is there a straightforward way to achieve this that I'm missing?
You can combine ordinal and time scales:
// Use this to draw x axis
var xScaleDate = d3.time.scale()
.domain(d3.extent(thisstat, function(d) { return d.date; }))
.range([0, width - margin.left - margin.right]);
// Add an ordinal scale
var ordinalXScale = d3.scale.ordinal()
.domain(d3.map(thisstat, function(d) { return d.date; }))
.rangeBands([0, width], 0.4, 0);
// Now you can use both of them to space columns evenly:
columnGroup.enter()
.append("rect")
.attr("class", "column")
.attr("width", ordinalXScale.rangeBand())
.attr("height", function (d) {
return height - yScale(d.value);
})
.attr("x", function (d) {
return xScaleDate(d.date);
})
.attr("y", function (d){
return yScale(d.value);
});
I've created an example a while ago to demonstrate this approach: http://codepen.io/coquin/pen/BNpQoO
I had the same problem, I've ended up accepting that some months are longer than others and adjusting the column bar width so that the gap between bars remains constant. So tweaking the barPath function in the crossfilter home page demo (http://square.github.com/crossfilter/ - uses d3) I got something like this:
var colWidth = Math.floor(x.range()[1] / groups.length) - 1;//9;
if (i < n - 1) {
//If there will be column on the right, end this column one pixel to the left
var nextX = x(groups[i+1].key)
colWidth = nextX - x(d.key) - 1;
}
path.push("M", x(d.key), ",", height, "V", yVal, "h",colWidth,"V", height);
Try d3.scale.ordinal:
var y = d3.scale.ordinal()
.domain(yourDomain)
.rangeRoundBands([0, chartHeight], 0.2);
Tweek 0.2 parameter.