Python / Matplotlib - Чи є спосіб зробити розривну вісь?


78

Я намагаюся створити графік за допомогою pyplot, який має розривну вісь x. Зазвичай це малюється так, що вісь матиме приблизно такий вигляд:

(значення) ---- // ---- (пізніші значення)

де // вказує на те, що ви пропускаєте все між (значеннями) і (пізнішими значеннями).

Я не зміг знайти жодного прикладу цього, тому мені цікаво, чи це взагалі можливо. Я знаю, що ви можете об’єднати дані через розрив, наприклад, для фінансових даних, але я хотів би зробити стрибок по осі більш явним. На даний момент я просто використовую підзаговори, але я б дуже хотів, щоб у підсумку все опинилося на одному графіку.

Відповіді:


84

Відповідь Павла - цілком чудовий спосіб зробити це.

Однак, якщо ви не хочете робити власну трансформацію, ви можете просто використовувати дві підзаголовки, щоб створити однаковий ефект.

Замість того, щоб складати приклад з нуля, є чудовий приклад цього, написаний Павлом Івановим у прикладах matplotlib (Це лише в поточній підказці git, оскільки це було зроблено лише кілька місяців тому. На веб-сторінці ще немає). .

Це лише проста модифікація цього прикладу, щоб замість осі y мати неперервну вісь x. (Саме тому я роблю цю публікацію CW)

В основному, ви просто робите щось подібне:

import matplotlib.pylab as plt
import numpy as np

# If you're not familiar with np.r_, don't worry too much about this. It's just 
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)

fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)

# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')

# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only

# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()

# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)

plt.show()

введіть тут опис зображення

Щоб додати //ефект ламаних ліній осі , ми можемо зробити це (знову ж таки, змінено на прикладі Павла Іванова):

import matplotlib.pylab as plt
import numpy as np

# If you're not familiar with np.r_, don't worry too much about this. It's just 
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)

fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)

# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')

# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only

# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()

# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)

# This looks pretty good, and was fairly painless, but you can get that
# cut-out diagonal lines look with just a bit more work. The important
# thing to know here is that in axes coordinates, which are always
# between 0-1, spine endpoints are at these locations (0,0), (0,1),
# (1,0), and (1,1). Thus, we just need to put the diagonals in the
# appropriate corners of each of our axes, and so long as we use the
# right transform and disable clipping.

d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
ax.plot((1-d,1+d),(-d,+d), **kwargs) # top-left diagonal
ax.plot((1-d,1+d),(1-d,1+d), **kwargs) # bottom-left diagonal

kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d,d),(-d,+d), **kwargs) # top-right diagonal
ax2.plot((-d,d),(1-d,1+d), **kwargs) # bottom-right diagonal

# What's cool about this is that now if we vary the distance between
# ax and ax2 via f.subplots_adjust(hspace=...) or plt.subplot_tool(),
# the diagonal lines will move accordingly, and stay right at the tips
# of the spines they are 'breaking'

plt.show()

введіть тут опис зображення


11
Я б сам не міг сказати цього краще;)
Павло Іванов

3
//Здається, метод отримання ефекту працює добре, лише якщо співвідношення допоміжних цифр дорівнює 1: 1. Чи знаєте ви, як змусити його працювати з будь-яким співвідношенням, введеним, наприклад GridSpec(width_ratio=[n,m])?
Фредерік Норд,

Фантастичний. З невеликими модифікаціями це може працювати для будь-якої кількості секцій осі х.
Крістіан Медсен,

Фредерік Норд правильний. Більше того, /ефект не пригнічує нормального кліща, що естетично
дратує

30

Я бачу багато пропозицій щодо цієї функції, але жодних ознак того, що вона впроваджена. Ось дієве рішення на даний момент. Він застосовує покрокове перетворення функції до осі х. Це багато коду, але він досить простий, оскільки більшість із них є типовими шаблонами. Я не додав жодної графіки, щоб вказати місце перерви, оскільки це питання стилю. Удачі вам у закінченні роботи.

from matplotlib import pyplot as plt
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
import numpy as np

def CustomScaleFactory(l, u):
    class CustomScale(mscale.ScaleBase):
        name = 'custom'

        def __init__(self, axis, **kwargs):
            mscale.ScaleBase.__init__(self)
            self.thresh = None #thresh

        def get_transform(self):
            return self.CustomTransform(self.thresh)

        def set_default_locators_and_formatters(self, axis):
            pass

        class CustomTransform(mtransforms.Transform):
            input_dims = 1
            output_dims = 1
            is_separable = True
            lower = l
            upper = u
            def __init__(self, thresh):
                mtransforms.Transform.__init__(self)
                self.thresh = thresh

            def transform(self, a):
                aa = a.copy()
                aa[a>self.lower] = a[a>self.lower]-(self.upper-self.lower)
                aa[(a>self.lower)&(a<self.upper)] = self.lower
                return aa

            def inverted(self):
                return CustomScale.InvertedCustomTransform(self.thresh)

        class InvertedCustomTransform(mtransforms.Transform):
            input_dims = 1
            output_dims = 1
            is_separable = True
            lower = l
            upper = u

            def __init__(self, thresh):
                mtransforms.Transform.__init__(self)
                self.thresh = thresh

            def transform(self, a):
                aa = a.copy()
                aa[a>self.lower] = a[a>self.lower]+(self.upper-self.lower)
                return aa

            def inverted(self):
                return CustomScale.CustomTransform(self.thresh)

    return CustomScale

mscale.register_scale(CustomScaleFactory(1.12, 8.88))

x = np.concatenate((np.linspace(0,1,10), np.linspace(9,10,10)))
xticks = np.concatenate((np.linspace(0,1,6), np.linspace(9,10,6)))
y = np.sin(x)
plt.plot(x, y, '.')
ax = plt.gca()
ax.set_xscale('custom')
ax.set_xticks(xticks)
plt.show()

введіть тут опис зображення


Думаю, що це просто доведеться робити поки що. Це буде мій перший раз, коли я возитимуться зі спеціальними сокирами, тож нам просто доведеться подивитися, як це буде.
Justin S

Існує невелика помилка в def transformпро InvertedCustomTransform, де слід читати self.upperзамість upper. Дякую за чудовий приклад!
Девід Цвікер

Ви можете додати пару рядків, щоб показати, як користуватися своїм класом?
Ruggero Turra,

@RuggeroTurra Це все є в моєму прикладі. Ймовірно, вам просто потрібно прокрутити до кінця блоку коду.
Пол

2
Приклад не працює для мене на matplotlib 1.4.3: imgur.com/4yHa9be . Схоже, ця версія розпізнає лише transform_non_affineзамість transform. Подивіться мій патч на stackoverflow.com/a/34582476/1214547 .
пастафаріан

25

Перевірте пакет брокерів :

import matplotlib.pyplot as plt
from brokenaxes import brokenaxes
import numpy as np

fig = plt.figure(figsize=(5,2))
bax = brokenaxes(xlims=((0, .1), (.4, .7)), ylims=((-1, .7), (.79, 1)), hspace=.05)
x = np.linspace(0, 1, 100)
bax.plot(x, np.sin(10 * x), label='sin')
bax.plot(x, np.cos(10 * x), label='cos')
bax.legend(loc=3)
bax.set_xlabel('time')
bax.set_ylabel('value')

приклад із брейкнаксів


Неможливо from brokenaxes import brokenaxesв Pycharm Community 2016.3.2 після встановлення. @ ben.dichter
emmmphd

1
Виникла помилка. Я полагодив це. Запустіть, pip install brokenaxes==0.2щоб встановити фіксовану версію коду.
ben.dichter

Здається, погано взаємодіє з ax.grid (True)
innisfree

1
Чи можуть зламані сокири придушити кліща? Або відформатувати осі ближче одна до одної по горизонталі?
ifly6

1
Привіт, Бен, я хочу видалити вісь y, однак, я спробував кілька команд, але не працює належним чином у поєднанні з брейк-оксами, (примітка осі х - це зламана вісь), thx
user3737702

0

Звертаючись до питання Фредеріка Норда, як забезпечити паралельну орієнтацію діагональних «розривних» ліній при використанні таблиці координат із нерівними співвідношеннями 1: 1, наступні зміни, засновані на пропозиціях Пола Іванова та Джо Кінгтона, можуть бути корисними. Коефіцієнт ширини можна варіювати, використовуючи змінні n і m.

import matplotlib.pylab as plt
import numpy as np
import matplotlib.gridspec as gridspec

x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)

n = 5; m = 1;
gs = gridspec.GridSpec(1,2, width_ratios = [n,m])

plt.figure(figsize=(10,8))

ax = plt.subplot(gs[0,0])
ax2 = plt.subplot(gs[0,1], sharey = ax)
plt.setp(ax2.get_yticklabels(), visible=False)
plt.subplots_adjust(wspace = 0.1)

ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')

ax.set_xlim(0,1)
ax2.set_xlim(10,8)

# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()

d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)

on = (n+m)/n; om = (n+m)/m;
ax.plot((1-d*on,1+d*on),(-d,d), **kwargs) # bottom-left diagonal
ax.plot((1-d*on,1+d*on),(1-d,1+d), **kwargs) # top-left diagonal
kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d*om,d*om),(-d,d), **kwargs) # bottom-right diagonal
ax2.plot((-d*om,d*om),(1-d,1+d), **kwargs) # top-right diagonal

plt.show()

0

Для зацікавлених я розширив відповідь @ Paul і додав її до пакета matplotlib ProPlot . Він може виконувати "стрибки", "прискорення" та "уповільнення" .

В даний час неможливо додати "хрестики", які вказують на дискретний стрибок, як у відповіді Джо, але я планую додати це в майбутньому. Я також планую додати "локатор тиків" за замовчуванням, який встановлює розумні місця тиків за замовчуванням залежно від CutoffScaleаргументів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.