I am simply trying to find the easiest/most sublime way to create a single row of stacked rects. Using a let counter as per How to stack rects respective of previous rect's height? to store the previous data point can work or calculating all the points outright can work too as in: http://bl.ocks.org/wpoely86/e285b8e4c7b84710e463. Yet both these seem really elaborate for what seems to be a simple task: find where to put the x of a rect and how wide that rect should be. I went with the let approach below in this snippet:
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var qScale = d3.scaleLinear()
.range([749,0])
.domain([0,20]);
var quartiles = [3.78, 6.69, 10.09];
let qCounter = 0;
let wCounter = 0;
graphGroup.selectAll('.markers')
.data(quartiles)
.enter()
.append('rect')
.attr('class', 'markers')
.attr('x', function(d) {
let previous = qCounter;
return (qCounter += qScale(d), previous)
})
.attr('y', 50)
.attr('width', function(d) {return 749-qScale(d); })
.attr('height', 50)
.style('fill', 'gray')
.style('stroke', '3px');
<script src="https://d3js.org/d3.v5.min.js"></script>
It doesn't appear to be working correctly, there are only 2 rects. There should be 3 and they should be stacked close together. Their widths are determined by the values in quartiles.
Question
Assuming my .attr('x') logic is right, how can I determine the right width for the rects?
I am running into the same problem because I have no means that are in scope to reference the previous data point if I wanted to trying calculating the width for the rect as the pseudocode: qScale(d[1]) - qScale(d[0]). Must I make another counter for each such attribute? I'm using d3 v5, is there really no easier way to find the previous datum for use with stacking rects and the like?
Your qScale range is inverted, it should be:
.range([0, 749])
Even better, you should avoid magic numbers:
.range([0, width])
After doing that, change the width to just this:
.attr('width', function(d) {
return qScale(d);
})
Here is the code with those changes, and using a colour scale to differentiate the bars:
var margins = {
top: 20,
bottom: 300,
left: 30,
right: 100
};
var height = 600;
var width = 900;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var colors = d3.schemeCategory10;
var qScale = d3.scaleLinear()
.range([0, width])
.domain([0, 20]);
var quartiles = [3.78, 6.69, 10.09];
let qCounter = 0;
let wCounter = 0;
graphGroup.selectAll('.markers')
.data(quartiles)
.enter()
.append('rect')
.attr('class', 'markers')
.attr('x', function(d) {
let previous = qCounter;
return (qCounter += qScale(d), previous)
})
.attr('y', 50)
.attr('width', function(d) {
return qScale(d);
})
.attr('height', 50)
.style('fill', function(_, i) {
return colors[i]
})
.style('stroke', '3px');
<script src="https://d3js.org/d3.v5.min.js"></script>
Related
I started the D3.js challenge on FreeCodeCamp, the problem is that I solved it with the chart but it only gives me a display on the rectum, only one with the width and height that it I put, I'll show the code below.
The entire code on
<script>
//set d3
var w = 1000, h = 500;
var padding = 50;
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h)
//title
svg.append('text')
.attr('x', w / 2)
.attr('y', 50)
.text('United States GDP')
fetch('https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json')
.then((result)=>result.json())
.then((data)=>{
var the_data = data['data']
//get vals
var get_max = d3.max(data['data'])
var get_mix = d3.min(data['data'])
//for x
var max_x = Number(get_max[0].split('-')[0])
var min_x = Number(get_mix[0].split('-')[0])
//for y
var max_y = get_max[1]
var min_y = get_mix[1]
var xScale = d3.scaleLinear()
.domain([min_x, max_x])
.range([padding, w-padding])
var yScale = d3.scaleLinear()
.domain([min_y, max_y])
.range([h-padding, padding])
//the_chars
for(var i in the_data){
var get_year = Number(the_data[i][0].split('-')[0])
the_data[i][0] = get_year
}
svg.selectAll('rect')
.data(the_data)
.enter()
.append('rect')
.attr("x", (d) => { xScale(d[0]) })
.attr('y', (d)=>{ yScale(d[1]) })
.attr("width", 200)
.attr("height", 20)
//axis
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
//display axis
svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append('g')
.attr('transform', 'translate(' + padding + ', 0)')
.call(yAxis)
})
Now, what I need to do to display the charts!
I mention that the script tags are embedded in the body
Problem: Arrow functions without a return value. Solution: Instead use an explicit or an implicit return.
.attr("x", (d) => { xScale(d[0]) }) // returns undefined
.attr("x", (d) => xScale(d[0])) // implicit return
.attr("x", (d) => { return xScale(d[0]) }) // explicit return
Problem: Fixed height value. Solution Evaluate the height of each based on the GDP value (d[1]) instead.
.attr('height', 20) // fixed height
.attr('height', d => yScale(min_y) - yScale(d[1]))
// subtract from min range to account for padding and inverted y coordinates in SVG
Full solution in this codepen
I'm trying to make a bar chart but I can't figure out a way to make the bar start from the 0 point of y axis and not from the very bottom of the svg. How can I fix that?
let url = "https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json";
const padding = 50;
const height = 460;
const width = 900;
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
var arr = [];
d3.json(url, function(data) {
for (let i = 0; i < data.data.length; i++) arr[i] = data.data[i];
const yScale = d3.scaleLinear()
.domain([0, d3.max(arr, (d) => d[1])])
.range([height - padding, padding]);
const yAxis = d3.axisLeft(yScale);
svg.append('g')
.attr('transform', 'translate(' + padding + ', 0)')
.call(yAxis)
svg.selectAll('rect')
.data(arr)
.enter()
.append('rect')
.attr('fill', 'blue')
.attr('height', d => d[1] + padding)
.attr('width', 2.909090909090909)
.attr('x', (d, i) => padding + (3.2 * i))
.attr('y', d => yScale(d[1]))
.append('title')
.text(d => d[1])
});
<script src="https://d3js.org/d3.v4.min.js"></script>
You are incorrectly calculating the height of the rectangle, and not using your scale. It's also trickier since your use of padding is not the typical D3 convention.
svg.selectAll('rect')
.data(arr)
.enter()
.append('rect')
.attr('fill', 'blue')
.attr('height', d => height - padding - yScale(d[1]))
To start, I am fairly new to D3.Js. I have spent the past week or so working on a D3.JS issue-specifically making a graph with a Y-axis label. However, I cannot get the graph exactly how I want. It is almost there but inverted or my data comes out wrong. Now I will briefly show some of my code and images of my main problem before showing all of the code. I have spent time looking at other Stack Overflow posts with a similar issue and I do what is on those posts and still have the same issue.
For example, I thought that this post would have the solution: reversed Y-axis D3
The data is the following:
[0,20,3,8] (It is actually an array of objects but I think this may be all that is needed.
So, to start, when the yScale is like this:
var yScale = d3.scaleLinear()
.domain([0, maxPound]) //Value of maxpound is 20
.range([0, 350]);
The bar chart looks like this:
As one can see the Y chart starts with zero at the top and 20 at the bottom-which at first I thought was an easy fix of flipping the values in the domain around to this:
var yScale = d3.scaleLinear()
.domain([0, maxPound]) //Value of maxpound is 20
.range([0, 350]);
I get this image:
In the second image the y-axis is right-20 is on top-Yay! But the graphs are wrong. 0 now returns a value of 350 pixels-the height of the SVG element. That is the value that 20 should be returning! If I try to switch the image range values, I get the same problem!
Now the code:
var w = 350;
var h = 350;
var barPadding = 1;
var margin = {top: 5, right: 200, bottom: 70, left: 25}
var maxPound = d3.max(poundDataArray,
function(d) {return parseInt(d.Pounds)}
);
//Y-Axis Code
var yScale = d3.scaleLinear()
.domain([maxPound, 0])
.range([0, h]);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Creating SVG element
var svg = d3.select(".pounds")
.append('svg')
.attr("width", w)
.attr('height', h)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
svg.selectAll("rect")
.data(poundDataArray)
.enter()
.append("rect")
.attr('x', function(d, i){
return i * (w / poundDataArray.length);
})
.attr('y', function(d) {
return 350 - yScale(d.Pounds);
})
.attr('width', (w / 4) - 25)
.attr('height', function(d){
return yScale(d.Pounds);
})
.attr('fill', 'steelblue');
//Create Y axis
svg.append("g")
.attr("class", "axis")
.call(yAxis);
Thank you for any help! I believe that the error may be in the y or height values and have spent time messing around there with no results.
That is not a D3 issue, but an SVG feature: in an SVG, the origin (0,0) is at the top left corner, not the bottom left, as in a common Cartesian plane. That's why using [0, h] as the range makes the axis seem to be inverted... actually, it is not inverted: that's the correct orientation in an SVG. By the way, HTML5 Canvas has the same coordinates system, and you would have the same issue using a canvas.
So, you have to flip the range, not the domain:
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h, 0]);//the range goes from the bottom to the top now
Or, in your case, using the margins:
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h - margin.bottom, margin.top]);
Besides that, the math for the y position and height is wrong. It should be:
.attr('y', function(d) {
return yScale(d.Pounds);
})
.attr('height', function(d) {
return h - margin.bottom - yScale(d.Pounds);
})
Also, as a bonus tip, don't hardcode the x position and the width. Use a band scale instead.
Here is your code with those changes:
var poundDataArray = [{
Pounds: 10
}, {
Pounds: 20
}, {
Pounds: 5
}, {
Pounds: 8
}, {
Pounds: 14
}, {
Pounds: 1
}, {
Pounds: 12
}];
var w = 350;
var h = 350;
var barPadding = 1;
var margin = {
top: 5,
right: 20,
bottom: 70,
left: 25
}
var maxPound = d3.max(poundDataArray,
function(d) {
return parseInt(d.Pounds)
}
);
//Y-Axis Code
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h - margin.bottom, margin.top]);
var xScale = d3.scaleBand()
.domain(d3.range(poundDataArray.length))
.range([margin.left, w - margin.right])
.padding(.2);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Creating SVG element
var svg = d3.select("body")
.append('svg')
.attr("width", w)
.attr('height', h)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
svg.selectAll("rect")
.data(poundDataArray)
.enter()
.append("rect")
.attr('x', function(d, i) {
return xScale(i);
})
.attr('y', function(d) {
return yScale(d.Pounds);
})
.attr('width', xScale.bandwidth())
.attr('height', function(d) {
return h - margin.bottom - yScale(d.Pounds);
})
.attr('fill', 'steelblue');
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + margin.left + ",0)")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
I have a bar chart see plunker the problem is that I would like to move the y-axis ticks to be at the middle left side of the rects but they appear on the top and end. and I cannot seem to move them without destroying the chart.
my code
var info = [{
name: "Walnuts",
value: 546546
}, {
name: "Almonds",
value: 456455
}
];
/* Set chart dimensions */
var width = 960,
height = 500,
margin = {
top: 10,
right: 10,
bottom: 20,
left: 60
};
//subtract margins
width = width - margin.left - margin.right;
height = height - margin.top - margin.bottom;
//sort data from highest to lowest
info = info.sort(function(a, b) {
return b.value - a.value;
});
//Sets the y scale from 0 to the maximum data element
var max_n = 0;
var category = []
for (var d in info) {
max_n = Math.max(info[d].value, max_n);
category.push(info[d].name)
}
var dx = width / max_n;
var dy = height / info.length;
var y = d3.scale.ordinal()
.domain(category)
.range([0, height]);
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
var svg = d3.select("#chart")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr('preserveAspectRatio', 'xMidYMin')
.attr("viewBox", '0 0 ' + parseInt(width + margin.left + margin.right) + ' ' + parseInt(height + margin.top + margin.bottom))
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.selectAll(".bar")
.data(info)
.enter()
.append("rect")
.attr("class", function(d, i) {
return "bar" + d.name;
})
.attr("x", function(d, i) {
return 0;
})
.attr("y", function(d, i) {
return dy * i;
})
.attr("width", function(d, i) {
return dx * d.value
})
.attr("height", dy)
.attr("fill", function(d, i) {
if (d.name == 'Walnuts') {
return 'red'
} else {
return 'green'
}
});
var y_xis = svg.append('g')
.attr('id', 'yaxis')
.call(yAxis);
You are using range in y axis like this:
var y = d3.scale.ordinal()
.domain(category)
.range([0, height]);
You should be using 'rangeRoundBands' since the y scale is ordinal
var y = d3.scale.ordinal()
.domain(category)
.rangeRoundBands([0, height], .1);
working code here
For d3 versions like v4/v5.
Defining height as the graph/plot height, and max as the maximum value of y.
import { parseSvg } from 'd3-interpolate/src/transform/parse'
const yScale = d3
.scaleLinear()
.domain([0, max])
.rangeRound([height, 0])
const yAxis = d3.axisLeft(yScale)
svg
.append('g')
.call(yAxis)
.selectAll('.tick')
.each(function(data) {
const tick = d3.select(this)
const { translateX, translateY } = parseSvg(tick.attr('transform'))
tick.attr(
'transform',
translate(translateX, translateY + height / (2 * max))
)
})
Recently I needed something very very similar and I solved this with a call with selecting all text elements in the selection and moving their dy upwards. I will give an example with OP's code:
var y_xis = svg.append('g')
.attr('id','yaxis')
.call(yAxis)
.call(selection => selection
.selectAll('text')
.attr('dy', '-110') // this moves the text labels upwards
.attr('x', '110')); // this does the same job but horizontally
I'm trying to implement an SVG mask in D3, similar to this very simple jsfiddle example, but I must have lost something in translation. My implementation all takes place in a class that renders a graph. I'm trying to apply the mask to define bounds for the graph, so that when the data exceeds those bounds, the graph is neatly clipped. When I apply the mask, the bars of the graph completely disappear. As far as I can tell the mask in the right place. HELP!
Here is where I define the mask in my init() function:
// Add an SVG element with the desired dimensions and margin.
this.graph = d3.select(this.config.id).append("svg:svg")
.attr("width", this.width + this.m[1] + this.m[3])
.attr("height", this.height + this.m[0] + this.m[2])
.append("svg:g")
.attr("transform", "translate(" + this.m[3] + "," + this.m[0] + ")");
var maskWidth = 640;
var maskHeight = 321;
this.graph.append('svg:defs') <------ I START DEFINING IT HERE !
.call(function (defs) {
// Appending the mask
defs.append('svg:mask')
.attr('id', 'mask')
.attr('width', maskWidth)
.attr('height', maskHeight)
.attr('x', 0)
.attr('y', 0)
.call(function(mask) {
mask.append('svg:rect')
.attr('width', maskWidth)
.attr('height', maskHeight)
.attr('fill', '#ffffff')
});
});
Here is the Method that draws bars on the graph where I attempt to apply the mask (see the last line):
addBars: function (data){
var numberOfBars = Math.floor(this.xMaximum);
var barWidth = this.width/numberOfBars;
// Generate a histogram using twenty uniformly-spaced bins.
var histogramData = d3.layout.histogram()
.bins(this.xScale.ticks(numberOfBars))
(data);
//console.trace('typeof: '+typeof this.xScale);
var xScale = this.xScale;
var yScale = this.yScale;
var height = this.height;
this.bars = this.graph.selectAll("bar")
.data(histogramData, function(d){ return d;})
.enter()
.append("rect")
.attr("class","bar")
.attr("fill","steelblue")
.attr("transform", function(d, i) {
var yOffset = height;
return "translate(" + (i * barWidth - barWidth/2) + ","+yOffset+")";
})
.attr("y", function(d,i) {
var yPosition = yScale(d.length)- height;
return (yScale(d.length)-height);
})
.attr("height", function(d) {
return height - yScale(d.length);
})
.attr("width", barWidth - 1)
.attr('mask', 'url(#mask)'); <---- OVER HERE !!!!
},
Here is a link to the resulting HTML in Chrome Developer Tools (I've highlighted the <defs> and one of the graph bars that should be masked):Chrome Developer Tools Dynamic HTML
As far as I can tell everything looks good. This leads me to believe that the mask is mis-aligned with the bar, causing the bar to be invisible. However, in the developer tools, when I hover over the <rect> element, it shows it as overlaying the graph bars, so it doesn't seem like an alignment issue. Any help would be appreciated.
Lastly, I've made a jsfiddle of the class being used in my application (see the comments for the link.). Below is also the entire class for drawing the graph, just in case it would be helpful to see the code in context:
// HistogramGrapher class - constructor
var HistogramGrapher = function() {
// assign default properties
this.config = {
id: "",
xAxisLabel: "xAxis",
yAxisLabel: "yAxis",
width: 1000,
height: 400,
title: "Title",
mean: 20
};
// define variables
this.m = [40, 80, 40, 80]; // margins
this.width; // width
this.height; // height
this.xAxisLabel;
this.yAxisLabel;
this.graph;
this.bars;
this.lines;
this.xScale;
this.xScaleInvert;
this.xAxis;
this.yScale;
this.yScaleInvert;
this.yAxis;
this.yMaximum = 25;
this.xMaximum = 2 * this.config.mean;
}
// methods for this class
HistogramGrapher.prototype = {
init: function (options) {
// copy properties of `options` to `config`. Will overwrite existing ones.
for(var prop in options) {
if(options.hasOwnProperty(prop)){
this.config[prop] = options[prop];
}
}
// update variables
this.updateWidth(this.config.width);
this.updateHeight(this.config.height);
this.updateXMaximum(this.config.mean);
// X scale will fit all values from datay[] within pixels 0-w
this.xScale = d3.scale.linear()
.domain([0, this.xMaximum])
.range([0, this.width]);
this.xScaleInvert = d3.scale.linear()
.range([0, this.xMaximum])
.domain([0, this.width]);
// Y scale
this.yScale = d3.scale.linear()
.domain([0, this.yMaximum])
.range([this.height,0]);
this.yScaleInvert = d3.scale.linear()
.range([0, this.yMaximum])
.domain([this.height,0]);
// Add an SVG element with the desired dimensions and margin.
this.graph = d3.select(this.config.id).append("svg:svg")
.attr("width", this.width + this.m[1] + this.m[3])
.attr("height", this.height + this.m[0] + this.m[2])
.append("svg:g")
.attr("transform", "translate(" + this.m[3] + "," + this.m[0] + ")");
var maskWidth = 640;
var maskHeight = 321;
this.graph.append('svg:defs')
.call(function (defs) {
// Appending the mask
defs.append('svg:mask')
.attr('id', 'mask')
.attr('width', maskWidth)
.attr('height', maskHeight)
.attr('x', 0)
.attr('y', 0)
.call(function(mask) {
mask.append('svg:rect')
.attr('width', maskWidth)
.attr('height', maskHeight)
.attr('fill', '#ffffff')
});
});
// create xAxis
this.xAxis = d3.svg.axis().scale(this.xScale)
.tickSize(-this.height)
.tickSubdivide(true);
// create yAxis
this.yAxis = d3.svg.axis().scale(this.yScale)
.tickSize(-this.width)
.tickSubdivide(true)
.orient("left");
// Add the x-axis label.
this.graph.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.attr("x", this.width)
.attr("y", this.height + 25)
.text(this.config.xAxisLabel);
// Add the y-axis label.
this.graph.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("y", -30)
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.text(this.config.yAxisLabel);
// add Title
this.graph.append("text")
.attr("x", this.width/2 )
.attr("y", -20 )
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text(this.config.title);
// Add the x-axis.
this.graph.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + this.height + ")")
.call(this.xAxis);
// Add the y-axis.
this.graph.append("svg:g")
.attr("class", "y axis")
.call(this.yAxis);
},
updateWidth: function(width){
this.width = width - this.m[1] - this.m[3];
},
updateHeight: function(height){
this.height = height - this.m[0] - this.m[2]; // height
},
updateXMaximum: function(mean){
this.xMaximum = 2.5 * mean;
},
addBars: function (data){
var numberOfBars = Math.floor(this.xMaximum);
var barWidth = this.width/numberOfBars;
// Generate a histogram using twenty uniformly-spaced bins.
var histogramData = d3.layout.histogram()
.bins(this.xScale.ticks(numberOfBars))
(data);
//console.trace('typeof: '+typeof this.xScale);
var xScale = this.xScale;
var yScale = this.yScale;
var height = this.height;
this.bars = this.graph.selectAll("bar")
.data(histogramData, function(d){ return d;})
.enter()
.append("rect")
.attr("class","bar")
.attr("fill","steelblue")
.attr("transform", function(d, i) {
var yOffset = height;
return "translate(" + (i * barWidth - barWidth/2) + ","+yOffset+")";
})
.attr("y", function(d,i) {
var yPosition = yScale(d.length)- height;
return (yScale(d.length)-height);
})
.attr("height", function(d) {
return height - yScale(d.length);
})
.attr("width", barWidth - 1)
.attr('mask', 'url(#mask)');
},
addLine: function (data){ // the data must be in the form " [ {'x':x1, 'y':y1} , {'x':x2, 'y':y2} , {'x':x3, 'y':y3} ... ]
var xScale = this.xScale;
var yScale = this.yScale;
var height = this.height;
// create a line function that can convert data[] into x and y points
var lineFunction = d3.svg.line()
// assign the X function to plot our line as we wish
.x(function(d) { return xScale(d.x); })
.y(function(d) { return yScale(d.y); })
.interpolate("linear");
this.lines = this.graph.append("path")
.attr("d", lineFunction(data))
.attr("class", "line")
.attr("stroke", "green")
.attr("stroke-width", 2)
.attr("fill","none");
},
clear: function () {
var bars = d3.selectAll(".bar").remove();
var lines = d3.selectAll(".line").remove();
},
getxScale: function () {
return this.xScale;
},
getxScaleInvert: function () {
return this.xScaleInvert;
}
}
Ok, I saw what's going on. You should apply the clipping mask to the bars and the line by appending a clipping mask to the graph area:
//clipping mask
yourSvg.append("clipPath")
.attr("id", "chart-area")
.append("rect")
.attr("x", yourXcoordinates)
.attr("y", yourYcoordinates)
.attr("width", 333) //this was the width provided by the webinspector
.attr("height", 649) //this was the height provided by the webinspector;
then when you plot the line and the bars, add this to both of the generators
.attr("clip-path", "url(#chart-area)")
and this should give you the clipping you're looking for. Basically what it does is clip everything outside the area of that rectangle, so if you plot it correctly, it should clip out unwanted things