Skip to content

Commit

Permalink
✨ Added timeline visualisation (WIP)
Browse files Browse the repository at this point in the history
This commit add lots of new dependencies and components. Other misc changes
include moving some component-specific styles to their respective .vue files.
  • Loading branch information
saul committed Feb 12, 2016
1 parent 87f7a8c commit a648dc5
Show file tree
Hide file tree
Showing 15 changed files with 675 additions and 50 deletions.
6 changes: 5 additions & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
],
"dependencies": {
"bootstrap": "~3.3.6",
"selectize": "~0.12.1"
"selectize": "~0.12.1",
"vis": "^4.14.0",
"color": "^2.5.0",
"components-font-awesome": "^4.5.0",
"fontawesome-iconpicker": "^1.2.0"
}
}
26 changes: 18 additions & 8 deletions components/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@

<ul class="nav nav-tabs" role="tablist">
<li><a href="#heatmap" data-toggle="tab">Heatmap</a></li>
<li class="active"><a href="#timeline" data-toggle="tab">Timeline</a></li>
</ul>

<div class="tab-content">
<div role="tabpanel" class="tab-pane" id="heatmap">
<gv-heatmap-visualisation></gv-heatmap-visualisation>
</div>

<div role="tabpanel" class="tab-pane active" id="timeline">
<gv-timeline-visualisation></gv-timeline-visualisation>
</div>
</div>
</div>
</template>
Expand All @@ -42,19 +47,24 @@
},
dismissAlert(index) {
this.alerts.splice(index, 1);
},
render() {
window.requestAnimationFrame(this.render.bind(this));
this.$broadcast('render');
},
}
},
events: {
error(err) {
this.onError(err);
}
},
ready() {
this.render();
}
}
</script>

<style lang="less" rel="stylesheet/less">
@import "../less/variables.less";
.alerts {
position: fixed;
bottom: 0;
left: 1em;
right: 1em;
z-index: @zindex-navbar-fixed;
}
</style>
19 changes: 4 additions & 15 deletions components/HeatmapVisualisation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@
this.switchQueryTab(i - 1);
},
closeQuery(index) {
console.log('closeQuery', index);
// find index of the selected tab
let selectedIndex = this.queries.findIndex(q => q.selected);
Expand All @@ -132,13 +130,12 @@
}
},
switchQueryTab(index) {
console.log('switchQueryTab', index);
this.queries.forEach((q, i) => {
q.selected = i == index
});
},
tick() {
window.requestAnimationFrame(this.tick.bind(this));
render() {
window.requestAnimationFrame(this.render.bind(this));
this.queries.filter(q => q.heatmap)
.forEach(q => {
Expand All @@ -162,21 +159,13 @@
$(this.$els.canvasBackdrop)
.css('background-image', `url(overviews/${session.game}/${session.level}.png)`)
.css('background-color', 'black');
},
render() {
this.queries.filter(q => q.heatmap)
.forEach(q => {
q.heatmap.update();
q.heatmap.display();
});
// allow children to listen to this event
return true;
}
},
ready() {
this.addQuery();
fs.readdir(GRADIENT_BASE, this.updateGradients.bind(this));
this.render();
}
}
</script>
33 changes: 33 additions & 0 deletions components/Iconpicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<template>
<div class="input-group">
<span class="input-group-addon">
<i :class="['fa', value]"></i>
</span>
<input class="form-control" v-el:input v-model="value">
</div>
</template>

<script type="text/babel">
require('dist/components/fontawesome-iconpicker/dist/js/fontawesome-iconpicker');
export default {
props: {
value: {
twoWay: true
}
},
data() {
return {
$icon: null
}
},
ready() {
this.$icon = $(this.$els.input).iconpicker();
// form value has changed but event may not have been triggered by the iconpicker plugin
this.$icon.on('iconpickerUpdated', () => {
this.value = this.$els.input.value;
});
}
}
</script>
6 changes: 3 additions & 3 deletions components/QueryForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
<fieldset :disabled="selectedEvent == null">
<label>{{filters.length | pluralize 'Filter'}} ({{filters.length}})</label>

<div class="session-form__filter form-group" v-for="filter in filters" track-by="id">
<select class="form-control" v-model="filter.target">
<div class="form-group-flex form-group" v-for="filter in filters" track-by="id">
<select class="form-control width--auto" v-model="filter.target">
<option value="_event">Event</option>
<option v-for="entity in selectedEvent.entities" :value="entity" :selected="$index == 0">
{{entity | capitalize}}
Expand All @@ -62,7 +62,7 @@

<input type="text" class="form-control" v-model="filter.prop">

<select class="form-control" v-model="filter.comparator">
<select class="form-control width--auto" v-model="filter.comparator">
<option v-for="comparator in comparators" :value="comparator" :selected="$index == 0">
{{{ comparator.text }}}
</option>
Expand Down
139 changes: 129 additions & 10 deletions components/SessionSelect.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,106 @@
<template>
<select class="form-control" v-model="selected" :multiple="multiple" :disabled="sessions.length == 0">
<option v-for="session in sessions" :value="session">
{{session.title}}
</option>
</select>
<fieldset :disabled="all.length == 0">
<div v-for="session in selected" track-by="id">
<div class="form-group form-group-flex">
<select class="form-control" v-model="session.record" :disabled="all.length == 0" @change="sessionChange(session)">
<option v-for="record in all" :value="record">
{{record.title}}
</option>
</select>

<button type="button" class="btn btn-danger" @click="remove($index)">
<span class="glyphicon glyphicon-minus-sign"></span>
</button>
</div>

<div class="gutter--left gutter--bottom">
<div class="form-group form-group-flex">
<label>Colour</label>
<input type="color" v-model="session.colour" class="form-control input-sm">
</div>
</div>
</div>

<div class="form-group clearfix">
<button type="button" class="btn btn-default pull-right" @click="add">
<span class="glyphicon glyphicon-plus-sign"></span>
</button>
</div>
</fieldset>
</template>

<script type="text/babel">
const color = window.require('./dist/components/color/one-color-all-debug');
const _ = window.require('lodash');
const models = window.models;
const db = window.db;
const GOLDEN_RATIO_CONJUGATE = 0.618033988749895;
const HUE_SEED = Math.random();
let sessionUid = 0;
export default {
props: ['gameLevel', 'selected', 'multiple'],
props: ['gameLevel', 'selected', 'events'],
data() {
return {
sessions: []
all: []
}
},
methods: {
add() {
let hue = Math.random();
if (this.selected.length > 0) {
// add the golden ratio conjugate to the last hue
hue = color(this.selected[this.selected.length - 1].colour)
.hue(GOLDEN_RATIO_CONJUGATE, true)
.hue();
}
let c = new color.HSV(hue, 0.5, 0.95);
let session = {
id: sessionUid++,
record: this.all[0],
minTick: 0,
maxTick: 0,
colour: c.hex(),
tickRange: [0, 0]
};
this.selected.push(session);
// trigger a 'change' so we grab the tick range data
this.sessionChange(session);
},
remove(index) {
this.selected.splice(index, 1);
},
sessionChange(session) {
session.minTicks = 0;
session.maxTicks = 0;
models.Event.find({
attributes: [
[db.fn('min', db.col('tick')), 'minTick'],
[db.fn('max', db.col('tick')), 'maxTick']
],
where: {
session_id: session.record.id,
}
})
.then(result => {
session.minTick = result.get('minTick');
session.maxTick = result.get('maxTick');
session.tickRange = [session.minTick, session.maxTick];
console.log(`Session ${session.record.title}: ${session.minTick} - ${session.maxTick}`);
})
.catch(err => this.$dispatch('error', err));
},
refresh() {
this.sessions = [];
this.all = [];
models.Session.findAll({
where: {
Expand All @@ -29,13 +111,50 @@
order: [['id', 'DESC']],
})
.then(sessions => {
this.sessions = sessions.map(x => _.toPlainObject(x.get({plain: true})));
this.all = sessions.map(x => _.toPlainObject(x.get({plain: true})));
})
.catch(err => this.$dispatch('error', err));
},
refreshEvents() {
const sessionIds = [].concat(this.selected)
.filter(s => s.record)
.map(s => s.record.id);
this.events = [];
if (!sessionIds.length) {
return;
}
console.time('session events');
db.query(`SELECT DISTINCT ON (name) name, locations, entities
FROM events
WHERE events.session_id IN (:sessionIds)
ORDER BY name`, {
type: db.QueryTypes.SELECT,
replacements: {sessionIds: sessionIds}
})
.then(results => {
this.events = results.map(row => {
return {
name: row.name,
locations: _.keys(row.locations),
entities: _.keys(row.entities)
}
});
console.timeEnd('session events');
})
.catch(err => this.$dispatch('error', err));
}
},
ready() {
this.$watch('gameLevel', () => this.refresh());
this.selected = [];
this.$watch('gameLevel', this.refresh.bind(this));
this.$watch('all', this.refreshEvents.bind(this));
this.$watch('selected', this.refreshEvents.bind(this), {deep: true});
}
}
</script>
55 changes: 55 additions & 0 deletions components/Timeline.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<template>
<div class="timeline" v-el:timeline></div>
</template>

<script type="text/babel">
const vis = window.require('./dist/components/vis/dist/vis');
export default {
props: ['items', 'groups'],
data() {
return {
timeline: null
}
},
ready() {
this.$watch('items', () => this.timeline.setItems(this.items));
this.$watch('groups', () => this.timeline.setGroups(this.groups));
var options = {
format: {
minorLabels: {
millisecond:'SSS[ms]',
second: 'HH:mm:ss',
minute: 'HH:mm',
hour: 'HH:mm',
},
majorLabels: {
millisecond:'HH:mm:ss',
second: 'HH:mm',
minute: 'HH [hours]',
hour: '',
weekday: '',
day: '',
month: '',
year: ''
}
},
min: 0, // msec
max: 1000 * 60 * 60 * 2, // msec
maxHeight: 300
};
this.timeline = new vis.Timeline(this.$els.timeline, this.items, options);
this.timeline.setGroups(this.groups);
window.timeline = this.timeline;
}
}
</script>

<style lang="less" rel="stylesheet/less">
.timeline {
width: 1024px;
}
</style>
Loading

0 comments on commit a648dc5

Please sign in to comment.