React Component, Highstock: Synchronize multiple charts? - reactjs

I am working with React and HighCharts. I am relatively new to both these technologies. I need to generate two synchronized HighStock charts. I was able to display the charts with the below layout.
<div class=container>
<div class=chart1>new highcharts.StockChart(newChartOptions) </div>
<div class=chart2>new highcharts.StockChart(newChartOptions)</div>
</div>
The Charts are displayed. I want to synchronize the charts to have a common tool tip, I see the http://www.highcharts.com/demo/synchronized-charts , not sure how to implement with React. I have tried to assign a function(handleEvent(e)) to plotOptions:{line:{ point:{ event:{click: and MouseOver}}}} but it did not help. Not sure how to invoke the handleEvent(e) method. I am not sure how/when to invoke the handleEvent(e). Any help is greatly is appreciated.
Below is the Component code:
import $ from 'jQuery';
import React from 'react';
import highcharts from 'highcharts-release/highstock';
export default class SynchronizedStatusChart extends React.Component {
constructor (props) {
super(props);
this.state = {
chartName: `chart${this.props.chartNum}`,
};
}
handleEvent(e){
let allCharts = highcharts.charts;
console("SynchronizedStatusChart:handleEvent:ChartsLength = " + allCharts.length);
var chart, point, i, event;
for (i = 0; i < allCharts.length; i = i + 1)
{
chart = highcharts.charts[i];
event = chart.pointer.normalize(e.originalEvent); // Find coordinates within the chart
point = chart.series[0].searchPoint(event, true); // Get the hovered point
if (point) {
this.onMouseOver(); // Show the hover marker
this.series.chart.tooltip.refresh(this); // Show the tooltip
this.series.chart.xAxis[0].drawCrosshair(event, this);
}
}
}
componentDidMount () {
}
componentWillUpdate (nextProps) {
for(let i=0; i<nextProps.data.length; i++){
this.generateChart(nextProps.data[i],i+1,nextProps.titles[i]);
}
}
generateChart(data, i, title) {
if(data == null)
{
data = [];
}
let ticksData = [0,1];
let newChartOptions =
{
chart: {
//renderTo: document.getElementById(this.state.chartName),
renderTo: document.getElementById(`SyncChart${i}`),
height:'125'
},
rangeSelector: {
enabled: false
},
credits: {
enabled: false
},
navigator: {
enabled: false
},
scrollbar: {
enabled: false
},
tooltip: {
shared: true,
pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y:.2f}</b> <br/>'
},
xAxis:{
},
yAxis: {
offset: 15,
labels: {
align: 'center',
x: -3,
y: 6
},
tickPositioner: function () {
var positions = ticksData;
return positions;
},
opposite:false,
showLastLabel: true,
title:{
text:title
}
},
series: [{
name: title,
type: this.props.status ? 'area' : 'line',
data: data,
showInNavigator: false
}],
};
new highcharts.StockChart(newChartOptions);
}
render () {
return (
<div className="med-chart col-md-9" id={this.state.chartName} style={this.props.chartStyle}>
<div id='SyncChart1'></div>
<div id='SyncChart2'></div>
</div>
);
}
}

I had the same problem recently. Here's what has worked for me.
I am using common parent component to add pure javascript event listeners for 'mousemove' and 'mouseleave' to each chart DOM element.
class ParentComponent extends Component {
...
componentDidMount() {
this.charts = document.querySelectorAll('.chart-container');
[].forEach.call(this.charts, (chart) => {
chart.addEventListener('mousemove', this.handleChartMouseMove);
chart.addEventListener('mouseleave', this.handleChartMouseLeave);
});
Highcharts.Pointer.prototype.reset = () => undefined;
}
componentWillUnmount() {
[].forEach.call(this.charts, (chart) => {
chart.removeEventListener('mousemove', this.handleChartMouseMove);
chart.removeEventListener('mousemove', this.handleChartMouseLeave);
});
}
handleChartMouseMove = (e) => {
const chartIndex = e.currentTarget.dataset.highchartsChart;
const chart = Highcharts.charts[chartIndex];
const event = chart.pointer.normalize(e);
const pointIndex = chart.series[0].searchPoint(event, true).index;
Highcharts.charts.forEach((chart) => {
const xAxis = chart.xAxis[0];
const point = chart.series[0].points[pointIndex];
const points = chart.series.map(s => s.points[pointIndex]); // if more than one series
point.onMouseOver();
xAxis.drawCrosshair(event, point);
// if more than one series, pass an array of points, took a me a long time to figure it out
chart.tooltip.refresh(points, event);
});
};
handleChartMouseLeave = () => {
Highcharts.charts.forEach((chart) => {
chart.tooltip.hide();
chart.xAxis[0].hideCrosshair();
});
};
...
}

Related

Gutenberg - Call google map render function in save after DOM has been rendered

I have a bit of a dilemma.
In the save function I need to call a function renderMap that renders a dynamic google map. However I need to call it after the DOM has been rendered. I can't seem to find a solution for this. I realised you can't add a class to the save function with the React lifecycle so I am stopped. It does work for the edit function though. What are the possibilities?
import { __ } from '#wordpress/i18n';
import { registerBlockType } from '#wordpress/blocks';
import { PluginDocumentSettingPanel } from '#wordpress/edit-post';
import { Component } from '#wordpress/element';
const renderMap = function() {
let googleMap = document.getElementById('google-map')
let map
map = new google.maps.Map(googleMap, {
center: { lat: 37.79406, lng: -122.4002 },
zoom: 14,
disableDefaultUI: true,
})
}
registerBlockType( 'splash-blocks/google-maps', {
title: __('Google maps locations', 'google-maps'),
icon: 'megaphone',
category: 'common',
keyword: [
__( 'Display Google maps locations' ),
],
atrributes: {
markers: {
type: 'object'
},
address: {
type: 'string',
default: 'xxxxxxxxx',
},
api_key: {
type: 'string',
default: 'xxxxxxxxx',
}
},
edit: class extends Component {
constructor(props) {
super(props)
}
componentDidMount() {
renderMap()
}
render() {
const { attributes, setAttributes } = this.props
return (
<div id='google-map'>
</div>
)
}
},
save: props => {
const {
className,
attributes: { mapHTML }
} = props;
renderMap()
return (
<div id='google-map'>
</div>
)
}
})

How do I get BootstrapTable to redraw with updated data?

I have a Table from react-bootstrap that displays a list of devices with several columns. I want to change this so you can reorder by the columns so I switched to BootstrapTable from react-bootstrap-table-next. But my problem is that changing the state of the Component doesn't cause the table to update - this only happens if I click on a column to reorder the table.
The code is written to create the devices_table and save it to state then call an API to get the device version and add that to the state, causing the component to redraw. But when render() is called again the additional data isn't added to the table.
I've created a working example at https://codesandbox.io/s/react-bootstrap-table-next-new-data-problem-5w0op
import React, { Component } from 'react'
import BootstrapTable from 'react-bootstrap-table-next'
class DeviceTable extends Component {
constructor(props) {
super(props)
this.state = {
devices_table: {}
}
}
componentDidUpdate(prevProps) {
if (prevProps.devices !== this.props.devices) {
let devices_table = this.props.devices.map(this.myFunction);
this.setState({
devices_table: devices_table
})
}
}
myFunction = (value, index, array) => {
let device = {};
device.device = value;
device.index = index + 1;
device.version = '';
apiGetDeviceVersion(value.identifier)
.then((res, deviceId = value.identifier) => {
let devices_table = this.state.devices_table;
let objIndex = devices_table.findIndex(d => d.device.identifier === deviceId)
devices_table[objIndex].version = res.valueReported;
this.setState({
devices_table: devices_table
})
})
.catch(e => {
console.log(e)
})
return device;
}
render() {
const devices = this.state.devices_table;
if (isEmpty(devices)) {
return (<div></div>)
}
let columns = [
{
text: "#",
dataField: "index",
sort: true
},
{
text: "ID",
dataField: "device.identifier",
sort: true
},
{
text: "Name",
dataField: "device.name",
sort: true
},
{
text: "Status",
dataField: "device.status",
sort: true
},
{
text: "Version",
dataField: "version",
sort: true
}
];
return (
<div>
<BootstrapTable keyField={"device.identifier"} data={devices} columns={columns}></BootstrapTable>
</div>
)
}
}
export default DeviceTable
componentDidUpdate(prevProps) {
if (prevProps.devices !== this.props.devices) {
*let devices_table* = this.props.devices.map(this.myFunction);
this.setState({
devices_table: devices_table
})
}
}
It may happen due to response is working as asynchronous function(because of API). Thus, you have to wait for the response to make it available, otherwise "devices_table" remains empty or undefined and leads to no data visualization.

set Highstock xAxis range within options object

My React components pulls data for an API. The options.series.data data for the yAxis is what receives the API data.
The component is enabled to pull the data range for day/hour/minute which comes with a datestamp for when the data was recorded. How do I dynamically set the xAxis min/max range to respect the day/hour/minute duration change?
The HighchartsReact instance receives the data series via the options object that's where I'd like to setup the dynamic xAxis handler method. Perhaps it's setExtemes().
The component code is below.
import React, { Fragment, useState, useEffect } from 'react';
import { connect } from 'react-redux';
import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import highchartsStockChart from 'highcharts/modules/stock';
import { getDaily, getHourly, getMinute } from '../actions/getData';
import Spinner from './Spinner';
Highcharts.setOptions({
lang: {
thousandsSep: ','
}
});
highchartsStockChart(Highcharts);
const Chart = ({
match,
list: { list, loading },
getDaily,
getHourly,
getMinute,
error
}) => {
const [method, setMethod] = useState(() => getDaily);
useEffect(() => {
method(match.params.currency.toUpperCase(), 30);
}, [match.params.currency, method]);
console.log('Chart.js list:', list);
console.log('Chart.js error:', error);
console.log('Chart.js loading:', loading);
const options = {
title: {
text: 'Close Price'
},
series: [{
name: 'close price',
data: list.map(item => item.close),
tooltip: {
pointFormat: 'close price: ${point.y:,.2f}'
},
animation: false
}],
scrollbar: {
enabled: false
},
navigator: {
enabled: false
},
rangeSelector: {
inputEnabled: false,
allButtonsEnabled: true,
buttonSpacing: 10,
buttonPosition: {
align: 'left'
},
buttons: [{
type: 'day',
count: 1,
text: 'Day',
events: {
click: () => setMethod(() => getDaily)
}
}, {
type: 'hour',
count: 1,
text: 'Hour',
events: {
click: () => setMethod(() => getHourly)
}
}, {
type: 'minute',
count: 1,
text: 'Minute',
events: {
click: () => setMethod(() => getMinute)
}
}]
}
};
let content;
if (error && error.message) {
content = error.message;
} else if (!list.length || loading) {
content = <Spinner />;
} else {
content = (
<Fragment>
{list.map(item => <span key={item.time}>{item.close} </span>)}
<button onClick={() => setMethod(() => getHourly)}>set Hourly</button>
<button onClick={() => setMethod(() => getMinute)}>set Minute</button>
<HighchartsReact
highcharts={Highcharts}
options={options}
constructorType={'stockChart'}
/>
</Fragment>
);
}
return (
<div>
Chart.
{content}
</div>
);
};
const mapStateToProps = state => ({
list: state.data,
error: state.error
});
export default connect(mapStateToProps, { getDaily, getHourly, getMinute })(Chart);
You can:
use chart redraw event callback function and call setExtremes:
chart: {
events: {
redraw: function() {
if (allowChartRedraw) {
allowChartRedraw = false;
this.xAxis[0].setExtremes(0, Math.random() * 3);
}
allowChartRedraw = true;
}
}
}
Live example: http://jsfiddle.net/BlackLabel/wvpnct9h/
API Reference: https://api.highcharts.com/highcharts/chart.events.redraw
keep all of the options in a state and manipulate axis extremes by min and max properties:
componentDidMount() {
this.setState({
chartOptions: {
series: [
{
data: [Math.random() * 3, Math.random() * 3, Math.random() * 3]
}
],
xAxis: {
min: 0,
max: Math.random() * 3
}
}
});
}
Live demo: https://codesandbox.io/s/highcharts-react-demo-jo6nw
get the chart reference and call setExtremes on the xAxis.
Docs: https://github.com/highcharts/highcharts-react#how-to-get-a-chart-instance

React ChartJS prevent new data from being added into state after it's redrawn?

I'm trying to update my chart using react-chartjs-2. I'm using date picker to filter different data and re-render the chart accordingly such as display data today, yesterday, last 7 days etc.. The data is being fetched from my database
However, when the chart gets redrawn, and re-rendered, it gets added onto the state which I don't want. I simply want to re-render the new data that was requested not re-render and add onto the old data that was in the chart.
I actually fixed this issue before with vanilla JavaScript because I wasn't using react I used the destroy() method that the chart documentation says to use but i'm not sure how to use it with react?
So upon further inspection, it seems my chart is re-rendering fine. However, when it gets re-rendered, additional data is being added onto my chartData state which I don't want, I just want the new data that got requested to show on the chart. I'm still trying to figure that portion out.
Here is my code I have a lot of it so I'll only show the relevant parts:
import React from "react";
import reportsService from "../../services/reportsService";
import update from "react-addons-update";
import moment from "moment";
import { Bar } from "react-chartjs-2";
import "chartjs-plugin-labels";
import "chartjs-plugin-datalabels";
class Reportspage extends React.Component {
constructor(props) {
super(props);
this.state = {
chartData: {
labels: [],
datasets: [
{
//label: "Quotes",
data: [],
backgroundColor: []
}
]
}
};
}
chartColors() {
let colors = [];
for (let i = 0; i < 100; i++) {
let r = Math.floor(Math.random() * 200);
let g = Math.floor(Math.random() * 200);
let b = Math.floor(Math.random() * 200);
let c = "rgb(" + r + ", " + g + ", " + b + ")";
colors.push(c);
}
// Update deep nested array of objects in state
this.setState({
chartData: update(this.state.chartData, {
datasets: { 0: { backgroundColor: { $set: colors } } }
})
});
}
datePicker() {
let _this = this;
let start = moment().subtract(29, "days");
let end = moment();
let showReports;
let data;
let reloNames = [];
let reloCount = [];
function focusDate(start, end) {
$("#daterangePicker span").html(
start.format("MMMM D, YYYY") + " - " + end.format("MMMM D, YYYY")
);
}
$("#daterangePicker").daterangepicker(
{
startDate: start,
endDate: end,
ranges: {
Today: [moment(), moment()],
Yesterday: [
moment().subtract(1, "days"),
moment().subtract(1, "days")
],
"Last 7 Days": [moment().subtract(6, "days"), moment()],
"Last 30 Days": [moment().subtract(29, "days"), moment()],
"This Month": [moment().startOf("month"), moment().endOf("month")],
"Last Month": [
moment()
.subtract(1, "month")
.startOf("month"),
moment()
.subtract(1, "month")
.endOf("month")
]
}
},
focusDate
);
focusDate(start, end);
$("#daterangePicker").on("apply.daterangepicker", async function(
event,
picker
) {
switch (picker.chosenLabel) {
case "Today":
showReports = await reportsService.reloQuotes({
reportStatus: "Today"
});
data = showReports.recordsets[0];
data.forEach((element, index, array) => {
reloNames.push(element.reloNames);
reloCount.push(element.NoofOrders);
});
_this.setState({
chartData: update(_this.state.chartData, {
labels: { $set: reloNames },
datasets: { 0: { data: { $set: reloCount } } }
})
});
console.log(_this.state);
break;
case "Yesterday":
showReports = await reportsService.reloQuotes({
reportStatus: "Yesterday"
});
data = showReports.recordsets[0];
data.forEach((element, index, array) => {
reloNames.push(element.reloNames);
reloCount.push(element.NoofOrders);
});
_this.setState({
chartData: update(_this.state.chartData, {
labels: { $set: reloNames },
datasets: { 0: { data: { $set: reloCount } } }
})
});
console.log(_this.state);
break;
case "Last 7 Days":
showReports = await reportsService.reloQuotes({
reportStatus: "Last 7 Days"
});
data = showReports.recordsets[0];
data.forEach((element, index, array) => {
reloNames.push(element.reloNames);
reloCount.push(element.NoofOrders);
});
_this.setState({
chartData: update(_this.state.chartData, {
labels: { $set: reloNames },
datasets: { 0: { data: { $set: reloCount } } }
})
});
console.log(_this.state);
break;
}
});
//console.log(this.state);
}
async reloQuotes() {
const showreloQuotes = await reportsService.reloQuotes();
let data = showreloQuotes.recordsets[0];
let reloNames = [];
let reloCount = [];
data.forEach((element, index, array) => {
reloNames.push(element.reloNames);
reloCount.push(element.NoofOrders);
});
this.setState({
chartData: update(this.state.chartData, {
labels: { $set: reloNames },
datasets: { 0: { data: { $set: reloCount } } }
})
});
}
async componentDidMount() {
await this.chartColors();
await this.datePicker();
// Execute models real time thus re-rendering live data on the chart real time
await this.reloQuotes();
}
render() {
return (
<div className="fluid-container">
<div className="container">
<h1>Reports</h1>
<div className="row">
<div className="daterangeContainer">
<div
id="daterangePicker"
style={{
background: "#fff",
cursor: "pointer",
padding: "5px 10px",
border: "1px solid #ccc",
width: "100%"
}}
>
<i className="fa fa-calendar" />
<span /> <i className="fa fa-caret-down" />
</div>
</div>
</div>
<div className="row">
<div className="col-md-12">
<Bar
data={this.state.chartData}
height={800}
options={{
maintainAspectRatio: false,
legend: {
display: false
},
scales: {
xAxes: [
{
ticks: {
beginAtZero: true,
autoSkip: false
},
scaleLabel: {
display: true
}
}
]
},
title: {
display: true,
text: "Quotes",
fontSize: 16
},
plugins: {
datalabels: {
display: true,
color: "white"
}
}
}}
redraw
/>
</div>
</div>
</div>
</div>
);
}
}
export default Reportspage;
So the problem that you describe is that data is added to your component state when the component is re-rendered. In the code snipped you have supplied, you dont use any of the life cycle methods in React that can be triggered on re-renders. And I cannot see any other hooks that should trigger on a re-render. Therefore I cant find any source to your problem.
However, I can see other issues that might make debugging harder for you. Solving these might help you in nailing down the actual problem. In the componentDidMount method you call functions whose only purpose is to update the state. This is not good design since it will force the component to immediately re-render several times whenever it is mounted.
A better design is to fully prepare the chartData-objekt in the constructor. For example, you could change your chartColors function to take a chartData object as a parameter, and return the new object with colors added. Then make your constructor look something like this:
constructor(props) {
super(props);
const chartDataWithoutColors = {
labels: [],
datasets: [
{
//label: "Quotes",
data: [],
backgroundColor: []
}
]
}
const chartDataWithColor = this.chartColors(chartDataWithoutColors);
this.state = {
chartData: chartDataWithColor
};
}
By removing unnecessary calls to setState you will make your components life both simpler and more performant. When you have simplified your component, start debugging by removing non-critical parts one at the time and try to nail down when your problem disappears. This should be sufficient to find the bug.
Good luck!

How to display the graph after it is stabilized (vis.js)?

I am rendering a graph using implementation of vis network as shown here. Right now the graph is taking some time to stabilize after being rendered. But I want the network to be stabilized before being displayed. I tried using the stabilization options under the physics module, but I could not achieve it.
The following is my Graph Component.
import {default as React, Component} from 'react';
import isEqual from 'lodash/isEqual';
import differenceWith from 'lodash/differenceWith';
import vis from 'vis';
import uuid from 'uuid';
import PropTypes from 'prop-types';
class Graph extends Component {
constructor(props) {
super(props);
const {identifier} = props;
this.updateGraph = this.updateGraph.bind(this);
this.state = {
identifier : identifier !== undefined ? identifier : uuid.v4()
};
}
componentDidMount() {
this.edges = new vis.DataSet();
this.edges.add(this.props.graph.edges);
this.nodes = new vis.DataSet();
this.nodes.add(this.props.graph.nodes);
this.updateGraph();
}
shouldComponentUpdate(nextProps, nextState) {
let nodesChange = !isEqual(this.nodes.get(), nextProps.graph.nodes);
let edgesChange = !isEqual(this.edges.get(), nextProps.graph.edges);
let optionsChange = !isEqual(this.props.options, nextProps.options);
let eventsChange = !isEqual(this.props.events, nextProps.events);
if (nodesChange) {
const idIsEqual = (n1, n2) => n1.id === n2.id;
const nodesRemoved = differenceWith(this.nodes.get(), nextProps.graph.nodes, idIsEqual);
const nodesAdded = differenceWith(nextProps.graph.nodes, this.nodes.get(), idIsEqual);
const nodesChanged = differenceWith(differenceWith(nextProps.graph.nodes, this.nodes.get(), isEqual), nodesAdded);
this.patchNodes({nodesRemoved, nodesAdded, nodesChanged});
}
if (edgesChange) {
const edgesRemoved = differenceWith(this.edges.get(), nextProps.graph.edges, isEqual);
const edgesAdded = differenceWith(nextProps.graph.edges, this.edges.get(), isEqual);
this.patchEdges({edgesRemoved, edgesAdded});
}
if (optionsChange) {
this.Network.setOptions(nextProps.options);
}
if (eventsChange) {
let events = this.props.events || {}
for (let eventName of Object.keys(events))
this.Network.off (eventName, events[eventName])
events = nextProps.events || {}
for (let eventName of Object.keys(events))
this.Network.on (eventName, events[eventName])
}
return false;
}
componentDidUpdate() {
this.updateGraph();
}
patchEdges({edgesRemoved, edgesAdded}) {
this.edges.remove(edgesRemoved);
this.edges.add(edgesAdded);
}
patchNodes({nodesRemoved, nodesAdded, nodesChanged}) {
this.nodes.remove(nodesRemoved);
this.nodes.add(nodesAdded);
this.nodes.update(nodesChanged);
}
updateGraph() {
let options = this.props.options;
this.Network = new vis.Network(
this.refs.nw,
Object.assign(
{},
this.props.graph,
{
edges: this.edges,
nodes: this.nodes
}
),
options
);
if (this.props.getNetwork) {
this.props.getNetwork(this.Network)
}
// Add user provided events to network
let events = this.props.events || {};
for (let eventName of Object.keys(events)) {
this.Network.on(eventName, events[eventName]);
}
}
render(){
return (<div ref="nw" style={{width:'100%' , height: '480px'}}/>);
}
}
Graph.defaultProps = {
graph: {},
style: { width: '100%', height: '480px' }
};
Graph.propTypes = {
graph: PropTypes.object,
style: PropTypes.object,
getNetwork: PropTypes.func
};
export default Graph;
This is my options object
let options = {
layout: {
hierarchical: false
},
autoResize: false,
edges: {
smooth: false,
color: '#000000',
width: 0.5,
arrows: {
to: {
enabled: true,
scaleFactor: 0.5
}
}
}
};
Any help would be greatly appreciated.
Thanks in advance !
You mention that you tried enabling stabilization without success. The following in the options should work:
physics: {
stabilization: {
enabled: true,
iterations: 5000 // YMMV
}
}
Is this different from what you tried?

Resources