adopt-plot 0.0.1__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.
- adopt_plot-0.0.1/LICENSE +22 -0
- adopt_plot-0.0.1/PKG-INFO +19 -0
- adopt_plot-0.0.1/README.md +4 -0
- adopt_plot-0.0.1/pyproject.toml +23 -0
- adopt_plot-0.0.1/setup.cfg +4 -0
- adopt_plot-0.0.1/src/adopt_plot/__init__.py +8 -0
- adopt_plot-0.0.1/src/adopt_plot/plot.py +401 -0
- adopt_plot-0.0.1/src/adopt_plot.egg-info/PKG-INFO +19 -0
- adopt_plot-0.0.1/src/adopt_plot.egg-info/SOURCES.txt +10 -0
- adopt_plot-0.0.1/src/adopt_plot.egg-info/dependency_links.txt +1 -0
- adopt_plot-0.0.1/src/adopt_plot.egg-info/requires.txt +3 -0
- adopt_plot-0.0.1/src/adopt_plot.egg-info/top_level.txt +1 -0
adopt_plot-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2026 VartRusData.
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
4
|
+
are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
7
|
+
this list of conditions and the following disclaimer.
|
|
8
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
9
|
+
this list of conditions and the following disclaimer in the documentation and/or other materials provided
|
|
10
|
+
with the distribution.
|
|
11
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
12
|
+
may be used to endorse or promote products derived from this software
|
|
13
|
+
without specific prior written permission.
|
|
14
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
15
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
16
|
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
17
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
18
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
19
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
20
|
+
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
21
|
+
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adopt-plot
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A universal plotter for implicit functions using adaptive engine selection.
|
|
5
|
+
Author: VartRusData
|
|
6
|
+
License: BSD 3-Clause
|
|
7
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: sympy
|
|
12
|
+
Requires-Dist: matplotlib
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# adopt-plot
|
|
17
|
+
|
|
18
|
+
A simple Python library for plotting implicit functions with adaptive engine selection.
|
|
19
|
+
Just pass an equation and get a beautiful, mathematically correct plot.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "adopt-plot"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "A universal plotter for implicit functions using adaptive engine selection."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [{name = "VartRusData"}]
|
|
12
|
+
license = {text = "BSD 3-Clause"}
|
|
13
|
+
dependencies = [
|
|
14
|
+
"sympy",
|
|
15
|
+
"matplotlib",
|
|
16
|
+
"numpy"
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"License :: OSI Approved :: BSD License",
|
|
20
|
+
]
|
|
21
|
+
[tool.setuptools]
|
|
22
|
+
packages = ["adopt_plot"]
|
|
23
|
+
package-dir = {"" = "src"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Adopt Plot package"""
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# Copyright (c) 2026 VartRusData. All rights reserved.
|
|
4
|
+
class TooManyVariablesError(ValueError):
|
|
5
|
+
"""Исключение, возникающее при попытке построить график для более чем двух переменных."""
|
|
6
|
+
pass
|
|
7
|
+
from .plot import AdoptPlot
|
|
8
|
+
__all__ = ["AdoptPlot", "TooManyVariablesError"]
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# Copyright (c) 2026 VartRusData. All rights reserved.
|
|
4
|
+
from sympy import *
|
|
5
|
+
import numpy as np
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
import sympy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from . import TooManyVariablesError
|
|
13
|
+
|
|
14
|
+
def get_all_sympy_function_names():
|
|
15
|
+
"""Собирает имена всех встроенных функций и констант SymPy."""
|
|
16
|
+
names = set(sympy.__all__)
|
|
17
|
+
#print(names)
|
|
18
|
+
return names
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def insert_multiplication_signs(expr: str, extra_functions=None) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Вставляет знаки умножения с учётом неявного умножения.
|
|
24
|
+
Поддерживает латиницу и кириллицу в именах переменных.
|
|
25
|
+
Защищает все известные SymPy-функции и константы от разбиения.
|
|
26
|
+
"""
|
|
27
|
+
func_set = get_all_sympy_function_names() # теперь там и функции, и константы
|
|
28
|
+
if extra_functions:
|
|
29
|
+
func_set.update(extra_functions)
|
|
30
|
+
|
|
31
|
+
sorted_funcs = sorted(func_set, key=len, reverse=True)
|
|
32
|
+
func_pattern = '|'.join(re.escape(f) for f in sorted_funcs)
|
|
33
|
+
|
|
34
|
+
LETTER = r'[A-Za-zА-Яа-яёЁ_]'
|
|
35
|
+
LETTER_DIGIT = r'[\dA-Za-zА-Яа-яёЁ_]'
|
|
36
|
+
|
|
37
|
+
# === ШАГ 1: Защита известных имён (функций и констант) ===
|
|
38
|
+
protected = {}
|
|
39
|
+
|
|
40
|
+
def protect_known(match):
|
|
41
|
+
name = match.group(0)
|
|
42
|
+
if name not in func_set:
|
|
43
|
+
return name
|
|
44
|
+
placeholder = f'\ue000{len(protected)}\ue001'
|
|
45
|
+
protected[placeholder] = name
|
|
46
|
+
return placeholder
|
|
47
|
+
|
|
48
|
+
#print(expr)
|
|
49
|
+
expr = re.sub(r'[A-Za-z_]\w*', protect_known, expr)
|
|
50
|
+
#print(expr)
|
|
51
|
+
# === ШАГ 2: Правила вставки умножения ===
|
|
52
|
+
expr = re.sub(rf'(\d+)(\ue000\d+\ue001)(?=\()', r'\1*\2', expr)
|
|
53
|
+
# Явно вставляем * между цифрой/буквой и известным именем перед '('
|
|
54
|
+
expr = re.sub(rf'(\d)({func_pattern})(?=\()', r'\1*\2', expr)
|
|
55
|
+
expr = re.sub(rf'({LETTER})({func_pattern})(?=\()', r'\1*\2', expr)
|
|
56
|
+
|
|
57
|
+
# Основные правила с кириллицей
|
|
58
|
+
expr = re.sub(rf'(\d)({LETTER})', r'\1*\2', expr) # 2x, 2я
|
|
59
|
+
# РАЗРЫВАЕМ ВСЕ ЦЕПОЧКИ БУКВ (кроме защищённых имён)
|
|
60
|
+
expr = re.sub(rf'({LETTER})(?={LETTER})', r'\1*', expr) # a*b*c, а*я
|
|
61
|
+
expr = re.sub(rf'({LETTER_DIGIT})(\()', r'\1*\2', expr) # a(, 3(, я(
|
|
62
|
+
expr = re.sub(rf'(\))({LETTER_DIGIT}\()', r'\1*\2', expr) # )a, )(, )я
|
|
63
|
+
|
|
64
|
+
# === ШАГ 3: Возвращаем защищённые имена на место ===
|
|
65
|
+
for placeholder, name in protected.items():
|
|
66
|
+
expr = expr.replace(placeholder, name)
|
|
67
|
+
|
|
68
|
+
return expr
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
from typing import Literal, Optional, Tuple
|
|
77
|
+
|
|
78
|
+
class AdoptPlot():
|
|
79
|
+
def __init__(self,
|
|
80
|
+
expr: str,
|
|
81
|
+
lib: Literal['contour', 'implicit', None] = None,
|
|
82
|
+
xlims: Tuple[float, float] = (-20, 20),
|
|
83
|
+
ylims: Tuple[float, float] = (-20, 20),
|
|
84
|
+
n: int = 10000,
|
|
85
|
+
|
|
86
|
+
depth: int = 9,
|
|
87
|
+
linewidth: float = 2.0,
|
|
88
|
+
limit: float | Tuple[float, float, float, float] = 100,
|
|
89
|
+
color: str = 'blue',
|
|
90
|
+
legend: bool = True,
|
|
91
|
+
points: bool = True,
|
|
92
|
+
grid: bool = True,
|
|
93
|
+
interact: bool = True,
|
|
94
|
+
show: bool = True,
|
|
95
|
+
text_legend: Optional[str] = None
|
|
96
|
+
):
|
|
97
|
+
"""
|
|
98
|
+
Plots an implicit function with adaptive engine selection.
|
|
99
|
+
|
|
100
|
+
Parameters:
|
|
101
|
+
expr: String containing the equation (e.g., "y = 1/x").
|
|
102
|
+
lib: Forced plotting engine ('contour', 'implicit', or None for auto-selection).
|
|
103
|
+
xlims :Initial visible range for the plot.
|
|
104
|
+
ylims: Initial visible range for the plot.
|
|
105
|
+
n: Grid density for the contour engine.
|
|
106
|
+
depth: Adaptive refinement depth for plot_implicit.
|
|
107
|
+
linewidth: Thickness of the plot line.
|
|
108
|
+
limit: Defines the computational domain for the plot. Accepts a single number (for a square area) or a tuple of 4 floats (x_min, x_max, y_min, y_max) for a rectangular area.
|
|
109
|
+
color: Line color.
|
|
110
|
+
legend: Whether to display the legend.
|
|
111
|
+
points: Whether to display axis intersection points.
|
|
112
|
+
grid: Whether to display the grid.
|
|
113
|
+
interact: Allow zooming/panning (does not fix limits rigidly).
|
|
114
|
+
show: If False, returns a PlotHandle object instead of displaying the plot.
|
|
115
|
+
text_legend: Additional text for the legend (supports "\\n" and a list of strings).
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
self.lib = lib
|
|
119
|
+
self.xlims = xlims
|
|
120
|
+
self.ylims = ylims
|
|
121
|
+
self.n = n
|
|
122
|
+
self.depth = depth
|
|
123
|
+
self.linewidth = linewidth
|
|
124
|
+
self.color = color
|
|
125
|
+
self.legend = legend
|
|
126
|
+
self.points = points
|
|
127
|
+
self.grid = grid
|
|
128
|
+
self.interact = interact
|
|
129
|
+
self.show = show
|
|
130
|
+
self.limit = limit
|
|
131
|
+
self.text_legend = text_legend
|
|
132
|
+
# Получаем уравнения из поля ввода
|
|
133
|
+
equations_str = expr
|
|
134
|
+
logging.info(f"Полученная строка уравнений: {equations_str}")
|
|
135
|
+
if equations_str == "":
|
|
136
|
+
raise ValueError("Empty string is not allowed.")
|
|
137
|
+
# Проверяем наличие запятых в строке
|
|
138
|
+
equation = equations_str
|
|
139
|
+
# Разбиение строки на отдельные уравнения
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Преобразование уравнений в объекты Sympy
|
|
144
|
+
expressions = []
|
|
145
|
+
used_variables = set() # Множество переменных, используемых в уравнениях
|
|
146
|
+
|
|
147
|
+
logging.info(f"Преобразование уравнения: {equation}")
|
|
148
|
+
|
|
149
|
+
equation = insert_multiplication_signs(equation)
|
|
150
|
+
|
|
151
|
+
lhs, rhs = equation.split('=')
|
|
152
|
+
logging.info(str(lhs))
|
|
153
|
+
logging.info(str(rhs))
|
|
154
|
+
expressions.append(Eq(sympify(lhs), sympify(rhs)))
|
|
155
|
+
logging.info(f"Добавлено уравнение: {expressions[-1]}")
|
|
156
|
+
|
|
157
|
+
# Определяем переменные, участвующие в текущем уравнении
|
|
158
|
+
used_variables.update(list(expressions[-1].free_symbols))
|
|
159
|
+
|
|
160
|
+
logging.info(f"Переменные, задействованные в уравнениях: {used_variables}")
|
|
161
|
+
|
|
162
|
+
# Проверка на недоопределённость системы
|
|
163
|
+
# Строим график для первого уравнения
|
|
164
|
+
self.eq = expressions[0]
|
|
165
|
+
self.vars_list = list(self.eq.free_symbols)
|
|
166
|
+
|
|
167
|
+
if len(self.vars_list) == 2:
|
|
168
|
+
self.var1, self.var2 = self.vars_list[0], self.vars_list[1]
|
|
169
|
+
self.plot_equation()
|
|
170
|
+
return
|
|
171
|
+
else:
|
|
172
|
+
raise TooManyVariablesError(
|
|
173
|
+
f"adopt_plot поддерживает только 2D-графики. Получено {len(self.vars_list)} переменных: {self.vars_list}. "
|
|
174
|
+
f"Если вам нужен 3D-график, подождите следующей версии!"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Обновляем историю
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
raise Exception(e)
|
|
185
|
+
|
|
186
|
+
def plot_equation(self):
|
|
187
|
+
try:
|
|
188
|
+
def has_odz(expr, var):
|
|
189
|
+
"""
|
|
190
|
+
Проверяет, есть ли у выражения ограничения на область допустимых значений (ОДЗ).
|
|
191
|
+
Если есть - лучше рисовать через plot_implicit.
|
|
192
|
+
"""
|
|
193
|
+
# 1. Проверяем наличие нелинейных функций с ограничениями (log, sqrt, asin, acos)
|
|
194
|
+
# В SymPy log и ln — одно и то же.
|
|
195
|
+
if expr.has(log) or expr.has(sqrt) or expr.has(asin) or expr.has(acos):
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
# 2. Проверяем, есть ли переменная в знаменателе (деление на ноль)
|
|
199
|
+
numer, denom = expr.as_numer_denom()
|
|
200
|
+
if var in denom.free_symbols:
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
expr = self.eq.lhs - self.eq.rhs
|
|
207
|
+
vars_list = sorted(self.eq.free_symbols, key=lambda s: str(s))
|
|
208
|
+
var1, var2 = vars_list[0], vars_list[1]
|
|
209
|
+
# === 1. Безопасный поиск пересечений (ловит ошибки abs) ===
|
|
210
|
+
x_intercepts = []
|
|
211
|
+
y_intercepts = []
|
|
212
|
+
try:
|
|
213
|
+
x_sol = solve(expr.subs(var2, 0), var1)
|
|
214
|
+
x_intercepts = [float(s) for s in x_sol if s.is_real]
|
|
215
|
+
except Exception:
|
|
216
|
+
pass # Если sympy не может решить (abs), пропускаем
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
y_sol = solve(expr.subs(var1, 0), var2)
|
|
220
|
+
y_intercepts = [float(s) for s in y_sol if s.is_real]
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def zoom_key_handler(event):
|
|
226
|
+
# Проверяем, что нажатие произошло на оси и это сочетание с Ctrl
|
|
227
|
+
if event.inaxes != self.ax:
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# Коэффициент масштабирования (1.2 = 120%, 0.8 = 80%)
|
|
231
|
+
scale_factor = 1.2
|
|
232
|
+
|
|
233
|
+
# Получаем текущие границы
|
|
234
|
+
cur_xlim = self.ax.get_xlim()
|
|
235
|
+
cur_ylim = self.ax.get_ylim()
|
|
236
|
+
|
|
237
|
+
# Вычисляем центр текущего обзора
|
|
238
|
+
x_center = (cur_xlim[0] + cur_xlim[1]) / 2
|
|
239
|
+
y_center = (cur_ylim[0] + cur_ylim[1]) / 2
|
|
240
|
+
|
|
241
|
+
# Длина текущего диапазона
|
|
242
|
+
cur_xrange = cur_xlim[1] - cur_xlim[0]
|
|
243
|
+
cur_yrange = cur_ylim[1] - cur_ylim[0]
|
|
244
|
+
# print(event.key)
|
|
245
|
+
# Если нажато Ctrl + (или Ctrl + =) — приближаем
|
|
246
|
+
if event.key == 'ctrl+plus' or event.key == 'ctrl+equal' or event.key == 'ctrl+=' or event.key == 'ctrl++':
|
|
247
|
+
new_xrange = cur_xrange / scale_factor
|
|
248
|
+
new_yrange = cur_yrange / scale_factor
|
|
249
|
+
# Если нажато Ctrl - — отдаляем
|
|
250
|
+
elif event.key == 'ctrl+minus' or event.key == 'ctrl+-':
|
|
251
|
+
new_xrange = cur_xrange * scale_factor
|
|
252
|
+
new_yrange = cur_yrange * scale_factor
|
|
253
|
+
else:
|
|
254
|
+
return # Если нажата другая клавиша, ничего не делаем
|
|
255
|
+
|
|
256
|
+
# Устанавливаем новые границы, центрируя обзор
|
|
257
|
+
self.ax.set_xlim([x_center - new_xrange / 2, x_center + new_xrange / 2])
|
|
258
|
+
self.ax.set_ylim([y_center - new_yrange / 2, y_center + new_yrange / 2])
|
|
259
|
+
|
|
260
|
+
# Принудительно перерисовываем график
|
|
261
|
+
event.canvas.draw_idle()
|
|
262
|
+
# === 2. Увеличиваем разрешение для гладкости (было 500, стало 1000) ===
|
|
263
|
+
if self.lib == 'implicit' or (has_odz and self.lib is None):
|
|
264
|
+
from sympy.plotting import plot_implicit
|
|
265
|
+
if isinstance(self.limit, (int, float)):
|
|
266
|
+
self.p = plot_implicit(self.eq, (var1, -self.limit, self.limit), (var2, -self.limit, self.limit), show=False, n=self.n, depth=self.depth)
|
|
267
|
+
elif isinstance(self.limit, tuple):
|
|
268
|
+
if len(self.limit) == 4:
|
|
269
|
+
self.p = plot_implicit(self.eq, (var1, *self.limit[:2]), (var2, *self.limit[2:]), show=False, n=self.n, depth=self.depth)
|
|
270
|
+
else:
|
|
271
|
+
raise ValueError(f"Length mismatch (expected 4), got {len(self.limit)}.")
|
|
272
|
+
else:
|
|
273
|
+
raise TypeError(f"Type mismatch.\nExpected float | tuple[float, float, float, float], got {type(self.limit)}.")
|
|
274
|
+
self.p.process_series() # Отрисовываем линию
|
|
275
|
+
|
|
276
|
+
self.ax = self.p.ax
|
|
277
|
+
logging.info(f"{self.ax}")
|
|
278
|
+
self.fig = self.p.fig
|
|
279
|
+
if self.interact:
|
|
280
|
+
self.fig.canvas.mpl_connect('key_press_event', zoom_key_handler)
|
|
281
|
+
x_coords_str = ", ".join([f"({x:.2f}, 0)" for x in x_intercepts]) if x_intercepts else "Нет"
|
|
282
|
+
y_coords_str = ", ".join([f"(0, {y:.2f})" for y in y_intercepts]) if y_intercepts else "Нет"
|
|
283
|
+
|
|
284
|
+
# Три пустые линии для легенды (они не видны на графике, но создают текст в легенде)
|
|
285
|
+
self.ax.plot([], [], ' ', label=f"${latex(self.eq)}$") # Красивое уравнение через LaTeX
|
|
286
|
+
self.ax.plot([], [], ' ', label=f"Пересечения с X: {x_coords_str}") # Текст про X
|
|
287
|
+
self.ax.plot([], [], ' ', label=f"Пересечения с Y: {y_coords_str}") # Текст про Y
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
patches = self.ax.patches
|
|
291
|
+
logging.info(f"{patches}")
|
|
292
|
+
# === УНИВЕРСАЛЬНОЕ ИЗМЕНЕНИЕ ТОЛЬКО ГРАФИКА (БЕЗ РАМОК) ===
|
|
293
|
+
|
|
294
|
+
# 1. Работаем с контурами и коллекциями (contour/plot_implicit)
|
|
295
|
+
for collection in self.ax.collections:
|
|
296
|
+
collection.set_linewidth(self.linewidth)
|
|
297
|
+
collection.set_edgecolor(self.color)
|
|
298
|
+
|
|
299
|
+
# 2. Работаем с патчами (заливки в implicit), но пропускаем оси (Spines)
|
|
300
|
+
from matplotlib.spines import Spine
|
|
301
|
+
for patch in self.ax.patches:
|
|
302
|
+
# Если это рамка — пропускаем
|
|
303
|
+
if isinstance(patch, Spine):
|
|
304
|
+
continue
|
|
305
|
+
patch.set_linewidth(self.linewidth)
|
|
306
|
+
patch.set_edgecolor(self.color)
|
|
307
|
+
|
|
308
|
+
# 3. Работаем с обычными линиями (например, ax.plot)
|
|
309
|
+
for line in self.ax.lines:
|
|
310
|
+
line.set_linewidth(self.linewidth)
|
|
311
|
+
line.set_color(self.color)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Дальше твой код с точками и оформлением...
|
|
316
|
+
try:
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
|
|
320
|
+
for x in x_intercepts: self.ax.scatter(x, 0, s=60, color='red', marker='o', zorder=5)
|
|
321
|
+
except:
|
|
322
|
+
pass
|
|
323
|
+
try:
|
|
324
|
+
|
|
325
|
+
for y in y_intercepts: self.ax.scatter(0, y, s=60, color='blue', marker='o', zorder=5)
|
|
326
|
+
except:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
self.ax.axhline(0, color='black', linewidth=1)
|
|
330
|
+
self.ax.axvline(0, color='black', linewidth=1)
|
|
331
|
+
self.ax.set_xlim(self.xlims)
|
|
332
|
+
self.ax.set_ylim(self.ylims)
|
|
333
|
+
self.ax.set_xlabel(str(var1))
|
|
334
|
+
self.ax.set_ylabel(str(var2))
|
|
335
|
+
if self.text_legend:
|
|
336
|
+
self.ax.plot([], [], ' ', label=f"{self.text_legend}")
|
|
337
|
+
if self.legend:
|
|
338
|
+
self.ax.legend()
|
|
339
|
+
self.ax.grid(True, linestyle='--', alpha=0.7)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logging.warning(f"Ошибка при добавлении оформления на p.ax: {e}")
|
|
342
|
+
|
|
343
|
+
self.p.fig.tight_layout()
|
|
344
|
+
if self.show:
|
|
345
|
+
self.p.plt.show()
|
|
346
|
+
return
|
|
347
|
+
else:
|
|
348
|
+
if isinstance(self.limit, (float, int)):
|
|
349
|
+
|
|
350
|
+
x_vals = np.linspace(-self.limit, self.limit, self.n)
|
|
351
|
+
y_vals = np.linspace(-self.limit, self.limit, self.n)
|
|
352
|
+
elif isinstance(self.limit, tuple):
|
|
353
|
+
if len(self.limit) == 4:
|
|
354
|
+
x_vals = np.linspace(self.limit[0], self.limit[1], self.n)
|
|
355
|
+
y_vals = np.linspace(self.limit[2], self.limit[3], self.n)
|
|
356
|
+
else:
|
|
357
|
+
raise ValueError(f"Length mismatch (expected 4), got {len(self.limit)}.")
|
|
358
|
+
else:
|
|
359
|
+
raise TypeError(f"Type mismatch.\nExpected float | tuple[float, float, float, float], got {type(self.limit)}.")
|
|
360
|
+
y_vals[~np.isfinite(y_vals)] = np.nan
|
|
361
|
+
X, Y = np.meshgrid(x_vals, y_vals)
|
|
362
|
+
f = lambdify((var1, var2), expr, modules='numpy')
|
|
363
|
+
# print(x_vals, y_vals, X, Y, f, sep='\n<------------------------------->\n')
|
|
364
|
+
Z = f(X, Y)
|
|
365
|
+
# print(Z)
|
|
366
|
+
self.fig, self.ax = plt.subplots()
|
|
367
|
+
x_coords_str = ", ".join([f"({x:.2f}, 0)" for x in x_intercepts]) if x_intercepts else "Нет"
|
|
368
|
+
y_coords_str = ", ".join([f"(0, {y:.2f})" for y in y_intercepts]) if y_intercepts else "Нет"
|
|
369
|
+
self.ax.contour(X, Y, Z, levels=[0], colors='blue', linewidths=self.linewidth)
|
|
370
|
+
|
|
371
|
+
self.ax.plot([], [], ' ', label=f"Уравнение: ${latex(self.eq)}$")
|
|
372
|
+
|
|
373
|
+
# Оси X и Y
|
|
374
|
+
self.ax.axhline(0, color='black', linewidth=1)
|
|
375
|
+
self.ax.axvline(0, color='black', linewidth=1)
|
|
376
|
+
|
|
377
|
+
# Точки пересечения (теперь они не вызовут ошибку, если не найдутся)
|
|
378
|
+
self.ax.scatter([x for x in x_intercepts], [0] * len(x_intercepts), s=60, color='red', marker='o', zorder=5,
|
|
379
|
+
label=f"Пересечения с X: {x_coords_str}")
|
|
380
|
+
|
|
381
|
+
self.ax.scatter([0] * len(y_intercepts), [y for y in y_intercepts], s=60, color='blue', marker='o', zorder=5,
|
|
382
|
+
label=f"Пересечения с Y: {y_coords_str}")
|
|
383
|
+
|
|
384
|
+
# Подписи осей строго по переменным
|
|
385
|
+
self.ax.set_xlabel(str(var1))
|
|
386
|
+
self.ax.set_ylabel(str(var2))
|
|
387
|
+
self.ax.grid(True, linestyle='--', alpha=0.7)
|
|
388
|
+
self.ax.set_xlim(self.xlims)
|
|
389
|
+
self.ax.set_ylim(self.ylims)
|
|
390
|
+
if self.text_legend:
|
|
391
|
+
self.ax.plot([], [], ' ', label=f"{self.text_legend}")
|
|
392
|
+
if self.legend:
|
|
393
|
+
self.ax.legend()
|
|
394
|
+
plt.show()
|
|
395
|
+
|
|
396
|
+
except Exception as e1:
|
|
397
|
+
logging.warning(f"Matplotlib contour не сработал: {e1}")
|
|
398
|
+
# Если контур упал (сингулярности), используем sympy.plot_implicit
|
|
399
|
+
from sympy.plotting import plot_implicit
|
|
400
|
+
p = plot_implicit(self.eq, (var1, -10, 10), (var2, -10, 10), show=False)
|
|
401
|
+
p.show()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adopt-plot
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A universal plotter for implicit functions using adaptive engine selection.
|
|
5
|
+
Author: VartRusData
|
|
6
|
+
License: BSD 3-Clause
|
|
7
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: sympy
|
|
12
|
+
Requires-Dist: matplotlib
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# adopt-plot
|
|
17
|
+
|
|
18
|
+
A simple Python library for plotting implicit functions with adaptive engine selection.
|
|
19
|
+
Just pass an equation and get a beautiful, mathematically correct plot.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/adopt_plot/__init__.py
|
|
5
|
+
src/adopt_plot/plot.py
|
|
6
|
+
src/adopt_plot.egg-info/PKG-INFO
|
|
7
|
+
src/adopt_plot.egg-info/SOURCES.txt
|
|
8
|
+
src/adopt_plot.egg-info/dependency_links.txt
|
|
9
|
+
src/adopt_plot.egg-info/requires.txt
|
|
10
|
+
src/adopt_plot.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adopt_plot
|