guidepost 0.3.2__tar.gz → 0.3.3__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.
- {guidepost-0.3.2/guidepost.egg-info → guidepost-0.3.3}/PKG-INFO +1 -1
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost/guidepost.py +26 -1
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost/seriation.py +63 -10
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost/static/guidepost.js +63 -19
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost/static/guidepost.js.map +2 -2
- guidepost-0.3.3/guidepost/version.py +2 -0
- {guidepost-0.3.2 → guidepost-0.3.3/guidepost.egg-info}/PKG-INFO +1 -1
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost.egg-info/SOURCES.txt +1 -0
- guidepost-0.3.3/tests/test_custom_sort.py +144 -0
- guidepost-0.3.2/guidepost/version.py +0 -2
- {guidepost-0.3.2 → guidepost-0.3.3}/LICENSE +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/MANIFEST.in +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/README.md +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost/__init__.py +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost/aggregation.py +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost/node_layout.py +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost/static/trailmark.js +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost/static/trailmark.js.map +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost/trailmark.py +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost/utils.py +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost.egg-info/dependency_links.txt +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost.egg-info/requires.txt +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/guidepost.egg-info/top_level.txt +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/pyproject.toml +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/setup.cfg +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/setup.py +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/tests/test_aggregation.py +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/tests/test_list_parsing.py +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/tests/test_node_layout.py +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/tests/test_seriation.py +0 -0
- {guidepost-0.3.2 → guidepost-0.3.3}/tutorials/__init__.py +0 -0
|
@@ -77,6 +77,16 @@ class Guidepost(anywidget.AnyWidget):
|
|
|
77
77
|
# identifiers the name heuristic misses, or to override an unwanted
|
|
78
78
|
# auto-classification. Mirrors list_columns; pulled before super().
|
|
79
79
|
categorical_columns = kwargs.pop("categorical_columns", None)
|
|
80
|
+
# Optional override for list-column x-axis ordering, replacing the
|
|
81
|
+
# built-in node-layout / seriation heuristics — for ordinal-but-not-
|
|
82
|
+
# hierarchical values the heuristics can't order (e.g. nodes named
|
|
83
|
+
# "1","2","3"). Either a single callable applied to every list column,
|
|
84
|
+
# or a {column: callable} dict for per-column control. Each callable is
|
|
85
|
+
# a full-list transform: it receives the list of distinct category
|
|
86
|
+
# values and returns them reordered. A callable can't be JSON-synced,
|
|
87
|
+
# so like list_columns this is a plain Python attribute (not a trait);
|
|
88
|
+
# it runs in load_data and only its *result* ships to the frontend.
|
|
89
|
+
sort_by = kwargs.pop("sort_by", None)
|
|
80
90
|
super().__init__(*args, **kwargs)
|
|
81
91
|
self._categorical_columns = set(categorical_columns) if categorical_columns else set()
|
|
82
92
|
self._list_columns = list(list_columns) if list_columns else []
|
|
@@ -85,6 +95,20 @@ class Guidepost(anywidget.AnyWidget):
|
|
|
85
95
|
# before the first load don't fail.
|
|
86
96
|
self._effective_list_columns = list(self._list_columns)
|
|
87
97
|
self._list_delimiter = list_delimiter or ","
|
|
98
|
+
# Validate up front so a bad sort_by fails at construction, not silently
|
|
99
|
+
# at render. None | callable | {str: callable} are the only valid shapes.
|
|
100
|
+
if sort_by is not None:
|
|
101
|
+
if isinstance(sort_by, dict):
|
|
102
|
+
bad = [k for k, v in sort_by.items() if not callable(v)]
|
|
103
|
+
if bad:
|
|
104
|
+
raise TypeError(
|
|
105
|
+
"sort_by dict values must be callables; non-callable "
|
|
106
|
+
f"value(s) for column(s): {bad}")
|
|
107
|
+
elif not callable(sort_by):
|
|
108
|
+
raise TypeError(
|
|
109
|
+
"sort_by must be a callable or a {column: callable} dict, "
|
|
110
|
+
f"got {type(sort_by).__name__}")
|
|
111
|
+
self._sort_by = sort_by
|
|
88
112
|
# DuckDB-backed aggregator. Lazily created when the first DataFrame
|
|
89
113
|
# is loaded — keeping it None until then so widgets constructed
|
|
90
114
|
# without data don't pay the import/registration cost.
|
|
@@ -194,7 +218,8 @@ class Guidepost(anywidget.AnyWidget):
|
|
|
194
218
|
# that retains every node); arbitrary categories fall back to spectral
|
|
195
219
|
# seriation (ships `category_score` for the column cap). Both share
|
|
196
220
|
# `category_order` for the kept/ordered sequence.
|
|
197
|
-
ordering = compute_category_ordering(
|
|
221
|
+
ordering = compute_category_ordering(
|
|
222
|
+
o_df, self._effective_list_columns, sort_overrides=self._sort_by)
|
|
198
223
|
for col, info in ordering.items():
|
|
199
224
|
if col not in summary_stats:
|
|
200
225
|
continue
|
|
@@ -15,31 +15,84 @@ Both derive from the same node x node co-occurrence matrix. sklearn/scipy are
|
|
|
15
15
|
imported lazily so the cost is only paid when a list column is present.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
+
import warnings
|
|
19
|
+
|
|
18
20
|
import numpy as np
|
|
19
21
|
|
|
20
22
|
from .node_layout import compute_node_layout
|
|
21
23
|
|
|
22
24
|
|
|
23
|
-
def
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
def _resolve_override(sort_overrides, col):
|
|
26
|
+
"""Resolve the user sort override applicable to *col*, if any. A bare
|
|
27
|
+
callable applies to every column; a dict maps column name -> callable."""
|
|
28
|
+
if sort_overrides is None:
|
|
29
|
+
return None
|
|
30
|
+
if callable(sort_overrides):
|
|
31
|
+
return sort_overrides
|
|
32
|
+
if isinstance(sort_overrides, dict):
|
|
33
|
+
return sort_overrides.get(col)
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _apply_override(fn, names):
|
|
38
|
+
"""Run a user override (full-list transform) over the distinct category
|
|
39
|
+
names, coercing the result to the String() keys the frontend uses. Raises
|
|
40
|
+
on a non-iterable return so the caller can fall back to the heuristics."""
|
|
41
|
+
ordered = fn(list(names))
|
|
42
|
+
return [str(k) for k in ordered]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def compute_category_ordering(o_df, list_columns, sort_overrides=None):
|
|
46
|
+
"""Dispatch ordering per list column: a user override (if provided) wins,
|
|
47
|
+
else prefer structure-aware node-name layout, else fall back to spectral
|
|
48
|
+
co-occurrence seriation.
|
|
49
|
+
|
|
50
|
+
Returns ``{col: info}`` where *info* is one of:
|
|
51
|
+
- ``{"order"}`` — a user ``sort_overrides`` transform for this column,
|
|
52
|
+
- ``{"order", "hierarchy", "levels"}`` — node names follow a hardware-layout
|
|
53
|
+
convention, or
|
|
54
|
+
- ``{"order", "score"}`` — arbitrary categories (seriation).
|
|
55
|
+
The frontend treats all three uniformly via ``category_order`` and branches
|
|
56
|
+
on the presence of ``category_hierarchy``.
|
|
57
|
+
|
|
58
|
+
*sort_overrides* is ``None``, a single callable applied to every list
|
|
59
|
+
column, or a ``{column: callable}`` dict. Each callable receives the list
|
|
60
|
+
of distinct category values and returns them reordered. A callable that
|
|
61
|
+
raises (or returns a non-iterable) falls back to the heuristics for that
|
|
62
|
+
column with a warning, so a bad override never aborts data loading.
|
|
32
63
|
"""
|
|
33
64
|
if not list_columns:
|
|
34
65
|
return {}
|
|
35
66
|
|
|
67
|
+
# Warn on dict keys that don't name a list column (typo / out of scope):
|
|
68
|
+
# they'd otherwise be silently ignored.
|
|
69
|
+
if isinstance(sort_overrides, dict):
|
|
70
|
+
unknown = [c for c in sort_overrides if c not in list_columns]
|
|
71
|
+
if unknown:
|
|
72
|
+
warnings.warn(
|
|
73
|
+
"sort_by names column(s) that are not list columns and will be "
|
|
74
|
+
f"ignored: {unknown}. Custom sort applies to list columns only.")
|
|
75
|
+
|
|
36
76
|
out = {}
|
|
37
77
|
fallback_cols = []
|
|
38
78
|
for col in list_columns:
|
|
39
79
|
if col not in o_df.columns:
|
|
40
80
|
continue
|
|
41
81
|
names = _distinct_node_names(o_df[col])
|
|
42
|
-
|
|
82
|
+
if not names:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
override = _resolve_override(sort_overrides, col)
|
|
86
|
+
if override is not None:
|
|
87
|
+
try:
|
|
88
|
+
out[col] = {"order": _apply_override(override, names)}
|
|
89
|
+
continue
|
|
90
|
+
except Exception as exc: # noqa: BLE001 - fall back, never abort load
|
|
91
|
+
warnings.warn(
|
|
92
|
+
f"sort_by override for column '{col}' raised {exc!r}; "
|
|
93
|
+
"falling back to the default ordering heuristic.")
|
|
94
|
+
|
|
95
|
+
layout = compute_node_layout(names)
|
|
43
96
|
if layout is not None:
|
|
44
97
|
out[col] = layout
|
|
45
98
|
else:
|
|
@@ -16630,15 +16630,23 @@ var JSModel = class {
|
|
|
16630
16630
|
return data;
|
|
16631
16631
|
}
|
|
16632
16632
|
/**
|
|
16633
|
-
* Sanitizes data for log scale by
|
|
16633
|
+
* Sanitizes data for a log scale by clamping any value below the log floor
|
|
16634
|
+
* up to the floor. A log axis starts at `log_values_floor`, so a value in
|
|
16635
|
+
* [0, floor) can't be placed on it — binValues would drop it entirely (no
|
|
16636
|
+
* bin matches `val < thresholds[0]`), silently losing real records (e.g.
|
|
16637
|
+
* sub-1-Wh energy for standby/short jobs). Clamping puts them in bin 0 so
|
|
16638
|
+
* cells, the right histogram, the count strip and brush/pin selection all
|
|
16639
|
+
* agree. null/NaN are guarded (note `null < 1` is true in JS) so genuine
|
|
16640
|
+
* missing values stay unbinned rather than masquerading as floor values.
|
|
16634
16641
|
* @param {Array} data - The data to sanitize.
|
|
16635
16642
|
* @param {string} col - The column to sanitize.
|
|
16636
16643
|
* @returns {Array} - The sanitized data.
|
|
16637
16644
|
*/
|
|
16638
16645
|
sanitize_data_for_log(data, col) {
|
|
16639
16646
|
data.forEach((element, i, arr) => {
|
|
16640
|
-
|
|
16641
|
-
|
|
16647
|
+
const v = element[col];
|
|
16648
|
+
if (v != null && !Number.isNaN(v) && v < this.log_values_floor) {
|
|
16649
|
+
arr[i][col] = this.log_values_floor;
|
|
16642
16650
|
}
|
|
16643
16651
|
});
|
|
16644
16652
|
return data;
|
|
@@ -17864,6 +17872,14 @@ var JSModel = class {
|
|
|
17864
17872
|
* displayed cells whose column is in col_range (empty = all columns) and
|
|
17865
17873
|
* whose row is in y_range (empty = all rows). Grouped columns carry
|
|
17866
17874
|
* lo/hi (node-index overlap); scalar columns are matched by array index.
|
|
17875
|
+
*
|
|
17876
|
+
* A column's `bins` only hold rows with a non-null, in-range y value, so
|
|
17877
|
+
* summing bin indices silently drops null/NaN-y rows — common for e.g.
|
|
17878
|
+
* Failed jobs that never recorded a runtime metric. When the brush spans
|
|
17879
|
+
* the FULL y extent (or sets no y restriction), the user is selecting whole
|
|
17880
|
+
* columns, so use `col.indices` (the column's full membership) to match the
|
|
17881
|
+
* displayed "(N records)" count and column-pin selection. A partial y-brush
|
|
17882
|
+
* still restricts to the covered cells (null-y rows have no cell to brush).
|
|
17867
17883
|
*/
|
|
17868
17884
|
_band_box_indices(facet) {
|
|
17869
17885
|
const cols = this.current_detail_columns(facet) || [];
|
|
@@ -17871,6 +17887,8 @@ var JSModel = class {
|
|
|
17871
17887
|
const yr = this.brushed_ranges[facet].y_range;
|
|
17872
17888
|
const has_col = cr.length === 2;
|
|
17873
17889
|
const has_y = yr.length === 2;
|
|
17890
|
+
const nbins = cols[0] && cols[0].bins && cols[0].bins.length || 0;
|
|
17891
|
+
const y_covers_all = !has_y || yr[1] <= 0 && yr[0] >= nbins - 1;
|
|
17874
17892
|
const ids = /* @__PURE__ */ new Set();
|
|
17875
17893
|
for (let ci = 0; ci < cols.length; ci++) {
|
|
17876
17894
|
const col = cols[ci];
|
|
@@ -17880,8 +17898,13 @@ var JSModel = class {
|
|
|
17880
17898
|
in_col = col.lo != null ? col.lo <= cr[1] && col.hi >= cr[0] : ci >= cr[0] && ci <= cr[1];
|
|
17881
17899
|
}
|
|
17882
17900
|
if (!in_col) continue;
|
|
17901
|
+
if (y_covers_all) {
|
|
17902
|
+
const idx = col.indices;
|
|
17903
|
+
if (idx) for (let i = 0; i < idx.length; i++) ids.add(idx[i]);
|
|
17904
|
+
continue;
|
|
17905
|
+
}
|
|
17883
17906
|
for (let row = 0; row < col.bins.length; row++) {
|
|
17884
|
-
if (
|
|
17907
|
+
if (!(row >= yr[1] && row < yr[0])) continue;
|
|
17885
17908
|
const idx = col.bins[row].indices;
|
|
17886
17909
|
if (!idx) continue;
|
|
17887
17910
|
for (let i = 0; i < idx.length; i++) ids.add(idx[i]);
|
|
@@ -18626,20 +18649,13 @@ var Heatmap = class {
|
|
|
18626
18649
|
let x_offset = X_VARIABLE_OFFSET + OVERVIEW_LAYOUT.outer_margin;
|
|
18627
18650
|
let y_offset = Y_VARIABLE_OFFSET + OVERVIEW_LAYOUT.outer_margin + TOP_MARGIN;
|
|
18628
18651
|
let view = this.parent.append("g").attr("class", "heatmap").attr("transform", (d, i) => `translate(${x_offset},${y_offset})`).attr("width", this.width).attr("height", this.height);
|
|
18652
|
+
this.view = view;
|
|
18629
18653
|
let axis_left = d32.axisLeft().scale(this.scale_y_inverse);
|
|
18630
18654
|
if (this.model.scale_types[this.facet].y.linear) {
|
|
18631
18655
|
axis_left.tickFormat(d32.format(".2s"));
|
|
18632
18656
|
}
|
|
18633
18657
|
view.append("g").attr("class", "left-axis").call(axis_left).attr("transform", `translate(${OVERVIEW_LAYOUT.inner_padding},${0})`);
|
|
18634
|
-
|
|
18635
|
-
const domain = this.scale_x.domain();
|
|
18636
|
-
const max_labels = 40;
|
|
18637
|
-
const step = Math.max(1, Math.ceil(domain.length / max_labels));
|
|
18638
|
-
const tick_vals = domain.filter((d, i) => i % step === 0);
|
|
18639
|
-
view.append("g").attr("class", "bottom-axis").call(d32.axisBottom(this.scale_x).tickValues(tick_vals)).attr("transform", `translate(${0},${OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding})`).selectAll("text").attr("text-anchor", "start").attr("transform", "rotate(45)").text((d) => typeof d === "string" && d.length > MAX_NODE_LABEL_CHARS ? d.slice(0, MAX_NODE_LABEL_CHARS) + "\u2026" : d);
|
|
18640
|
-
} else {
|
|
18641
|
-
view.append("g").attr("class", "bottom-axis").call(d32.axisBottom().scale(this.scale_x.scale).ticks(this.scale_x.get_ticks())).attr("transform", `translate(${0},${OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding})`);
|
|
18642
|
-
}
|
|
18658
|
+
this.draw_x_axis();
|
|
18643
18659
|
const facet_stats = this.model.feature_summary_stats[this.model.vars.facet_by];
|
|
18644
18660
|
const synthetic_facet = !!(facet_stats && facet_stats.is_synthetic);
|
|
18645
18661
|
if (!synthetic_facet) {
|
|
@@ -18651,7 +18667,25 @@ var Heatmap = class {
|
|
|
18651
18667
|
}
|
|
18652
18668
|
return this.model.vars.y + "(log)";
|
|
18653
18669
|
}).attr("text-anchor", "middle").attr("transform", `translate(${-10},${this.height / 2}) rotate(270)`);
|
|
18654
|
-
|
|
18670
|
+
}
|
|
18671
|
+
/** Draws (or redraws) the bottom x-axis. Idempotent — selects the existing
|
|
18672
|
+
* `.bottom-axis` group or creates it — so it can be re-called on detail-zoom
|
|
18673
|
+
* re-renders to keep a grouped/list (band) x-axis in sync with the columns. */
|
|
18674
|
+
draw_x_axis() {
|
|
18675
|
+
let axis_g = this.view.select(".bottom-axis");
|
|
18676
|
+
if (axis_g.empty()) {
|
|
18677
|
+
axis_g = this.view.append("g").attr("class", "bottom-axis");
|
|
18678
|
+
}
|
|
18679
|
+
axis_g.attr("transform", `translate(${0},${OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding})`);
|
|
18680
|
+
if (this.x_is_band) {
|
|
18681
|
+
const domain = this.scale_x.domain();
|
|
18682
|
+
const max_labels = 40;
|
|
18683
|
+
const step = Math.max(1, Math.ceil(domain.length / max_labels));
|
|
18684
|
+
const tick_vals = domain.filter((d, i) => i % step === 0);
|
|
18685
|
+
axis_g.call(d32.axisBottom(this.scale_x).tickValues(tick_vals)).selectAll("text").attr("text-anchor", "start").attr("transform", "rotate(45)").text((d) => typeof d === "string" && d.length > MAX_NODE_LABEL_CHARS ? d.slice(0, MAX_NODE_LABEL_CHARS) + "\u2026" : d);
|
|
18686
|
+
} else {
|
|
18687
|
+
axis_g.call(d32.axisBottom().scale(this.scale_x.scale).ticks(this.scale_x.get_ticks()));
|
|
18688
|
+
}
|
|
18655
18689
|
}
|
|
18656
18690
|
/**
|
|
18657
18691
|
* Draws sparklines (placeholder function).
|
|
@@ -19137,6 +19171,7 @@ var Heatmap = class {
|
|
|
19137
19171
|
this.model.detail_range[this.facet] = range;
|
|
19138
19172
|
this._cols_cache = null;
|
|
19139
19173
|
this.update_scales();
|
|
19174
|
+
this.draw_x_axis();
|
|
19140
19175
|
this.render();
|
|
19141
19176
|
}
|
|
19142
19177
|
/**
|
|
@@ -19611,17 +19646,22 @@ var Heatmap = class {
|
|
|
19611
19646
|
const ranges = this.model.brushed_ranges[this.facet];
|
|
19612
19647
|
if (this.x_is_band) {
|
|
19613
19648
|
const cr = ranges.col_range, yr2 = ranges.y_range;
|
|
19649
|
+
const has_col = cr.length === 2, has_y = yr2.length === 2;
|
|
19650
|
+
if (!has_col && !has_y) {
|
|
19651
|
+
g.call(this.cell_brush.move, null);
|
|
19652
|
+
return;
|
|
19653
|
+
}
|
|
19614
19654
|
const cols = this.current_columns();
|
|
19615
|
-
const covered =
|
|
19616
|
-
const idxCovered =
|
|
19617
|
-
if (
|
|
19655
|
+
const covered = has_col ? cols.filter((c) => c.lo != null ? c.lo <= cr[1] && c.hi >= cr[0] : true) : cols;
|
|
19656
|
+
const idxCovered = has_col && cols.length && cols[0].lo == null ? cols.filter((c, i) => i >= cr[0] && i <= cr[1]) : covered;
|
|
19657
|
+
if (!idxCovered.length) {
|
|
19618
19658
|
g.call(this.cell_brush.move, null);
|
|
19619
19659
|
return;
|
|
19620
19660
|
}
|
|
19621
19661
|
const cw = this.col_width();
|
|
19622
19662
|
const px0 = Math.min(...idxCovered.map((c) => this.x_pos(c.threshold)));
|
|
19623
19663
|
const px1 = Math.max(...idxCovered.map((c) => this.x_pos(c.threshold))) + cw;
|
|
19624
|
-
const py = [this.scale_y_blocks(yr2[0]), this.scale_y_blocks(yr2[1])];
|
|
19664
|
+
const py = has_y ? [this.scale_y_blocks(yr2[0]), this.scale_y_blocks(yr2[1])] : [OVERVIEW_LAYOUT.inner_padding, OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding];
|
|
19625
19665
|
g.call(this.cell_brush.move, [[px0, Math.min(...py)], [px1, Math.max(...py)]]);
|
|
19626
19666
|
return;
|
|
19627
19667
|
}
|
|
@@ -19676,9 +19716,13 @@ var Heatmap = class {
|
|
|
19676
19716
|
hi = Math.max(hi, ci);
|
|
19677
19717
|
}
|
|
19678
19718
|
}
|
|
19719
|
+
const full_y0 = OVERVIEW_LAYOUT.inner_padding;
|
|
19720
|
+
const full_y1 = OVERVIEW_LAYOUT.height - OVERVIEW_LAYOUT.inner_padding;
|
|
19721
|
+
const tol = this._cell_row_height(cols);
|
|
19722
|
+
const covers_full_y = y0 <= full_y0 + tol && y1 >= full_y1 - tol;
|
|
19679
19723
|
ranges.x_range = [];
|
|
19680
19724
|
ranges.col_range = lo !== Infinity ? [lo, hi] : [];
|
|
19681
|
-
ranges.y_range = y_range;
|
|
19725
|
+
ranges.y_range = covers_full_y ? [] : y_range;
|
|
19682
19726
|
} else {
|
|
19683
19727
|
const xa = this.scale_x.scale.invert(x0);
|
|
19684
19728
|
const xb = this.scale_x.scale.invert(x1);
|