Using paperjs on a vuejs project based on SFC - vue.js

I have a vuejs project based on single file components. I want to add a canvas and dinamically draw things with paperjs, based on my component data. What is the proper way to do it?

Self response. A working SFC.
<template>
<canvas resize id="main-canvas">{{circle_diameter}}</canvas>
</template>
<script>
import paper from 'paper'
export default {
data: () => ({ x: 20, y: 20 }),
props: ['circle_diameter'],
methods: {
updateDrawing() {
paper.setup(document.getElementById('main-canvas'))
// Now, draw your things based on component state.
const point = new paper.Point(this.x, this.y)
const circle = new paper.Path.Circle(point, this.circle_diameter/2)
circle.fillColor = 'grey'
circle.strokeColor = 'black'
},
},
updated() {
this.updateDrawing()
},
}
</script>
EDIT
Since paperjs will render outside vue scope, drawing is not reactive until you place {{circle_diameter}} into the canvas html tags. This way, you force Vue to call update() each time a prop changes.

Related

JS Vue3 dynamic bar chart graph with real-time reactive data

Goal
I'm using Vue3 with the Composition API. I want to stream real-time data into a chart / graph (60 fps, possibly multiple streams / multiple updating arrays). To simplify for this question, I want to create a bar chart that updates it bars reactively to data changes by a button.
Attempts
I tried various JavaScript chart libraries like PrimeVue (Chart.js under the hood), vue-chartjs,Vue-ECharts, plotly.js. However, I'm still struggling either getting it to work or getting smooth animation. I thought real-time plotting would be a more common case, but I have a hard time finding examples (or my Google-foo fails me).
PrimeVue attempt
My best progress is with PrimeVue. I got the reactive part to work, but the issue is that the whole graph animates from scratch at each data update.
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import PrimeVue from 'primevue/config';
import Chart from 'primevue/chart';
const app = createApp(App);
app.use(PrimeVue);
app.component('Chart', Chart);
app.mount("#app");
<script setup lang="ts">
import { reactive, ref } from 'vue'
const chartValues = reactive([0.3, 1])
const basicData = ref({
labels: ['January', 'February'],
datasets: [
{
label: 'My First dataset',
backgroundColor: '#42A5F5',
data: chartValues
},
{
label: 'My Second dataset',
backgroundColor: '#FFA726',
data: [0.4, 0.1]
}
]
});
const horizontalOptions = ref(
{
// animation: {
// duration: 0
// }
}
);
function increment() {
chartValues[0] += 0.1
if (chartValues[0] > 1) {
chartValues[0] = 0
}
}
</script>
<template>
<h1>PrimeVue chart 2</h1>
<button #click="increment">count is: {{ chartValues[0] }}</button>
<div>
<div class="card">
<h5>Horizontal</h5>
<Chart type="bar" :data="basicData" :options="horizontalOptions" />
</div>
</div>
</template>
I can prevent the whole graph animation by setting animation: {duration: 0}, but this does not look dynamic/fluid. There is no transition between values.
Questions
It would be helpful if you could:
(best) Share a code snippet / link to a demo (any chart/graph library is okay) that only updates bars in a bar chart for which the data has changed (or line point in a line chart) with a smooth transition (or anything related to a single array) using Vue3.
Recommend a good JS chart library for smooth real-time plot animation that works with Vue3's reactive() / ref().
Any other advice that would help me
It depends on what kind of changes you want to make.
If you only want to change existing data, then it's relatively easy. If you want to add additional data to the chart it gets quite a bit harder, but based on your example, you're looking to mutate the a specific data inside an array so I won't cover the later.
The problem appears to be that these libraries don't handle the reactive data. Whether you have a reactive in ref or just using reactive The data passed to chart.js loses it's reactivity. I haven't looked into any of them to see why, but seems like prime-vue, vue-chartjs, and #j-t-mcc/vue3-chartjs all lose reactivity. I suspect they might be watching reactivity at a higher level, and when arrays and objects are mixed within reactive, it doesn't work well
to get around it, you can can manually call the update of the chartjs component. To do that pass the component a ref and then call the update() method when you are making an update (or using a watch)
<script>
import { reactive, ref, watch } from "vue";
export default {
setup() {
const barGraph = ref(null); // define the $ref
const basicData = reactive({
labels: ["January", "February"],
datasets: [
{
label: "My First dataset",
backgroundColor: "#42A5F5",
data: [0.3, 1],
},
{
label: "My Second dataset",
backgroundColor: "#FFA726",
data: [0.4, 0.1],
},
],
});
const horizontalOptions = {
animation: {
duration: 400,
},
};
function increment() {
let val = basicData.datasets[0].data[0];
basicData.datasets[0].data[0] = ((val * 10 + 1) / 10) % 1;
// when the data changes and the barGraph $ref is not null
if (barGraph.value) {
// call update method
barGraph.value.chart.update();
}
}
return {
basicData,
horizontalOptions,
increment,
barGraph,
};
},
};
</script>
<template>
<button #click="increment">
count is: {{ basicData.datasets[0].data[0] }}
</button>
<div>
<div class="card">
<h5>Horizontal</h5>
<Chart
type="bar"
:data="basicData"
:options="horizontalOptions"
ref="barGraph"
/>
</div>
</div>
</template>
With the Vue-ApexCharts library and learning from Daniel's answer, I was able to get a reactive bar chart to work.
Install Vue-ApexCharts in Vue3:
npm install --save apexcharts
npm install --save vue3-apexcharts
main.ts
import { createApp } from 'vue'
import App from './App.vue'
import VueApexCharts from "vue3-apexcharts";
const app = createApp(App);
app.use(VueApexCharts);
app.mount("#app");
App.vue
<template>
<div id="app">
<button #click="increment">Increment bar 1</button>
<button #click="addSeries">Add data series</button>
<button #click="incrementExtra">Increment appended series data</button>
<VueApexCharts :options="chartData.options" :series="chartData.series"/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import VueApexCharts from 'vue3-apexcharts'
const seriesData1 = reactive([0.3, 1])
const seriesDataExtra = ref([0.1, 0.7])
const chartData = reactive({
series: [
{
name: 'My First series',
data: seriesData1
},
{
name: 'My Second series',
data: [0.4, 0.1]
}
],
options: {
chart: {
type: 'bar',
// https://apexcharts.com/docs/options/chart/animations/
animations: {
enabled: true,
easing: 'linear',
speed: 200,
animateGradually: {
enabled: false
},
dynamicAnimation: {
enabled: true,
speed: 150
}
}
},
xaxis: {
categories: ['January', 'February']
}
}
})
function increment() {
seriesData1[0] = ((seriesData1[0] * 10 + 1) / 10) % 1 // + 0.1
// chartData.series[0].data[0] = ((chartData.series[0].data[0] * 10 + 1) / 10) % 1
console.log(seriesData1)
}
function incrementExtra() {
seriesDataExtra.value = seriesDataExtra.value.map(element => ((element * 10 + 1) / 10) % 1)
console.log(seriesDataExtra)
}
function addSeries() {
console.log("Add extra series")
chartData.series.push({
name: 'My Next series',
data: seriesDataExtra
})
}
</script>
The above code can update a single bar's data, which triggers only an animation for that bar. This is done by:
Creating a reactive variable: const seriesData1 = reactive([0.3, 1])
With data: seriesData1 we assign this reactive array to the Chart
The first button triggers the function increment, which increments the first bar's data by 0.1: seriesData1[0] = ((seriesData1[0] * 10 + 1) / 10) % 1
Note that for step 3 we could have also directly updated the data in the Chart with chartData.series[0].data[0] = ((chartData.series[0].data[0] * 10 + 1) / 10) % 1 and this would give the same effect (this line of code also updates the variable seriesData1).
However, by updating the reactive variable seriesData1, we can separate data and the chart logic. You don't have to know how to assign data to the chart, but still get your reactive chart by just modifying the data.
As a bonus I also added a demonstration on how to:
Add an extra data stream / series to the plot with the function addSeries and
Reactively animate this new data by incrementing all values in a ref array using the function incrementExtra.
Modify the animation behavior so it updates all bars at the same time without a bouncy effect (default)
Demo video: https://i.imgur.com/e5a0y8Z.mp4

Transition using this.router.push with tracking end of animation in VUE CLI

I really hope for your help! I'm a beginner, this is my first experience in creating web applications.
I work with Vue Cli, there is a lottie element that should be animated by click (I did it), then I should go to the “other page” (I did it) But, how do I implement the transition to the page only after the animation finishes? Help! You are welcome! For animation I use Anime.js
<script>
import { translate } from '../js/animate'
export default {
methods: {
go () {
translate(this.$refs.square)
this.$router.push('Comprestore')
}
}
}
</script>
/vue
<template>
<div id="animate" v-on:click = "go" ref="square">
<app-lottie></app-lottie>
</div>
</template>
<style scoped>
</style>
import anime from 'animejs'
export function translate (element) {
anime({
targets: element,
translateX: 500
})
}
You can use complete callback to wait until the animation is completed.
Now your code may looks like this:
...
go () {
translate(this.$refs.square, () => {
this.$router.push('Comprestore')
})
}
...
And
export function translate (element, callback) {
anime({
targets: element,
translateX: 500,
complete: callback
})
}
I create the example here.
In the example I also use page transition by using Vue built-in transition to transition between page. See Enter/Leave & List Transitions and Transitions in Vue Router.

Lazy loading a specific component in Vue.js

I just make it quick:
In normal loading of a component (for example "Picker" component from emoji-mart-vue package) this syntax should be used:
import {Picker} from "./emoji-mart-vue";
Vue.component("picker", Picker);
And it works just fine.
But when I try to lazy load this component I'm not sure exactly what code to write. Note that the following syntax which is written in the documentation doesn't work in this case as expected:
let Picker = ()=>import("./emoji-mart-vue");
The problem, I'm assuming, is that you're using
let Picker = ()=>import("./emoji-mart-vue");
Vue.component("picker", Picker);
to be clear, you're defining the component directly before the promise is resolved, so the component is assigned a promise, rather than a resolved component.
The solution is not clear and depends on "what are you trying to accomplish"
One possible solution:
import("./emoji-mart-vue")
.then(Picker=> {
Vue.component("picker", Picker);
// other vue stuff
});
This will (block) wait until the component is loaded before loading rest of the application. IMHO, this defeats the purpose of code-spliting, since the application overall load time is likely worse.
Another option
is to load it on the component that needs it.
so you could put this into the .vue sfc that uses it:
export default {
components: {
Picker: () => import("./emoji-mart-vue")
}
};
But this would make it so that all components that use it need to have this added, however, this may have benefits in code-splitting, since it will load only when needed the 1st time, so if user lands on a route that doesn't require it, the load time will be faster.
A witty way to solve it
can be done by using a placeholder component while the other one loads
const Picker= () => ({
component: import("./emoji-mart-vue"),
loading: SomeLoadingComponent
});
Vue.component("picker", Picker);
or if you don't want to load another component (SomeLoadingComponent), you can pass a template like this
const Picker= () => ({
component: import("./emoji-mart-vue"),
loading: {template:`<h1>LOADING</h1>`},
});
Vue.component("picker", Picker);
In PluginPicker.vue you do this:
<template>
<picker />
</template>
<script>
import { Picker } from "./emoji-mart-vue";
export default {
components: { Picker }
}
</script>
And in comp where you like to lazy load do this:
The component will not be loaded until it is required in the DOM, which is as soon as the v-if value changes to true.
<template>
<div>
<plugin-picker v-if="compLoaded" />
</div>
</template>
<script>
const PluginPicker = () => import('./PluginPicker.vue')
export default {
data() = { return { compLoaded: false }}
components: { PluginPicker }
}
// Another syntax
export default {
components: {
PluginPicker: () => import('./PluginPicker.vue')
}
}
</script>

Delay the rendering of a component in vue

I've created a component in vue which wraps a vue-apexchart donut graph. As soon as the page loads and this component is loaded, the vue-apexchart animates and displays a small graph.
Now I would like to instantiate multiple of these components from a dataset side by side. Instead of the components to all load an animate at the same time, I would like a small rendering delay to give it an overall nice effect. Something like this would be nice:
<donut :items="series1"></donut>
<donut :items="series2" delay=1500></donut>
The vue-apexchart doesent support initialization delays, and as far as I can see there isn't any vue-specific official solution to delay the rendering of components.
I've tried to put a setTimeout in any of the component hooks to stall the initialization,
I´ve also tried to inject the all the graph DOM in the template element on a v-html tag in a setTimeout, but apexchart doesent notice this new dom content, and vue doesent notice the html bindings either.
I´ve created this fiddle which loads two instances of a graph:
https://jsfiddle.net/4f2zkq5c/7/
Any creative suggestions?
There are several ways you can do this, and it depends on whether you can actually modify the <animated-component> logic yourself:
1. Use VueJS's built-in <transition-group> to handle list rendering
VueJS comes with a very handy support for transitions that you can use to sequentially show your <animated-component>. You will need to use a custom animation library (like VelocityJS) and simply store the delay in the element's dataset, e.g. v-bind:data-delay="500". VueJS docs has a very good example on how to introduce staggered transitions for <transition-group>, and the example below is largely adapted from it.
You then use the beforeAppear and appear hooks to set the opacity of the individual children of the <transition-group>.
Vue.component('animated-component', {
template: '#animatedComponentTemplate',
props: {
data: {
required: true
}
}
});
new Vue({
el: '#app',
data: {
dataset: {
first: 'Hello world',
second: 'Foo bar',
third: 'Lorem ipsum'
}
},
methods: {
beforeAppear: function(el) {
el.style.opacity = 0;
},
appear: function(el, done) {
var delay = +el.dataset.delay;
setTimeout(function() {
Velocity(
el, {
opacity: 1
}, {
complete: done
}
)
}, delay)
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<transition-group name="fade" v-on:before-appear="beforeAppear" v-on:appear="appear">
<animated-component v-bind:data="dataset.first" v-bind:key="0"> </animated-component>
<animated-component v-bind:data="dataset.second" v-bind:key="1" v-bind:data-delay="500"> </animated-component>
<animated-component v-bind:data="dataset.third" v-bind:key="2" v-bind:data-delay="1000"> </animated-component>
</transition-group>
</div>
<script type="text/x-template" id="animatedComponentTemplate">
<div>
<h1>Animated Component</h1>
{{ data }}
</div>
</script>
2. Let <animated-component> handle its own rendering
In this example, you simply pass the a number to the delay property (remember to use v-bind:delay="<number>" so that you pass a number and not a string). Then, in the <animated-component>'s mounted lifecycle hook, you use a timer to toggle the visibility of the component itself.
The technique on how you want to show the initially hidden component is up to you, but here I simply apply an initial opacity of 0 and then transition it after a setTimeout.
Vue.component('animated-component', {
template: '#animatedComponentTemplate',
props: {
data: {
required: true
},
delay: {
type: Number,
default: 0
}
},
data: function() {
return {
isVisible: false
};
},
computed: {
styleObject: function() {
return {
opacity: this.isVisible ? 1 : 0
};
}
},
mounted: function() {
var that = this;
window.setTimeout(function() {
that.isVisible = true;
}, that.delay);
}
});
new Vue({
el: '#app',
data: {
dataset: {
first: 'Hello world',
second: 'Foo bar',
third: 'Lorem ipsum'
}
}
});
.animated-component {
transition: opacity 0.25s ease-in-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<animated-component v-bind:data="dataset.first"> </animated-component>
<animated-component v-bind:data="dataset.second" v-bind:delay="500"> </animated-component>
<animated-component v-bind:data="dataset.third" v-bind:delay="1000"> </animated-component>
</div>
<script type="text/x-template" id="animatedComponentTemplate">
<div class="animated-component" v-bind:style="styleObject">
<h1>Animated Component, delay: {{ delay }}</h1>
{{ data }}
</div>
</script>
If you have the possibility to reformat your data, you can build an array of series objects, add a show: true/false property and iterate it:
//template
<div v-for="serie in series">
<donut :items="serie.data" v-if="serie.show"></donut>
</div>
//script
data: function() {
return {
series: [
{ data: [44, 55, 41, 17, 15], show: false },
{ data: [10, 20, 30], show: false },
]
}
}
Now you can create a setTimeout function which will change the serie.show to true by incrementing the delay based on the serie index.
Then add the function on the mounted hook:
methods: {
delayedShow (serie, idx) {
let delay = 1500 * idx
setTimeout(() => {
serie.show = true
}, delay)
}
},
mounted () {
this.series.forEach((serie, idx) => {
this.delayedShow(serie, idx)
})
}
Live example
Faced the same problem with ApexCharts Pie Charts being redrawn rapidly in sequence due to data being pulled from a pinia store mutating too quickly for the chart to keep up, leading to ugly errors in the console.
I resolved the issue by using a boolean ref in a v-if="showChart" on the component and then using a setTimeout to trigger a delayed drawing of the chart:
import { ref } from "vue";
import useStore from "#/store/myChartStore";
const store = useStore();
const showChart = ref(false);
store.$subscribe((mutation, state) =>{
showChart.value = false;
setTimeout(()=> {
showChart.value = true;
}
, 100);
});
If you're not using a store, you may find another way to watch the initial availability of the chart data and then delay the rendering using that same approach.

How do I update props on a manually mounted vue component?

Question:
Is there any way to update the props of a manually mounted vue component/instance that is created like this? I'm passing in an object called item as the component's data prop.
let ComponentClass = Vue.extend(MyComponent);
let instance = new ComponentClass({
propsData: { data: item }
});
// mount it
instance.$mount();
Why
I have a non vue 3rd party library that renders content on a timeline (vis.js). Because the rest of my app is written in vue I'm attempting to use vue components for the content on the timeline itself.
I've managed to render components on the timeline by creating and mounting them manually in vis.js's template function like so.
template: function(item, element, data) {
// create a class from the component that was passed in with the item
let ComponentClass = Vue.extend(item.component);
// create a new vue instance from that component and pass the item in as a prop
let instance = new ComponentClass({
propsData: { data: item },
parent: vm
});
// mount it
instance.$mount();
return instance.$el;
}
item.component is a vue component that accepts a data prop.
I am able to create and mount the vue component this way, however when item changes I need to update the data prop on the component.
If you define an object outside of Vue and then use it in the data for a Vue instance, it will be made reactive. In the example below, I use dataObj that way. Although I follow the convention of using a data function, it returns a pre-defined object (and would work exactly the same way if I'd used data: dataObj).
After I mount the instance, I update dataObj.data, and you can see that the component updates to reflect the new value.
const ComponentClass = Vue.extend({
template: '<div>Hi {{data}}</div>'
});
const dataObj = {
data: 'something'
}
let instance = new ComponentClass({
data() {
return dataObj;
}
});
// mount it
instance.$mount();
document.getElementById('target').appendChild(instance.$el);
setTimeout(() => {
dataObj.data = 'another thing';
}, 1500);
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="target">
</div>
This has changed in Vue 3, but it's still possible when using the new Application API.
You can achieve this by passing a reactive object to App.provide():
<!-- Counter.vue -->
<script setup>
import { inject } from "vue";
const state = inject("state");
</script>
<template>
<div>Count: {{ state.count }}</div>
</template>
// script.js
import Counter from "./Counter.vue";
let counter;
let counterState;
function create() {
counterState = reactive({ count: 0 });
counter = createApp(Counter);
counter.provide("state", counterState);
counter.mount("#my-element");
}
function increment() {
// This will cause the component to update
counterState.count++;
}
In Vue 2.2+, you can use $props.
In fact, I have the exact same use case as yours, with a Vue project, vis-timeline, and items with manually mounted components.
In my case, assigning something to $props.data triggers watchers and the whole reactivity machine.
EDIT: And, as I should have noticed earlier, it is NOT what you should do. There is an error log in the console, telling that prop shouldn't be mutated directly like this. So, I'll try to find another solution.
Here's how I'm able to pass and update props programmatically:
const ComponentClass = Vue.extend({
template: '<div>Hi {{data}}</div>',
props: {
data: String
}
});
const propsObj = {
data: 'something'
}
const instance = new ComponentClass()
const props = Vue.observable({
...instance._props,
...propsObj
})
instance._props = props
// mount it
instance.$mount();
document.getElementById('target').appendChild(instance.$el);
setTimeout(() => {
props.data = 'another thing'; // or instance.data = ...
}, 1500);
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="target">
</div>