D3 - Chart with positive and negative values - javascript

I am trying to build the d3 chart with the positive and negative number values as below
and I found some examples of this and this. I am facing difficulties in customizing it because I have no prior experience in d3 and I think it would need some time for learning. I tried that as well. Created some simple chart examples but could not achieve the above. So I thought of reaching for help. Maybe someone can help with this if they have already done a similar chart or some guidance would be greatly appreciated. Thanks in advance.

The first step would be to identify how this chart can be simplified. Removing features until the most basic thing remains. Then, build that and gradually add features until it resembles what you want.
In your case, that'd be a horizontal bar chart. Then, add some negative values and a centred zero-line. Finally, make the height of the bars less so they become nodes, and add the text.
I'll try to add something like this, in these steps, without the layout and everything, but hopefully you'll be able to see my logic.
The basic vertical bar chart
// Some fake data
const data = ['SaaS', 'Sales', 'Fruits & Veggies', 'IT'].map((v, i) => ({
name: v,
value: 3 * i + 2
}));
const width = 600,
height = 300
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// Process it to find the x and y axis domains
// scaleLinear because it considers numbers
const x = d3.scaleLinear()
.domain([0, d3.max(data.map(d => d.value))]) // the possible values
.range([0, width]); // the available screen space
// scaleBand because it's just categorical data
const y = d3.scaleBand()
.domain(data.map(d => d.name)) // all possible values
.range([height, 0]) // little weird, y-axis is always backwards, because (0,0) is the top left
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
// Append a container element. This will hold the chart
.append('g')
// Move it a little to account for the axes and labels
.attr('transform', `translate(${margin.left} ${margin.right})`);
// Draw the bars
// First, assign the data to the bar objects, this will decide which to remove, update, and add
const bars = g.append('g')
.selectAll('rect')
.data(data);
// Good practice: always call remove before adding stuff
bars.exit().remove();
// Add the new bars and assign any attributes that do not depend on the data
// for example, font for texts
bars.enter()
.append('rect')
.attr('fill', 'steelblue')
// Now merge it with the existing bars
.merge(bars)
// From now on we operate on both the old and the new bars
// Bars are weird, first we position the top left corner of each bar
.attr('x', 0)
.attr('y', d => y(d.name))
// Then we determine the width and height
.attr('width', d => x(d.value))
.attr('height', y.bandwidth())
// Draw the x and y axes
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
Now I'll remove all old comments and explain what I'm doing differently.
The negative horizontal bar chart
// Now, the data can also be negative
const data = ['SaaS', 'Sales', 'Fruits & Veggies', 'IT'].map((v, i) => ({
name: v,
value: 3 * i - 5
}));
const width = 600,
height = 300,
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// Now, we don't use 0 as a minimum, but get it from the data using d3.extent
const x = d3.scaleLinear()
.domain(d3.extent(data.map(d => d.value)))
.range([0, width]);
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([height, 0])
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left} ${margin.right})`);
const bars = g.append('g')
.selectAll('rect')
.data(data);
bars.exit().remove();
bars.enter()
.append('rect')
.merge(bars)
// All the same until here
// Now, if a bar is positive it starts at x = 0, and has positive width
// If a bar is negative it starts at x < 0 and ends at x = 0
.attr('x', d => d.value > 0 ? x(0) : x(d.value))
.attr('y', d => y(d.name))
// If the bar is positive it ends at x = v, but that means it's x(v) - x(0) wide
// If the bar is negative it ends at x = 0, but that means it's x(0) - x(v) wide
.attr('width', d => d.value > 0 ? x(d.value) - x(0) : x(0) - x(d.value))
.attr('height', y.bandwidth())
// Let's color the bar based on whether the value is positive or negative
.attr('fill', d => d.value > 0 ? 'darkgreen' : 'darkred')
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
And now, I'll change the bars to the nodes you have in your example code.
The horizontal chart with nodes
const data = ['SaaS', 'Sales', 'Fruits & Veggies', 'IT'].map((v, i) => ({
name: v,
value: 3 * i - 5
}));
// We want to center each rect around the value it's supposed to have.
// That means that we need to have a node width
const nodeWidth = 60;
const width = 600,
height = 300,
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// We also need to make sure there is space for all nodes, even at the edges.
// One way to get this is by just extending the domain a little.
const domain = d3.extent(data.map(d => d.value));
const x = d3.scaleLinear()
.domain([domain[0] - 1.5, domain[1] + 1.5])
.range([0, width]);
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([height, 0])
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left} ${margin.right})`);
const bars = g.append('g')
.selectAll('rect')
.data(data);
bars.exit().remove();
// All the same until here
bars.enter()
.append('rect')
// width has become a constant
.attr('width', nodeWidth)
// Now, transform each node so it centers around the value it's supposed to have
.attr('transform', `translate(${-nodeWidth / 2} 0)`)
// Round the corners for aesthetics
.attr('rx', 15)
.merge(bars)
// `x` denotes the placement directly again
.attr('x', d => x(d.value))
.attr('y', d => y(d.name))
.attr('height', y.bandwidth())
.attr('fill', d => d.value > 0 ? 'darkgreen' : 'darkred');
// Now one more thing, we want to add labels to each node.
// `<rect>` can't have children, we we add them to the plot seperately
// using the same `data` as for the bars
const labels = g.append('g')
.selectAll('text')
.data(data);
labels.exit().remove();
labels.enter()
.append('text')
.attr('fill', 'white')
.attr('text-anchor', 'middle') // center-align the text
.attr('dy', 5) // place it down a little so it middles nicely in the node.
.merge(bars)
// `x` denotes the placement directly
.attr('x', d => x(d.value))
// Add half a bar's height to target the center of each node
.attr('y', d => y(d.name) + y.bandwidth() / 2)
// Actually fill in the text
.text(d => d.value);
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I hope you can follow this. Let me know if anything about this tutorial is unclear.

Related

How to place the bars of a bar chart in the right positions of the xAxis using d3.js?

I'm making a bar chart but I'm having problems to match the bar positions with the xAxis. They're not in the right place, for example, by hovering the bar above the 2010 mark, you can see it shows a 2007 value. 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 = 940;
const barthickness = 2.909090909090909;
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
var arr = [];
var years = [];
d3.json(url, function(data) {
for (let i = 0; i < data.data.length; i++) {
arr[i] = data.data[i];
years[i] = parseInt(data.data[i][0].slice(0,4));
}
const yScale = d3.scaleLinear()
.domain([0, d3.max(arr, (d) => d[1])])
.range([height - padding, padding]);
const xScale = d3.scaleLinear()
.domain([d3.min(years, d => d), d3.max(years, (d) => d)])
.range([padding, width - padding]);
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
svg.append("g")
.attr("transform", "translate(0," + (height - padding) + ")")
.call(xAxis);
svg.append('g')
.attr('transform', 'translate(' + padding + ', 0)')
.call(yAxis)
svg.selectAll('rect')
.data(arr)
.enter()
.append('rect')
.attr('fill', 'blue')
.attr('height', d => height - padding - yScale(d[1]))
.attr('width', barthickness)
.attr('x', (d, i) => padding + (3.2* i))
.attr('y', d => yScale(d[1]))
.append('title')
.text((d, i) => years[i] + ': ' + d[1])
});
<script src="https://d3js.org/d3.v4.min.js"></script>
The problem is that you are not using your x-scale to position the bars. You are using padding + (3.2* i) to set the x coordinate of the bars, which does not line up with your scale. Your chart is 840 pixels wide and has 275 bars, which would be ~3.055 pixels per bar. Your code is placing bars every 3.2 pixels, which is too far.
Typically with bar charts, rather than hard-coding a bar thickness, you use a band scale. You'll want to use your scales both in your axes and to position the bars.
Alternatively, since you are working with temporal data, you could also consider using an area chart instead of a bar chart.
Below I've provided two similarly looking charts for your data. One is a bar chart and the other an area chart.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="bar-chart"></div>
<div id="area-chart"></div>
<script>
const url = 'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json';
d3.json(url).then(json => {
// convert the string into Date objects
const parse = d3.timeParse('%Y-%m-%d');
const data = json.data.map(d => ({ date: parse(d[0]), value: d[1] }));
barchart(data);
areachart(data);
});
function barchart(data) {
// set up
const margin = { top: 20, right: 20, bottom: 20, left: 30 };
const width = 600 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select('#bar-chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// scales
const x = d3.scaleBand()
.domain(data.map(d => d.date))
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height, 0]);
// axes
// by default, axes for band scales show tick marks for every bar
// that would be too cluttered for this data, so we override this
// by explicitly setting tickValues()
const [minDate, maxDate] = d3.extent(data, d => d.date);
const xAxis = d3.axisBottom(x)
.tickSizeOuter(0)
// only show the year in the tick labels
.tickFormat(d3.timeFormat('%Y'))
.tickValues(d3.timeTicks(minDate, maxDate, 10));
const yAxis = d3.axisLeft(y)
.tickSizeOuter(0)
.ticks(10, '~s');
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis);
svg.append('g')
.call(yAxis);
// bars
// function to convert Date into string showing the month and year
const format = d3.timeFormat('%b %Y');
svg.selectAll('rect')
.data(data)
.join('rect')
.attr('x', d => x(d.date))
.attr('width', d => x.bandwidth())
.attr('y', d => y(d.value))
.attr('height', d => height - y(d.value))
.attr('fill', 'steelblue')
.append('title')
.text(d => `${format(d.date)}: ${d.value}`)
}
function areachart(data) {
// set up
const margin = { top: 20, right: 20, bottom: 20, left: 30 };
const width = 600 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select('#area-chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// scales
const x = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height, 0]);
// area generator
const area = d3.area()
.x(d => x(d.date))
.y0(y(0))
.y1(d => y(d.value))
.curve(d3.curveStepAfter);
// axes
const xAxis = d3.axisBottom(x)
.tickSizeOuter(0)
// only show the year in the tick labels
.tickFormat(d3.timeFormat('%Y'));
const yAxis = d3.axisLeft(y)
.tickSizeOuter(0)
.ticks(10, '~s');
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis);
svg.append('g')
.call(yAxis);
// area
svg.append('path')
.attr('d', area(data))
.attr('fill', 'steelblue')
}
</script>
</body>
</html>

Offset x-axis ticks with scaleTime

I'm trying to figure out how I can offset the x-axis ticks, as shown in this example, to be in the center of the bar when the x-axis uses scaleTime.
Right now, I'm applying a transform to the axis when I append it to the svg as such:
// x-axis code
const x = d3.scaleTime()
.domain([d3.min(dates), d3.max(dates)])
.range([margin.left, width - margin.right]);
const x_axis = d3.axisBottom()
.scale(x);
...
// offsetting the axis horizontally when I append it with bandwidth / 2
svg.append('g')
.attr('transform', `translate(${bandwidth / 2},${height - margin.bottom})`)
.call(x_axis);
But this feels hacky and leaves space between the x-axis and the y-axis.
It seems like the example I mentioned has this right because it's not using scaleTime but once scaleTime comes into the picture then things get bad. How can offset my scaleTime ticks so they line up with the middle of my bars?
Full code below:
import * as d3 from 'd3';
import rawData from './data/readings.json';
import {
barSpacing,
margin,
getBandwidth,
} from './helpers';
const width = 1000;
const height = 500;
const animationDurationRatio = 5;
const barStyle = {
color: 'steelblue',
opacity: {
default: .9,
hover: 1
}
};
const getStepData = (data, stepNum) => {
return data.map((item, i) => {
const value = i < stepNum ? item.value : 0
return {
...item,
value
};
});
};
const data = rawData.map(item => {
return {
date: new Date(item.date),
value: item.breakfast
}
});
const dates = data.map(d => d.date);
const x = d3.scaleTime()
.domain([d3.min(dates), d3.max(dates)])
.range([margin.left, width - margin.right]);
const y = d3.scaleLinear()
.domain([0, d3.max(data.map(d => d.value))]).nice()
.range([height - margin.bottom, margin.top]);
const color = d3.scaleSequential(d3.interpolateRdYlGn)
.domain([140, 115]);
// Got these values using trial and error
// Still not 100% sure how this domain works
const x_axis = d3.axisBottom()
.scale(x);
const y_axis = d3.axisLeft()
.scale(y);
const chartWidth = x.range()[1];
const bandwidth = getBandwidth(chartWidth, data, barSpacing);
const svg = d3.create('svg')
.attr('width', chartWidth)
.attr('height', height)
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('text-anchor', 'end');
const bar = svg.selectAll('g')
.data(getStepData(data, 0))
.join('g');
bar.append('rect')
.attr('fill', d => {
return color(d.value);
})
.attr('opacity', barStyle.opacity.default)
.attr('x', d => {
return x(d.date)
})
.attr('y', d => y(d.value))
.attr('width', bandwidth)
.attr('height', d => y(0) - y(d.value))
.on('mouseover', function() {
d3.select(this)
.transition(30)
.attr('opacity', barStyle.opacity.hover);
})
.on('mouseout', function() {
d3.select(this)
.transition()
.attr('opacity', barStyle.opacity.default);
});
bar.append('text')
.attr('fill', 'white')
.attr('x', (d, i) => x(d.date) + bandwidth / 2)
.attr('y', d => y(0) - 10)
.attr('dx', d => `0.${d.value.toString().length * 50}em`)
.text((d, i) => data[i].value);
svg.append('g')
.attr('transform', `translate(${bandwidth / 2},${height - margin.bottom})`)
.call(x_axis);
svg.append('g')
.attr('transform', `translate(${margin.left},0)`)
.call(y_axis);
document.querySelector('body').appendChild(svg.node());
function animateBars (data) {
const bars = svg.selectAll('rect')
.data(data);
bars
.transition()
.ease(d3.easeLinear)
.duration(d => animationDurationRatio * d.value)
.attr('y', d => y(d.value))
.attr('fill', d => {
return color(d.value);
})
.attr('height', d => y(0) - y(d.value));
}
animateBars(data)
Because scaleTime is a continuous scale, not a banded one, this isn't supported without some sort of hack or workaround (see workarounds below).
Note: Perhaps one reason for this is that some think bar charts aren't a good fit for time scales. Instead time is better visualized with a line chart or an area chart.
Solution
The best solution for this is to switch to scaleBand or something else that supports discrete bands (here's an example).
Workarounds
One workaround is to make the first bar half the size and then offset all bars by half their width:
.attr('x', d => {
return x(d.date) - bandwidth / 2
})
.attr('width', (d, i) => i === 0 ? bandwidth / 2 : bandwidth)
Other hacks/workarounds might include adding an extra day, hiding the first tick, or messing with the axis offset.
Source: https://observablehq.com/#d3/stacked-bar-chart#comment-af5453e2ab24d987
You need to translate both the axes with the same x units.
svg.append('g')
.attr('transform', `translate(${margin.left},${height-margin.bottom})`)
.call(x_axis);
svg.append('g')
.attr('transform', `translate(${margin.left},0)`)
.call(y_axis);
const numberOfTicks = 5;
const x_axis = d3.axisBottom()
.scale(x)
.ticks(numberOfTicks - 1);
You can set the number of ticks in x-axis. The number of ticks generated on graph will be numberOfTicks + 1

Beautify scaleBand

I just tried out d3js for some days and I want to beautify the x and y scales of my graph to be something like this
But this is what I got so far.
I have tried changing from scaleBand() to scaleLinear() and fix the normally bandwidth() method to a constant value, the graph just would not show.
This is the code
mounted () {
this.generateChart()
},
methods: {
generateChart () {
// set the dimensions and margins of the graph
const margin = { top: 20, right: 20, bottom: 30, left: 30 }
const width = 1850 - margin.left - margin.right
const height = 200 - margin.top - margin.bottom
// make the area for the graph to stay
const svg = d3.select('#heatmap')
.append('svg') // svg area can include headers and color scales
.attr('width', width + margin.left + margin.right) // set width
.attr('height', height + margin.top + margin.bottom) // set height
.append('g') // new g tag area for graph only
.attr('transform', `translate(${margin.left}, ${margin.bottom})`)
// stick g tag to the bottom
// range function generate graph scales
// TODO: make a range using date and time
const xLabel = d3.range(259)
const yLabel = d3.range(23, -1, -1)
// create x, y scales and axes
const x = d3.scaleBand()
.domain(xLabel)
.range([0, width])
.padding(0.05)
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
const y = d3.scaleBand()
.domain(yLabel)
.range([height, 0])
.padding(0.05)
svg.append('g').call(d3.axisLeft(y))
d3.json('../predictions.json').then(function (data) {
svg.selectAll()
.data(data.heatmaps.kw.Sand_Heads)
.enter()
.append('rect')
.attr('x', function (d) {
return x(d[1]) // return cell's position
})
.attr('y', function (d) {
return y(d[0])
})
.attr('cx', 1)
.attr('cy', 1)
.attr('width', x.bandwidth()) // return cell's width
.attr('height', y.bandwidth()) // return cell's height
.style('fill', function (d) {
return rgbaToHex(0, 128, 255, 100 * d[2])
})
.on('mouseover', function () { // box stroke when hover
d3.select(this)
.style('stroke', 'black')
.style('opacity', 1)
})
.on('mouseout', function () { // fade block stroke when mouse leave the cell
d3.select(this)
.style('stroke', 'none')
.style('opacity', 0.8)
})
})
}
Note: I have to make it work with date selection in the future too.
This is the structure of the data I'm working on.
{
"days": ["2019-04-11", "2019-04-12", ..., "2019-12-25"],
"heatmaps": {
"kw": {
"Tilly_Point": [[5, 112, 0.0012], [6, 112, 0.0016], ...],
"Mouat_Point": [...]
},
"hw": {
...
}
}
}
Explanation:
the first element of subarray in Tilly_Point is the time of the whale found. ranging from 0 to 23 (midnight to next midnight) and 5 means 05:00 A.M. to 06:00 A.M.
the second element is the nth day of the operation. It's 112 meaning it's the 112th day of the operation. which is 1 August 2019
the last element is the real data being plotted on the graph. the higher -> darker colour towards the real color with 1 opacity
By looking at the desired design we can understand what you mean by "beautify" is reducing the number of ticks. And you are absolutely correct: in very few and specific situations we need to show all of them; most of the times, the design is cleaner and the user benefits from a more tidy dataviz if we choose what ticks to display.
That's clear if we look at this basic example I wrote, simulating your axes:
const svg = d3.select("svg");
const yScale = d3.scaleBand()
.domain(d3.range(25))
.range([10, 80])
.paddingInner(1);
const xScale = d3.scaleBand()
.domain(d3.range(261))
.range([25, 490])
.paddingInner(1);
d3.axisLeft(yScale)(svg.append("g").attr("transform", "translate(25,0)"));
d3.axisBottom(xScale)(svg.append("g").attr("transform", "translate(0,80)"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="100"></svg>
There are different approaches for reducing the number of ticks here: you can explicitly chose the ticks to show by value or, as I'll do in this answer, you can simply choose how many of them to show. Here, I'll do this using the remainder operator (%) filtering the scale's domain and passing it to tickValues (since you have a band scale we cannot use ticks), for instance showing every 6th value for the y axis:
.tickValues(yScale.domain().filter((_, i) => !(i % 6)))
Here is the result:
const svg = d3.select("svg");
const yScale = d3.scaleBand()
.domain(d3.range(25))
.range([10, 80])
.paddingInner(1);
const xScale = d3.scaleBand()
.domain(d3.range(261))
.range([25, 490])
.paddingInner(1);
d3.axisLeft(yScale).tickValues(yScale.domain().filter((_, i) => !(i % 6)))(svg.append("g").attr("transform", "translate(25,0)"));
d3.axisBottom(xScale).tickValues(xScale.domain().filter((_, i) => !(i % 20)))(svg.append("g").attr("transform", "translate(0,80)"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="300"></svg>

D3.js bar chart, bars extending from top to bottom, instead of bottom to top

Barchart image
D3.js bar chart, bars extending from top to bottom, instead of bottom to top.
I am not sure what attributes i should be changing to correct this.
I have posted my code and an image of the resulting chart.
...
const marketCataRender = marketCataData => {
const marketCataSVG = d3.select('.marketCataChart').append('svg')
marketCataSVG.attr('class', 'marketCataSVG')
.attr('height', marketCataHeight)
.attr('width', marketCataWidth);
// x y values
const xValue = d => d.loc_start_str;
const yValue = d => d.total_matched;
// x y scales
const xScale = d3.scaleBand()
.domain(marketCataData.map(xValue))
.range(\[0, innerWidth\]);
const yScale = d3.scaleLinear()
.domain(d3.extent(marketCataData, yValue))
.range(\[innerHeight, 0\])
.nice();
// x y axis
const xAxis = d3.axisBottom(xScale)
const yAxis = d3.axisLeft(yScale)
// set chart group to make it easier to transform
const g = marketCataSVG.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// x y axis groups
const xAxisG = g.append('g')
.call(xAxis)
.attr('transform', `translate(0, ${innerHeight})`)
.selectAll('text')
.style('text-anchor', 'end')
.attr('transform', `rotate(-90)`)
.attr('x', -7)
const yAxisG = g.append('g')
.call(yAxis)
// Apply bar chart rectangle to chart
const marketCataRect = g.selectAll('rect')
marketCataRect.data(marketCataData)
.enter().append('rect')
.attr('x', d => xScale(xValue(d)))
.attr('height', d => yScale(yValue(d)))
.attr('width', xScale.bandwidth());
}][1]
...
You haven't declared the Y coordinates for your rectangles.
You need to scale the y coordinate of your rectangles.
const marketCataRect = g.selectAll('rect')
marketCataRect.data(marketCataData)
.enter().append('rect')
.attr('x', d => xScale(d.loc_start_str) )
.attr('y', d => yScale(d.total_matched) ) // set y
.attr('height', d => marketCataHeight - yScale(d.total_matched)) // find height by subtracting y value from height of the chart.
.attr('width', xScale.bandwidth());
example here: https://bl.ocks.org/d3noob/8952219
Try to always provide a Minimal, Complete, and Verifiable example. https://stackoverflow.com/help/mcve
I tried to do this by taking your code and adding dummy data etc. and then modifying it
The result is this (Demo here- https://codepen.io/Alexander9111/pen/gObEZym):
HTML:
<div class="marketCataChart"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
Javascript:
const marketCataHeight = 800;
const marketCataWidth = 2000;
const innerWidth = 1500;
const innerHeight = 500;
const margin = {
top: 30,
left: 30,
bottom: 30,
right: 30
};
const marketCataRender = marketCataData => {
const marketCataSVG = d3.select('.marketCataChart').append('svg')
marketCataSVG.attr('class', 'marketCataSVG')
.attr('height', marketCataHeight)
.attr('width', marketCataWidth);
// x y values
const xValue = d => d.loc_start_str;
const yValue = d => d.total_matched;
// x y scales
const xScale = d3.scaleBand();
xScale.domain(marketCataData.map(xValue))
.range([0, innerWidth]);
const yScale = d3.scaleLinear();
yScale.domain(d3.extent(marketCataData, yValue))
.range([innerHeight, 0])
.nice();
// x y axis
//const xAxis = d3.axisBottom(xScale)
const xAxis = d3.axisTop(xScale) //change to axisTop
const yAxis = d3.axisLeft(yScale)
// set chart group to make it easier to transform
const g = marketCataSVG.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// x y axis groups
const xAxisG = g.append('g')
.call(xAxis)
.attr('class', 'x-axis')
.attr('transform', `translate(0, ${0})`) // no longer need to translate by innerHeight as the x-axis is on the top
.selectAll('text')
.style('text-anchor', 'middle')
.attr('transform', `rotate(-0)`) //-90
.attr('x', -7)
const yAxisG = g.append('g')
.call(yAxis)
.attr('class', 'y-axis');
// Apply bar chart rectangle to chart
const marketCataRect = g.selectAll('rect');
marketCataRect.data(marketCataData)
.enter().append('rect')
.attr('x', d => xScale(xValue(d)))
.attr('height', d => yScale(yValue(d)))
.attr('width', xScale.bandwidth());
//Optional - add chart border:
g.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', innerWidth)
.attr('height', innerHeight)
.attr('stroke', 'black')
.attr('stroke-width', '1px')
.attr('fill', 'none')
.attr('class', 'chart-boarder');
};
const marketCataData = [
{loc_start_str: "example0", total_matched: 0},
{loc_start_str: "example1", total_matched: 100},
{loc_start_str: "example2", total_matched: 200},
{loc_start_str: "example3", total_matched: 300},
{loc_start_str: "example4", total_matched: 400},
]
marketCataRender(marketCataData);
Most important lines were: const xAxis = d3.axisTop(xScale) and const xAxisG.attr('transform', `translate(0, ${0})`)

Changing direction of D3 animation / transition?

I've recently built a small heatmap whose individual cells appear through a fade-in one after another after the page is loaded:
https://codepen.io/ChrisBean/pen/KKwpmjb
The fade-in animation is triggered by setting the initial opacity value of the cells to 0
squares.transition()
.duration(1000)
.delay((_d, i) => i * 200)
.style('opacity', 1);
As of now, the cells fade in from the bottom to the top column by column. I want them to fade in from left to right, row by row. This is the succession that I'm aiming at, quickly visualized with a pen drawing on a thinkpad:
Can anyone push me in the right direction of what to change in the transition trigger to change the direction?
There is no such a thing as the "direction of a transition" in D3. The whole issue here is that you're using the indices of the elements to set the delay. That being said, just change the order of the objects inside the data array, so the indices match the direction you want.
For instance:
data.sort(function(a,b){
return myVars.indexOf(b.variable) - myVars.indexOf(a.variable) ||
myGroups.indexOf(a.group) - myGroups.indexOf(b.group)
});
Here is the code with that change:
// set the dimensions and margins of the graph
const margin = {
top: 0,
right: 0,
bottom: 0,
left: 0,
};
const width = 400 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
// eslint-disable-next-line no-undef
const svg = d3.select('#my_dataviz')
.append('svg')
.attr('viewBox', '0 0 900 320')
.append('g')
.attr('transform',
`translate(${margin.left},${margin.top})`);
// Labels of row and columns
const myGroups = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
const myVars = ['v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7', 'v8', 'v9', 'v10'];
// 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 X scales and axis:
const y = d3.scaleBand()
.range([height, 0])
.domain(myVars)
.padding(0.00);
svg.append('g')
.call(d3.axisLeft(y));
// Build color scale
const myColor = d3.scaleLinear()
.range(['white', '#363636'])
.domain([1, 100]);
// 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)
});
// create a tooltip
const tooltip = d3.select('#my_dataviz')
.append('div')
.style('opacity', 0)
.attr('class', 'tooltip')
.style('background-color', 'white')
.style('border', 'solid')
.style('border-width', '2px')
.style('border-radius', '5px')
.style('padding', '5px');
// Three function that change the tooltip when user hover / move / leave a cell
const mouseover = function() {
tooltip.style('opacity', 1);
};
const mousemove = function(d) {
tooltip
.html(`Client Branch:${d.value} <br>
Project: <br>`)
.style('left', `${d3.mouse(this)[0] + 70}px`)
.style('top', `${d3.mouse(this)[1]}px`);
};
const mouseleave = function() {
tooltip.style('opacity', 0);
};
// add the squares
const squares = svg.selectAll()
.data(data, (d) => `${d.group}:${d.variable}`)
.enter()
.append('rect')
.attr('x', (d) => x(d.group))
.attr('y', (d) => y(d.variable))
.attr('width', x.bandwidth())
.attr('height', y.bandwidth())
.style('fill', (d) => myColor(d.value))
.style('opacity', 0)
.on('mouseover', mouseover)
.on('mousemove', mousemove)
.on('mouseleave', mouseleave);
squares.transition()
.duration(1000)
.delay((_d, i) => i * 200)
.style('opacity', 1);
});
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>

Categories