First of all let me introduce you to my project. I am desining a web application that will show some data about devices scattered around a country. To create this I used Vue.js and HighCharts (HighMaps for the map part). This is the result I have achieved now.
What I want to add now is the possibility for the end-user to click on a marked region and show all of the devices in that region. To do that I need the region's "ID", called code in HighMaps, to send a ajax request to my db and I would also like to make this new "div" a component so that I can use it freely in my application. I'll put a sketch image of what I mean (excuse me for my really bad paint skills :D):
The black lines are not important, what I would like to achieve is to show a new component besides the map (or wherever really). Next is my current code, I am using the one page, one component style so both template and script tags are in the same file and I omitted in the script tag all the unecessary things. Right now I just set up a div with curly brackets to update a variable on change, just to debug more easily. My main problem is that, in the plotOptions.series.point.events.click when I try to reference the this.foo variable it doesn't set it since the div doesn't update. I think that might be a scope issue but I wouldn't know where to start looking.
<template>
<div id="canvasContainer">
<highmaps :options="chartOptions"></highmaps>
<app-componentForDevices><app-componentForDevices>
<div>{{foo}}</div>
</div>
</template>
<script>
import HighCharts from 'vue-highcharts';
import json from '../map.json'
export default {
data: function () {
return {
foo: 'NO',
/* Map */
chartOptions: {
chart: {
map: json, // The map data is taken from the .json file imported above
},
plotOptions: {
series: {
point: {
events: {
click: function () {
this.foo='OK'
}
}
}
},
map: {
joinBy: ['hc-key', 'code'],
allAreas: false,
tooltip: {
headerFormat: '',
pointFormat: '{point.name}: <b>{series.name}</b>'
},
}
},
/* Zoom and move */
mapNavigation: {
enabled: true,
buttonOptions: {
verticalAlign: 'bottom'
}
},
series: [
{
allAreas: true,
showInLegend: false,
},
{
borderColor: '#a0451c',
cursor: 'pointer',
name: 'ERROR',
color: "red",
data: ['it-na', 'it-mi', 'it-mo', 'it-ud'].map(function (code) {
return {code: code};
}),
},
{
borderColor: '#a09e21',
cursor: 'pointer',
name: 'WARNING',
color: "yellow",
data: ['it-ts', 'it-av', 'it-ri'].map(function (code) {
return {code: code};
}),
},
{
borderColor: '#4ea02a',
cursor: 'pointer',
name: "OK",
color: "lightgreen",
data: ['it-pa', 'it-ve', 'it-bo', 'it-ta', 'it-pn'].map(function (code) {
return {code: code};
}),
},
],
}
}
}
}
</script>
<style scoped>
svg{
align-items: center;
}
Thanks in advance for the help. Cheers!
EDIT
I have just tried #icecream_hobbit 's suggestion and using a ECMA6 arrow function helped since now I can access the variable store in Vue but now I lost the access to the local arguments like this.name which made possibile for me to print the selected region. Any ideas? Am I missing something?
EDITv2
Thanks to #icecream_hobbit I have found a way to do what I wanted. You just need to add an object inside the parenthesis so that you can use this for the global variable and e for your mouse click event.
events: {
click: (e) => {
this.foo = 'OK'
this.foo = e.point.name
}
}
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
Until arrow functions, every new function defined its own this value
(a new object in the case of a constructor, undefined in strict mode
function calls, the base object if the function is called as an
"object method", etc.)
The this you were accessing did not belong to the Vue instance. You can use an arrow function () => {/*function body*/} to inherit the this of the Vue instance.
An arrow function expression has a shorter syntax than a function
expression and does not have its own this, arguments, super, or
new.target.
EDIT: For the added question of how do I do I get the Vue instance, and the object instance at the same time you can use a closure.
click: function(VueInstance){
return function() {
VueInstance.Foo = 'OK';
}(this)
}
Related
So I have been trying to make a linechart work with Echarts. I made this LineChart.vue and expect it to get props, which are arrays, from its father component as options data of Echarts.
But the props, which are proxies of arrays, doesn't seem to work well. It is shown in the console that it has the right target, but this proxy is not recognized by Echarts, so there was no data on my chart.
And to make it wierder to me, I accidently found out that if I keep my terminal open, make some changes to the code (which is nothing but comment and uncomment the same lines), and save it (which probably rerends this component), the props somehow works and the linechart actually shows up! But if I refresh the page, the data goes blank again.
Here is my code:
<template>
<div id="chart"></div>
</template>
<script>
let chart;
export default {
data() {
return {
option: {
name: "demo",
xAxis: {
type: "category",
data: [],
},
yAxis: {
// type: "value",
},
series: [
{
data: [],
type: "line",
},
],
},
};
},
props: {
xAxisData: Array,
seriesData: Array,
},
methods: {
initChart() {
chart = this.$echarts.init(document.getElementById("chart"));
// these are the four lines that I commented and uncommented to make things wierd
this.option.xAxis.data = this.xAxisData;
this.option.series[0].data = this.seriesData;
console.log(this.option.xAxis.data);
console.log(this.option.series[0].data);
chart.setOption(this.option);
},
},
mounted() {
this.initChart();
},
watch: {
xAxisData: {
handler: function (newData) {
this.option.xAxis.data = newData;
},
deep: true,
},
seriesData: {
handler: function (newData) {
this.option.series[0].data = newData;
},
deep: true,
},
},
};
</script>
<style scoped>
#chart {
height: 250px;
width: 400px;
}
</style>
And here iswhat is the proxy like before and after I made some minor changes to the code
I also tried to turn this proxy xAxisData into an object using Object.assign(), but it turns out to be empty! I am starting to think that it might have somthing to do with component life cycle, but I have no clue when and where I can get a functional proxy. Can someone tell me what is actually going on?
FYI, here are value of props in console and value of props in vue devtool.
Just figured it out. Just so you know, the info provided above was insufficient, and I made a noob move.
My vue component was fine, it was async request that caused this problem. The data for my Echarts props is requseted through a Axios request, and my child-component (the linechart) was rendered before I got the data. Some how, the proxies of the arrays donot have the data, yet they got the target shown right. By the time my child-component got the right data, the Echart was already rendered with outdated options data, which by the way was empty. And that is why re-render it can show us the data. It has nothing to do with proxy, proxy works just fine. It is me that needs to pay more attention to aysnc movement. Also, I learned that obviously Echarts was not reactive at all, so I watched the props and updated the option like this:
watch :{
xAxisData: {
handler: function (newData) {
this.option.xAxis.data = newData;
this.chart.clear();
this.chart.setOption(this.option);
},
deep: true,
},
}
It works.
I still have trouble why certain ways of changing data work, while others do not. I tried this example:
watch: {
'$store.state.linedata': function() {this.redraw()}
},
methods: {
redraw() {
this.chartOptions.series[0].data = this.$store.state.linedata
}
},
data() {
return {
chartOptions: {
chart: {
type: this.type
},
series: [{
data: this.$store.state.linedata,
name: "Test Series",
color: this.color
}]
}
}
}
This setup works, whenever I change the linedata in my store, the component gets updated. However, for me it would be more intuitive to update the data like this, without referencing this.chartOptions.series[0].data:
redraw() {
this.$store.state.linedata = [1,2,3,4,5,6]
}
This will update the state correctly, but however not cause to update the component with the new data. Why does the second way not work and is my first way the correct way to do it? I feel like I am missunderstanding some core concept here.What would a best practice look like?
Thank you!
From the Vuex docs you can read:
The only way to actually change state in a Vuex store is by committing a mutation
It means that you should not try to do this.$store.state.linedata = [1,2,3,4,5,6]. It may throw an error depending on your Vuex settings in the console by the way. Instead, create a mutation like so:
mutations: {
updateLineDate(state, lineDate) {
state.lineData = lineData;
}
}
And then call:
this.$store.commit("updateLineDate", [1, 2, 3, 4, 5, 6]);
To automatically update your chart's data, I would suggest creating a computed property in your Vue component. To make it reactive to changes, map your attribute using mapState:
import { mapState } from "vuex";
// ...
computed: {
...mapState("lineData"),
chartData() {
return {
chart: {
type: this.type
},
series: [{
data: this.lineData,
name: "Test Series",
color: this.color
}]
}
}
}
Then remember to provide chartData to your chart component instead of chartOptions in my example.
I have different groups of words on a single page, let's say nouns verbs and adjectives. The way to describe each group is with its 'part-of-speech'. This 'part-of-speech' is being printed inside a little box. So you have the 'part-of-speech' of the group, noun, in the little box, and I want to achieve that when I click on that box I hide verbs and adjectives. If I were to click on verb I would hide nouns and adjetives, and so on. Right now the 'part-of-speech' of each group is being passed in as a prop.
The problem is that I'd like to compare parts of speech that exist on the current page with the clicked part of speech, but I cannot manage to differentiate it.
In my template I've got:
<div class="part-of-speech">
<p class="pos">{{ pos }}</p>
</div>
and this { pos } is coming from my props
props: {
pos: {
type: String,
required: false,
default: "na"
}
},
mounted() {
console.log(this.pos)
}
This gives me all the parts-of-speech that are being printed on the page (take into account that this is a child-component of something else and these groups of words are printing as many times as there are groups). So I though that adding a method to detect the clicked part-of-speech would help.
<div class="part-of-speech" #click="handleSelectedPos(pos)">
<p class="pos">{{ pos }}</p>
</div>
props: {
pos: {
type: String,
required: false,
default: "na"
}
},
methods: {
handleSelectedPos(pos) {
console.log(pos);
console.log(this.pos);
}
}
When I click on the current item, I get the current part-of-speech, and as you can see this.pos in this context is no longer the list of parts-of-speech that are on the page but it has become the currently clicked part-of-speech. My idea was to make the comparison somehow between pos and this.pos, but they are now identical.
How to make the comparison to say: If there are parts-of-speech that are not equal to the one currently clicked, take some action (add a class or wtv) to hide the element.
If I understand well, what you would like to achieve, then some of the events shouldn't be handled by the subcomponents, but by the parent component.
Vue.component('partOfSpeech', {
props: ['pos', 'text'],
template: `<div :class="pos" #click='emitEvent'>{{text}}</div>`,
methods: {
emitEvent() {
this.$emit('emitpos', this.pos)
}
}
})
new Vue({
el: '#app',
data: {
words: [{
pos: 'noun',
text: 'noun1',
compare: false
},
{
pos: 'verb',
text: 'verb1',
compare: false
},
{
pos: 'adjective',
text: 'adjective1',
compare: false
},
{
pos: 'noun',
text: 'noun2',
compare: false
},
{
pos: 'verb',
text: 'verb2',
compare: false
},
{
pos: 'adjective',
text: 'adjective2',
compare: false
},
{
pos: 'verb',
text: 'verb3',
compare: false
},
{
pos: 'noun',
text: 'noun3',
compare: false
},
{
pos: 'adjective',
text: 'adjective1',
compare: false
},
]
},
methods: {
filterWords(val) {
this.words.forEach(e => {
if (e.pos === val) {
e.compare = true
} else {
e.compare = false
}
})
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<part-of-speech :key="i" v-for="(word, i) in words" :pos="word.pos" :text="word.text" v-on:emitpos="filterWords" :style="{ color: !word.compare ? 'black' : 'red'}" />
</div>
In the snippet above you can see that every
data is passed down to the child component as prop (except for compare - that's not needed there)
a click event is set up on each of the child components, that (#click) $emit() an event and their prop back to the parent
the parent has a v-on: for the event emitted, and executes a function on ALL the parts of speech (words in my app; the function colors words red that have the same pos as the clicked one).
MORE INFO
The problem is that sibling elements do not know anything about each other: they’re not supposed to share any information with each other.
A component can share its own unique state information with sibling components either via their common parent (by emitting an event (with a data payload)) or by using some form of state management solution (event bus or Vuex store are the most common in Vue - the latter is for serious cases, the former is for occasions that require more than simple event emitting, but doesn’t require anything really complicated).
Custom events in Vue: https://v2.vuejs.org/v2/guide/components-custom-events.html
Event bus: https://alligator.io/vuejs/global-event-bus/
Vuex state management: https://vuex.vuejs.org/
I have a map of the United States rendered in Highmaps, with enableDoubleClickZoomTo set to true. I've gotten stuck trying to discern what state a user has double clicked on to zoom the map, and wondered if there was information buried in the redraw event that would help me calculate this.
Here's a fiddle of the issue: http://jsfiddle.net/tjnicolaides/x8q1d1cs/
$('#container').highcharts('Map', {
chart: {
events: {
redraw: function (event) {
console.log(event);
console.log(this.getSelectedPoints());
}
}
},
mapNavigation: {
enabled: true,
enableDoubleClickZoomTo: true
},
series: [{
data: data,
mapData: Highcharts.maps['countries/us/us-all'],
joinBy: 'hc-key',
allowPointSelect: true,
states: {
hover: {
color: '#BADA55'
},
select: {
color: 'purple'
}
}
}]
});
When I console.log event after double clicking on a state, I'm getting a large object with information about the state of the entire chart.
If I single click to select a state, and then double click to zoom in on it, I get information about the state from this.getSelectedPoints() - however, it is not reasonable to expect that anything will be selected prior to zooming. In some maps, it may be disabled altogether. I included it here as a demonstration of the type of output I was originally hoping to get from redraw. Calculating a postal code, state name, or index to filter the original series with would be acceptable.
I think the easiest way to detect that point by wrapping Pointer.onContainerDblClick, like this:
(function (H) {
H.wrap(H.Pointer.prototype, "onContainerDblClick", function (p, event) {
console.log(this.chart.hoverPoint); // hovered point - may not exist, e.g. when clicking on the blank space
p.call(this, event);
});
})(Highcharts)
And live demo: http://jsfiddle.net/x8q1d1cs/8/
In my MVC application, I am trying to create a SVG Map with data that comes from a database. Using jQuery, I call an action in the controller which returns data in the format that is expected in the MapSvg region's parameter.
The format that is expected goes as follows:
regions : {
'Yemen': {disabled: true},
'USA': {tooltip: 'USA: Click to go to Google.com', attr: {fill: '#ff0000', href: 'http://google.com', 'cursor': 'help'}}},
'France': {tooltip: 'This is France!', attr: {'cursor': 'help'}},
'Kazakhstan': {tooltip: 'Kazakhstan - the ninth largest country in the world.'}
},
In my controller, I have an action that will be called in the view by a jQuery ajax request
public ActionResult GetCountries()
{
List<ScratchMap> allitems = this.Worker.GetAllItems();
var allItemsAsArray = allitems.Select(x => string.Format("'{0}': {{ tooltip: 'Test', attr: {{ fill: '{1}' }} }}", x.PluginCountryName, x.Color)).ToArray();
return Json(allItemsAsArray, JsonRequestBehavior.AllowGet);
}
In the View, the following code is executed after the jQuery plugins and the MapSvg plugins are loaded:
$.get('/ScratchMap/GetCountries', {},
function (data) {
var regionsData = '{' + data + '}';
$('#map').mapSvg(
{
source: '#Url.Content("~/Content/Maps/world_high.svg")',
loadingText: 'Loading map...',
tooltipsMode: 'names',
responsive: true,
zoom: true,
pan: true,
zoomButtons: {
'show': true,
'location': 'right'
},
regions: regionsData
});
}), 'json';
When the page is rendered, the map does not fill any countries that were retrieved from the database. However, when I copy and assign the output of the regionsData variable directly to the regions parameter, the map loads everything correctly.
The following article teaches me that this could have something to do with the input data type. However, if I parse the regionsData to JSON, it tells me it is in a wrong format. But the given example by the creators of MapSvg is also in a wrong format.
Does anybody have any ideas for a workaround?
Thanks.
The problem is that even if the regions variable is edited the map plugin doesnt watch the change in its object contents so you must either wait till the data is returned before graphing the contents. Or re-graph with the method below.
If you wish to wait to graph till the data has been returned from your database call you can use a promise to delay the graphing of the object. Also important to know is the format in which the OPTS variables needs to have here is a sample.
var OPTS = {
source: sourcepath, // Path to SVG map
colors: {background: '#fff',base: '#0066FF', stroke: '#fff', selected: 10, disabled: '#ff0000'},
tooltipsMode: 'combined',
zoom: true,
pan: true,
responsive: true,
width: 1170,
zoomLimit: [0,100],
onClick: function (e, m) {
//do something here on each map click
},
regions:{
id_of_svg_path(the actual region you want to add data for):{
disabled:true,/*or whatever you need*/
tooltip:'<h4>Something to say</h4>'
}
}
}
In-order to re-graph I used the return object:
OPTS are just your specific chart options.
A variable javascript object that contains a regions variable.
var chartObj = $('#chart_container').mapSvg(OPTS);
chartObj.destroy();
This call will destroy then you re-graph it with OPTS that you have passed in.
Once you have destroyed it and passed in the new data you can just call.
var chartObj = $('#chart_container').mapSvg(OPTS);
Re-graphing it with the new data.
It turns out that it was an issue with the way I created the javascript output. The answer to this question can be found in this article.