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]);
})
Related
This is my simple code. And I am just rendering a single marker and try to click it.
But it is not clickable and nothing happens.
import React, { PureComponent } from 'react';
import ReactDOMServer from 'react-dom/server';
import * as PIXI from 'pixi.js';
import 'leaflet-pixi-overlay';
import L from 'leaflet';
import { SvgMarker } from '../../../utils/mapIcon';
const pixiMarkerContainer = new PIXI.Container();
let markerTextures = {};
class MapRouteMarkers extends PureComponent {
constructor(props) {
super(props);
this.state = { markerTexturesLoaded: false };
}
This is my markerOverlay, I have made it interactive by adding newMarker.interactive = true; and also registered click event on newMarker.
markerOverlay = L.pixiOverlay(utils => {
const map = utils.getMap();
const scale = utils.getScale();
const renderer = utils.getRenderer();
const container = utils.getContainer();
if (map && Object.keys(markerTextures).length !== 0) {
if (container.children.length) container.removeChildren();
const newMarker = new PIXI.Sprite(markerTextures.default);
const newMarkerPoint = utils.latLngToLayerPoint([50.63, 13.047]);
newMarker.x = newMarkerPoint.x;
newMarker.y = newMarkerPoint.y;
newMarker.scale.set(1 / scale);
newMarker.interactive = true;
newMarker.buttonMode = true;
newMarker.on('click', () => {
console.log('gggg');
});
container.addChild(newMarker);
renderer.render(container);
}
}, pixiMarkerContainer);
Here I am redrawing markers
componentDidUpdate() {
if (this.state.markerTexturesLoaded) {
const map = this.props.mapRef;
this.markerOverlay.addTo(map);
this.markerOverlay.redraw();
}
}
Here I am loading baseTexture from svg.
componentDidMount() {
if (Object.keys(markerTextures).length === 0) {
const loader = new PIXI.Loader();
const svgString = ReactDOMServer.renderToString(<SvgMarker iconColor="yellow" />);
const DOMURL = self.URL || self.webkitURL || self;
const img = new Image();
const svg = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
const url = DOMURL.createObjectURL(svg);
img.src = url;
const texture = new PIXI.Texture.from(img);
loader.load((thisLoader, resources) => {
markerTextures = { default: texture };
this.setState({
markerTexturesLoaded: true,
});
});
}
}
render() {
return null;
}
}
export default MapRouteMarkers;
here is the image attached for the same
Do anyone know what I am doing wrong here?
Try to handle the event differently, this worked for me:
newMarker.click = (event) => {
console.log(`Click, info=${JSON.stringify(event.data.global)}`);
};
You can also use the 'hover' and 'mouseout' events the same way for instance.
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.
I have requirement in my project to customize player controls. I found a lot documentation how to make it possible, except TextTrackDisplay component. For some reason this component has been rendered by videojs, but rendered code seems have no clue about any existing text tracks of standard hls stream (bip-bop).
I've created sample of code where play button successfully customized, but TextTrackDisplay wasn't for some reason...
https://codepen.io/jurij-sergeewich-gerc/pen/xxxwPmN
class Player extends React.Component {
init = (videoRef) => {
const videoJsOptions = {
controls: false,
html5: {nativeTextTracks: false},
};
const player = videojs(videoRef, videoJsOptions, () => {
const type = 'application/x-mpegURL';
const src = 'https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8'
player.pause();
player.src({type, src});
player.play();
this.connectPlayButton(player);
this.connectTextTracks(player);
});
}
connectPlayButton = (player) => {
const selector = '[data-play-btn]';
const allElements = Array.from(document.querySelectorAll(selector));
allElements.forEach((box) => {
const PlayToggle = videojs.getComponent('PlayToggle');
const playToggle = new PlayToggle(player);
box.appendChild(playToggle.el());
});
};
connectTextTracks = (player) => {
const selector = '[data-text-tracks]';
const allElements = Array.from(document.querySelectorAll(selector));
allElements.forEach((box) => {
const TextTrackDisplay = videojs.getComponent('TextTrackDisplay');
const textTrackDisplay = new TextTrackDisplay(player);
box.appendChild(textTrackDisplay.el());
});
};
render () {
return (
<div data-vjs-player>
<video ref={this.init} className="video-js-video">
</video>
<div data-play-btn></div>
<div data-text-tracks></div>
</div>
)
}
}
ReactDOM.render(
<Player/>,
document.getElementById('root')
);
Could any one help me please :))
I just was tired at night yesterday, this is stupid, but for show "cc" in control panel, we need to use not 'TextTrackDisplay' component but 'SubtitlesButton'.
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 add the google api script programmatically when it is required. However, I get an error that google is not defined. I can see that the script is added in before the end of the body tag.
Earlier I had the script loaded in the index.html file however, I have created a different component elsewhere in the app now which require its own script as it has a different api key. Therefore, I had to remove the script from the index.html as it was giving an exception for multiple use of the script. Now I would like to add it when it is the component is loading.
Please refer the code below for the main component:
import React from 'react';
import { Button } from 'reactstrap';
import CitySuggestionBar from './CitySuggestionBar';
export default class Destination extends React.Component{
componentDidMount(){
this.renderScript();
}
renderScript = () => {
loadScript('https://maps.googleapis.com/maps/api/js?key=MY_API_KEY&libraries=places');
}
showPlaceDetails(place) {
let city = place.address_components[0].long_name.toString();
try{
city+= '+' + place.address_components[2].long_name.toString();
}catch(e){}
city = city.replace(/\s/g, "+");
sessionStorage.setItem('city', city);
console.log(city);
}
redirect = () =>{
sessionStorage.getItem('city') ? this.props.history.push("/hotels") : alert('Please select a city first');
}
render(){
return(
<div className="location-search-container">
<div className="location-search-wrapper">
<h1>Search for a city...</h1>
<CitySuggestionBar onPlaceChanged={this.showPlaceDetails.bind(this)} />
<Button onClick={this.redirect} className="btns" to="/hotels" color="primary">Proceed</Button>
</div>
</div>
);
}
}
const loadScript = (url) => {
const index = window.document.getElementsByTagName('script')[0];
const script = window.document.createElement('script');
script.src=url;
index.parentNode.insertBefore(script, index);
}
Below is the code for the component where the google map is being used and it is a sub component of the above main component:
import React from "react";
/* global google */
export default class CitySuggestionBar extends React.Component {
constructor(props) {
super(props);
this.autocompleteInput = React.createRef();
this.autocomplete = null;
this.handlePlaceChanged = this.handlePlaceChanged.bind(this);
}
componentDidMount() {
this.autocomplete = new window.google.maps.places.Autocomplete(this.autocompleteInput.current,
{"types": ['(cities)']});
this.autocomplete.addListener('place_changed', this.handlePlaceChanged);
}
handlePlaceChanged(){
const place = this.autocomplete.getPlace();
this.props.onPlaceChanged(place);
}
render() {
return (
<input ref={this.autocompleteInput} id="autocomplete" placeholder="Search"
type="text"></input>
);
}
}
Please Help!
Thanks in advance.
In the above snippet, I can see that every time the componentDidMount it will again create another script tag to avoid this, you can modify the loadScript methods as follows:
const loadScript = (url) => {
const googleScript = window.document.getElementByClassName('google-script');
if (googleScript.length === 0) {
const script = window.document.createElement('script');
script.src=url;
script.class="google-script"
document.body.appendChild(script)
}
}
If you like to remove the google script you can handle this inside componentWillUnmount.
Using this will not show you an exception for multiple uses of the script tag.
Also if you like to know that the script tag is loaded or not you can find it by adding another like in loadScript method as follows:
const loadScript = (url) => {
const googleScript = window.document.getElementByClassName('google-script');
if (googleScript.length === 0) {
const script = window.document.createElement('script');
script.src=url;
script.class="google-script"
document.body.appendChild(script)
script.onload = () => {
// Place code here to do further action.
};
}
}
<----------------------------Update--------------------------->
In order to resolve "google is undefined" error you can try following the approach where you create a promise for the Google Maps API, and resolve that promise in a (global) callback function the Google Maps API can run. In your component code, you'd then wait for the promise to be resolved before proceeding.
const loadScript = () => {
if (!this.googleMapsPromise) {
this.googleMapsPromise = new Promise((resolve) => {
// Add a global handler for when the API finishes loading
window.resolveGoogleMapsPromise = () => {
// Resolve the promise
resolve(google);
// Tidy up
delete window.resolveGoogleMapsPromise;
};
// Load the Google Maps API
const script = document.createElement("script");
const API = //your api key;
script.src = `https://maps.googleapis.com/maps/api/js?key=${API}&callback=resolveGoogleMapsPromise`;
script.async = true;
document.body.appendChild(script);
});
}
// Return a promise for the Google Maps API
return this.googleMapsPromise;
}
componentWillMount() {
// Start Google Maps API loading since we know we'll soon need it
this.loadScript();
}
componentDidMount() {
// Once the Google Maps API has finished loading, initialize the map
this.getGoogleMaps().then((google) => {
const uluru = { lat: -25.366, lng: 131.044 };
const map = new google.maps.Map(document.getElementById('map'), {
zoom: 4,
center: uluru
});
const marker = new google.maps.Marker({
position: uluru,
map: map
});
});
}
render() {
return (
<div>
<div id="map" style={{width: 600, height: 300}}></div>
</div>
)
}