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"> <ng-container *ngIf="chartData.length || circleData.length">
<canvas <canvas
*ngIf="type === 'bar' || type === 'stackedBar' || type === 'horizontalBar' || type === 'line'" *ngIf="type === 'bar' || type === 'stackedBar' || type === 'horizontalBar' || type === 'line'"

View File

@ -1,4 +1,15 @@
.charts-wrapper { .charts-wrapper {
position: relative; position: relative;
display: block; 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; 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. * Fires an event, when the user clicks on the chart.
*/ */
@ -135,6 +161,13 @@ export class ChartsComponent extends BaseViewComponent {
@Output() @Output()
public hover = new EventEmitter<ChartEvent>(); 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. * The general data for the chart.
* This is only needed for `type == 'bar' || 'line'` * This is only needed for `type == 'bar' || 'line'`
@ -191,6 +224,11 @@ export class ChartsComponent extends BaseViewComponent {
private _chartLegendSize: ChartLegendSize = 'middle'; private _chartLegendSize: ChartLegendSize = 'middle';
/**
* Holds the value for `max-width`.
*/
private _size = 100;
/** /**
* Constructor. * Constructor.
* *

View File

@ -47,58 +47,67 @@
<div *ngIf="poll.stateHasVotes"> <div *ngIf="poll.stateHasVotes">
<h2 translate>Result</h2> <h2 translate>Result</h2>
<div class="chart-wrapper"></div> <div class="chart-wrapper" [ngClass]="{ flex: isVotedPoll }">
<mat-table [dataSource]="poll.tableData"> <mat-table [dataSource]="poll.tableData">
<ng-container matColumnDef="user" sticky> <ng-container matColumnDef="user" sticky>
<mat-header-cell *matHeaderCellDef>{{ 'Candidates' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef>{{ 'Candidates' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.user }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.user }}</mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="yes"> <div *ngIf="!isVotedPoll">
<mat-header-cell *matHeaderCellDef>{{ 'Yes' | translate }}</mat-header-cell> <ng-container matColumnDef="yes">
<mat-cell *matCellDef="let row">{{ row.yes }}</mat-cell> <mat-header-cell *matHeaderCellDef>{{ 'Yes' | translate }}</mat-header-cell>
</ng-container> <mat-cell *matCellDef="let row">{{ row.yes }}</mat-cell>
</ng-container>
<ng-container matColumnDef="no"> <ng-container matColumnDef="no">
<mat-header-cell *matHeaderCellDef>{{ 'No' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef>{{ 'No' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.no }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.no }}</mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="abstain"> <ng-container matColumnDef="abstain">
<mat-header-cell *matHeaderCellDef>{{ 'Abstain' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef>{{ 'Abstain' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.abstain }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.abstain }}</mat-cell>
</ng-container> </ng-container>
</div>
<ng-container matColumnDef="quorum"> <div *ngIf="isVotedPoll">
<mat-header-cell *matHeaderCellDef>{{ 'Quorum' | translate }}</mat-header-cell> <ng-container matColumnDef="votes">
<mat-cell *matCellDef="let row"></mat-cell> <mat-header-cell *matHeaderCellDef>{{ 'Votes' | translate }}</mat-header-cell>
</ng-container> <mat-cell *matCellDef="let row">{{ row.yes }}</mat-cell>
</ng-container>
</div>
<mat-header-row *matHeaderRowDef="columnDefinitionOverview"></mat-header-row> <mat-header-row *matHeaderRowDef="columnDefinitionOverview"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnDefinitionOverview"></mat-row> <mat-row *matRowDef="let row; columns: columnDefinitionOverview"></mat-row>
</mat-table> </mat-table>
<os-charts <div class="chart-inner-wrapper">
*ngIf="chartDataSubject.value" <os-charts
[type]="chartType" *ngIf="chartDataSubject.value"
[labels]="candidatesLabels" [type]="chartType"
[showLegend]="true" [labels]="candidatesLabels"
[data]="chartDataSubject" [size]="isVotedPoll ? 70 : 100"
></os-charts> [legendPosition]="isVotedPoll ? 'right' : 'top'"
[showLegend]="true"
[data]="chartDataSubject"
></os-charts>
</div>
</div>
<ng-container *ngIf="poll.type === 'named' && votesDataSource.data"> <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"> <mat-table [dataSource]="votesDataSource">
<ng-container matColumnDef="users" sticky> <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"> <mat-cell *matCellDef="let row">
<div *ngIf="row.user">{{ row.user.getFullName() }}</div> <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> </mat-cell>
</ng-container> </ng-container>
<ng-container [matColumnDef]="'votes-' + option.user_id" *ngFor="let option of poll.options" sticky> <ng-container [matColumnDef]="'votes-' + option.user_id" *ngFor="let option of poll.options" sticky>
<mat-header-cell *matHeaderCellDef> <mat-header-cell *matHeaderCellDef>
<div *ngIf="option.user">{{ option.user.getFullName() }}</div> <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-header-cell>
<mat-cell *matCellDef="let row"> <mat-cell *matCellDef="let row">
{{ row.votes[option.user_id] }} {{ 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 candidatesLabels: string[] = [];
public get chartType(): ChartType { public get chartType(): ChartType {
return 'horizontalBar'; return this._chartType;
}
public get isVotedPoll(): boolean {
return this.poll.pollmethod === AssignmentPollMethods.Votes;
} }
public get columnDefinitionOverview(): string[] { 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) { if (this.poll.pollmethod === AssignmentPollMethods.YNA) {
columns.splice(3, 0, 'abstain'); columns.splice(3, 0, 'abstain');
} }
@ -39,6 +43,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
public columnDefinitionPerName: string[]; public columnDefinitionPerName: string[];
private _chartType: ChartType = 'horizontalBar';
public constructor( public constructor(
title: Title, title: Title,
translate: TranslateService, translate: TranslateService,
@ -62,7 +68,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
for (const vote of option.votes) { for (const vote of option.votes) {
// if poll was pseudoanonymized, use a negative index to not interfere with // if poll was pseudoanonymized, use a negative index to not interfere with
// possible named votes (although this should never happen) // possible named votes (although this should never happen)
const userId = vote.user_id || i--; const userId = vote.user_id || --i;
if (!votes[userId]) { if (!votes[userId]) {
votes[userId] = { votes[userId] = {
user: vote.user, user: vote.user,
@ -81,6 +87,13 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
this.isReady = true; this.isReady = true;
} }
protected initChartData(): void {
if (this.isVotedPoll) {
this._chartType = 'doughnut';
this.chartDataSubject.next(this.poll.generateCircleChartData());
}
}
protected hasPerms(): boolean { protected hasPerms(): boolean {
return this.operator.hasPerms('assignments.can_manage'); 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. * Sets the type of the shown chart, if votes are entered.
*/ */
// public chartType = 'horizontalBar';
public abstract get chartType(): ChartType; 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. * Opens dialog for editing the poll
*
* @param isChecked boolean, if the chart should show the amount of entered votes.
*/ */
public changeChart(): void { public openDialog(): void {
this.chartDataSubject.next(this.poll.generateChartData()); this.pollDialog.openDialog(this.poll);
} }
protected onDeleted(): void {} 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. * This checks, if the poll has votes.
*/ */
private checkData(): void { private checkData(): void {
if (this.poll.state === 3 || this.poll.state === 4) { 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) { if (poll) {
this.poll = poll; this.poll = poll;
this.updateBreadcrumbs(); this.updateBreadcrumbs();
this.checkData();
this.onPollLoaded(); this.onPollLoaded();
this.waitForOptions();
// wait for options to be loaded this.checkData();
(function waitForOptions(): void {
if (!this.poll.options || !this.poll.options.length) {
setTimeout(waitForOptions.bind(this), 1);
} else {
this.onPollWithOptionsLoaded();
}
}.call(this));
} }
}) })
); );
@ -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 { private waitForOptions(): void {
this.pollDialog.openDialog(this.poll); if (!this.poll.options || !this.poll.options.length) {
setTimeout(() => this.waitForOptions(), 1);
} else {
this.onPollWithOptionsLoaded();
}
} }
/** /**