fastplotpy 0.3.0a1__py3-none-win_amd64.whl
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.
- fastplotpy/README.md +70 -0
- fastplotpy/__init__.py +33 -0
- fastplotpy/assets/dist/assets/scene.worker-CcnexaKr.js +1 -0
- fastplotpy/assets/dist/index.js +15071 -0
- fastplotpy/assets/native/NotoSans-OFL.txt +96 -0
- fastplotpy/assets/native/THIRD_PARTY_NOTICES.md +24 -0
- fastplotpy/assets/native/fastplot_python_abi.dll +0 -0
- fastplotpy/assets/package.json +1 -0
- fastplotpy/axes_validation.py +102 -0
- fastplotpy/camera3d.py +104 -0
- fastplotpy/color_scale.py +88 -0
- fastplotpy/defaults/area-fill.json +5 -0
- fastplotpy/defaults/bar.json +22 -0
- fastplotpy/defaults/camera3d.json +22 -0
- fastplotpy/defaults/cartesian-references.json +10 -0
- fastplotpy/defaults/control.json +3 -0
- fastplotpy/defaults/crosshair.json +19 -0
- fastplotpy/defaults/error-bars.json +7 -0
- fastplotpy/defaults/heatmap.json +70 -0
- fastplotpy/defaults/interaction.json +3 -0
- fastplotpy/defaults/layout.json +32 -0
- fastplotpy/defaults/legend.json +24 -0
- fastplotpy/defaults/line-axes.json +28 -0
- fastplotpy/defaults/line3d.json +13 -0
- fastplotpy/defaults/markers.json +6 -0
- fastplotpy/defaults/mesh3d.json +13 -0
- fastplotpy/defaults/pan.json +7 -0
- fastplotpy/defaults/pie.json +27 -0
- fastplotpy/defaults/pie3d.json +7 -0
- fastplotpy/defaults/point-labels.json +13 -0
- fastplotpy/defaults/pointClick.json +6 -0
- fastplotpy/defaults/pointSelection.json +13 -0
- fastplotpy/defaults/polar.json +71 -0
- fastplotpy/defaults/polarRotate.json +5 -0
- fastplotpy/defaults/scatter3d.json +39 -0
- fastplotpy/defaults/scene3d.json +61 -0
- fastplotpy/defaults/surface.json +35 -0
- fastplotpy/defaults/tooltip.json +18 -0
- fastplotpy/defaults/trace-colors.json +10 -0
- fastplotpy/defaults/trace-options.json +24 -0
- fastplotpy/defaults/wonderplot.json +58 -0
- fastplotpy/defaults/zoom.json +12 -0
- fastplotpy/defaults.py +15 -0
- fastplotpy/error_bars.py +118 -0
- fastplotpy/exports.py +153 -0
- fastplotpy/families2d.py +896 -0
- fastplotpy/heatmap_labels.py +146 -0
- fastplotpy/interactions.py +243 -0
- fastplotpy/interactive.py +1922 -0
- fastplotpy/js_serialize.py +47 -0
- fastplotpy/lighting3d.py +113 -0
- fastplotpy/line3d_lod.py +138 -0
- fastplotpy/lod3d.py +105 -0
- fastplotpy/mesh3d_lod.py +107 -0
- fastplotpy/native.py +163 -0
- fastplotpy/overlays.py +561 -0
- fastplotpy/point_labels.py +86 -0
- fastplotpy/polar.py +445 -0
- fastplotpy/polygon_clip3d.py +148 -0
- fastplotpy/references.py +72 -0
- fastplotpy/rendering.py +65 -0
- fastplotpy/rust_scenes.py +314 -0
- fastplotpy/scene.py +1185 -0
- fastplotpy/scene3d.py +1215 -0
- fastplotpy/scene3d_accessibility.py +183 -0
- fastplotpy/scene3d_collection.py +158 -0
- fastplotpy/subplots.py +115 -0
- fastplotpy/surface_contours.py +122 -0
- fastplotpy/surface_lod.py +109 -0
- fastplotpy/svg3d.py +235 -0
- fastplotpy/trendline.py +236 -0
- fastplotpy/triangle_wireframe.py +51 -0
- fastplotpy/types.py +3027 -0
- fastplotpy-0.3.0a1.dist-info/METADATA +86 -0
- fastplotpy-0.3.0a1.dist-info/RECORD +78 -0
- fastplotpy-0.3.0a1.dist-info/WHEEL +5 -0
- fastplotpy-0.3.0a1.dist-info/licenses/LICENSE +21 -0
- fastplotpy-0.3.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Source-compatible heatmap cell labels and fill contrast."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
7
|
+
from math import floor, isfinite
|
|
8
|
+
from typing import Mapping
|
|
9
|
+
|
|
10
|
+
from .defaults import get_defaults
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _number(value, fallback, positive=False):
|
|
14
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not isfinite(value):
|
|
15
|
+
return fallback
|
|
16
|
+
if value <= 0 if positive else value < 0:
|
|
17
|
+
return fallback
|
|
18
|
+
return value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_labels(plot_input, trace_input):
|
|
22
|
+
defaults = get_defaults("heatmap")["cellLabels"]
|
|
23
|
+
result = dict(defaults)
|
|
24
|
+
for source in (plot_input, trace_input):
|
|
25
|
+
if source is None:
|
|
26
|
+
continue
|
|
27
|
+
if isinstance(source, bool):
|
|
28
|
+
result["show"] = source
|
|
29
|
+
continue
|
|
30
|
+
if not isinstance(source, Mapping):
|
|
31
|
+
continue
|
|
32
|
+
result["show"] = source.get("show", True)
|
|
33
|
+
color = source.get("color")
|
|
34
|
+
if isinstance(color, str) and color.strip():
|
|
35
|
+
result["color"] = color
|
|
36
|
+
result["autoContrast"] = False
|
|
37
|
+
for key in ("font", "prefix", "suffix"):
|
|
38
|
+
if isinstance(source.get(key), str):
|
|
39
|
+
result[key] = source[key]
|
|
40
|
+
for key in ("lineHeight", "padding", "minCellWidth", "minCellHeight"):
|
|
41
|
+
if key in source:
|
|
42
|
+
result[key] = _number(source[key], result[key], key == "lineHeight")
|
|
43
|
+
if "decimals" in source:
|
|
44
|
+
value = source["decimals"]
|
|
45
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool) and isfinite(value):
|
|
46
|
+
result["decimals"] = max(0, min(6, floor(value + .5)))
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parse_color(value):
|
|
51
|
+
if not isinstance(value, str):
|
|
52
|
+
return None
|
|
53
|
+
value = value.strip().lower()
|
|
54
|
+
match = re.fullmatch(r"#([0-9a-f]{3}|[0-9a-f]{6})", value)
|
|
55
|
+
if match:
|
|
56
|
+
digits = match.group(1)
|
|
57
|
+
if len(digits) == 3:
|
|
58
|
+
digits = "".join(digit * 2 for digit in digits)
|
|
59
|
+
return tuple(int(digits[index:index + 2], 16) for index in (0, 2, 4))
|
|
60
|
+
match = re.fullmatch(r"rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*[\d.]+)?\s*\)", value)
|
|
61
|
+
if match:
|
|
62
|
+
return tuple(max(0, min(255, float(item))) for item in match.groups()[:3])
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _contrast(foreground, background):
|
|
67
|
+
first, second = _parse_color(foreground), _parse_color(background)
|
|
68
|
+
if first is None or second is None:
|
|
69
|
+
return None
|
|
70
|
+
def luminance(color):
|
|
71
|
+
channels = []
|
|
72
|
+
for value in color:
|
|
73
|
+
normalized = value / 255
|
|
74
|
+
channels.append(normalized / 12.92 if normalized <= .04045 else ((normalized + .055) / 1.055) ** 2.4)
|
|
75
|
+
return .2126 * channels[0] + .7152 * channels[1] + .0722 * channels[2]
|
|
76
|
+
a, b = luminance(first), luminance(second)
|
|
77
|
+
return (max(a, b) + .05) / (min(a, b) + .05)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _label_color(labels, fill):
|
|
81
|
+
color = labels["color"]
|
|
82
|
+
if not labels["autoContrast"]:
|
|
83
|
+
return color
|
|
84
|
+
defaults = get_defaults("heatmap")["cellLabels"]
|
|
85
|
+
contrast = _contrast(color, fill)
|
|
86
|
+
if contrast is None or contrast >= defaults["minimumContrastRatio"]:
|
|
87
|
+
return color
|
|
88
|
+
for candidate in (defaults["lightTextColor"], defaults["darkTextColor"]):
|
|
89
|
+
if (_contrast(candidate, fill) or 0) > (_contrast(color, fill) or 0):
|
|
90
|
+
color = candidate
|
|
91
|
+
if (_contrast(color, fill) or 0) >= defaults["minimumContrastRatio"]:
|
|
92
|
+
return color
|
|
93
|
+
for candidate in ("#ffffff", "#000000"):
|
|
94
|
+
if (_contrast(candidate, fill) or 0) > (_contrast(color, fill) or 0):
|
|
95
|
+
color = candidate
|
|
96
|
+
return color
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _fixed(value, decimals):
|
|
100
|
+
if decimals <= 0:
|
|
101
|
+
return str(floor(value + .5))
|
|
102
|
+
quantum = Decimal(1).scaleb(-decimals)
|
|
103
|
+
return format(Decimal(str(value)).quantize(quantum, rounding=ROUND_HALF_UP), f".{decimals}f")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _compact(value, prefix, suffix):
|
|
107
|
+
defaults = get_defaults("heatmap")["cellLabels"]
|
|
108
|
+
decimals = defaults["compactDecimals"]
|
|
109
|
+
for threshold, symbol in ((1e12, "T"), (1e9, "B"), (1e6, "M"), (1e3, "k")):
|
|
110
|
+
if abs(value) >= threshold:
|
|
111
|
+
label = _fixed(value / threshold, decimals).rstrip("0").rstrip(".")
|
|
112
|
+
return f"{prefix}{label}{symbol}{suffix}"
|
|
113
|
+
if 0 < abs(value) < .001:
|
|
114
|
+
mantissa, exponent = f"{value:.{decimals}e}".split("e")
|
|
115
|
+
return f"{prefix}{mantissa}e{int(exponent):+d}{suffix}"
|
|
116
|
+
return f"{prefix}{_fixed(value, decimals)}{suffix}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_cell_labels(cells, labels, border_width):
|
|
120
|
+
if not labels["show"]:
|
|
121
|
+
return None
|
|
122
|
+
output = []
|
|
123
|
+
for cell in cells:
|
|
124
|
+
if cell.get("missing"):
|
|
125
|
+
continue
|
|
126
|
+
value = cell["value"]
|
|
127
|
+
text = f"{labels['prefix']}{_fixed(value, labels['decimals'])}{labels['suffix']}"
|
|
128
|
+
compact = _compact(value, labels["prefix"], labels["suffix"])
|
|
129
|
+
item = {"x": cell["x"] + cell["width"] / 2, "y": cell["y"] + cell["height"] / 2,
|
|
130
|
+
"value": value, "xValue": cell["xValue"], "yValue": cell["yValue"],
|
|
131
|
+
"row": cell["row"], "column": cell["column"], "text": text, "fullText": text}
|
|
132
|
+
if len(compact) < len(text):
|
|
133
|
+
item["compactText"] = compact
|
|
134
|
+
for source, target in (("sourceIndex", "sourceIndex"), ("xCategory", "xCategory"),
|
|
135
|
+
("yCategory", "yCategory"), ("cornerRadius", "cellCornerRadius")):
|
|
136
|
+
if source in cell:
|
|
137
|
+
item[target] = cell[source]
|
|
138
|
+
item.update({"cellX": cell["x"], "cellY": cell["y"], "cellWidth": cell["width"],
|
|
139
|
+
"cellHeight": cell["height"]})
|
|
140
|
+
if border_width > 0:
|
|
141
|
+
item["cellBorderWidth"] = border_width
|
|
142
|
+
item.update({"lineHeight": labels["lineHeight"], "padding": labels["padding"],
|
|
143
|
+
"minCellWidth": labels["minCellWidth"], "minCellHeight": labels["minCellHeight"],
|
|
144
|
+
"color": _label_color(labels, cell["fillColor"]), "font": labels["font"]})
|
|
145
|
+
output.append(item)
|
|
146
|
+
return output or None
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Python counterparts of the source's public viewport state helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from math import isfinite
|
|
7
|
+
from sys import float_info
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
|
|
10
|
+
from .subplots import resolveTraceSubplotBindings
|
|
11
|
+
from .scene import _domain, _number
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def resolveSharedAxisInteractionPatch(options: Mapping[str, Any]) -> dict:
|
|
15
|
+
mode = options.get("mode", "xy")
|
|
16
|
+
active = [binding for binding in options["bindings"]
|
|
17
|
+
if binding["subplotId"] == options["activeSubplotId"]]
|
|
18
|
+
if not active:
|
|
19
|
+
raise ValueError(f"Unknown subplot id '{options['activeSubplotId']}' for interaction")
|
|
20
|
+
patch = {"xAxes": {}, "yAxes": {}}
|
|
21
|
+
for letter in "xy":
|
|
22
|
+
if letter in mode and options.get(f"{letter}Range"):
|
|
23
|
+
for binding in active:
|
|
24
|
+
patch[f"{letter}Axes"][binding[f"{letter}AxisId"]] = deepcopy(options[f"{letter}Range"])
|
|
25
|
+
return patch
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def mergeAxisViewportState(current: Mapping | None, patch: Mapping) -> dict | None:
|
|
29
|
+
x_axes = deepcopy((current or {}).get("xAxes") or {})
|
|
30
|
+
y_axes = deepcopy((current or {}).get("yAxes") or {})
|
|
31
|
+
for axis_id in patch.get("resetXAxes") or []:
|
|
32
|
+
x_axes.pop(axis_id, None)
|
|
33
|
+
for axis_id in patch.get("resetYAxes") or []:
|
|
34
|
+
y_axes.pop(axis_id, None)
|
|
35
|
+
x_axes.update(deepcopy(patch.get("xAxes") or {}))
|
|
36
|
+
y_axes.update(deepcopy(patch.get("yAxes") or {}))
|
|
37
|
+
state = {}
|
|
38
|
+
if x_axes:
|
|
39
|
+
state["xAxes"] = x_axes
|
|
40
|
+
if y_axes:
|
|
41
|
+
state["yAxes"] = y_axes
|
|
42
|
+
return state or None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def viewportStateToAxesOptions(axes: Mapping | None, viewport: Mapping | None) -> dict | None:
|
|
46
|
+
if not viewport or not (viewport.get("xAxes") or viewport.get("yAxes")):
|
|
47
|
+
return deepcopy(axes)
|
|
48
|
+
result = deepcopy(dict(axes or {}))
|
|
49
|
+
for dimension in ("xAxes", "yAxes"):
|
|
50
|
+
result[dimension] = deepcopy(result.get(dimension) or {})
|
|
51
|
+
for axis_id, range_value in (viewport.get(dimension) or {}).items():
|
|
52
|
+
result[dimension][axis_id] = {**result[dimension].get(axis_id, {}),
|
|
53
|
+
"range": deepcopy(range_value)}
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def resolveInteractionBindingsFromPlotOptions(options: Mapping) -> list[dict]:
|
|
58
|
+
return [{"subplotId": binding["subplotId"], "xAxisId": binding["xAxisId"],
|
|
59
|
+
"yAxisId": binding["yAxisId"]}
|
|
60
|
+
for binding in resolveTraceSubplotBindings(options)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def resolveLinkedAxisViewportPatch(options: Mapping, patch: Mapping) -> dict:
|
|
64
|
+
result = {"xAxes": deepcopy(patch.get("xAxes") or {}),
|
|
65
|
+
"yAxes": deepcopy(patch.get("yAxes") or {})}
|
|
66
|
+
for dimension, letter in (("xAxes", "x"), ("yAxes", "y")):
|
|
67
|
+
axes = deepcopy(((options.get("axes") or {}).get(dimension) or {}))
|
|
68
|
+
axes.setdefault(letter, {})
|
|
69
|
+
groups: dict[str, list[tuple[str, dict]]] = {}
|
|
70
|
+
for axis_id, axis in axes.items():
|
|
71
|
+
group_id = axis.get("viewportGroup")
|
|
72
|
+
if group_id is None:
|
|
73
|
+
continue
|
|
74
|
+
if not isinstance(group_id, str) or not group_id.strip():
|
|
75
|
+
raise ValueError(f"{letter}-axis '{axis_id}' viewportGroup must be a non-empty string")
|
|
76
|
+
groups.setdefault(group_id.strip(), []).append((axis_id, axis))
|
|
77
|
+
resets = list(dict.fromkeys(patch.get("reset" + letter.upper() + "Axes") or []))
|
|
78
|
+
for group_id, members in groups.items():
|
|
79
|
+
scales = {axis.get("scale", "linear") for _, axis in members}
|
|
80
|
+
if len(scales) > 1:
|
|
81
|
+
raise ValueError(f"{letter}-axis viewportGroup '{group_id}' requires matching scale types")
|
|
82
|
+
categories = [axis.get("categories") for _, axis in members]
|
|
83
|
+
if any(value != categories[0] for value in categories):
|
|
84
|
+
raise ValueError(f"{letter}-axis viewportGroup '{group_id}' requires matching categories")
|
|
85
|
+
ranges = [(result[dimension][axis_id], axis) for axis_id, axis in members
|
|
86
|
+
if axis_id in result[dimension]]
|
|
87
|
+
if ranges:
|
|
88
|
+
first, first_axis = ranges[0]
|
|
89
|
+
first_pair = (_number(first["min"], first_axis), _number(first["max"], first_axis))
|
|
90
|
+
if any((_number(value["min"], axis), _number(value["max"], axis)) != first_pair
|
|
91
|
+
for value, axis in ranges[1:]):
|
|
92
|
+
raise ValueError(f"viewportGroup '{group_id}' received conflicting ranges")
|
|
93
|
+
for axis_id, _ in members:
|
|
94
|
+
result[dimension][axis_id] = deepcopy(first)
|
|
95
|
+
if axis_id in resets:
|
|
96
|
+
resets.remove(axis_id)
|
|
97
|
+
elif any(axis_id in resets for axis_id, _ in members):
|
|
98
|
+
for axis_id, _ in members:
|
|
99
|
+
if axis_id not in resets:
|
|
100
|
+
resets.append(axis_id)
|
|
101
|
+
if resets:
|
|
102
|
+
result["reset" + letter.upper() + "Axes"] = resets
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def resolveLinkedAxisViewportState(options: Mapping) -> dict | None:
|
|
107
|
+
viewport = options.get("viewport") or {}
|
|
108
|
+
patch = resolveLinkedAxisViewportPatch(options, {
|
|
109
|
+
"xAxes": viewport.get("xAxes") or {}, "yAxes": viewport.get("yAxes") or {},
|
|
110
|
+
})
|
|
111
|
+
return mergeAxisViewportState(None, patch)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def applyAxisViewportState(options: Mapping, viewport: Mapping | None) -> dict:
|
|
115
|
+
result = deepcopy(dict(options))
|
|
116
|
+
linked = resolveLinkedAxisViewportState({"axes": result.get("axes"), "viewport": viewport})
|
|
117
|
+
result["axes"] = viewportStateToAxesOptions(result.get("axes"), linked)
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _interaction_context(input_options: Mapping) -> tuple[dict, list, dict]:
|
|
122
|
+
options = input_options["options"]
|
|
123
|
+
bindings = resolveTraceSubplotBindings(options)
|
|
124
|
+
active = [item for item in bindings if item["subplotId"] == input_options["activeSubplotId"]]
|
|
125
|
+
if not active:
|
|
126
|
+
raise ValueError(f"Unknown subplot id '{input_options['activeSubplotId']}' for interaction")
|
|
127
|
+
return options, active, {"xAxes": {}, "yAxes": {}}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _axis_domains(options: Mapping, axis_id: str, letter: str):
|
|
131
|
+
resolved = applyAxisViewportState(options, options.get("viewport"))
|
|
132
|
+
dimension = f"{letter}Axes"
|
|
133
|
+
current_axis = ((resolved.get("axes") or {}).get(dimension) or {}).get(axis_id) or {}
|
|
134
|
+
base_axis = ((options.get("axes") or {}).get(dimension) or {}).get(axis_id) or {}
|
|
135
|
+
values = []
|
|
136
|
+
for binding in resolveTraceSubplotBindings(options):
|
|
137
|
+
if binding["trace"].get("visible") is False or binding[f"{letter}AxisId"] != axis_id:
|
|
138
|
+
continue
|
|
139
|
+
for point in binding["trace"].get("data") or []:
|
|
140
|
+
if point.get(letter) is not None:
|
|
141
|
+
values.append(_number(point[letter], current_axis))
|
|
142
|
+
return _domain(values, current_axis), _domain(values, base_axis), current_axis
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _axis_ids(bindings, letter):
|
|
146
|
+
return list(dict.fromkeys(item[f"{letter}AxisId"] for item in bindings))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _unit(value):
|
|
150
|
+
return max(0, min(1, value)) if isinstance(value, (int, float)) and isfinite(value) else .5
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _range(domain, axis):
|
|
154
|
+
low, high = domain
|
|
155
|
+
if axis.get("scale") == "logarithmic":
|
|
156
|
+
low, high = 10 ** low, 10 ** high
|
|
157
|
+
return {"min": low, "max": high}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _same_domain(left, right):
|
|
161
|
+
tolerance = float_info.epsilon * max(1, *(abs(value) for value in (*left, *right))) * 16
|
|
162
|
+
return abs(left[0] - right[0]) <= tolerance and abs(left[1] - right[1]) <= tolerance
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def resolveAxisZoomInteractionPatch(input_options: Mapping) -> dict:
|
|
166
|
+
options, active, patch = _interaction_context(input_options)
|
|
167
|
+
mode = input_options.get("mode", "xy")
|
|
168
|
+
scale = input_options["scale"]
|
|
169
|
+
for letter in "xy":
|
|
170
|
+
if letter not in mode:
|
|
171
|
+
continue
|
|
172
|
+
dimension = f"{letter}Axes"
|
|
173
|
+
ratio = input_options[f"{letter}Ratio"]
|
|
174
|
+
ratio = _unit(ratio if letter == "x" else 1 - ratio)
|
|
175
|
+
for axis_id in _axis_ids(active, letter):
|
|
176
|
+
domain, base, axis = _axis_domains(options, axis_id, letter)
|
|
177
|
+
if domain is None or not isinstance(scale, (int, float)) or not isfinite(scale) or scale <= 0:
|
|
178
|
+
continue
|
|
179
|
+
low, high = domain
|
|
180
|
+
span = max((high - low) * scale, float_info.epsilon)
|
|
181
|
+
center = low + (high - low) * ratio
|
|
182
|
+
zoomed = (center - span * ratio, center + span * (1 - ratio))
|
|
183
|
+
if scale > 1 and base is not None:
|
|
184
|
+
if high - low >= base[1] - base[0]:
|
|
185
|
+
continue
|
|
186
|
+
zoomed = (max(base[0], zoomed[0]), min(base[1], zoomed[1]))
|
|
187
|
+
if zoomed[0] >= zoomed[1]:
|
|
188
|
+
continue
|
|
189
|
+
if _same_domain(zoomed, base):
|
|
190
|
+
patch.setdefault("reset" + letter.upper() + "Axes", []).append(axis_id)
|
|
191
|
+
continue
|
|
192
|
+
patch[dimension][axis_id] = _range(zoomed, axis)
|
|
193
|
+
return resolveLinkedAxisViewportPatch(options, patch)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def resolveAxisBoxZoomInteractionPatch(input_options: Mapping) -> dict:
|
|
197
|
+
options, active, patch = _interaction_context(input_options)
|
|
198
|
+
mode = input_options.get("mode", "xy")
|
|
199
|
+
for letter in "xy":
|
|
200
|
+
if letter not in mode:
|
|
201
|
+
continue
|
|
202
|
+
for axis_id in _axis_ids(active, letter):
|
|
203
|
+
domain, _, axis = _axis_domains(options, axis_id, letter)
|
|
204
|
+
if domain is None:
|
|
205
|
+
continue
|
|
206
|
+
first = input_options[f"{letter}StartRatio"]
|
|
207
|
+
second = input_options[f"{letter}EndRatio"]
|
|
208
|
+
if letter == "y":
|
|
209
|
+
first, second = 1 - first, 1 - second
|
|
210
|
+
start, end = _unit(min(first, second)), _unit(max(first, second))
|
|
211
|
+
if start >= end:
|
|
212
|
+
continue
|
|
213
|
+
low, high = domain
|
|
214
|
+
patch[f"{letter}Axes"][axis_id] = _range((low + (high - low) * start,
|
|
215
|
+
low + (high - low) * end), axis)
|
|
216
|
+
return resolveLinkedAxisViewportPatch(options, patch)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def resolveAxisPanInteractionPatch(input_options: Mapping) -> dict:
|
|
220
|
+
options, active, patch = _interaction_context(input_options)
|
|
221
|
+
mode = input_options.get("mode", "xy")
|
|
222
|
+
for letter in "xy":
|
|
223
|
+
if letter not in mode:
|
|
224
|
+
continue
|
|
225
|
+
ratio = input_options[f"{letter}Ratio"]
|
|
226
|
+
if not isinstance(ratio, (int, float)) or not isfinite(ratio) or ratio == 0:
|
|
227
|
+
continue
|
|
228
|
+
for axis_id in _axis_ids(active, letter):
|
|
229
|
+
domain, base, axis = _axis_domains(options, axis_id, letter)
|
|
230
|
+
if domain is None or domain[0] >= domain[1]:
|
|
231
|
+
continue
|
|
232
|
+
span = domain[1] - domain[0]
|
|
233
|
+
next_range = (domain[0] + span * ratio, domain[1] + span * ratio)
|
|
234
|
+
if base is not None:
|
|
235
|
+
if span >= base[1] - base[0]:
|
|
236
|
+
continue
|
|
237
|
+
if next_range[0] < base[0]:
|
|
238
|
+
next_range = (base[0], base[0] + span)
|
|
239
|
+
elif next_range[1] > base[1]:
|
|
240
|
+
next_range = (base[1] - span, base[1])
|
|
241
|
+
if not _same_domain(next_range, domain):
|
|
242
|
+
patch[f"{letter}Axes"][axis_id] = _range(next_range, axis)
|
|
243
|
+
return resolveLinkedAxisViewportPatch(options, patch)
|