Building an ActivityRenderer
Tags: webdev, javascript, beginners, programming
Published: 2024-07-10 12:36:57

The Gantt activity renderer is the main renderer of the ScheduleJS Viewer. This article will discuss how it is built and what are the specificities of this activity renderer.

## How to build a custom renderer class
The first step to building a renderer class is to inherit attributes and methods by extending a higher-order framework class.

We want to represent tasks only through their start and end time dimensions. The [ScheduleJS](https://schedulejs.com/en/) base renderer class for doing this is the **ActivityBarRenderer** class.

We need to provide the custom-type arguments to the **ActivityBarRenderer** class so the attributes and methods provided by our custom **Row** and **Activity** classes will be accessible using the base class API.

Let’s create the **ScheduleJsViewerTaskActivityRenderer** class to draw every **ScheduleJsViewerTaskActivity** in their respective **ScheduleJsViewerTaskRow**.

```js
// Import the base ActivityBarRenderer class from ScheduleJS
import {ActivityBarRenderer} from "schedule";

// Import our custom Activity and Row types
import {ScheduleJsViewerTaskActivity} from "...";
import {ScheduleJsViewerTaskRow} from "...";

// Create our custom renderer by extending the ActivityBarRenderer class
export class ScheduleJsViewerTaskActivityRenderer extends ActivityBarRenderer<ScheduleJsViewerTaskActivity, ScheduleJsViewerTaskRow> { }
```
As-is, the renderer can already be registered to draw our activities using the default behavior of the **ActivityBarRenderer**. Now let’s dive into how to customize it.

## The base architecture
In [ScheduleJS](https://schedulejs.com/en/), an **ActivityRenderer** is a class we register programmatically using the Graphics API to draw a specific **Activity** on its **Row**. To organize our **ScheduleJsViewerTaskActivityRenderer**, we will separate its code into three sections:

- The attributes will hold variables that let us change the behavior for a specific drawing procedure.  
- The constructor will let us define a default state for the renderer.
- The drawing methods will hold all the instructions for drawing our activities on the canvas.

## Attributes
Attributes are constants that will be reused throughout the renderer. As-is, these properties will only be edited directly in the renderer code. We can imagine a specific screen where the user could modify these settings directly in the UI.

```js
// Attributes

// Pixels sizings
private readonly _parentActivityTrianglesWidthPx: number = 5;
private readonly _parentActivityTrianglesHeightPx: number = 8;
private readonly _defaultLineWidthPx: number = 0.5;

// Colors palette
private readonly _parentActivityColor: string = Color.GRAY.toCssString();
private readonly _strokeColor: string = Color.BLACK.toCssString();
private readonly _defaultActivityGreen: Color = Color.rgb(28, 187, 158);
private readonly _defaultActivityBlue: Color = Color.rgb(53, 152, 214);
private readonly _onHoverFillColor: string = Color.ORANGE.toCssString();

// Opacity ratio for baseline activities
private readonly _baselineOpacityRatio: number = 0.6;
```

## Constructor
The constructor is tightly coupled to our renderer lifecycle method. In the ScheduleJS Viewer, we decided to instantiate the renderer whenever the user switches screens to define specificities and reuse our code in every tab that implements this renderer. It means the constructor function is run every time the user selects a screen featuring this renderer.

```js
// Constructor

// The renderer requires the graphics and the current tab variable
constructor(graphics: GraphicsBase<ScheduleJsViewerTaskRow>,
            private _currentRibbonMenuTab: ScheduleJsViewerRibbonMenuTabsEnum) {
            
  // The ActivityBarRenderer class requires the graphics and a name for the renderer
  super(graphics, ScheduleJsViewerRenderingConstants.taskActivityRendererName);
  
  // Default fill color when hovering an activity
  this.setFillHover(Color.web(this._onHoverFillColor));
  
  // Default stroke color when hovering an activity
  this.setStrokeHover(Color.BLACK);
  
  // Default stroke color
  this.setStroke(Color.BLACK);
  
  // Default thickness
  this.setLineWidth(this._defaultLineWidthPx);
  
  // Default bar height
  this.setBarHeight(8);
  
  // Default fill color based on current tab 
  switch (_currentRibbonMenuTab) {
    // Change color for the WBS tab
    case ScheduleJsViewerRibbonMenuTabsEnum.WBS:
      this._parentActivityColor = ScheduleJsViewerColors.brown;
      this.setFill(this._defaultActivityBlue);
      break;
    default:
      this._parentActivityColor = Color.GRAY.toCssString();
      this.setFill(this._defaultActivityGreen);
      break;
  }
  
}
```
The `setFill`, s`etStroke`, `setFillHover`, `setStrokeHover`, `setLineWidth`, and `setBarHeight` are inherited and used to alter the default rendering characteristics of the ActivityBarRenderer class.

The default features of this renderer are the following:

- A custom color when hovering activities
- A black line stroke (for activity borders)
- A stroke line thickness of 0.5 pixels
- An activity bar height of 8 pixels
- A conditional fill color:
Blue for children and brown for parents in the WBS tab
Green for children and gray for parents in the other tabs

## Drawing
The framework will automatically call the `drawActivity` method to render our activities on the canvas. All its parameters are dynamically filled, allowing you to react in real-time to the current state of your activities.

```js
// Main drawing method

drawActivity(activityRef: ActivityRef<ScheduleJsViewerTaskActivity>,
             position: ViewPosition,
             ctx: CanvasRenderingContext2D,
             x: number,
             y: number,
             w: number,
             h: number,
             selected: boolean,    
             hover: boolean,
             highlighted: boolean,
             pressed: boolean     
            ): ActivityBounds {    // This method has to return ActivityBounds
             
    // True if current activity includes a comparison task
    const hasModifications = !!activityRef.getActivity().diffTask;
    
    // True if current row has children
    const isParent = activityRef.getRow().getChildren().length;
    
    // Set colors dynamically
    this._setActivityColor(activityRef, hasModifications);
    
    // Draw text
    this._drawActivityText(activityRef, ctx, x, y, w, h, hasModifications);
    
    // Run a custom method to draw parent activities or delegate to the default method
    return isParent
      ? this._drawParentActivity(activityRef, ctx, x, y, w, h, hover, hasModifications)
      : super.drawActivity(activityRef, position, ctx, x, y, w, h, selected, hover, highlighted, pressed);
  }
```
The drawing will occur this way:

- Get information on the current **Activity** and **Row** using the **ActivityRef API**
- Set colors dynamically using our `_setActivityColor` method
- Draw activity text using our `_drawActivityText` method
- Draw the activity itself based using two methods:
The `_drawParentActivity` method to draw parents
The `super.drawActivity` default ActivityBarRenderer method to draw children

## Custom activity drawing methods
Let’s take a closer look at how to freely draw your activity by designing your own methods with the `_drawParentActivity` method.

```js
// Draw the parent activity

private _drawParentActivity(activityRef: ActivityRef<ScheduleJsViewerTaskActivity>,
                            ctx: CanvasRenderingContext2D,
                            x: number,
                            y: number,
                            w: number,
                            h: number,
                            hover: boolean,
                            hasModifications: boolean
                           ): ActivityBounds {

    // Set padding
    const topPadding = h / 3.5;
    const leftPadding = 1;
    
    // Set CanvasRenderingContext2D
    ctx.lineWidth = this._defaultLineWidthPx;
    if (hover) {
      ctx.fillStyle = this._onHoverFillColor;
      ctx.strokeStyle = ScheduleJsViewerColors.brown;
    } else if (hasModifications) {
      ctx.fillStyle = Color.web(this._parentActivityColor).withOpacity(this._baselineOpacityRatio).toCssString();
      ctx.strokeStyle = `rgba(0,0,0,${this._baselineOpacityRatio})`;
    } else {
      ctx.fillStyle = this._parentActivityColor;
      ctx.strokeStyle = this._strokeColor;
    }
    
    // Draw elements
    ScheduleJsViewerTaskActivityRenderer._drawParentActivityStartTriangle(ctx, x + leftPadding, y + topPadding, this._parentActivityTrianglesWidthPx, this._parentActivityTrianglesHeightPx);
    ScheduleJsViewerTaskActivityRenderer._drawParentActivityBody(ctx, x + leftPadding, y + topPadding, w, this._parentActivityTrianglesWidthPx, this._parentActivityTrianglesHeightPx);
    ScheduleJsViewerTaskActivityRenderer._drawParentActivityEndTriangle(ctx, x + leftPadding, y + topPadding, w, this._parentActivityTrianglesWidthPx, this._parentActivityTrianglesHeightPx);
    
    // Return positions to update where your activity should be responsive
    return new ActivityBounds(activityRef, x, y, w, h);
  }
```
Here we directly use the **HTMLCanvas API** to define our drawing strategy by setting up the **CanvasRenderingContex2D**. The only framework-related operation done in this method is creating some new **ActivityBounds** for the current parent **Activity**.

The framework creates a map using **ActivityBounds** under the hood to register all the activities on the screen. This map helps the developer by providing an element-like logic to build advanced user experiences based on accurate information while taking advantage of the performance of the **HTMLCanvas API**.

The draw elements methods like `_drawParentActivityStartTriangle` rely on the **CanvasRenderingContext2D API** to draw at the pixel level.

```js
// Draw the start triangle element of the parent activity

private static _drawParentActivityStartTriangle(ctx: CanvasRenderingContext2D,
                                                x: number,
                                                y: number,
                                                triangleWidth: number,
                                                triangleHeight: number): void {
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x , y + triangleHeight);
    ctx.lineTo(x + triangleWidth, y);
    ctx.lineTo(x, y);
    ctx.fill();
    ctx.stroke();
    ctx.closePath();
}
```

## Final result
To register your brand-new renderer, use the `graphics.setActivityRenderer` method:

```js
// Register the renderer

graphics.setActivityRenderer(ScheduleJsViewerTaskActivity, GanttLayout, new ScheduleJsViewerTaskActivityRenderer(graphics, currentRibbonMenuTab));

```

![brand-new renderer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/smq0wg41mdx6aa7x1tpi.png)

To see the video of the final result you can go to see: [Building an ActivityRenderer](https://schedulejs.com/en/activity-renderer-in-javascript/)