1

My goal is to have a simple form that allows a user to input a function to graph, along with a display window, and once the user hits the submit button, I would like numpy and matplotlib to take the form data and generate a figure, then display that figure in the new web page.

So far, I followed this where it says "Create the Contact Form View" to get the form to generate and make the data usable. I found this which allows me to display the image. However, the image takes up the whole browser window like so. I would instead like that image to display in my web page. I'm very new to Django, so sorry if this doesn't make sense or isn't enough info. I'd love to give clarification or additional info if needed.

Here is my app's views.py:

from django.shortcuts import render
from django.http import HttpResponse
from matplotlib.backends.backend_agg import FigureCanvasAgg
from .forms import GraphingInput
from .graphing_tool import graph

import io
# Create your views here.


def grapher_tool_input(request):
    submitted = False
    if request.method == 'POST':
        form = GraphingInput(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            fig = graph(cd['left_end'], cd['right_end'], cd['top'], cd['bottom'], cd['function'])
            response = HttpResponse(content_type='image/jpg')
            canvas = FigureCanvasAgg(fig)
            canvas.print_jpg(response)
            return response
    else:
        form = GraphingInput(initial={'left_end':-10, 'right_end':10, 'bottom':-10, 'top':10})
        if 'submitted' in request.GET:
            submitted = True
        context ={
            'form': form,
            'submitted': submitted,
        }
    return render(request, 'gtdefault.html', context)

Here is my forms.py

from django import forms


class GraphingInput(forms.Form):
    left_end = forms.FloatField(max_value=10000000, min_value=-10000000, label='Left End Point')
    right_end = forms.FloatField(max_value=10000000, min_value=-10000000, label='Right End Point')
    bottom = forms.FloatField(max_value=10000000, min_value=-10000000, label='Bottom of Window')
    top = forms.FloatField(max_value=10000000, min_value=-10000000, label='Top of Window')
    function = forms.CharField(min_length=1, label='f(x)')

Here is my graphing_tool.py file that generates the image:

import numpy as np
import matplotlib.pyplot as plt
import numexpr as ne
from numpy import pi, e


def graph(left, right, top, bottom, func):
    step = (right - left)/10000.
    x = np.arange(left, right, step)
    y = ne.evaluate(func)
    fig = plt.figure()
    plt.xlim(left, right)
    plt.ylim(bottom, top)
    plt.plot(x,y)
    return fig

And here is my gtdefault.html file:

{% extends 'base.html' %}

{% block content %}
{% if submitted %}
    <!--Display image-->
{% else %}
    <form action="" method="post" novalidate>
    {{ form.as_p }}
    <input type="submit" value="Graph">
    {% csrf_token %}
    </form>
{% endif %}
{% endblock content %}
Jared H
  • 83
  • 4

2 Answers2

1

First of all, your views.py is returning the image in case of POST. The following part will tell the browser that the "page" you return is actually an image and asks the browser to show the image. That's why the browser only shows the image.

            response = HttpResponse(content_type='image/jpg')
            canvas = FigureCanvasAgg(fig)
            canvas.print_jpg(response)
            return response

So, in both cases you should return the template rendered. return render(request, 'gtdefault.html', context)

I understood you want to show the image in the web page (gtdefault.html)? It would mean something like this.

{% if submitted %}
    <img src="you need source here" />
{% else %}

Now, the tricky part is getting the source url into the context. You can either upload the generated image to django meda files or some external storage (like AWS S3) for a while and use the url you get from there.

Or you can follow this to deliver the image inside the page: How to display picture from memory in Django?

In the first method you can use browser caching if the image is something that will be looked again later. With the latter case you can ignore the storage but it is "fiddlier" to implement.

Juho Rutila
  • 2,316
  • 1
  • 25
  • 40
  • Thanks for the reply! I guess I should have mentioned, I'm using Python 3.7, and so cStringIO doesn't exit. I am trying to use io, which I also found [here](https://medium.com/@mdhv.kothari99/matplotlib-into-django-template-5def2e159997). I changed `cStringIO.StringIO(picBin)` to `io.Bytes()`, then tried `fig.savefig(mStream, format='jpg').` So far so good. Then I define `data_uri` and do `data_uri += mStream.getvalue().encode('base64').replace('\n','')`, but I get an AttributeError, 'bytes' object has no attribute 'encode'. Thoughts? – Jared H Oct 05 '20 at 07:34
  • I should mention, I had previously tried the method in the link in the above comment, but when I clicked Graph, the form disappeared and I had no image, though I still had the Graph button. Edit: I should also add, although I have it set to render to the same url, I am not opposed to sending it to a different url, I just thought this would be easier. – Jared H Oct 05 '20 at 07:42
  • Does this help with encoding: https://pymotw.com/2/base64/ – Juho Rutila Oct 05 '20 at 10:37
  • The urls don't matter in this case. Did you get that your view is faulty? When you click the submit button the `request.method == "POST"` is true and it won't render the template. It will only render the image. You should render the template in both cases with the correct base64 uri in the context. – Juho Rutila Oct 05 '20 at 10:39
  • And if you don't get the base64 encoding to work you should write another question to SO (please link it here) about it. But I guess there might be answer already somewhere about bytes not having encode function. – Juho Rutila Oct 05 '20 at 10:42
  • Well, I should have said I did change the first bit of code you quoted to `return render(request, 'gtdefault.html', context)`. After I did that and the stuff I describe in my first comment and click Graph, that's when the form goes away and I don't have an image being displayed, just my base.html with a graph button, which makes me think the `{% if submitted %} isn't working in my gtdefault.html file. – Jared H Oct 05 '20 at 18:57
  • I finally got it working. I had to first take my output `fig` and save it to `buf = io.BytesIO()`, then convert that to a `PIL Image` using `im = Image.open(buf)`, `buf2 = io.BytesIO()`, `im.save(buf2, format='png')`, then encode that `im_str = base64.b64encode(buf2.getvalue()).decode()` and use `data_uri ='data:image/png;base64,'+im_str`, add that to my context. I will accept your answer, and post more details how I modified my code in another answer below. Thanks for all the help! – Jared H Oct 05 '20 at 20:34
1

Per @Juho Rutila's answer, I changed the response in my views.py to a render(request, 'gtdefault.html', context). To encode the image in base64, I had to go through PIL's Image, then from PIL's Image to base64. I also removed submitted from my code and instead used request.method == 'POST'. Thanks again @Juho Rutila!

I'm sure there may be a less round-about way of doing it, but this was the first method I could get to work.

My modified views.py:

import io
import base64
from PIL import Image

def grapher_tool_input(request):
    if request.method == 'POST':
        form = GraphingInput(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            fig = graph(cd['left_end'], cd['right_end'], cd['top'], cd['bottom'], cd['function'])
            buf = io.BytesIO()
            fig.savefig(buf, format='png')
            im = Image.open(buf)
            buf2 = io.BytesIO()
            im.save(buf2, format='png')
            im_str = base64.b64encode(buf2.getvalue()).decode()
            data_uri = 'data:image/png;base64,'
            data_uri += im_str
            context = dict()
            context['data'] = data_uri
            return render(request, 'gtdefault.html', context)
    else:
        form = GraphingInput(initial={'left_end':-5, 'right_end':5, 'bottom':-5, 'top':5})
        context ={
            'form': form,
        }
    return render(request, 'gtdefault.html', context)

My modified gtdefault.html:

{% extends 'base.html' %}

{% block content %}
{% if request.method == 'POST' %}
    <img src={{ data }} alt="" height="250" ,width="250">
{% else %}
    <form action="" method="post" novalidate>
        <table>
            {{ form.as_table }}
        </table>
        <input type="submit" value="Graph">
    {% csrf_token %}
    </form>
{% endif %}
{% endblock content %}
Jared H
  • 83
  • 4