Skip to content

Commit

Permalink
Merge pull request #237 from BiAPoL/histogram1d
Browse files Browse the repository at this point in the history
Implement Histogram1D if the same measurement is selected for both axes
  • Loading branch information
haesleinhuepf authored Apr 29, 2023
2 parents 3a9edf9 + 09414ca commit 2a8774c
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 72 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ on `Update Axes/Clustering Options` to refresh them. Click on `Plot` to draw the

Currently, you can select between two types of plots: scatter plot and 2D histogram. Click on "expand for advanced
options" to see the selection of the plot type. Clustering (manual and automatic) is possible using both types of plots.
2D histogram is recommended if you have a very high number of data points. Under advanced options you will also find the
checkbox determining whether not selected data points should be automatically clustered as another cluster or displayed as
gray data points and not visualized in the generated clusters IDs layer.
2D histogram is recommended if you have a very high number of data points. If you select the same measurement for both axes,
and histogram is selected as the plotting type, 1D histogram will be plotted. Clustering 1D histogram is not yet possible.

Under advanced options you will also find the checkbox determining whether not-selected data points should be automatically
clustered as another cluster or displayed as gray data points and not visualized in the generated clusters IDs layer.

![](https://github.com/BiAPoL/napari-clusters-plotter/raw/main/images/plot_plain.png)

Expand Down
73 changes: 71 additions & 2 deletions napari_clusters_plotter/_Qt_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
from matplotlib.path import Path
from matplotlib.widgets import LassoSelector, RectangleSelector
from matplotlib.widgets import LassoSelector, RectangleSelector, SpanSelector
from napari.layers import Image, Labels
from qtpy.QtCore import QRect
from qtpy.QtGui import QIcon
Expand Down Expand Up @@ -414,6 +414,43 @@ def disconnect(self):
self.canvas.draw_idle()


class SelectFrom1DHistogram:
def __init__(self, parent, ax, full_data):
self.parent = parent
self.ax = ax
self.canvas = ax.figure.canvas
self.xys = full_data

self.span_selector = SpanSelector(
ax,
onselect=self.onselect,
direction="horizontal",
props=dict(facecolor="#1f77b4", alpha=0.5),
)
self.click_id = self.canvas.mpl_connect("button_press_event", self.on_click)

def onselect(self, vmin, vmax):
self.ind_mask = np.logical_and(self.xys >= vmin, self.xys <= vmax).values

if self.parent.manual_clustering_method is not None:
self.parent.manual_clustering_method(self.ind_mask)

def on_click(self, event):
# Clear selection if user right-clicks (without moving) outside of the histogram
if event.inaxes != self.ax:
return
if event.button == 3:
# clear selection
self.ind_mask = np.zeros_like(self.xys, dtype=bool)
if self.parent.manual_clustering_method is not None:
self.parent.manual_clustering_method(self.ind_mask)

def disconnect(self):
self.span_selector.disconnect_events()
self.canvas.mpl_disconnect(self.click_id)
self.canvas.draw_idle()


# Class below was based upon matplotlib lasso selection example:
# https://matplotlib.org/stable/gallery/widgets/lasso_selector_demo_sgskip.html
class SelectFromCollection:
Expand Down Expand Up @@ -564,11 +601,42 @@ def make_2d_histogram(
self.axes.set_ylim(yedges[0], yedges[-1])
self.histogram = (h, xedges, yedges)

full_data = pd.concat([data_x, data_y], axis=1)
full_data = pd.concat([pd.DataFrame(data_x), pd.DataFrame(data_y)], axis=1)
self.selector.disconnect()
self.selector = SelectFrom2DHistogram(self, self.axes, full_data)
self.axes.figure.canvas.draw_idle()

def make_1d_histogram(
self,
data: "numpy.typing.ArrayLike",
bin_number: int = 400,
log_scale: bool = False,
):
counts, bins = np.histogram(data, bins=bin_number)
self.axes.hist(
bins[:-1],
bins,
edgecolor="white",
weights=counts,
log=log_scale,
color="#9A9A9A",
)
self.histogram = (counts, bins)
bin_width = bins[1] - bins[0]
self.axes.set_xlim(min(bins) - (bin_width / 2), max(bins) + (bin_width / 2))
ymin = 0
if log_scale:
ymin = 1
self.axes.set_ylim(ymin, max(counts) * 1.1)

if log_scale:
self.axes.set_xscale("linear")
self.axes.set_yscale("log")

self.selector.disconnect()
self.selector = SelectFrom1DHistogram(self, self.axes, data)
self.axes.figure.canvas.draw_idle()

def make_scatter_plot(
self,
data_x: "numpy.typing.ArrayLike",
Expand Down Expand Up @@ -613,6 +681,7 @@ def match_napari_layout(self):
# changing colors of axes labels
self.axes.xaxis.label.set_color("white")
self.axes.yaxis.label.set_color("white")
self.fig.canvas.draw_idle()


# overriding NavigationToolbar method to change the background and axes colors of saved figure
Expand Down
165 changes: 105 additions & 60 deletions napari_clusters_plotter/_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
)

from ._plotter_utilities import (
apply_cluster_colors_to_bars,
clustered_plot_parameters,
estimate_number_bins,
make_cluster_overlay_img,
Expand Down Expand Up @@ -53,7 +54,7 @@


class PlottingType(Enum):
HISTOGRAM_2D = auto()
HISTOGRAM = auto()
SCATTER = auto()


Expand Down Expand Up @@ -193,7 +194,6 @@ def replot():
features = get_layer_tabular_data(self.analysed_layer)

# redraw the whole plot

try:
self.run(
features,
Expand All @@ -210,7 +210,7 @@ def checkbox_status_changed():
replot()

def plotting_type_changed():
if self.plotting_type.currentText() == PlottingType.HISTOGRAM_2D.name:
if self.plotting_type.currentText() == PlottingType.HISTOGRAM.name:
self.bin_number_container.setVisible(True)
self.log_scale_container.setVisible(True)
self.plot_hide_non_selected.setChecked(True)
Expand All @@ -235,7 +235,7 @@ def bin_auto():
combobox_plotting_container.layout().addWidget(QLabel("Plotting type"))
self.plotting_type = QComboBox()
self.plotting_type.addItems(
[PlottingType.SCATTER.name, PlottingType.HISTOGRAM_2D.name]
[PlottingType.SCATTER.name, PlottingType.HISTOGRAM.name]
)
self.plotting_type.currentIndexChanged.connect(plotting_type_changed)
combobox_plotting_container.layout().addWidget(self.plotting_type)
Expand Down Expand Up @@ -427,7 +427,10 @@ def reset_choices(self, event=None):
def change_state_of_nonselected_checkbox(self):
# make the checkbox visible only if clustering is done manually
visible = (
True if "MANUAL_CLUSTER_ID" in self.plot_cluster_id.currentText() else False
True
if "MANUAL_CLUSTER_ID" in self.plot_cluster_id.currentText()
or self.plot_cluster_id.currentText() == ""
else False
)
self.hide_nonselected_checkbox_container.setVisible(visible)

Expand Down Expand Up @@ -556,51 +559,77 @@ def run(
self.graphics_widget.make_scatter_plot(
self.data_x, self.data_y, colors_plot, sizes, a
)

self.graphics_widget.axes.set_xlabel(plot_x_axis_name)
self.graphics_widget.axes.set_ylabel(plot_y_axis_name)
else:
if self.bin_auto.isChecked():
number_bins = int(
np.max(
[
estimate_number_bins(self.data_x),
estimate_number_bins(self.data_y),
]
if plot_x_axis_name == plot_y_axis_name:
number_bins = int(estimate_number_bins(self.data_x))
else:
number_bins = int(
np.max(
[
estimate_number_bins(self.data_x),
estimate_number_bins(self.data_y),
]
)
)
)
self.bin_number_spinner.setValue(number_bins)
self.bin_number_spinner.setValue(number_bins)
else:
number_bins = int(self.bin_number_spinner.value())

self.graphics_widget.make_2d_histogram(
self.data_x,
self.data_y,
colors,
bin_number=number_bins,
log_scale=self.log_scale.isChecked(),
)
# if both axes are the same, plot 1D histogram
if plot_x_axis_name == plot_y_axis_name:
self.graphics_widget.make_1d_histogram(
self.data_x,
bin_number=number_bins,
log_scale=self.log_scale.isChecked(),
)
# update bar colors to cluster ids
self.graphics_widget.axes = apply_cluster_colors_to_bars(
self.graphics_widget.axes,
cluster_name=plot_cluster_name,
features=features,
number_bins=number_bins,
feature_x=self.plot_x_axis_name,
colors=colors,
)
self.graphics_widget.figure.canvas.draw_idle()
self.graphics_widget.axes.set_xlabel(plot_x_axis_name)
self.graphics_widget.axes.set_ylabel("frequency")
else:
self.graphics_widget.make_2d_histogram(
self.data_x,
self.data_y,
colors,
bin_number=number_bins,
log_scale=self.log_scale.isChecked(),
)

rgb_img = make_cluster_overlay_img(
cluster_id=plot_cluster_name,
features=features,
feature_x=self.plot_x_axis_name,
feature_y=self.plot_y_axis_name,
colors=colors,
histogram_data=self.graphics_widget.histogram,
hide_first_cluster=self.plot_hide_non_selected.isChecked(),
)
xedges = self.graphics_widget.histogram[1]
yedges = self.graphics_widget.histogram[2]

self.graphics_widget.axes.imshow(
rgb_img,
extent=[xedges[0], xedges[-1], yedges[0], yedges[-1]],
origin="lower",
alpha=1,
aspect="auto",
)
self.graphics_widget.figure.canvas.draw_idle()
rgb_img = make_cluster_overlay_img(
cluster_id=plot_cluster_name,
features=features,
feature_x=self.plot_x_axis_name,
feature_y=self.plot_y_axis_name,
colors=colors,
histogram_data=self.graphics_widget.histogram,
hide_first_cluster=self.plot_hide_non_selected.isChecked(),
)
xedges = self.graphics_widget.histogram[1]
yedges = self.graphics_widget.histogram[2]

self.graphics_widget.axes.imshow(
rgb_img,
extent=[xedges[0], xedges[-1], yedges[0], yedges[-1]],
origin="lower",
alpha=1,
aspect="auto",
)
self.graphics_widget.figure.canvas.draw_idle()
self.graphics_widget.axes.set_xlabel(plot_x_axis_name)
self.graphics_widget.axes.set_ylabel(plot_y_axis_name)

self.graphics_widget.axes.set_xlabel(plot_x_axis_name)
self.graphics_widget.axes.set_ylabel(plot_y_axis_name)
self.graphics_widget.match_napari_layout()

from vispy.color import Color
Expand Down Expand Up @@ -705,31 +734,47 @@ def run(
self.graphics_widget.make_scatter_plot(
self.data_x, self.data_y, colors_plot, sizes, a
)

self.graphics_widget.axes.set_xlabel(plot_x_axis_name)
self.graphics_widget.axes.set_ylabel(plot_y_axis_name)
else:
if self.bin_auto.isChecked():
number_bins = int(
np.max(
[
estimate_number_bins(self.data_x),
estimate_number_bins(self.data_y),
]
if plot_x_axis_name == plot_y_axis_name:
number_bins = int(estimate_number_bins(self.data_x))
else:
number_bins = int(
np.max(
[
estimate_number_bins(self.data_x),
estimate_number_bins(self.data_y),
]
)
)
)
self.bin_number_spinner.setValue(number_bins)
else:
number_bins = int(self.bin_number_spinner.value())

self.graphics_widget.make_2d_histogram(
self.data_x,
self.data_y,
colors,
bin_number=number_bins,
log_scale=self.log_scale.isChecked(),
)
self.graphics_widget.axes.set_xlabel(plot_x_axis_name)
self.graphics_widget.axes.set_ylabel(plot_y_axis_name)
# if both axes are the same, plot 1D histogram
if plot_x_axis_name == plot_y_axis_name:
self.graphics_widget.make_1d_histogram(
self.data_x,
bin_number=number_bins,
log_scale=self.log_scale.isChecked(),
)
self.graphics_widget.axes.set_xlabel(plot_x_axis_name)
self.graphics_widget.axes.set_ylabel("frequency")
else:
self.graphics_widget.make_2d_histogram(
self.data_x,
self.data_y,
colors,
bin_number=number_bins,
log_scale=self.log_scale.isChecked(),
)
self.graphics_widget.axes.set_xlabel(plot_x_axis_name)
self.graphics_widget.axes.set_ylabel(plot_y_axis_name)

self.graphics_widget.match_napari_layout()

self.graphics_widget.draw() # Only redraws when cluster is not manually selected
# because manual selection already does that when selector is disconnected
self.graphics_widget.draw() # Always redraws, oterwise y-axis may not get updated in histograms
self.graphics_widget.reset_zoom()
Loading

0 comments on commit 2a8774c

Please sign in to comment.