From 28ba67242fc0509dde1aa64b60699fdee9d12b58 Mon Sep 17 00:00:00 2001 From: Matt Yoder Date: Wed, 21 Aug 2024 14:17:37 -0400 Subject: [PATCH] feat(resource-group-projects): add charts for project statistics --- .../api/resource-groups/1/projects/all.json | 70 ++++++++ .../resource-groups/1/projects/educator.json | 30 ++++ .../1/projects/grad-student.json | 38 +++++ .../1/projects/researcher.json | 66 ++++++++ src/base.css | 1 + src/donut-chart.css | 39 +++++ src/donut-chart.jsx | 155 ++++++++++++++++++ src/resource-catalog.jsx | 4 + src/resource-group-affinity-group.jsx | 28 ++-- src/resource-group-detail.jsx | 5 + src/resource-group-projects.css | 34 ++++ src/resource-group-projects.jsx | 59 +++++++ 12 files changed, 514 insertions(+), 15 deletions(-) create mode 100644 public/api/resource-groups/1/projects/all.json create mode 100644 public/api/resource-groups/1/projects/educator.json create mode 100644 public/api/resource-groups/1/projects/grad-student.json create mode 100644 public/api/resource-groups/1/projects/researcher.json create mode 100644 src/donut-chart.css create mode 100644 src/donut-chart.jsx create mode 100644 src/resource-group-projects.css create mode 100644 src/resource-group-projects.jsx diff --git a/public/api/resource-groups/1/projects/all.json b/public/api/resource-groups/1/projects/all.json new file mode 100644 index 0000000..723b16b --- /dev/null +++ b/public/api/resource-groups/1/projects/all.json @@ -0,0 +1,70 @@ +{ + "projects": { + "projectType": [ + { + "name": "Explore ACCESS", + "count": 354 + }, + { + "name": "Discover ACCESS", + "count": 191 + }, + { + "name": "Accelerate ACCESS", + "count": 87 + }, + { + "name": "Maximize ACCESS", + "count": 14 + } + ], + "fieldOfScience": [ + { + "name": "Materials Engineering", + "count": 157 + }, + { + "name": "Physical Chemistry", + "count": 143 + }, + { + "name": "Applied Computer Science", + "count": 126 + }, + { + "name": "Health Sciences", + "count": 114 + }, + { + "name": "Ecology", + "count": 90 + }, + { + "name": "Training", + "count": 16 + } + ], + "institution": [ + { + "name": "Carnegie Mellon University", + "count": 321 + }, + { + "name": "University of Pittsburgh", + "count": 190 + }, + { + "name": "University of Somewhere", + "count": 110 + }, + { + "name": "Another University", + "count": 16 + }, + { + "name": "Computing College", + "count": 9 + } + ] + } +} diff --git a/public/api/resource-groups/1/projects/educator.json b/public/api/resource-groups/1/projects/educator.json new file mode 100644 index 0000000..e259095 --- /dev/null +++ b/public/api/resource-groups/1/projects/educator.json @@ -0,0 +1,30 @@ +{ + "projects": { + "projectType": [ + { + "name": "Explore ACCESS", + "count": 16 + } + ], + "fieldOfScience": [ + { + "name": "Applied Computer Science", + "count": 10 + }, + { + "name": "Training", + "count": 6 + } + ], + "institution": [ + { + "name": "Carnegie Mellon University", + "count": 8 + }, + { + "name": "University of Pittsburgh", + "count": 8 + } + ] + } +} diff --git a/public/api/resource-groups/1/projects/grad-student.json b/public/api/resource-groups/1/projects/grad-student.json new file mode 100644 index 0000000..2aaf25a --- /dev/null +++ b/public/api/resource-groups/1/projects/grad-student.json @@ -0,0 +1,38 @@ +{ + "projects": { + "projectType": [ + { + "name": "Explore ACCESS", + "count": 52 + } + ], + "fieldOfScience": [ + { + "name": "Materials Engineering", + "count": 21 + }, + { + "name": "Physical Chemistry", + "count": 12 + }, + { + "name": "Applied Computer Science", + "count": 19 + } + ], + "institution": [ + { + "name": "University of Somewhere", + "count": 26 + }, + { + "name": "Another University", + "count": 22 + }, + { + "name": "Computing College", + "count": 4 + } + ] + } +} diff --git a/public/api/resource-groups/1/projects/researcher.json b/public/api/resource-groups/1/projects/researcher.json new file mode 100644 index 0000000..07e8ed1 --- /dev/null +++ b/public/api/resource-groups/1/projects/researcher.json @@ -0,0 +1,66 @@ +{ + "projects": { + "projectType": [ + { + "name": "Explore ACCESS", + "count": 264 + }, + { + "name": "Discover ACCESS", + "count": 36 + }, + { + "name": "Accelerate ACCESS", + "count": 9 + }, + { + "name": "Maximize ACCESS", + "count": 14 + } + ], + "fieldOfScience": [ + { + "name": "Materials Engineering", + "count": 99 + }, + { + "name": "Physical Chemistry", + "count": 67 + }, + { + "name": "Applied Computer Science", + "count": 66 + }, + { + "name": "Health Sciences", + "count": 49 + }, + { + "name": "Ecology", + "count": 42 + } + ], + "institution": [ + { + "name": "Carnegie Mellon University", + "count": 187 + }, + { + "name": "University of Pittsburgh", + "count": 65 + }, + { + "name": "University of Somewhere", + "count": 26 + }, + { + "name": "Another University", + "count": 36 + }, + { + "name": "Computing College", + "count": 9 + } + ] + } +} diff --git a/src/base.css b/src/base.css index b49d10d..8c59442 100644 --- a/src/base.css +++ b/src/base.css @@ -6,6 +6,7 @@ --contrast-6: #999999; --contrast-8: #dfdfdf; --contrast-9: #f2f2f2; + --green-400: #288654; --red-400: #a70000; --orange-200: #f6d8ca; --orange-400: #f07537; diff --git a/src/donut-chart.css b/src/donut-chart.css new file mode 100644 index 0000000..ab23e90 --- /dev/null +++ b/src/donut-chart.css @@ -0,0 +1,39 @@ +.donut-chart svg path { + cursor: pointer; +} +.donut-chart .center-text { + align-items: center; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + line-height: 1.2; +} +.donut-chart .center-text * { + cursor: default; + display: block; + margin-bottom: 0.25rem; + text-align: center; +} +.donut-chart .center-text .percent { + color: var(--contrast-2); + font-size: 0.7rem; +} +.donut-chart .top-items { + list-style-type: none; + margin: 0; + padding: 0; +} +.donut-chart .top-items li { + display: flex; + flex-direction: row; + margin-bottom: 5px; +} +.donut-chart .symbol { + border-radius: 8px; + display: inline-block; + height: 16px; + margin-right: 5px; + vertical-align: text-top; + width: 16px; +} diff --git a/src/donut-chart.jsx b/src/donut-chart.jsx new file mode 100644 index 0000000..190d2ca --- /dev/null +++ b/src/donut-chart.jsx @@ -0,0 +1,155 @@ +import { useState } from "preact/hooks"; + +const circleCoords = (pct, radius) => + `${Math.cos(2 * Math.PI * pct) * radius} ${ + Math.sin(2 * Math.PI * pct) * radius + }`; + +const formatItem = ( + { name, count }, + total = null, + itemLabel, + separator = "", + noBreak = false +) => { + return ( + <> + + {name} + {separator} + {" "} + + {count.toLocaleString()} + {noBreak ? <>  : " "} + {itemLabel} + + {total !== null ? ( + <> + {" "} + + ( + {((count * 100) / total).toLocaleString("en-US", { + maximumFractionDigits: 1, + })} + %) + + + ) : null} + + ); +}; + +export default function DonutChart({ + colors = [ + "var(--teal-700)", + "var(--yellow-400)", + "var(--teal-400)", + "var(--orange-400)", + "var(--green-400)", + "var(--red-400)", + ], + title, + itemLabel = "", + items, + topItems = 3, + topItemsHeading = null, +}) { + const [activeIdx, setActiveIdx] = useState(null); + const sortedItems = items.toSorted((a, b) => + a.count < b.count || (a.count == b.count && a.name < b.name) ? 1 : -1 + ); + const total = items + .map(({ count }) => count) + .reduce((result, count) => result + count, 0); + let cumulativePct = -0.25; + + const svgPaths = sortedItems.map(({ name, count }, i) => { + const pct = count / total; + if (pct == 0) return null; + const startPct = cumulativePct; + const endPct = (cumulativePct += pct); + const lgArc = pct > 0.5 ? 1 : 0; + const pathData = [ + `M ${circleCoords(startPct, 100)}`, + `A 100 100 0 ${lgArc} 1 ${circleCoords(endPct, 100)}`, + `L ${circleCoords(endPct, 75)}`, + `A 75 75 0 ${lgArc} 0 ${circleCoords(startPct, 75)}`, + `L ${circleCoords(startPct, 100)}`, + ].join(" "); + return ( + { + e.stopPropagation(); + setActiveIdx(activeIdx === i ? null : i); + }} + /> + ); + }); + const activeItem = activeIdx === null ? null : items[activeIdx]; + const labelContent = + activeItem === null ? ( + <> + {title}{" "} + + {total.toLocaleString()} {itemLabel} + + + ) : ( + <> + {activeItem.name}{" "} + + {activeItem.count.toLocaleString()} {itemLabel} + + + ( + {((activeItem.count * 100) / total).toLocaleString("en-US", { + maximumFractionDigits: 1, + })} + %) + + + ); + const svgLabel = ( + +
{labelContent}
+
+ ); + + return ( +
+ setActiveIdx(null)} + > + {svgPaths} + {svgLabel} + + {topItems > 0 ? ( + <> +

{topItemsHeading || `Top ${title}s`}

+
    + {sortedItems.slice(0, topItems).map((item, i) => ( +
  • + + {formatItem(item, null, itemLabel, ":", true)} +
  • + ))} +
+ + ) : null} +
+ ); +} diff --git a/src/resource-catalog.jsx b/src/resource-catalog.jsx index 7a9c4df..1341277 100644 --- a/src/resource-catalog.jsx +++ b/src/resource-catalog.jsx @@ -4,6 +4,7 @@ import alertStyle from "./alert.css?inline"; import baseStyle from "./base.css?inline"; import componentsStyle from "./components.css?inline"; import contentStyle from "./content.css?inline"; +import donutChartStyle from "./donut-chart.css?inline"; import iconStyle from "./icon.css?inline"; import infoTipStyle from "./info-tip.css?inline"; import glideCoreStyle from "@glidejs/glide/dist/css/glide.core.min.css?inline"; @@ -13,6 +14,7 @@ import carouselStyle from "./carousel.css?inline"; import resourceFiltersStyle from "./resource-filters.css?inline"; import resourceGroupStyle from "./resource-group.css?inline"; import resourceGroupEventStyle from "./resource-group-event.css?inline"; +import resourceGroupProjectsStyle from "./resource-group-projects.css?inline"; import searchStyle from "./search.css?inline"; import sectionNavigationStyle from "./section-navigation.css?inline"; import tagsStyle from "./tags.css?inline"; @@ -53,6 +55,7 @@ export function ResourceCatalog({ + @@ -62,6 +65,7 @@ export function ResourceCatalog({ + diff --git a/src/resource-group-affinity-group.jsx b/src/resource-group-affinity-group.jsx index 9ee0f28..7d3e9d0 100644 --- a/src/resource-group-affinity-group.jsx +++ b/src/resource-group-affinity-group.jsx @@ -6,21 +6,19 @@ export default function ResourceGroupAffinityGroup({ }) { return (
- <> -

- Join the community! Members get update about announcements, events, - and outages. -

- - Join - - - Slack - - - Q&A - - +

+ Join the community! Members get update about announcements, events, and + outages. +

+ + Join + + + Slack + + + Q&A +
); } diff --git a/src/resource-group-detail.jsx b/src/resource-group-detail.jsx index e03f7b3..2a0a19c 100644 --- a/src/resource-group-detail.jsx +++ b/src/resource-group-detail.jsx @@ -1,6 +1,7 @@ import ResourceGroupAffinityGroup from "./resource-group-affinity-group"; import ResourceGroupDescription from "./resource-group-description"; import ResourceGroupEvents from "./resource-group-events"; +import ResourceGroupProjects from "./resource-group-projects"; import ResourceGroupQueueMetrics from "./resource-group-queue-metrics"; import ResourceGroupResources from "./resource-group-resources"; import ResourceGroupSoftware from "./resource-group-software"; @@ -30,6 +31,10 @@ export default function ResourceGroupDetail({ baseUri, resourceGroupId }) { baseUri={baseUri} resourceGroupId={resourceGroupId} /> + { + return ( +
+ +
+ ); +}; + +export default function ResourceGroupProjects({ baseUri, resourceGroupId }) { + const [persona, setPersona] = useState("all"); + const data = useJSON( + `${baseUri}/api/resource-groups/${resourceGroupId}/projects/${persona}.json`, + null + ); + + if (!data || data.error) return; + + const headerComponents = [ + , + ]; + + return ( +
+
+ {column(data.projects.projectType, "Project Type")} + {column( + data.projects.fieldOfScience, + "Field of Science", + "Top Fields of Science" + )} + {column( + data.projects.institution, + "PI's Institution", + "Top Institutions" + )} +
+
+ ); +}