Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 66 additions & 6 deletions chaco/data_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
from numpy import array, transpose

from traits.api import Bool, Enum, Instance, Property
from traits.api import Bool, Enum, Instance, Int, observe, Property, Union
from enable.api import color_table

from .abstract_overlay import AbstractOverlay
Expand Down Expand Up @@ -207,11 +207,71 @@ class DataView(OverlayPlotContainer):
#: Background color (overrides Enable Component)
bgcolor = "white"

#: Padding defaults.
padding_top = 50
padding_bottom = 50
padding_left = 50
padding_right = 50
# Padding defaults. These were converted to properties as a hacky fix for
# enthought/chaco#564
#: Top Padding default
padding_top = Property(
Copy link
Copy Markdown
Contributor Author

@aaronayres35 aaronayres35 May 12, 2021

Choose a reason for hiding this comment

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

if explicitly set (or if padding trait is sett, which then explicitly sets these), that value will always be used. Setting sets the shadow traits (with leading underscore), and in the getters we explicitly check if the shadow trait is set and return it. Otherwise we then check for axis titles and adjust as needed either returning 80 or 50 as a default.

Note I am just now considering if a user explicitly sets one off these traits to 0 I dont think it will work as then we will not enter the if getattr(self, MAP[side]) block. This is a bug that needs fixing.
EDIT: fixed

observe='y_axis.[title,orientation], x_axis.[title,orientation]'
)
#: Bottom Padding default
padding_bottom = Property(
observe='y_axis.[title,orientation], x_axis.[title,orientation]'
)
#: Left Padding default
padding_left = Property(
observe='y_axis.[title,orientation], x_axis.[title,orientation]'
)
#: Right Padding default
padding_right = Property(
observe='y_axis.[title,orientation], x_axis.[title,orientation]'
)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I handle all 4 cases because it is possible to set axis orientation to "top" or "bottom" as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

as an example, this code:

rom numpy import linspace, sin
from traits.api import HasTraits, Instance
from traitsui.api import View, Item
from chaco.api import Plot, ArrayPlotData
from enable.api import ComponentEditor

class LinePlot(HasTraits):
    plot = Instance(Plot)

    traits_view = View(
        Item('plot',editor=ComponentEditor(), show_label=False),
        width=500, height=500, resizable=True, title="Chaco Plot")

    def _plot_default(self):
        x = linspace(-14, 14, 100)
        y = sin(x) * x**3
        plotdata = ArrayPlotData(x=x, y=y)

        plot = Plot(plotdata)
        plot.plot(("x", "y"), type="line", color="blue")
        plot.title = "sin(x) * x^3"

        plot.padding_left = 0
        
        plot.x_axis.title = "X AXIS"
        plot.y_axis.title = "Y AXIS"
        
        plot.y_axis.orientation = "top"
       
        return plot

if __name__ == "__main__":
    LinePlot().configure_traits()

yields this on master:
Screen Shot 2021-05-12 at 9 38 47 AM

and this on this branch:
Screen Shot 2021-05-12 at 9 39 02 AM

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The extra padding on the bottom on this branch (80 rather than 50) I'm not a huge fan of. This is due to the simplistic nature of just adding space if we have an axis title. As you can see, at the top we need extra space so axis title doesn't overlap with plot title. Similarly left right we need it (hence the original issue), because axis ticks / values take up some of the spacing and sort of push the axis title further away from the plot. The x axis doesn't usually have this problem because of the orientation of the numbers on the ticks. In other words the text for 1000 for example is much wider than it is tall, and 100000 even wider, but the same height.

In reality an ideal solution would calculate explicitly the extent of the ticks, and the size of the axis title and allocate spacing to perfectly fit what we need. The awkward thing about this code feels like it would live in a draw method inside the axis itself, but the DataView is where the padding is held. Should the Axis take control over the DataViews padding? That feels wrong.
In any case that is a separate issue / approach entirely. I am just pointing out something I don't love about the current solution.

Perhaps we just make the assumption that padding_bottom can be 50 regardless of the presence or absence of an axis title? We were just assuming 50 always before anyway.


_padding_top = Union(None, Int())
_padding_bottom= Union(None, Int())
_padding_left = Union(None, Int())
_padding_right = Union(None, Int())
Comment on lines +229 to +232
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@rahulporuri
I looked into it and Int(allow_none=True) does not work / is not implemented.

>>> from traits.api import HasTraits, Instance, Int
>>> class Thing(HasTraits):
...     a = Int()
...     b = Int(allow_none=True)
...     c = Instance(HasTraits, allow_none=False)
...     d = Instance(HasTraits)  # default value of allow_none for Instance trait type is True
... 
>>> thing = Thing()
>>> thing.d = None
>>> thing.c = None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/aayres/Desktop/traits/traits/base_trait_handler.py", line 75, in error
    object, name, self.full_info(object, name, value), value
traits.trait_errors.TraitError: The 'c' trait of a Thing instance must be a HasTraits, but a value of None <class 'NoneType'> was specified.
>>> thing.b = None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/aayres/Desktop/traits/traits/base_trait_handler.py", line 75, in error
    object, name, self.full_info(object, name, value), value
traits.trait_errors.TraitError: The 'b' trait of a Thing instance must be an integer, but a value of None <class 'NoneType'> was specified.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this deserves an issue in traits - traits should explicitly complain during class definition instead of at runtime.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That makes sense, for my own understanding we want to catch a user setting allow_none in a trait definition correct? I suspect, since allow_none is not a supported arg/kwarg for the Int trait type, it is now being used as metadata? Ie one could do something like @observe('+allow_none')?
We should probably special handle this case


def _find_padding(self, side):
SIDE_TO_TRAIT_MAP = {
"top": "_padding_top",
"bottom": "_padding_bottom",
"left": "_padding_left",
"right": "_padding_right",
}
if getattr(self, SIDE_TO_TRAIT_MAP[side]) is not None:
return getattr(self, SIDE_TO_TRAIT_MAP[side])
else:
if self.y_axis:
if (self.y_axis.title and self.y_axis.orientation == side):
return 80
if self.x_axis:
if (self.x_axis.title and self.x_axis.orientation == side):
return 80
return 50

def _get_padding_top(self):
return self._find_padding("top")

def _get_padding_bottom(self):
return self._find_padding("bottom")

def _get_padding_left(self):
return self._find_padding("left")

def _get_padding_right(self):
return self._find_padding("right")

def _set_padding_top(self, value):
self._padding_top = value

def _set_padding_bottom(self, value):
self._padding_bottom = value

def _set_padding_left(self, value):
self._padding_left = value

def _set_padding_right(self, value):
self._padding_right = value

border_visible = True

Expand Down
41 changes: 40 additions & 1 deletion chaco/tests/test_data_view.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
import unittest

from chaco.api import DataRange2D, DataView, GridDataSource
from numpy import linspace, sin

from traits.api import Enum, HasTraits, Instance
from traitsui.api import UItem, View
from chaco.api import (
ArrayPlotData, DataRange2D, DataView, GridDataSource, Plot
)
from enable.api import ComponentEditor


class DummyPaddingPlot(HasTraits):
orientation = Enum("top", "bottom", "left", "right")
plot = Instance(Plot)

traits_view = View(
UItem(
'plot',
editor=ComponentEditor()
),
)

def _plot_default(self):
x = linspace(0, 10, 10)
plotdata = ArrayPlotData(x=x, y=x)

plot = Plot(plotdata)
plot.plot(("x", "y"), type="line", color="blue")

plot.x_axis.title = "X AXIS"
plot.x_axis.orientation = self.orientation

return plot


class DataViewTestCase(unittest.TestCase):
Expand Down Expand Up @@ -35,3 +66,11 @@ def test_range2d_changed(self):
self.assertTrue(old_range.sources == [])
self.assertTrue(dv.range2d.x_range is dv.index_mapper.range)
self.assertTrue(dv.range2d.y_range is dv.value_mapper.range)

# regression test for enthought/chaco#735
def test_padding_with_axis_title(self):
for orientation in ["top", "bottom", "left", "right"]:
dummy_plot = DummyPaddingPlot(orientation=orientation)
self.assertEqual(
getattr(dummy_plot.plot, "padding_" + orientation), 80
)