0

Unsolved

Last updated: 4:15 p.m.

Where I'm at

I have three categories for people to choose: Gender, Age, Ethnicity. Each with their own set of options Male/Female; under 35, 36-64, 65+; White, Black, Aboriginal, Metis and Asian, respectively.

When one of the options is clicked, the number should subtract from var total (which is the number of MLAs). I've set global variables for the numbers, which changes the span.number located in my index.html file to say, "There are x-number of MLAs that fit in your demographic."

Where I'm stuck

a) I'm looking for a more efficient way of doing the math in Javascript, right now it's pretty janky. If you look at the Gender option in scripts.js, I've created a variable var resultMale = total - female but it gets a lot less pretty when generating span.number relies on the result of the previously selected option.

b) I've tried using the pipe boolean, but I'm unclear about how in the second category "Age", which includes var resultLow = resultMale || resultFemale -(middle + high) how it will know what has been clicked/chosen in category "Gender" so that it can do the proper math.

scripts.js

$(function() {

    // Numbers
    var total = 56
    var male = 41;
    var female = 15;
    var low = {}
    var middle = {}
    var high = {}
    var white = 41
    var black = 0;
    var aboriginal = null;
    var metis = null;
    var asian = null;

    // MLAs
    var MLAs = [
      {
        "Name": "Nancy Allan",
        "Age": 62,
        "Constuency": "St. Vital",
        "Party": "NDP",
        "Gender": "Female",
        "Ethnicity": "White"
      },
      {
        "Name": "James Allum",
        "Age": null,
        "Constuency": "Fort Garry-Riverview",
        "Party": "NDP",
        "Gender": "Male",
        "Ethnicity": "White"
      },
      {
        "Name": "Rob Altemeyer",
        "Age": null,
        "Constuency": "Wolseley",
        "Party": "NDP",
        "Gender": "Male",
        "Ethnicity": "White"
      }]

    // Option #1: Gender
    $( ".G1" ).click(function() {
        $(".G2").toggleClass("disabled");
        $(".headshot").not(".Female").toggleClass("opacity");

        var resultMale = total - female;
        $(".number").html(resultMale);
    });

    $( ".G2" ).click(function() {
        $(".G1").toggleClass("disabled");
        $(".headshot").not(".Male").toggleClass("opacity");

        var resultFemale = total - male
        $(".number").html(resultFemale);
    });

    // Option #2: Age
    $( ".A1" ).click(function() {
        $(".A2").toggleClass("disabled");
        $(".A3").toggleClass("disabled");
        $(".Low").toggleClass("show");

        var resultLow = resultMale || resultFemale - (middle + high);
        $(".number").html("?");
    });

    $( ".A2" ).click(function() {
        $(".A1").toggleClass("disabled");
        $(".A3").toggleClass("disabled");
        $(".Medium").toggleClass("show");

        var resultMiddle = resultMale || resultFemale - (low + high);
        $(".number").html("?");
    });

    $( ".A3" ).click(function() {
        $(".A1").toggleClass("disabled");
        $(".A2").toggleClass("disabled");
        $(".High").toggleClass("show");

        var resultHigh = resultMale || resultFemale - (low + middle);
        $(".number").html("?");
    });

    // OLD
    $( ".E1" ).click(function() {
        $( ".White" ).toggleClass("show");

        var resultWhite = resultLow || resultMiddle || resultHigh - (black + aboriginal + metis + asian);
        $(".number").html(resultWhite);
    });

    $( ".E2" ).click(function() {
        $( ".Black" ).toggleClass("show");

        var resultBlack = resultLow || resultMiddle || resultHigh - (white + aboriginal + metis + asian);
        $(".number").html("0");
    });

    $( ".E3" ).click(function() {
        $( ".Aboriginal" ).toggleClass("show");

        var resultAboriginal = resultLow || resultMiddle || resultHigh - (white + black + metis + asian);
        $(".number").html(null);
    });

    $( ".E4" ).click(function() {
        $( ".Metis" ).toggleClass("show");

        var resultMetis = resultLow || resultMiddle || resultHigh - (white + black + aboriginal + asian);
        $(".number").html(null);
    });

    $( ".E5" ).click(function() {
        $( ".Asian" ).toggleClass("show");

        var resultAsian = resultLow || resultMiddle || resultHigh - (white + black + aboriginal + metis);
        $(".number").html(null);
    });

    // Option #3: Ethnicity
    // $("input[name='ethnicity']").on("change", function() {
    //     var $checkedbox = $("input[name='ethnicity']:checked");

    //  if($checkedbox.length >= 2)
    //  {
    //         var $uncheckedbox = $("input[name='ethnicity']:not(:checked)");
    //      $.each($uncheckedbox,function() {
    //          $(this).attr("disabled", "disabled");
    //      });
    //  }
    //     else
    //     {
    //         $("input[name='ethnicity']").removeAttr("disabled");
    //     }
    // });

    // Shows a popup with MLA information
    $(".headshot").click(function(){
        var idx = $(this).index() - 1;

        $(".tooltip").fadeIn("slow");
        $(".tooltipName").html(MLAs[idx].Name);
        $(".tooltipParty").html(MLAs[idx].Party);
        $(".tooltipConstuency").html(MLAs[idx].Constuency);
        $(".tooltipEthnicity").html(MLAs[idx].Ethnicity) + ",";
        $(".tooltipAge").html(MLAs[idx].Age);
    });

    // Bounce and show result
    $(".rect").click(function(){
            console.log("Bounce test");
           $(".others").fadeIn("slow");
           $(".others").effect( "bounce",
            {times:3}, 600 );
        });

    // This hides the footer on click
    $(".crossContainer").click(function(){
        $("footer").slideUp("slow", function(){
            console.log("No feedback makes us sad.");
        });
    });

    // This hides the credits
    $(".credits").hide();
});

index.html (includes span.number)

<!-- Three options readers can click -->
        <section class="interactive clearfix">
            <section class="selection" id="selection">
                <div class="gender">
                    <p class="category">Gender</p>
                    <div class="options">
                        <p class="rect G1">Male</p>
                        <p class="rect G2">Female</p>
                    </div><!-- /.options -->
                </div><!-- /.gender -->

                <div class="age">
                    <p class="category">Age</p>
                    <div class="options">
                        <p class="rect A1">Under 35</p>
                        <p class="rect A2">36-64</p>
                        <p class="rect A3">65+</p>
                    </div><!-- /.options -->
                </div><!-- /.age -->

                <div class="ethnicity">
                    <p class="category">Ethnicity<span>*<span></p>
                    <div class="options">
                        <p class="rect E1 E">White</p>
                        <p class="rect E2 E">Black</p>
                        <p class="rect E3 E">Aboriginal</p>
                        <p class="rect E4 E">Metis</p>
                        <p class="rect E5 E">Asian</p>
                    </div><!-- /.options -->
                </div><!-- /.ethnicity -->
</section>

        <section class="others">
            <h2>There are <span class="number">56</span> MLAs that fit in your demographic</h2>
            <figcaption class="special">(Does not include the single vacant seat for the Pas.)</figcaption>
        </section>
Andrew Nguyen
  • 1,276
  • 3
  • 17
  • 37

2 Answers2

1

If I've understood it correctly the following demo should do what you want. You can find the code below and here at jsFiddle. (old version of the jsfiddle, new version see edit below.)

It's basically doing what andi suggested. For "calculation" I have used UnderscoreJS to find the matching key/value pair for the currently clicked selection with the where method. Then it's easy to get the total of the selection with the length.

In the demo I have done it for male, female and ethnic. It should work exactly like that for the other options.


Edit 07.02.2015

I have improved the code. Now you can activate multiple filters at your list. Also the subtraction is removed because the filtered list holds always the number of items with the length property.

The code is working but there are still some points to improve:

  • removeFilter needs to be changed to easily remove the filter with-out specifing the filter parameter
  • setFilter improvment: if ( index == -1 ) { is not really required because the filters are always removed before adding a new filter.
  • add reset buttons for each radio group. At the moment, there is only a "reset all" button.
  • Using AngularJS for updating the DOM would be better because the script is always recreating the DOM at every filter change.
  • Maybe with AngularJS you could check if there are filters for ng-repeat to do a similar behavior.

It's also likely that I haven't tested if any combination of the filter is working correctly, because I have only three list items. So you have to do more tests with the code.

I updated the code here to the latest version and here is the jsFiddle.


More code explanations 16.02.2015

The functions refreshList and setTotal are used to update the DOM.

The filter functions gender, ethnicity and age are returning the new array after the filter is apply.

The array activeFilters is storing the currently selected filters. It stores the filter functions so they can be called by iterating through this array. More details to this later.

The setFilter method is setting the filter. It adds the filter to the activeFilters array and then it applies the filter.

The applyFilter function is probably the hardest to understand. It iterates through each item of the activeFilters array. If you have two active filters (here gender & ethnic) the array activeFilters will look like this:

activeFilters = [{
   method: function(array, gender){...},
   param: 'male'
   }, {
   method: function (array, ethnic){...},
   param: 'white'
   }];

With activeFilters[0].method(MLAs,'male') you could call the gender filter manually. That's what the $.each loop is doing inside the loop this is {method: ..., param: ...}. So with this.method(filtered, this.param) the filter will be applied to the variable filtered. After the loop every active filter will be applied to the array.

var $total = $('#total');
var $MLA_List = $('#MLA_List');

// MLAs
var MLAs = [{
    "Name": "Nancy Allan",
        "Age": 62,
        "Constuency": "St. Vital",
        "Party": "NDP",
        "Gender": "Female",
        "Ethnicity": "White"
}, {
    "Name": "James Allum",
        "Age": 34,
        "Constuency": "Fort Garry-Riverview",
        "Party": "NDP",
        "Gender": "Male",
        "Ethnicity": "Black"
}, {
    "Name": "Rob Altemeyer",
        "Age": 36,
        "Constuency": "Wolseley",
        "Party": "NDP",
        "Gender": "Male",
        "Ethnicity": "White"
}];

var filteredMLAs = MLAs.slice(0); // copy MLAs
var total = filteredMLAs.length;

var refreshList = function () {
    var list = filteredMLAs;
    setTotal(list.length);

    $MLA_List.empty();
    $.each(list, function (index, value) {
        $MLA_List.append($('<li/>').text(list[index].Name));
    });
};
var setTotal = function (value) {
    $total.text(value);
};

// filter methods
var gender = function (array, gender) {
    //console.log('gender filter called!', gender);
    return _.where(array, {
        "Gender": gender
    });
};

var ethnicity = function (array, ethnic) {
    //console.log('ethnic filter called!', array, ethnic);
    return _.where(array, {
        "Ethnicity": ethnic
    });
};

var age = function(array, ageRange) {
    //under 35, 36-64, 65+
    return _.filter(array, function(MLA) {
        //console.log(MLA.Age);
        switch(ageRange) {
            case 35:
                return ( MLA.Age <= 35 );
            case 36:
                return ( MLA.Age >= 35 && MLA.Age <= 64);
            case 65:
                return ( MLA.Age >= 65 );
        };
        return false;
    });
};

var activeFilters = [];
var setFilter = function (method, param) {
    var newFilter = {
        method: method,
        param: param
    };

    var matchedFilter = _.find(activeFilters, newFilter),
        index = activeFilters.indexOf(matchedFilter);
    
    if ( index == -1 ) {
        activeFilters.push(newFilter);
    } 
    
    applyFilter();
};

var removeFilter = function(method, param) {
    var filter = {
        method: method,
        param: param
    };
    
    var index = activeFilters.indexOf(_.find(activeFilters, filter));
    
    if (index > -1) {
        activeFilters.splice(index, 1);
    }
    
    applyFilter(); // re-apply filter to update list
};

var applyFilter = function () {
    var filtered = MLAs.slice(0);
    $.each(activeFilters, function () {
        filtered = this.method(filtered, this.param);
    });
    filteredMLAs = filtered ? filtered: [];
    refreshList();
};

$('#Male, #Female').click(function () {
    //console.log(this.id);
    removeFilter(gender, this.id=='Male'? 'Female': 'Male'); // remove not active filter
    setFilter(gender, this.id);
});

$('#White, #Black').click(function () {
    //console.log(this.checked);
    if ( this.checked )
        setFilter(ethnicity, this.id); //'White');
    else 
        removeFilter(ethnicity, this.id); //'White');
});

$('.Age').click(function() {
    removeFilter(age, 35); // improvement of remove filter required, e.g. remove all age filters
    removeFilter(age, 36);
    removeFilter(age, 65);
    setFilter(age, parseInt(this.value));
});

$('#reset').click(function(){
    //console.log('reset form');
    activeFilters = [];
    $(':checkbox, :radio').attr('checked', false);
    applyFilter();
});

$(function () {
    refreshList();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
There are <span id="total"></span> MLAs matching.
<ul id="MLA_List"></ul>
<hr/>
<p>click to filter:</p>
<input type="radio" id="Male" name="gender">male</input>
<input type="radio" id="Female" name="gender">female</input>
<input type="checkbox" id="White">white</input>
<input type="checkbox" id="Black">black</input>
<input type="radio" class="Age" name="age" value="35">under 35</input>
<input type="radio" class="Age" name="age" value="36">36-64</input>
<input type="radio" class="Age" name="age" value="65">65+</input>
<button id="reset">reset</button>
<!-- <a href="#" id="male">male</a>
<a href="#" id="female">female</a>
<a href="#" id="white">white</a>
-->
AWolf
  • 8,062
  • 4
  • 29
  • 36
  • This is an amazing answer, I was hoping that you could explain a little more about your thought process, what the code is doing. Likewise, I was wondering if "checkboxes" are the only way to do this as I currently have divs/rectangles that are clicked? – Andrew Nguyen Feb 16 '15 at 22:27
  • It's also possible to use buttons/divs instead of the checkboxes, you'll only have to re-code the behavior differently. But I would use standard elements and do CSS styling on them. That's easier. Please have a look at this [SO question](http://stackoverflow.com/questions/4148499/how-to-style-checkbox-using-css) for an example how to style checkboxes. I'll update my answer to give you more details to the code. – AWolf Feb 16 '15 at 22:47
  • Looking into styling checkboxes/ radio buttons at the moment. – Andrew Nguyen Feb 16 '15 at 23:48
  • Super close, I have everything working except for the "Ethnicity" part, which needs two checkboxes to be selected max. Think you could help out with this? – Andrew Nguyen Feb 22 '15 at 21:39
  • I have had a look at the demo and I think for the ethnic part it would be better to have also radio buttons for it. Because I think it makes no sense to apply both filters as it would always return no match. Or where do you need help? – AWolf Feb 22 '15 at 21:49
  • I'm basically looking have a user be able to select two ethnicities maximum. Curious why you think radio buttons might work better? – Andrew Nguyen Feb 22 '15 at 21:57
  • Oh I see, sorry, I have thought there are only two options to select, but there are more to select. I'll add the limit to two options in the fiddle. – AWolf Feb 22 '15 at 22:01
  • OK, I've added the limit function in the [fiddle](http://jsfiddle.net/awolf2904/1rukefyr/). (same link as in my answer.) – AWolf Feb 22 '15 at 22:22
  • There's one last thing the numbers are not adding up properly on mine, I've made a JSFiddle so you can see: http://jsfiddle.net/ghkz4hzh/ – Andrew Nguyen Feb 23 '15 at 04:01
  • You haven't added Jquery & Underscore scripts properly. See this [fiddle](http://jsfiddle.net/ghkz4hzh/1/). – AWolf Feb 23 '15 at 13:56
  • I'm unclear on what you changed between the two JSFiddles, in terms of the jQuery and Underscore scripts. – Andrew Nguyen Feb 23 '15 at 20:10
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/71524/discussion-between-awolf-and-andrew-nguyen). – AWolf Feb 23 '15 at 20:11
0

How about recalculating number each time a category is clicked by looping through every MLA and incrementing a counter? I think you're complicating it by trying to use the previous calculations as a starting point, but it's easier to start fresh every time.

andi
  • 6,270
  • 1
  • 15
  • 43