Related
I have decided to create a interactive force directed graph in React using D3 but and everything is working for after the simulation started but the dragging doesn't work in React StrictMode I assume it must be due to the mounting and remount of component in ReactStrict mode 18 but I can't really pinpoint the reason.
Where is the content of my use effect hook
useEffect(function () {
if (!svgRef.current) return;
const svg = d3.select(svgRef.current);
const nodes = d3.map(
props.nodes,
(node): Node => ({
...node,
})
);
const links = d3
.map(props.links, (link) => ({
source: nodes.find((node) => node.id === link.source),
target: nodes.find((node) => node.id === link.target),
}))
.filter(
(link): link is { source: Node; target: Node } =>
link.source !== undefined && link.target !== undefined
);
const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(links)
.id((_, i) => nodes[i].id)
.distance(LINK_LENGHT)
)
.force("charge", d3.forceManyBody().strength(-NODE_REPULSION))
.force("center", d3.forceCenter())
.force("forceX", d3.forceX().strength(NODE_GRAVITY))
.force("forceY", d3.forceY().strength(NODE_GRAVITY))
.force("colide", d3.forceCollide(NODE_RADIUS * NODE_MARGIN))
.on("tick", ticked);
const link = svg
.selectAll("line")
.data(links)
.enter()
.append("line")
.style("stroke", "#aaa");
const node = svg
.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.call(
d3
.drag<SVGCircleElement, Node>()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
)
.attr("fill", (node) => node.color))
.attr("r", NODE_RADIUS)
.attr("stroke", "#000000")
.attr("stroke-width", 1);
function ticked() {
link
.attr("x1", (d) => d.source.x!)
.attr("y1", (d) => d.source.y!)
.attr("x2", (d) => d.target.x!)
.attr("y2", (d) => d.target.y!);
node.attr("cx", (d) => d.x!).attr("cy", (d) => d.y!);
}
function dragstarted(event: any, d: Node) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event: any, d: Node) {
console.log(simulation.alpha());
d.fx = event.x;
d.fy = event.y;
}
function dragended(event: any, d: Node) {
if (!event.active) simulation.alphaTarget(0.0001);
d.fx = null;
d.fy = null;
}
},
[props.nodes.length, props.links.length]
)
Any clue or help is appreciated.
Due to it working after removing the StrictMode I speculated that i need a cleanup function inside my useEffect. Turns out that i just have to remove the links and nodes that I have inserted like so.
return () => {
node.remove();
link.remove();
}
I would like to draw new treemap every time the data (data is generated based on the user interaction such as button clicks) is changed. At the moment every time the data is changed a new treemap is created but it is drawn on top of the previous treemap - meaning I can see both treemaps on the screen. Somehow the previous instance is not removed.
I have looked at this and this, and applied remove() function to remove the previous instance via d3. However, it does not work. I may be incorrectly used the remove function in the life cycle.
d3.select("treemap").remove(); is used in the second useEffect method.
export function Treemap({ width, height, data }) {
const ref = useRef();
useEffect(() => {
const svg = d3
.select(ref.current)
.attr("id", "treemap")
.attr("width", width)
.attr("height", height);
}, []);
useEffect(() => {
//THIS IS WHERE I USE REMOVE FUNCTION;
d3.select("treemap").remove();
draw();
}, [data]);
const draw = () => {
const svg = d3.select(ref.current);
// Give the data to this cluster layout:
var root = d3.hierarchy(data).sum(function (d) {
return d.value;
});
// initialize treemap
d3
.treemap()
.size([width, height])
.paddingTop(28)
.paddingLeft(0)
.paddingRight(0)
.paddingBottom(7)
.paddingInner(3)(root);
const color = d3
.scaleOrdinal()
.domain(["Diary", "Sweetner", "Fruit"])
.range(["#8FD175", "#402D54", "#E67E22"]);
const opacity = d3.scaleLinear().domain([10, 30]).range([0.5, 1]);
// Select the nodes
var nodes = svg.selectAll("rect").data(root.leaves());
// draw rectangles
nodes
.enter()
.append("rect")
.attr("x", function (d) {
return d.x0;
})
.attr("y", function (d) {
return d.y0;
})
.attr("width", function (d) {
return d.x1 - d.x0;
})
.attr("height", function (d) {
return d.y1 - d.y0;
})
.style("stroke", "black")
.style("fill", function (d) {
return color(d.parent.data.name);
})
.style("opacity", function (d) {
return opacity(d.data.value);
});
nodes.exit().remove();
// select node titles
var nodeText = svg.selectAll("text").data(root.leaves());
// add the text
nodeText
.enter()
.append("text")
.attr("x", function (d) {
return d.x0 + 5;
}) // +10 to adjust position (more right)
.attr("y", function (d) {
return d.y0 + 20;
}) // +20 to adjust position (lower)
.text(function (d) {
return d.data.name;
})
.attr("font-size", "19px")
.attr("fill", "white");
// select node titles
var nodeVals = svg.selectAll("vals").data(root.leaves());
// add the values
nodeVals
.enter()
.append("text")
.attr("x", function (d) {
return d.x0 + 5;
}) // +10 to adjust position (more right)
.attr("y", function (d) {
return d.y0 + 35;
}) // +20 to adjust position (lower)
.text(function (d) {
return d.data.value;
})
.attr("font-size", "11px")
.attr("fill", "white");
// add the parent node titles
svg
.selectAll("titles")
.data(
root.descendants().filter(function (d) {
return d.depth == 1;
})
)
.enter()
.append("text")
.attr("x", function (d) {
return d.x0;
})
.attr("y", function (d) {
return d.y0 + 21;
})
.text(function (d) {
return d.data.name;
})
.attr("font-size", "19px")
.attr("fill", function (d) {
return color(d.data.name);
});
};
return (
<div className="chart">
<svg ref={ref}></svg>
</div>
);
}
Normally, with d3, you never remove the already drawn nodes. It's much cheaper (computationally) and easier to just repurpose them!
If you really want to remove all nodes, d3.select("treemap") doesn't do anything, because "treemap" is not a valid selector. Try #treemap instead.
Or, if you want to repurpose the already drawn bits, consider the following:
function Treemap({
width,
height
}) {
const ref = React.useRef();
const [data, setData] = React.useState(`
{
"name": "Fruit",
"children": [
{ "name": "Apples", "value": 1 },
{ "name": "Oranges", "value": 1 },
{ "name": "Bananas", "value": 1 }
]
}
`);
React.useEffect(() => {
const svg = d3
.select(ref.current)
.attr("id", "treemap")
.attr("width", width)
.attr("height", height);
}, []);
React.useEffect(() => {
draw();
}, [data]);
const draw = () => {
const svg = d3.select(ref.current);
let parsedData
try {
parsedData = JSON.parse(data);
} catch (e) {
console.log(e);
return;
}
// Give the data to this cluster layout:
var root = d3.hierarchy(parsedData).sum(function(d) {
return d.value;
});
// initialize treemap
d3
.treemap()
.size([width, height])
.paddingTop(28)
.paddingLeft(0)
.paddingRight(0)
.paddingBottom(7)
.paddingInner(3)(root);
const color = d3
.scaleOrdinal()
.domain(["Diary", "Sweetner", "Fruit"])
.range(["#8FD175", "#402D54", "#E67E22"]);
const opacity = d3.scaleLinear().domain([10, 30]).range([0.5, 1]);
// Select the nodes
var nodes = svg.selectAll("rect").data(root.leaves());
// draw rectangles
var newNodes = nodes
.enter()
.append("rect")
.style("stroke", "black");
nodes.merge(newNodes)
.attr("x", function(d) {
return d.x0;
})
.attr("y", function(d) {
return d.y0;
})
.attr("width", function(d) {
return d.x1 - d.x0;
})
.attr("height", function(d) {
return d.y1 - d.y0;
})
.style("fill", function(d) {
return color(d.parent.data.name);
})
.style("opacity", function(d) {
return opacity(d.data.value);
});
nodes.exit().remove();
// select node titles
var nodeVals = svg.selectAll(".val").data(root.leaves());
nodeVals.exit().remove();
// add the values
var newNodeVals = nodeVals
.enter()
.append("text")
.classed("val", true)
.attr("font-size", "11px")
.attr("fill", "white");
nodeVals.merge(newNodeVals)
.attr("x", function(d) {
return d.x0 + 5;
}) // +10 to adjust position (more right)
.attr("y", function(d) {
return d.y0 + 35;
}) // +20 to adjust position (lower)
.text(function(d) {
return d.data.value;
});
// add the parent node titles
var titles = svg
.selectAll(".title")
.data(
root.descendants().filter(function(d) {
return d.depth == 1;
})
);
titles.exit().remove();
var newTitles = titles
.enter()
.append("text")
.classed("title", true)
.attr("font-size", "19px");
titles.merge(newTitles)
.attr("x", function(d) {
return d.x0 + 5;
})
.attr("y", function(d) {
return d.y0 + 21;
})
.text(function(d) {
return d.data.name;
})
.attr("fill", function(d) {
return color(d.data.name);
});
};
return ( <
div className = "chart" >
<
textarea onChange = {
(el) => setData(el.target.value)
}
value = {
data
}
rows = "20"
cols = "50" / >
<
svg ref = {
ref
} > < /svg> <
/div>
);
}
ReactDOM.render( <
Treemap width = "600"
height = "300" / > ,
document.getElementById('root')
)
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.js"></script>
<div id="root"></div>
In any case, all your selectors were missing the .-class prefix. I'd recommend to read up on selectors, and on the enter(), exit(), merge() lifecycle.
I am trying to build a hierarchical tree visualization using React and D3.
My component receives the hierarchical data in a CSV format as props, which I pass through the stratify function of D3 to obtain a root node to my tree. The root node is set as state.
Currently, I am using a single useEffect(()=>{},[root]) to build the tree, which re-renders on any changes to root.
It is messy as all the action happens inside this useEffect(). I want to know how can I decouple the update() method and use it separately.
As I am a beginner is both React and D3, I welcome any other suggestion on how to handle the state , how to make it more declarative et al.
Here is the code:
useEffect(() => {
if (root) {
//Declare a tree layout
//nodeSize ensure each node has it's own space and does not overlap
const tree = d3
.tree()
.nodeSize([
attributes.nodeWidth,
attributes.nodeHeight + attributes.veritcalNodeGap,
]);
root.x0 = 0;
root.y0 = attributes.width / 2;
//Set children of nodes deeper than 2 to null;
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
if (d.depth && d.data.child.length !== 7) d.children = null;
});
// append the svg object to the body of the page
// and define zoom behaviours
const svg = d3
.select(d3Ref.current)
.call(
d3
.zoom()
.scaleExtent([0.05, 3])
.on("zoom", () => svg.attr("transform", d3.event.transform))
)
.on("dblclick.zoom", null)
.append("svg")
.attr("viewBox", [0, 0, attributes.width, attributes.height])
.append("g")
.attr("transform", (d) => `translate(${attributes.width / 2},120)`);
//Group all links together
const gLink = svg
.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 1)
.attr("stroke-width", 1.5);
// .attr("x", "200 ");
//Group all nodes together
const gNode = svg
.append("g")
.attr("cursor", "pointer")
.attr("pointer-events", "all");
const diagonal = linkVertical()
.x((d) => d.x)
.y((d) => d.y);
update();
function update() {
const nodes = root.descendants().reverse();
const links = root.links();
tree(root);
//Define group and join the data
const node = gNode.selectAll("g").data(nodes, (d) => d.id);
let nodeEnter = node
.enter()
.append("g")
.attr("class", "node")
.attr("transform", (d) => `translate(${root.x0},${root.y0})`)
.on("click", (d) => {
d.children = d.children ? null : d._children;
update();
});
let nodeGroup = nodeEnter.append("g").attr("class", "node-group");
nodeEnter
.append("circle")
.attr("r", 7)
.attr("cursor", (d) => (d._children ? "pointer" : "none"))
.attr("fill", (d) => (d._children ? "lightsteelblue" : "#999"))
.attr("stroke", (d) => (d._children ? "steelblue" : "#999"))
.attr("stroke-width", 2);
//add text
nodeEnter
.append("text")
.attr("dy", ".35em")
.attr("x", 25)
.text((d) => d.data.child);
//Transition nodes to their new positions
const nodeUpdate = node //SVG.data()
.merge(nodeEnter)
.transition()
.duration(attributes.duration)
.attr("transform", (d) => `translate(${d.x},${d.y})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
//Transition exiting nodes to the parent's new position
const nodeExit = node
.exit()
.transition()
.duration(attributes.duration)
.remove()
.attr("transform", (d) => `translate(${root.x},${root.y})`);
// // Update the links…
const link = gLink.selectAll("path").data(links, (d) => d.target.id);
// // Enter any new links at the parent's previous position.
const linkEnter = link
.enter()
.append("path")
.attr("class", "link")
.attr("d", (d) => {
const o = { x: root.x0, y: root.y0 };
return diagonal({ source: o, target: o });
});
// //Transition links to their new position
link
.merge(linkEnter)
.transition()
.duration(attributes.duration)
.attr("d", diagonal);
// // Transition exiting nodes to the parent's new position.
link
.exit()
.transition()
.duration(attributes.duration)
.remove()
.attr("d", (d) => {
const o = { x: root.x, y: root.y };
return diagonal({ source: o, target: o });
});
root.eachBefore((d) => {
d.x0 = d.x;
d.y0 = d.y;
});
}
}
}, [root]);
Here is one idea.
You can have the initial effect onMount once, then updates only when root change,
You can use two useEffect on your component.
// hook on onMount, you prepare root, tree etc
let tree;
useEffect(() => {
if (root) {
//Declare a tree layout
//nodeSize ensure each node has it's own space and does not overlap
tree = d3
.tree()
.nodeSize([
attributes.nodeWidth,
attributes.nodeHeight + attributes.veritcalNodeGap,
]);
root.x0 = 0;
root.y0 = attributes.width / 2;
//Set children of nodes deeper than 2 to null;
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
if (d.depth && d.data.child.length !== 7) d.children = null;
});
// append the svg object to the body of the page
// and define zoom behaviours
const svg = d3
.select(d3Ref.current)
.call(
d3
.zoom()
.scaleExtent([0.05, 3])
.on('zoom', () => svg.attr('transform', d3.event.transform))
)
.on('dblclick.zoom', null)
.append('svg')
.attr('viewBox', [0, 0, attributes.width, attributes.height])
.append('g')
.attr('transform', (d) => `translate(${attributes.width / 2},120)`);
//Group all links together
const gLink = svg
.append('g')
.attr('fill', 'none')
.attr('stroke', '#555')
.attr('stroke-opacity', 1)
.attr('stroke-width', 1.5);
// .attr("x", "200 ");
//Group all nodes together
const gNode = svg
.append('g')
.attr('cursor', 'pointer')
.attr('pointer-events', 'all');
const diagonal = linkVertical()
.x((d) => d.x)
.y((d) => d.y);
}
}, []);
// here is the update only when [root] changes
useEffect(() => {
const nodes = root.descendants().reverse();
const links = root.links();
tree(root);
//Define group and join the data
const node = gNode.selectAll('g').data(nodes, (d) => d.id);
let nodeEnter = node
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', (d) => `translate(${root.x0},${root.y0})`)
.on('click', (d) => {
d.children = d.children ? null : d._children;
update();
});
let nodeGroup = nodeEnter.append('g').attr('class', 'node-group');
nodeEnter
.append('circle')
.attr('r', 7)
.attr('cursor', (d) => (d._children ? 'pointer' : 'none'))
.attr('fill', (d) => (d._children ? 'lightsteelblue' : '#999'))
.attr('stroke', (d) => (d._children ? 'steelblue' : '#999'))
.attr('stroke-width', 2);
//add text
nodeEnter
.append('text')
.attr('dy', '.35em')
.attr('x', 25)
.text((d) => d.data.child);
//Transition nodes to their new positions
const nodeUpdate = node //SVG.data()
.merge(nodeEnter)
.transition()
.duration(attributes.duration)
.attr('transform', (d) => `translate(${d.x},${d.y})`)
.attr('fill-opacity', 1)
.attr('stroke-opacity', 1);
//Transition exiting nodes to the parent's new position
const nodeExit = node
.exit()
.transition()
.duration(attributes.duration)
.remove()
.attr('transform', (d) => `translate(${root.x},${root.y})`);
// // Update the links…
const link = gLink.selectAll('path').data(links, (d) => d.target.id);
// // Enter any new links at the parent's previous position.
const linkEnter = link
.enter()
.append('path')
.attr('class', 'link')
.attr('d', (d) => {
const o = { x: root.x0, y: root.y0 };
return diagonal({ source: o, target: o });
});
// //Transition links to their new position
link
.merge(linkEnter)
.transition()
.duration(attributes.duration)
.attr('d', diagonal);
// // Transition exiting nodes to the parent's new position.
link
.exit()
.transition()
.duration(attributes.duration)
.remove()
.attr('d', (d) => {
const o = { x: root.x, y: root.y };
return diagonal({ source: o, target: o });
});
root.eachBefore((d) => {
d.x0 = d.x;
d.y0 = d.y;
});
}, [root]);
I'm trying to make Force-Directed Tree with React and it works. But I cannot modify "link strength", if I pass it outside component through the props.
Honestly, I can change "strength", but I need to append d3 svg to my react ref div after that to see the changes. And whole graph will be redrawn.
I find example by Mike Bostock. He advice to modify the parameters of a force-directed graph with reheat the simulation using simulation.alpha and simulation.restart. But I cannot make it works with react. Nothing happens.
Here is my code:
export default function Hierarchy(props) {
const {
strength,
lineColor,
lineStroke,
width,
height,
nodeSize,
nodeColor,
} = props;
const root = d3.hierarchy(data);
const links = root.links();
const nodes = root.descendants();
const svg = d3.create("svg");
const link = svg
.append("g")
.selectAll("line")
.data(links)
.join("line");
const node = svg
.append("g")
.selectAll("circle")
.data(nodes)
.join("circle");
function applyStyle(selectionSVG) {
selectionSVG
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height]);
selectionSVG
.selectAll("circle")
.attr("r", nodeSize)
.attr("fill", nodeColor)
selectionSVG
.selectAll("line")
.attr("stroke", lineColor)
.attr("stroke-width", lineStroke);
}
applyStyle(svg);
const divRef = React.useRef(null);
const linkForce = d3
.forceLink(links)
.id(d => d.id)
.distance(0)
.strength(strength);
const simulation = d3
.forceSimulation(nodes)
.force("link", linkForce)
.force("charge", d3.forceManyBody().strength(-500))
.force("x", d3.forceX())
.force("y", d3.forceY());
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("cx", d => d.x).attr("cy", d => d.y);
});
//ComponentDidMount
useEffect(() => {
//Append d3 svg to ref div
var div = d3.select(divRef.current);
if (div.node().firstChild) {
div.node().removeChild(div.node().firstChild);
}
div.node().appendChild(svg.node());
}, []);
//ComponentDidUpdate
useEffect(() => {
simulation.force("link").strength(strength);
simulation.alpha(1).restart();
}, [strength]);
//ComponentDidUpdate
useEffect(() => {
var div = d3.select(divRef.current);
applyStyle(div.select("svg"));
});
//Render
return <div id="hierarchyTree" ref={divRef} />;
}
Here is Sandbox.
I find solution, if anybody interesting.
The fact is simulation was not saved when component was updated. So I create ref for it.
const simulationRef = React.useRef(simulation)
and replace it in useEffect section
//ComponentDidUpdate
useEffect(() => {
simulationRef.current.force("link").strength(strength)
simulationRef.current.alpha(1).restart()
console.log(simulationRef.current)
}, [strength])
After that everything works fine.
I am trying to pass data from react class base component to a vanillajs class so this class is able to render D3 bar chart ,
I've tried passing the data from the react component through the contractor of the vanilla class , i have the data available in the vanilla class when i try to consol log it , but when i want to call the data variable in the method call d3.data() it is empty , here is the code
React class
//imports..
const _data = []
const firebaseConfig = {
//configuration ..
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore()
class TableOfD3 extends Component {
constructor(){
super()
this.svgId = `SVG_${uuid()}`
}
getData(){
db.collection('db').get().then( res=>{
res.docs.forEach(doc => {
_data.push(doc.data())
})
}
componentDidMount(){
this.start()
}
componentDidUpdate(){
this.start()
}
start(){
this._graph = new D3TableEngine('#' + this.svgId,_data)
this._graph.start()
}
render() {
return (
<div>
<svg id={this.svgId}></svg>
</div>
);
}
}
export default TableOfD3;
// vanillajs class
export default class D3TableEngine {
constructor(svgId, passedData) {
this._svg = d3.select(`${svgId}`);
this._svg.attr('width', _WIDTH)
this._svg.attr('height', _HEIGHT)
this._passedData = passedData
}
start() {
const self = this;
var _g = self._svg;
const graphWidth = _WIDTH - _MARGIN.left - _MARGIN.right
const graphHeight = _HEIGHT - _MARGIN.top - _MARGIN.bottom
const graph = _g.append('g')
.attr('width', graphWidth)
.attr('height', graphHeight)
.attr('transform', `translate(${_MARGIN.left + 20}, ${_MARGIN.top})`)
const xAxisGroup = graph.append('g')
.attr('transform', `translate(0,${graphHeight })`)
const yAxisGroup = graph.append('g')
const yScale = d3.scaleLinear()
.domain([0,d3.max(self._passedData, (d) => d.orders)])
.range([graphHeight,0])
const xScale = d3.scaleBand()
.domain(self._passedData.map((el) => el.name))
.range([0,500])
.paddingInner(0.2)
.paddingOuter(0.2)
const rects = graph.selectAll("rect").data(self._passedData);
rects
.attr("x", (d)=> xScale(d.name))
.attr("y", (d) => yScale( d.orders))
.attr("height", (d)=> graphHeight - yScale( d.orders))
.attr("width", xScale.bandwidth)
.attr('fill', 'blue')
rects
.enter()
.append("rect")
.attr("x", (d)=> xScale(d.name))
.attr("y", (d) => yScale( d.orders))
.attr("height", (d)=> graphHeight - yScale( d.orders ))
.attr("width", xScale.bandwidth)
.attr('fill', 'blue')
const xAxis = d3.axisBottom(xScale)
xAxisGroup.call(xAxis)
const yAxis = d3.axisLeft(yScale)
.ticks(5)
.tickFormat((d) => 'Orders ' +d )
yAxisGroup.call(yAxis)
xAxisGroup.selectAll('text')
.attr('transform', 'rotate(-40)' )
.attr('text-anchor', 'end')
} )
}
refresh() {}
}
I re-wrote your React class because you were doing many things that would be considered anti-pattern. In general, you want to shove as much as you can in this.state. Otherwise, you miss out on the main advantage of React - and that is optimally re-rendering the DOM when variables change. I think the main issue you're likely having is that you're updating the DOM from componentDidUpdate(), which will fire another update. It'll continue infinitely and crash. I would strongly recommend refactoring D3TableEngine into a React Component instead of a plain JS class. The challenge is that the way you have written the d3 component, it has to be destroyed and re-created for each render, which is a problem because React doesn't know what to do other than re-create it.
import React, { Component } from 'react';
class TableOfD3 extends Component {
constructor() {
super();
const firebaseConfig = {
//configuration ..
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();
this.state = {
svgId: `SVG_${uuid()}`,
data: [],
db: db
};
}
componentDidMount() {
const response = await this.state.db.collection('db').get();
const data = response.docs.map(doc => doc.data());
this.setState({
data
});
}
componentDidUpdate() {
}
render() {
return (
<div>
<D3TableEngine
id={this.state.svgId}
data={this.state.data}
/>
</div>
);
}
}
UPDATE: I gave a shot at refactoring your d3 class into a React Component. The important pieces here are the ref, which let's you get a reference to the element so redraw can execute all the d3 code on the right svg element. Then, inside componentDidMount and componentDidUpdate, you must call redraw. However, I would refactor the redraw method to break out the parts that will change from the parts that will not change (eg: move the graph pieces into a different function and call that in componentDidUpdate). We do this so that React is performing as expected and only updating the elements in the DOM that have changed. If you need additional help, you may take a look at this jsfiddle example/medium article.
const MARGIN = 0;
const WIDTH = 0;
const HEIGHT = 0;
class D3TableEngine extends Component {
componentDidMount() {
redraw();
}
componentDidUpdate() {
redraw();
}
redraw = () => {
this.svg = d3.select(this.svg);
const graphWidth = WIDTH - MARGIN.left - MARGIN.right
const graphHeight = HEIGHT - MARGIN.top - MARGIN.bottom
const graph = this.svg.append('g')
.attr('width', graphWidth)
.attr('height', graphHeight)
.attr('transform', `translate(${_MARGIN.left + 20}, ${_MARGIN.top})`)
const xAxisGroup = graph.append('g')
.attr('transform', `translate(0,${graphHeight})`)
const yAxisGroup = graph.append('g')
const yScale = d3.scaleLinear()
.domain([0, d3.max(props.data, (d) => d.orders)])
.range([graphHeight, 0])
const xScale = d3.scaleBand()
.domain(props.data.map((el) => el.name))
.range([0, 500])
.paddingInner(0.2)
.paddingOuter(0.2)
const rects = graph.selectAll("rect").data(props.data);
rects
.attr("x", (d) => xScale(d.name))
.attr("y", (d) => yScale(d.orders))
.attr("height", (d) => graphHeight - yScale(d.orders))
.attr("width", xScale.bandwidth)
.attr('fill', 'blue')
rects
.enter()
.append("rect")
.attr("x", (d) => xScale(d.name))
.attr("y", (d) => yScale(d.orders))
.attr("height", (d) => graphHeight - yScale(d.orders))
.attr("width", xScale.bandwidth)
.attr('fill', 'blue')
const xAxis = d3.axisBottom(xScale)
xAxisGroup.call(xAxis)
const yAxis = d3.axisLeft(yScale)
.ticks(5)
.tickFormat((d) => 'Orders ' + d)
yAxisGroup.call(yAxis)
xAxisGroup.selectAll('text')
.attr('transform', 'rotate(-40)')
.attr('text-anchor', 'end')
}
render() {
return (
<svg
id={this.props.svgId}
width={WIDTH}
height={HEIGHT}
ref={el => (this.svg = d3.select(el))}
>
</svg>
);
}
}