Columns Header Layout
I've never been a fan of the current columns header layout, using a full row for each single hierarchy header seems like a screen real estate waste. Furthermore the interleaved hierarchy names and members make harder to provide visual clues to a hierarchy dragging operation. After looking around for a more compact layout solution, I've finally ended up creating a totally new design. The idea was to keep the columns hierarchies together and, as close as possible to the rows hierarchies. My solution was to pile up the columns hierarchies headers on top of the rows hierarchies and provide a visual clue for the user to properly link each hierarchy label with the corresponding hierarchy members.The hard part of it was to draw the slides linking each hierarchy with its members. You can't use fixed sized images, because when they are scaled to fit the cell height the line width is also scaled. I'm using dinamically generated images to get lines with the proper line width. This is how it works:
- Each slide has its own cell with
.cgaoSlide
class (rows axis header has to compensate for this) - The last step in the cell set table generation calls
drawSlides(table)
drawSlides
sets the cell background with an image named after the current cell width and height- A dedicated Java servlet draws the slide with the correct size
drawSlides
function drawSlides(table) { var slides = $('th.cgaoSlide'); var i; for( i = 0; i < slides.size(); i++ ) { var first = slides.get(i); slides.css('background','url("imgd/line_'+ first.offsetWidth+'_'+ (first.offsetHeight)+'.png") no-repeat'); slides.css('background-width','100%'); slides.css('background-height','100%'); } }And the Servlet generating the images. Note that I've disabled antialiasing, otherwise the line may be be antialiased twice resulting in a blurred line.
public class TableBorderImage extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = request.getRequestURI().substring(request.getContextPath().length()+1); String[] nameParts = name.split("[_\\.]"); int width = Integer.parseInt(nameParts[1]); int height = Integer.parseInt(nameParts[2]); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D gr = (Graphics2D) image.getGraphics(); gr.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); gr.setColor(Color.WHITE); gr.fillRect(0, 0, width+1, height); gr.setColor(new Color(0xdd,0xdd,0xdd)); Path2D.Double path = new Path2D.Double(); path.moveTo(0,0); path.curveTo(width/2,0,width/2,height/4,width/2,height/2); path.curveTo(width/2,3*height/4,width/2,height,width-1,height-1); gr.draw(path); response.setContentType("image/png"); ImageIO.write(image, "PNG", response.getOutputStream()); } }
Visual Clue for Drop Point
jquery-ui uses draggable
widgets to designate elements that can be dragged, and droppable
widgets to designate elements that can be dropped. In my implementation I make <th>
elements for hierarchy labes both, draggable and droppable. Unfortunately when you drop a hierarchy over another one, the insertion point is not totally defined. It can be inserted before or after the later.
The idea is to insert the hierarchy after, when the user drops the dragged hierarchy on the right side of the hierarchy, and insert it before when dropped on the left side (change left by top and right by bottom for hierarchies in the columns axis). The insertion point lights up as the user drags the hierarchy.
To achieve this I'm using two jQuery events:
droppable.over
- to set the currently dragged-on object as a data property of the droppable
function onHierarchyDragOver(event,ui){ ui.helper.data('over', $(event.target)); }
draggable.drag
- to track the drag operation and set css classes based on the dragged element position relative to the current dragged-on object. CSS styles highlight the right border based on the axis (
.colHierarchy
/.rowHierarchy
) class and the dragged element relative postion class(.cgaoDropLeft
,.cgaoDropRight
,.cgaoDropTop
,.cgaoDropBottom
)function onHierarchyDrag(event, ui){ var over = ui.helper.data('over'); if ( over ) { var pos = within(over,event.pageX, event.pageY); if ( !pos ) { over.removeClass(dropHighlightH.join(' ')+dropHighlightV.join(' ')); } else { over.addClass(dropHighlightH[pos[0]]+' '+dropHighlightV[pos[1]]); over.removeClass(dropHighlightH[(pos[0]+1)%2]+' '+dropHighlightV[(pos[1]+1)%2]); } } } var dropHighlightH = ['cgaoDropLeft','cgaoDropRight']; var dropHighlightV= ['cgaoDropTop','cgaoDropBottom']; function within(elem, x, y) { var left = elem.offset().left; var top = elem.offset().top; var width = elem.width(); var height = elem.height(); var result = null; if ( x >= left && (x-left <= width) && y >= top && (y-top) <= height ) { var result = []; result.push(((x-left) < width/2) ? 0 : 1); result.push(((y-top) < height/2) ? 0 : 1); } return result; }
th.rowHierarchy.cgaoDropLeft { border-left:3px solid #0088cc; } th.rowHierarchy.cgaoDropRight { border-right:3px solid #0088cc; } th.colHierarchy.cgaoDropTop { border-top:3px solid #0088cc; } th.colHierarchy.cgaoDropBottom { border-bottom:3px solid #0088cc; }
And, finally, these are the calls to initialize the draggable/droppable TH
s containing the hierarchy labels:
var headers = table.find('.rowHierarchy,.colHierarchy'); headers.draggable({ helper:function(e,ui){ var src = $(e.currentTarget); return src.clone() .css('border','1px solid #dddddd') .css('background','rgba(255,255,255,0.5)') }, revertDuration:0, revert:true, cursor:'pointer', drag:onHierarchyDrag }); headers.droppable({ accept:'.rowHierarchy,.colHierarchy', tolerance:'pointer', over:onHierarchyDragOver, out:onHierarchyDragOut, drop:function(event,ui){ // Clears insertion point visual clues $(event.target).removeClass(dropHighlightH.concat(dropHighlightV).join(' ')); // Retrieves hierarchy info for dropped and dragged-on elements var metadataFrom = parent[0].olapGetMetadataAt(ui.draggable); var dest = angular.element(this); var metadataTo = parent[0].olapGetMetadataAt(dest); var pos = 0; if ( metadataFrom ) { // Adjusts insertion point based on drop position if ( metadataTo.axisOrdinal === 0 ) { pos = dest.closest('tr').prevAll().length; pos += within(dest, event.pageX, event.pageY)[1]; } else if ( metadataTo.axisOrdinal === 1 ) { pos += dest.get(0).cellIndex; pos += within(dest, event.pageX, event.pageY)[0]; } // Executes hierarchy change move(metadataFrom.hierarchy.caption, metadataTo.axisOrdinal, pos); } } });
Server Side Changes
The call on line 40 in the previous code snippet triggers an AJAX call to move the hierarchy to the desired position, updating the cell set accordingly. The QueryContoller
method
answering this request moves the hierarchy to the desired axis if needed, and uses QueryAxis#pullUp
/QueryAxis#pushDown
to move the hierarchy to its final position.
@POST @Path("hierarchies/move") @Produces(MediaType.APPLICATION_JSON) public QueryCellSet moveHierarchy(@FormParam("hierarchy") String hierarchyName, @FormParam("axis") int targetAxisOrdinal, @FormParam("position") int targetPos) { QueryHierarchy hierarchy = _query.getHierarchy(hierarchyName); QueryAxis axis = _query.getAxis(Axis.Factory.forOrdinal(targetAxisOrdinal)); int hierarchySrcPos = axis.getHierarchies().indexOf(hierarchy); if ( hierarchySrcPos < 0 ) { axis.addHierarchy(hierarchy); hierarchySrcPos = axis.getHierarchies().size()-1; } if ( targetPos > hierarchySrcPos) { for(; targetPos > hierarchySrcPos; ++hierarchySrcPos ) { axis.pushDown(hierarchySrcPos); } } else if ( targetPos < hierarchySrcPos ) { for (; targetPos < hierarchySrcPos; --hierarchySrcPos) { axis.pullUp(hierarchySrcPos); } } return doExecuteQuery(); }
No comments :
Post a Comment