I've run into an issue with ChartJS's Pan/Zoom plugin event onPan not firing when the Chart configuration is assigned from inside of a useEffect hook. I am listening to the pan & zoom event and firing an event bus accordingly to synchronize multiple charts' zoom/pan bounds with each other. The problem arises with only onPan as onZoom works perfectly fine.
Here's the general important code (this is all in a functional component):
useEffect(() => {
if(applicableEffectCondition.isMet) {
return; // to prevent infinite loop
}
// do code magic ...
function dispatchInteractEvent(reference: MutableRefObject < any | undefined > ) {
dispatch({
type: "##charts/INTERACT",
payload: {
sender: reference.current.id,
x: {
min: reference.current.scales.x.min,
max: reference.current.scales.x.max,
},
}
});
}
const options: ChartOptions = {
// ...
plugins: {
zoom: {
limits: {
// minimum X value & maximum X value
x: {
min: Math.min(...labelsNumber),
max: Math.max(...labelsNumber)
},
},
zoom: {
wheel: {
enabled: true,
speed: 0.1,
},
pinch: {
enabled: true
},
mode: 'x',
onZoom: () => {
alert("I am zooming!");
dispatchInteractEvent(chartRef);
}
},
pan: {
enabled: true,
mode: 'x',
rangeMin: {
y: allDataMin
},
rangeMax: {
y: allDataMax
},
onPan: () => {
alert("I am panning!");
dispatchInteractEvent(chartRef);
}
}
as any
}
},
};
setChartOptions(options); // use state
const data: ChartData = {
labels: labelsString,
datasets: datasetState.getDatasets(),
};
setChartData(data); // use state
}, [applicableChannels]);
useBus(
'##charts/INTERACT',
(data) => {
const payload = data.payload;
if (payload.sender === chartRef.current.id) {
return;
}
// update chart scale/zoom/pan to be in sync with all others
chartRef.current.options.scales.x.min = payload.x.min;
chartRef.current.options.scales.x.max = payload.x.max;
chartRef.current.update();
},
[],
);
return (
<Chart type={"line"} ref={chartRef} options={chartOptions} data={chartData} />
);
As you can see in the configuration, I put two alerts to notify me when the events are fired. "I am zooming!" is fired perfectly fine, but "I am panning!" is never fired. Nor is any other pan-specific event.
This is a very odd issue. Any help would be appreciated!
Thanks!
The issue has been resolved.
When a chart is initialized, HammerJS is initialized and adds listeners for panning/zooming to the canvas. The issue happens when the chart is initialized with a configuration before the configuration is created (setting the configuration to an empty state), and when the real configuration is updated (with the zoom/pan plugin), the state of enabled in the pan object is not updated correctly, so panning becomes disabled.
The solution is to wait to display the chart React Element until the configuration is available to populate it.
This issue was really happening because the configuration was being set in a useEffect hook, making it available in a later event loop tick than the actual JSX element was being created in.
When you use useEffect(()=>{},[xxx]) hooks with [xxx] as the second parameter, you should know that only when xxx has changed it will run the function. In your case, I didn't see any "applicableChannels" in your code, so it's hard to know where the problem is.
Related
Env
I have a Vue 3 Application which requires a constant setInterval() to be running in the background (Game Loop).
I put that in store/index.js and call it from views/Playground.vue on mounted().
When leaving Playground i call beforeUnmount(). Making sure that not multiple setInterval() are running.
// store/index.js
startGameLoop({ commit, dispatch, getters }) {
commit(
"setIntervalId",
setInterval(() => {
dispatch("addPower", getters["ship/getFuelPerSecond"]);
}, 1000)
);
},
// Playground.vue
beforeUnmount() {
clearInterval(this.intervalId);
}
In the top section of Playground.vue there is a score displayed and updated within the setInterval(). I use a library called gsap to make the changing numbers a bit pleasant for the eye.
<h2>Points: {{ tweened.toFixed(0) }}</h2>
watch: {
points(n) {
console.log("gsap");
gsap.to(this, { duration: 0.2, tweened: Number(n) || 0 });
},
},
Problem
methods from the Playground.vue are fired differently and i'm struggling to understand why that is the case.
gsap
the watch from the gsap is fired every second like i would expect from the setInterval() but...
Image
In the center of the Playground i display and image where the src part is v-bind to a method called getEnemyShipImage. In the future i would like to change the displayed enemie ship programmatically - but the method is called 34 times per second. Why is that?
<img
:class="{ flashing: flash }"
#click="fightEnemie()"
:src="getEnemyShipImage()"
alt=""
/>
getEnemyShipImage() {
console.log("image");
return require("#/assets/ships/ship-001.png");
}
Log (Browser)
Console Log Output
moved it to a part without using a method and switch changing images to a watch.
data: () => {
return {
...
selectedImages: "",
images: [
require("#/assets/ships/ship-001.png"),
require("#/assets/ships/ship-002.png"),
require("#/assets/ships/ship-003.png"),
],
};
},
// initial value
mounted() {
this.selectedImages =
this.images[Math.floor(Math.random() * this.images.length)];
this.$store.dispatch("startGameLoop");
}
// watch score
watch: {
score() {
this.selectedImages =
this.images[Math.floor(Math.random() * this.images.length)];
},
}
it's not perfect but better as initialy.
I am using Chart.js 3.5 and Vue 3.
I was successfully able to draw a chart, and I am trying to trigger a data change, inside a Vue method. Unfortunately, I encounter the following issue: "Uncaught TypeError: Cannot set property 'fullSize' of undefined".
Edit2: Added a missed }. Code should now be runnable
MyChart.vue:
<template>
<canvas id="chartEl" width="400" height="400" ref="chartEl"></canvas>
<button #click="addData">Update Chart</button>
</template>
<script>
import Chart from 'chart.js/auto';
export default {
name: "Raw",
data() {
return {
chart: null
}
},
methods: {
createChart() {
this.chart= new Chart(this.$refs["chartEl"], {
type: 'doughnut',
data: {
labels: ['VueJs', 'EmberJs', 'ReactJs', 'AngularJs'],
datasets: [
{
backgroundColor: [
'#41B883',
'#E46651',
'#00D8FF',
'#DD1B16'
],
data: [100, 20, 80, 20]
}
]
},
options: {
plugins: {}
}
})
},
addData() {
const data = this.chart.data;
if (data.datasets.length > 0) {
data.labels.push('data #' + (data.labels.length + 1));
for (var index = 0; index < data.datasets.length; ++index) {
data.datasets[index].data.push(123);
}
// Edit2: added missed }
this.chart.update(); } // this line seems to cause the error}
}
},
mounted () {
this.createChart()
},
}
</script>
Edit1: Adding the following to the options makes the chart update successfully, but the error is still present and the animation does not work. The chart flickers and displays the final (updated) state. Other animations, such as hiding/showing arcs do not seem to be afected
options: {
responsive: true,
}
Edit3: Adding "maintainAspectRatio:false" option seems to again stop chart from updating (the above mentioned error is still present)
By walking through the debugger, the following function from 'chart.esm.js' seems to be called successfully a few times, and then error out on last call:
beforeUpdate(chart, _args, options) {
const title = map.get(chart); // this returns null, which will cause the next call to error with the above mentioned exception.
layouts.configure(chart, title, options);
title.options = options;
},
//////////////////////
configure(chart, item, options) {
item.fullSize = options.fullSize;
item.position = options.position;
item.weight = options.weight;
},
This may be a stale post but I just spent several hours wrestling with what seems like the same problem. Perhaps this will help you and/or future people with this issue:
Before assigning the Chart object as an attribute of your Vue component, call Object.seal(...) on it.
Eg:
const chartObj = new Chart(...);
Object.seal(chartObj);
this.chart = chartObj;
This is what worked for me. Vue aggressively mutates attributes of objects under its purview to add reactivity, and as near as I can tell, this prevents the internals of Chart from recognising those objects to retrieve their configurations from its internal mapping when needed. Object.seal prevents this by barring the object from having any new attributes added to it. I'm counting on Chart having added all the attributes it needs at init time - if I notice any weird behaviour from this I'll update this post.
1 year later, Alan's answer helps me too, but my code failed when calling chart.destroy().
So I searched and found what seems to be the "vue way" of handling it: markRaw, here is an example using options API:
import { markRaw } from 'vue'
// ...
export default {
// ...
beforeUnmount () {
if (this.chart) {
this.chart.destroy()
}
},
methods: {
createChart() {
const chart = new Chart(this.$refs["chartEl"], {
// ... your chart data and options
})
this.chart = markRaw(chart)
},
addData() {
// ... your update
this.chart.update()
},
},
}
so I am trying to update a boolean state using this.setState inside the event dataPointMouseEnter of a Apexcharts line graph. However, I got an error saying
Uncaught TypeError: Cannot read property 'filter' of null
at e.filter (apexcharts.common.js:15731)
at t.value (apexcharts.common.js:622)
at t.value (apexcharts.common.js:662)
at t.value (apexcharts.common.js:1095)
Once I have this error, the event dataPointMouseLeave will not trigger.
I tried to see where the exact problem is so I console logged out some statement, and it seems that the code gets this error while I have the line this.setState({ showYAxis: true })
Below is the code I have for this part
The options configuration on the events part for the graph, and the returning component
state = {showYAxis: false, series: []}
render() {
const options = {
chart: {
events: {
dataPointMouseEnter: this.onDataPointMouseEnter,
dataPointMouseLeave: this.onDataPointMouseLeave,
}
}
}
return (
<Chart
type="line"
height="500"
options={options}
series={this.state.series}
/>
);
}
These are the code for the two helper functions onDataPointMouseEnter and onDataPointMouseLeave
onDataPointMouseEnter = (event, chartContext, config) => {
const { series, showYAxis } = this.state;
if(series && series[config.seriesIndex].name === "myYValue") {
this.setState({ showYAxis: true });
}
}
onDataPointMouseLeave = (event, chartContext, config) => {
console.log("This line should be printed in console when mouse leave data point"); // This line did not get called
}
When I remove the line this.setState({ showYAxis: true }), both functions gets called when mouse enter and mouse leave the data point. I'm trying to hide/show the y axis and the way I'm approaching this is to use setState to call a re-render, but now setState is causing an error. I would appreciate any kind of help and advise.
I got a codesandbox that reproduces my problem: codesandbox example watcher not triggering.
I am working on a component that relies on an object with data that can be dynamically added to the object, so for example, in a seperate .js file, I am exporting the following object:
export default {
defaultSection1: {
displayName: 'Action',
},
defaultSection2: {
displayName: 'Thriller',
},
}
I import this object in my component.
I got a debouncer setup from Lodash so that when data changes, it only fires the watcher once after two seconds. The watcher IS triggered perfectly fine for the data that is already in the object (type in the input text box in my example, and the watcher is triggered). But when I dynamically add data to the object, the watcher is not triggered at all. Only when I change routes back and forth, the data is updated, but the watcher is not triggered. Why is this? What can I do so that the watcher is triggered when data is dynamically being added to an object.
methods: {
fetchAndUpdateData(){
console.log('Fetching data...')
},
addCustomSection(){
//The watcher should be triggered when this function is called
const newSectionId = Math.floor(Math.random() * 1000);
this.data[newSectionId] = {
displayName: 'Custom'
}
}
},
computed: {
dataWatcher() {
return this.data;
},
updateData() {
return debounce(this.fetchAndUpdateData, 2000);
},
},
watch: {
dataWatcher: {
handler() {
console.log('Watcher triggered')
this.updateData();
},
deep: true,
},
},
Why is the watcher not triggered, when clearly the data is being changed?
Another thing I noticed that is quite strange, is that in Vue 3.0, the watcher IS triggered with exactly the same code.
Codesandbox in Vue 2.6.11, watcher not triggering.
Codesandbox in Vue 3.0, watcher IS triggered with exactly the same code.
In vue 2 there's reactivity issue when updating an item in an array or a nested field in an object, to solve this you've to use this.$set() method :
this.$set(this.data,newSectionId, {displayName: 'Custom'})
this issue is solved in Vue 3. and you could just do :
const newSectionId = Math.floor(Math.random() * 1000);
this.data[newSectionId] = {
displayName: 'Custom'
}
im creating a chart using chart.js. and i have 3 function here dataSurvey i use it to return my data from database and createBarChart to generate the chart and then handle to handle onclick chart, im want to display some info when user click the chart.
my variable and loaded function
dataSurveys:any
ionViewDidEnter() {
this.dataSurvey()
}
my dataSurvey code
dataSurvey() {
this.api.get('product/getsurvey/'+this.CekLogin.data.id)
.subscribe((result:any) => {
this.dataSurveys = result.data
if (this.dataSurveys) {
this.createBarChart()
}
})
}
my createBarChart code
createBarChart() {
this.bars = new Chart(this.barChart.nativeElement, {
type: 'pie',
data: {
labels: this.dataSurveys.prodName,
datasets: [{
data: this.dataSurveys.prodScore,
backgroundColor: this.dataSurveys.prodColor,
borderColor: 'rgb(38, 194, 129)',
borderWidth: 1
}]
},
options: {
onClick: this.handle
}
});
}
and my handle code
handle(point, event) {
let item = event[0]
console.log(item)
console.log(this.dataSurveys)
}
i try this command console.log(this.dataSurveys) on dataSurvey and createBarChart function, it return the data, but when i run it from handle function it return me 'undefined'.
how i can fix this issue?
Most probably this in the context of handle method is something else ... not your Ionic Page. You can console.log(this) and see what it actually is.
If you actually need to use the variables logic from your page you can use the JS method apply. It will basically substitute the context with the one you need.
options: {
onClick: this.handle.apply(this)
}
See more information here https://www.w3schools.com/js/js_function_apply.asp