view city names on the map - reactjs

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));

Related

D3 + ReactJS network graph hangs the browser indefinitely after continuously updating graph

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()
...

d3 elements not rendering in React

So I did a a treemap for freecodecamp and it's working fine, but I wanted to make it a bit more dynamic, to let the user choose which dataset to visualize (there's a choice of three datasets). To do this I found this Medium article explaining how to integrate d3 into react.
Essentially, in my render method I used ref={node=>this.node=node} for the ref attribute in the div being returned by the method, and am creating a function that renders the data onto that div using the reference and calling the function in componentDidMount and componentDidUpdate.
The problem I'm having is that the rect elements in the svg aren't rendering.
I know the integration is working because the first thing I rendered with the function is an h4 header. The next thing I rendered is the svg element. I know the svg is rendering, because I can change the background-color in the CSS and it appears fine. I know the d3 hierarchy is working because I displayed root.leaves() in the console and it displayed the appropriate data. I'm not getting any error messages and I know the entire createMap method is running b/c I can log something to the console fine at the end of the function. I thought maybe it was an issue with the color scale, so I set the fill of the rects to black and still got nothing.
Here is a link to my project and below is the JS code. Can anyone tell me what's going on with it?
$(function(){
const DATASETS = [
{
TYPE: 'KICKSTARTERS',
NAME: 'Kickstarters',
URL: 'https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/kickstarter-funding-data.json'
},
{
TYPE: 'MOVIES',
NAME: 'Movies',
URL: 'https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/movie-data.json'
},
{
TYPE: 'GAMES',
NAME: 'Games',
URL: 'https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/video-game-sales-data.json'
}
];
//REDUX
const INDEX = 'INDEX';
const INITIAL_STATE = 0;
//action generator
function setIndex(index){
return {
type: INDEX,
index
};
}
//reducer
function indexReducer(state = INITIAL_STATE, action){
switch(action.type){
case INDEX: return action.index;
default: return state;
}
}
const store = Redux.createStore(indexReducer);
//react
class DropDown extends React.Component{
constructor(props){
super(props);
this.handler = this.handler.bind(this);
}
handler(e){
this.props.setIndex(e.target.selectedIndex);
}
render(){
return (
<select
id='dropdown'
onChange={this.handler}>
{DATASETS.map(e=><option>{e.NAME}</option>)}
</select>
)
}
}
class Svg extends React.Component{
constructor(props){
super(props);
this.createMap = this.createMap.bind(this);
}
componentDidMount(){
this.createMap();
}
componentDidUpdate(){
this.createMap();
}
createMap(){
const NODE = this.node
const DATASET = DATASETS[this.props.index];
d3.select(NODE)
.html('')
.append('h4')
.attr('id', 'description')
.attr('class', 'text-center')
.html(`Top ${DATASET.NAME}`)
//svg setup
const SVG_PADDING = 20;
const SVG_WIDTH = 1000;
const SVG_HEIGHT = 1400;
var svg = d3.select(NODE)
.append('svg')
.attr('width', SVG_WIDTH)
.attr('height', SVG_HEIGHT)
.attr('id', 'map');
d3.json(DATASET.URL)
.then(
function(data){
//heirarchy and map
var root = d3.hierarchy(data)
.sum(d=>d.balue)
.sort((a,b)=>b.height-a.height || b.value-a.value);
var nodes = d3.treemap()
.size([SVG_WIDTH, SVG_HEIGHT * 2/3])
.padding(1)(root)
.descendants();
//scale
var categories = [...new Set(root.leaves().map(e => e.data.category))].sort();
var unityScale = d3.scaleLinear()
.domain([0, categories.length]).range([0,1]);
var colorScale = d3.scaleSequential(d3.interpolateRainbow);
var discreteScale = d3.scaleOrdinal(d3.schemeCategory20b);
//map cells
var cell = svg.selectAll('.cell')
.data(root.leaves())
.enter().append('g')
.attr('class', 'cell')
.attr('transform', d=>`translate(${d.x0}, ${d.y0})`);
cell.append('rect')
.attr('class', 'tile')
.attr('data-category', d=>d.data.category)
.attr('data-name', d=>d.data.name)
.attr('data-value', d=> d.data.value)
.attr('x', 0).attr('y', 0)
.attr('width', d=>d.x1-d.x0)
.attr('height', d=>d.y1-d.y0)
.attr('fill', d => colorScale(unityScale(categories.indexOf(d.data.category))))
});
}
render(){
const test = d3.select('#container')
return <div ref={node=>this.node=node}/>
}
}
//ReactRedux
const Provider = ReactRedux.Provider;
const connect = ReactRedux.connect;
function mapDispatchToProps(dispatch){
return {
setIndex: index=>dispatch(setIndex(index))
}
}
function mapStateToProps(state){
return {
index: state
}
}
const DropDownConnection = connect(null, mapDispatchToProps)(DropDown);
const SvgConnection = connect(mapStateToProps, null)(Svg);
class Wrapper extends React.Component {
render(){
return(
<Provider store={store}>
<DropDownConnection/>
<SvgConnection/>
</Provider>
)
}
}
ReactDOM.render(<Wrapper/>, $('#container')[0]);
})

passing data from a react based component to vanilla js class

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>
);
}
}

React + d3.js - svg rerenders on top of previous one with new props

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();
}
}

D3.js V4 zoom doesn't work in React + faux

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

Resources