This post is the third in the series describing the web-interface I created to administer a questionnaire with the Delphi method. In this post I describe the code used to give feedback to the participants in round 2 of the Delphi study on functional magnetic resonance imaging on tinnitus. If you are interested in the previous posts, this post describes the interface for round 1 whereas this post describes (the first part of) the interface for round 2.
The framework guiding the building of the interface was to create reusable code. Because I wanted to display the feedback from the previous round through bar plots, I had to make two graphs for each of the 40 questions of round 1. I wanted to be able to build both graphs using the same function. Moreover, to keep things simple and straightforward, I wanted to avoid to have to process the input arguments given to the function to create the two different graphs. Therefore the plotting function should have been called without passing arguments while keeping the graphs’ content split on the main page. This implied that retrieving 1) group responses and 2) the current user’s responses had to be done with the same XMLHttpRequest.
Let’s look at the XMLHttpRequest first. The XMLHttpRequest sends two identifiers, one for the experts’ group (expType) and one for the current question (qNum). The identifiers are passed through a JSON object to the fetchResults.php page. They then enter the MySQL SELECT statement addressing the database to find a count the responses. The XMLHttpRequest returns a JSON array with the responses of a given group to the current question. The array is passed to the barPlot() function through the datum field of d3.select().
if ( < 41){ document.getElementById('firstPlot').innerHTML = 'MRI experts'; document.getElementById('secondPlot').innerHTML = 'EEG, MEG, psychoacoustic experts'; var xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var myArr = JSON.parse(this.responseText); d3.select("#second").datum(myArr).call(barPlot()); }}; var selResps = { qNum:"", expType: 1}; xmlhttp.open("GET", "fetchResults.php?indexQ=" + JSON.stringify(selResps), true); xmlhttp.send(); }
The responses from MySQL SELECT are stored into an array which is extended with the response of the current individual if s/he belongs to the current expert group (i.e. if the second select statement does not return empty). The current user’s responses are selected through $_SESSION[‘ppid’]. $_SESSION[‘ppid’] is stored in the PHP session, so there is no need to pass it through the XMLHttpRequest. Since the counter for the question is also stored in the PHP $_SESSION I could have retrieved the current question number from the $_SESSION variable instead of passing it through the XMLHttpRequest. At the time of building the page I did not think about it, but now I realize that trick would have made things smoother… The array returned by the XMLHttpRequest contains the data to specify the height of the bars in the bar plot.
$obj = json_decode($_REQUEST['indexQ']); $expertType = $obj->expType; $obj = 'q' . $obj->qNum ; require '../includeDatabase.php'; $conn = mysqli_connect($servername,$username,$password,$dbname); if (mysqli_connect_errno()) { die('Could not connect: ' . mysqli_connect_error()); } $sql = "SELECT `{$obj}`, COUNT(`{$obj}`) AS counts FROM `mriTIN` WHERE session = 1 AND `id` NOT IN ('1', '2', '3', '7', '10') AND `uniqueID` IN ( SELECT `uniqueID` FROM `ppPim` WHERE `rType` ={$expertType}) GROUP BY `{$obj}`;"; $result = mysqli_query($conn,$sql); $response = array(); while($row = mysqli_fetch_row($result)) { $json['response'] = $row[0]; $json['counts'] = $row[1]; array_push($response, $json); } $sql = "SELECT uniqueID FROM `ppPim` WHERE uniqueID='{$_SESSION['ppid']}' AND rType={$expertType};" ; $result = mysqli_query($conn,$sql); if(mysqli_num_rows($result) > 0){ $sql = "SELECT `{$obj}`, COUNT(`{$obj}`) AS counts FROM `mriTIN` WHERE session = 1 AND uniqueID = '{$_SESSION['ppid']}';"; $result = mysqli_query($conn,$sql); while($row = mysqli_fetch_row($result)){ $json['response'] = $row[0]; $json['counts'] = $row[1]; array_push($response, $json);} } mysqli_close($conn); echo json_encode($response);
The framework I used to build the bar plot is from Mike Bostock’s Let’s Make a Bar Chart d3.js example. I embedded the bar plot code into Mike’s Towards Reusable Charts example. This allowed me to recycle the bar plot function to draw the bar plots for each of the experts’ groups. Considering the aim of re-usability it is a bit contradictory that I copy-pasted the same XMLHttpRequest twice. Building this web-page would otherwise have been a truly reusable code experience. Pity. Since in explaining the code Mike did a way better job than what I could possibly do, below I will outline what I added to Mike’s code to adapt it to my needs. The complete source for the barplot function is on github.
To keep a constant ratio between the x-axis of both graph I used d3.scaleBand() which is use for categorical data instead of continuous data. Treating the numbers identifying the radio buttons as categorical variables gives a pleasant constancy and allows a straightforward comparison of the graphs. Moreover d3.scaleBand() allows flexibility because it displays all the range of possible answer even if a given item did not receive a single response (many times the extreme points in the rating scale did not receive votes, this will create graphs of different horizontal width if the numbers were treated as numbers instead of categories.). Therefore the x-axis of bar plot always ranges from 1 to 10, even if, for example, participants only gave responses in the range between 4 and 6.
The Delphi method aims to show the respondents what the community think is relevant and what is not. The salience of an item will be reflected by high scores from many respondents. To accentuate the salience of the item to the user I color coded 1) the radio buttons of the tables and 2) the bars of the bar plot. The color code I used followed the traffic-light convention. I am not sure any traffic-light convention exists, but I like calling it like this. The traffic-light convention boils down to paint 1) green the ‘Not important’ items, 2) yellow the ‘Important but not critical’ items, and 3) red the ‘Critical’ items. ‘Unable to score’ was pink because it was pink already in round 1 of the Delphi study and what is there not like in a pink bar/button?
I am not sure the way I implemented my color scale is the most efficient or effective. It was efficient in the amount of time it costed me implementing it, which boiled down to create a vector with ten colors matching the bars and index inside of the svg rect object.
var color = ["#1a9850", "#1a9850", "#1a9850", // green "#ffd700", "#ffd700", "#ffd700", // yellow "#d73027", "#d73027", "#d73027", // red "#f1b6da"]; // pink g.selectAll(".bar") .data(data) .enter() .append("rect") .attr("fill",function(d,i){return color[d.response-1];}) ...
The response of the current user was highlighted coloring the corresponding bar in blue. To determine which bar should have been blue I used the redundancy of ‘bar indexes’ in the responses array. I introduced the redundancy in the responses array with the second MySQL SELECT statement in the XMLHttpRequest. The second SELECT statement extended the array containing the responses with an extra entry of responses if the current user belonged to the given group. Given that one of the two groups had one index double, when I identified the double entry I replaced the value for the given bar in the vector color with the value for the color blue (#386cb0). I used a match in a for loop to identify the presence of the redundancy. If a match was found the slot in the ‘color’ array was updated accordingly. Using a for loop is not very elegant, but it does the job.
for(var item = 0, nItems = data.length - 1; item < nItems; item++){ if(data[item].response===data[nItems].response){ color[data[nItems].response-1]="#386cb0"; }}
The solution above might appear intricate. The key to understanding it is to think in terms of array's indexes. The last entry in the data object contained the entry for the response of the current participant. The response field in the data object contained the index of the answer chosen by the current participant. The answer could be accessed through the last index in the object (e.g., data[nItems].response-1). Using that index to access the color array would have returned the color value for the given bar in the bar plot. Therefore, if a match was found, this index replaced the entry of the color array with the blue value (i.e., #386cb0).
All considered it was a nice project, and I am proud of the result. Adding more words to describe it might actually damage it. ;-P