I am trying to accomplish something similar to what is here : https://www.opportunityatlas.org/. If you proceed further to this link and click on 'Show Distribution' to see the graph and select 'On Screen' and then move the cursor around the map you will see the size of the rectangles changes and also the update patterns works i.e. if a rectangle was already there it moves horizontally to the new value.
I have tried doing the same but could not achieve the update part. Could you please point me out where I missed out. I have attached a part of my code where there are two data sets data1 and data2 with some id's common but as you can see when you click on update to change the data set all rectangles are in the enter phase and none of them change position for existing ones (none in update phase). It will be helpful if someone can guide me through this one. If there is some other way to implement the same graph that I provided in the link, that would also be helpful if there is some other approach. Thanks in advance !
let initialRender = true;
let selectedData1 = true;
const margin = {
top: 10,
right: 30,
bottom: 30,
left: 30
},
width = 550 - margin.left - margin.right,
height = 150 - margin.top - margin.bottom;
function printChart(asd, data, dataGradients) {
const svg = d3.select('#data-viz')
// const isZoomed = map.getZoom() > zoomThreshold;
// if (isZoomed !== changedZoom) {
// initialRender = true;
// changedZoom = isZoomed;
// }
// X axis and scale ------------->>>>>>>>>>>>>>>>>>>>
const xScale = d3.scaleLinear()
.domain(d3.extent(data.map(d => d.value)))
.range([0, width])
const xAxisCall = d3.axisBottom(xScale)
.tickFormat(d3.format(".2s"))
.ticks(5)
.tickSizeOuter(0);
let xAxis = null
if (initialRender) {
d3.select(".axis-x").remove()
xAxis = svg.append("g")
.attr("class", "axis-x")
.attr("transform", "translate(0," + 115 + ")")
initialRender = false
} else {
xAxis = d3.select(".axis-x")
}
xAxis.transition()
.duration(2000)
.ease(d3.easeSinInOut)
.call(xAxisCall)
// X axis and scale <<<<<<<<<<<<<<<<-----------------------------
const binMin = 5;
const binMax = 150;
const tDuration = 3000;
// Just to calculate max elements in each bin ---------->>>>>>>>>>>>>>>>>>
let histogram = d3.histogram()
.value(d => d.value)
.domain(xScale.domain())
.thresholds(xScale.ticks(10));
let bins = histogram(data).filter(d => d.length > 0);
console.log(bins);
const max = d3.max(bins.map(bin => bin.length))
const maxBinSize = max <= 10 ? 10 : max
// Just to calculate max elements in each bin <<<<<<<<<<<<----------------
// Decide parameters for histogram ------------>>>>>>>>>>>>>>>>>
const dotSizeScale = d3.scaleLinear()
.domain([binMin, binMax])
.range([10, 4])
const dotSize = dotSizeScale(maxBinSize);
const dotSpacingScale = d3.scaleLinear()
.domain([binMin, binMax])
.range([12, 6])
const dotSpacing = dotSpacingScale(maxBinSize);
const thresholdScale = d3.scaleLinear()
.domain([binMin, binMax])
.range([10, 100])
const threshold = thresholdScale(maxBinSize);
const yTransformMarginScale = d3.scaleLinear()
.domain([binMin, binMax])
.range([100, 100])
const yTransformMargin = yTransformMarginScale(maxBinSize);
if (dotSize !== 10) {
d3.selectAll('.gBin').remove()
d3.selectAll('rect').remove()
}
histogram = d3.histogram()
.value(d => d.value)
.domain(xScale.domain())
.thresholds(xScale.ticks(threshold));
bins = histogram(data).filter(d => d.length > 0);
// Decide parameters for histogram <<<<<<<<<<<<<<<<<<<<--------------------------
// Y axis scale -------------------->>>>>>>>>>>>>>>>>>>>
var yScale = d3.scaleLinear()
.range([height, 0]);
yScale.domain([0, d3.max(bins, (d) => d.length)]);
svg.append("g")
.attr("class", "axis-y")
.call(d3.axisLeft(yScale));
d3.select(".axis-y")
.remove()
// Y axis scale <<<<<<<<<<<<<<<<<<<<<<<-----------------
const binGroup = svg.selectAll(".gBin")
.data(bins,
(d) => {
console.log('id 1', d.x0)
return d.x0
}
)
binGroup
.exit()
.transition()
.duration(2000)
.style("opacity", 0)
.remove()
const binGroupEnter = binGroup
.enter()
.append("g")
.merge(binGroup)
.attr("class", "gBin")
.attr("x", 1)
.attr("transform", function(d) {
return "translate(" + xScale(d.x0) + "," + yTransformMargin + ")";
})
.attr("width", 10)
const elements = binGroupEnter.selectAll("rect")
.data(d => d.map((p, i) => ({
id: p.id,
idx: i,
value: p.value,
})),
function(d) {
console.log('id 2', d)
return d.id
}
)
elements.exit()
.transition()
.duration(tDuration)
.style("opacity", 0)
.remove()
elements
.enter()
.append("rect")
.merge(elements)
.attr("y", -(height + margin.top))
// .on("mouseover", tooltipOn)
// .on("mouseout", tooltipOff)
.transition()
.delay(function(d, i) {
return 50 * i;
})
.duration(tDuration)
.attr("id", d => d.value)
.attr("y", (d, i) => -(i * dotSpacing))
.attr("width", dotSize)
.attr("height", dotSize)
// .style("fill", (d) => getBinColor(d.value, dataGradients))
.style("fill", 'red')
}
const data1 = [{
id: 1,
value: 14
}, {
id: 13,
value: 12
}, {
id: 2,
value: 50
}, {
id: 32,
value: 142
}]
const data2 = [{
id: 1,
value: 135
}, {
id: 7,
value: 2
}, {
id: 2,
value: 50
}, {
id: 32,
value: 50
}]
printChart(null, data1, null)
function changeData() {
selectedData1 ?
printChart(null, data2, null) :
printChart(null, data1, null)
selectedData1 = !selectedData1
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<button onclick="changeData()"> Update data </button>
<svg width="550" height="250" id="data-viz">
<g transform="translate(30, 100)">
</g>
</svg>
Your problem appears to be these lines:
if (dotSize !== 10) {
d3.selectAll('.gBin').remove();
d3.selectAll('rect').remove();
}
All your elements are removed before any selections are calculated so everything (both your bin g and element rect) become enter.
Another interesting thing is your data key for your bins. Since you are using the x0 your g will also enter/exit depending on how the histogram function calculates the bins.
<html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<button onclick="changeData()">Update data</button>
<svg width="550" height="250" id="data-viz">
<g transform="translate(30, 100)"></g>
</svg>
<script>
let initialRender = true;
let selectedData1 = true;
const margin = {
top: 10,
right: 30,
bottom: 30,
left: 30,
},
width = 550 - margin.left - margin.right,
height = 150 - margin.top - margin.bottom;
function printChart(asd, data, dataGradients) {
console.clear();
const svg = d3.select('#data-viz');
// const isZoomed = map.getZoom() > zoomThreshold;
// if (isZoomed !== changedZoom) {
// initialRender = true;
// changedZoom = isZoomed;
// }
// X axis and scale ------------->>>>>>>>>>>>>>>>>>>>
const xScale = d3
.scaleLinear()
.domain(d3.extent(data.map((d) => d.value)))
.range([0, width]);
const xAxisCall = d3
.axisBottom(xScale)
.tickFormat(d3.format('.2s'))
.ticks(5)
.tickSizeOuter(0);
let xAxis = null;
if (initialRender) {
d3.select('.axis-x').remove();
xAxis = svg
.append('g')
.attr('class', 'axis-x')
.attr('transform', 'translate(0,' + 115 + ')');
initialRender = false;
} else {
xAxis = d3.select('.axis-x');
}
xAxis.transition().duration(2000).ease(d3.easeSinInOut).call(xAxisCall);
// X axis and scale <<<<<<<<<<<<<<<<-----------------------------
const binMin = 5;
const binMax = 150;
const tDuration = 3000;
// Just to calculate max elements in each bin ---------->>>>>>>>>>>>>>>>>>
let histogram = d3
.histogram()
.value((d) => d.value)
.domain(xScale.domain())
.thresholds(xScale.ticks(10));
let bins = histogram(data).filter((d) => d.length > 0);
//console.log(bins);
const max = d3.max(bins.map((bin) => bin.length));
const maxBinSize = max <= 10 ? 10 : max;
// Just to calculate max elements in each bin <<<<<<<<<<<<----------------
// Decide parameters for histogram ------------>>>>>>>>>>>>>>>>>
const dotSizeScale = d3
.scaleLinear()
.domain([binMin, binMax])
.range([10, 4]);
const dotSize = dotSizeScale(maxBinSize);
const dotSpacingScale = d3
.scaleLinear()
.domain([binMin, binMax])
.range([12, 6]);
const dotSpacing = dotSpacingScale(maxBinSize);
const thresholdScale = d3
.scaleLinear()
.domain([binMin, binMax])
.range([10, 100]);
const threshold = thresholdScale(maxBinSize);
const yTransformMarginScale = d3
.scaleLinear()
.domain([binMin, binMax])
.range([100, 100]);
const yTransformMargin = yTransformMarginScale(maxBinSize);
/*
if (dotSize !== 10) {
d3.selectAll('.gBin').remove()
d3.selectAll('rect').remove()
}
*/
histogram = d3
.histogram()
.value((d) => d.value)
.domain(xScale.domain())
.thresholds(xScale.ticks(threshold));
bins = histogram(data).filter((d) => d.length > 0);
// Decide parameters for histogram <<<<<<<<<<<<<<<<<<<<--------------------------
// Y axis scale -------------------->>>>>>>>>>>>>>>>>>>>
var yScale = d3.scaleLinear().range([height, 0]);
yScale.domain([0, d3.max(bins, (d) => d.length)]);
svg.append('g').attr('class', 'axis-y').call(d3.axisLeft(yScale));
d3.select('.axis-y').remove();
// Y axis scale <<<<<<<<<<<<<<<<<<<<<<<-----------------
const binGroup = svg.selectAll('.gBin').data(bins, (d) => {
//console.log('id 1', d.x0)
return d.x0;
});
binGroup.exit().transition().duration(2000).style('opacity', 0).remove();
const binGroupEnter = binGroup
.enter()
.append('g')
.merge(binGroup)
.attr('class', 'gBin')
.attr('x', 1)
.attr('transform', function (d) {
return 'translate(' + xScale(d.x0) + ',' + yTransformMargin + ')';
})
.attr('width', 10);
const elements = binGroupEnter.selectAll('rect').data(
(d) =>
d.map((p, i) => ({
id: p.id,
idx: i,
value: p.value,
})),
function (d) {
//console.log('id 2', d)
return d.id;
}
);
let eex = elements
.exit()
.transition()
.duration(tDuration)
.style('opacity', 0)
.remove();
console.log("rects exiting", eex.nodes().map(e => "rect" + e.getAttribute('id')))
let een = elements
.enter()
.append('rect')
.attr('id', (d) => d.value);
console.log("rects entering", een.nodes().map(e => "rect" + e.getAttribute('id')))
let eem = een
.merge(elements);
console.log("rects merge", eem.nodes().map(e => "rect" + e.getAttribute('id')))
eem
.attr('y', -(height + margin.top))
// .on("mouseover", tooltipOn)
// .on("mouseout", tooltipOff)
.transition()
.delay(function (d, i) {
return 50 * i;
})
.duration(tDuration)
.attr('y', (d, i) => -(i * dotSpacing))
.attr('width', dotSize)
.attr('height', dotSize)
// .style("fill", (d) => getBinColor(d.value, dataGradients))
.style('fill', 'red');
}
const data1 = [
{
id: 1,
value: 14,
},
{
id: 13,
value: 12,
},
{
id: 2,
value: 50,
},
{
id: 32,
value: 142,
},
];
const data2 = [
{
id: 1,
value: 135,
},
{
id: 7,
value: 2,
},
{
id: 2,
value: 50,
},
{
id: 32,
value: 50,
},
];
printChart(null, data1, null);
function changeData() {
selectedData1
? printChart(null, data2, null)
: printChart(null, data1, null);
selectedData1 = !selectedData1;
}
</script>
</html>
Related
I have a project where I draw a doughnut/pie chart. Each slice in the chart consists of a group which in term has a path(the slice) and an image.
In order to transition the slices I have used the general update pattern to update the slices and added two attrTween methods to handle the transitions of the slices.
In order to add the images to the slices I have first put each slice into a group. Then I add an image to each group and use the arc.centroid method in order to position the images in the center of each slice. This works very well the first time I load the in the chart. But when the chart updates, the images stay in the center of their group as it was positioned previously.
I have console logged both the d element and the output of the arc.controid method with that d element. You can see clearly that the coordinates of the d elements don't change and that is why the images don't get an updated new position. But I do not get why they still get the old version of this d element and not the new updated one.
.append("svg:image")
.attr("transform", (d, i) => {
if (i === 1) {
console.log(d);
console.log(this.arc.centroid(d));
}
var x = this.arc.centroid(d)[0] - image_width / 2;
var y = this.arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
This is a snippet of the code I am using. I have tried to keep this as short as possible. But all the elements that are included are needed in order to get this snippet to work for this particular problem:
var margin = 1;
this.width = 250;
this.height = 250;
this.index = 0;
this.radius = Math.min(this.width, this.height) / 2 - margin;
this.svg = d3
.select(".canvas")
.append("svg")
.attr("width", this.width)
.attr("height", this.height)
.append("g")
.attr(
"transform",
"translate(" + this.width / 2 + "," + this.height / 2 + ")"
);
this.pie = d3
.pie()
.sort(null)
.value(d => d.value);
this.arc = d3
.arc()
.outerRadius(100)
.innerRadius(50);
const setSlicesOnDoughnut = (data) => {
this.arcs = this.svg.selectAll("path").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
.attr("class", "arc")
.attr("fill", "#206BF3")
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.each(function(d) {
this._current = d;
})
.transition()
.duration(1000)
.attrTween("d", arcTweenEnter);
},
update => {
update
.transition()
.duration(1000)
.attrTween("d", arcTweenUpdate);
},
exit => {
exit.remove();
}
);
}
const addImagesToSlices = () => {
var image_width = 20;
var image_height = 20;
this.svg.selectAll("image").remove();
this.svg
.selectAll("g")
.append("svg:image")
.attr("transform", (d, i) => {
if (i === 1) {
console.log(d);
console.log(this.arc.centroid(d));
}
var x = this.arc.centroid(d)[0] - image_width / 2;
var y = this.arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
.attr("class", "logo")
.attr("class", function(d) {
return `${d.data.key}-logo`;
})
.attr("href", d => d.data.icon)
.attr("width", image_width)
.attr("height", image_height)
.attr("opacity", 0)
.transition()
.duration(1500)
.attr("opacity", 1);
}
var data = [
[{
key: "One",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
],
[{
key: "One",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
]
]
const arcTweenEnter = (d) => {
var i = d3.interpolate(d.endAngle, d.startAngle);
return t => {
d.startAngle = i(t);
return this.arc(d);
};
}
const arcTweenUpdate = (d, i, n) => {
var interpolate = d3.interpolate(n[i]._current, d);
n[i]._current = d;
return t => {
return this.arc(interpolate(t));
};
}
setSlicesOnDoughnut(data);
addImagesToSlices();
const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
if (this.index === 0) this.index = 1;
else this.index = 0;
setSlicesOnDoughnut(data);
addImagesToSlices();
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>
The issue is that you never update the datum bound to the g. Let's look at how you enter and update the wedges:
this.arcs = this.svg.selectAll("path").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
...
},
update => {
update
.transition()
...
},
You select all the paths and bind data to them (selectAll("path")). On enter you append a g, which means the g gets a bound datum, as does the child path. On update however, as you've only selected paths, you only bind new data to the paths. The parent g is left with the original bound datum.
Instead, let's select the parent g with selectAll, and tweak our update function slightly to account for this:
// select parent g elements instead of paths:
this.arcs = this.svg.selectAll("g").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
...
},
update => {
update
.select("path") // select the child path.
.transition()
...
},
Now when we (re-)append the images, they are using the most recent datum bound to their parent g:
var margin = 1;
this.width = 250;
this.height = 250;
this.index = 0;
this.radius = Math.min(this.width, this.height) / 2 - margin;
this.svg = d3
.select(".canvas")
.append("svg")
.attr("width", this.width)
.attr("height", this.height)
.append("g")
.attr(
"transform",
"translate(" + this.width / 2 + "," + this.height / 2 + ")"
);
this.pie = d3
.pie()
.sort(null)
.value(d => d.value);
this.arc = d3
.arc()
.outerRadius(100)
.innerRadius(50);
const setSlicesOnDoughnut = (data) => {
this.arcs = this.svg.selectAll("g").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
.attr("class", "arc")
.attr("fill", "#206BF3")
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.each(function(d) {
this._current = d;
})
.transition()
.duration(1000)
.attrTween("d", arcTweenEnter);
},
update => {
update
.select("path")
.transition()
.duration(1000)
.attrTween("d", arcTweenUpdate);
},
exit => {
exit.remove();
}
);
}
const addImagesToSlices = () => {
var image_width = 20;
var image_height = 20;
this.svg.selectAll("image").remove();
this.svg
.selectAll("g")
.append("svg:image")
.attr("transform", (d, i) => {
if (i === 1) {
console.log(this.arc.centroid(d));
}
var x = this.arc.centroid(d)[0] - image_width / 2;
var y = this.arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
.attr("class", "logo")
.attr("class", function(d) {
return `${d.data.key}-logo`;
})
.attr("href", d => d.data.icon)
.attr("width", image_width)
.attr("height", image_height)
.attr("opacity", 0)
.transition()
.duration(1500)
.attr("opacity", 1);
}
var data = [
[{
key: "One",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
],
[{
key: "One",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 100,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
]
]
const arcTweenEnter = (d) => {
var i = d3.interpolate(d.endAngle, d.startAngle);
return t => {
d.startAngle = i(t);
return this.arc(d);
};
}
const arcTweenUpdate = (d, i, n) => {
var interpolate = d3.interpolate(n[i]._current, d);
n[i]._current = d;
return t => {
return this.arc(interpolate(t));
};
}
setSlicesOnDoughnut(data);
addImagesToSlices();
const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
if (this.index === 0) this.index = 1;
else this.index = 0;
setSlicesOnDoughnut(data);
addImagesToSlices();
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>
I am developing a d3 chart that updates every second.
After leaving the tab opened in the background for a while get the "Oh Snap" error on the browser.
I have tracked down the issue to the axis of the chart that dynamically updates depending on the current limits of the data I am receiving (Minimum an Maximum).
CHART FUNCTION:
function realtimeChart($chartContainer, width, height) {
// data arrays
this.dataMin = [];
this.dataMax = [];
// chart dimensions
this.width = width || 500;
this.height = height || 500;
//
this.globalX = 0;
this.duration = 100;
this.max = 50;
this.step = 1;
//
this.chart = d3.select($chartContainer.get(0))
.attr('width', this.width)
.attr('height', this.height + 25);
var x = d3.scaleLinear()
.domain([0, this.width])
.range([40, this.width]),
y = d3.scaleLinear()
.domain([0, 10])
.range([this.height - 20, 10]);
this.line = d3.line()
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return y(d.y);
});
this.smoothLine = d3.line()
.curve(d3.curveCardinal)
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return y(d.y);
});
this.lineArea = d3.area()
.x(function(d) {
return x(d.x);
})
.y0(y(0))
.y1(function(d) {
return y(d.y);
})
.curve(d3.curveCardinal);
// Draw the axis
this.xAxis = d3.axisBottom().scale(x);
this.yAxis = d3.axisLeft().scale(y);
this.axisX = this.chart.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + (this.height - 20) + ')')
.call(this.xAxis);
this.axisY = this.chart.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(40, 0)')
.call(this.yAxis);
// Append the holder for line chart and fill area
this.pathMin = this.chart.append('path');
this.pathMax = this.chart.append('path');
this.next = function(min, max) {
// Generate new data
var pointMin = {
x: this.globalX,
y: min || 0
},
pointMax = {
x: this.globalX,
y: max || cur
};
this.dataMin.push(pointMin);
this.dataMax.push(pointMax);
this.globalX += this.step;
// Shift the chart left
x.domain([this.globalX - (this.max - this.step) - 1, this.globalX - 1]);
y.domain([min - 2, max + 2]);
//THE PROBLEM IS SOLVED WHEN I COMMENT THE AXIS UPDATE
this.axisX.transition()
.duration(this.duration)
.ease(d3.easeLinear, 2)
.call(this.xAxis);
this.axisY.transition()
.duration(this.duration)
.ease(d3.easeLinear, 2)
.call(this.yAxis);
// Draw new line
this.pathMin.datum(this.dataMin)
.attr('class', 'smoothline min')
.attr('d', this.smoothLine);
this.pathMax.datum(this.dataMax)
.attr('class', 'smoothline max')
.attr('d', this.smoothLine);
// Remote old data (max 49 pointCurs)
if (this.dataCur.length > 49) {
this.dataMin.shift();
this.dataMax.shift();
}
}
}
HOW I AM INITIALIZING THE CHART:
var $chartDiv;
$chartDiv = $("<div>")
.addClass("timerRow")
.addClass("chartRow")
.append(function() {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
return $(svg)
.attr({
"width": "500",
"height": "500",
"class": "chart"
});
});
$chartDiv.data({
"chart": new realtimeChart(
$chartDiv.find("svg").first(),
$("#dataDiv").innerWidth() - 40,
150)
})
HOW I UPDATE THE CHART AFTER RECIEVING NEW DATA:
chart = $chartRow.data("chart")
chart.next(timeData.min, timeData.max);
Does anyone know how to solve this issue without removing the axis?
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>
import React, { Component } from 'react';
import { json } from 'd3-request';
import { rgb } from 'd3-color';
import { interpolateHcl } from 'd3-interpolate';
import { scaleLinear, scaleOrdinal } from 'd3-scale';
import { arc, line, pie, curveMonotoneX } from 'd3-shape';
import { format } from 'd3-format';
import { min, max } from 'd3-array';
import { select } from 'd3-selection';
import { sankey as sankeyGraph, sankeyLinkHorizontal } from 'd3-sankey';
class Graph extends React.Component {
constructor(props) {
super(props);
this.createLineGraph = this.createLineGraph.bind(this);
this.createBarChart = this.createBarChart.bind(this);
this.createPieChart = this.createPieChart.bind(this);
this.createSankeyGraph = this.createSankeyGraph.bind(this);
// this.createRadialChart = this.createRadialChart.bind(this);
this.createTheGraphs = this.createTheGraphs.bind(this);
this.state = {
loading: false
};
}
getDimensions() {
const margin = {top: 20, right: 20, bottom: 20, left: 20},
padding = {top: 40, right: 40, bottom: 40, left: 40},
outerWidth = parseInt(this.props.size[0]),
outerHeight = parseInt(this.props.size[1]),
innerWidth = outerWidth - margin.left - margin.right,
innerHeight = outerHeight - margin.top - margin.bottom,
width = innerWidth - padding.left - padding.right,
height = innerHeight - padding.top - padding.botto,
radius = parseInt(min([innerWidth, innerHeight]) / 2),
donutHole = this.props.type === "DONUT" ? radius / 2 : 0,
color = scaleLinear()
.domain([1, this.props.data.length])
.interpolate(interpolateHcl)
.range([rgb("#AF2192"), rgb("#98D719")]);
// DON'T DO DATA MAPPING ON SANKEY GRAPH SINCE DATA STRUCTURE IS DIFFERENT
if (this.props.type !== "SANKEY") {
// HIGHEST VALUE OF ITEMS IN DATA ARRAY
const dataMax = max(this.props.data.map(item => item.value)),
dataSpread = (innerWidth / this.props.data.length),
// DEPEND SCALE OF ITEMS ON THE Y AXIS BASED ON HIGHEST VALUE
yScale = scaleLinear()
.domain([0, dataMax])
.range([0, innerHeight]),
// GENERATE THE LINE USING THE TOTAL SPACE AVAILABLE FROM THE SIZE PROP DIVIDED BY THE LENGTH OF THE DATA ARRAY
lineGen = line()
.x((d, i) => i * dataSpread)
.y(d => innerHeight - yScale(d))
// CURVEMONOTONEX GAVE THE BEST RESULTS
.curve(curveMonotoneX);
dimensions = {margin, padding, outerWidth, outerHeight, innerWidth, innerHeight, radius, donutHole, color, dataMax, dataSpread, yScale, lineGen};
} else {
dimensions = {margin, padding, outerWidth, outerHeight, innerWidth, innerHeight, radius, donutHole, color};
}
}
createSankeyGraph(data) {
const sankeyNode = this.node;
let graphData = this.props.data;
// console.log(graphData);
// console.log(graphData.links);
// console.log(graphData.nodes);
// console.log(dimensions.outerWidth, dimensions.outerHeight);
// GET DIMENSIONS IN A GLOBAL-VAR-LIKE WAY
this.getDimensions();
const formatNumber = format('.1f');
const formatted = function(d) {return formatNumber(d) + " Potential Guests"};
const color = scaleLinear()
.domain([1, 3])
.interpolate(interpolateHcl)
.range([rgb('#126382'), rgb('#417685')]);
var sankey = sankeyGraph()
.nodeWidth(15)
.nodePadding(10)
.extent([1, 1], [parseInt(dimensions.outerWidth) - 1, parseInt(dimensions.outerHeight) - 6]);
var SVG = select(sankeyNode)
.append('g')
.attr('transform', 'translate(' + dimensions.margin.left + ',' + dimensions.margin.top +')');
var link = SVG.append('g')
.attr('class', 'links')
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
.selectAll('path')
var node = SVG.append('g')
.attr('class', 'nodes')
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll('g')
// json('https://api.myjson.com/bins/15xgsd', function(error, graphData){
sankey(graphData);
// console.log(graphData.nodes, graphData.links);
link = link
.data(graphData.links)
.enter()
.append('path')
.attr('d', sankeyLinkHorizontal())
.attr('stroke-width', function(d) { return Math.max(1, d.width); });
link.append('title')
.text(function(d) { return d.source.name + " → " + d.target.name + "\n" + formatted(d.value); });
node = node
.data(graphData.nodes)
.enter()
.append('g')
node.append('rect')
.attr('x', function(d) { return d.x0; })
.attr('y', function(d) { return d.y0; })
.attr('height', function(d) { return d.y1 - d.y0})
.attr('width', function(d) { return d.x1 - d.x0})
.attr("fill", function(d, i) { return color(i); })
.attr('stroke', 'black');
node.append('text')
.attr('x', function(d) {return d.x0 - 6})
.attr('y', function(d) {return (d.y1 + d.y0) / 2})
.attr('dy', '.35em')
.attr('text-anchor', 'end')
.text(function(d) { return d.name; })
.filter(function(d) { return d.x0 < dimensions.innerWidth / 2; })
.attr('x', function(d) { return d.x1 + 6; })
.attr('text-anchor', 'start');
node.append('title')
.text(d => d.name + "\n" + formatted(d.value));
// });
}
createTheGraphs() {
(this.props.type === "LINE") ? this.createLineGraph() : "";
(this.props.type === "BAR") ? this.createBarChart() : "";
(this.props.type === "PIE" || this.props.type === "DONUT") ? this.createPieChart() : "";
(this.props.type === "SANKEY") ? this.createSankeyGraph() : "";
(this.props.type === "RADIAL") ? this.createRadialChart() : "";
}
componentWillMount() {
this.setState({ loading: true });
}
componentDidMount() {
this.createTheGraphs();
}
componentDidUpdate() {
this.createTheGraphs();
}
render() {
return(
<div className="Graph">
<svg className='Graph_Container' ref={node => this.node = node}></svg>
<h2>{this.props.type} Placeholder</h2>
</div>
);
}
}
Graph.propTypes = {
};
export default Graph;
What's happening? Well, basically the console is outputting
Error: missing: Peter modules.js:54276:20
find http://localhost:3000/packages/modules.js:54276:20
computeNodeLinks/< http://localhost:3000/packages/modules.js:54353:62
forEach self-hosted:267:13
computeNodeLinks http://localhost:3000/packages/modules.js:54350:5
sankey http://localhost:3000/packages/modules.js:54292:5
createSankeyGraph http://localhost:3000/app/app.js:554:13
createSankeyGraph self-hosted:941:17
createTheGraphs http://localhost:3000/app/app.js:598:44
createTheGraphs self-hosted:941:17
componentDidMount http://localhost:3000/app/app.js:617:13
mountComponent/</< http://localhost:3000/packages/modules.js:17838:20
measureLifeCyclePerf http://localhost:3000/packages/modules.js:17649:12
mountComponent/< http://localhost:3000/packages/modules.js:17837:11
notifyAll http://localhost:3000/packages/modules.js:10464:9
close http://localhost:3000/packages/modules.js:20865:5
closeAll http://localhost:3000/packages/modules.js:11623:11
perform http://localhost:3000/packages/modules.js:11570:11
batchedMountComponentIntoNode http://localhost:3000/packages/modules.js:22897:3
perform http://localhost:3000/packages/modules.js:11557:13
batchedUpdates http://localhost:3000/packages/modules.js:20563:14
batchedUpdates http://localhost:3000/packages/modules.js:10225:10
_renderNewRootComponent http://localhost:3000/packages/modules.js:23090:5
_renderSubtreeIntoContainer http://localhost:3000/packages/modules.js:23172:21
render http://localhost:3000/packages/modules.js:23193:12
routes.js/< http://localhost:3000/app/app.js:1504:3
maybeReady http://localhost:3000/packages/meteor.js:821:6
loadingCompleted http://localhost:3000/packages/meteor.js:833:5
Which results in the graph not rendering the nodes it needs to base the lines (paths) on. The only HTML I get back is:
<svg class="Graph_Container">
<g transform="translate(20,20)">
<g class="links" fill="none" stroke="#000" stroke-opacity="0.2"></g>
<g class="nodes" font-family="sans-serif" font-size="10"></g>
</g>
</svg>
No nodes in the 'g.nodes' thus no links in the 'g.links'. The data structure this graph should be processing looks like:
<Graph type="SANKEY"
data={{
nodes: [
{name: "Peter"},
{name: "Test.com"},
{name: "Thing.com"},
{name: "AnotherName"}
], links: [
{source: "Peter", target: "Test.com", value: 50},
{source: "Peter", target: "Thing.com", value: 50},
{source: "Test.com", target: "AnotherName", value: 50},
{source: "Thing.com", target: "AnotherName", value: 50}
]
}}
size={[500, 500]} />
I don't know where to go from here. With this package I jumped from issue to issue altering the code line by line. The original issue looked like this.
In case links are specified using a source and target name (i.e. a string) instead of node indices, the solution is to specify a nodeId mapping function:
d3.sankey()
.nodeId(d => d.name) // Needed to avoid "Error: Missing: myNode"
Of course one may have to adjust the function d => d.name to correspond to the actual data.
I am trying to add a tooltip for my dual line chart graph.
However, instead of using timeScale or scaleLinear, I used scalePoint to graph my chart.
I am trying to achieve the following effect:
https://bl.ocks.org/mbostock/3902569
this.x = d3.scalePoint().range([ this.margin.left, this.width - this.margin.right ]);
this.xAxis = d3.axisBottom(this.x);
this.x.domain(
this.dataArray.map(d => {
return this.format(d[ 'year' ]);
}));
Here is my mouseover function,
function mousemove() {
//d3.mouse(this)[ 0 ]
//x.invert
var x0 = d3.mouse(this)[ 0 ],
i = bisectDate(data, x0, 1),
d0 = data[ i - 1 ],
d1 = data[ i ],
d = x0 - d0.year > d1.year - x0 ? d1 : d0;
console.log(x0);
// focus.attr("transform", "translate(" + x(format(d.year)) + "," + y(d.housing_index_change) + ")");
// focus.select("text").text(d.housing_index_change);
}
Since I am using scalePoint, there is obviously no invert function to map the coordinates to my data. and I am only retrieving the first element in the array and it is the only one that is being display regardless of the position of the mouse.
So my question is, how can I implement the invert functionality here while still using scalePoint?
Thank you :)
You are right, there is no invert for a point scale. But you can create your own function to get the corresponding domain of a given x position:
function scalePointPosition() {
var xPos = d3.mouse(this)[0];
var domain = xScale.domain();
var range = xScale.range();
var rangePoints = d3.range(range[0], range[1], xScale.step())
var yPos = domain[d3.bisect(rangePoints, xPos) -1];
console.log(yPos);
}
Step by step explanation
First, we get the x mouse position.
var xPos = d3.mouse(this)[0];
Then, based on your scale's range and domain...
var domain = xScale.domain();
var range = xScale.range();
...we create an array with all the steps in the point scale using d3.range:
var rangePoints = d3.range(range[0], range[1], xScale.step())
Finally, we get the corresponding domain using bisect:
var yPos = domain[d3.bisect(rangePoints, xPos) -1];
Check the console.log in this demo:
var data = [{
A: "groupA",
B: 10
}, {
A: "groupB",
B: 20
}, {
A: "groupC",
B: 30
}, {
A: "groupD",
B: 10
}, {
A: "groupE",
B: 17
}]
var width = 500,
height = 200;
var svg = d3.selectAll("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var color = d3.scaleOrdinal(d3.schemeCategory10)
.domain(data.map(function(d) {
return d.A
}));
var xScale = d3.scalePoint()
.domain(data.map(function(d) {
return d.A
}))
.range([50, width - 50])
.padding(0.5);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return d.B
}) * 1.1])
.range([height - 50, 10]);
var circles = svg.selectAll(".circles")
.data(data)
.enter()
.append("circle")
.attr("r", 8)
.attr("cx", function(d) {
return xScale(d.A)
})
.attr("cy", function(d) {
return yScale(d.B)
})
.attr("fill", function(d) {
return color(d.A)
});
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
svg.append("g").attr("transform", "translate(0,150)")
.attr("class", "xAxis")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(50,0)")
.attr("class", "yAxis")
.call(yAxis);
svg.append("rect")
.attr("opacity", 0)
.attr("x", 50)
.attr("width", width - 50)
.attr("height", height)
.on("mousemove", scalePointPosition);
function scalePointPosition() {
var xPos = d3.mouse(this)[0];
var domain = xScale.domain();
var range = xScale.range();
var rangePoints = d3.range(range[0], range[1], xScale.step())
var yPos = domain[d3.bisect(rangePoints, xPos) - 1];
console.log(yPos);
}
.as-console-wrapper { max-height: 20% !important;}
<script src="https://d3js.org/d3.v4.min.js"></script>