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
Related
I am trying to implement star voting with font awesome icons,
on Product.js i have 2 component , problem is when i change styling for a component that has prop, naturally it changes other component because of querySelectorAll, so how can i change the class for only component which has props i pass.
Product.js
const [rating, setRating] = useState(0);
const ratingHandler = (e) => {
setRating(e);
};
<Rating handleRating={ratingHandler} />
<Rating />
for first child i want to add some styling like when i mouse over star ,it lights up etc. So i want to make it based on handleRating props.
Rating.js
const Rating = (handleRating = false) => {
useEffect(() => {
if (handleRating) {
hover();
}
}, []);
function hover() {
const spans = document.querySelectorAll(".ratinger span:not(.texting)");
console.log(spans);
spans.forEach((spanon) => {
const onStar = parseInt(spanon.dataset.value, 10);
spanon.onmouseover = () => {
spans.forEach((span) =>
span.dataset.value <= onStar
? span.classList.add("hover")
: span.classList.remove("hover")
);
};
spanon.onmouseout = () => {
spans.forEach((span) => {
span.classList.remove("hover");
});
};
spanon.onclick = () => {
spans.forEach((span) =>
span.dataset.value <= onStar
? span.classList.add("onclick")
: span.classList.remove("onclick")
);
handleRating(parseInt(spanon.dataset.value, 10));
};
});
}
i didnt add unnecessary parts on below i have icons within a div.
Try:
const node = ReactDOM.findDOMNode(this);
const spans = node.querySelectorAll('...');
I'm currently fetching data in Component1, then dispatching an action to update the store with the response. The data can be seen in Component2 in this.props, but how can I render it when the response is returned? I need a way to reload the component when the data comes back.
Initially I had a series of functions run in componentDidMount but those are all executed before the data is returned to the Redux store from Component1. Is there some sort of async/await style between components?
class Component1 extends React.Component {
componentDidMount() {
this.retrieveData()
}
retrieveData = async () => {
let res = await axios.get('url')
updateParam(res.data) // Redux action creator
}
}
class Component2 extends React.Component {
componentDidMount() {
this.sortData()
}
sortData = props => {
const { param } = this.props
let result = param.sort((a,b) => a - b)
}
}
mapStateToProps = state => {
return { param: state.param }
}
connect(mapStateToProps)(Component2)
In Component2, this.props is undefined initially because the data has not yet returned. By the time it is returned, the component will not rerender despite this.props being populated with data.
Assuming updateParam action creator is correctly wrapped in call to dispatch in mapDispatchToProps in the connect HOC AND properly accessed from props in Component1, then I suggest checking/comparing props with previous props in componentDidUpdate and calling sortData if specifically the param prop value updated.
class Component2 extends React.Component {
componentDidMount() {
this.sortData()
}
componentDidUpdate(prevProps) {
const { param } = this.props;
if (prevProps.param !== param) { // <-- if param prop updated, sort
this.sortData();
}
}
sortData = () => {
const { param } = this.props
let result = param.sort((a, b) => a - b));
// do something with result
}
}
mapStateToProps = state => ({
param: state.param,
});
connect(mapStateToProps)(Component2);
EDIT
Given component code from repository
let appointmentDates: object = {};
class Appointments extends React.Component<ApptProps> {
componentDidUpdate(prevProps: any) {
if (prevProps.apptList !== this.props.apptList) {
appointmentDates = {};
this.setAppointmentDates();
this.sortAppointmentsByDate();
this.forceUpdate();
}
}
setAppointmentDates = () => {
const { date } = this.props;
for (let i = 0; i < 5; i++) {
const d = new Date(
new Date(date).setDate(new Date(date).getDate() + i)
);
let month = new Date(d).toLocaleString("default", {
month: "long"
});
let dateOfMonth = new Date(d).getDate();
let dayOfWeek = new Date(d).toLocaleString("default", {
weekday: "short"
});
// #ts-ignore
appointmentDates[dayOfWeek + ". " + month + " " + dateOfMonth] = [];
}
};
sortAppointmentsByDate = () => {
const { apptList } = this.props;
let dates: string[] = [];
dates = Object.keys(appointmentDates);
apptList.map((appt: AppointmentQuery) => {
return dates.map(date => {
if (
new Date(appt.appointmentTime).getDate().toString() ===
// #ts-ignore
date.match(/\d+/)[0]
) {
// #ts-ignore
appointmentDates[date].push(appt);
}
return null;
});
});
};
render() {
let list: any = appointmentDates;
return (
<section id="appointmentContainer">
{Object.keys(appointmentDates).map(date => {
return (
<div className="appointmentDateColumn" key={date}>
<span className="appointmentDate">{date}</span>
{list[date].map(
(apptInfo: AppointmentQuery, i: number) => {
return (
<AppointmentCard
key={i}
apptInfo={apptInfo}
/>
);
}
)}
</div>
);
})}
</section>
);
}
}
appointmentDates should really be a local component state object, then when you update it in a lifecycle function react will correctly rerender and you won't need to force anything. OR since you aren't doing anything other than computing formatted data to render, Appointments should just call setAppointmentDates and sortAppointmentsByDate in the render function.
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.
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 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();
}
}