I'm using Highchart inside React application. I want to make animated effect for Highcart.
For instance, it's to show the progress of uploading other data. I pass the progress via this.props.progress. However, I cannot pass props to data property in Highchart.
Is it possible to make an animated effect with updating data? What could be the best practice?
Highchart.js
class Highchart extends React.Component {
static propTypes = {
data: React.PropTypes.array,
text: React.PropTypes.string,
colors: React.PropTypes.array,
size: React.PropTypes.any,
bgcolor: React.PropTypes.string,
width: React.PropTypes.number
}
constructor (props) {
super(props)
this.state = {
uuid: uuid()
}
}
componentDidMount () {
Highcharts.chart(this.state.uuid, {
chart: {
renderTo: 'container',
type: 'pie',
width: this.props.width,
backgroundColor: this.props.bgcolor
},
title: {
text: this.props.text,
useHTML: true,
verticalAlign: 'middle',
floating: true
},
plotOptions: {
pie: {
shadow: false,
allowPointSelect: false,
size: '100%',
dataLabels: { enabled: false }
},
series: {
states: {
hover: {
enabled: false
}
}
}
},
tooltip: { enabled: false },
credits: { enabled: false },
colors:this.props.colors,
series: [{
data: this.props.data,
size: this.props.size,
innerSize: '90%',
showInLegend:false,
dataLabels: {
enabled: false
}
}]
})
}
render () {
return (
<div id={this.state.uuid} className='high-chart' />
)
}
}
export default Highchart
ProgressMeter.js
import React from 'react'
import Highchart from 'components/Highchart'
class ProgressMeter extends React.Component {
static propTypes = {
progress: React.PropTypes.number,
}
render () {
return (
<div
className='signup-percents-meter'
>
<DonutChart data={[this.props.progress, 100-this.props.progress]}
//this code does not work.
colors={['#ee382a', '#eaeaea']}
/>
</div>
)
}
}
export default ProgressMeter
Changing only options will not work for Highcharts - those options are used once for generating chart. If you want to change those options you should call chart.update with new options or a more dedicated update e.g. for series data you can use series.setData.
How others are handling this:
Official Highcharts React wrapper highcharts-react is using chart.update - the relevant code line.
Third party react-highcharts is rebuilding Highcharts chart using new options. It's less optimal, but generally a more secure approach for them (it's third party code, so in case of any bugs they need to wait for the bug to be resolved). Relevant code: call renderChart on updates and creating the chart in the renderChart.
Third party react-highcharts-wrapper also rebuilds a chart on update - here is explained why.
About animation:
When chart is rebuild (created anew) initial animation runs (unless otherwise specified in chart's animation option) and for dynamic updates like chart.update chart is redrawn with animation enabled by default.
Related
First, I would like to emphasize that I am a completely new to Vue (and webdev in general).
I have to make a UI where I have to show some data that I fetch from an API. But I have some issues with chart.js: I can't update my chart when prop change.
Currently, I have the following setup:
Card.vue:
<script>
import DoughnutChart from './DoughnutChart.js'
export default {
name: 'Card',
components: {
DoughnutChart
},
props: ['scheda'],
created() {
console.log(this.scheda)
}
}
</script>
<template>
<DoughnutChart type="abcd" :chartData="this.scheda"/>
</template>
DoughnutChart.js:
import { defineComponent, h } from 'vue'
import { Doughnut } from 'vue-chartjs'
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
ArcElement,
CategoryScale
} from 'chart.js'
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale)
export default defineComponent({
name: 'DoughnutChart',
components: {
Doughnut
},
props: ['type', 'chartData'],
setup(props) {
const chartData = {
labels: ['A', 'B'],
datasets: [
{
backgroundColor: ["#ff3333", "#131b2e"],
cutout: "75%",
borderWidth: 0,
data: [props.chartData.value, (100 - props.chartData.value)]
}
]
}
const chartOptions = {
plugins: {
legend: {
display: false
}
},
responsive: true,
maintainAspectRatio: false
}
return () =>
h(Doughnut, {
chartData,
chartOptions
})
}
})
this.scheda is a reactive value and when it changes the chart should update accordingly.
I have read the documentation, but I simply can't wrap my head around it.
I have also tried searching on the internet, but all examples reference the older version of the chart library.
Can someone help me with this issue? Thanks!
If you want to create a chart js and integrate with view, you can create your canvas on js outside of vue js flow, and then get the element reference inside vue and then integrate it, the big problem rendering components inside vue is that you need update your charts, it may cause unexpected behavior on canvas. Because tools like Vue are designed to refresh the html and canvas always are a single html tag with a lot of interactivity in js(not much html).
Vue-chart
You can install
vue-chart.js
It supports data updates and also you can access to the chart ref.
Updating charts: https://vue-chartjs.org/guide/#updating-charts
Acesing to ref: https://vue-chartjs.org/guide/#access-to-chart-instance
I have a bubble map chart that shows the location of cities on the map. The map has the default label but I want to use a custom react component as the label on the map. This is my source code but it has error and doesn't work:
import React, { Component, Fragment } from "react";
import Highcharts from "highcharts";
import HighchartsReact from "highcharts-react-official";
import HighchartsMap from "highcharts/modules/map";
import mapData from "#highcharts/map-collection/countries/gb/gb-all.geo.json";
import proj4 from "proj4";
import CustomLabel from "./CustomLabel";
HighchartsMap(Highcharts);
class BubbleMapChart extends Component {
render() {
const options = {
chart: {
map: "countries/gb/gb-all",
proj4
},
series: [
{
name: "countries",
nullColor: "#fff",
showInLegend: false,
mapData: mapData
},
{
// Specify points using lat/lon
type: "mapbubble",
// PAY ATTENTION TO THIS SECTION - USE A CUSTOM LABEL COMPONENT
dataLabels: {
enabled: true,
format: <CustomLabel name={"point.name"} />
},
minSize: "5%",
maxSize: "15%",
showInLegend: true,
data: [
{
name: "London",
lat: 51.507222,
lon: -0.1275
},
{
name: "Birmingham",
lat: 52.483056,
lon: -1.893611
}
]
}
]
};
return (
<HighchartsReact
highcharts={Highcharts}
options={options}
constructorType={"mapChart"}
/>
);
}
}
and this is a customLabel component as an example:
import React, { Component } from "react";
class CustomLabel extends Component {
render() {
return (
<div>
{/* Doesn't show this Division (actually doesn't apply the style ...) */}
<div
style={{ BackgroundColor: "red", width: "10px", height: "10px" }}
></div>
<span>{this.props.name}</span>
<br />
{/* Doesn't show the red bullet inside the text */}
<Badge color="#f50" text={this.props.name} />
</div>
);
}
}
export default CustomLabel;
How can I customize the data label in highcharts? actually I want to use a custom component as the label.
Use ReactDOMServer and renderToStaticMarkup or renderToString method in formatter function for data labels:
dataLabels: {
enabled: true,
formatter: function () {
return renderToStaticMarkup(<CustomLabel name={this.point.name} />);
}
}
Live demo: https://codesandbox.io/s/highcharts-react-demo-forked-40icn?file=/demo.jsx
Docs: https://reactjs.org/docs/react-dom-server.html
API Reference: https://api.highcharts.com/highmaps/series.mapbubble.dataLabels.formatter
Or if you need to use some reactive logic inside CustomLabel take advantage of Portals in React.
Docs: https://reactjs.org/docs/portals.html
Example: https://www.npmjs.com/package/highcharts-react-official#how-to-add-react-component-to-a-charts-element
I am trying to create multiple chart in angular but I am not sure the way I try to implement will is correct or not and I am unable to create multiple charts it replacing one with another
<div *ngIf="chartData.length !== 0">
<app-limus-utilisation-chart
*ngFor="let entity of chartData" [chartdata]="entity"
></app-limus-utilisation-chart>
</div>
ChartComponent.ts
getStackedChart() {
const canvas: any = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
var data = {
labels: this.chartdata.buyernames,
datasets: [{
label: 'Utilised Limit',
data: this.chartdata.utilisedlimitData,
backgroundColor: '#22aa99'
}, {
label: 'Available Limit',
data: this.chartdata.availablelimit,
backgroundColor: '#994499'
}]
}
chartJsLoaded$.pipe(take(1)).subscribe(() => {
setTimeout(() => {
this.myChart = new Chart(ctx, {
type: 'bar',
data: data,
options: {
tooltips: {
mode: 'index',
intersect: true,
position: 'custom',
yAlign: 'bottom'
},
scales: {
xAxes: [{
stacked: true
}],
yAxes: [{
stacked: false,
display: false
}]
}
}
});
})
})
}
I tried two ways
using view child the chart not created, getElementById chart created but the second chart replacing the first one. but I want two stacked charts side by side how to achieve this
and the current chart taking below 100 values but as per my actual requirment I need to show tootip amount like (1000000, 700000) that too currency format
Like this I tried to acheive
https://stackblitz.com/edit/angular-chart-js-j26qhm?file=src%2Fapp%2Fapp.component.html
Please give suggestions
after getting answers I acheived few things
https://stackblitz.com/edit/angular-chart-js-tyggan
The problem is this line here:
const canvas: any = document.getElementById('canvas1');
You have multiple elements with that ID on the page (Because you did *ngFor), so it always attaches itself to the first element on the page.
Instead of using getElementByID, you should use Angular's built-in #ViewChild.
Like this:
chart.component.html:
<canvas #stackchartcanvas></canvas>
chart.component.ts:
#ViewChild("stackchartcanvas") myCanvas: ElementRef<HTMLCanvasElement>;
....
....
....
getStackedChart() {
const canvas = this.myCanvas.nativeElement;
}
Stackblitz: https://stackblitz.com/edit/angular-chart-js-kny4en?file=src%2Fapp%2Fchart.component.ts
(Also, in your original code, this.chartData.push() ran EVERY time a checkbox was clicked, even if the checkbox was false, but that's a different, unrelated problem, which has also been fixed.)
You can use ViewChild to reference html element and use it inside you component. I have also modified few things in your code to toggle charts.
To summarize:
Use ViewChild to access html element in your component
Updated app component to toggle charts as opposed to just adding data
Updated label to accept click event to toggle checkbox
Take a look at this stackblitz.
app.component.html
<label>
<input type="checkbox" value=1
(change)="chooseEntity($event.target.checked, 1, entityData[0])">
Microsoft
</label>
<label>
<input type="checkbox" (change)="chooseEntity($event.target.checked, 2, entityData[1])">
IBM
</label>
<div *ngIf="chartData.length !== 0">
<app-limus-utilisation-chart *ngFor="let entity of chartData" [chartdata]="entity"></app-limus-utilisation-chart>
</div>
chart.component.html
<div #chartReport>
<canvas #canvas></canvas>
</div>
chart.component.ts
import {
Component,
ElementRef,
Input,
OnInit,
ViewChild,
ViewEncapsulation
} from "#angular/core";
import { Chart } from "chart.js";
import { OnChanges } from "#angular/core";
#Component({
selector: "app-limus-utilisation-chart",
templateUrl: "./chart.component.html",
encapsulation: ViewEncapsulation.None
})
export class LimusUtilisationChartComponent implements OnInit, OnChanges {
myChart: Chart;
#Input() chartdata: any;
#ViewChild("canvas") stackchartcanvas: ElementRef;
constructor() {}
ngOnChanges() {
this.getStackedChart();
}
ngOnInit(): void {
this.getStackedChart();
}
getStackedChart() {
Chart.Tooltip.positioners.custom = function(elements, position) {
//debugger;
return {
x: position.x,
y:
elements[0]._view.base - (elements[0].height() + elements[1].height())
};
};
const canvas: any = this.stackchartcanvas.nativeElement;
const ctx = canvas.getContext("2d");
var data = {
labels: this.chartdata.buyernames,
datasets: [
{
label: "Utilised Limit",
data: this.chartdata.utilisedlimitData,
backgroundColor: "#22aa99"
},
{
label: "Available Limit",
data: this.chartdata.availablelimit,
backgroundColor: "#994499"
}
]
};
setTimeout(() => {
this.myChart = new Chart(ctx, {
type: "bar",
data: data,
options: {
tooltips: {
mode: "index",
intersect: true,
position: "custom",
yAlign: "bottom"
},
scales: {
xAxes: [
{
stacked: true
}
],
yAxes: [
{
stacked: false,
display: false
}
]
}
}
});
});
}
}
Fairly new to React. I have a lottery wheel as part of a hobby project website:
The Wheel object was downloaded with npm:
npm install lottery-wheel
import Wheel from 'lotter-wheel'
class Lottery extends Component {
constructor() {
bla bla
}
componentDidMount() {
new Wheel( {
el: document.querySelector("#wheel"),
onSuccess(data) {
alert(`Congratulations, you picked up ${data.text}`)
/* I want to pass the data here to Parent */
},
onButtonHover(anime, button) {
anime({
targets: button,
scale: 1.3,
perspective: 80,
duration: 400
});
},
});
}
render() {
return (
<div id="wheel"></div>
)
}
}
SO, In the callback-function 'onSuccess' I want to pass the 'data' from the Wheel child component to the 'Lottery' parent component.
How can I do this? I know how props work but not in this case.. Can I use a hook, in that case, how?
I want avoid downloading and going into 'Wheel' definition since it was not created by me.
Define a function and set as the onSuccess callback in the Wheel.
class Lottery extends Component {
successHandler = data => {
alert(`Congratulations, you picked up ${data.text}`);
};
componentDidMount() {
new Wheel({
el: document.querySelector("#wheel"),
data: [{
text: 'apple',
chance: 20
}, {
text: 'banana'
}, {
text: 'orange'
}, {
text: 'peach'
}],
onSuccess: this.successHandler,
onButtonHover(anime, button) {
anime({
targets: button,
scale: 1.3,
perspective: 80,
duration: 400
});
}
});
}
render() {
return <div id="wheel"></div>;
}
}
I am trying to render a Line chart using react-chartjs-2. Right now it works fine when I pass in static props to the Line Component. But, when I fetch data from an API, set it to a variable, then pass that variable as the prop. My Line Component is not re-rendering with the new data.
I am logging the props being passed into the Line Component and I can see it first arrives as null and then I receive the good data from the API. So it looks like the Line Component is not re-rendering after receiving the props? I am probably doing this wrong. Please help.
import React from "react";
import { Line } from "react-chartjs-2";
export default class ExpenseChart extends React.Component {
constructor(props) {
super(props);
this.state = {
marketData: [100, 200, 300],
chartData: {
labels: this.props.monthNames,
datasets: [
{
backgroundColor: "rgba(142, 243, 197, 0.5)",
pointBackgroundColor: "#fff",
pointHoverBackgroundColor: '#fff',
pointStyle: "circle",
label: "Monthly Expenses",
borderColor: "#2be1d8",
borderWidth: 3,
borderJoinStyle: "round",
lineTension: 0.3,
fontColor: "#fff",
hitRadius: 5,
hoverRadius: 8,
radius: 4,
data: this.props.monthExpenses
},
],
},
};
}
render() {
console.log("why no names", this.props.monthNames)
return (
<div className="expenseChart">
<h2 className="expenseChart__name">{this.props.graphname}</h2>
<Line
data={this.state.chartData}
options={{
maintainAspectRatio: false,
responsive: true,
aspectRatio: 3,
scales: {
yAxes: [
{
ticks: {
display: false,
},
},
],
},
layout: {
padding: {
right: 10,
},
},
}}
/>
</div>
);
}
}
And then the parent component is connected to a redux store and it looks like this:
import React from "react";
import { connect } from 'react-redux';
import ExpenseChart from "../elements/ExpenseChart";
import { fetchExpenses } from '../../actions/index';
class Dashboard extends React.Component {
componentDidMount() {
this.props.dispatch(fetchExpenses());
}
render() {
let labels = this.props.months && this.props.months;
return (
<main className="dashboard">
<ExpenseChart
monthNames={labels}
monthExpenses={["123", "123", "12312", "12341", "231231", "1231", "1231"]}
// I am receiving monthExpenses props into the ExpenseChart component
// but not monthNames
/>
</main>
);
}
}
const mapStateToProps = state => ({
auth: state.app.auth,
months: state.app.months,
});
export default connect(mapStateToProps)(Dashboard);
I've done something similar before, fetching data from an API and passing it as props to the Line Component. But only difference is I am using redux here. And obviously, this time the Line Component is not receiving the good data.
The issue might be on this componentDidMount code.
componentDidMount() {
this.props.dispatch(fetchExpenses());
}
As per docs on dispatch
action (Object†): A plain object describing the change that makes
sense for your application. Actions are the only way to get data into
the store, so any data, whether from the UI events, network callbacks,
or other sources such as WebSockets needs to eventually be dispatched
as actions. Actions must have a type field that indicates the type of
action being performed. Types can be defined as constants and imported
from another module. It's better to use strings for type than Symbols
because strings are serializable. Other than type, the structure of an
action object is really up to you. If you're interested, check out
Flux Standard Action for recommendations on how actions could be
constructed.
Here fetchExpenses might be returning a promise and in that case you might need
fetchExpenses().then(apiRes => dipatch({type: "ACTION_TYPE": payload: apiRes}))
Another approach can be to use redux-thunk
Ok I solved this just by checking if the data I was receiving was not null before passing it as props. Which is what I thought this line would do let labels = this.props.months && this.props.months;
<main className="dashboard">
{ this.props.months != null ? <ExpenseChart monthNames={labels}/> : ''; }
</main>