Am using ng2-charts - https://github.com/valor-software/ng2-charts
I have a pie chart, if I hard code the data when I declare the variable at the top of my component.ts file like in the example my pie chart displays.
But I obviously want to make the pie char data dynamic. I can call the data through a service (which is a number), add the number to the data array, and the pie chart does not work. But if I do console log, the array prints out with the new data/number I have added to it.
I need to redraw the table somehow. Can't figure out how.
public pieChartLabels:string[] = ['Red Flags','Green Flags'];
public pieChartData: number[] = [200, 400];
public chartType:string = 'pie';
public redFlagsTotal: any;
public greenFlagsTotal: any;
constructor(private dataService:flagService) {
let component = this;
this.redFlagsTotal = this.dataService.getRedFlags().then(function(result){
component.redFlagsTotal = result.length;
console.log(component.redFlagsTotal);
component.pieChartData.push(component.redFlagsTotal);
console.log(component.pieChartData);
});
this.greenFlagsTotal = this.dataService.getGreenFlags().then(function(result){
component.greenFlagsTotal = result.length;
console.log(component.greenFlagsTotal);
component.pieChartData.push(component.greenFlagsTotal);
console.log(component.pieChartData);
});
}
You can hide and show the chart for a millisecond like this:
refreshChart(){
this.mychart.show = false;
setTimeout(()=>{
this.mychart.show = true;
},1);
}
in the template use than mychart.show with *ngIf like this:
<div style="display: block">
<canvas baseChart
*ngIf="mychart.show"
[data]="pieChartData"
[labels]="pieChartLabels"
[chartType]="pieChartType"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"></canvas>
</div>
in your function than you can use the refreshChart() function when you want to refresh the chart.
EDIT:
If you reinitialize the array than the char should update automatically, instead of this:
component.pieChartData.push(component.greenFlagsTotal);
do this:
let temp = [...component.pieChartData, ...component.greenFlagsTotal];
component.pieChartData = [...temp];
Solved! Hide the canvas until the data has loaded.
<div *ngIf="pieChartData.length > 1" style="display: block">
<canvas baseChart
[data]="pieChartData"
[labels]="flagPieChartLabels"
[chartType]="chartType"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"></canvas>
</div>
Another way to make your chart data dynamic is to bind the chart directive via ViewChild like so:
...
export class HomeComponent {
#ViewChild(BaseChartDirective)
public chart: BaseChartDirective;
void updateChart() {
this.chart.chart.update(); // This re-renders the canvas element.
}
Now you can call updateChart everytime your dataset has changed to keep your chart up to date!
Hiding the canvas with *ngIf reload the whole component.
It seems that if you push data and label then remove them after 1 ms, it will reload the correct chart data.
reloadChart(){
this.pieChartLabels.push('reload');
this.pieChartData.push(1);
setTimeout(() => {
this.pieChartData.pop();
this.pieChartLabels.pop();
},1);
}
Related
I have a component ResultPill with a tooltip (implemented via vuikit) for the main container. The tooltip text is calculated by a getter function tooltip (I use vue-property-decorator) so the relevant bits are:
<template>
<div class="pill"
v-vk-tooltip="{ title: tooltip, duration: 0, cls: 'some-custom-class uk-active' }"
ref="container"
>
..some content goes here..
</div>
</template>
<script lang="ts">
#Component({ props: ... })
export default class ResultPill extends Vue {
...
get tooltip (): string { ..calcing tooltip here.. }
isContainerSqueezed (): boolean {
const container = this.$refs.container as HTMLElement | undefined;
if(!container) return false;
return container.scrollWidth != container.clientWidth;
}
...
</script>
<style lang="stylus" scoped>
.pill
white-space pre
overflow hidden
text-overflow ellipsis
...
</style>
Now I'm trying to add some content to the tooltip when the component is squeezed by the container's width and hence the overflow styles are applied. Using console, I can roughly check this using $0.scrollWidth == $0.clientWidth (where $0 is the selected element), but when I start tooltip implementation with
get tooltip (): string {
if(this.isContainerSqueezed())
return 'aha!'
I find that for many instances of my component this.$refs.container is undefined so isContainerSqueezed doesn't help really. Do I have to somehow set unique ref per component instance? Are there other problems with this approach? How can I check whether the element is overflown?
PS to check if the non-uniqueness of refs may affect the case, I've tried to add to the class a random id property:
containerId = 'ref' + Math.random();
and use it like this:
:ref="containerId"
>
....
const container = this.$refs[this.containerId] as HTMLElement | undefined;
but it didn't help: still tooltip isn't altered.
And even better, there's the $el property which I can use instead of refs, but that still doesn't help. Looks like the cause is this:
An important note about the ref registration timing: because the refs themselves are created as a result of the render function, you cannot access them on the initial render - they don’t exist yet! $refs is also non-reactive, therefore you should not attempt to use it in templates for data-binding.
(presumably the same is applicable to $el) So I have to somehow recalc tooltip on mount. This question looks like what I need, but the answer is not applicable for my case.
So, like I've mentioned in one of the edits, docs warn that $refs shouldn't be used for initial rendering since they are not defined at that time. So, I've made tooltip a property instead of a getter and calcuate it in mounted:
export default class ResultPill extends Vue {
...
tooltip = '';
calcTooltip () {
// specific logic here is not important, the important bit is this.isContainerSqueezed()
// works correctly at this point
this.tooltip = !this.isContainerSqueezed() ? this.mainTooltip :
this.label + (this.mainTooltip ? '\n\n' + this.mainTooltip : '');
}
get mainTooltip (): string { ..previously used calculation.. }
...
mounted () {
this.calcTooltip()
}
}
I want to show an Angular Material tooltip when its component is initialized/loaded.
I know I can add an HTML attribute to show it when an event happens. My overall goal is to have the tooltip showing when the component loads, then hide after a few seconds.
I've tried the following:
<div (load)="tooltip.show()"
#tooltip="matTooltip"
matTooltip="blah blah">
</div>
YoukouleleY is almost correct, you need to put it into ngAfterViewInit() and add setTimeout() to make it work:
#ViewChild('tooltip') tooltip: MatTooltip;
constructor(private cd: ChangeDetectorRef) { }
ngAfterViewInit() {
this.tooltip.show();
this.cd.detectChanges();
setTimeout(() => this.tooltip.hide(2000));
}
Added update with changeDetectorRef to avoid ExpressionChangedAfterItHasBeenCheckedError. Hope that helps.
Try this:
#ViewChild('tooltip') tooltip: MatToolTip;
ngOnInit() {
this.tooltip.show();
this.tooltip.hide(2000);
}
I am trying to modify the input box model of Angular 4 such that if the user types in, say 23, in the box, the box should display 23%.
I have tried appending the % to the model value after accessing it in events like (input) or (change). This however changes the model entirely. What I want is that the Angular model variable should still contain 23 but should display 23%.
Is there any way I can build a directive around this. Any suggestions or link ?
It's a little hacky way, but you can do this:
<input #input1 value="{{title}}%" (keydown)=setModel(input1.value)>
And in component:
title = '23';
setModel(val) {
this.title = val.split('%').join('');
console.log(this.title)
};
value="{{title}}%" will take title value and will add % at the end. And you can set new value using setModel method, but before setting, you need to remove all % characters, like this: this.title = val.split('%').join('');.
Since you're trying to change the way how a component displays the value, you should use a directive instead of changing the actual value in the model. In other words, you need an input mask here. You can use one of the existing packages (e.g. https://github.com/text-mask/text-mask) or write your own directive.
You can create a component with 2 values: one is the bound value, and the other is the displayed value. The component would look a little like this:
#Component({
selector: 'hello-world',
template: `
<input type="text" [(ngModel)]="display" (keyup)="updateInput($event)" placeholder="Enter a name here">
<hr>
<h1>Input: {{input}}</h1>
`
})
export class HelloWorld {
#Input() input = 0;
#Output() inputChange = new EventEmitter();
display;
ngOnInit() {
this.display = `${this.input}%`
}
updateInput(evt) {
this.input = this.display.replace(/[^0-9.]/g, "");
this.display = `${this.input}%`;
this.inputChange.emit(this.input);
}
}
And you can bind to the component like so:
<hello-world [(input)]="myVar"></hello-world>
I'm using react-chartjs-2 to create a Line chart for my application.
For this app, I did a legend customisation and I could generate them using this:
// Chart component
<Line ref={ (chart) => chart ? this.insertLegends(chart) : null }
data={this.state.chart}
options={this.state.options}
/>
// Method which insert the html content
insertLegends(chart) {
this.refs.chartLegendContainerGlobal.innerHTML = chart.chart_instance.generateLegend();
}
First, is this a right approach?
I had to create an inline condition inside the component to prevent chart to be null.
Second, how and where can I put an onClick event for each legend?
I'm very lost on this, is there a better way to do this legend customisation??
If you give the ref a callback, then you won't get a value of null. Doing an inline ref like this causes the first render to be null and then the second render will have the element.
So you should change your refs to:
applyRef(ref) {
this.legend = ref;
}
render() {
return (
// Chart component
<Line ref={this.applyRef}
data={this.state.chart}
options={this.state.options}
/>
)
}
For adding a click event handler, if you can't add an onClick attrib for some reason, then you can set it in your insertLegends method:
handleClick(e) {
// Do something here...
}
insertLegends(chart) {
this.refs.chartLegendContainerGlobal.innerHTML = chart.chart_instance.generateLegend();
this.refs.chartLegendContainerGlobal.addEventListener('click', this.handleClick);
}
After some trouble and research, I figure out how to add the legend and control the click inside of it.
// Inside my render method I added a simple ref to my component
<Line ref='chart' data={this.convertData(this.props.data)} options={this.state.options} />
// Inside this method I'm able to get all the references that
// I need to inject the html inside a container for the legends and
// also to assign a click for each legend label
componentDidMount() {
let legends = this.refs.chart.chart_instance.generateLegend();
this.refs.chartLegendContainer.innerHTML = legends;
$(this.refs.chartLegendContainer).find('.legend-item').on('click', (e) => {
let index = $(e.currentTarget).index();
this.refs.chart.chart_instance.data.datasets[index].hidden = !this.refs.chart.chart_instance.data.datasets[index].hidden;
$(e.currentTarget).toggleClass('disable-legend');
this.refs.chart.chart_instance.update();
});
}
UPDATED
After the commect of #Chase DeAnda, I changed a little bit based on his considerations:
// Applying the callback function to the ref
<Line ref={this.applyRef} data={this.convertData(this.props.data)} options={this.state.options} />
// Inside the method I call the method to insert the legends
applyRef(ref) {
this.legend = ref;
this.insertLegends();
}
// Generates the legend and added them to my container element
// Also give them the onClick event
insertLegends() {
let legends = this.legend.chart_instance.generateLegend();
this.refs.chartLegendContainer.innerHTML = legends;
$(this.refs.chartLegendContainer).find('.legend-item').on('click', (e) => this.onClickLegend(e));
}
// During onClick I update the chart
onClickLegend(e) {
let index = $(e.currentTarget).index();
this.legend.chart_instance.data.datasets[index].hidden = !this.legend.chart_instance.data.datasets[index].hidden;
$(e.currentTarget).toggleClass('disable-legend');
this.legend.chart_instance.update();
}
I am trying to implement ng2-charts in my Angular 2 project and I was wondering about creating custom onclick events. Meaning, I want to override the current onclick events on the carts to do some custom functions (redirect to a page, have a modal show up, etc).
Is there a simple way to do this? Is it built in at all?
Any insight would be appreciated it
I found this solution at https://github.com/valor-software/ng2-charts/issues/489
public chartClicked(e: any): void {
if (e.active.length > 0) {
const chart = e.active[0]._chart;
const activePoints = chart.getElementAtEvent(e.event);
if ( activePoints.length > 0) {
// get the internal index of slice in pie chart
const clickedElementIndex = activePoints[0]._index;
const label = chart.data.labels[clickedElementIndex];
// get value by index
const value = chart.data.datasets[0].data[clickedElementIndex];
console.log(clickedElementIndex, label, value)
}
}
}
Try to read DOCS
They have pretty good and understandable explanation of use.
There-are built-in 2 event handlers:
Events
chartClick: fires when click on a chart has occurred, returns information regarding active points and labels
chartHover: fires when mousemove (hover) on a chart has occurred, returns information regarding active points and labels
In code it looks like that:
<base-chart class="chart"
[datasets]="lineChartData"
[labels]="lineChartLabels"
[options]="lineChartOptions"
[colors]="lineChartColours"
[legend]="lineChartLegend"
[chartType]="lineChartType"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"></base-chart>
</div>
that chartHovered and chartClicked are your custom functions, which could has another names, and do custom things like showing modal, redirect to url etc.
public chartClicked(e: any): void {
console.log(e);
}
e.active[0]._model and e.active[0]._view contain information about the part of the chart you clicked (i.e. label).
I hope my answer is correct. After much searching for the only solution I found was:
public chartClicked(e:any):void {
if(e.active.length > 0){
var points = [];
var pointSelected = e.active[0]._chart.tooltip._model.caretY;
var legends = e.active[0]._chart.legend.legendItems;
for (var i = 0; i < e.active.length; ++i) {
points.push(e.active[i]._model.y);
}
let position = points.indexOf(pointSelected);
let label = legends[position].text
console.log("Point: "+label)
}}
After checking multiple places, I got it working like this for click event.
HTML:
<div class="chart">
<canvas
baseChart
[data]="pieChartData"
[type]="pieChartType"
[options]="pieChartOptions"
[plugins]="pieChartPlugins"
(chartHover)="chartHovered($event)"
>
</canvas>
</div>
TS:
public chartHovered(e: any): void {
if (e.event.type == "click") {
const clickedIndex = e.active[0]?.index;
console.log("Clicked index=" + clickedIndex);
}
}
Ref