Skip to content

Commit

Permalink
adds data table to course vocab term viz.
Browse files Browse the repository at this point in the history
consolidate data loading while at it, there were way too many moving
parts involved in this prior.
  • Loading branch information
stopfstedt committed Jul 17, 2024
1 parent f539095 commit dfe91e1
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,55 @@
</SimpleChart>
{{/if}}
{{/if}}
{{#if (and (not @isIcon) @showDataTable)}}
<div class="data-table" data-test-data-table>
<table>
<thead>
<tr>
<SortableTh
@sortedAscending={{this.sortedAscending}}
@sortedBy={{or (eq this.sortBy "sessionType") (eq this.sortBy "sessionType:desc")}}
@onClick={{fn this.setSortBy "sessionType"}}
data-test-vocabulary-term
>
{{t "general.sessionType"}}
</SortableTh>
<SortableTh
@colspan="2"
@sortedAscending={{this.sortedAscending}}
@sortedBy={{or (eq this.sortBy "sessionTitles") (eq this.sortBy "sessionTitles:desc")}}
@onClick={{fn this.setSortBy "sessionTitles"}}
data-test-sessions
>
{{t "general.sessions"}}
</SortableTh>
<SortableTh
@sortedAscending={{this.sortedAscending}}
@sortedBy={{or (eq this.sortBy "minutes") (eq this.sortBy "minutes:desc")}}
@onClick={{fn this.setSortBy "minutes"}}
@sortType="numeric"
data-test-minutes
>
{{t "general.minutes"}}
</SortableTh>
</tr>
</thead>
<tbody>
{{#each (sort-by this.sortBy this.tableData) as |row|}}
<tr>
<td data-test-vocabulary-session-type>{{row.sessionType}}</td>
<td colspan="2" data-test-sessions>
{{#each row.sessions as |session index|}}
<LinkTo @route="session" @models={{array @course session}}>
{{session.title~}}
</LinkTo>{{if (not-eq index (sub row.sessions.length 1)) ","}}
{{/each}}
</td>
<td data-test-minutes>{{row.minutes}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/if}}
</div>
128 changes: 71 additions & 57 deletions packages/ilios-common/addon/components/course/visualize-term-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,92 +4,105 @@ import { restartableTask, timeout } from 'ember-concurrency';
import { service } from '@ember/service';
import { cached, tracked } from '@glimmer/tracking';
import { TrackedAsyncData } from 'ember-async-data';
import {
findBy,
findById,
mapBy,
uniqueById,
uniqueValues,
} from 'ilios-common/utils/array-helpers';
import { findById, mapBy } from 'ilios-common/utils/array-helpers';
import { action } from '@ember/object';

export default class CourseVisualizeTermGraph extends Component {
@service router;
@service intl;
@tracked tooltipContent = null;
@tracked tooltipTitle = null;
@tracked sortBy = 'minutes';

@cached
get sessionsData() {
return new TrackedAsyncData(this.args.course.sessions);
}

get sessions() {
return this.sessionsData.isResolved ? this.sessionsData.value : null;
return this.sessionsData.isResolved ? this.sessionsData.value : [];
}

get termSessionIds() {
return this.args.term.hasMany('sessions').ids();
}

@cached
get sessionTypesData() {
if (!this.sessionsData.isResolved) {
return null;
}
return new TrackedAsyncData(Promise.all(this.sessionsData.value.map((s) => s.sessionType)));
get outputData() {
return new TrackedAsyncData(this.getDataObjects(this.sessions, this.termSessionIds));
}

get sessionTypes() {
return this.sessionTypesData?.isResolved ? uniqueById(this.sessionTypesData.value) : null;
get data() {
return this.outputData.isResolved ? this.outputData.value : [];
}

get isLoaded() {
return !!this.sessionTypes;
return this.outputData.isResolved;
}

get termSessionIds() {
return this.args.term.hasMany('sessions').ids();
get tableData() {
return this.data.map((obj) => {
const rhett = {};
rhett.minutes = obj.data;
rhett.sessions = obj.meta.sessions;
rhett.sessionType = obj.meta.sessionType.title;
rhett.sessionTitles = mapBy(rhett.sessions, 'title').join(', ');
return rhett;
});
}

get termSessionsInCourse() {
return this.sessions.filter((session) => this.termSessionIds.includes(session.id));
get sortedAscending() {
return this.sortBy.search(/desc/) === -1;
}

get data() {
const sessionTypeData = this.termSessionsInCourse.map((session) => {
@action
setSortBy(prop) {
if (this.sortBy === prop) {
prop += ':desc';
}
this.sortBy = prop;
}

async getDataObjects(sessions, termIds) {
const filteredSessions = sessions.filter((session) => termIds.includes(session.id));
const sessionTypes = await Promise.all(filteredSessions.map((s) => s.sessionType));
const sessionTypeData = filteredSessions.map((session) => {
const minutes = Math.round(session.totalSumDuration * 60);
const sessionType = findById(this.sessionTypes, session.belongsTo('sessionType').id());
const sessionType = findById(sessionTypes, session.belongsTo('sessionType').id());
return {
sessionTitle: session.title,
sessionTypeTitle: sessionType.title,
session,
sessionType,
minutes,
};
});

const data = sessionTypeData.reduce((set, obj) => {
let existing = findBy(set, 'label', obj.sessionTypeTitle);
if (!existing) {
existing = {
data: 0,
label: obj.sessionTypeTitle,
meta: {
sessionTypeTitle: obj.sessionTypeTitle,
sessions: [],
},
};
set.push(existing);
}
existing.data += obj.minutes;
existing.meta.sessions.push(obj.sessionTitle);

return set;
}, []);

const totalMinutes = mapBy(data, 'data').reduce((total, minutes) => total + minutes, 0);

return data.map((obj) => {
const percent = ((obj.data / totalMinutes) * 100).toFixed(1);
obj.label = `${obj.meta.sessionTypeTitle} ${percent}%`;
obj.meta.totalMinutes = totalMinutes;
obj.meta.percent = percent;
return obj;
});
return sessionTypeData
.reduce((set, obj) => {
const id = obj.sessionType.id;
let existing = findById(set, id);
if (!existing) {
existing = {
id,
data: 0,
label: obj.sessionType.title,
meta: {
sessionType: obj.sessionType,
sessions: [],
},
};
set.push(existing);
}
existing.data += obj.minutes;
existing.meta.sessions.push(obj.session);
return set;
}, [])
.map((obj) => {
delete obj.id;
return obj;
})
.sort((first, second) => {
return first.data - second.data;
});
}

barHover = restartableTask(async (obj) => {
Expand All @@ -99,9 +112,10 @@ export default class CourseVisualizeTermGraph extends Component {
this.tooltipContent = null;
return;
}
const { label, data, meta } = obj;

this.tooltipTitle = htmlSafe(`${label} ${data} ${this.intl.t('general.minutes')}`);
this.tooltipContent = uniqueValues(meta.sessions).sort().join(', ');
const { data, meta } = obj;
this.tooltipTitle = htmlSafe(
`${meta.sessionType.title} &bull; ${data} ${this.intl.t('general.minutes')}`,
);
this.tooltipContent = mapBy(meta.sessions, 'title').sort().join(', ');
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@
</LinkTo>
</h3>
<div class="visualizations">
<Course::VisualizeTermGraph @course={{@model.course}} @term={{@model.term}} />
<Course::VisualizeTermGraph
@course={{@model.course}}
@term={{@model.term}}
@showDataTable={{true}}
/>
</div>
{{/unless}}
</section>
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
@use "../../colors" as c;
@use "../../mixins" as m;

.course-visualize-term-graph {
display: inline-block;
height: 1rem;
width: 1rem;

.data-table {
grid-column: -1/1;
padding-top: 2rem;

table {
@include m.ilios-table-structure;
@include m.ilios-table-colors;
@include m.ilios-removable-table;
@include m.ilios-zebra-table;

thead {
background-color: c.$culturedGrey;
}

td {
vertical-align: top;
}
}
}

&.not-icon {
height: 75vh;
width: 75vw;
display: grid;
height: auto;
width: auto;

.simple-chart-tooltip {
.title {
Expand Down

0 comments on commit dfe91e1

Please sign in to comment.