guidepost 0.2.14__tar.gz → 0.2.15__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ include LICENSE
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: guidepost
3
- Version: 0.2.14
3
+ Version: 0.2.15
4
4
  Summary: Guidepost. An overview visualization for understanding supercomputer queue data.
5
5
  Home-page: https://github.com/cscully-allison/guidepost
6
6
  Author: Connor Scully-Allison
@@ -22,6 +22,7 @@ Dynamic: classifier
22
22
  Dynamic: description
23
23
  Dynamic: description-content-type
24
24
  Dynamic: home-page
25
+ Dynamic: license-file
25
26
  Dynamic: requires-dist
26
27
  Dynamic: requires-python
27
28
  Dynamic: summary
@@ -212,7 +213,7 @@ Guidepost is licensed under the MIT License. See the `LICENSE` file for details.
212
213
 
213
214
  ## Acknowledgments
214
215
 
215
- Guidepost was developed under the auspices and with funding provided by the National Renewable Energy Laboratory (NREL).
216
+ Guidepost was developed under the auspices and with funding provided by the National Renewable Energy Laboratory (NREL), the National Science Foundation under NSF IIS-1844573 and IIS-2324465, and the Department of Energy under DE-SC0022044 and DE-SC0024635.
216
217
 
217
218
  ---
218
219
 
@@ -184,7 +184,7 @@ Guidepost is licensed under the MIT License. See the `LICENSE` file for details.
184
184
 
185
185
  ## Acknowledgments
186
186
 
187
- Guidepost was developed under the auspices and with funding provided by the National Renewable Energy Laboratory (NREL).
187
+ Guidepost was developed under the auspices and with funding provided by the National Renewable Energy Laboratory (NREL), the National Science Foundation under NSF IIS-1844573 and IIS-2324465, and the Department of Energy under DE-SC0022044 and DE-SC0024635.
188
188
 
189
189
  ---
190
190
 
@@ -1,11 +1,14 @@
1
1
  import * as d3 from "https://esm.sh/d3@7";
2
+ // import * as d3 from "d3";
3
+
4
+
2
5
  //layout vars
3
6
  const FACET_LAYOUT = {
4
7
  outer_margin: 30
5
8
  }
6
9
 
7
10
  const OVERVIEW_LAYOUT = {
8
- width: 1100,
11
+ width: 1000,
9
12
  height: 300,
10
13
  outer_margin: 10,
11
14
  inner_padding: 30
@@ -54,6 +57,8 @@ const num_cols = 150;
54
57
 
55
58
  const MIN_BAR_WIDTH = 45;
56
59
 
60
+ const SHARED_X_SCALE = false
61
+
57
62
  // COLORS
58
63
  const BLUE = 'rgba(32, 61, 192, 0.7)';
59
64
  const RICH_BLUE = 'rgb(32, 61, 192)';
@@ -377,8 +382,11 @@ class JSModel{
377
382
  let current_bins = this.faceted_bins[fac].column;
378
383
  let sum_stats = this.faceted_sum_stats[fac];
379
384
 
385
+ // console.log("CALC BOX METRICS: ", fac, current_bins, x_axis_thresholds, y_axis_thresholds);
386
+
380
387
  // Iterate over the columns that divide the data along the x axis
381
388
  for(let bin in current_bins){
389
+
382
390
  let filtered_bin;
383
391
 
384
392
  //Do not filter if no filter is specified currently
@@ -400,34 +408,72 @@ class JSModel{
400
408
  temp_box_stats.bins = [];
401
409
 
402
410
  //box bins for this column
403
- let row_bins = d3.bin()
404
- .value(d => d[this.vars.y])
405
- .domain([sum_stats.y.min, sum_stats.y.max]).thresholds(y_axis_thresholds)(filtered_bin);
411
+ function binValues(values, thresholds, accessor) {
412
+ const bins = [];
413
+ // Create an empty bin for each interval between consecutive thresholds
414
+ for (let i = 0; i < thresholds.length - 1; i++) {
415
+ bins.push([]);
416
+ }
417
+ // Place each value in the appropriate bin
418
+ values.forEach(d => {
419
+ const val = accessor(d);
420
+ for (let i = 0; i < thresholds.length - 1; i++) {
421
+ // For the last bin, include values equal to the upper bound
422
+ if (val >= thresholds[i] && (i === thresholds.length - 2 || val < thresholds[i + 1])) {
423
+ bins[i].push(d);
424
+ break;
425
+ }
426
+ }
427
+ });
428
+ return bins;
429
+ }
406
430
 
431
+ const customBins = binValues(filtered_bin, y_axis_thresholds, d => d[this.vars.y]);
432
+
433
+ // Process each bin's summary statistics and update color scale range
434
+ temp_box_stats.bins = customBins.map((bin, index) => {
435
+ const stats = this.get_summary_stats(bin, this.vars.color);
436
+ stats.values = bin;
437
+ stats.std_ratio = stats.std / this.faceted_sum_stats[fac].color.std;
438
+ stats.threshold = y_axis_thresholds[index];
439
+ this.color_scale_range[0] = Math.min(this.color_scale_range[0], stats[this.vars.color_agg]);
440
+ this.color_scale_range[1] = Math.max(this.color_scale_range[1], stats[this.vars.color_agg]);
441
+ return stats;
442
+ });
407
443
 
408
- //clamps down last bin(s) if more are produced than desired
409
- // idk why but d3 produces too many bins sometimes
410
- if(row_bins.length > y_axis_thresholds.length-1){
411
- let diff = (y_axis_thresholds.length-1)-row_bins.length;
412
- let head = row_bins.slice(0, diff);
413
444
 
414
- for(let i = row_bins.length-1; i > y_axis_thresholds.length-2; i--){
415
- head[head.length-1] = head[head.length-1].concat(row_bins[i]);
416
- }
417
- row_bins = head;
418
- }
445
+ // let row_bins = d3.bin()
446
+ // .value(d => d[this.vars.y])
447
+ // .domain([sum_stats.y.min, sum_stats.y.max]).thresholds(y_axis_thresholds)(filtered_bin);
419
448
 
420
- //load individual boxes of values with summary statistics describing them
421
- for(let index in row_bins){
422
- let row = row_bins[index];
423
- let sum_stats = this.get_summary_stats(row, this.vars.color);
424
- sum_stats.values = row;
425
- sum_stats['std_ratio'] = sum_stats.std/this.faceted_sum_stats[fac].color.std;
426
- sum_stats.threshold = y_axis_thresholds[index];
427
- temp_box_stats.bins.push(sum_stats);
428
- this.color_scale_range[0] = Math.min(this.color_scale_range[0], sum_stats[this.vars.color_agg]);
429
- this.color_scale_range[1] = Math.max(this.color_scale_range[1], sum_stats[this.vars.color_agg]);
430
- }
449
+ // // console.log("ROW BINS BEFORE CLAMP: ", row_bins.length, y_axis_thresholds.length);
450
+
451
+
452
+ // //clamps down last bin(s) if more are produced than desired
453
+ // // idk why but d3 produces too many bins sometimes
454
+ // if(row_bins.length > y_axis_thresholds.length-1){
455
+ // let diff = (y_axis_thresholds.length-1)-row_bins.length;
456
+ // let head = row_bins.slice(0, diff);
457
+
458
+ // for(let i = row_bins.length-1; i > y_axis_thresholds.length-2; i--){
459
+ // head[head.length-1] = head[head.length-1].concat(row_bins[i]);
460
+ // }
461
+ // row_bins = head;
462
+ // }
463
+
464
+ // // console.log("ROW BINS AFTER CLAMP: ", row_bins.length, y_axis_thresholds.length);
465
+
466
+ // //load individual boxes of values with summary statistics describing them
467
+ // for(let index in row_bins){
468
+ // let row = row_bins[index];
469
+ // let sum_stats = this.get_summary_stats(row, this.vars.color);
470
+ // sum_stats.values = row;
471
+ // sum_stats['std_ratio'] = sum_stats.std/this.faceted_sum_stats[fac].color.std;
472
+ // sum_stats.threshold = y_axis_thresholds[index];
473
+ // temp_box_stats.bins.push(sum_stats);
474
+ // this.color_scale_range[0] = Math.min(this.color_scale_range[0], sum_stats[this.vars.color_agg]);
475
+ // this.color_scale_range[1] = Math.max(this.color_scale_range[1], sum_stats[this.vars.color_agg]);
476
+ // }
431
477
 
432
478
  temp_box_stats.column_values = filtered_bin;
433
479
  this.faceted_bins[fac].column[bin] = temp_box_stats;
@@ -443,7 +489,21 @@ class JSModel{
443
489
  * @returns {Array} - The sanitized and initialized data.
444
490
  */
445
491
  sanitize_and_intialize_data(data){
446
- this.global_sum_stats = {x:{},y:{},color:{}};
492
+ this.global_sum_stats = {
493
+ x:{
494
+ max: Number.MIN_SAFE_INTEGER,
495
+ min: Number.MAX_SAFE_INTEGER
496
+ },
497
+ y:{
498
+ max: Number.MIN_SAFE_INTEGER,
499
+ min: Number.MAX_SAFE_INTEGER
500
+ },
501
+ color:{
502
+ max: Number.MIN_SAFE_INTEGER,
503
+ min: Number.MAX_SAFE_INTEGER
504
+ },
505
+ num_cols: 0
506
+ };
447
507
  for(let fac of this.facets){
448
508
  //store data about what types of scales x and y are
449
509
  this.scale_types[fac] = {
@@ -466,14 +526,6 @@ class JSModel{
466
526
  }
467
527
 
468
528
 
469
- // this.global_sum_stats.x.max = Math.max(this.faceted_sum_stats[fac].x.max, this.global_sum_stats.x.max);
470
- // this.global_sum_stats.y.max = Math.max(this.faceted_sum_stats[fac].y.max, this.global_sum_stats.y.max);
471
- // this.global_sum_stats.color.max = Math.max(this.faceted_sum_stats[fac].color.max, this.global_sum_stats.color.max);
472
-
473
- // this.global_sum_stats.x.min = Math.min(this.faceted_sum_stats[fac].x.min, this.global_sum_stats.x.min);
474
- // this.global_sum_stats.y.min = Math.min(this.faceted_sum_stats[fac].y.min, this.global_sum_stats.y.min);
475
- // this.global_sum_stats.color.min = Math.min(this.faceted_sum_stats[fac].color.min, this.global_sum_stats.color.max);
476
-
477
529
 
478
530
  data[fac] = this.sanitize_data_for_log(data[fac], this.vars.y);
479
531
 
@@ -484,9 +536,20 @@ class JSModel{
484
536
 
485
537
  let sum_stats = this.faceted_sum_stats[fac];
486
538
 
539
+ this.global_sum_stats.x.max = Math.max(this.faceted_sum_stats[fac].x.max, this.global_sum_stats.x.max);
540
+ this.global_sum_stats.y.max = Math.max(this.faceted_sum_stats[fac].y.max, this.global_sum_stats.y.max);
541
+ this.global_sum_stats.color.max = Math.max(this.faceted_sum_stats[fac].color.max, this.global_sum_stats.color.max);
542
+
543
+ this.global_sum_stats.x.min = Math.min(this.faceted_sum_stats[fac].x.min, this.global_sum_stats.x.min);
544
+ this.global_sum_stats.y.min = Math.min(this.faceted_sum_stats[fac].y.min, this.global_sum_stats.y.min);
545
+ this.global_sum_stats.color.min = Math.min(this.faceted_sum_stats[fac].color.min, this.global_sum_stats.color.max);
546
+
487
547
 
488
548
  this.faceted_bins[fac] = {}
489
549
 
550
+
551
+ console.log("SUM STATS: ", fac, sum_stats);
552
+
490
553
  //conditional x axis thresholds based on time or numbers
491
554
  // important for calculating the scales which layout the columns
492
555
  // of the "heatmap"
@@ -503,7 +566,7 @@ class JSModel{
503
566
  // just do linerats if not
504
567
  if(this.is_more_than_n_orders_of_magnitude(sum_stats.x.min, sum_stats.x.max, 3)){
505
568
  this.scale_types[fac].x.log = true;
506
- this.x_axis_thresholds[fac] = this.logScale(this.log_values_floor, sum_stats.x.max, num_cols);
569
+ this.x_axis_thresholds[fac] = this.logScale(this.log_values_floor, sum_stats.x.max+1, num_cols-1);
507
570
  this.faceted_bins[fac].column = d3.bin()
508
571
  .value(d => d[this.vars.x])
509
572
  .domain([this.log_values_floor, sum_stats.x.max])
@@ -512,7 +575,7 @@ class JSModel{
512
575
  }
513
576
  else{
514
577
  this.scale_types[fac].x.linear = true;
515
- this.x_axis_thresholds[fac] = this.linearScale(sum_stats.x.min, sum_stats.x.max, num_rows);
578
+ this.x_axis_thresholds[fac] = this.linearScale(sum_stats.x.min, sum_stats.x.max+1, num_cols-1);
516
579
  this.faceted_bins[fac].column = d3.bin()
517
580
  .value(d => d[this.vars.x])
518
581
  .domain([sum_stats.x.min, sum_stats.x.max])
@@ -524,9 +587,11 @@ class JSModel{
524
587
  if(this.is_more_than_n_orders_of_magnitude(sum_stats.y.min, sum_stats.y.max, 3)){
525
588
  this.scale_types[fac].y.log = true;
526
589
  this.y_axis_thresholds[fac] = this.logScale(this.log_values_floor, sum_stats.y.max, num_rows);
590
+ console.log("Y AXIS THRESHOLDS LOG: ", fac, this.y_axis_thresholds[fac].length);
527
591
  } else {
528
592
  this.scale_types[fac].y.linear = true;
529
593
  this.y_axis_thresholds[fac] = this.linearScale(sum_stats.y.min, sum_stats.y.max, num_rows);
594
+ console.log("Y AXIS THRESHOLDS LINEAR: ", fac, this.y_axis_thresholds[fac].length);
530
595
  }
531
596
 
532
597
  sum_stats.col_counts = {
@@ -539,6 +604,9 @@ class JSModel{
539
604
  sum_stats.col_counts.min = Math.min(sum_stats.col_counts.min, bin.length);
540
605
  }
541
606
 
607
+
608
+ this.global_sum_stats.num_cols = Math.max(this.faceted_bins[fac].column.length, this.global_sum_stats.num_cols);
609
+
542
610
  // temporary as Y AXIS IS FIXED LOG
543
611
  this.calculate_box_metrics(fac, this.x_axis_thresholds[fac], this.y_axis_thresholds[fac]);
544
612
  this.calc_row_major_counts(fac);
@@ -554,6 +622,7 @@ class JSModel{
554
622
  this.categorical_bins[fac] = Object.keys(cat_counts).map((key) => { return {"key": key, "val":cat_counts[key]} }).sort((a, b) => b['val'] - a['val']);
555
623
  }
556
624
 
625
+
557
626
  return data;
558
627
  }
559
628
 
@@ -582,8 +651,6 @@ class JSModel{
582
651
  */
583
652
  filter_data_by_category(filter, facet, source, targets){
584
653
 
585
- console.log("DATA FILTERS: ", filter)
586
-
587
654
  this.faceted_states[facet].filter = filter;
588
655
 
589
656
  // is anything pinned
@@ -635,9 +702,6 @@ class JSModel{
635
702
 
636
703
  }
637
704
 
638
- console.log(this.brushed_ranges[facet].x_range, this.brushed_ranges[facet].y_range);
639
-
640
-
641
705
 
642
706
  if(this.brushed_ranges[facet].x_range.length != 0){
643
707
  for(let bin of this.faceted_bins[facet].column){
@@ -683,8 +747,6 @@ class JSModel{
683
747
  }
684
748
  }
685
749
 
686
- console.log("TEST", test);
687
-
688
750
  this.anywidget_model.set("selected_records", JSON.stringify(return_ids));
689
751
  this.anywidget_model.save_changes();
690
752
 
@@ -815,7 +877,7 @@ class SmartScale {
815
877
  if(this.model.is_more_than_n_orders_of_magnitude(this.domain[0], this.domain[1], 3)){
816
878
  return d3.scaleLog().domain([this.model.log_values_floor, this.domain[1]]).range(this.range);
817
879
  } else {
818
- return d3.scaleLinear().domain([this.domain[0], this.domain[1]]).range(this.range);
880
+ return d3.scaleLinear().domain(this.domain).range(this.range);
819
881
  }
820
882
  } else {
821
883
  throw new Error("Unsupported domain type");
@@ -894,16 +956,23 @@ class Heatmap{
894
956
  // .domain(this.model.faceted_bins[this.facet].column.keys())
895
957
  // .range([OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.width - OVERVIEW_LAYOUT.inner_padding]);
896
958
 
897
- this.scale_x = new SmartScale([sum_stats.x.min, sum_stats.x.max],
898
- [OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.width-OVERVIEW_LAYOUT.inner_padding],
899
- this.model);
900
- // d3.scaleUtc()
901
- // .domain([new Date(sum_stats.x.min), this.model.addDays(new Date(sum_stats.x.max),1)])
902
- // .range([OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.width-OVERVIEW_LAYOUT.inner_padding]);
959
+
960
+ if(SHARED_X_SCALE){
961
+ this.scale_x = new SmartScale([this.model.global_sum_stats.x.min, this.model.global_sum_stats.x.max],
962
+ [OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.width-OVERVIEW_LAYOUT.inner_padding],
963
+ this.model);
964
+ }
965
+ else{
966
+ this.scale_x = new SmartScale([sum_stats.x.min, sum_stats.x.max],
967
+ [OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.width-OVERVIEW_LAYOUT.inner_padding],
968
+ this.model);
969
+
970
+ }
971
+
903
972
 
904
973
  //Determine if y scale is log or linear based on input data
905
- console.log(this.model.scale_types[this.facet]);
906
974
  if(this.model.scale_types[this.facet].y.log){
975
+
907
976
  this.scale_y = d3.scaleLog()
908
977
  .domain([this.model.log_values_floor, sum_stats.y.max])
909
978
  .range([OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding]);
@@ -1043,19 +1112,24 @@ class Heatmap{
1043
1112
  * Raises and zooms on a column slightly
1044
1113
  */
1045
1114
  focus_col(update_element){
1046
- let self = this;
1047
-
1048
- let base_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.faceted_bins[self.facet].column.length))
1049
-
1050
- self.scale_y_blocks.range([OVERVIEW_LAYOUT.inner_padding-(zoom_factor_v/2), OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding + (zoom_factor_v/2)]);
1115
+ let self = this;
1116
+ let base_width;
1117
+ if(SHARED_X_SCALE){
1118
+ base_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.global_sum_stats.num_cols))
1119
+ }
1120
+ else{
1121
+ base_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.faceted_bins[self.facet].column.length))
1122
+ }
1123
+
1124
+ self.scale_y_blocks.range([OVERVIEW_LAYOUT.inner_padding-(zoom_factor_v/2), OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding + (zoom_factor_v/2)]);
1051
1125
 
1052
- update_element.raise();
1053
-
1054
- update_element.selectAll('.row')
1055
- .attr('width', ()=>{return base_width + zoom_factor_h})
1056
- .attr('height', ()=>{return ( (OVERVIEW_LAYOUT.height + zoom_factor_v) - 2*OVERVIEW_LAYOUT.inner_padding) / self.model.faceted_bins[self.facet].column[0].bins.length})
1057
- .attr('y', (d, i)=>{return self.scale_y_blocks(i) - OVERVIEW_LAYOUT.inner_padding});
1126
+ update_element.raise();
1058
1127
 
1128
+ update_element.selectAll('.row')
1129
+ .attr('width', ()=>{return base_width + zoom_factor_h})
1130
+ .attr('height', ()=>{return ( (OVERVIEW_LAYOUT.height + zoom_factor_v) - 2*OVERVIEW_LAYOUT.inner_padding) / self.model.faceted_bins[self.facet].column[0].bins.length})
1131
+ .attr('y', (d, i)=>{return self.scale_y_blocks(i) - OVERVIEW_LAYOUT.inner_padding});
1132
+
1059
1133
  update_element.selectAll('.col-bg')
1060
1134
  .attr('width', ()=>{return base_width + zoom_factor_h})
1061
1135
  .attr('height', ()=>{return ( (OVERVIEW_LAYOUT.height + zoom_factor_v) - 2*OVERVIEW_LAYOUT.inner_padding)})
@@ -1075,9 +1149,14 @@ class Heatmap{
1075
1149
  */
1076
1150
  unfocus_col(update_element){
1077
1151
  let self = this;
1078
-
1079
-
1080
- let base_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.faceted_bins[self.facet].column.length))
1152
+ let base_width;
1153
+ if(SHARED_X_SCALE){
1154
+ base_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.global_sum_stats.num_cols))
1155
+ }
1156
+ else{
1157
+ base_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.faceted_bins[self.facet].column.length))
1158
+ }
1159
+
1081
1160
  self.scale_y_blocks.range([OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding]);
1082
1161
 
1083
1162
 
@@ -1130,9 +1209,16 @@ class Heatmap{
1130
1209
  render(){
1131
1210
  const self = this;
1132
1211
 
1133
- console.log(this.model.faceted_bins[this.facet].column);
1134
1212
 
1135
- let base_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.faceted_bins[self.facet].column.length))
1213
+ let base_width = 0;
1214
+ if(SHARED_X_SCALE){
1215
+ base_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.global_sum_stats.num_cols))
1216
+ }
1217
+ else{
1218
+ base_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.faceted_bins[self.facet].column.length))
1219
+ }
1220
+
1221
+
1136
1222
 
1137
1223
  if(self.model.row_major_counts[self.facet].length < 2){
1138
1224
  this.view
@@ -1142,6 +1228,7 @@ class Heatmap{
1142
1228
  .attr('transform', `translate(${draw_width/2},${draw_height/2})`)
1143
1229
  }
1144
1230
  else{
1231
+
1145
1232
  this.view
1146
1233
  .selectAll('.column')
1147
1234
  .data(this.model.faceted_bins[this.facet].column)
@@ -1214,6 +1301,9 @@ class Heatmap{
1214
1301
  )
1215
1302
  col.on('mouseenter', function (e, d){
1216
1303
  delete self.cached_bins['hover'];
1304
+
1305
+ console.log("HOVERING OVER: ", d);
1306
+
1217
1307
  self.focus_col(d3.select(e.target));
1218
1308
  if(!Object.keys(self.cached_bins).includes(String(d.threshold))){
1219
1309
  let dt_text_selection = d3.select(e.target).select('.text-field');
@@ -1364,10 +1454,17 @@ class Histogram{
1364
1454
  let y_offset = Y_VARIABLE_OFFSET + OVERVIEW_LAYOUT.height + HISTOGRAM_LAYOUT.outer_margin;
1365
1455
 
1366
1456
 
1457
+
1367
1458
  //create the histograms
1368
1459
  let h_hist = this.parent.append('g')
1369
1460
  .attr('class', 'faceted-h-hist')
1370
1461
  .attr('transform', `translate(${x_offset},${y_offset})`);
1462
+
1463
+ h_hist.append('rect')
1464
+ .attr('width', this.width - 2*HISTOGRAM_LAYOUT.inner_padding)
1465
+ .attr('height', this.height - HISTOGRAM_LAYOUT.inner_padding)
1466
+ .attr('fill', 'rgba(240,240,240)')
1467
+ .attr('transform', `translate(${HISTOGRAM_LAYOUT.inner_padding},${0})`);;
1371
1468
 
1372
1469
  h_hist.append('g')
1373
1470
  .attr('class', 'left-axis')
@@ -1389,7 +1486,12 @@ class Histogram{
1389
1486
  .attr('text-anchor', 'middle')
1390
1487
  .attr('transform', `translate(${this.width/2},${this.height})`);
1391
1488
 
1489
+
1490
+ h_hist.append("g")
1491
+ .attr('class', 'bars');
1492
+
1392
1493
  this.view = h_hist;
1494
+
1393
1495
 
1394
1496
  this.brush = d3.brushX()
1395
1497
  .extent([[OVERVIEW_LAYOUT.inner_padding, 0], [OVERVIEW_LAYOUT.width - OVERVIEW_LAYOUT.inner_padding, this.height-HISTOGRAM_LAYOUT.inner_padding]])
@@ -1400,9 +1502,8 @@ class Histogram{
1400
1502
  select = selection.map(self.scale_x.scale.invert, self.scale_x.scale).map(d3.utcDay.round);
1401
1503
  }
1402
1504
  if(self.model.scale_types[self.facet]['x']['log'] || self.model.scale_types[self.facet]['x']['linear']){
1403
- select = selection.map(self.scale_x.scale.invert, self.scale_x.scale).map((d)=>{return Math.floor(d+1)});
1505
+ select = selection.map(self.scale_x.scale.invert, self.scale_x.scale).map((d)=>{return d});
1404
1506
  }
1405
- console.log(select);
1406
1507
  }else{
1407
1508
  select = [];
1408
1509
  }
@@ -1410,46 +1511,62 @@ class Histogram{
1410
1511
  });
1411
1512
 
1412
1513
  h_hist.append("g")
1514
+ .attr('class', 'h-brush')
1413
1515
  .call(this.brush);
1414
1516
  }
1415
1517
 
1416
1518
 
1417
1519
  else if(this.orientation == 'right'){
1418
1520
 
1419
- let x_offset = X_VARIABLE_OFFSET + OVERVIEW_LAYOUT.width;
1521
+ let x_offset = X_VARIABLE_OFFSET + OVERVIEW_LAYOUT.width - 5;
1420
1522
  let y_offset = Y_VARIABLE_OFFSET + VERT_HISTOGRAM_LAYOUT.outer_margin;
1421
1523
 
1422
1524
  let v_hist = this.parent.append('g')
1423
1525
  .attr('class', 'faceted-v-hist')
1424
1526
  .attr('transform', `translate(${x_offset},${y_offset})`);
1425
1527
 
1528
+ v_hist.append('rect')
1529
+ .attr('width', this.width)
1530
+ .attr('height', this.height - 2*HISTOGRAM_LAYOUT.inner_padding)
1531
+ .attr('fill', 'rgba(240,240,240)')
1532
+ .attr('transform', `translate(${0},${HISTOGRAM_LAYOUT.inner_padding})`);
1533
+ ;
1534
+
1426
1535
  v_hist.append('g')
1427
1536
  .attr('class', 'bot-axis')
1428
1537
  .call(d3.axisBottom().scale(this.scale_x).ticks(5))
1429
1538
  .attr('transform', `translate(${VERT_HISTOGRAM_LAYOUT.inner_padding*4},${VERT_HISTOGRAM_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding})`);
1430
1539
 
1431
- // v_hist.append('g')
1432
- // .attr('class', 'left-axis')
1433
- // .call(d3.axisLeft().scale(this.scale_y_inverse))
1434
- // .attr('transform', `translate(${VERT_HISTOGRAM_LAYOUT.inner_padding},${0})`);
1540
+ v_hist.append('g')
1541
+ .attr('class', 'left-axis')
1542
+ .call(d3.axisRight().scale(this.axis_scale_y_inverse))
1543
+ .attr('transform', `translate(${self.width-VERT_HISTOGRAM_LAYOUT.inner_padding},${0})`);
1435
1544
 
1436
1545
  this.brush = d3.brushY()
1437
1546
  .extent([[0, HISTOGRAM_LAYOUT.inner_padding], [this.width, this.height - OVERVIEW_LAYOUT.inner_padding]])
1438
1547
  .on("end", function({selection}){
1439
1548
  let select;
1440
1549
  if(selection){
1441
- select = selection.map(self.scale_y.invert, self.scale_y).map((d)=>{return Math.floor(d+1)})
1550
+ select = selection.map(self.scale_y.invert, self.scale_y).map((d)=>{return d+0.1})
1442
1551
  }else{
1443
1552
  select = [];
1444
1553
  }
1445
1554
  self.model.update_subselected_data(self.facet, [`${self.facet}_heatmap`, `${self.facet}_legend`], select, "y");
1446
1555
  });
1447
1556
 
1557
+
1558
+ v_hist.append("g")
1559
+ .attr('class', 'bars');
1560
+
1448
1561
  v_hist.append("g")
1562
+ .attr('class', 'v-brush')
1449
1563
  .call(this.brush);
1450
1564
 
1451
1565
  this.view = v_hist;
1452
1566
  }
1567
+
1568
+
1569
+
1453
1570
  }
1454
1571
 
1455
1572
  /**
@@ -1469,20 +1586,56 @@ class Histogram{
1469
1586
 
1470
1587
  //references OVERVIEW LAYOUT SIZES
1471
1588
  //BE CAREFUL
1472
- this.scale_x = new SmartScale([sum_stats.x.min, sum_stats.x.max],
1589
+ if(SHARED_X_SCALE){
1590
+ this.scale_x = new SmartScale([this.model.global_sum_stats.x.min, this.model.global_sum_stats.x.max],
1473
1591
  [OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.width-OVERVIEW_LAYOUT.inner_padding],
1474
1592
  this.model);
1593
+ }
1594
+ else{
1595
+ this.scale_x = new SmartScale([sum_stats.x.min, sum_stats.x.max],
1596
+ [OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.width-OVERVIEW_LAYOUT.inner_padding],
1597
+ this.model);
1598
+ }
1599
+
1475
1600
  }
1476
1601
 
1477
1602
  else if(this.orientation == 'right'){
1478
1603
 
1479
- this.scale_y = d3.scaleLinear()
1604
+ if(this.model.is_more_than_n_orders_of_magnitude(0, Math.max(...this.model.row_major_counts[this.facet]), 3)){
1605
+ let local_log_floor = 0.3
1606
+ this.scale_x = d3.scaleLog()
1607
+ .domain([local_log_floor, Math.max(...this.model.row_major_counts[this.facet])])
1608
+ .range([0, VERT_HISTOGRAM_LAYOUT.width - VERT_HISTOGRAM_LAYOUT.inner_padding]);
1609
+ }else{
1610
+ this.scale_x = d3.scaleLinear()
1611
+ .domain([0, Math.max(...this.model.row_major_counts[this.facet])])
1612
+ .range([0, VERT_HISTOGRAM_LAYOUT.width - VERT_HISTOGRAM_LAYOUT.inner_padding]);
1613
+ }
1614
+
1615
+
1616
+ if(this.model.scale_types[this.facet].y.log){
1617
+ this.axis_scale_y = d3.scaleLog()
1618
+ .domain([this.model.log_values_floor, sum_stats.y.max])
1619
+ .range([OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding]);
1620
+
1621
+ this.axis_scale_y_inverse = d3.scaleLog()
1622
+ .domain([sum_stats.y.max, this.model.log_values_floor])
1623
+ .range([OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding]);
1624
+ }
1625
+ else if(this.model.scale_types[this.facet].y.linear){
1626
+ this.axis_scale_y = d3.scaleLinear()
1627
+ .domain([sum_stats.y.min, sum_stats.y.max])
1628
+ .range([OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding]);
1629
+
1630
+ this.axis_scale_y_inverse = d3.scaleLinear()
1631
+ .domain([sum_stats.y.max, sum_stats.y.min])
1632
+ .range([OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding]);
1633
+ }
1634
+
1635
+ this.scale_y = d3.scaleLinear()
1480
1636
  .domain([num_rows-2, -1])
1481
1637
  .range([OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding]);
1482
1638
 
1483
- this.scale_x = d3.scaleLinear()
1484
- .domain([0, Math.max(...this.model.row_major_counts[this.facet])])
1485
- .range([0, VERT_HISTOGRAM_LAYOUT.width - VERT_HISTOGRAM_LAYOUT.inner_padding]);
1486
1639
  }
1487
1640
  }
1488
1641
 
@@ -1491,12 +1644,21 @@ class Histogram{
1491
1644
  */
1492
1645
  render(){
1493
1646
  const self = this;
1494
- let bar_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.faceted_bins[self.facet].column.length))
1647
+ let bar_width = 0;
1648
+ if(SHARED_X_SCALE){
1649
+ bar_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.global_sum_stats.num_cols))
1650
+ }
1651
+ else{
1652
+ bar_width = Math.min(MIN_BAR_WIDTH, (draw_width / self.model.faceted_bins[self.facet].column.length))
1653
+ }
1654
+
1655
+ let bar_layer = this.view.select('.bars');
1495
1656
 
1657
+
1496
1658
  if(self.model.row_major_counts[self.facet].length > 2){
1497
1659
  if(this.orientation == 'bottom'){
1498
- this.view.selectAll('.column')
1499
- .data(self.model.faceted_bins[self.facet].column, function(d){return this.id} )
1660
+ bar_layer.selectAll('.column')
1661
+ .data(self.model.faceted_bins[self.facet].column, function(){return this.id} )
1500
1662
  .join(
1501
1663
  function(enter){
1502
1664
  let col = enter.append('g')
@@ -1524,8 +1686,7 @@ class Histogram{
1524
1686
  }
1525
1687
 
1526
1688
  if(this.orientation == "right"){
1527
- this.view
1528
- .selectAll('.row')
1689
+ bar_layer.selectAll('.row')
1529
1690
  .data(self.model.row_major_counts[self.facet])
1530
1691
  .join(
1531
1692
  function(enter){
@@ -1535,7 +1696,9 @@ class Histogram{
1535
1696
 
1536
1697
  row.append('rect')
1537
1698
  .attr('class', 'bar')
1538
- .attr('width', (d)=>{return self.scale_x(d)})
1699
+ .attr('width', (d)=>{
1700
+ return self.scale_x(d) ? self.scale_x(d) : 0;
1701
+ })
1539
1702
  .attr('height', (d)=>{return draw_height / self.model.faceted_bins[self.facet].column[0].bins.length})
1540
1703
  .attr('fill', TAN);
1541
1704
 
@@ -1544,7 +1707,9 @@ class Histogram{
1544
1707
  function(update){
1545
1708
  update.select('.bar')
1546
1709
  .transition()
1547
- .attr('width', (d)=>{return self.scale_x(d)});
1710
+ .attr('width', (d)=>{
1711
+ return self.scale_x(d) ? self.scale_x(d) : 0;
1712
+ });
1548
1713
  },
1549
1714
  function(exit){
1550
1715
  exit.remove();
@@ -1552,6 +1717,7 @@ class Histogram{
1552
1717
  )
1553
1718
  }
1554
1719
  }
1720
+
1555
1721
  }
1556
1722
  }
1557
1723
 
@@ -1586,8 +1752,8 @@ class CategoricalBarChart{
1586
1752
 
1587
1753
  //create the histograms
1588
1754
 
1589
- let x_offset = X_VARIABLE_OFFSET + HISTOGRAM_LAYOUT.outer_margin + OVERVIEW_LAYOUT.width;
1590
- let y_offset = Y_VARIABLE_OFFSET + OVERVIEW_LAYOUT.height + HISTOGRAM_LAYOUT.outer_margin;
1755
+ let x_offset = X_VARIABLE_OFFSET + OVERVIEW_LAYOUT.width;
1756
+ let y_offset = Y_VARIABLE_OFFSET + HISTOGRAM_LAYOUT.outer_margin + OVERVIEW_LAYOUT.height ;
1591
1757
 
1592
1758
  let h_hist = this.parent.append('g')
1593
1759
  .attr('class', 'faceted-h-hist')
@@ -1648,15 +1814,31 @@ class CategoricalBarChart{
1648
1814
  setup_scales(){
1649
1815
  this.n = Math.min(this.model.categorical_bins[this.facet].length, this.n);
1650
1816
  let top_n_cats = this.model.categorical_bins[this.facet].slice(0,this.n);
1817
+
1818
+ this.max_bar_width = 30;
1819
+ this.drawable_width = (this.width-2*CAT_HISTOGRAM_LAYOUT.inner_padding);
1820
+ this.calc_bar_width = Math.min(this.max_bar_width, this.drawable_width/this.n);
1651
1821
 
1652
1822
  if(this.orientation == 'bottom'){
1653
- this.scale_y = d3.scaleLinear()
1654
- .domain([0, top_n_cats[0].val])
1655
- .range([0, this.height - CAT_HISTOGRAM_LAYOUT.inner_padding]);
1656
-
1657
- this.scale_y_inverse = d3.scaleLinear()
1658
- .domain([top_n_cats[0].val, 0])
1659
- .range([0, this.height - CAT_HISTOGRAM_LAYOUT.inner_padding]);
1823
+ if(this.model.is_more_than_n_orders_of_magnitude(0, top_n_cats[0].val, 3)){
1824
+ let local_log_floor = 0.3
1825
+ this.scale_y = d3.scaleLog()
1826
+ .domain([local_log_floor, top_n_cats[0].val])
1827
+ .range([0, this.height - CAT_HISTOGRAM_LAYOUT.inner_padding]);
1828
+
1829
+ this.scale_y_inverse = d3.scaleLog()
1830
+ .domain([top_n_cats[0].val, local_log_floor])
1831
+ .range([0, this.height - CAT_HISTOGRAM_LAYOUT.inner_padding]);
1832
+ }
1833
+ else{
1834
+ this.scale_y = d3.scaleLinear()
1835
+ .domain([0, top_n_cats[0].val])
1836
+ .range([0, this.height - CAT_HISTOGRAM_LAYOUT.inner_padding]);
1837
+
1838
+ this.scale_y_inverse = d3.scaleLinear()
1839
+ .domain([top_n_cats[0].val, 0])
1840
+ .range([0, this.height - CAT_HISTOGRAM_LAYOUT.inner_padding]);
1841
+ }
1660
1842
 
1661
1843
  //references OVERVIEW LAYOUT SIZES
1662
1844
  //BE CAREFUL
@@ -1664,7 +1846,8 @@ class CategoricalBarChart{
1664
1846
  .domain(top_n_cats.map((obj)=>{
1665
1847
  return obj.key;
1666
1848
  }))
1667
- .range([CAT_HISTOGRAM_LAYOUT.inner_padding, this.width - CAT_HISTOGRAM_LAYOUT.inner_padding]);
1849
+ .range([CAT_HISTOGRAM_LAYOUT.inner_padding, this.width - CAT_HISTOGRAM_LAYOUT.inner_padding])
1850
+ .padding(0.1);
1668
1851
  }
1669
1852
 
1670
1853
  else if(this.orientation == 'right'){
@@ -1684,11 +1867,10 @@ class CategoricalBarChart{
1684
1867
  */
1685
1868
  render(){
1686
1869
 
1870
+ const self = this;
1687
1871
  let top_n_cats = this.model.categorical_bins[this.facet].slice(0,this.n);
1688
1872
  let update_targets = [`${this.facet}_heatmap`, `${this.facet}_right_histogram`, `${this.facet}_bottom_histogram`, `${this.facet}_legend`];
1689
1873
 
1690
- const self = this;
1691
-
1692
1874
  if(self.model.row_major_counts[self.facet].length > 2){
1693
1875
 
1694
1876
  if(this.orientation == 'bottom'){
@@ -1705,17 +1887,18 @@ class CategoricalBarChart{
1705
1887
  let col = enter.append('g')
1706
1888
  .attr('class', 'column')
1707
1889
  .attr('transform', (d, i)=>{
1708
- if(self.scale_x(d.key)){
1709
- return `translate(${self.scale_x(d.key)}, ${CAT_HISTOGRAM_LAYOUT.inner_padding})`
1710
- }
1711
- return `translate(${0}, ${CAT_HISTOGRAM_LAYOUT.inner_padding})`
1890
+ const tickPos = self.scale_x(d.key);
1891
+ const bandWidth = self.scale_x.bandwidth();
1892
+ // Center bar if calc_bar_width < bandWidth
1893
+ const offset = (bandWidth - self.calc_bar_width) / 2;
1894
+ return `translate(${tickPos + offset}, ${CAT_HISTOGRAM_LAYOUT.inner_padding})`;
1712
1895
  });
1713
1896
 
1714
1897
  col.append('rect')
1715
1898
  .attr('class', 'bar')
1716
1899
  .attr('height', (d)=>{return self.scale_y(d.val)})
1717
1900
  // .attr('width', (d)=>{return ((HISTOGRAM_LAYOUT.width - 2*HISTOGRAM_LAYOUT.inner_padding) / faceted_bins[d.facet].x.length)})
1718
- .attr('width', (self.width-2*CAT_HISTOGRAM_LAYOUT.inner_padding)/self.n)
1901
+ .attr('width', self.calc_bar_width)
1719
1902
  .attr('fill', TAN)
1720
1903
  .attr(`transform`, (d)=>{return `translate(${0}, ${(CAT_HISTOGRAM_LAYOUT.height- self.scale_y(d.val))-2*CAT_HISTOGRAM_LAYOUT.inner_padding})`})
1721
1904
  .on('mouseover', function (e,d){
@@ -1910,7 +2093,6 @@ class Validator{
1910
2093
  */
1911
2094
  isValidDate(dateString) {
1912
2095
  const date_time = new Date(dateString);
1913
- console.log("AAAAA", date_time.getTime());
1914
2096
  return !isNaN(date_time.getTime());
1915
2097
  }
1916
2098
 
@@ -2002,6 +2184,15 @@ class Validator{
2002
2184
  return missing;
2003
2185
  }
2004
2186
 
2187
+
2188
+ // Function to coerce an entire column’s values to strings
2189
+ coerceColumnToString(columnData) {
2190
+ return Object.keys(columnData).reduce((result, key) => {
2191
+ result[key] = String(columnData[key]);
2192
+ return result;
2193
+ }, {});
2194
+ }
2195
+
2005
2196
  /**
2006
2197
  * Ensures that all values in this.var_specs are logically appropriate
2007
2198
  * @param {Object} this.var_specs - The variable specifications.
@@ -2023,11 +2214,11 @@ class Validator{
2023
2214
  if (typeof test_val !== 'number'){
2024
2215
  if(typeof test_val == 'string'){
2025
2216
  if(!this.isValidDate(test_val)){
2026
- incorrect.push({ key: key, value: this.var_specs[key], message: 'The x-axis aaaaa only supports floats, integers and dates. Please specify a different variable or verify that the datetime is properly formatted.' });
2217
+ incorrect.push({ key: key, value: this.var_specs[key], message: 'The x-axis only supports floats, integers and dates. Please specify a different variable or verify that the datetime is properly formatted.' });
2027
2218
  }
2028
2219
  }
2029
2220
  else {
2030
- incorrect.push({ key: key, value: this.var_specs[key], message: 'The x-axis bbbbbb only supports floats, integers and dates. Please specify a different variable or verify that the datetime is properly formatted.' });
2221
+ incorrect.push({ key: key, value: this.var_specs[key], message: 'The x-axis only supports floats, integers and dates. Please specify a different variable or verify that the datetime is properly formatted.' });
2031
2222
  }
2032
2223
  }
2033
2224
  }
@@ -2045,8 +2236,15 @@ class Validator{
2045
2236
  }
2046
2237
  else if (key === 'categorical'){
2047
2238
  let test_val = this.data[this.var_specs[key]][Object.keys(this.data[this.var_specs[key]])[0]];
2239
+ // For categorical variables, coerce data to strings if necessary.
2048
2240
  if(typeof test_val !== 'string'){
2049
- incorrect.push({ key: key, value: this.var_specs[key], message: 'The categorical view only supports categorical variables formatted as strings. Please specify a different column on your dataset or reformat an exisitng column.' });
2241
+ // Coerce the column data at this.data[this.var_specs[key]]
2242
+ this.data[this.var_specs[key]] = coerceColumnToString(this.data[this.var_specs[key]]);
2243
+ // Re-check the data type after coercion
2244
+ test_val = this.data[this.var_specs[key]][Object.keys(this.data[this.var_specs[key]])[0]];
2245
+ if(typeof test_val !== 'string'){
2246
+ incorrect.push({ key: key, value: this.var_specs[key], message: 'The categorical view only supports categorical variables formatted as strings. Please specify a different column on your dataset or reformat an existing column.' });
2247
+ }
2050
2248
  }
2051
2249
  }
2052
2250
  }
@@ -2144,7 +2342,6 @@ function render({model, el}){
2144
2342
 
2145
2343
  validator.data = data;
2146
2344
  is_valid = validator.validate();
2147
- console.log()
2148
2345
 
2149
2346
  if(is_valid){
2150
2347
  let jsmodel = new JSModel(data, var_specs, model);
@@ -2157,4 +2354,4 @@ function render({model, el}){
2157
2354
 
2158
2355
 
2159
2356
 
2160
- export default{ render }
2357
+ export default{ render };
@@ -0,0 +1,2 @@
1
+ __version_info__ = ("0", "2", "15")
2
+ __version__ = ".".join(__version_info__)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: guidepost
3
- Version: 0.2.14
3
+ Version: 0.2.15
4
4
  Summary: Guidepost. An overview visualization for understanding supercomputer queue data.
5
5
  Home-page: https://github.com/cscully-allison/guidepost
6
6
  Author: Connor Scully-Allison
@@ -22,6 +22,7 @@ Dynamic: classifier
22
22
  Dynamic: description
23
23
  Dynamic: description-content-type
24
24
  Dynamic: home-page
25
+ Dynamic: license-file
25
26
  Dynamic: requires-dist
26
27
  Dynamic: requires-python
27
28
  Dynamic: summary
@@ -212,7 +213,7 @@ Guidepost is licensed under the MIT License. See the `LICENSE` file for details.
212
213
 
213
214
  ## Acknowledgments
214
215
 
215
- Guidepost was developed under the auspices and with funding provided by the National Renewable Energy Laboratory (NREL).
216
+ Guidepost was developed under the auspices and with funding provided by the National Renewable Energy Laboratory (NREL), the National Science Foundation under NSF IIS-1844573 and IIS-2324465, and the Department of Energy under DE-SC0022044 and DE-SC0024635.
216
217
 
217
218
  ---
218
219
 
@@ -1,9 +1,8 @@
1
1
  LICENSE
2
+ MANIFEST.in
2
3
  README.md
3
4
  pyproject.toml
4
5
  setup.py
5
- figs/__init__.py
6
- figs/guidepost_tutorial_info.png
7
6
  guidepost/__init__.py
8
7
  guidepost/guidepost.js
9
8
  guidepost/guidepost.py
@@ -1,3 +1,2 @@
1
- figs
2
1
  guidepost
3
2
  tutorials
@@ -4,7 +4,7 @@ from os import path
4
4
 
5
5
  here = path.abspath(path.dirname(__file__))
6
6
 
7
- with open("README.md", "r", encoding="utf-8") as fh:
7
+ with open(path.join(here, "README.md"), "r", encoding="utf-8") as fh:
8
8
  long_description = fh.read()
9
9
 
10
10
  # Get the version in a safe way
@@ -1,2 +0,0 @@
1
- __version_info__ = ("0", "2", "14")
2
- __version__ = ".".join(__version_info__)
File without changes
File without changes
File without changes
File without changes