Part 4: Controlling D3 Trees via SPFx Web Part Properties

In this series of blog posts, I am seeking to build a new UI to access SharePoint using the D3 data visualisation library and SharePoint client-side web parts, built using the SPFx. So far, we have:

  • Part 1: Getting started with SPFx and D3
  • Part 2: SPFx and D3 Trees
  • Part 3: Making D3 Trees SharePointy in SPFx Web Parts

Parts 1 and 3 are fairly easy going but Part 2 is a bit of a beast as we needed to figure out how to use TypeScript to construct a D3 Tree that could be rendered in client web part.

If you haven’t been through these previous blog posts, I suggest that you don’t start here, as everything in this series builds on what has gone before.

In Part 2, I did a little bit of coding that would provide some flexibility for the web part. Essentially, I defined 3 line-styles that could be used to connect nodes in the tree in 3 different ways, as follows:

  • Arc: A nicely curved diagonal
  • Line: A string line connection between nodes
  • Elbow: A vertical/horizontal stepped line that connects nodes

Up until now we have been using the default Arc method which provides us something like the following:

In this post, I want to explain how to create a web part property that can then be used to allow the user to select a different line style.

The LinkType Enumeration

I’m a great fan of code abstraction and separation and so created a new TypeScript file in my web part project called enums.ts. In this file, I defined the enumeration type that we will use to select the line type that will be used to connect nodes in the tree, as shown below:

export enum LinkType{
  Arc,
  Line,
  Elbow
}

The Web Part Link Property

Then I added a new link property (of type LinkType) to the interface of my web part class:

export interface ID3TestWebPartProps { 
  layout: LayoutType;  //ToDo
  link: LinkType;
}

And I also added an entry for this property in the preconfiguredEntries for the web part on the manifest.json file for the web part as shown below:

"preconfiguredEntries": [{

  "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other

  "group": { "default": "Other" },

  "title": { "default": "D3Test" },

  "description": { "default": "This is just to test if D3 can work with SPFx" },

  "officeFabricIconFontName": "Page",

  "properties": {

    "layout" : 0,

    "link" : 0

  }

}]

Note that we can’t define the type of the link property here but we can set a number that will be cast to the index of the enumeration type. By setting the value to 0 we are specifying that the default value for the property will be Arc.

Adding a Field Control

Next, we need to add a field control to the enable the user to set a line type. This is done by adding a PropertyPaneDropdown control to the configuration object returned by the getPropertPaneConfiguration method, as shown below:

protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {

  return {

    pages: [

      {

        header: { description: "" },

        groups: [

          {             

            groupName: "SETTINGS",

            groupFields: [               

              PropertyPaneDropdown('link', {

                label: "Link Type",

                options: [

                  {key: 0, text: 'Arc'},

                  {key: 1, text: 'Line'},

                  {key: 2, text: 'Elbow'},                  

                ], selectedKey: 0

              })                                             

            ]

          }

        ]

      }

    ]

  };

}

If you are not comfortable with how web part properties work them please feel free to check out my blog post covering the Adventures with the SharePoint Framework (SPFx): Part 6 – Web Part Properties.

In the above code it can be seen that the dropdown field control is associated with the link property and provides the 3 options that we are making available for selection by users.

The ChartConfig property

I have created a custom class called ChartConfig and an instance of that class is defined as a property of the web part as shown below:

private _chartConfig: ChartConfig = null;

public get chartConfig(): ChartConfig {

  if (this._chartConfig == null){

    this._chartConfig = new ChartConfig();
    this._chartConfig.svg = this.svg;
    this._chartConfig.treeLayout = d3.tree().size([this.svgHeight, 0]);         
    this._chartConfig.layerSpace = Constants.LAYER_SPACE;
    this._chartConfig.root = this.rootNode;
    this._chartConfig.link = this.NodeConnectionType;
    this._chartConfig.layout = this.Layout;

  }

  return this._chartConfig;

}

This chartConfig object is passed to the methods we use to render the tree. These methods are defined in a custom utilities class I created called D3Utilities (see part 2 for more details).

Notice how the link property of the chartConfig object is set by the NodeConnectType property of the web part, which just simply returns the value of the link property as set by the user in the web part property pane, as shown below:

public get NodeConnectionType(): LinkType {

  return this.properties.link;

}

The linkPath method

The linkPath method in the D3Utilities class uses the link property of the config object passed in as a parameter to determine which of the 3 link drawing methods should be called.

public static linkPath(sourceNode, destinationNode, config: ChartConfig) : string {

    switch (+config.link)
    {
        case LinkType.Arc: return this.arcLink(sourceNode, destinationNode);
        case LinkType.Line: return this.directLineLink(sourceNode, destinationNode);
        case LinkType.Elbow: return this.elbowLink(sourceNode, destinationNode, config);
        default: "";
    }
   
}

I went through the code for drawing the links in the 2nd post in this series, so I won’t go through that again here, but all these methods do is return a formatted text string that can be used to create an SVG Path element to draw the link between nodes in the tree.

Shifting Node Text

When we only provided the option to draw the links as an arc then aligning the node text was fairly easy. Basically, I just translated the y axis to shift the node text down a bit and then, depending whether it was a leaf node or a node with child elements, determined the text anchor point and where it was translated in the x axis so that the text shot out from the right or left of the node respectively.

When rendering the nodes with a straight-line connector, this same sample simple algorithm works just fine but with an elbow lien style things are not so elegant and to avoid having the connection lines runs through node text, we need to position things a bit differently. Of course, you can adjust things in any way you deem fit but what I decided to do when rendering an elbow connection was:

  • For a leaf node position the text as it was before
  • For a node with children use a text anchor point that was the middle of the node and shift it up a bit in the y-axis so that the text appears above the node graphic.

In order to achieve this feat of wizardry, I swapped out the simple checks in the function calls of the append text element of the enterNode method with method calls to methods defined in the D3Utilities class, as shown below:

public static enterNode(node: any, sourceNode: any, config: ChartConfig): any{

    //Append a group node
    let nodeEnter = node.enter().append('g')
                    .attr("class", `${styles.node}`)
                    .attr("transform", function(d){
                      return `translate(${sourceNode.y0}, ${sourceNode.x0`;                                             
                     });

    //Append the text
    nodeEnter.append('text')                      
        .attr("y", function(d) {
                        return D3Utilities.getTextY(d, config);
                        //return D3Utilities.hasChildren(d) ? -12: 12;
                    })           
        .attr("x", function(d) {
                        return D3Utilities.getTextX(d, config);
                        //return D3Utilities.hasChildren(d) ? -12: 12;
                    })
        .attr("text-anchor", function(d) {
                              return D3Utilities.getTextAnchor(d, config);
                            })                                 
        .text(function(d) {
            return d.data.name;
        });
...

These 3 methods, get passed in the config object, so that the link type can be checked and used to position the text accordingly, as shown below:

private static getTextX(d: any, config : ChartConfig): number{       

    if (config.link == LinkType.Elbow){

        return D3Utilities.hasChildren(d) ? 0 : 12;

    }

    else {

        return D3Utilities.hasChildren(d) ? -12 : 12; 

    }

}

private static getTextY(d: any, config: ChartConfig){       

    if (config.link == LinkType.Elbow){

        return D3Utilities.hasChildren(d) ? -10 : 5;

    }

    else{

        return 5;

    }       

}

private static getTextAnchor(d: any, config: ChartConfig): string {       

    if (config.link == LinkType.Elbow){

        return D3Utilities.hasChildren(d) ? "middle" : "start";

    }

    else {

        return D3Utilities.hasChildren(d) ? "end" : "start"; 

    }

}

The Reveal

With these changes made to the test web part class and the D3Utilities support class we can just gulp serve the project to see the results. Remember that in part 3 of this series we suck up icon images from SharePoint and so in the screenshot below I am using a remote workbench. If you use a local workbench the icons won’t show.

Here is the web part with a straight-line connection.

And here it is with the Elbow connector.

Other Web Part Properties

The eagle-eyed amongst you will have spotted that I defined another enumeration type in the enums.ts file, the LayoutType as follows:

export enum LayoutType {
  LeftToRight,
  RightToLeft,
  TopDown,
  BottomUp
}

My thoughts here are to declare another property that will change the orientation of the tree as Left-to-Right may not be the best option for all occasions. If I were to use this technique to generate an organisation chart for example, then a Top-to-Bottom orientation might be better and there may be other instances in which a Bottom-to-Top or Right-to-Left layout might be preferred (though I’ll admit to being hard pushed to think of common scenarios).

A Center-Out layout might also be useful, say if we want the root node to be centred in the SVG and child nodes to be arranged around it. Not so much a radial tree (and that’s yet another option) but more like a tree with first level nodes spreading out limbs left and right and where 3rd level (and below) nodes fan out in the same direction as their parent, level 1, node.  The sort of layout that is common in mind-mapping tools.

All of this should be possible but I just a haven’t yet devoted the time to making it happen.

There are other chart render attributes that we could set via web part properties as well. The following readily spring to mind:

  • Link line attributes such as stroke-width and colour
  • Text positioning in both the x and y axis, could use slider controls for this
  • Layer spacing, to control the width that should separate layers in the tree
  • Node spacing, to control the width between nodes
  • Elbow reticulation point, be default an elbow link kicks through 90 degrees at a distance half way between the nodes but that could be configurable
  • Chart height, currently this is fixed but we could make this a configurable setting in the web part as well

And these are just some of the possibilities to do with rendering the tree. Once we get access to real data, I feel there will be a raft of web properties we will want, to control what data is displayed, how and where and what happens when users interact with the tree, by clicking on a leaf node for example.

In Conclusion

The chart configuration settings of a D3 tree are usually static and set by the designer of the visualisation. However, if you render the visualisation inside a client web part, that does not need to be the case. In this article I demonstrated how you could create a web part property to control aspects of the visualisation.

Specifically, I demonstrated how to create a web part property that would allow users to select between one of 3 type of line connections that link nodes in the tree. But that was just an example and there are many potential ways in which we could externalise tree rendering features as options that can be set be users through web part properties.

I guess there is a balance to be struck here between providing enough configurability without adding too much complexity to the web part. The only advice I can offer here is to ask yourself, “do I see a scenario in which I might want to change this aspect of the tree visualisation?” If the answer to that is “yes” then we have a potential configuration setting that might be externalised as a web part property.

What’s next?

I think we’ve gone about as far as we need to with fake data, don’t you? In the next article I’ll figure out how to suck up some real data from SharePoint and feed it into our tree.

2 comments

  1. This is such a great and thorough tutorial. I’m fairly new to JS, and this is my first time doing TS. I did a course on SharePoint development, and it’s great to see someone share their information about how it’s done. I’ve learned a lot!

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: