Пример #1
0
class Plot:

    WIDTH = 800
    HEIGHT = 600

    def __init__(self, shifts, multiplicities, deviations):

        self.shifts = shifts
        self.multiplicities = multiplicities
        self.deviations = deviations

        xr = Range1d(start=220, end=-20)

        self.plot = figure(x_axis_label="ppm", x_range=xr, tools="save,reset", plot_width=self.WIDTH, plot_height=self.HEIGHT)

        self.lineSource = ColumnDataSource(data=dict(x=[0], y=[0]))
        self.plot.line('x', 'y', source=self.lineSource, line_width=2)

        # Remove grid from plot
        self.plot.xgrid.grid_line_color = None
        self.plot.ygrid.grid_line_color = None

        # Remove bokeh logo
        self.plot.toolbar.logo = None

        horizontalBoxZoomTool = HorizontalBoxZoomTool()
        self.plot.add_tools(horizontalBoxZoomTool)

        fixedZoomOutTool = FixedZoomOutTool(factor=0.4)
        self.plot.add_tools(fixedZoomOutTool)

        self.plot.extra_y_ranges['box'] = Range1d(start=0, end=0.1)
        self.selectionSource = ColumnDataSource(data=dict(left=[], right=[]), callback=CustomJS(code="""
            if ('data' in this.selected) {
                window.top.location.href = "http://localhost:8080?" + this.selected.data.query + "style=plot";
            }
        """))
        self.selectionSource.on_change('selected', lambda attr, old, new: self.delete(new['1d']['indices']))
        rect = HBar(
            left='left',
            right='right',
            y=0.5,
            height=1,
            line_alpha=0.2,
            fill_alpha=0.2,
            line_color="red",
            fill_color="red"
        )
        renderer = self.plot.add_glyph(self.selectionSource, rect)
        renderer.y_range_name = "box"

        self.labelSource = ColumnDataSource(data=dict(x=[], y=[], text=[]))
        label = Text(
            x='x',
            y='y',
            text='text',
            text_align='center',
            text_font_size="10pt"
        )
        renderer = self.plot.add_glyph(self.labelSource, label)
        renderer.y_range_name = "box"

        removeTool = CustomTapTool()
        self.plot.add_tools(removeTool)
        self.plot.toolbar.active_tap = None

        callback = CustomJS(args=dict(), code="""
            /// get BoxSelectTool dimensions from cb_data parameter of Callback
            var geometry = cb_data['geometry'];

            var shift = (geometry['x0'] + geometry['x1']) / 2;
            var deviation = Math.abs(geometry['x1'] - shift);
            var query = cb_data['query'] + "shift%5B%5D=" + shift + '&multiplicity%5B%5D=any&deviation%5B%5D=' + deviation + '&style=plot';

            window.top.location.href = "http://localhost:8080?" + query;
        """)
        selectTool = CustomBoxSelectTool(
            tool_name="Select Area",
            dimensions = "width",
            callback = callback,
            query=self.paramsToQuery()
        )
        self.plot.add_tools(selectTool)

        self.initSelect()

        self.drawButton = CustomButton(id='myButton')
        self.drawButton.on_click(self.drawPlot)

        curdoc().add_root(
            row(
                column(row(self.plot)),
                column(CustomRow(column(self.drawButton), hide=True))
            )
        )

    def paramsToQuery(self):
        query = ""
        for (shift, multiplicity, deviation) in zip(self.shifts, self.multiplicities, self.deviations):
            query += "shift%5B%5D={}&multiplicity%5B%5D={}&deviation%5B%5D={}&".format(shift, multiplicity, deviation)
        return query

    def initSelect(self):
        left, right = [], []
        labelX, labelY, labelText = [], [], []
        for (shift, multiplicity, deviation) in zip(self.shifts, self.multiplicities, self.deviations):
            left.append(shift - deviation)
            right.append(shift + deviation)

            labelX.append(shift)
            labelY.append(0.08)
            labelText.append(multiplicity)

        self.selectionSource.stream({
            'left':  left,
            'right': right
        })

        self.labelSource.stream({
            'x':    labelX,
            'y':    labelY,
            'text': labelText
        })

    def drawPlot(self, data):
        dic, _ = ng.bruker.read("../data/{}".format(data['id']))
        _, pdata = ng.bruker.read_pdata("../data/{}/pdata/1/".format(data['id']))

        udic = ng.bruker.guess_udic(dic, pdata)
        uc = ng.fileiobase.uc_from_udic(udic)
        ppmScale = uc.ppm_scale()

        self.plot.y_range = None
        self.lineSource.data = {
            'x': ppmScale,
            'y': pdata
        }

    def delete(self, ids):
        if ids:
            left = list(self.selectionSource.data['left'])
            right = list(self.selectionSource.data['right'])

            for i in ids:
                left.pop(i)
                right.pop(i)

                self.shifts.pop(i)
                self.multiplicities.pop(i)
                self.deviations.pop(i)

            self.selectionSource.data = {
                'left': left,
                'right': right
            }

            # Deselect all
            self.selectionSource.selected = {
                '0d': {'glyph': None, 'indices': []},
                '1d': {'indices': []},
                '2d': {'indices': {}},
                'data': {'query': self.paramsToQuery()}
            }

    def selectArea(self, dimensions):
        patch = {
            'left': [dimensions['x0']],
            'right': [dimensions['x1']]
        }
        self.selectionSource.stream(patch)
Пример #2
0
class Integration(Observer):
    def __init__(self, logger, spectrumId, pdata, dataSource, reference):
        Observer.__init__(self, logger)
        self.logger = logger
        self.id = spectrumId

        self.pdata = pdata
        self.dataSource = dataSource

        reference.addObserver(lambda n: referenceObserver(self, n))

        self.sources = dict()
        self.sources['integration'] = ColumnDataSource(
            data=dict(x=[], y=[], width=[], height=[]))

        self.initIntegral = None

    def create(self):

        self.sources['table'] = ColumnDataSource(
            dict(xStart=[], xStop=[], top=[], bottom=[], integral=[]))
        columns = [
            TableColumn(field="xStart",
                        title="start",
                        editor=NumberEditor(step=0.01),
                        formatter=NumberFormatter(format="0.00")),
            TableColumn(field="xStop",
                        title="stop",
                        editor=NumberEditor(step=0.01),
                        formatter=NumberFormatter(format="0.00")),
            TableColumn(field="integral",
                        title="integral",
                        editor=NumberEditor(step=0.01),
                        formatter=NumberFormatter(format="0.00"))
        ]
        self.dataTable = DataTable(source=self.sources['table'],
                                   columns=columns,
                                   reorderable=False,
                                   width=500,
                                   editable=True)
        self.sources['table'].on_change(
            'selected',
            lambda attr, old, new: self.rowSelect(new['1d']['indices']))
        self.sources['table'].on_change(
            'data', lambda attr, old, new: self.changeData(old, new))

        self.manual = CustomButton(
            label="Manual Integration",
            button_type="primary",
            width=250,
            error="Please select area using the integration tool.")
        self.manual.on_click(self.manualIntegration)

        self.createDeselectButton()
        self.createDeleteButton()

        callback = CustomJS(args=dict(button=self.manual),
                            code="""
            /// get BoxSelectTool dimensions from cb_data parameter of Callback
            var geometry = cb_data['geometry'];

            button.data = {
                x0: geometry['x0'],
                x1: geometry['x1'],
                y0: geometry['y0'],
                y1: geometry['y1']
            };

            // Callback to the backend
            button.clicks++;
        """)
        self.tool = CustomBoxSelectTool(tool_name="Integration",
                                        icon="my_icon_integration",
                                        dimensions="width",
                                        callback=callback,
                                        id="integrationTool")

    def changeData(self, old, new):
        self.checkIntegral(old, new)

        self.checkRange(old, new, 'xStart')
        self.checkRange(old, new, 'xStop')

    def checkIntegral(self, old, new):
        diff = 1
        if len(old['integral']) == len(new['integral']):
            for o, n in zip(old['integral'], new['integral']):
                if o != n:
                    diff = n / o
                    break

        if diff != 1:
            self.updateIntervals(diff, old)
            self.notifyObservers(diff)

    def updateIntervals(self, ratio, old):

        self.initIntegral /= ratio
        patch = {
            'integral': [(i, old['integral'][i] * ratio)
                         for i in xrange(len(old['integral']))]
        }
        self.sources['table'].patch(patch)

    def checkRange(self, old, new, key):
        if len(old[key]) == len(new[key]):
            for pos, o, n in zip(xrange(len(old[key])), old[key], new[key]):
                if o != n:
                    points = [
                        point
                        for (point, i) in zip(self.dataSource.data['data'],
                                              self.dataSource.data['ppm'])
                        if i <= new['xStart'][pos] and i >= new['xStop'][pos]
                    ]
                    integral = np.trapz(points, axis=0)
                    ratio = integral / self.initIntegral

                    patch = {'integral': [(pos, ratio)]}
                    self.sources['table'].patch(patch)
                    self.rowSelect([pos])

    def manualIntegration(self, dimensions):

        integral = self.calcIntegral(dimensions)

        # Update DataTable Values
        data = {
            'xStart': [dimensions['x0']],
            'xStop': [dimensions['x1']],
            'top': [dimensions['y1']],
            'bottom': [dimensions['y0']],
            'integral': [integral]
        }
        self.sources['table'].stream(data)

    def calcIntegral(self, dimensions):
        points = [
            point for (point, pos) in zip(self.dataSource.data['data'],
                                          self.dataSource.data['ppm'])
            if pos <= dimensions['x0'] and pos >= dimensions['x1']
        ]
        integral = np.trapz(points, axis=0)

        ratio = 1.0
        if self.initIntegral is None:
            self.initIntegral = integral
        else:
            ratio = integral / self.initIntegral

        return ratio

    def rowSelect(self, ids):

        maxBottom = min(
            max(self.sources['table'].data['bottom'])
            if self.sources['table'].data['bottom'] else 0, 0)
        minTop = max(
            min(self.sources['table'].data['top'])
            if self.sources['table'].data['top'] else 0, self.pdata.max())
        tempHeight = minTop - maxBottom

        x, y, width, height = [], [], [], []
        for i in ids:
            sx0 = self.sources['table'].data['xStart'][i]
            sx1 = self.sources['table'].data['xStop'][i]

            tempWidth = sx1 - sx0

            x.append(sx0 + tempWidth / 2)
            y.append(maxBottom + tempHeight / 2)
            width.append(tempWidth)
            height.append(tempHeight)

        self.sources['integration'].data = {
            'x': x,
            'y': y,
            'width': width,
            'height': height
        }

    def createDeselectButton(self):
        self.deselectButton = Button(label="Deselect all integrals",
                                     button_type="default",
                                     width=250)
        self.deselectButton.on_click(
            lambda: deselectRows(self.sources['table']))

    def createDeleteButton(self):
        self.deleteButton = Button(label="Delete selected integrals",
                                   button_type="danger",
                                   width=250)
        self.deleteButton.on_click(self.deleteIntegrals)

    def deleteIntegrals(self):
        xStart = list(self.sources['table'].data['xStart'])
        xStop = list(self.sources['table'].data['xStop'])
        top = list(self.sources['table'].data['top'])
        bottom = list(self.sources['table'].data['bottom'])
        integral = list(self.sources['table'].data['integral'])

        ids = self.sources['table'].selected['1d']['indices']
        for i in sorted(ids, reverse=True):
            try:
                xStart.pop(i)
                xStop.pop(i)
                top.pop(i)
                bottom.pop(i)
                integral.pop(i)
            except IndexError:
                pass

        # Update DataTable Values
        self.sources['table'].data = {
            'xStart': xStart,
            'xStop': xStop,
            'top': top,
            'bottom': bottom,
            'integral': integral
        }
        deselectRows(self.sources['table'])

    def draw(self, plot):
        rect = Rect(x="x",
                    y="y",
                    width='width',
                    height='height',
                    fill_alpha=0.3,
                    fill_color="#de5eff")
        plot.add_glyph(self.sources['integration'],
                       rect,
                       selection_glyph=rect,
                       nonselection_glyph=rect)

        plot.add_tools(self.tool)
Пример #3
0
class PeakPicking(Observer):

    def __init__(self, logger, spectrumId, dic, udic, pdata, dataSource, reference):
        Observer.__init__(self, logger)
        self.logger = logger
        self.id = spectrumId

        self.dic = dic
        self.udic = udic
        self.pdata = pdata
        self.mpdata = np.array(map(lambda x: -x, pdata))
        self.dataSource = dataSource

        reference.addObserver(lambda n: referenceObserver(self, n))

        self.sources = dict()
        self.sources['peaks'] = ColumnDataSource(data=dict(x=[], y=[]))

    def create(self):

        self.sources['table'] = ColumnDataSource(dict(x=[], y=[]))
        self.sources['background'] = ColumnDataSource(dict(x=[], y=[]))
        columns = [
                TableColumn(field="x", title="ppm", formatter=NumberFormatter(format="0.00")),
                TableColumn(field="y", title="y", formatter=NumberFormatter(format="0.00"))
            ]
        self.dataTable = DataTable(source=self.sources['table'], columns=columns, reorderable=False, width=500)
        self.sources['table'].on_change('selected', lambda attr, old, new: self.rowSelect(new['1d']['indices']))
        self.sources['table'].on_change('data', lambda attr, old, new: self.dataChanged(old, new))

        self.manual = CustomButton(label="Manual Peaks", button_type="success", width=500, error="Please select area using the peak picking tool.")
        self.manual.on_click(self.manualPeakPicking)

        self.peak = CustomButton(label="Peak By Peak", button_type="primary", width=250, error="Please select area using the peak by peak tool.")
        self.peak.on_click(self.peakByPeakPicking)
        self.peakTool = CustomTapTool.Create(self.peak, tapTool=PeakByPeakTapTool, auto=True, id="peakByPeakTool")

        self.createManualTool()

        self.createDeselectButton()
        self.createDeleteButton()

        self.chemicalShiftReportTitle = Div(text="<strong>Chemical Shift Report</strong>" if getLabel(self.udic) == "13C" else "")
        self.chemicalShiftReport = Paragraph(text=self.getChemicalShiftReport(), width=500)

    def createManualTool(self):
        callback = CustomJS(args=dict(button=self.manual), code="""
            /// get BoxSelectTool dimensions from cb_data parameter of Callback
            var geometry = cb_data['geometry'];

            button.data = {
                x0: geometry['x0'],
                x1: geometry['x1'],
                y:  geometry['y']
            };

            // Callback to the backend
            button.clicks++;
        """)
        self.manualTool = BothDimensionsSelectTool(
            tool_name = "Peak Picking By Threshold",
            icon = "my_icon_peak_picking",
            callback = callback,
            id = "peakPickingByThresholdTool"
        )

    def dataChanged(self, old, new):

        label = getLabel(self.udic)
        if label == "13C":
            added = [(peak, 'm') for peak in (set(new['x']) - set(old['x']))]
            removed = [(peak, 'm') for peak in (set(old['x']) - set(new['x']))]

            SpectrumDB.RemovePeaks(self.id, removed)
            SpectrumDB.AddPeaks(self.id, added)

        # Update Chemical Shift Report
        self.updateChemicalShiftReport()

    def updateChemicalShiftReport(self):
        self.chemicalShiftReport.text = self.getChemicalShiftReport()

    def getChemicalShiftReport(self):
        label = getLabel(self.udic)
        if label == "13C":
            return getMetadata(self.dic, self.udic) + " δ " + ", ".join("{:0.2f}".format(x) for x in [round(x, 2) for x in self.sources['table'].data['x']]) + "."
        else:
            return ""

    def createDeselectButton(self):
        self.deselectButton = Button(label="Deselect all peaks", button_type="default", width=250)
        self.deselectButton.on_click(lambda: deselectRows(self.sources['table']))

    def createDeleteButton(self):
        self.ids = []
        self.deleteButton = Button(label="Delete selected peaks", button_type="danger", width=250)
        self.deleteButton.on_click(self.deletePeaks)

    def deletePeaks(self):
        self.sources['peaks'].data = dict(x=[], y=[])

        newX = list(self.sources['table'].data['x'])
        newY = list(self.sources['table'].data['y'])

        ids = self.sources['table'].selected['1d']['indices']
        for i in sorted(ids, reverse=True):
            try:
                newX.pop(i)
                newY.pop(i)
            except IndexError:
                pass

        self.sources['table'].data = {
            'x': list(newX),
            'y': list(newY)
        }
        self.sources['background'].data = {
            'x': list(newX),
            'y': list(newY)
        }
        deselectRows(self.sources['table'])
        self.notifyObservers()

    def manualPeakPicking(self, dimensions, notify=True):

        # Positive Peaks
        self.peaksIndices = list(self.manualPeakPickingOnData(self.pdata, dimensions))
        # Negative Peaks
        self.peaksIndices.extend(self.manualPeakPickingOnData(self.mpdata, dimensions))
        # Sort Peaks
        self.peaksIndices = sorted(self.peaksIndices, reverse=True)

        if len(self.peaksIndices) > 0:
            self.updateDataValues({
                'x': [self.dataSource.data['ppm'][i] for i in self.peaksIndices],
                'y': [self.pdata[i] for i in self.peaksIndices]
            })
            if notify:
                self.notifyObservers()

    def manualPeakPickingOnData(self, data, dimensions):

        threshold = abs(dimensions['y'])
        if data.max() < threshold:
            return []

        peaks = ng.peakpick.pick(data, abs(dimensions['y']), algorithm="downward")

        peaksIndices = [int(peak[0]) for peak in peaks]
        # Filter left
        peaksIndices = [i for i in peaksIndices if self.dataSource.data['ppm'][i] <= dimensions['x0']]
        # Filter right
        peaksIndices = [i for i in peaksIndices if self.dataSource.data['ppm'][i] >= dimensions['x1']]
        return peaksIndices

    def peakByPeakPicking(self, dimensions):

        self.updateDataValues({
            'x': [dimensions['x']],
            'y': [dimensions['y']]
        })
        self.notifyObservers()

    def updateDataValues(self, data):
        # Update DataTable Values
        newData = list(OrderedDict.fromkeys(
            zip(
                self.sources['table'].data['x'] + data['x'],
                self.sources['table'].data['y'] + data['y']
            )
        ))
        newX, newY = zip(*sorted(newData, reverse=True))
        self.sources['table'].data = {
            'x': list(newX),
            'y': list(newY)
        }

        self.sources['background'].data = {
            'x': list(newX),
            'y': list(newY)
        }

    def selectByPPM(self, peaks):
        self.sources['table'].selected = {
            '0d': {'glyph': None, 'indices': []},
            '1d': {'indices': [self.sources['table'].data['x'].index(peak) for peak in peaks]},
            '2d': {'indices': {}}
        }

    def rowSelect(self, ids):
        self.sources['peaks'].data = {
            'x': [self.sources['table'].data['x'][i] for i in ids],
            'y': [self.sources['table'].data['y'][i] for i in ids]
        }

    def getPeaksInSpace(self, start, stop):
        return [y for x, y in zip(self.sources['table'].data['x'], self.sources['table'].data['y']) if x <= start and x >= stop]

    def getPPMInSpace(self, start, stop):
        return [x for x in self.sources['table'].data['x'] if x <= start and x >= stop]

    def draw(self, plot):

        peak = Circle(
            x="x",
            y="y",
            size=10,
            line_color="#C0C0C0",
            fill_color="#C0C0C0",
            line_width=1
        )
        plot.add_glyph(self.sources['background'], peak, selection_glyph=peak, nonselection_glyph=peak)

        selected = Circle(
            x="x",
            y="y",
            size=10,
            line_color="#ff0000",
            fill_color="#ff0000",
            line_width=1
        )
        plot.add_glyph(self.sources['peaks'], selected, selection_glyph=selected, nonselection_glyph=selected)

        self.manualTool.addToPlot(plot)

        plot.add_tools(self.peakTool)
Пример #4
0
class MultipletAnalysis:

    MULTIPLET_ERROR = 1000000
    MULTIPLETS = {
        's': {
            'table': [1],
            'sum': 1,
            'j': []
        },
        'd': {
            'table': [1, 1],
            'sum': 2,
            'j': [[0, 1]]
        },
        't': {
            'table': [1, 2, 1],
            'sum': 3,
            'j': [[0, 1]]
        },
        'q': {
            'table': [1, 3, 3, 1],
            'sum': 4,
            'j': [[0, 1]]
        },
        'p': {
            'table': [1, 4, 6, 4, 1],
            'sum': 5,
            'j': [[0, 1]]
        },
        'h': {
            'table': [1, 5, 10, 10, 5, 1],
            'sum': 6,
            'j': [[0, 1]]
        },
        'hept': {
            'table': [1, 6, 15, 20, 15, 6, 1],
            'sum': 7,
            'j': [[0, 1]]
        },
        'dd': {
            'table': [[1, 1], [1, 1]],
            'sum': 4,
            'j': [[0, 1], [0, 2]]
        },
        'ddd': {
            'table': [[1, 1], [1, 1], [1, 1], [1, 1]],
            'sum': 8,
            'j': [[0, 1], [0, 2], [0, 4]]
        },
        'dt': {
            'table': [[1, 2, 1], [1, 2, 1]],
            'sum': 6,
            'j': [[0, 1], [0, 3]]
        },
        'td': {
            'table': [1, 1, 2, 2, 1, 1],
            'sum': 6,
            'j': [[0, 1], [0, 2]]
        },
        'ddt': {
            'table': [[1, 2, 1], [1, 2, 1], [1, 2, 1], [1, 2, 1]],
            'sum': 12,
            'j': [[0, 1], [0, 3], [0, 6]]
        }
    }

    def __init__(self, logger, spectrumId, dic, udic, pdata, dataSource,
                 peakPicking, integration, reference):
        self.logger = logger
        self.id = spectrumId

        self.dic = dic
        self.udic = udic
        self.pdata = pdata
        self.dataSource = dataSource

        self.peakPicking = peakPicking
        peakPicking.addObserver(self.recalculateAllMultipletsForPeaks)

        self.integration = integration
        self.integration.addObserver(lambda ratio: self.updateIntervals(
            ratio, self.sources['table'].data['integral']))

        reference.addObserver(lambda n: referenceObserver(self, n))

        self.sources = dict()

    def create(self):

        self.oldData = dict(peaks=[], classes=[])
        self.sources['table'] = ColumnDataSource(
            dict(xStart=[],
                 xStop=[],
                 name=[],
                 classes=[],
                 j=[],
                 h=[],
                 integral=[],
                 peaks=[],
                 top=[],
                 bottom=[]))
        columns = [
            TableColumn(field="xStart",
                        title="start",
                        formatter=NumberFormatter(format="0.00")),
            TableColumn(field="xStop",
                        title="stop",
                        formatter=NumberFormatter(format="0.00")),
            TableColumn(field="name", title="Name"),
            TableColumn(field="classes", title="Class"),
            TableColumn(field="j", title="J"),
            TableColumn(field="h",
                        title="H",
                        formatter=NumberFormatter(format="0")),
            TableColumn(field="integral",
                        title="Integral",
                        formatter=NumberFormatter(format="0.00"))
        ]
        self.dataTable = DataTable(source=self.sources['table'],
                                   columns=columns,
                                   reorderable=False,
                                   width=500)
        self.sources['table'].on_change(
            'selected',
            lambda attr, old, new: self.rowSelect(new['1d']['indices']))
        self.sources['table'].on_change(
            'data', lambda attr, old, new: self.dataChanged(new))

        self.manual = CustomButton(
            label="Multiplet Analysis",
            button_type="primary",
            width=250,
            error="Please select area using the multiplet analysis tool.")
        self.manual.on_click(self.manualMultipletAnalysis)

        self.createTool()

        self.title = Div(text="<strong>Edit Multiplet:</strong>", width=500)

        self.classes = Select(title="Class:",
                              options=[
                                  "m", "s", "d", "t", "q", "p", "h", "hept",
                                  "dd", "ddd", "dt", "td", "ddt"
                              ],
                              width=100,
                              disabled=True)
        self.classes.on_change(
            'value', lambda attr, old, new: self.manualChange('classes', new))

        self.integral = TextInput(title="Integral:",
                                  value="",
                                  placeholder="Integral",
                                  width=175,
                                  disabled=True)
        self.integral.on_change(
            'value', lambda attr, old, new: self.changeIntegral(new))

        self.j = TextInput(title='J-list:', value="", width=175, disabled=True)
        self.j.on_change('value',
                         lambda attr, old, new: self.manualChange('j', new))

        self.delete = Button(label="Delete Multiplet",
                             button_type="danger",
                             width=500,
                             disabled=True)
        self.delete.on_click(self.deleteMultiplet)

        self.reportTitle = Div(text="<strong>Multiplet Report:</strong>")
        self.report = Paragraph(width=500)

    def createTool(self):
        callback = CustomJS(args=dict(button=self.manual),
                            code="""
            /// get BoxSelectTool dimensions from cb_data parameter of Callback
            var geometry = cb_data['geometry'];

            button.data = {
                x0: geometry['x0'],
                x1: geometry['x1'],
                y:  geometry['y'],
                y0: geometry['y0'],
                y1: geometry['y1']
            };

            // Callback to the backend
            button.clicks++;
        """)
        self.tool = BothDimensionsSelectTool(tool_name="Multiplet Analysis",
                                             icon="my_icon_multiplet_analysis",
                                             callback=callback,
                                             id="multipletAnalysisTool")

    def rowSelect(self, ids):

        if len(ids) == 1:
            self.selected = ids[0]

            # Enable options
            self.classes.disabled = False
            self.classes.value = self.sources['table'].data['classes'][
                self.selected]

            self.integral.disabled = False
            self.integral.value = str(
                self.sources['table'].data['integral'][self.selected])

            self.j.disabled = False
            self.j.value = self.sources['table'].data['j'][self.selected]

            self.delete.disabled = False

            self.peakPicking.selectByPPM(
                self.sources['table'].data['peaks'][self.selected])
        else:
            deselectRows(self.sources['table'])

    def recalculateAllMultipletsForPeaks(self):
        data = self.sources['table'].data

        patch = dict(classes=[], j=[])
        for pos, start, stop in zip(range(len(data['xStart'])), data['xStart'],
                                    data['xStop']):
            ppm = self.peakPicking.getPPMInSpace(start, stop)
            peaks = self.peakPicking.getPeaksInSpace(start, stop)

            multiplet = self.predictMultiplet(peaks)

            patch['classes'].append((pos, multiplet))
            patch['j'].append((pos, self.calcJ(ppm, multiplet)))
        self.sources['table'].patch(patch)

    def manualMultipletAnalysis(self, dimensions):

        self.peakPicking.manualPeakPicking(dimensions, notify=False)
        # Check if empty
        if not self.peakPicking.peaksIndices:
            return

        integral = round(self.integration.calcIntegral(dimensions), 3)

        peaks = [self.pdata[i] for i in self.peakPicking.peaksIndices]
        multiplet = self.predictMultiplet(peaks)

        ppm = sorted([
            self.dataSource.data['ppm'][i]
            for i in self.peakPicking.peaksIndices
        ])
        data = {
            'xStart': [dimensions['x0']],
            'xStop': [dimensions['x1']],
            'name': [
                'A' if not self.sources['table'].data['name'] else
                chr(ord(self.sources['table'].data['name'][-1]) + 1)
            ],
            'classes': [multiplet],
            'j': [self.calcJ(ppm, multiplet)],
            'h': [round(integral)],
            'integral': [integral],
            'peaks': [ppm],
            'top': [dimensions['y1']],
            'bottom': [dimensions['y0']]
        }

        # Add to DataTable
        self.sources['table'].stream(data)

        # Select the multiplet in the table
        self.sources['table'].selected = {
            '0d': {
                'glyph': None,
                'indices': []
            },
            '1d': {
                'indices': [len(self.sources['table'].data['xStart']) - 1]
            },
            '2d': {
                'indices': {}
            }
        }

    def calcJ(self, ppm, multiplet):
        if multiplet in self.MULTIPLETS:
            js = self.MULTIPLETS[multiplet]['j']

            calc = sorted([
                round(abs(ppm[j[0]] - ppm[j[1]]) * getFrequency(self.udic), 1)
                for j in js
            ],
                          reverse=True)
            return ', '.join(str(j) for j in calc) + ' Hz'
        return ""

    def predictMultiplet(self, peaks):

        for key, value in self.MULTIPLETS.iteritems():
            if len(peaks) == value['sum'] and self.checkMultiplet(
                    value['table'], peaks):
                return key

        return "m"

    def checkMultiplet(self, multiplet, peaks):

        if not multiplet:
            return True

        # check list
        if isinstance(multiplet[0], list):
            return self.checkMultiplet(multiplet[0],
                                       peaks) and self.checkMultiplet(
                                           multiplet[1:], peaks)
        else:
            return self.checkMultiplicity(multiplet, peaks)

    def checkMultiplicity(self, multiplet, peaks):

        if not multiplet:
            return True

        for (m, peak) in zip(multiplet[1:], peaks[1:]):

            low = m * peaks[0] - self.MULTIPLET_ERROR
            high = m * peaks[0] + self.MULTIPLET_ERROR

            if peak < low or peak > high:
                return False

        return True

    def dataChanged(self, data):

        self.updateMultipletReport()

        label = getLabel(self.udic)
        if label == "1H":
            newData = [(np.median(peaks), c)
                       for (peaks, c) in zip(data['peaks'], data['classes'])]
            oldData = [
                (np.median(peaks), c)
                for (peaks,
                     c) in zip(self.oldData['peaks'], self.oldData['classes'])
            ]

            added = list(set(newData) - set(oldData))
            removed = list(set(oldData) - set(newData))

            SpectrumDB.RemovePeaks(self.id, removed)
            SpectrumDB.AddPeaks(self.id, added)

            self.oldData = {
                'peaks': [i[0] for i in newData],
                'classes': [i[1] for i in newData]
            }

    def updateMultipletReport(self):
        label = getLabel(self.udic)

        text = ""
        if label == "1H":
            data = self.sources['table'].data
            text = getMetadata(self.dic, self.udic) + " δ = " + ", ".join(
                ("{:0.2f}".format(np.median(peaks)) if classes != 'm' else
                 "{:0.2f}-{:0.2f}".format(peaks[-1], peaks[0])) +
                " ({}, ".format(classes) +
                ("J={}, ".format(j) if classes != 'm' and classes != 's' else
                 "") + "{:d}H)".format(int(h))
                for (peaks, classes, j, h) in sorted(
                    zip(data['peaks'], data['classes'], data['j'], data['h']),
                    reverse=True) if h > 0) + "."

        self.report.text = text

    def manualChange(self, key, new):
        if self.sources['table'].data[key][self.selected] != new:
            patch = {key: [(self.selected, new)]}
            self.sources['table'].patch(patch)

    def changeIntegral(self, new):
        try:
            new = float(new)

            data = self.sources['table'].data['integral']

            old = data[self.selected]
            ratio = new / old

            self.updateIntervals(ratio, data)

            self.integration.updateIntervals(
                ratio, self.integration.sources['table'].data)
        except:
            pass

    def updateIntervals(self, ratio, data):
        h, integral = [], []
        for pos, val in zip(xrange(len(data)), data):
            newIntegral = val * ratio
            h.append((pos, round(newIntegral)))
            integral.append((pos, newIntegral))
        self.sources['table'].patch(dict(h=h, integral=integral))

    def deleteMultiplet(self):

        xStart = list(self.sources['table'].data['xStart'])
        xStop = list(self.sources['table'].data['xStop'])
        name = list(self.sources['table'].data['name'])
        classes = list(self.sources['table'].data['classes'])
        j = list(self.sources['table'].data['j'])
        h = list(self.sources['table'].data['h'])
        integral = list(self.sources['table'].data['integral'])
        peaks = list(self.sources['table'].data['peaks'])
        top = list(self.sources['table'].data['top'])
        bottom = list(self.sources['table'].data['bottom'])

        xStart.pop(self.selected)
        xStop.pop(self.selected)
        name.pop(self.selected)
        classes.pop(self.selected)
        j.pop(self.selected)
        h.pop(self.selected)
        integral.pop(self.selected)
        peaks.pop(self.selected)
        top.pop(self.selected)
        bottom.pop(self.selected)

        self.sources['table'].data = {
            'xStart': xStart,
            'xStop': xStop,
            'name': name,
            'classes': classes,
            'j': j,
            'h': h,
            'integral': integral,
            'peaks': peaks,
            'top': top,
            'bottom': bottom
        }
        deselectRows(self.sources['table'])

        self.disableOptions()

    def disableOptions(self):
        self.classes.disabled = True
        self.integral.disabled = True
        self.j.disabled = True
        self.delete.disabled = True

    def draw(self, plot):
        self.tool.addToPlot(plot)