Skip to content

Component Statistics

Jonathan Couldridge edited this page Jun 4, 2019 · 2 revisions

The Survey Platform offers a Dashboard view for in Progress and Completed Survey instances, which provides an overview of Participant progress for each Survey Question, and some basic statistics for the responses to each question.

Because of the Component based approach to response types in DECSYS, it is up to individual Response Components to specify what statistics they can provide, given a set of their own results data.

This is necessary because the statistics for a set of Free Text responses are wholly incompatible with those for a set of Discrete Scale value responses (e.g. "on a Scale of 1 - 10...").

You can do math on values from 1 to 10; you can't on a series of words.

So, how do you specify stats for your Response Component? Let's take a look.

Component.stats.js

We recommend adding a Stats spec in its own file, similarly to how the Props spec in the boilerplate is in its own Component.props.js.

You can of course have your stats depend on any other modules (such as custom mathematical functions) or React components (such as those providing graphical visualisations) you wish.

Structure

The structure of the Stats spec is incredibly straightforward.

  1. It must be a function which takes two arguments: params and results.
    • At runtime, the Survey Platform will pass the Stats() function the Parameters your Component was configured with for the relevant question, and an array of results payloads for the question. Results payloads will always be objects, matching those your Component logged using logResults().
  2. The function should return an object.
  3. The object may contain a stats property, which must be a shallow object of key/value pairs. the values should be strings (or sensibly convertible to strings).
    • The Platform will render any stats it finds here directly.
  4. The object may contain a visualizations property, which must be an array of objects with two keys: name and component.
    • The Platform will render the first visualization component, if there are any.

Adding Stats to the Component

For the Platform to recognise the Stats spec, it must be added to the top level Component as a metadata property - the same way params is.

In Componentjs, simply

  • import stats from "./Component.stats.js";
  • then set Component.stats = stats at the bottom, where params, propTypes and defaultProps are set.

Example

Let's take the demo component from Making a DECSYS Response Component and give it some stats.

Start by adding Component.stats.js, and add the Stats spec function, returning an empty template structure for now:

const stats = (params, results) => ({
  visualizations: [],
  stats: {}
});

export default stats;

Stats

We'll ignore visualizations for the moment, and look at actual stats.

Since the results from our Component is the count of button presses, that's a nice numeric value. We could take the average number of button presses across our results set.

For this, we'll take advantage of a global library the Survey Platform makes available for Components to use, so we won't have to add a new dependency: math.js.

import * as math from "mathjs";

const stats = (params, results) => ({
  visualizations: [],
  stats: {
    ["Mean Button Presses"]: math.mean(results.map(x => x.count))
  }
});

export default stats;

Here we've imported everything from MathJS. This is fine since we're not bundling it, so there's no tree shaking to be done.

Then we've added a key value pair to the stats object.

The key is a string, but because it will be used directly in the Survey Platform as the name of the stat, we've used Title Case and spaces to make it friendly. This forces us to use the ES2015 syntax ["Whitespace Key"]: value.

The value is the result of calling math.mean on the results. We have to remember that results will be an array of the results objects our Component logged, which look like: { count: <number> }, so we use Array.prototype.map() to get just the count values. That should give us a nice average of our results.

It may be meaningful to anyone running a Survey with our Component to exclude those who never pressed the button - find the average number of presses from those who at least pressed it once.

// ...

  stats: {
    ["Mean Button Presses"]: math.mean(results.map(x => x.count)),
    ["Mean Button Presses (excluding skippers!)"]: math.mean(results.map(x => x.count).filter(x => x !== 0))
  }

// ...

As you can see, building a Stats spec is quite straightforward.

Visualizations

Let's visualize our results. The Platform will take the first visualization Component and render it (it may take more in future, hence the array with name properties).

So, we're going to want to write a Component for our visualization. This is great, as it's truly flexible as to what you can use for visualizing data. Even if it's just a datatable, you can do that. Or you want to display an image of your grandmother but the color saturation is based on the results set. I guess you could do that.

The two most common cases are likely to be graphing numeric data, and somehow displaying word data.

To this end, the Platform provides Victory and react-wordcloud globally.

React WordCloud is just that - a React component that renders a Word Cloud from a data set. DECSYS' FreeText Component uses that for its visualization.

Victory is a graphing component library for React, built on top of the popular d3.js. It provides React Components for many common d3 use cases when drawing graphs.

If you have more specific needs, remember you can always add other npm packages as you need.

Example

For our example, let's draw a histogram of respondents by button press count:

First, let's add a Visualization.js inside components/.

import React from "react";
import { VictoryBar, VictoryChart, VictoryAxis } from "victory";

const Visualization = ({ values }) => {
  const aggregate = values.reduce((agg, v) => {
    agg[v] = (agg[v] || 0) + 1;
    return agg;
  }, {});

  const counts = Object.keys(aggregate).map(v => aggregate[v]);
  const data = Object.keys(aggregate).map(x => ({ x, y: aggregate[x] }));

  return (
    <VictoryChart domainPadding={20}>
      <VictoryAxis
        label="Number of Participants"
        dependentAxis
        tickCount={Math.max(...counts)}
        tickFormat={x => parseInt(x).toString()}
      />
      <VictoryAxis label="Number of Button Presses" />
      <VictoryBar data={data} />
    </VictoryChart>
  );
};

export default Visualization;

You'll see we're importing React so we can use JSX, and a number of components from Victory. This isn't a Guide on Victory, so we'll only briefly discuss it here.

Our component takes as props the already mapped results values.

It then uses Array.prototype.reduce() to produce a lookup of counts for each results value.

We transform the data slightly to make component props easier, then we use that to setup our axes, and finally render a bar chart of our data.

We'll then need to wire up our new Visualization component in Component.stats.js:

import React from "react";
import * as math from "mathjs";
import Visualization from "./components/Visualization";

const stats = (_, results) => {
  const values = results.map(x => x.count);
  return {
    visualizations: [
      {
        name: "Results",
        component: <Visualization values={values} />
      }
    ],
    stats: {
      ["Mean Button Presses"]: math.mean(values),
      ["Mean Button Presses (excluding skippers!)"]: math.mean(values.filter(x => x !== 0))
    }
  };
}

export default stats;

And that's all there is to it.

If you really wanted, you could add a Storybook story to test the stats and visualizations. To do that I would recommend looking at the DECSYS Discrete or Ellipse Components for their Component.stories.js files, as they contain some useful helper code, and examples of passing dummy results data into the Stats spec.

Clone this wiki locally