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
}
]
}
}
});
});
}
}
Related
working on a covid app to get familiar with vue2js.
Am now trying to get a graph with vue-chartjs but am failing to pass the data to the graph/chart component.
I make an API request with vuex and passing the data to my component: CountryGraph.vue which contains a Graph.vue with the chart itself.
vuex -> CountryGraph.vue -> Graph.vue
Passing data into CountryGraph.vue works:
But when I try to pass my data (countryGraph) as props to my char/Graph.vue component, then it is not done and I get in Graph.vue only the value undefined:
Why?
Below my code, first the CountryGraph.vue:
<template>
<section class="countryGraph">
<LineChart
:chartdata="chartData"
:options="chartOptions"
/>
</section>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import LineChart from "../graph/Graph";
export default {
name: "CountryGraph",
components: { LineChart },
data: () => ({
chartData: {
labels: this.countryGraph.map((el) => el.date),
datasets: [
{
label: "Confirmed",
backgroundColor: "#f87979",
data: this.countryGraph.map(
(el) => el.confirmed
),
},
],
},
chartOptions: {
responsive: true,
maintainAspectRatio: false,
},
}),
methods: {
...mapActions(["selectCountryGraph"]),
},
computed: {
...mapGetters(["countryGraph"]),
},
};
</script>
<style></style>
And my chart/Graph.vue component which is made so, that I can reuse it (as stated in vue-chartjs guide):
<script>
import { Bar } from "vue-chartjs";
export default {
extends: Bar,
props: {
chartdata: {
type: Object,
default: null,
},
options: {
type: Object,
default: null,
},
},
mounted() {
this.renderChart(this.chartdata, this.options);
},
};
</script>
<style />
When I use mocked data, like instead of
labels: this.countryGraph.map((el) => el.data)
I do labels: ["q", "w", "e", "r", "t"]
and instead of
data: this.countryGraph.map(el => el.confirmed)
I do data: [0, 1, 2, 3, 4]
everything works fine.
Also, when I pass my variables directly into the component, like:
<LineChart
:chartdata="this.countryGraph.map((el) => el.data)"
:options="chartOptions"
/>
Then I can see the data as props in the child (Graph.vue) component.
But in this case I use v-bind: and in the earlier one not. Maybe that is the problem?
A couple issues to note:
It looks like you're mapping a nonexisting property (el.data should be el.date). Possibly just a typo in the question.
this.countryGraph.map((el) => el.data) ❌
^
data() is not reactive, and cannot rely on computed props, so the countryGraph computed prop will not be available in data() and will not update chartData with changes. One way to fix this is to make chartData a computed prop:
export default {
computed: {
...mapGetters(["countryGraph"]),
// don't use an arrow function here, as we need access to component instance (i.e., this.countryGraph)
chartData() {
return {
labels: this.countryGraph.map((el) => el.date),
datasets: [
{
label: "Confirmed",
backgroundColor: "#f87979",
data: this.countryGraph.map((el) => el.confirmed),
},
],
}
}
}
}
Hello I use parent child communication. At parent I have array which represents graph datas. Demo
My problem is that when i change one item in array i doesn't fire child component appchanges. In demo I created example which doesn't work. When I click update I want to update child component graph.
My child component
import { Component, OnInit, Input, OnChanges } from "#angular/core";
import * as Highcharts from "highcharts";
import { ChartService } from "./chart.service";
#Component({
selector: "hello",
template: `
<highcharts-chart
[Highcharts]="Highcharts"
[options]="options"
[oneToOne]="true"
[update]="updateFromInput"
style="width: calc(100% ); height: calc(100% - 17px); display: block;margin-top:15px;overflow: auto !important;"
>
</highcharts-chart>
`,
styles: [
`
h1 {
font-family: Lato;
}
`
]
})
export class HelloComponent implements OnInit, OnChanges {
innerHeight: any;
innerWidth: any;
#Input() data: any;
Highcharts: typeof Highcharts = Highcharts;
updateFromInput = false;
options: any;
constructor(private chart_service: ChartService) {
this.innerHeight = window.screen.height;
this.innerWidth = window.screen.width;
}
onResize(event) {
this.innerWidth = event.target.innerWidth;
this.update();
}
ngOnInit() {
this.update();
}
ngOnChanges() {
console.log(this.data);
console.log("değişti");
this.update();
}
update() {
var series = this.chart_service.groupBy(
this.data.LINE.DATA,
"GRUP",
"line"
);
var categories = this.chart_service.ArrNoDupe(
this.data.LINE.DATA.map(x => x.NAME)
);
let isLegend = series.length > 1 ? true : false;
var size = this.innerWidth / (12 / this.data.SIZE) - 30;
if (this.innerWidth <= 992) {
size = this.innerWidth - 30;
}
this.options = {
title: { enabled: false, text: "" },
credits: { enabled: false },
legend: { enabled: true },
tooltip: { hideDelay: 0, outside: true, shared: true },
plotOptions: {
line: { dataLabels: { enabled: !isLegend }, enableMouseTracking: true }
},
yAxis: { title: { enabled: false } },
xAxis: { categories: categories },
series: series
};
}
}
In your app.component.ts:
filter(id) {
this.reports.filter(x => x.ID == id)[0] = {
ID: 3233.0,
...
}
}
Array.prototype.filter does not mutate the given array, but returns a new array with the filtered properties.
You must reassign this value to make the Angular change detection detect this new array:
filter(id) {
this.reports = this.reports.filter(x => x.ID == id)[0] = {
ID: 3233.0,
...
}
}
It looks like there are three mistakes in your code:
Your #Input() getter/setter are switched
#Input() set data(data: any) {
this._param = data;
}
get data(): any {
return this._param;
}
Your filter actually does not filter anything (at least as far as I see from the data)
Your *ngFor needs to be notified, that your data has changed - for that you could implement the ngOnChanges lifecycle hook and assign the latest value from the SimpleChanges of your reports to the member variable reports
Exactly as #code-gorilla mentioned, it is not working because you are not mutating the reports array so the changes are not detected = your chart is not updated.
After modifying your code so it will mutate the reports I was able to make it working, you can find more details in the demo I attached below.
filter(id) {
const updatedReport = (this.reports.filter(
x => x.CHART_ITEM_ID == id
)[0] = { ... }
});
this.reports[0] = updatedReport;
}
Live demo:
https://stackblitz.com/edit/angular-7yr5qg?file=src%2Fapp%2Fapp.component.ts
In general, OnChanges is fired when the data object source has changed, but not if the data has been mutated. In order for an array to cause OnChanges to be called, it must be updated in an immutable fashion.
In this article example, slice is a immutable function - meaning it does not change the original array, whereas splice is a mutable function- meaning it changes the original array.
So in your example, in order for onChanges to be called, you would need to use an immutable function like map instead of filter.
In your specific case, it looks like you do not really want to use filter, since you just need the first element that matches the predicate. You should really use find instead.
have a separate property of report, then have your function call report = this.reports.find(x => x.CHART_ITEM_ID == id) , and bind report to the child
After helpful answer above I assing firstly index to variable then updated with using index like below code Demo
let index=this.reports.findIndex( x => x.CHART_ITEM_ID == id );
this.reports[index]= ...
I have created a doughnut chart using vue-chart.js, Chart.js and using some values which are within my vuex state. The chart works apart from when the vuex state is updated the chart doesn't updated too.
I have tried to use computed properties to try keep the chart up to date. But then I get the following errors:
Error in callback for watcher "chartData": "TypeError: Cannot set property 'data' of undefined"
TypeError: Cannot set property 'data' of undefined
DoughnutChart.vue:
<script>
import { Doughnut, mixins } from 'vue-chartjs';
const { reactiveProp } = mixins;
export default {
extends: Doughnut,
mixins: [reactiveProp],
props: ['chartData', 'options'],
mounted () {
this.renderChart(this.chartdata, this.options)
}
}
</script>
AppCharts.vue:
<template>
<div id="chart_section">
<h2 class="section_heading">Charts</h2>
<div id="charts">
<DoughnutChart :chart-data="datacollection" :options="chartOptions" class="chart"></DoughnutChart>
</div>
</div>
</template>
<script>
import DoughnutChart from './DoughnutChart';
import { mapGetters } from 'vuex';
export default {
components: {
DoughnutChart
},
computed: {
...mapGetters(['boardColumnData']),
datacollection() {
return {
datasets: [{
data: [this.boardColumnData[0].total.$numberDecimal, this.boardColumnData[1].total.$numberDecimal, this.boardColumnData[2].total.$numberDecimal, this.boardColumnData[3].total.$numberDecimal],
backgroundColor: [
'#83dd1a',
'#d5d814',
'#fdab2f',
'#1ad4dd'
],
borderColor: [
'#83dd1a',
'#d5d814',
'#fdab2f',
'#1ad4dd'
],
}]
}
},
},
data() {
return {
chartOptions: null
}
},
mounted () {
this.fillData();
},
methods: {
fillData() {
this.chartOptions = {
responsive: true,
maintainAspectRatio: false
}
}
}
}
</script>
this is what boardColumnData looks like after getting the state:
[
{
"name":"Opportunities",
"percentage":{
"$numberDecimal":"0"
},
"total":{
"$numberDecimal":70269
}
},
{
"name":"Prospects",
"percentage":{
"$numberDecimal":"0.25"
},
"total":{
"$numberDecimal":0
}
},
{
"name":"Proposals",
"percentage":{
"$numberDecimal":"0.5"
},
"total":{
"$numberDecimal":5376
}
},
{
"name":"Presentations",
"percentage":{
"$numberDecimal":"0.75"
},
"total":{
"$numberDecimal":21480
}
},
]
The $numberDecimal value is what is updated inside vuex when an event on another component happens. When these values are changed I want the chart to update with the new values.
You must pass the data in the prop. I.e. fastest solution for you would be to have fillData() return the datacollection in the correct format.
E.g.
fillData(){
return {
datasets: [
{
data: [
this.boardColumnData[0].total.$numberDecimal,
this.boardColumnData[1].total.$numberDecimal,
this.boardColumnData[2].total.$numberDecimal,
this.boardColumnData[3].total.$numberDecimal,
],
backgroundColor: ["#83dd1a", "#d5d814", "#fdab2f", "#1ad4dd"],
borderColor: ["#83dd1a", "#d5d814", "#fdab2f", "#1ad4dd"],
},
];
}
}
You need to do the same for the options, but pass them in a new options=yourOptions() prop.
This drove me absolutely bonkers. I hope this still helps you (or someone else).
I want to call a method to handle click event from this vue file:
<template>
<Chart
type="bar"
:labels="labels"
:datasets="datasets"
:options="options"
></Chart>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import Chart from '#/components/Chart.vue';
import { colors } from '#/constants';
#Component({
components: {
Chart
},
})
export default class AlarmsChart extends Vue {
#Prop()
public stats!: any[];
...
get options() {
const options = {
maintainAspectRatio: false,
scales: {
yAxes: [
{
scaleLabel: {
display: true,
labelString: 'Number of alarms',
},
},
],
xAxes: [
{
type: 'time',
time: {
parser: 'YYYY-MM-DD',
displayFormats: {
day: 'DD/MM/YY',
},
},
scaleLabel: {
display: true,
labelString: 'Date',
},
},
],
},
legend: {
labels: {
fontFamily: 'Overpass',
},
},
'onClick' : (event, item) => {
if ( item.length == 0 ) return; // Clicked outside any bar.
clickHandler(event);
},
};
return options;
}
}
</script>
To this one:
<template>
<div class="chart-container">
<canvas ref="chart"></canvas>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import Chart from 'chart.js';
/**
* The Chart component acts as a wrapper around Chart.js.
*
* This component has various properties that will be passed onto Chart.js.
* This component should be as generic as possible to allow different kinds of
* charts with different kinds of datasets in order to encourage reuse of this
* component without having to deal with tedious workarounds.
*/
#Component
export default class ChartComponent extends Vue {
#Prop()
public datasets!: any;
#Prop()
public labels!: string[];
#Prop()
public options!: any;
#Prop()
public type!: string;
private chart?: Chart;
public mounted() {
// Since we are updating the UI ourselves, always create the chart
// *after* Vue has finished rendering the DOM.
const chartElement = this.$refs.chart as HTMLElement;
this.chart = new Chart(chartElement, {
type: this.type,
data: {
labels: this.labels,
datasets: this.datasets,
},
options: this.options,
});
}
public clickHandler(evt) {
var firstPoint = this.chart.getElementAtEvent(evt)[0];
if (firstPoint) {
var label = this.chart.data.labels[firstPoint._index];
var value = this.chart.data.datasets[firstPoint._datasetIndex].data[firstPoint._index];
}
}
/** Automatically update Chart.js upon dataset changes. */
#Watch('datasets', { deep: true })
private onChange() {
this.chart.data.labels = this.labels;
this.chart.data.datasets = this.datasets;
this.chart.update();
}
}
</script>
<style lang="scss" scoped>
.chart-container {
position: relative;
height: 100%;
width: 100%;
}
</style>
In the first file in options there is a method call inside the onClick event to: clickHandler(event), I want to call to the method that is in the second file. But no idea how to do it, is it possible?
I want to do this to get data from the bar clicked. So the second file is like a common chart, and the first one is a custom. Several files like the first one use the second file.
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.