I'm trying to zoom my timeline by x axis,
but it doesn't show any reaction on zooming, I've looked through many tutorials, tried separate zoom functions, eventually here what I've got (with this https://coderwall.com/p/psogia/simplest-way-to-add-zoom-pan-on-d3-js manual):
const { dataset } = this.props;
var minDt = 0;
var maxDt = 300;
var intervalMinWidth = 1;
var elementWidth = 1200; // TODO: make adoptable
var elementHeight = 200;
var width = 500;
var height = 600;
var groupHeight = 20;
var xTickSize = 5;
var x = d3.scaleLinear()
.domain([minDt, maxDt])
.range([0, width]);
var xAxis = d3.axisBottom(x)
.tickSize(xTickSize);
var svg = d3.select(svgRoot)
.append('svg')
.attr('id', 'chart')
.attr('class', 'timeline-chart')
.attr("width", "100%")
.attr("height", "100%")
.call(d3.zoom().on("zoom", function () {
svg.attr("transform", d3.event.transform)
}))
.append('g');
var gX = svg.append("g")
.attr("class", "axis axis--x")
.call(xAxis);
var groupExonItems = svg
.selectAll('.group-interval-item')
.data(dataset)
.enter()
.append('g')
.attr('clip-path', 'url(#chart-content)')
.attr('class', 'item')
.attr('transform', function (d, i) {
return 'translate(0, ' + groupHeight * i + ')';
})
.selectAll('.rect').data(function (d) {
return d.data.filter(function (_) {
return _.type === 'exon';
});
}).enter();
var exonBarHeight = 0.8 * groupHeight;
var exonBarMargin = 30;
var exons = groupExonItems
.append('rect')
.attr('class', 'interval')
.attr('width', function (d) {
return Math.max(intervalMinWidth, x(d.to) - x(d.from));
})
.attr('height', exonBarHeight)
.attr('y', exonBarMargin)
.attr('x', function (d) {
return x(d.from);
});
I have tried separate function, not any progress as well :
function zoomFunction(){
var new_xScale = d3.event.transform.rescaleX(x)
gX.call(xAxis.scale(new_xScale));
svg.selectAll('rect.interval').attr('x', function (d) {
return x(d.from);
}).attr('width', function (d) {
return Math.max(intervalMinWidth, x(d.to) - x(d.from));
});
};
If I log zoom function - I can see it's reachable, and scale changes, but not for my rectangles.
May be the reason is I'm using react + faux to draw svg, I have tried without faux - by putting D3 code into the componentDidMount function - still no any progress.
What am I missing? Appreciate any help.
Even react-faux-dom is the simplest way to render D3, it breaks dynamic changes of the object, such as zoom. And without faux here is a very simple example to implement zoom: https://swizec.com/blog/two-ways-build-zoomable-dataviz-component-d3-zoom-react/swizec/7753
As far as I need only x-axis zoom, here is solution:
import { Component} from 'react'
import { PropTypes} from 'prop-types'
import React from 'react'
import ReactDOM from 'react-dom';
import ReactFauxDOM from 'react-faux-dom'
import * as d3 from 'd3'
import styles from './chart.css';
const random = d3.randomNormal(2, 1);
class Scatterplot extends React.Component {
constructor(props) {
super(props);
this.updateD3(props);
}
componentWillUpdate(nextProps) {
this.updateD3(nextProps);
}
updateD3(props) {
const { data, width, height, zoomTransform } = props;
this.xScale = d3.scaleLinear()
.domain([0, d3.max(data, ([x, y]) => x)])
.range([0, width]);
}
get transform() {
const { x, y, zoomTransform } = this.props;
let transform = "";
if (zoomTransform) {
transform = `translate(${x + zoomTransform.x}, ${y}) scale(${zoomTransform.k} 1)`;
}
return transform;
}
render() {
const { data } = this.props;
return (
<g transform={this.transform} ref="scatterplot">
{data.map(([x, y]) => <rect x={this.xScale(x)} y={y} width={this.xScale(4)} height = {5} />)}
</g>
)
}
}
class Chart extends React.Component {
constructor(props) {
super(props);
this.state = {
data: d3.range(5).map(_ => [random(), random()]),
zoomTransform: null
}
this.zoom = d3.zoom()
.scaleExtent([-5, 5])
.translateExtent([[-100, -100], [props.width+100, props.height]])
.extent([[-100, -100], [props.width+100, props.height]])
.on("zoom", this.zoomed.bind(this))
}
componentDidMount() {
d3.select(this.refs.svg)
.call(this.zoom)
}
componentDidUpdate() {
d3.select(this.refs.svg)
.call(this.zoom)
}
zoomed() {
this.setState({
zoomTransform: d3.event.transform
});
}
render() {
const { zoomTransform } = this.state,
{ width, height } = this.props;
return (
<svg width={width} height={height} ref="svg">
<Scatterplot data={this.state.data}
x={0} y={0}
width={width/2}
height={height}
zoomTransform={zoomTransform}/>
</svg>
)
}
}
export default Chart
Related
I am currently learning reactjs and d3 and working on how to visualize the dfs algorithm using network graph. After some iterations of updating the states, the browser hangs and page becomes unresponsive.
I think that I may not be clearing the svg before updating but not getting exactly how to do it.
Below is the renderer code which call the ForceGraph function
import { useEffect, useState } from "react";
import { ForceGraph } from "../forceGraph/forceGraph";
import { Container, Row, Button } from "react-bootstrap";
import "./renderer.css";
import data_dummy from "../../data/dummyGraph.json";
import OptionsButton from "../../components/buttons/optionsButton";
import DFS from "../../algorithms/graphTraversal/dfs";
function GraphOptionTab({
handleStartVisualization,
handleGenerateRandomGraph,
}) {
return (
<Container className="graph_option_container">
<Row className="graph_option_container_row">
<OptionsButton onClick={handleGenerateRandomGraph}>
Random graph
</OptionsButton>
<OptionsButton onClick={handleStartVisualization}>
Start visualization
</OptionsButton>
</Row>
</Container>
);
}
function generateRandomGraph() {
var numberOfNodes = Math.floor(Math.random() * 30);
var nodes = [];
var links = [];
var currentNode = Math.floor(Math.random() * numberOfNodes);
for (var i = 0; i < numberOfNodes; i++) {
var node = {};
node.id = i;
nodes.push({ ...node });
var links_for_each_node = Math.floor(Math.random() * 5);
for (var j = 0; j < links_for_each_node; j++) {
var link = {};
var source = i;
var target = i;
while (source === target) {
target = Math.floor(Math.random() * numberOfNodes);
}
link.source = source;
link.target = target;
link.weight = Math.floor(Math.random() * numberOfNodes);
links.push({ ...link });
}
}
var data = {};
data.nodes = [...nodes];
data.links = [...links];
return data;
}
export default function Renderer() {
const [renderer, setRenderer] = useState("graph");
const [data, setData] = useState(generateRandomGraph());
const [c, setC] = useState(0);
const [isRunning, setIsRunning] = useState(false);
function runAnimation() {
if (data === null) return;
var seq = DFS(data);
console.log(seq);
seq.forEach((a, i) => {
setTimeout(() => {
setData({ ...a });
}, i * 1000 * 2);
});
}
return renderer === "graph" ? (
<Container className="graph_container">
<Row className="graph_container_row1">
<GraphOptionTab
handleGenerateRandomGraph={() => {
setData({ ...generateRandomGraph() });
}}
handleStartVisualization={() => {
if (isRunning) {
runAnimation();
}
setIsRunning(!isRunning);
}}
></GraphOptionTab>
</Row>
<Row className="graph_container_row2">
<ForceGraph data={data}></ForceGraph>
</Row>
</Container>
) : null;
}
ForceGraph
import React from "react";
import { runForceGraph } from "./forceGraphGenerator";
import styles from "./forceGraph.module.css";
export function ForceGraph({ data }) {
const containerRef = React.useRef(null);
React.useEffect(() => {
if (data != null) {
if (containerRef.current) {
const { svg, simulation } = runForceGraph(
containerRef.current,
data.links,
data.nodes,
data.vis,
data.currentNode
);
return function cleanup(destroyFn) {
console.log("Cleanup called");
console.log(simulation);
console.log(svg);
simulation.stop();
svg.selectAll("*").remove();
svg.remove();
};
}
}
});
return <div ref={containerRef} className={styles.container} />;
}
ForceGraphGenerator
import * as d3 from "d3";
import "#fortawesome/fontawesome-free/css/all.min.css";
import styles from "./forceGraph.module.css";
export function runForceGraph(
container,
linksData,
nodesData,
vis,
currentNode
) {
const links = linksData.map((d) => Object.assign({}, d));
const nodes = nodesData.map((d) => Object.assign({}, d));
console.log(`hello current node = ${currentNode}`);
const containerRect = container.getBoundingClientRect();
const height = containerRect.height;
const width = containerRect.width;
const color = () => {
return "#29FF29";
};
const icon = (d) => {
return d.gender === "male" ? "\uf222" : "\uf221";
};
const getClass = (d) => {
return d.gender === "male" ? styles.male : styles.female;
};
const drag = (simulation) => {
const dragstarted = (event, d) => {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
};
const dragged = (event, d) => {
d.fx = event.x;
d.fy = event.y;
};
const dragended = (event, d) => {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
};
return d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
};
const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(links)
.id((d) => {
return d.id;
})
.distance(200)
.strength(1)
)
.force("charge", d3.forceManyBody().strength(-1000))
.force("x", d3.forceX())
.force("y", d3.forceY());
simulation.tick(300);
const svg = d3
.select(container)
.append("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height]);
const link = svg
.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 1)
.attr("stroke-width", 2)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", (d) => Math.sqrt(d.value));
const node = svg
.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 24)
.attr("fill", (d) => {
if (currentNode === d.id) return "#ff3b76";
if (vis && vis[d.id]) return "#fff";
return color();
})
.call(drag(simulation));
const edge_node = svg
.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.selectAll("circle")
.data(links)
.join("circle")
.attr("r", 8)
.attr("fill", "#fff")
.call(drag(simulation));
const label = svg
.append("g")
.attr("class", "labels")
.selectAll("text")
.data(nodes)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("class", styles.node_text)
.attr("dominant-baseline", "central")
.text((d) => {
return d.id;
})
.call(drag(simulation));
const edge_label = svg
.append("g")
.attr("class", "labels")
.selectAll("text")
.data(links)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("class", styles.edge_label)
.attr("dominant-baseline", "central")
.text((d) => {
return d.weight;
})
.call(drag(simulation));
simulation.on("tick", () => {
//update link positions
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);
// update node positions
node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
// update label positions
label
.attr("x", (d) => {
return d.x;
})
.attr("y", (d) => {
return d.y;
});
edge_node
.attr("cx", (d) => Math.floor((d.source.x + d.target.x) / 2))
.attr("cy", (d) => Math.floor((d.source.y + d.target.y) / 2));
edge_label
.attr("x", (d) => {
return Math.floor((d.source.x + d.target.x) / 2);
})
.attr("y", (d) => {
return Math.floor((d.source.y + d.target.y) / 2);
});
});
return {
simulation,
svg,
};
}
DFS
vis[v] = true;
var state = {};
state.vis = [...vis];
state.currentNode = v;
state.nodes = data.nodes;
state.links = data.links;
seq.push({ ...state });
adjList[v].forEach((i) => {
if (!vis[i]) {
dfsWrapper(data, adjList, vis, i, seq);
}
});
}
export default function DFS(data) {
var seq = [];
var numberOfNodes = data.nodes.length;
var vis = new Array(numberOfNodes).fill(false);
var adjList = new Array(numberOfNodes);
for (var i = 0; i < adjList.length; i++) {
adjList[i] = new Array();
}
console.log(data.links);
for (var i = 0; i < data.links.length; i++) {
// console.log(data.links[i]["source"]);
adjList[data.links[i]["source"]].push(data.links[i]["target"]);
// console.log(adjList[data.links[i]["source"]]);
}
console.log(adjList);
for (var i = 0; i < numberOfNodes; i++) {
if (!vis[i]) {
dfsWrapper(data, adjList, vis, data.nodes[i].id, seq);
}
}
var state = {};
state.vis = [...vis];
state.currentNode = numberOfNodes + 1;
state.nodes = data.nodes;
state.links = data.links;
seq.push({ ...state });
return seq;
}
Hope that my question is clear and it would be very kind if anyone suggest what is going wrong.
Try to split runForceGraph into 2 functions: createForceGraph and updateForceGraph
Call createForceGraph once when you mount your component:
export function createForceGraph(...) {
...
const svg = d3
.select(container)
.append("svg")
...
svg
.append("g")
.attr("class", "labels")
...
}
Call updateForceGraph each time the data is changed:
export function updateForceGraph(...) {
...
const edge_label = d3
.select('.labels')
.selectAll("text")
.data(links)
.enter()
...
I am trying to view the names of the cities on a d3.js v5 map
I tried some solutions so I am able to "just simply view the names of the cities " without the need of tooltip but I'm getting the following error in React "You can't update unmounted component".
Below my current code, which is in a class based component, I tried to use hooks to no avail, all this concepts are new to me.
// ... imports
class MapPathHolder extends Component {
constructor(props) {
super(props);
this.state = {
algyData: null,
toCity: "Algiers",
};
}
navigateToCity = () => {
this.props.history.push("/ToCityProducts");
};
componentWillMount() {
d3.json("algy.json").then((algyData) => {
this.setState({
algyData,
});
});
}
componentDidUpdate() {
const { width, height } = this.props;
const dz = this.state.algyData;
const svg = d3.select(this.refs.anchor);
var projection = d3
.geoEquirectangular()
// .parallels([29.5, 45.5])
.scale(1900.347057471)
.center([3, 40])
.translate([width / 1.4, height / 9]);
const path = d3.geoPath().projection(projection);
const g = svg.append("g");
g.append("g")
.attr("fill", "#4b4b4b")
.attr("cursor", "pointer")
.selectAll("path")
.data(
topojson.feature(dz, dz.objects.collection).features
)
.join("path")
.attr("stroke", "#A8846A")
.attr("stroke-width", 1)
.attr("d", path)
.on("click", (d) => {
this.props.postCity(d.properties.name);
this.navigateToCity();
})
.on("mouseover", function (d) {
d3.select(this).attr("fill", "orange");
})
.on("mouseout", function (d) {
d3.select(this).attr("fill", "#4b4b4b");
})
.append("title")
.text((d) => d.properties.name);
g.append("path")
.attr("fill", "none")
.attr("stroke-linejoin", "round")
.attr(
"d",
path(topojson.mesh(dz, dz.objects.collection, (a, b) => a !== b))
);
// error occurs after adding this line
g.append("title").text((d) => d.properties.name);
}
render() {
const { algyData, toCity } = this.state;
if (!algyData) {
return null;
}
return <g ref="anchor" />;
}
}
// ... mapState/DispatchToProps()
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(MapPathHolder));
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>
);
}
}
I'm new to react and d3. I'm trying our barChart but only see one overlapped rect. Examining each rect element, I see that x, y, height and width are expected. But I don't understand why the other 3 rect are not shown.
BarChart.js
import React, { Component } from 'react'
import { scaleLinear } from 'd3-scale';
import { max } from 'd3-array';
import { select } from 'd3-selection';
export default class BarChart extends Component {
constructor(props) {
super(props)
}
componentDidMount() {
this.createBarChart()
}
componentDidUpdate() {
this.createBarChart()
}
createBarChart = () => {
const node = this.node
const dataMax = max(this.props.data)
const yScale = scaleLinear()
.domain([0, dataMax])
.range([0, this.props.size[1]])
select(node)
.selectAll('rect')
.data(this.props.data)
.enter() // placeholder selection
.append('rect') // return a selection of appended rects
select(node)
.selectAll('rect') // rect selection
.data(this.props.data) // update data in rect selection
.exit()
.remove() // exit and remove rects
select(node)
.selectAll('rect') // rect selection
.data(this.props.data) // join data with rect selection
.style('fill', '#fe9922')
.attr('x', (d, i) => i * 25)
.attr('y', d => this.props.size[1] - yScale(d))
.attr('height', d => yScale(d))
.attr('width', 25)
}
render() {
return (
// Pass a reference to the node for D3 to use
<svg ref={node => this.node = node}
width={this.props.width} height={this.props.height}
>
</svg>
)
}
}
With this answer, I've updated createBarChart() but still seeing the same odd rendering.
createBarChart = () => {
const node = this.node
const dataMax = max(this.props.data)
const yScale = scaleLinear()
.domain([0, dataMax])
.range([0, this.props.size[1]])
this.rects = select(node)
.selectAll('rect')
.data(this.props.data)
this.rects
.exit()
.remove()
this.rects = this.rects.enter()
.append('rect')
.merge(this.rects)
.style('fill', '#fe9922')
.attr('x', (d, i) => i * 25)
.attr('y', d => this.props.size[1] - yScale(d))
.attr('height', d => yScale(d))
.attr('width', 25)
}
App.js
<div>
<BarChart data={[50,10,11,13]} size={[500,500]}/>
</div>
Found the bug, I passed in wrong props, this.props.width and this.props.height doesn't exist in this case. As height of my rect overflows the svg box, I can only see one longest bar, but not the other shorter ones.
<svg ref={node => this.node = node}
width={this.props.size[0]} height={this.props.size[1]}
>
</svg>
I am trying to render a simple bar chart with data fetched from an API. After the first chart is created, when props with data change the component rerenders, but the old chart is not disappearing.
I believe this has something to do with how d3.js and react differ in dom handling, but my knowledge of d3 is very limited. Is there anything I can do to make the old svg disappear and rerender after props change?
Below is the component code.
class BarChart extends React.Component {
constructor(props) {
super(props);
this.state = {
fetchedData: [],
};
this.createBarChart = this.createBarChart.bind(this);
this.fetchRequiredData = this.fetchRequiredData.bind(this);
}
fetchRequiredData() {
//fetches data and assigns it to components state, then calls createBarChart() in callback
}
componentDidMount() {
this.fetchRequiredData();
}
componentDidUpdate(prevProps) {
if (this.props !== prevProps) {
this.fetchRequiredData();
}
}
createBarChart() {
const node = this.node;
const width = this.props.size[0];
const height = this.props.size[1];
const chart = select(node).append('g')
.attr('transform', `translate(${30}, ${10})`);
const xScale = scaleBand()
.range([0, width])
.domain(this.state.fetchedData.map((s) => s.analyte))
.padding(0.2);
const yScale = scaleLinear()
.range([height, 0])
.domain([0, 100]);
const makeYLines = () => axisLeft()
.scale(yScale);
chart.append('g')
.attr('transform', `translate(0, ${height})`)
.call(axisBottom(xScale));
chart.append('g')
.call(axisLeft(yScale));
chart.append('g')
.attr('class', 'grid')
.call(makeYLines()
.tickSize(-width, 0, 0)
.tickFormat('')
);
const barGroups = chart.selectAll()
.data(this.state.fetchedData)
.enter()
.append('g');
barGroups
.append('rect')
.attr('class', 'bar')
.attr('x', (g) => xScale(g.analyte))
.attr('y', (g) => yScale(g.value))
.attr('height', (g) => height - yScale(g.value))
.attr('width', xScale.bandwidth());
barGroups
.append('text')
.attr('class', 'value')
.attr('x', (a) => xScale(a.analyte) + xScale.bandwidth() / 2)
.attr('y', (a) => yScale(a.value) + 30)
.attr('text-anchor', 'middle')
.text((a) => `${a.value}%`);
}
render() {
return (
<div>
<svg ref={node => this.node = node}
height={550} width={600}>
</svg>
</div>
)
}
I think this.state.fetchedData instead this.props
componentDidUpdate(prevProps) {
if (this.state.fetchedData !== prevProps.fetchedData ) {
this.fetchRequiredData();
}
}