How to make legend text responsive for D3 chart - d3.js

I am trying to fix an issue in which there is a D3 donut chart with a legend located just to the right. The text of the legend keeps being cutoff. It's either visible outside of the container, or it's not displayed outside. Either way, it doesn't fit within the container, even though I can see that both the legend and the donut chart are part of the same SVG. You can see what I'm referring to in this image:
https://imgur.com/a/J3KrTA6
I am very new to working with D3, but I've been stuck on this issue for a while now. This isn't my code that I'm trying to fix, but here is where the options for generating the SVG are being passed in:
const donutOptions: DonutChartOptions = {
showPercentageInDonut: false,
width: 500,
height: 260,
title: {text: '', textStyle: {fontSize: '14px'}},
margin: {top: 120, right: 10, bottom: 65, left: 100},
colors: [Theme.Emerald.hex, Theme.Lime.hex, Theme.Teal.hex, Theme.SkyBlue.hex,
Theme.Marigold.hex, Theme.Azure.hex, Theme.Red.hex, Theme.Orange.hex]
};
const legendTextWrapWidthEdge = 1440;
const donutEthnicityOptions: DonutChartOptionsExtended = {
showPercentageInDonut: false,
width: 470,
height: 260,
title: {text: '', textStyle: {fontSize: '14px'}},
margin: {top: 120, right: 10, bottom: 65, left: 85},
colors: [Theme.Emerald.hex, Theme.Lime.hex, Theme.Teal.hex, Theme.SkyBlue.hex,
Theme.Marigold.hex, Theme.Azure.hex, Theme.Red.hex, Theme.Orange.hex],
legend: {textStyle: {fontSize: '14px'}},
legendOptions: {
legendRectVSpace: 10,
legendPositionX: 110,
legendPositionY: 85,
legendPercentagePositionX: 46,
legendPercentagePositionY: 15,
legendTextPositionX: 20,
legendTextWidth: (!!this.browserScreenWidth && this.browserScreenWidth < legendTextWrapWidthEdge) ? 100 : 200
}
};
I have tried experimenting with viewBox and preserveAspectRatio attributes, but I am apparently not doing something correctly.
This is the code that actually creates the chart using the aforementioned options. Messing with this code is kind of a last resort option if it can be avoided though. I think of it as a black box that I am providing merely for context:
import { DataSet } from '../data-set';
import { DonutChartOptionsExtended, ILegendOptions } from '../interfaces/DonutChartOptionsExtended';
import { XYChartSettings } from '../interfaces/XYChartSettings';
import { DEFAULTS } from '../interfaces/DEFAULTS';
import { TextStyle } from '../interfaces/TextStyle';
import Defaults from 'lodash-es/defaults';
import { getColorScale, initializeSvg, wrapText } from '../d3-fns';
export class DonutChart {
options: any;
dataset: any;
draw(dataSet?: DataSet, options?: DonutChartOptionsExtended) {
Promise.all([
import(/* webpackChunkName: "d3" */ 'd3-shape'),
import(/* webpackChunkName: "d3" */ 'd3-interpolate'),
import(/* webpackChunkName: "d3" */ 'd3-selection'),
import(/* webpackChunkName: "d3" */ 'd3-scale'),
import(/* webpackChunkName: "d3" */ 'd3-transition')
]).then(([d3Shape, d3Interpolate, d3Select, d3Scale, trans]) => {
if (dataSet) {
this.dataset = dataSet;
}
if (options) {
this.options = options;
}
const pie = d3Shape.pie()
.value((d: any) => d.value)
.sort(null)
.padAngle(.03);
const width = this.options.width;
const outerRadius = (width - 300) / 2;
const innerRadius = outerRadius / 2;
const arc = d3Shape.arc()
.outerRadius(outerRadius)
.innerRadius(innerRadius);
const settings: any = new XYChartSettings(this.options);
const svg = initializeSvg(d3Select, settings);
const color = getColorScale(d3Scale, settings.colors);
const dataRows = this.dataset.dataRows;
const path = svg.selectAll('path')
.data(pie(dataRows as any))
.enter()
.append('path')
.attr('d', (d: any, i: number, groups: any[]) => arc(d))
.attr('fill', (d: any, i: number, groups: any[]) => String(color(String(d.data.label))));
if (options && options.showPieAnimation) {
path.transition()
.duration(1000)
.attrTween('d', function (d: any) {
const interpolate = d3Interpolate.interpolate({startAngle: 0, endAngle: 0}, d);
return function (t: any) {
return arc(interpolate(t));
};
});
}
const restOfTheData = (mydata: any) => {
try {
const legendOptions: ILegendOptions = this.options.legendOptions;
const legendRectSize = !!legendOptions && legendOptions.legendRectHeight ? legendOptions.legendRectHeight : 20;
const legendSpacing = !!legendOptions && legendOptions.legendRectVSpace ? legendOptions.legendRectVSpace : 7;
const legendHeight = legendRectSize + legendSpacing;
const positionx = !!legendOptions && legendOptions.legendPositionX ? legendOptions.legendPositionX : 115;
const positiony = !!legendOptions && legendOptions.legendPositionY ? legendOptions.legendPositionY : 65;
if (options && options.showPercentageInDonut) {
this.displayPercentageOnThePie(mydata, svg, pie, arc);
}
const defaultColor = getColorScale(d3Scale, settings.colors);
if (this.options.colors) {
this.displayPercentageNextToLegend(
mydata, svg, defaultColor, positionx,
positiony, legendHeight,
settings.legend.textStyle.fontSize || '14px'
);
this.displayLengend(
d3Select, mydata, svg, defaultColor, legendHeight,
positionx, positiony, legendRectSize,
settings.legend.textStyle.fontSize || '14px'
);
} else {
this.displayPercentageNextToLegendDefault(
mydata, svg, positionx, positiony, legendHeight,
settings.legend.textStyle.fontSize || '14px'
);
this.displayLengendDefault(
svg, defaultColor, legendHeight,
positionx, positiony, legendRectSize,
settings.legend.textStyle.fontSize || '14px'
);
}
this.displayTitle(svg, settings);
} catch (ex) {
console.log(ex);
}
};
setTimeout(restOfTheData(dataRows), 1000);
})
}
private displayPercentageOnThePie(mydata: any, svg: any, pie: any, arc: any) {
svg.selectAll('text')
.data(pie(mydata))
.enter()
.append('text')
.transition()
.duration(200)
.attr('transform', (d: any, i: number, groups: any[]) => 'translate(' + arc.centroid(d) + ')')
.attr('dy', '.4em')
.attr('text-anchor', 'middle')
.text((d: any) => d.data.value + '%')
.style('fill', '#fff')
.style('font-size', '10px');
}
private displayPercentageNextToLegend(
mydata: any, svg: any, defaultColor: any, positionX: any,
positionY: any, legendHeight: any, fontSize: any) {
svg.selectAll('.percentage')
.data(mydata)
.enter().append('g')
.attr('class', 'percentage')
.attr('transform', (d: any, i: number, groups: any[]) => 'translate(' + (positionX + 40) +
',' + ((i * legendHeight) - positionY) + ')')
.append('text')
.style('fill', (d: any, i: number, groups: any[]) => defaultColor(i))
.style('text-anchor', 'end')
.style('font-size', fontSize)
.text((d: any) => d.value + '%');
}
private displayPercentageNextToLegendDefault(mydata: any, svg: any, positionX: any, positionY: any, legendHeight: any, fontSize: any) {
svg.selectAll('.percentage')
.data(mydata)
.enter().append('g')
.attr('class', 'percentage')
.attr('transform', (d: any, i: number, groups: any[]) => 'translate(' + (positionX + 40) +
',' + ((i * legendHeight) - positionY) + ')')
.append('text')
.style('fill', '#000')
.style('text-anchor', 'end')
.style('font-size', fontSize)
.text((d: any) => d.value + '%');
}
private displayLengend(d3Select: any, mydata: any, svg: any, defaultColor: any, legendHeight: any,
positionX: any, positionY: any, legendRectSize: any, fontSize: any) {
const legendOptions: ILegendOptions = this.options.legendOptions;
const legendRectWidth = !!legendOptions && legendOptions.legendRectWidth ? legendOptions.legendRectWidth : 10;
const percentageOffsetX = !!legendOptions && legendOptions.legendPercentagePositionX ? legendOptions.legendPercentagePositionX : 56;
const percentageOffsetY = !!legendOptions && legendOptions.legendPercentagePositionY ? legendOptions.legendPercentagePositionY : 15;
const textOffsetX = !!legendOptions && legendOptions.legendTextPositionX ? legendOptions.legendTextPositionX : 30;
const textOffsetY = !!legendOptions && legendOptions.legendTextPositionY ? legendOptions.legendTextPositionY : 15;
const textWidth = !!legendOptions && legendOptions.legendTextWidth ? legendOptions.legendTextWidth : 200;
const legend = svg.selectAll('.legend')
.data(mydata)
.enter()
.append('g')
.attr('class', 'legend')
// Just a calculation for x & y position
.attr('transform',
(d: any, i: number, groups: any[]) => `translate(${positionX
+ percentageOffsetX},${(i * legendHeight) - (positionY + percentageOffsetY)})`);
legend.append('rect')
.attr('width', legendRectWidth)
.attr('height', legendRectSize)
.attr('rx', 1)
.attr('ry', 1)
.style('fill', (d: any, i: number, groups: any[]) => defaultColor(i))
.style('stroke', (d: any, i: number, groups: any[]) => defaultColor(i));
legend.append('text')
.attr('x', textOffsetX)
.attr('y', textOffsetY)
.text((d: any) => d.label)
.style('fill', '#000')
.style('font-size', fontSize)
.call(wrapText, d3Select, textWidth);
}
private displayLengendDefault(svg: any, defaultColor: any, legendHeight: any,
positionX: any, positionY: any, legendRectSize: any, fontSize: any) {
const legendRectWidth = 10;
const legend = svg.selectAll('.legend')
.data(defaultColor.domain())
.enter()
.append('g')
.attr('class', 'legend')
// Just a calculation for x & y position
.attr('transform', (d: any, i: number, groups: any[]) => 'translate(' + (positionX + 50) +
',' + ((i * legendHeight) - (positionY + 15)) + ')');
legend.append('rect')
.attr('width', legendRectWidth)
.attr('height', legendRectSize)
.attr('rx', 1)
.attr('ry', 1)
.style('fill', defaultColor)
.style('stroke', defaultColor);
legend.append('text')
.attr('x', 30)
.attr('y', 15)
.text((d: any) => d)
.style('fill', '#929DAF')
.style('font-size', fontSize);
}
private displayTitle(svg: any, settings: any) {
const textStyle = <TextStyle>Defaults(settings.title.textStyle || {}, DEFAULTS.textStyleTitle);
svg.append('text')
.attr('x', settings.widthInner / 2)
.attr('y', 0 - (settings.margin.top / 1.15 ))
.attr('text-anchor', 'middle')
.style('font-size', textStyle.fontSize)
.style('text-decoration', textStyle.textDecoration)
.text(settings.title.text);
}
}

Related

Animate area of a d3 line graph

I am trying a transition of a d3 area.
Here's the example link which I tried to follow. It is doing the area transition from outside the data line path.
I need this transition to be from inside the data line area, and as the data line progresses the area transition should progress.
Here's the StackBlitz link.
And here's the code:
maxAllowed = 0;
graphData = [
{
hrCount: 4,
adjCount: 2
},
{
hrCount: 8,
adjCount: 5
},
{
hrCount: 12,
adjCount: 10
},
{
hrCount: 16,
adjCount: 13
},
{
hrCount: 20,
adjCount: 19
},
{
hrCount: 24,
adjCount: 25
},
{
hrCount: 28,
adjCount: 33
},
{
hrCount: 32,
adjCount: 37
},
{
hrCount: 36,
adjCount: 40
},
{
hrCount: 40,
adjCount: 42
},
{
hrCount: 44,
adjCount: 44
},
{
hrCount: 48,
adjCount: 47
},
{
hrCount: 52,
adjCount: 48
},
{
hrCount: 56,
adjCount: 50
},
{
hrCount: 60,
adjCount: 53
}
];
margin: {
top: number;
right: number;
bottom: number;
left: number;
};
width: number;
height: number;
g: any;
xScale: any;
yScale: any;
xAxis: any;
yAxis: any;
ngOnInit() {
this.maxAllowed = d3.max(this.graphData, (d: any) => d.hrCount) + 5;
this.drawLineGraph();
}
drawLineGraph() {
this.margin = {
top: 5,
right: 10,
bottom: 20,
left: 25
};
this.width =
document.getElementById("svgcontainer").parentElement.offsetWidth -
(this.margin.left + this.margin.right);
this.height =
document.getElementById("svgcontainer").parentElement.offsetHeight -
(this.margin.top + this.margin.bottom) +
80;
// Remove any existing SVG
d3.select("#svgcontainer")
.selectAll("svg > *")
.remove();
this.createGroup();
this.createScale();
this.createYAxisGridLine();
this.createShadowEffect();
this.createAxis();
this.createDataPathAndDots();
// Removing y-axis 0 tick-line
d3.selectAll(".y-axis-tick .tick line").each(function(d, i) {
if (i === 0) {
this.remove();
}
});
}
createGroup(): void {
this.g = d3
.select("#svgcontainer")
.append("svg")
.attr("width", this.width + this.margin.left + this.margin.right)
.attr("height", this.height + this.margin.top + this.margin.bottom)
.style("background-color", "#0e1a30")
.append("g")
.attr(
"transform",
"translate(" + this.margin.left + ", " + this.margin.top + ")"
);
}
createScale(): void {
// x-scale
this.xScale = d3
.scaleBand()
.domain(this.graphData.map((d: any) => d.hrCount))
.range([0, this.width]);
// y-scale
this.yScale = d3
.scaleLinear()
.domain([0, this.maxAllowed])
.range([this.height, 0]);
}
createYAxisGridLine(): void {
this.g
.append("g")
.attr("class", "y-axis-grid")
.call(
d3
.axisLeft(this.yScale)
.tickSize(-this.width)
.tickFormat("")
.ticks(5)
);
}
createShadowEffect(): void {
const colorArray = [
["rgb(8, 141, 218)", "0.8"],
["rgb(8, 141, 218)", "0.5"],
["rgb(8, 141, 218)", "0"]
];
const defs = this.g.append("defs");
const grad = defs
.append("linearGradient")
.attr("id", "grad")
.attr("x1", "0%")
.attr("x2", "0%")
.attr("y1", "0%")
.attr("y2", "100%")
.attr("gradientTransform", "rotate(-15)");
grad
.selectAll("stop")
.data(colorArray)
.enter()
.append("stop")
.style("stop-color", (d: any) => {
return d[0];
})
.style("stop-opacity", (d: any) => {
return d[1];
})
.attr("offset", (d: any, i: any) => {
return 100 * (i / 2) + "%";
});
const area = (datum, boolean) => {
return d3
.area()
.y0(this.height)
.y1((d: any) => this.yScale(d.adjCount))
.x((d: any) =>
boolean ? this.xScale(d.hrCount) + this.xScale.bandwidth() / 2 : 0
)(datum);
};
this.g
.append("path")
.attr("d", area(this.graphData, false))
.attr("fill", "url(#grad)")
.transition()
.duration(5000)
.attr("d", area(this.graphData, true));
}
createAxis(): void {
// x-axis
this.xAxis = d3.axisBottom(this.xScale).tickSizeOuter(0);
this.g
.append("g")
.attr("transform", "translate(0, " + this.height + ")")
.attr("class", "graph-axis")
.call(this.xAxis.scale(this.xScale))
.append("text")
.attr("x", this.width)
.attr("y", -6)
.attr("text-anchor", "end")
.attr("font", "10px sans-serif")
.attr("letter-spacing", "1px")
.attr("fill", "#8997b1")
.text("Hours");
// y-axis
this.yAxis = d3
.axisLeft(this.yScale)
.ticks(5)
.tickSizeOuter(0);
this.g
.append("g")
.attr("class", "graph-axis y-axis-tick")
.call(this.yAxis.scale(this.yScale))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.attr("font", "10px sans-serif")
.attr("letter-spacing", "1px")
.attr("fill", "#8997b1")
.text("Adjusters");
}
createDataPathAndDots(): void {
const line = d3
.line()
.x((d: any) => this.xScale(d.hrCount) + this.xScale.bandwidth() / 2)
.y((d: any) => this.yScale(d.adjCount));
const path = this.g
.append("path")
.attr("fill", "none")
.attr("stroke", "#088dda")
.attr("stroke-width", "2px")
.attr("d", line(this.graphData));
this.createPathTransition(path);
// Data dots
this.g
.selectAll("line-circle")
.data(this.graphData)
.enter()
.append("circle")
.attr("r", 4)
.attr("fill", (d: any) => {
if (d.hrCount === 0) {
return "none";
} else {
return "#088dda";
}
})
.attr(
"cx",
(d: any) => this.xScale(d.hrCount) + this.xScale.bandwidth() / 2
)
.attr("cy", (d: any) => this.yScale(d.adjCount));
}
createPathTransition(path: any): void {
const totLength = path.node().getTotalLength();
path
.attr("stroke-dasharray", totLength + " " + totLength)
.attr("stroke-dashoffset", totLength);
path
.transition()
.duration(5000)
.attr("stroke-dashoffset", 0);
}
Here's one potential solution. First, I modified your area generator like so:
const area = d3.area()
.y0(this.height)
.y1((d: any) => d.x)
.x((d: any) => d.y );
Then I set up the area transition as:
this.g
.append("path")
.attr("fill", "url(#grad)")
.transition()
.duration(5000)
.attrTween("d", function(d){
var p = d3.select(".line").node(), // find the line graph
l = p.getTotalLength(), // get length
i = d3.interpolate(0,l), // interpolator over length
dAtT = []; // array to hold accumulated datapoints
return function(t) {
dAtT.push(p.getPointAtLength(i(t))) // on every iteration get a point on the line
return area(dAtT); // draw area
};
});
Updated stackblitz.

Multi-line graph does not matches the required result in d3js

I am new in d3.js.I made the multiline plot
Using this code file
import {
select,
csv,
scaleLinear,
scaleTime,
scaleOrdinal,
extent,
axisLeft,
scaleBand ,
axisBottom,
line,
curveBasis,
nest,
schemeCategory10,
timeFormat,
descending
} from 'd3';
import { colorLegend } from './colorLegend';
const svg = select('svg');
const width = +svg.attr('width');
const height = +svg.attr('height');
const render = data => {
const title='Profit Comparision by Segment by Region'
// Region,Sub_Category,Profit
const xValue = d => d.Sub_Category;
const xAxisLabel="Sub-Category"
const yValue = d => d.Profit;
const circleRadius = 6;
const yAxisLabel="Profit"
var barPadding = 0.2;
const colorValue = d => d.Region;
const margin = { top: 60, right: 160, bottom: 128, left: 105 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
/*const xScale = scaleLinear()
.domain(extent(data, xValue))
.range([0, innerWidth])
.nice();*/
var xScale = d3.scalePoint().domain(data.map(xValue))
.range([0, innerWidth]);
/* var xScale = scaleOrdinal().domain(extent(data, xValue))
.range([0, innerWidth]);*/
const yScale = scaleLinear()
.domain(extent(data, yValue))
.range([innerHeight, 0])
.nice();
const colorScale = scaleOrdinal(schemeCategory10);
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const xAxis = axisBottom(xScale)
.tickSize(-innerHeight)
.tickPadding(15);
const yAxis = axisLeft(yScale)
.tickSize(-innerWidth)
.tickPadding(10);
const yAxisG = g.append('g').call(yAxis);
yAxisG.selectAll('.domain').remove();
yAxisG.append('text')
.attr('class', 'axis-label')
.attr('y', -60)
.attr('x', -innerHeight / 2)
.attr('fill', 'black')
.attr('transform', `rotate(-90)`)
.attr('text-anchor', 'middle')
.text(yAxisLabel);
const xAxisG = g.append('g').call(xAxis)
.attr('transform', `translate(0,${innerHeight})`);
xAxisG
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", function(d) {
return "rotate(-65)"
});
xAxisG.select('.domain').remove();
xAxisG.append('text')
.attr('class', 'axis-label')
.attr('y', 110)
.attr('x', innerWidth / 2)
.attr('fill', 'black')
.text(xAxisLabel);
const lineGenerator = line()
.x(d => xScale(xValue(d)))
.y(d => yScale(yValue(d)))
.curve(curveBasis);
const lastYValue = d =>
yValue(d.values[d.values.length - 1]);
const nested = nest()
.key(colorValue)
.entries(data)
.sort((a, b) =>
descending(lastYValue(a), lastYValue(b))
);
console.log(nested);
colorScale.domain(nested.map(d => d.key));
g.selectAll('.line-path').data(nested)
.enter().append('path')
.attr('class', 'line-path')
.attr('d', d => lineGenerator(d.values))
.attr('stroke', d => colorScale(d.key));
g.append('text')
.attr('class', 'title')
.attr('y', -10)
.text(title);
svg.append('g')
.attr('transform', `translate(820,121)`)
.call(colorLegend, {
colorScale,
circleRadius: 10,
spacing: 38,
textOffset: 20
});
};
csv('data4.csv')
.then(data => {
data.forEach(d => {
// Region,Sub_Category,Profit
d.Profit = +d.Profit;
});
render(data);
});
This multiline plot does not match with the plot actual result on the same data. Here this picture in the actual result on the same data
How I can make this my multiline plot just like the result. What changes in the code is needed. Here is the link to vizhub code
Update:
I filtered and sorted them in alphabetical order and profit sorted in a descending order. But I got a different graph not the required result
How I can get the required result?

How to make axis label of stacked bar chart in the middle of ticks chart in D3?

Begin, the x-axis labels are in the middle of ticks. However, the x-axis labels show some problem after I rotate the x-axis labels.
Now, I want the x-axis labels to be in the middle of ticks. How can I do?
If transform is used, how to get the middle point of x-axis?
The result looks like https://drive.google.com/open?id=1Fen0to5Ih86alOXu6UXeJzeicX1E1JFJ
const data = [
{
'group': 'G1',
'sample': 's1',
'Actinomyces': 12.55802794990189,
'Alloprevotella': 0.3671446023182472,
'Atopobium': 0.15760660109181326,
'Anaerococcus': 0
},
{
'group': 'G1',
'sample': 's2',
'Actinomyces': 9.55802794990189,
'Alloprevotella': 0.3671446023182472,
'Atopobium': 0.12760660109181326,
'Anaerococcus': 10.0
},
{
'group': 'G2',
'sample': 's3',
'Actinomyces': 11.55802794990189,
'Alloprevotella': 0.3671446023182472,
'Atopobium': 0.9760660109181326,
'Anaerococcus': 5.0
},
{
'group': 'G2',
'sample': 's4',
'Actinomyces': 19.55802794990189,
'Alloprevotella': 1.3671446023182472,
'Atopobium': 2.15760660109181326,
'Anaerococcus': 4.0
}
]
const w = 800
const h = 400
const margin = { top: 50, right: 50, bottom: 50, left: 150 }
const keys = Object.keys(data[0]).filter(function (val) {
return val !== 'sample' && val !== 'group'
})
// create a stack generator
let stack = d3.stack()
.keys(keys)
const xScale = d3.scaleBand()
.domain(d3.range(data.length))
.range([margin.left, w - margin.right])
.paddingOuter(0.02)
const yScale = d3.scaleLinear()
.domain([0,
d3.max(data, function (d) {
return d3.sum(keys.map(val => d[val]))
})
])
.range([h - margin.bottom, margin.top])
const colorScale = d3.scaleLinear()
.domain([0, keys.length - 1])
.range([0, 1])
// create svg
const svg = d3.select('#app')
.append('svg')
.attr('width', w)
.attr('height', h)
const groups = svg.selectAll('g')
.data(stack(data))
.enter()
.append('g')
.style('fill', function (d, i) {
return d3.interpolateSpectral(colorScale(i))
})
groups.selectAll('rect')
.data(function (d) {
return d
})
.enter()
.append('rect')
.attr('x', (d, i) => xScale(i))
.attr('y', d => yScale(d[1]))
.attr('height', d => yScale(d[0]) - yScale(d[1]))
.attr('width', xScale.bandwidth())
// add axis
const xAxis = d3.axisBottom(xScale)
.tickFormat(d => keys[d])
svg.append('g')
.attr('class', 'xAxis')
.attr('transform', 'translate(0, ' + yScale(0) + ')')
.call(xAxis)
.selectAll('text')
.attr('text-anchor', 'start')
.attr('dx', '10px')
.attr('transform', 'rotate(90)')
const yAxis = d3.axisLeft(yScale)
svg.append('g')
.attr('class', 'yAxis')
.attr('transform', 'translate(' + xScale(0) + ', 0)')
.call(yAxis)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="app">
</div>
My English is poor, so I build a transform process picture and it can be find here https://drive.google.com/open?id=19zmGFwivdjPqVabVGIgdNVXKRGdR8v2s
The part of code has been changed locate between
/** change **/
...
/***********/
const data = [
{
'group': 'G1',
'sample': 's1',
'Actinomyces': 12.55802794990189,
'Alloprevotella': 0.3671446023182472,
'Atopobium': 0.15760660109181326,
'Anaerococcus': 0
},
{
'group': 'G1',
'sample': 's2',
'Actinomyces': 9.55802794990189,
'Alloprevotella': 0.3671446023182472,
'Atopobium': 0.12760660109181326,
'Anaerococcus': 10.0
},
{
'group': 'G2',
'sample': 's3',
'Actinomyces': 11.55802794990189,
'Alloprevotella': 0.3671446023182472,
'Atopobium': 0.9760660109181326,
'Anaerococcus': 5.0
},
{
'group': 'G2',
'sample': 's4',
'Actinomyces': 19.55802794990189,
'Alloprevotella': 1.3671446023182472,
'Atopobium': 2.15760660109181326,
'Anaerococcus': 4.0
}
]
const w = 800
const h = 400
const margin = { top: 50, right: 50, bottom: 50, left: 150 }
const keys = Object.keys(data[0]).filter(function (val) {
return val !== 'sample' && val !== 'group'
})
// create a stack generator
let stack = d3.stack()
.keys(keys)
const xScale = d3.scaleBand()
.domain(d3.range(data.length))
.range([margin.left, w - margin.right])
.paddingOuter(0.02)
const yScale = d3.scaleLinear()
.domain([0,
d3.max(data, function (d) {
return d3.sum(keys.map(val => d[val]))
})
])
.range([h - margin.bottom, margin.top])
const colorScale = d3.scaleLinear()
.domain([0, keys.length - 1])
.range([0, 1])
// create svg
const svg = d3.select('#app')
.append('svg')
.attr('width', w)
.attr('height', h)
const groups = svg.selectAll('g')
.data(stack(data))
.enter()
.append('g')
.style('fill', function (d, i) {
return d3.interpolateSpectral(colorScale(i))
})
groups.selectAll('rect')
.data(function (d) {
return d
})
.enter()
.append('rect')
.attr('x', (d, i) => xScale(i))
.attr('y', d => yScale(d[1]))
.attr('height', d => yScale(d[0]) - yScale(d[1]))
.attr('width', xScale.bandwidth())
// add axis
const xAxis = d3.axisBottom(xScale)
.tickFormat(d => keys[d])
svg.append('g')
.attr('class', 'xAxis')
.attr('transform', 'translate(0, ' + yScale(0) + ')')
.call(xAxis)
/** change **/
.selectAll('text')
.attr('text-anchor', 'start')
.attr('y', 0)
.attr('transform', 'rotate(90)')
.attr('dx', 9) // let the text move a little to the bottom
.attr('dy', 2) // at the beginning, the top of text is parallel with the tick. So we need move a little to the right
/***********/
const yAxis = d3.axisLeft(yScale)
svg.append('g')
.attr('class', 'yAxis')
.attr('transform', 'translate(' + xScale(0) + ', 0)')
.call(yAxis)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="app">
</div>

How to implement a smooth transition when new bands are created in d3?

In the example below, I'm trying to animate new items appearance.
As you can see, they animate from the bottom of the chart to their position.
However, existing items ("second" in this example) jump, instead of smoothly transitioning to their new position.
I thought it is because the new band suddenly appears, without a transition. So, I tried to add a transition:
const band = bandUpdate.enter()
.append('g')
.attr('class', 'band')
.merge(bandUpdate)
.transition(t)
.attr('transform', (_, i) => `translate(0, ${i * bandHeight})`);
But, I'm getting:
Uncaught TypeError: band.selectAll(...).data is not a function
Could you explain the error please, and suggest a way to avoid the undesired jump?
Bonus: How could I animate the y axis labels?
Playground
const width = 300;
const height = 200;
const margin = { top: 30, right: 30, bottom: 30, left: 50 };
let data = {};
const main = d3.select('.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})`);
const xScale = d3.scaleLinear().domain([0, 16]).range([0, width]);
const xAxis = d3.axisBottom(xScale);
main.append('g')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
const yScale = d3.scaleBand().domain([]).range([0, height]);
const yAxis = d3.axisLeft(yScale);
const yAxisG = main.append('g').call(yAxis);
const bandG = main.append('g');
function update() {
const t = d3.transition().duration(500);
const ids = Object.keys(data);
yScale.domain(ids);
yAxisG.call(yAxis);
const bandHeight = yScale.bandwidth();
const bandUpdate = bandG.selectAll('.band').data(ids, id => id);
const band = bandUpdate.enter()
.append('g')
.attr('class', 'band')
.merge(bandUpdate)
// .transition(t) // Throws: Uncaught TypeError: band.selectAll(...).data is not a function
.attr('transform', (_, i) => `translate(0, ${i * bandHeight})`);
bandUpdate.exit().remove();
const itemUpdate = band.selectAll('.item')
.data(id => data[id], item => item.value);
const itemG = itemUpdate.enter().append('g').attr('class', 'item');
const rectHeight = 4;
itemG
.append('rect')
.attr('class', (_, i) => `item-${i}`)
.attr('x', d => xScale(d.value))
.attr('width', d => width - xScale(d.value))
.attr('height', rectHeight)
.attr('y', height)
.transition(t)
.attr('y', bandHeight / 2 - rectHeight / 2);
itemG
.append('circle')
.attr('class', (_, i) => `item-${i}`)
.attr('cx', d => xScale(d.value))
.attr('r', 6)
.attr('cy', height)
.transition(t)
.attr('cy', bandHeight / 2);
itemUpdate
.select('rect')
.attr('x', d => xScale(d.value))
.attr('width', d => width - xScale(d.value))
.transition(t)
.attr('y', bandHeight / 2 - rectHeight / 2);
itemUpdate
.select('circle')
.attr('cx', d => xScale(d.value))
.transition(t)
.attr('cy', bandHeight / 2);
itemUpdate.exit().remove();
}
update();
setTimeout(() => {
data['first'] = [
{
value: 7
},
{
value: 10
}
];
update();
}, 1000);
setTimeout(() => {
data['second'] = [
{
value: 1
}
];
update();
}, 2000);
setTimeout(() => {
data['third'] = [
{
value: 13
}
];
update();
}, 3000);
svg {
margin: 0 30px 30px 30px;
}
.item-0 {
fill: red;
}
.item-1 {
fill: green;
}
<div class="chart"></div>
<script src="https://unpkg.com/d3#4.4.1/build/d3.js"></script>
Just break your band constant:
const band = bandUpdate.enter()
.append('g')
.attr('class', 'band')
.merge(bandUpdate);
band.transition(t)
.attr('transform', (_, i) => `translate(0, ${i * bandHeight})`);
Here is the updated CodePen: http://codepen.io/anon/pen/oBWJdp?editors=0010
Explanation:
According to the documentation, selection.transition([name]):
Returns a new transition on the given selection with the specified name.
So, when you later in the code do this:
const itemUpdate = band.selectAll('.item')
.data(id => data[id], item => item.value);
You're selecting a new transition, and that's giving you the error (you cannot bind data to a transition).
Breaking the band constant makes itemUpdate a selection based in the band selection, not in the following transition.

Rotate horizontal bar chart to vertical chart

Saw this graph on JSFiddle. I modified it with my own data, but I'd like to have a vertical bar chart instead of a horizontal. Here's what it looks like now. I tried fiddling with the X and Y axes and can't seem to get it right. Any help would be greatly appreciated.
Here's my code:
var margins = {
top: 12,
left: 48,
right: 24,
bottom: 40
},
legendPanel = {
width: 180
},
width = 400 - margins.left - margins.right - legendPanel.width,
height = 200 - margins.top - margins.bottom,
dataset = [{
data: [{
year: '2009',
count: 80
}, {
year: '2010',
count: 79
}, {
year: '2011',
count: 65
},
{
year: '2012',
count: 70
},
{
year: '2013',
count: 72
} ,
{
year: '2014*',
count: 38
}
],
name: 'Male'
}, {
data: [{
year: '2009',
count: 15
}, {
year: '2010',
count: 17
}, {
year: '2011',
count: 18
}, {
year: '2012',
count: 20
}, {
year: '2013',
count: 17
},
{
year: '2014*',
count: 8
}],
name: 'Female'
}
],
series = dataset.map(function (d) {
return d.name;
}),
dataset = dataset.map(function (d) {
return d.data.map(function (o, i) {
// Structure it so that your numeric
// axis (the stacked amount) is y
return {
y: o.count,
x: o.year
};
});
}),
stack = d3.layout.stack();
stack(dataset);
var dataset = dataset.map(function (group) {
return group.map(function (d) {
// Invert the x and y values, and y0 becomes x0
return {
x: d.y,
y: d.x,
x0: d.y0
};
});
}),
svg = d3.select('#sex')
.append('svg')
.attr('width', width + margins.left + margins.right + legendPanel.width)
.attr('height', height + margins.top + margins.bottom)
.append('g')
.attr('transform', 'translate(' + margins.left + ',' + margins.top + ')'),
xMax = d3.max(dataset, function (group) {
return d3.max(group, function (d) {
return d.x + d.x0;
});
}),
xScale = d3.scale.linear()
.domain([0, xMax])
.range([0, 200]),
months = dataset[0].map(function (d) {
return d.y;
}),
_ = console.log(months),
yScale = d3.scale.ordinal()
.domain(months)
.rangeRoundBands([0, height], .1),
xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom'),
yAxis = d3.svg.axis()
.scale(yScale)
.orient('left'),
colours = d3.scale.category20c(),
groups = svg.selectAll('g')
.data(dataset)
.enter()
.append('g')
.style('fill', function (d, i) {
return colours(i);
}),
rects = groups.selectAll('rect')
.data(function (d) {
return d;
})
.enter()
.append('rect')
.attr('x', function (d) {
return xScale(d.x0);
})
.attr('y', function (d, i) {
return yScale(d.y);
})
.attr('height', function (d) {
return yScale.rangeBand();
})
.attr('width', function (d) {
return xScale(d.x);
})
.on('mouseover', function (d) {
var xPos = parseFloat(d3.select(this).attr('x')) / 2 + width / 2;
var yPos = parseFloat(d3.select(this).attr('y')) + yScale.rangeBand() / 2;
d3.select('#tooltip')
.style('left', xPos + 'px')
.style('top', yPos + 'px')
.select('#value')
.text(d.x);
d3.select('#tooltip').classed('hidden', false);
})
.on('mouseout', function () {
d3.select('#tooltip').classed('hidden', true);
})
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
svg.append('g')
.attr('class', 'axis')
.call(yAxis);
svg.append('rect')
.attr('fill', 'white')
.attr('width', 160)
.attr('height', 30 * dataset.length)
.attr('x', width + margins.left)
.attr('y', 0);
//X AXIS LABEL
svg.append("text")
.attr("class", 'x label')
.attr("y", height + 35)
.attr("x", width / 2)
.text("Victims")
.style("font-size","13px")
.style("font-weight", "600");
/*
//FOOTNOTE
svg.append("text")
.attr("class", 'x label')
.attr("y", height + 35)
.attr("x", width + 65)
.text("*As of July 2014")
.style("font-size","11px")
.style("font-style", "italic");
*/
//LEGEND
series.forEach(function (s, i) {
svg.append('text')
.attr('fill', 'black')
.attr('x', width + margins.left + 18)
.attr('y', i * 24 + 24)
.text(s);
svg.append('rect')
.attr('fill', colours(i))
.attr('width', 20)
.attr('height', 20)
.attr('x', width + margins.left + 75)
.attr('y', i * 24 + 6);
});

Resources