Vue.component('time-header', { template: '#time-header', props: { elapsedtime: { type: Number, default: 0 } }, data: function () { return { tweenedTime: this.elapsedtime }; }, watch: { elapsedtime: function (newVal) { TweenLite.to(this.$data, 0.75, { tweenedTime: newVal, roundProps: "tweenedTime" }); } }, computed: { animatedTime: function () { let tweenedTime = this.tweenedTime; let tweenedMsg = null; if (tweenedTime == 1) { tweenedMsg = tweenedTime + ' ' + 'Month'; } else if (tweenedTime < 12) { tweenedMsg = tweenedTime + ' ' + 'Months'; } else { let year = parseInt(tweenedTime / 12); let month = tweenedTime % 12; let yearText = year > 1 ? year + ' Years' : year + ' Year'; let monthText = month == 0 ? '' : month == 1 ? month + ' Month' : month + ' Months'; let dateText = month == 0 ? yearText : yearText + ', ' + monthText; tweenedMsg = dateText; } return tweenedMsg; } } }); Vue.component('category-count', { template: '#category-count', props: { x: { type: Number, default: 0 }, fill: { type: String }, count: { type: Number, default: 0 } }, data: function () { return { tweenedCount: this.count, categoryColor: this.fill }; }, watch: { count: function (newVal) { TweenLite.to(this.$data, 0.75, { tweenedCount: newVal, roundProps: "tweenedCount" }); } }, methods: {}, computed: { animatedCount: function () { return this.tweenedCount; }, animatedColor: function () { return this.categoryColor; } } }); Vue.component('category-name', { template: '#category-name', props: { x: { type: Number, default: 0 }, fill: { type: String }, name: { type: String } }, data: function () { return { categoryName: this.name, categoryColor: this.fill }; }, computed: { animatedColor: function () { return this.categoryColor; } } }); Vue.component('svg-circle', { template: '#svg-circle', props: { r: { type: Number, default: 3 }, stroke: { type: String, default: 'black' }, strokewidth: { type: Number, default: 0.75 }, opacity: { type: String, default: '0.8' }, fillopacity: { type: String, default: '1' }, strokeopacity: { type: String, default: '1' }, fill: { type: String, default: 'steelblue' } }, data: function () { return { tweenRadius: this.r }; }, watch: { r: function (newVal) { TweenLite.to(this.$data, 0.75, { tweenRadius: newVal }); } }, methods: { circleClick: function (event) { console.log("This is the circleClick function to handle the click event.\nInfo on the event: ", event.target.value); } }, computed: { animatedRadius: function () { return this.tweenRadius; } } }); new Vue({ el: '#chart', data: function () { return { chart: { width: window.innerWidth, height: 360 }, margins: { top: 20, right: 20, bottom: 20, left: 20 }, colors: ['#66545e', '#aa6f73', '#eea990', '#000000'], simulation: null, radius: 5, padding: 0.5, // Space between nodes cluster_padding: 5, groups: {}, people: {}, nodes: [], time_so_far: 0 }; }, mounted: function () { this.calculateGroups(); fetch('./stages.json', { headers: { "Method": "GET", "Accept": "applicaiton/json" } }) .then(response => response.json() ) .then(results => { this.parseData(results); this.computeNodes(); this.simulation = d3.forceSimulation(this.nodes) .force("x", d => d3.forceX(d.x)) .force("y", d => d3.forceY(d.y)) .force("cluster", this.forceCluster()) .force("collide", this.forceCollide()) .alpha(0.35) .alphaDecay(0) ; }); setTimeout(() => { this.movePosition(); setInterval(() => { this.movePosition(); }, 2000); }, 5000); }, methods: { parseData: function (data) { let that = this; data.forEach(d => { if (Object.keys(that.people).indexOf(d.pid + "") > -1) { that.people[d.pid + ""].push(d); } else { that.people[d.pid + ""] = [d]; } }); }, calculateGroups: function () { this.groups = { "met": { x: this.chart.width / 5, y: this.chart.height / 2, color: this.colors[0], cnt: 0, fullname: "Met" }, "romantic": { x: 2 * this.chart.width / 5, y: this.chart.height / 2, color: this.colors[1], cnt: 0, fullname: "Romantic" }, "lived": { x: 3 * this.chart.width / 5, y: this.chart.height / 2, color: this.colors[2], cnt: 0, fullname: "Lived Together" }, "married": { x: 4 * this.chart.width / 5, y: this.chart.height / 2, color: this.colors[3], cnt: 0, fullname: "Married" } }; }, computeNodes: function () { let that = this; this.nodes = Object.keys(this.people).map(function (d) { // Initialize count for each group. that.groups[that.people[d][0].grp].cnt += 1; return { id: "node" + d, x: that.groups[that.people[d][0].grp].x + Math.random(), y: that.groups[that.people[d][0].grp].y + Math.random(), r: that.radius, color: that.groups[that.people[d][0].grp].color, group: that.people[d][0].grp, timeleft: that.people[d][0].duration, istage: 0, stages: that.people[d] }; }); }, forceCluster: function () { const strength = 0.11; let nodes; let that = this; function force(alpha) { const l = alpha * strength; for (const d of nodes) { d.vx -= (d.x - that.groups[d.group].x) * l; d.vy -= (d.y - that.groups[d.group].y) * l; } } force.initialize = _ => nodes = _; return force; }, forceCollide: function () { const alpha = 0.2; const padding1 = this.padding; const padding2 = this.cluster_padding; let nodes; let maxRadius; function force() { const quadtree = d3.quadtree(nodes, d => d.x, d => d.y); for (const d of nodes) { const r = d.r + maxRadius; const nx1 = d.x - r, ny1 = d.y - r; const nx2 = d.x + r, ny2 = d.y + r; quadtree.visit((q, x1, y1, x2, y2) => { if (!q.length) do { if (q.data !== d) { const r = d.r + q.data.r + (d.group === q.data.group ? padding1 : padding2); let x = d.x - q.data.x, y = d.y - q.data.y, l = Math.hypot(x, y); if (l < r) { l = (l - r) / l * alpha; d.x -= x *= l, d.y -= y *= l; q.data.x += x, q.data.y += y; } } } while (q = q.next); return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1; }); } } force.initialize = _ => maxRadius = Math.max(nodes = _, d => d.r) + Math.max(padding1, padding2); return force; }, enter: function (el, i) { const circle = el.children[0]; const currRadius = circle.getAttribute('r'); const currId = circle.getAttribute('id'); let currIndex = Number(currId.slice(currId.indexOf("_") + 1, currId.length)); TweenLite.fromTo( circle, 0.5, { attr: { r: 0 } }, { attr: { r: currRadius }, delay: 0.005 * currIndex } ); }, movePosition: function () { let that = this; this.nodes.forEach((node) => { node.timeleft -= 1; if (node.timeleft == 0 && node.istage < node.stages.length - 1) { // Decrease count for previous group. that.groups[node.group].cnt -= 1; // Update current node to new group. node.istage += 1; node.group = node.stages[node.istage].grp; node.timeleft = node.stages[node.istage].duration; // Increment count for new group. that.groups[node.group].cnt += 1; } }); this.time_so_far += 1; } } });