In experimental economics, participants are often asked to vote on tax or redistribution rates. When measured, these votes are usually a central part of the research. Nevertheless, it is hard to know whether the participant possesses an accurate understanding of their chosen tax rate's effect.
In one of my experimental studies, I tried to remedy this by providing participants with a graph of other participants' scores and a slider through which they can set their desired tax rate.
As the slider moves, the distribution changes accordingly, reflecting the impact that a given tax rate would have on the distribution if it were to be applied.
The colors of the icons represent the income quintile that each participant belongs to. When applied in an interactive experiment, the columns and icons of the interactive graph below refer to real participants.
You can test the example below before moving on to learning how to apply it in your experiments.
This part of the Python code initializes the page class and sets the conditions for when the page is displayed. It also defines the player form model and fields, so that we can collect the tax votes properly.
class InteractiveGraph(Page):
form_model = 'player'
form_fields = ['tax_vote']
def is_displayed(player):
return player.consent and not player.remove
This part of the Python code handles the core logic for generating the player data used to display the interactive graph. It processes the players' group type, rankings, and calculates the scores and bar heights for the graph.
In this specific case, I have split the participants in two groups. The redistribution rate chosen by each participant will only apply to the participants of the other group, so we are only going to display to each participant the data of that other group.
def vars_for_template(player):
current_player = player
player_group_type = current_player.group_type
player.percentage_ranking_100 = 100 - current_player.percentage_ranking * 100
players = sorted(
[p for p in player.group.get_players() if p.group_type != player_group_type and p.field_maybe_none('score2') is not None],
key=lambda p: p.score2
)
scores2 = [p.score2 for p in players]
id_in_group_list = [p.id_in_group for p in players]
The final part of the Python code assigns colors to players based on their rankings and prepares the data for the front-end. This is where you can adjust the bar heights and payment structure depending on how much each question paid in your "score" task. It also handles players who time out before progressing to the next page.
for player in players:
if player.percentage_ranking < 0.20:
player.color = 'red_human.png'
elif player.percentage_ranking < 0.40:
player.color = 'yellow_human.png'
elif player.percentage_ranking < 0.60:
player.color = 'green_human.png'
elif player.percentage_ranking < 0.80:
player.color = 'orange_human.png'
else:
player.color = 'blue_human.png'
player_colors = [f"/static/Test_Luck_Merit/Images/{p.color}" for p in players]
bar_heights = [score * 20 + 5 for score in scores2]
rankings = [f"{(1 - p.percentage_ranking) * 100:.2f}%" for p in players]
money_earned = [f"€{(score / 15) * 2.5:.2f}" for score in scores2]
scores_and_colors = zip(scores2, player_colors, bar_heights, rankings, money_earned, id_in_group_list)
return {
'scores_and_colors': list(scores_and_colors), # Convert zip to list
'percentage_ranking_100': current_player.percentage_ranking_100,
'participant_color': current_player.field_maybe_none('color') or 'default_icon.png',
'specific_participant_id_in_group': current_player.id_in_group,
'bin_index': player.get_rank_bin(),
'all_histogram': [], # Provide a default value for all_histogram
'bins_labels': [str(i) for i in range(len(players))] # Use indices as dummy labels
}
def before_next_page(player, timeout_happened):
if timeout_happened:
player.remove = True
The HTML structure defines the layout and basic styles for the interactive graph. This is only a short sample of the styles, there are many others you could include, some more needed (such as containers) and others less (such as hover effects). You can always use a .css file instead. If you would like to see all styles, you can download the corresponding HTML code through the button above.
<style>
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
color: #333;
}
.histogram-wrapper {
width: 100%;
margin: 20px auto;
background: #ffffff;
padding: 20px;
border-radius: 15px;
}
</style>
This section includes the main wrapper and style settings for the histogram and handles the dynamic bar graph. Each player is represented by a bar, and the heights of these bars change based on their score. The `icon` image is used to mark each participant. The information displayed on the graph (money, score) has been defined earlier in our Python code and can be adjusted stylistically in the styles block.
<div class="histogram-wrapper">
<div class="histogram-title">Interactive Distribution of Scores</div>
<div class="histogram-container" id="iconsHistogramInteractive">
<div class="horizontal-line"></div>
{% for score, icon, height, ranking, money, id_in_group in scores_and_colors %}
<div class="bar" style="flex: 1 1 calc((100% / {{ scores_and_colors|length }}) - 4px);">
<div class="bar-graph" style="height: {{ height }}px;">
<span class="earnings-value">{{ money }}</span>
<div class="score-value">{{ score }}</div>
</div>
<img src="{{ icon }}" class="icon {% if specific_participant_id_in_group == id_in_group %}highlight pulse-effect{% endif %}">
</div>
{% endfor %}
</div>
Here we are adding the slider controls the tax rate (which we also saw earlier in the Python code), and the graph dynamically updates based on the selected value. The dynamic relationship is built later, in the JavaScript. We are also adding a button to submit the response.
This part also adds an explanation box at the bottom of the graph, where users are informed that they need to move the slider to adjust the tax rate. You do not need to include this part, but in the JavaScript later, you will see that I added a functionality that requires that the participants touch the slider before proceeding. I added the functionality to avoid people accidentally choosing a 0% rate and moving to the next page.
<div class="explanation-box">
<p>You must move the slider in order to continue, even if you move it only to bring it back to zero.</p>
</div>
<div class="explanation-box">
<label for="taxRateSlider">Tax Rate:</label>
<input type="range" id="taxRateSlider" name="taxRateSlider" min="0" max="100" value="0">
<span id="taxRateValue">0%</span>
</div>
<form id="taxVoteForm" method="post">
{{ form }}
<input type="hidden" id="taxVoteInput" name="tax_vote" value="0">
<button type="submit" id="nextButton" class="btn btn-primary" disabled>Next</button>
</form>
This part of the JavaScript listens for changes to the tax rate slider. When the user adjusts the slider, the tax rate is applied to each player's score, and the graph is updated in real-time to reflect the new earnings for each player.
Here you can adjust the type of redistribution you want to apply on the graph (but you need a separate Python code to actually apply the redistribution when making the payments). In this case, the deduction is proportionally applied to everyone and then the same deduction is equally redistributed to each participant.
<script>
document.addEventListener('DOMContentLoaded', function() {
const taxRateSlider = document.getElementById('taxRateSlider');
taxRateSlider.addEventListener('input', function() {
const taxRate = parseFloat(this.value);
document.getElementById('taxRateValue').innerText = `${taxRate}%`;
const bars = document.querySelectorAll('.bar-graph');
const originalScores = Array.from(document.querySelectorAll('.icon')).map(icon => parseFloat(icon.getAttribute('data-original-score')));
const deductedScores = originalScores.map(score => score - (score * taxRate / 100));
const totalDeduction = originalScores.reduce((acc, score) => acc + (score * taxRate / 100), 0);
const redistributedScore = totalDeduction / originalScores.length;
bars.forEach((bar, index) => {
const newScore = deductedScores[index] + redistributedScore;
const newHeight = newScore * 20 + 5;
bar.style.height = `${newHeight}px`;
bar.querySelector('.earnings-value').innerText = `€${(newScore / 15 * 2.5).toFixed(2)}`;
});
});
});
This final part ensures that the form submission captures the selected tax rate. It also handles the enabling of the "Next" button once the slider is moved. This is the functionality I mentioned in Step 6 when I included the explanation box.
const taxVoteForm = document.getElementById('taxVoteForm');
const nextButton = document.getElementById('nextButton');
let sliderMoved = false;
taxRateSlider.addEventListener('input', function() {
if (!sliderMoved) {
sliderMoved = true;
nextButton.disabled = false;
}
});
taxVoteForm.addEventListener('submit', function(event) {
taxVoteInput.value = taxRateSlider.value;
});
</script>
Thank you for reading this tutorial, if you have any questions, please don't hesitate to contact me.
More tutorials: