[
  {
    "path": ".gitignore",
    "content": "/.idea\n/venv*/\n\n*.pyc\n*.egg-info\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# Pandas DataFrame GUI\n\nA minimalistic GUI for analyzing Pandas DataFrames based on wxPython.\n\n**Update:** I'm currently working on a successor [tabloo](https://github.com/bluenote10/tabloo) which avoids native dependencies and offers a more modern user interface.\n\n## Usage\n\n```python\nimport dfgui\ndfgui.show(df)\n```\n\n## Features\n\n- Tabular view of data frame\n- Columns are sortable (by clicking column header)\n- Columns can be enabled/disabled (left click on 'Columns' tab)\n- Columns can be rearranged (right click drag on 'Columns' tab)\n- Generic filtering: Write arbitrary Python expression to filter rows. *Warning:* Uses Python's `eval` -- use with care.\n- Histogram plots\n- Scatter plots\n\n## Demo & Docs\n\nThe default view: Nothing fancy, just scrolling and sorting. The value of cell can be copied to clipboard by right clicking on a cell.\n\n![screen1](/../screenshots/screenshots/screen1.png)\n\nThe column selection view: Left clicking enables or disables a column in the data frame view. Columns can be dragged with a right click to rearrange them.\n\n![screen2](/../screenshots/screenshots/screen2.png)\n\nThe filter view: Allows to write arbitrary Pandas selection expressions. The syntax is: An underscore `_` will be replaced by the corresponding data frame column. That is, setting the combo box to a column named \"A\" and adding the condition `_ == 1` would result in an expression like `df[df[\"A\"] == 1, :]`. The following example filters the data frame to rows which have the value 669944 in column \"UserID\" and `datetime.date` value between 2016-01-01 and 2016-03-01.\n\n![screen3](/../screenshots/screenshots/screen3.png)\n\nHistogram view:\n\n![screen4](/../screenshots/screenshots/screen4.png)\n\nScatter plot view:\n\n![screen5](/../screenshots/screenshots/screen5.png)\n\n## Requirements\n\nSince wxPython is not pip-installable, dfgui does not handle dependencies automatically. You have to make sure the following packages are installed:\n\n- pandas/numpy\n- matplotlib\n- wx\n\n## Installation Instructions\n\nI haven't submitted dfgui to PyPI (yet), but you can install directly from git (having met all requirements). For instance:\n\n```bash\npip install git+https://github.com/bluenote10/PandasDataFrameGUI\n```\n\nor if you prefer a regular git clone:\n\n```bash\ngit clone git@github.com:bluenote10/PandasDataFrameGUI.git dfgui\ncd dfgui\npip install -e .\n# and to check if everything works:\n./demo.py\n```\n\nIn fact, dfgui only consists of a single module, so you might as well just download the file [`dfgui/dfgui.py`](dfgui/dfgui.py).\n\n### Anaconda/Windows Instructions\n\nInstall wxpython through conda or the Anaconda GUI.\n\n\"Open terminal\" in the Anaconda GUI environment.\n\n```bash\ngit clone \"https://github.com/bluenote10/PandasDataFrameGUI.git\"\ncd dfgui\npip install -e .\nconda package --pkg-name=dfgui --pkg-version=0.1 # this should create a package file\nconda install --offline dfgui-0.1-py27_0.tar.bz2 # this should install into your conda environment\n```\nThen restart your Jupyter kernel.\n\n"
  },
  {
    "path": "demo.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8\n\nfrom __future__ import absolute_import, division, print_function\n\n\"\"\"\nIf you are getting wx related import errors when running in a virtualenv:\nEither make sure that the virtualenv has been created using\n`virtualenv --system-site-packages venv` or manually add the wx library\npath (e.g. /usr/lib/python2.7/dist-packages/wx-2.8-gtk2-unicode) to the\npython path.\n\"\"\"\n\nimport datetime\nimport pandas as pd\nimport numpy as np\nimport dfgui\n\n\ndef create_dummy_data(size):\n\n    user_ids = np.random.randint(1, 1000000, 10)\n    product_ids = np.random.randint(1, 1000000, 100)\n\n    def choice(*values):\n        return np.random.choice(values, size)\n\n    random_dates = [\n        datetime.date(2016, 1, 1) + datetime.timedelta(days=int(delta))\n        for delta in np.random.randint(1, 50, size)\n    ]\n\n    return pd.DataFrame.from_items([\n        (\"Date\", random_dates),\n        (\"UserID\", choice(*user_ids)),\n        (\"ProductID\", choice(*product_ids)),\n        (\"IntColumn\", choice(1, 2, 3)),\n        (\"FloatColumn\", choice(np.nan, 1.0, 2.0, 3.0)),\n        (\"StringColumn\", choice(\"A\", \"B\", \"C\")),\n        (\"Gaussian 1\", np.random.normal(0, 1, size)),\n        (\"Gaussian 2\", np.random.normal(0, 1, size)),\n        (\"Uniform\", np.random.uniform(0, 1, size)),\n        (\"Binomial\", np.random.binomial(20, 0.1, size)),\n        (\"Poisson\", np.random.poisson(1.0, size)),\n    ])\n\ndf = create_dummy_data(1000)\n\ndfgui.show(df)\n"
  },
  {
    "path": "dfgui/__init__.py",
    "content": "from __future__ import absolute_import\n\nfrom dfgui.dfgui import show\n\n__all__ = [\n    \"show\"\n]\n"
  },
  {
    "path": "dfgui/dfgui.py",
    "content": "#!/usr/bin/env python\n# -*- encoding: utf-8\n\nfrom __future__ import absolute_import, division, print_function\n\ntry:\n    import wx\nexcept ImportError:\n    import sys\n    sys.path += [\n        \"/usr/lib/python2.7/dist-packages/wx-2.8-gtk2-unicode\",\n        \"/usr/lib/python2.7/dist-packages\"\n    ]\n    import wx\n\nimport matplotlib\nmatplotlib.use('WXAgg')\nfrom matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas\nfrom matplotlib.backends.backend_wx import NavigationToolbar2Wx\nfrom matplotlib.figure import Figure\nfrom bisect import bisect\n\nimport numpy as np\nimport pandas as pd\n\n# unused import required to allow 'eval' of date filters\nimport datetime\nfrom datetime import date\n\n# try to get nicer plotting styles\ntry:\n    import seaborn\n    seaborn.set()\nexcept ImportError:\n    try:\n        from matplotlib import pyplot as plt\n        plt.style.use('ggplot')\n    except AttributeError:\n        pass\n\n\nclass ListCtrlDataFrame(wx.ListCtrl):\n\n    # TODO: we could do something more sophisticated to come\n    # TODO: up with a reasonable column width...\n    DEFAULT_COLUMN_WIDTH = 100\n    TMP_SELECTION_COLUMN = 'tmp_selection_column'\n\n    def __init__(self, parent, df, status_bar_callback):\n        wx.ListCtrl.__init__(\n            self, parent, -1,\n            style=wx.LC_REPORT | wx.LC_VIRTUAL | wx.LC_HRULES | wx.LC_VRULES | wx.LB_MULTIPLE\n        )\n        self.status_bar_callback = status_bar_callback\n\n        self.df_orig = df\n        self.original_columns = self.df_orig.columns[:]\n        if isinstance(self.original_columns,(pd.RangeIndex,pd.Int64Index)):\n            # RangeIndex is not supported by self._update_columns\n            self.original_columns = pd.Index([str(i) for i in self.original_columns])\n        self.current_columns = self.df_orig.columns[:]\n\n        self.sort_by_column = None\n\n        self._reset_mask()\n\n        # prepare attribute for alternating colors of rows\n        self.attr_light_blue = wx.ListItemAttr()\n        self.attr_light_blue.SetBackgroundColour(\"#D6EBFF\")\n\n        self.Bind(wx.EVT_LIST_COL_CLICK, self._on_col_click)\n        self.Bind(wx.EVT_RIGHT_DOWN, self._on_right_click)\n\n        self.df = pd.DataFrame({})  # init empty to force initial update\n        self._update_rows()\n        self._update_columns(self.original_columns)\n\n    def _reset_mask(self):\n        #self.mask = [True] * self.df_orig.shape[0]\n        self.mask = pd.Series([True] * self.df_orig.shape[0], index=self.df_orig.index)\n\n    def _update_columns(self, columns):\n        self.ClearAll()\n        for i, col in enumerate(columns):\n            self.InsertColumn(i, col)\n            self.SetColumnWidth(i, self.DEFAULT_COLUMN_WIDTH)\n        # Note that we have to reset the count as well because ClearAll()\n        # not only deletes columns but also the count...\n        self.SetItemCount(len(self.df))\n\n    def set_columns(self, columns_to_use):\n        \"\"\"\n        External interface to set the column projections.\n        \"\"\"\n        self.current_columns = columns_to_use\n        self._update_rows()\n        self._update_columns(columns_to_use)\n\n    def _update_rows(self):\n        old_len = len(self.df)\n        self.df = self.df_orig.loc[self.mask.values, self.current_columns]\n        new_len = len(self.df)\n        if old_len != new_len:\n            self.SetItemCount(new_len)\n            self.status_bar_callback(0, \"Number of rows: {}\".format(new_len))\n\n    def apply_filter(self, conditions):\n        \"\"\"\n        External interface to set a filter.\n        \"\"\"\n        old_mask = self.mask.copy()\n\n        if len(conditions) == 0:\n            self._reset_mask()\n\n        else:\n            self._reset_mask()  # set all to True for destructive conjunction\n\n            no_error = True\n            for column, condition in conditions:\n                if condition.strip() == '':\n                    continue\n                condition = condition.replace(\"_\", \"self.df_orig['{}']\".format(column))\n                print(\"Evaluating condition:\", condition)\n                try:\n                    tmp_mask = eval(condition)\n                    if isinstance(tmp_mask, pd.Series) and tmp_mask.dtype == np.bool:\n                        self.mask &= tmp_mask\n                except Exception as e:\n                    print(\"Failed with:\", e)\n                    no_error = False\n                    self.status_bar_callback(\n                        1,\n                        \"Evaluating '{}' failed with: {}\".format(condition, e)\n                    )\n\n            if no_error:\n                self.status_bar_callback(1, \"\")\n\n        has_changed = any(old_mask != self.mask)\n        if has_changed:\n            self._update_rows()\n\n        return len(self.df), has_changed\n\n    def get_selected_items(self):\n        \"\"\"\n        Gets the selected items for the list control.\n        Selection is returned as a list of selected indices,\n        low to high.\n        \"\"\"\n        selection = []\n        current = -1    # start at -1 to get the first selected item\n        while True:\n            next = self.GetNextItem(current, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)\n            if next == -1:\n                return selection\n            else:\n                selection.append(next)\n                current = next\n\n    def get_filtered_df(self):\n        return self.df_orig.loc[self.mask, :]\n\n    def _on_col_click(self, event):\n        \"\"\"\n        Sort data frame by selected column.\n        \"\"\"\n        # get currently selected items\n        selected = self.get_selected_items()\n\n        # append a temporary column to store the currently selected items\n        self.df[self.TMP_SELECTION_COLUMN] = False\n        self.df.iloc[selected, -1] = True\n\n        # get column name to use for sorting\n        col = event.GetColumn()\n\n        # determine if ascending or descending\n        if self.sort_by_column is None or self.sort_by_column[0] != col:\n            ascending = True\n        else:\n            ascending = not self.sort_by_column[1]\n\n        # store sort column and sort direction\n        self.sort_by_column = (col, ascending)\n\n        try:\n            # pandas 0.17\n            self.df.sort_values(self.df.columns[col], inplace=True, ascending=ascending)\n        except AttributeError:\n            # pandas 0.16 compatibility\n            self.df.sort(self.df.columns[col], inplace=True, ascending=ascending)\n\n        # deselect all previously selected\n        for i in selected:\n            self.Select(i, on=False)\n\n        # determine indices of selection after sorting\n        selected_bool = self.df.iloc[:, -1] == True\n        selected = self.df.reset_index().index[selected_bool]\n\n        # select corresponding rows\n        for i in selected:\n            self.Select(i, on=True)\n\n        # delete temporary column\n        del self.df[self.TMP_SELECTION_COLUMN]\n\n    def _on_right_click(self, event):\n        \"\"\"\n        Copies a cell into clipboard on right click. Unfortunately,\n        determining the clicked column is not straightforward. This\n        appraoch is inspired by the TextEditMixin in:\n        /usr/lib/python2.7/dist-packages/wx-2.8-gtk2-unicode/wx/lib/mixins/listctrl.py\n        More references:\n        - http://wxpython-users.1045709.n5.nabble.com/Getting-row-col-of-selected-cell-in-ListCtrl-td2360831.html\n        - https://groups.google.com/forum/#!topic/wxpython-users/7BNl9TA5Y5U\n        - https://groups.google.com/forum/#!topic/wxpython-users/wyayJIARG8c\n        \"\"\"\n        if self.HitTest(event.GetPosition()) != wx.NOT_FOUND:\n            x, y = event.GetPosition()\n            row, flags = self.HitTest((x, y))\n\n            col_locs = [0]\n            loc = 0\n            for n in range(self.GetColumnCount()):\n                loc = loc + self.GetColumnWidth(n)\n                col_locs.append(loc)\n\n            scroll_pos = self.GetScrollPos(wx.HORIZONTAL)\n            # this is crucial step to get the scroll pixel units\n            unit_x, unit_y = self.GetMainWindow().GetScrollPixelsPerUnit()\n\n            col = bisect(col_locs, x + scroll_pos * unit_x) - 1\n\n            value = self.df.iloc[row, col]\n            # print(row, col, scroll_pos, value)\n\n            clipdata = wx.TextDataObject()\n            clipdata.SetText(str(value))\n            wx.TheClipboard.Open()\n            wx.TheClipboard.SetData(clipdata)\n            wx.TheClipboard.Close()\n\n    def OnGetItemText(self, item, col):\n        \"\"\"\n        Implements the item getter for a \"virtual\" ListCtrl.\n        \"\"\"\n        value = self.df.iloc[item, col]\n        # print(\"retrieving %d %d %s\" % (item, col, value))\n        return str(value)\n\n    def OnGetItemAttr(self, item):\n        \"\"\"\n        Implements the attribute getter for a \"virtual\" ListCtrl.\n        \"\"\"\n        if item % 2 == 0:\n            return self.attr_light_blue\n        else:\n            return None\n\n\nclass DataframePanel(wx.Panel):\n    \"\"\"\n    Panel providing the main data frame table view.\n    \"\"\"\n    def __init__(self, parent, df, status_bar_callback):\n        wx.Panel.__init__(self, parent)\n\n        self.df_list_ctrl = ListCtrlDataFrame(self, df, status_bar_callback)\n\n        sizer = wx.BoxSizer(wx.VERTICAL)\n        sizer.Add(self.df_list_ctrl, 1, wx.ALL | wx.EXPAND | wx.GROW, 5)\n        self.SetSizer(sizer)\n        self.Show()\n\n\nclass ListBoxDraggable(wx.ListBox):\n    \"\"\"\n    Helper class to provide ListBox with extended behavior.\n    \"\"\"\n    def __init__(self, parent, size, data, *args, **kwargs):\n\n        wx.ListBox.__init__(self, parent, size, **kwargs)\n\n        if isinstance(data,(pd.RangeIndex,pd.Int64Index)):\n            # RangeIndex is not supported by self._update_columns\n            data = pd.Index([str(i) for i in data])\n        self.data = data\n\n        self.InsertItems(self.data, 0)\n\n        self.Bind(wx.EVT_LISTBOX, self.on_selection_changed)\n\n        self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)\n\n        self.Bind(wx.EVT_RIGHT_DOWN, self.on_right_down)\n        self.Bind(wx.EVT_RIGHT_UP, self.on_right_up)\n        self.Bind(wx.EVT_MOTION, self.on_move)\n\n        self.index_iter = range(len(self.data))\n\n        self.selected_items = [True] * len(self.data)\n        self.index_mapping = list(range(len(self.data)))\n\n        self.drag_start_index = None\n\n        self.update_selection()\n        self.SetFocus()\n\n    def on_left_down(self, event):\n        if self.HitTest(event.GetPosition()) != wx.NOT_FOUND:\n            index = self.HitTest(event.GetPosition())\n            self.selected_items[index] = not self.selected_items[index]\n            # doesn't really work to update selection direclty (focus issues)\n            # instead we wait for the EVT_LISTBOX event and fix the selection\n            # there...\n            # self.update_selection()\n            # TODO: we could probably use wx.CallAfter\n        event.Skip()\n\n    def update_selection(self):\n        # self.SetFocus()\n        # print(self.selected_items)\n        for i in self.index_iter:\n            if self.IsSelected(i) and not self.selected_items[i]:\n                #print(\"Deselecting\", i)\n                self.Deselect(i)\n            elif not self.IsSelected(i) and self.selected_items[i]:\n                #print(\"Selecting\", i)\n                self.Select(i)\n\n    def on_selection_changed(self, evt):\n        self.update_selection()\n        evt.Skip()\n\n    def on_right_down(self, event):\n        if self.HitTest(event.GetPosition()) != wx.NOT_FOUND:\n            index = self.HitTest(event.GetPosition())\n            self.drag_start_index = index\n\n    def on_right_up(self, event):\n        self.drag_start_index = None\n        event.Skip()\n\n    def on_move(self, event):\n        if self.drag_start_index is not None:\n            if self.HitTest(event.GetPosition()) != wx.NOT_FOUND:\n                index = self.HitTest(event.GetPosition())\n                if self.drag_start_index != index:\n                    self.swap(self.drag_start_index, index)\n                    self.drag_start_index = index\n\n    def swap(self, i, j):\n        self.index_mapping[i], self.index_mapping[j] = self.index_mapping[j], self.index_mapping[i]\n        self.SetString(i, self.data[self.index_mapping[i]])\n        self.SetString(j, self.data[self.index_mapping[j]])\n        self.selected_items[i], self.selected_items[j] = self.selected_items[j], self.selected_items[i]\n        # self.update_selection()\n        # print(\"Updated mapping:\", self.index_mapping)\n        new_event = wx.PyCommandEvent(wx.EVT_LISTBOX.typeId, self.GetId())\n        self.GetEventHandler().ProcessEvent(new_event)\n\n    def get_selected_data(self):\n        selected = []\n        for i, col in enumerate(self.data):\n            if self.IsSelected(i):\n                index = self.index_mapping[i]\n                value = self.data[index]\n                selected.append(value)\n        # print(\"Selected data:\", selected)\n        return selected\n\n\nclass ColumnSelectionPanel(wx.Panel):\n    \"\"\"\n    Panel for selecting and re-arranging columns.\n    \"\"\"\n    def __init__(self, parent, columns, df_list_ctrl):\n        wx.Panel.__init__(self, parent)\n\n        self.columns = columns\n        self.df_list_ctrl = df_list_ctrl\n\n        self.list_box = ListBoxDraggable(self, -1, columns, style=wx.LB_EXTENDED)\n        self.Bind(wx.EVT_LISTBOX, self.update_selected_columns)\n\n        sizer = wx.BoxSizer(wx.VERTICAL)\n        sizer.Add(self.list_box, 1, wx.ALL | wx.EXPAND | wx.GROW, 5)\n        self.SetSizer(sizer)\n        self.list_box.SetFocus()\n\n    def update_selected_columns(self, evt):\n        selected = self.list_box.get_selected_data()\n        self.df_list_ctrl.set_columns(selected)\n\n\nclass FilterPanel(wx.Panel):\n    \"\"\"\n    Panel for defining filter expressions.\n    \"\"\"\n    def __init__(self, parent, columns, df_list_ctrl, change_callback):\n        wx.Panel.__init__(self, parent)\n\n        columns_with_neutral_selection = [''] + list(columns)\n        self.columns = columns\n        self.df_list_ctrl = df_list_ctrl\n        self.change_callback = change_callback\n\n        self.num_filters = 10\n\n        self.main_sizer = wx.BoxSizer(wx.VERTICAL)\n\n        self.combo_boxes = []\n        self.text_controls = []\n\n        for i in range(self.num_filters):\n            combo_box = wx.ComboBox(self, choices=columns_with_neutral_selection, style=wx.CB_READONLY)\n            text_ctrl = wx.TextCtrl(self, wx.ID_ANY, '')\n\n            self.Bind(wx.EVT_COMBOBOX, self.on_combo_box_select)\n            self.Bind(wx.EVT_TEXT, self.on_text_change)\n\n            row_sizer = wx.BoxSizer(wx.HORIZONTAL)\n            row_sizer.Add(combo_box, 0, wx.ALL, 5)\n            row_sizer.Add(text_ctrl, 1, wx.ALL | wx.EXPAND | wx.ALIGN_RIGHT, 5)\n\n            self.combo_boxes.append(combo_box)\n            self.text_controls.append(text_ctrl)\n            self.main_sizer.Add(row_sizer, 0, wx.EXPAND)\n\n        self.SetSizer(self.main_sizer)\n\n    def on_combo_box_select(self, event):\n        self.update_conditions()\n\n    def on_text_change(self, event):\n        self.update_conditions()\n\n    def update_conditions(self):\n        # print(\"Updating conditions\")\n        conditions = []\n        for i in range(self.num_filters):\n            column_index = self.combo_boxes[i].GetSelection()\n            condition = self.text_controls[i].GetValue()\n            if column_index != wx.NOT_FOUND and column_index != 0:\n                # since we have added a dummy column for \"deselect\", we have to subtract one\n                column = self.columns[column_index - 1]\n                conditions += [(column, condition)]\n        num_matching, has_changed = self.df_list_ctrl.apply_filter(conditions)\n        if has_changed:\n            self.change_callback()\n        # print(\"Num matching:\", num_matching)\n\n\nclass HistogramPlot(wx.Panel):\n    \"\"\"\n    Panel providing a histogram plot.\n    \"\"\"\n    def __init__(self, parent, columns, df_list_ctrl):\n        wx.Panel.__init__(self, parent)\n\n        columns_with_neutral_selection = [''] + list(columns)\n        self.columns = columns\n        self.df_list_ctrl = df_list_ctrl\n\n        self.figure = Figure(facecolor=\"white\", figsize=(1, 1))\n        self.axes = self.figure.add_subplot(111)\n        self.canvas = FigureCanvas(self, -1, self.figure)\n\n        chart_toolbar = NavigationToolbar2Wx(self.canvas)\n\n        self.combo_box1 = wx.ComboBox(self, choices=columns_with_neutral_selection, style=wx.CB_READONLY)\n\n        self.Bind(wx.EVT_COMBOBOX, self.on_combo_box_select)\n\n        row_sizer = wx.BoxSizer(wx.HORIZONTAL)\n        row_sizer.Add(self.combo_box1, 0, wx.ALL | wx.ALIGN_CENTER, 5)\n        row_sizer.Add(chart_toolbar, 0, wx.ALL, 5)\n\n        sizer = wx.BoxSizer(wx.VERTICAL)\n        sizer.Add(self.canvas, 1, flag=wx.EXPAND, border=5)\n        sizer.Add(row_sizer)\n        self.SetSizer(sizer)\n\n    def on_combo_box_select(self, event):\n        self.redraw()\n\n    def redraw(self):\n        column_index1 = self.combo_box1.GetSelection()\n        if column_index1 != wx.NOT_FOUND and column_index1 != 0:\n            # subtract one to remove the neutral selection index\n            column_index1 -= 1\n\n            df = self.df_list_ctrl.get_filtered_df()\n\n            if len(df) > 0:\n                self.axes.clear()\n\n                column = df.iloc[:, column_index1]\n                is_string_col = column.dtype == np.object and isinstance(column.values[0], str)\n                if is_string_col:\n                    value_counts = column.value_counts().sort_index()\n                    value_counts.plot(kind='bar', ax=self.axes)\n                else:\n                    self.axes.hist(column.values, bins=100)\n\n                self.canvas.draw()\n\n\nclass ScatterPlot(wx.Panel):\n    \"\"\"\n    Panel providing a scatter plot.\n    \"\"\"\n    def __init__(self, parent, columns, df_list_ctrl):\n        wx.Panel.__init__(self, parent)\n\n        columns_with_neutral_selection = [''] + list(columns)\n        self.columns = columns\n        self.df_list_ctrl = df_list_ctrl\n\n        self.figure = Figure(facecolor=\"white\", figsize=(1, 1))\n        self.axes = self.figure.add_subplot(111)\n        self.canvas = FigureCanvas(self, -1, self.figure)\n\n        chart_toolbar = NavigationToolbar2Wx(self.canvas)\n\n        self.combo_box1 = wx.ComboBox(self, choices=columns_with_neutral_selection, style=wx.CB_READONLY)\n        self.combo_box2 = wx.ComboBox(self, choices=columns_with_neutral_selection, style=wx.CB_READONLY)\n\n        self.Bind(wx.EVT_COMBOBOX, self.on_combo_box_select)\n\n        row_sizer = wx.BoxSizer(wx.HORIZONTAL)\n        row_sizer.Add(self.combo_box1, 0, wx.ALL | wx.ALIGN_CENTER, 5)\n        row_sizer.Add(self.combo_box2, 0, wx.ALL | wx.ALIGN_CENTER, 5)\n        row_sizer.Add(chart_toolbar, 0, wx.ALL, 5)\n\n        sizer = wx.BoxSizer(wx.VERTICAL)\n        sizer.Add(self.canvas, 1, flag=wx.EXPAND, border=5)\n        sizer.Add(row_sizer)\n        self.SetSizer(sizer)\n\n    def on_combo_box_select(self, event):\n        self.redraw()\n\n    def redraw(self):\n        column_index1 = self.combo_box1.GetSelection()\n        column_index2 = self.combo_box2.GetSelection()\n        if column_index1 != wx.NOT_FOUND and column_index1 != 0 and \\\n           column_index2 != wx.NOT_FOUND and column_index2 != 0:\n            # subtract one to remove the neutral selection index\n            column_index1 -= 1\n            column_index2 -= 1\n            df = self.df_list_ctrl.get_filtered_df()\n\n            # It looks like using pandas dataframe.plot causes something weird to\n            # crash in wx internally. Therefore we use plain axes.plot functionality.\n            # column_name1 = self.columns[column_index1]\n            # column_name2 = self.columns[column_index2]\n            # df.plot(kind='scatter', x=column_name1, y=column_name2)\n\n            if len(df) > 0:\n                self.axes.clear()\n                self.axes.plot(df.iloc[:, column_index1].values, df.iloc[:, column_index2].values, 'o', clip_on=False)\n\n                self.canvas.draw()\n\n\nclass MainFrame(wx.Frame):\n    \"\"\"\n    The main GUI window.\n    \"\"\"\n    def __init__(self, df):\n        wx.Frame.__init__(self, None, -1, \"Pandas DataFrame GUI\")\n\n        # Here we create a panel and a notebook on the panel\n        p = wx.Panel(self)\n        nb = wx.Notebook(p)\n        self.nb = nb\n\n        columns = df.columns[:]\n        if isinstance(columns,(pd.RangeIndex,pd.Int64Index)):\n            # RangeIndex is not supported\n            columns = pd.Index([str(i) for i in columns])\n        self.CreateStatusBar(2, style=0)\n        self.SetStatusWidths([200, -1])\n\n        # create the page windows as children of the notebook\n        self.page1 = DataframePanel(nb, df, self.status_bar_callback)\n        self.page2 = ColumnSelectionPanel(nb, columns, self.page1.df_list_ctrl)\n        self.page3 = FilterPanel(nb, columns, self.page1.df_list_ctrl, self.selection_change_callback)\n        self.page4 = HistogramPlot(nb, columns, self.page1.df_list_ctrl)\n        self.page5 = ScatterPlot(nb, columns, self.page1.df_list_ctrl)\n\n        # add the pages to the notebook with the label to show on the tab\n        nb.AddPage(self.page1, \"Data Frame\")\n        nb.AddPage(self.page2, \"Columns\")\n        nb.AddPage(self.page3, \"Filters\")\n        nb.AddPage(self.page4, \"Histogram\")\n        nb.AddPage(self.page5, \"Scatter Plot\")\n\n        nb.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.on_tab_change)\n\n        # finally, put the notebook in a sizer for the panel to manage\n        # the layout\n        sizer = wx.BoxSizer()\n        sizer.Add(nb, 1, wx.EXPAND)\n        p.SetSizer(sizer)\n\n        self.SetSize((800, 600))\n        self.Center()\n\n    def on_tab_change(self, event):\n        self.page2.list_box.SetFocus()\n        page_to_select = event.GetSelection()\n        wx.CallAfter(self.fix_focus, page_to_select)\n        event.Skip(True)\n\n    def fix_focus(self, page_to_select):\n        page = self.nb.GetPage(page_to_select)\n        page.SetFocus()\n        if isinstance(page, DataframePanel):\n            self.page1.df_list_ctrl.SetFocus()\n        elif isinstance(page, ColumnSelectionPanel):\n            self.page2.list_box.SetFocus()\n\n    def status_bar_callback(self, i, new_text):\n        self.SetStatusText(new_text, i)\n\n    def selection_change_callback(self):\n        self.page4.redraw()\n        self.page5.redraw()\n\n\ndef show(df):\n    \"\"\"\n    The main function to start the data frame GUI.\n    \"\"\"\n\n    app = wx.App(False)\n    frame = MainFrame(df)\n    frame.Show()\n    app.MainLoop()\n"
  },
  {
    "path": "dfgui/dnd_list.py",
    "content": "\"\"\" DnD demo with listctrl. \"\"\"\nimport sys\nsys.path.append(\"/usr/lib/python2.7/dist-packages/wx-2.8-gtk2-unicode\")\n\nimport wx\n\nclass DragList(wx.ListCtrl):\n    def __init__(self, *arg, **kw):\n        if 'style' in kw and (kw['style']&wx.LC_LIST or kw['style']&wx.LC_REPORT):\n            kw['style'] |= wx.LC_SINGLE_SEL\n        else:\n            kw['style'] = wx.LC_SINGLE_SEL|wx.LC_LIST\n\n        wx.ListCtrl.__init__(self, *arg, **kw)\n\n        self.Bind(wx.EVT_LIST_BEGIN_DRAG, self._startDrag)\n\n        dt = ListDrop(self._insert)\n        self.SetDropTarget(dt)\n\n    def _startDrag(self, e):\n        \"\"\" Put together a data object for drag-and-drop _from_ this list. \"\"\"\n\n        # Create the data object: Just use plain text.\n        data = wx.PyTextDataObject()\n        idx = e.GetIndex()\n        text = self.GetItem(idx).GetText()\n        data.SetText(text)\n\n        # Create drop source and begin drag-and-drop.\n        dropSource = wx.DropSource(self)\n        dropSource.SetData(data)\n        res = dropSource.DoDragDrop(flags=wx.Drag_DefaultMove)\n\n        # If move, we want to remove the item from this list.\n        if res == wx.DragMove:\n            # It's possible we are dragging/dropping from this list to this list.  In which case, the\n            # index we are removing may have changed...\n\n            # Find correct position.\n            pos = self.FindItem(idx, text)\n            self.DeleteItem(pos)\n\n    def _insert(self, x, y, text):\n        \"\"\" Insert text at given x, y coordinates --- used with drag-and-drop. \"\"\"\n\n        # Clean text.\n        import string\n        text = filter(lambda x: x in (string.letters + string.digits + string.punctuation + ' '), text)\n\n        # Find insertion point.\n        index, flags = self.HitTest((x, y))\n\n        if index == wx.NOT_FOUND:\n            if flags & wx.LIST_HITTEST_NOWHERE:\n                index = self.GetItemCount()\n            else:\n                return\n\n        # Get bounding rectangle for the item the user is dropping over.\n        rect = self.GetItemRect(index)\n\n        # If the user is dropping into the lower half of the rect, we want to insert _after_ this item.\n        if y > rect.y + rect.height/2:\n            index += 1\n\n        self.InsertStringItem(index, text)\n\nclass ListDrop(wx.PyDropTarget):\n    \"\"\" Drop target for simple lists. \"\"\"\n\n    def __init__(self, setFn):\n        \"\"\" Arguments:\n         - setFn: Function to call on drop.\n        \"\"\"\n        wx.PyDropTarget.__init__(self)\n\n        self.setFn = setFn\n\n        # specify the type of data we will accept\n        self.data = wx.PyTextDataObject()\n        self.SetDataObject(self.data)\n\n    # Called when OnDrop returns True.  We need to get the data and\n    # do something with it.\n    def OnData(self, x, y, d):\n        # copy the data from the drag source to our data object\n        if self.GetData():\n            self.setFn(x, y, self.data.GetText())\n\n        # what is returned signals the source what to do\n        # with the original data (move, copy, etc.)  In this\n        # case we just return the suggested value given to us.\n        return d\n\nif __name__ == '__main__':\n    items = ['Foo', 'Bar', 'Baz', 'Zif', 'Zaf', 'Zof']\n\n    class MyApp(wx.App):\n        def OnInit(self):\n            self.frame = wx.Frame(None, title='Main Frame')\n            self.frame.Show(True)\n            self.SetTopWindow(self.frame)\n            return True\n\n    app = MyApp(redirect=False)\n    dl1 = DragList(app.frame)\n    dl2 = DragList(app.frame)\n    sizer = wx.BoxSizer()\n    app.frame.SetSizer(sizer)\n    sizer.Add(dl1, proportion=1, flag=wx.EXPAND)\n    sizer.Add(dl2, proportion=1, flag=wx.EXPAND)\n    for item in items:\n        dl1.InsertStringItem(99, item)\n        dl2.InsertStringItem(99, item)\n    app.frame.Layout()\n    app.MainLoop()"
  },
  {
    "path": "dfgui/listmixin.py",
    "content": "#----------------------------------------------------------------------------\n# Name:        wxPython.lib.mixins.listctrl\n# Purpose:     Helpful mix-in classes for wxListCtrl\n#\n# Author:      Robin Dunn\n#\n# Created:     15-May-2001\n# RCS-ID:      $Id: listctrl.py 63322 2010-01-30 00:59:55Z RD $\n# Copyright:   (c) 2001 by Total Control Software\n# Licence:     wxWindows license\n#----------------------------------------------------------------------------\n# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net)\n#\n# o 2.5 compatability update.\n# o ListCtrlSelectionManagerMix untested.\n#\n# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net)\n#\n# o wxColumnSorterMixin -> ColumnSorterMixin \n# o wxListCtrlAutoWidthMixin -> ListCtrlAutoWidthMixin\n# ...\n# 13/10/2004 - Pim Van Heuven (pim@think-wize.com)\n# o wxTextEditMixin: Support Horizontal scrolling when TAB is pressed on long\n#       ListCtrls, support for WXK_DOWN, WXK_UP, performance improvements on\n#       very long ListCtrls, Support for virtual ListCtrls\n#\n# 15-Oct-2004 - Robin Dunn\n# o wxTextEditMixin: Added Shift-TAB support\n#\n# 2008-11-19 - raf <raf@raf.org>\n# o ColumnSorterMixin: Added GetSortState()\n#\n\nimport  locale\nimport  wx\n\n#----------------------------------------------------------------------------\n\nclass ColumnSorterMixin:\n    \"\"\"\n    A mixin class that handles sorting of a wx.ListCtrl in REPORT mode when\n    the column header is clicked on.\n\n    There are a few requirments needed in order for this to work genericly:\n\n      1. The combined class must have a GetListCtrl method that\n         returns the wx.ListCtrl to be sorted, and the list control\n         must exist at the time the wx.ColumnSorterMixin.__init__\n         method is called because it uses GetListCtrl.\n\n      2. Items in the list control must have a unique data value set\n         with list.SetItemData.\n\n      3. The combined class must have an attribute named itemDataMap\n         that is a dictionary mapping the data values to a sequence of\n         objects representing the values in each column.  These values\n         are compared in the column sorter to determine sort order.\n\n    Interesting methods to override are GetColumnSorter,\n    GetSecondarySortValues, and GetSortImages.  See below for details.\n    \"\"\"\n\n    def __init__(self, numColumns, preSortCallback = None):\n        self.SetColumnCount(numColumns)\n        self.preSortCallback = preSortCallback\n        list = self.GetListCtrl()\n        if not list:\n            raise ValueError, \"No wx.ListCtrl available\"\n        list.Bind(wx.EVT_LIST_COL_CLICK, self.__OnColClick, list)\n\n\n    def SetColumnCount(self, newNumColumns):\n        self._colSortFlag = [0] * newNumColumns\n        self._col = -1\n\n\n    def SortListItems(self, col=-1, ascending=1):\n        \"\"\"Sort the list on demand.  Can also be used to set the sort column and order.\"\"\"\n        oldCol = self._col\n        if col != -1:\n            self._col = col\n            self._colSortFlag[col] = ascending\n        self.GetListCtrl().SortItems(self.GetColumnSorter())\n        self.__updateImages(oldCol)\n\n\n    def GetColumnWidths(self):\n        \"\"\"\n        Returns a list of column widths.  Can be used to help restore the current\n        view later.\n        \"\"\"\n        list = self.GetListCtrl()\n        rv = []\n        for x in range(len(self._colSortFlag)):\n            rv.append(list.GetColumnWidth(x))\n        return rv\n\n\n    def GetSortImages(self):\n        \"\"\"\n        Returns a tuple of image list indexesthe indexes in the image list for an image to be put on the column\n        header when sorting in descending order.\n        \"\"\"\n        return (-1, -1)  # (decending, ascending) image IDs\n\n\n    def GetColumnSorter(self):\n        \"\"\"Returns a callable object to be used for comparing column values when sorting.\"\"\"\n        return self.__ColumnSorter\n\n\n    def GetSecondarySortValues(self, col, key1, key2):\n        \"\"\"Returns a tuple of 2 values to use for secondary sort values when the\n           items in the selected column match equal.  The default just returns the\n           item data values.\"\"\"\n        return (key1, key2)\n\n\n    def __OnColClick(self, evt):\n        if self.preSortCallback is not None:\n          self.preSortCallback()\n        oldCol = self._col\n        self._col = col = evt.GetColumn()\n        self._colSortFlag[col] = int(not self._colSortFlag[col])\n        self.GetListCtrl().SortItems(self.GetColumnSorter())\n        if wx.Platform != \"__WXMAC__\" or wx.SystemOptions.GetOptionInt(\"mac.listctrl.always_use_generic\") == 1:\n            self.__updateImages(oldCol)\n        evt.Skip()\n        self.OnSortOrderChanged()\n        \n        \n    def OnSortOrderChanged(self):\n        \"\"\"\n        Callback called after sort order has changed (whenever user\n        clicked column header).\n        \"\"\"\n        pass\n\n\n    def GetSortState(self):\n        \"\"\"\n        Return a tuple containing the index of the column that was last sorted\n        and the sort direction of that column.\n        Usage:\n        col, ascending = self.GetSortState()\n        # Make changes to list items... then resort\n        self.SortListItems(col, ascending)\n        \"\"\"\n        return (self._col, self._colSortFlag[self._col])\n\n\n    def __ColumnSorter(self, key1, key2):\n        col = self._col\n        ascending = self._colSortFlag[col]\n        item1 = self.itemDataMap[key1][col]\n        item2 = self.itemDataMap[key2][col]\n\n        #--- Internationalization of string sorting with locale module\n        if type(item1) == unicode and type(item2) == unicode:\n            cmpVal = locale.strcoll(item1, item2)\n        elif type(item1) == str or type(item2) == str:\n            cmpVal = locale.strcoll(str(item1), str(item2))\n        else:\n            cmpVal = cmp(item1, item2)\n        #---\n\n        # If the items are equal then pick something else to make the sort value unique\n        if cmpVal == 0:\n            cmpVal = apply(cmp, self.GetSecondarySortValues(col, key1, key2))\n\n        if ascending:\n            return cmpVal\n        else:\n            return -cmpVal\n\n\n    def __updateImages(self, oldCol):\n        sortImages = self.GetSortImages()\n        if self._col != -1 and sortImages[0] != -1:\n            img = sortImages[self._colSortFlag[self._col]]\n            list = self.GetListCtrl()\n            if oldCol != -1:\n                list.ClearColumnImage(oldCol)\n            list.SetColumnImage(self._col, img)\n\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup\n\nsetup(\n    name='dfgui',\n    version='0.1',\n    description='Pandas DataFrame GUI',\n    url='http://github.com/bluenote10/PandasDataFrameGUI',\n    author='Fabian Keller',\n    author_email='fabian.keller@blue-yonder.com',\n    license='MIT',\n    packages=['dfgui'],\n    zip_safe=False\n)\n"
  }
]