Skip to content

Commit 4655160

Browse files
Add "Asset type" group-by option to the over-time plot (#105)
* Initial plan * Add 'asset type' group by plot for over-time view Agent-Logs-Url: https://github.com/dandi/access-page/sessions/92e80034-ec48-4848-9ec9-00abb0fdb238 Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> * Fix label key lookups to use effective_aggregation instead of TIME_AGGREGATION Agent-Logs-Url: https://github.com/dandi/access-page/sessions/92e80034-ec48-4848-9ec9-00abb0fdb238 Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> * Remove redundant 'daily' key from bin_label_prefix lookup in asset_type branch Agent-Logs-Url: https://github.com/dandi/access-page/sessions/92e80034-ec48-4848-9ec9-00abb0fdb238 Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> * Hide 'Dandisets' group-by option for non-archive dandisets; always show legend for asset type Agent-Logs-Url: https://github.com/dandi/access-page/sessions/a977682e-c073-4a33-8e98-dc226e82ddd1 Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com>
1 parent 7308dca commit 4655160

3 files changed

Lines changed: 125 additions & 36 deletions

File tree

package-lock.json

Lines changed: 0 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@
163163
<select id="over_time_group_by">
164164
<option value="none">None</option>
165165
<option value="dandisets">Dandisets</option>
166+
<option value="asset_type">Asset type</option>
166167
</select>
167168
</div>
168169
</div>

src/plots.js

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,17 +110,30 @@ function syncThemeToggleIcon() {
110110
// ────────────────────────────────────────────────────────────────────────────
111111

112112
/**
113-
* Shows or hides the "Group by" control for the over-time plot.
114-
* The control is only relevant when the plot view is active and the archive
115-
* dandiset is selected — it has no effect for individual dandiset pages or
116-
* when the table view is shown.
113+
* Shows or hides the "Group by" control for the over-time plot, and
114+
* shows/hides the "Dandisets" option based on whether the archive is selected.
115+
* When a non-archive dandiset is selected, the "Dandisets" option is hidden and
116+
* any active "dandisets" group-by is reset to "none".
117+
* The whole control is hidden when the table view is active.
117118
*/
118119
function apply_over_time_group_by_visibility() {
119120
const container = document.getElementById("over_time_group_by_container");
120121
if (!container) return;
122+
container.style.display = !USE_OVER_TIME_TABLE ? "" : "none";
123+
121124
const selector = document.getElementById("dandiset_selector");
122125
const isArchive = !selector || selector.value === "archive";
123-
container.style.display = (!USE_OVER_TIME_TABLE && isArchive) ? "" : "none";
126+
const dandisets_option = document.querySelector('#over_time_group_by option[value="dandisets"]');
127+
if (dandisets_option) {
128+
dandisets_option.hidden = !isArchive;
129+
}
130+
131+
// If "dandisets" was selected but a non-archive dandiset is now active, reset to "none"
132+
const groupBySelector = document.getElementById("over_time_group_by");
133+
if (!isArchive && groupBySelector && groupBySelector.value === "dandisets") {
134+
groupBySelector.value = "none";
135+
OVER_TIME_GROUP_BY = "none";
136+
}
124137
}
125138

126139
/**
@@ -431,7 +444,7 @@ function syncFromUrl() {
431444
const groupBySelector = document.getElementById("over_time_group_by");
432445
if (groupBySelector) {
433446
const urlGroupBy = params.get("group_by");
434-
OVER_TIME_GROUP_BY = ["none", "dandisets"].includes(urlGroupBy) ? urlGroupBy : "none";
447+
OVER_TIME_GROUP_BY = ["none", "dandisets", "asset_type"].includes(urlGroupBy) ? urlGroupBy : "none";
435448
groupBySelector.value = OVER_TIME_GROUP_BY;
436449
}
437450

@@ -939,6 +952,27 @@ function parse_by_day_tsv(text) {
939952
};
940953
}
941954

955+
/**
956+
* Parses a by_asset_type_per_week TSV text string.
957+
* Returns { dates, asset_types, series_map } where:
958+
* - dates: string[] of week_start dates
959+
* - asset_types: string[] of column names (excluding week_start)
960+
* - series_map: Map from asset_type -> number[] of weekly bytes
961+
*/
962+
function parse_by_asset_type_per_week_tsv(text) {
963+
const rows = text.split("\n").filter((row) => row.trim() !== "");
964+
if (rows.length < 2) throw new Error("TSV file does not contain enough data.");
965+
const headers = rows[0].split("\t");
966+
const asset_types = headers.slice(1);
967+
const data_rows = rows.slice(1).map((row) => row.split("\t"));
968+
const dates = data_rows.map((row) => row[0]);
969+
const series_map = new Map();
970+
asset_types.forEach((type, col_idx) => {
971+
series_map.set(type, data_rows.map((row) => parseInt(row[col_idx + 1], 10) || 0));
972+
});
973+
return { dates, asset_types, series_map };
974+
}
975+
942976
/**
943977
* Builds the shared layout options used by both single-series and grouped
944978
* over-time plots.
@@ -1041,6 +1075,90 @@ function load_over_time_plot(dandiset_id) {
10411075
const section_el = over_time_el && over_time_el.closest('.view-section');
10421076
if (section_el) section_el.style.minHeight = "";
10431077

1078+
// ── Grouped mode: overlay asset types ────────────────────────────────────
1079+
if (OVER_TIME_GROUP_BY === "asset_type") {
1080+
const tsv_url = `${BASE_TSV_URL}/${dandiset_id}/by_asset_type_per_week.tsv`;
1081+
1082+
return fetch(tsv_url)
1083+
.then((r) => {
1084+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
1085+
return r.text();
1086+
})
1087+
.then((text) => {
1088+
const { dates: raw_dates, asset_types, series_map } = parse_by_asset_type_per_week_tsv(text);
1089+
1090+
// Data is weekly; treat "daily" aggregation as weekly since no finer data exists
1091+
const effective_aggregation = TIME_AGGREGATION === "daily" ? "weekly" : TIME_AGGREGATION;
1092+
1093+
const bin_label_prefix = {
1094+
weekly: "Week of ", monthly: "Month: ", yearly: "Year: ",
1095+
}[effective_aggregation];
1096+
1097+
const all_dates_for_layout = [];
1098+
1099+
const plot_info = asset_types.map((type, i) => {
1100+
const raw_bytes = series_map.get(type);
1101+
const agg = aggregate_by_timebin(raw_dates, raw_bytes, effective_aggregation);
1102+
const plot_data = USE_CUMULATIVE ? make_cumulative(agg.bytes_sent) : agg.bytes_sent;
1103+
const human_readable = plot_data.map((b) => format_bytes(b));
1104+
const color = DANDISET_BAR_COLORS[i % DANDISET_BAR_COLORS.length];
1105+
all_dates_for_layout.push(...agg.dates);
1106+
return {
1107+
type: "bar",
1108+
name: type,
1109+
x: agg.dates,
1110+
y: plot_data,
1111+
text: agg.dates.map((date, idx) =>
1112+
`${type}<br>${bin_label_prefix}${date}<br>${human_readable[idx]}`
1113+
),
1114+
textposition: "none",
1115+
hoverinfo: "text",
1116+
marker: { color },
1117+
};
1118+
});
1119+
1120+
const unique_dates = [...new Set(all_dates_for_layout)].sort();
1121+
const layout = build_over_time_layout(unique_dates);
1122+
layout.barmode = "overlay";
1123+
layout.showlegend = true;
1124+
layout.legend = { title: { text: "Asset type" } };
1125+
1126+
// Override title for "daily" since we show weekly granularity
1127+
if (!USE_CUMULATIVE && TIME_AGGREGATION === "daily") {
1128+
layout.title.text = "Usage per week";
1129+
}
1130+
1131+
Plotly.newPlot(plot_element_id, plot_info, layout);
1132+
1133+
// Table: show total bytes per time bin (sum across all asset types)
1134+
const total_bytes = raw_dates.map((_, i) =>
1135+
asset_types.reduce((sum, type) => sum + (series_map.get(type)[i] || 0), 0)
1136+
);
1137+
const agg_total = aggregate_by_timebin(raw_dates, total_bytes, effective_aggregation);
1138+
const combined = agg_total.dates.map((date, i) => ({ date, bytes: agg_total.bytes_sent[i] }));
1139+
const per_bin_titles = {
1140+
weekly: "Usage per week",
1141+
monthly: "Usage per month", yearly: "Usage per year",
1142+
};
1143+
const date_col_labels = {
1144+
weekly: "Week of", monthly: "Month", yearly: "Year",
1145+
};
1146+
render_sortable_table("over_time_table", per_bin_titles[effective_aggregation], [
1147+
{ label: date_col_labels[effective_aggregation], key: "date", numeric: false },
1148+
{ label: "Usage", key: "bytes", numeric: true },
1149+
], combined, tsv_url);
1150+
1151+
apply_view_mode(plot_element_id, "over_time_table", USE_OVER_TIME_TABLE);
1152+
})
1153+
.catch((error) => {
1154+
console.error("Error in asset type grouped over-time plot:", error);
1155+
const plot_element = document.getElementById(plot_element_id);
1156+
if (plot_element) {
1157+
plot_element.innerText = "Failed to load data for asset type grouped plot.";
1158+
}
1159+
});
1160+
}
1161+
10441162
// ── Grouped mode: overlay top-N dandisets (archive view only) ────────────
10451163
if (OVER_TIME_GROUP_BY === "dandisets" && dandiset_id === "archive") {
10461164
const top_dandiset_ids = Object.entries(ALL_DANDISET_TOTALS)

0 commit comments

Comments
 (0)