Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow slice and fancy indexing to only show some markers #2662

Merged

Conversation

rtrwalker
Copy link
Contributor

The markevery keword argument can be passed to plt.plot in order to plot a marker every N points. I've expanded the functionality to allow using slices and fancy indexing to specify specific points to plot the markers with. This avoids having to plot two series (the line, and the markers) when putting a marker on a custom subset of points. Goes someway to addressing:
#1981
http://stackoverflow.com/questions/5318639/matplotlib-ugly-plots-when-using-markers
http://stackoverflow.com/questions/17406758/plotting-a-curve-with-equidistant-arc-length-markers
http://stackoverflow.com/questions/2040306/plot-with-fewer-markers-than-data-points-or-a-better-way-to-plot-cdfs-matplo
http://stackoverflow.com/questions/8409095/matplotlib-set-markers-for-individual-points-on-a-line

for example:

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0,8,35)
y = np.sin(x)
plt.plot(x,y,'o', ls='-')
plt.plot(x,y+1,'s', ls='-', markevery = 4) #old functionality
plt.plot(x,y+2,'^', ls='-', markevery = [2,4]) #old functionality
plt.plot(x,y+3,'h', ls='-', markevery=[4,7,12,15,16]) #new functionality
plt.plot(x,y+4,'+', ls='-', markevery=slice(4,9,2)) #new functionality
plt.show()

figure_1

every=slice(i, j + 1, j - i) otherwise every j-th point starting
at point i will have a marker.

ACCEPTS: None | integer | (startind, stride) | slice object | list
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as you are touching the doc-string can you re-write it in the numpy-doc style?

@tacaswell
Copy link
Member

All of the test failures are PEP8 violations (specifically trailing white space).

Your editor should have an option to truncate trailing white space on save. It is also worth your time to figure out how to get in-line pep8 testing in your editor. For better or worse, one of our tests is pep8 compliance, taking care of it as you code saves time.

# as a work-around use markevery = slice(i,j+1,j-i)
# to show only point numbers i and j.
startind, stride = markevery
inds = slice(startind, None, stride)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the behaviour where if you just so happen to have 2 indices the behaviour changes.
I'd be tempted to raise a warning here stating that the behaviour has changed and in the warning give an example of how to use the new behaviour, but no matter what, an iterable is considered to be a fancy index.

Obviously this is a backwards incompatible change, so we will need an entry in the docs/api/api_changes.rst.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be left as it is (len 2 -> old method) but with a warning for a release (1.4) before being change over entirely to the new behavior.

Changing how a length 2 iterable is interpreted will break too much existing code.

It would add more complexity, but it might be worth adding an rcparam for newstyle vs oldstyle to smooth over the transition.

@rtrwalker
Copy link
Contributor Author

Here's a newbie question: I've added a bit more functionality to my code above to allow markers at a roughly fixed display coordinate distance along the plot; should I push my changes to this pull request or do a completely new pull request?

@tacaswell
Copy link
Member

Depends. I think this is a good feature to have and should be merged (modulo you cleaning up pep8 and adressing @pelson 's concerns about length 2 iterabels being special).

Adding more (related) features to this PR could make it take longer to get sorted and merged, which argues for making a new PR.

On the other hand, if the additional code depends on this PR, then it can't be merged first and you will probably end up rebasing it multiple times due to changes from fixing this PR. The new PR would basically be on hold until this one is sorted out, so might as well add it into this one.

On the third hand, if the extra features are independent of this PR, then put them in their own PR (but you might still end up having to rebase repeatedly).

Evenly spaced markers in figure space does sound useful.

@rtrwalker
Copy link
Contributor Author

I've done two things in my lastest commit 54906e3.

  1. In the original docs I saw that the acceptable arguments were, None | int | length-2 tuple of int. i.e. according to the docs you can only use a length-2 tuple of int, whereas the code itself accepts a sequence of lenght 2. I thus decided to split the sequence behaviour into tuples and lists. The tuple handles the old behaviour, while the list/array handles the fancy indexing without any special cases. I assume most people would have followed the docs and used a tuple so I don't think this will cause too many backwards compatibility issues.
  2. I have also added float aguments to markevery. This allows roughly evenly spaced markers in figure space. Lines with many data points tend to clutter up the figure space with markers. The new functionality: takes the axes-bounding-box-diagonal distance in display coordinates as length one; multiplies diagonal length by the markevery value to get the theoretical distance between markers; calculates the straight-line cummulative distance along the line; finds the closest actual points that match the theoretical distance along the line.

Here's some example code and plots:

from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

cases = [None,
         8,
         (30, 8),
         [16, 24, 30], [0,-1],
         slice(100,200,3),
         0.1, 0.3, 1.5,
         (0.0, 0.1), (0.45, 0.1)]

cols = 3
gs = gridspec.GridSpec(len(cases) // cols + 1, cols)

delta = 0.11
x = np.linspace(0, 10-2*delta, 200) + delta
y = np.sin(x) + 1.0 + delta

#normal
fig1 = plt.figure(num=1, figsize=(10,8))
ax=[]
for i, case in enumerate(cases):
    row = (i // cols)
    col = i % cols
    ax.append(fig1.add_subplot(gs[row,col]))
    ax[-1].set_title('markevery=%s' % str(case))
    ax[-1].plot(x, y,'o', ls='-', ms=4,  markevery=case)
fig1.tight_layout()

#logscale
fig2 = plt.figure(num=2, figsize=(10,8))
axlog=[]
for i, case in enumerate(cases):
    row = (i // cols)
    col = i % cols
    axlog.append(fig2.add_subplot(gs[row,col]))
    axlog[-1].set_title('markevery=%s' % str(case))
    axlog[-1].set_xscale('log')
    axlog[-1].set_yscale('log')
    axlog[-1].plot(x, y,'o', ls='-', ms=4,  markevery=case)
fig2.tight_layout()

#zoomed
fig3 = plt.figure(num=3, figsize=(10,8))
axzoom=[]
for i, case in enumerate(cases):
    row = (i // cols)
    col = i % cols
    axzoom.append(fig3.add_subplot(gs[row,col]))
    axzoom[-1].set_title('markevery=%s' % str(case))
    axzoom[-1].plot(x, y,'o', ls='-', ms=4,  markevery=case)
    axzoom[-1].set_xlim((6,6.7))
    axzoom[-1].set_ylim((1.1, 1.7))
fig3.tight_layout()



r = np.linspace(0, 3.0, 200)
theta = 2 * np.pi * r

#polar
fig4 = plt.figure(num=4, figsize=(10,8))
axpolar=[]
for i, case in enumerate(cases):
    row = (i // cols)
    col = i % cols
    axpolar.append(fig4.add_subplot(gs[row,col], polar = True))
    axpolar[-1].set_title('markevery=%s' % str(case))
    axpolar[-1].plot(theta, r,'o', ls='-', ms=4,  markevery=case)
fig4.tight_layout()


figure_1
figure_2
figure_3
figure_4

if isinstance(markevery, float):
markevery = (0.0, markevery)
if isinstance(markevery, tuple):
start, step = markevery
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should raise a value error if len(markevery) != 2 instead of users getting the cryptic error about too many/few values to unpack.

@tacaswell
Copy link
Member

This is good stuff!

I do not share your faith in users reading the documentation (mainly due to answering SO questions, a good fraction of which you can answer by copy/pasting the documentation).

I left some comments in the code, if we are going to have such complex type handling then it needs to be bullet proof (in part because users don't actually read the documentation).

The test failure for pep8 need to be cleaned up before this can be merged.

Can you add some or all of those very nice examples as tests?

inds = np.unique(inds)
elif isinstance(markevery, int):
start, step = 0, markevery
inds = slice(start, None, step)
else:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be an elif iterable and then have a final else which raises a ValueError

@rtrwalker
Copy link
Contributor Author

I'm out of my comfort zone with the testing I added in commit 0838b94 and b55fb1c. I didn't touch the existing markevery tests in test_axes.py, but added some more to test the new functionality.

I'm not really sure what happened with my branch merging but I think I got it sorted.

It's still possible to sneak through the type checking and ValueError checking. e.g. markevery='hello' will sneak through because a string is iterable. How many cases do we have to checkfor?

inds = markevery
elif iterable(markevery):
#fancy indexing
inds = markevery
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a test here that all elements of markevery are integers. This addresses markevery = 'hello' and markevery = [np.pi, .5].

I am not sure the best way to do this test.

@tacaswell
Copy link
Member

At a quick glance the test images look good to me and they are passing on travis.

I don't understand what is going on with that last commit on this branch. As it seems to be a no-op, I would just point your local branch at the commit before and use git push --force to remove it from the repo.

There are still pep8 issues:

======================================================================
FAIL: matplotlib.tests.test_coding_standards.test_pep8_conformance
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/travis/virtualenv/python3.3/lib/python3.3/site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/home/travis/virtualenv/python3.3/lib/python3.3/site-packages/matplotlib-1.4.x-py3.3-linux-x86_64.egg/matplotlib/tests/test_coding_standards.py", line 242, in test_pep8_conformance
    assert_pep8_conformance()
  File "/home/travis/virtualenv/python3.3/lib/python3.3/site-packages/matplotlib-1.4.x-py3.3-linux-x86_64.egg/matplotlib/tests/test_coding_standards.py", line 226, in assert_pep8_conformance
    assert_equal(result.total_errors, 0, msg)
AssertionError: 6 != 0 : Found code syntax errors (and warnings):
/home/travis/virtualenv/python3.3/lib/python3.3/site-packages/matplotlib-1.4.x-py3.3-linux-x86_64.egg/matplotlib/lines.py:636:80: E501 line too long (80 > 79 characters)
/home/travis/virtualenv/python3.3/lib/python3.3/site-packages/matplotlib-1.4.x-py3.3-linux-x86_64.egg/matplotlib/lines.py:646:79: E231 missing whitespace after ','
/home/travis/virtualenv/python3.3/lib/python3.3/site-packages/matplotlib-1.4.x-py3.3-linux-x86_64.egg/matplotlib/lines.py:646:80: E501 line too long (90 > 79 characters)
/home/travis/virtualenv/python3.3/lib/python3.3/site-packages/matplotlib-1.4.x-py3.3-linux-x86_64.egg/matplotlib/lines.py:646:82: E231 missing whitespace after ','
/home/travis/virtualenv/python3.3/lib/python3.3/site-packages/matplotlib-1.4.x-py3.3-linux-x86_64.egg/matplotlib/lines.py:646:85: E231 missing whitespace after ','
/home/travis/virtualenv/python3.3/lib/python3.3/site-packages/matplotlib-1.4.x-py3.3-linux-x86_64.egg/matplotlib/lines.py:650:80: E501 line too long (92 > 79 characters)
----------------------------------------------------------------------

@rtrwalker
Copy link
Contributor Author

Hopefully I've cleared up the pep8 errors.

To check for a valid iterable, because there are a number of valid ways to specify a valid numpy fancy index (list of int, array of int, list of bool etc. ) I put in a try except block. This ultimately results in some duplication in that for a valid iterable tpath.vertices[inds] will be caluclated twice. I don't know haow to avoid this without making the code less clear.

functionality as `every`=0.1 is exhibited but the first marker will
be 0.5 multiplied by the display-cordinate-diagonal-distance along
the line.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you do this as a bullet list?

@tacaswell
Copy link
Member

This is great functionality, but the behavior change based on the type of every is starting to be worrying (tuple of int vs tuple of float). This could turn into a support nightmare.

I've tagged it as 1.4 so that it gets looked at by the core devs soonish.

@rtrwalker
Copy link
Contributor Author

I reworked the parameters section of set_markevery into a more readable bullet list. I had some trouble using backticks for the every variable in the bullet list. When I referred to every in a bullet point, the first backtick would get rendered as a hyperlink. Normally when using numpydoc every would result in every being rendered without backticks but in italics (as for the markevery at the top of the docstring). I ended up just removing the backticks in the bullet points.


Notes
-----
Using `markevery` will only show markers at actual data points. When
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be every not markevery? In either case, it should be consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure which to use. every is the argument to the method set_markevery whereas markevery is a keyword argument of plt.plot. I'll try and delineate between every the method argument, and the markevery property.

@tacaswell
Copy link
Member

@rtrwalker Will you be able to address the comments in the near future?

@tacaswell
Copy link
Member

note to self: once this is merged someone should add an answer to https://stackoverflow.com/questions/8409095/matplotlib-set-markers-for-individual-points-on-a-line advertising this feature.

@rtrwalker
Copy link
Contributor Author

I've been out of action for a bit. I will address the comments soon.

@pelson
Copy link
Member

pelson commented Jan 17, 2014

(note you will need two positive 'really-ies' to out weigh my one negative 'really' :))

😄 - well here's one from me - I'd like to remove the logic from the draw method to keep it readable. @tacaswell - I give you the casting vote.

@tacaswell
Copy link
Member

I just did it my self. I thought it should be done, but felt bad telling @rtrwalker to do it. There is a PR against this branch.

@rtrwalker
Copy link
Contributor Author

I'm not sure why the last travis build (commit 33ca595) failed. It passes for python 2.6, 2.7 and 3.3; it fails for python 3.2. Looking at the log, failure occurs for the multipe cases of matplotlib.tests.test_animation.test_save_animation_smoketest where a nose.plugins.multiprocess.TimedOutException is raised.

@tacaswell
Copy link
Member

That is in un related to this pr
On Jan 20, 2014 10:51 PM, "rtrwalker" notifications@github.com wrote:

I'm not sure why the last travis build (commit 33ca59533ca595)
failed. It passes for python 2.6, 2.7 and 3.3; it fails for python 3.2.
Looking at the log, failure occurs for the multipe cases of
matplotlib.tests.test_animation.test_save_animation_smoketest where a
nose.plugins.multiprocess.TimedOutException is raised.


Reply to this email directly or view it on GitHubhttps://github.com//pull/2662#issuecomment-32819357
.

@tacaswell
Copy link
Member

@rtrwalker Can you merge my PR against your branch so we can move this forward?

@tacaswell
Copy link
Member

erm, sorry for the noise, I am confused by the new github page layout and now see it is already done.

What this is missing still is entries in CHANGELOG, whats_new.rst and api_changes.rst.

@tacaswell
Copy link
Member

@rtrwalker Can you add the documentation details so we can get this merged?

@tacaswell
Copy link
Member

@rtrwalker Can you rebase this again?

@WeatherGod
Copy link
Member

@tacaswell This has been rebased, I think. I would love to see this added.

@tacaswell
Copy link
Member

It also needs the docs which is going to end up being my problem. It claims to still not merge cleanly...

@tacaswell
Copy link
Member

@rtrwalker Can you rebase this (yet) again?

@tacaswell tacaswell merged commit eaec4bd into matplotlib:master May 18, 2014
@tacaswell
Copy link
Member

patched up conflict by hand in 9d8561c

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants