Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member

Recently our company came up with a use case for displaying a scatter plot of three data values, but only one of the values was numeric.  The standard scatter plot requires X and Y values to be measures.  To put a dimension along the Y axis would be some custom code - a great use case for a Lumira Extension.

The requirements document shows that our user wants the location displayed vertically on the left (the "Y Axis") against miles on the X axis.  Each location can have two or more points.  The dots should be labeled and color-coded by category. Horizontal grid lines make it clear which dots belong to each location.

To get started, my first thought was to just grab the pre-made scatter chart from the Lumira examples and extend it ... but unfortunately we don't get the basic charts to start with.  Instead we'll start with Mike Bostock's D3 scatter chart and convert it to an extension. We save Mike's data set as CSV and load it into Vizpacker:

Once we have the data, we use the code from the same URL in the body of the render() function.  We will need one dimension and two measures, so we use the standard Lumira names in our code:

var dsets = data.meta.dimensions(),

    msets = data.meta.measures();

      

var mset1 = data.meta.measures(0);

var ms1 = mset1[0];

var ms2 = mset1[1];

We can omit the load function d3.tsv. Where the original has hard-coded the column names like "d.sepalLength" and "d.sepalWidth", we replace it with d[ms1] and d[ms2]. A few more tweaks and we have a working Lumira scatter plot that looks just like the original on the left:

The .profile file for this extension is attached to this blog post and will be submitted to the Lumira Visualization project on GitHub. That will give a head start to the next person who extends a scatter plot.

From this starting point, now we want to work through to the requirements for our custom chart.  This time we will have three dimensions and one measure. Rather than number the dimensions, they get names for maintainability.  We won't be using ms2 but it's helpful to leave it for the moment, so we can see our changes without code errors.

// a single measure for the horizontal axis

var mset1 = data.meta.measures(0);

var ms1 = mset1[0]; 

var ms2 = mset1[0]; // this is just here while we work, so we don't get javascript errors

// three measures for the y-axis, dot labels, and color/legend

var dset1 = data.meta.dimensions(0);

var ds_yAxis = dset1[0];

var ds_label = dset1[1];

var ds_color = dset1[2];

Our Y-Axis is going to be text - a list of locations (cities).  Since the data has multiple values for each of those locations, we will need a de-deuplicated list for the axis dataset.  We'll create this as array yAxisValues.

yAxisValues = [];

data.forEach(function (d) {

  dimensionValue = d[ds_yAxis];

  if (!dimensionValue) {dimensionValue = ""};

  if (yAxisValues.indexOf(dimensionValue)==-1) {

    yAxisValues.push(dimensionValue);

  }

});

yAxisValues.sort();

Now we replace the chart's y-axis with our new one, containing text values.  The number of ticks is the size of the array, and the tick format is the array values  This gives us the location names down the left side.  Our data points are all in the x=y position for now, due to the ms2 hack.

        var yAxisLeft = d3.svg.axis()

            .scale(y)

            .tickSize(0)

            .ticks(yAxisValues.length)

            .tickFormat(function(d,i) { return yAxisValues[i]; })

            .orient("left");

To get the lines all the way across, we make the tick marks extend the full chart width.  I couldn't find a way to put the tick marks on the inside with the labels on the outside.  I tried it double Y axis and got this appearance, which turned out to be very readable, and well-suited for our use case - lucky for me!

The code to add the secondary axis is below.  We set the tick size to the full width of the chart.

var yAxisRight = d3.svg.axis()

            .scale(y)

            .tickSize(plotWidth,0)

            .ticks(yAxisValues.length)

            .tickFormat(function(d,i) { return yAxisValues[i]; })

            .orient("right");

vis.append("g")

            .attr("class", "y axis")

            .call(yAxisRight)

and we need a dark-gray color for the line in the CSS:

.axis line {

  shape-rendering: crispEdges;

  stroke: #AAA;

}


So we have completed adding the location data on the y-axis, and created the horizontal grid.  Now we can put the data on it.  In the original scatter chart, the data is positioned by the cx and cy attributes:

.attr("cx", function(d) { return x(d[ms1]); })

.attr("cy", function(d) { return y(d[ms2]); })

We want to use the location name instead as the y-axis value.  So we get the location name (city) for the dot being plotted - d[ds_yAxis] - and then find its position in the array using yAxisValues.indexOf().  We pass this value to the y function, and the dot will appear at the y-coordinate of its location name.

.attr("cx", function(d) { return x(d[ms1]); })

.attr("cy", function(d) { return y(yAxisValues.indexOf(d[ds_yAxis])); })

But first we have to remember to set the y-axis input domain.  Originally it relied on the input data set and its measure:

y.domain(d3.extent(data, function(d) { return d[ms2]; })).nice();

now we switch it to use our de-duplicated array of Y-axis values.

y.domain(d3.extent(yAxisValues, function(d,i) { return i; })).nice();

That completes the next requirement in the chart - we now have our data plotted with a text Y axis and a numeric X axis.

To color the dots by the color category, we change it from the dimension sample data to the named dimension we set earlier:

.style("fill", function(d) { return color(d[ds1]); });

to

.style("fill", function(d) { return color(d[ds_color]); })

and D3 does the rest for us.  It even changes the legend.

Now we have to label the dots.  This seemed like it would be easy - something like .attr("text") = d[ds_label].  But unfortunately not.  The "title"."text" attribute on a dot works fine as a "tool-tip" but not as a label.  We'll add the tool-tip anyway, because it seems likely that the users would need this to identify dots closely spaced together.  (I know - it's scope creep).

      .append("title")

            .text(function(d) {

              var xAxisLabel=ms1+": "+d[ms1];

              var yAxisLabel=ds_yAxis+": "+d[ds_yAxis];

              var colorLabel=ds_color+": "+d[ds_color];

              var dotLabel=ds_label+": "+d[ds_label];

              return yAxisLabel+"\n"+dotLabel+"\n"+xAxisLabel+"\n"+colorLabel; })

To get labels to appear near the dots, we have to put SVG Text objects near them.  That means the dots and their labels will have to be grouped - our old friend the SVG "g" container.  Since we'll be using the dot set group repeatedly in the code, we'll define them with a variable.

       var dots = vis.selectAll(".dot")

            .data(data)

          .enter().append("g");

Then use the new variable to replace the one where we previously attached the dots ("circles").  That is,

vis.selectAll(".dot")

      .data(data)

    .enter().append("circle")

...

becomes

dots.append("circle")

...

Now we can add text to the group "g" which includes the dot:

dots.append("text")

          .attr("x", function(d) { return x(d[ms1]); })

          .attr("y", function(d) { return y(yAxisValues.indexOf(d[ds_yAxis]))-10; })

          .text(function(d) { return d[ds_label]; })

          .style("text-anchor", "middle")

          .style("font-size", 12);

And the dots are now labeled.

Obviously we don't want those magic numbers for the offset and the font size, so we'll set up constants for these label attributes in the final code.

Except for cleanup/cosmetic, our requirements are complete.  The working code is attached and of course will be submitted to the Lumira Visualization project on GitHub.

4 Comments
Labels in this area