Skip to content

Instantly share code, notes, and snippets.

@prayerslayer
Last active March 17, 2021 16:52
Show Gist options
  • Save prayerslayer/5c0a9a9b0640e621ef6b7d9c1e06023c to your computer and use it in GitHub Desktop.
Save prayerslayer/5c0a9a9b0640e621ef6b7d9c1e06023c to your computer and use it in GitHub Desktop.
Vega as Vue (v2) Component

Vega as Vue.js Component

Vega is great for data visualizations, so you don't have to write everything in D3 by yourself. But when the rest of your app is not in Vega, it is not straightforward to integrate.

Here an example how it's possible to have a Vega Vue component. It takes a Vega-lite spec as property, plus some data. It then compiles the spec to Vega and uses the View API from Vega to render it. Interaction with the rest of the application (Vue -> Vega) is done via Vega Signals. The convention here is that every signal provided as property must be a Vuex getter. The other way around (Vega -> Vue) can be done in a similar fashion, I suppose.

<template>
<Vega :signals="['inputVarsInMap']" :spec="spec" :values="correlations" />
</template>
<script lang="ts">
import { StoreState } from "@/types/store";
import { melt } from "@/util/matrix";
import { Component, Vue } from "vue-property-decorator";
import Vega from "@/components/Vega.vue";
import spec from "./spec.json";
@Component({
components: { Vega },
computed: {
correlations: function() {
const state = this.$store.state as StoreState;
const molten = melt<number>(
state.dataset.correlations,
state.dataset.colnames,
state.dataset.colnames
);
return molten;
},
spec: function() {
return spec;
}
}
})
export default class CorrelationMatrix extends Vue {}
</script>
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"mark": {
"type": "point",
"tooltip": true
},
"width": 200,
"height": 200,
"params": [
{
"name": "inputVarsInMap",
"value": []
}
],
"encoding": {
"shape": {
"value": "square"
},
"x": {
"field": "colName",
"axis": {
"title": "Correlations"
}
},
"y": {
"field": "rowName",
"axis": {
"title": null
}
},
"size": {
"condition": {
"test": "indexof(inputVarsInMap, datum.rowName) >= 0 || indexof(inputVarsInMap, datum.colName) >=0",
"value": 100
},
"value": 50
},
"stroke": {
"value": null
},
"fill": {
"field": "value",
"type": "quantitative",
"legend": null,
"scale": {
"scheme": "redblue",
"domain": [
-1,
1
]
}
}
}
}
<template>
<div :data-signals="storeSignalStateHash">
<div ref="vega"></div>
</div>
</template>
<script lang="ts">
import * as vg from "vega";
import * as vgLite from "vega-lite";
import { hash } from "@/util/util";
import { Component, Prop, Vue } from "vue-property-decorator";
class VegaClass extends Vue {
@Prop()
public spec!: any;
@Prop()
public values!: any;
@Prop()
public signals!: string[];
public view: any = null;
public actualSpec!: any;
public storeSignals!: { [k: string]: any };
}
@Component<VegaClass>({
computed: {
actualSpec: function() {
return {
...this.$props.spec,
data: { values: this.$props.values }
};
},
storeSignals: function() {
const obj: { [x: string]: any } = {};
for (const s of this.$props.signals) {
const v = this.$store.getters[s]();
obj[s] = v;
}
return obj;
},
storeSignalStateHash: function() {
return hash(JSON.stringify(this.storeSignals));
}
}
})
export default class Vega extends VegaClass {
mounted() {
const vgSpec = vgLite.compile(this.actualSpec).spec;
this.view = new vg.View(vg.parse(vgSpec), {
logLevel: vg.Warn,
renderer: "svg"
});
this.view.initialize(this.$refs.vega);
this.view.width(this.spec.width);
this.view.height(this.spec.height);
this.view.run();
}
updated() {
Object.keys(this.storeSignals).forEach(k =>
this.view.signal(k, this.storeSignals[k])
);
this.view.run();
}
beforeDestroy() {
this.view.finalize();
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment