├── example.gif ├── README.md ├── .gitignore └── draggable_plot.py /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuma-m/matplotlib-draggable-plot/HEAD/example.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Matplotlib draggable plot example 2 | 3 | ![Draggable plot example](./example.gif) 4 | 5 | ## Installation 6 | 7 | ```bash 8 | $ pip install matplotlib 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```bash 14 | $ python draggable_plot.py 15 | ``` 16 | 17 | - Left click to add new point 18 | - Drag to move point 19 | - Right click to remove point 20 | 21 | 22 | # matplotlib でプロット上の点をドラッグする例 23 | 24 | ## 使い方 25 | 26 | - プロット上を左クリックで点を追加 27 | - プロット上の点をドラッグで移動 28 | - プロット上の点を右クリックで削除 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | ### Jetbrains 4 | .idea/ 5 | 6 | ### Python template 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | -------------------------------------------------------------------------------- /draggable_plot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import math 4 | 5 | import matplotlib.pyplot as plt 6 | from matplotlib.backend_bases import MouseEvent 7 | 8 | 9 | class DraggablePlotExample(object): 10 | u""" An example of plot with draggable markers """ 11 | 12 | def __init__(self): 13 | self._figure, self._axes, self._line = None, None, None 14 | self._dragging_point = None 15 | self._points = {} 16 | 17 | self._init_plot() 18 | 19 | def _init_plot(self): 20 | self._figure = plt.figure("Example plot") 21 | axes = plt.subplot(1, 1, 1) 22 | axes.set_xlim(0, 100) 23 | axes.set_ylim(0, 100) 24 | axes.grid(which="both") 25 | self._axes = axes 26 | 27 | self._figure.canvas.mpl_connect('button_press_event', self._on_click) 28 | self._figure.canvas.mpl_connect('button_release_event', self._on_release) 29 | self._figure.canvas.mpl_connect('motion_notify_event', self._on_motion) 30 | plt.show() 31 | 32 | def _update_plot(self): 33 | if not self._points: 34 | self._line.set_data([], []) 35 | else: 36 | x, y = zip(*sorted(self._points.items())) 37 | # Add new plot 38 | if not self._line: 39 | self._line, = self._axes.plot(x, y, "b", marker="o", markersize=10) 40 | # Update current plot 41 | else: 42 | self._line.set_data(x, y) 43 | self._figure.canvas.draw() 44 | 45 | def _add_point(self, x, y=None): 46 | if isinstance(x, MouseEvent): 47 | x, y = int(x.xdata), int(x.ydata) 48 | self._points[x] = y 49 | return x, y 50 | 51 | def _remove_point(self, x, _): 52 | if x in self._points: 53 | self._points.pop(x) 54 | 55 | def _find_neighbor_point(self, event): 56 | u""" Find point around mouse position 57 | 58 | :rtype: ((int, int)|None) 59 | :return: (x, y) if there are any point around mouse else None 60 | """ 61 | distance_threshold = 3.0 62 | nearest_point = None 63 | min_distance = math.sqrt(2 * (100 ** 2)) 64 | for x, y in self._points.items(): 65 | distance = math.hypot(event.xdata - x, event.ydata - y) 66 | if distance < min_distance: 67 | min_distance = distance 68 | nearest_point = (x, y) 69 | if min_distance < distance_threshold: 70 | return nearest_point 71 | return None 72 | 73 | def _on_click(self, event): 74 | u""" callback method for mouse click event 75 | 76 | :type event: MouseEvent 77 | """ 78 | # left click 79 | if event.button == 1 and event.inaxes in [self._axes]: 80 | point = self._find_neighbor_point(event) 81 | if point: 82 | self._dragging_point = point 83 | else: 84 | self._add_point(event) 85 | self._update_plot() 86 | # right click 87 | elif event.button == 3 and event.inaxes in [self._axes]: 88 | point = self._find_neighbor_point(event) 89 | if point: 90 | self._remove_point(*point) 91 | self._update_plot() 92 | 93 | def _on_release(self, event): 94 | u""" callback method for mouse release event 95 | 96 | :type event: MouseEvent 97 | """ 98 | if event.button == 1 and event.inaxes in [self._axes] and self._dragging_point: 99 | self._dragging_point = None 100 | self._update_plot() 101 | 102 | def _on_motion(self, event): 103 | u""" callback method for mouse motion event 104 | 105 | :type event: MouseEvent 106 | """ 107 | if not self._dragging_point: 108 | return 109 | if event.xdata is None or event.ydata is None: 110 | return 111 | self._remove_point(*self._dragging_point) 112 | self._dragging_point = self._add_point(event) 113 | self._update_plot() 114 | 115 | 116 | if __name__ == "__main__": 117 | plot = DraggablePlotExample() 118 | --------------------------------------------------------------------------------