render: function (animation, tracks) {
    this.unrender();

    this.animation = animation;
    let {state} = this.animation;

    // Create a container element to hold the delay and iterations.
    // It is positioned according to its delay (divided by the playbackrate),
    // and its width is according to its duration (divided by the playbackrate).
    const {x, delayX, delayW, endDelayX, endDelayW} =
      TimeScale.getAnimationDimensions(animation);

    // Animation summary graph element.
    const summaryEl = createSVGNode({
      parent: this.containerEl,
      nodeType: "svg",
      attributes: {
        "class": "summary",
        "preserveAspectRatio": "none",
        "style": `left: ${ x - (state.delay > 0 ? delayW : 0) }%`
      }
    });

    // Total displayed duration
    const totalDisplayedDuration = state.playbackRate * TimeScale.getDuration();

    // Calculate stroke height in viewBox to display stroke of path.
    const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;

    // Set viewBox
    summaryEl.setAttribute("viewBox",
                           `${ state.delay < 0 ? state.delay : 0 }
                            -${ 1 + strokeHeightForViewBox }
                            ${ totalDisplayedDuration }
                            ${ 1 + strokeHeightForViewBox * 2 }`);

    // Minimum segment duration is the duration of one pixel.
    const minSegmentDuration = totalDisplayedDuration / this.containerEl.clientWidth;
    // Minimum progress threshold for effect timing.
    const minEffectProgressThreshold = getPreferredProgressThreshold(state.easing);

    // Render summary graph.
    // The summary graph is constructed from keyframes's easing and effect timing.
    const graphHelper = new SummaryGraphHelper(this.win, state, minSegmentDuration);
    renderKeyframesEasingGraph(summaryEl, state, totalDisplayedDuration,
                               minEffectProgressThreshold, tracks, graphHelper);
    if (state.easing !== "linear") {
      renderEffectEasingGraph(summaryEl, state, totalDisplayedDuration,
                              minEffectProgressThreshold, graphHelper);
    }
    graphHelper.destroy();

    // The animation name is displayed over the animation.
    const nameEl = createNode({
      parent: this.containerEl,
      attributes: {
        "class": "name",
        "title": this.getTooltipText(state)
      }
    });

    createSVGNode({
      parent: createSVGNode({
        parent: nameEl,
        nodeType: "svg",
      }),
      nodeType: "text",
      attributes: {
        "y": "50%",
        "x": "100%",
      },
      textContent: state.name
    });

    // Delay.
    if (state.delay) {
      // Negative delays need to start at 0.
      createNode({
        parent: this.containerEl,
        attributes: {
          "class": "delay"
                   + (state.delay < 0 ? " negative" : " positive")
                   + (state.fill === "both" ||
                      state.fill === "backwards" ? " fill" : ""),
          "style": `left:${ delayX }%; width:${ delayW }%;`
        }
      });
    }

    // endDelay
    if (state.iterationCount && state.endDelay) {
      createNode({
        parent: this.containerEl,
        attributes: {
          "class": "end-delay"
                   + (state.endDelay < 0 ? " negative" : " positive")
                   + (state.fill === "both" ||
                      state.fill === "forwards" ? " fill" : ""),
          "style": `left:${ endDelayX }%; width:${ endDelayW }%;`
        }
      });
    }
  },
  render: function (animation, tracks) {
    this.unrender();

    this.animation = animation;

    // Animation summary graph element.
    const summaryEl = createSVGNode({
      parent: this.containerEl,
      nodeType: "svg",
      attributes: {
        "class": "summary",
        "preserveAspectRatio": "none"
      }
    });
    this.updateSummaryGraphViewBox(summaryEl);

    const {state} = this.animation;
    // Total displayed duration
    const totalDisplayedDuration = this.getTotalDisplayedDuration();
    // Minimum segment duration is the duration of one pixel.
    const minSegmentDuration = totalDisplayedDuration / this.containerEl.clientWidth;
    // Minimum progress threshold for effect timing.
    const minEffectProgressThreshold = getPreferredProgressThreshold(state.easing);

    // Render summary graph.
    // The summary graph is constructed from keyframes's easing and effect timing.
    const graphHelper = new SummaryGraphHelper(this.win, state, minSegmentDuration);
    renderKeyframesEasingGraph(summaryEl, state, totalDisplayedDuration,
                               minEffectProgressThreshold, tracks, graphHelper);
    if (state.easing !== "linear") {
      renderEffectEasingGraph(summaryEl, state, totalDisplayedDuration,
                              minEffectProgressThreshold, graphHelper);
    }
    graphHelper.destroy();

    // The animation name is displayed over the animation.
    const nameEl = createNode({
      parent: this.containerEl,
      attributes: {
        "class": "name",
        "title": this.getTooltipText(state)
      }
    });

    createSVGNode({
      parent: createSVGNode({
        parent: nameEl,
        nodeType: "svg",
      }),
      nodeType: "text",
      attributes: {
        "y": "50%",
        "x": "100%",
      },
      textContent: state.name
    });

    // Delay.
    if (state.delay) {
      // Negative delays need to start at 0.
      const delayEl = createNode({
        parent: this.containerEl,
        attributes: {
          "class": "delay"
                   + (state.delay < 0 ? " negative" : " positive")
                   + (state.fill === "both" ||
                      state.fill === "backwards" ? " fill" : "")
        }
      });
      this.updateDelayBounds(delayEl);
    }

    // endDelay
    if (state.iterationCount && state.endDelay) {
      const endDelayEl = createNode({
        parent: this.containerEl,
        attributes: {
          "class": "end-delay"
                   + (state.endDelay < 0 ? " negative" : " positive")
                   + (state.fill === "both" ||
                      state.fill === "forwards" ? " fill" : "")
        }
      });
      this.updateEndDelayBounds(endDelayEl);
    }
  },