Skip to content

Commit

Permalink
quick-n-dirty osf-metrics report ui
Browse files Browse the repository at this point in the history
  • Loading branch information
aaxelb committed Nov 10, 2022
1 parent 81c41de commit 8683062
Show file tree
Hide file tree
Showing 13 changed files with 510 additions and 1 deletion.
7 changes: 7 additions & 0 deletions app/helpers/includes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { helper } from '@ember/component/helper';

export function includes([array, value]: [unknown[], unknown]): boolean {
return array.includes(value);
}

export default helper(includes);
52 changes: 52 additions & 0 deletions app/modifiers/metrics-chart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Modifier from 'ember-modifier';
import bb, { line, subchart } from 'billboard.js';

interface MetricsChartArgs {
positional: [];
named: {
dataColumns?: Array<Array<string|number>>,
dataRows?: Array<Array<string|number>>,
};
}

export default class MetricsChart extends Modifier<MetricsChartArgs> {
chart: any = null;

didReceiveArguments() {
if (this.chart) {
this.chart.destroy();
}
this.chart = bb.generate({
bindto: this.element,
data: {
type: line(),
x: 'report_date',
// columns: this.args.named.dataColumns,
rows: this.args.named.dataRows,
},
axis: {
x: {
type: 'timeseries',
tick: {
format: '%Y-%m-%d',
},
},
},
subchart: {
show: subchart(),
showHandle: true,
},
tooltip: {
grouped: false,
linked: true,
},
});
}

willRemove() {
if (this.chart) {
this.chart.destroy();
}
}
}

1 change: 1 addition & 0 deletions app/osf-metrics/loading.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<LoadingIndicator />
124 changes: 124 additions & 0 deletions app/osf-metrics/report-detail/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import Controller from '@ember/controller';
import { action, get } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { MetricsReportAttrs } from './route';


type ReportFields = {
keywordFields: string[],
numericFields: string[],
};


function gatherFields(obj: any): ReportFields {
const keywordFields: string[] = []
const numericFields: string[] = []
for (const fieldName in obj) {
if (fieldName === 'report_date' || fieldName === 'timestamp') {
continue;
}
const fieldValue = obj[fieldName];
switch (typeof fieldValue) {
case 'string':
keywordFields.push(fieldName);
break;
case 'number':
numericFields.push(fieldName);
break;
case 'object':
const nestedFields = gatherFields(fieldValue);
keywordFields.push(...nestedFields.keywordFields.map(
nestedFieldName => `${fieldName}.${nestedFieldName}`,
));
numericFields.push(...nestedFields.numericFields.map(
nestedFieldName => `${fieldName}.${nestedFieldName}`,
));
break;
default:
console.log(`ignoring unexpected ${fieldName}: ${fieldValue}`)
}
}
return {
keywordFields,
numericFields,
};
}


export default class MetricsReportDetailController extends Controller {
queryParams = [
{ daysBack: { scope: 'controller' as const } },
'yFields',
'xGroupField',
'xGroupFilter',
]

@tracked daysBack: string = '13';
@tracked model: MetricsReportAttrs[] = [];
@tracked yFields: string[] = [];
@tracked xGroupField?: string;
@tracked xField: string = 'report_date';
@tracked xGroupFilter: string = '';

get reportFields(): ReportFields {
const aReport: MetricsReportAttrs = this.model![0];
return gatherFields(aReport);
}

get chartRows(): Array<Array<string|number|null>>{
if (!this.xGroupField) {
const fieldNames = [this.xField, ...this.yFields];
const rows = this.model.map(
datum => fieldNames.map(
fieldName => (get(datum, fieldName) as string | number | undefined) ?? null,
),
);
return [fieldNames, ...rows];
}
const groupedFieldNames = new Set<string>();
const rowsByX: any = {};
for (const datum of this.model) {
const xValue = get(datum, this.xField) as string;
if (!rowsByX[xValue]) {
rowsByX[xValue] = {};
}
const groupName = get(datum, this.xGroupField) as string;
if (!this.xGroupFilter || groupName.includes(this.xGroupFilter)) {
this.yFields.forEach(fieldName => {
const groupedField = `${groupName} ${fieldName}`;
groupedFieldNames.add(groupedField);
const fieldValue = get(datum, fieldName);
rowsByX[xValue][groupedField] = fieldValue;
});
}
}
const rows = Object.entries(rowsByX).map(
([xValue, rowData]: [string, any]) => {
const yValues = [...groupedFieldNames].map(
groupedFieldName => (rowData[groupedFieldName] as string | number | undefined) ?? null,
);
return [xValue, ...yValues];
},
);
return [
[this.xField, ...groupedFieldNames],
...rows,
];
}

@action
yFieldToggle(fieldName: string) {
if (this.yFields.includes(fieldName)) {
this.yFields = this.yFields.filter(f => f !== fieldName);
} else {
this.yFields = [...this.yFields, fieldName];
}
}
}

declare module '@ember/controller' {
interface Registry {
'osf-metrics.report-detail': MetricsReportDetailController;
}
}

47 changes: 47 additions & 0 deletions app/osf-metrics/report-detail/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Route from '@ember/routing/route';
import config from 'ember-get-config';

const {
OSF: {
apiUrl,
},
} = config;

export interface MetricsReportAttrs {
report_date: string, // YYYY-MM-DD
[attr: string]: string | number | object,
}

interface MetricsReport {
id: string;
type: string;
attributes: MetricsReportAttrs;
}

interface RecentMetricsReportResponse {
data: MetricsReport[];
}

export default class OsfMetricsRoute extends Route {
queryParams = {
daysBack: {
refreshModel: true,
},
yFields: {
replace: true,
},
xGroupField: {
replace: true,
},
xGroupFilter: {
replace: true,
},
}

async model(params: { daysBack: string, reportName?: string }) {
const url = `${apiUrl}/_/metrics/reports/${params.reportName}/recent/?days_back=${params.daysBack}`
const response = await fetch(url);
const responseJson: RecentMetricsReportResponse = await response.json();
return responseJson.data.map(datum => datum.attributes);
}
}
43 changes: 43 additions & 0 deletions app/osf-metrics/report-detail/template.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<p>
days back
{{#each (array '7' '13' '31' '73' '371') as |daysBackOption|}}
| <LinkTo @query={{hash daysBack=daysBackOption}}>{{daysBackOption}}</LinkTo>
{{/each}}
</p>
<p>
data fields (y-axis)
{{#each this.reportFields.numericFields as |fieldName|}}
| <label>
<Input
@type='checkbox'
@checked={{includes this.yFields fieldName}}
{{on 'input' (fn this.yFieldToggle fieldName)}}
/>
{{fieldName}}
</label>
{{/each}}
</p>
{{#if this.reportFields.keywordFields.length}}
<p>
group by
<PowerSelect
@options={{this.reportFields.keywordFields}}
@selected={{this.xGroupField}}
@onChange={{fn (mut this.xGroupField)}}
as |item|
>
{{item}}
</PowerSelect>
</p>
<p>
<label>
filter groups
<Input @type='text' @value={{mut this.xGroupFilter}} />
</label>
</p>
{{/if}}
{{#if (and this.model.length this.yFields.length)}}
<section>
<div {{metrics-chart dataRows=this.chartRows}}></div>
</section>
{{/if}}
29 changes: 29 additions & 0 deletions app/osf-metrics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Route from '@ember/routing/route';
import config from 'ember-get-config';

const {
OSF: {
apiUrl,
},
} = config;

interface MetricsReportName {
id: string;
type: 'metrics-report-name';
links: {
recent: string,
};
}

interface MetricsReportNameResponse {
data: MetricsReportName[];
}

export default class OsfMetricsRoute extends Route {
async model() {
const url = `${apiUrl}/_/metrics/reports/`;
const response = await fetch(url);
const responseJson: MetricsReportNameResponse = await response.json();
return responseJson.data.map(metricsReport => metricsReport.id);
}
}
17 changes: 17 additions & 0 deletions app/osf-metrics/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.OsfMetrics {
display: flex;
flex-direction: column;
align-items: center;
}

.OsfMetrics > p {
max-width: 62vw;
}

.OsfMetrics > section {
width: 87vw;
}

.OsfMetrics :global(.active) {
font-weight: bold;
}
12 changes: 12 additions & 0 deletions app/osf-metrics/template.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{{page-title 'osf metrics'}}
<section local-class='OsfMetrics'>
<h1>osf metrics</h1>
<p>
reports
{{#each @model as |reportName|}}
|
<LinkTo @route='osf-metrics.report-detail' @model={{reportName}}>{{reportName}}</LinkTo>
{{/each}}
</p>
{{outlet}}
</section>
3 changes: 3 additions & 0 deletions app/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ Router.map(function() {
});
});
this.route('support');
this.route('osf-metrics', function() {
this.route('report-detail', { path: '/:reportName' });
});
this.route('meetings', function() {
this.route('detail', { path: '/:meeting_id' });
});
Expand Down
2 changes: 2 additions & 0 deletions ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,7 @@ module.exports = function(defaults) {

app.import('node_modules/wicg-inert/dist/inert.min.js');

app.import('node_modules/billboard.js/dist/billboard.css');

return app.toTree();
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"babel-eslint": "^8.0.0",
"billboard.js": "3.6.3",
"bootstrap-sass": "^3.3.7",
"broccoli-asset-rev": "^3.0.0",
"chai": "^4.1.2",
Expand Down
Loading

0 comments on commit 8683062

Please sign in to comment.