Adds a chart for assignment-poll-detail

This commit is contained in:
GabrielMeyer 2020-01-29 17:15:16 +01:00 committed by FinnStutzenstein
parent c46369c6a7
commit a0c3a28456
7 changed files with 147 additions and 60 deletions

View File

@ -1,4 +1,4 @@
<div class="charts-wrapper">
<div class="charts-wrapper" [ngClass]="[classes, hasPadding ? 'has-padding' : '']">
<ng-container *ngIf="chartData.length || circleData.length">
<canvas
*ngIf="type === 'bar' || type === 'stackedBar' || type === 'horizontalBar' || type === 'line'"

View File

@ -1,4 +1,15 @@
.charts-wrapper {
position: relative;
display: block;
margin: auto;
&.has-padding {
padding: 16px;
}
}
@for $i from 1 through 100 {
.os-charts--#{$i} {
width: unquote($string: $i + '%');
}
}

View File

@ -123,6 +123,32 @@ export class ChartsComponent extends BaseViewComponent {
this.chartOptions.legend.position = position;
}
/**
* Determine, if the chart has some padding at the borders.
*/
@Input()
public hasPadding = true;
/**
* Optional passing a number as percentage value for `max-width`.
* Range from 1 to 100.
* Defaults to `100`.
*/
@Input()
public set size(size: number) {
if (size > 100) {
size = 100;
}
if (size < 1) {
size = 1;
}
this._size = size;
}
public get size(): number {
return this._size;
}
/**
* Fires an event, when the user clicks on the chart.
*/
@ -135,6 +161,13 @@ export class ChartsComponent extends BaseViewComponent {
@Output()
public hover = new EventEmitter<ChartEvent>();
/**
* Returns a string to append to the `chart-wrapper's` classes.
*/
public get classes(): string {
return 'os-charts os-charts--' + this.size;
}
/**
* The general data for the chart.
* This is only needed for `type == 'bar' || 'line'`
@ -191,6 +224,11 @@ export class ChartsComponent extends BaseViewComponent {
private _chartLegendSize: ChartLegendSize = 'middle';
/**
* Holds the value for `max-width`.
*/
private _size = 100;
/**
* Constructor.
*

View File

@ -47,12 +47,13 @@
<div *ngIf="poll.stateHasVotes">
<h2 translate>Result</h2>
<div class="chart-wrapper"></div>
<div class="chart-wrapper" [ngClass]="{ flex: isVotedPoll }">
<mat-table [dataSource]="poll.tableData">
<ng-container matColumnDef="user" sticky>
<mat-header-cell *matHeaderCellDef>{{ 'Candidates' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.user }}</mat-cell>
</ng-container>
<div *ngIf="!isVotedPoll">
<ng-container matColumnDef="yes">
<mat-header-cell *matHeaderCellDef>{{ 'Yes' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.yes }}</mat-cell>
@ -67,38 +68,46 @@
<mat-header-cell *matHeaderCellDef>{{ 'Abstain' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.abstain }}</mat-cell>
</ng-container>
</div>
<ng-container matColumnDef="quorum">
<mat-header-cell *matHeaderCellDef>{{ 'Quorum' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row"></mat-cell>
<div *ngIf="isVotedPoll">
<ng-container matColumnDef="votes">
<mat-header-cell *matHeaderCellDef>{{ 'Votes' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.yes }}</mat-cell>
</ng-container>
</div>
<mat-header-row *matHeaderRowDef="columnDefinitionOverview"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnDefinitionOverview"></mat-row>
</mat-table>
<div class="chart-inner-wrapper">
<os-charts
*ngIf="chartDataSubject.value"
[type]="chartType"
[labels]="candidatesLabels"
[size]="isVotedPoll ? 70 : 100"
[legendPosition]="isVotedPoll ? 'right' : 'top'"
[showLegend]="true"
[data]="chartDataSubject"
></os-charts>
</div>
</div>
<ng-container *ngIf="poll.type === 'named' && votesDataSource.data">
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter"/>
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
<mat-table [dataSource]="votesDataSource">
<ng-container matColumnDef="users" sticky>
<mat-header-cell *matHeaderCellDef>{{ "User" | translate }}</mat-header-cell>
<mat-header-cell *matHeaderCellDef>{{ 'User' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">
<div *ngIf="row.user">{{ row.user.getFullName() }}</div>
<div *ngIf="!row.user">{{ "Unknown user" | translate }}</div>
<div *ngIf="!row.user">{{ 'Unknown user' | translate }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'votes-' + option.user_id" *ngFor="let option of poll.options" sticky>
<mat-header-cell *matHeaderCellDef>
<div *ngIf="option.user">{{ option.user.getFullName() }}</div>
<div *ngIf="!option.user">{{ "Unknown user" | translate }}</div>
<div *ngIf="!option.user">{{ 'Unknown user' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let row">
{{ row.votes[option.user_id] }}

View File

@ -0,0 +1,15 @@
.chart-wrapper {
&.flex {
display: flex;
.mat-table {
flex: 2;
.mat-column-votes {
justify-content: center;
}
}
.chart-inner-wrapper {
flex: 3;
}
}
}

View File

@ -26,11 +26,15 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
public candidatesLabels: string[] = [];
public get chartType(): ChartType {
return 'horizontalBar';
return this._chartType;
}
public get isVotedPoll(): boolean {
return this.poll.pollmethod === AssignmentPollMethods.Votes;
}
public get columnDefinitionOverview(): string[] {
const columns = ['user', 'yes', 'no', 'quorum'];
const columns = this.isVotedPoll ? ['user', 'votes'] : ['user', 'yes', 'no'];
if (this.poll.pollmethod === AssignmentPollMethods.YNA) {
columns.splice(3, 0, 'abstain');
}
@ -39,6 +43,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
public columnDefinitionPerName: string[];
private _chartType: ChartType = 'horizontalBar';
public constructor(
title: Title,
translate: TranslateService,
@ -62,7 +68,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
for (const vote of option.votes) {
// if poll was pseudoanonymized, use a negative index to not interfere with
// possible named votes (although this should never happen)
const userId = vote.user_id || i--;
const userId = vote.user_id || --i;
if (!votes[userId]) {
votes[userId] = {
user: vote.user,
@ -81,6 +87,13 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
this.isReady = true;
}
protected initChartData(): void {
if (this.isVotedPoll) {
this._chartType = 'doughnut';
this.chartDataSubject.next(this.poll.generateCircleChartData());
}
}
protected hasPerms(): boolean {
return this.operator.hasPerms('assignments.can_manage');
}

View File

@ -47,7 +47,6 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
/**
* Sets the type of the shown chart, if votes are entered.
*/
// public chartType = 'horizontalBar';
public abstract get chartType(): ChartType;
/**
@ -123,12 +122,10 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
}
/**
* This changes the data for the chart depending on the switch in the detail-view.
*
* @param isChecked boolean, if the chart should show the amount of entered votes.
* Opens dialog for editing the poll
*/
public changeChart(): void {
this.chartDataSubject.next(this.poll.generateChartData());
public openDialog(): void {
this.pollDialog.openDialog(this.poll);
}
protected onDeleted(): void {}
@ -167,12 +164,20 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
}
}
/**
* Initializes data for the shown chart.
* Could be overwritten to implement custom chart data.
*/
protected initChartData(): void {
this.chartDataSubject.next(this.poll.generateChartData());
}
/**
* This checks, if the poll has votes.
*/
private checkData(): void {
if (this.poll.state === 3 || this.poll.state === 4) {
setTimeout(() => this.chartDataSubject.next(this.poll.generateChartData()));
setTimeout(() => this.initChartData());
}
}
@ -187,17 +192,9 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
if (poll) {
this.poll = poll;
this.updateBreadcrumbs();
this.checkData();
this.onPollLoaded();
// wait for options to be loaded
(function waitForOptions(): void {
if (!this.poll.options || !this.poll.options.length) {
setTimeout(waitForOptions.bind(this), 1);
} else {
this.onPollWithOptionsLoaded();
}
}.call(this));
this.waitForOptions();
this.checkData();
}
})
);
@ -205,10 +202,14 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
}
/**
* Opens dialog for editing the poll
* Waits until poll's options are loaded.
*/
public openDialog(): void {
this.pollDialog.openDialog(this.poll);
private waitForOptions(): void {
if (!this.poll.options || !this.poll.options.length) {
setTimeout(() => this.waitForOptions(), 1);
} else {
this.onPollWithOptionsLoaded();
}
}
/**