Solid guage refresh when receives new data (point value) - vue.js

I am having issues with my solid gauge component using the highcharts-vue wrapper.
Basically every time it receives new data it re-draws the animation from 0. Please see sandbox link below for example: https://codesandbox.io/s/n0no0jky1l
The desired behavour im trying to achieve is shown here: https://www.highcharts.com/demo/gauge-solid
Where the gauge animates from old to new value smoothly.
From what i can tell it is not recommended to access the charts methods manually (for example to call the points.update method). (source: https://github.com/highcharts/highcharts-vue#chart-object-reference)
Any ideas?

It occurs because of mutating data in Highcharts. Please use Point.update, instead of updating whole series, to make animation work. You can access whole Chart object by reference in your component, specifically this.children[0].chart. Here is the code:
watch: {
title(newValue) {
this.chartOptions.title.text = newValue;
},
points(newValue, oldValue) {
console.log("watch firing", newValue, oldValue);
this.$children[0].chart.series[0].data[0].update(oldValue += 1);
},
units(newValue) {
this.chartOptions.series[0].dataLabels.format =
'<div style="text-align:center"><span style="font-size:2rem;color: black">{y}</span><div>' +
newValue +
"</div><div/>";
},
min(newValue) {
this.chartOptions.yAxis.min = newValue;
},
max(newValue) {
this.chartOptions.yAxis.max = newValue;
}
},
Don't worry about your data, Highcharts mutate it, so your points prop should be updated every time.
Live example: https://codesandbox.io/s/ojkzvo05z
Kind regards!

Related

Vuejs - update array of an object which is in an array

I'm developing a helpdesk tool in which I have a kanban view.
I previously used nested serializers in my backend and I managed to have everything working with a single query but it's not scalable (and it was ugly) so I switched to another schema :
I query my helpdesk team ('test' in the screenshot)
I query the stages of that team ('new', 'in progress')
I query tickets for each stage in stages
So when I mount my component, I do the following :
async mounted () {
if (this.helpdeskTeamId) {
await this.getTeam(this.helpdeskTeamId)
if (this.team) {
await this.getTeamStages(this.helpdeskTeamId)
if (this.stages) {
for (let stage of this.stages) {
await this.getStageTickets(stage)
}
}
}
}
},
where getTeam, getTeamStages and getStageTickets are :
async getTeam (teamId) {
this.team = await HelpdeskTeamService.getTeam(teamId)
},
async getTeamStages (teamId) {
this.stages = await HelpdeskTeamService.getTeamStages(teamId)
for (let stage of this.stages) {
this.$set(stage, 'tickets', [])
}
},
async getStageTickets (stage) {
const tickets = await HelpdeskTeamService.getTeamStageTickets(this.helpdeskTeamId, stage.id)
// tried many things here below but nothing worked.
// stage.tickets = stage.tickets.splice(0, 0, tickets)
// Even if I try to only put one :
// this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0])
// I see it in the data but It doesn't appear in the view...
// Even replacing the whole stage with its tickets :
// stage.tickets = tickets
// this.stages.splice(this.stages.indexOf(stage), 1, stage)
},
In getTeamStages I add an attribute 'tickets' to every stage to an empty list. The problem is when I query all the tickets for every stage. I know how to insert a single object in an array with splice or how to delete one object from an array but I don't know how to assign a whole array to an attribute of an object that is in an array while triggering the Vue reactivity. Here I'd like to put all the tickets (which is a list), to stage.tickets.
Is it possible to achieve this ?
If not, what is the correct design to achieve something similar ?
Thanks in advance !
EDIT:
It turns out that there was an error generated by the template part. I didn't think it was the root cause since a part of the view was rendered. I thought that it would have prevent the whole view from being rendered if it was the case. But finally, in my template I had a part doing stage.tickets.length which was working when using a single query to populate my view. When making my API more granular and querying tickets independently from stages, there is a moment when stage has no tickets attribute until I set it manually with this.$set(stage, 'tickets', []). Because of that, the template stops rendering and raises an issue. But the ways of updating my stage.tickets would have worked without that template issue.
I could update the stages reactively. Here is my full code; I used the push method of an array object and it works:
<template>
<div>
<li v-for="item in stages" :key="item.stageId">
{{ item }}
</li>
</div>
</template>
<script>
export default {
data() {
return {
stages: [],
};
},
methods: {
async getTeamStages() {
this.stages = [{ stageId: 1 }, { stageId: 2 }];
for (let stage of this.stages) {
this.$set(stage, "tickets", []);
}
for (let stage of this.stages) {
await this.getStageTickets(stage);
}
},
async getStageTickets(stage) {
const tickets = ["a", "b", "c"];
for (let ticket of tickets) {
this.stages[this.stages.indexOf(stage)].tickets.push(ticket);
}
},
},
mounted() {
this.getTeamStages();
},
};
</script>
It should be noted that I used the concat method of an array object and also works:
this.stages[this.stages.indexOf(stage)].tickets = this.stages[this.stages.indexOf(stage)].tickets.concat(tickets);
I tried your approaches some of them work correctly:
NOT WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, tickets)
WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0]);
WORKED
stage.tickets = tickets
this.stages.splice(this.stages.indexOf(stage), 1, stage)
I'm sure it is XY problem..
A possible solution would be to watch the selected team and load the values from there. You seem to be loading everything from the mounted() hook, and I suspect this won't actually load all the content on demand as you'd expect.
I managed to make it work here without needing to resort to $set magic, just the pure old traditional vue magic. Vue will notice the properties of new objects and automatically make then reactive, so if you assign to them later, everything will respond accordingly.
My setup was something like this (showing just the relevant parts) -- typing from memory here, beware of typos:
data(){
teams: [],
teamId: null,
team: null
},
watch:{
teamId(v){
this.refreshTeam(v)
}
},
methods: {
async refreshTeam(id){
let team = await fetchTeam(id)
if(!team) return
//here, vue will auomaticlly make this.team.stages reactive
this.team = {stages:[], ...team}
let stages = await fetchStages(team.id)
if(!stages) return
//since this.team.stages is reactive, vue will update reactivelly
//turning the {tickets} property of each stage reactive also
this.team.stages = stages.map(v => ({tickets:[], ...v}))
for(let stage of this.team.stages){
let tickets = await fetchTickets(stage.id)
if(!tickets) continue
//since tickets is reactive, vue will update it accordingly
stage.tickets = tickets
}
}
},
async mounted(){
this.teams = fetchTeams()
}
Notice that my 'fetchXXX' methods would just return the data retrieved from the server, without trying to actually set the component data
Edit: typos

how to reload value after clicking in vue

I'm trying to reload values when switching tab without reloading the page. I'm getting the value from a method.
mounted() {
this.getOriginalSpace();
},
methods: {
getOriginalSpace() {
retrieveQuotaSummary(this.value.organisation, this.value.dataCenter)
.then((result) => {
this.quotaSummary = result;
});
}
}
after that, I read the needed value out of quotaSummary like this (computed):
previouslyYarnCPU() {
return this.quotaSummary.currentAcceptedYarnRequest
? this.quotaSummary.currentAcceptedYarnRequest.cpu
: 0;
},
Then, when I switch tab, and call an other function in computed mode, I still have the same value which was loaded above. But when I refresh the page, then I get the correct (new value).
Can someone please help me, how I can get the latest values without refreshing the whole page?
It is difficult as I would need to see the rest of your code but in order to get values when they change you need to use a computed function. You can read more here

Vue.js - Highmaps - Redraw map on series change

I have a highmaps 'chart' and the only thing that I want is to redraw the whole map inside an external function. Let me explain better. The map draws itself immediatly when the page loads up but I fetch some data from an external service and set it to a variable. Then I would like to just redraw the chart so that the new data appears in the map itself. Below is my code.
<template>
<div>
<highmaps :options="chartOptions"></highmaps>
</div>
</template>
<script>
import axios from 'axios';
import HighCharts from 'vue-highcharts';
import json from '../map.json'
let regions = [];
export default {
data: function () {
return {
chartOptions: {
chart: {
map: json, // The map data is taken from the .json file imported above
},
map: {
/* hc-a2 is the specific code used, you can find all codes in the map.json file */
joinBy: ['hc-key', 'code'],
allAreas: false,
tooltip: {
headerFormat: '',
pointFormat: '{point.name}: <b>{series.name}</b>'
},
series: [
{
borderColor: '#a0451c',
cursor: 'pointer',
name: 'ERROR',
color: "red",
data: regions.map(function (code) {
return {code: code};
}),
}
],
}
},
created: function(){
let app = this;
/* Ajax call to get all parameters from database */
axios.get('http://localhost:8080/devices')
.then(function (response) {
region.push(response.parameter)
/* I would like to redraw the chart right here */
}).catch(function (error){
console.error("Download Devices ERROR: " + error);
})
}
}
</script>
As you can see I import my map and the regions variable is set to an empty array. Doing this results in the map having only the borders and no region is colored in red. After that there is the created:function() function that is used to make the ajax call and retrieve data. After that I just save the data pushing it into the array and then obviously nothing happens but I would like to redraw the map so that the newly imported data will be shown. Down here is the image of what I would like to create.
If you have any idea on how to implement a thing like this or just want to suggest a better way of handling the problem, please comment.
Thanks in advance for the help. Cheers!
After a few days without any answer I found some marginal help online and came to a pretty satisfying conclusion on this problem so I hope it can help someone else.
So the first thing I did was to understand how created and mounted were different in Vue.js. I used the keyword created at first when working on this project. Because of that, inside this function, I placed my ajax call that gave me data which I then loaded inside the 'chart' by using the .addSeries method of the chart itself.
To reference the chart itself I used this: let chart: this.$refs.highcharts.chart. This searches for the field refs in any of your components/html elements and links it to the variable. So in the html there was something like this:
<template>
<div>
<highmaps :options="chartOptions" ref="highcharts"></highmaps>
</div>
</template>
The real problem was that the chart didn't even start rendering while all this process was going on so I changed the created keyword with mounted which means that it executes all the code when all of the components are correctly mounted and so my chart would be already rendered.
To give you (maybe) a better idea of what I am talking about I will post some code down below
mounted: function(){
let errorRegions = [];
let chart = this.$refs.highcharts.chart;
axios.get('localhost:8080/foo').then(function(response)
{
/* Code to work on data */
response.forEach(function(device){
errorRegions.push(device);
}
chart.addSeries({
name: "ERROR",
color: "red",
data: errorRegions
}
/* ...Some more code... */
})
}
And this is the result (have been adding some more series in the same exact manner)
Really hoping I have been of help to someone else. Cheers!

Updating a chart with new data in App SDK 2.0

I am using a chart to visualize data in a TimeboxScopedApp, and I want to update the data when scope changes. The more brute-force approach of using remove() then redrawing the chart as described here leaves me with an overlaid "Loading..." mask, but otherwise works. The natural approach of using the Highchart native redraw() method would be my preference, only I don't know how to access the actual Highchart object and not the App SDK wrapper.
Here's the relevant part of the code:
var chart = Ext.getCmp('componentQualityChart');
if (chart) {
var chartCfg = chart.getChartConfig();
chartCfg.xAxis.categories = components;
chart.setChartConfig(chartCfg);
chart.setChartData(data);
chart.redraw(); // this doesn't work with the wrapper object
} else { // draw chart for the first time
How do I go about redrawing the chart with the new data?
Assuming chart (componentQualityChart) is an instance of Rally.ui.chart.Chart, you can access the HighCharts instance like this:
var highcharts = chart.down('highchart').chart;
// Now you have access to the normal highcharts interface, so
// you could change the xAxis
highcharts.xAxis[0].setCategories([...], true);
// Or you could change the series data
highcharts.series[0].data.push({ ... }); //Add a new data point
// Or pretty much anything else highcharts lets you do
Using _unmask() removes the overlaid "Loading.." mask
if (this.down('#myChart')) {
this.remove('myChart');
}
this.add(
{
xtype: 'rallychart',
height: 400,
itemId: 'myChart',
chartConfig: {
//....
},
chartData: {
categories: scheduleStateGroups,
series: [
{
//...
}
]
}
}
);
this.down('#myChart')._unmask();

WinJS Listview shows undefined when navigating quickly

I have a WinJS application with listviews in which if quickly navigate between pages before the listview is fully loaded, the next page shows the listview with all elements in it bound as "undefined".
So say I have a hub page with a "to do" that is filtered to only show 6 items, and there is a header that navigates to the full "to do" page, when the hub page is displayed but before it is fully loaded I click on the header link to the "to do" page, the app then goes to the "to do" page, but the items show up with all the properties in the tile as "undefined".
I am using IndexedDB as my data store.
My home page code looks like this:
WinJS.UI.Pages.define("/pages/home/home.html", {
ready: function (element, options) {
WinJS.Utilities.query("a").listen("click", function (e) {
e.preventDefault();
WinJS.Navigation.navigate(e.currentTarget.href);
}, false);
viewModel = new HomeViewModel(element);
viewModel.load(); //loads from indexed db
},
//etc...
To Do Page:
WinJS.UI.Pages.define("/pages/ToDo/ToDo.html", {
ready: function (element, options) {
viewModel = new ToDoViewModel(element);
viewModel.load();
},
etc//
I know there isn't much to go off, but any ideas would be appreciated.
Also tips on how to debug something like this would be great.
Update
I narrowed it down to this one line from the Hub Page:
myLib.GetData(todaysDate, function (result) {
that.trendsModel.today = result;
WinJS.Binding.processAll(that.el.querySelector("#dataPanel"), that.trendsModel); //<--Right Here
});
If I remove that, then when I load the second page the data doesn't show as undefined. What is interesting is the data initially shows correctly on the second page and then it changes to "undefined".
Solution
My fix:
myLib.GetData(todaysDate, function (result) {
var element = that.el.querySelector("#dataPanel");
that.trendsModel.today = result;
if(element) {
WinJS.Binding.processAll(element, that.trendsModel);
}
});
At the point when when the callback returns, I am already on the second page. So the selector was not found returning null. If you pass null to processAll it tries to bind the whole page which is why I was able to see the correct data for a second then it changes to undefined...Wow, what a doozy. I guess it makes sense but what a pain to find.
Hope it helps someone in the future :)
Your ToDoViewModel, and HomeViewModel need to be observable. This means they need to mix in from WinJS.Binding.mixin, and for the properties that you pull in asynchronously, they need to call this.notify("propertyName", newVal, oldVal) from the property setter.
Note that you need to have getter/setter properties. e.g.
var bindingBase = WinJS.Class.mix(function() {}, WinJS.Binding.mixin);
WinJS.Namespace.define("YourNamespace", {
ToDoViewModel: WinJS.Class.derive(bindingBase, function constructor() {
}, {
_titleStorage: "",
title: {
get: function() { return this._titleStorage; },
set: function(newValue) {
if(newValue === this._titleStorage) {
return;
}
var old = this._titleStorage;
this._titleStorage = newValue;
this.notify("title", newValue, old);
}
}
}),
});
myLib.GetData(todaysDate, function (result) {
var element = that.el.querySelector("#dataPanel");
that.trendsModel.today = result;
if(element) {
WinJS.Binding.processAll(element, that.trendsModel);
}
});
At the point when when the callback returns, I am already on the second page. So the selector was not found returning null. If you pass null to processAll it tries to bind the whole page which is why I was able to see the correct data for a second then it change to undefined...Wow, what doozy. I guess it makes sense but what a pain to find.