""" Demo of a javascript inspector tool that shows a black bar over the active value axis, as well as the values at the current index, with the numbers shown in the colors matching the legend. To Use ------ Run Python on this file which will generate six files, plot_hover_coords.html plot_hover_coords.png plot_hover2_coords.png hover_coords.js plot_hover_coords_png_hover_data.js plot_hover2_coords_png_hover_data.js Alternatively, if you pass in '-e' or '--embedded' on the command-line, then a single HTML file will be created that has all JavaScript and images directly embedded into it. The script should automatically load your webbrowser on the output file, but if it does not, then manually open the file hover_coords_plot.html in your browser to see the output. Author: Judah De Paula <judah@enthought.com> Date: November 21, 2008. """ # Standard library imports import os, sys, webbrowser, cStringIO from base64 import encodestring # Major library imports import Image from numpy import arange, searchsorted, where, array, vstack, linspace from scipy.special import jn # Chaco imports from enthought.chaco.api \ import ArrayPlotData, Plot, PlotGraphicsContext, LinePlot from enthought.chaco.example_support import COLOR_PALETTE #-- Constants ----------------------------------------------------------------- DPI = 72.0 #------------------------------------------------------------------------------ # File templates: # In a real application, these templates should be their own files, # and a real templating engine (ex. Mako) would make things more flexible. #------------------------------------------------------------------------------ html_template_keys = {'filename1' : 'plot_hover_coords.png', 'filename2' : 'plot_hover2_coords.png', 'file1_src' : 'plot_hover_coords.png', 'file2_src' : 'plot_hover2_coords.png', 'hover_coords' :'src="hover_coords.js">', 'data1' :'src="plot_hover_coords_png_hover_data.js">', 'data2' :'src="plot_hover2_coords_png_hover_data.js">' } # Turns into index.html. html_template = """ <html> <head> <script type="text/javascript" %(hover_coords)s </script> <script type="text/javascript" %(data1)s </script> <script type="text/javascript" %(data2)s </script> </head> <body> <!------------------ What gets shown onmouseover. ------------------> <div id="Hover" style="background-color: White; color: Black; padding: 5px; position: absolute; visibility: hidden;"> 0 </div> <div id="BlackPixel" style="background-color: White; color: Black; padding: 0px; position: absolute; visibility: hidden;"> <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAxJREFUCNdjYGBgAAAABAABJzQnCgAAAABJRU5ErkJggg==" width="1" height="100" alt="black_pixel.inline" id="black_pixel.inline" /> </div> <!------------------ Demo plot -------------------------------------> <div class = "tabbertab"> <img src="%(file1_src)s" alt="%(filename1)s" id="%(filename1)s" onmouseover="javascript:ShowHovers('%(filename1)s', 'Hover', 'BlackPixel')" onmouseout="javascript:HideHovers(event, 'Hover', 'BlackPixel')" /> <img src="%(file2_src)s" alt="%(filename2)s" id="%(filename2)s" onmouseover="javascript:ShowHovers('%(filename2)s', 'Hover', 'BlackPixel')" onmouseout="javascript:HideHovers(event, 'Hover', 'BlackPixel')" /> </div> </body> </html> """ # Turns into plot_hover_coords_png_hover_data.js javascript_data_template = """ // Create a new "CursorData" object obj = Object(); obj.height = %(height)s; obj.border_width = %(border_width)s; obj.padding_top = %(padding_top)s + 1; obj.padding_left = %(padding_left)s; obj.padding_bottom = %(padding_bottom)s; obj.colors = Array( %(colors)s ); // Store it in the global CursorData, using the unique array_id as a key. GlobalCursorData["%(array_id)s"] = obj // Store all the data in the array obj.data = Array( %(data_s)s ); """ # Turns into hover_coords.js. Absolutely no string substitution is done. hover_coords_js_file = """ <!------------------ Javascript hover functions -----------------> <!-- Depends upon *_hover_data.js files. --> <!---------------------------------------------------------------> /* Global variables */ // Maps an image name to a CursorData object that contains an array of // data and plot visual attributes like height, padding, and border width. GlobalCursorData = Array(); // The hash key into GlobalCursorArray that corresponds to the the current // plot that the mouse is over. var CurrentPlotKey = ""; // The ID of the <div> box that contains the cursor line image. var BoxElementID; // The ID of the <img> of the cursor line image. var LineElementID; // The number of pixels to offset the readout box from cursor position var _xOffset = 15; var _yOffset = 15; /* Convert an array of floats into an HTML formatted string ready for the screen. */ function FormatToHTML(data) { var colors = GlobalCursorData[CurrentPlotKey].colors; var s = '<font size=2 face="Courier New">'; for (var count = 0; count < data.length; count++) { s = s + '<font color=' + colors[count] + '>' + data[count] + '</font><br>'; } if (data.length > 0) { s = s.substr(0, s.length-4); } s += "</font>" return s; } /* Slightly more clever get x function than the default. Limited testing. */ function GetMouseX(event) { if (!event) { event = window.event; } if (event.clientX) { return event.clientX + (document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft); } else if (event.pageX) { return event.pageX; } else { return 0; } } /* Slightly more clever get y function than the default. Limited testing. */ function GetMouseY(event) { if (!event) { event = window.event; } if (event.clientY) { return event.clientY + (document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop); } else if (event.pageY) { return event.pageY; } else { return 0; } } /* Uses data registered by external *hover_data.js files. Cannot figure out how to get relative coordinates. Have to calculate it instead using the reference image element registered with the hover IDs. */ function ScreenXToImageX(mouseX) { var referenceX = document.getElementById(CurrentPlotKey); var cursor_data = GlobalCursorData[CurrentPlotKey]; var relativeX = mouseX - cursor_data.padding_left - cursor_data.border_width - referenceX.x; return relativeX; } /* Uses data registered by external *hover_data.js files. The Screen pixel coordinate that the data pixels in the plot start. */ function ScreenYPlotStart(event) { var referenceElement = document.getElementById(CurrentPlotKey); var cursor_data = GlobalCursorData[CurrentPlotKey]; return referenceElement.y + cursor_data.padding_top + cursor_data.border_width; } /* Uses data registered by external *hover_data.js files. Given the browser X coordinate compute the plot screen X coordinate and use that value to index into the data array of plot values for that index. */ function ScreenXToDataY(mouseX) { var dataY; var relativeX = ScreenXToImageX(mouseX); var cursor_data = GlobalCursorData[CurrentPlotKey]; if (relativeX >= 0 && relativeX < cursor_data.data.length) { dataY = cursor_data.data[relativeX]; // Hack to have bar hidden on the left and right side of images. document.getElementById(LineElementID).style.visibility = 'visible'; document.getElementById(BoxElementID).style.visibility = 'visible'; } else { dataY = 0; // Hack to have bar hidden on the left and right side of images. document.getElementById(LineElementID).style.visibility = 'hidden'; document.getElementById(BoxElementID).style.visibility = 'hidden'; } return dataY; } /* Uses data registered by external *hover_data.js files. Every time the mouse moves over the image, Follow() changes the hovering object attributes so they move to the new location. */ function Follow(event) { // Move the box var referenceElement = document.getElementById(CurrentPlotKey); var element = document.getElementById(BoxElementID); if (element != null) { var style = element.style; // ScreenXToDataY() hacked to have better control of line visibility. //style.visibility = 'visible'; style.left = (parseInt(GetMouseX(event)) + _xOffset) + 'px'; style.top = (parseInt(GetMouseY(event)) + _yOffset) + 'px'; element.innerHTML = FormatToHTML(ScreenXToDataY(GetMouseX(event))); } // Move the line var element = document.getElementById(LineElementID); var image_element = document.getElementById("black_pixel.inline"); if (element != null) { var style = element.style; // ScreenXToDataY() hacked to have better control of line visibility. //style.visibility = 'visible'; style.left = parseInt(GetMouseX(event)) + 'px'; style.top = parseInt(ScreenYPlotStart()) + 'px'; element.height = GlobalCursorData[CurrentPlotKey].height - 1; image_element.height = element.height; } } /* Abstracted out Show() that assigns the Follow() to mouse movements. */ function Show(referenceElementID) { var referenceElement = document.getElementById(referenceElementID); if (referenceElement) { referenceElement.onmousemove = Follow; } } /* Abstracted out Hide() that assigns the Follow() to mouse movements. Turns the hover object invisible and disables the Follow() listener. FIXME: The listener is turned off for both hover objects even though only one object is hidden by this call. */ function Hide(referenceElementID, elementID) { var divStyle = document.getElementById(elementID).style; var referenceElement = document.getElementById(referenceElementID); divStyle.visibility = 'hidden'; referenceElement.onmousemove = ''; } /* Main entry point for onmouseover event of images. Sets the globals to the active image the mouse just entered and turns on the hovers. */ function ShowHovers(referenceElementID, boxElementID, lineElementID) { //_dataIndex = plot_names.indexOf(referenceElementID); CurrentPlotKey = referenceElementID; BoxElementID = boxElementID; LineElementID = lineElementID; Show(CurrentPlotKey); Show(CurrentPlotKey); } /* Main event call point for onmouseout event for images */ function HideHovers(event, boxElementID, lineElementID) { var relatedTarget = event.relatedTarget; if ((relatedTarget == null) || (relatedTarget.id != boxElementID) && (relatedTarget.id != lineElementID) && (relatedTarget.id != 'black_pixel.inline')) { Hide(CurrentPlotKey, boxElementID); Hide(CurrentPlotKey, lineElementID); } } """ #------------------------------------------------------------------------------ # Data generation functions for *_hover_data.js files. #------------------------------------------------------------------------------ def get_pixel_data(segment, renderer, screen_width): """ Take a segment of points and use a renderer to map them to screen coords. Algorithm --------- Fast little function that only works if the samples are sorted. FIXME: Need more explanation. """ screen_points = renderer.map_screen(segment) if len(screen_points) == 0: return array([]) s_index = screen_points[:,0].astype(int) d_value = segment[:,1] if len(s_index) != len(d_value): raise ValueError('Data-to-screen mapping not 1-to-1.') indices = searchsorted(s_index, arange(0, screen_width)) indices[where(indices == 0)] = 1 return d_value[indices-1] def write_hover_coords(container, array_id, script_filename=None): """ Create a JavaScript formatted file of screen index to data values. Parameters ---------- container : Plot Chaco container object such as a plot that contain renderers and border padding information. array_id : str An identifier that will be used as the key when assigning the data array to the global list of data arrays. This identifier should be unique among all the scripts that will be loaded in a single HTML file. script_filename : str Full path to the desired output javascript file. If no name is passed in, then no file is written. Returns ------- Returns the JavaScript data file as a string. Description ----------- To have a PNG plot show the values of the curves in a browser, JavaScript is used to get the screen X/Y and convert the X into the corresponding plot data values. This function creates that mapping array. A couple of extra padding variables are also exported so the JavaScript can do some additional offset calculations. """ screen_width = container.width # For every renderer in the plot, create an array of the same length # as the screen width that contains the data value for each screen # position. Stack all of these arrays together into the 2D 'pixel_data' # array, then pass it into the template for the hover_data javascript file. segment_data = [] colors = [] for renderer_name in sorted(container.legend.plots.keys()): renderer = container.legend.plots[renderer_name][0] data_points = renderer._cached_data_pts if not isinstance(renderer, LinePlot): data_points = [data_points] for segment in data_points: segment_data.append(get_pixel_data(segment, renderer, screen_width)) colors.append(renderer.color_) if len(segment_data) > 0: pixel_data = vstack(segment_data).T else: pixel_data = array([]).reshape(0, 2) # Fill out a template using the just-created data. colstrings = [] for color in colors: if len(color) == 4: alpha = color[3] else: alpha = 1 r, g, b = [int(component * alpha * 255) for component in color[:3]] colstrings.append('"#%02x%02x%02x"' % (r,g,b)) colors = ",".join(colstrings); line_template = ",".join(['"%1.2f"'] * pixel_data.shape[1]) data_s = '' for row in pixel_data[:-1]: data_s += '[' + line_template % tuple(row) + '],' if len(pixel_data) > 0: data_s += '[' + line_template % tuple(row) + ']' template_keys = dict(height = container.height, padding_top = container.padding_top, padding_left = container.padding_left, padding_bottom = container.padding_bottom, border_width = container.border_width, array_id = array_id, pixel_data = pixel_data, colors = colors, data_s = data_s) # Write out and return the result. output = javascript_data_template % template_keys if script_filename: f = open(script_filename, 'wt') f.write(output) f.close() return output #------------------------------------------------------------------------------ # Plot and renderer generation functions. #------------------------------------------------------------------------------ def create_plot(num_plots=8, type='line'): """ Create a single plot object, with multiple renderers. """ # This is a bit of a hack to work around that line widths don't scale # with the GraphicsContext's CTM. dpi_scale = DPI / 72.0 numpoints = 100 low = -5 high = 15.0 x = linspace(low, high, numpoints) pd = ArrayPlotData(index=x) p = Plot(pd, bgcolor="white", padding=50, border_visible=True) for i in range(1,num_plots+2): pd.set_data("y" + str(i), jn(i,x)) p.plot(("index", "y" + str(i)), color=tuple(COLOR_PALETTE[i]), width = 2.0 * dpi_scale, type=type) p.x_grid.visible = True p.x_grid.line_width *= dpi_scale p.y_grid.visible = True p.y_grid.line_width *= dpi_scale p.legend.visible = True return p def draw_plot(filename, size=(800,600), num_plots=8, type='line', key=''): """ Save the plot, and generate the hover_data file. """ container = create_plot(num_plots, type) container.outer_bounds = list(size) container.do_layout(force=True) gc = PlotGraphicsContext(size, dpi=DPI) gc.render_component(container) if filename: gc.save(filename) script_filename = filename[:-4] + "_png_hover_data.js" else: script_filename = None plot = make_palettized_png_str(gc) script_data = write_hover_coords(container, key, script_filename) return (plot, script_data) def make_palettized_png_str(gc): """ Generate a png file in a string, base64 encoded. """ format = gc.format()[:-2].upper() if format != "RGBA": gc = gc.convert_pixel_format("rgba32") img = Image.fromstring("RGBA", (gc.width(), gc.height()), gc.bmp_array.tostring()) img2 = img.convert("P") output_buf = cStringIO.StringIO() img2.save(output_buf, 'png') output = encodestring(output_buf.getvalue()) output_buf.close() return output #------------------------------------------------------------------------------ # Main #------------------------------------------------------------------------------ def main(embedded=False): """ Create the files and load the output in a webbrowser. """ # 1. Create the JavaScript hover tool file. # Only doing this to keep the demo file self-contained. target_path = os.path.join(os.getcwd(), 'hover_coords.js') if embedded: html_template_keys['hover_coords'] = '>\n%s\n' % hover_coords_js_file else: f = open(target_path, 'wt') f.write(hover_coords_js_file) f.close() # 2. Create the dynamically generated JavaScript data files. if embedded: html_template_keys['file1_src'] = None html_template_keys['file2_src'] = None file1_strs = draw_plot(html_template_keys['file1_src'], size=(800, 600), key=html_template_keys['filename1']) file2_strs = draw_plot(html_template_keys['file2_src'], size=(600, 400), num_plots = 4, type='scatter', key=html_template_keys['filename2']) # 3. Choose the correct src type for the HTML file if embedded. if embedded: src1 = 'data:image/png;base64,' + file1_strs[0] html_template_keys['file1_src'] = src1 src2 = 'data:image/png;base64,' + file2_strs[0] html_template_keys['file2_src'] = src2 html_template_keys['data1'] = '>\n%s\n' % file1_strs[1] html_template_keys['data2'] = '>\n%s\n' % file2_strs[1] # 4. Create the HTML file. out_html = os.path.join(os.getcwd(), 'plot_hover_coords.html') f = open(out_html, 'wt') f.write(html_template % html_template_keys) f.close() # 5. Load the finished product. try: webbrowser.open(out_html) except Exception, e: print 'Browser did not open properly. Exception %s. The results' \ 'can be viewed with the file plot_hover_coords.html.' % str(e) raise return #=============================================================================== # # Demo class that is used by the demo.py application. #=============================================================================== # NOTE: The Demo class is being created for the purpose of running this # example using a TraitsDemo-like app (see examples/demo/demo.py in Traits3). # The demo.py file looks for a 'demo' or 'popup' or 'modal popup' keyword # when it executes this file, and displays a view for it. # NOTE2: In this case, Demo class is just a mock object. Essentially we want to # execute main instead of displaying a UI for Demo: so we hack this by # overriding configure_traits and edit_traits to return a blank UI. from enthought.traits.api import HasTraits from enthought.traits.ui.api import UI, Handler class Demo(HasTraits): def configure_traits(self, *args, **kws): main(embedded=True) return True def edit_traits(self, *args, **kws): main(embedded=True) return UI(handler=Handler()) popup = Demo() if __name__ == "__main__": if '-e' in sys.argv or '--embedded' in sys.argv: main(embedded=True) else: main(embedded=False) #-- eof -----------------------------------------------------------------------