diff --git a/docs/source/user_manual/images/tutorial_hyetograph_final.png b/docs/source/user_manual/images/tutorial_hyetograph_final.png index 27c64d5cf..8e1b4bd55 100644 Binary files a/docs/source/user_manual/images/tutorial_hyetograph_final.png and b/docs/source/user_manual/images/tutorial_hyetograph_final.png differ diff --git a/docs/source/user_manual/images/tutorial_hyetograph_nodata.png b/docs/source/user_manual/images/tutorial_hyetograph_nodata.png index b6d5b7c17..98f3b230d 100644 Binary files a/docs/source/user_manual/images/tutorial_hyetograph_nodata.png and b/docs/source/user_manual/images/tutorial_hyetograph_nodata.png differ diff --git a/docs/source/user_manual/tutorial_hyetograph.rst b/docs/source/user_manual/tutorial_hyetograph.rst index 09850fbe5..2cda43010 100644 --- a/docs/source/user_manual/tutorial_hyetograph.rst +++ b/docs/source/user_manual/tutorial_hyetograph.rst @@ -24,52 +24,35 @@ specifying the Curve Number (determined based on the permeability of the soil) a plot shows the intensity vs. time hyetograph plots. -Development Setup -================= - -To run this demo you must have Chaco and its dependencies installed: - -* Traits -* TraitsUI -* Enable - - -Why use Traits for this application? -==================================== - -1. **Event notification** Every time that a trait is changed it sends - out notification to all listening functions. This means when a - trait is changed in places such as the UI the program will then - notify other traits or functions automatically. - -2. **Typing** Within traits you are allowed to define trats as certain - types. Say you have a trait named Name, you can then define it to - be a string. Then when you visualize it using the UI, it will - interprit the data as a string. - -3. **UI-Generation** After setting up your traits and performing all - the calculations, the Trait's will automatically generate a GUI - view without needing any additional programming. - - Importing the necessary functions ================================= -In This tutorial we will be using numpy, traits, traitsui, and chaco. -In calling your function you want to specify where the function is and -then import it. The following code snippet imports all the names that -will be used for our application. :: +In this example we will be using numpy, traits, traitsui, and chaco. +The following code snippet imports all the names that will be used for our +application, and defines our tiny database of coefficients. + +:: + from chaco.api import ArrayPlotData, Plot + from enable.api import ComponentEditor from traits.api import ( HasTraits, + Instance, Int, Range, Array, Enum, - on_trait_change, + observe, ) - from traitsui.api import View, Item - from chaco.chaco_plot_editor import ChacoPlotItem + from traitsui.api import Item, View + + COUNTIES = {'Brazos': 0, 'Dallas': 3, 'El Paso': 6, 'Harris': 9} + YEARS = { + 2: [65, 8, .806, 54, 8.3, .791, 24, 9.5, .797, 68, 7.9, .800], + 10: [80, 8.5, .763, 78, 8.7, .777, 42, 12., .795, 81, 7.7, .753], + 25: [89, 8.5, .754, 90, 8.7, .774, 60, 12., .843, 81, 7.7, .724], + 100: [96, 8., .730, 106, 8.3, .762, 65, 9.5, .825, 91, 7.9, .706] + } Trait Definitions @@ -77,22 +60,23 @@ Trait Definitions This application only requires one class that will contain the Traits and mathematical calculations together. Classes that contain Traits -must inherit from the HasTraits class. Python's multiple -inheritance allows for mixing HasTraits objects with other class -hierarchies if needed. +must inherit from the :class:`HasTraits` class or one of its subclasses. +Python's multiple inheritance allows for mixing HasTraits objects with other +class hierarchies if needed. Within this class we define all the variables using Traits types -which will later be used in the UI. These traits are set to equal -their type similar to many typed languages. :: +which will later be used in the UI. + +:: class Hyetograph(HasTraits): """ Creates a simple hyetograph demo. """ - timeline = Array + timeline = Array() - intensity = Array + intensity = Array() - nrcs = Array + nrcs = Array() duration = Int(12, desc='In Hours') @@ -103,36 +87,45 @@ their type similar to many typed languages. :: curve_number = Range(70, 100) plot_type = Enum('line', 'scatter') + + intensity_plot = Instance(Plot) + + nrcs_plot = Instance(Plot) + + ... The above code snippet shows a number of Traits features, -1. The naming convention with traits is that types are capitalized. +1. Traits are explicitly typed. -2. An Array is an array, an Int is an integer, an Enum is a single +2. The naming convention with traits is that types are capitalized. + +3. An Array is an array, an Int is an integer, an Enum is a single value from a list of options, and a Range is a value between two numbers. -3. All traits get a default value, such as whats done in the +4. All traits get a default value, such as whats done in the Arrays, or they can be assigned an initial value as is done in the duration trait. -4. Descriptions can be added to traits, such as is done in +5. Descriptions can be added to traits, such as is done in duration. This description is not visible except when viewing the trait in a TraitsUI view, and then the description is seen when the mouse hovers over the variable. -5. Traits are always contained within the class definition, and +6. Traits are always contained within the class definition, and each instance of the class will have a unique copy of the traits. -The Traits API Reference contains more information about the standard -Trait types; see the :mod:`trait_types` module in the `Traits API Reference -`_. +The `Traits API Reference +`_ +contains more information about the standard Trait types; specifically, see the +:mod:`trait_types` module. Setting up the User Interface (UI) ================================== -HasTraits classes will automatically generate a view that contains an +:class:`HasTraits` classes will automatically generate a view that contains an editable entry for each trait within the class. But a user-defined view usually looks better, so we'll use View and Items to change the default class view. Changing the default UI is done by creating a @@ -146,45 +139,19 @@ Continuing with our application, here is the View definition. :: ... - view = View( + traits_view = View( Item('plot_type'), - ChacoPlotItem( - 'timeline', - 'intensity', - type_trait='plot_type', - resizable=True, - x_label='Time (hr)', - y_label='Intensity (in/hr)', - color='blue', - bgcolor='white', - border_visible=True, - border_width=1, - padding_bg_color='lightgray' - ), + Item("intensity_plot", editor=ComponentEditor()), Item(name='duration'), Item(name='year_storm'), Item(name='county'), - # After infiltration using the nrcs curve number method. - ChacoPlotItem( - 'timeline', - 'nrcs', - type_trait='plot_type', - resizable=True, - x_label='Time', - y_label='Intensity', - color='blue', - bgcolor='white', - border_visible=True, - border_width=1, - padding_bg_color='lightgray' - ), + Item("nrcs_plot", editor=ComponentEditor()), Item('curve_number'), - resizable = True, + resizable=True, width=800, height=800, ) - - + Views generally contain Item objects and named parameters. Views can also contain Groups of Items as well as many other types of layout features not covered here. By default, Item objects take a string of @@ -194,56 +161,77 @@ the user can select from. There are three important observations to be seen in the above view -definition. First, there are two Chaco plot items embedded in the -view. The top plot is the intensity versus time and the bottom is -nrcs versus time. Second, default window will be sized at 800 by 800 +definition. First, there are two Chaco plots embedded in the +view. This is done by explicitly specifying the Item's editor to be a +:class:`ComponentEditor`. The top plot is the intensity versus time and the +bottom is nrcs versus time. Second, default window will be sized at 800 by 800 pixels, but the option ``resizable = True`` will allow the user to change the size of the window. And third, the traits are split up so 3 of them are displayed below the first plot and only 1 is displayed below the second. Here is a snapshot of what our application will display. The plots are empty because we have yet to populate the data -traits +traits or intialize the plot traits. .. image:: images/tutorial_hyetograph_nodata.png - Performing the Hyetograph Calculations ====================================== The UI for the application is complete, however there is no data. Changing the traits within the GUI by moving the sliders and typing in numbers does nothing because they're hooked up to nothing and there -are no listeners on the trait event notifications. So , next we'll -add some hyetograph calculations that modify the intensity and nrcs -Array traits. :: +are no listeners on the trait event notifications. First, we need to actually +set up the plots by defining methods to provide their defaults. + +:: + + def _intensity_plot_default(self): + intensity_plot = Plot(ArrayPlotData(x=self.timeline, y=self.intensity)) + intensity_plot.x_axis.title = "Time (hr)" + intensity_plot.y_axis.title = "Intensity (in/hr)" + intensity_plot.plot( + ("x", "y"), type=self.plot_type, name=self.plot_type, color="blue" + ) + return intensity_plot + + def _nrcs_plot_default(self): + nrcs_plot = Plot(ArrayPlotData(x=self.timeline, y=self.nrcs)) + nrcs_plot.x_axis.title = "Time" + nrcs_plot.y_axis.title = "Intensity" + nrcs_plot.plot( + ("x", "y"), type=self.plot_type, name=self.plot_type, color="blue" + ) + return nrcs_plot + +Here we have created an :class:`ArrayPlotData` instance to hold the data to be +plotted and we use that to create a :class:`Plot` instance. We configure some +properties of the plot, and finally call the :meth:`plot` method to create the +appropriate renderer for the plot. However, at this point we still have not +actually specified any values for the data. So, we'll add some hyetograph +calculations that modify the :attr:`intensity` and :attr:`nrcs` Array traits. + +:: def calculate_intensity(self): """ The Hyetograph calculations. """ # Assigning A, B, and C values based on year, storm, and county - counties = {'Brazos': 0, 'Dallas': 3, 'El Paso': 6, 'Harris': 9} - years = { - 2 : [65, 8, .806, 54, 8.3, .791, 24, 9.5, .797, 68, 7.9, .800], - 10: [80, 8.5, .763, 78, 8.7, .777, 42, 12., .795,81, 7.7, .753], - 25: [89, 8.5, .754, 90, 8.7, .774, 60, 12.,.843, 81, 7.7, .724], - 100: [96, 8., .730, 106, 8.3, .762, 65, 9.5, .825, 91, 7.9, .706] - } - year = years[self.year_storm] - value = counties[self.county] + year = YEARS[self.year_storm] + value = COUNTIES[self.county] a, b, c = year[value], year[value+1], year[value+2] - - self.timeline=range(2, self.duration + 1, 2) - intensity=a / (self.timeline * 60 + b)**c - cumulative_depth=intensity * self.timeline - temp=cumulative_depth[0] - result=[] + self.timeline = [i for i in range(2, self.duration + 1, 2)] + intensity = a / (self.timeline * 60 + b)**c + cumulative_depth = intensity * self.timeline + + temp = cumulative_depth[0] + result = [] for i in cumulative_depth[1:]: result.append(i-temp) - temp=i - result.insert(0,cumulative_depth[0]) + temp = i + result.insert(0, cumulative_depth[0]) - # Alternating block method implementation. + # Alternating block method implementation. result.reverse() switch = True o, e = [], [] @@ -256,24 +244,23 @@ Array traits. :: e.reverse() result = o + e self.intensity = result - def calculate_runoff(self): - """ NRCS method to get run-off based on permeability of ground. """ + """ NRCS method to get run-off based on permeability of ground. """ s = (1000 / self.curve_number) - 10 a = self.intensity - (.2 * s) vr = a**2 / (self.intensity + (.8 * s)) # There's no such thing as negative run-off. for i in range(0, len(a)): if a[i] <= 0: - vr[i] = 0 + vr[i] = 0 self.nrcs = vr In the calculation functions, the traits are treated just like normal -class attributes. Behind the scenes, Traits will automatically cast +attributes. Behind the scenes, Traits will automatically cast compatible types such as ints to Floats, but will raise an exception -if the user tries to pass a string to an Dict trait. +if, for example, the user tries to pass a string to a Dict trait. Recalculating when event notification occurs @@ -287,29 +274,57 @@ specific convention. Alternatively, a dynamic handler is set up by calling a function at runtime, providing for on-the-fly event processing. Below is a function that calls the two calculation functions. The interesting line is the decorator, -``@on_trait_change`` that tells Traits to call the function whenever +``@observe`` that tells Traits to call the function whenever any of the values within the list of traits change. :: - @on_trait_change('duration, year_storm, county, curve_number') - def _perform_calculations(self): + @observe('duration, year_storm, county, curve_number') + def _perform_calculations(self, event=None): self.calculate_intensity() self.calculate_runoff() + self.intensity_plot.data.set_data("y", self.intensity) + self.nrcs_plot.data.set_data("y", self.nrcs) + +So now when the application is run, when any of the four listed traits change, +the calculation functions are automatically called and the data changes. Then +the 2 plots will be updated to use this new data. These traits will +automatically change when the user adjusts the widgets in the UI. So when the +user changes the :attr:`duration` in the UI from 12 hours to 24 hours this will +automatically effect both of the plots since the listeners force a +recalculation of both of the functions. + +Furthermore, we also want the user to be able to select a :attr:`plot_type` +and have the plots update accordingly. To do so, we need to define a seperate +method to make this adjustment that listens to the :attr:`plot_type` trait. +The code for this is as follows: + +:: + + @observe("plot_type") + def _update_polt_type(self, event): + old_plot_type, new_plot_type = event.old, event.new + + self.intensity_plot.delplot(old_plot_type) + self.nrcs_plot.delplot(old_plot_type) + self.intensity_plot.plot( + ("x", "y"), type=new_plot_type, name=new_plot_type, color="blue" + ) + self.nrcs_plot.plot( + ("x", "y"), type=new_plot_type, name=new_plot_type, color="blue" + ) + self.intensity_plot.invalidate_and_redraw() + self.nrcs_plot.invalidate_and_redraw() -So now when the application is run, when the ``duration`` trait is -changed or any of the four listed traits change, the calculation -functions are automatically called and the data changes. And these -traits will automatically change when the user adjusts the widgets -in the UI. So when the user changes the ``duration`` in the UI -from 12 hours to 24 hours this will automatically effect both of -the plots since the listeners force a recalculation of both of the -functions. +Previously when creating plot renderers for our plots, we assigned their names +to simply match the :attr:`plot_type` trait. This way we can easily +delete the old plot and then create a new on of the correct type. Finally, we +call :meth:`invalidate_and_redraw` on the plots to ensur the UI gets refreshed. Showing the Display =================== In order to start the GUI application an instance of the class must be -instantiated, and then a configure_traits() call is done. However we +instantiated, and then a :meth:`configure_traits` call is done. However we must first call the data calculation functions from within the class to initialize the data arrays. Here's the last piece of the program. :: @@ -319,10 +334,10 @@ to initialize the data arrays. Here's the last piece of the program. :: if __name__ == "__main__": - hyetograph=Hyetograph() + hyetograph = Hyetograph() hyetograph.start() -start() performs the calculations needed for the Arrays used to plot, +:meth:`start` performs the calculations needed for the Arrays used to plot, and then triggers the UI. The application is complete, and if you now run the program, you should get a running application that resembles the following image, @@ -335,137 +350,8 @@ Congratulations! Source Code =========== -The final version of the program, `hyetograph.py`. :: +The final version of the program, +`hyetograph.py `_. - from traits.api import ( - HasTraits, - Int, - Range, - Array, - Enum, - on_trait_change, - ) - from traitsui.api import View, Item - from chaco.chaco_plot_editor import ChacoPlotItem - - COUNTIES = {'Brazos': 0, 'Dallas': 3, 'El Paso': 6, 'Harris': 9} - YEARS = { - 2 : [65, 8, .806, 54, 8.3, .791, 24, 9.5, .797, 68, 7.9, .800], - 10: [80, 8.5, .763, 78, 8.7, .777, 42, 12., .795,81, 7.7, .753], - 25: [89, 8.5, .754, 90, 8.7, .774, 60, 12.,.843, 81, 7.7, .724], - 100: [96, 8., .730, 106, 8.3, .762, 65, 9.5, .825, 91, 7.9, .706] - } - - class Hyetograph(HasTraits): - """ Creates a simple hyetograph demo. """ - - timeline = Array - - intensity = Array - - nrcs = Array - - duration = Int(12, desc='In Hours') - - year_storm = Enum(2, 10, 25, 100) - - county = Enum('Brazos', 'Dallas', 'El Paso', 'Harris') - - curve_number = Range(70, 100) - - plot_type = Enum('line', 'scatter') - - view1 = View( - Item('plot_type'), - ChacoPlotItem( - 'timeline', - 'intensity', - type_trait='plot_type', - resizable=True, - x_label='Time (hr)', - y_label='Intensity (in/hr)', - color='blue', - bgcolor='white', - border_visible=True, - border_width=1, - padding_bg_color='lightgray', - ), - Item(name='duration'), - Item(name='year_storm'), - Item(name='county'), - # After infiltration using the nrcs curve number method. - ChacoPlotItem( - 'timeline', - 'nrcs', - type_trait='plot_type', - resizable=True, - x_label='Time', - y_label='Intensity', - color='blue', - bgcolor='white', - border_visible=True, - border_width=1, - padding_bg_color='lightgray', - ), - Item('curve_number'), - resizable=True, - width=800, - height=800, - ) - - def calculate_intensity(self): - """ The Hyetograph calculations. """ - # Assigning A, B, and C values based on year, storm, and county - year = YEARS[self.year_storm] - value = COUNTIES[self.county] - a, b, c = year[value], year[value+1], year[value+2] - - self.timeline=range(2, self.duration + 1, 2) - intensity=a / (self.timeline * 60 + b)**c - cumulative_depth=intensity * self.timeline - - temp=cumulative_depth[0] - result=[] - for i in cumulative_depth[1:]: - result.append(i-temp) - temp=i - result.insert(0,cumulative_depth[0]) - - # Alternating block method implementation. - result.reverse() - switch = True - o, e = [], [] - for i in result: - if switch: - o.append(i) - else: - e.append(i) - switch = not switch - e.reverse() - result = o + e - self.intensity = result - - def calculate_runoff(self): - """ NRCS method to get run-off based on permeability of ground. """ - s = (1000 / self.curve_number) - 10 - a = self.intensity - (.2 * s) - vr = a**2 / (self.intensity + (.8 * s)) - # There's no such thing as negative run-off. - for i in range(0, len(a)): - if a[i] <= 0: - vr[i] = 0 - self.nrcs = vr - - @on_trait_change('duration, year_storm, county, curve_number') - def _perform_calculations(self): - self.calculate_intensity() - self.calculate_runoff() - - def start(self): - self._perform_calculations() - self.configure_traits() - - - if __name__ == "__main__": - hyetograph=Hyetograph() - hyetograph.start() +.. literalinclude:: /../../examples/demo/hyetograph.py + :language: python diff --git a/examples/demo/hyetograph.py b/examples/demo/hyetograph.py new file mode 100644 index 000000000..8ca85f282 --- /dev/null +++ b/examples/demo/hyetograph.py @@ -0,0 +1,149 @@ +from chaco.api import ArrayPlotData, Plot +from enable.api import ComponentEditor +from traits.api import ( + HasTraits, + Instance, + Int, + Range, + Array, + Enum, + observe, +) +from traitsui.api import Item, View + +COUNTIES = {'Brazos': 0, 'Dallas': 3, 'El Paso': 6, 'Harris': 9} +YEARS = { + 2: [65, 8, .806, 54, 8.3, .791, 24, 9.5, .797, 68, 7.9, .800], + 10: [80, 8.5, .763, 78, 8.7, .777, 42, 12., .795, 81, 7.7, .753], + 25: [89, 8.5, .754, 90, 8.7, .774, 60, 12., .843, 81, 7.7, .724], + 100: [96, 8., .730, 106, 8.3, .762, 65, 9.5, .825, 91, 7.9, .706] +} + + +class Hyetograph(HasTraits): + """ Creates a simple hyetograph demo. """ + + timeline = Array() + + intensity = Array() + + nrcs = Array() + + duration = Int(12, desc='In Hours') + + year_storm = Enum(2, 10, 25, 100) + + county = Enum('Brazos', 'Dallas', 'El Paso', 'Harris') + + curve_number = Range(70, 100) + + plot_type = Enum('line', 'scatter') + + intensity_plot = Instance(Plot) + + nrcs_plot = Instance(Plot) + + def _intensity_plot_default(self): + intensity_plot = Plot(ArrayPlotData(x=self.timeline, y=self.intensity)) + intensity_plot.x_axis.title = "Time (hr)" + intensity_plot.y_axis.title = "Intensity (in/hr)" + intensity_plot.plot( + ("x", "y"), type=self.plot_type, name=self.plot_type, color="blue" + ) + return intensity_plot + + def _nrcs_plot_default(self): + nrcs_plot = Plot(ArrayPlotData(x=self.timeline, y=self.nrcs)) + nrcs_plot.x_axis.title = "Time" + nrcs_plot.y_axis.title = "Intensity" + nrcs_plot.plot( + ("x", "y"), type=self.plot_type, name=self.plot_type, color="blue" + ) + return nrcs_plot + + def calculate_intensity(self): + """ The Hyetograph calculations. """ + # Assigning A, B, and C values based on year, storm, and county + year = YEARS[self.year_storm] + value = COUNTIES[self.county] + a, b, c = year[value], year[value+1], year[value+2] + + self.timeline = [i for i in range(2, self.duration + 1, 2)] + intensity = a / (self.timeline * 60 + b)**c + cumulative_depth = intensity * self.timeline + + temp = cumulative_depth[0] + result = [] + for i in cumulative_depth[1:]: + result.append(i-temp) + temp = i + result.insert(0, cumulative_depth[0]) + + # Alternating block method implementation. + result.reverse() + switch = True + o, e = [], [] + for i in result: + if switch: + o.append(i) + else: + e.append(i) + switch = not switch + e.reverse() + result = o + e + self.intensity = result + + def calculate_runoff(self): + """ NRCS method to get run-off based on permeability of ground. """ + s = (1000 / self.curve_number) - 10 + a = self.intensity - (.2 * s) + vr = a**2 / (self.intensity + (.8 * s)) + # There's no such thing as negative run-off. + for i in range(0, len(a)): + if a[i] <= 0: + vr[i] = 0 + self.nrcs = vr + + @observe('duration, year_storm, county, curve_number') + def _perform_calculations(self, event=None): + self.calculate_intensity() + self.calculate_runoff() + self.intensity_plot.data.set_data("y", self.intensity) + self.nrcs_plot.data.set_data("y", self.nrcs) + + @observe("plot_type") + def _update_polt_type(self, event): + old_plot_type, new_plot_type = event.old, event.new + + self.intensity_plot.delplot(old_plot_type) + self.nrcs_plot.delplot(old_plot_type) + self.intensity_plot.plot( + ("x", "y"), type=new_plot_type, name=new_plot_type, color="blue" + ) + self.nrcs_plot.plot( + ("x", "y"), type=new_plot_type, name=new_plot_type, color="blue" + ) + self.intensity_plot.invalidate_and_redraw() + self.nrcs_plot.invalidate_and_redraw() + + def start(self): + self._perform_calculations() + self.configure_traits() + + traits_view = View( + Item('plot_type'), + Item("intensity_plot", editor=ComponentEditor()), + Item(name='duration'), + Item(name='year_storm'), + Item(name='county'), + Item("nrcs_plot", editor=ComponentEditor()), + Item('curve_number'), + resizable=True, + width=800, + height=800, + ) + + +if __name__ == "__main__": + hyetograph = Hyetograph() + hyetograph.start()