DEV Community

Cover image for RockOn pt 5: Filtering Data
dianakw8591
dianakw8591

Posted on • Edited on

RockOn pt 5: Filtering Data

This is the fifth post in my series about building RockOn. Get the full background starting here.

We've made it to the point in RockOn where the user has entered some of their ascents and has a couple data points saved. What now? Well, a bunch of graphs and filter options of course!

stats page view of RockOn

In broad strokes, a user's entries are fetched from the database and passed through a filtering function that is controlled by the user. That filtered array is then passed to the graphs and logbook so that only ascents that meet the selected criteria are displayed. Here's a better view of all those filter options:

filter options

There's a lot of climbing terminology in there but don't let that dissuade you. You can think of these options just like filters for a house search on Zillow: are you looking to rent or buy? What square footage and number of bathrooms do you want? Is in-unit laundry a must have? Yes, rock climbing is just like searching for your next apartment.

Let's break this down and start from the beginning. When a user logs in, all of their entries are fetched from the back end and saved in state to be passed to other components as needed. The entries come as an array of objects - here's an example of an entry object:

{
 climb: {
  area_array: ["International", "Europe", "Spain", 
   "Catalonia", "…],
  climb_id: 110372,
  full_type: "Sport",
  key_type: "Sport",
  name: "La Discórdia",
  numeric_rating: 13,
  rating: "5.10"
 }
 beta: "",
 notes: "very cold and raining...",
 outcome: "Onsight",
 partners: "",
 pitches: 1,
 rack: "",
 start_date: "2019-11-23",
 style: "Lead",
}

This object has all the information needed for the graphs and logbooks, and most of the keys are filterable. We'll walk through the process of filtering by outcome - the many ways that climbers describe whether they fell off the climb at some point, or whether they held on until the top. Again, don't worry about the terminology too much - just know that only one outcome can be applied to a given entry.

In the filter options, everything begins as selected. In the case of outcomes, that means that entries with any of the nine outcome options attached will be displayed.

outcome filter options

All of the filter options are just part of a controlled form!

<Form.Group >
  <Form.Label>Select outcomes:</Form.Label>
  <div key={`inline-checkbox`} className="mb-3">
  {Object.keys(outcome).map(key => (
    <span className={mappingRopeOutcomes[key]} key={key}>
      <Form.Check inline
       type="checkbox"
       onChange={handleOutcomeToggle}
       label={key}
       name={key}
       checked={outcome[key]}
      />
    </span>
  ))}
  </div>
</Form.Group>

The span surrounding each check box is simply for styling purposes, but the functionality of the form follows the same pattern as discussed in last week's post. Any change to the form is handled by the handleOutcomeToggle function:

const handleOutcomeToggle = ({ target }) => setOutcome(s => ({ ...s, [target.name]: !s[target.name] }));

This function in turn updates state using setOutcome and simply flips the boolean from what it was before. The Stats component where these functions live is functional, so I am using the useState hook to save and set my outcomes:

  const [outcome, setOutcome] = useState({
    'Onsight': true,
    'Flash': true,
    'Redpoint': true,
    'Pinkpoint': true,
    'Tronsight': true,
    'No Falls': true,
    'TR Attempt': true,
    'Repeat': true,
    'Attempt': true
  })

Rather than having a useState hook for every outcome possibility like this:

const [onsight, setOnisght] = useState(true)
...

I can use one hook that handles the entire object, and update each key of the object appropriately by looking at the target.name that comes from my form. Nifty!

So following this flow, if I don't want to see any of my climbs with an outcome of 'Onsight', I unselect that checkbox, which changes the value of outcome.Onsight to false.

Next I need a way to use state to filter out any climbs from the entry array that have an outcome of Onsight. I'm sure there are lots of ways to solve this filtering problem (and quite a few different helper function to use, both built into Javascript or from outside libraries like Lodash) but I'll share my original solution here. In the first step I built an array of outcomes that only included the ones selected, where the value is true.

  const outcomeArray = Object.keys(outcome).filter(key => outcome[key])

Here Object.keys(outcome) is an array of the outcome keys. The filter removes any keys that have false values in the original outcome object.

Using outcomeArray, I then filtered my entries array such that only entries with outcomes included in the outcomeArray are kept:

entries.filter(e => outcomeArray.includes(e.outcome))

The filtered array is then passed to the graphs and logbook components, so that as a user select filters the displays are updated in real time.

This is the same flow I followed for all of the filter options - dates, grades, style, etc. Some of those data types took a bit more manipulation or a slightly different method to achieve the filter but the concept is the same. Finally, by stringing the filter functions together, I was able to apply all of the filters at once. Here's that final function:

  const filtered = entries
    .filter(e => moment(e.start_date) >= moment(start))
    .filter(e => moment(e.start_date) <= moment(end))
    .filter(e => typeArray.includes(e.climb.key_type))
    .filter(e => e.climb.key_type === "Boulder" || styleArray.includes(e.style))
    .filter(e => outcomeArray.includes(e.outcome))
    .filter(e => e.climb.numeric_rating >= grade.low && e.climb.numeric_rating <= grade.high)

To plot the filtered array even more manipulation is needed - I'll dive into that next week in part 6. Have a great weekend and thanks for reading!

Top comments (0)