@cosla/sensemaking-web-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +35 -0
  2. package/angular.json +116 -0
  3. package/health_check.js +68 -0
  4. package/karma.conf.js +43 -0
  5. package/package.json +93 -0
  6. package/public/favicon.ico +0 -0
  7. package/server.ts +60 -0
  8. package/single-html-build.js +100 -0
  9. package/site-build.ts +54 -0
  10. package/src/app/app.component.html +1 -0
  11. package/src/app/app.component.scss +0 -0
  12. package/src/app/app.component.spec.ts +32 -0
  13. package/src/app/app.component.ts +21 -0
  14. package/src/app/app.config.server.ts +9 -0
  15. package/src/app/app.config.ts +19 -0
  16. package/src/app/app.routes.ts +8 -0
  17. package/src/app/components/dialog/dialog.component.html +15 -0
  18. package/src/app/components/dialog/dialog.component.scss +42 -0
  19. package/src/app/components/dialog/dialog.component.spec.ts +27 -0
  20. package/src/app/components/dialog/dialog.component.ts +40 -0
  21. package/src/app/components/sensemaking-chart-wrapper/sensemaking-chart-wrapper.component.ts +66 -0
  22. package/src/app/components/statement-card/statement-card.component.html +51 -0
  23. package/src/app/components/statement-card/statement-card.component.scss +134 -0
  24. package/src/app/components/statement-card/statement-card.component.spec.ts +23 -0
  25. package/src/app/components/statement-card/statement-card.component.ts +50 -0
  26. package/src/app/directives/custom-tooltip/custom-tooltip.directive.spec.ts +39 -0
  27. package/src/app/directives/custom-tooltip/custom-tooltip.directive.ts +89 -0
  28. package/src/app/models/report.model.ts +48 -0
  29. package/src/app/pages/report/report.component.html +363 -0
  30. package/src/app/pages/report/report.component.scss +600 -0
  31. package/src/app/pages/report/report.component.spec.ts +29 -0
  32. package/src/app/pages/report/report.component.ts +276 -0
  33. package/src/environments/environment.ts +5 -0
  34. package/src/index.html +17 -0
  35. package/src/main.server.ts +7 -0
  36. package/src/main.ts +5 -0
  37. package/src/style-vars.scss +40 -0
  38. package/src/styles.scss +23 -0
  39. package/tsconfig.app.json +19 -0
  40. package/tsconfig.json +32 -0
  41. package/tsconfig.spec.json +15 -0
@@ -0,0 +1,276 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import { Component, CUSTOM_ELEMENTS_SCHEMA, QueryList, ViewChildren } from '@angular/core';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { MatButtonModule } from '@angular/material/button';
5
+ import { MatButtonToggleModule } from '@angular/material/button-toggle';
6
+ import { MatDialog, MatDialogModule } from "@angular/material/dialog";
7
+ import { MatExpansionModule, MatExpansionPanel } from '@angular/material/expansion';
8
+ import { MatIconModule } from '@angular/material/icon';
9
+ import { MatSidenavModule } from '@angular/material/sidenav';
10
+ import { MatTooltipModule } from '@angular/material/tooltip';
11
+ import { MarkdownModule } from 'ngx-markdown';
12
+ import { DialogComponent } from '../../components/dialog/dialog.component';
13
+ import { SensemakingChartWrapperComponent } from '../../components/sensemaking-chart-wrapper/sensemaking-chart-wrapper.component';
14
+ import { StatementCardComponent } from '../../components/statement-card/statement-card.component';
15
+
16
+ import importedTopicData from "../../../../data/topic-stats.json";
17
+ import importedSummaryData from "../../../../data/summary.json";
18
+ import importedCommentData from "../../../../data/comments.json";
19
+ import importedReportMetadata from "../../../../data/metadata.json";
20
+
21
+ import {
22
+ VoteGroup,
23
+ Statement,
24
+ Subtopic,
25
+ Topic,
26
+ } from "../../models/report.model";
27
+
28
+ type AlignmentType = "high-alignment" | "low-alignment" | "high-uncertainty";
29
+
30
+ let totalVoteNumber = 0;
31
+
32
+ // use comment/topic(subtopic) relationship in comment data to add each comment's details to the topic configuration object
33
+ // also adding up each comment's votes to get total votes for report
34
+ importedCommentData.forEach((comment: Statement) => {
35
+ const commentTopics = comment.topics?.split(";");
36
+ commentTopics?.forEach((topicSubtopicString: string) => {
37
+ const [topic, subtopic] = topicSubtopicString.split(":");
38
+
39
+ // add comment to each of its subtopics in the topic data
40
+ const configTopic = importedTopicData.find((topicData: Topic) => topicData.name === topic);
41
+ const configSubtopic: Subtopic|undefined = configTopic?.subtopicStats?.find((subtopicData: Subtopic) => subtopicData.name === subtopic);
42
+ if(!configSubtopic) return;
43
+ if(!configSubtopic.comments) {
44
+ configSubtopic.comments = [comment];
45
+ } else {
46
+ configSubtopic.comments.push(comment);
47
+ }
48
+ });
49
+
50
+ // add comment votes to running vote total
51
+ const voteGroups = Object.values(comment.votes || {});
52
+ const commentVotes: number = voteGroups.reduce((commentAcc: number, group: VoteGroup) => {
53
+ return commentAcc + group.agreeCount + group.disagreeCount + group.passCount;
54
+ }, 0);
55
+ totalVoteNumber += commentVotes;
56
+ });
57
+
58
+ const allSubtopicIds: string[] = [];
59
+ importedTopicData.forEach((t: Topic) => {
60
+ t.subtopicStats.forEach((s: Subtopic) => {
61
+ const subtopicId = `${t.name}-${s.name}`;
62
+ s.id = subtopicId;
63
+ allSubtopicIds.push(subtopicId);
64
+ });
65
+ });
66
+
67
+ @Component({
68
+ selector: 'app-report',
69
+ standalone: true,
70
+ imports: [
71
+ CommonModule,
72
+ FormsModule,
73
+ MarkdownModule,
74
+ MatButtonModule,
75
+ MatButtonToggleModule,
76
+ MatDialogModule,
77
+ MatExpansionModule,
78
+ MatIconModule,
79
+ MatSidenavModule,
80
+ MatTooltipModule,
81
+ SensemakingChartWrapperComponent,
82
+ StatementCardComponent,
83
+ ],
84
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
85
+ templateUrl: './report.component.html',
86
+ styleUrl: './report.component.scss'
87
+ })
88
+ export class ReportComponent {
89
+ // data sources
90
+ topicData = importedTopicData as Topic[];
91
+ summaryData = importedSummaryData;
92
+ commentData = importedCommentData;
93
+ reportMetadata = importedReportMetadata;
94
+
95
+ reportTitle: string = this.reportMetadata.title || "Report";
96
+ selectedAlignmentType: AlignmentType = "high-alignment";
97
+ isStatementDrawerOpen = false;
98
+ drawerSubtopicName = "";
99
+ drawerSubtopicStatementNumber = 0;
100
+ drawerSubtopicStatementsHighAlignment: Statement[] = [];
101
+ drawerSubtopicStatementsLowAlignment: Statement[] = [];
102
+ drawerSubtopicStatementsHighUncertainty: Statement[] = [];
103
+ drawerSubtopicStatementsUncategorized: Statement[] = [];
104
+
105
+ topicAlignmentViews: { [key: string]: string } = {};
106
+ topicsDistributionView = 'cluster';
107
+
108
+ @ViewChildren("subtopicPanel") subtopicPanels!: QueryList<MatExpansionPanel>;
109
+
110
+ constructor(private dialog: MatDialog) {}
111
+
112
+ ngOnInit(): void {
113
+ // Initialize view states for each topic
114
+ this.topicData.forEach((topic: { name: string }) => {
115
+ this.topicAlignmentViews[topic.name] = 'solid';
116
+ });
117
+ }
118
+
119
+ updateTopicView(topicName: string, view: string): void {
120
+ this.topicAlignmentViews[topicName] = view;
121
+ }
122
+
123
+ openShareReportDialog({
124
+ elementId,
125
+ text,
126
+ title,
127
+ }: {
128
+ elementId?: string,
129
+ text: string,
130
+ title: string,
131
+ }) {
132
+ let link = window.location.origin + window.location.pathname;
133
+ if(elementId) {
134
+ link += `#${encodeURIComponent(elementId)}`;
135
+ }
136
+ this.dialog.open(DialogComponent, {
137
+ data: {
138
+ link,
139
+ text,
140
+ title,
141
+ }
142
+ });
143
+ }
144
+
145
+ scrollToElement(elementId?: string) {
146
+ const element = document.getElementById(elementId || "");
147
+ element?.scrollIntoView({ behavior: "smooth" });
148
+ }
149
+
150
+ // used by nav to open subtopic accordion panel
151
+ // scrolling to subtopic is handled by event listener responding to "expand" event
152
+ openSubtopicPanel(elementId?: string) {
153
+ const panelIndex = allSubtopicIds.indexOf(elementId || "");
154
+ this.subtopicPanels.toArray()[panelIndex]?.open();
155
+ }
156
+
157
+ // triggering scroll following opening of subtopic panel by 1) nav and by 2) direct accordion interaction
158
+ afterSubtopicPanelOpen(elementId?: string) {
159
+ this.scrollToElement(elementId);
160
+ }
161
+
162
+ topicNumber = this.topicData.length;
163
+ subtopicNumber = allSubtopicIds.length;
164
+ totalStatements = this.commentData.length;
165
+ totalVotes = totalVoteNumber;
166
+
167
+ get alignmentString() {
168
+ switch(this.selectedAlignmentType) {
169
+ case "high-alignment":
170
+ return "highest alignment";
171
+ case "low-alignment":
172
+ return "lowest alignment";
173
+ case "high-uncertainty":
174
+ return "highest uncertainty";
175
+ default:
176
+ return "";
177
+ }
178
+ }
179
+
180
+ getTopStatements(statements: Statement[], category: AlignmentType): Statement[] {
181
+ // no need to remove "isFilteredOut" statements first since "isFilteredOut" is exclusive to the 3 categories
182
+ switch(category) {
183
+ case "high-alignment":
184
+ return statements
185
+ .filter((statement: Statement) => statement.isHighAlignment)
186
+ .sort((a: Statement, b: Statement) => b.highAlignmentScore - a.highAlignmentScore)
187
+ .slice(0, 12);
188
+ case "low-alignment":
189
+ return statements
190
+ .filter((statement: Statement) => statement.isLowAlignment)
191
+ .sort((a: Statement, b: Statement) => b.lowAlignmentScore - a.lowAlignmentScore)
192
+ .slice(0, 12);
193
+ case "high-uncertainty":
194
+ return statements
195
+ .filter((statement: Statement) => statement.isHighUncertainty)
196
+ .sort((a: Statement, b: Statement) => b.highUncertaintyScore - a.highUncertaintyScore)
197
+ .slice(0, 12);
198
+ default:
199
+ return [];
200
+ }
201
+ }
202
+
203
+ get alignmentCards() {
204
+ return this.getTopStatements(this.commentData, this.selectedAlignmentType);
205
+ }
206
+
207
+ getTopicSummaryData(topicName: string): any {
208
+ const summaryTopicData: any = this.summaryData.contents.find(c => c.title.includes("Topics"));
209
+ const topicData = summaryTopicData?.subContents.find((s: any) => s.title.includes(topicName));
210
+ return topicData;
211
+ }
212
+
213
+ getSubtopicSummaryData(topicName: string, subtopicName: string): any {
214
+ const topicData = this.getTopicSummaryData(topicName);
215
+ const subtopicData = topicData?.subContents.find((s: any) => s.title.includes(subtopicName));
216
+ return subtopicData;
217
+ }
218
+
219
+ getSubtopicThemesData(topicName: string, subtopicName: string): any {
220
+ const subtopicData = this.getSubtopicSummaryData(topicName, subtopicName);
221
+ const subtopicThemesData = subtopicData?.subContents.find((s: any) => s.title.includes("themes"));
222
+ return subtopicThemesData;
223
+ }
224
+
225
+ getSubtopicThemesText(topicName: string, subtopicName: string): string {
226
+ const subtopicThemesData = this.getSubtopicThemesData(topicName, subtopicName);
227
+ return subtopicThemesData?.text || "";
228
+ }
229
+
230
+ getSubtopicStatements(topicName: string, subtopicName: string): Statement[] {
231
+ const topic: Topic|undefined = this.topicData.find(t => t.name === topicName);
232
+ const subtopic = topic?.subtopicStats.find((s: Subtopic) => s.name === subtopicName);
233
+ return subtopic?.comments || [];
234
+ }
235
+
236
+ getTopSubtopicStatements(topicName: string, subtopicName: string, category: AlignmentType): Statement[] {
237
+ const subtopicStatements = this.getSubtopicStatements(topicName, subtopicName);
238
+ return this.getTopStatements(subtopicStatements, category);
239
+ }
240
+
241
+ openStatementDrawer(subtopic: Subtopic) {
242
+ this.drawerSubtopicName = subtopic.name;
243
+ this.drawerSubtopicStatementNumber = subtopic.commentCount;
244
+ const highAlignmentStatements: Statement[] = [];
245
+ const lowAlignmentStatements: Statement[] = [];
246
+ const highUncertaintyStatements: Statement[] = [];
247
+ const uncategorizedStatements: Statement[] = [];
248
+ subtopic.comments?.forEach((statement: Statement) => {
249
+ if(statement.isHighAlignment) {
250
+ highAlignmentStatements.push(statement);
251
+ } else if(statement.isLowAlignment) {
252
+ lowAlignmentStatements.push(statement);
253
+ } else if(statement.isHighUncertainty) {
254
+ highUncertaintyStatements.push(statement);
255
+ } else {
256
+ uncategorizedStatements.push(statement);
257
+ }
258
+ });
259
+ this.drawerSubtopicStatementsHighAlignment = highAlignmentStatements.sort((a: Statement, b: Statement) => b.highAlignmentScore - a.highAlignmentScore);
260
+ this.drawerSubtopicStatementsLowAlignment = lowAlignmentStatements.sort((a: Statement, b: Statement) => b.lowAlignmentScore - a.lowAlignmentScore);
261
+ this.drawerSubtopicStatementsHighUncertainty = highUncertaintyStatements.sort((a: Statement, b: Statement) => b.highUncertaintyScore - a.highUncertaintyScore);
262
+ this.drawerSubtopicStatementsUncategorized = uncategorizedStatements.sort((a: Statement, b: Statement) => b.agreeRate - a.agreeRate);
263
+ this.isStatementDrawerOpen = true;
264
+ }
265
+
266
+ closeStatementDrawer() {
267
+ this.isStatementDrawerOpen = false;
268
+ this.drawerSubtopicName = "";
269
+ this.drawerSubtopicStatementNumber = 0;
270
+ // clearing statement arrays also ensures that scroll position is reset to top
271
+ this.drawerSubtopicStatementsHighAlignment = [];
272
+ this.drawerSubtopicStatementsLowAlignment = [];
273
+ this.drawerSubtopicStatementsHighUncertainty = [];
274
+ this.drawerSubtopicStatementsUncategorized = [];
275
+ }
276
+ }
@@ -0,0 +1,5 @@
1
+ // Default (development) env
2
+ export const environment = {
3
+ production: false,
4
+ apiUrl: 'http://localhost:3000',
5
+ };
package/src/index.html ADDED
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Client</title>
6
+ <base href="/">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
12
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
13
+ </head>
14
+ <body>
15
+ <app-root></app-root>
16
+ </body>
17
+ </html>
@@ -0,0 +1,7 @@
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { AppComponent } from './app/app.component';
3
+ import { config } from './app/app.config.server';
4
+
5
+ const bootstrap = () => bootstrapApplication(AppComponent, config);
6
+
7
+ export default bootstrap;
package/src/main.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { appConfig } from './app/app.config';
3
+ import { AppComponent } from './app/app.component';
4
+
5
+ bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));
@@ -0,0 +1,40 @@
1
+ $main-font: "Noto Sans", "Helvetica Neue", sans-serif;
2
+
3
+ // colors
4
+ $alto: #DEDEDE;
5
+ $black-squeeze: #F0F4F9;
6
+ $boulder: #757575;
7
+ $cape-cod: #444746;
8
+ $cape-cod-2: #3C4043;
9
+ $cloud-burst: #1E2656;
10
+ $concrete: #F2F2F2;
11
+ $congress-blue: #0842A0;
12
+ $gray-nurse: #E1E3E1;
13
+ $hawkes-blue: #D3E3FD;
14
+ $iron: #DADCE0;
15
+ $link-water: #DDDCF5;
16
+ $link-water-2: #F8FAFD;
17
+ $link-water-3: #EDEFFA;
18
+ $magic-mint: #A5EFBA;
19
+ $mine-shaft: #1F1F1F;
20
+ $mine-shaft-2: #303030;
21
+ $mystic: #DDE1EB;
22
+ $mystic-2: #DDE3EA;
23
+ $off-white: #FDFDFD;
24
+ $periwinkle-gray: #D1CFEC;
25
+ $science-blue: #0B57D0;
26
+ $selago: #E2EAFB;
27
+ $shark: #252526;
28
+ $shuttle-gray: #5F6368;
29
+ $silver: #CACACA;
30
+ $whisper: #ECEBF5;
31
+ $white: #FFF;
32
+ $white-lilac: #EDEFF9;
33
+ $your-pink: #FFBFBD;
34
+
35
+ @mixin truncateText($numberOfLines) {
36
+ display: -webkit-box;
37
+ -webkit-box-orient: vertical;
38
+ -webkit-line-clamp: $numberOfLines;
39
+ overflow: hidden;
40
+ }
@@ -0,0 +1,23 @@
1
+ /* You can add global styles to this file, and also import other style files */
2
+
3
+ @import "style-vars";
4
+
5
+ html, body {
6
+ height: 100%;
7
+ }
8
+
9
+ body {
10
+ color: #1F1F1F;
11
+ font-family: $main-font;
12
+ margin: 0;
13
+ }
14
+
15
+ *, *::before, *::after {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ h1, h2, h3, h4, h5, h6, p, ul {
20
+ color: #1F1F1F;
21
+ font-weight: 400;
22
+ margin: 0;
23
+ }
@@ -0,0 +1,19 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "./tsconfig.json",
5
+ "compilerOptions": {
6
+ "outDir": "./out-tsc/app",
7
+ "types": [
8
+ "node"
9
+ ]
10
+ },
11
+ "files": [
12
+ "src/main.ts",
13
+ "src/main.server.ts",
14
+ "server.ts"
15
+ ],
16
+ "include": [
17
+ "src/**/*.d.ts"
18
+ ]
19
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,32 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "compileOnSave": false,
5
+ "compilerOptions": {
6
+ "outDir": "./dist/out-tsc",
7
+ "strict": true,
8
+ "noImplicitOverride": true,
9
+ "noPropertyAccessFromIndexSignature": true,
10
+ "noImplicitReturns": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "skipLibCheck": true,
13
+ "esModuleInterop": true,
14
+ "sourceMap": true,
15
+ "declaration": false,
16
+ "experimentalDecorators": true,
17
+ "moduleResolution": "bundler",
18
+ "importHelpers": true,
19
+ "target": "ES2022",
20
+ "module": "ES2022",
21
+ "lib": [
22
+ "ES2022",
23
+ "dom"
24
+ ]
25
+ },
26
+ "angularCompilerOptions": {
27
+ "enableI18nLegacyMessageIdFormat": false,
28
+ "strictInjectionParameters": true,
29
+ "strictInputAccessModifiers": true,
30
+ "strictTemplates": true
31
+ }
32
+ }
@@ -0,0 +1,15 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "./tsconfig.json",
5
+ "compilerOptions": {
6
+ "outDir": "./out-tsc/spec",
7
+ "types": [
8
+ "jasmine"
9
+ ]
10
+ },
11
+ "include": [
12
+ "src/**/*.spec.ts",
13
+ "src/**/*.d.ts"
14
+ ]
15
+ }