class MyChart extends React.Component {
constructor(props) {
super(props);
this.state = {
data: null
}
this.fetchData = this.fetchData.bind(this);
this.barChart = this.barChart.bind(this);
}
componentDidMount() {
this.fetchData();
this.barChart(this.state.data);
}
fetchData() {
const API = 'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json'
fetch(API)
.then(response => response.json())
.then(data => {
this.setState({
data: data.data
})
})
}
barChart(dataset) {
const canvasWidth = 600;
const canvasHeight = 400;
const svgCanvas = d3.select(this.refs.canvas)
.append('svg')
.attr('width', canvasWidth)
.attr('height', canvasHeight)
const xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, d => d[0])])
.range([0, canvasWidth])
const yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, d => d[1])])
.range([canvasHeight, 0])
svgCanvas.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', (d, i) => i * 30)
.attr('y', d => yScale(d[1]))
.attr('width', xScale(25))
.attr('height', d => yScale(d[1]))
}
render() {
return (
<d>
<div id='title'>my chart</div>
<div ref='canvas'></div>
</d>
)
}
}
ReactDOM.render(<MyChart />, document.getElementById('app'))
I am using React to visualize the graph with D3.
I tried to fetch data of GDP, and use the data, but I have got nothing on the page.
when I just put an array as an input to test by myself instead of fetching data,
it shows at least something based on the input. I think the problem occurs when fetching data
Any thoughts on this matter?
When the component start rendering, first thing will be called componentDidMount() .So in your componentDidMount() two things are there
fetching api
rendering barchart , which will be run in same batch
So when api call happen setState will not assign state.data value as it is asynchronous.
But next when your barchart want to render, it's getting null value as argument.
Thats the reason it's not working.
I suggest u to put the this.barChart(data.data) inside the fetch api.
Related
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));
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]);
})
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 have a React component that renders a D3 bar chart where the bars are clickable. It works as expected until the parent state is updated. The bars update correctly but the click events on the bars are still bound to the previous data. I'm not sure if this is something I'm doing wrong in React or D3 or both. Any help is appreciated.
const DATA1 = [{"value":1}, {"value":1}]
const DATA2 = [{"value":3}, {"value":3}, {"value":3}, {"value":2}, {"value":2}]
const CLICK = 'chartClick'
function handleClick(data, i, els) {
const element = els[i]
const event = new Event(CLICK, {bubbles: true, detail: data})
return element.dispatchEvent(event)
}
class D3Chart {
constructor(selector) {
this.svg = d3.select(selector).append('svg')
}
draw(data) {
const xScale = d3.scaleLinear()
.domain([0, d3.max(data.map(i => i.value))])
.range([0, 500])
const barGroups = this.svg.selectAll('.barGroup').data(data)
const barGroupsEnter = barGroups.enter().append('g')
.attr('class', 'barGroup')
.attr('transform', (d, i) => {
const y = (i * 25)
return `translate(0, ${y})`
})
barGroupsEnter.append('rect')
.attr('class', 'bar')
.attr('height', 20)
.attr('width', d => xScale(d.value))
.on('click', handleClick)
barGroups.exit().remove()
}
}
class Chart extends React.Component {
chartRef = React.createRef()
componentDidMount() {
const {data, onClick} = this.props
this.chartRef.current.addEventListener(CLICK, onClick)
this.chart = new D3Chart(this.chartRef.current)
this.chart.draw(data)
}
shouldComponentUpdate(nextProps) {
const {data} = this.props
return !_.isEqual(data, nextProps.data)
}
componentDidUpdate() {
const {data} = this.props
this.chart.draw(data)
}
render() {
return <div ref={this.chartRef}></div>
}
}
class App extends React.Component {
state = {data: DATA1}
handleButtonClick = () => this.setState({data: DATA2})
handleChartClick = (data, event) => console.log('data length on click', data.length)
render() {
const {data} = this.state
console.log('data length on render', data.length)
return (
<React.Fragment>
<button onClick={this.handleButtonClick}>Update Data</button>
<Chart data={data} onClick={(event) => this.handleChartClick(data, event)} />
</React.Fragment>
)
}
}
ReactDOM.render(<App />, document.querySelector('.root'))
The output on the initial render/click is:
"data length on render" 2
"data length on click" 2
The output after the data has been updated is:
"data length on render" 5
"data length on click" 2
I'm expecting the latter to be:
"data length on render" 5
"data length on click" 5
Codepen example here: https://codepen.io/bohmanart/pen/QPQJdX
You can try the below solution, change the onclick event inside chart to this.chartRef.current.addEventListener(CLICK, ()=>onClick(this.props.data.length);), the change the onClick props on chart to onClick={this.handleChartClick} and then change the handle chart click to handleChartClick = (data) =>{ console.log('data length on click', data);}
I am not sure of the use case on why you want to the pass the data from Chart component via handleChartClick when it is already available in App class state. You can just use this.state.data.length in handleChartClick
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();
}
}