poll evaluation as stacked bar chart
This commit is contained in:
parent
03de059fd1
commit
277d51e650
9 changed files with 396 additions and 74 deletions
76
app/components/poll-evaluation-chart.js
Normal file
76
app/components/poll-evaluation-chart.js
Normal 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
|
||||
});
|
|
@ -56,9 +56,32 @@ export default Ember.Controller.extend({
|
|||
/*
|
||||
* handles options if they are dates
|
||||
*/
|
||||
dates: function() {
|
||||
dates: Ember.computed('model.options.@each', 'useLocalTimezone', function() {
|
||||
let timezone = false;
|
||||
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
|
||||
// we return an empty array
|
||||
|
@ -76,20 +99,23 @@ export default Ember.Controller.extend({
|
|||
timezone = this.get('model.timezone');
|
||||
}
|
||||
|
||||
const container = this.get('container');
|
||||
dates = this.get('model.options').map((option) => {
|
||||
const date = moment(option.get('title'));
|
||||
const hasTime = moment(option.get('title'), 'YYYY-MM-DD', true).isValid() === false;
|
||||
if (timezone) {
|
||||
date.tz(timezone);
|
||||
}
|
||||
return {
|
||||
return dateObject.create({
|
||||
title: date,
|
||||
hasTime
|
||||
};
|
||||
hasTime,
|
||||
// inject container otherwise we could not inject i18n service
|
||||
container
|
||||
});
|
||||
});
|
||||
|
||||
return dates;
|
||||
}.property('model.options.@each', 'useLocalTimezone'),
|
||||
}),
|
||||
|
||||
pollUrl: function() {
|
||||
return window.location.href;
|
||||
|
|
8
app/templates/components/poll-evaluation-chart.hbs
Normal file
8
app/templates/components/poll-evaluation-chart.hbs
Normal file
|
@ -0,0 +1,8 @@
|
|||
{{ember-chart
|
||||
type=type
|
||||
data=data
|
||||
options=options
|
||||
width=width
|
||||
height=height
|
||||
legend=legend
|
||||
}}
|
|
@ -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> </th>
|
||||
{{#each dateGroups as |dateGroup|}}
|
||||
<th colspan="{{dateGroup.colspan}}">
|
||||
{{formattedDate dateGroup.value}}
|
||||
</th>
|
||||
{{/each}}
|
||||
<th> </th>
|
||||
</tr>
|
||||
{{/if}}
|
||||
<tr>
|
||||
<th> </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> </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> </td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{#if isEvaluable}}
|
||||
{{poll-evaluation-summary poll=model dates=dates sortedUsers=sortedUsers}}
|
||||
|
||||
<h3>Übersicht</h3>
|
||||
{{poll-evaluation-chart
|
||||
answerType=model.answerType
|
||||
dates=dates
|
||||
users=model.users
|
||||
}}
|
||||
{{/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> </th>
|
||||
{{#each dateGroups as |dateGroup|}}
|
||||
<th colspan="{{dateGroup.colspan}}">
|
||||
{{formattedDate dateGroup.value}}
|
||||
</th>
|
||||
{{/each}}
|
||||
<th> </th>
|
||||
</tr>
|
||||
{{/if}}
|
||||
<tr>
|
||||
<th> </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> </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> </td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
"pretender": "^0.6.0",
|
||||
"moment": ">= 2.8.0",
|
||||
"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": {
|
||||
"ember-data-model-fragments": "1.13.1"
|
||||
|
|
|
@ -63,6 +63,9 @@ module.exports = function(defaults) {
|
|||
|
||||
app.import('bower_components/modernizr/modernizr.js');
|
||||
|
||||
// ChartJS StackedBar addon
|
||||
app.import('bower_components/Chart.StackedBar.js/src/Chart.StackedBar.js');
|
||||
|
||||
// webshim
|
||||
app.import({
|
||||
development: 'bower_components/webshim/js-webshim/dev/polyfiller.js',
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"ember-cli-babel": "^5.1.5",
|
||||
"ember-cli-bootstrap-datepicker": "0.5.3",
|
||||
"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-dependency-checker": "^1.1.0",
|
||||
"ember-cli-htmlbars": "^1.0.1",
|
||||
|
|
75
tests/integration/components/poll-evaluation-chart-test.js
Normal file
75
tests/integration/components/poll-evaluation-chart-test.js
Normal 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');
|
||||
});
|
123
tests/unit/components/poll-evaluation-chart-test.js
Normal file
123
tests/unit/components/poll-evaluation-chart-test.js
Normal 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'
|
||||
);
|
||||
});
|
Loading…
Reference in a new issue