Skip to content

Commit 9f74eda

Browse files
guguclaude
andcommitted
feat: improve chart configuration UI and add time-based axis support
- Separate chart configuration from preview section in chart-edit - Add "Configure Chart" link to dashboard widget menu - Rename "Edit" to "Change Query" for clarity - Add time-based X axis for datetime label type with gap support - Install chartjs-adapter-date-fns for Chart.js time scale Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d87cb28 commit 9f74eda

17 files changed

Lines changed: 588 additions & 391 deletions

File tree

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"angular-password-strength-meter": "npm:@eresearchqut/angular-password-strength-meter@^13.0.7",
4141
"angulartics2": "^14.1.0",
4242
"chart.js": "^4.5.1",
43+
"chartjs-adapter-date-fns": "^3.0.0",
4344
"color-string": "^2.0.1",
4445
"convert": "^5.12.0",
4546
"date-fns": "^4.1.0",

frontend/src/app/components/charts/chart-edit/chart-edit.component.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@
7272
}
7373

7474
.editor-section,
75+
.right-panel {
76+
display: flex;
77+
flex-direction: column;
78+
gap: 24px;
79+
}
80+
81+
.config-section,
7582
.preview-section {
7683
display: flex;
7784
flex-direction: column;

frontend/src/app/components/charts/chart-edit/chart-edit.component.html

Lines changed: 64 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -57,64 +57,72 @@ <h3>SQL Query</h3>
5757
</div>
5858
</div>
5959

60-
<div class="preview-section" *ngIf="showResults()">
61-
<div class="section-header">
62-
<h3>Chart Preview</h3>
63-
<span class="execution-time" *ngIf="executionTime() !== null">
64-
Executed in {{ executionTime() }}ms
65-
</span>
66-
</div>
67-
68-
<div class="chart-config">
69-
<mat-form-field appearance="outline" class="chart-config-field">
70-
<mat-label>Chart Type</mat-label>
71-
<mat-select [ngModel]="chartType()" (ngModelChange)="chartType.set($event)" data-testid="chart-type-select">
72-
<mat-option *ngFor="let type of chartTypes" [value]="type.value">
73-
{{ type.label }}
74-
</mat-option>
75-
</mat-select>
76-
</mat-form-field>
77-
78-
<mat-form-field appearance="outline" class="chart-config-field">
79-
<mat-label>Label Column</mat-label>
80-
<mat-select [ngModel]="labelColumn()" (ngModelChange)="labelColumn.set($event)" data-testid="label-column-select">
81-
<mat-option *ngFor="let col of resultColumns()" [value]="col">
82-
{{ col }}
83-
</mat-option>
84-
</mat-select>
85-
</mat-form-field>
86-
87-
<mat-form-field appearance="outline" class="chart-config-field" *ngIf="showLabelTypeOption()">
88-
<mat-label>Label Type</mat-label>
89-
<mat-select [ngModel]="labelType()" (ngModelChange)="labelType.set($event)" data-testid="label-type-select">
90-
<mat-option *ngFor="let type of labelTypes" [value]="type.value">
91-
{{ type.label }}
92-
</mat-option>
93-
</mat-select>
94-
</mat-form-field>
95-
96-
<mat-form-field appearance="outline" class="chart-config-field">
97-
<mat-label>Value Column</mat-label>
98-
<mat-select [ngModel]="valueColumn()" (ngModelChange)="valueColumn.set($event)" data-testid="value-column-select">
99-
<mat-option *ngFor="let col of resultColumns()" [value]="col">
100-
{{ col }}
101-
</mat-option>
102-
</mat-select>
103-
</mat-form-field>
104-
</div>
105-
106-
<div class="chart-container" *ngIf="hasChartData()">
107-
<app-chart-preview
108-
[chartType]="chartType()"
109-
[data]="testResults()"
110-
[labelColumn]="labelColumn()"
111-
[valueColumn]="valueColumn()"
112-
[labelType]="labelType()">
113-
</app-chart-preview>
60+
<div class="right-panel" *ngIf="showResults()">
61+
<div class="config-section">
62+
<div class="section-header">
63+
<h3>Chart Configuration</h3>
64+
<span class="execution-time" *ngIf="executionTime() !== null">
65+
Executed in {{ executionTime() }}ms
66+
</span>
67+
</div>
68+
69+
<div class="chart-config">
70+
<mat-form-field appearance="outline" class="chart-config-field">
71+
<mat-label>Chart Type</mat-label>
72+
<mat-select [ngModel]="chartType()" (ngModelChange)="chartType.set($event)" data-testid="chart-type-select">
73+
<mat-option *ngFor="let type of chartTypes" [value]="type.value">
74+
{{ type.label }}
75+
</mat-option>
76+
</mat-select>
77+
</mat-form-field>
78+
79+
<mat-form-field appearance="outline" class="chart-config-field">
80+
<mat-label>Label Column</mat-label>
81+
<mat-select [ngModel]="labelColumn()" (ngModelChange)="labelColumn.set($event)" data-testid="label-column-select">
82+
<mat-option *ngFor="let col of resultColumns()" [value]="col">
83+
{{ col }}
84+
</mat-option>
85+
</mat-select>
86+
</mat-form-field>
87+
88+
<mat-form-field appearance="outline" class="chart-config-field" *ngIf="showLabelTypeOption()">
89+
<mat-label>Label Type</mat-label>
90+
<mat-select [ngModel]="labelType()" (ngModelChange)="labelType.set($event)" data-testid="label-type-select">
91+
<mat-option *ngFor="let type of labelTypes" [value]="type.value">
92+
{{ type.label }}
93+
</mat-option>
94+
</mat-select>
95+
</mat-form-field>
96+
97+
<mat-form-field appearance="outline" class="chart-config-field">
98+
<mat-label>Value Column</mat-label>
99+
<mat-select [ngModel]="valueColumn()" (ngModelChange)="valueColumn.set($event)" data-testid="value-column-select">
100+
<mat-option *ngFor="let col of resultColumns()" [value]="col">
101+
{{ col }}
102+
</mat-option>
103+
</mat-select>
104+
</mat-form-field>
105+
</div>
114106
</div>
115107

116-
<div class="no-chart-data" *ngIf="!hasChartData() && testResults().length > 0">
117-
<p>Select label and value columns to display the chart</p>
108+
<div class="preview-section">
109+
<div class="section-header">
110+
<h3>Chart Preview</h3>
111+
</div>
112+
113+
<div class="chart-container" *ngIf="hasChartData()">
114+
<app-chart-preview
115+
[chartType]="chartType()"
116+
[data]="testResults()"
117+
[labelColumn]="labelColumn()"
118+
[valueColumn]="valueColumn()"
119+
[labelType]="labelType()">
120+
</app-chart-preview>
121+
</div>
122+
123+
<div class="no-chart-data" *ngIf="!hasChartData() && testResults().length > 0">
124+
<p>Select label and value columns to display the chart</p>
125+
</div>
118126
</div>
119127
</div>
120128
</div>
Lines changed: 115 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CommonModule } from '@angular/common';
22
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
33
import { ChartConfiguration, ChartData, ChartType as ChartJsType } from 'chart.js';
4+
import 'chartjs-adapter-date-fns';
45
import { BaseChartDirective } from 'ng2-charts';
56
import { ChartType } from 'src/app/models/saved-query';
67

@@ -18,16 +19,7 @@ export class ChartPreviewComponent implements OnChanges {
1819
@Input() labelType: 'values' | 'datetime' = 'values';
1920

2021
public chartData: ChartData<ChartJsType> | null = null;
21-
public chartOptions: ChartConfiguration['options'] = {
22-
responsive: true,
23-
maintainAspectRatio: false,
24-
plugins: {
25-
legend: {
26-
display: true,
27-
position: 'top',
28-
},
29-
},
30-
};
22+
public chartOptions: ChartConfiguration['options'] = this._getDefaultOptions();
3123

3224
private colorPalette = [
3325
'rgba(99, 102, 241, 0.8)',
@@ -58,71 +50,147 @@ export class ChartPreviewComponent implements OnChanges {
5850
return this.chartType as ChartJsType;
5951
}
6052

53+
private _getDefaultOptions(): ChartConfiguration['options'] {
54+
return {
55+
responsive: true,
56+
maintainAspectRatio: false,
57+
plugins: {
58+
legend: {
59+
display: true,
60+
position: 'top',
61+
},
62+
},
63+
};
64+
}
65+
66+
private _getTimeScaleOptions(): ChartConfiguration['options'] {
67+
return {
68+
responsive: true,
69+
maintainAspectRatio: false,
70+
plugins: {
71+
legend: {
72+
display: true,
73+
position: 'top',
74+
},
75+
},
76+
scales: {
77+
x: {
78+
type: 'time',
79+
time: {
80+
tooltipFormat: 'MMM d, yyyy',
81+
displayFormats: {
82+
day: 'MMM d',
83+
week: 'MMM d',
84+
month: 'MMM yyyy',
85+
year: 'yyyy',
86+
},
87+
},
88+
title: {
89+
display: true,
90+
text: this.labelColumn,
91+
},
92+
},
93+
y: {
94+
beginAtZero: true,
95+
title: {
96+
display: true,
97+
text: this.valueColumn,
98+
},
99+
},
100+
},
101+
};
102+
}
103+
61104
private updateChartData(): void {
62105
if (!this.data.length || !this.labelColumn || !this.valueColumn) {
63106
this.chartData = null;
64107
return;
65108
}
66109

67-
const labels = this.data.map((row) => {
68-
const val = row[this.labelColumn];
69-
if (this.labelType === 'datetime' && val) {
70-
return this.formatDatetime(val);
71-
}
72-
return String(val ?? '');
73-
});
74-
const values = this.data.map((row) => {
75-
const val = row[this.valueColumn];
76-
return typeof val === 'number' ? val : parseFloat(String(val)) || 0;
77-
});
78-
79110
const isPieType = ['pie', 'doughnut', 'polarArea'].includes(this.chartType);
111+
const useTimeScale = this.labelType === 'datetime' && !isPieType;
112+
113+
// Update options based on whether we're using time scale
114+
this.chartOptions = useTimeScale ? this._getTimeScaleOptions() : this._getDefaultOptions();
115+
116+
if (useTimeScale) {
117+
// For time scale, use {x, y} data points
118+
const dataPoints = this.data
119+
.map((row) => {
120+
const dateVal = row[this.labelColumn];
121+
const numVal = row[this.valueColumn];
122+
const date = this._parseDate(dateVal);
123+
if (!date) return null;
124+
return {
125+
x: date.getTime(),
126+
y: typeof numVal === 'number' ? numVal : parseFloat(String(numVal)) || 0,
127+
};
128+
})
129+
.filter((point): point is { x: number; y: number } => point !== null)
130+
.sort((a, b) => a.x - b.x);
80131

81-
if (isPieType) {
82-
this.chartData = {
83-
labels,
84-
datasets: [
85-
{
86-
data: values,
87-
backgroundColor: this.colorPalette.slice(0, values.length),
88-
borderColor: this.colorPalette.slice(0, values.length).map((c) => c.replace('0.8', '1')),
89-
borderWidth: 1,
90-
},
91-
],
92-
};
93-
} else {
94132
this.chartData = {
95-
labels,
96133
datasets: [
97134
{
98135
label: this.valueColumn,
99-
data: values,
136+
data: dataPoints,
100137
backgroundColor: this.colorPalette[0],
101138
borderColor: this.colorPalette[0].replace('0.8', '1'),
102139
borderWidth: 1,
103140
fill: this.chartType === 'line',
141+
spanGaps: false,
104142
},
105143
],
106144
};
145+
} else {
146+
// For categorical scale, use labels + values
147+
const labels = this.data.map((row) => String(row[this.labelColumn] ?? ''));
148+
const values = this.data.map((row) => {
149+
const val = row[this.valueColumn];
150+
return typeof val === 'number' ? val : parseFloat(String(val)) || 0;
151+
});
152+
153+
if (isPieType) {
154+
this.chartData = {
155+
labels,
156+
datasets: [
157+
{
158+
data: values,
159+
backgroundColor: this.colorPalette.slice(0, values.length),
160+
borderColor: this.colorPalette.slice(0, values.length).map((c) => c.replace('0.8', '1')),
161+
borderWidth: 1,
162+
},
163+
],
164+
};
165+
} else {
166+
this.chartData = {
167+
labels,
168+
datasets: [
169+
{
170+
label: this.valueColumn,
171+
data: values,
172+
backgroundColor: this.colorPalette[0],
173+
borderColor: this.colorPalette[0].replace('0.8', '1'),
174+
borderWidth: 1,
175+
fill: this.chartType === 'line',
176+
},
177+
],
178+
};
179+
}
107180
}
108181
}
109182

110-
private formatDatetime(value: unknown): string {
111-
if (!value) return '';
183+
private _parseDate(value: unknown): Date | null {
184+
if (!value) return null;
112185

113186
try {
114187
const date = new Date(value as string | number | Date);
115188
if (isNaN(date.getTime())) {
116-
return String(value);
189+
return null;
117190
}
118-
// Format as localized date string
119-
return date.toLocaleDateString(undefined, {
120-
year: 'numeric',
121-
month: 'short',
122-
day: 'numeric',
123-
});
191+
return date;
124192
} catch {
125-
return String(value);
193+
return null;
126194
}
127195
}
128196
}

frontend/src/app/components/dashboards/dashboard-delete-dialog/dashboard-delete-dialog.component.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,15 @@ export class DashboardDeleteDialogComponent {
2323
private angulartics2: Angulartics2,
2424
) {}
2525

26-
onDelete(): void {
26+
async onDelete(): Promise<void> {
2727
this.submitting.set(true);
28-
this._dashboards.deleteDashboard(this.data.connectionId, this.data.dashboard.id).subscribe({
29-
next: () => {
30-
this.angulartics2.eventTrack.next({
31-
action: 'Dashboards: dashboard deleted successfully',
32-
});
33-
this.submitting.set(false);
34-
this.dialogRef.close(true);
35-
},
36-
error: () => {
37-
this.submitting.set(false);
38-
},
39-
});
28+
const result = await this._dashboards.deleteDashboard(this.data.connectionId, this.data.dashboard.id);
29+
if (result) {
30+
this.angulartics2.eventTrack.next({
31+
action: 'Dashboards: dashboard deleted successfully',
32+
});
33+
this.dialogRef.close(true);
34+
}
35+
this.submitting.set(false);
4036
}
4137
}

0 commit comments

Comments
 (0)