I am creating a graph using the react-plotly library. The data from the graph is being queried from an API endpoint.
The service file is shown below
import React from 'react'
export default async function GetStockData(token,ticker,setData, setSuccess) {
var myHeaders = new Headers();
myHeaders.append("Authorization", "Bearer " + token)
var formdata = new FormData();
formdata.append("Tick", ticker);
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: formdata,
redirect: 'follow'
};
await fetch("https://abc.azurewebsites.net/api/stocks", requestOptions)
.then(response => response.json())
.then(response => setData(JSON.parse(response.content))
}
I am still the add error handling. I have fixed the ticker to a value that I know works for now.
This service is then called by the component below:
import React, { useState, useContext, useEffect} from 'react'
import GetStockData from '../Services/GetStockData'
import Plot from 'react-plotly.js';
import { sampleData } from './sampleChartdata';
import AuthContext from "../Store/AuthContext";
export default function CandleStick() {
const authCtx = useContext(AuthContext);
const token = authCtx.token
const [chartData, setChartData] = useState(sampleData);
const [chartSuccess, setChartSuccess] = useState(false)
const [preppedData, setpreppedData] = useState(null);
const [layout, setlayout] = useState(null);
const ticker = 'MSFT'
const clickHandler = async()=>{
await GetStockData(token,ticker,setChartData, setChartSuccess)
const {data, preplayout} = dataPrep()
setpreppedData(data)
setlayout(preplayout)
}
const dataPrep =()=>{
var dateData = []
var closeData = []
var openData = []
var lowData = []
var highData = []
for(var prop in chartData["Time Series (Daily)"]){
dateData.push(prop)
for(var prop2 in chartData["Time Series (Daily)"][prop]){
if (prop2=="1. open"){ openData.push(chartData["Time Series (Daily)"][prop][prop2])}
if (prop2=="2. high"){ highData.push(chartData["Time Series (Daily)"][prop][prop2])}
if (prop2=="3. low"){ lowData.push(chartData["Time Series (Daily)"][prop][prop2])}
if (prop2=="5. adjusted close"){ closeData.push(chartData["Time Series (Daily)"][prop][prop2])}
}
}
var trace1 = {
x: dateData,
close:closeData,
increasing: {line: {color: 'green'}},
decreasing: {line: {color: 'red'}},
high: highData,
line: {color: 'rgba(31,119,180,1)'},
low: lowData,
open: openData,
type: 'candlestick',
xaxis: 'x',
yaxis: 'y'
};
var data = [trace1]
var layout = {
dragmode: 'zoom',
margin: {
r: 10,
t: 25,
b: 40,
l: 60
},
showlegend: false,
xaxis: {
autorange: true,
domain: [0, 1],
title: 'Date',
type: 'date'
},
yaxis: {
autorange: true,
domain: [0, 1],
type: 'linear'
}
};
return {data , layout}
} ;
useEffect(() => {
if (preppedData !== null) setIsDataLoaded(true);console.log(preppedData)
}, [preppedData]);
return (
<>
<div>{isDataLoaded?
<Plot
data={preppedData}
layout={layout}>
</Plot>: null}
</div>
<button onClick={()=>clickHandler()}>Refresh</button>
</>
)
}
There are probably a whole host of things that could be improved here. I am using too stateful variables for a start. I currently have a local store of data to use as a sample to prevent an error occurring prior to the request being made. Any comment on how to manage first render when awaiting web based content would be massively appreciated.
My core question relate to setChartData, setChartSuccess. On the press of the button, these are passed to the service file where they are updated simultaneously. However, the chartSuccess variable seems to update prior to ChartData. The jsx conditional triggers and renders the graph but it doesn't contain the latest preppedData. On pressing the button a second time the updated data appears. Am I making an error in the sequencing?
From what I see, you can do 2 things to solve this. Either set success state inside the click handler and if the success state can't be moved to elsewhere as it might be used in some other place, you can have a new state variable and use it for the condition for render or not.
I'm showing you how to do it with the 2nd method here, but you can give 1st method a try if you don't have problem I mentioned.
So, your component code will look like this.
import React, { useState, useContext, useEffect } from 'react';
import GetStockData from '../Services/GetStockData';
import Plot from 'react-plotly.js';
import { sampleData } from './sampleChartdata';
import AuthContext from '../Store/AuthContext';
export default function CandleStick() {
const authCtx = useContext(AuthContext);
const token = authCtx.token;
const [chartData, setChartData] = useState(sampleData);
const [chartSuccess, setChartSuccess] = useState(false);
const [preppedData, setpreppedData] = useState(null);
const [layout, setlayout] = useState(null);
const [isDataLoaded, setIsDataLoaded] = useState(false);
const ticker = 'MSFT';
const clickHandler = async () => {
await GetStockData(token, ticker, setChartData, setChartSuccess);
const { data, preplayout } = dataPrep();
setpreppedData(data);
setlayout(preplayout);
setIsDataLoaded(true);
};
const dataPrep = () => {
var dateData = [];
var closeData = [];
var openData = [];
var lowData = [];
var highData = [];
for (var prop in chartData['Time Series (Daily)']) {
dateData.push(prop);
for (var prop2 in chartData['Time Series (Daily)'][prop]) {
if (prop2 == '1. open') {
openData.push(chartData['Time Series (Daily)'][prop][prop2]);
}
if (prop2 == '2. high') {
highData.push(chartData['Time Series (Daily)'][prop][prop2]);
}
if (prop2 == '3. low') {
lowData.push(chartData['Time Series (Daily)'][prop][prop2]);
}
if (prop2 == '5. adjusted close') {
closeData.push(chartData['Time Series (Daily)'][prop][prop2]);
}
}
}
var trace1 = {
x: dateData,
close: closeData,
increasing: { line: { color: 'green' } },
decreasing: { line: { color: 'red' } },
high: highData,
line: { color: 'rgba(31,119,180,1)' },
low: lowData,
open: openData,
type: 'candlestick',
xaxis: 'x',
yaxis: 'y',
};
var data = [trace1];
var layout = {
dragmode: 'zoom',
margin: {
r: 10,
t: 25,
b: 40,
l: 60,
},
showlegend: false,
xaxis: {
autorange: true,
domain: [0, 1],
title: 'Date',
type: 'date',
},
yaxis: {
autorange: true,
domain: [0, 1],
type: 'linear',
},
};
return { data, layout };
};
return (
<>
<div>
{isDataLoaded ? <Plot data={preppedData} layout={layout}></Plot> : null}
</div>
<button onClick={() => clickHandler()}>Refresh</button>
</>
);
}
UPDATED as per requirement
import React, { useState, useContext, useEffect } from 'react';
import GetStockData from '../Services/GetStockData';
import Plot from 'react-plotly.js';
import { sampleData } from './sampleChartdata';
import AuthContext from '../Store/AuthContext';
export default function CandleStick() {
const authCtx = useContext(AuthContext);
const token = authCtx.token;
const [chartData, setChartData] = useState(sampleData);
const [chartSuccess, setChartSuccess] = useState(false);
const [preppedData, setpreppedData] = useState(null);
const [layout, setlayout] = useState(null);
const [isDataLoaded, setIsDataLoaded] = useState(false);
const ticker = 'MSFT';
const clickHandler = async () => {
await GetStockData(token, ticker, setChartData, setChartSuccess);
const { data, preplayout } = dataPrep();
setpreppedData(data);
setlayout(preplayout);
};
const dataPrep = () => {
var dateData = [];
var closeData = [];
var openData = [];
var lowData = [];
var highData = [];
for (var prop in chartData['Time Series (Daily)']) {
dateData.push(prop);
for (var prop2 in chartData['Time Series (Daily)'][prop]) {
if (prop2 == '1. open') {
openData.push(chartData['Time Series (Daily)'][prop][prop2]);
}
if (prop2 == '2. high') {
highData.push(chartData['Time Series (Daily)'][prop][prop2]);
}
if (prop2 == '3. low') {
lowData.push(chartData['Time Series (Daily)'][prop][prop2]);
}
if (prop2 == '5. adjusted close') {
closeData.push(chartData['Time Series (Daily)'][prop][prop2]);
}
}
}
var trace1 = {
x: dateData,
close: closeData,
increasing: { line: { color: 'green' } },
decreasing: { line: { color: 'red' } },
high: highData,
line: { color: 'rgba(31,119,180,1)' },
low: lowData,
open: openData,
type: 'candlestick',
xaxis: 'x',
yaxis: 'y',
};
var data = [trace1];
var layout = {
dragmode: 'zoom',
margin: {
r: 10,
t: 25,
b: 40,
l: 60,
},
showlegend: false,
xaxis: {
autorange: true,
domain: [0, 1],
title: 'Date',
type: 'date',
},
yaxis: {
autorange: true,
domain: [0, 1],
type: 'linear',
},
};
return { data, layout };
};
useEffect(() => {
if (preppedData !== null) setIsDataLoaded(true);
}, [preppedData]);
return (
<>
<div>
{isDataLoaded ? <Plot data={preppedData} layout={layout}></Plot> : null}
</div>
<button onClick={() => clickHandler()}>Refresh</button>
</>
);
}
Related
I want the react quill rich text editor able to convert link into social media, like this
link: https://www.tiktok.com/#epicgardening/video/7055411162212633903
My RTE Code
import { useCallback, useMemo, useEffect } from 'react';
import ImageResize from 'quill-image-resize-module-react';
import ReactQuill, { Quill } from 'react-quill';
import { message } from 'antd';
import { uploadFiles } from 'utils';
import 'react-quill/dist/quill.bubble.css';
import 'react-quill/dist/quill.snow.css';
import './styles.scss';
Quill.register('modules/imageResize', ImageResize);
const RichTextEditor = ({
editorState,
onChange,
readOnly = false,
setLoading = () => {}
}) => {
window.Quill = Quill;
let quillRef = null; // Quill instance
let reactQuillRef = null; // ReactQuill component
useEffect(() => {
attachQuillRefs();
}, []);
const attachQuillRefs = useCallback(() => {
if (typeof reactQuillRef.getEditor !== 'function') return;
quillRef = reactQuillRef.getEditor();
}, []);
const imageHandler = () => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.click();
input.onchange = async () => {
const file = input.files[0];
if (file.size > 1500000)
return message.error('Image size exceeded 1.5Mb');
setLoading({ image: true });
const formData = new FormData();
formData.append('image', file);
const fileName = file.name;
const imgUrl = await uploadFiles(file, quillRef);
const range = quillRef.getSelection();
quillRef.insertEmbed(range.index, 'image', imgUrl, 'user');
let existingDelta = quillRef.getContents();
const indexOf = existingDelta.ops.findIndex((eachOps) => {
return eachOps.insert?.image === imgUrl;
});
const selectedOps = existingDelta.ops[indexOf];
if (indexOf !== -1) {
selectedOps.attributes = {};
selectedOps.attributes = { alt: fileName };
}
quillRef.setContents(existingDelta);
setLoading({ image: false });
};
};
const modules = useMemo(
() => ({
toolbar: {
container: [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
['bold', 'italic', 'underline'],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ align: [] }],
['link', 'image'],
['clean'],
[{ color: [] }]
],
handlers: {
image: imageHandler
}
},
imageResize: {
modules: ['Resize', 'DisplaySize']
}
}),
[]
);
return (
<div className="react-quill-wrapper">
<ReactQuill
readOnly={readOnly}
theme={readOnly ? 'bubble' : 'snow'}
ref={(e) => {
reactQuillRef = e;
}}
value={editorState}
modules={modules}
placeholder="Add content of your article!"
onChange={onChange}
/>
</div>
);
};
export { RichTextEditor };
const [editorState, setEditorState] = useState('');
<RichTextEditor
editorState={editorState}
onChange={setEditorState}
setLoading={setLoading}
/>
called by parent like this
I've been working on this for almost a week, I really need help
I expected to have an string HTML output, like this library or image above
My attempts:
Get the social media url typed by user based on link, use that typed link to determined what social media, and use react-social-media-embed to give me an output link image above.
I belive(maybe) that the output from react-social-media-embed is jsx, and I need to convert it to html, and parsed it to string.
I have highcharts3d and I wanted to implement Anaglyph effect with highcharts3d
the below code is working component, You can copy paste this code into any of the reactJS application Only thing is not working in below example is anaglyph effect.
Expecting some suggestions or changes based on my current code
Thanks in advance.
import React, { useState, useEffect, useRef } from 'react';
import * as THREE from 'three';
import { AnaglyphEffect } from 'three/examples/jsm/effects/AnaglyphEffect.js'
import HighchartsReact from 'highcharts-react-official'
import Highcharts from 'highcharts'
import highcharts3d from 'highcharts/highcharts-3d'
import highchartsMore from 'highcharts/highcharts-more'
highcharts3d(Highcharts)
highchartsMore(Highcharts)
const HChart = () => {
const chartRef = useRef(null)
const [chartOptions] = useState({
chart: {
type: "column",
options3d: {
enabled: true,
},
events: {
render: (event) => {
event.target.reflow()
},
},
},
title: {
text: '3D Chart',
},
accessibility: {
enabled: false,
},
series: [
{
data: [],
color: 'rgba(0,255,255,0.5)'
},
]
})
useEffect(() => {
// const container = document.createElement('div');
const container = document.querySelector('.highcharts-container')
let camera, scene, renderer, effect;
init()
animate()
function init() {
camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.01,
100,
)
camera.position.z = 3
camera.focalLength = 3
scene = new THREE.Scene()
renderer = new THREE.WebGLRenderer()
renderer.setPixelRatio(window.devicePixelRatio)
container.appendChild(renderer.domElement)
// chartRef.current.appendChild(renderer.domElement) // This line is giving error
const width = window.innerWidth || 2
const height = window.innerHeight || 2
effect = new AnaglyphEffect(renderer)
effect.setSize(width, height)
window.addEventListener('resize', onWindowResize)
}
function animate() {
requestAnimationFrame(animate)
render()
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
effect.setSize(window.innerWidth, window.innerHeight)
}
function render() {
effect.render(scene, camera)
}
}, [])
useEffect(() => {
const baseData = [49.9, 71.5, 106.4, 129.2, 144.0, 176.0, 135.6, 148.5, 216.4, 194.1, 95.6, 54.4];
const distance = 0.05;
const anaglyphicData = [];
baseData.forEach((dataEl, index) => {
anaglyphicData.push(
[index, dataEl], {
x: index - distance,
y: dataEl + distance,
color: 'rgba(255,0,0,0.5)'
}
);
});
chartRef.current.chart.series[0].setData(anaglyphicData)
},[])
function addEvents(H, chart) {
function dragStart(eStart) {
eStart = chart.pointer.normalize(eStart)
let posX = eStart.chartX,
posY = eStart.chartY,
alpha = chart.options.chart.options3d.alpha,
beta = chart.options.chart.options3d.beta,
sensitivity = 5, // lower is more sensitive
handlers = []
function drag(event) {
// Get e.chartX and e.chartY
event = chart.pointer.normalize(event)
chart.update(
{
chart: {
options3d: {
alpha: alpha + (event.chartY - posY) / sensitivity,
beta: beta + (posX - event.chartX) / sensitivity,
},
},
},
undefined,
undefined,
false,
)
}
function unbindAll() {
handlers.forEach(function(unbind) {
if (unbind) {
unbind()
}
})
handlers.length = 0
}
handlers.push(H.addEvent(document, 'mousemove', drag))
handlers.push(H.addEvent(document, 'touchmove', drag))
handlers.push(H.addEvent(document, 'mouseup', unbindAll))
handlers.push(H.addEvent(document, 'touchend', unbindAll))
}
H.addEvent(chart.container, 'mousedown', dragStart)
H.addEvent(chart.container, 'touchstart', dragStart)
// chartContainer = chart.container
}
return (
<div className="chartContainer" >
<HighchartsReact
highcharts={Highcharts}
ref={chartRef}
options={chartOptions}
allowChartUpdate={true}
containerProps={{ style: { width: '100%', height: '100%' } }}
callback={function(chart) {
addEvents(Highcharts, chart)
}}
/>
</div>
)
}
export default HChart;
There is not any integration between Highcharts and threejs. However, implementing an anaglyph effect seems to be really simple, for example by duplicating data.
For example:
const baseData = [1, 2, 3, 4, 5];
const distance = 0.05;
const anaglyphicData = [];
baseData.forEach((dataEl, index) => {
anaglyphicData.push(
[index, dataEl], {
x: index - distance,
y: dataEl + distance,
color: 'rgba(255,0,0,0.5)'
}
);
});
Highcharts.chart('container', {
...,
series: [{
data: anaglyphicData,
color: 'rgba(0,255,255,0.5)'
}]
});
Live demo: https://jsfiddle.net/BlackLabel/csrzj4d3/
Docs: https://www.highcharts.com/docs/chart-concepts/3d-charts
I am new to React-mapbox GL. I have tried for a while now to look at the examples but can't figure out how to change the layer's color on enter/hover. I have 2 questions so far.
map.on('mouseenter', 'clusters', () => {
map.getCanvas().style.cursor = 'pointer';
});
How can I define the the cluster element for each function in Reactmapbox gl? I don't quite understand how the interactiveLayerIds works I suppose?
question 2.
const onMouseEnter = useCallback(event =>{
if (event.features[0].layer.id==="unclustered-point"){
/* console.log(event.features[0].layer.paint.'circle-color') */
}
})
I have attempted this so far(the whole code is below) but it tells me that circle-color is a unexpected token. OnEnter this unclustered-point layer I want to change the color of the element so the user can clearly see what element they are hovering over? How would I go about doing this in React mapbox gl if I cant change the circle color?
THE WHOLE CODE:
import React, { useContext, useEffect, useRef,useState,useCallback } from 'react';
import './MapViews.css';
import { useNavigate } from 'react-router-dom';
import ReactMapGL, { Marker, Layer, Source } from 'react-map-gl';
import SourceFinder from '../../Apis/SourceFinder';
import { SourceContext } from '../../context/SourceContext';
import { clusterLayer, clusterCountLayer, unclusteredPointLayer } from './Layers';
const MapView = () => {
const navigate = useNavigate()
const { sources, setSources } = useContext(SourceContext)
const [viewport, setViewport] = React.useState({
longitude: 10.757933,
latitude: 59.91149,
zoom: 12,
bearing: 0,
pitch: 0
});
const mapRef = useRef(null);
function getCursor({isHovering, isDragging}) {
return isDragging ? 'grabbing' : isHovering ? 'pointer' : 'default';
}
useEffect(() => {
const fetchData = async () => {
try {
const response = await SourceFinder.get("/sources");
setSources(response.data.data.plass);
} catch (error) { }
};
fetchData();
}, [])
const onMouseEnter = useCallback(event =>{
if (event.features[0].layer.id==="unclustered-point"){
/* console.log(event.features[0].layer.paint.'circle-color') */
}
})
const ShowMore = event => {
if(event.features[0].layer.id==="unclustered-point"){
const feature = event.features[0];
console.log(feature)
mapRef.current.getMap().getCanvas().style.cursor="pointer"
}else{
const feature = event.features[0];
const clusterId = feature.properties.cluster_id;
const mapboxSource = mapRef.current.getMap().getSource('stasjoner');
mapboxSource.getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) {
return;
}
setViewport({
...viewport,
longitude: feature.geometry.coordinates[0],
latitude: feature.geometry.coordinates[1],
zoom,
transitionDuration: 500
});
});
}
};
return (
<ReactMapGL {...viewport} width="100%" height="100%" getCursor={getCursor} onMouseEnter={onMouseEnter} interactiveLayerIds={[clusterLayer.id,unclusteredPointLayer.id]} mapboxApiAccessToken={"SECRET"} clickRadius={2} onViewportChange={setViewport} mapStyle="mapbox://styles/mapbox/streets-v11" onClick={ShowMore} ref={mapRef}>
<Source id="stasjoner" type="geojson" data={sources} cluster={true} clusterMaxZoom={14} clusterRadius={50} >
<Layer {...clusterLayer} />
<Layer {...clusterCountLayer} />
<Layer {...unclusteredPointLayer}/>
</Source>
</ReactMapGL>
);
};
export default MapView;
LAYERS.JS
//Hvergang vi skal ha 2 eller flere baller
export const clusterLayer = {
id: 'clusters',
type: 'circle',
source: 'stasjoner',
filter: ['has', 'point_count'],
paint: {
'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 100, '#f1f075', 500, '#f28cb1'],
'circle-radius': ['step', ['get', 'point_count'], 20, 100, 30, 750, 40],
}
};
//Dette er tallene som er inne i ballene
export const clusterCountLayer = {
id: 'cluster-count',
type: 'symbol',
source: 'stasjoner',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12,
}
};
//Per punkt
export const unclusteredPointLayer = {
id: 'unclustered-point',
type: 'circle',
source: 'stasjoner',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 8,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff',
}
};
I'm using REACT-PLOTLY.JS to create a scatter graph. I've got everything working apart from the graph redrawing it self every-time I change a data point when using a list of objects in the array data prop. BUT... when I manually write the array and it's containing objects, my graph does not re-draw when I change a data point, which is how it should work. The 2nd solution is not dynamic and is unusable. Can anyone help please?
re-draws graph when data point changes.
<Plot
data={plotlyData}
does not re-draw graph when data point is changed but is not dynamic and therefore unusable.
<Plot
data={[plotlyData[0],plotlyData[1]]}
I'm using functional components.
How plotData is generated. I'm using an API to get the coordinates for the X and Y axis.
import { React, useState } from "react";
import Plot from "react-plotly.js";
import { useQuery } from "react-query";
import axios from "axios";
const PlotlyGraph = (props) => {
const [plot, setPlot] = useState([]);///not in use
const { datasets } = props;
const QueryUuid = datasets.map((d) => {
// console.log("Individual Dataset:", d);
return `${d.age}-${d.kmax}-${d.frontK}-${d.pachymetry}`;
});
const { error, isLoading, data } = useQuery(
`data-${QueryUuid.join("-")}`,
() =>
axios.post("/api/calculate_cxl", {
age_baseline: datasets.map((d) => d.age),
kmax: datasets.map((d) => d.kmax),
front_k1: datasets.map((d) => d.frontK),
tpt2: datasets.map((d) => d.pachymetry),
color: datasets.map((d) => d.color),
})
);
let plotlyData;
if (error !== null || isLoading === true) {
plotlyData = [];
} else {
plotlyData = data.data.map((d, i) => {
return {
x: d.x,
y: d.y,
type: "scatter",
mode: "lines",
marker: { color: datasets[i].color },
line: {
width: 3,
},
name: `Patient ${i + 1}`,
showlegend: true,
};
});
}
console.log("plot:", plotlyData);
//- Graph Configuration
const config = {
editable: false,
scrollZoom: true,
displayModeBar: true,
displaylogo: false,
};
return (
<>
<Plot
data={plotlyData}
layout={{
yaxis: { range: [0, 1] },
xaxis: { range: [0, 5] },
autoSize: "true",
title: "Patient Comparison",
}}
style={{ width: "100%", height: " 700px" }}
useResizeHandler={true}
config={config}
revision={0}
// onInitialized={plot}
// onUpdate={(plot) => setPlot(plotlyData)}
/>
</>
);
};
export default PlotlyGraph;
I had a similar issue and was able to figure out how to dynamically update my plot with new data after every API fetch (see State Management). Since I don't have access to the API you are using, I've included my own example.
From my API, I fetch an object that looks like this:
{
28AD7D49F6E13C0A: [69.36, 64.11, 68.69, 62.1, ...],
28DDC649F6003C1C: [69.59, 63.18, 60.63, 63.08, ...],
Time: ['20:50:15', '20:50:17', '20:50:19', '20:50:21', ...]
}
Every two seconds, that objects gets updated with a new item in each array. When the state data gets updated with setData, the state object gets updated, which then causes the plot to render with new data.
Full example:
import React, { useState, useEffect } from "react";
import Plot from "react-plotly.js";
function TemperatureGraph() {
const [data, setData] = useState({});
const state = {
data: [
{
x: data["Time"],
y: data["28AD7D49F6E13C0A"],
name: "28AD7D49F6E13C0A",
type: "scatter",
mode: "lines+markers",
marker: {
color: "red",
},
},
{
x: data["Time"],
y: data["28DDC649F6003C1C"],
name: "28DDC649F6003C1C",
type: "scatter",
mode: "lines+markers",
marker: {
color: "black",
},
},
],
layout: {
width: 800,
height: 500,
title: "",
xaxis: {
title: "Time",
},
yaxis: {
title: "Temperature (F)",
},
},
frames: [],
config: {},
};
useEffect(() => {
const timer = setInterval(() => {
fetch("/get-temperatures")
.then((response) => response.json())
.then((data) => setData(data));
}, 2000);
return () => clearInterval(timer);
}, []);
return (
<div>
<Plot data={state.data} layout={state.layout} />
</div>
);
}
export default TemperatureGraph;
I tried to refactor the code here using a functional component instead of a class component and am seeing that the state the copy event handler picks up is the initial state. I tried adding other copy event handlers and found the same behavior and was wondering how I can address this so that it can pick up the current state instead.
import React, { useState, useEffect, Component } from "react";
import ReactDOM from "react-dom";
import { range } from 'lodash';
import ReactDataGrid from 'react-data-grid'; // Tested with v5.0.4, earlier versions MAY NOT HAVE cellRangeSelection
import "./styles.css";
function App() {
return (
<div className="App">
<MyDataGrid />
</div>
);
}
const columns = [
{ key: 'id', name: 'ID', editable: true },
{ key: 'title', name: 'Title', editable: true },
{ key: 'count', name: 'Complete', editable: true },
{ key: 'sarah', name: 'Sarah', editable: true },
{ key: 'jessica', name: 'Jessica', editable: true },
];
const initialRows = Array.from(Array(1000).keys(), (_, x) => (
{ id: x, title: x * 2, count: x * 3, sarah: x * 4, jessica: x * 5 }
));
const defaultParsePaste = str => (
str.split(/\r\n|\n|\r/)
.map(row => row.split('\t'))
);
const MyDataGrid = props => {
const [state, setState] = useState({
rows: initialRows,
topLeft: {},
botRight: {},
});
useEffect(() => {
// Copy paste event handler
document.addEventListener('copy', handleCopy);
document.addEventListener('paste', handlePaste);
return () => {
document.removeEventListener('copy', handleCopy);
document.removeEventListener('paste', handlePaste);
}
}, [])
const rowGetter = (i) => {
const { rows } = state;
return rows[i];
}
const updateRows = (startIdx, newRows) => {
setState((state) => {
const rows = state.rows.slice();
for (let i = 0; i < newRows.length; i++) {
if (startIdx + i < rows.length) {
rows[startIdx + i] = { ...rows[startIdx + i], ...newRows[i] };
}
}
return { rows };
});
}
const handleCopy = (e) => {
console.debug('handleCopy Called');
e.preventDefault();
const { topLeft, botRight } = state;
// Loop through each row
const text = range(topLeft.rowIdx, botRight.rowIdx + 1).map(
// Loop through each column
rowIdx => columns.slice(topLeft.colIdx, botRight.colIdx + 1).map(
// Grab the row values and make a text string
col => rowGetter(rowIdx)[col.key],
).join('\t'),
).join('\n');
console.debug('text', text);
e.clipboardData.setData('text/plain', text);
}
const handlePaste = (e) => {
console.debug('handlePaste Called');
e.preventDefault();
const { topLeft } = state;
const newRows = [];
const pasteData = defaultParsePaste(e.clipboardData.getData('text/plain'));
console.debug('pasteData', pasteData);
pasteData.forEach((row) => {
const rowData = {};
// Merge the values from pasting and the keys from the columns
columns.slice(topLeft.colIdx, topLeft.colIdx + row.length)
.forEach((col, j) => {
// Create the key-value pair for the row
rowData[col.key] = row[j];
});
// Push the new row to the changes
newRows.push(rowData);
});
console.debug('newRows', newRows);
updateRows(topLeft.rowIdx, newRows);
}
const onGridRowsUpdated = ({ fromRow, toRow, updated, action }) => {
console.debug('onGridRowsUpdated!', action);
console.debug('updated', updated);
if (action !== 'COPY_PASTE') {
setState((state) => {
const rows = state.rows.slice();
for (let i = fromRow; i <= toRow; i++) {
rows[i] = { ...rows[i], ...updated };
}
return { rows };
});
}
};
const setSelection = (args) => {
console.log('setting... >>', args)
setState({
...state,
topLeft: {
rowIdx: args.topLeft.rowIdx,
colIdx: args.topLeft.idx,
},
botRight: {
rowIdx: args.bottomRight.rowIdx,
colIdx: args.bottomRight.idx,
},
});
};
const { rows } = state;
return (
<div>
<ReactDataGrid
columns={columns}
rowGetter={i => rows[i]}
rowsCount={rows.length}
onGridRowsUpdated={onGridRowsUpdated}
enableCellSelect
minColumnWidth={40}
cellRangeSelection={{
onComplete: setSelection,
}}
/>
</div>
);
}
export default MyDataGrid;
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
The second parameter that you pass to useEffect tells react to skip the effect unless one of the items in the array has changed. You passed in an empty array, so other than the initial render, it will never update. Thus, you set up the event listeners with functions that have the original state in their closure, and nothing else.
To get it to update as state changes, either remove the array, or fil it with variables that your code depends on:
useEffect(() => {
document.addEventListener('copy', handleCopy);
document.addEventListener('paste', handlePaste);
return () => {
document.removeEventListener('copy', handleCopy);
document.removeEventListener('paste', handlePaste);
}
}, [state])