TL;DR: live demo of the Poll Rank component on Code Pen: https://codepen.io/lucasjellema/pen/NWpoBeM
Bringing things to order is a common requirement. Making clear how things are mutually ordered, which comes first and which comes next, is often relevant. Order by value, by time or by preference are just some of the options. General classification in the Tour de France or the current league standings, the result of an election, the rank of the stars or the sequence of historical events, the best performing stocks, the top 10 longest rivers are just some examples of ordered data collections.
Whenever the end users of our applications play a role themselves in determining the order of some options, they should be supported in doing so. In an intuitive way. For me, that is by moving around the elements that need to be ordered, Pick up an object and put it in its proper place. In UI terms: drag & drop. And to get strong visual feedback on the ordering actions I take.
In recent months I have done some work with SVG (Scalable Vector Graphics) and the d3.js library. It seemed only natural to apply my experiences to my new found requirement of creating a way to allow users to specify the order of things in an engaging, intuitive way. This article reports on my first steps towards creating a ranking component.
My objectives are:
- create a UI component – that can be used in regular web application – that allows a user to rank objects (with regard to each other) and score choices
- the value range can be discrete (specific levels) and continuous
- discrete levels can be unique (only one object can be at a given level) or allow multiple options with equal rank / level
- the component can be used to poll a user for their opinion or quiz them for their knowledge
- the component can be used in read only mode to just visualize the ranking of a set of objects
- I want to support 3-15 options to be ranked and 2-12 discrete levels
The input to the component consists of:
- the data objects to be ranked (with optional initial value)
- discrete or continuous
- continuous range: minimum value – maximum value
- discrete levels: value, label, shape color, area background color for each level
The output is the ranked set of data objects with values based on the position assigned by the user to each shape.
In this example, the data objects are cities in the Netherlands. They are to be ranked by population size. The axis with values is shown and the position of the city shape does not just represent the rank (in comparison with the other cities) but also the absolute size of the population.
The component can be configured to plot the shapes either on one track or on multiple parallel tracks. The shapes can have horizontal lines to make it easier to define value and rank. In this case the color (yellow) and shape (circle) and size are fixed; these can easily be changed or made dynamic.
Demonstration of poll rank component with continuous range enabled:
The data set fueling the component is shown in the table. This table is updated as the shapes are dragged.
The same component with some other settings is used for discrete level setting. Each level is indicated on the axis and marked with the colored area. The shapes are assigned a color based on the level they are set at. When the shape is dropped (after a drag) it is repositioned at the position for the level value (the middle of the colored area). In this case, multiple shapes at the same level are allowed.
A demonstration of the component for a discrete set of values is shown next. Watch how the shapes are repositioned after being released and see the color change when a new discrete value area is entered.
Further enhancements
Some features I would like to add:
- horizontal display and movement
- explicit support for continuous timescale
- custom shapes or even images for the elements
- present the correct answer vs the solution created by the user (for example for ordering historic events on a time axis)
- read the data set and the discrete level definitions from a Google Sheet
- post the ordering created by the user to a Google Sheet (and run a true poll rank survey)
- use size of elements as an additional value dimension; likewise with color
Implementation
The component is implemented using SVG and JavaScript through the d3.js library that makes it so easy to map a data set to a visual representation (and vice versa). Another boon of d3 that plays a major role in this component is of course the ability to drag & drop components and update visual properties while the dragging takes place. It is important to realize that rendering of this visualization is so fast, we do not have to worry about updating individual visual aspects; in many cases we can just redraw the entire SVG content and to the user it looks like only a minute detail was selectively updated. Knowing this makes development a lot more relaxed. Also note that SVG does not have the concept of layers. What is drawn last comes at the top. LIOT – Last In On Top. And finally: the y coordinate increases as you go down; y ==0 is at the top, y increases in the vertical direction. That is counterintuitive at times when high values are high up on the page (with low y-values).
The structure of the code is not too difficult to follow:
- prepare data set (to be visualized and ordered) and level definitions (in case of discrete values)
- create HTML components (checkboxes) to configure the ordering component
and add event handlers for the checkboxes
- create an SVG component in HTML body (and clear it of all contents if it already exists)
- draw colored background areas for discrete values (if so desired)
- draw axis
- create SVG group elements for all data objects – and make the groups drag & drop enabled
- move the groups vertically to a value based position; move the group horizontally based on their index (unless the one track option is enabled that positions all groups in one straight vertical line)
- define drag and drag end handlers to update the vertical position of the group based on the vertical drag position, update the value of the underlying data object based on the vertical position of the group, update the color of the child shape in the group based on the value (if applyShapeColors is configured and the value range is discrete)
- show horizontal dotted lines for the groups – if the component is so configured
- create circle objects in the group – using the default color or the color derived from the value; the size of the circle is currently fixed – but could easily be made configurable
- render the text label for the group – underneath the shape or to the side (in case one track is enabled)
The part of the code that took me longest to put together is probably the handler for the drag operations. This handler is added through the line .call(dragHandler) for the groups created for the data objects. This code is somewhat different in d3 v6 than it was in earlier versions.
The function dragHandler looks like this:
This function leverages d3.drag() to add three listeners to the element the function is invoked for. The start handler visually highlights the element being dropped (in a fairly understated way with a black outer perimeter). The drag event handler is invoked for every update of the drag position. It calculates the new y position for the group (making sure that even if the user tries to drag outside the designated area (y< 0 or y > height), the y is still positioned inside it. Then it calculates the value for the data object associated with the dragged element – from its y value. This value can be on the continuous data range or from the set of discrete values. The vertical position for the group is updated from the derived value of y. Finally, the fill color for the child element within the dragged group with style class set to shape is updated. This causes the shape to change color when crossing the border between two discrete values.
The final event handler is for the end (drag) event – fired when the user releases the group. This handler resets the color of the outer group perimeter . It derives the y coordinate from the final value assigned to the data object; this is relevant in case of discrete values; the y position is set to the vertical center of the discrete value area rather than the arbitrary position where the user ended the drag. Finally, the group is moved along the vertical axis to its final y destination.
Resources
Code for this component: https://github.com/lucasjellema/code-cafe-intro-to-svg/blob/main/poll-rank-component.html
Ranked voting – https://en.wikipedia.org/wiki/Ranked_voting
Likert Scale – https://en.wikipedia.org/wiki/Likert_scale
d3 v6 Drag & Drop API – https://observablehq.com/@d3/d3v6-migration-guide#event_drag ; d3 v6 migration guide: https://observablehq.com/@d3/d3v6-migration-guide