Offset x-axis ticks with scaleTime - javascript

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

Related

D3JS V6: Multi Line Group

I'm stuck trying to make a multi line graph with D3JS V6. The problem is about group the data (long format) and trying to plot the path for every group.
My data follows this structure:
Sexo
Año
D
Male
2000
25
Male
2001
58
Female
2000
55
Female
2001
75
Total
2000
80
Total
2001
133
function dataviz2() {
var dataset;
h = 400
w = 650
padding = 40
count = 0
// Convertir Strings a Fechas
var parseTime = d3.timeParse('%Y')
// Convertir Fechas a Strings
var parseDate = d3.timeFormat('%Y')
var rowConverter = function (d) {
return {
Sexo: d.Sexo,
Año: parseTime(d.Año),
N: parseInt(d.N),
}
}
d3.csv('data.csv', rowConverter).then(function (data) {
dataset = data
dataset.sort(function (x, y) {
return d3.ascending(x.Año, y.Año)
})
dataBySex = d3.group(data, d => d.Sexo)
dataBySexGr = Array.from(dataBySex, ([sexo, value]) => ({ sexo, value }))
console.log(dataBySex)
console.log(dataBySexGr)
chart()
})
function chart() {
// Scale X
xScale = d3.scaleTime()
.domain([
d3.min(dataset, function (d) { return d.Año }),
d3.max(dataset, function (d) { return d.Año })
])
.range([padding, w - padding])
// Scale Y
yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function (d) { return d.N })])
.range([h - padding, padding])
// Create Axis
var xAxis = d3.axisBottom(xScale)
var yAxis = d3.axisLeft(yScale)
var svg = d3.select('#dataviz1')
.append('svg')
.attr('width', w)
.attr('height', h)
let id = 0;
const ids = function () {
return "line-" + id++
}
var lines = svg.selectAll('.line')
.data(dataBySexGr)
.join('path')
.attr("class", ids)
**.attr('d', d => {
return d3.line()
.x(d => x(d.Año))
.y(d => y(d.N))
})**
.attr('stroke', 'blue')
.attr('stroke-width', 3)
.attr('fill', 'none')
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + (h - padding) + ')')
.attr('visibility', 'visible')
.call(xAxis)
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(' + padding + ',0)')
.attr('visibility', 'visible')
.call(yAxis)
}
}
What I know after a lot of tries is that the .attr('d', d => ...) is the problem. The argument of that function says "'d' is declared, but its value is never read". When I check the HTML, I receive the different 3 paths I'm looking for but there are no values, are a function.
I'm really stuck in here, so I appreciate any help.
Thanks!!
d3.line returns a line generator function, you can see that function as the d attribute. You need to pass the datum and execute that function to get the path data:
.attr('d', d => {
return d3.line()
.x(d => x(d.Año))
.y(d => y(d.N))(d)
})
Or alternatively, you can define the line generator before hand and simply use .attr('d',line):
const line = d3.line()
.x(d => x(d.Año))
.y(d => y(d.N))
selection.attr("d",line);

D3 - Chart with positive and negative values

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.

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

d3 path.line stroke-width with IF statement / ternary operator

Trying to change line stroke-width with an IF statement / ternary operator e.g. if d.country === "China" stroke-width: 2. This has to be a path.line attribute so this is what I'm attaching it to after line is called.
I've added countryName to the emissions object, I have also noticed that condition is always FALSE so the stroke-width is 0.5. Why its not TRUE?
Codepen
//Define line chart with and height
const width = fullWidth - margin.left - margin.right;
const height = fullHeight - margin.top - margin.bottom;
//Define x and y scale range
let xScale = d3.scaleLinear()
.range([0, width])
let yScale = d3.scaleLinear()
.range([0, height])
//Define x and y axis
let xAxis = d3.axisBottom(xScale)
.ticks(15)
let yAxis = d3.axisLeft(yScale)
.ticks(10)
//Draw svg
let svg = d3.select("body")
.append("svg")
.attr("width", fullWidth)
.attr("height", fullHeight)
.append("g")
.attr("transform", "translate(" + 53 + "," + 0 +")");
d3.json("https://api.myjson.com/bins/izmg6").then(data => {
console.log(data);
//Structure data so should be an array of arrays etc [[x,y], [x,y], [x,y]]
let years = d3.keys(data[0]).slice(0, 50);
console.log(years);
let dataset = [];
data.forEach((d, i) => {
let myEmissions = [];
years.forEach(y => {
if (d[y]) {
myEmissions.push({
country: d.countryName,
year: y,
amount: d[y]
})
}
})
dataset.push({
country: d.countryName,
emissions: myEmissions
});
})
console.log(dataset);
//Define x and y domain
xScale
.domain(d3.extent(years, d =>d))
yScale
.domain([d3.max(dataset, d =>
d3.max(d.emissions, d =>
+d.amount)), 0])
//Generate line
let line = d3.line()
.curve(d3.curveBasis)
.x(d =>
xScale(d.year))
.y(d =>
yScale(d.amount));
let groups = svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
groups.append("title")
.text(d => d.country)
groups.selectAll("path")
.data(d => [d.emissions])
.enter()
.append("path").classed("line", true)
.attr("d", line)
.style("stroke-width", d =>
d.country === "China" ? 10 : 0.5
)
}).catch(error => console.log(error))
What rioV8 meant is that you already have your group selection, so you just need to use groups to append new elements.
groups is a selection of all your g, it's where you want to append your paths. The same way you're not selecting again to add the titles.
groups
.append("path").classed("line", true)
.attr("d", d=> line(d.emissions))
.style("stroke-width", d =>
d.country === "China" ? 5 : 0.5
)

D3 combination of bar and area chart

I am wondering is it possible to achieve the combination of area and bar chart in the way shown in the screenshot below?
Along with making the area in between clickable for some other action.
It would be really helpful if you can guide me to some of the examples to get an idea how to achieve the same.
I posted a codepen here. That creates a bar chart, and then separate area charts between each bar chart.
const BarChart = () => {
// set data
const data = [
{
value: 48,
label: 'One Rect'
},
{
value: 32,
label: 'Two Rect'
},
{
value: 40,
label: 'Three Rect'
}
];
// set selector of container div
const selector = '#bar-chart';
// set margin
const margin = {top: 60, right: 0, bottom: 90, left: 30};
// width and height of chart
let width;
let height;
// skeleton of the chart
let svg;
// scales
let xScale;
let yScale;
// axes
let xAxis;
let yAxis;
// bars
let rect;
// area
let areas = [];
function init() {
// get size of container
width = parseInt(d3.select(selector).style('width')) - margin.left - margin.right;
height = parseInt(d3.select(selector).style('height')) - margin.top - margin.bottom;
// create the skeleton of the chart
svg = d3.select(selector)
.append('svg')
.attr('width', '100%')
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
xScale = d3.scaleBand().padding(0.15);
xAxis = d3.axisBottom(xScale);
yScale = d3.scaleLinear();
yAxis = d3.axisLeft(yScale);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0, ${height})`);
svg.append('g')
.attr('class', 'y axis');
svg.append('g')
.attr('class', 'x label')
.attr('transform', `translate(10, 20)`)
.append('text')
.text('Value');
xScale
.domain(data.map(d => d.label))
.range([0, width])
.padding(0.3);
yScale
.domain([0, 75])
.range([height, 0]);
xAxis
.scale(xScale);
yAxis
.scale(yScale);
rect = svg.selectAll('rect')
.data(data);
rect
.enter()
.append('rect')
.style('fill', d => '#00BCD4')
.attr('y', d => yScale(d.value))
.attr('height', d => height - yScale(d.value))
.attr('x', d => xScale(d.label))
.attr('width', xScale.bandwidth());
// call the axes
svg.select('.x.axis')
.call(xAxis);
svg.select('.y.axis')
.call(yAxis);
// rotate axis text
svg.select('.x.axis')
.selectAll('text')
.attr('transform', 'rotate(45)')
.style('text-anchor', 'start');
if (parseInt(width) >= 600) {
// level axis text
svg.select('.x.axis')
.selectAll('text')
.attr('transform', 'rotate(0)')
.style('text-anchor', 'middle');
}
data.forEach(
(d, i) => {
if (data[i + 1]) {
areas.push([
{
x: d.label,
y: d.value
},
{
x: data[i + 1].label,
y: data[i + 1].value
}
]);
}
}
);
areas = areas.filter(
d => Object.keys(d).length !== 0
);
areas.forEach(
a => {
const area = d3.area()
.x((d, i) => {
return i === 0 ?
xScale(d.x) + xScale.bandwidth() :
xScale(d.x);
})
.y0(height)
.y1(d => yScale(d.y));
svg.append('path')
.datum(a)
.attr('class', 'area')
.style('fill', d => '#B2EBF2')
.attr('d', area)
.on('click', d => {
console.log('hello click!');
});
}
)
}
return { init };
};
const myChart = BarChart();
myChart.init();
#bar-chart {
height: 500px;
width: 100%;
}
<script src="https://unpkg.com/d3#5.2.0/dist/d3.min.js"></script>
<div id="bar-chart"></div>
After creating the bar chart, I repackage the data to make it conducive to creating an area chart. I created an areas array where each item is going to be a separate area chart. I'm basically taking the values for the first bar and the next bar, and packaging them together.
data.forEach(
(d, i) => {
if (data[i + 1]) {
areas.push([
{
x: d.label,
y: d.value
},
{
x: data[i + 1].label,
y: data[i + 1].value
}
]);
}
}
);
areas = areas.filter(
d => Object.keys(d).length !== 0
);
I then iterate through each element on areas and create the area charts.
The only tricky thing here, I think, is getting the area chart to span from the end of the first bar to the start of the second bar, as opposed to from the end of the first bar to the end of the second bar. To accomplish this, I added a rectangle width from my x-scale to the expected x value of the area chart when the first data point is being dealt with, but not the second.
I thought of this as making two points on a line: one for the first bar and one for the next bar. D3's area function can shade all the area under a line. So, the first point on my line should be the top-right corner of the first bar. The second point should be the top-left corner of the next bar.
Attaching a click event at the end is pretty straightforward.
areas.forEach(
a => {
const area = d3.area()
.x((d, i) => {
return i === 0 ?
xScale(d.x) + xScale.bandwidth() :
xScale(d.x);
})
.y0(height)
.y1(d => yScale(d.y));
svg.append('path')
.datum(a)
.attr('class', 'area')
.style('fill', d => '#B2EBF2')
.attr('d', area)
.on('click', d => {
console.log('hello click!');
});
}
)
In the example below, I have combined a simple bar chart (like in this famous bl.lock) with some polygons in between. I guess it could also be achieved with a path.
const data = [
{ letter: "a", value: 9 },
{ letter: "b", value: 6 },
{ letter: "c", value: 3 },
{ letter: "d", value: 8 }
];
const svg = d3.select("#chart");
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = +svg.attr("width") - margin.left - margin.right;
const height = +svg.attr("height") - margin.top - margin.bottom;
const xScale = d3.scaleBand()
.rangeRound([0, width]).padding(0.5)
.domain(data.map(d => d.letter));
const yScale = d3.scaleLinear()
.rangeRound([height, 0])
.domain([0, 10]);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(yScale));
g.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", d => xScale(d.letter))
.attr("y", d => yScale(d.value))
.attr("width", xScale.bandwidth())
.attr("height", d => height - yScale(d.value));
// Add polygons
g.selectAll(".area")
.data(data)
.enter().append("polygon")
.attr("class", "area")
.attr("points", (d,i,nodes) => {
if (i < nodes.length - 1) {
const dNext = d3.select(nodes[i + 1]).datum();
const x1 = xScale(d.letter) + xScale.bandwidth();
const y1 = height;
const x2 = x1;
const y2 = yScale(d.value);
const x3 = xScale(dNext.letter);
const y3 = yScale(dNext.value);
const x4 = x3;
const y4 = height;
return `${x1},${y1} ${x2},${y2} ${x3},${y3} ${x4},${y4} ${x1},${y1}`;
}
})
.on("click", (d,i,nodes) => {
const dNext = d3.select(nodes[i + 1]).datum();
const pc = Math.round((dNext.value - d.value) / d.value * 100.0);
alert(`${d.letter} to ${dNext.letter}: ${pc > 0 ? '+' : ''}${pc} %`);
});
.bar {
fill: steelblue;
}
.area {
fill: lightblue;
}
.area:hover {
fill: sandybrown;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="400" height="300" id="chart"></svg>

Categories