@@ -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 */
118119function 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