Program Statement: Designing mesmerising, interactive timeline for the user’s weight loss journey.

For the past few months we are working on a health app which helps users in their weight loss journey. Here we call it more of a Wellness Journey. This app keeps track of your weight, your inches, water intake and of course your calories. We are creating a platform which acts as a personal assistant in their journey. Lately our users have been asking us for a feature in the app where they can view their entire journey i.e what was their weight when they started, and how things progressed along the weeks, their major achievements(things like the 1st pound lost, after a month and so on….). So we decided to give it a go. Read along to see what we came up with.

Solution: After brainstorming with UX designer, UI designer, and fellow developers, and discussing multiple approaches from a simple listing to complex timelines we finally settled on the one which had maximum opportunity for micro-interactions, animation, and user engagement.

The idea was to show the weight loss journey to a user, quite symbolic to the  journey on the road which can have milestones in between based on the achievements the user has attained. A path which will be laid out in front of the user and then will be filled accordingly to the point where the user has reached at a particular point in the duration of the program.

Below is the final implementation of the feature in our app.

 

I am only going to cover the animating path in this blog and other animation and interaction in the next one.

Step 1

Drawing leader line

Leader Line is basically the center line (dotted line in above video) which is equidistant from the inner and outer contour line.

This is the simplest part of this animation. We have to draw a series of connected horizontal lines and semicircles.

The above illustration explains how we draw in SVG. You can read more about SVG paths here. The negative sign in horizontal lines means we want our lines drawn from right to left. Arcs in SVG require extra attention.

Arcs accept parameters as `rx`, `ry`, `x-axis rotation`, `large arc flag`, `sweep flag`, `finalX`, `finalY`. As we are basically drawing a semicircle, we don’t need `x-axis rotation flag` and `large arc flag`. `Sweep flag` determines which side we want to draw. 1 means the positive side and 0 means the negative side. To know more about SVG arc check this link.

export function getPath(month, line) {
    let fullPath = 'M90 100';
    const forwardLine = `l${line.width} 0`;
    const backwardLine = `l${-line.width} 0`;
    const leftCurve = `a${line.radius} ${line.radius} 0 0 0 0 ${line.radius * 2}`;
    const rightCurve = `a${line.radius} ${line.radius} 0 0 1 0 ${
    line.radius * 2
    }`;
    
    for (let i = 0; i < month; ++i) {
        if (i % 2 == 0) {
            fullPath += `${forwardLine} ${rightCurve}`;
        } else {
            fullPath += `${backwardLine} ${leftCurve}`;
        }
    }

    return fullPath;
}
.
. App.js
.
this.granularity = 5;this.leaderLineProperty = {
     lineWidth: width - 180,
      radius: 50,
};
this.path = PathHelper.getPath(this.month, PathHelper.LeaderPathProperty);

From the above line of code, we have our leader line ready.

Step 2

For this step, I want you to stop and have a look at the animation video above. Our ultimate goal is to animate the path. For smooth animation we have to break down into smaller segments and then attach them piece by piece.

To divide the leader line into smaller chunks we will use svg-path-properties library.

// path is our leader line svg, totalDays is the no of segments we have divided our lines into radius is distance between leader line and outer, inner line
getPathProperty(path, totalDays, radius) {
      const pathProperty = new svgPathProperties(path);
      const pathSegmentArray = [];

    let {
        x:
        previousX, y:previousY
    } = pathProperty.getPropertiesAtLength(0);

    for (let i = 1; i <= totalDays; ++i) {
         const leaderSegment = (i / totalDays) *  
                                pathProperty.getTotalLength();

      const{ x:lx, y:ly
           } =pathProperty.getPropertiesAtLength(leaderSegment);

      const diffX = lx - previousX;
      const diffY = ly - previousY;
      previousX = lx;
      previousY = ly;

      const angleForOuterContourLine = Math.atan2(diffY, diffX);
      const angleForInnerContourLine = Math.PI - 
                                       angleForOuterContourLine;

      const ox = lx + radius * Math.sin(angleForOuterContourLine);
      const oy = ly - radius * Math.cos(angleForOuterContourLine);
      const ix = lx - radius * Math.sin(angleForInnerContourLine);
      const iy = ly - radius * Math.cos(angleForInnerContourLine);

      const point = {
           outer: {x:  ox, y:oy },
           leader:{x:  lx, y:ly },
           inner: {x:  ix, y:iy },
        };        pathSegmentArray.push(point);
    }

    return pathSegmentArray;
}

Suppose the height of the road is 20pt. Then to make a close path from a single line we need an outer and inner line having 10 points distance from the leader line each. In the above lines of codes, we get points for the leader line directly. Now we have to calculate the same for the inner and outer line.

Another point of significance that I would like to mention here is that we just can’t subtract or add 10 to the leader lines to get our outer and the inner path without considering direction.

Just like vectors, we will also use direction here and for this we will use our old friend ‘Trigonometry’. We will get two points on the leader line and find the angle between the line formed by the last two points and the x-axis.

If you have two points, (x0, y0) and (x1, y1), then the angle of the line joining them (relative to the X axis) is given by:

theta = atan2((y1 – y0), (x1 – x0)) 

We know tanθ = Perpendicular / Base = (Difference in height)/(Difference in width). We can calculate θ by using formula for tan inverse. After calculating angle we calculate x and y position as:

Δx = radius * sin(θ)

Δy = radius * cos(θ)

Step 3

Now since we have coordinates for the inner and outer path, we need to translate them into a closed SVG path to form our road. Here D3 library comes to our rescue. We use d3-shapes’ “area” method to get our path.

// progress is a no. between 0 to totalDays and `pathSegment` is an object containing our inner and outer path segmentsexport const calculateProgressArea = (progress, pathSegment) => {
        const forwardArray = [];
        const backwardArray = [];

        let point = pathSegment[0];
        forwardArray.push(point.outer);
        backwardArray.push(point.inner);

        for (let i = 1; i < progress; ++i) {
         point = pathSegment[i];
         forwardArray.push(point.outer);
         backwardArray.push(point.inner);
        }

        backwardArray.reverse();
        const allPoint = [...forwardArray, ...backwardArray, forwardArray[0]];

        const area = d3
            .area()
            .x1((x) => {
            return x.x;
            })
            .y1((y) => {
            return y.y;
            })
            .y0(allPoint[0].y)
            .x0(allPoint[0].x);

        return area(allPoint);
};.
. In App.js
.this.area = PathHelper.calculateProgressArea(
                    this.totalDays,
                    this.pathSegmentArray,
                );.
. In render function
.<Svg style={{width: '100%', height: this.month * 100 + 120}}>       
      <Path
          d={this.area}
          stroke="none"
          strokeWidth={1}
          fill="#afafaf30"
          fillRule="evenodd"
        />
</Svg>

Please note that I am appending the Inner path point at the end of the Outer path point.

Step 4

Till now we have only laid down the solid block of the path. Now we have to animate the path as well. As I said earlier, for animating the path we will render pieces after pieces. By pieces, I meant we will take a small subset from the outer and inner path segment array and convert it into a path and render it.

 

addAnimationListener = () => {
    this.state.animation.addListener(({value}) => {
          const progress = value * this.totalDays;
          const path = PathHelper.calculateProgressArea(
          progress,
          this.pathSegmentArray,
          );

          if (this.progressPath) {
              this.progressPath.setNativeProps({
                d: path,
              });
          }
    });
};

 

You might also like