// Use this to read a CSV from a URL > which were stored through the targets pipeline
data_1 = {
const response = await fetch("https://files.grant-witness.us/nih_overview_breakdown.csv");
const text = await response.text();
return d3.csvParse(text, d3.autoType);
}Funding Curves: Grantmaking over Time by Agency
data = {
if(grant_type == "All"){
const response = await fetch("https://files.grant-witness.us/nih_IC_breakdown.csv");
const text = await response.text();
return d3.csvParse(text, d3.autoType);
}
if(grant_type == "New & Competitive Renewal"){
const response = await fetch("https://files.grant-witness.us/nih_IC_newcomp_breakdown.csv");
const text = await response.text();
return d3.csvParse(text, d3.autoType);
}
if(grant_type == "Non-Competitive Renewal"){
const response = await fetch("https://files.grant-witness.us/nih_IC_noncomp_breakdown.csv");
const text = await response.text();
return d3.csvParse(text, d3.autoType);
}
}
data_2 = data.filter(function(grant) {
return grant.IC === inst_centers;
});data_3 = {
const response = await fetch("https://files.grant-witness.us/nih_new_comp_breakdown.csv");
const text = await response.text();
return d3.csvParse(text, d3.autoType);
}data_4 = {
const response = await fetch("https://files.grant-witness.us/nih_noncomp_breakdown.csv");
const text = await response.text();
return d3.csvParse(text, d3.autoType);
}// Define a helper to normalize today's date to the year 2016/2017
todayNormalized = {
const now = new Date();
const month = now.getMonth(); // 0 = Jan, 9 = Oct
const year = month >= 9 ? 2016 : 2017;
// Create the date and force the specific year
const d = new Date(now);
d.setFullYear(year);
return d;
}usdFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
// For the "Overview" graphs
// 1. Cumulative 2021 - 2024 Values
cum_award_avg = {
// Filter by years
const filtered = data_1.filter(d =>
[2021, 2022, 2023, 2024].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
// Summarize (Mean) and Round
const mean = latestEntries.reduce((sum, d) => sum + d.Cum_Award, 0) / latestEntries.length;
return Math.round(mean);
}
cum_cost_avg = {
// 1. Filter by years
const filtered = data_1.filter(d =>
[2021, 2022, 2023, 2024].includes(d.Original_Year)
);
// 2. Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
// 3. Summarize (Mean) and Round
const mean = latestEntries.reduce((sum, d) => sum + d.Cum_Cost, 0) / latestEntries.length;
return Math.round(mean);
}
formatted_cum_cost_avg = usdFormatter.format(cum_cost_avg / 1e9) + "B"
// 2. Cumulative 2025 values
cum_award_2025 = {
// Filter by years
const filtered = data_1.filter(d =>
[2025].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Award;
}
cum_cost_2025 = {
// Filter by years
const filtered = data_1.filter(d =>
[2025].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Cost;
}
formatted_cum_cost_2025 = usdFormatter.format(cum_cost_2025 / 1e9) + "B"
// 3. Cumulative 2026 Values
last_cum_award_2026 = {
// Filter by years
const filtered = data_1.filter(d =>
[2026].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Award;
}
last_cum_cost_2026 = {
// Filter by years
const filtered = data_1.filter(d =>
[2026].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Cost;
}
formatted_last_cum_cost_2026 = usdFormatter.format(last_cum_cost_2026 / 1e9) + "B"newcomp_award_avg = {
// Filter by years
const filtered = data_3.filter(d =>
[2021, 2022, 2023, 2024].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
// Summarize (Mean) and Round
const mean = latestEntries.reduce((sum, d) => sum + d.Cum_Award, 0) / latestEntries.length;
return Math.round(mean);
}
// 2. Cumulative 2025 values
newcomp_award_2025 = {
// Filter by years
const filtered = data_3.filter(d =>
[2025].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Award;
}
// 3. Cumulative 2026 Values
newcomp_cum_award_2026 = {
// Filter by years
const filtered = data_3.filter(d =>
[2026].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Award;
}noncomp_award_avg = {
// Filter by years
const filtered = data_4.filter(d =>
[2021, 2022, 2023, 2024].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
// Summarize (Mean) and Round
const mean = latestEntries.reduce((sum, d) => sum + d.Cum_Award, 0) / latestEntries.length;
return Math.round(mean);
}
// 2. Cumulative 2025 values
noncomp_award_2025 = {
// Filter by years
const filtered = data_4.filter(d =>
[2025].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Award;
}
// 3. Cumulative 2026 Values
noncomp_cum_award_2026 = {
// Filter by years
const filtered = data_4.filter(d =>
[2026].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Award;
}The following interactive funding curves are adapted from Jeremy Berg, using data publicly available on NIH RePORTER and NSF. These visualizations demonstrate differences in the cumulative number of awards and cumulative amount obligated across NIH or NSF awards from fiscal year (FY) 2021 to 2026.
For more discussions about these funding curves, read the related articles published by Nature and Science.
NIH Graphs
Click or move your cursor along the graph to compare data across FY 2021 to 2026.
The tooltip will provide information about a given day compared to previous fiscal years. The black dotted line represents today’s date, while the bolded orange line demonstrates the trends for FY 2025 and the bolded red line demonstrates the current trend for FY 2026, with the remaining lines demonstrating trends from FY 2021-2024. The FY runs from October 1st of the previous year through September 30th of the current year.
Overview
Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Number of Awards",
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_1.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Award",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_1,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_1,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_1 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_1, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_1, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_1, {
y: "Original_Year",
text: "Cum_Award",
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text(data_1, {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Fraction of Awards",
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_1.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Award_Norm",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_1,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_1,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_1 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_1, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_1, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_1, {
y: "Original_Year",
text: "Cum_Award_Norm",
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text([data_1[0]], {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Obligations",
tickFormat: d => `$${(d / 1e9).toFixed(1)}B`,
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_1.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Cost",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_1,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_1,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_1 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_1, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_1, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_1, {
y: "Original_Year",
text: d => {
if (d.Cum_Cost >= 1000000000) {
// Format as Billions if 1,000,000,000 or greater
return `$${(d.Cum_Cost / 1000000000).toFixed(1)}B`;
} else {
// Format as Millions otherwise
return `$${(d.Cum_Cost / 1000000).toFixed(1)}M`;
}
},
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text(data_1, {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})The above visualizations are restricted to Type 1 (new) and Type 5 (non-competing continuation) grants, to be consistent with the method that NIH RePORTER chose to display funding curves. More information about the different types of NIH grants can be found here.
From these graphs, we can see that there was a significant lag in FY 2025 funding, which occurred during the spring and into the summer (March through August). This is consistent with reported disruptions to grant review and funding processes that occurred in 2025.
- Specifically, the total number of awards made in FY 2025 is .
- This total number is less than the average number of awards across FY 2021 - 2024, which was .
- Similarly, the total funding obligated across these awards was less in FY 2025, which was .
- This is compared to the average amount obligated across FY 2021 through 2024, which was .
- Additionally, we are seeing a lag in awards made in FY 2026 so far, with only awards.
- The cumulative cost from these obligations in FY 2026 is .
By Award Type
The following graphs visualize funding across FY based on award types, comparing new and competitive renewal awards (Type 1, 2, 4, 9) with non-competitive renewal awards (Type 5, 6, 7, 8). More information about the different types of NIH grants can be found here.
- New applications are the first request for funding for a project that has not previously received that specific grant.
- Competitive Renewals are requests for continued funding after the original project period ends; these applications go through full peer review and must compete again for funding.
- In contrast, Non-Competitive Renewals provide continued funding for the next budget period within an already approved multi-year project and generally do not undergo full peer review, assuming satisfactory progress and compliance with administrative requirements.
Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Number of Awards",
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_3.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Award",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_3,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_3,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_3 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_3, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_3, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_3, {
y: "Original_Year",
text: "Cum_Award",
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text(data_3, {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Number of Awards",
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_4.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Award",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_4,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_4,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_4 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_4, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_4, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_4, {
y: "Original_Year",
text: "Cum_Award",
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text(data_4, {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})- In FY 2025, the total number of New and Competitively Renewed awards (n = ) was lower than previous years.
- The average annual total in the previous four FYs was awards.
- So far, in FY 2026, New and Competitive Renewal awards have been made.
- Similar trends are observed for Non-Competitive Renewals, where by the end of FY 2025, awards had been made.
- This is compared with an average year-end total of awards across FY 2021–2024.
- Currently in FY 2026, only Non-Competitive Renewal Awards have been made so far.
By Institutes and Centers (ICs)
Use the following drop-down to compare FY trends in award types based on NIH Institute or Center.
viewof grant_type = Inputs.radio(
new Map([
["All" ,"All"],
["New & Competitive Renewal" ,"New & Competitive Renewal"],
["Non-Competitive Renewal" ,"Non-Competitive Renewal"]
]),
{label: "Award Type:", value: "All"} // 'value' sets the default
)
viewof inst_centers = Inputs.select(
new Map([
["Advancing Translational Sciences" ,"TR"],
["Aging" ,"AG"],
["Alcohol Abuse and Alcoholism" ,"AA"],
["Allergy and Infectious Diseases" ,"AI"],
["Arthritis and Musculoskeletal and Skin Diseases" ,"AR"],
["Biomedical Imaging and Bioengineering" ,"EB"],
["Cancer Institute" ,"CA"],
["Child Health and Human Development" ,"HD"],
["Complementary and Integrative Health" ,"AT"],
["Deafness and Other Communication Disorders" ,"DC"],
["Dental and Craniofacial Research" ,"DE"],
["Diabetes and Digestive and Kidney Diseases" ,"DK"],
["Drug Abuse" ,"DA"],
["Environmental Health Sciences" ,"ES"],
["Eye" ,"EY"],
["Fogarty International Center", "TW"],
["General Medical Sciences" ,"GM"],
["Heart, Lung, and Blood Institute" ,"HL"],
["Human Genome Research" ,"HG"],
["Library of Medicine" ,"LM"],
["Mental Health" ,"MH"],
["Minority Health and Health Disparities" ,"MD"],
["Neurological Disorders and Stroke" ,"NS"],
["Nursing Research" ,"NR"],
["Office of Director", "OD"]
]),
{label: "Institute or Center:", value: "TR"} // 'value' sets the default
)inst_award_avg = {
// Filter by years
const filtered = data_2.filter(d =>
[2021, 2022, 2023, 2024].includes(d.Original_Year) &&
d.IC == inst_centers
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
// Summarize (Mean) and Round
const mean = latestEntries.reduce((sum, d) => sum + d.Cum_Award, 0) / latestEntries.length;
return Math.round(mean);
}
// 2. Cumulative 2025 values
inst_award_2025 = {
// Filter by years
const filtered = data_2.filter(d =>
[2025].includes(d.Original_Year) &&
d.IC == inst_centers
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Award;
}
// 3. Cumulative 2026 Values
inst_cum_award_2026 = {
// Filter by years
const filtered = data_2.filter(d =>
[2026].includes(d.Original_Year) &&
d.IC == inst_centers
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0]?.Cum_Award ?? "there is no data available about how many";
}Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Number of Awards",
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_2.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Award",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_2,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_2,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_2 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_2, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_2, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_2, {
y: "Original_Year",
text: "Cum_Award",
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text(data_2, {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Obligations",
tickFormat: d => {
if (Math.abs(d) >= 1e9) {
return `$${(d / 1e9).toFixed(1)}B`; // Billions
} else {
return `$${(d / 1e6).toFixed(1)}M`; // Millions
}
},
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_2.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Cost",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_2,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_2,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_2 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_2, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_2, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_2, {
y: "Original_Year",
text: d => new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
notation: 'compact',
compactDisplay: 'short'
}).format(d.Cum_Cost),
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text(data_2, {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})These visualizations demonstrate that there is a difference in the number of awards and amount of funding obligated in FY 2025 to 2026 when filtering by IC.
inst_label = {
inst_centers; // This "listens" for changes to the dropdown
const select = viewof inst_centers.querySelector("select");
return select.options[select.selectedIndex].text;
}For example, based on your selection of Awards in , the following totals are determined:
- In FY 2025, only awards had been awarded.
- This is less than the average number of awards granted from FY 2021-2024, which was
- Currently, in FY 2026, grants have been awarded.
To compare these totals with a different award type from “Advancing Translational Sciences”, select a different “Award Type” radio button.
NSF Graphs
Click or move your cursor along the graph to compare data across FY 2021 to 2026.
The tooltip will provide information about a given day compared to previous fiscal years. The black dotted line represents today’s date, while the bolded orange line demonstrates the trends for FY 2025 and the bolded red line demonstrates the current trend for FY 2026, with the remaining lines demonstrating trends from FY 2021-2024. The FY runs from October 1st of the previous year through September 30th of the current year.
Overview
// Use this to read a CSV from a URL > which were stored through the targets pipeline
data_5 = {
const response = await fetch("https://files.grant-witness.us/nsf_awards_breakdown.csv");
const text = await response.text();
return d3.csvParse(text, d3.autoType);
}// Use this to read a CSV from a URL > which were stored through the targets pipeline
data_6 = {
const response = await fetch("https://files.grant-witness.us/nsf_obligate_breakdown.csv");
const text = await response.text();
return d3.csvParse(text, d3.autoType);
}nsf_cum_award_avg = {
// Filter by years
const filtered = data_5.filter(d =>
[2021, 2022, 2023, 2024].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
// Summarize (Mean) and Round
const mean = latestEntries.reduce((sum, d) => sum + d.Cum_Award, 0) / latestEntries.length;
return Math.round(mean);
}
nsf_cum_cost_avg = {
// 1. Filter by years
const filtered = data_6.filter(d =>
[2021, 2022, 2023, 2024].includes(d.Original_Year)
);
// 2. Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
// 3. Summarize (Mean) and Round
const mean = latestEntries.reduce((sum, d) => sum + d.Cum_Cost, 0) / latestEntries.length;
return Math.round(mean);
}
nsf_formatted_cum_cost_avg = usdFormatter.format(nsf_cum_cost_avg / 1e9) + "B"
// 2. Cumulative 2025 values
nsf_cum_award_2025 = {
// Filter by years
const filtered = data_5.filter(d =>
[2025].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Award;
}
nsf_cum_cost_2025 = {
// Filter by years
const filtered = data_6.filter(d =>
[2025].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Cost;
}
nsf_formatted_cum_cost_2025 = usdFormatter.format(nsf_cum_cost_2025 / 1e9) + "B"
// 3. Cumulative 2026 Values
nsf_last_cum_award_2026 = {
// Filter by years
const filtered = data_5.filter(d =>
[2026].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Award;
}
nsf_last_cum_cost_2026 = {
// Filter by years
const filtered = data_6.filter(d =>
[2026].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Cost;
}
nsf_formatted_last_cum_cost_2026 = usdFormatter.format(nsf_last_cum_cost_2026 / 1e9) + "B"Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Number of Awards",
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_5.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Award",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_5,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_5,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_5 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_5, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_5, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_5, {
y: "Original_Year",
text: "Cum_Award",
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text([data_5[0]], {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Fraction of Awards",
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_5.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Award_Norm",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_5,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_5,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_5 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_5, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_5, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_5, {
y: "Original_Year",
text: "Cum_Award_Norm",
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text(data_5, {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Obligations",
tickFormat: d => `$${(d / 1e9).toFixed(1)}B`,
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_6.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Cost",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_5,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_6,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_6 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_6, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_6, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_6, {
y: "Original_Year",
text: d => {
if (d.Cum_Cost >= 1000000000) {
// Format as Billions if 1,000,000,000 or greater
return `$${(d.Cum_Cost / 1000000000).toFixed(1)}B`;
} else {
// Format as Millions otherwise
return `$${(d.Cum_Cost / 1000000).toFixed(1)}M`;
}
},
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text(data_6, {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})Similar to the NIH visualizations, through these NSF funding curves, we can see that there was a significant lag in funding during FY 2025 occurring during the spring and into the summer (March through August), which is consistent with reported disruptions to award review and funding processes.
- The average number of awards across FY 2021 - 2024, which was .
- That average is greater than total number of awards made in FY 2025, which was only .
- Similarly, the total funding obligated across these awards was less in FY 2025, which was .
- This is compared to the average across FY 2021 through 2024, which was .
- Additionally, we are seeing a lag in awards made in FY 2026 so far, with only awards.
- The cumulative cost from these obligations in FY 2026 is .
By Directorate
Use the following drop-down to compare trends in FY funding for each NSF Directorate.
viewof nsf_directorates = Inputs.select(
new Map([
["Biological Sciences",
"Directorate for Biological Sciences"],
["Computer and Information Science and Engineering" ,
"Directorate for Computer and Information Science and Engineering"],
["Engineering",
"Directorate for Engineering"],
["Geosciences",
"Directorate for Geosciences"],
["Mathematical and Physical Sciences" ,
"Directorate for Mathematical and Physical Sciences"],
["STEM Education" ,
"Directorate for STEM Education"],
["Social, Behavioral and Economic Sciences" ,
"Directorate for Social, Behavioral and Economic Sciences"],
["Technology, Innovation, and Partnerships",
"Directorate for Technology, Innovation, and Partnerships"],
["Office Of The Director",
"Office Of The Director"]
]),
{label: "Choose an NSF Directorate:", value: "Directorate for STEM Education"} // 'value' sets the default
)data_7 = {
const response = await fetch("https://files.grant-witness.us/nsf_direct_awards_breakdown.csv");
const text = await response.text();
return d3.csvParse(text, d3.autoType);
}
data_8 = data_7.filter(function(grant) {
return grant.Directorate === nsf_directorates;
});data_9 = {
const response = await fetch("https://files.grant-witness.us/nsf_direct_obligate_breakdown.csv");
const text = await response.text();
return d3.csvParse(text, d3.autoType);
}
data_10 = data_9.filter(function(grant) {
return grant.Directorate === nsf_directorates;
});direct_award_avg = {
// Filter by years
const filtered = data_8.filter(d =>
[2021, 2022, 2023, 2024].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
// Summarize (Mean) and Round
const mean = latestEntries.reduce((sum, d) => sum + d.Cum_Award, 0) / latestEntries.length;
return Math.round(mean);
}
// 2. Cumulative 2025 values
direct_award_2025 = {
// Filter by years
const filtered = data_8.filter(d =>
[2025].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Award;
}
// 3. Cumulative 2026 Values
direct_cum_award_2026 = {
// Filter by years
const filtered = data_8.filter(d =>
[2026].includes(d.Original_Year)
);
// Group & Slice Max (Get the latest entry for each year)
// Reduce the array into a Map to keep only the entry with the max NoA_date per year
const latestEntries = Array.from(
filtered.reduce((acc, curr) => {
const existing = acc.get(curr.Original_Year);
if (!existing || new Date(curr.NoA_date) > new Date(existing.NoA_date)) {
acc.set(curr.Original_Year, curr);
}
return acc;
}, new Map()).values()
);
return latestEntries[0].Cum_Award;
}Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Number of Awards",
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_8.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Award",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_8,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_8,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_8 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_8, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_8, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_8, {
y: "Original_Year",
text: "Cum_Award",
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text(data_8, {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})Plot.plot({
marginLeft: 50,
width: 928,
// Add labels to the axes here
x: {
label: "Award Date",
tickFormat: "%b", // Shows 'Jan', 'Feb', etc. Use "%B" for full names
},
y: {
label: "Obligations",
tickFormat: d => {
if (Math.abs(d) >= 1e9) {
return `$${(d / 1e9).toFixed(1)}B`; // Billions
} else {
return `$${(d / 1e6).toFixed(1)}M`; // Millions
}
},
grid: true // Optional: makes it easier to read values
},
// specify colors here for outstanding lines
color: {
domain: [2021, 2022, 2023, 2024, 2025, 2026], // The specific values in your data
range: ["#6605a6", "#0b72b3", "#2A9D8F", "#e96ab6", "#F4A261", "#E63946"] // The specific colors you want (e.g., Red and Blue)
},
marks: [
// Filter for years that ARE NOT 2025 or 2026 (as an example)
Plot.lineY(data_10.filter(d => ![2015, 2016, 2017, 2018, 2019, 2020].includes(d.Original_Year)), {
x: d => new Date(d.NoA_date),
y: "Cum_Cost",
stroke: "Original_Year", // This will now use the color scale
strokeWidth: 3 // Optional: make them pop a bit more
}),
Plot.ruleX(
data_10,
Plot.pointerX(Plot.binX({}, { x: "NoA_date", thresholds: 1000, insetTop: 20 }))
),
// The Current Day line
Plot.ruleX([todayNormalized], {
stroke: "black",
strokeDasharray: "4", // 4px dash, 4px gap
strokeOpacity: 1 // Makes it look a bit more subtle
}),
Plot.text([todayNormalized], {
x: d => d,
text: d => "Today",
textAnchor: "start",
fontSize: 20,
dx: 10,
fill: "black"
}),
//Tooltip code
Plot.tip(
data_10,
Plot.pointerX(
Plot.binX(
{title: (v) => v},
{
x: "NoA_date",
thresholds: 1000,
render(index, scales, values, dimensions, context) {
const g = d3.select(context.ownerSVGElement).append("g");
const [i] = index;
if (i !== undefined) {
const data_10 = values.title[i];
g.attr(
"transform",
`translate(${Math.min(
values.x1[i],
dimensions.width - dimensions.marginRight - 200
)}, 20)`
).append(() =>
Plot.plot({
marginTop: 20,
height: 150,
width: 150,
axis: null,
y: {domain: scales.scales.color.domain},
marks: [
Plot.frame({ fill: "white", stroke: "currentColor" }),
Plot.dot(data_10, {
y: "Original_Year",
fill: (d) => scales.color(d.Original_Year),
r: 5,
frameAnchor: "left",
symbol: "square2",
dx: 6
}),
Plot.text(data_10, {
y: "Original_Year",
text: (d) => `FY ${d.Original_Year} :`,
fontSize: 16,
frameAnchor: "left",
dx: 13
}),
Plot.text(data_10, {
y: "Original_Year",
text: d => new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
notation: 'compact',
compactDisplay: 'short'
}).format(d.Cum_Cost),
fontSize: 16,
frameAnchor: "right",
dx: -8
}),
Plot.text(data_10, {
frameAnchor: "top-left",
dy: -20,
text: (d) => {
// 1. Convert the string "2026-03-30" into a JS Date object
const dateObj = new Date(d.NoA_date);
// 2. Format it to "Month Day" (e.g., "March 30")
return dateObj.toLocaleString("en-US", {
month: "long",
day: "numeric"
});
},
fontSize: 16,
fontWeight: "bold"
})
]
})
);
}
return g.node();
}
}
)
)
),
Plot.ruleY([0])
]
})These visualizations demonstrate that there is a difference in the number of awards and amount of funding obligated in FY 2025 to 2026 when filtering by NSF Directorate
For example, based on your selection of the , the following totals are determined:
- However, in FY 2025, only grants had been awarded.
- This is less than the average number of awards granted from FY 2021-2024, which was
- Currently, in FY 2026, grants have been awarded.
To compare these trends with other research focuses, use the drop-down menu to select another NSF Directorate.