"""
Canvas clustering functionality that uses GUI libraries
There are classes to perform custering and to support
graphical interfaces to the clustering options.
Copyright Schrodinger, LLC. All rights reserved.
"""
# Contributors: Quentin McDonald
import csv
import numpy
from matplotlib import cm
from matplotlib.patches import Rectangle
from matplotlib.widgets import Cursor
import schrodinger.application.canvas.cluster as cluster
import schrodinger.ui.qt.appframework as appframework
import schrodinger.ui.qt.structure2d as structure2d
import schrodinger.ui.qt.swidgets as swidgets
from schrodinger.infra import canvas2d
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import smatplotlib
from schrodinger.utils import csv_unicode
try:
    from schrodinger import maestro
except ImportError:
    maestro = None  # Running outside of Maestro
# PyQt GUI Classes start here ###########################
[docs]class CanvasFingerprintClusterGUI(cluster.CanvasFingerprintCluster):
    """
    A subclass of the canvas fingerprint clusterer which is to be
    used from a program with a TKInter interface. This class has methods
    for creating a component which displays the clustering linkage options
    """
    CREATE_RADIOBUTTON_NAMES = [
        "Duplicate entries to a new group for each cluster",
        "Move entries to a new group for each cluster",
        "A group containing the structures nearest the centroid in each cluster",
        "Cluster index and size properties for each entry"
    ]
    DUPLICATE, MOVE, REPRESENTATIVE, ENTRY = list(range(4))
[docs]    def __init__(self, logger):
        super(CanvasFingerprintClusterGUI, self).__init__(logger)
        self.plot_dialog = None
        self.dendrogram_dialog = None
        self.num_clusters_entry = None
        self.distance_matrix_dialog = None
        self.distance_matrix_callback = None
        self.times_applied = 0 
[docs]    def getTab(self,
               command=None,
               linkage=True,
               cluster=True,
               results=True,
               apply=True,
               msg=None):
        """
        Creates a tab that can be used in a QTabWidget for calculating
        clustering.  The tab has the following sections:
        Linkage, Cluster, Clustering Results, Apply Clustering
        :type command: callable object
        :param command: function to be called when the Calculate Clustering
            button is pressed.
        :type linkage: bool
        :param linkage: True if this section should be included (default)
        :type cluster: bool
        :param cluster: True if this section should be included (default)
        :type results: bool
        :param results: True if this section should be included (default)
        :type apply: bool
        :param apply: True if this section should be included (default)
        :type msg: str
        :param msg: The message that appears right above the Calculate
            Clustering button.
        :rtype: QWidget
        :return: widget containing the clustering gui
        Usage: QTabWidget.addTab(fp_sim.getTab(doClustering))
        """
        widget = QtWidgets.QWidget()
        tab_layout = swidgets.SVBoxLayout(widget)
        if linkage:
            self.linkage_gui = self.getLinkageGUI()
            tab_layout.addWidget(self.linkage_gui)
        if cluster:
            self.cluster_gui = self.getClusterGUI(command, msg=msg)
            tab_layout.addWidget(self.cluster_gui)
        if results:
            self.results_gui = self.getResultsGUI()
            tab_layout.addWidget(self.results_gui)
        if apply:
            self.apply_gui = self.getApplyGUI()
            tab_layout.addWidget(self.apply_gui)
        tab_layout.addStretch()
        return widget 
[docs]    def getClusterGUI(self, command, msg=None):
        """
        Returns a GUI Group Box which displays the Cluster Calculation button
        :type msg: str
        :param msg: The message that appears right above the Calculate
            Clustering button.
        :type command: callable object
        :param command: function to be called when the Calculate Clustering
            button is pressed.
        :rtype: swidgets.SGroupBox (QGroupBox)
        :return: groupbox containing the cluster widgets
        """
        self.cluster_group = swidgets.SGroupBox('Cluster')
        if msg is None:
            msg = 'Clustering is performed on the selected entries in the ' + \
                    
'project\nusing the current fingerprint and similarity' + \
                    
' settings.'
        self.cluster_group.layout.addWidget(QtWidgets.QLabel(msg))
        swidgets.SPushButton('Calculate Clustering',
                             command=command,
                             layout=self.cluster_group.layout)
        return self.cluster_group 
[docs]    def getLinkageGUI(self):
        """
        Returns a GUI component which displays the cluster linkage options
        :rtype: swidgets.SGroupBox (QGroupBox)
        :return: groupbox containing the linkage widgets
        """
        # Create the widgets
        self.base_group = swidgets.SGroupBox('Linkage', layout='horizontal')
        self._linkage_label = QtWidgets.QLabel('Linkage method:')
        self.base_group.layout.addWidget(self._linkage_label)
        # Put the methods into the combobox and select 'Average' as the default
        self.linkage_combobox = swidgets.SComboBox(
            items=self.LINKAGE_TYPES,
            default_item='Average',
            command=self.setLinkage,
            layout=self.base_group.layout)
        self.base_group.layout.addStretch()
        return self.base_group 
[docs]    def getResultsGUI(self):
        """
        Returns a GUI which displays the clustering results in terms
        of the strain and best number of clusters and provides access to
        plots for the dendrogram and cluster statistics. This will be
        deactivated until the update() method is called at which time it
        will be activated and refreshed with the results from the most
        recent clustering
        """
        self.results_base_group = swidgets.SGroupBox('Clustering Results')
        # Create and lay out the labels
        self.strain_label = QtWidgets.QLabel('Cluster strain is:')
        self.best_clu_label = QtWidgets.QLabel('Best number of clusters is:')
        self.results_base_group.layout.addWidget(self.strain_label)
        self.results_base_group.layout.addWidget(self.best_clu_label)
        # Create and lay out the buttons
        self._button_layout = swidgets.SHBoxLayout()
        self._clustering_button = swidgets.SPushButton(
            'Clustering statistics...', command=self.showStatisticsPlot)
        self._dendrogram_button = swidgets.SPushButton(
            'Dendrogram...', command=self.showDendrogramPlot)
        self._distance_button = swidgets.SPushButton(
            'Distance matrix...', command=self.showDistanceMatrixPlot)
        self._button_layout.addWidget(self._clustering_button)
        self._button_layout.addWidget(self._dendrogram_button)
        self._button_layout.addWidget(self._distance_button)
        self._button_layout.addStretch()
        self.results_base_group.layout.addLayout(self._button_layout)
        # Start with all results disabled:
        self.results_base_group.setEnabled(False)
        return self.results_base_group 
[docs]    def setNumClusters(self, num):
        """
        Set the number of clusters in the Apply Clustering
        Section
        """
        self.apply_num_edit.setText(str(num)) 
[docs]    def getApplyGUI(self):
        """
        Return the controls used to apply the clustering to selected
        entries in the project table
        """
        self.apply_clu_group = swidgets.SGroupBox('Apply Clustering')
        self._apply_num_layout = swidgets.SHBoxLayout()
        self._apply_button_layout = swidgets.SHBoxLayout()
        self.apply_clu_group.layout.addLayout(self._apply_num_layout)
        num_label = QtWidgets.QLabel('Number of clusters:')
        # A QLineEdit that isn't too wide and only accepts positive ints
        self.apply_num_edit = QtWidgets.QLineEdit('1')
        self.apply_num_edit.setMaximumWidth(60)
        posint_validator = QtGui.QIntValidator(self.apply_num_edit)
        posint_validator.setBottom(1)
        self.apply_num_edit.setValidator(posint_validator)
        self._apply_num_layout.addWidget(num_label)
        self._apply_num_layout.addWidget(self.apply_num_edit)
        self._apply_num_layout.addStretch()
        create_label = QtWidgets.QLabel('Create:')
        self.apply_clu_group.layout.addWidget(create_label)
        # Now radio buttons, with the first one selected
        self.apply_radio_buttons = []
        for astring in self.CREATE_RADIOBUTTON_NAMES:
            self.apply_radio_buttons.append(QtWidgets.QRadioButton(astring))
            self.apply_clu_group.layout.addWidget(self.apply_radio_buttons[-1])
        self.apply_radio_buttons[0].setChecked(True)
        # Finally the apply button
        self._apply_button = swidgets.SPushButton(
            'Apply Clustering', command=self.doApplyClustering)
        self._apply_button_layout.addWidget(self._apply_button)
        self._apply_button_layout.addStretch()
        self.apply_clu_group.layout.addLayout(self._apply_button_layout)
        # Start with apply disabled:
        self.apply_clu_group.setEnabled(False)
        return self.apply_clu_group 
[docs]    def doApplyClustering(self):
        """
        Once the clustering has been performed for the selected entries
        this method will apply it
        """
        pt = maestro.project_table_get()
        try:
            num_clusters = int(self.apply_num_edit.text())
        except ValueError:
            # This happens for blank edits
            maestro.warning("Please specify the number of clusters")
            return
        # Calculate the groupings with this number of clusters:
        self.group(num_clusters)
        # Apply the clustering via the cluster maps:
        cluster_map = self.getClusteringMap()
        cluster_contents = self.getClusterContents()
        ids = ""
        for cluster in list(cluster_contents):
            for c in cluster_contents[cluster]:
                ids += " %s" % c
        # Select the input entries:
        maestro.command("entryselectonly entry_id %s" % ids)
        if num_clusters > len(pt.selected_rows):
            maestro.warning("%s %d %s %d %s" %
                            ("It is not possible to generate", num_clusters,
                             "clusters when there are only",
                             len(pt.selected_rows), "entries to cluster."))
            return
        # Figure out which option the user selected
        for on_button, button in enumerate(self.apply_radio_buttons):
            if button.isChecked():
                break
        # For each entry add the properties:
        for entry in list(cluster_map):
            _add_cluster_statistics(entry, self, None, pt[entry])
            cluster_index = cluster_map[entry]
            pt[entry]['i_canvas_Canvas_Cluster_Index'] = int(cluster_index)
            pt[entry]['i_canvas_Canvas_Cluster_Size'] = \
                                  
len(cluster_contents[cluster_index])
        if on_button == self.DUPLICATE:
            # Entry for each group, but not disrupting any existing ones:
            # Duplicate the entries in place:
            maestro.command("entryduplicate")
            # Create a group for each entry:
            for cluster in list(cluster_contents):
                ids = ", ".join(cluster_contents[cluster])
                # We need to create unique group names. Check the
                # proposed name is unique and add an increment until it is:
                if self.times_applied == 0:
                    gname = "Cluster %d" % int(cluster)
                else:
                    gname = "Cluster %d_%d" % (int(cluster), self.times_applied)
                cmd = 'entrygroupcreate "%s" entry_id %s' % (gname, ids)
                maestro.command(cmd)
            self.times_applied += 1
            # Clear the properties added for the original selected entries:
            pnames = [
                "r_canvas_Distance_To_Centroid",
                "b_canvas_Is_Nearest_To_Centroid",
                "b_canvas_Is_Farthest_From_Centroid",
                "r_canvas_Max_Distance_From_Centroid",
                "r_canvas_Average_Distance_From_Centroid",
                "r_canvas_Cluster_Variance", "i_canvas_Canvas_Cluster_Index",
                "i_canvas_Canvas_Cluster_Size"
            ]
            for p in pnames:
                maestro.command('propertyclearvalue  allentries=selected "%s"' %
                                p)
        elif on_button == self.MOVE:
            # Create a group for each entry:
            for cluster in list(cluster_contents):
                ids = ", ".join(cluster_contents[cluster])
                if self.times_applied == 0:
                    cmd = 'entrygroupcreate "Cluster %d" entry_id %s' % (
                        int(cluster), ids)
                else:
                    cmd = 'entrygroupcreate "Cluster %d_%d" entry_id %s' % (
                        int(cluster), self.times_applied, ids)
                maestro.command(cmd)
            self.times_applied += 1
            # Delete any empty groups which are left:
            to_delete = []
            for g in pt.groups:
                if len(g.all_rows) == 0:
                    to_delete.append(g.name)
            for d in to_delete:
                maestro.command('entrygroupdelete "%s" isexp=false' % d)
        elif on_button == self.REPRESENTATIVE:
            # Create a group with a single structure from each entry in it
            # Note do this as a dictionary as we only want a single
            # entry per cluster. Becase there can be more than one
            # entry with "nearesttocentroid" marked we choose only the first:
            repr_entries = {}
            for entry in list(cluster_map):
                if self.getIsNearestToCentroid(entry):
                    cluster_index = int(cluster_map[entry])
                    if cluster_index not in repr_entries:
                        repr_entries[cluster_index] = entry
            ids = ", ".join(list(repr_entries.values()))
            # Duplicate the representative entries:
            maestro.command("entryduplicate entry_id %s" % ids)
            # This causes the duplicate entries to be selected
            # Move them to a new group:
            group_name = "Representative Entries"
            if self.times_applied > 0:
                group_name += " %d" % self.times_applied
            maestro.command('entrygroupcreate "%s" selected' % group_name)
            self.times_applied += 1
        pt.update() 
[docs]    def updateResults(self):
        """
        Once clustering has been performed this method should be
        called to update the clustering results GUI:
        """
        # Enable the results and apply groups:
        self.results_base_group.setEnabled(True)
        self.apply_clu_group.setEnabled(True)
        self.strain_label.setText('Clustering strain is: %5.3f' % self._strain)
        self.best_clu_label.setText('Best number of clusters is: %d' %
                                    self.getBestNumberOfClusters())
        # Update the plots if they are visible:
        if self.plot_dialog:
            self.plot_dialog.redraw()
        if self.dendrogram_dialog:
            self.dendrogram_dialog.redraw()
        if self.distance_matrix_dialog:
            self.distance_matrix_dialog.redraw() 
[docs]    def close(self):
        """
        Perform the tasks necessary when closing the panel. This will include
        closing all the open plot windows
        """
        if self.plot_dialog:
            self.plot_dialog.close()
        if self.dendrogram_dialog:
            self.dendrogram_dialog.close()
        if self.distance_matrix_dialog:
            self.distance_matrix_dialog.close() 
[docs]    def showStatisticsPlot(self):
        """
        Display a plot of clustering statistics
        """
        if not self.plot_dialog:
            self.plot_dialog = ClusterStatisticsPlotDialog(self)
            self.plot_dialog.num_clusters_selected.connect(self.setNumClusters)
        self.plot_dialog.show()
        return 
[docs]    def showDendrogramPlot(self):
        """
        Display the clustering dendgoram
        """
        if not self.dendrogram_dialog:
            self.dendrogram_dialog = DendrogramPlotDialog(self)
            self.dendrogram_dialog.num_clusters_selected.connect(
                self.setNumClusters)
        self.dendrogram_dialog.show()
        return 
[docs]    def showDistanceMatrixPlot(self):
        """
        Display the distance matrix
        """
        if not self.distance_matrix_dialog:
            self.distance_matrix_dialog = DistanceMatrixPlotDialog(
                self, self.apply_num_edit, self.distance_matrix_callback)
        self.distance_matrix_dialog.show()
        return 
[docs]    def setDistanceMatrixCallback(self, cb):
        """
        Set a callback to be called when the mouse is clicked in the
        distance matrix plot. This should expect to receive two entry
        IDs
        """
        self.distance_matrix_callback = cb 
[docs]    def getClusteringStatistics(self, id):
        return {
            'r_canvas_Distance_To_Centroid': self.getDistanceToCentroid(id),
            'b_canvas_Is_Nearest_To_Centroid': self.getIsNearestToCentroid(id),
            'b_canvas_Is_Farthest_From_Centroid':
                self.getIsFarthestFromCentroid(id),
            'r_canvas_Max_Distance_From_Centroid':
                self.getMaxDistanceFromCentroid(id),
            'r_canvas_Average_Distance_From_Centroid':
                self.getAverageDistanceFromCentroid(id),
            'r_canvas_Cluster_Variance': self.getClusterVariance(id),
        }  
# TODO: Port to AppFramework2, factor out common code with DendrogramPlotDialog
[docs]class ClusterStatisticsPlotDialog(appframework.AppFramework):
    """
    A class which displays a dialog with a plot of the statistics for the
    most recent clustering
    """
    num_clusters_selected = QtCore.pyqtSignal(int)
    PLOT_TYPES = [
        "Kelley Penalty", "R-Squared", "Semipartial R-Squared",
        "Merge Distance", "Separation Ratio"
    ]
[docs]    def __init__(self, canvas_cluster):
        """
        Create an instance of the dialog. Objects passed are the parent
        and the CanvasFingerprintCluster object which will have the statistics
        :type canvas_cluster: CanvasFingerprintCluster object
        :param canvas_cluster: object that contains the clustering statistics
        """
        # Store arguments
        self.fp_clu = canvas_cluster
        self.visible = False
        # Create the window
        buttons = {'close': {'command': self.close}}
        appframework.AppFramework.__init__(self,
                                           buttons=buttons,
                                           title='Clustering Statistics',
                                           subwindow=True)
        # Create the plot
        self.canvas = smatplotlib.SmatplotlibCanvas(width=5,
                                                    height=5,
                                                    layout=self.interior_layout)
        self.sub_plot = self.canvas.figure.add_subplot(111)
        self.canvas.show()
        # The control frame widgets
        self._ctrl_layout = swidgets.SHBoxLayout()
        self._ctrl_label = QtWidgets.QLabel('Plot:')
        self.ctrl_combobox = swidgets.SComboBox(items=self.PLOT_TYPES,
                                                command=self.setPlotType)
        self._ctrl_click_label = QtWidgets.QLabel(
            'Click in plot to set number of clusters')
        # Lay out the control frame
        self._ctrl_layout.addWidget(self._ctrl_label)
        self._ctrl_layout.addWidget(self.ctrl_combobox)
        self._ctrl_layout.addStretch()
        self._ctrl_layout.addWidget(self._ctrl_click_label)
        self._ctrl_layout.addStretch()
        self.interior_layout.addLayout(self._ctrl_layout) 
[docs]    def redraw(self):
        """
        Redraw the plot with the current settings
        """
        if not self.visible:
            return
        x_list = self.fp_clu.getNumberOfClustersList()
        # Get the type of plot to draw
        if self.plot_type == self.PLOT_TYPES[0]:
            y_list = self.fp_clu.getKelleyPenaltyList()
        elif self.plot_type == self.PLOT_TYPES[1]:
            y_list = self.fp_clu.getRSquaredList()
        elif self.plot_type == self.PLOT_TYPES[2]:
            y_list = self.fp_clu.getSemiPartialRSquaredList()
        elif self.plot_type == self.PLOT_TYPES[3]:
            y_list = self.fp_clu.getMergeDistanceList()
        elif self.plot_type == self.PLOT_TYPES[4]:
            y_list = self.fp_clu.getSeparationRatioList()
        # Plot the data
        self.sub_plot.clear()
        self.sub_plot.plot(x_list, y_list, 'r')
        self.sub_plot.set_title(self.plot_type)
        self.sub_plot.set_xlabel("Number of clusters")
        self.sub_plot.set_ylabel(self.plot_type)
        # Hook up the mouse click event and show the plot
        self.canvas.mpl_connect('button_release_event', self.click)
        self.canvas.show()
        self.canvas.draw()
        return 
[docs]    def setPlotType(self, plot_type):
        """
        Called when the plot type option menu is changed
        """
        self.plot_type = str(plot_type)
        self.redraw()
        return 
[docs]    def show(self):
        """
        Show the plot dialog
        """
        self.visible = True
        self.redraw()
        appframework.AppFramework.show(self)
        # These next two lines help ensure that the plot shows the first time
        # without a corrupt background.
        self.canvas.flush_events()
        self.repaint() 
[docs]    def close(self):
        """
        Dismiss the window
        """
        self.visible = False
        self.closePanel() 
[docs]    def click(self, event):
        """
        Click in plot handler
        """
        if not self.canvas.toolbar.mode and event.inaxes and event.button == 1:
            num_clu = int(round(event.xdata))
            self.num_clusters_selected.emit(num_clu)  
# TODO: Port to AppFramework2, factor out common code shared with
# ClusterStatisticsPlotDialog class.
[docs]class DendrogramPlotDialog(appframework.AppFramework):
    """
    A class which displays a dialog with a plot of the dendrogram from the
    most recent clustering
    """
    num_clusters_selected = QtCore.pyqtSignal(int)
[docs]    def __init__(self, canvas_cluster=None):
        """
        Create an instance of the dialog. Either canvas_cluster should be
        specified (CanvasFingerprintCluster object with data)
        :type canvas_cluster: CanvasFingerprintCluster object
        :param canvas_cluster: object that contains the clustering statistics
        """
        # Store arguments
        self.fp_clu = canvas_cluster
        # TODO: Reimplement as a signal:
        self.visible = False
        # Create the window
        buttons = {'close': {'command': self.close}}
        appframework.AppFramework.__init__(self,
                                           buttons=buttons,
                                           title='Dendrogram',
                                           subwindow=True)
        # Create the plot
        self.canvas = smatplotlib.SmatplotlibCanvas(width=5,
                                                    height=4,
                                                    layout=self.interior_layout)
        self.sub_plot = self.canvas.figure.add_subplot(111)
        self.canvas.mpl_connect('button_release_event', self.click)
        self.canvas.show()
        # Set the cursor equal to a horizontal line
        cursor = Cursor(self.sub_plot, useblit=True, color='red', linewidth=0.8)
        cursor.vertOn = False
        # Add a label and pack them up
        self._ctrl_click_label = QtWidgets.QLabel(
            'Click in plot to set number of clusters')
        self.addCentralWidget(self._ctrl_click_label)
        self.canvas.mpl_connect('button_release_event', self.click) 
[docs]    def show(self):
        """
        Show the plot dialog
        """
        self.visible = True
        self.redraw()
        appframework.AppFramework.show(self)
        # These next two lines help ensure that the plot shows the first time
        # without a corrupt background.
        self.canvas.flush_events()
        self.repaint() 
[docs]    def close(self):
        """
        Dismiss the window
        """
        self.visible = False
        self.closePanel() 
[docs]    def redraw(self):
        """
        Redraw the plot
        """
        if not self.visible:
            return
        self.sub_plot.clear()
        (lines, x_axis_ticks, x_axis_tick_labels) = \
                                                
self.fp_clu.getDendrogramData()
        maxX = 0
        for l in lines:
            self.sub_plot.plot(l[0], l[1], 'k', linewidth=0.2)
            lx = max(l[0])
            if lx > maxX:
                maxX = lx
        # Set up the misc. plot details
        self.sub_plot.set_title("Dendrogram")
        self.sub_plot.set_xlabel("Structure")
        self.sub_plot.set_ylabel("Merge Distance")
        self.sub_plot.set_xticks(x_axis_ticks)
        self.sub_plot.set_xlim([0, maxX + 1])
        self.sub_plot.set_xticklabels(x_axis_tick_labels,
                                      size=6,
                                      rotation='vertical')
        # Turn X-axis tick labels off:
        for line in self.sub_plot.get_xticklines():
            line.set_visible(False)
        self.canvas.show()
        self.canvas.draw() 
[docs]    def click(self, event):
        """
        Click in plot handler
        """
        if not self.canvas.toolbar.mode and event.inaxes and event.button == 1:
            # Merge distance corresponding to user's click in the chart:
            click_distance = event.ydata
            merge_distances = self.fp_clu.getMergeDistanceList()
            for i, md in reversed(list(enumerate(merge_distances))):
                if click_distance < md:
                    num_clu = i + 1
                    self.num_clusters_selected.emit(num_clu)
                    break  
# TODO: Port to AppFramework2
[docs]class DistanceMatrixPlotDialog(appframework.AppFramework):
    """
    A class which displays a dialog with a plot of the distance matrix
    associated with the most recent clustering
    """
    PLOT_TYPES = ["Cluster Order", "Original Order"]
[docs]    def __init__(self,
                 canvas_cluster,
                 num_clusters_edit=None,
                 distance_matrix_callback=None,
                 structures=True,
                 num_clusters=None):
        """
        Create an instance of the dialog.
        :type canvas_cluster: CanvasFingerprintCluster object
        :param canvas_cluster: object that contains the clustering statistics
        :type num_clusters_edit: QLineEdit
        :param num_clusters_edit: the widget that contains the number of
            clusters
        :type distance_matrix_callback: function
        :param distance_matrix_callback: function called with the IDs of the
            structures which are clicked
        :type structures: bool
        :param structures: True if the distance matrix should show structures
            when the user clicks on the plot, False if not
        """
        # Store arguments
        self.fp_clu = canvas_cluster
        self.num_clusters_edit = num_clusters_edit
        self.num_clusters = num_clusters
        self.distance_matrix_callback = distance_matrix_callback
        self.structures = structures
        # Initialize some properties
        self.visible = False
        self.colorbar = None
        self.original_mat = None
        self.cluster_mat = None
        self.num_col_edit = None
        # Create the window
        buttons = {'close': {'command': self.close}}
        appframework.AppFramework.__init__(self,
                                           buttons=buttons,
                                           title='Distance Matrix',
                                           subwindow=True)
        # Create the two master layouts
        self._data_layout = swidgets.SHBoxLayout()
        self._options_layout = swidgets.SVBoxLayout()
        self.interior_layout.addLayout(self._data_layout)
        self.interior_layout.addLayout(self._options_layout)
        # Now the two data sublayouts
        if self.structures:
            self._structures_layout = swidgets.SVBoxLayout()
            self._data_layout.addLayout(self._structures_layout)
        # Now the two option sublayouts
        self._opt1_layout = swidgets.SHBoxLayout()
        self._opt2_layout = swidgets.SHBoxLayout()
        self._options_layout.addLayout(self._opt1_layout)
        self._options_layout.addLayout(self._opt2_layout)
        # Create the matplotlib plot
        self.canvas = smatplotlib.SmatplotlibCanvas(width=5, height=5)
        self.sub_plot = self.canvas.figure.add_subplot(111, aspect='equal')
        self.canvas.mpl_connect('button_release_event', self.click)
        self.canvas.show()
        self.picking_rect = Rectangle((0.0, 0.0),
                                      1.0,
                                      1.0,
                                      fill=True,
                                      edgecolor='k',
                                      facecolor='white')
        self.interior_layout.insertWidget(0, self.canvas.toolbar)
        self._data_layout.insertWidget(0, self.canvas)
        if self.structures:
            # Create the structure picture for the X-axis structure
            self.x_structure = structure2d.StructurePicture()
            self.x_structure.model.setShowHydrogenPreference(0)
            self.x_structure.model.setBondLineWidth(2)
            self.x_structure.model.setAtomRadius(1.2)
            self.x_structure.model.setTransparent(False)
            # And now the Y-axis structure
            self.y_structure = structure2d.StructurePicture()
            self.y_structure.model.setShowHydrogenPreference(0)
            self.y_structure.model.setBondLineWidth(2)
            self.y_structure.model.setAtomRadius(1.2)
            self.y_structure.model.setTransparent(False)
            # Now fill the structure region
            self.x_struct_label = QtWidgets.QLabel('X-Axis structure:')
            self.x_title_label = QtWidgets.QLabel('')
            self.y_struct_label = QtWidgets.QLabel('Y-Axis structure:')
            self.y_title_label = QtWidgets.QLabel('')
            self._structures_layout.addWidget(self.x_struct_label)
            self._structures_layout.addWidget(self.x_structure)
            self._structures_layout.addWidget(self.x_title_label)
            self._structures_layout.addWidget(self.y_struct_label)
            self._structures_layout.addWidget(self.y_structure)
            self._structures_layout.addWidget(self.y_title_label)
        # Now the first row of options
        self.option_label = QtWidgets.QLabel('Show distance matrix in:')
        self.plot_option = swidgets.SComboBox(items=self.PLOT_TYPES,
                                              command=self.setPlotType)
        if self.structures:
            self.plot_label = QtWidgets.QLabel(
                'Click in plot to display structures.')
            self._opt1_layout.addWidget(self.plot_label)
        self.plot_include_toggle = QtWidgets.QCheckBox(
            'Include clicked structures in Workspace')
        self._opt1_layout.addWidget(self.option_label)
        self._opt1_layout.addWidget(self.plot_option)
        self._opt1_layout.addSpacing(8)
        self._opt1_layout.addSpacing(8)
        self._opt1_layout.addWidget(self.plot_include_toggle)
        self._opt1_layout.addStretch()
        # And the second row of options
        bad_colormaps = set(
            ['brg', 'bwr', 'gist_rainbow', 'seismic', 'terrain'])
        colormaps = []
        for m in list(cm.datad):
            if not m.endswith("_r") and m not in bad_colormaps:
                colormaps.append(m)
        def mapkey(value):
            return value.lower()
        colormaps.sort(key=mapkey)
        self.cmap_label = QtWidgets.QLabel('Colormap:')
        self.cmap_combobox = swidgets.SComboBox(items=colormaps,
                                                command=self.setColormap,
                                                default_item='jet')
        self.num_col_label = QtWidgets.QLabel('Number of colors:')
        self.num_col_edit = QtWidgets.QLineEdit('100')
        # Redraw the plot when the user types in a new number of colors
        self.num_col_edit.editingFinished.connect(self.draw)
        # FIXME use the new API for connecting the signal
        self.num_col_edit.setValidator(swidgets.SNonNegativeIntValidator())
        self.num_col_edit.setMaximumWidth(60)
        self._opt2_layout.addWidget(self.cmap_label)
        self._opt2_layout.addWidget(self.cmap_combobox)
        self._opt2_layout.addWidget(self.num_col_label)
        self._opt2_layout.addWidget(self.num_col_edit)
        self._opt2_layout.addStretch() 
[docs]    def setPlotType(self, plot_type):
        """
        Called when the plot type combobox is changed
        :type plot_type: string
        :param plot_type: the new plot type
        """
        self.plot_type = str(plot_type)
        self.draw() 
[docs]    def setColormap(self, color_map):
        """
        Called when the color map combobox is changed
        :type color_map: string
        :param color_map: the new color map
        """
        self.color_map = str(color_map)
        self.draw() 
[docs]    def redraw(self):
        """
        Force a redraw with refetching of the distance map data
        """
        if self.structures:
            self.x_structure.clear()
            self.y_structure.clear()
        self.original_mat = None
        self.cluster_mat = None
        self.draw() 
[docs]    def draw(self):
        """
        Called when the plot type option menu is changed
        """
        if self.num_col_edit is None:
            # Leave if redraw is called before plot object constucted
            return
        if self.original_mat is None or self.cluster_mat is None:
            # Need to read the distance matrix file:
            self.n = len(self.fp_clu.getNumberOfClustersList())
            self.original_mat = numpy.zeros((self.n, self.n), numpy.double)
            self.cluster_mat = numpy.zeros((self.n, self.n), numpy.double)
            if self.num_clusters_edit:
                try:
                    num_clusters = int(self.num_clusters_edit.text())
                except ValueError:
                    # Can happen for empty edits
                    maestro.warning('Please enter a valid number of clusters')
                    return
            elif self.num_clusters:
                num_clusters = self.num_clusters
            else:
                raise RuntimeError(
                    'Either num_clusters or num_clusters_edit must be specified'
                )
            cluster_order = self.fp_clu.getClusterOrderMap(num_clusters)
            self.reverse_cluster_order = {}
            for eid in list(cluster_order):
                self.reverse_cluster_order[cluster_order[eid]] = eid
            self.entry_id_list = []
            csv_filename = self.fp_clu.getDistanceMatrixFile()
            with csv_unicode.reader_open(csv_filename) as fh:
                csv_reader = csv.reader(fh)
                for r, row in enumerate(csv_reader):
                    if r == 0:
                        for c, eid in enumerate(row):
                            if c == 0:
                                continue
                            else:
                                self.entry_id_list.append(eid)
                        continue
                    for c, dist in enumerate(row):
                        if c == 0:
                            continue
                        self.original_mat[r - 1, c - 1] = float(dist)
                        r_eid = self.entry_id_list[r - 1]
                        c_eid = self.entry_id_list[c - 1]
                        r_clu_order = int(cluster_order[r_eid])
                        c_clu_order = int(cluster_order[c_eid])
                        self.cluster_mat[r_clu_order, c_clu_order] = float(dist)
        self.sub_plot.clear()
        try:
            num_col = int(self.num_col_edit.text())
        except ValueError:
            # Can happen for empty edits
            maestro.warning('Please enter a valid number of colors')
            return
        cmap = cm.get_cmap(self.color_map, num_col)
        if self.plot_type == self.PLOT_TYPES[0]:
            matrix = self.cluster_mat
        else:
            matrix = self.original_mat
        pcol = self.sub_plot.pcolormesh(matrix, cmap=cmap, shading='flat')
        self.sub_plot.set_title(self.plot_type)
        self.sub_plot.set_xlim(0.0, self.n)
        self.sub_plot.set_ylim(0.0, self.n)
        self.sub_plot.set_xlabel("")
        self.sub_plot.set_ylabel("")
        if not self.colorbar:
            self.colorbar = self.canvas.figure.colorbar(pcol,
                                                        shrink=0.8,
                                                        extend='neither')
        else:
            self.colorbar.set_cmap(cmap)
            self.colorbar.changed()
            self.colorbar.draw_all()
        self.canvas.show()
        self.canvas.draw()
        return 
[docs]    def show(self):
        """
        Show the plot dialog
        """
        self.visible = True
        self.redraw()
        appframework.AppFramework.show(self)
        # These next two lines help ensure that the plot shows the first time
        # without a corrupt background.
        self.canvas.flush_events()
        self.repaint() 
[docs]    def close(self):
        """
        Dismiss the window
        """
        if self.structures:
            self.x_structure.clear()
            self.y_structure.clear()
        self.visible = False
        self.closePanel() 
[docs]    def click(self, event):
        """
        Click in plot handler
        """
        if not self.canvas.toolbar.mode and event.inaxes and event.button == 1:
            pt = maestro.project_table_get()
            x = int(event.xdata)
            y = int(event.ydata)
            if self.plot_type == self.PLOT_TYPES[0]:
                x_eid = self.reverse_cluster_order[x]
                y_eid = self.reverse_cluster_order[y]
            else:
                x_eid = self.entry_id_list[x]
                y_eid = self.entry_id_list[y]
            if self.structures:
                self.drawStructure(self.x_structure, x_eid, self.x_title_label)
                self.drawStructure(self.y_structure, y_eid, self.y_title_label)
            # If the "Include in Workspace" toggle is on then we will also
            # issue Maestro commands to include the associated structures
            if self.plot_include_toggle.isChecked():
                maestro.command('entrywsincludeonly entry "%d"' % int(x_eid))
                maestro.command('entrywsinclude entry "%d"' % int(y_eid))
                # Make the WS resize to fit these entries
                maestro.command('fit entry.id %s, %s' % (x_eid, y_eid))
            self.picking_rect.set_x(x)
            self.picking_rect.set_y(y)
            self.picking_rect.set_visible(True)
            self.sub_plot.add_patch(self.picking_rect)
            self.sub_plot.draw_artist(self.picking_rect)
            self.canvas.show()
            self.canvas.draw()
            if self.distance_matrix_callback:
                self.distance_matrix_callback(x_eid, y_eid) 
[docs]    def drawStructure(self, canv, eid, title_label):
        """
        Draw the structure from the project with entry id 'eid' in
        the Canvas 'canv'
        """
        pt = maestro.project_table_get()
        st = pt[eid].getStructure()
        title = pt[eid]['s_m_title']
        if len(title) > 50:
            title = title[0:50] + "..."
        title_label.setText(title)
        # Use Canvas renderer to create a QPicture
        canv.drawStructure(
            st, canvas2d.ChmMmctAdaptor.StereoFromAnnotationAndGeometry_Safe)  
def _add_cluster_statistics(id, fp_clu, st, row):
    """
    A private function used to add the clustering statistics to
    a Structure object or Project table row. Only one of 'st' or
    'row' should be not None
    """
    props = fp_clu.getClusteringStatistics(id)
    if st is not None:
        st.property.update(props)
    else:
        for prop, value in props.items():
            row[prop] = value