Repository: pandas-dev/pandas

Issue:
Add table id option to pandas.DataFrame.to_html 
I'm using the pandas.DataFrame.to_html() method to generate a table to embed into a a templated document which references the table via an id. It would be useful if pandas.DataFrame.to_html()  supported a `table_id` parameter:

`df.to_html(table_id='myTable')`

to produce:

`<table border="1" class="dataframe" id='mytable'>`



Discussion:
see #6488 , #3190, #4315

We are looking for a more general soln (I pointed to some related issues). If you have some ideas, pls post/indicate!

I'm not sure my py skills are up to contributing code but if I can abstract anything from use cases I come across I'll add them in to an appropriate thread.

Something that comes to mind in terms of generalisation is how to cope with outputting metadata, either about the dataframe itself, or additional to the dataframe in terms of presentation (for example, the id attribute for the table, or column layout information etc).

The ONS (Office of National Statistics) in the UK have started [experimenting with the Tabular Data Package format](http://digitalpublishing.ons.gov.uk/2014/09/02/introducing-our-pilot-tabular-data-package/) which bundles a JSON file carrying metadata about the contents of one or more CSV files with those files packaged alongside it.

I've been wondering what a _pandas_ `to_datapackage()` write method might have to do and what additional items are _required_ for the datapackage that can't be extracted from a dataframe itself (we should be able to get column type information from the dataframe itself, for example).

(How to make effective use of all the metadata when doing a `from_datapackage()` read also raises interesting questions about how data metadata and provenance information could or should be managed within the _pandas_ context.)

It seems to me that there are perhaps three sorts of metadata that need addressing in outputs:
1. metadata about the data itself (eg column typing)
2. metadata about data presentation (so eg column styling, table id etc)
3. metadata about provenance, dataset title, collection methods etc etc. This might be passed to an output template when rendering the dataset, or bundled up as core/required metadata in a `datapackage.json` file for example.


Resolving patch:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -1727,7 +1727,7 @@ def to_html(self, buf=None, columns=None, col_space=None, header=True,
                 sparsify=None, index_names=True, justify=None, bold_rows=True,
                 classes=None, escape=True, max_rows=None, max_cols=None,
                 show_dimensions=False, notebook=False, decimal='.',
-                border=None):
+                border=None, table_id=None):
         """
         Render a DataFrame as an HTML table.
 
@@ -1755,6 +1755,12 @@ def to_html(self, buf=None, columns=None, col_space=None, header=True,
             `<table>` tag. Default ``pd.options.html.border``.
 
             .. versionadded:: 0.19.0
+
+        table_id : str, optional
+            A css id is included in the opening `<table>` tag if specified.
+
+            .. versionadded:: 0.23.0
+
         """
 
         if (justify is not None and
@@ -1772,7 +1778,7 @@ def to_html(self, buf=None, columns=None, col_space=None, header=True,
                                            max_rows=max_rows,
                                            max_cols=max_cols,
                                            show_dimensions=show_dimensions,
-                                           decimal=decimal)
+                                           decimal=decimal, table_id=table_id)
         # TODO: a generic formatter wld b in DataFrameFormatter
         formatter.to_html(classes=classes, notebook=notebook, border=border)
 
diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py
--- a/pandas/io/formats/format.py
+++ b/pandas/io/formats/format.py
@@ -77,7 +77,11 @@
     index_names : bool, optional
         Prints the names of the indexes, default True
     line_width : int, optional
-        Width to wrap a line in characters, default no wrap"""
+        Width to wrap a line in characters, default no wrap
+    table_id : str, optional
+        id for the <table> element create by to_html
+
+        .. versionadded:: 0.23.0"""
 
 _VALID_JUSTIFY_PARAMETERS = ("left", "right", "center", "justify",
                              "justify-all", "start", "end", "inherit",
@@ -387,7 +391,8 @@ def __init__(self, frame, buf=None, columns=None, col_space=None,
                  header=True, index=True, na_rep='NaN', formatters=None,
                  justify=None, float_format=None, sparsify=None,
                  index_names=True, line_width=None, max_rows=None,
-                 max_cols=None, show_dimensions=False, decimal='.', **kwds):
+                 max_cols=None, show_dimensions=False, decimal='.',
+                 table_id=None, **kwds):
         self.frame = frame
         if buf is not None:
             self.buf = _expand_user(_stringify_path(buf))
@@ -413,6 +418,7 @@ def __init__(self, frame, buf=None, columns=None, col_space=None,
         self.max_rows_displayed = min(max_rows or len(self.frame),
                                       len(self.frame))
         self.show_dimensions = show_dimensions
+        self.table_id = table_id
 
         if justify is None:
             self.justify = get_option("display.colheader_justify")
@@ -740,7 +746,8 @@ def to_html(self, classes=None, notebook=False, border=None):
                                       max_rows=self.max_rows,
                                       max_cols=self.max_cols,
                                       notebook=notebook,
-                                      border=border)
+                                      border=border,
+                                      table_id=self.table_id)
         if hasattr(self.buf, 'write'):
             html_renderer.write_result(self.buf)
         elif isinstance(self.buf, compat.string_types):
@@ -1082,7 +1089,7 @@ class HTMLFormatter(TableFormatter):
     indent_delta = 2
 
     def __init__(self, formatter, classes=None, max_rows=None, max_cols=None,
-                 notebook=False, border=None):
+                 notebook=False, border=None, table_id=None):
         self.fmt = formatter
         self.classes = classes
 
@@ -1101,6 +1108,7 @@ def __init__(self, formatter, classes=None, max_rows=None, max_cols=None,
         if border is None:
             border = get_option('display.html.border')
         self.border = border
+        self.table_id = table_id
 
     def write(self, s, indent=0):
         rs = pprint_thing(s)
@@ -1197,6 +1205,7 @@ def write_style(self):
 
     def write_result(self, buf):
         indent = 0
+        id_section = ""
         frame = self.frame
 
         _classes = ['dataframe']  # Default class.
@@ -1220,8 +1229,12 @@ def write_result(self, buf):
             self.write('<div{style}>'.format(style=div_style))
 
         self.write_style()
-        self.write('<table border="{border}" class="{cls}">'
-                   .format(border=self.border, cls=' '.join(_classes)), indent)
+
+        if self.table_id is not None:
+            id_section = ' id="{table_id}"'.format(table_id=self.table_id)
+        self.write('<table border="{border}" class="{cls}"{id_section}>'
+                   .format(border=self.border, cls=' '.join(_classes),
+                           id_section=id_section), indent)
 
         indent += self.indent_delta
         indent = self._write_header(indent)


Test patch:
diff --git a/pandas/tests/io/formats/test_format.py b/pandas/tests/io/formats/test_format.py
--- a/pandas/tests/io/formats/test_format.py
+++ b/pandas/tests/io/formats/test_format.py
@@ -1492,7 +1492,7 @@ def test_repr_html_float(self):
                             'B': np.arange(41, 41 + h)}).set_index('idx')
             reg_repr = df._repr_html_()
             assert '..' not in reg_repr
-            assert str(40 + h) in reg_repr
+            assert '<td>{val}</td>'.format(val=str(40 + h)) in reg_repr
 
             h = max_rows + 1
             df = DataFrame({'idx': np.linspace(-10, 10, h),
@@ -1500,7 +1500,7 @@ def test_repr_html_float(self):
                             'B': np.arange(41, 41 + h)}).set_index('idx')
             long_repr = df._repr_html_()
             assert '..' in long_repr
-            assert '31' not in long_repr
+            assert '<td>{val}</td>'.format(val='31') not in long_repr
             assert u('{h} rows ').format(h=h) in long_repr
             assert u('2 columns') in long_repr
 
diff --git a/pandas/tests/io/formats/test_to_html.py b/pandas/tests/io/formats/test_to_html.py
--- a/pandas/tests/io/formats/test_to_html.py
+++ b/pandas/tests/io/formats/test_to_html.py
@@ -1864,3 +1864,10 @@ def test_to_html_with_index_names_false(self):
                                                         name='myindexname'))
         result = df.to_html(index_names=False)
         assert 'myindexname' not in result
+
+    def test_to_html_with_id(self):
+        # gh-8496
+        df = pd.DataFrame({"A": [1, 2]}, index=pd.Index(['a', 'b'],
+                                                        name='myindexname'))
+        result = df.to_html(index_names=False, table_id="TEST_ID")
+        assert ' id="TEST_ID"' in result