Prototyping a Project Management App - Part 5

Alright, it's finally code time but before we dive into our functions, I want to talk briefly about my "Configuration" section. When writing code, it's common to setup a number of variables that can be easily tweaked from a central location. For example, let's say we wanted all our boxes to be standardized to the same size, but we knew that we wanted the ability to change these defaults later on, perhaps in relation to the type of device our user has, or even the resolution of their screen. If we have hard-coded the values for box width and height into our various functions, then it could be quite complicated to handle these changes.

Here's a quick look at the configuration that I put together so far:

var Configuration = {
 activeStep: null,
 minWidthBetweenBoxes: 40,
 minHeightBetweenBoxes: 20,
 subStepPadding: 20,
 minBoxWidth: 100,
 minBoxHeight: 50,
 rootStep: {
  x: 10,
  y: 10,
  width: 0,
  height: 0,
  subSteps: [],
  nextSteps: [],
  priorSteps: [],
  parentStep: null,
  d3Rectangle: null,
  arrows: []
 }
}

Don't worry too much about "activeStep" and "rootStep" just now. I'll explain those concepts a bit later on. Hopefully the other settings are all fairly intuitive. I am simply setting default values for the size and spacing of the boxes on the screen.

Ok, onto our functions. Let's start from the top and look at our first function from the last post "createNewBox".

var createNewBox = function (stepType, parentStep, stepComingFrom) {
 var x = 0;
 var y = 0;
 var width = Configuration.minBoxWidth;
 var height = Configuration.minBoxHeight;
 var priorSteps = [];
 // If stepType == "Beginning Step"
 if (stepType == "Beginning Step" || stepType == "Sub-Step") {
  // Render a box at the top left of the screen
  // but without overlapping any existing boxes
  var dimensionsOfSiblings = getDimensionsOfSubSteps(parentStep);
  x = parentStep.x + Configuration.subStepPadding;
  if (dimensionsOfSiblings.height == 0) {
   y = parentStep.y + Configuration.subStepPadding;
  }
  else {
   y = parentStep.y + Configuration.subStepPadding + dimensionsOfSiblings.height + Configuration.minHeightBetweenBoxes;
  }                    
 }
 // If stepType == "Next Step"
 if (stepType == "Next Step") {                
  // Render a box directly to the right of stepComingFrom
  // without overlapping any existing boxes
  // Render a new box using the following values for x and y
  // x = boxComingFrom.x + boxComingFrom.width + (some fixed space between boxes)
  x = stepComingFrom.x + stepComingFrom.width + Configuration.minWidthBetweenBoxes;
  // y = boxComingFrom.y
  var dimensionsOfExistingNextSteps = getDimensionsOfNextSteps(stepComingFrom);
  y = 0;
  if (dimensionsOfExistingNextSteps.height > 0) {
   y = stepComingFrom.y + dimensionsOfExistingNextSteps.height + Configuration.minHeightBetweenBoxes;
  }
  else {
   y = stepComingFrom.y;
  }
  priorSteps.push(stepComingFrom);
 }           
 var newStep = createNewStep(x, y, width, height, priorSteps, parentStep);
 parentStep.subSteps.push(newStep);
 if (stepComingFrom != null) {
  stepComingFrom.nextSteps.push(newStep);
 }                
 renderBox(x, y, width, height, newStep);
 // If this is a next step then we need to render an arrow
 // from stepComingFrom to newStep
 if (stepComingFrom != null) {
  renderArrow(stepComingFrom.x + stepComingFrom.width, stepComingFrom.y + (0.5 * stepComingFrom.height), newStep.x, newStep.y + (0.5 * newStep.height), stepComingFrom, newStep);
 }                    
 // We have added a new step, so we need to re-size the parent step
 // This should also trigger repositioning any other steps that need it
 if (stepComingFrom != null) {
  resizeAndRepositionStep(stepComingFrom);
 }
 else {
  resizeAndRepositionStep(parentStep);
 }
}

As is typical of this prototyping step, when implementing this function and following the basic logic we outlined in comments, it became necessary to add some pieces here and there. The meat of the function does follow the comments fairly well but let's break down what is happening line by line.

Firstly, the function now takes in two additional input variables "parentStep" and "stepComingFrom". One thing I realized as soon as I started writing the code for adding a "next step" was that my logic referenced knowing the location from the "previous step" (i.e. the step that this "next step" is coming from). Similarly, the logic for adding a "sub-step" needs to know which step it is a sub-step of (i.e. what is its parent step). This meant adding these input variables to the function so that these logic flows could access the information they needed.

Whether we are adding a "beginning step", "next step" or "sub-step" each logic flow is looking to set the x and y coordinates of where the new box will be created as well as the width and height for the new box, so we start the function by defining these variables. The logic for x and y are unique to each type of step that we are adding, but when adding a new box, we'll be setting the width and height to the same default values "minBoxWidth" and "minBoxHeight" from our "Configuration" section. We also define a variable "priorSteps" and set it equal to an empty array. I'll explain what this variable is doing here once we get to creating a "next step" box.

After setting our variables, we get to our first piece of real logic. Here it is again, for your convenience:

if (stepType == "Beginning Step" || stepType == "Sub-Step") {
 // Render a box at the top left of the screen
 // but without overlapping any existing boxes
 var dimensionsOfSiblings = getDimensionsOfSubSteps(parentStep);
 x = parentStep.x + Configuration.subStepPadding;
 if (dimensionsOfSiblings.height == 0) {
  y = parentStep.y + Configuration.subStepPadding;
 }
 else {
  y = parentStep.y + Configuration.subStepPadding + dimensionsOfSiblings.height + Configuration.minHeightBetweenBoxes;
 }                    
}

Although our original comments contained different logic for adding a "Beginning Step" vs adding a "Sub-Step", when it came to coding, I realized that the logic is essentially the same. Try thinking of a "beginning step" as being the first "sub-step" of a single high level step (perhaps a step named "Do all the things!") and hopefully you'll understand what I mean.

As we discussed in our last post, the logic for adding a new sub-step is to first check for the existence of any pre-existing "sibling" sub-steps. If there are already sub-steps on our diagram, then we don't want to overlap them with our new box. We make use of our function "getDimensionsOfSubSteps" to get the width and height of the full span of any pre-existing sibling sub-steps (we'll cover the logic for "getDimensionsOfSubSteps" later on). Note that we pass this function our "parentStep" as our new step's siblings are the children of their shared parent (that's not confusing at all right?!).

Setting the x coordinate for our new box is actually simple. We just want it to be the same as the x coordinate for the parent step plus a little bit of padding ("subStepPadding" taken from our configuration). If there are no other sub-steps already sitting in our parent step, then setting our y coordinate is also simple. It's essentially the same idea, we just use the y coordinate from the parent step plus a little padding. However, if there are already some other sub-steps to consider, then we have to factor in the height of all those other sub-steps "dimensionsOfSiblings.height" as well as add an extra bit of spacing to keep our boxes nicely separated "minHeightBetweenBoxes" (again from our configuration section).

Let's look at the logic for adding a "next step":


// If stepType == "Next Step"
if (stepType == "Next Step") {                
 // Render a box directly to the right of stepComingFrom
 // without overlapping any existing boxes
 // Render a new box using the following values for x and y
 // x = boxComingFrom.x + boxComingFrom.width + (some fixed space between boxes)
 x = stepComingFrom.x + stepComingFrom.width + Configuration.minWidthBetweenBoxes;
 // y = boxComingFrom.y
 var dimensionsOfExistingNextSteps = getDimensionsOfNextSteps(stepComingFrom);
 y = 0;
 if (dimensionsOfExistingNextSteps.height > 0) {
  y = stepComingFrom.y + dimensionsOfExistingNextSteps.height + Configuration.minHeightBetweenBoxes;
 }
 else {
  y = stepComingFrom.y;
 }
 priorSteps.push(stepComingFrom);
} 

Here, we want to set our x coordinate to be some amount to the right of the box where our next-step is coming from. We do this by using the x coordinate from "stepComingFrom" as well as it's width. This essentially gives us the x coordinate for the right of the box where our "next step" is coming from. With that we just need to add a standard spacing between boxes from our configuration section "minWidthBetweenBoxes" and we're good to go.

If there are no other pre-existing "next steps" then our y coordinate can just be set to the y coordinate of our "stepComingFrom" but if there are already a set of next steps then we need to grab their dimensions from our handy function "getDimensionsOfNextSteps" (again, logic to be covered later) and be sure to include the "height" of those "dimensionsOfExistingNextSteps" as well as a standard "minHeightBetweenBoxes" from our configuration.

The last step is to add "stepComingFrom" to our priorSteps array. We'll be using this array in the next part. After we're done with the logic specific to each type of step, we have the following line of code:

var newStep = createNewStep(x, y, width, height, priorSteps, parentStep);

This calls a new function "createNewStep" and provides it with the x, y, width and height values that we have created so far. We also give it our "priorSteps" array and the "parentStep" that was provided to this function as an input. Here's the function "createNewStep":


var createNewStep = function (x, y, width, height, priorSteps, parentStep) {
 return {
  x: x,
  y: y,
  width: width,
  height: height,
  subSteps: [],
  nextSteps: [],
  priorSteps: priorSteps,
  parentStep: parentStep,
  d3Rectangle: null,
  arrows: []
 }
}

This function just creates a simple object and sets attribute values for a number of things that we want each step to track about itself. We've already covered x, y, width and height but lets go over the other properties.

  • "subSteps" is an array for holding any sub-steps that we might later add to our step. For now it begins as an empty array as our new step doesn't yet have any sub-steps
  • "nextSteps" is an array for holding any future next steps that we might add to our step. 
  • "priorSteps" we have seen before, and is there to hold any prior steps (steps that point to our new step)
  • "parentStep" is a single value that we set equal to the parent step provided to our "createNewBox" function. Each step can only have one parent box that it sits inside of, so this attribute is not an array but a single value
  • "d3Rectangle" is where we will ultimately keep a reference to the actual rectangle that we are going to render on the screen. As we didn't draw this yet, we don't set the value just now (we set to "null", which you can think of as meaning empty, nothing or no value)
  • "arrows" is an empty array that we'll later use for holding our connecting arrows between boxes
OK, back to our main function. The next lines of code are:


parentStep.subSteps.push(newStep);
if (stepComingFrom != null) {
 stepComingFrom.nextSteps.push(newStep);
}                
renderBox(x, y, width, height, newStep);

Firstly, we add our new step to the sub-steps holder of our "parentStep". Next, if we have added a "next step" then we also add our new step to the next steps holder of our "stepComingFrom". With all this bookkeeping completed, we finally call to "renderBox" to actually draw a box on the screen. The "renderBox" function is one we looked at in an earlier post, but I did extend it a little so let's take a look at it again:

var renderBox = function (x, y, width, height, newStep) {
 // select the existing SVG container that we named "svgCanvas"
 var svgCanvas = d3.select("#svgCanvas");
 // draw a rectangle using the x, y, width and height variables to set its attributes
 var box = svgCanvas
  .append("rect")
  .attr("x", x)
  .attr("y", y)
  .attr("width", width)
  .attr("height", height)
  .attr("style", "stroke:teal;stroke-width:5;fill:white;")
  .on("click", function () {
   showStepDetails(newStep);
  });
 newStep.d3Rectangle = box;
}

As before, this function finds the "svgCanvas" already present on the page and then draws a rectangle on it. I added a "click handler" to the rectangle, so when a user clicks on it, they will trigger the function "showStepDetails". We'll cover this new function later on, but its job will be to show information about the step and potentially provide access to functionality like adding new steps from the selected step.

The last line populates the "d3Rectangle" attribute of our new step by setting it equal to the newly drawn rectangle.

Moving back to "createNewBox", the next piece is:

// If this is a next step then we need to render an arrow
// from stepComingFrom to newStep
if (stepComingFrom != null) {
 renderArrow(stepComingFrom.x + stepComingFrom.width, stepComingFrom.y + (0.5 * stepComingFrom.height), newStep.x, newStep.y + (0.5 * newStep.height), stepComingFrom, newStep);
}

If the step we are adding is a "next step", then we need to render an arrow from the box we are coming from to our new step. We make use of the "renderArrow" function that we created earlier. We start the arrow at the right middle of the step the arrow is coming from, and draw it to the left middle of our new step.

The final lines of code in this function are:


// We have added a new step, so we need to re-size the parent step
// This should also trigger repositioning any other steps that need it
if (stepComingFrom != null) {
 resizeAndRepositionStep(stepComingFrom);
}
else {
 resizeAndRepositionStep(parentStep);
}

If we are adding a new sub-step, then we know we'll need to make our parent step bigger, which itself may then mean that some other steps need to re-position themselves to avoid being drawn on top of. When we add a new next step, it's sometimes the case that we need to re-position some steps earlier in our chain and then ultimately also the size of the parent step. In either case, we make use of a call to a new function "resizeAndRepositionStep" that we'll go over in detail in our next post.


Comments

Popular Posts