In this assignment, you will write code to build an interactive parallel coordinates visualization for the Iris Dataset used in the last assignment. Your main focus will be on designing interactions that support analysis with parallel coordinates.

Please click here to create your repository

Objectives

This assignment is designed to provide additional practice with more advanced uses of brushes and callback handlers in d3. Specific objectives include:

  • Implementing a parallel coordinates view suitable for viewing multi-dimensional datasets
  • Experimenting with using SVG path types for drawing polylines.
  • Practicing interactions with a parallel coordinates plot, particularly through implementing click events for selection and d3.brush’s for brushing and linking.
  • Designing callback handlers that support the above interactions of selection and filtering
  • Understanding the types of tasks that parallel coordinates are suitable for.

Description

In this assignment, you will create a visualization that uses a parallel coordinates display to investigate the Iris dataset. Recall that this dataset has four quantitative attributes (sepal length, sepal width, petal length, petal width) and 1 categorical attibute (species). There are 50 samples in each of 3 species (setosa, versicolor, and virginica), for a dataset with 150 total flowers.

Your parallel coordinates view should draw all four dimensions simultaneously, and allow the user to investigate how the data spans the four dimensions through interactions. Specifically, your visualization must support: (1) brushing on any axis to select and filter a subset of the data and (2) reordering the axes by clicking on them which should swap them with their neighbor on the right.

In this assignment, you should get comfortable with doing two types of selections and data joins: those that are mapped to the actual data array (called data in the template) and those that are mapped to an array of dimensions (called dims). The array of dimensions will be used heavily in this assignment, and we will be a one-to-one mapping between dims and many different visual elements.

You’ll note in the template code provided, I’ve included code that initializes, one per each dimension, a element of the yScale, axes, brushes, and brushRanges arrays. You are welcome to customize this initialization further if you like, but the basic version I’ve provide should be sufficient.

Part 1: Drawing the Data

The first of four selections that you need to complete involves drawing one SVG path element per data element. These should be draw as a polyline with four points, one point per the four quantitative dimensions of the data element. To do so, we will use d3.line(), which accepts an array of pairs [[x1,y1], [x2,y2], ...] and returns the associated SVG path attribute d. Your first task is to write the logic that makes use of the pre-defined scales to compute this parameter.

Notably, I have provided both xScale and yScales to do this. xScale, which is of type d3.scalePoint() will return for any element of dims the appropriate \(x\)-coordinate. For example xScale(dims[0]) will return the \(x\)-coordinate of the first (leftmost) dimension. yScales is a tuple of scales, one per dimension, that can be used to access the \(y\)-coordinate of specific data elements on a particular scale. For example, yScales[dims[0]] returns the scale associated with dim[0] and as a result if you call yScales[dims[0]](datum[dims[0]) it will return the actual \(y\)-coordinate of a particular datum for dimension dims[0].

I recommend looking at some examples of parallel coordinates for examples of how one uses d3.line(). For example, see Jason Davies’ Parallel Coordinate block and this example of Most basic parallel coordinates chart in d3.js. See this link for more information about how to use d3.line()

Besides using paths, you should set the stroke color to be define by the species of each data element, and the opacity to be 0.75. You should also be sure to set the class of each path to datapath for easy access in interactions.

Part 2: Drawing the Axes

To draw the axes, you will complete the other 3 selections. Specifically, you will need a selection that maps from dims to SVG groups (g) that will contain the axes. Each axis group should be given the class axis.

To draw the axes, you will need to both appropriately transform the group as well as make sure you call the appropriate axis function that we initialized. For the transforms, you should translate each axis by an \(x\)-value corresponding to its xScale.

To call the axes functions, we will use a d3 trick, using the function .each() so that we can call the appropriate axis function. Your .each() functions should look something like this:

.each(function(d) {
  return d3.select(this).call(axes[d]);
})

See this link for more information about the specifics of .each() vs. .call()

Here, d3.select(this) returns the current selection, and then we call the appropriate axis function axis[d] for that selection. .each() allows for anonymous functions so that we can select per datum d.

Besides calling the axes functions, you will also have to create text labels above each axis using the class label. This should be created similarly, but you’ll transform them to be centered above each axis. The array dimNames stores a string for each dimension’s label text. You’ll also want to make sure each text label is associated with the onClick() function.

Finally, for each axis we will create a brush group, given the class brush. Like with axes, you’ll use .each() to call the appropriate brush function that we created in the initialization. And, like with axes, you’ll have to transform this to the appropriate location to align with the axis.

For all three of this selections, you may want to do you data join using keys so that you can associate each axis/label/brush with the name of the dimension rather than the dimension’s index in dims.

Part 3: Interaction

You will implement two interactions. First, when a text label is clicked, you should swap it with the text label that is immediately to its right. If the rightmost label is clicked, it should swap with the one immediately to its left.

To do so, you should swap the location of the dimension in the dims array. After doing this, you can:

  1. Rebuild xScale, specifically you will change its domain
  2. Rebind the data for axes/labels/brushes, and redefine their transforms using the updated xScale
  3. Rebind the data for the data paths, and update the positions of all polylines, again using the updated xScale

All of the above should be transitioned using a duration of 1 second for each click.

For our second interaction, the user should be able to brush over a particular axis and this will select a subset of the data. If the user brushes over multiple axes, the selection must satisfy all selected ranges.

We will implement this similar to A07, by defining a function isSelected() which should return true for any data element that is within the range. The brushes we created are 1-dimensional, using d3.brushY(), which means that their selected ranges are a bit simpler. Specifically, [0] is the minimum of the range and [1] is the maximum. So, for a given dimension dim, brushRanges[dim][0] is the minimum \(y\) (in pixels) and brushranges[dim][1] is the maximum \(y\) (in pixels).

Your isSelected() function should check all dimensions, and for an input data element d, return true if d is within the range of each brush. If an axes’s brush is turned off, brushRanges for that dimension will be null.

All selected elements should be set to have an opacity of 0.75, whereas elements that are not selected should have an opacity of 0.1. If no brushes are enabled, then all elements should have opacity of 0.75.

Submission

You should use git to submit all source code files. The expectation is that your code will be graded by cloning your repo and then executing it within a modern browser (Chrome, Firefox, etc.)

Please provide a README.md file that provides a text description of how to run your program and any parameters that you used. Also document any idiosyncrasies, behaviors, or bugs of note that you want us to be aware of.

To summarize, my expectation is that your repo will contain:

  1. A README.md file
  2. A index.html file
  3. An a08.js file
  4. All other Javascript files necessary to run the code (including iris.js and d3.v5.js plus any others you require)
  5. Any .css files containing style information

Grading

Deductions

Reason Value
Bugs or syntax errors -10 each bug at grader's discretion to fix


Point Breakdown of Features

Requirement Value
Consistent modular coding style 5
External documentation (README.md) following the template provided in the base repository 5
Header documentation, Internal documentation (Block for functions and Inline descriptive comments). Wherever applicable / for all files 10
Expected output / behavior for your chart based on the assignment specification, including

Implementing the data drawing discussed in Part 120
Properly adding groups for the axes, labels, and brushes in Part 220
Implementing the swap interaction. For full credit, you swap should also swap any enabled brushes and include transitions20
Correctly selecting data and updating its opacity using brushes20

80
Total 100/100


Cumulative Relationship to Final Grade

Worth 5% of your final grade

Extra Credit

Implementing features above and beyond the specification may result in additional extra credit, please document these in your README.md.

Notably, you may want to consider interactions that include:

  • Inverting axes (e.g. switching their minimum and maximum)
  • Using a drag-and-drop interface to reorder the axes instead of clicking to swap
  • Augmenting the visual interface to show other statistical information per axis
  • Generally improving upon the visual look-and-feel of parallel coordinates. If you find other inspirational examples from the web, be sure to appropriate reference them in your README.