Plotly boxplot not relayouting properly when route changes - javascript

I'm using Plotly in Vue, and the plots are not properly rendering when I visit a new route. Only after hovering will the plot change to its proper size. When I change to another route with a different grid (for the same plot UUID), the plot seems to have some kind of wrong size, and it's overflowing the container. But if I hover on it, then it seems fine.
Here is my code that I use for the plotting and relayout:
mounted() {
Plotly.plot(
this.$refs[this.chartCopy.uuid],
this.chartCopy.traces,
this.chartCopy.layout
);
this.$refs[this.chartCopy.uuid].on("plotly_hover", this.hover);
this.$refs[this.chartCopy.uuid].on("plotly_unhover", this.unhover);
},
watch: {
chart: {
handler: function () {
Plotly.react(
this.$refs[this.chartCopy.uuid],
this.chartCopy.traces,
this.chartCopy.layout
);
},
deep: true,
},
},
In the options and the layout, I set autosize and responsive to true
Is there a way to make the plot already the proper size (both height and width) without the need to hover over it?
Here is the reproduced example where this behavior can be clearly seen:
https://codesandbox.io/s/vuetify-with-plotly-20nev
Any kind of relayout sort of destroys the set layout of the plot, and only after hovering, it becomes correct.
One more thing is that I can't use Vue wrapper for Plotly (it has other issues), so I have to stick with using plotly.js library only.

Responsive charts
To make the charts responsive to the window size, set the responsive property of the config option, which is the fourth argument to Plotly.plot():
// Boxplot.vue
export default {
mounted() {
Plotly.plot(
this.$refs[this.chartCopy.uuid],
this.chartCopy.traces,
this.chartCopy.layout,
{ responsive: true } 👈
)
}
}
Relayout
To perform relayout of the chart, use an IntersectionObsesrver that calls Plotly.relayout(). This should be setup in the mounted hook and torn down in beforeDestroy hook:
// Boxplot.vue
export default {
mounted() {
const chart = this.$refs[this.chartCopy.uuid]
const intersectionObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
requestIdleCallback(() => Plotly.relayout(entry.target, {}))
}
})
})
intersectionObserver.observe(chart)
this._unobserveChart = () => intersectionObserver.unobserve(chart)
},
beforeDestroy() {
this._unobserveChart()
},
}

Is there a reason you use tabs to render the routes? The layout is actually not valid. What actually happens in User.vue is that you render views multiple times. This causes the artifacts:
// you render same contents in all tabs many times
<v-tab-item v-for="tab of tabs" :key="tab.id" :value="tab.route">
<router-view></router-view>
</v-tab-item>
Just use the <router-link> instead of tabs:
// User.vue
<template>
<div>
<router-link v-for="tab of tabs" :to="tab.route" :key="tab.id">
{{ tab.name }}
</router-link>
<div>
<router-view></router-view>
</div>
</div>
</template>
And like #tony19 mentioned, make charts responsive:
Plotly.plot(
this.$refs[this.chartCopy.uuid],
this.chartCopy.traces,
this.chartCopy.layout,
{ responsive: true }
);
That's it! Check the sandbox

Related

How to make component appear/disappear animation in React?

There is a block of content:
<div className="col-7">
{content}
</div>
A component is placed in "content" depending on the state:
switch (activeItem) {
case "home": {
content = <AppHome/>
break;
}
case "about-me": {
content = <AppAboutMe/>
break;
}
default:
content = null
}
How to add content change animation so that one disappears smoothly, the other appears?
I tried to add animation through CSS class. It worked, but the disappearance was interrupted by the appearance. I tried through CSS transition but it appeared only once and did not disappear. When the content was subsequently changed, the animations no longer worked.
Here's a little CodeSandbox project I made to refer as an example:
To handle animation among several components, I recommend having all of your available components in a single constant. For example:
const components = {
title: {
key: '1',
content: <div>New York</div>,
},
body: {
key: '2',
content: (
<div>
<ul>
<li>cheese</li>
<li>tomato</li>
<li>onions</li>
</ul>
</div>
),
},
};
And then you can initialise your state with the wanted ones.
const [fields, setFields] = useState([]); // [] || [components.title]
Then, depending on your logic you can either add components on top of existing ones or remove by setting your state.
const onEnter = () => {
setFields([...fields, components.title]);
};
const onExit = () => {
setFields([]);
};
Lastly, in order to animate state changes, you can wrap your components with <motion.div>{component}<motion.div/>, setting variants will take care of the details of your animation process.

How do you optimize high total blocking time vue

I have encountered problem that I am not capable of solving. I have high total blocking time on my page (2+ sec). I have tried loading every vue component asynchronously, but it does not matter, still 2+ sec tbt. I don't really understand what can cause such high tbt and what can I do about it, as it is just a simple page without much underlying logic (https://i.stack.imgur.com/o7LSk.png) (Just 21 simple cards).
I have removed everything I can, compressed code, and left only the most nessesary stuff. Still it does not solve the issue. Is there any way to make it go down to 100-200ms? What can cause such a problem in your experience?
I have high amount of components though (cards, buttons, lazy-load picture, rating), so in the end there will be around 100-300 components on page. But I don't see there any possibilities of removing it, as this will break neat structure
The only way I found to fix high total blocking time in this case, is to use intersection observer and load it only when it enters the screen. I think same job can do some virtual scroller plugin.
<template>
<div class="grid">
<div class="grid__item" v-for="item in items" :data-id="item.id">
<Item v-if="itemsInScreen[item.id]" :item="item" />
</div>
</div>
</template>
<script>
export default {
name: 'Grid',
props: {
items: {required: true}
},
data() {
return {
itemsInScreen: {}
}
},
methods: {
initObserver() {
const callback = entries => {
entries.forEach(entry => {
if(entry.isIntersecting) this.$set(this.itemsInScreen, entry.target.dataset.id, true);
});
};
const options = {
rootMargin: "20px 20px 20px 20px"
};
const observer = new IntersectionObserver(callback, options);
const itemEls = this.$el.querySelectorAll('.grid__item');
itemEls.forEach(itemEl => observer.observe(itemEl));
}
},
mounted() {
this.initObserver();
}
}
</script>

Changing a button to say "Loading" with a loading animation on click

I have created a Vue button that displays "Load More" and then "Loading..." when clicked and loading more content. But, I would now like to add another component being a loading animation next to the "Loading." The button works completely fine, but I just would like to add that animation alongside the word "loading."
I have tried using Vue's ref tag, but have not had much luck in successfully using that in my method.
Loader.vue:
<template>
<div
ref="sdcVueLoader"
class="sdc-vue-Loader"
>
Loading...
</div>
</template>
<script>
export default {
name: 'Loader'
</script>
App.vue:
<Button
:disabled="clicked"
#click="loadMore"
>
{{ loadMoreText }}
</Button>
<script>
import Button from './components/Button'
import Loader from './components/Loader'
export default {
name: 'ParentApp',
components: {
Button,
Loader
},
data () {
return {
loadMoreText: 'Load More',
clicked: false
}
},
methods: {
loadMore () {
if ... {
this.page += 1
this.loadMoreText = 'Loading...' + this.$refs.sdcVueLoader
this.clicked = true
this.somerequest().then(resp => {
this.clicked = false
this.loadMoreText = 'Load More'
})
return this.loadMoreText
}
}
</script>
I am hoping for the button to continue working as it is now, but now to also have the "Loader" component displaying next to "Loading..." when the button is clicked in the app.vue loadMore method.
If you want to do anything with any form of complexity in html, it is best to move it over to your template. In your case, you have two states: It is either loading, or it is not loading. So lets create a variable loading that is either true or false.
data () {
return {
loading: false,
page: 1,
}
},
methods: {
async loadMore () {
if (this.loading) {
return;
}
this.page += 1;
this.loading = true;
const response = await this.somerequest();
this.loading = false;
// Oddly enough, we do nothing with the response
}
}
Now, in the template use a v-if with a v-else:
<button
:disabled="loading"
#click="loadMore"
>
<template v-if="loading">
<icon name="loader" />
Loading...
</template>
<template v-else>
Load more
</template>
</button>
If you want to move the logic to a different component, you have two options:
Add loading as a prop to that different component, and move the template code to that component
Use a slot and pass the html directly into your loading button. This is especially useful if you have several different configurations, and don't want to deal with increasingly complex configuration options just to accommodate them all.

Dynamic / Async Component Render

I am quite new to VueJS and have been playing around with the framework for a couple of days.
I am building a sort of dashboard with a widget based look and feel and the problem I have is that when the user adds a lot of widgets to the dashboard, problems arise on the loading of the page since the widgets make simultaneous calls to the API's to retrieve subsets of data.
To give you a better understanding of what I am doing, the concept is the below. (This is a brief idea to keep the code clean and simple).
Home.vue
<template>
<div class="Home">
<h1>Homepage</h1>
<div v-for="w in widgets">
<component :is="widget"></component>
</div>
</div>
</template>
<script>
export default {
name: 'Home',
mounted() {
for (var i = 0; i < availableWidgets; i++) {
widgets.push(availableWidgets);
}
},
};
</script>
Widget 1
<template>
<div class="Widget1">
<span>Widget 1</span>
</div>
</template>
<script>
export default {
name: 'Widget1',
mounted() {
//Get data from API and render
},
};
</script>
Widget 2
<template>
<div class="Widget2">
<span>Widget 2</span>
</div>
</template>
<script>
export default {
name: 'Widget2',
mounted() {
//Get data from API and render
},
};
</script>
As you can see, I am sort of loading the widgets and adding them dynamically depending on what the user has in his dashboard.
The problem I have is that Widget 1 and Widget 2 (in my case there are like 20-30 widgets), will be making API calls and this works fine when 1 or 2 widgets are loaded. But once the page grows a lot and there will be like 10 widgets on the page, everything starts lagging.
What would you suggest to do to make this more performant? Is it possible to allow once component to load at a time before loading the second component and so on? I was thinking of adding async calls, but that would not stop the components from being loaded at the same time?
Looking forward to your feedback and help that you could provide.
A common pattern would be to have the first render be without data, then re-render whenever your data comes in. The browser will make sure that not too many network requests run at the same time, so you should not have lag perse from that. You just perceive lag, because your component does not render until the data loads.
I would suggest using something like Axios, which uses promises and makes it easy to create asynchronous http requests while still keeping your code readable.
<template>
<div class="widget graph">
<div v-if="loading">
<span>Loading...</span>
<img src="assets/loader.svg">
</div>
<div v-else>
<!-- Do whatever you need to do whenever data loads in -->
</div>
</div>
</template>
<script>
export default {
name: 'WidgetGraph',
data () {
return {
loading: true,
error: null,
graphData: {}
}
},
created () {
this.loadData();
},
methods: {
loadData () {
return axios.get(...).then((data) => {
this.loading = false;
}).catch(() => {
this.error = 'Something went wrong.... Panic!';
this.loading = false;
});
}
}
}
</script>
<style>
</style>

Vue component with height of 100% changes height on v-if re-render

I have a Vue component, which has the class "tab-body-wrapper" whose purpose is to display the "slot" for whichever tab is currently active.
After troubleshooting, it appears that this ".tab-body-wrapper" component, which I have set a height of 100% in my CSS, reduces in height during the v-if re-render even though the height of the element that contains it (".sub-menu-tabs-body") remains exactly the same at the same point in time.
My Vue component (with class of "tab-body-wrapper")
Vue.component("tab", {
template: `
<div v-if="isActive"
class="tab-body-wrapper">
<slot></slot>
</div>
`,
props: {
name: { required: true },
icon: { required: true },
selected: { default: false }
},
data() {
return {
isActive: false
}
},
mounted() {
this.isActive = this.selected;
}
});
The red arrow below shows where this Vue component resides within the DOM
The relevant CSS
.sub-menu-tabs-wrapper,
.sub-menu-tabs-body,
.tab-body-wrapper {
height: 100%;
}
The way I troubleshot my problem was by placing the following code in the mounted() hook of my other Vue component, which is contained in the slot of the above Vue component.
Below is the troubleshooting code in the mounted hook of my other Vue component, which resides in the slot. Note that I also used getBoundingClientRect and the results were the same so the issue is not with d3.
console.log(d3.select(".sub-menu-tabs-wrapper").style("height"));
console.log(d3.select(".sub-menu-tabs-body").style("height"));
console.log(d3.select(".tab-body-wrapper").style("height"));
On initial render, the heights were as expected:
scripts.js:791 1207px // .sub-menu-tabs-wrapper
scripts.js:792 1153px // .sub-menu-tabs-body
scripts.js:793 1151px // .tab-body-wrapper
However, upon clicking away and then clicking back onto the tab, you can see that the containing element (".sub-menu-tabs-body") retains its height of 1153px but ".tab-body-wrapper" has shrunk to 574.5px.
scripts.js:791 1207px // .sub-menu-tabs-wrapper
scripts.js:792 1153px // .sub-menu-tabs-body
scripts.js:793 574.5px // .tab-body-wrapper
What's stranger is that when I console.log the height of ".tab-body-wrapper" afterwards, I get 1151px.
For some context, the reason why I need the height of ".tab-body-wrapper" to be correct during the render is because I have a bunch of d3 charts that use the height and width of elements within ".tab-body-wrapper" during the render to determine their respective height and width. As such, the issue I am having is causing these charts to render with the incorrect height and width.
Note that using v-show for my tab component is not an option for reasons I will not go into here.
Try wrapping the calculation in a this.$nextTick() callback. As stated by the docs it will:
Defer the callback to be executed after the next DOM update cycle
so the element is correctly rendered when executing it.
new Vue({
el: '#app',
data: {
pRect: 0
},
mounted() {
this.$nextTick(() => {
this.pRect = document.querySelector('p').getBoundingClientRect();
})
}
})
<script src="https://unpkg.com/vue#2.5.13/dist/vue.js"></script>
<body>
<div id="app">
<p>My BoundingClientRect is: {{ pRect }}<p>
</div>
</body>

Categories

Resources