Skip to content

Instantly share code, notes, and snippets.

@Sphinxxxx
Last active March 14, 2024 16:02
Show Gist options
  • Save Sphinxxxx/d0f91aff183474404c0e82407b0f2c5f to your computer and use it in GitHub Desktop.
Save Sphinxxxx/d0f91aff183474404c0e82407b0f2c5f to your computer and use it in GitHub Desktop.
Vue bar chart race
<script>
console.clear();
//Super-duper-lightweight tweening "library":
//https://gist.github.com/Sphinxxxx/fc9bcc601a2f903ff167112855d0437c
function TWEEN(b,f,h,k,c){function g(a){if(d){var e=(a-d)/h;a=1>e?b+l*c(e):f;if(1<=e)return}else d=a,a=b;k(a);requestAnimationFrame(g)}c=c||function(a){return a*(2-a)};var l=f-b,d;requestAnimationFrame(g)};
</script>
<script src="//unpkg.com/vue@2"></script>
<div id="app">
<h1>Vue bar chart race</h1>
<!-- https://vuejs.org/v2/guide/transitions.html#List-Transitions -->
<transition-group tag="div" name="bar-race" class="chart">
<bar v-for="(item, index) in orderedItems" :key="item.id" :item="item" :max="maxVal"></bar>
</transition-group>
</div>
<script id="bar-template" type="text/x-template">
<div class="bar-wrapper">
<div class="bar" :id="item.id" :style="styleObj" :data-value="item.value">{{ item.name }}</div>
</div>
</script>
Vue.component('bar', {
template: '#bar-template',
props: ['item', 'max'],
data() {
return {
animWidth: this.getWidth(),
};
},
computed: {
//https://stackoverflow.com/questions/42737034/vue-js-watch-multiple-properties-with-single-handler
targetWidth() {
return this.getWidth();
},
styleObj() {
return {
background: this.item.color,
//It *almost* works to just use getWidth() here, and use standard CSS `transition` for "tweening",
//but sometimes a bar will just jump to its new with without any transition.
//Therefore we need to use a tweening library to animate the width (`animWidth`) the hard way..
//https://vuejs.org/v2/guide/transitioning-state.html#Animating-State-with-Watchers
//
// width: this.getWidth(),
width: this.animWidth + '%',
};
}
},
watch: {
//Won't react to `max` changes if there's a step where the value doesn't change..
// 'item.value': function(newVal, oldVal) {
targetWidth: function(newW, oldW) {
//Calculate and slowly animate the new width when we go to a new step:
TWEEN(oldW, newW, 1000,
val => this.animWidth = val);
}
},
methods: {
getWidth(value) {
const val = value || this.item.value,
w = this.max ? ((val / this.max) * 100) : 0;
return w;
},
}
});
(function() {
const items = [
{
id: 'aaa',
name: 'Aardvark',
value: 0,
color: 'tomato',
},
{
id: 'bbb',
name: 'Baboon',
value: 0,
color: 'deepskyblue',
},
{
id: 'ccc',
name: 'Chihuahua',
value: 0,
color: 'lime',
},
{
id: 'ddd',
name: 'Dodo',
value: 0,
color: 'gold',
},
];
new Vue({
el: '#app',
data() {
return {
items: items,
};
},
computed: {
orderedItems() {
console.log('Reordering...');
return this.items.slice().sort((a, b) => b.value - a.value);
},
maxVal() {
const max = Math.max.apply(Math, this.items.map(x => x.value));
//console.log('Max', max);
return max;
}
},
mounted() {
},
methods: {
}
});
const INTERVAL = 1500,
STEPS = 100;
function rnd(max) {
return Math.floor(Math.random() * (max + 1));
}
let stepCount = 0;
function nextStep() {
stepCount++;
items.forEach((x, i) => {
x.value += rnd(stepCount * 2);
});
if(stepCount < STEPS) {
setTimeout(nextStep, INTERVAL);
}
}
nextStep();
})();
body {
font-family: Georgia, sans-serif;
}
.chart {
display: flex;
flex-flow: column;
margin-top: 1em;
padding-right: 3em;
.bar-wrapper {
margin: .5em 0;
}
.bar {
position: relative;
padding: 1em 0;
white-space: nowrap;
text-indent: 1em;
//Needed if normal `transition` would work reliably on bar widths (see comment in styleObj()).
// transition: all 1s;
&::after {
content: attr(data-value);
position: absolute;
left: 100%;
}
}
}
//https://vuejs.org/v2/guide/transitions.html#List-Transitions
.bar-race {
//Smooth transition on add & shuffle:
&-move {
transition: transform 1s;
}
////Smooth transition on remove:
//&-leave-active {
// position: absolute;
// opacity: 0;
//}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment