# -*- coding: utf-8 -*-
"""
Graphs is a simple log viewer using matplotlib to create tracks of log
data. Allows graphically editing curve data through manual changes and
bulk shifting.
"""
import os
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.text import Text
from matplotlib.widgets import RadioButtons
import xml.etree.ElementTree as ET
[docs]class LogViewer(object):
"""LogViewer
Uses matplotlib to create a figure and axes to display log data.
XML templates are required to diplay curve data, with a few
defaults provided.
Attributes
----------
log : Log
The Log class with data updated from any edits performed
within LogViewer
fig : Figure
The matplotlib Figure object
axes : ndarray
numpy ndarray of AxesSubplot objects where each axes is a
curve track
Parameters
----------
log : Log
A `Log` object with curve data to display in the LogViewer
template_xml_path : str (default None)
Path to xml template.
template_defaults : str
Name of default template options. Uses prebuilt template to
display data
top : float (default None)
Setting to set initial top of graph
height : float (default None)
Setting to set amout of feet to display
Note
----
template_defaults string must be from these options
.. code-block:: bash
raw
multimin_oil
multimin_gas
multimin_oil_sum
multimin_gas_sum
Examples
--------
>>> import petropy as ptr
# create LogViewer with default triple-combo template and display
#
# Load sample wolfcamp log
>>> log = ptr.log_data('WFMP')
# create LogViewer with 'raw' template
>>> viewer = ptr.LogViewer(log, template_defaults = 'raw')
>>> viewer.show() # diplay log data
>>> import petropy as ptr
# create LogViewer for 11 x 17 paper
# with default triple-combo template and save
#
# Load sample wolfcamp log
>>> log = ptr.log_data('WFMP')
# create LogViewer with 'raw' template (default)
>>> viewer = ptr.LogViewer(log)
# use fig attribute to change size to 17 x 11
>>> viewer.fig.set_size_inches(17, 11)
# file path for image file
>>> s = 'path/to/save/wfmp_log.png'
# saves matplotlib figure
>>> viewer.fig.savefig(s)
>>> import petropy as ptr
# create LogViewer with custom template and save
#
# Load sample wolfcamp log
>>> log = ptr.log_data('WFMP')
# specify path to template file
>>> t = 'path/to/my/custom/template.xml'
# create log view with template
>>> viewer = ptr.LogViewer(log, template_xml_path = t)
# define path to save the log picture
>>> s = 'path/to/save/figure.png'
# save figure using matplotlib savefig method
>>> viewer.fig.savefig(s)
>>> import petropy as ptr
# create LogViewer with default triple-combo template,
# make graphical edits, then save log changes
#
# Load sample wolfcamp log
>>> log = ptr.log_data('WFMP')
>>> viewer = ptr.LogViewer(log)
# diplay log data and graphically edit
>>> viewer.show(edit_mode = True)
# define path to save the updated log data
>>> file_path = 'path/to/save/wfmp_edits.las'
# write updated log to new las file.
# Executed after figures are closed.
>>> viewer.log.write(file_path)
Raises
------
ValueError
If template_defaults parameter is not in key of dictionary
items
ValueError
If template_xml_path are both specified
ValueError
tick spacing must be specified in depth track
ValueError
number spacing must be specified in depth track
ValueError
left and right values for each curve must be specified in xml
template
ValueError
curve must be in log
ValueError
curve display_name must be specified in xml template
ValueError
curve curve_name must be specified in xml template
ValueError
curve fill_color must be specified for cumulative tracks
"""
def __init__(self, log, template_xml_path = None,
template_defaults = None, top = None, height = None):
self.log = log
self.template_xml_path = template_xml_path
self.template_defaults = template_defaults
self.top = top
self.height = height
### private parameters for graphically editing curves ###
# stores display name of curve
self._edit_curve = None
# stores matplotlib line objects by display name
self._edit_curve_lines = {}
# stores bool to show downclick to edit curve
self._edit_lock = False
# radio button for editing modes
self._radio_button = None
# display name to log column name dictionary
self._display_name_to_curve_name = {}
default_templates_paths = {
'raw': 'default_raw_template.xml',
'multimin_oil': 'default_multimin_oil_template.xml',
'multimin_oil_sum':'default_multimin_oil_sum_template.xml',
'full_oil': 'default_oil_full_template.xml',
'multimin_gas': 'default_multimin_gas_template.xml',
'multimin_gas_sum': 'default_multimin_gas_sum_template.xml',
'full_gas': 'default_gas_full_template.xml'
}
file_dir = os.path.dirname(__file__)
if template_xml_path is None and template_defaults is None:
template_xml_path = os.path.join(file_dir, 'data',
'default_raw_template.xml')
elif template_xml_path is None and \
template_defaults is not None:
if template_defaults in default_templates_paths:
file_name = default_templates_paths[template_defaults]
else:
print('template_defaults paramter must be in:')
for key in default_templates_paths:
print(key)
raise ValueError("%s is not valid template_defaults \
parameter" % template_defaults)
template_xml_path = os.path.join(file_dir, 'data',
file_name)
elif template_xml_path is not None and \
template_defaults is None:
template_xml_path = template_xml_path
else:
raise ValueError('template_xml_path and template_defaults \
cannot both be specified.')
with open(template_xml_path, 'r') as f:
root = ET.fromstring(f.read())
if 'top' in root.attrib and top is None:
top = float(root.attrib['top'])
elif top is None:
top = self.log[0].min()
if 'height' in root.attrib and height is None:
bottom = top + float(root.attrib['height'])
elif height is None:
bottom = top + 500
else:
bottom = top + height
### format matplotlib figure ###
tracks = root.findall('track')
formations = root.findall('formation')
num_tracks = len(tracks)
self.fig, self.axes = plt.subplots(1, num_tracks + 2,
sharey = True)
### add formation background color ###
for formation in formations:
if 'name' in formation.attrib:
name = formation.attrib['name']
else:
raise ValueError('Formation name required in \
template %s' % template_xml_path)
if 'color' in formation.attrib:
color = formation.attrib['color']
else:
raise ValueError('Color required for formation %s \
in template %s' % (name, template_xml_path))
if 'alpha' in formation.attrib:
alpha = float(formation.attrib['alpha'])
else:
alpha = 0.5
formation_top = self.log.tops[name]
formation_bottom = self.log.next_formation_depth(name)
formation_mid = (formation_top + formation_bottom) / 2.0
for ax in self.axes:
ax.axhspan(formation_top, formation_bottom,
facecolor = color, alpha = alpha)
for ax in (self.axes[0], self.axes[-1]):
ax.text(0.5, formation_mid, name,
verticalalignment = 'center',
horizontalalignment = 'center',
rotation = 90, fontsize = 16)
numbers = None
ticks = None
track_widths = [0.5]
track_height = 0.78
depth_track_numbers = []
track_names = []
max_track_curves = 0
c = 0
for t, track in enumerate(tracks):
ax = self.axes[t + 1]
scale = 'linear'
if 'display_name' in track.attrib:
track_display_name = track.attrib['display_name']
track_names.append(track_display_name)
else:
track_names.append(None)
track_display_name = ''
if 'scale' in track.attrib:
scale = track.attrib['scale']
ax.set_xscale(scale, nonposx = 'clip')
track_width = 1
if 'width' in track.attrib:
track_width = float(track.attrib['width'])
track_widths.append(track_width)
if track_display_name == 'DEPTH':
ax.set_xlim(0,1)
depth_track_numbers.append(t + 1)
if 'tick_spacing' in track.attrib:
tick_spacing = int(track.attrib['tick_spacing'])
else:
raise ValueError('tick_spacing is required for \
depth track.')
if 'number_spacing' in track.attrib:
number_spacing =int(track.attrib['number_spacing'])
else:
raise ValueError('number_spacing is required for \
depth track.')
if 'line_spacing' in track.attrib:
line_spacing = int(track.attrib['line_spacing'])
else:
raise ValueError('line_spacing is required for \
depth track.')
font_size = 16
if 'font_size' in track.attrib:
font_size = float(track.attrib['font_size'])
max_depth = self.log[0].max()
ticks = range(0, int(max_depth) + tick_spacing,
tick_spacing)
numbers = range(0, int(max_depth) + number_spacing,
number_spacing)
lines = range(0, int(max_depth) + line_spacing,
line_spacing)
for n in numbers:
ax.text(0.5, n, str(int(n)),
horizontalalignment='center',
verticalalignment = 'center',
clip_on = True,
fontsize = font_size)
elif 'cumulative' in track.attrib:
if 'left' not in track.attrib or \
'right' not in track.attrib:
raise ValueError('left and right values must be \
specified in cumulative tracks for template %s.' \
% template_xml_path)
left = float(track.attrib['left'])
right = float(track.attrib['right'])
invert_axis = False
if right < left:
left, right = right, left
invert_axis = True
curve_sums = []
colors = []
summation = np.asarray(self.log[0] * 0)
for c, curve in enumerate(track):
### names ###
if 'curve_name' not in curve.attrib:
raise ValueError('Curve Name required in \
template at %s' % template_xml_path)
curve_name = curve.attrib['curve_name']
if curve_name not in self.log.keys():
raise ValueError('Curve %s not found in log.' \
% curve_name)
if 'fill_color' not in curve.attrib:
raise ValueError('Curve fill_color must be \
specificied for cumulative \
track in template at %s' % \
template_xml_path)
fill_color = curve.attrib['fill_color']
increase=summation+np.asarray(self.log[curve_name])
ax.fill_betweenx(self.log[0],
summation,
increase,
color = fill_color)
summation = increase
### display ###
if invert_axis:
x=(len(track) - float(c)) / len(track) - 1.0/ \
(2 * len(track))
else:
x=float(c) / len(track) + 1.0 /(2 * len(track))
ax.text(x, 1.03, curve_name, rotation = 90,
horizontalalignment = 'center',
verticalalignment = 'bottom',
transform = ax.transAxes,
fontsize = 12,
color = fill_color)
if 'major_lines' in track.attrib:
num_major_lines=int(track.attrib['major_lines']) +1
dist = abs(left - right) / num_major_lines
major_lines = np.arange(left + dist, right, dist)
for m in major_lines:
ax.plot((m, m), (0, self.log[0].max()),
color = '#c0c0c0', lw = 0.5)
ax.set_xlim(left, right)
if invert_axis:
ax.invert_xaxis()
c = int(c / 2) + 1
else:
for c, curve in enumerate(track):
### names ###
if 'curve_name' in curve.attrib:
curve_name = curve.attrib['curve_name']
else:
raise ValueError('Curve Name required in \
template at %s' % template_xml_path)
if curve_name not in self.log.keys():
raise ValueError('Curve %s not found in log.' \
% curve_name)
### style and scale ###
left = None
right = None
if 'left' in curve.attrib:
left = float(curve.attrib['left'])
left_label = ' ' + curve.attrib['left']
if 'right' in curve.attrib:
right = float(curve.attrib['right'])
right_label = curve.attrib['right'] + ' '
if left is None:
print('Adjust template at %s.' % \
template_xml_path)
raise ValueError('Left X axis not found for \
curve %s.' % curve_name)
if right is None:
print('Adjust template at %s.' \
% template_xml_path)
raise ValueError('Right X axis not found for \
curve %s.' % curve_name)
line_style = '-'
color = '#000000'
width = 1
alpha = 1
marker = None
marker_size = 0
if 'line_style' in curve.attrib:
line_style = curve.attrib['line_style']
if 'color' in curve.attrib:
color = curve.attrib['color']
if 'width' in curve.attrib:
width = float(curve.attrib['width'])
if 'alpha' in curve.attrib:
alpha = float(curve.attrib['alpha'])
if 'marker' in curve.attrib:
marker = curve.attrib['marker']
if 'marker_size' in curve.attrib:
marker_size =float(curve.attrib['marker_size'])
if scale == 'log':
ax.set_xlim(left, right)
x = self.log[curve_name]
m = None
b = None
if 'left_color_value' in curve.attrib:
left_color_value = \
float(curve.attrib['left_color_value'])
else:
left_color_value = left
if 'right_color_value' in curve.attrib:
right_color_value = \
float(curve.attrib['right_color_value'])
else:
right_color_value = right
else:
ax.set_xlim(0,1)
m = (1 - 0) / (right - left)
b = -m * left
x = m * self.log[curve_name] + b
left = 0
right = 1
if 'left_color_value' in curve.attrib:
left_color_value = \
float(curve.attrib['left_color_value'])
left_color_value = m * left_color_value + b
else:
left_color_value = left
if 'right_color_value' in curve.attrib:
right_color_value = \
float(curve.attrib['right_color_value'])
right_color_value=m * right_color_value + b
else:
right_color_value = right
### label ###
if 'display_name' in curve.attrib:
self._display_name_to_curve_name\
[curve.attrib['display_name']] = curve_name
ax.text(0.5, 0.98 + 0.035 * (c + 1),
curve.attrib['display_name'],
horizontalalignment = 'center',
verticalalignment = 'bottom',
transform = ax.transAxes,
fontsize = 12,
color = color,
picker = True)
ax.text(0, 0.98 + 0.035 * (c + 1),
left_label,
horizontalalignment = 'left',
verticalalignment = 'bottom',
transform = ax.transAxes,
fontsize = 12,
color = color)
ax.text(1, 0.98 + 0.035 * (c + 1),
right_label,
horizontalalignment = 'right',
verticalalignment = 'bottom',
transform = ax.transAxes,
fontsize = 12,
color = color)
if 'fill' in curve.attrib:
fill_color = '#000000'
if 'fill_color' in curve.attrib:
fill_color = curve.attrib['fill_color']
if curve.attrib['fill'] == 'left':
baseline = left
elif curve.attrib['fill'] == 'right':
baseline = right
ax.fill_betweenx(self.log[0],
baseline,
x,
color = fill_color)
elif 'fill_color_map' in curve.attrib:
cmap_name = curve.attrib['fill_color_map']
span = \
abs(left_color_value - right_color_value)
cmap = plt.get_cmap(cmap_name)
if len(np.unique(x)) < 50:
colored_index = np.unique(x)
else:
color_index=np.arange(left_color_value,\
right_color_value,\
span / 50.0)
if curve.attrib['fill'] == 'left':
baseline = np.ones(len(x)) * \
min(left_color_value, left)
elif curve.attrib['fill'] == 'right':
baseline = np.ones(len(x)) * \
max(right_color_value, right)
for ci in sorted(color_index):
ci_value = (ci - left_color_value)/span
color = cmap(ci_value)
ax.fill_betweenx(self.log[0],
baseline,
x,
where = x >= ci,
color = color)
if 'right_cutoff_fill' in curve.attrib:
fill_color = '#000000'
if 'right_cutoff_fill_color' in curve.attrib:
fill_color = \
curve.attrib['right_cutoff_fill_color']
cutoff_value = \
float(curve.attrib['right_cutoff_fill'])
if scale != 'log':
v = m * cutoff_value + b
else:
v = cutoff_value
ax.fill_betweenx(self.log[0],
v,
x,
color = fill_color,
where = v < x)
ax.plot(v * np.ones(len(self.log[0][v < x])),
self.log[0][v < x], c = "#000000",
lw = 0.5)
if 'left_cutoff_fill' in curve.attrib:
fill_color = '#000000'
if 'left_cutoff_fill_color' in curve.attrib:
fill_color = \
curve.attrib['left_cutoff_fill_color']
cutoff_value = \
float(curve.attrib['left_cutoff_fill'])
if scale != 'log':
v = m * cutoff_value + b
else:
v = cutoff_value
ax.fill_betweenx(self.log[0],
v,
x,
color = fill_color,
where = x < v)
ax.plot(v * np.ones(len(self.log[0][x < v])),
self.log[0][x < v], c = "#000000",
lw = 0.5)
if 'left_crossover' in curve.attrib:
fill_color = '#000000'
if 'left_crossover_fill_color' in curve.attrib:
fill_color = \
curve.attrib['left_crossover_fill_color']
left_curve = curve.attrib['left_crossover']
if left_curve not in self.log.keys():
raise ValueError('Curve %s not found in \
log.' % left_curve)
if 'left_crossover_left' not in curve.attrib \
and 'left_crossover_right' not in curve.attrib:
raise ValueError('left and right crossover \
values not found in template %s' \
% template_xml_path)
left_crossover_left = \
float(curve.attrib['left_crossover_left'])
left_crossover_right = \
float(curve.attrib['left_crossover_right'])
if scale != 'log':
m = (1 - 0) / (left_crossover_right - \
left_crossover_left)
b = -m * left_crossover_left
v = m * self.log[left_curve] + b
else:
v = self.log[left_curve]
ax.fill_betweenx(self.log[0],
x,
v,
color = fill_color,
where = v < x)
if 'right_crossover' in curve.attrib:
fill_color = '#000000'
if 'right_crossover_fill_color' in curve.attrib:
fill_color = \
curve.attrib['right_crossover_fill_color']
left_curve = curve.attrib['right_crossover']
if left_curve not in self.log.keys():
raise ValueError('Curve %s not found in \
log.' % left_curve)
if 'right_crossover_left' not in curve.attrib \
and 'right_crossover_right' not in curve.attrib:
raise ValueError('left and right crossover \
values not found in template %s' \
% template_xml_path)
left_crossover_left = \
float(curve.attrib['right_crossover_left'])
left_crossover_right = \
float(curve.attrib['right_crossover_right'])
if scale != 'log':
m = (1 - 0) / (left_crossover_right - \
left_crossover_left)
b = -m * left_crossover_left
v = m * self.log[left_curve] + b
else:
v = self.log[left_curve]
ax.fill_betweenx(self.log[0],
x,
v,
color = fill_color,
where = v > x)
curve_line = ax.plot(x,
self.log[0],
c = color,
lw = width,
ls = line_style,
marker = marker,
ms = marker_size)[0]
self._edit_curve_lines[curve_name] = \
(curve_line, m, b)
if scale == 'log':
ax.xaxis.grid(True, which = 'both',color='#e0e0e0')
elif 'major_lines' in track.attrib:
num_major_lines = \
int(track.attrib['major_lines']) + 1
dist = abs(left - right) / num_major_lines
major_lines = np.arange(left + dist, right, dist)
for m in major_lines:
ax.plot((m, m), (0, self.log[0].max()),
color = '#c0c0c0', lw = 0.5)
if max_track_curves < 5:
max_track_curves = 5
else:
max_track_curves += 1
### adjust track widths ###
track_widths.append(0.5)
track_widths = np.asarray(track_widths)
track_widths = track_widths / np.sum(track_widths)
track_locations = [0]
for t in range(1, len(track_widths)):
track_locations.append(track_locations[t - 1] + \
track_widths[t - 1])
for a, ax in enumerate(self.axes):
post = ax.get_position()
new_post = (track_locations[a], 0.01, track_widths[a],
post.height)
ax.set_position(new_post)
if ticks is not None:
ax.set_yticks(lines, minor = False)
ax.set_yticks(ticks, minor = True)
ax.tick_params(axis = 'y', direction = 'inout',
length = 6, width = 1,
colors = '#000000', which = 'minor')
if a not in depth_track_numbers:
ax.yaxis.grid(True, which = 'major')
else:
ax.set_yticks([])
ax.set_xticks([])
if a > 0 and a < len(self.axes) - 1:
track_title = track_names[a - 1]
if track_title is not None:
for i in range(max_track_curves):
track_title += '\n'
ax.set_title(track_title, fontweight = 'bold')
if top is not None and bottom is not None:
plt.ylim((top, bottom))
plt.gca().invert_yaxis()
self.fig.set_size_inches(11, 8.5)
[docs] def show(self, edit_mode = False, window_location = (10, 30)):
"""
Calls matplotlib.pyplot.show() to display log viewer. If
edit_mode == True, it includes options to graphically edit
curve data, and stores these changes within the LogViewer
object. After editing is finished, access the updated
:class:`petropy.Log` data with the .log property.
Parameters
-----------
edit_mode : bool (default False)
Setting to allow editing of curve data
window_location : float, float tuple (default 10, 30)
Tuple of floats to specify top left location of LogViewer
First value is pixels from the left of the screen.
Second value is pixels from the top of the screen.
Example
-------
>>> import petropy as ptr
>>> log = ptr.log_data('WFMP') # sample Wolfcamp log
>>> viewer = ptr.LogViewer(log) # default triple-combo template
>>> viewer.show() # display graphs
>>> import petropy as ptr
>>> log = ptr.log_data('WFMP') # sample Wolfcamp log
>>> viewer = ptr.LogViewer(log)
# display graphs with editing option
>>> viewer.show(edit_mode = True)
>>> file_path = 'path/to/new_file.las'
# writes changed data to new las file
>>> viewer.log.write(file_path)
"""
mngr = self.fig.canvas.manager
geom = mngr.window.geometry()
x, y, dx, dy = geom.getRect()
x = window_location[0]
y = window_location[1]
mngr.window.setGeometry(x, y, dx, dy)
if edit_mode:
self.edit_fig, self.edit_axes = plt.subplots(1)
rax = plt.axes([0, 0, 1, 1])
self._radio_button = RadioButtons(rax, ('No Edit',
'Manual Edit', 'Bulk Shift'))
self._radio_button.on_clicked(self._radio_click)
self.fig.canvas.mpl_connect('pick_event',
self._curve_pick)
self.fig.canvas.mpl_connect('button_press_event',
self._edit_lock_toggle)
self.fig.canvas.mpl_connect('button_release_event',
self._edit_lock_toggle)
self.fig.canvas.mpl_connect('motion_notify_event',
self._draw_curve)
edit_x = x + dx + 17
edit_y = y
mngr = self.edit_fig.canvas.manager
geom = mngr.window.geometry()
x, y, dx, dy = geom.getRect()
mngr.window.setGeometry(edit_x, edit_y, 225, 225)
plt.show()
def _curve_pick(self, event):
"""
Event handler for selecting a curve to edit. Results in
self._edit_curve being set to matplotlib text object. Connected
on line 765 with 'pick_event'.
"""
if self._edit_curve is not None:
self._edit_curve.set_bbox({'facecolor':'white',
'edgecolor': 'white',
'alpha': 0})
self._edit_curve = event.artist
if self._radio_button.value_selected != 'No Edit':
self._edit_curve.set_bbox({'facecolor':'khaki',
'edgecolor': 'khaki',
'alpha': 1})
self.fig.canvas.draw()
def _radio_click(self, label):
"""
Event handler for selecting which editing type to use.
Connected on line 763 with on_click method for RadioButtons
object.
"""
if label == 'No Edit':
if self._edit_curve is not None:
self._edit_curve.set_bbox({'facecolor':'white',
'edgecolor': 'white',
'alpha': 0})
elif label == 'Manual Edit':
if self._edit_curve is not None:
self._edit_curve.set_bbox({'facecolor':'khaki',
'edgecolor': 'khaki',
'alpha': 1})
elif label == 'Bulk Shift':
if self._edit_curve is not None:
self._edit_curve.set_bbox({'facecolor':'khaki',
'edgecolor': 'khaki',
'alpha': 1})
self.fig.canvas.draw()
def _edit_lock_toggle(self, event):
"""
Event handler to check for correct axis associated with
selected curve. Will allow _draw_curve to function if click is
in proper axis based on the self._edit_curve property set with
_curve_pick. Connected on lines 767 and 769 to
:code:`button_press_event` and :code:`button_release_event`.
"""
if self._edit_curve and hasattr(event, 'inaxes'):
if event.inaxes:
ax_num = np.where(self.axes == event.inaxes)[0]
if len(ax_num) > 0:
curve_num = \
self.axes.tolist().index(self._edit_curve.axes)
if ax_num == curve_num:
self._edit_lock = not self._edit_lock
def _draw_curve(self, event):
"""
Event handler for changing data in the figure and in the log
object. Connected on line 771 with :code:`motion_notify_event`.
"""
if self._radio_button.value_selected == 'Manual Edit' and \
self._edit_lock:
x, y = event.xdata, event.ydata
curve_name = \
self._display_name_to_curve_name[self._edit_curve.get_text()]
cursor_depth_index = np.argmin(np.abs(self.log[0] - y))
depth = self.log[0][cursor_depth_index]
line, m, b = self._edit_curve_lines[curve_name]
x_data = line.get_xdata()
x_data[cursor_depth_index] = x
line.set_xdata(x_data)
if m is not None and b is not None:
x = (x - b) / m
self.log[curve_name][cursor_depth_index] = x
self.fig.canvas.draw()
elif self._radio_button.value_selected == 'Bulk Shift' and \
self._edit_lock:
x, y = event.xdata, event.ydata
curve_name = \
self._display_name_to_curve_name[self._edit_curve.get_text()]
cursor_depth_index = np.argmin(np.abs(self.log[0] - y))
depth = self.log[0][cursor_depth_index]
line, m, b = self._edit_curve_lines[curve_name]
x_data = line.get_xdata()
x_diff = x_data[cursor_depth_index] - x
x_data = x_data - x_diff
line.set_xdata(x_data)
if m is not None and b is not None:
x_data = (x_data - b) / m
self.log[curve_name] = x_data
self.fig.canvas.draw()