azkaban-aplcache

Flow page coming along. Got basic graph back and running, and

7/11/2012 11:47:41 PM

Details

diff --git a/src/java/azkaban/flow/Flow.java b/src/java/azkaban/flow/Flow.java
index c0d2fba..4b657d9 100644
--- a/src/java/azkaban/flow/Flow.java
+++ b/src/java/azkaban/flow/Flow.java
@@ -13,8 +13,9 @@ public class Flow {
 		READY, RUNNING, RUNNING_WITH_FAILURE, FAILED, SUCCEEDED
 	}
 	private final String id;
-	private ArrayList<Node> startNodes;
-	private ArrayList<Node> endNodes;
+	private ArrayList<Node> startNodes = null;
+	private ArrayList<Node> endNodes = null;
+	private int numLevels = -1;
 	
 	private HashMap<String, Node> nodes = new HashMap<String, Node>();
 
@@ -49,6 +50,7 @@ public class Flow {
 			
 			for (Node node: startNodes) {
 				node.setLevel(0);
+				numLevels = 0;
 				recursiveSetLevels(node);
 			}
 		}
@@ -65,11 +67,16 @@ public class Flow {
 				// We pick whichever is higher to get the max distance from root.
 				int level = Math.max(node.getLevel() + 1, nextNode.getLevel());
 				nextNode.setLevel(level);
+				numLevels = Math.max(level, numLevels);
 				recursiveSetLevels(nextNode);
 			}
 		}
 	}
 
+	public int getNumLevels() {
+		return numLevels;
+	}
+	
 	public List<Node> getStartNodes() {
 		return startNodes;
 	}
@@ -170,6 +177,7 @@ public class Flow {
 		flowObj.put("props", objectizeProperties());
 		flowObj.put("nodes", objectizeNodes());
 		flowObj.put("edges", objectizeEdges());
+		flowObj.put("layedout", isLayedOut);
 		if (errors != null) {
 			flowObj.put("errors", errors);
 		}
@@ -212,7 +220,11 @@ public class Flow {
 		Map<String, Object> flowObject = (Map<String,Object>)object;
 		
 		String id = (String)flowObject.get("id");
+		Boolean layedout = (Boolean)flowObject.get("layedout");
 		Flow flow = new Flow(id);
+		if (layedout != null) {
+			flow.setLayedOut(layedout);
+		}
 		
 		// Loading projects
 		List<Object> propertiesList = (List<Object>)flowObject.get("props");
diff --git a/src/java/azkaban/flow/LayeredFlowLayout.java b/src/java/azkaban/flow/LayeredFlowLayout.java
index 33b10b3..c72cea4 100644
--- a/src/java/azkaban/flow/LayeredFlowLayout.java
+++ b/src/java/azkaban/flow/LayeredFlowLayout.java
@@ -1,48 +1,428 @@
 package azkaban.flow;
 
-import java.awt.geom.Point2D;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Random;
 
 public class LayeredFlowLayout implements FlowLayout{
+	private static final double EPSILON = 0.000001;
+	private static final double MIN_X_SPACING = 1.0;
+	private static final double MIN_Y_SPACING = 1.0;
+	
 	@Override
 	public void layoutFlow(Flow flow) {
-		analyzeFlow(flow);
+		ArrayList<ArrayList<LayeredNode>> nodeLayers = setupFlow(flow);
+		
+		int maxNodesInLevel = 0;
+		int levelWithMax = 0;
+		int index = 0;
+		for (ArrayList<LayeredNode> layer : nodeLayers) {
+
+			int num = layer.size();
+			if (num < maxNodesInLevel) {
+				maxNodesInLevel = num;
+				levelWithMax = index;
+			}
+			index++;
+		}
+		
+		midUpDownScheme(nodeLayers, levelWithMax);
+
+		printLayer(flow.getId(), nodeLayers, maxNodesInLevel, levelWithMax);
 		flow.setLayedOut(true);
 	}
 
-	private void analyzeFlow(Flow flow) {
-		Map<String, Node> node = flow.getNodeMap();
+	private void assignPointsToFlowNodes(ArrayList<ArrayList<LayeredNode>> nodeLayers, double xScale, double minXSpacing) {
+		for (ArrayList<LayeredNode> layer: nodeLayers) {
+			for (LayeredNode lnode : layer) {
+				if (lnode instanceof WrappedNode) {
+					WrappedNode wnode = (WrappedNode)lnode;
+					Node node = wnode.getNode();
+					node.setPosition(wnode.getX()*xScale, lnode.level * minXSpacing);
+				}
+			}
+		}
+	}
+	
+	// Lays out by the longest row item, up... then do all the whole thing.
+	private void midUpDownScheme(ArrayList<ArrayList<LayeredNode>> nodeLayers, int levelWithMax) {
+		LevelComparator comparator = new LevelComparator();
+		
+		//ArrayList<LayeredNode> level = nodeLayers.get(levelWithMax);
+		ArrayList<LayeredNode> level = nodeLayers.get(nodeLayers.size() - 1);
+		Collections.sort(level, comparator);
+		
+		Random random = new Random(1);
+		intializeLevel(level, random);
+		double min = Double.MAX_VALUE;
+		
+		if (nodeLayers.size() > 2) {
+			// Going from the last item unwrapping upwards
+			min = Math.min(min, uncrossLayers(nodeLayers, nodeLayers.size() - 2, 0, comparator));
+			
+			// Going from the first item unwrapping downward
+			min = Math.min(min, uncrossLayers(nodeLayers, 1, nodeLayers.size() - 1, comparator));
+		}
+		else if (nodeLayers.size() > 1) {
+			min = uncrossLayers(nodeLayers, 1, 1, comparator);
+		}
+		
+		System.out.println("min:" + min);
+		double scale = min > 0 ? MIN_X_SPACING / min : MIN_X_SPACING;
+		scale = Math.max(scale, 1);
 		
+		assignPointsToFlowNodes(nodeLayers, scale, MIN_Y_SPACING);
+	}
+	
+	private void intializeLevel(ArrayList<LayeredNode> layers, Random random) {
+		double starting = 0;
+		for (LayeredNode node: layers) {
+			node.setX(starting);
+			node.setMaxX(starting + 0.5);
+			node.setMinX(starting - 0.5);
+
+			// Why random hopping? between 0.5 to 1.0
+			double randomHop = random.nextDouble();
+			starting += (1 + randomHop*0.5);
+		}
+	}
+
+	private double uncrossLayers(ArrayList<ArrayList<LayeredNode>> layers, int from, int to, LevelComparator comparator) {
+		double minDistance = Double.MAX_VALUE;
+
+		if (from < to) {
+			for (int i = from; i <= to; ++i) {
+				// Uncross layer
+				ArrayList<LayeredNode> layer = layers.get(i);
+				uncrossLayerFromIn(layer);
+				// Sort layer
+				Collections.sort(layer, comparator);
+				minDistance = Math.min(minDistance, separateLevels(layer));
+			}
+		}
+		else if (to < from){
+			for (int i = from; i >= to; --i) {
+				// Uncross layer
+				ArrayList<LayeredNode> layer = layers.get(i);
+				uncrossLayerFromOut(layer);
+				// Sort layer
+				Collections.sort(layer, comparator);
+				minDistance = Math.min(minDistance, separateLevels(layer));
+			}
+		}
+		else {
+			uncrossLayerFromIn(layers.get(from));
+		}
+		
+		return minDistance;
+	}
+	
+	private double separateLevels(ArrayList<LayeredNode> layer) {
+		int startIndex = -1;
+		int endIndex = -1;
+		
+		if (layer.size() == 1) {
+			return Double.MAX_VALUE;
+		}
+		
+		double xPrev = Double.NaN;
+		double xCurrent = Double.NaN;
+		
+		double minDistance = Double.MAX_VALUE;
+		
+		for (int i = 0; i < layer.size(); ++i) {
+			LayeredNode node = layer.get(i);
+			xCurrent = node.getX();
+			if (Double.isNaN(xPrev)) {
+				xPrev = xCurrent;
+				continue;
+			}
+			
+			double delta = xCurrent - xPrev;
+			if (delta < EPSILON) {
+				if (startIndex == -1) {
+					startIndex = i - 1;
+				}
+				endIndex = i;
+			}
+			else {
+				if (startIndex != -1) {
+					minDistance = Math.min(separateRange(layer, startIndex, endIndex), minDistance);
+					// Reset
+					startIndex = -1;
+					endIndex = -1;
+				}
+				else {
+					minDistance = Math.min(delta, minDistance);
+				}
+			}
+			
+			xPrev = xCurrent;
+		}
+		
+		// Finish it off
+		if (startIndex != -1) {
+			minDistance = Math.min(separateRange(layer, startIndex, layer.size() - 1), minDistance);
+		}
+		
+		return minDistance;
+	}
+	
+	// Range is inclusive
+	private double separateRange(ArrayList<LayeredNode> layer, int startIndex, int endIndex) {
+		double startSplit = 0;
+		double endSplit = 0;
+		if (startIndex == 0) {
+			startSplit = layer.get(0).getMinX();
+		}
+		else {
+			startSplit = (layer.get(startIndex).getX() + layer.get(startIndex - 1).getX())/2.0;
+		}
+
+		if (endIndex == layer.size() - 1) {
+			endSplit = layer.get(endIndex).getMaxX();
+		}
+		else {
+			endSplit = (layer.get(endIndex + 1).getX() + layer.get(endIndex).getX())/2.0;
+		}
+		
+		double deltaDiff = endSplit - startSplit;
+		if (deltaDiff < EPSILON) {
+			System.err.println("WTH It's 0!!");
+		}
+		else {
+			// startIndex - endIndex should be at least 2.
+			double step = deltaDiff / (double)(endIndex - startIndex);
+			double start = startSplit;
+			for (int i = startIndex; i <= endIndex; ++i) {
+				LayeredNode node = layer.get(i);
+				node.setX(start);
+				start += step;
+			}
+			
+			return step;
+		}
+
+		return Double.NaN;
 	}
 
+	//Return the scale
+	private void uncrossLayerFromIn(ArrayList<LayeredNode> layer) {
+		for (LayeredNode node: layer) {
+			double xSum = 0;
+			double minX = Double.POSITIVE_INFINITY;
+			double maxX = Double.NEGATIVE_INFINITY;
+			int count = 0;
+			
+			for (LayeredNode upperLayer : node.getInNode()) {
+				minX = Math.min(minX, upperLayer.getMinX());
+				maxX = Math.max(maxX, upperLayer.getMaxX());
+				xSum += upperLayer.getX();
+				
+				count++;
+			}
+			
+			if (count == 0) {
+				System.err.println("This is not right");
+			}
+			else {
+				double x = count == 0 ? 0 : xSum / (double)count;
+				node.setX(x);
+				node.setMaxX(maxX);
+				node.setMinX(minX);
+			}
+		}
+	}
+	
+	private void uncrossLayerFromOut(ArrayList<LayeredNode> layer) {
+		for (LayeredNode node: layer) {
+			double xSum = 0;
+			double minX = Double.POSITIVE_INFINITY;
+			double maxX = Double.NEGATIVE_INFINITY;
+			int count = 0;
+			
+			
+			for (LayeredNode lowerLayer : node.getOutNode()) {
+				minX = Math.min(minX, lowerLayer.getMinX());
+				maxX = Math.max(maxX, lowerLayer.getMaxX());
+				xSum += lowerLayer.getX();
+				
+				count++;
+			}
+			
+			if (count == 0) {
+				System.err.println("This is not right");
+			}
+			else {
+				double x = xSum / (double)count;
+				node.setX(x);
+				node.setMaxX(maxX);
+				node.setMinX(minX);
+			}
+		}
+	}
+	
+	private ArrayList<ArrayList<LayeredNode>> setupFlow(Flow flow) {
+		Map<String, Node> nodesMap = flow.getNodeMap();
+		int levels = flow.getNumLevels();
+
+		ArrayList<ArrayList<LayeredNode>> nodeLevels = new ArrayList<ArrayList<LayeredNode>>();
+		for (int i = 0; i < levels + 1; ++i) {
+			nodeLevels.add(new ArrayList<LayeredNode>());
+		}
+
+		HashMap<String, WrappedNode> layeredNodeMap = new HashMap<String, WrappedNode>();
+		for (Node node: nodesMap.values()) {
+			int level = node.getLevel();
+			WrappedNode wNode = new WrappedNode(node);
+			layeredNodeMap.put(node.getId(), wNode);
+
+			ArrayList<LayeredNode> nodeList = nodeLevels.get(level);
+			nodeList.add(wNode);
+		}
+		
+		// Adding edges and dummy nodes.
+		for(Edge edge : flow.getEdges()) {
+			if (edge.hasError()) {
+				continue;
+			}
+
+			LayeredNode source = layeredNodeMap.get(edge.getSourceId());
+			LayeredNode dest = layeredNodeMap.get(edge.getTargetId());
+			int sourceLevel = source.getLevel();
+			int destLevel = source.getLevel();
+			
+			for (int index = sourceLevel + 1; index < destLevel; index++) {
+				LayeredNode dummyNode = new LayeredNode();
+				dummyNode.setLevel(index);
+				ArrayList<LayeredNode> nodeList = nodeLevels.get(index);
+				nodeList.add(dummyNode);
+
+				source.addOutNode(dummyNode);
+				dummyNode.addInNode(source);
+				source = dummyNode;
+			}
+			source.addOutNode(dest);
+			dest.addInNode(source);
+		}
+		
+		return nodeLevels;
+	}
+	
 	private class WrappedNode extends LayeredNode {
 		private Node node;
 		public WrappedNode(Node node) {
 			this.node = node;
+			super.level = node.getLevel();
 		}
 		public Node getNode() {
 			return node;
 		}
+		@Override
+		public String getId() {
+			return node.getId();
+		}
 	}
 
 	private class LayeredNode {
-		private Point2D point;
 		private int level;
 		private ArrayList<LayeredNode> inNodes;
 		private ArrayList<LayeredNode> outNodes;
-
+		private double minX;
+		private double maxX;
+		private double x;
+		private double y;
+		
+		public LayeredNode() {
+			inNodes = new ArrayList<LayeredNode>();
+			outNodes = new ArrayList<LayeredNode>();
+		}
+		
+		public String getId() {
+			return "dummy";
+		}
+		
 		public int getLevel() {
 			return level;
 		}
 		public void setLevel(int level) {
 			this.level = level;
 		}
-		public Point2D getPoint() {
-			return point;
+		public double getX() {
+			return x;
+		}
+		public void setX(double x) {
+			this.x = x;
+		}
+		public double getMinX() {
+			return minX;
+		}
+		public void setMinX(double min) {
+			minX = min;
+		}
+		public double getMaxX() {
+			return maxX;
+		}
+		public void setMaxX(double max) {
+			this.maxX = max;
 		}
-		public void setPoint(Point2D point) {
-			this.point = point;
+		public void setY(double y) {
+			this.y = y;
+		}
+		public double getY() {
+			return y;
+		}
+		
+		public void addInNode(LayeredNode node) {
+			inNodes.add(node);
+		}
+		
+		public void addOutNode(LayeredNode node) {
+			outNodes.add(node);
+		}
+		
+		public List<LayeredNode> getInNode() {
+			return inNodes;
+		}
+		
+		public List<LayeredNode> getOutNode() {
+			return outNodes;
+		}
+	}
+	
+	private void printLayer(String flowName, ArrayList<ArrayList<LayeredNode>> nodeLayers, int maxNodesInLevel, int levelWithMax) {
+		System.out.println("Layout " + flowName);
+		System.out.println("  Max Width " + maxNodesInLevel);
+		System.out.println("  Level with max " + levelWithMax);
+		int index = 0;
+		for (ArrayList<LayeredNode> lnodes: nodeLayers) {
+			StringBuffer nodeStr = new StringBuffer();
+			nodeStr.append("  ");
+			nodeStr.append(index);
+			nodeStr.append(" [");
+			for (LayeredNode node: lnodes) {
+				nodeStr.append(node.getId());
+				nodeStr.append(",");
+			}
+			nodeStr.append("]");
+			index++;
+			System.out.println(nodeStr);
+		}
+		
+	}
+	
+	private class LevelComparator implements Comparator<LayeredNode> {
+
+		@Override
+		public int compare(LayeredNode o1, LayeredNode o2) {
+			Double x1 = o1.getX();
+			Double x2 = o2.getX();
+			
+			return x1.compareTo(x2);
 		}
 	}
 }
diff --git a/src/java/azkaban/flow/Node.java b/src/java/azkaban/flow/Node.java
index 97320e4..bf37599 100644
--- a/src/java/azkaban/flow/Node.java
+++ b/src/java/azkaban/flow/Node.java
@@ -4,6 +4,8 @@ import java.awt.geom.Point2D;
 import java.util.HashMap;
 import java.util.Map;
 
+import azkaban.utils.Utils;
+
 public class Node {
 	public enum State {
 		FAILED, SUCCEEDED, RUNNING, WAITING, IGNORED
@@ -14,7 +16,7 @@ public class Node {
 	private String propsSource;
 	private State state = State.WAITING;
 
-	private Point2D.Double position = null;
+	private Point2D position = null;
 	private int level;
 	private int expectedRunTimeSec = 1;
 
@@ -45,14 +47,18 @@ public class Node {
 		this.state = state;
 	}
 
-	public Point2D.Double getPosition() {
+	public Point2D getPosition() {
 		return position;
 	}
 
-	public void setPosition(Point2D.Double position) {
+	public void setPosition(Point2D position) {
 		this.position = position;
 	}
 
+	public void setPosition(double x, double y) {
+		this.position = new Point2D.Double(x,y);
+	}
+	
 	public int getLevel() {
 		return level;
 	}
@@ -111,9 +117,18 @@ public class Node {
 		
 		Map<String,Object> layoutInfo = (Map<String,Object>)mapObj.get("layout");
 		if (layoutInfo != null) {
-			Double x = (Double)layoutInfo.get("x");
-			Double y = (Double)layoutInfo.get("y");
-			Integer level = (Integer)layoutInfo.get("level");
+			Double x = null;
+			Double y = null;
+			Integer level = null;
+
+			try {
+				x = Utils.convertToDouble(layoutInfo.get("x"));
+				y = Utils.convertToDouble(layoutInfo.get("y"));
+				level = (Integer)layoutInfo.get("level");
+			}
+			catch (ClassCastException e) {
+				throw new RuntimeException("Error creating node " + id, e);
+			}
 			
 			if (x != null && y != null) {
 				node.setPosition(new Point2D.Double(x, y));
@@ -136,8 +151,8 @@ public class Node {
 
 		HashMap<String, Object> layoutInfo = new HashMap<String, Object>();
 		if (position != null) {
-			layoutInfo.put("x", position.x);
-			layoutInfo.put("y", position.y);
+			layoutInfo.put("x", position.getX());
+			layoutInfo.put("y", position.getY());
 		}
 		layoutInfo.put("level", level);
 		objMap.put("layout", layoutInfo);
diff --git a/src/java/azkaban/project/FileProjectManager.java b/src/java/azkaban/project/FileProjectManager.java
index 041d90f..22b7efc 100644
--- a/src/java/azkaban/project/FileProjectManager.java
+++ b/src/java/azkaban/project/FileProjectManager.java
@@ -16,6 +16,7 @@ import org.joda.time.format.DateTimeFormat;
 import org.joda.time.format.DateTimeFormatter;
 
 import azkaban.flow.Flow;
+import azkaban.flow.LayeredFlowLayout;
 import azkaban.user.Permission;
 import azkaban.user.Permission.Type;
 import azkaban.user.User;
@@ -108,13 +109,33 @@ public class FileProjectManager implements ProjectManager {
 							try {
 								objectizedFlow = JSONUtils.parseJSONFromFile(flowFile);
 							} catch (IOException e) {
-								logger.error("Error parsing flow file " + flowFile.toString());
+								logger.error("Error parsing flow file " + flowFile.toString(), e);
+								continue;
 							}
 
 							//Recreate Flow
-							Flow flow = Flow.flowFromObject(objectizedFlow);
+							Flow flow = null;
+							
+							try {
+								flow = Flow.flowFromObject(objectizedFlow);
+							}
+							catch (Exception e) {
+								logger.error("Error loading flow " + flowFile.getName() + " in project " + project.getName(), e);
+								continue;
+							}
 							logger.debug("Loaded flow " + project.getName() + ": " + flow.getId());
 							flow.initialize();
+							//if (!flow.isLayedOut()) {
+								LayeredFlowLayout layout = new LayeredFlowLayout();
+								layout.layoutFlow(flow);
+								
+								try {
+									writeFlowFile(flowFile.getParentFile(), flow);
+								} catch (IOException e) {
+									e.printStackTrace();
+								}
+							//}
+							
 							flowMap.put(flow.getId(), flow);
 						}
 
@@ -183,11 +204,13 @@ public class FileProjectManager implements ProjectManager {
 				if (flow.getErrors() != null) {
 					errors.addAll(flow.getErrors());
 				}
+				flow.initialize();
+				LayeredFlowLayout layout = new LayeredFlowLayout();
+				layout.layoutFlow(flow);
 				writeFlowFile(installDir, flow);
 			} catch (IOException e) {
 				throw new ProjectManagerException(
-						"Project directory " + projectName + 
-						" cannot be created in " + projectDirectory, e);
+						"Project directory " + projectName + " cannot be created in " + projectDirectory, e);
 			}
 		}
 		
@@ -310,6 +333,12 @@ public class FileProjectManager implements ProjectManager {
 		Object object = flow.toObject();
 		String filename = flow.getId() + FLOW_EXTENSION;
 		File outputFile = new File(directory, filename);
+		File oldOutputFile = new File(directory, filename + ".old");
+		
+		if (outputFile.exists()) {
+			outputFile.renameTo(oldOutputFile);
+		}
+		
 		logger.info("Writing flow file " + outputFile);
 		String output = JSONUtils.toJSON(object, true);
 		
@@ -324,6 +353,10 @@ public class FileProjectManager implements ProjectManager {
 			throw e;
 		}
 		writer.close();
+		
+		if (oldOutputFile.exists()) {
+			oldOutputFile.delete();
+		}
 	}
 
 	@Override
diff --git a/src/java/azkaban/utils/Utils.java b/src/java/azkaban/utils/Utils.java
index 1d22bb7..5d95084 100644
--- a/src/java/azkaban/utils/Utils.java
+++ b/src/java/azkaban/utils/Utils.java
@@ -160,4 +160,12 @@ public class Utils {
     	}
     	return buffer.toString();
     }
+    
+    public static Double convertToDouble(Object obj) {
+    	if (obj instanceof String) {
+    		return Double.parseDouble((String)obj);
+    	}
+    	
+    	return (Double)obj;
+    }
 }
\ No newline at end of file
diff --git a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
index e11e39e..986a8e7 100644
--- a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
+++ b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
@@ -121,47 +121,90 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 		}
 
 		String jsonName = getParam(req, "json");
-		if (jsonName.equals("expandflow")) {
-			String flowId = getParam(req, "flow");
-			Flow flow = project.getFlow(flowId);
+		if (jsonName.equals("fetchflowlist")) {
+			jsonFetchFlow(project, ret, req, resp);
+		}
+		else if (jsonName.equals("fetchflowgraph")) {
+			jsonFetchFlowGraph(project, ret, req, resp);
+		}
+		
+		this.writeJSON(resp, ret);
+	}
+	
+	private void jsonFetchFlowGraph(Project project, HashMap<String, Object> ret, HttpServletRequest req, HttpServletResponse resp) throws ServletException {
+		String flowId = getParam(req, "flow");
+		Flow flow = project.getFlow(flowId);
+		
+		//Collections.sort(flowNodes, NODE_LEVEL_COMPARATOR);
+		ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();
+		for (Node node: flow.getNodes()) {
+			HashMap<String, Object> nodeObj = new HashMap<String,Object>();
+			nodeObj.put("id", node.getId());
+			nodeObj.put("x", node.getPosition().getX());
+			nodeObj.put("y", node.getPosition().getY());
+			if (node.getState() != Node.State.WAITING) {
+				nodeObj.put("state", node.getState());
+			}
+			nodeList.add(nodeObj);
+		}
+		
+		ArrayList<Map<String, Object>> edgeList = new ArrayList<Map<String, Object>>();
+		for (Edge edge: flow.getEdges()) {
+			HashMap<String, Object> edgeObj = new HashMap<String,Object>();
+			edgeObj.put("from", edge.getSourceId());
+			edgeObj.put("target", edge.getTargetId());
+			
+			if (edge.hasError()) {
+				edgeObj.put("error", edge.getError());
+			}
+			
+			edgeList.add(edgeObj);
+		}
+		
+		ret.put("flowId", flowId);
+		ret.put("nodes", nodeList);
+		ret.put("edges", edgeList);
+	}
+	
+	private void jsonFetchFlow(Project project, HashMap<String, Object> ret, HttpServletRequest req, HttpServletResponse resp) throws ServletException {
+		String flowId = getParam(req, "flow");
+		Flow flow = project.getFlow(flowId);
 
-			ArrayList<Node> flowNodes = new ArrayList<Node>(flow.getNodes());
-			Collections.sort(flowNodes, NODE_LEVEL_COMPARATOR);
+		ArrayList<Node> flowNodes = new ArrayList<Node>(flow.getNodes());
+		Collections.sort(flowNodes, NODE_LEVEL_COMPARATOR);
 
-			ArrayList<Object> nodeList = new ArrayList<Object>();
-			for (Node node: flowNodes) {
-				HashMap<String, Object> nodeObj = new HashMap<String, Object>();
-				nodeObj.put("id", node.getId());
-				
-				ArrayList<String> dependencies = new ArrayList<String>();
-				Collection<Edge> collection = flow.getInEdges(node.getId());
-				if (collection != null) {
-					for (Edge edge: collection) {
-						dependencies.add(edge.getSourceId());
-					}
+		ArrayList<Object> nodeList = new ArrayList<Object>();
+		for (Node node: flowNodes) {
+			HashMap<String, Object> nodeObj = new HashMap<String, Object>();
+			nodeObj.put("id", node.getId());
+			
+			ArrayList<String> dependencies = new ArrayList<String>();
+			Collection<Edge> collection = flow.getInEdges(node.getId());
+			if (collection != null) {
+				for (Edge edge: collection) {
+					dependencies.add(edge.getSourceId());
 				}
-				
-				ArrayList<String> dependents = new ArrayList<String>();
-				collection = flow.getOutEdges(node.getId());
-				if (collection != null) {
-					for (Edge edge: collection) {
-						dependents.add(edge.getTargetId());
-					}
+			}
+			
+			ArrayList<String> dependents = new ArrayList<String>();
+			collection = flow.getOutEdges(node.getId());
+			if (collection != null) {
+				for (Edge edge: collection) {
+					dependents.add(edge.getTargetId());
 				}
-				
-				nodeObj.put("dependencies", dependencies);
-				nodeObj.put("dependents", dependents);
-				nodeObj.put("level", node.getLevel());
-				nodeList.add(nodeObj);
 			}
 			
-			ret.put("flowId", flowId);
-			ret.put("nodes", nodeList);
+			nodeObj.put("dependencies", dependencies);
+			nodeObj.put("dependents", dependents);
+			nodeObj.put("level", node.getLevel());
+			nodeList.add(nodeObj);
 		}
 		
-		this.writeJSON(resp, ret);
+		ret.put("flowId", flowId);
+		ret.put("nodes", nodeList);
 	}
 	
+	
 	private void handleFlowPage(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException {
 		Page page = newPage(req, resp, session, "azkaban/webapp/servlet/velocity/flowpage.vm");
 		String projectName = getParam(req, "project");
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
index 145d820..6d3f7bd 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
@@ -10,6 +10,7 @@
 		<script type="text/javascript" src="${context}/js/jquery.simplemodal.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.nav.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.flow.view.js"></script>
+		<script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -42,7 +43,7 @@
 						<h3><a href="${context}/manager?project=${project.name}&flow=${flowid}">Flow <span>$flowid</span></a></h3>
 					</div>
 					
-					<div class="headertabs">
+					<div id="headertabs" class="headertabs">
 						<ul>
 							<li><a id="graphViewLink" href="#graph">Graph</a></li>
 							<li class="lidivider">|</li>
@@ -50,7 +51,15 @@
 						</ul>
 					</div>
 					<div id="graphView">
-					<p>This is my graph view</p>
+						<div class="relative">
+							<div id="jobList">
+								Loading Flow ${flowid} 
+							</div>
+							<div id="svgDiv" >
+								<svg id="svgGraph" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100% 100%" > 
+								</svg>
+							</div>
+						</div>
 					</div>
 					<div id="jobListView">
 					<p>This is my joblist view</p>
diff --git a/src/web/css/azkaban.css b/src/web/css/azkaban.css
index ee55776..0b640e8 100644
--- a/src/web/css/azkaban.css
+++ b/src/web/css/azkaban.css
@@ -113,7 +113,8 @@ textarea {
 .content {
   background-color: #E0E0E0;
   border: 1px solid #cdcdcd;
-  margin: 0 4.75% 10px;
+  margin: 0 50px 10px;
+  min-height: 240px;
 }
 
 .section-ft {
@@ -899,7 +900,7 @@ tr:hover td {
 }
 
 .headertabs {
-	height: 24px;
+	height: 26px;
 	width:100%;
 	background-image: -o-linear-gradient(bottom, rgb(56,56,56) 33%, rgb(73,73,73) 66%);
 	background-image: -moz-linear-gradient(bottom, rgb(56,56,56) 33%, rgb(73,73,73) 66%);
@@ -951,6 +952,46 @@ tr:hover td {
 	clear: both;
 }
 
+/* Graph SVG */
+#graphView {
+	position: absolute;
+	top: 210px;
+	bottom: 5px;
+	left: 50px;
+	right: 50px;
+	padding: 10px;
+	background: #E0E0E0;
+}
+
+.relative {
+	position: relative;
+	width: 100%;
+	height: 100%;
+}
+
+#svgGraph {
+	width: 100%;
+	height: 100%;
+	background: #fff;
+}
+
+#svgDiv {
+	position: absolute;
+	top: 0px;
+	right: 0px;
+	left: 260px;
+	bottom: 0px;
+	height: 100%;
+}
+
+#jobList {
+	position: absolute;
+	top: 0px;
+	left: 0px;
+	height: 100%;
+	width: 260px;
+}
+
 /* old styles */
 
 .azkaban-charts .hitarea {
diff --git a/src/web/js/azkaban.flow.view.js b/src/web/js/azkaban.flow.view.js
index 596f5f5..5129e40 100644
--- a/src/web/js/azkaban.flow.view.js
+++ b/src/web/js/azkaban.flow.view.js
@@ -1,7 +1,7 @@
 $.namespace('azkaban');
 
-var flowView;
-azkaban.FlowView= Backbone.View.extend({
+var flowTabView;
+azkaban.FlowTabView= Backbone.View.extend({
   events : {
   	"click #graphViewLink" : "handleGraphLinkClick",
   	"click #jobslistViewLink" : "handleJobslistLinkClick"
@@ -17,7 +17,7 @@ azkaban.FlowView= Backbone.View.extend({
 
   },
   render: function() {
-  
+  	console.log("render graph");
   },
   handleGraphLinkClick: function(){
   	$("#jobslistViewLink").removeClass("selected");
@@ -35,6 +35,65 @@ azkaban.FlowView= Backbone.View.extend({
   }
 });
 
+var svgGraphView;
+azkaban.SvgGraphView = Backbone.View.extend({
+	events: {
+	},
+	initialize: function(settings) {
+		this.model.bind('change:selected', this.changeSelected, this);
+		this.model.bind('change:graph', this.render, this);
+		
+		this.svgns = "http://www.w3.org/2000/svg";
+		this.xlinksn = "http://www.w3.org/1999/xlink";
+		
+		var graphDiv = this.el[0];
+		var svg = $('#svgGraph')[0];
+		this.svgGraph = svg;
+		
+		var gNode = document.createElementNS(this.svgns, 'g');
+		gNode.setAttribute("id", "group");
+		svg.appendChild(gNode);
+		this.mainG = gNode;
+
+		$(svg).svgNavigate();
+	},
+	render: function(self) {
+		console.log("graph render");
+
+		var data = this.model.get("data");
+		var nodes = data.nodes;
+		for (var i = 0; i < nodes.length; ++i) {
+			this.drawNode(this, nodes[i]);
+		}
+	},
+	changeSelected: function(self) {
+		console.log("change selected");
+	},
+	drawNode: function(self, node) {
+		var svg = self.svgGraph;
+		var svgns = self.svgns;
+
+		var nodeG = document.createElementNS(svgns, "g");
+		nodeG.setAttributeNS(null, "id", node.id);
+		nodeG.setAttributeNS(null, "font-family", "helvetica");
+		nodeG.setAttributeNS(null, "transform", "translate(" + (node.x * 100) + "," + (node.y*100)+ ")");
+		
+		var rect1 = document.createElementNS(svgns, 'rect');
+		rect1.setAttributeNS(null, "y", 2);
+		rect1.setAttributeNS(null, "x", 2);
+		rect1.setAttributeNS(null, "ry", 12);
+		rect1.setAttributeNS(null, "width", 20);
+		rect1.setAttributeNS(null, "height", 30);
+		rect1.setAttributeNS(null, "style", "width:inherit;fill-opacity:1.0;stroke-opacity:1");
+		
+		nodeG.appendChild(rect1);
+		self.mainG.appendChild(nodeG);
+	}
+});
+
+var graphModel;
+azkaban.GraphModel = Backbone.Model.extend({});
+
 $(function() {
 	var selected;
 	if (window.location.hash) {
@@ -50,5 +109,21 @@ $(function() {
 			selected = "graph";
 		}
 	}
-	flowView = new azkaban.FlowView({el:$( '#all-jobs-content'), selectedView: selected });
+	flowTabView = new azkaban.FlowTabView({el:$( '#headertabs'), selectedView: selected });
+
+	graphModel = new azkaban.GraphModel();
+	svgGraphView = new azkaban.SvgGraphView({el:$('#svgDiv'), model: graphModel});
+	
+	var requestURL = contextURL + "/manager";
+
+	$.get(
+	      requestURL,
+	      {"project": projectName, "json":"fetchflowgraph", "flow":flowName},
+	      function(data) {
+	          console.log("data fetched");
+	          graphModel.set({data: data});
+	          graphModel.trigger("change:graph");
+	      },
+	      "json"
+	    );
 });
diff --git a/src/web/js/azkaban.project.view.js b/src/web/js/azkaban.project.view.js
index 6616e4a..76dee8d 100644
--- a/src/web/js/azkaban.project.view.js
+++ b/src/web/js/azkaban.project.view.js
@@ -82,7 +82,7 @@ azkaban.FlowTableView= Backbone.View.extend({
 	    
 	    $.get(
 	      requestURL,
-	      {"project": projectId, "json":"expandflow", "flow":targetId},
+	      {"project": projectId, "json":"fetchflowlist", "flow":targetId},
 	      function(data) {
 	        console.log("Success");
 	        target.loaded = true;
diff --git a/src/web/js/svgNavigate.js b/src/web/js/svgNavigate.js
new file mode 100644
index 0000000..71f1915
--- /dev/null
+++ b/src/web/js/svgNavigate.js
@@ -0,0 +1,350 @@
+(function($) {	
+
+	var mouseUp = function(evt) {
+		var target = evt.target;
+		target.mx = evt.clientX;
+		target.my = evt.clientY;
+		target.mDown = false;
+	}
+	
+	var mouseDown = function(evt) {
+		//alert("mouseDown");
+		var target = evt.target;
+		target.mx = evt.clientX;
+		target.my = evt.clientY;
+		target.mDown = true;
+	}
+	
+	var mouseOut = function(evt) {
+		var target = evt.target;
+		target.mx = evt.clientX;
+		target.my = evt.clientY;
+		target.mDown = false;
+	}
+	
+	var mouseMove = function(evt) {
+		var target = evt.target;
+		if (target.mDown) {
+			var dx = evt.clientX - target.mx;
+			var dy = evt.clientY - target.my;
+			
+			evt.dragX = dx;
+			evt.dragY = dy;
+			mouseDrag(evt);
+		}
+		
+		target.mx = evt.clientX;
+		target.my = evt.clientY;
+	}
+	
+	var mouseDrag = function(evt) {
+		//alert("mouseDragged ");
+		translateDeltaGraph(evt.target, evt.dragX, evt.dragY);
+	}
+	
+	var mouseScrolled = function(evt) {
+		//alert("scroll");
+		if (!evt) {
+			evt = window.event;
+		}
+		var target = evt.target;
+		var leftOffset = 0;
+		var topOffset = 0;
+		if (!target.marker) {
+			while (!target.farthestViewportElement) {
+				target = target.parentNode;
+			}
+
+			target = target.farthestViewportElement;
+		}
+		
+		if (target.parentNode.offsetLeft) {
+			leftOffset = target.parentNode.offsetLeft;
+		}
+		if (target.parentNode.offsetTop) {
+			topOffset = target.parentNode.offsetTop;
+		}
+		
+		// Trackball/trackpad vs wheel. Need to accommodate
+		var delta = 0;
+		if (evt.wheelDelta) {
+			delta = event.wheelDelta / 120;
+		}
+		else if (evt.detail) {
+			delta = -evt.detail / 3;
+		}
+		
+		var zoomLevel = boundZoomLevel(target, Math.floor(target.zoomIndex + delta));
+		target.zoomIndex = zoomLevel;
+		var scale = target.zoomLevels[zoomLevel];
+		
+		var x = evt.clientX - leftOffset;
+		var y = evt.clientY - topOffset;
+		
+		scaleGraph(target, scale, x, y);
+	}
+	
+	this.boundZoomLevel = function(target, level) {
+		if (level >= target.settings.zoomNumLevels) {
+			return target.settings.zoomNumLevels - 1;
+		}
+		else if (level <= 0 ) {
+			return 0;
+		}
+		
+		return level;
+	}
+	
+	this.scaleGraph = function(target, scale, x, y) {
+		var sfactor = scale/target.scale;
+		target.scale = scale;
+		
+		target.translateX = sfactor*target.translateX + x - sfactor*x;
+		target.translateY = sfactor*target.translateY + y - sfactor*y;
+		
+		if (target.model) {
+			target.model.trigger("scaled");
+		}
+		retransform(target);
+	}
+	
+	this.translateDeltaGraph = function(target, x, y) {
+		target.translateX += x;
+		target.translateY += y;
+		if (target.model) {
+			target.model.trigger("panned");
+		}
+		retransform(target);
+	}
+	
+	this.retransform = function(target) {
+		//$(target).children('g').attr("transform", "translate(" + target.translateX + "," + target.translateY + ") scale(" + target.scale + ")");
+		var gs = target.childNodes;
+		
+		var transformString = "translate(" + target.translateX + "," + target.translateY + ") scale(" + target.scale + ")";
+		for (var i = 0; i < gs.length; ++i) {
+			var g = gs[i];
+			if (g.nodeName == 'g') {
+				g.setAttribute("transform", transformString);
+			}
+		}
+		
+		if (target.model) {
+			var obj = target.model.get("transform");
+			if (obj) {
+				obj.scale = target.scale;
+				obj.height = target.parentNode.clientHeight;
+				obj.width = target.parentNode.clientWidth;
+				
+				obj.x1 = target.translateX;
+				obj.y1 = target.translateY;
+				obj.x2 = obj.x1 + obj.width*obj.scale;
+				obj.y2 = obj.y1 + obj.height*obj.scale;
+			}
+		}
+	}
+	
+	this.resetTransform = function(target) {
+		var settings = target.settings;
+		target.translateX = settings.x;
+		target.translateY = settings.y;
+		
+		if (settings.x < settings.x2) {
+			var factor = 0.90;
+			
+			// Reset scale and stuff.
+			var divHeight = target.parentNode.clientHeight;
+			var divWidth = target.parentNode.clientWidth;
+			
+			var width = settings.x2 - settings.x;
+			var height = settings.y2 - settings.y;
+			var aspectRatioGraph = height/width;
+			var aspectRatioDiv = divHeight/divWidth;
+			
+			var scale = aspectRatioGraph > aspectRatioDiv ? (divHeight/height)*factor : (divWidth/width)*factor;
+			target.scale = scale;
+		}
+		else {
+			target.zoomIndex = boundZoomLevel(target, settings.zoomIndex);
+			target.scale = target.zoomLevels[target.zoomIndex];
+		}
+	}
+	
+	this.animateTransform = function(target, scale, x, y, duration) {
+		var zoomLevel = calculateZoomLevel(scale, target.zoomLevels);
+		target.fromScaleLevel = target.zoomIndex;
+		target.toScaleLevel = zoomLevel;
+		target.fromX = target.translateX;
+		target.fromY = target.translateY;
+		target.fromScale = target.scale;
+		target.toScale = target.zoomLevels[zoomLevel];
+		target.toX = x;
+		target.toY = y;
+		target.startTime = new Date().getTime();
+		target.endTime = target.startTime + duration;
+		
+		this.animateTick(target);
+	}
+	
+	this.animateTick = function(target) {
+		var time = new Date().getTime();
+		if (time < target.endTime) {
+			var timeDiff = time - target.startTime;
+			var progress = timeDiff / (target.endTime - target.startTime);
+			
+			//target.zoomIndex = Math.floor((target.toScaleLevel - target.fromScaleLevel)*progress) + target.fromScaleLevel;
+			//target.scale = target.zoomLevels[target.zoomIndex];
+			target.scale = (target.toScale - target.fromScale)*progress + target.fromScale;
+			target.translateX = (target.toX - target.fromX)*progress + target.fromX;
+			target.translateY = (target.toY - target.fromY)*progress + target.fromY;
+			retransform(target);
+			setTimeout(function() {this.animateTick(target)}, 1);
+		}
+		else {
+			target.zoomIndex = target.toScaleLevel;
+			target.scale = target.zoomLevels[target.zoomIndex];
+			target.translateX = target.toX;
+			target.translateY = target.toY;
+			retransform(target);
+		}
+	}
+	
+	this.calculateZoomScale = function(scaleLevel, numLevels, points) {
+		if (scaleLevel <= 0) {
+			return points[0];
+		}
+		else if (scaleLevel >= numLevels) {
+			return points[points.length - 1];
+		}
+		var factor = (scaleLevel / numLevels) * (points.length - 1);
+		var floorIdx = Math.floor(factor);
+		var ceilingIdx = Math.ceil(factor);
+		
+		var b = factor - floorIdx;
+		
+		return b*(points[ceilingIdx] - points[floorIdx]) + points[floorIdx];
+	}
+	
+	this.calculateZoomLevel = function(scale, zoomLevels) {
+		if (scale >= zoomLevels[zoomLevels.length - 1]) {
+			return zoomLevels.length - 1;
+		}
+		else if (scale <= zoomLevels[0]) {
+			return 0;
+		}
+		
+		var i = 0;
+		// Plain old linear scan
+		for (; i < zoomLevels.length; ++i) {
+			if (scale < zoomLevels[i]) {
+				i--;
+				break;
+			}
+		}
+		
+		if (i < 0) {
+			return 0;
+		}
+		
+		return i;
+	}
+	
+	var methods = {	
+		init : function(options) {
+			var settings = {
+				x: 0,
+				y: 0,
+				x2: 0,
+				y2: 0,
+				minX: -1000,
+				minY: -1000,
+				maxX: 1000,
+				maxY: 1000,
+				zoomIndex: 24,
+				zoomPoints: [0.1, 0.14, 0.2, 0.4, 0.8, 1, 1.6, 2.4, 4, 8, 16],
+				zoomNumLevels: 48
+			};
+			if (options) {
+				$.extend(settings, options);
+			}
+			return this.each(function() {
+				var $this = $(this);
+				this.settings = settings;
+				this.marker = true;
+				
+				if (window.addEventListener) this.addEventListener('DOMMouseScroll', mouseScrolled, false);
+				this.onmousewheel = mouseScrolled;
+				this.onmousedown = mouseDown;
+				this.onmouseup = mouseUp;
+				this.onmousemove = mouseMove;
+				this.onmouseout = mouseOut;
+				
+				this.zoomLevels = new Array(settings.zoomNumLevels);
+				for (var i = 0; i < settings.zoomNumLevels; ++i) {
+					var scale = calculateZoomScale(i, settings.zoomNumLevels, settings.zoomPoints);
+					this.zoomLevels[i] = scale;
+				}
+				resetTransform(this);
+			});
+		},
+		transformToBox : function(arguments) {
+			var $this = $(this);
+			var target = ($this)[0];
+			var x = arguments.x;
+			var y = arguments.y;
+			var factor = 0.9;
+			
+			var width = arguments.width ? arguments.width : 1;
+			var height = arguments.height ? arguments.height : 1;
+			
+			var divHeight = target.parentNode.clientHeight;
+			var divWidth = target.parentNode.clientWidth;
+			
+			var aspectRatioGraph = height/width;
+			var aspectRatioDiv = divHeight/divWidth;
+
+			var scale = aspectRatioGraph > aspectRatioDiv ? (divHeight/height)*factor : (divWidth/width)*factor;
+			console.log("(" + x + "," + y + "," + width.toPrecision(4) + "," + height.toPrecision(4) + ")");
+			console.log("(rg:" + aspectRatioGraph.toPrecision(3) + ",rd:" + aspectRatioDiv.toPrecision(3) + "," + scale.toPrecision(3) + ")");
+			
+			// Center
+			var scaledWidth = width*scale;
+			var scaledHeight = height*scale;
+			
+			var sx = (divWidth - scaledWidth)/2 -scale*x;
+			var sy = (divHeight - scaledHeight)/2 -scale*y;
+			console.log("sx,sy:" + sx + "," + sy);
+			animateTransform(target, scale, sx, sy, 500);
+		},
+		attachNavigateModel : function(arguments) {
+			var $this = $(this);
+			var target = ($this)[0];
+			target.model = arguments;
+			
+			if (target.model) {
+				var obj = {};
+				obj.scale = target.scale;
+				obj.height = target.parentNode.clientHeight;
+				obj.width = target.parentNode.clientWidth;
+				
+				obj.x1 = target.translateX;
+				obj.y1 = target.translateY;
+				obj.x2 = obj.x1 + obj.height*obj.scale;
+				obj.y2 = obj.y1 + obj.width*obj.scale;
+				
+				target.model.set({transform : obj});
+			}
+		}
+	};
+	
+	// Main Constructor
+	$.fn.svgNavigate = function(method) {
+		if (methods[method]) {
+			return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+		} else if (typeof method === 'object' || !method) {
+			return methods.init.apply(this, arguments);
+		} else {
+			$.error('Method ' + method + ' does not exist on svgNavigate');
+		}
+	};
+})(jQuery);
\ No newline at end of file