Forum Archive

Putting a matplotlib.plot() image into a ui.ImageView?

ccc

matplotlib.plot() puts an image onto the Pythonista console. I can tap and hold on that image to copy it to the clipboard, etc.

Is there a programmatic way (image_context, etc.) to move a plot() image into a ui.ImageView?

JonB

show() puts it in the console... iirc there is another option which copies an image.

omz

You can use savefig to save the plot to a file or file-like object. Here's an example using BytesIO:

import matplotlib.pyplot as plt
from io import BytesIO
import ui

plt.plot([1, 2, 3])

b = BytesIO()
plt.savefig(b)
img = ui.Image.from_data(b.getvalue())

img_view = ui.ImageView(background_color='white')
img_view.content_mode = ui.CONTENT_SCALE_ASPECT_FIT
img_view.image = img
img_view.present()
ccc

Awesome (as always). Thanks. Now, how do I make it pinchable, zoomable, scrollable?

# coding: utf-8
import matplotlib.pyplot as plt
import io, math, ui

# plt must be matplotlib.pyplot or its alias
def plot_to_scrollable_image_view(plt):
    img_view = ui.ImageView()
    b = io.BytesIO()
    plt.savefig(b)
    img_view.image = ui.Image.from_data(b.getvalue())
    view = ui.ScrollView()
    view.add_subview(img_view)
    return view

plt.plot([math.sin(x/10.0) for x in xrange(95)])
plt.xlim(0, 94.2)
view = plot_to_scrollable_image_view(plt)
view.present()
img_view = view.subviews[0]
img_view.frame = view.bounds
img_view.width *= 2
img_view.height *= 2
view.content_size = img_view.width, img_view.height

EDIT: It is now draggable.

ccc

How do I make it pinchable, zoomable?

omz

How do I make it pinchable, zoomable?

There isn't really an easy way to do that, I'm afraid. Your best bet may be to save the image to a file, and then load it in a ui.WebView (a base64-encoded data: URL may also work, not sure about that right now).

JonB

ccc, i think we really want a custom view which sets axis limits, which is relayed to plt, then a new image generated. not sure how slow that will be...

Webmaster4o

@JonB I think it'd be better to PIL crop the previously-generated image.

JonB

that wont be feasible if you want to zoom, otherwise you lose resolution. as an example, somone here is trying to be able to zoom into one hour from a 24 hour graph. that means you'd need to generate the original image at over 2000 dpi in order to do a simple crop with a final rsolution of 92dpi. try that in matplotlib, it is very slow, plus you then need to read and write very large files, which will be slow as well.

here is a proof of concept of calling matplotlib repeatedly in the ui loop.
The key to responsiveness is to generate very low quality images in matplotlib (say, 16 dpi) while dragging, then replace this with a higher quality version once the dragging stops.

I'm working on a custom touch view, which will implement zooming, two finger scrolling, to see how this scales with more complex plots.

# coding: utf-8
import matplotlib.pyplot as plt
import io, math, ui


def plot_to_scrollable_image_view(plt):
    img_view = ui.ImageView()
    b = io.BytesIO()
    plt.savefig(b,format='png',dpi=160)
    img_view.image = ui.Image.from_data(b.getvalue())
    view = ui.ScrollView()
    view.add_subview(img_view)
    view.dx=0
    view.ready=True
    view.bounces=False
    return view
class delegate(object):
   #@ui.in_background  #ui.delay called from backgrounded was unreliable.
   def scrollview_did_scroll(self,sender):
      ui.cancel_delays()
      d = sender.content_offset[0]-5.0
      sender.content_offset=(5,5)
      sender.dx=d+sender.dx
      def hq():
        sender.ready=False
        dx=sender.dx
        sender.dx=0
        xl=plt.xlim()
        xl=[x+dx for x in xl]
        plt.xlim(xl)
        b = io.BytesIO()
        plt.savefig(b,format='jpeg',dpi=160)
        sender.subviews[0].image = ui.Image.from_data(b.getvalue())
        sender.ready=True
      if sender.ready:
        sender.ready=False
        dx=sender.dx
        sender.dx=0
        xl=plt.xlim()
        xl=[x+dx for x in xl]
        plt.xlim(xl)
        b = io.BytesIO()
        plt.savefig(b,format='jpeg',dpi=16)
        sender.subviews[0].image = ui.Image.from_data(b.getvalue())
        sender.ready=True
      ui.delay(hq,0.2)


plt.plot([math.sin(x/10.0) for x in xrange(95)])
plt.xlim(0, 94.2)
view = plot_to_scrollable_image_view(plt)
view.present()
view.subviews[0].frame = view.bounds
view.subviews[0].x,view.subviews[0].y=(5,5)
view.content_size=tuple(view.subviews[0].bounds.size+(11,11))
view.content_offset=(5,5)
view.delegate=delegate()
ccc

Sweet! I updated a few things:

  • raised the x limit from 95 to 950 to increase the content that can be scrolled through
  • use subplots_adjust() to reduce white space around the graph
  • reuse hq() to remove repeated lines
  • reduce ui.delay to zero to make refresh more snappy. Are there downsides to this?

I not figure out how to stop the scrolling when the graph ends (i.e. below zero or above 950)

I will try to apply the lessons learned to SPLnFFT_Reader.py.

# coding: utf-8
import matplotlib.pyplot as plt
import io, math, ui

def plot_to_scrollable_image_view(plt):
    img_view = ui.ImageView()
    b = io.BytesIO()
    plt.savefig(b, format='png', dpi=160)
    img_view.image = ui.Image.from_data(b.getvalue())
    view = ui.ScrollView()
    view.add_subview(img_view)
    view.delegate = delegate()
    view.dx = 0
    view.ready = True;
    view.bounces = False
    return view

class delegate(object):
   #@ui.in_background  #ui.delay called from backgrounded was unreliable.
   def scrollview_did_scroll(self,sender):
      ui.cancel_delays()
      sender.dx += sender.content_offset[0] - 5.0
      sender.content_offset = (5, 5)
      def hq(dpi=160):
        sender.ready=False
        dx, sender.dx = sender.dx, 0
        plt.xlim([x + dx for x in plt.xlim()])
        b = io.BytesIO()
        plt.savefig(b, format='jpeg', dpi=dpi)
        sender.subviews[0].image = ui.Image.from_data(b.getvalue())
        sender.ready = True
      if sender.ready:
        hq(16)
      ui.delay(hq, 0)

plt.plot([math.sin(x/10.0) for x in xrange(950)])
plt.xlim(0, 95)  # approx 1 cycle of the sin wave
plt.subplots_adjust(left=0.06, bottom=0.05, right=0.98, top=0.97)
view = plot_to_scrollable_image_view(plt)
view.hidden = True  # wait until view is setup before displaying
view.present()
img_view = view.subviews[0]
img_view.frame = view.bounds
img_view.x, img_view.y = view.content_offset = (5, 5)
view.content_size = tuple(img_view.bounds.size + (11, 11))
view.hidden = False
ccc

Sweet! I updated a few things:

  • raised the x limit from 95 to 950 to give us more content to scroll through
  • use subplots_adjust() to reduce white space around the graph
  • reuse hq() to remove repeated lines of code
  • reduce ui.delay to zero to make refresh more snappy. Are there downsides to doing this?

I could not figure out how to stop the scrolling when the graph ends (i.e. below zero or above 950)

I will try to apply the lessons learned to SPLnFFT_Reader.py.

# coding: utf-8
import matplotlib.pyplot as plt
import io, math, ui

def plot_to_scrollable_image_view(plt):
    img_view = ui.ImageView()
    b = io.BytesIO()
    plt.savefig(b, format='png', dpi=160)
    img_view.image = ui.Image.from_data(b.getvalue())
    view = ui.ScrollView()
    view.add_subview(img_view)
    view.delegate = delegate()
    view.dx = 0
    view.ready = True;
    view.bounces = False
    return view

class delegate(object):
   #@ui.in_background  #ui.delay called from backgrounded was unreliable.
   def scrollview_did_scroll(self,sender):
      ui.cancel_delays()
      sender.dx += sender.content_offset[0] - 5.0
      sender.content_offset = (5, 5)
      def hq(dpi=160):
        sender.ready=False
        dx, sender.dx = sender.dx, 0
        plt.xlim([x + dx for x in plt.xlim()])
        b = io.BytesIO()
        plt.savefig(b, format='jpeg', dpi=dpi)
        sender.subviews[0].image = ui.Image.from_data(b.getvalue())
        sender.ready = True
      if sender.ready:
        hq(16)
      ui.delay(hq, 0)

plt.plot([math.sin(x/10.0) for x in xrange(950)])
plt.xlim(0, 95)  # approx 1 cycle of the sin wave
plt.subplots_adjust(left=0.06, bottom=0.05, right=0.98, top=0.97)
view = plot_to_scrollable_image_view(plt)
view.hidden = True  # wait until view is setup before displaying
view.present()
img_view = view.subviews[0]
img_view.frame = view.bounds
img_view.x, img_view.y = view.content_offset = (5, 5)
view.content_size = tuple(img_view.bounds.size + (11, 11))
view.hidden = False
JonB

in retrospect, the whole ready checking is not needed if we dont run in the background. my original attempt had long update times, so backgrounding was neded to keep ui responsive (basicslly adding up motion, then drawing when ready).

it isnt too hard to have the y axis scroll control "width".
also, it would be possible to stop once the xlim()[0] < 0, etc
we also need to properly scale screen width to xlim axes, so finger motion tracks correctly.

howver, this approach did not scale well to a plot with 600000 points, since even a low dpi plot took many seconds to generate.
i think we need to actually generate a new plot each time, only plotting the portion of data within the view, and reducing the amount of data points for large plots. i.e if zoomed out, resample data to an appropriate number of points.