World Map Zoom with D3 + ReactJS - reactjs

I found following code for zoom in a simple D3 solution which works well with Javascript:
var zoom = d3.behavior.zoom()
.on("zoom", function() {
projection.translate(d3.event.translate).scale(d3.event.scale);
feature.attr("d", path);
circle.attr("transform", ctr);
})
;
zoom.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([h / 6, h])
;
When I try to convert this code in D3 + ReactJS, I get issues. There have been many since I've tried so many different solutions. The latest one zooms my world map once but it's all a mess.
This is my code:
import React, { Component } from 'react';
import 'd3';
import * as d3Z from 'd3-zoom'
import * as d3 from 'd3-selection';
//import d3Scale from 'd3-scale'
import { geoMercator, geoPath } from 'd3-geo';
import './WorldMap.css';
import countries from '../resources/world-countries.json';
//
export default class WorldMap extends Component {
componentDidUpdate() {
const width = 1300, //document.body.clientWidth,
height = 600;//document.body.clientHeight;
const circleRadius = 2;
const lstRadius = [7, circleRadius];
const lstColor = ["green", "white"];
const duration = 500;
const lstShots = [];
const projection = geoMercator();
const path = geoPath()
.projection(projection);
const d3Zoom = d3Z.zoom();
for (var i = 0; i < this.props.data.length; i++) {
lstShots.push(JSON.parse(JSON.stringify(this.props.data[i])));
}
const getCirclePos = (d) => {
return "translate(" + projection([d.long, d.lat]) + ")";
}
const svgObj = d3.select(this.refs.svgMap);
const feature = svgObj
.selectAll("path.feature")
.data(countries.features)
.enter().append("path")
.attr("class", "feature");
projection
.scale(width / 6.5)
.translate([width / 2, height / 1.6]);
const zoom = d3Zoom
.on("zoom", function() {
console.log(d3.event);
projection.translate([d3.event.transform.x, d3.event.transform.y]).scale(d3.event.scale);
feature.attr("d", path);
// circle.attr("transform", getCirclePos);
})
;
svgObj.call(zoom);
// console.log(zoom);
// zoom.translateTo(projection.translate())
// .scale(projection.scale())
// .scaleExtent([height / 6, height])
// ;
feature.attr("d", path);
// eslint-disable-next-line
const circle = svgObj.selectAll("circle")
.data(lstShots)
.enter()
.append("circle")
.attr("r", circleRadius)
.attr("fill", 'white')
.attr("transform", getCirclePos)
.attr("node", function(d) { return JSON.stringify(d); })
.style("cursor", "pointer");
}
render() {
return (
<svg ref="svgMap"></svg>
);
}
}
The problem is that d3.event does not contain fields of translate or scale which I have seen people use in other example of d3-zoom. While I have replaced translate with transform in zoom function, I don't know what to replace d3.event.scale with.

If you port code from d3v3 to d3v4 you need to do it all, d3.event.scale does not exist in d3v4
projection.translate([d3.event.transform.x, d3.event.transform.y]).scale(d3.event.transform.k);
You need to initialise the zoom too
svgObj.call(zoom);
svgObj.call(zoom.transform, d3.zoomIdentity.translate(projection.translate()[0], projection.translate()[1]).scale(projection.scale()));
Don't use ref strings but Callback Refs
I needed to use componentDidMount() to see anything.

Related

ReactJS D3 chart not showing data mapped from Context, but works with static variable

New to D3 and React.
I'm trying to show data on a grid (x/y axis) for each record against a background image (logo), showing a small images.
I've got the D3 graph working if I use static data (gcs in my example code below). But, if I try to filter/map the data from CohortContext, the elements are not being shown on the graph.
I think it's a timing issue that the D3 is rendering before the .map returns with the data, but I may be wrong. I tried wrapping the useEffect in an async, but I'm grasping at straws now and it didn't resolve the issue anyway, based on all the code being within the async.
The code:
import React, { useEffect, useContext } from "react"
import * as d3 from "d3"
import logo from '../../assets/images//charts/360Chart.svg'
import CohortContext from "../../utility/context/cohort/cohort-context"
import Saboteur from '../../assets/images/charts/chartBubbleKey.svg'
function ChartData({ goal }) {
const gcs = [
{
chartAsset: Saboteur,
xCoordinates: 200,
yCoordinates: 300
},
{
chartAsset: Saboteur,
xCoordinates: 150,
yCoordinates: 150
}
]
console.log(gcs)
const width = 800
const height = 800
const { cohorts } = useContext(CohortContext)
useEffect(() => {
//Retrieve the Goal data from the useContext
const fetchData = async () => {
const goalCohorts = cohorts
.filter(records => records.goalId === goal.goalId)
.map((records) => {
return {
chartAsset: Saboteur,
xCoordinates: records.xcoordinates,
yCoordinates: records.ycoordinates
}
})
console.log('goal cohorts for Chart :', goalCohorts)
const svg = d3
.select("#svgcontainer")
.append("svg")
.attr("width", width)
.attr("height", height)
//background Chart
svg
.append("svg:image")
.attr("xlink:href", () => logo)
.attr("width", 580)
.attr("height", 580)
.attr("x", 110)
.attr("y", 20)
// Add in the data for the chart data
const g = svg
.selectAll("g")
.append("g")
.data(goalCohorts) // works if I use .data(gcs)
.enter()
g.append("svg:image")
.attr("xlink:href", (data) => data.chartAsset)
.attr("width", 50)
.attr("height", 50)
.attr("x", (data) => data.xCoordinates)
.attr("y", (data) => data.yCoordinates)
}
fetchData()
}, [])
return (
< div className="App" >
<div id="svgcontainer"></div>
</div >
)
}
export default ChartData
If I look at the console.logs, the data is the same - so assuming a timing issue.
Can anyone see anything obvious on why data(goalCohorts) is not showing the data?
I'm using React v18.1, D3 v7.8
useEffect(() => {
//Retrieve the Goal data from the useContext
const fetchData = async () => {
const goalCohorts = cohorts
.....
, [cohorts]);
Please replace [cohorts] instead of [].

Using lookAt in #react-three/fiber

I am trying learn how to use three js with react.For this purpose I am using #react-three/fiber and #react-three/drei.
This project by chriscourses was what I used to learn three js and I hope to use it to learn using threejs with react.
The below code should show boxes sprouting out of the cordinates given however currently they are all skewed towards one direction. Using lookat should solve the problem, however I do not know how to use lookat in #react-three/fiber / #react-three/drei.
import React, { FC, useRef } from 'react';
import { Box as NativeBox } from '#react-three/drei'
import { Vector3 } from 'three';
import { useFrame } from '#react-three/fiber';
type Props = {
country: any,
}
const lookAtCubePosition = new Vector3(0, 0, 0)
const Box: FC<Props> = (props) => {
const mesh = useRef()
const { country } = props;
const scale = country.population / 1000000000
const lat = country.latlng[0]
const lng = country.latlng[1]
const zScale = 0.8 * scale
const latitude = (lat / 180) * Math.PI
const longitude = (lng / 180) * Math.PI
const radius = 5
const x = radius * Math.cos(latitude) * Math.sin(longitude)
const y = radius * Math.sin(latitude)
const z = radius * Math.cos(latitude) * Math.cos(longitude)
console.log('box');
const lookAtFunc = (lookAt: Vector3) => {
lookAt.x = 0;
lookAt.y = 0;
lookAt.z = 0;
}
useFrame((state) => {
state.camera.lookAt(lookAtCubePosition)
})
return <NativeBox
args={[
Math.max(0.1, 0.2 * scale),
Math.max(0.1, 0.2 * scale),
Math.max(zScale, 0.4 * Math.random())
]}
position={[
x, y, z
]}
lookAt={lookAtFunc}
ref={mesh}
>
<meshBasicMaterial
attach="material"
color="#3BF7FF"
opacity={0.6}
transparent={true}
/>
</NativeBox>;
};
export default Box;
If you are using OrbitControls, you must set the 'target' for OrbitControls rather than lookAt of the Camera since the lookAt of the camera is being overridden by OrbitControls
I think (the documentation is not too specific on that) that we are not meant to set lookAt as a prop but instead use it as a function. We can gain access to the active camera using the useThree hook.
In a component which is mounted as a child of Canvas do:
import { useThree } from "#react-three/fiber";
// Takes a lookAt position and adjusts the camera accordingly
const SetupComponent = (
{ lookAtPosition }: { lookAtPosition: { x: number, y: number, z: number } }
) => {
// "Hook into" camera and set the lookAt position
const { camera } = useThree();
camera.lookAt(lookAtPosition.x, lookAtPosition.y, lookAtPosition.z);
// Return an empty fragment
return <></>;
}

Chart in d3js not showing up in react app

I am trying to follow the tutorial here but using react app. The chart is not showing up and I am not sure what I am doing wrong, please help. I am using d3 v7 and react 17. I get no errors but the buttons show up so the component is called correctly.
This is my component:
import * as d3 from 'd3'
import * as React from 'react'
import { useState, useEffect} from 'react'
const PieChart = (props) => {
const ref = React.createRef()
useEffect(() => {
draw()
});
// set the dimensions and margins of the graph
const width = 450, height = 450, margin = 40;
// The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
const radius = Math.min(width, height) / 2 - margin;
// set the color scale
const color = d3.scaleOrdinal()
.domain(["a", "b", "c", "d", "e", "f"])
.range(d3.schemeDark2);
// create 2 data_set
const data1 = {a: 9, b: 20, c:30, d:8, e:12}
const data2 = {a: 6, b: 16, c:20, d:14, e:19, f:12}
const svg = d3.select(".PieChart")
// A function that create / update the plot for a given variable:
const update = (data) => {
if (data == "data1"){
data = data1;
}else{
data = data2;
}
// Compute the position of each group on the pie:
const pie = d3.pie()
.value(function(d) {return d[1]; })
.sort(function(a, b) { return d3.ascending(a.key, b.key);} ) // This make sure that group order remains the same in the pie chart
const data_ready = pie(Object.entries(data))
// map to data
const u = svg.selectAll("path")
.data(data_ready)
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
u
.join('path')
.transition()
.duration(1000)
.attr('d', d3.arc()
.innerRadius(0)
.outerRadius(radius)
)
.attr('fill', function(d){ return(color(d.data[0])) })
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 1)
}
const draw = () => {
svg.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${width/2}, ${height/2})`);
// Initialize the plot with the first dataset
update("data1")
};
return <div ref={ref} >
<button onClick={update("data1")}>Data 1</button>
<button onClick={update("data2")}>Data 2</button>
<div className="PieChart" />
</div>
}
export default PieChart
This is what I get, no graph
I think there are quite a few things that need to be fixed.
First & foremost is the usage of ref. You have to do const ref = React.useRef() i.e use useRef instead of createRef.
Also, the ref has to be set to the svg element and not the div.
The 'almost' working code is here. https://codesandbox.io/s/flamboyant-heisenberg-vppny?file=/src/pie.js
I say almost because the chart does not render on page load! I do not know why (I am not very familiar with react). To render the chart, change the value of width or height from 450 to 451. I do not know why this works but it does.
One the page is loaded with the chart, the buttons & the chart transition work fine.
If you figure how to render the chart on page load, please let me know.

D3 Chart does not redraw on new data

Using react I draw a D3 scatter chart. When I try to add new lines on the chart through the svg i initialize at the start, it works i.e. when using the button rendered in the svg and the onlick function.
When I try to do this using the parent's props, the function to add the lines gets triggered but the lines are not added. I have read other posts about using enter() but this does not work. What I think is wrong is the way I setup the div structure to mount the original SVG, since I have a div with class chart-container and inside it a div with id chart, which I add the SVG inside. Does anyone thing the way I am selecting the svg when adding the lines is wrong? I do not understand why the lines are not rendered on the D3 chart.
import React, { useEffect, useLayoutEffect } from 'react';
import * as d3 from 'd3';
import './Chart.css';
const Chart = (props) => {
useEffect(() => {
drawChart(props.data)
}, [props.columnType]);
useLayoutEffect(() => {
if (showLines) {
addLines(props.data)
return
} else {
removeLines()
}
}, [props.showLines]);
const addLines = (data) => {
const x = d3.scaleLinear().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
const parsedData = parseData(data, columnType);
parsedData.forEach((dataSet, index) => {
const { points } = dataSet;
const line = d3.line()
.x((d) => x(d.x))
.y((d) => y(d.yhat));
// If I use this selection, I cannot add the lines
d3.select("svg").enter().append("path")
.datum(points)
.attr("class", `line`)
.attr("d", line);
});
}
const drawChart = (data) => {
..... // Other code not added (related to configuring the graph)
// If I use this SVG I can add the lines
const svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("id", "chart-svg")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
..... // other code not added (related to rendering the points using the data)
}
return (
<div className="chart-container">
<div id="chart"></div>
</div>
);
}
Figured it out. I need to reset the x and y ranges again, as it was rendering the lines off screen.

Is there a way to access a value computed using props in componentDidMount?

I'm working on converting a vanilla js gsap(GreenSock) animation to react, and want to access values based on props in componentDidMount(where gsap tweens are called). The animation is based on https://codepen.io/zadvorsky/pen/dILAG, using Delaunay triangulation and tweens to explode a shattered image. How do I access said values?
Originally I had the calculations contained in if(shatter) inside render but the values of rx, ry, and delay were undefined in the tween used in componentDidMount. I moved said calculations to componentDidMount so said values would be available, but the props are not available then, even with the use of a constructor.
export class Explosion extends React.Component {
constructor(props) {
super(props)
this.state = {
shatter: false
}
}
tl0 = new TimelineMax();
tl1 = new TimelineMax();
fragmentNode = React.createRef()
containerNode = React.createRef()
componentDidMount() {
if (this.state.shatter) {
const triangulatedValues = triangulate({
this.props.width,
this.props.height,
this.props.startX,
this.props.startY
})
const {
vertices,
indices
} = triangulatedValues
const ExplosionFragments = []
for (let i = 0; i < indices.length; i += 3) {
let point0 = vertices[indices[i + 0]]
let point1 = vertices[indices[i + 1]]
let point2 = vertices[indices[i + 2]]
let fragmentCentroid = computeCentroid({
point0,
point1,
point2
})
let dx = fragmentCentroid[0] - startX
let dy = fragmentCentroid[1] - startY
let d = Math.sqrt(dx * dx + dy * dy)
let rx = -30 * sign(dy)
let ry = -90 * -sign(dx)
let delay = d * 0.003 * randomRange(0.7, 1.1)
let box = computeBoundingBox({
point0,
point1,
point2
})
}
}
this.tl1
.to(this.fragmentNode, 1, {
z:500,
rotationX:rx,
rotationY:ry,
ease:Power2.easeInOut
})
.to(this.fragmentNode, 0.4,{alpha:0}, 1)
this.tl0
.insert(this.tl1, delay)
}
render() {
const {
shatter
} = this.state
return (
<Container
ref={this.containerNode}
onClick={this.handleToggleShatter}
>
{shatter ? (
<div>
<OverlayImage
src="http://i67.tinypic.com/2eq9utk.jpg"
/>
<canvas
width={width}
height={height}
ref={this.fragmentRef}>
</canvas>
</div>
) : (
<OverlayImage
src="http://i67.tinypic.com/2eq9utk.jpg"
/>
)}
</Container>
)
}
}
export default Explosion
https://codesandbox.io/s/ll139x3nx7
I expect the props to be available in componentDidMount, but the error "Parsing error: this is a reserved word" gets thrown. This is just an error for a possible solution, and I'm open to any way forward.
When you do:
const triangulatedValues = triangulate({
this.props.width,
this.props.height,
this.props.startX,
this.props.startY
})
You're passing in an Object where the keys are this.props.width etc and the values are also this.props.width because you haven't specified any other key, so it'll try to call the key the name of the variable.
Try:
const triangulatedValues = triangulate({
width: this.props.width,
height: this.props.height,
startX: this.props.startX,
startY: this.props.startY
})

Resources