Search code examples
matplotlibjupyter-notebookmarkdown

plot and markdown side by side in Jupyter Notebook


I wish to display a plot and markdown side by side in Jupyter Notebook (to be used in RISE), using matplotlib. I found the following answer which uses plotly,

Show interactive plot and Markdown text, side-by-side, in Jupyter

and simply modified it by adding some matplotlib elements

from ipywidgets import widgets, Layout
from IPython.display import display, Javascript, Markdown as md
import numpy as np
import matplotlib.pyplot as plt

def func(x, a, b, c, d):
    return a + b*np.exp(c*x + d)

the_x = np.arange(0, 10, 0.5)
the_y = func(the_x, 1, 1, -1, 2)

fig, ax = plt.subplots()
ax.plot(the_x, the_y)

len_x = len(the_x)

mdout = md(f"""There are {len_x} **elements** of both `the_x`, and `the_y`;

Those values are plotted on the diagram on the left""")


box_layout = Layout(display='flex',
                    flex_flow='row',
                    justify_content='space-around',
                    width='auto'
                   )

hbox1 = widgets.Box(children=[fig, widgets.HTML(mdout._repr_markdown_())], layout=box_layout)
display(hbox1)

But, it does not work with the following error message:

TraitError: The 'children' trait of a Box instance contains an Instance of a TypedTuple which expected a Widget, not the Figure <Figure size 640x480 with 1 Axes>.

which is about the second line from the bottom.

Can anyone help?


Solution

  • In my hands the following variation of your code works as a next step (see the Update section below for final):

    #based on https://stackoverflow.com/q/78670150/8508004, and adapting to put matplotlib in a widget from https://stackoverflow.com/a/51060721/8508004 (which I think was adapted in some ways from https://github.com/jupyter-widgets/ipywidgets/issues/2821)
    from ipywidgets import widgets, Layout
    from IPython.display import display, Javascript, Markdown as md
    import numpy as np
    import matplotlib.pyplot as plt
    
    left_widget = widgets.Output()
    def func(x, a, b, c, d):
        return a + b*np.exp(c*x + d)
    
    the_x = np.arange(0, 10, 0.5)
    the_y = func(the_x, 1, 1, -1, 2)
    
    with left_widget:
        fig, ax = plt.subplots()
        ax.plot(the_x, the_y)
        plt.show(fig)
    
    len_x = len(the_x)
    
    mdout = md(f"""There are {len_x} **elements** of both `the_x`, and `the_y`;
    Those values are plotted on the diagram on the left""")
    
    box_layout = Layout(display='flex',
                        flex_flow='row',
                        justify_content='space-around',
                        width='auto'
                       )
    
    hbox1 = widgets.Box(children=[left_widget, widgets.HTML(mdout._repr_markdown_())], layout=box_layout)
    display(hbox1)
    

    The only real change was wrapping the Matplotlib plot object in the context of ipywidget's Output() widget, which "can capture and display stdout, stderr and rich output generated by IPython", and then using that widget as the left side for the children listed in the ipywidget's Box. (Look for the code relating to left_widget to follow the differences from the original, provided code.)

    Related Resources:


    UPDATE:

    To address the fact the source of the adapted solution didn't include handling displaying the raw markdown code as fully-rendered markdown in the area on the right, I've now added handling this. You'll need to install packages for the first two approaches I offer. (For markdown2, I put the command in the linked notebooks: "Install it from within the running Juyter notebooks with either %conda install -c conda-forge markdown2 or %pip install markdown2.").
    The first two solutions actually add rendering another element of markdown to show they may be more quickly adaptable. And in fact, if you don't require the text between backticks in shown as 'code' styling, you can can do the styling without needing CSS, see the full details in a companion Jupyter notebook referenced at the bottom of this section for further information.
    (In fact the CSS used in the first two options can be simpler, but I put full CSS details for all the elements here for ensuring it works fully.)

    Proposed solution A:

    #based on https://stackoverflow.com/q/78670150/8508004, and adapting to put matplotlib in a widget from https://stackoverflow.com/a/51060721/8508004 (which I think was adapted in some ways from https://github.com/jupyter-widgets/ipywidgets/issues/2821)
    from ipywidgets import widgets, Layout, HTML
    from IPython.display import display
    import numpy as np
    import matplotlib.pyplot as plt
    import markdown
    
    left_widget = widgets.Output()
    def func(x, a, b, c, d):
        return a + b*np.exp(c*x + d)
    
    the_x = np.arange(0, 10, 0.5)
    the_y = func(the_x, 1, 1, -1, 2)
    
    with left_widget:
        fig, ax = plt.subplots()
        ax.plot(the_x, the_y)
        plt.show(fig)
    
    len_x = len(the_x)
    
    md_content = f"""# (Markdown Heading Rendered) RESULTS:
    
    There are {len_x} **elements** of both `the_x`, and `the_y`;
    Those values are plotted on the diagram on the left"""
    
    box_layout = Layout(display='flex',
                        flex_flow='row',
                        justify_content='space-around',
                        width='auto'
                       )
    html_content = markdown.markdown(md_content)
    markdown_widget = HTML(
        value=f"""
        <style>
        .markdown-body {{
            font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
            font-size: 16px;
            line-height: 1.5;
            word-wrap: break-word;
        }}
        .markdown-body h1 {{
            padding-bottom: .3em;
            font-size: 2em;
            border-bottom: 1px solid #eaecef;
        }}
        .markdown-body strong {{
            font-weight: 600;
        }}
        .markdown-body code {{
            padding: .2em .4em;
            margin: 0;
            font-size: 85%;
            background-color: rgba(27,31,35,.05);
            border-radius: 3px;
        }}
        </style>
        <div class="markdown-body">
        {html_content}
        </div>
        """
    )
    hbox1 = widgets.Box(children=[left_widget, markdown_widget], layout=box_layout)
    display(hbox1)
    

    Proposed solution B:

    #based on https://stackoverflow.com/q/78670150/8508004, and adapting to put matplotlib in a widget from https://stackoverflow.com/a/51060721/8508004 (which I think was adapted in some ways from https://github.com/jupyter-widgets/ipywidgets/issues/2821)
    from ipywidgets import widgets, Layout
    import numpy as np
    import matplotlib.pyplot as plt
    import markdown2
    
    left_widget = widgets.Output()
    def func(x, a, b, c, d):
        return a + b*np.exp(c*x + d)
    
    the_x = np.arange(0, 10, 0.5)
    the_y = func(the_x, 1, 1, -1, 2)
    
    with left_widget:
        fig, ax = plt.subplots()
        ax.plot(the_x, the_y)
        plt.show(fig)
    
    len_x = len(the_x)
    
    md_content = f"""# (Markdown Heading Rendered) RESULTS:
    
    There are {len_x} **elements** of both `the_x`, and `the_y`;
    Those values are plotted on the diagram on the left"""
    
    box_layout = Layout(display='flex',
                        flex_flow='row',
                        justify_content='space-around',
                        width='auto'
                       )
    
    html_content = markdown2.markdown(md_content)
    markdown_widget = widgets.HTML(
        value=f"""
        <style>
        .markdown-body {{
            font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
            font-size: 16px;
            line-height: 1.5;
            word-wrap: break-word;
        }}
        .markdown-body h1 {{
            padding-bottom: .3em;
            font-size: 2em;
            border-bottom: 1px solid #eaecef;
        }}
        .markdown-body strong {{
            font-weight: 600;
        }}
        .markdown-body code {{
            padding: .2em .4em;
            margin: 0;
            font-size: 85%;
            background-color: rgba(27,31,35,.05);
            border-radius: 3px;
        }}
        </style>
        <div class="markdown-body">
        {html_content}
        </div>
        """
    )
    hbox1 = widgets.Box(children=[left_widget, markdown_widget], layout=box_layout)
    display(hbox1)
    

    Proposed solution C:

    from ipywidgets import widgets, Layout
    import numpy as np
    import matplotlib.pyplot as plt
    
    left_widget = widgets.Output()
    def func(x, a, b, c, d):
        return a + b*np.exp(c*x + d)
    
    the_x = np.arange(0, 10, 0.5)
    the_y = func(the_x, 1, 1, -1, 2)
    
    with left_widget:
        fig, ax = plt.subplots()
        ax.plot(the_x, the_y)
        plt.show(fig)
    
    len_x = len(the_x)
    
    md_content = f"""There are {len_x} **elements** of both `the_x`, and `the_y`;
    Those values are plotted on the diagram on the left"""
    
    box_layout = Layout(display='flex',
                        flex_flow='row',
                        justify_content='space-around',
                        width='auto'
                       )
    
    def simple_markdown_to_html(text):
        # Handle bold text
        text = text.replace('**', '<strong>', 1).replace('**', '</strong>', 1)
        # Handle inline code
        while '`' in text:
            text = text.replace('`', '<code>', 1).replace('`', '</code>', 1)
        return text
    
    html_content = simple_markdown_to_html(md_content)
    
    markdown_widget = widgets.HTML(
        value=f"""
        <style>
        .markdown-body {{
            font-family: Arial, sans-serif;
            padding: 10px;
        }}
        .markdown-body strong {{
            font-weight: bold;
        }}
        .markdown-body code {{
            background-color: #f0f0f0;
            padding: 2px 4px;
            border-radius: 3px;
            font-family: monospace;
        }}
        </style>
        <div class="markdown-body">
        {html_content}
        </div>
        """
    )
    
    hbox1 = widgets.Box(children=[left_widget, markdown_widget], layout=box_layout)
    display(hbox1)
    

    For full details, view the code and result worked out from the starting point in a Jupyter notebook file best viewed here.

    Related Resources for conversion of the markdown text and styling: