I want a ‘mirrored’ bar chart (i.e. one that looks like a sound wave) and have come up with the following using the d3 stack generator and a linear y scale:
import * as d3 from "d3";
const WIDTH = 300;
const HEIGHT = 300;
const LIMIT = 4;
const container = d3.select("svg").append("g");
var data = [];
for (var i = 0; i < 100; i++) {
data.push({
index: i,
value: Math.random() * LIMIT
});
}
var stack = d3
.stack()
.keys(["value"])
.order(d3.stackOrderNone)
.offset(d3.stackOffsetSilhouette);
var series = stack(data);
var xScale = d3
.scaleLinear()
.range([0, WIDTH])
.domain([0, data.length]);
var yScale = d3
.scaleLinear()
.range([HEIGHT, 0])
.domain([0, LIMIT / 2]);
var xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft().scale(yScale);
container
.selectAll(".bar")
.data(series[0])
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", d => {
return xScale(d.data.index);
})
.attr("y", d => {
return yScale(d[0]) / 2 - HEIGHT / 2;
})
.attr("width", WIDTH / series[0].length)
.attr("height", d => yScale(d[1]));
However, I feel like I’ve hacked the calculations for both the y scale domain and for positioning the blocks.
For the domain I currently use 0 to the data's upper limit / 2.
For my y position I use yScale(d[0]) / 2 - HEIGHT / 2; despite the height being directly based off the scale i.e. d => yScale(d[1]).
Is there a better, more idiomatic way to achieve what I want?
It seems the way the stack function calculates values has changed since D3 v2, and therefore I had to do two things to achieve this in a nicer way.
I switched my y scale domain to be the extents of the data and then translated by -0.5 * HEIGHT
I modified my calculation for the y position and height:
.attr('y', d => yScale(d[1]))
.attr('height', d => yScale(d[0]) - yScale(d[1]));
Related
I was trying to make a Bar Chart using D3 by calling the data from the JSON API, I was able to make a single bar visible but not able to make other bars visible. I hope there is some issue placing of the bars.
Different rectangles are visible in the developer tool, but all are coming up in a same place.
Any help would be much appreciated.
// javascript
const svg = d3.select('svg');
const width = +svg.attr('width');
const height = +svg.attr('height');
// Get the data from the JSON api
d3.json("https://api.covid19india.org/data.json")
.then( data => {
// Store the data in two variales
var stateNames = [];
var confirmedCases = [];
for (let i=1; i <= 10; i++){ //i <= (data.statewise.length) - 1
stateNames.push(data.statewise[i].state);
confirmedCases.push(+(data.statewise[i].confirmed));
}
//console.log(stateNames);
//console.log(confirmedCases);
// Max number of cases
let sortedData = [...confirmedCases];
let sortedCases = sortedData.sort(function(a,b){
return a-b;
})
// Measurement of the SVG Element
const margin = { top: 20, right: 20, bottom: 20, left: 100};
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Horizontal Visualization of Bar Graph
// X scale
const xScale = d3.scaleLinear()
.domain([0, sortedCases[sortedCases.length-1]])
.range([0, innerWidth]);
//console.log(xScale.domain());
//console.log(xScale.range());
const yScale = d3.scaleBand()
.domain(stateNames)
.range([0, innerHeight])
.padding(0.2);
//console.log(yScale.domain());
const yAxis = d3.axisLeft(yScale);
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
g.append('g').call(d3.axisLeft(yScale));
g.append('g').call(d3.axisBottom(xScale))
.attr('transform', `translate(0,${innerHeight})`);
g.selectAll('rect').data(confirmedCases).enter()
.append('rect')
.attr('width', xScale)
.attr('height', 30)
})
Reference to the codepen link, where I have tried
https://codepen.io/Jagat_Nayan/pen/mdepwgZ
You need to defin y property to relate the states to Y asix, and for that you need to pass both stateName and cases while creating rect.
I created an array inputData which containes both stateName and their respective cases and used it while creating rectangles. Below is your updated working code.
// javascript
const svg = d3.select("svg");
const width = +svg.attr("width");
const height = +svg.attr("height");
// Get the data from the JSON api
d3.json("https://api.covid19india.org/data.json").then((data) => {
var inputData = [];
for (let i = 1; i <= 10; i++) {
inputData.push({stateName: data.statewise[i].state, cases: +data.statewise[i].confirmed})
}
// Measurement of the SVG Element
const margin = { top: 20, right: 20, bottom: 20, left: 100 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
//const barWidth = innerWidth;
//const barPadding = 5;
// Horizontal Visualization of Bar Graph
// X scale
const xScale = d3
.scaleLinear()
.domain([0, d3.max(inputData, function(d){ return d.cases; })])
.range([0, innerWidth]);
const yScale = d3
.scaleBand()
.domain(inputData.map(function(d) { return d.stateName; }))
.range([0, innerHeight])
.padding(0.2);
const yAxis = d3.axisLeft(yScale);
const g = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
g.append("g").call(d3.axisLeft(yScale));
g.append("g")
.call(d3.axisBottom(xScale))
.attr("transform", `translate(0,${innerHeight})`);
g.selectAll("rect")
.data(inputData)
.enter()
.append("rect")
.attr("width", function(d) {return xScale(d.cases); })
.attr("y", function(d) {return yScale(d.stateName); })
.attr("height", 30);
});
Working graph on codepen - https://codepen.io/puneet2412/pen/LYpeOzd
Using D3, I want to take the data visualization type of a classical heatmap...
.. onto a compartmentalized version of several heatmap groups drawing data from a single data source.
Technically this should be one heatmap element drawing its data from a single source - separation and thus clustering/grouping is supposed to happen through sorting the data in the *.csv file (group one, group two, group three..) and the D3 *.JS file handling the styling.
While generating a single map:
// Build X scales and axis:
const x = d3.scaleBand()
.range([0, width])
.domain(myGroups)
.padding(0.00);
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
// Build Y scales and axis:
const y = d3.scaleBand()
.range([height, 0])
.domain(myVars)
.padding(0.00);
svg.append('g')
.call(d3.axisLeft(y));
assigning a color:
// Assign color scale
const myColor = d3.scaleLinear()
.range(['red', '#750606'])
.domain([1, 100]);
and fetching (sample) data:
// Read the data
d3.csv('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/heatmap_data.csv', (data) => {
data.sort(function(a, b) {
return myVars.indexOf(b.variable) - myVars.indexOf(a.variable) || myGroups.indexOf(a.group) - myGroups.indexOf(b.group)
});
Has been working like a charm:
CodePen
I'm struggling to expand this basic structure onto the generation of multiple groups as described above. Expanding the color scheme, trying to build several additional X and Y axis that cover different ranges result in a complete break of the D3 element rendering the map unable to be displayed at all.
Can someone point me in the right direction on how to generate multiple heatmap groups without breaking the heatmap?
I was able to solve the compartmentalization using a row and column based procedure to construct the compartments:
// Dimensions
const numCategoryCols = 4;
const numCategoryRows = Math.ceil(grouped.length / numCategoryCols);
const numEntryCols = 3;
const numEntryRows = Math.ceil(grouped[0].values.length / numEntryCols);
const gridSize = 20;
const width = gridSize * numCategoryCols * numEntryCols;
const height = gridSize * numCategoryRows * numEntryRows;
const tooltipArrowSize = 8;
// Containers
const container = d3
.select("#" + containerId)
.classed("heatmap-grid", true)
.style("position", "relative");
const svg = container
.append("svg")
.style("display", "block")
.style("width", "100%")
.attr("viewBox", [0, 0, width, height])
.style("opacity", 0);
svg.transition()
.duration(3000)
.delay((d,i) => i*200)
.style("opacity", 1)
// Heatmap
const gCategory = svg
.selectAll(".category-g")
.data(grouped, (d) => d.key)
.join("g")
.attr("class", "category-g")
.attr("fill", (d) => color(d.key))
.attr("transform", (_, i) => {
const y = Math.floor(i / numCategoryCols);
const x = i % numCategoryCols;
return `translate(${gridSize * numEntryCols * x},${
gridSize * numEntryRows * y
})`;
});
const gEntry = gCategory
.selectAll(".entry-g")
.data((d) => d.values)
.join("g")
.attr("class", "entry-g")
.attr("transform", (_, i) => {
const y = Math.floor(i / numEntryCols);
const x = i % numEntryCols;
return `translate(${gridSize * x},${gridSize * y})`;
});
const entry = gEntry
.append("rect")
.attr("width", gridSize)
.attr("height", gridSize)
.attr("fill-opacity", (d) => d.Severity / 100)
.on("mouseenter", showTooltip)
.on("mouseleave", hideTooltip);
I need some help with scaling with D3.In my codepen I am attempting to create a graph with some retrieve GDP data.
The data is retrieved and displayed correctly, but when I attempt to scale the graph only one vertical bar is displayed.
Can someone point me in the right direction?
Here is a link to project codepen:
https://codepen.io/henrycuffy/pen/gKVdgv
The main.js:
$(document).ready(function() {
$.getJSON(
'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json',
function(data) {
const dataset = data.data;
const w = 1000;
const h = 500;
var maxX = d3.max(dataset, d => d[1]);
var minDate = new Date(dataset[0][0]);
var maxDate = new Date(dataset[dataset.length - 1][0]);
var xScale = d3
.scaleTime()
.domain([minDate, maxDate])
.range([0, w]);
var yScale = d3
.scaleLinear()
.domain([0, maxX])
.range([h, 0]);
var xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft().scale(yScale);
const svg = d3
.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);
svg
.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.attr('x', (d, i) => xScale(i * 10))
.attr('y', d => h - yScale(d[1]))
.attr('width', 2)
.attr('height', d => yScale(d[1]))
.attr('fill', 'navy');
}
);
});
You are using a time scale for the x axis. But you aren't positioning the bars based on a time:
.attr('x', (d, i) => xScale(i * 10))
You are positioning each bar based on its index. The scale expects you to feed a date to it (it is taking the provided number and treating it as a date, which is pretty near the beginning of the epoch (Jan 1, 1970), which explains the positioning. The bars appear as one because each one is placed 10 milliseconds out from the previous one on a scale that covers decades, an imperceptible difference).
Instead let's feed the x scale the date in the data:
.attr('x', d => xScale(new Date(d[0]) )
Since the datum contains a string representation of the date, I'm converting to a date object here. You could do this to the data once it is loaded, but to minimize changes I'm just doing it when assigning the x attribute.
Here's an updated plunkr.
I have the following bar chart.
https://jsfiddle.net/zyjp1abo/
As you can see the values are between 1000 and 1005. Showing all the data from 0 to 1005 does not sense since the differences aren't visible.
I'd like to show the bars from 1000 and 1005 and change the y axis accordingly. Simply using extent and changing the domain does not work since the bars are drawn through the bottom margin. I want them to stop at the lowest value, i.e 1000.
https://jsfiddle.net/zyjp1abo/1/
Any ideas? Thank you!
If you want the domain to go from 1000 to 1005, you should use d3.extent. That's not the problem.
The problem is that you are using d3.extent but you keep using y(0) both for translating your x axis and for calculating the bars heights, which is wrong. You have to use your height and your margins.
Here is your code with those changes:
var defaults = {
target: '#chart',
width: 500,
height: 170,
margin: {
top: 20,
right: 20,
bottom: 20,
left: 50
},
yTicks: 5
}
class Barchart {
constructor(config) {
Object.assign(this, defaults, config)
const {
target,
width,
height,
margin
} = this
const w = width - margin.left - margin.right
const h = height - margin.top - margin.bottom
const {
yTicks
} = this
this.chart = d3.select(target)
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
this.x = d3.scaleBand()
.rangeRound([0, w])
.padding(0.1)
this.y = d3.scaleLinear()
.rangeRound([h, 0])
this.xAxis = d3.axisBottom(this.x)
this.chart.append('g')
.attr('class', 'x axis')
this.yAxis = d3.axisLeft(this.y)
.ticks(yTicks)
this.chart.append('g')
.attr('class', 'y axis')
}
render(data) {
const {
x,
y,
xAxis,
yAxis,
chart
} = this
// y.domain(d3.extent(data, v => v.value))
y.domain(d3.extent(data, v => v.value))
const domain = data.map(d => d.timestamp)
x.domain(domain)
chart.select('.x.axis')
.attr('transform', `translate(0, ${(defaults.height - defaults.margin.bottom - defaults.margin.top)})`)
.call(xAxis)
chart.select('.y.axis')
.call(yAxis)
const bars = chart.selectAll('.bar')
.data(data)
bars
.enter()
.append('rect')
.attr('class', 'bar')
.merge(bars)
.attr('x', d => x(d.timestamp))
.attr('y', d => y(d.value))
.attr('width', x.bandwidth())
.attr('height', d => defaults.height - defaults.margin.bottom - defaults.margin.top - y(d.value))
}
}
const random = (min = -10, max = 10) => (
window.Math.floor(window.Math.random() * (max - min + 1)) + min
)
let bar = []
for (let i = 0; i < 20; i++) {
bar.push({
timestamp: Date.now() - (19 - i) * 500,
value: random(1000, 1005)
})
}
const barchart = new Barchart()
barchart.render(bar)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg id="chart">
</svg>
PS: You could simplify the math for the SVG size and margins.
PPS: There is a lot of debate regarding if a dataviz can have a non-zero baseline. I believe that some kinds of charts, in some situations, can and should have a non-zero baseline, specially line charts, when the change rate is more important than the absolute value, for instance. However, bar charts should always have a zero baseline.
After you change your domain, this line:
.attr('height', d => Math.abs(y(d.value) - y(0)))
Is still calculating the height of the bar based of a 0 value. The conventional way to calculate the height as in this example, is to base it off the inner height of your chart (your variable h).
Here's an updated fiddle.
y.domain([0, d3.max(data, v => v.value)])
change 0 here to 1000, and other places respectively
How to use D3 to convert and display the right information from different units
E.g.
All data is in mm..
[
{ label: 'sample1', x: 300 },
{ label: 'sample2', x: 1200 },
{ label: 'sample3', x: 4150 }
]
So, the question is, how can I create a scale that understand the sample3 should be point in same place after the 4 and before 5.
Consider
10000, its just a sample, can be 102301 or any value
I want to use D3 scale if possible to do this conversion
Attempt
let scaleX = d3.scale.linear().domain([-10, 10]).range([0, 500]) // Missing the mm information...
You have a conceptual problem here:
Mapping an input (domain) to an output (range): that's the task of the scale.
Formatting the number and the unit (if any) in the axis: that's the task of the axis generator
Thus, in your scale, you'll have to set the domain to accept the raw, actual data (that is, the data the way it is) you have:
var scale = d3.scaleLinear()
.domain([-10000, 10000])//the extent of your actual data
.range([min, max]);
Then, in the axis generator, you change the value in the display. Here, I'm simply dividing it by 1000 and adding "mm":
var axis = d3.axisBottom(scale)
.tickFormat(d => d / 1000 + "mm");
Note that I'm using D3 v4 in these snippets.
Here is a demo using these values: -7500, 500 and 4250. You can see that the circles are in the adequate position, but the axis shows the values in mm.
var data = [-7500, 500, 4250];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var scale = d3.scaleLinear()
.domain([-10000, 10000])
.range([20, 480]);
var axis = d3.axisBottom(scale)
.tickFormat(d => d / 1000 + "mm");
var circles = svg.selectAll("foo")
.data(data)
.enter()
.append("circle")
.attr("r", 4)
.attr("fill", "teal")
.attr("cy", 40)
.attr("cx", d => scale(d));
var g = svg.append("g")
.attr("transform", "translate(0,60)")
.call(axis);
<script src="https://d3js.org/d3.v4.js"></script>
I found a way to do that..
const SIZE_MM = 10000
const SIZE_PX = 500
const scaleFormat = d3.scale.linear().domain([0, SIZE_MM]).range([-10, 10])
const ticksFormat = d => Math.round(scaleFormat(d))
const ticks = SIZE_MM / SIZE_PX
const lineScale = d3.scale.linear().domain([0, SIZE_MM ]).range([0, SIZE_PX])
lineScale(9500)
// 475