poll evaluation as stacked bar chart

This commit is contained in:
jelhan 2016-03-17 13:32:53 +01:00
parent 03de059fd1
commit 277d51e650
9 changed files with 396 additions and 74 deletions

View file

@ -0,0 +1,76 @@
import Ember from 'ember';
const addArrays = function() {
const args = Array.prototype.slice.call(arguments);
const basis = args.shift();
args.forEach(function(array) {
array.forEach(function(value, index) {
if (Ember.isPresent(value)) {
basis[index] = basis[index] + value;
}
});
});
return basis;
};
export default Ember.Component.extend({
i18n: Ember.inject.service(),
type: 'StackedBar',
data: Ember.computed('users.[]', 'dates.[]', 'dates.@each.formatted', 'i18n.locale', function() {
const labels = this.get('dates').map((date) => {
return Ember.get(date, 'formatted');
});
let datasets = [];
const participants = this.get('users.length');
if (this.get('answerType') === 'YesNoMaybe') {
const maybe = this.get('users').map((user) => {
return user.get('selections').map((selection) => {
return selection.get('type') === 'maybe' ? 1 : 0;
});
});
datasets.push({
label: this.get('i18n').t('answerTypes.maybe.label').toString(),
fillColor: 'rgba(220,220,220,0.5)',
strokeColor: 'rgba(220,220,220,0.8)',
highlightFill: 'rgba(220,220,220,0.75)',
highlightStroke: 'rgba(220,220,220,1)',
data: addArrays.apply(this, maybe).map((value) => Math.round(value / participants * 100))
});
}
const yes = this.get('users').map((user) => {
return user.get('selections').map((selection) => {
return selection.get('type') === 'yes' ? 1 : 0;
});
});
datasets.push({
label: this.get('i18n').t('answerTypes.yes.label').toString(),
fillColor: 'rgba(151,187,205,0.5)',
strokeColor: 'rgba(151,187,205,0.8)',
highlightFill: 'rgba(151,187,205,0.75)',
highlightStroke: 'rgba(151,187,205,1)',
data: addArrays.apply(this, yes).map((value) => Math.round(value / participants * 100))
});
return {
datasets,
labels
};
}),
options: {
// fixed scale to 0 to 100%
scaleOverride: true,
scaleSteps: 10,
scaleStepWidth: 10,
scaleStartValue: 0,
// prepand % to y axis values,
scaleLabel: '<%=value%> %',
// tooltips
tooltipTemplate: '<%=datasetLabel%>: <%= value + " %" %>',
multiTooltipTemplate: '<%=datasetLabel%>: <%= value + " %" %>'
},
width: '800',
height: '400',
legend: false
});

View file

@ -56,9 +56,32 @@ export default Ember.Controller.extend({
/* /*
* handles options if they are dates * handles options if they are dates
*/ */
dates: function() { dates: Ember.computed('model.options.@each', 'useLocalTimezone', function() {
let timezone = false; let timezone = false;
let dates = []; let dates = [];
const dateObject = Ember.Object.extend({
i18n: Ember.inject.service(),
init() {
// retrive locale to setup observers
this.get('i18n.locale');
},
formatted: Ember.computed('title', 'i18n.locale', function() {
const date = this.get('title');
// locale is stored on date, we have to override it if it has changed since creation
if (date.locale() !== this.get('i18n.locale')) {
date.locale(this.get('i18n.locale'));
}
return this.get('hasTime') ? date.format('LLLL') : date.format(
moment.localeData()
.longDateFormat('LLLL')
.replace(
moment.localeData().longDateFormat('LT'), '')
.trim()
);
})
});
// if poll type is find a date // if poll type is find a date
// we return an empty array // we return an empty array
@ -76,20 +99,23 @@ export default Ember.Controller.extend({
timezone = this.get('model.timezone'); timezone = this.get('model.timezone');
} }
const container = this.get('container');
dates = this.get('model.options').map((option) => { dates = this.get('model.options').map((option) => {
const date = moment(option.get('title')); const date = moment(option.get('title'));
const hasTime = moment(option.get('title'), 'YYYY-MM-DD', true).isValid() === false; const hasTime = moment(option.get('title'), 'YYYY-MM-DD', true).isValid() === false;
if (timezone) { if (timezone) {
date.tz(timezone); date.tz(timezone);
} }
return { return dateObject.create({
title: date, title: date,
hasTime hasTime,
}; // inject container otherwise we could not inject i18n service
container
});
}); });
return dates; return dates;
}.property('model.options.@each', 'useLocalTimezone'), }),
pollUrl: function() { pollUrl: function() {
return window.location.href; return window.location.href;

View file

@ -0,0 +1,8 @@
{{ember-chart
type=type
data=data
options=options
width=width
height=height
legend=legend
}}

View file

@ -1,71 +1,79 @@
<div class="table-scroll">
<table class="user-selections-table table table-striped table-condensed">
<thead>
{{#if model.isDateTime}}
<tr class="dateGroups">
<th>&nbsp;</th>
{{#each dateGroups as |dateGroup|}}
<th colspan="{{dateGroup.colspan}}">
{{formattedDate dateGroup.value}}
</th>
{{/each}}
<th>&nbsp;</th>
</tr>
{{/if}}
<tr>
<th>&nbsp;</th>
{{#if model.isFindADate}}
{{#each dates as |date|}}
<th>
{{#if dateGroups}}
{{#if date.hasTime}}
{{formattedDate date.title format="LT" times=true}}
{{/if}}
{{else}}
{{formattedDate date.title}}
{{/if}}
</th>
{{/each}}
{{else}}
{{#each model.options as |option|}}
<th>
{{option.title}}
</th>
{{/each}}
{{/if}}
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{{#each sortedUsers as |user|}}
<tr class="user">
<td>{{user.name}}</td>
{{#each user.selections as |selection|}}
<td>
{{#if selection.label}}
{{#if model.isFreeText}}
{{selection.label}}
{{/if}}
{{/if}}
{{#if selection.labelTranslation}}
{{#unless model.isFreeText}}
<span class="{{selection.type}}">
<span class="{{selection.icon}}"></span>
{{t selection.labelTranslation}}
</span>
{{/unless}}
{{/if}}
</td>
{{/each}}
<td>&nbsp;</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{#if isEvaluable}} {{#if isEvaluable}}
{{poll-evaluation-summary poll=model dates=dates sortedUsers=sortedUsers}} {{poll-evaluation-summary poll=model dates=dates sortedUsers=sortedUsers}}
<h3>Übersicht</h3>
{{poll-evaluation-chart
answerType=model.answerType
dates=dates
users=model.users
}}
{{/if}} {{/if}}
<h3>Teilnehmende und ihre Antworten</h3>
<div class="table-scroll">
<table class="user-selections-table table table-striped table-condensed">
<thead>
{{#if model.isDateTime}}
<tr class="dateGroups">
<th>&nbsp;</th>
{{#each dateGroups as |dateGroup|}}
<th colspan="{{dateGroup.colspan}}">
{{formattedDate dateGroup.value}}
</th>
{{/each}}
<th>&nbsp;</th>
</tr>
{{/if}}
<tr>
<th>&nbsp;</th>
{{#if model.isFindADate}}
{{#each dates as |date|}}
<th>
{{#if dateGroups}}
{{#if date.hasTime}}
{{formattedDate date.title format="LT" times=true}}
{{/if}}
{{else}}
{{formattedDate date.title}}
{{/if}}
</th>
{{/each}}
{{else}}
{{#each model.options as |option|}}
<th>
{{option.title}}
</th>
{{/each}}
{{/if}}
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{{#each sortedUsers as |user|}}
<tr class="user">
<td>{{user.name}}</td>
{{#each user.selections as |selection|}}
<td>
{{#if selection.label}}
{{#if model.isFreeText}}
{{selection.label}}
{{/if}}
{{/if}}
{{#if selection.labelTranslation}}
{{#unless model.isFreeText}}
<span class="{{selection.type}}">
<span class="{{selection.icon}}"></span>
{{t selection.labelTranslation}}
</span>
{{/unless}}
{{/if}}
</td>
{{/each}}
<td>&nbsp;</td>
</tr>
{{/each}}
</tbody>
</table>
</div>

View file

@ -23,7 +23,9 @@
"pretender": "^0.6.0", "pretender": "^0.6.0",
"moment": ">= 2.8.0", "moment": ">= 2.8.0",
"webshim": "~1.15.10", "webshim": "~1.15.10",
"bootstrap-switch": "^3.3.2" "bootstrap-switch": "^3.3.2",
"chartjs": "1.0.2",
"Chart.StackedBar.js": "jelhan/Chart.StackedBar.js#3a81baa3a191cd3ff14a8249bf86e388bc650640"
}, },
"devDependencies": { "devDependencies": {
"ember-data-model-fragments": "1.13.1" "ember-data-model-fragments": "1.13.1"

View file

@ -63,6 +63,9 @@ module.exports = function(defaults) {
app.import('bower_components/modernizr/modernizr.js'); app.import('bower_components/modernizr/modernizr.js');
// ChartJS StackedBar addon
app.import('bower_components/Chart.StackedBar.js/src/Chart.StackedBar.js');
// webshim // webshim
app.import({ app.import({
development: 'bower_components/webshim/js-webshim/dev/polyfiller.js', development: 'bower_components/webshim/js-webshim/dev/polyfiller.js',

View file

@ -32,6 +32,7 @@
"ember-cli-babel": "^5.1.5", "ember-cli-babel": "^5.1.5",
"ember-cli-bootstrap-datepicker": "0.5.3", "ember-cli-bootstrap-datepicker": "0.5.3",
"ember-cli-build-info": "0.2.0", "ember-cli-build-info": "0.2.0",
"ember-cli-chart": "jelhan/ember-cli-chart#87cc4f125ce69e22197c992206cca700edc70267",
"ember-cli-content-security-policy": "0.4.0", "ember-cli-content-security-policy": "0.4.0",
"ember-cli-dependency-checker": "^1.1.0", "ember-cli-dependency-checker": "^1.1.0",
"ember-cli-htmlbars": "^1.0.1", "ember-cli-htmlbars": "^1.0.1",

View file

@ -0,0 +1,75 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import Ember from 'ember';
import moment from 'moment';
moduleForComponent('poll-evaluation-chart', 'Integration | Component | poll evaluation chart', {
integration: true,
beforeEach() {
moment.locale('en');
}
});
test('it renders', function(assert) {
this.set('dates', [
Ember.Object.create({
formatted: 'Thursday, January 1, 2015',
title: moment('2015-01-01'),
hasTime: false
}),
Ember.Object.create({
formatted: 'Monday, February 2, 2015',
title: moment('2015-02-02'),
hasTime: false
}),
Ember.Object.create({
formatted: 'Tuesday, March 3, 2015 1:00 AM',
title: moment('2015-03-03T01:00'),
hasTime: true
}),
Ember.Object.create({
formatted: 'Tuesday, March 3, 2015 11:00 AM',
title: moment('2015-03-03T11:00'),
hasTime: true
})
]);
this.set('answerType', 'YesNoMaybe');
this.set('users', [
Ember.Object.create({
id: 1,
selections: [
Ember.Object.create({
type: 'yes'
}),
Ember.Object.create({
type: 'yes'
}),
Ember.Object.create({
type: 'maybe'
}),
Ember.Object.create({
type: 'no'
})
]
}),
Ember.Object.create({
id: 2,
selections: [
Ember.Object.create({
type: 'yes'
}),
Ember.Object.create({
type: 'maybe'
}),
Ember.Object.create({
type: 'no'
}),
Ember.Object.create({
type: 'no'
})
]
})
]);
this.render(hbs`{{poll-evaluation-chart dates=dates answerType=answerType users=users}}`);
assert.ok(this.$('canvas'), 'it renders a canvas element');
});

View file

@ -0,0 +1,123 @@
import { moduleForComponent, test } from 'ember-qunit';
import moment from 'moment';
import Ember from 'ember';
import tHelper from 'ember-i18n/helper';
import localeConfig from 'ember-i18n/config/en';
moduleForComponent('poll-evaluation-chart', 'Unit | Component | poll evaluation chart', {
unit: true,
// https://github.com/jamesarosen/ember-i18n/wiki/Doc:-Testing#unit-tests
needs: [
'service:i18n',
'locale:en/translations',
'util:i18n/missing-message',
'util:i18n/compile-template',
'config:environment'
],
beforeEach() {
moment.locale('en');
this.container.lookup('service:i18n').set('locale', 'en');
this.registry.register('locale:en/config', localeConfig);
this.registry.register('helper:t', tHelper);
}
});
test('data is a valid ChartJS dataset', function(assert) {
const dates = [
Ember.Object.create({
formatted: 'Thursday, January 1, 2015',
title: moment('2015-01-01'),
hasTime: false
}),
Ember.Object.create({
formatted: 'Monday, February 2, 2015',
title: moment('2015-02-02'),
hasTime: false
}),
Ember.Object.create({
formatted: 'Tuesday, March 3, 2015 1:00 AM',
title: moment('2015-03-03T01:00'),
hasTime: true
}),
Ember.Object.create({
formatted: 'Tuesday, March 3, 2015 11:00 AM',
title: moment('2015-03-03T11:00'),
hasTime: true
})
];
let component = this.subject({
answerType: 'YesNoMaybe',
dates,
users: [
Ember.Object.create({
id: 1,
selections: [
Ember.Object.create({
type: 'yes'
}),
Ember.Object.create({
type: 'yes'
}),
Ember.Object.create({
type: 'maybe'
}),
Ember.Object.create({
type: 'no'
})
]
}),
Ember.Object.create({
id: 2,
selections: [
Ember.Object.create({
type: 'yes'
}),
Ember.Object.create({
type: 'maybe'
}),
Ember.Object.create({
type: 'no'
}),
Ember.Object.create({
type: 'no'
})
]
})
]
});
const data = component.get('data');
assert.deepEqual(
data.labels,
dates.map((date) => {
return date.hasTime ? date.title.format('LLLL') : date.title.format(
moment.localeData()
.longDateFormat('LLLL')
.replace(
moment.localeData().longDateFormat('LT'), '')
.trim()
);
}),
'Labels are correct'
);
assert.equal(
data.datasets.length,
2,
'there are two datasets'
);
assert.deepEqual(
data.datasets.map((dataset) => dataset.label),
['Maybe', 'Yes'],
'datasets having answers as label and are in correct order'
);
assert.deepEqual(
data.datasets[0].data,
[0, 50, 50, 0],
'dataset for maybe is correct'
);
assert.deepEqual(
data.datasets[1].data,
[100, 50, 0, 0],
'dataset for yes is correct'
);
});