Roosevelt Island: Actor Network
Developed by Anna Cao. Dataset built and populated by Anna Cao, Victoria Li, and Audrey Lau.
Roosevelt Networks is an interactive model of the actors that shape Roosevelt Island (agencies, organizations, institutions, buildings and people) and the relationships between them. This documentation explains how to use the app, how the model is constructed, and how to load your own dataset.
Open the app →1. Overview
Roosevelt Networks treats the island as a network. Each actor (an agency, company, organization, institution, site or person with a stake in the island) is a node. Each documented relationship between two actors (funding, ownership, administration, collaboration and so on) is an edge. The model is built entirely in the browser from a shared spreadsheet. Every view in the app (Relationships, Map and Timeline) is a different arrangement of that same graph.
The pipeline, in order:
- Load the actor and adjacency tables from the published Google Sheet, falling back to a bundled snapshot.
- Join news-derived public interest and sentiment values onto actors.
- Build a typed multigraph: one node per actor and one edge per unique relationship.
- Derive edge widths and line styles from each relationship's parameters.
- Compute a force-directed layout from the relationship types that are switched on.
- Calculate centrality on whatever subset of the network is currently visible, and use it for halos, labels and the Centrality filter.
2. Data sources
Project spreadsheet
The primary dataset is a Google Sheet maintained by the project team, published to the web as CSV. The app requests each tab separately from the published endpoint:
https://docs.google.com/spreadsheets/d/e/<id>/pub?gid=<tab>&single=true&output=csv
A refresh timestamp parameter and cache: 'no-store' are added so a reload always returns
the latest edits rather than a cached copy. Each request times out after 10 seconds. Actors are read from
gid=1300872278 and adjacencies from gid=548852791.
View the published dataset.
Local snapshot
A copy of each tab is bundled with the app under data/ (actors.csv and
edges/adjacency.csv). If the sheet cannot be reached (a network error, an error response or a
timeout), the app loads the snapshot instead. Actors that exist in the live
sheet but not in the snapshot are tracked separately, so newly added actors can be identified.
Relink data repeats the fetch on demand.
News metrics
Public interest (news hits) and public sentiment are read from a separate bundled table,
actor database - actor_working - with news hits.csv, with columns actor,
hits and sentiment. Rows are matched to actors by name, ignoring case and surrounding
whitespace. Non-numeric values are treated as missing, as are negative hit counts. In the current snapshot, all 277
actors have a hits value (0–100) and 213 have a sentiment value (about −0.69 to +0.77). How these
values are collected is described under Public interest + sentiment.
Geographic data
- Building footprints: NYC Open Data / DoITT BUILDING dataset (
5zhs-2jue), filtered to the island, one GeoJSON file per BIN. - Lot attributes: NYC DCP PLUTO 26v2 (
64uk-42ks), joined by BBL. - Island boundary: OpenStreetMap relation 2389564.
- Building–actor links: the buildings tab of the project spreadsheet (
gid=1593670637).
3. Spreadsheet structure
The spreadsheet has three tabs the app depends on. Header names must match exactly, because the app reads columns by name rather than by position.
Actors (actor_working)
One row per actor. In the current snapshot, 277 actors.
| Column | Content | How the app uses it |
|---|---|---|
actor | Full name. The unique identifier. | Node id, matched against source/target in the adjacency tab. Leading and trailing spaces are trimmed, and duplicate rows are ignored after the first. |
abbrev | Short name, e.g. RIOC. | Node label. Falls back to the full name if empty. |
topic | Comma-separated categories: GOVERNANCE, COMMUNITY, ECONOMIC, INFRASTRUCTURE, HOUSING, ECOLOGY, TRANSIT. | Category filter and Category colour mode. Uppercased, so letter case does not matter. An actor can belong to several. |
type | Sector: PUBLIC, PRIVATE, COMMUNITY or OTHER. | Sector filter and Sector colour mode. Blank values become Unknown. |
scale | Reach of influence, written as 0 - MICRO, 1 - MESO or 2 - MACRO. | Scale filter and node size. Only the text after the last hyphen is read; the number keeps the sheet sortable. |
year_start | First active year. | Time range filter and Timeline. Blank means unknown. |
year_end | Last active year. 3000 marks an actor that is still active. | Time range filter and Timeline. Shown as “Present” in details. |
notes | Research notes and sources. | Kept with the actor record for reference. |
public sentiment | Team-assessed sentiment. | Kept for reference. The Public sentiment colour mode uses the news metrics table instead (see Public interest + sentiment). |
present on island | 1 if the actor has a physical presence on the island. | Kept for reference. |
In the current snapshot, actors break down by sector as 182 public, 84 private, 5 community, 2 other and 4 blank,
and by scale as 115 micro, 155 meso, 5 macro and 2 blank. 173 actors are marked as ongoing (year_end
3000), and 9 have no years at all.
Adjacency
One row per relationship. The tab must contain the six columns below, or the import fails with a message naming the missing header. Extra columns (such as the team's “Item missing from the actor_working” check column) are ignored.
| Column | Content |
|---|---|
source-target relationship | Relationship type, written as in the table under Network model. Case-insensitive. |
source | The actor in the first role of the type name (e.g. the owner in Owner–Tenant). Must exactly match an actor value. |
target | The actor in the second role (e.g. the tenant). |
parameter 0 | Type-specific attribute: parcel area, collaboration frequency, upfront investment or appointment method. |
parameter 1 | Type-specific attribute: collaboration closeness or yearly funding. |
parameter 2 | Reserved for further attributes. Not currently encoded. |
Rows missing a type, source or target are skipped, and so are rows whose type is not one of the nine recognised types. Of the 435 rows in the current snapshot, 354 have a recognised type.
Buildings
One row per building footprint, keyed by BIN (Building Identification Number). This tab is not read live. It is
downloaded by a build script (npm run sync:buildings) that bakes the geometry into a single map overlay
(see Map). Its columns are:
- Identity and linkage:
BIN,BBL(borough-block-lot),NameandActor.Actormust match anactorvalue to anchor that actor on the map. - Site points: rows named
point - "<name>"link a hand-placed point (for sites without a footprint, such as parks and ferry landings) to an actor. - Building attributes (DoITT):
Address,Building Use,Construction Year,Roof Height. - Lot attributes (PLUTO):
Land Use Code,Building Class,PLUTO Year Built,Floors,Residential Units,Total Units,Building Area (sq ft),Lot Area, commercial, residential, office, retail and other floor areas,Zoning District,Community District,Zip Code,Historic District,Landmark. - Scenario fields:
Proposed Use,Scenario,Groups Impacted,Impact Notes.
PLUTO describes tax lots, not buildings. The island's 68 footprints fall on only 17 lots, so lot-level values repeat across every building on the same lot. Treat them as lot totals, not per-building figures.
4. Network model
Graph construction
The network is stored as a graphology
mixed multigraph (new Graph({ multi: true })). Multi means two actors can be joined by
several edges of different types, e.g. a parent organisation that also funds its child. Mixed means
directed and undirected edges coexist. While building the graph, the app:
- Skips self-loops (an actor related to itself).
- Keeps only the first occurrence of each type + source + target combination. For collaboration, the pair is order-independent, so A–B and B–A count as the same edge.
- Skips any edge whose source or target is not in the actor table, and logs a warning naming the unmatched id.
Relationship types
Collaboration is the only undirected type. Every other type is directed from source to
target and drawn with an arrow. Financial relationships are recorded recipient → sender (per the
type name), but the arrow is drawn the other way so it follows the money from sender to recipient. Centrality
calculations use the direction as recorded.
| Sheet value | Direction | Parameters | Encoding |
|---|---|---|---|
| ADMINISTRATOR-ADMINISTRATED | Administrator → administrated | — | Uniform line |
| COLLABORATION | Undirected | p0: one-time or ongoing; p1: closeness (positive whole number) | Dashed if one-time. Width scales with closeness. |
| CREATOR-CREATION | Creator → creation | — | Uniform line |
| FINANCIAL (RECIPIENT-SENDER) | Recipient → sender (drawn reversed) | p0: upfront investment ($); p1: yearly funds ($) | Width scales with yearly funds. Upfront investment is drawn as a secondary line. |
| OWNER-TENANT | Owner → tenant | p0: parcel area (sq ft) | Width scales with the square root of the area. |
| PARENT-CHILD | Parent → child | — | Uniform line |
| POSITION-INCUMBENT | Position → incumbent | p0: elected, succession or appointment | Solid, dotted or double line |
| PREDECESSOR-SUCCESSOR | Predecessor → successor | — | Uniform line. Also chains actors in Timeline. |
| REPRESENTATIVE-ELECTOR | Representative → elector | — | Uniform line |
Parameter parsing
- Money accepts an optional
$, thousands separators and decimals (e.g.$1,250,000). Negative or non-numeric values are ignored. - Area accepts a number with an optional
sq ft,sq feetorft2suffix. Values must be positive. - Closeness must be a positive whole number.
- Edges with a missing or unparseable parameter keep the minimum width and are still part of the network.
Width scaling
Weighted widths are scaled linearly between 1 px and 5 px across the range of values in the whole dataset, so the smallest value in a type gets 1 px and the largest gets 5 px:
width = 1 + 4 × (v − vmin) / (vmax − vmin)
For Owner–Tenant, v is the square root of the parcel area, so line width tracks a parcel's side length rather than its area. This stops a few very large parcels from flattening all the others to the minimum. The legend reports the real minimum and maximum values behind each scale.
Parallel edges
When several edges join the same two actors, each is given a progressively larger curvature (starting at 0.05 and increasing in steps of 0.025) so they fan out instead of overlapping. The Connection curvature slider multiplies these offsets: 20% is the default curvature and 100% is five times as curved.
5. Centrality
Centrality measures how important an actor is to the structure of the network. Roosevelt Networks offers four measures. Degree is calculated directly, and the other three use graphology-metrics.
Which network is measured
Scores are never precomputed. Each time filters change, the app builds a temporary metric graph containing:
- Actors that pass the Sector, Category, Scale and Time range filters, plus any pinned actors.
- Edges whose relationship type is switched on (or which are pinned), and whose two ends are both included.
Every edge has a weight of 1, so parameters such as funding size do not influence centrality. Collaboration edges are undirected and all others keep their direction. As a result, an actor's centrality is relative to the slice of the network on screen. Turning off Financial relationships, for example, recalculates every score without funding links.
For closeness and betweenness, parallel relationships in the same direction are collapsed into one step, and undirected relationships become steps in both directions. Degree and eigenvector retain each relationship separately. Centrality filter scores are calculated before applying the centrality threshold, so raising that threshold does not change the scores being filtered.
In the formulas below, N is the number of actors in the metric graph and d(u, v) is the number of steps on the shortest path from u to v.
Degree
The number of edges touching an actor: incoming plus outgoing plus undirected.
CD(v) = degin(v) + degout(v) + degundirected(v)
Because the graph is a multigraph, two edges of different types between the same pair count twice. Degree is reported as a raw count rather than normalised to 0–1, so the Centrality filter can be expressed in whole connections.
Closeness
How few steps it takes for the rest of the network to reach an actor. Computed with
closenessCentrality(graph, { wassermanFaust: true }). A breadth-first search follows edges
inbound (undirected edges count both ways), so distance is measured from other actors to v. If
r is the number of other actors that can reach v:
CC(v) = (r / Σu d(u, v)) × (r / (N − 1))
The first term is ordinary closeness within the reachable part of the network. The second is the Wasserman–Faust correction, which scales the score by the share of the network that can reach v. Without it, an actor in a tiny isolated cluster would score as “close” as one at the heart of the main network. Actors that nobody can reach score 0.
Betweenness
How often an actor lies on the shortest paths between other actors, i.e. how much it acts as a broker or bridge.
Computed with betweennessCentrality(graph, { getEdgeWeight: null }), which implements Brandes'
algorithm on the unweighted graph:
CB(v) = [ Σs ≠ v ≠ t σst(v) / σst ] / ((N − 1)(N − 2))
Here σst is the number of shortest paths from s to t, and σst(v) is how many of them pass through v. Paths follow edge direction. The result is normalised by the number of ordered pairs of other actors, giving a value from 0 to 1.
Eigenvector
An actor is important if it is connected to other important actors. Computed with
eigenvectorCentrality(graph, { getEdgeWeight: null, maxIterations: 500 }) by power iteration:
- Every actor starts with a score of 1/N.
- Each iteration, every actor passes its current score along its outgoing (and undirected) edges, and each actor keeps its own previous score as well: x ← x + ATx.
- Scores are divided by their Euclidean norm so they stay bounded.
- Iteration stops when the total change across all actors falls below N × 10−6, or after 500 iterations.
Because scores travel along edge direction, an actor ranks highly when well-ranked actors point to it. If the calculation fails to converge, which is rare, no eigenvector scores are shown and a warning is logged.
Where centrality is used
- Halo: any of the four measures can set halo size (see Visual encoding).
- Labels: actors are ranked by the selected measure. The top 10 get the largest labels. The rest are split at the median into medium and small labels, and smaller tiers only appear as you zoom in. The Labels slider hides labels by percentile rank, with tied scores sharing the same rank.
- Centrality filter: steps through ranked values of the selected measure to hide lower-ranked actors. Degree shows whole connection counts; closeness, betweenness and eigenvector show fractional scores. The slider, step buttons and editable score all use the selected measure.
- Details panel: “more central than X% of all other actors” is the share of other visible actors with a strictly lower score than the selected actor.
6. Public interest + sentiment
Two news-derived measures describe how visible each actor is in the press and the tone of that coverage. Both are
produced offline by a Python script, analyze_actors.py, and bundled with the app as the news metrics
table (see Data sources). The browser does not contact any news service.
Collecting headlines
-
Query. Each actor is searched as an exact phrase together with the island, for example
"Roosevelt Island Operating Corporation" "Roosevelt Island". Quotation marks inside names are removed, and a trailing00(used in the sheet to mark repeated people) is dropped, so both rows share one search. Other names are searched exactly as written. -
Feed. The query is sent to the Google News RSS search endpoint,
https://news.google.com/rss/search?q=<query>&hl=en-US&gl=US&ceid=US:en, which returns up to 100 English-language US articles, ranked by Google. No API key is needed. Up to four feeds are requested at once. -
Parse. For each item the script keeps the headline, link, publisher name, publisher URL and
publication date. The
- Publishersuffix that Google appends to headlines is removed. - Cache + audit. Each parsed feed is saved, keyed by a SHA-256 hash of its query, so later runs reuse it instead of searching again. Every run appends the query, retrieval time, chosen headlines and their individual scores to an audit log, so any value can be traced back to its sources. The current values come from feeds retrieved on 2 and 6 October 2026.
Public interest (hits)
hits is the number of Google News results returned for the actor's query, capped at 100 (the most
the feed returns). It counts all results before any filtering, so it measures how often the actor appears in news
coverage that also mentions Roosevelt Island. Actors with no matching coverage have 0. In the current snapshot, 6
actors reach the cap of 100.
In the app, hits can set halo size (Public interest). The halo grows linearly from the smallest to the largest hits value among visible actors (see Visual encoding). Because of the cap, actors at 100 cannot be told apart.
Public sentiment
Sentiment is scored with VADER (Valence Aware Dictionary and sEntiment Reasoner, version 3.3.2). It is a rule-based model: each word has a human-rated valence, adjusted for negation (“not good”), intensifiers (“very”), capitals, exclamation marks and contrast words such as “but”. VADER runs locally with no trained model to download.
Before scoring, the headlines are filtered for a broader spread of sources:
- Headlines with no link, publisher or text are skipped.
- Repeated links and identical headlines (ignoring letter case and spacing) are kept only once.
- At most 2 headlines are kept from any one publisher, so a single outlet cannot dominate the score.
Each remaining headline gets VADER's compound score, a normalised sum of its word valences from
−1 (most negative) to +1 (most positive). An actor's sentiment is the mean compound score of its
headlines, rounded to 4 decimal places. Actors with no usable headlines are left blank and appear grey in the
Public sentiment colour mode. In the current snapshot, 213 of 277 actors have a score. The median actor is scored
from 9 headlines, ranging from 1 to 78, and 61 actors are scored from fewer than 5.
Updating the values
- Copy the current actor list into the working CSV, keeping the existing
hitsandsentimentcolumns. - Run
python analyze_actors.py(requiresvaderSentiment==3.3.2). The script backs up the CSV first, then searches only actors without a saved feed. Use--refreshto search every actor again, and--dry-runto preview queries. - Fill
hitsfrom the number of results in each actor's feed. - Copy the result to
public/data/actor database - actor_working - with news hits.csv.
Repeating a search later can return different headlines, so scores change over time. Saved feeds keep earlier values reproducible.
7. Layout
Relationships mode uses a force-directed layout, so actors with many shared relationships settle near each other. The steps are:
- Copy the graph, keeping only edges whose type is switched on.
- Place every actor on a circle (
graphology-layoutcircular) to get a deterministic starting point. - Run 150 iterations of ForceAtlas2 (
graphology-layout-forceatlas2) withscalingRatio: 10. Connected actors attract, all actors repel, and gravity pulls everything toward the centre.
The Gravity slider (0–25) is converted to ForceAtlas2's gravity setting as 25 minus the slider value, so higher slider values apply weaker gravity and spread the layout out. The default of 20 gives gravity 5. The layout is recalculated and animated whenever relationship types are switched on or off, unless Freeze actor position is on.
Positions encode connectivity only. Distance on screen roughly reflects how many relationships separate two actors, but the axes have no meaning, and the same data can settle into a mirrored or rotated arrangement.
8. Visual encoding
Node size
Size encodes scale: micro actors are drawn at 0.5×, meso at 1.5× and macro at 4×
a base radius. The Node size slider adjusts the base radius.
Node colour
- Sector: fixed colours per
type: public #FFD23D (yellow), private #3D94FF (blue), community/voluntary #FF3BA9 (pink) and other #DBDBDB (grey). Any other value gets a colour from a ten-colour palette. - Category: one colour per topic, assigned the same way. An actor with several active topics is drawn as a pie with one slice per topic.
- Public sentiment: a diverging scale from white (neutral) toward red, rgb(220, 28, 42), for negative sentiment and green, rgb(0, 154, 70), for positive. Intensity is (|s| / max|s|)0.7, where max|s| is the largest absolute sentiment in the dataset. The 0.7 exponent lifts weak values so mild sentiment stays visible. Actors without a value are grey. Scores come from news headlines (see Public interest + sentiment).
Halo
The halo is a ring behind each node whose radius grows linearly from the node's own radius to 50 px. It is scaled by the actor's position between the minimum and maximum of the chosen metric among visible actors. The metric is Public interest (news hits) or one of the four centralities.
Edges
Each relationship type has a fixed colour (the financial colour darkens in light theme for contrast). Width and line style follow the parameter rules under Network model. When an actor or edge is selected, everything outside its neighbourhood is dimmed to 20% opacity.
9. Map
Building the overlay
Map geometry is prepared ahead of time by scripts/build-map-overlay.mjs:
- Optionally downloads the buildings tab as CSV (
--sync), checking that the first header isBIN. - Reads the island boundary and the
bin-<BIN>.geojsonfootprint for every row with a BIN. - Reprojects all coordinates from WGS84 longitude/latitude (EPSG:4326) to Web Mercator metres (EPSG:3857): x = Rλ, y = R ln tan(π/4 + φ/2), with R = 6,378,137 m. This keeps building proportions true instead of stretching them east–west.
- Writes everything into one SVG (
map-overlay.svg), so the app makes one request instead of about 70. Each footprint path carries its BIN, name and linked actor as data attributes. - Matches each
point - "<name>"row to apoint-<name>.svgfile and writes the pairs tomap-points.json.
Site points
Sites without a building footprint are drawn as individual SVGs (exported from Rhino), each containing the island
outline in a group with id="boundary" and exactly one circle. The app applies any SVG transforms,
measures the circle's position within the bounding box of the traced outline, and maps it to the same relative
position within the real island's bounding box. Points therefore only need to be traced against the island
outline, with no shared coordinate system.
Placing actors
- An actor linked to one or more footprints is anchored at the average of those footprints' centroids.
- An actor linked to a site point is anchored at that point, which takes priority over footprints.
- The map is fitted, preserving its aspect ratio, into 60% of the network's extent and centred on it.
- Anchored actors are fixed in place. ForceAtlas2 then runs again with the same settings, so unanchored actors settle around the anchors they are connected to. Gravity is recentred on the map so free actors gather around the island rather than the origin.
- Leaving Map mode restores each anchored actor's previous position.
Unanchored actors' positions on the map are therefore relational, not geographic. They show who an actor is connected to, not where it is.
10. Timeline
Timeline mode (beta) arranges actors by their active years:
- Each actor's bar runs from
year_starttoyear_end, clipped to the visible range. A missing start or end extends the bar to that edge of the range. - Actors with no years at all are counted but not drawn.
- Actors joined by Predecessor–Successor relationships are merged into a single row, ordered so each predecessor comes before its successor (a topological sort, with ties broken by start year then name).
- Rows are nested under their parent through Parent–Child relationships. When a row has several parents, the closest earlier-starting parent is chosen.
- Rows are ordered by start year, then alphabetically.
11. Libraries + APIs
The app is a static site with no server: all loading, analysis and rendering runs in the browser.
| Library | Version | Role |
|---|---|---|
| graphology | 0.25 | In-memory graph data structure (mixed multigraph). |
| graphology-metrics | 2.4 | Closeness, betweenness and eigenvector centrality. |
| graphology-layout-forceatlas2 | 0.10 | Force-directed layout. |
| graphology-layout | 0.6 | Circular starting layout. |
| sigma.js | 3 | WebGL rendering of nodes, edges and labels; camera; node animation. Extra canvas layers draw halos, pies, line styles and the map. |
| @sigma/edge-curve | 3.1 | Curved edges and arrows for parallel relationships. |
| Papa Parse | 5.4 | CSV parsing of sheet and snapshot data. |
| html2canvas | 1.4 | Raster (PNG) export of the workspace and legend. |
| jsPDF + svg2pdf.js | 4.2 / 2.8 | Vector PDF export. |
| Vite | 5.4 | Development server and production build. |
Leaflet is listed as a dependency but not used. The map is drawn on sigma's own canvas layers so it pans and zooms with the network.
External services
- Google Sheets “Publish to the web” CSV endpoint: live actor, adjacency and buildings data. No API key is needed; the sheet must be published.
- NYC Open Data (Socrata): BUILDING footprints and PLUTO, retrieved once and stored with the app.
- OpenStreetMap: the island boundary, retrieved once and stored with the app.
- Google News RSS search (offline, via
analyze_actors.py): headlines for public interest and sentiment. No API key; results are cached and bundled, so the app makes no news requests.
The news script is written in Python and uses only the standard library (urllib, xml.etree,
csv, hashlib, concurrent.futures) plus
vaderSentiment 3.3.2
for scoring.
12. Limitations
- Coverage: the network contains only relationships the team has documented. A missing edge means “not recorded”, not “no relationship”, and centrality inherits that bias.
- Unweighted metrics: centrality ignores funding amounts, areas and collaboration closeness. Every relationship counts equally.
- Relative scores: because centrality is recomputed on the visible network, scores are not comparable between different filter settings.
- Mixed relationship semantics: direction means different things for different types (ownership, funding, succession), so directed measures such as closeness and eigenvector are most interpretable with a single relationship type, or a few related types, switched on.
- News metrics are a proxy for public visibility and perception, matched by actor name. They are only available for actors with news coverage.
- Sentiment is headline tone, not opinion: VADER scores the wording of headlines, not readers' views of the actor. A negative headline may be about an event rather than the actor itself, and VADER handles irony, policy language and historical context poorly.
- Small, non-random samples: headlines are whatever Google News ranks first, so heavily reported events and outlets that publish often weigh more. Scores from only a few headlines (61 actors have fewer than 5) are unreliable.
- Name matching: generic names, abbreviations and job titles can match unrelated articles or none. Historical actors and repeated people are searched in present-day coverage, regardless of their active years.
- Capped interest: hits stop at 100, so the most-covered actors are indistinguishable, and counts change as Google's index changes.
- Lot-level data: PLUTO values describe tax lots, which can contain several buildings.