I have a line graph where I have line transition. However on the initial graph drawing line path is ok, within the graph. But when I use the transition the line goes out of the container to the margins.
Transition function:
that.graph.select('.data-line')
.transition().duration(750)
.attr('fill', 'none')
.attr('stroke', '#ff0000')
.attr('stroke-width', 2)
.attr('d', line);
Line:
const line = d3.line()
.x(d => xAxis(new Date(d.n)))
.y(d => yAxis(d.v))
.defined(d => d.v || d.v === 0);
Axes:
const xAxis = d3.scaleTime()
.range([0, width])
.domain(xDomain);
const yAxis = d3.scaleLinear()
.range([height, 0])
.domain(yDomain);
Domains:
const yDomain = d3.extent(data, d => d.v);
const xDomain = d3.extent(data, d => new Date(d.n));
Dimensions:
const width = window.innerWidth - this.margins.left - this.margins.right;
const height = window.innerHeight - this.margins.top - this.margins.bottom;
Initial line drawing: (this is ok)
this.graph.append('g').attr('class', 'data-wrapper');
const line = d3.line()
.x(d => this.xAxis(new Date(d.n)))
.y(d => this.yAxis(d.v))
.defined(d => d.v || d.v === 0);
this.graph
.select('.data-wrapper')
.append('path')
.datum(data)
.attr('class', 'data-line')
.attr('fill', 'none')
.attr('stroke', '#ff0000')
.attr('stroke-width', 2)
.attr('d', line);
Here is the screenshot:
Any help is appreciated.
As #rioV8 suggested adding clip path solved issue.
If anyone in the feature has this problem here is the solution:
On your topmost SVG add this:
this.graph.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', this.width)
.attr('height', this.height);
And on the function where you draw data add
.attr('clip-path', 'url(#clip)')
Related
I am working with line-area chart using d3. But somehow the chart is not showing and there's no error in the console. I'm unable to figure out the issue. It will be helpful if anyone can help me with that.
I've added the entire code in Codepen. This is the link
Here's the sample code for ref:
const svg = d3.select("#chart")
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
svg.append('defs');
svg.call(createGradient);
svg.call(createGlowFilter);
const xScale = d3.scaleTime()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.x)),
d3.max(parsedData, d => d3.max(d.values, v => v.x))
])
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.y)),
d3.max(parsedData, d => d3.max(d.values, v => v.y))
])
.range([height, 0]);
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.curve(d3.curveCatmullRom.alpha());
svg.selectAll('.line')
.data(parsedData)
.enter()
.append('path')
.attr('d', (d) => {
const lineValues = line(d.values).slice(1);
const splitedValues = lineValues.split(',');
return `M0,${height},${lineValues},l0,${height - splitedValues[splitedValues.length - 1]}`
})
.style('fill', 'url(#gradient)')
svg.selectAll('.line')
.data(parsedData)
.enter()
.append('path')
.attr('d', d => line(d.values))
.attr('stroke-width', '2')
.style('fill', 'none')
.style('filter', 'url(#glow)')
.attr('stroke', '#e4647f');
Thanks in advance!
I've created a fork of your code that has the line and area showing.
Here are the major changes.
First, I parsed the strings into dates:
const parseTime = d3.timeParse("%b %Y");
const parsedData = bar_chart_data.map(val => ({
close: val.y,
date: parseTime(val.x)
}));
parsedData is now an array of objects, so we can update the scales accordingly:
const xScale = d3.scaleTime()
// extent returns an array containing the min and max
.domain(d3.extent(parsedData, d => d.date))
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(parsedData, d => d.close)])
.range([height, 0]);
Next, I switched d3.line() to d3.area(), since you want to draw an area underneath the line. This will save you from having to manually construct the "d" attribute for the area.
const area = d3.area()
.x(d => xScale(d.date))
.y1(d => yScale(d.close)) // top line
.y0(yScale(0)) // base line, always at 0
.curve(d3.curveCatmullRom.alpha(0.5));
For drawing the line and area, we do not need to do a data join, we can just use append() and pass the parsedData array to the area generator for the area and to area.lineY1() for the top line:
svg.append("path")
.attr("d", area(parsedData))
.style("fill", "url(#gradient)");
svg.append("path")
.attr("d", area.lineY1()(parsedData))
.attr("stroke-width", "2")
.style("fill", "none")
.style("filter", "url(#glow)")
.attr("stroke", "#e4647f");
I'm trying to append a line to an end of an area chart path. The most difficult part my area is animated. I have a clipPath which width being transformed from width: 0 to width: 960 and the line at the end goes along with it so shoud be synchronised. Also the text on top of that line needs to be updated while it goes along.
Desired output:
My initial idea was to build a chart area and add a clipPath and then add a bar chart inside of area chart so I can update my text based on the bar appended, however bars are not inside my area chart. What am I doing wrong to place bars inside area chart or is there a better solution to this?
// Area chart width and height
const width1 = 1000,
height1 = 100;
// Define x and y scale for area chart
const xScale1 = d3.scaleTime().range([0, width1]);
const yScale1 = d3.scaleLinear().range([height1, 0]);
// Define x and y range for bar chart
let xScale2 = d3.scaleBand().range([0, width1]);
let yScale2 = d3.scaleLinear().range([height1, 0]);
// Add SVG to #areachart
const svg1 = d3
.select('#areachart')
.append('svg')
.attr('viewBox', `0 0 ${width1} ${height1}`)
.attr('transform', 'translate(' + 0 + ',' + -50 + ')');
const g1 = svg1.append('g');
// Fetch data
d3.json(
'https://api.coronavirus.data.gov.uk/v1/data?filters=areaName=United%2520Kingdom;areaType=overview&structure=%7B%22areaType%22:%22areaType%22,%22areaName%22:%22areaName%22,%22areaCode%22:%22areaCode%22,%22date%22:%22date%22,%22newCasesByPublishDate%22:%22newCasesByPublishDate%22,%22cumCasesByPublishDate%22:%22cumCasesByPublishDate%22%7D&format=json'
)
.then(function(data) {
console.log('DATES SLICED ----->', data.data.slice(30, 281));
//Define xScale1 & yScale1 domain after data loaded
yScale1.domain([
0,
d3.max(data.data, function(d) {
return +d.cumCasesByPublishDate;
}),
]);
xScale1.domain(
d3.extent(data.data, function(d) {
return new Date(d.date);
})
);
// Area generator
const area = d3
.area()
.curve(d3.curveStepAfter)
.x((d) => xScale1(new Date(d.date)))
.y1((d) => yScale1(+d.cumCasesByPublishDate))
.y0(yScale1(0));
g1.append('path')
.datum(data.data.slice(30, 200))
.attr('d', area)
.classed('placeholder-layer', true)
.style('fill', '#dadada')
.style('opacity', '0.3');
// clipPath for areachart fill animation
const clip = g1.append('clipPath').attr('id', 'clip');
const clipRect = clip.append('rect').attr('width', 0).attr('height', 750);
g1.append('path')
.datum(data.data.slice(30, 200))
.attr('d', area)
.attr('clip-path', 'url(#clip)')
.classed('overlay-layer', true)
.style('fill', 'yellow')
.style('opacity', '0.3');
g1.append('line').attr('stroke-width', 960).style('stroke', 'yellow');
clipRect
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr('width', 960);
//x and y domain for bar chart
xScale2.domain(data.data.slice(30, 200).map((d) => new Date(d.date)));
yScale2.domain([
0,
d3.max(data.data, function(d) {
return +d.cumCasesByPublishDate;
}),
]);
g1.selectAll('rect')
.data(data.data.slice(30, 200))
.enter()
.append('rect')
.style('fill', 'red')
.attr('width', xScale2.bandwidth() * 10)
.attr('height', (d) => yScale2(+d.cumCasesByPublishDate))
.attr('x', 0)
.attr('y', function(d) {
return yScale2(+d.cumCasesByPublishDate);
})
.transition()
.delay(function(d, i) {
return i * 30;
})
.attr('x', function(d) {
return xScale2(new Date(d.date));
})
.duration(100);
})
// if there's an error, log it
.catch((error) => console.log(error));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<section id="map">
<div id="visualisation-container">
<div id="visualisation"></div>
<div id="areachart"></div>
</div>
</section>
I'd just use a line, no bars, and use transition.tween with d3.interpolateDate to make the text change.
// Area chart width and height
const width1 = 800,
height1 = 250,
marginBottom = 50
// Define x and y scale for area chart
const xScale1 = d3.scaleTime().range([0, width1]);
const yScale1 = d3.scaleLinear().range([height1, marginBottom]);
// Define x and y range for bar chart
let xScale2 = d3.scaleBand().range([0, width1]);
let yScale2 = d3.scaleLinear().range([height1, marginBottom]);
// Add SVG to #areachart
const svg1 = d3
.select('#areachart')
.append('svg')
.attr('width', width1)
.attr('height', height1);
const g1 = svg1.append('g');
// Fetch data
d3.json(
'https://api.coronavirus.data.gov.uk/v1/data?filters=areaName=United%2520Kingdom;areaType=overview&structure=%7B%22areaType%22:%22areaType%22,%22areaName%22:%22areaName%22,%22areaCode%22:%22areaCode%22,%22date%22:%22date%22,%22newCasesByPublishDate%22:%22newCasesByPublishDate%22,%22cumCasesByPublishDate%22:%22cumCasesByPublishDate%22%7D&format=json'
)
.then(data => {
data.data.forEach(d => {
d.cumCasesByPublishDate = +d.cumCasesByPublishDate;
d.date = new Date(d.date);
});
return data.data.slice(30, 200);
})
.then(function(data) {
//Define xScale1 & yScale1 domain after data loaded
yScale1.domain([
0,
d3.max(data, d => d.cumCasesByPublishDate),
]);
xScale1.domain(
d3.extent(data, d => d.date)
);
// Area generator
const area = d3
.area()
.curve(d3.curveStepAfter)
.x((d) => xScale1(d.date))
.y1((d) => yScale1(d.cumCasesByPublishDate))
.y0(yScale1(0));
g1.append('path')
.datum(data)
.attr('d', area)
.classed('placeholder-layer', true)
.style('fill', '#dadada')
.style('opacity', '0.3');
// clipPath for areachart fill animation
const clip = g1.append('clipPath').attr('id', 'clip');
const clipRect = clip.append('rect').attr('width', 0).attr('height', 750);
g1.append('path')
.datum(data)
.attr('d', area)
.attr('clip-path', 'url(#clip)')
.classed('overlay-layer', true)
.style('fill', 'yellow')
.style('opacity', '0.3');
const format = d3.timeFormat("%B %d, %Y");
const duration = 10000;
g1.append('line')
.attr('stroke-width', 5)
.style('stroke', 'black')
.attr('x1', xScale1.range()[0])
.attr('x2', xScale1.range()[0])
.attr('y1', yScale1.range()[0])
.attr('y2', yScale1.range()[1])
.transition()
.duration(duration)
.ease(d3.easeLinear)
.attr('x1', xScale1.range()[1])
.attr('x2', xScale1.range()[1])
g1.append('text')
.attr('x', xScale1.range()[0])
.attr('y', marginBottom / 2)
.attr('text-anchor', 'middle')
.transition()
.duration(duration)
.ease(d3.easeLinear)
.attr('x', xScale1.range()[1])
.tween('text', function() {
const i = d3.interpolateDate(xScale1.domain()[0], xScale1.domain()[1]);
return (t) => d3.select(this).text(format(i(t)));
})
})
// if there's an error, log it
.catch((error) => console.log(error));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<section id="map">
<div id="visualisation-container">
<div id="visualisation"></div>
<div id="areachart"></div>
</div>
</section>
I'm trying to create integer bands in the y axes.
Have tried changing .scaleband to .scalelinear and .ticks "arbitraryMetric" is stored as an integer.
Code:
const categories = vizData.map(d => d.arbitraryMetric);
const yScale = d3
.scaleBand()
.domain(categories)
.range([0, vizHeight - timelineMargin.bottom]);
const labels = vizCanvas
.selectAll('text')
.data(categories)
.enter()
.append('text')
.style('font-family', style.fontFamily)
.style('fill', '#3C4043')
.attr('x', timelineMargin.left - 10)
.attr('y', d => yScale(d) + yScale.bandwidth() / 2)
.attr('text-anchor', 'end')
.text(d => d);
This following solution will do the trick - credit to this answer
const yAxisTicks = yScale.ticks()
.filter(tick => Number.isInteger(tick));
const yAxis = d3.axisLeft(yScale)
.tickValues(yAxisTicks)
.tickFormat(d3.format('d'));
I'm writing a simple bar chart that draws a rect element for each piece of data. This is a part of my code without scales, which works fine:
const rect = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", (d, i) => (i*4))
.attr("y", (d) => h - d[1]/50)
However, if I add y-scale, my bars flip over, and if I add x-scale, I can only see one bar. Here's my code with scales:
const xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, (d) => d[0])])
.range([padding, w - padding]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, (d) => d[1])])
.range([h - padding, padding]);
//some other code
const rect = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", (d, i) => xScale(i*4))
.attr("y", (d) => yScale(d[1]/50))
Here's my CodePen with scales, that's what it looks like: http://codepen.io/enk/pen/vgbvWq?editors=1111
I'd be really greatful if somebody told me what am I doing wrong.
D3 scales are not messing with your chart. The problem here is simple.
Right now, this is your dataset:
[["1947-01-01", 243.1], ...]
As you can see, d[0] is a string (representing a date), not a number. And, in d3.scaleLinear, the domain is an array with two values only.
The solution here is using a scaleBand instead:
const xScale = d3.scaleBand()
.domain(dataset.map(d => d[0]))
.range([padding, w - padding]);
And changing the code of the rectangles to actually use the scales (I made several changes, check them):
const rect = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width", xScale.bandwidth())
.attr("x", (d => xScale(d[0]))) //xScale
.attr("height", (d => h - yScale(d[1]))) //yScale
.style("y", (d => yScale(d[1])))
.attr("data-date", (d) => d[0])
.attr("data-gdp", (d) => d[1])
.attr("class", 'bar')
Here is your updated CodePen: http://codepen.io/anon/pen/NdJGdo?editors=0010
Hello I have this code that I use to create a stacked area chart:
updateArea(yOffset, data = [], categories) {
const parseTime = this.parseTime;
const xScale = this.getScale(yOffset, data, categories).date;
const yScale = this.getScale(yOffset, data, categories).y;
const area = d3.area()
.curve(d3.curveCardinal)
.x(d => xScale(parseTime(d.data.date)))
.y0(d => yScale(d[0] || 0))
.y1(d => yScale(d[1] || 0));
const stack = d3.stack()
.keys(categories)
.order(d3.stackOrderReverse)
.offset(d3.stackOffsetNone);
if (data.length > 0) {
const stackContainer = this.vis.append('g')
.attr('class', 'stack');
const layer = stackContainer.selectAll('.layer')
.data(stack(data))
.enter()
.append('g')
.attr('class', 'layer');
layer.append('path')
.attr('class', 'area')
.style('fill', (d, i) => d3.schemeCategory20[i])
.attr('d', area);
}
const legend = this.vis.append('g')
.attr('class', 'legend');
legend.selectAll('.legend-item')
.data(stack(data))
.enter()
.append('circle')
.attr('r', 5)
.attr('cx', 20)
.attr('cy', (d, i) => yOffset + 20 + i * 12)
.attr('stroke', 'none')
.attr('fill', (d, i) => d3.schemeCategory20[i]);
legend.selectAll('.legend-item')
.data(stack(data))
.enter()
.append('text')
.attr('class', 'legend-item')
.attr('x', 30)
.attr('y', (d, i) => yOffset + 24 + i * 12)
.text(d => d.key);
}
I want to un stack the stacked areas so that they overlap and I can then make the areas opacity .3 or something.
When I try and do this:
.data(data)
.enter()
.append('g')
.attr('class', 'layer');
None of the areas show up. So just trying to figure out why!!
Thanks!
I just ended up doing this:
layer.append('path')
.attr('class', 'area')
.style('fill', 'transparent')
.style('stroke', (d, i) => d3.schemeCategory20[i])
.style('stroke-width', 1)
.attr('d', area);
and that seemed to work. Thanks everyone for the help!