Repository: pandas-dev/pandas

Issue:
Improvement: DataFrame.to_html() to create hyperlinks
When the data value is a string that represents an URL, have DataFrame.to_html() create hyperlinks out of them in the resulting html file.



Discussion:
Hi all- 

I'm trying to implement this, as it could be useful. Anyways, it looks like all that would need to be done is add a check if is valid url in `write_tr` (`core/format.py`) and if so, add the appropriate `<a>` tag. However, I was wondering if there is a `is_valid_url` type function elsewhere that I could import to use as a check? 

https://github.com/pydata/pandas/blob/master/pandas/io/common.py#L49

but may want to filter out url-like first (e.g. `.startswith('http')` or something first

Working on this one!

Any new on this issue? Has this been implemented?

Any new on this issue? Has this been implemented?


Resolving patch:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -2044,8 +2044,8 @@ def to_html(self, buf=None, columns=None, col_space=None, header=True,
                 index=True, na_rep='NaN', formatters=None, float_format=None,
                 sparsify=None, index_names=True, justify=None, max_rows=None,
                 max_cols=None, show_dimensions=False, decimal='.',
-                bold_rows=True, classes=None, escape=True,
-                notebook=False, border=None, table_id=None):
+                bold_rows=True, classes=None, escape=True, notebook=False,
+                border=None, table_id=None, render_links=False):
         """
         Render a DataFrame as an HTML table.
         %(shared_params)s
@@ -2067,6 +2067,12 @@ def to_html(self, buf=None, columns=None, col_space=None, header=True,
             A css id is included in the opening `<table>` tag if specified.
 
             .. versionadded:: 0.23.0
+
+        render_links : bool, default False
+            Convert URLs to HTML links.
+
+            .. versionadded:: 0.24.0
+
         %(returns)s
         See Also
         --------
@@ -2088,7 +2094,8 @@ 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, table_id=table_id)
+                                           decimal=decimal, table_id=table_id,
+                                           render_links=render_links)
         # 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
@@ -383,7 +383,7 @@ def __init__(self, frame, buf=None, columns=None, col_space=None,
                  justify=None, float_format=None, sparsify=None,
                  index_names=True, line_width=None, max_rows=None,
                  max_cols=None, show_dimensions=False, decimal='.',
-                 table_id=None, **kwds):
+                 table_id=None, render_links=False, **kwds):
         self.frame = frame
         if buf is not None:
             self.buf = _expand_user(_stringify_path(buf))
@@ -410,6 +410,7 @@ def __init__(self, frame, buf=None, columns=None, col_space=None,
                                       len(self.frame))
         self.show_dimensions = show_dimensions
         self.table_id = table_id
+        self.render_links = render_links
 
         if justify is None:
             self.justify = get_option("display.colheader_justify")
@@ -731,7 +732,8 @@ def to_html(self, classes=None, notebook=False, border=None):
          """
         from pandas.io.formats.html import HTMLFormatter
         html_renderer = HTMLFormatter(self, classes=classes, notebook=notebook,
-                                      border=border, table_id=self.table_id)
+                                      border=border, table_id=self.table_id,
+                                      render_links=self.render_links)
         if hasattr(self.buf, 'write'):
             html_renderer.write_result(self.buf)
         elif isinstance(self.buf, compat.string_types):
diff --git a/pandas/io/formats/html.py b/pandas/io/formats/html.py
--- a/pandas/io/formats/html.py
+++ b/pandas/io/formats/html.py
@@ -15,6 +15,7 @@
 import pandas.core.common as com
 from pandas.core.config import get_option
 
+from pandas.io.common import _is_url
 from pandas.io.formats.format import (
     TableFormatter, buffer_put_lines, get_level_lengths)
 from pandas.io.formats.printing import pprint_thing
@@ -25,7 +26,7 @@ class HTMLFormatter(TableFormatter):
     indent_delta = 2
 
     def __init__(self, formatter, classes=None, notebook=False, border=None,
-                 table_id=None):
+                 table_id=None, render_links=False):
         self.fmt = formatter
         self.classes = classes
 
@@ -40,6 +41,7 @@ def __init__(self, formatter, classes=None, notebook=False, border=None,
             border = get_option('display.html.border')
         self.border = border
         self.table_id = table_id
+        self.render_links = render_links
 
     @property
     def is_truncated(self):
@@ -76,9 +78,19 @@ def _write_cell(self, s, kind='td', indent=0, tags=None):
                                ('>', r'&gt;')])
         else:
             esc = {}
+
         rs = pprint_thing(s, escape_chars=esc).strip()
-        self.write(u'{start}{rs}</{kind}>'
-                   .format(start=start_tag, rs=rs, kind=kind), indent)
+
+        if self.render_links and _is_url(rs):
+            rs_unescaped = pprint_thing(s, escape_chars={}).strip()
+            start_tag += '<a href="{url}" target="_blank">'.format(
+                url=rs_unescaped)
+            end_a = '</a>'
+        else:
+            end_a = ''
+
+        self.write(u'{start}{rs}{end_a}</{kind}>'.format(
+            start=start_tag, rs=rs, end_a=end_a, kind=kind), indent)
 
     def write_tr(self, line, indent=0, indent_delta=0, header=False,
                  align=None, tags=None, nindex_levels=0):


Test patch:
diff --git a/pandas/tests/io/formats/data/render_links_false.html b/pandas/tests/io/formats/data/render_links_false.html
new file mode 100644
--- /dev/null
+++ b/pandas/tests/io/formats/data/render_links_false.html
@@ -0,0 +1,24 @@
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>foo</th>
+      <th>bar</th>
+      <th>None</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0</td>
+      <td>http://pandas.pydata.org/?q1=a&amp;q2=b</td>
+      <td>pydata.org</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0</td>
+      <td>www.pydata.org</td>
+      <td>pydata.org</td>
+    </tr>
+  </tbody>
+</table>
diff --git a/pandas/tests/io/formats/data/render_links_true.html b/pandas/tests/io/formats/data/render_links_true.html
new file mode 100644
--- /dev/null
+++ b/pandas/tests/io/formats/data/render_links_true.html
@@ -0,0 +1,24 @@
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>foo</th>
+      <th>bar</th>
+      <th>None</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0</td>
+      <td><a href="http://pandas.pydata.org/?q1=a&q2=b" target="_blank">http://pandas.pydata.org/?q1=a&amp;q2=b</a></td>
+      <td>pydata.org</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0</td>
+      <td>www.pydata.org</td>
+      <td>pydata.org</td>
+    </tr>
+  </tbody>
+</table>
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
@@ -477,3 +477,19 @@ def test_to_html_float_format_no_fixed_width(self, datapath):
         df = DataFrame({'x': [100.0]})
         expected = expected_html(datapath, 'gh22270_expected_output')
         assert df.to_html(float_format='%.0f') == expected
+
+    @pytest.mark.parametrize("render_links, file_name", [
+        (True, 'render_links_true'),
+        (False, 'render_links_false'),
+    ])
+    def test_to_html_render_links(self, render_links, file_name, datapath):
+        # GH 2679
+        data = [
+            [0, 'http://pandas.pydata.org/?q1=a&q2=b', 'pydata.org'],
+            [0, 'www.pydata.org', 'pydata.org']
+        ]
+        df = DataFrame(data, columns=['foo', 'bar', None])
+
+        result = df.to_html(render_links=render_links)
+        expected = expected_html(datapath, file_name)
+        assert result == expected