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.
Related
here is an attempt to make a data tree so that whenever the user clicks on any element the element must be removed from the dataset and the tree will once again be regenerated.
for example, before clicking
let us suppose after clicking on p3 the new graph generated is
the code through which the graph is generated is
const svg = d3.select("body").append('svg');
const margin = { left: 80, right: 20, top: 20, bottom: 20 }
var NODES;
const height = 700 - margin.top - margin.bottom;
const width = 800 - margin.left - margin.right;
const nodeElements = [];
let treeData = { id: 0, name: 'p0', children: [{ id: 1, name: 'p1', children: [{ id: 3, name: 'p3', children: [] },] }, { id: 2, name: 'p2', children: [{ id: 4, name: 'p4', children: [] },] }] };
let duration = 1513;
let i = 0;
let root;
let treemap = d3.tree().size([height, width])
svg.attr('height', height + margin.top + margin.bottom)
.attr('width', width + margin.right + margin.left)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
root = d3.hierarchy(treeData, (d) => {
console.log(d);
return d.children;
})
root.x0 = height / 2;
root.y0 = 0;
console.log('ROOT::', root);
update(root);
function update(source) {
let treedata = treemap(root);
let nodes = treedata.descendants();
NODES = nodes;
nodes.forEach(d => {
d.y = d.depth * width / 5;
});
let node = svg.selectAll("g.node").data(nodes, (d) => d.id || (d.id = ++i));
// links
function diagonal(s, d) {
path = `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x}
${(s.y + d.y) / 2} ${d.x}
${d.y} ${d.x}`;
return path;
}
let links = treedata.descendants().slice(1);
let link = svg.selectAll('path.link').data(links, (d) => {
return d.id;
})
let linkEnter = link
.enter()
.insert('path', 'g')
.attr('class', 'link')
.attr('d', (d) => {
let o = { x: source.x0, y: source.y0 + 40 }
return diagonal(o, o)
})
let linkUpdate = linkEnter.merge(link);
linkUpdate
.transition()
.duration(duration)
.attr("d", (d) => {
return diagonal(d, d.parent);
});
let linkExit = link
.exit()
.transition()
.attr('d', (d) => {
let o = { x: source.x0, y: source.y0 }
return diagonal(o, o);
})
.remove();
let nodeEnter = node
.enter()
.append("g")
.attr("class", "node")
.attr("transform", d => {
return `translate(${source.y0 + 20},${source.x0})`
})
.on("click", clicked);
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 0)
.style('fill', d => {
return d._children ? "red" : "white";
})
let nodeUpdate = nodeEnter.merge(node);
nodeUpdate.transition()
.duration(duration)
.attr("transform", d => `translate(${d.y + 20},${d.x})`)
.attr("opacity", 1)
nodeUpdate.select("circle.node")
.attr('r', 10)
.style("fill", d => d._children ? "red" : "black")
.attr("cursor", "pointer");
nodeUpdate.append('rect')
.attr('x', 0)
.attr('y', -20)
.attr('rx', 5)
.attr('ry', 5)
.attr('width', 80)
.attr('height', 40)
.attr('fill', 'grey')
nodeUpdate.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('dx', 10)
.text(d => {
console.log(d.data.name)
return d.data.name;
});
nodeExit = node.exit()
.transition()
.duration(duration)
.attr("transform", function () { return `translate(${source.y + 20},${source.x})` })
.attr("opacity", 0.5)
.remove();
// collapsing of the nodes
nodes.forEach(d => {
d.x0 = d.x;
d.y0 = d.y;
})
}
function clicked(event, d) {
let child;
child = childrenCollector(d.id - 1, treeData.children)
root = d3.hierarchy(treeData, (da) => {
return da.children;
})
console.log("MANIPULATED:::", root)
root.x0 = height / 2;
root.y0 = 0;
update(root);
}
function childrenCollector(sourceId, nestedArray) {
const i = nestedArray.findIndex(({ id }) => id === sourceId);
let found;
if (i > -1) [found] = nestedArray.splice(i, 1)
else nestedArray.some(({ children }) =>
found = childrenCollector(sourceId, children)
);
return found;
}
the problem being faced is once it generates the new structure the new one is not getting modified.
REASON:::: in the clicked() function at 'd.id' when the element is clicked first the id value is equal to the id given in the data but after modification when I click the 'd.id' value keeps on increasing. Why is it happening, even when I am generating the graph over a new dataset
I am trying to set the quantity value inside of each individual bar in my bar graph like the image I have provided below:
Unfortunately, the code I have tried hovers the percentage in a really weird spot and I'm not sure what I can do to achieve the desired effect.
Here is my code:
import React, { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
import './BarChart.css';
const dataSet = [
{ category: '1', quantity: 15 },
{ category: '2', quantity: 10 },
{ category: '3', quantity: 50 },
{ category: '4', quantity: 30 },
{ category: '4', quantity: 75 },
{ category: '5', quantity: 5 }
];
const BarChartTest = () => {
const d3Chart = useRef();
const [dimensions, setDimensions] = useState({
width: window.innerWidth,
height: window.innerHeight
});
const update = useRef(false);
useEffect(() => {
// Listen for any resize event update
window.addEventListener('resize', () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight
});
// if resize, remove the previous chart
if (update.current) {
d3.selectAll('g').remove();
} else {
update.current = true;
}
});
DrawChart(dataSet, dimensions);
}, [dimensions]);
const margin = { top: 50, right: 30, bottom: 30, left: 60 };
const DrawChart = (data, dimensions) => {
const chartWidth = parseInt(d3.select('#d3RenewalChart').style('width')) - margin.left - margin.right;
const chartHeight = parseInt(d3.select('#d3RenewalChart').style('height')) - margin.top - margin.bottom;
const colors = ['#7fc97f', '#beaed4', '#fdc086', '#ffff99', '#386cb0', '#f0027f', '#bf5b17', '#666666'];
const svg = d3
.select(d3Chart.current)
.attr('width', chartWidth + margin.left + margin.right)
.attr('height', chartHeight + margin.top + margin.bottom);
const x = d3
.scaleBand()
.domain(d3.range(data.length))
.range([margin.left, chartWidth + margin.right])
.padding(0.1);
svg.append('g')
.attr('transform', 'translate(0,' + chartHeight + ')')
.call(
d3
.axisBottom(x)
.tickFormat((i) => data[i].category)
.tickSizeOuter(0)
);
const max = d3.max(data, function (d) {
return d.quantity;
});
const y = d3.scaleLinear().domain([0, 100]).range([chartHeight, margin.top]);
svg.append('g')
.attr('transform', 'translate(' + margin.left + ',0)')
.call(d3.axisLeft(y).tickFormat((d) => d + '%'));
svg.append('g')
.attr('fill', function (d, i, j) {
return colors[i];
})
.selectAll('rect')
.data(data)
.join('rect')
.attr('x', (d, i) => x(i))
.attr('y', (d) => y(d.quantity))
.attr('height', (d) => y(0) - y(d.quantity))
.attr('width', x.bandwidth())
.attr('fill', function (d, i) {
return colors[i];
})
.append('text')
.text(function (d) {
return d.quantity;
})
.on('click', (d) => {
location.replace('https://www.google.com');
});
svg.selectAll('.text')
.data(data)
.enter()
.append('text')
// .attr('text-anchor', 'middle')
.attr('fill', 'green')
.attr('class', 'label')
.attr('x', function (d) {
return x(d.quantity);
})
.attr('y', function (d) {
return y(d.quantity) - 20;
})
.attr('dy', '0')
.text(function (d) {
return d.quantity + '%';
})
.attr('x', function (d, i) {
console.log(i * (chartWidth / data.length));
return i * (chartWidth / data.length);
})
.attr('y', function (d) {
console.log(chartHeight - d.quantity * 4);
return chartHeight - d.quantity * 4;
});
};
return (
<div id="d3RenewalChart">
<svg ref={d3Chart}></svg>
</div>
);
};
export default BarChartTest;
Here is a link to my codesandbox.
The Codesandbox provided didn't contain any React code.
Copy-pasting the above into the component code and calling it from App.js revealed that resizing the window would cause problems, because the line svg.selectAll(".text") was having a fresh copy appended with every render (re-size).
Here is the original code in a working Codesandbox.
A refactored version of that code is in this updated Codesandbox.
Solution:
In addition to the .append() call appending the .text element to the svg without being removed, the code above appears to set the x and y attributes with .attr() twice; removing the additional code and changing a few values made it possible to position the bar labels in what is presumably the correct position.
Here's a refactored version:
// create labels
svg
.append("g")
.attr("fill", "black")
.attr("text-anchor", "end")
.style("font", "24px sans-serif")
.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("class", "label");
// position labels
svg
.selectAll(".label")
.data(data)
.attr("x", (d, index) => x(index) + x.bandwidth() / 2 + 24)
.text((d) => d.quantity + "%")
// to exclude the animation, remove these two lines
.transition()
.delay((d, i) => i * 20)
//
.attr("y", (d) => y(d.quantity) + 22);
On another note, a color with higher contrast is advised here. White text on lighter backgrounds may not be visible and won't provide an accessible experience for everyone. Here's the refactored Codesandbox again.
Hope this was helpful! ✌️
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>
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>
I am trying to modfiy the example stacked area chart here
currently it gives me an error as such:
Error: <path> attribute d: Expected number, "MNaN,22770LNaN,21…".
at this line: .attr('d', area);
This is my code so far:
let margin = {top: 20, right: 60, bottom: 30, left: 60},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
let parseDate = d3.timeParse('%m/%d/%Y');
let x = d3.scaleTime().range([0, width]),
y = d3.scaleLinear()
.range([height, 0]),
z = d3.scaleOrdinal()
.domain(['Detractors', 'Passives' , 'Promoters'])
.range(['#e81123', '#a0a1a2', '#7fba00']);
let stack = d3.stack();
let area = d3.area()
.x(function(d) { return x(d.data.date); })
.y0(function(d) { return y(d.y0); })
.y1(function(d) { return y(d.y0 + d.y); });
let g = this._svg.append('g')
.attr('transform', 'translate(' + 0 + ',' + margin.top + ')');
let data = [
{ 'date': '04/23/12' , 'Detractors': 20 , 'Passives': 30 , 'Promoters': 50 },
{ 'date': '04/24/12' , 'Detractors': 32 , 'Passives': 19 , 'Promoters': 42 },
{ 'date': '04/25/12' , 'Detractors': 45 , 'Passives': 11 , 'Promoters': 44 },
{ 'date': '04/26/12' , 'Detractors': 20 , 'Passives': 13 , 'Promoters': 64 }];
// console.log(myData.map(function (d) { return d.key; }));
// console.log('KEYS: '+ keys);
x.domain(d3.extent(data, function(d) {
console.log(parseDate(d.date));
return parseDate(d.date); }));
// let keys = (d3.keys(data[0]).filter(function (key) { return key !== 'date'; }));
z.domain(d3.keys(data[0]).filter(function (key) { return key !== 'date'; }));
// z.domain(keys);
stack.keys(d3.keys(data[0]).filter(function (key) { return key !== 'date'; }));
let layer = g.selectAll('.layer')
.data(stack(data))
.enter().append('g')
.attr('class', 'layer');
layer.append('path')
.attr('class', 'area')
.style('fill', function(d) {
// console.log('d.key: ' + d.key);
console.log('d.key: ' + d.key + ' color: ' + z(d.key));
return z(d.key); })
.attr('d', area);
layer.filter(function(d) { return d[d.length - 1][1] - d[d.length - 1][0] > 0.01; })
.append('text')
.attr('x', width - 6)
.attr('y', function(d) { return y((d[d.length - 1][0] + d[d.length - 1][1]) / 2); })
.attr('dy', '.35em')
.style('font', '10px sans-serif')
.style('text-anchor', 'end')
.text(function(d) {
// console.log('key label: ' + d.key);
return d.key; });
g.append('g')
.attr('class', 'axis axis--x')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(x));
// console.log(height);
g.append('g')
.attr('class', 'axis axis--y')
.call(d3.axisLeft(y).ticks(5, '%'));
}
I am just trying to parse the array so I can have the date, keys and value parsing properly. I just can't seem to get it right.
Much help is appreciated!!
The specific error that you are getting is due to the format of the date in your data. You have a line commented out in your code: let parseDate = d3.timeParse("%m/%d/%Y"); You do want to use the d3.timeParse function to transform your data to date format:
let tp = d3.timeParse("%m/%d/%y")
for(var i=0; i<data.length;i++){
data[i].date = tp(data[i].date)
}