From 0fb57d7f75b46d39e1fe5461ac657fab1e1efffc Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Thu, 28 Oct 2021 15:35:26 -0400 Subject: [PATCH 01/24] Added dashboard.py / Bar & Envelope Dashboard - Added the dashboard.py file and with it a function to create plotly figures for dashboards of envelope and bar based rolling window plots --- endaq/plot/dashboards.py | 311 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 endaq/plot/dashboards.py diff --git a/endaq/plot/dashboards.py b/endaq/plot/dashboards.py new file mode 100644 index 0000000..a7e09fe --- /dev/null +++ b/endaq/plot/dashboards.py @@ -0,0 +1,311 @@ +import collections + +import numpy as np +import plotly.graph_objects as go +from plotly.subplots import make_subplots +import plotly.io as pio + +from typing import Optional + +import endaq.ide +import endaq.plot + + +def rolling_enveloped_dashboard( + channel_df_dict: dict, desired_num_points: int = 250, num_rows: Optional[int] = None, + num_cols: Optional[int] = 3, width_for_subplot_row: int = 400, height_for_subplot_row: int = 400, + subplot_colors: Optional[collections.Container] = None, min_points_to_plot: int = 1, + plot_as_bars: bool = False, y_axis_bar_plot_padding: float = 0.06) -> go.Figure: + """ + A function to create a Plotly Figure with sub-plots for each of the available data sub-channels, designed to reduce + the number of points/data being plotted without minimizing the insight available from the plots. It will plot + either an envelope for rolling windows of the data (plotting the max and the min as line plots), or a bar based + plot where the top of the bar (rectangle) is the highest value in the time window that bar spans, and the bottom of + the bar is the lowest point in that time window (choosing between them is done with the `plot_as_bars` parameter). + + + :param channel_df_dict: A dictionary mapping channel names to Pandas DataFrames of that channels data + :param desired_num_points: The desired number of points to be plotted in each subplot. The number of points + will be reduced from it's original sampling rate by applying metrics (e.g. min, max) over sliding windows + and then using that information to represent/visualize the data contained within the original data. If less than + the desired number of points are present, then a sliding window will NOT be used, and instead the points will be + plotted as they were originally recorded (also the subplot will NOT be plotted as a bar based plot even if + `plot_as_bars` was set to true). + :param num_rows: The number of columns of subplots to be created inside the Plotly figure. If None is given, (then + `num_cols` must not be None), then this number will automatically be determined by what's needed. If more rows + are specified than are needed, the number of rows will be reduced to the minimum needed to contain all the subplots + :param num_cols: The number of columns of subplots to be created inside the Plotly figure. See the description of + the `num_rows` parameter for more details on this parameter, and how the two interact. This also follows the same + approach to handling None when given + :param width_for_subplot_row: The width of the area used for a single subplot (in pixels). + :param height_for_subplot_row: The height of the area used for a single subplot (in pixels). + :param subplot_colors: An 'array-like' object of strings containing colors to be cycled through for the subplots. + If None is given (which is the default), then the `colorway` variable in Plotly's current theme/template will + be used to color the data on each of the subplots uniquely, repeating from the start of the `colorway` if + all colors have been used. + :param min_points_to_plot: The minimum number of data points required to be present to create a subplot for a + channel/subchannel (NOT including `NaN` values). + :param plot_as_bars: A boolean value indicating if the plot should be visualized as a set of rectangles, where a + shaded rectangle is used to represent the maximum and minimum values of the data during the time window + covered by the rectangle. These maximum and minimum values are visualized by the locations of the top and bottom + edges of the rectangle respectively, unless the height of the rectangle would be 0, in which case a line segment + will be displayed in it's place. If this parameter is `False`, two lines will be plotted for each + of the subplots in the figure being created, creating an 'envelope' around the data. An 'envelope' around the + data consists of a line plotted for the maximum values contained in each of the time windows, and another line + plotted for the minimum values. Together these lines create a boundary which contains all the data points + recorded in the originally recorded data. + :param y_axis_bar_plot_padding: Due to some unknown reason the bar subplots aren't having their y axis ranges + automatically scaled so this is the ratio of the total y-axis data range to pad both the top and bottom of the + y axis with. The default value is the one it appears Plotly uses as well. + :return: The Plotly Figure containing the subplots of sensor data (the 'dashboard') + """ + if not (num_rows is None or isinstance(num_rows, (int, np.integer))): + raise TypeError(f"`num_rows` is of type `{type(num_rows)}`, which is not allowed. " + "`num_rows` can either be `None` or some type of integer.") + elif not (num_cols is None or isinstance(num_cols, (int, np.integer))): + raise TypeError(f"`num_cols` is of type `{type(num_cols)}`, which is not allowed. " + "`num_cols` can either be `None` or some type of integer.") + + # I'm pretty sure the below is correct and it appears to work correctly, but it'd be nice if someone could + # double check this logic + + # This removes any channels with less than `min_points_to_plot` data points per sub-channel, and removes any + # sub-channels which have less than `min_points_to_plot` non-NaN data points + channel_df_dict = { + k: v.drop(columns=v.columns[v.notna().sum(axis=0) < min_points_to_plot]) + for (k, v) in channel_df_dict.items() if v.shape[0] >= min_points_to_plot} + + subplot_titles = [' '.join((k, col)) for (k, v) in channel_df_dict.items() for col in v.columns] + + if num_rows is None and num_cols is None: + raise Exception("Both `num_rows` and `num_columns` were given as `None`! " + "A maximum of one of these two parameters may be given as None.") + elif num_rows is None: + num_rows = 1 + (len(subplot_titles) - 1) // num_cols + elif num_cols is None: + num_cols = 1 + (len(subplot_titles) - 1) // num_rows + elif len(subplot_titles) > num_rows * num_cols: + raise Exception("The values given for `num_rows` and `num_columns` result in a maximum " + f"of {num_rows * num_cols} avaialable sub-plots, but {len(subplot_titles)} subplots need " + "to be plotted! Try setting one of these variables to `None`, it will then " + "automatically be set to the optimal number of rows/columns.") + else: + num_rows = 1 + (len(subplot_titles) - 1) // num_cols + num_cols = int(np.ceil(len(subplot_titles)/num_rows)) + + if subplot_colors is None: + colorway = pio.templates[pio.templates.default]['layout']['colorway'] + else: + colorway = subplot_colors + + fig = make_subplots( + rows=num_rows, + cols=num_cols, + subplot_titles=subplot_titles, + figure=go.Figure( + layout_height=height_for_subplot_row * num_rows, + layout_width=width_for_subplot_row * num_cols, + ), + ) + + # A counter to keep track of which subplot is currently being worked on + subplot_num = 0 + + # A dictionary to be used to modify the Plotly Figure layout all at once after all the elements have been added + layout_changes_to_make = {} + + for channel_data in channel_df_dict.values(): + window = int(np.around(1+(channel_data.shape[0]-1) / desired_num_points, decimals=0)) + + # If a window size of 1 is determined, it sets the stride to 1 so we don't get an error as a result of the + # 0 length stride + stride = 1 if window == 1 else window - 1 + + rolling_n = channel_data.rolling(window) + min_max_tuple = (rolling_n.min()[::stride], rolling_n.max()[::stride]) + min_max_equal = min_max_tuple[0] == min_max_tuple[1] + + # Loop through each of the sub-channels, and their respective '0-height rectangle mask' + for subchannel_name, cur_min_max_equal in min_max_equal[channel_data.columns].iteritems(): + + traces = [] + cur_color = colorway[subplot_num % len(colorway)] + + # If it's going to plot the data with bars + # if plot_as_bars and len(min_max_tuple[0]) > desired_num_points: + if plot_as_bars and len(channel_data) > desired_num_points: + # If there are any 0-height rectangles + if np.any(cur_min_max_equal): + equal_data_df = min_max_tuple[0].loc[cur_min_max_equal.values, subchannel_name] + + # Half of the sampling period + half_dt = np.diff(cur_min_max_equal.index[[0, -1]])[0] / (2 * (len(cur_min_max_equal) - 1)) + + # Initialize the arrays we'll use for creating line segments where + # rectangles would have 0 width so it will end up formatted as follows + # (duplicate values for y since line segements are horizontal): + # x = [x1, x2, None, x3, x4, None, ...] + # y = [y12, y12, None, y34, y34, None, ...] + x_patch_line_segs = np.repeat(equal_data_df.index.values, 3) + y_patch_line_segs = np.repeat(equal_data_df.values, 3) + + # All X axis values are the same, but these values are supposed to represent pairs of start and end + # times for line segments, so the time stamp is shifted half it's duration backwards for the start + # time, and half it's duration forward for the end time + x_patch_line_segs[::3] -= half_dt + x_patch_line_segs[1::3] += half_dt + + # This is done every third value so that every two pairs of points is unconnected from eachother, + # since the (None, None) point will not connect to either the point before it nor behind it + x_patch_line_segs[2::3] = None + y_patch_line_segs[2::3] = None + + traces.append( + go.Scatter( + x=x_patch_line_segs, + y=y_patch_line_segs, + name=subchannel_name, + mode='lines', + line_color=cur_color, + showlegend=False, + ) + ) + + min_data_point = np.min(min_max_tuple[0][subchannel_name]) + max_data_point = np.max(min_max_tuple[1][subchannel_name]) + + y_padding = (max_data_point - min_data_point) * y_axis_bar_plot_padding + + traces.append( + go.Bar( + x=min_max_tuple[0].index, + y=min_max_tuple[1][subchannel_name] - min_max_tuple[0][subchannel_name], + marker_color=cur_color, + marker_line_width=0, + base=min_max_tuple[0][subchannel_name], + showlegend=False, + name=subchannel_name, + ) + ) + + # Adds a (key, value) pair to the dict for setting this subplot's Y-axis display range (applied later) + layout_changes_to_make[f'yaxis{1 + subplot_num}_range'] = [ + min_data_point - y_padding, + max_data_point + y_padding + ] + else: + for cur_df in min_max_tuple: + traces.append( + go.Scatter( + x=cur_df.index, + y=cur_df[subchannel_name], + name=subchannel_name, + line_color=cur_color, + showlegend=False, + ) + ) + + # Add the traces created for the current subchannel of data to the plotly figure + fig.add_traces( + traces, + rows=1 + subplot_num // num_cols, + cols=1 + subplot_num % num_cols, + ) + + subplot_num += 1 + + fig.update_layout( + **layout_changes_to_make, + bargap=0, + ) + return fig + + + + + + +if __name__ == '__main__': + file_urls = ['https://info.endaq.com/hubfs/data/surgical-instrument.ide', + 'https://info.endaq.com/hubfs/data/97c3990f-Drive-Home_70-1616632444.ide', + 'https://info.endaq.com/hubfs/data/High-Drop.ide', + 'https://info.endaq.com/hubfs/data/HiTest-Shock.ide', + 'https://info.endaq.com/hubfs/data/Drive-Home_01.ide', + 'https://info.endaq.com/hubfs/data/Tower-of-Terror.ide', + 'https://info.endaq.com/hubfs/data/Punching-Bag.ide', + 'https://info.endaq.com/hubfs/data/Gun-Stock.ide', + 'https://info.endaq.com/hubfs/data/Seat-Base_21.ide', + 'https://info.endaq.com/hubfs/data/Seat-Top_09.ide', + 'https://info.endaq.com/hubfs/data/Bolted.ide', + 'https://info.endaq.com/hubfs/data/Motorcycle-Car-Crash.ide', + 'https://info.endaq.com/hubfs/data/train-passing.ide', + 'https://info.endaq.com/hubfs/data/baseball.ide', + 'https://info.endaq.com/hubfs/data/Clean-Room-VC.ide', + 'https://info.endaq.com/hubfs/data/enDAQ_Cropped.ide', + 'https://info.endaq.com/hubfs/data/Drive-Home_07.ide', + 'https://info.endaq.com/hubfs/data/ford_f150.ide', + 'https://info.endaq.com/hubfs/data/Drive-Home.ide', + 'https://info.endaq.com/hubfs/data/Mining-Data.ide', + 'https://info.endaq.com/hubfs/data/Mide-Airport-Drive-Lexus-Hybrid-Dash-W8.ide'] + + endaq.plot.utilities.set_theme() + + for j in [4]: + doc = endaq.ide.get_doc(file_urls[j]) + table = endaq.ide.get_channel_table(doc) + + # (IMPORTANT NOTE) The use of this as a dictionary is dependent on it maintaining being 'insertion ordered', + # which is a thing in Python 3.7 (may have existed in a different way in python 3.6, but I'm not sure) + CHANNEL_DFS = { + doc.channels[ch].name: endaq.ide.to_pandas(doc.channels[ch], time_mode='datetime') for ch in doc.channels} + + + + # Examples + rolling_enveloped_dashboard( + CHANNEL_DFS, + desired_num_points=100, + min_points_to_plot=10, + plot_as_bars=True, + height_for_subplot_row=600, + width_for_subplot_row=600, + num_cols=2, + num_rows=None, + ).show() + + + + rolling_enveloped_dashboard( + CHANNEL_DFS, + desired_num_points=1000, + num_rows=2, + num_cols=9999999, + width_for_subplot_row=250, + height_for_subplot_row=250, + ).show() + + rolling_enveloped_dashboard( + CHANNEL_DFS, + desired_num_points=1000, + num_rows=9999999, + num_cols=2, + width_for_subplot_row=250, + height_for_subplot_row=250, + ).show() + + rolling_enveloped_dashboard( + CHANNEL_DFS, + desired_num_points=1000, + min_points_to_plot=10, + plot_as_bars=True, + ).show() + + rolling_enveloped_dashboard( + CHANNEL_DFS, + desired_num_points=1000, + min_points_to_plot=10, + num_rows=999999, + num_cols=4, + ).show() + + print(str(j) + " done!") \ No newline at end of file From e80f5069569eff539dc6fa540405695d1a718328 Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Fri, 29 Oct 2021 11:19:08 -0400 Subject: [PATCH 02/24] Single Plot Envelope/Bar Plot Addition - Modified the `dashboards.rolling_enveloped_dashboard` plot to be able to plot multiple subchannels in a single figure - Added a fucntion to endaq.plots which produces windowed envelope/bar plots for a single dataframe of subchannel data --- endaq/plot/dashboards.py | 86 ++++++++++++++++------- endaq/plot/plots.py | 143 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 200 insertions(+), 29 deletions(-) diff --git a/endaq/plot/dashboards.py b/endaq/plot/dashboards.py index a7e09fe..404d7eb 100644 --- a/endaq/plot/dashboards.py +++ b/endaq/plot/dashboards.py @@ -15,7 +15,8 @@ def rolling_enveloped_dashboard( channel_df_dict: dict, desired_num_points: int = 250, num_rows: Optional[int] = None, num_cols: Optional[int] = 3, width_for_subplot_row: int = 400, height_for_subplot_row: int = 400, subplot_colors: Optional[collections.Container] = None, min_points_to_plot: int = 1, - plot_as_bars: bool = False, y_axis_bar_plot_padding: float = 0.06) -> go.Figure: + plot_as_bars: bool = False, plot_full_single_channel: bool = False, opacity: float = 1, y_axis_bar_plot_padding: float = 0.06 +) -> go.Figure: """ A function to create a Plotly Figure with sub-plots for each of the available data sub-channels, designed to reduce the number of points/data being plotted without minimizing the insight available from the plots. It will plot @@ -54,6 +55,9 @@ def rolling_enveloped_dashboard( data consists of a line plotted for the maximum values contained in each of the time windows, and another line plotted for the minimum values. Together these lines create a boundary which contains all the data points recorded in the originally recorded data. + :param plot_full_single_channel: If instead of a dashboard of subplots a single plot with multiple sub-channels + should be created. If this is True, only one (key, value) pair can be given for the `channel_df_dict` parameter + :param opacity: The opacity to use for plotting bars/lines :param y_axis_bar_plot_padding: Due to some unknown reason the bar subplots aren't having their y axis ranges automatically scaled so this is the ratio of the total y-axis data range to pad both the top and bottom of the y axis with. The default value is the one it appears Plotly uses as well. @@ -78,14 +82,14 @@ def rolling_enveloped_dashboard( subplot_titles = [' '.join((k, col)) for (k, v) in channel_df_dict.items() for col in v.columns] if num_rows is None and num_cols is None: - raise Exception("Both `num_rows` and `num_columns` were given as `None`! " + raise TypeError("Both `num_rows` and `num_columns` were given as `None`! " "A maximum of one of these two parameters may be given as None.") elif num_rows is None: num_rows = 1 + (len(subplot_titles) - 1) // num_cols elif num_cols is None: num_cols = 1 + (len(subplot_titles) - 1) // num_rows elif len(subplot_titles) > num_rows * num_cols: - raise Exception("The values given for `num_rows` and `num_columns` result in a maximum " + raise ValueError("The values given for `num_rows` and `num_columns` result in a maximum " f"of {num_rows * num_cols} avaialable sub-plots, but {len(subplot_titles)} subplots need " "to be plotted! Try setting one of these variables to `None`, it will then " "automatically be set to the optimal number of rows/columns.") @@ -98,15 +102,24 @@ def rolling_enveloped_dashboard( else: colorway = subplot_colors - fig = make_subplots( - rows=num_rows, - cols=num_cols, - subplot_titles=subplot_titles, - figure=go.Figure( - layout_height=height_for_subplot_row * num_rows, - layout_width=width_for_subplot_row * num_cols, - ), - ) + if plot_full_single_channel: + if len(channel_df_dict) != 1: + raise ValueError("The 'channel_df_dict' parameter must be length 1 when " + "'plot_full_single_channel' is set to true!") + + num_rows = 1 + num_cols = 1 + fig = go.Figure(layout_title_text=list(channel_df_dict.keys())[0]) + else: + fig = make_subplots( + rows=num_rows, + cols=num_cols, + subplot_titles=subplot_titles, + figure=go.Figure( + layout_height=height_for_subplot_row * num_rows, + layout_width=width_for_subplot_row * num_cols, + ), + ) # A counter to keep track of which subplot is currently being worked on subplot_num = 0 @@ -115,7 +128,7 @@ def rolling_enveloped_dashboard( layout_changes_to_make = {} for channel_data in channel_df_dict.values(): - window = int(np.around(1+(channel_data.shape[0]-1) / desired_num_points, decimals=0)) + window = int(np.around((channel_data.shape[0]-1) / desired_num_points, decimals=0)) # If a window size of 1 is determined, it sets the stride to 1 so we don't get an error as a result of the # 0 length stride @@ -165,6 +178,7 @@ def rolling_enveloped_dashboard( x=x_patch_line_segs, y=y_patch_line_segs, name=subchannel_name, + opacity=opacity, mode='lines', line_color=cur_color, showlegend=False, @@ -181,6 +195,7 @@ def rolling_enveloped_dashboard( x=min_max_tuple[0].index, y=min_max_tuple[1][subchannel_name] - min_max_tuple[0][subchannel_name], marker_color=cur_color, + opacity=opacity, marker_line_width=0, base=min_max_tuple[0][subchannel_name], showlegend=False, @@ -189,10 +204,16 @@ def rolling_enveloped_dashboard( ) # Adds a (key, value) pair to the dict for setting this subplot's Y-axis display range (applied later) - layout_changes_to_make[f'yaxis{1 + subplot_num}_range'] = [ - min_data_point - y_padding, - max_data_point + y_padding - ] + min_y_range = min_data_point - y_padding + max_y_range = max_data_point + y_padding + y_axis_id = f'yaxis{1 + subplot_num}_range' + if plot_full_single_channel: + y_axis_id = 'yaxis_range' + if layout_changes_to_make: + min_y_range = min(min_y_range, layout_changes_to_make[y_axis_id][0]) + max_y_range = max(max_y_range, layout_changes_to_make[y_axis_id][1]) + + layout_changes_to_make[y_axis_id] = [min_y_range, max_y_range] else: for cur_df in min_max_tuple: traces.append( @@ -200,23 +221,28 @@ def rolling_enveloped_dashboard( x=cur_df.index, y=cur_df[subchannel_name], name=subchannel_name, + opacity=opacity, line_color=cur_color, showlegend=False, ) ) # Add the traces created for the current subchannel of data to the plotly figure - fig.add_traces( - traces, - rows=1 + subplot_num // num_cols, - cols=1 + subplot_num % num_cols, - ) + if plot_full_single_channel: + fig.add_traces(traces) + else: + fig.add_traces( + traces, + rows=1 + subplot_num // num_cols, + cols=1 + subplot_num % num_cols, + ) subplot_num += 1 fig.update_layout( **layout_changes_to_make, bargap=0, + barmode='overlay' ) return fig @@ -259,9 +285,21 @@ def rolling_enveloped_dashboard( CHANNEL_DFS = { doc.channels[ch].name: endaq.ide.to_pandas(doc.channels[ch], time_mode='datetime') for ch in doc.channels} - + SINGLE_CHANNEL = r'40g DC Acceleration' + JUST_ACCEL_DFS = {SINGLE_CHANNEL: CHANNEL_DFS[SINGLE_CHANNEL]} # Examples + rolling_enveloped_dashboard( + JUST_ACCEL_DFS, + plot_full_single_channel=True, + ).show() + + rolling_enveloped_dashboard( + JUST_ACCEL_DFS, + plot_as_bars=True, + plot_full_single_channel=True, + ).show() + rolling_enveloped_dashboard( CHANNEL_DFS, desired_num_points=100, @@ -273,8 +311,6 @@ def rolling_enveloped_dashboard( num_rows=None, ).show() - - rolling_enveloped_dashboard( CHANNEL_DFS, desired_num_points=1000, diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index b5b1e84..5e28453 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -4,17 +4,100 @@ import plotly.express as px from plotly.subplots import make_subplots from scipy import signal +import typing +from typing import Optional +import collections -from endaq.calc.psd import to_octave, welch +from endaq.calc.psd import to_octave, welch # ,sample_spacing THIS ISN'T YET IN MASTER from .utilities import determine_plotly_map_zoom, get_center_of_coordinates - +from .dashboards import rolling_enveloped_dashboard DEFAULT_ATTRIBUTES_TO_PLOT_INDIVIDUALLY = np.array([ 'accelerationPeakFull', 'accelerationRMSFull', 'velocityRMSFull', 'psuedoVelocityPeakFull', 'displacementRMSFull', 'gpsSpeedFull', 'gyroscopeRMSFull', 'microphonoeRMSFull', 'temperatureMeanFull', 'pressureMeanFull']) +def sample_spacing( + df: pd.DataFrame, convert: typing.Literal[None, "to_seconds"] = "to_seconds" +): + """ + REMOVE THIS FUNCTION WHEN IT GETS MERGED INTO ENDAQ.CALC + + Calculate the average spacing between individual samples. + For time indices, this calculates the sampling period `dt`. + :param df: the input data + :param convert: if `"to_seconds"` (default), convert any time objects into + floating-point seconds + """ + dt = (df.index[-1] - df.index[0]) / (len(df.index) - 1) + if convert == "to_seconds" and isinstance(dt, (np.timedelta64, pd.Timedelta)): + dt = dt / np.timedelta64(1, "s") + + return dt + + +def get_channel_ids(doc): + return [list(d)[0] for d in list(doc.channels.items())] + + +def plot_row(doc, plot_len, mean_thresh, dfs, df_ide, line_color="#EE7F27", std_color="#6914F0"): + col = 1 + cols = df_ide.shape[0] + fig = make_subplots(rows=1, cols=cols, subplot_titles=list(df_ide["Name"])) + + for i in get_channel_ids(doc): + df = dfs[df_ide[df_ide["CH ID"] == i]["CH #"].iloc[0]] + fs = df_ide[df_ide["CH ID"] == i]["Frequency (Hz)"].iloc[0] + + n = int(df.shape[0] / plot_len) + time = df.reset_index()["Time (s)"] + if n > 0: + time = time.rolling(n).mean().iloc[::n] + + for c, c_i in enumerate(df.columns): + if n == 0: + fig.add_trace( + go.Scatter(x=time, y=df[c], name=str(i + c_i / 10), line=dict(color=line_color)), + row=1, + col=col, + ) + elif fs < mean_thresh: + fig.add_trace( + go.Scatter( + x=time, + y=df[c].rolling(n).mean().iloc[::n], + name="Mean", + line=dict(color=line_color), + ), + row=1, + col=col, + ) + else: + fig.add_trace( + go.Scatter( + x=time, + y=df[c].abs().rolling(n).max().iloc[::n], + name="Max", + line=dict(color=line_color), + ), + row=1, + col=col, + ) + fig.add_trace( + go.Scatter( + x=time, + y=df[c].rolling(n).std().iloc[::n], + name="Std Dev", + line=dict(color=std_color), + ), + row=1, + col=col, + ) + col += 1 + + return fig.update_layout(width=cols * 400, showlegend=False) + def multi_file_plot_attributes(multi_file_db, rows_to_plot=DEFAULT_ATTRIBUTES_TO_PLOT_INDIVIDUALLY, recording_colors=None, width_per_subplot=400): @@ -239,7 +322,7 @@ def octave_spectrogram(df, window, bins_per_octave=3, freq_start=20, max_freq=fl """ Produces an octave spectrogram of the given data. - :param df: The dataframe of sensor data + :param df: The dataframe of sensor data. This must only have 1 column. :param window: The time window for each of the columns in the spectrogram :param bins_per_octave: The number of frequency bins per octave :param freq_start: The center of the first frequency bin @@ -252,9 +335,12 @@ def octave_spectrogram(df, window, bins_per_octave=3, freq_start=20, max_freq=fl - the spectrogram data - the corresponding plotly figure """ + if len(df) != 1: + raise ValueError("The parameter 'df' must have only one column of data!") + ary = df.values.squeeze() - fs = (len(df) - 1) / (df.index[-1] - df.index[0]) + fs = 1/sample_spacing(df)#(len(df) - 1) / (df.index[-1] - df.index[0]) N = int(fs * window) #Number of points in the fft w = signal.blackman(N, False) @@ -331,3 +417,52 @@ def octave_psd_bar_plot(df, bins_per_octave=3, f_start=20, yaxis_title='', log_s return fig +def rolling_min_max_envelope(df: pd.DataFrame, desired_num_points: int = 250, plot_as_bars: bool = False, + plot_title: str = "", opacity: float = 1, + colors_to_use: Optional[collections.Container] = None + ) -> go.Figure: + """ + A function to create a Plotly Figure to plot the data for each of the available data sub-channels, designed to + reduce the number of points/data being plotted without minimizing the insight available from the plots. It will + plot either an envelope for rolling windows of the data (plotting the max and the min as line plots), or a bar based + plot where the top of the bar (rectangle) is the highest value in the time window that bar spans, and the bottom of + the bar is the lowest point in that time window (choosing between them is done with the `plot_as_bars` parameter). + + :param df: The dataframe of sub-channel data indexed by time stamps + :param desired_num_points: The desired number of points to be plotted for each subchannel. The number of points + will be reduced from it's original sampling rate by applying metrics (e.g. min, max) over sliding windows + and then using that information to represent/visualize the data contained within the original data. If less than + the desired number of points are present, then a sliding window will NOT be used, and instead the points will be + plotted as they were originally recorded (also the subchannel will NOT be plotted as a bar based plot even if + `plot_as_bars` was set to true). + :param plot_as_bars: A boolean value indicating if the data should be visualized as a set of rectangles, where a + shaded rectangle is used to represent the maximum and minimum values of the data during the time window + covered by the rectangle. These maximum and minimum values are visualized by the locations of the top and bottom + edges of the rectangle respectively, unless the height of the rectangle would be 0, in which case a line segment + will be displayed in it's place. If this parameter is `False`, two lines will be plotted for each + of the sub-channels in the figure being created, creating an 'envelope' around the data. An 'envelope' around the + data consists of a line plotted for the maximum values contained in each of the time windows, and another line + plotted for the minimum values. Together these lines create a boundary which contains all the data points + recorded in the originally recorded data. + :param plot_title: The title for the plot + :param opacity: The opacity to use for plotting bars/lines + :param colors_to_use: An 'array-like' object of strings containing colors to be cycled through for the sub-channels. + If None is given (which is the default), then the `colorway` variable in Plotly's current theme/template will + be used to color the data on each of the sub-channels uniquely, repeating from the start of the `colorway` if + all colors have been used. + :return: The Plotly Figure with the data plotted + + TO-DO: + - Ensure that this works with dataframes of subchannels that were merged together and don't necessarily + share the same time stamps (and thus have a ton of NaN values) + """ + + return rolling_enveloped_dashboard( + {plot_title: df}, + desired_num_points=desired_num_points, + subplot_colors=colors_to_use, + plot_as_bars=plot_as_bars, + plot_full_single_channel=True, + opacity=opacity + ) + From e999d7cb3ce9da85af74fcb3e3c02095a465874b Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Fri, 29 Oct 2021 14:57:18 -0400 Subject: [PATCH 03/24] Added Function To Plot Data Around Peak --- endaq/plot/plots.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index 5e28453..5d5f886 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -466,3 +466,41 @@ def rolling_min_max_envelope(df: pd.DataFrame, desired_num_points: int = 250, pl opacity=opacity ) + +def around_peak(df: pd.DataFrame, num: int = 1000, leading_ratio: float = 0.5): + """ + A function to plot the data surrounding the largest peak (or valley) in the given data. + The 'peak' is defined by the point in the absolute value of the given data with the largest value. + + :param df: A dataframe indexed by time stamps + :param num: The number of points to plot + :param leading_ratio: The ratio of the data to be viewed that will come before the peak + :return: A Plotly figure containing the plot + """ + if not isinstance(df, pd.DataFrame): + raise TypeError(f"The `df` parmeter must be of type `pd.DataFrame` but was given type {type(df)} instead.") + + if not isinstance(num, int): + raise TypeError(f"The `num` parameter must be an `int` type, but was given {type(num)}.") + + if not isinstance(leading_ratio, float): + raise TypeError(f"The `leading_ratio` parameter must be a `float` type, but was given {type(leading_ratio)}.") + + if len(df) == 0: + raise ValueError(f"The parameter `df` must have nonzero length, but has shape {df.shape} instead") + + if num < 3: + raise ValueError(f"The `num` parameter must be at least 3, but {num} was given.") + + if leading_ratio < 0 or leading_ratio > 1: + raise ValueError("The `leading_ratio` parameter must be a float value in the " + f"range [0,1], but was given {leading_ratio} instead.") + + max_i = df.abs().max(axis=1).reset_index(drop=True).idxmax() + + # These can go below and above the number of valid indices, but that can be ignored since + # they'll only be used to slice the data in a way that is okay to go over/under + window_start = max_i - int(num * leading_ratio) + window_end = max_i + int(num * (1-leading_ratio)) + + return px.line(df.iloc[window_start: window_end]) From 4dd3f3ae392be2844e5b07980e7d2eee407fcad5 Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Mon, 1 Nov 2021 12:01:31 -0400 Subject: [PATCH 04/24] Handle NaNs Created From Subchannel Concatenation - Added the ability to handle excessive NaN values in subchannel data created from concatenating subchannels not sampled at the same times --- endaq/plot/dashboards.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/endaq/plot/dashboards.py b/endaq/plot/dashboards.py index 404d7eb..60ed243 100644 --- a/endaq/plot/dashboards.py +++ b/endaq/plot/dashboards.py @@ -1,6 +1,7 @@ import collections import numpy as np +import pandas as pd import plotly.graph_objects as go from plotly.subplots import make_subplots import plotly.io as pio @@ -138,15 +139,34 @@ def rolling_enveloped_dashboard( min_max_tuple = (rolling_n.min()[::stride], rolling_n.max()[::stride]) min_max_equal = min_max_tuple[0] == min_max_tuple[1] + is_nan_min_max_mask = np.logical_not( + np.logical_and( + pd.isnull(min_max_tuple[0]), + pd.isnull(min_max_tuple[1]))) + # Loop through each of the sub-channels, and their respective '0-height rectangle mask' for subchannel_name, cur_min_max_equal in min_max_equal[channel_data.columns].iteritems(): traces = [] cur_color = colorway[subplot_num % len(colorway)] + cur_subchannel_non_nan_mask = is_nan_min_max_mask[subchannel_name] + # If there are less data points than the desired number of points + # to be plotted, just plot the data as a line plot + if len(channel_data) < desired_num_points: + not_nan_mask = np.logical_not(pd.isnull(channel_data[subchannel_name])) + traces.append( + go.Scatter( + x=channel_data.index[not_nan_mask], + y=channel_data.loc[not_nan_mask, subchannel_name], + name=subchannel_name, + opacity=opacity, + line_color=cur_color, + showlegend=False, + ) + ) # If it's going to plot the data with bars - # if plot_as_bars and len(min_max_tuple[0]) > desired_num_points: - if plot_as_bars and len(channel_data) > desired_num_points: + elif plot_as_bars: # If there are any 0-height rectangles if np.any(cur_min_max_equal): equal_data_df = min_max_tuple[0].loc[cur_min_max_equal.values, subchannel_name] @@ -192,12 +212,13 @@ def rolling_enveloped_dashboard( traces.append( go.Bar( - x=min_max_tuple[0].index, - y=min_max_tuple[1][subchannel_name] - min_max_tuple[0][subchannel_name], + x=min_max_tuple[0].index[cur_subchannel_non_nan_mask], + y=(min_max_tuple[1].loc[cur_subchannel_non_nan_mask, subchannel_name] - + min_max_tuple[0].loc[cur_subchannel_non_nan_mask, subchannel_name]), marker_color=cur_color, opacity=opacity, marker_line_width=0, - base=min_max_tuple[0][subchannel_name], + base=min_max_tuple[0].loc[cur_subchannel_non_nan_mask, subchannel_name], showlegend=False, name=subchannel_name, ) @@ -218,8 +239,8 @@ def rolling_enveloped_dashboard( for cur_df in min_max_tuple: traces.append( go.Scatter( - x=cur_df.index, - y=cur_df[subchannel_name], + x=cur_df.index[cur_subchannel_non_nan_mask], + y=cur_df.loc[cur_subchannel_non_nan_mask, subchannel_name], name=subchannel_name, opacity=opacity, line_color=cur_color, From c14aab04dfb1b8442a1f09eabb1b70ac1fd27884 Mon Sep 17 00:00:00 2001 From: Becker Awqatty Date: Mon, 1 Nov 2021 15:10:58 -0400 Subject: [PATCH 05/24] added future-annotations import to fix annotation error --- endaq/plot/dashboards.py | 2 ++ endaq/plot/plots.py | 2 ++ endaq/plot/utilities.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/endaq/plot/dashboards.py b/endaq/plot/dashboards.py index 60ed243..70016d6 100644 --- a/endaq/plot/dashboards.py +++ b/endaq/plot/dashboards.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections import numpy as np diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index 5d5f886..a4e8609 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np import pandas as pd import plotly.graph_objects as go diff --git a/endaq/plot/utilities.py b/endaq/plot/utilities.py index b870db6..30f77b4 100644 --- a/endaq/plot/utilities.py +++ b/endaq/plot/utilities.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import plotly.io as pio import numpy as np From 7b634ea3422061a679698e267d3d5d3cec90c125 Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Tue, 2 Nov 2021 14:33:20 -0400 Subject: [PATCH 06/24] Added Rolling Metric Dashboard Function - Added a function to create dashboards of subplots which have rolling metrics plotted --- endaq/plot/dashboards.py | 121 ++++++++++++++++++++++++++++++++++++++- endaq/plot/plots.py | 62 -------------------- 2 files changed, 118 insertions(+), 65 deletions(-) diff --git a/endaq/plot/dashboards.py b/endaq/plot/dashboards.py index 60ed243..8e8c474 100644 --- a/endaq/plot/dashboards.py +++ b/endaq/plot/dashboards.py @@ -15,8 +15,8 @@ def rolling_enveloped_dashboard( channel_df_dict: dict, desired_num_points: int = 250, num_rows: Optional[int] = None, num_cols: Optional[int] = 3, width_for_subplot_row: int = 400, height_for_subplot_row: int = 400, - subplot_colors: Optional[collections.Container] = None, min_points_to_plot: int = 1, - plot_as_bars: bool = False, plot_full_single_channel: bool = False, opacity: float = 1, y_axis_bar_plot_padding: float = 0.06 + subplot_colors: Optional[collections.Container] = None, min_points_to_plot: int = 1, plot_as_bars: bool = False, + plot_full_single_channel: bool = False, opacity: float = 1, y_axis_bar_plot_padding: float = 0.06 ) -> go.Figure: """ A function to create a Plotly Figure with sub-plots for each of the available data sub-channels, designed to reduce @@ -268,6 +268,116 @@ def rolling_enveloped_dashboard( return fig +def rolling_metric_dashboard(channel_df_dict: dict, desired_num_points: int = 250, num_rows: Optional[int] = None, + num_cols: Optional[int] = 3, rolling_metrics_to_plot: tuple = ('mean', 'std'), + metric_colors: Optional[collections.Container] = None, width_for_subplot_row: int = 400, + height_for_subplot_row: int = 400) -> go.Figure: + """ + A function to create a dashboard of subplots of the given data, plotting a set of rolling metrics. + + :param channel_df_dict: A dictionary mapping channel names to Pandas DataFrames of that channels data + :param desired_num_points: The desired number of points to be plotted in each subplot. The number of points + will be reduced from it's original sampling rate by applying metrics (e.g. min, max) over sliding windows + and then using that information to represent/visualize the data contained within the original data. If less than + the desired number of points are present, then a sliding window will NOT be used, and instead the points will be + plotted as they were originally recorded (also the subplot will NOT be plotted as a bar based plot even if + `plot_as_bars` was set to true). + :param num_rows: The number of columns of subplots to be created inside the Plotly figure. If None is given, (then + `num_cols` must not be None), then this number will automatically be determined by what's needed. If more rows + are specified than are needed, the number of rows will be reduced to the minimum needed to contain all the subplots + :param num_cols:The number of columns of subplots to be created inside the Plotly figure. See the description of + the `num_rows` parameter for more details on this parameter, and how the two interact. This also follows the same + approach to handling None when given + :param rolling_metrics_to_plot: A tuple of strings which indicate what rolling metrics to plot for each subchannel. + The options are ['mean', 'std', 'absolute max'] which correspond to the mean, standard deviation, and maximum + of the absolute value. + :param metric_colors: An 'array-like' object of strings containing colors to be cycled through for the metrics. + If None is given (which is the default), then the `colorway` variable in Plotly's current theme/template will + be used to color the metric data, repeating from the start of the `colorway` if all colors have been used. + The first value corresponds to the color if not enough points of data exist for a rolling metric, + and the others correspond to the metric in `rolling_metrics_to_plot` in the same order they are given + :param width_for_subplot_row: The width of the area used for a single subplot (in pixels). + :param height_for_subplot_row: The height of the area used for a single subplot (in pixels). + :return: The Plotly Figure containing the subplots of sensor data (the 'dashboard') + """ + if len(rolling_metrics_to_plot) == 0: + raise ValueError("At least one rolling metric must be specified in `rolling_metrics_to_plot`!") + + subplot_titles = [' '.join((k, col)) for (k, v) in channel_df_dict.items() for col in v.columns] + + if num_rows is None and num_cols is None: + raise TypeError("Both `num_rows` and `num_columns` were given as `None`! " + "A maximum of one of these two parameters may be given as None.") + elif num_rows is None: + num_rows = 1 + (len(subplot_titles) - 1) // num_cols + elif num_cols is None: + num_cols = 1 + (len(subplot_titles) - 1) // num_rows + elif len(subplot_titles) > num_rows * num_cols: + raise ValueError("The values given for `num_rows` and `num_columns` result in a maximum " + f"of {num_rows * num_cols} avaialable sub-plots, but {len(subplot_titles)} subplots need " + "to be plotted! Try setting one of these variables to `None`, it will then " + "automatically be set to the optimal number of rows/columns.") + else: + num_rows = 1 + (len(subplot_titles) - 1) // num_cols + num_cols = int(np.ceil(len(subplot_titles)/num_rows)) + + if metric_colors is None: + colorway = pio.templates[pio.templates.default]['layout']['colorway'] + else: + colorway = metric_colors + fig = make_subplots(rows=num_rows, cols=num_cols, subplot_titles=subplot_titles) + subplot_num = 0 + for channel_name, channel_data in channel_df_dict.items(): + n = int(channel_data.shape[0] / desired_num_points) + + if n > 0: + time = channel_data.rolling(n).mean().iloc[::n].index + else: + time = channel_data.index + + for c_i, (subchannel_name, subchannel_data) in enumerate(channel_data.iteritems()): + for metric in rolling_metrics_to_plot: + if n == 0: + data = subchannel_data.values + name = subchannel_name + color = colorway[0] + elif metric == 'mean': + data = subchannel_data.rolling(n).mean().iloc[::n] + name = 'Mean' + color = colorway[1] + elif metric == 'std': + data = subchannel_data.rolling(n).std().iloc[::n] + name = 'STD Dev' + color = colorway[2] + elif metric == 'absolute max': + data = subchannel_data.abs().rolling(n).max().iloc[::n] + name = 'Max' + color = colorway[3] + else: + raise ValueError(f"metric given to `rolling_metrics_to_plot` is not valid! Was given {metric}" + " which is not in the allowed options of ['mean', 'std', 'absolute max']") + fig.add_trace( + go.Scatter( + x=time, + y=data, + name=name, + line=dict(color=color)), + row=1 + subplot_num // num_cols, + col=1 + subplot_num % num_cols, + ) + + if n == 0: + break + + subplot_num += 1 + + return fig.update_layout( + width=num_cols * width_for_subplot_row, + height=num_rows * height_for_subplot_row, + showlegend=False, + ) + + @@ -365,4 +475,9 @@ def rolling_enveloped_dashboard( num_cols=4, ).show() - print(str(j) + " done!") \ No newline at end of file + rolling_metric_dashboard( + CHANNEL_DFS, + rolling_metrics_to_plot=('mean', 'absolute max', 'std'), + ).show() + + print(str(j) + " done!") diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index 5d5f886..61453d2 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -37,68 +37,6 @@ def sample_spacing( return dt -def get_channel_ids(doc): - return [list(d)[0] for d in list(doc.channels.items())] - - -def plot_row(doc, plot_len, mean_thresh, dfs, df_ide, line_color="#EE7F27", std_color="#6914F0"): - col = 1 - cols = df_ide.shape[0] - fig = make_subplots(rows=1, cols=cols, subplot_titles=list(df_ide["Name"])) - - for i in get_channel_ids(doc): - df = dfs[df_ide[df_ide["CH ID"] == i]["CH #"].iloc[0]] - fs = df_ide[df_ide["CH ID"] == i]["Frequency (Hz)"].iloc[0] - - n = int(df.shape[0] / plot_len) - time = df.reset_index()["Time (s)"] - if n > 0: - time = time.rolling(n).mean().iloc[::n] - - for c, c_i in enumerate(df.columns): - if n == 0: - fig.add_trace( - go.Scatter(x=time, y=df[c], name=str(i + c_i / 10), line=dict(color=line_color)), - row=1, - col=col, - ) - elif fs < mean_thresh: - fig.add_trace( - go.Scatter( - x=time, - y=df[c].rolling(n).mean().iloc[::n], - name="Mean", - line=dict(color=line_color), - ), - row=1, - col=col, - ) - else: - fig.add_trace( - go.Scatter( - x=time, - y=df[c].abs().rolling(n).max().iloc[::n], - name="Max", - line=dict(color=line_color), - ), - row=1, - col=col, - ) - fig.add_trace( - go.Scatter( - x=time, - y=df[c].rolling(n).std().iloc[::n], - name="Std Dev", - line=dict(color=std_color), - ), - row=1, - col=col, - ) - col += 1 - - return fig.update_layout(width=cols * 400, showlegend=False) - - def multi_file_plot_attributes(multi_file_db, rows_to_plot=DEFAULT_ATTRIBUTES_TO_PLOT_INDIVIDUALLY, recording_colors=None, width_per_subplot=400): """ From 0cb7c473f849ea10905ee1f57e0be828aac64616 Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Tue, 2 Nov 2021 15:51:49 -0400 Subject: [PATCH 07/24] Improved Import Structure --- endaq/plot/dashboards.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/endaq/plot/dashboards.py b/endaq/plot/dashboards.py index e2d87e3..895218e 100644 --- a/endaq/plot/dashboards.py +++ b/endaq/plot/dashboards.py @@ -1,18 +1,13 @@ from __future__ import annotations import collections - import numpy as np import pandas as pd import plotly.graph_objects as go from plotly.subplots import make_subplots import plotly.io as pio - from typing import Optional -import endaq.ide -import endaq.plot - def rolling_enveloped_dashboard( channel_df_dict: dict, desired_num_points: int = 250, num_rows: Optional[int] = None, @@ -385,6 +380,8 @@ def rolling_metric_dashboard(channel_df_dict: dict, desired_num_points: int = 25 if __name__ == '__main__': + import endaq.ide + file_urls = ['https://info.endaq.com/hubfs/data/surgical-instrument.ide', 'https://info.endaq.com/hubfs/data/97c3990f-Drive-Home_70-1616632444.ide', 'https://info.endaq.com/hubfs/data/High-Drop.ide', @@ -407,8 +404,6 @@ def rolling_metric_dashboard(channel_df_dict: dict, desired_num_points: int = 25 'https://info.endaq.com/hubfs/data/Mining-Data.ide', 'https://info.endaq.com/hubfs/data/Mide-Airport-Drive-Lexus-Hybrid-Dash-W8.ide'] - endaq.plot.utilities.set_theme() - for j in [4]: doc = endaq.ide.get_doc(file_urls[j]) table = endaq.ide.get_channel_table(doc) From aa760f5dcb896bae792c22d8f1e07b30584818e7 Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Tue, 2 Nov 2021 15:55:11 -0400 Subject: [PATCH 08/24] Removed A Completed To-Do --- endaq/plot/plots.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index 35580bd..b8e751c 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -391,10 +391,6 @@ def rolling_min_max_envelope(df: pd.DataFrame, desired_num_points: int = 250, pl be used to color the data on each of the sub-channels uniquely, repeating from the start of the `colorway` if all colors have been used. :return: The Plotly Figure with the data plotted - - TO-DO: - - Ensure that this works with dataframes of subchannels that were merged together and don't necessarily - share the same time stamps (and thus have a ton of NaN values) """ return rolling_enveloped_dashboard( From 92d1417f6523a12dfe9fac79e938f218585e1e6c Mon Sep 17 00:00:00 2001 From: Becker Awqatty Date: Fri, 29 Oct 2021 17:10:02 -0400 Subject: [PATCH 09/24] added `__all__` member to exclude unintended code from imports & docs --- endaq/plot/plots.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index b8e751c..e489737 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -15,6 +15,15 @@ from .utilities import determine_plotly_map_zoom, get_center_of_coordinates from .dashboards import rolling_enveloped_dashboard +__all__ = [ + 'multi_file_plot_attributes', + 'general_get_correlation_figure', + 'get_pure_numpy_2d_pca', + 'gen_map', + 'octave_spectrogram', + 'octave_psd_bar_plot', +] + DEFAULT_ATTRIBUTES_TO_PLOT_INDIVIDUALLY = np.array([ 'accelerationPeakFull', 'accelerationRMSFull', 'velocityRMSFull', 'psuedoVelocityPeakFull', 'displacementRMSFull', 'gpsSpeedFull', 'gyroscopeRMSFull', 'microphonoeRMSFull', From bd5d73517f18b531ebc4e086542af515760ed74c Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Tue, 2 Nov 2021 17:28:15 -0400 Subject: [PATCH 10/24] Forced Uniform Spacing For Bars - Now forcing uniform spacing of time steps when plotting data with bars (so that no-discontinuities appear in the X-axis) --- endaq/plot/dashboards.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/endaq/plot/dashboards.py b/endaq/plot/dashboards.py index 895218e..50faa15 100644 --- a/endaq/plot/dashboards.py +++ b/endaq/plot/dashboards.py @@ -141,6 +141,17 @@ def rolling_enveloped_dashboard( pd.isnull(min_max_tuple[0]), pd.isnull(min_max_tuple[1]))) + # If it's going to be plotted as bars, force it's time stamps to be uniformly spaced + # so that the bars don't have any discontinuities in the X-axis + if len(channel_data) >= desired_num_points and plot_as_bars: + channel_data.set_index( + pd.interval_range( + channel_data.index.values[0], + channel_data.index.values[-1], + periods=len(channel_data), + ) + ) + # Loop through each of the sub-channels, and their respective '0-height rectangle mask' for subchannel_name, cur_min_max_equal in min_max_equal[channel_data.columns].iteritems(): From 5302b6fab6d91e9d958db7a5f5a5acfb1fabbb6f Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Wed, 3 Nov 2021 10:03:43 -0400 Subject: [PATCH 11/24] Minor Fix --- endaq/plot/dashboards.py | 13 ++++++++++--- endaq/plot/plots.py | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/endaq/plot/dashboards.py b/endaq/plot/dashboards.py index 50faa15..d8bd5cf 100644 --- a/endaq/plot/dashboards.py +++ b/endaq/plot/dashboards.py @@ -144,13 +144,20 @@ def rolling_enveloped_dashboard( # If it's going to be plotted as bars, force it's time stamps to be uniformly spaced # so that the bars don't have any discontinuities in the X-axis if len(channel_data) >= desired_num_points and plot_as_bars: - channel_data.set_index( - pd.interval_range( + if isinstance(channel_data.index, pd.core.indexes.datetimes.DatetimeIndex): + new_index = pd.date_range( channel_data.index.values[0], channel_data.index.values[-1], periods=len(channel_data), ) - ) + else: + new_index = np.linspace( + channel_data.index.values[0], + channel_data.index.values[-1], + num=len(channel_data), + ) + + channel_data.set_index(new_index) # Loop through each of the sub-channels, and their respective '0-height rectangle mask' for subchannel_name, cur_min_max_equal in min_max_equal[channel_data.columns].iteritems(): diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index b8e751c..e098981 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -275,7 +275,7 @@ def octave_spectrogram(df, window, bins_per_octave=3, freq_start=20, max_freq=fl - the spectrogram data - the corresponding plotly figure """ - if len(df) != 1: + if len(df.columns) < 1: raise ValueError("The parameter 'df' must have only one column of data!") ary = df.values.squeeze() From 272244bef84194dcc40b47ea730ca0dd8e581485 Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Wed, 3 Nov 2021 10:20:03 -0400 Subject: [PATCH 12/24] Removed Function Now Added To Calc - Removed the function sample_spacing from endaq.plot.plots now that it's endaq.calc - Made single plot rolling envelope have a legend --- endaq/plot/dashboards.py | 10 ++++++---- endaq/plot/plots.py | 34 ++++++++++++---------------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/endaq/plot/dashboards.py b/endaq/plot/dashboards.py index d8bd5cf..38fe7e1 100644 --- a/endaq/plot/dashboards.py +++ b/endaq/plot/dashboards.py @@ -177,7 +177,7 @@ def rolling_enveloped_dashboard( name=subchannel_name, opacity=opacity, line_color=cur_color, - showlegend=False, + showlegend=plot_full_single_channel, ) ) # If it's going to plot the data with bars @@ -216,7 +216,7 @@ def rolling_enveloped_dashboard( opacity=opacity, mode='lines', line_color=cur_color, - showlegend=False, + showlegend=plot_full_single_channel, ) ) @@ -234,7 +234,7 @@ def rolling_enveloped_dashboard( opacity=opacity, marker_line_width=0, base=min_max_tuple[0].loc[cur_subchannel_non_nan_mask, subchannel_name], - showlegend=False, + showlegend=plot_full_single_channel, name=subchannel_name, ) ) @@ -259,7 +259,7 @@ def rolling_enveloped_dashboard( name=subchannel_name, opacity=opacity, line_color=cur_color, - showlegend=False, + showlegend=plot_full_single_channel, ) ) @@ -399,6 +399,8 @@ def rolling_metric_dashboard(channel_df_dict: dict, desired_num_points: int = 25 if __name__ == '__main__': import endaq.ide + from utilities import set_theme + set_theme() file_urls = ['https://info.endaq.com/hubfs/data/surgical-instrument.ide', 'https://info.endaq.com/hubfs/data/97c3990f-Drive-Home_70-1616632444.ide', diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index e098981..a57be6a 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -6,38 +6,20 @@ import plotly.express as px from plotly.subplots import make_subplots from scipy import signal -import typing from typing import Optional import collections -from endaq.calc.psd import to_octave, welch # ,sample_spacing THIS ISN'T YET IN MASTER +from endaq.calc import sample_spacing +from endaq.calc.psd import to_octave, welch -from .utilities import determine_plotly_map_zoom, get_center_of_coordinates -from .dashboards import rolling_enveloped_dashboard +from utilities import determine_plotly_map_zoom, get_center_of_coordinates +from dashboards import rolling_enveloped_dashboard DEFAULT_ATTRIBUTES_TO_PLOT_INDIVIDUALLY = np.array([ 'accelerationPeakFull', 'accelerationRMSFull', 'velocityRMSFull', 'psuedoVelocityPeakFull', 'displacementRMSFull', 'gpsSpeedFull', 'gyroscopeRMSFull', 'microphonoeRMSFull', 'temperatureMeanFull', 'pressureMeanFull']) -def sample_spacing( - df: pd.DataFrame, convert: typing.Literal[None, "to_seconds"] = "to_seconds" -): - """ - REMOVE THIS FUNCTION WHEN IT GETS MERGED INTO ENDAQ.CALC - - Calculate the average spacing between individual samples. - For time indices, this calculates the sampling period `dt`. - :param df: the input data - :param convert: if `"to_seconds"` (default), convert any time objects into - floating-point seconds - """ - dt = (df.index[-1] - df.index[0]) / (len(df.index) - 1) - if convert == "to_seconds" and isinstance(dt, (np.timedelta64, pd.Timedelta)): - dt = dt / np.timedelta64(1, "s") - - return dt - def multi_file_plot_attributes(multi_file_db, rows_to_plot=DEFAULT_ATTRIBUTES_TO_PLOT_INDIVIDUALLY, recording_colors=None, width_per_subplot=400): @@ -440,3 +422,11 @@ def around_peak(df: pd.DataFrame, num: int = 1000, leading_ratio: float = 0.5): window_end = max_i + int(num * (1-leading_ratio)) return px.line(df.iloc[window_start: window_end]) + + +if __name__ == "__main__": + df_vibe = pd.read_csv('https://info.endaq.com/hubfs/data/motorcycle-vibration-moving-frequency.csv',index_col=0) + df_vibe = df_vibe - df_vibe.median() + + freqs, bins, Pxx, fig = octave_spectrogram(df_vibe[['Z (40g)']], window=0.5, bins_per_octave=6) + fig.show() \ No newline at end of file From db6490533b20ea6a821a415a98f782440baafc9c Mon Sep 17 00:00:00 2001 From: Sam Ragusa Date: Wed, 3 Nov 2021 10:24:26 -0400 Subject: [PATCH 13/24] Added Missing Functions to __all__ in plots.py --- endaq/plot/plots.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index e489737..18806c4 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -22,6 +22,8 @@ 'gen_map', 'octave_spectrogram', 'octave_psd_bar_plot', + 'rolling_min_max_envelope', + 'around_peak', ] DEFAULT_ATTRIBUTES_TO_PLOT_INDIVIDUALLY = np.array([ From 57f18383a725a0ff49921cd9310fbc35c0609e28 Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Wed, 3 Nov 2021 10:29:28 -0400 Subject: [PATCH 14/24] Docs Fix - Fixed some formatting causing issues in doc generation --- endaq/plot/dashboards.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/endaq/plot/dashboards.py b/endaq/plot/dashboards.py index 38fe7e1..1541965 100644 --- a/endaq/plot/dashboards.py +++ b/endaq/plot/dashboards.py @@ -31,7 +31,7 @@ def rolling_enveloped_dashboard( plotted as they were originally recorded (also the subplot will NOT be plotted as a bar based plot even if `plot_as_bars` was set to true). :param num_rows: The number of columns of subplots to be created inside the Plotly figure. If None is given, (then - `num_cols` must not be None), then this number will automatically be determined by what's needed. If more rows + `num_cols` must not be None), then this number will automatically be determined by what's needed. If more rows are specified than are needed, the number of rows will be reduced to the minimum needed to contain all the subplots :param num_cols: The number of columns of subplots to be created inside the Plotly figure. See the description of the `num_rows` parameter for more details on this parameter, and how the two interact. This also follows the same @@ -298,9 +298,9 @@ def rolling_metric_dashboard(channel_df_dict: dict, desired_num_points: int = 25 plotted as they were originally recorded (also the subplot will NOT be plotted as a bar based plot even if `plot_as_bars` was set to true). :param num_rows: The number of columns of subplots to be created inside the Plotly figure. If None is given, (then - `num_cols` must not be None), then this number will automatically be determined by what's needed. If more rows + `num_cols` must not be None), then this number will automatically be determined by what's needed. If more rows are specified than are needed, the number of rows will be reduced to the minimum needed to contain all the subplots - :param num_cols:The number of columns of subplots to be created inside the Plotly figure. See the description of + :param num_cols: The number of columns of subplots to be created inside the Plotly figure. See the description of the `num_rows` parameter for more details on this parameter, and how the two interact. This also follows the same approach to handling None when given :param rolling_metrics_to_plot: A tuple of strings which indicate what rolling metrics to plot for each subchannel. From 481e6cf443dc391f4133804be4e01342b75911bd Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Wed, 3 Nov 2021 10:33:07 -0400 Subject: [PATCH 15/24] Updated Requirement Version - Changed requirement version of endaq.calc from >= 1.0.1b1 to 1.1.0 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 605ee67..8f6e207 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ numpy>=1.19.5 pandas>=1.1.5 plotly>=5.3.1 scipy>=1.7.1 -endaq-calc>=1.0.0b1 \ No newline at end of file +endaq-calc>=1.1.0 \ No newline at end of file diff --git a/setup.py b/setup.py index cc536df..1db06b4 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ "pandas>=1.1.5", "plotly>=5.3.1", "scipy>=1.7.1", - "endaq-calc>=1.0.0b1", + "endaq-calc>=1.1.0", ] TEST_REQUIRES = [ From aa3b33746f8d5c88fb28655e656e514ead82eb9a Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Wed, 3 Nov 2021 10:51:47 -0400 Subject: [PATCH 16/24] Switched Back To Relative Imports --- endaq/plot/plots.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index f163e56..1d4fb68 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -12,8 +12,8 @@ from endaq.calc import sample_spacing from endaq.calc.psd import to_octave, welch -from utilities import determine_plotly_map_zoom, get_center_of_coordinates -from dashboards import rolling_enveloped_dashboard +from .utilities import determine_plotly_map_zoom, get_center_of_coordinates +from .dashboards import rolling_enveloped_dashboard __all__ = [ 'multi_file_plot_attributes', @@ -433,11 +433,3 @@ def around_peak(df: pd.DataFrame, num: int = 1000, leading_ratio: float = 0.5): window_end = max_i + int(num * (1-leading_ratio)) return px.line(df.iloc[window_start: window_end]) - - -if __name__ == "__main__": - df_vibe = pd.read_csv('https://info.endaq.com/hubfs/data/motorcycle-vibration-moving-frequency.csv',index_col=0) - df_vibe = df_vibe - df_vibe.median() - - freqs, bins, Pxx, fig = octave_spectrogram(df_vibe[['Z (40g)']], window=0.5, bins_per_octave=6) - fig.show() \ No newline at end of file From 2e0f5a017a25faad53e9d1caafbfd9454841d788 Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Wed, 3 Nov 2021 12:19:55 -0400 Subject: [PATCH 17/24] Minor Bug Fix --- endaq/plot/plots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index 1d4fb68..5fb77e8 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -268,7 +268,7 @@ def octave_spectrogram(df, window, bins_per_octave=3, freq_start=20, max_freq=fl - the spectrogram data - the corresponding plotly figure """ - if len(df.columns) < 1: + if len(df.columns) != 1: raise ValueError("The parameter 'df' must have only one column of data!") ary = df.values.squeeze() From 2a6c064ebb7354dbbcbc167c1d342ab75ab8ab3f Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Wed, 3 Nov 2021 12:29:15 -0400 Subject: [PATCH 18/24] Added Additional Type Hinting - Added type hinting to functions where it didn't previously exist --- endaq/plot/plots.py | 27 +++++++++++++++++---------- endaq/plot/utilities.py | 17 ++++++++++------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index b8e751c..4a6fac9 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -39,8 +39,10 @@ def sample_spacing( return dt -def multi_file_plot_attributes(multi_file_db, rows_to_plot=DEFAULT_ATTRIBUTES_TO_PLOT_INDIVIDUALLY, recording_colors=None, - width_per_subplot=400): +def multi_file_plot_attributes(multi_file_db: pd.DataFrame, + rows_to_plot: np.ndarray = DEFAULT_ATTRIBUTES_TO_PLOT_INDIVIDUALLY, + recording_colors: Optional[collections.Container] = None, + width_per_subplot: int = 400) -> go.Figure: """ Creates a Plotly figure plotting all the desired attributes from the given DataFrame. @@ -84,8 +86,10 @@ def multi_file_plot_attributes(multi_file_db, rows_to_plot=DEFAULT_ATTRIBUTES_TO return fig.update_layout(width=len(rows_to_plot)*width_per_subplot, showlegend=False) -def general_get_correlation_figure(merged_df, recording_colors=None, hover_names=None, - characteristics_to_show_on_hover=[], starting_cols=None): +def general_get_correlation_figure(merged_df: pd.DataFrame, recording_colors: Optional[collections.Container] = None, + hover_names: Optional[collections.Container] = None, + characteristics_to_show_on_hover: list = [], + starting_cols: collections.Container = None) -> go.Figure: """ A function to create a plot with two drop-down menus, each populated with a set of options corresponding to the scalar quantities contained in the given dataframe. The data points will then be plotted with the X and Y axis @@ -175,7 +179,7 @@ def general_get_correlation_figure(merged_df, recording_colors=None, hover_names return fig -def get_pure_numpy_2d_pca(df, recording_colors=None): +def get_pure_numpy_2d_pca(df: pd.DataFrame, recording_colors: Optional[collections.Container] = None) -> go.Figure: """ Get a Plotly figure of the 2d PCA for the given DataFrame. This will have dropdown menus to select which components are being used for the X and Y axis. @@ -224,7 +228,8 @@ def get_pure_numpy_2d_pca(df, recording_colors=None): return fig -def gen_map(df_map, mapbox_access_token, filter_points_by_positive_groud_speed=True, color_by_column="GNSS Speed: Ground Speed"): +def gen_map(df_map: pd.DataFrame, mapbox_access_token: str, filter_points_by_positive_groud_speed: bool = True, + color_by_column: str = "GNSS Speed: Ground Speed") -> go.Figure: """ Plots GPS data on a map from a single recording, shading the points based some characteristic (defaults to ground speed). @@ -258,7 +263,9 @@ def gen_map(df_map, mapbox_access_token, filter_points_by_positive_groud_speed=T return fig -def octave_spectrogram(df, window, bins_per_octave=3, freq_start=20, max_freq=float('inf'), db_scale=True, log_scale_y_axis=True): +def octave_spectrogram(df: pd.DataFrame, window: float, bins_per_octave: int = 3, freq_start: float = 20.0, + max_freq: float = float('inf'), db_scale: bool = True, log_scale_y_axis: bool = True + ) -> go.Figure: """ Produces an octave spectrogram of the given data. @@ -318,7 +325,8 @@ def octave_spectrogram(df, window, bins_per_octave=3, freq_start=20, max_freq=fl return freqs, bins, Pxx, fig -def octave_psd_bar_plot(df, bins_per_octave=3, f_start=20, yaxis_title='', log_scale_y_axis=True): +def octave_psd_bar_plot(df: pd.DataFrame, bins_per_octave: int = 3, f_start: float = 20.0, yaxis_title: str = '', + log_scale_y_axis: bool = True) -> go.Figure: """ Produces a bar plot of an octave psd. @@ -359,8 +367,7 @@ def octave_psd_bar_plot(df, bins_per_octave=3, f_start=20, yaxis_title='', log_s def rolling_min_max_envelope(df: pd.DataFrame, desired_num_points: int = 250, plot_as_bars: bool = False, plot_title: str = "", opacity: float = 1, - colors_to_use: Optional[collections.Container] = None - ) -> go.Figure: + colors_to_use: Optional[collections.Container] = None) -> go.Figure: """ A function to create a Plotly Figure to plot the data for each of the available data sub-channels, designed to reduce the number of points/data being plotted without minimizing the insight available from the plots. It will diff --git a/endaq/plot/utilities.py b/endaq/plot/utilities.py index 30f77b4..c11935a 100644 --- a/endaq/plot/utilities.py +++ b/endaq/plot/utilities.py @@ -1,14 +1,16 @@ from __future__ import annotations import plotly.io as pio +import plotly.graph_objects as go import numpy as np +from typing import Union -def define_theme(template_name="endaq_cloud", - default_plotly_template='plotly_dark', - text_color='#DAD9D8', font_family="Open Sans", title_font_family="Open Sans SemiBold", - graph_line_color='#DAD9D8', grid_line_color="#404041", background_color='#262626', - plot_background_color='#0F0F0F'): +def define_theme(template_name: str = "endaq_cloud", default_plotly_template: str = 'plotly_dark', + text_color: str = '#DAD9D8', font_family: str = "Open Sans", + title_font_family: str = "Open Sans SemiBold", graph_line_color: str = '#DAD9D8', + grid_line_color: str = "#404041", background_color: str = '#262626', + plot_background_color: str = '#0F0F0F') -> go.layout._template.Template: """ Define a Plotly theme (template), allowing completely custom aesthetics @@ -96,7 +98,7 @@ def define_theme(template_name="endaq_cloud", return pio.templates[template_name] -def set_theme(theme='endaq'): +def set_theme(theme: str = 'endaq') -> go.layout._template.Template: """ Sets the plot appearances based on a known 'theme'. @@ -131,7 +133,8 @@ def set_theme(theme='endaq'): return pio.templates[theme] -def get_center_of_coordinates(lats, lons, as_list=False, as_degrees=True): +def get_center_of_coordinates(lats: np.ndarray, lons: np.ndarray, as_list: bool = False, as_degrees: bool = True + ) -> Union[list, dict]: """ Inputs and outputs are measured in degrees. From c92972271a6a6cdeb4552e14ea674af120349fec Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Wed, 3 Nov 2021 12:31:48 -0400 Subject: [PATCH 19/24] Changed Version Number In `setup.py` --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cc536df..1c34590 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setuptools.setup( name='endaq-plot', - version='1.0.0', + version='1.1.0', author='Mide Technology', author_email='help@mide.com', description='A comprehensive, user-centric Python API for working with enDAQ data and devices', From 4ade6fcd56afdc102252e75973019746b79386d3 Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Wed, 3 Nov 2021 13:57:50 -0400 Subject: [PATCH 20/24] Cleaned Up Use Of collections.Container --- endaq/plot/dashboards.py | 8 ++++---- endaq/plot/plots.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/endaq/plot/dashboards.py b/endaq/plot/dashboards.py index 1541965..a42ec1e 100644 --- a/endaq/plot/dashboards.py +++ b/endaq/plot/dashboards.py @@ -1,6 +1,6 @@ from __future__ import annotations -import collections +from collections import Container import numpy as np import pandas as pd import plotly.graph_objects as go @@ -12,7 +12,7 @@ def rolling_enveloped_dashboard( channel_df_dict: dict, desired_num_points: int = 250, num_rows: Optional[int] = None, num_cols: Optional[int] = 3, width_for_subplot_row: int = 400, height_for_subplot_row: int = 400, - subplot_colors: Optional[collections.Container] = None, min_points_to_plot: int = 1, plot_as_bars: bool = False, + subplot_colors: Optional[Container] = None, min_points_to_plot: int = 1, plot_as_bars: bool = False, plot_full_single_channel: bool = False, opacity: float = 1, y_axis_bar_plot_padding: float = 0.06 ) -> go.Figure: """ @@ -285,7 +285,7 @@ def rolling_enveloped_dashboard( def rolling_metric_dashboard(channel_df_dict: dict, desired_num_points: int = 250, num_rows: Optional[int] = None, num_cols: Optional[int] = 3, rolling_metrics_to_plot: tuple = ('mean', 'std'), - metric_colors: Optional[collections.Container] = None, width_for_subplot_row: int = 400, + metric_colors: Optional[Container] = None, width_for_subplot_row: int = 400, height_for_subplot_row: int = 400) -> go.Figure: """ A function to create a dashboard of subplots of the given data, plotting a set of rolling metrics. @@ -431,7 +431,7 @@ def rolling_metric_dashboard(channel_df_dict: dict, desired_num_points: int = 25 # (IMPORTANT NOTE) The use of this as a dictionary is dependent on it maintaining being 'insertion ordered', # which is a thing in Python 3.7 (may have existed in a different way in python 3.6, but I'm not sure) CHANNEL_DFS = { - doc.channels[ch].name: endaq.ide.to_pandas(doc.channels[ch], time_mode='datetime') for ch in doc.channels} + doc.channels[ch].name: endaq.ide.to_pandas(doc.channels[ch], time_mode='seconds') for ch in doc.channels} SINGLE_CHANNEL = r'40g DC Acceleration' JUST_ACCEL_DFS = {SINGLE_CHANNEL: CHANNEL_DFS[SINGLE_CHANNEL]} diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index a658862..c96c34e 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -7,7 +7,7 @@ from plotly.subplots import make_subplots from scipy import signal from typing import Optional -import collections +from collections import Container from endaq.calc import sample_spacing from endaq.calc.psd import to_octave, welch @@ -34,7 +34,7 @@ def multi_file_plot_attributes(multi_file_db: pd.DataFrame, rows_to_plot: np.ndarray = DEFAULT_ATTRIBUTES_TO_PLOT_INDIVIDUALLY, - recording_colors: Optional[collections.Container] = None, + recording_colors: Optional[Container] = None, width_per_subplot: int = 400) -> go.Figure: """ Creates a Plotly figure plotting all the desired attributes from the given DataFrame. @@ -79,10 +79,10 @@ def multi_file_plot_attributes(multi_file_db: pd.DataFrame, return fig.update_layout(width=len(rows_to_plot)*width_per_subplot, showlegend=False) -def general_get_correlation_figure(merged_df: pd.DataFrame, recording_colors: Optional[collections.Container] = None, - hover_names: Optional[collections.Container] = None, +def general_get_correlation_figure(merged_df: pd.DataFrame, recording_colors: Optional[Container] = None, + hover_names: Optional[Container] = None, characteristics_to_show_on_hover: list = [], - starting_cols: collections.Container = None) -> go.Figure: + starting_cols: Container = None) -> go.Figure: """ A function to create a plot with two drop-down menus, each populated with a set of options corresponding to the scalar quantities contained in the given dataframe. The data points will then be plotted with the X and Y axis @@ -172,7 +172,7 @@ def general_get_correlation_figure(merged_df: pd.DataFrame, recording_colors: Op return fig -def get_pure_numpy_2d_pca(df: pd.DataFrame, recording_colors: Optional[collections.Container] = None) -> go.Figure: +def get_pure_numpy_2d_pca(df: pd.DataFrame, recording_colors: Optional[Container] = None) -> go.Figure: """ Get a Plotly figure of the 2d PCA for the given DataFrame. This will have dropdown menus to select which components are being used for the X and Y axis. @@ -360,7 +360,7 @@ def octave_psd_bar_plot(df: pd.DataFrame, bins_per_octave: int = 3, f_start: flo def rolling_min_max_envelope(df: pd.DataFrame, desired_num_points: int = 250, plot_as_bars: bool = False, plot_title: str = "", opacity: float = 1, - colors_to_use: Optional[collections.Container] = None) -> go.Figure: + colors_to_use: Optional[Container] = None) -> go.Figure: """ A function to create a Plotly Figure to plot the data for each of the available data sub-channels, designed to reduce the number of points/data being plotted without minimizing the insight available from the plots. It will From 1947fa6259c0031923f82b304a8a9280a4809f8f Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Wed, 3 Nov 2021 14:35:27 -0400 Subject: [PATCH 21/24] Docstring Fix --- endaq/plot/utilities.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/endaq/plot/utilities.py b/endaq/plot/utilities.py index c11935a..44b9784 100644 --- a/endaq/plot/utilities.py +++ b/endaq/plot/utilities.py @@ -189,7 +189,6 @@ def determine_plotly_map_zoom( margin: float = 1.2, ) -> float: """ - Originally based on the following post: https://stackoverflow.com/questions/63787612/plotly-automatic-zooming-for-mapbox-maps Finds optimal zoom for a plotly mapbox. @@ -206,12 +205,12 @@ def determine_plotly_map_zoom( :param width_to_height: float, expected ratio of final graph's with to height, used to select the constrained axis. :param margin: The desired margin around the plotted points (where 1 would be no-margin) - :return: + :return: The zoom scaling for the Plotly map NOTES: - - This could be potentially problematic. By simply averaging min/max coorindates + This implementation could be potentially problematic. By simply averaging min/max coorindates you end up with situations such as the longitude lines -179.99 and 179.99 being - almost right next to each other, but their center is calculated at 0, the other side of the earth. + almost right next to each other, but their center is calculated at 0, the other side of the earth. """ if lons is None and lats is None: if isinstance(lonlats, tuple): From 52b466b80dc69704a8d9d13700c46ecaf9cdca8a Mon Sep 17 00:00:00 2001 From: Becker Awqatty Date: Wed, 3 Nov 2021 14:40:05 -0400 Subject: [PATCH 22/24] fixup: fixed docstring --- endaq/plot/utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endaq/plot/utilities.py b/endaq/plot/utilities.py index 44b9784..b00be9c 100644 --- a/endaq/plot/utilities.py +++ b/endaq/plot/utilities.py @@ -208,7 +208,7 @@ def determine_plotly_map_zoom( :return: The zoom scaling for the Plotly map NOTES: - This implementation could be potentially problematic. By simply averaging min/max coorindates + This implementation could be potentially problematic. By simply averaging min/max coorindates you end up with situations such as the longitude lines -179.99 and 179.99 being almost right next to each other, but their center is calculated at 0, the other side of the earth. """ From 72ed7cbfb65f2b977117b2961dbf9f37aae12460 Mon Sep 17 00:00:00 2001 From: SamRagusa Date: Wed, 3 Nov 2021 14:53:26 -0400 Subject: [PATCH 23/24] One Additional Docstring Added --- endaq/plot/utilities.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/endaq/plot/utilities.py b/endaq/plot/utilities.py index b00be9c..4d14b26 100644 --- a/endaq/plot/utilities.py +++ b/endaq/plot/utilities.py @@ -144,7 +144,9 @@ def get_center_of_coordinates(lats: np.ndarray, lons: np.ndarray, as_list: bool dictionary of format {"lon": lon_center, "lat": lat_center} :param as_degrees: A boolean value representing if the 'lats' and 'lons' parameters are given in degrees (as opposed to radians). These units will be used for the returned values as well. - :return: + :return: The latitude and longitude values as either a dictionary or a list, which is + determined by the value of the `as_list` parameter (see the `as_list` docstring for details + on the formatting of this return value """ # Convert coordinates to radians if given in degrees if as_degrees: From b7052070a4e7ed8e48d61210809b1593c170b0c9 Mon Sep 17 00:00:00 2001 From: Becker Awqatty Date: Wed, 3 Nov 2021 14:58:43 -0400 Subject: [PATCH 24/24] changed docstrings to use appropriate sphinx directives --- endaq/plot/plots.py | 2 +- endaq/plot/utilities.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/endaq/plot/plots.py b/endaq/plot/plots.py index c96c34e..534799c 100644 --- a/endaq/plot/plots.py +++ b/endaq/plot/plots.py @@ -181,7 +181,7 @@ def get_pure_numpy_2d_pca(df: pd.DataFrame, recording_colors: Optional[Container :param recording_colors: See the same parameter in the general_get_correlation_figure function :return: A plotly figure as described in the main function description - TODO: + .. todo:: - Add type checking statements to ensure the given dataframe contains enough values of the desired type - Add type checking statements to ensure the recording_colors given (if not None) are the proper length """ diff --git a/endaq/plot/utilities.py b/endaq/plot/utilities.py index 4d14b26..1b57a29 100644 --- a/endaq/plot/utilities.py +++ b/endaq/plot/utilities.py @@ -209,7 +209,7 @@ def determine_plotly_map_zoom( :param margin: The desired margin around the plotted points (where 1 would be no-margin) :return: The zoom scaling for the Plotly map - NOTES: + .. note:: This implementation could be potentially problematic. By simply averaging min/max coorindates you end up with situations such as the longitude lines -179.99 and 179.99 being almost right next to each other, but their center is calculated at 0, the other side of the earth.