Have an idea?

Visit Sawtooth Software Feedback to share your ideas on how we can improve our products.

Gabor–Granger method

I have a client asking about programming a study with a random laddering technique (Gabor Granger).  I'm not familiar with this but have looked up the basic method.  Does anyone have experience on the best way/method to program this logic?  

From what I've seen, you're looking at 5 price points and asking one randomly on a 5 point scale.  If in the top 2 box move to the next highest amount until they no longer answer in the top 2 box then stop.  If the first price is not in the top 2 box, ask the next lowest price until they give a response in the top 2 box or until all lower prices have been asked.
asked Feb 8, 2018 by Jay Rutherford Gold (38,005 points)
Hi Guys! Great, this code helped me a lot! I am seeking for e way to hide the Next button untill the Gabor-Granger is finished. Is there someone with experience doing this? Thanks a lot in advance!

1 Answer

0 votes
I've just finished my Gabor-Granger question.  It was a bit of a unique challenge from a design perspective, so I would be interesting in hearing any thoughts on user experience and clarity and all that fun stuff.

To build it, start with a free format question.  The free format requires a hidden, text variable named "_Price."

Somewhere in the question texts, place something like this to display the current price to the respondent:

How likely are you to buy this product for <b><span class="gaborGrangerPrice"></span></b>?


Optionally place something like this elsewhere in the question texts to inform respondents that the question is complete:

<span class="gaborGrangerFinished"><i>Your responses have been recorded.  Please continue with the survey.</i></span>


Finally, set the question HTML to this:

<input name="[% QuestionName() %]_Price" id="[% QuestionName() %]_Price" type="hidden" value=""/>
<div class="gaborGrangerContainer"></div>

<style>
.gaborGrangerFinished {
    display: none;
}

.gaborGrangerButton {
    display: block;
    width: 200px;
    margin: 10px 0px;
    padding: 5px;
    cursor: pointer;
}

.gaborGrangerButton:hover {
    background-color: #CDCDCD;
}
</style>

<script>
$(document).ready(function(){
    var prices = ['$1', '$2', '$3', '$4', '$5'];
    var startPosition = 'middle'; // the position of the first price to show, or 'middle' to automatically select middle price; 'random' to select random starting price
    // Gabor-Granger experiments are typically performed with either two acceptables and three unacceptables, or one acceptable and one unaccpetable
    var acceptableResponses = ['Extremely likely', 'Very likely'];
    var unacceptableResponses = ['Somewhat likely', 'Not very likely', 'Not at all likely'];
    var noneResponse = 'NONE';
    
    // initialize price
    var gaborPosition;
    switch (startPosition.toString().toLowerCase()) {
        case 'middle':
            gaborPosition = Math.ceil(prices.length / 2);
            break;
        case 'random':
            gaborPosition = Math.ceil(Math.random() * prices.length)
            break;
        default:
            gaborPosition = Number(startPosition);
            break;
    }
    $('#[% QuestionName() %]_div .gaborGrangerPrice').text(prices[gaborPosition - 1]);
    
    // create buttons
    acceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .gaborGrangerContainer').append('<button type="button" class="gaborGrangerButton acceptable">' + response + '</button>');
    });
    unacceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .gaborGrangerContainer').append('<button type="button" class="gaborGrangerButton unacceptable">' + response + '</button>');
    });
    
    // click events
    var gaborPreviousPosition;
    $('#[% QuestionName() %]_div .gaborGrangerButton').click(function(){
        var acceptable = $(this).is('.acceptable');
        if (gaborPosition == 1 && !acceptable) {
            finishGaborGranger('[% QuestionName() %]', noneResponse);
        }
        else if ((gaborPosition == prices.length && acceptable) ||
            (acceptable && gaborPreviousPosition && gaborPosition < gaborPreviousPosition)) {
            finishGaborGranger('[% QuestionName() %]', prices[gaborPosition - 1]);
        }
        else if ((gaborPosition == prices.length && !acceptable && gaborPreviousPosition) ||
            (gaborPosition == 1 && acceptable && gaborPreviousPosition) ||
            (!acceptable && gaborPreviousPosition && gaborPosition > gaborPreviousPosition)) {
            finishGaborGranger('[% QuestionName() %]', prices[gaborPreviousPosition - 1]);
        }
        else {
            gaborPreviousPosition = gaborPosition;
            gaborPosition += acceptable ? 1 : -1;
            updateGaborGranger('[% QuestionName() %]', prices[gaborPosition - 1]);
        }
    });
})

function updateGaborGranger(question, price) {
    var qdiv = $('#' + question + '_div');
    $(qdiv).fadeOut(500, function(){
        $(qdiv).find('.gaborGrangerPrice').text(price);
        var bgColor = $(qdiv).css('background-color');
        $(qdiv).css('background-color', 'lightcyan');
        $(qdiv).fadeIn(1000, function(){
            $(qdiv).css('background-color', bgColor);
        });
    });
}

function finishGaborGranger(question, price) {
    $('#' + question + '_Price').val(price);
    var qdiv = $('#' + question + '_div');
    $(qdiv).find('.gaborGrangerButton').prop('disabled', true).css('cursor', 'default');
    $(qdiv).find('.gaborGrangerFinished').show();
}
</script>


Line 24 should be updated with the relevant prices, from lowest to highest.

Line 25 can be updated to set which price a respondent starts on.

Line 27 and 28 can be updated with the acceptable and unacceptable response options.  From what I can tell, most Gabor-Granger experiments have (A) two acceptables and three unacceptables, or (B) an acceptable "Yes" and an unacceptable "No."

Line 29 can be updated with the value to be recorded if a respondent says that the lowest price is unacceptable.
answered Feb 9, 2018 by Zachary Platinum Sawtooth Software, Inc. (156,150 points)
That's great Zach.  I'll take a deeper dive into your code on Monday and give it a test drive.  I actually just got finished writing up a test program of my own just using a series of questions with a lot of list building along with Pass-In variables to see if I could get something to work.  Nowhere near as elegant to what you have and wouldn't be too friendly if it got more complex with more items, but it would work in a pinch.  Excited to look over what you have more and I appreciate you taking the time to put something together.
I like what you put together.  I have a few questions/comments regarding this.
- I see how it flashes and moves to the next price point, however, at the last item it just goes gray and isn't intuitive that you should click next to move on.  Can this automatically move forward when completed, or get an instruction?
- Is it possible to store each response in a different variable, should the individual data be wanted for any reason?
- Not being familiar with this method, I have to wonder, would they want the value set to the highest one with the highest rating if none were in the top two box?  As you have it, it defaults to 'none' or whatever value you want.  When I was working through this I had set a value to the highest even if that wasn't in that acceptable (top 2 box) range.  Did you find your method to be the standard for this analysis?  
-
From what I've read it would seem there should always be a price point generated (even if not in the top 2 box, then whatever their highest threshold is).
Clarity for when the question ends was a concern for me as well.  Currently, my simple fix is to show the extra text (within "<span class="gaborGrangerFinished">") when the question is finished.  If this is insufficient, we may need to consider other options.  Automatically submitting the page is a possibility, but would limit survey creators from putting this question on the same page as other questions.

What prices a respondent saw and whether they answered acceptable or unacceptable can be deduced from their final price point (as long as you don't start at a random price).  But I could add a second variable that records all of the respondent's choices if there is statistical relevance as to which acceptable answer the respondent selected and which unacceptable answer the respondent selected.

The resources I have briefly looked through have not offered specifics regarding what should happen if a respondent never marks a price as acceptable.  If you have sources with more information on Gabor-Granger implementation, I would be interested in reading them.
I think using the text within the span tags would work to let them know the exercise is complete.  Thanks.  You are correct and it looks like it's suggested to code any that don't give a top 2 box rating as a 0 or to just remove from the analysis.  I'll have to discuss with this client further if they proceed with this as to what they want for deliverables. Just the final variable score or the data from the steps as well.  It's probably just the final score, but the steps data is good to check/validate the result in the data.
I have made adjustments to record exact responses.

Add two more hidden, text variables "_StartPrice" and "_Responses," then change the HTML to this:

<input name="[% QuestionName() %]_Price" id="[% QuestionName() %]_Price" type="hidden" value=""/>
<input name="[% QuestionName() %]_StartPrice" id="[% QuestionName() %]_StartPrice" type="hidden" value=""/>
<input name="[% QuestionName() %]_Responses" id="[% QuestionName() %]_Responses" type="hidden" value=""/>

<div class="gaborGrangerContainer"></div>

<style>
.gaborGrangerFinished {
    display: none;
}

.gaborGrangerButton {
    display: block;
    width: 200px;
    margin: 10px 0px;
    padding: 5px;
    cursor: pointer;
}

.gaborGrangerButton:hover {
    background-color: #CDCDCD;
}
</style>

<script>
$(document).ready(function(){
    var prices = ['$1', '$2', '$3', '$4', '$5'];
    var startPosition = 'middle'; // the position of the first price to show, or 'middle' to automatically select middle price, or 'random' to select random starting price
    // Gabor-Granger experiments are typically performed with either two acceptables and three unacceptables, or one acceptable and one unaccpetable
    var acceptableResponses = ['Extremely likely', 'Very likely'];
    var unacceptableResponses = ['Somewhat likely', 'Not very likely', 'Not at all likely'];
    var noneResponse = 'NONE';
    
    // initialize price
    var gaborPosition;
    switch (startPosition.toString().toLowerCase()) {
        case 'middle':
            gaborPosition = Math.ceil(prices.length / 2);
            break;
        case 'random':
            gaborPosition = Math.ceil(Math.random() * prices.length)
            break;
        default:
            gaborPosition = Number(startPosition);
            break;
    }
    $('#[% QuestionName() %]_div .gaborGrangerPrice').text(prices[gaborPosition - 1]);
    
    // create buttons
    var responseCounter = 1;
    acceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .gaborGrangerContainer').append('<button type="button" class="gaborGrangerButton acceptable" data-gabor="' + responseCounter + '">' + response + '</button>');
        responseCounter++;
    });
    unacceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .gaborGrangerContainer').append('<button type="button" class="gaborGrangerButton unacceptable" data-gabor="' + responseCounter + '">' + response + '</button>');
        responseCounter++;
    });
    
    // click events
    var gaborPreviousPosition;
    var firstClick = true;
    var responsesSep = '';
    $('#[% QuestionName() %]_div .gaborGrangerButton').click(function(){
        if (firstClick) {
            $('#[% QuestionName() %]_Price').val('');
            $('#[% QuestionName() %]_StartPrice').val(prices[gaborPosition - 1]);
            $('#[% QuestionName() %]_Responses').val('');
            firstClick = false;
        }
        
        $('#[% QuestionName() %]_Responses').val($('#[% QuestionName() %]_Responses').val() + responsesSep + $(this).data('gabor'));
        responsesSep = ',';
    
        var acceptable = $(this).is('.acceptable');
        if (gaborPosition == 1 && !acceptable) {
            finishGaborGranger('[% QuestionName() %]', noneResponse);
        }
        else if ((gaborPosition == prices.length && acceptable) ||
            (acceptable && gaborPreviousPosition && gaborPosition < gaborPreviousPosition)) {
            finishGaborGranger('[% QuestionName() %]', prices[gaborPosition - 1]);
        }
        else if ((gaborPosition == prices.length && !acceptable && gaborPreviousPosition) ||
            (gaborPosition == 1 && acceptable && gaborPreviousPosition) ||
            (!acceptable && gaborPreviousPosition && gaborPosition > gaborPreviousPosition)) {
            finishGaborGranger('[% QuestionName() %]', prices[gaborPreviousPosition - 1]);
        }
        else {
            gaborPreviousPosition = gaborPosition;
            gaborPosition += acceptable ? 1 : -1;
            updateGaborGranger('[% QuestionName() %]', prices[gaborPosition - 1]);
        }
    });
})

function updateGaborGranger(question, price) {
    var qdiv = $('#' + question + '_div');
    $(qdiv).fadeOut(500, function(){
        $(qdiv).find('.gaborGrangerPrice').text(price);
        var bgColor = $(qdiv).css('background-color');
        $(qdiv).css('background-color', 'lightcyan');
        $(qdiv).fadeIn(1000, function(){
            $(qdiv).css('background-color', bgColor);
        });
    });
}

function finishGaborGranger(question, price) {
    $('#' + question + '_Price').val(price);
    var qdiv = $('#' + question + '_div');
    $(qdiv).find('.gaborGrangerButton').prop('disabled', true).css('cursor', 'default');
    $(qdiv).find('.gaborGrangerFinished').show();
}
</script>


In addition, here's the custom JS verification to require a response:

if (!SSI_GetValue('[% QuestionName() %]_Price')) {
    strErrorMessage = 'A response is required.';
}
Fantastic work as always!!!  Works nicely and now should they want the response data, it's available.
Everyone,
this is a great addition to Sawtooth.  I'm really grateful.  I was trying to take it a step further, but am too new to coding to make it work.  Can anyone make the following upgrade/changes that allow for RANDOMISED price options?
For example:
1. If they are willing to pay that price, they are offered a higher (randomly chosen) price within the acceptable price options.
2. If they are not willing to pay that price, they are offered a lower (randomly chosen) price within the acceptable price options.
3. The algorithm repeats until we find the highest price each respondent is willing to pay.

The current sequential method seems to encourage the respondent to not reach the higher price, if they are randomly started at the lower prices.
This works great. As Jay requested, can experts modify the script to randomize price options for subsequent steps? Thank you!
Hi Zachary,
What would be the code if we want to use a radio button question format instead of the gaborGrangerButton so it matches with the rest of the survey's look and feel? Or at least to change the button colors (white fill/green borders).
Radio buttons would take a bit more time, but changing the button appearance should be easy enough.  There's a ".gaborGrangerButton" section in the CSS already - you could add /modify rules to that like "background-color: white;" or "border: 1px solid green;"
Thank you! That works. Also, I would like to save the answer each price shown receives. I want the dataset to have a variable per price (eg. Price1, Price2, Price 3, Price4, Price5). If someone starts with price 3 and says "Not Very likely" then the column for that price point with get a 4, if he then goes to Price 2 and gives a Not very likely, then the Price2 column gets a 4 and when he goes to Price 1 and gives a Very likely, then the column for Price 1 gets a 1, and so on. The prices not shown would have a 0. What would be the code to record all the answers for each of the shown prices, besides the 3 hidden variables you already created? I'm starting with a random price, so trying to figure out what prices were shown and their answers could be time consuming prompt to error for a large sample.
Zach,
I added the code for the individual variables but get a Require answer error. I assume is because not all prices are shown and some of those hidden variables have missing values. How can I populate those missing values with zeros so the system considers that question answered?

here is my code. I'm not able to move forward with that error


<input name="[% QuestionName() %]_Price" id="[% QuestionName() %]_Price" type="hidden" value=""/>
<input name="[% QuestionName() %]_StartPrice" id="[% QuestionName() %]_StartPrice" type="hidden" value=""/>
<input name="[% QuestionName() %]_Responses" id="[% QuestionName() %]_Responses" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response1" id="[% QuestionName() %]_Response1" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response2" id="[% QuestionName() %]_Response2" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response3" id="[% QuestionName() %]_Response3" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response4" id="[% QuestionName() %]_Response4" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response5" id="[% QuestionName() %]_Response5" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response6" id="[% QuestionName() %]_Response6" type="hidden" value=""/>

<div class="gaborGrangerContainer"></div>
 
<style>
.gaborGrangerFinished {
    display: none;
}
 
.gaborGrangerButton {
    display: block;
    background-color: white;
    border: 1px solid green;
    width: 250px;
    margin: 20px 0px;
    padding: 10px;
    font-family: "Open Sans";
    font-size: medium;
    font-weight: 400;
    cursor: pointer;
}
 
.gaborGrangerButton:hover {
    background-color: #CDCDCD;
}
</style>
 
<script>
$(document).ready(function(){
    var prices = ['$100', '$125', '$150', '$175', '$200', '$225'];
    var startPosition = 'random'; // the position of the first price to show, or 'middle' to automatically select middle price; 'random' to select random starting price
    // Gabor-Granger experiments are typically performed with either two acceptables and three unacceptables, or one acceptable and one unaccpetable
    var acceptableResponses = ['Definitely would pay', 'Probably would pay'];
    var unacceptableResponses = ['Might or might not pay', 'Probably would NOT pay', 'Definitely would NOT pay'];
    var noneResponse = 'NONE';
     
    // initialize price
    var gaborPosition;
    switch (startPosition.toString().toLowerCase()) {
        case 'middle':
            gaborPosition = Math.ceil(prices.length / 2);
            break;
        case 'random':
            gaborPosition = Math.ceil(Math.random() * prices.length)
            break;
        default:
            gaborPosition = Number(startPosition);
            break;
    }
    $('#[% QuestionName() %]_div .gaborGrangerPrice').text(prices[gaborPosition - 1]);

    // reset individual responses
    $('#[% QuestionName() %]_Response1').val('');
    $('#[% QuestionName() %]_Response2').val('');
    $('#[% QuestionName() %]_Response3').val('');
    $('#[% QuestionName() %]_Response4').val(''); 
    $('#[% QuestionName() %]_Response5').val('');
    $('#[% QuestionName() %]_Response6').val('');
     
    // create buttons
    var responseCounter = 1;
    acceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .gaborGrangerContainer').append('<button type="button" class="gaborGrangerButton acceptable" data-gabor="' + responseCounter + '">' + response + '</button>');
        responseCounter++;
    });
    unacceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .gaborGrangerContainer').append('<button type="button" class="gaborGrangerButton unacceptable" data-gabor="' + responseCounter + '">' + response + '</button>');
        responseCounter++;
    });
     
    // click events
    var gaborPreviousPosition;
    var firstClick = true;
    var responsesSep = '';
    $('#[% QuestionName() %]_div .gaborGrangerButton').click(function(){
        if (firstClick) {
            $('#[% QuestionName() %]_Price').val('');
            $('#[% QuestionName() %]_StartPrice').val(prices[gaborPosition - 1]);
            $('#[% QuestionName() %]_Responses').val('');
            firstClick = false;
        }
         
        $('#[% QuestionName() %]_Responses').val($('#[% QuestionName() %]_Responses').val() + responsesSep + $(this).data('gabor'));
        responsesSep = ',';
     
        var acceptable = $(this).is('.acceptable');
        if (gaborPosition == 1 && !acceptable) {
            finishGaborGranger('[% QuestionName() %]', noneResponse);
        }
        else if ((gaborPosition == prices.length && acceptable) ||
            (acceptable && gaborPreviousPosition && gaborPosition < gaborPreviousPosition)) {
            finishGaborGranger('[% QuestionName() %]', prices[gaborPosition - 1]);
        }
        else if ((gaborPosition == prices.length && !acceptable && gaborPreviousPosition) ||
            (gaborPosition == 1 && acceptable && gaborPreviousPosition) ||
            (!acceptable && gaborPreviousPosition && gaborPosition > gaborPreviousPosition)) {
            finishGaborGranger('[% QuestionName() %]', prices[gaborPreviousPosition - 1]);
        }
        else {
            gaborPreviousPosition = gaborPosition;
            gaborPosition += acceptable ? 1 : -1;
            updateGaborGranger('[% QuestionName() %]', prices[gaborPosition - 1]);
        }
    });
})
 
function updateGaborGranger(question, price) {
    var qdiv = $('#' + question + '_div');
    $(qdiv).fadeOut(500, function(){
        $(qdiv).find('.gaborGrangerPrice').text(price);
        var bgColor = $(qdiv).css('background-color');
        $(qdiv).css('background-color', 'lightcyan');
        $(qdiv).fadeIn(1000, function(){
            $(qdiv).css('background-color', bgColor);
        });
    });
}
 
function finishGaborGranger(question, price) {
    $('#' + question + '_Price').val(price);
    var qdiv = $('#' + question + '_div');
    $(qdiv).find('.gaborGrangerButton').prop('disabled', true).css('cursor', 'default');
    $(qdiv).find('.gaborGrangerFinished').show();
}
</script>



This is added in theAdvanced settings Customer Javascript for the question

if (!SSI_GetValue('[% QuestionName() %]_Price')) {
    strErrorMessage = 'A response is required.';
}
You can set the variables to not require a response in your question settings.
I found the issue. They were not being hidden. The problem I have now is that those variables are not recording anything. I see values in the Price, StartPrice and Responses, but not the individual price variables. What is missing?
Is there a reason you need to stitch together the new code with the old one?  Why not just use the new code from Jay's link?
I didn't know that was "newer" than this one and  couldn't detect the differences except for the variable naming. So now I used the "new," but I only see the recording of the values for the answers for the prices seen between the first and the last. I need to record of the scale values for all the prices seen. How do I do that? I had to add Price5 and Price6 variables since I have 6 prices, but I don't seem to be able to record anything for Price5 and Price6, even if shown
Here is my code:
<input name="[% QuestionName() %]_Price" id="[% QuestionName() %]_Price" type="hidden" value=""/>
<input name="[% QuestionName() %]_StartPrice" id="[% QuestionName() %]_StartPrice" type="hidden" value=""/>
<input name="[% QuestionName() %]_Responses" id="[% QuestionName() %]_Responses" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response1" id="[% QuestionName() %]_Response1" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response2" id="[% QuestionName() %]_Response2" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response3" id="[% QuestionName() %]_Response3" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response4" id="[% QuestionName() %]_Response4" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response3" id="[% QuestionName() %]_Response5" type="hidden" value=""/>
<input name="[% QuestionName() %]_Response4" id="[% QuestionName() %]_Response6" type="hidden" value=""/>
 
<div class="priceLadderingContainer"></div>
 
<style>
#[% QuestionName() %]_div {
    transition: background 0.5s linear;
}
 
.priceLadderingFinished {
    display: none;
}
 
.priceLadderingButton
{
    display: block;
    background-color: white;
    border: 1px solid green;
    width: 250px;
    margin: 20px 0px;
    padding: 10px;
    font-family: "Open Sans";
    font-size: medium;
    font-weight: 400;
    cursor: pointer;
}
}
 
.priceLadderingButton:hover {
    background-color: #CDCDCD;
}
</style>
 
<script>
$(document).ready(function(){
    var prices = ['$100', '$125', '$150', '$175', '$200', '$225'];
    var startPosition = 'random'; // the position of the first price to show, or 'middle' to automatically select middle price, or 'random' to select random starting price
    // Price laddering experiments are typically performed with either two acceptables and three unacceptables, or one acceptable and one unaccpetable (yes and no)
    var acceptableResponses = ['Extremely likely', 'Very likely'];
    var unacceptableResponses = ['Somewhat likely', 'Not very likely', 'Not at all likely'];
    var noneResponse = 'NONE';
     
    // initialize price
    var priceLadderingPosition;
    switch (startPosition.toString().toLowerCase()) {
        case 'middle':
            priceLadderingPosition = Math.ceil(prices.length / 2);
            break;
        case 'random':
            priceLadderingPosition = Math.ceil(Math.random() * prices.length)
            break;
        default:
            priceLadderingPosition = Number(startPosition);
            break;
    }
    $('#[% QuestionName() %]_div .priceLadderingPrice').text(prices[priceLadderingPosition - 1]);
     
    // reset individual responses
    $('#[% QuestionName() %]_Response1').val('');
    $('#[% QuestionName() %]_Response2').val('');
    $('#[% QuestionName() %]_Response3').val('');
    $('#[% QuestionName() %]_Response4').val('');
    $('#[% QuestionName() %]_Response5').val('');
    $('#[% QuestionName() %]_Response6').val('');
     
    // create buttons
    var responseCounter = 1;
    acceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .priceLadderingContainer').append('<button type="button" class="priceLadderingButton acceptable" data-priceladdering="' + responseCounter + '">' + response + '</button>');
        responseCounter++;
    });
    unacceptableResponses.forEach(function(response){
        $('#[% QuestionName() %]_div .priceLadderingContainer').append('<button type="button" class="priceLadderingButton unacceptable" data-priceladdering="' + responseCounter + '">' + response + '</button>');
        responseCounter++;
    });
     
    // click events
    var priceLadderingPreviousPosition;
    var firstClick = true;
    var responsesSep = '';
    $('#[% QuestionName() %]_div .priceLadderingButton').click(function(){
        if (firstClick) {
            $('#[% QuestionName() %]_Price').val('');
            $('#[% QuestionName() %]_StartPrice').val(prices[priceLadderingPosition - 1]);
            $('#[% QuestionName() %]_Responses').val('');
            firstClick = false;
        }
         
        var resp = $(this).data('priceladdering');
        $('#[% QuestionName() %]_Responses').val($('#[% QuestionName() %]_Responses').val() + responsesSep + resp);
        responsesSep = ',';
         
        $('#[% QuestionName() %]_Response' + priceLadderingPosition).val(resp);
     
        var acceptable = $(this).is('.acceptable');
        if (priceLadderingPosition == 1 && !acceptable) {
            finishPriceLaddering('[% QuestionName() %]', noneResponse);
        }
        else if ((priceLadderingPosition == prices.length && acceptable) ||
            (acceptable && priceLadderingPreviousPosition && priceLadderingPosition < priceLadderingPreviousPosition)) {
            finishPriceLaddering('[% QuestionName() %]', prices[priceLadderingPosition - 1]);
        }
        else if ((priceLadderingPosition == prices.length && !acceptable && priceLadderingPreviousPosition) ||
            (priceLadderingPosition == 1 && acceptable && priceLadderingPreviousPosition) ||
            (!acceptable && priceLadderingPreviousPosition && priceLadderingPosition > priceLadderingPreviousPosition)) {
            finishPriceLaddering('[% QuestionName() %]', prices[priceLadderingPreviousPosition - 1]);
        }
        else {
            priceLadderingPreviousPosition = priceLadderingPosition;
            priceLadderingPosition += acceptable ? 1 : -1;
            updatePriceLaddering('[% QuestionName() %]', prices[priceLadderingPosition - 1]);
        }
    });
})
 
function updatePriceLaddering(question, price) {
    var qdiv = $('#' + question + '_div');
    $(qdiv).find('.priceLadderingPrice').text(price);
    $(qdiv).css({
        'transition': 'background-color 0.5s ease-in-out',
        'background-color': '#e6ffff'
    });
    var interval = setInterval(function(){
        $(qdiv).css({
            'transition': 'background-color 1s ease-in-out',
            'background-color': 'transparent'
        });
        clearInterval(interval);
    }, 500);
}
 
function finishPriceLaddering(question, price) {
    $('#' + question + '_Price').val(price);
    var qdiv = $('#' + question + '_div');
    $(qdiv).find('.priceLadderingButton').prop('disabled', true).css('cursor', 'default');
    $(qdiv).find('.priceLadderingFinished').show();
}
</script>
You missed something in lines 8 and 9.
Good catch. Thanks!
...