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.
Related
I need to create a geo chart and I have installed the chartjs-chart-geo to implement a chart that simple show the total sales per state.
I got this error when trying to fetch the features from the topojson object and I got stack in here. Can any of you please help me ?
Error:
Property 'features' does not exist on type 'Feature<Point, GeoJsonProperties>'.
Here's my unfinish component code:
import { Component, OnInit } from '#angular/core';
import { DashboardService } from '../../../modules/dashboard.service';
import { Chart, registerables } from 'chart.js';
import { ChoroplethController, GeoFeature, ColorScale, ProjectionScale } from 'chartjs-chart-geo';
import * as ChartGeo from 'chartjs-chart-geo'
import ChartDataLabels from 'chartjs-plugin-datalabels';
// register controller in chart.js and ensure the defaults are set
Chart.register(ChoroplethController, GeoFeature, ColorScale, ProjectionScale);
Chart.register(...registerables);
Chart.register(ChartDataLabels);
// Get the topojson file examples
const url = 'https://unpkg.com/world-atlas#2.0.2/countries-50m.json';
// const url = 'https://unpkg.com/us-atlas/states-10m.json';
#Component({
selector: 'quotes-by-state',
templateUrl: './quotes-by-state.component.html',
styleUrls: ['./quotes-by-state.component.css']
})
export class QuotesByStateComponent implements OnInit {
chart:any;
constructor(private dashService: DashboardService) { }
ngOnInit(): void {
// Prepare chart
this.createChart();
}
// Create chart component
createChart() {
// fetch('https://unpkg.com/us-atlas/states-10m.json').then((r) => r.json()).then((us) => {
// const nation = ChartGeo.topojson.feature(us, us.objects.nation).features[0];
// const states = ChartGeo.topojson.feature(us, us.objects.states).features;
fetch(url).then((result) => result.json()).then((datapoint) => {
// Get ChartGeo features from json
const nation = ChartGeo.topojson.feature(datapoint, datapoint.objects.countries).features;
this.chart = new Chart('geo-chart', {
type: 'choropleth',
data: {
labels: ['a', 'b', 'c'],
datasets: [
{
label: 'Contries',
outline: nation,
data: null, //states.map(country => ({feature: country, value: Math.random() * 100})),
},
]
},
options: {
responsive: true,
}
});
});
}
}
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
}
]
}
}
});
});
}
}
I have a parent component with 2 child components that both inherit from the same base component. (This parent component is being created and used in a Vue Storybook). Both SiblingAComponent and SiblingBComponent inherit the same BaseComponent, and instantiate the same inherited data classInstance, which is a vanilla JS class instance from another library. I am trying to access this classInstance from the parent component to pass as data into the second sibling component (in this case, from SiblingAComponent to SiblingBComponent), by using an reference siblingARef. However, I get this error from the storybook compiler:
too much recursion
isArguments#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:49010:16
keys#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:49073:28
_traverse#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:119972:19
_traverse#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:119974:28
ParentComponent Story:
storiesOf("ParentComponent Story", module)
.addDecorator(
withKnobs({
escapeHTML: false
})
)
.add("Passing data from A to B", () => ({
name: 'ParentComponent',
components: {
SiblingAComponent,
SiblingBComponent,
},
data() {
return {
siblingAData: [....], // array of objects
siblingAOptions: {
axes: {},
height: "50px",
},
siblingBData: [...], // array of objects
siblingBOptions: null,
}
},
mounted() {
const siblingAInstance = this.$refs.siblingARef.classInstance;
const newOptions = {
legend: {
external: {
reference: siblingAInstance,
},
},
};
// this line is where I am getting an error
this.siblingBOptions = legendExternal;
},
template: `
<SiblingAComponent ref="siblingARef" :data="siblingAData" :options="siblingAOptions"/>
<SiblingBComponent v-if="siblingBData" :data="siblingBData" :options="siblingBOptions"/>
`,
}));
SiblingAComponent:
<template>
<div class="sibling-a-component"></div>
</template>
<script>
import { ComponentA } from '#libraryexample/components';
import BaseComponent from './base-component.vue';
export default {
name: 'SiblingAComponent',
extends: BaseComponent,
mounted() {
this.classInstance = new ComponentA(this.$el, {
data: this.data,
options: this.options,
});
},
};
</script>
SiblingBComponent:
<template>
<div class="sibling-b-component"></div>
</template>
<script>
import { ComponentB } from '#libraryexample/components';
import BaseComponent from './base-component.vue';
export default {
name: 'SiblingBComponent',
extends: BaseComponent,
mounted() {
this.classInstance = new ComponentB(this.$el, {
data: this.data,
options: this.options,
});
},
};
</script>
BaseComponent:
<script>
export default {
name: 'BaseComponent',
data() {
return {
classInstance: null,
};
},
props: {
data: { type: [Object, Array], required: true },
options: { type: Object, required: true },
},
};
</script>
Coming from the Angular and React worlds, using a reference to access a Vanilla class instance from another Component is nothing new, even if it's unconventional. I am new to Vue, so I am wondering why would trying to access a class instance fail (works fine for primitive data types) and give me such a weird error? Where is the recursion occurring?
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 was relatively new to Angular and had serious issues before i figured out a way to do this.
use this link to preview the solution
<https://plnkr.co/edit/OtJI13uA89caf8TG5lbI?p=preview>?
To dymanically populate your ng2-smart-table, you may follow the steps below.
1. Import smart table component in your module.
import { LocalDataSource } from "ng2-smart-table";
2.add the following code to your class.
#Component({
selector: 'my-app',
template: `
<div>
<h2>Hello {{name}}</h2>
<button (click)="addColumn()">Add Column</button>
<ng2-smart-table [settings]="settings" [source]="source"></ng2-smart-table>
</div>
`,
})
export class ResultComponent implements OnInit
{
source: LocalDataSource;
i = 0;
settings;
mySettings = {
mode: 'inline',
actions: {
delete:false,
},
add: {
confirmCreate: true,
},
delete: {
confirmDelete: true,
},
edit: {
confirmSave: true,
},
columns: {
}
};
//method that adds the column. You can use trigger events to do this
public addColumn() {
this.mySettings.columns["new column " + this.i] = { title: 'new column
' + this.i.toString()};
this.settings = Object.assign({}, this.mySettings);
this.i++;
}
}