From 7a5c3e04f9b18a5a6274386f82e1252d7a15b8e0 Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Mon, 31 Jul 2023 16:30:21 -0500 Subject: [PATCH 1/4] Add Project Aggregate Charts --- assets/js/components/pie-chart.js | 146 ++++++++++++++++++++++++++++++ assets/js/project-page-v1.js | 54 +++++++++-- projects.html | 23 +++++ 3 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 assets/js/components/pie-chart.js diff --git a/assets/js/components/pie-chart.js b/assets/js/components/pie-chart.js new file mode 100644 index 000000000..94f3c7c82 --- /dev/null +++ b/assets/js/components/pie-chart.js @@ -0,0 +1,146 @@ +import {Chart, registerables} from 'https://cdn.jsdelivr.net/npm/chart.js@4.3.2/+esm' + +Chart.register(...registerables); + +const getOrCreateLegendList = (chart, id) => { + const legendContainer = document.getElementById(id); + let listContainer = legendContainer.querySelector('ul'); + + if (!listContainer) { + listContainer = document.createElement('ul'); + listContainer.style.display = 'flex'; + listContainer.style.flexDirection = 'column'; + listContainer.style.margin = 0; + listContainer.style.padding = 0; + listContainer.style.height = `${chart.canvas.height}px`; + listContainer.style.overflowY = 'auto'; + + legendContainer.appendChild(listContainer); + } + + return listContainer; +}; + +const htmlLegendPlugin = { + id: 'htmlLegend', + afterUpdate(chart, args, options) { + const ul = getOrCreateLegendList(chart, options.containerID); + + // Remove old legend items + while (ul.firstChild) { + ul.firstChild.remove(); + } + + // Reuse the built-in legendItems generator + const items = chart.options.plugins.legend.labels.generateLabels(chart); + + items.forEach(item => { + const li = document.createElement('li'); + li.style.alignItems = 'center'; + li.style.cursor = 'pointer'; + li.style.display = 'flex'; + li.style.flexDirection = 'row'; + li.style.marginLeft = '10px'; + li.style.paddingBottom = '2px' + + li.onclick = () => { + const {type} = chart.config; + if (type === 'pie' || type === 'doughnut') { + // Pie and doughnut charts only have a single dataset and visibility is per item + chart.toggleDataVisibility(item.index); + } else { + chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); + } + chart.update(); + }; + + // Color box + const boxSpan = document.createElement('span'); + boxSpan.style.background = item.fillStyle; + boxSpan.style.borderColor = item.strokeStyle; + boxSpan.style.borderWidth = item.lineWidth + 'px'; + boxSpan.style.display = 'inline-block'; + boxSpan.style.flexShrink = 0; + boxSpan.style.height = '20px'; + boxSpan.style.marginRight = '10px'; + boxSpan.style.width = '20px'; + + // Text + const textContainer = document.createElement('p'); + textContainer.style.color = item.fontColor; + textContainer.style.margin = 0; + textContainer.style.padding = 0; + textContainer.style.fontSize = ".8rem" + textContainer.style.textDecoration = item.hidden ? 'line-through' : ''; + + const text = document.createTextNode(item.text); + textContainer.appendChild(text); + + li.appendChild(boxSpan); + li.appendChild(textContainer); + ul.appendChild(li); + + ul.style.height = `${chart.canvas.style.height}`; + }); + } +}; + +export class PieChart { + constructor(id, data) { + this.id = id + this.data = data + + this.createHtmlElements() + + this.createGraph() + } + + createHtmlElements() { + + let parent = document.getElementById(this.id); + parent.className = 'row gx-0'; + + this.canvasContainer = document.createElement('div'); + this.canvasContainer.className = "col-md-8 col-12" + + this.canvas = document.createElement('canvas'); + this.canvas.id = `canvas-${this.id}`; + + this.canvasContainer.appendChild(this.canvas); + + this.legend = document.createElement('div'); + this.legend.id = `legend-${this.id}`; + this.legend.className = "col-md-4 col-12" + + + parent.appendChild(this.canvasContainer); + parent.appendChild(this.legend); + } + + async createGraph() { + new Chart(this.canvas, { + type: 'pie', + data: { + labels: this.data['labels'], + datasets: [{ + label: '# of Jobs', + data: this.data['data'], + borderWidth: 1 + }] + }, + options: { + responsive: true, + plugins: { + htmlLegend: { + // ID of the container to put the legend in + containerID: `legend-${this.id}` + }, + legend: { + display: false + } + } + }, + plugins: [htmlLegendPlugin], + }); + } +} \ No newline at end of file diff --git a/assets/js/project-page-v1.js b/assets/js/project-page-v1.js index 331c72c99..c79bbfb17 100644 --- a/assets/js/project-page-v1.js +++ b/assets/js/project-page-v1.js @@ -4,6 +4,8 @@ import ElasticSearchQuery, {ENDPOINT, DATE_RANGE, SUMMARY_INDEX, OSPOOL_FILTER} from "./elasticsearch.js"; import {GraccDisplay, locale_int_string_sort, string_sort, hideNode} from "./util.js"; +import {PieChart} from "./components/pie-chart.js"; + function makeDelay(ms) { let timer = 0; @@ -13,6 +15,8 @@ function makeDelay(ms) { }; } + + /** * A suite of Boolean functions deciding the visual status of a certain grafana graph * @@ -78,7 +82,9 @@ class UsageToggles { this.usage = projectBuckets.reduce((p, v) => { p[v['key']] = { + cpuHours: v['projectCpuUse']['value'], cpu: v['projectCpuUse']['value'] != 0, + gpuHours: v['projectGpuUse']['value'], gpu: v['projectGpuUse']['value'] != 0, jobs: v['projectJobsRan']['value'] } @@ -112,6 +118,8 @@ const GRAFANA_BASE = { to: DATE_RANGE['now'] } + + /** * A node wrapping the project information break down */ @@ -349,13 +357,18 @@ class DataManager { this.toggleConsumers() } + getData = async () => { + if(!this.data) { + this.data = this._getData() + } + return this.data + } + /** * Compiles the project data and does some prefilters to dump unwanted data * @returns {Promise<*>} */ - getData = async () => { - - if( this.data ){ return this.data } + _getData = async () => { let response; @@ -383,7 +396,7 @@ class DataManager { return p }, {}) - console.log(JSON.stringify(Object.keys(this.data))) + console.log(this.data) return this.data } @@ -399,6 +412,26 @@ class DataManager { } return filteredData } + + reduceByKey = async (key) => { + let data = await this.getData() + let reducedData = Object.values(data).reduce((p, v) => { + if(v[key] in p) { + p[v[key]] += v['jobs'] + } else { + p[v[key]] = v['jobs'] + } + return p + }, {}) + let sortedData = Object.entries(reducedData) + .map(([k,v]) => {return {label: k, jobs: v}}) + .sort((a, b) => b.jobs - a.jobs) + return { + labels: sortedData.map(x => x.label), + data: sortedData.map(x => x.jobs) + } + } + } class ProjectPage{ @@ -416,6 +449,15 @@ class ProjectPage{ this.mode = undefined this.dataManager = new DataManager() + new PieChart( + "project-institution-summary", + (await this.dataManager.reduceByKey("Organization")) + ) + new PieChart( + "project-fos-summary", + (await this.dataManager.reduceByKey("FieldOfScience")) + ) + let projectDisplayNode = document.getElementById("project-display") this.projectDisplay = new ProjectDisplay(projectDisplayNode) @@ -436,12 +478,12 @@ class ProjectPage{ } } + + minimumJobsFilter = (data) => { return Object.entries(data).reduce((pv, [k,v]) => { if(v['jobs'] >= 100){ pv[k] = v - } else { - console.log(k) } return pv }, {}) diff --git a/projects.html b/projects.html index 77aa50648..11101040f 100644 --- a/projects.html +++ b/projects.html @@ -41,6 +41,29 @@ The below projects used OSPool resources to advance their research in the past year and ran more than 100 jobs. To run your own research on the OSPool sign up now on the OSG Portal.

+ + + +
+
+
+

Project Aggregates

+
+
+

Project Institutions

+
+
+
+

Project Fields of Science

+
+
+
+
+
+
+
+ +

By Project

Click on a row to view project details and their resource usage.

From ad101c0308cfdf2ec2a80b0438f8b57afb4eacab Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Mon, 31 Jul 2023 20:35:51 -0500 Subject: [PATCH 2/4] Have search bar effect the aggregates --- assets/js/components/pie-chart.js | 18 ++++++++++++++---- assets/js/project-page-v1.js | 18 +++++++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/assets/js/components/pie-chart.js b/assets/js/components/pie-chart.js index 94f3c7c82..d1bf0773e 100644 --- a/assets/js/components/pie-chart.js +++ b/assets/js/components/pie-chart.js @@ -86,15 +86,25 @@ const htmlLegendPlugin = { }; export class PieChart { - constructor(id, data) { + constructor(id, dataGetter) { this.id = id - this.data = data + this.dataGetter = dataGetter + this.initialize() + } + async initialize() { + this.data = await this.dataGetter() this.createHtmlElements() - this.createGraph() } + update = async () => { + this.data = await this.dataGetter() + this.chart.data.labels = this.data['labels'] + this.chart.data.datasets[0].data = this.data['data'] + this.chart.update() + } + createHtmlElements() { let parent = document.getElementById(this.id); @@ -118,7 +128,7 @@ export class PieChart { } async createGraph() { - new Chart(this.canvas, { + this.chart = new Chart(this.canvas, { type: 'pie', data: { labels: this.data['labels'], diff --git a/assets/js/project-page-v1.js b/assets/js/project-page-v1.js index c79bbfb17..7f752a23e 100644 --- a/assets/js/project-page-v1.js +++ b/assets/js/project-page-v1.js @@ -245,7 +245,8 @@ class Search { if(this.node.value == ""){ return data } else { - let table_keys = this.lunr_idx.search("*" + this.node.value + "*").map(r => r.ref) + console.log(this.node.value) + let table_keys = this.lunr_idx.search("" + this.node.value + "~2").map(r => r.ref) return table_keys.reduce((pv, k) => { pv[k] = data[k] return pv @@ -414,7 +415,7 @@ class DataManager { } reduceByKey = async (key) => { - let data = await this.getData() + let data = await this.getFilteredData() let reducedData = Object.values(data).reduce((p, v) => { if(v[key] in p) { p[v[key]] += v['jobs'] @@ -449,15 +450,18 @@ class ProjectPage{ this.mode = undefined this.dataManager = new DataManager() - new PieChart( + this.orgPieChart = new PieChart( "project-institution-summary", - (await this.dataManager.reduceByKey("Organization")) + this.dataManager.reduceByKey.bind(this.dataManager, "Organization") ) - new PieChart( + this.FosPieChart = new PieChart( "project-fos-summary", - (await this.dataManager.reduceByKey("FieldOfScience")) + this.dataManager.reduceByKey.bind(this.dataManager, "FieldOfScience") ) + this.dataManager.consumerToggles.push(this.orgPieChart.update) + this.dataManager.consumerToggles.push(this.FosPieChart.update) + let projectDisplayNode = document.getElementById("project-display") this.projectDisplay = new ProjectDisplay(projectDisplayNode) @@ -465,7 +469,7 @@ class ProjectPage{ this.table = new Table(this.wrapper, this.dataManager.getFilteredData, this.projectDisplay.update.bind(this.projectDisplay)) this.dataManager.consumerToggles.push(this.table.update) - this.search = new Search(Object.values(await this.dataManager.getData()), this.table.update) + this.search = new Search(Object.values(await this.dataManager.getData()), this.dataManager.toggleConsumers) this.dataManager.addFilter("search", this.search.filter) this.dataManager.addFilter("minimumJobsFilter", this.minimumJobsFilter) From 380a3ef48c2ee0347fc6078a2fb8be8c21535dd9 Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Fri, 2 Feb 2024 13:13:11 -0600 Subject: [PATCH 3/4] Add more graphs - Loading icons - Data Updated Time - Open the project on click --- assets/js/components/pie-chart.js | 53 ++++++++++---- assets/js/project-page-v1.js | 83 ++++++++++++++++----- projects.html | 116 ++++++++++++++++++++++++++---- 3 files changed, 209 insertions(+), 43 deletions(-) diff --git a/assets/js/components/pie-chart.js b/assets/js/components/pie-chart.js index d1bf0773e..ae7ca2f52 100644 --- a/assets/js/components/pie-chart.js +++ b/assets/js/components/pie-chart.js @@ -44,14 +44,19 @@ const htmlLegendPlugin = { li.style.paddingBottom = '2px' li.onclick = () => { - const {type} = chart.config; - if (type === 'pie' || type === 'doughnut') { - // Pie and doughnut charts only have a single dataset and visibility is per item - chart.toggleDataVisibility(item.index); + + if(options.onClick) { + options.onClick({label: item.text, value: null}) } else { - chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); + const {type} = chart.config; + if (type === 'pie' || type === 'doughnut') { + // Pie and doughnut charts only have a single dataset and visibility is per item + chart.toggleDataVisibility(item.index); + } else { + chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); + } + chart.update(); } - chart.update(); }; // Color box @@ -67,7 +72,7 @@ const htmlLegendPlugin = { // Text const textContainer = document.createElement('p'); - textContainer.style.color = item.fontColor; + textContainer.style.color = "white"; textContainer.style.margin = 0; textContainer.style.padding = 0; textContainer.style.fontSize = ".8rem" @@ -86,9 +91,11 @@ const htmlLegendPlugin = { }; export class PieChart { - constructor(id, dataGetter) { + constructor(id, dataGetter, label, onClick = null) { this.id = id this.dataGetter = dataGetter + this.label = label + this.onClick = onClick this.initialize() } @@ -116,13 +123,16 @@ export class PieChart { this.canvas = document.createElement('canvas'); this.canvas.id = `canvas-${this.id}`; - this.canvasContainer.appendChild(this.canvas); - this.legend = document.createElement('div'); this.legend.id = `legend-${this.id}`; this.legend.className = "col-md-4 col-12" + // Empty the children out of the parent element + while (parent.firstChild) { + parent.removeChild(parent.firstChild); + } + this.canvasContainer.appendChild(this.canvas); parent.appendChild(this.canvasContainer); parent.appendChild(this.legend); } @@ -133,7 +143,7 @@ export class PieChart { data: { labels: this.data['labels'], datasets: [{ - label: '# of Jobs', + label: this.label, data: this.data['data'], borderWidth: 1 }] @@ -143,7 +153,8 @@ export class PieChart { plugins: { htmlLegend: { // ID of the container to put the legend in - containerID: `legend-${this.id}` + containerID: `legend-${this.id}`, + onClick: this.onClick }, legend: { display: false @@ -152,5 +163,23 @@ export class PieChart { }, plugins: [htmlLegendPlugin], }); + + // Add a click event to the chart + if(this.onClick) { + this.canvas.onclick = this.onGraphClick.bind(this) + } + } + + async onGraphClick(e) { + const points = this.chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, true); + if (points.length) { + const firstPoint = points[0]; + const label = this.chart.data.labels[firstPoint.index]; + const value = this.chart.data.datasets[firstPoint.datasetIndex].data[firstPoint.index]; + + console.log({label: label, value: value}) + + this.onClick({label: label, value: value}) + } } } \ No newline at end of file diff --git a/assets/js/project-page-v1.js b/assets/js/project-page-v1.js index 7f752a23e..b063306d3 100644 --- a/assets/js/project-page-v1.js +++ b/assets/js/project-page-v1.js @@ -215,6 +215,20 @@ class ProjectDisplay{ } } +class ProjectCount { + constructor(dataGetter, node) { + this.node = node + this.dataGetter = dataGetter + this.update() + } + + update = async () => { + let data = await this.dataGetter() + this.node.textContent = Object.keys(data).length + console.log(Object.keys(data).length) + } +} + class Search { constructor(data, listener) { this.node = document.getElementById("project-search") @@ -414,22 +428,23 @@ class DataManager { return filteredData } - reduceByKey = async (key) => { + reduceByKey = async (key, value) => { let data = await this.getFilteredData() let reducedData = Object.values(data).reduce((p, v) => { if(v[key] in p) { - p[v[key]] += v['jobs'] + p[v[key]] += v[value] } else { - p[v[key]] = v['jobs'] + p[v[key]] = v[value] } return p }, {}) let sortedData = Object.entries(reducedData) - .map(([k,v]) => {return {label: k, jobs: v}}) - .sort((a, b) => b.jobs - a.jobs) + .filter(([k,v]) => v > 0) + .map(([k,v]) => {return {label: k, [value]: Math.round(v)}}) + .sort((a, b) => b[value] - a[value]) return { labels: sortedData.map(x => x.label), - data: sortedData.map(x => x.jobs) + data: sortedData.map(x => x[value]) } } @@ -450,18 +465,6 @@ class ProjectPage{ this.mode = undefined this.dataManager = new DataManager() - this.orgPieChart = new PieChart( - "project-institution-summary", - this.dataManager.reduceByKey.bind(this.dataManager, "Organization") - ) - this.FosPieChart = new PieChart( - "project-fos-summary", - this.dataManager.reduceByKey.bind(this.dataManager, "FieldOfScience") - ) - - this.dataManager.consumerToggles.push(this.orgPieChart.update) - this.dataManager.consumerToggles.push(this.FosPieChart.update) - let projectDisplayNode = document.getElementById("project-display") this.projectDisplay = new ProjectDisplay(projectDisplayNode) @@ -476,10 +479,54 @@ class ProjectPage{ this.toggleActiveFilterButton = document.getElementById("toggle-active-filter") this.toggleActiveFilterButton.addEventListener("click", this.toggleActiveFilter) + this.projectCount = new ProjectCount(this.dataManager.getFilteredData, document.getElementById("project-count")) + let urlProject = new URLSearchParams(window.location.search).get('project') if(urlProject){ this.projectDisplay.update((await this.dataManager.getData())[urlProject]) } + + this.orgPieChart = new PieChart( + "project-institution-summary", + this.dataManager.reduceByKey.bind(this.dataManager, "Organization", "jobs"), + "# of Jobs by Institution" + ) + this.FosPieChart = new PieChart( + "project-fos-summary", + this.dataManager.reduceByKey.bind(this.dataManager, "FieldOfScience", "jobs"), + "# of Jobs by Field Of Science" + ) + this.jobPieChart = new PieChart( + "project-job-summary", + this.dataManager.reduceByKey.bind(this.dataManager, "Name", "jobs"), + "# of Jobs by Project", + ({label, value}) => { + this.table.updateProjectDisplay(this.dataManager.data[label]) + } + ) + this.cpuPieChart = new PieChart( + "project-cpu-summary", + this.dataManager.reduceByKey.bind(this.dataManager, "Name", "cpuHours"), + "# of CPU Hours by Project", + ({label, value}) => { + this.table.updateProjectDisplay(this.dataManager.data[label]) + } + ) + this.gpuPieChart = new PieChart( + "project-gpu-summary", + this.dataManager.reduceByKey.bind(this.dataManager, "Name", "gpuHours"), + "# of GPU Hours by Project", + ({label, value}) => { + this.table.updateProjectDisplay(this.dataManager.data[label]) + } + ) + + this.dataManager.consumerToggles.push(this.orgPieChart.update) + this.dataManager.consumerToggles.push(this.FosPieChart.update) + this.dataManager.consumerToggles.push(this.jobPieChart.update) + this.dataManager.consumerToggles.push(this.cpuPieChart.update) + this.dataManager.consumerToggles.push(this.gpuPieChart.update) + this.dataManager.consumerToggles.push(this.projectCount.update) } diff --git a/projects.html b/projects.html index 11101040f..34aa85c7a 100644 --- a/projects.html +++ b/projects.html @@ -35,7 +35,13 @@
{% include layout/title.html %} -
+
+
Data updated:
+ +
+

The below projects used OSPool resources to advance their research in the past year and ran more than 100 jobs. @@ -44,18 +50,101 @@

-
-
-
-

Project Aggregates

-
-
-

Project Institutions

-
+
+
+
+
+

Project Overview

+
+
+

+ By Jobs + + + + + + + +

+
+
+ Loading... +
+
+
+
+

+ By CPU Hours + + + + + + + +

+
+
+ Loading... +
+
+
+
+

+ By GPU Hours + + + + + + + +

+
+
+ Loading... +
+
+
-
-

Project Fields of Science

-
+
+
+

Job Aggregates

+
+
+

+ By Project Institutions + + + + + + +

+
+
+ Loading... +
+
+
+
+

+ By Project Fields of Science + + + + + + + +

+
+
+ Loading... +
+
+
@@ -64,7 +153,8 @@

Project Fields of Science

By Project

-

+

X Projects
+

Click on a row to view project details and their resource usage.

From 1422ec23bbadc4b80179466b9cbb4099c765e90f Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Wed, 7 Feb 2024 12:58:55 -0600 Subject: [PATCH 4/4] Update the projects page --- assets/js/components/pie-chart.js | 48 +++++++++++++++++++++++++++++-- assets/js/project-page-v2.js | 8 +++--- projects.html | 39 ++++++++++++------------- 3 files changed, 68 insertions(+), 27 deletions(-) diff --git a/assets/js/components/pie-chart.js b/assets/js/components/pie-chart.js index ae7ca2f52..3ed0e6e1b 100644 --- a/assets/js/components/pie-chart.js +++ b/assets/js/components/pie-chart.js @@ -1,4 +1,5 @@ import {Chart, registerables} from 'https://cdn.jsdelivr.net/npm/chart.js@4.3.2/+esm' +import Color from "https://colorjs.io/dist/color.js"; Chart.register(...registerables); @@ -21,6 +22,46 @@ const getOrCreateLegendList = (chart, id) => { return listContainer; }; +/** + * Returns a hash code from a string + * @param {String} str The string to hash. + * @return {Number} A 32bit integer + * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ + */ +function hashCode(str) { + let hash = 0; + for (let i = 0, len = str.length; i < len; i++) { + let chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; // Convert to 32bit integer + } + return hash; +} + +const getColor = (context) => { + + const colors = [ + "#37a2eb", + "#ff6384", + "#ff9e40", + "#9966ff", + "#ffcd56", + "#4dbd74" + ] + + let hash = hashCode(context) ** 2 // Make sure its positive + + let color = colors[hash % (colors.length - 1)] + + let colorObj = new Color(color).to("lch"); + + // Manipulate the color based on the hash + colorObj = colorObj.lighten((hash % 19) / 100) + colorObj = colorObj.darken((hash % 23) / 100) + + return colorObj.to("srgb").toString(); +} + const htmlLegendPlugin = { id: 'htmlLegend', afterUpdate(chart, args, options) { @@ -118,14 +159,14 @@ export class PieChart { parent.className = 'row gx-0'; this.canvasContainer = document.createElement('div'); - this.canvasContainer.className = "col-md-8 col-12" + this.canvasContainer.className = "col-8" this.canvas = document.createElement('canvas'); this.canvas.id = `canvas-${this.id}`; this.legend = document.createElement('div'); this.legend.id = `legend-${this.id}`; - this.legend.className = "col-md-4 col-12" + this.legend.className = "col-4" // Empty the children out of the parent element while (parent.firstChild) { @@ -145,7 +186,8 @@ export class PieChart { datasets: [{ label: this.label, data: this.data['data'], - borderWidth: 1 + borderWidth: 1, + backgroundColor: this.data['labels'].map(getColor), }] }, options: { diff --git a/assets/js/project-page-v2.js b/assets/js/project-page-v2.js index 5e4bf6314..1f0877f6c 100644 --- a/assets/js/project-page-v2.js +++ b/assets/js/project-page-v2.js @@ -488,12 +488,12 @@ class ProjectPage{ } this.orgPieChart = new PieChart( - "project-institution-summary", - this.dataManager.reduceByKey.bind(this.dataManager, "Organization", "jobs"), - "# of Jobs by Institution" + "project-fos-cpu-summary", + this.dataManager.reduceByKey.bind(this.dataManager, "FieldOfScience", "cpuHours"), + "# of CPU Hours by Field of Science" ) this.FosPieChart = new PieChart( - "project-fos-summary", + "project-fos-job-summary", this.dataManager.reduceByKey.bind(this.dataManager, "FieldOfScience", "jobs"), "# of Jobs by Field Of Science" ) diff --git a/projects.html b/projects.html index 2920814aa..eb2ee1e4e 100644 --- a/projects.html +++ b/projects.html @@ -34,7 +34,12 @@ }
- {% include layout/title.html %} +

+ {{page.title}} + + X + +

Data updated: