Repository: oppia/oppia

Issue:
Add an item selection input visualization to the statistics tab of the exploration editor.
For item selection interactions, there is no visualization shown in the state statistics modal in the stats tab of the exploration editor. This makes it difficult to address issues in those questions, and we should find a way to fix this.

As a first step, just showing a table of frequently-submitted answers (arranged in decreasing order by frequency) would be great.

To reproduce:
1. Create an exploration locally with a text input interaction and an item-selection interaction.
2. As a learner, submit some answers to both questions in that exploration.
3. Log in as an admin, go to /admin, go to the Jobs tab and start the InteractionAnswerSummariesAggregator continuous computation. Wait for it to finish running at least once.
4. Go to the stats tab in the exploration editor, and click on a state to view its stats. (Ignore the "Times entered: 0" part -- that's a separate issue that we're currently investigating.) You'll see that the text input state has a table of answers sorted in decreasing order by frequency, but the visualization for the item-selection interaction is missing.


Discussion:

Resolving patch:
diff --git a/core/domain/stats_services.py b/core/domain/stats_services.py
--- a/core/domain/stats_services.py
+++ b/core/domain/stats_services.py
@@ -307,7 +307,7 @@ def get_visualizations_info(exp_id, state_name, interaction_id):
     Returns:
         list(dict). Each item in the list is a dict with keys representing
         - 'id': str. The visualization ID.
-        - 'data': list(dict). A list of calculation IDs.
+        - 'data': list(dict). A list of answer/frequency dicts.
         - 'options': dict. The visualization options.
 
         An example of the returned value may be:
@@ -343,7 +343,7 @@ def get_visualizations_info(exp_id, state_name, interaction_id):
             continue
 
         calculation_ids_to_outputs[calculation_id] = (
-            calc_output_domain_object.calculation_output)
+            calc_output_domain_object.calculation_output.to_raw_type())
     return [{
         'id': visualization.id,
         'data': calculation_ids_to_outputs[visualization.calculation_id],
diff --git a/extensions/interactions/ItemSelectionInput/ItemSelectionInput.py b/extensions/interactions/ItemSelectionInput/ItemSelectionInput.py
--- a/extensions/interactions/ItemSelectionInput/ItemSelectionInput.py
+++ b/extensions/interactions/ItemSelectionInput/ItemSelectionInput.py
@@ -70,3 +70,13 @@ class ItemSelectionInput(base.BaseInteraction):
         },
         'default_value': [''],
     }]
+
+    _answer_visualization_specs = [{
+        # Table with keyed answer counts for top N answers.
+        'id': 'EnumeratedFrequencyTable',
+        'options': {
+            'column_headers': ['Answer (click to expand/collapse)', 'Count'],
+            'title': 'Top answers',
+        },
+        'calculation_id': 'Top10AnswerFrequencies',
+    }]
diff --git a/extensions/visualizations/models.py b/extensions/visualizations/models.py
--- a/extensions/visualizations/models.py
+++ b/extensions/visualizations/models.py
@@ -97,3 +97,28 @@ class FrequencyTable(BaseVisualization):
         'description': 'The title of the visualization.',
         'schema': {'type': 'unicode'}
     }]
+
+
+class EnumeratedFrequencyTable(BaseVisualization):
+    """A visualization representing a two-column table with answer counts. The
+    answer column is made up of a clickable ranking, which toggles the
+    visibility of the answer's content below it.
+
+    The #1 entry is shown by default, all others start hidden.
+    """
+
+    _OPTIONS_SPECS = [{
+        'name': 'column_headers',
+        'description': 'The headers for the columns.',
+        'schema': {
+            'type': 'list',
+            'items': {
+                'type': 'unicode',
+            },
+            'len': 2,
+        },
+    }, {
+        'name': 'title',
+        'description': 'The title of the visualization.',
+        'schema': {'type': 'unicode'}
+    }]


Test patch:
diff --git a/core/domain/stats_services_test.py b/core/domain/stats_services_test.py
--- a/core/domain/stats_services_test.py
+++ b/core/domain/stats_services_test.py
@@ -14,6 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import operator
 import os
 
 from core import jobs_registry
@@ -1243,7 +1244,7 @@ def test_has_vis_info_for_exp_with_answer_for_one_calculation(self):
 
             visualization = visualizations[0]
             self.assertEqual(visualization['id'], 'FrequencyTable')
-            self.assertEqual(visualization['data'].to_raw_type(), [{
+            self.assertEqual(visualization['data'], [{
                 'answer': 'Answer A',
                 'frequency': 1
             }])
@@ -1261,7 +1262,7 @@ def test_has_vis_info_for_exp_with_many_answers_for_one_calculation(self):
 
             visualization = visualizations[0]
             self.assertEqual(visualization['id'], 'FrequencyTable')
-            self.assertEqual(visualization['data'].to_raw_type(), [{
+            self.assertEqual(visualization['data'], [{
                 'answer': 'Answer A',
                 'frequency': 3
             }, {
@@ -1283,7 +1284,7 @@ def test_has_vis_info_for_each_calculation_for_multi_calc_exp(self):
             self._run_answer_summaries_aggregator()
             visualizations = sorted(
                 self._get_visualizations(exp_id=self.SET_INPUT_EXP_ID),
-                key=lambda x: x['data'].to_raw_type())
+                key=operator.itemgetter('data'))
             self.assertEqual(len(visualizations), 2)
 
             # Use options to distinguish between the two visualizations, since
@@ -1293,7 +1294,7 @@ def test_has_vis_info_for_each_calculation_for_multi_calc_exp(self):
             self.assertEqual(
                 top_answers_visualization['options']['column_headers'],
                 ['Answer', 'Count'])
-            self.assertEqual(top_answers_visualization['data'].to_raw_type(), [{
+            self.assertEqual(top_answers_visualization['data'], [{
                 'answer': ['A', 'B'],
                 'frequency': 3
             }, {
@@ -1312,7 +1313,7 @@ def test_has_vis_info_for_each_calculation_for_multi_calc_exp(self):
                 ['Element', 'Count'])
 
             common_visualization_data = (
-                common_elements_visualization['data'].to_raw_type())
+                common_elements_visualization['data'])
             self.assertEqual(common_visualization_data, [{
                 'answer': 'A',
                 'frequency': 6
@@ -1338,7 +1339,7 @@ def test_retrieves_latest_vis_info_with_rounds_of_calculations(self):
 
             visualization = visualizations[0]
             # The latest data should include all submitted answers.
-            self.assertEqual(visualization['data'].to_raw_type(), [{
+            self.assertEqual(visualization['data'], [{
                 'answer': 'Answer A',
                 'frequency': 2
             }, {
@@ -1361,7 +1362,7 @@ def test_retrieves_vis_info_across_multiple_exploration_versions(self):
 
             visualization = visualizations[0]
             # The latest data should include all submitted answers.
-            self.assertEqual(visualization['data'].to_raw_type(), [{
+            self.assertEqual(visualization['data'], [{
                 'answer': 'Answer A',
                 'frequency': 2
             }, {