The Bob Graham 24 Hour Club

Data API

This file explains how to use the API to embed data about Club members in a website. You will need to have an understanding of Javascript modules, Promises and template strings when looking at the examples.

Import the API file:

Since the file you will import is a module you will need a slightly different setting up of your code compared to traditional Javascript. If you don't set the type to “module” then things won't work. It's also worth noting that doing this forces “strict mode” so your code will have to be compliant with the strictures imposed by that.

<script src="yourscript.js" type ="module"></script>

Then as part of the first block of code, there can’t be anything before imports, in yourscript.js put:

import Bgr from "http://www.bobgrahamclub.org.uk/api/bgr.js";

The module does not define a namespace so it’s up to you to define one, in this case it’s “Bgr”.

Function Calls

memberData

The main API function is “memberData”, this returns the data that you’ll use in a Promise. The function takes an optional configuration object with the following elements. The first three are ANDed together meaning if you set more than one then all the conditions must be true. i.e ({club: "dark peak", year: "1980"} would return those members of Dark Peak FR who succeeded in 1980.


memberData({
	// name: default value, comment
	// filters
	club: "", //This is the club name of a member at the time they did the Round not their current club. 
			  // Case is not important.

	name: "", //This is the family name (surname) of a member. Case is not important.

	year: "", //The year of a successful round. 
              // For a decade leave off the last digit, i.e. "199" would return members in all years 1990 - 1999

	// additional rounds
	members: [], //an array of membership numbers. Defaults to an empty array.

	second: false, //determines if second (non qualifying) rounds should be included.

	// behavioural/sorting
	sortCol: "mem", //sets the column to order the table by.  
	//If you set second to true then it's a good idea to set this to "date" otherwise second 
	//rounds can appear before that person's qualifying round. Note that you shouldn't use "name"
	//for this as that column is a combination of given name and family name, use "fn" (family name) instead.

	sortOrder: "asc" // For most purposes ascending order is what you will 
	//need but set this to 'desc' to reverse that.

	debug: false // set to true to get console logging.
});

Information returned

The data returned consists of an array of objects containing the following fields:


{
	member: // the membership number. For second rounds this is inside angle brackets - <52>
	name: // the full name, i.e. forename surname.
	age: // their age on the day that they did their round.
	gender: // Male or Female.
	date: // the date of their round.
	time: // the time taken to do the round.
	direction: // clockwise or anti-clockwise.
	route: // Sergeant Man or High Raise first.
	nationality: // obvious
	club: // obvious. Note that this is the club the person belonged to when they did the round, 
		  // we don't monitor changes to club membership.
	prev: // The number of previous attempts (only sinc 2014)

	// There are additional fields that in some cases are used to generate one of the above.
	// gn, fn and died become "name" for example
	record: // rounds that were part of the progression of the record.
	lr: // rounds that were part of the progression of the women's record.
	gn: // given name
	fn: // family name - combined with gn into "name".
	died: // year of death, actually appended to name in the form (died: 2010)
	orig: // For second rounds this is the original membership number.
	season: // summer, winter or mid-winter.
	opt: // This is the basis for "route" and depends on dir as to what text appears in "route"
	dir: // This is the numerical basis for "direction"
	weekNum: // the ISO week number in which the round took place
}

Here𔄩s an example:


{
	age: "38",
	club: "Ambleside Athletics Club",
	date: "1975-06-21",
	died: "0",
	dir: "C",
	direction: "CW", // derived from "dir"
	fn: "Astles",
	gender: "M",
	gn: "Bob",
	lr: "0",
	mem: "35",
	member: "35", // derived from "mem"
	name: "Bob Astles ", // derived from "gn", "fn" and "died"
	nationality: "British",
	opt: "0",
	orig: "0",
   	prev: "0",
	record: "0",
	route: "SM -> HR", // derived from "opt" and "dir"
	season: "summer",
	time: "23:06",
	weekNum: "25",
}

Note that for debugging purposes if you set the debug property to true you will see log messages in the browser console.

assists


assists({
	members: [] // An array of comma separated membership numbers.
});

This call returns how an individual has helped other members

Information returned

The data returned consists of an array of objects containing the following fields:


{
	leg_five: // the number to times help has been provided on each leg.
	leg_four:
	leg_one:
	leg_three:
	leg_two:
	mem_num: // membership number
	name: // full name
	road: // the number of times help provided at road crossings.
	total: // total number of members helped
}

Note that “total” isn't the sum of legs one to five but the number of individuals helped. If someone has paced legs one and two for just one person then leg_one = 1, leg_two = 1, total = 1.

Here’s an example:


{
	leg_five: "16",
	leg_four: "13",
	leg_one: "13",
	leg_three: "24",
	leg_two: "15",
	mem_num: "23",
	name: "Peter Dawes",
	road: "3",
	total: "49"
}

augmentData

If you wish to add your own data, reports, lists of helpers, links as an extra column then use this function.

Parameters

augmentData(
	data: [] // the array to augment.
	extra: [] // a sparse array, indexed by membership number, holding the data for the new column
	columnName: "" // the name of the new column. Should not be the same as an existing column.
)

The function returns the original array with each element having an extra property - “columnName”.

An example.


const target = document.getElementById("bgr_members");
const copy_target = document.getElementById("bgr_copyright");
const reports = [];
reports[170] = "<a href='jb_report.html'>Report</a>";
reports[1248] = "<a href='to_report.html'>Report</a>";

Bgr.memberData({club: "Ambleside"})
.then((text) => Bgr.augmentData(text, reports, "report"))
.then((text) => {
    createTable(target, 
        text,
        ['member', 'name', 'date', 'time', 'direction', 'route', 'report']);
    showCopyright(copy_target, Bgr.copyright());
})
.catch((error) => console.log(error));

Usage

The most likely scenario would be to set the club name in a call to memberData(). The name doesn't have to be complete, just sufficient that it can be distinguished from other clubs.

The name or club field can filter for text at the start or end of a name:

  • “ight” will find all those containing ight anywhere in their name.
  • “ ight” or “^ight” will find all those whose name begins with ight. I.e. start with a space or a caret ‘^’
  • “ight ” or “ight$” will find all those whose name ends with ight. I.e. end with a space or a dollar sign.

The year field uses a text comparison so if you wish to get a decade’s worth of members leave off the last digit.

Bgr.memberData({club: "^cumber", year: "199"});

This call will now return an array containing all those members of Cumberland Fell Runners (but not West Cumberland Fell Runners or West Cumberland Orienteering Club because of the caret) who have succeeded on the BGR during the years 1990 to 1999 inclusive ordered by membership number.

Bgr.memberData({club: "kesw", name: "bland"});

This call will return an array containing all those members of Keswick AC called “Bland”.

We can now do what we want by chaining “then()” calls. Here it's assumed that the client has a function “createTable” to perform that action. Note that you should also display the copyright notice next to the table, usually just beneath it.


const target = document.getElementById("bgr_members");
const copy_target = document.getElementById("bgr_copyright");

Bgr.memberData({club: "Ambleside"})
.then((text) => {
    createTable(target, text, ['member', 'name', 'date', 'time', 'direction', 'route']);
    showCopyright(copy_target, Bgr.copyright());
})
.catch((error) => console.log(error));

Note that internally the code has already extracted the text object from the Promise so you don’t need to do this:

.then((response) => response.text())

A more dynamic selection

This example assumes that there’s a drop down list of year values: 1980, 1981, etc. and will return just those club members from the selected year. Perhaps an advantage if your club has many BG successes.

See this page for “live” examples where you can make various selections.


const target = document.getElementById("bgr_members"),
    copy_target = document.getElementById("bgr_copyright"),
    curYear = new Date().getFullYear(),
    yearList = d.getElementById('year_list');
let yr = 1960;

yearList.options.length = 0;
yearList.append(new Option(curLang.init_val, -1));
for (yr = 1960; yr < curYear; yr += 1) {
    yearList.append(new Option(yr, yr));
}

yearList.addEventListener('change', function () {
    clearTable(target);
    Bgr.memberData({year: this.value, club: "Ambleside"})
        .then((text) => {
            createTable(target, text);
            showCopyright(copy_target, Bgr.copyright());
        })
        .catch((error));
});

A complete example

If you wish to include current members of your club who did their round whilst a member of a different club then you'll need to add a members array to the config object holding the membership numbers you wish to include.

The following code is that used by this page to create the table below. The call selects all those whose name ends in “and” who have done the round, including their second rounds, plus the BGC committee members, again including their second rounds. Second rounds have a gold background, name and club columns are left aligned. The sort column is set to "date" to put the second rounds in their correct order. The initial array has a couple of reports (dummy links BTW) added. The copyright notice is added as a caption to the table, the CSS places this at the table foot.


/*Copyright: Bob Graham Club 2021 */

// best read from bottom to top.

import Bgr from 'http://www.bobgrahamclub.org.uk/api/bgr.js';

const target = document.getElementById("bgr_members");
const links = [];
links[170] = "<a href='jb_report.html'>Report</a>";
links[1248] = "<a href='to_report.html'>Report</a>";


// 3. helper function
// applies styles to rows
function formatRows(test, styles, name) {
    let format = "";

    if (test) {
        const styleName = `rows_${name}`;

        if (styles[styleName]) {
            format = ` class='${styles[styleName]}'`;
        }
    }

    return format;
}

// 3. helper function
// applies styles to columns.
function formatCols(col, styles, names) {
    let format = "",
        result = "";

    names.forEach((name) => {
        const styleName = `cols_${name}`;

        if (styles[styleName]) {
            if (styles[styleName].includes(col)) {
                result += `${styles[name]} `;
            }
        }
    });

    if (result.length > 0) {
        format = ` class='${result}'`;
    }

    return format;
}

// 2.
// Example createTable function.
// The styles object contains a class for the table style used in the tables used for the membership
// table and the record data tables. This site's own CSS has classes for headers and 
// also stripes alternate rows for any table with that class.
// The sample_header class uses text-transform to capitalise the header text.
// The table_caption class sets the caption to beneath the table.
function createTable(elem, data, fields, caption, styles = {}) {
    const table = typeof styles.table !== undefined ? `class='${styles.table}'` : "";
    const header = typeof styles.header !== undefined ? `class='${styles.header}'` : "";
    const body = typeof styles.body !== undefined ? `class='${styles.body}'` : "";
    const row = typeof styles.row !== undefined ? `class='${styles.row}'` : "";
    const capt = typeof styles.caption !== undefined ? `class='${styles.caption}'` : "";
    let text = `<table ${table}><caption ${capt}>${caption}</caption>`;

    text += `<thead ${header}>`;
    fields.forEach((field) => text += `<th>${field}</th>`);
    text += `</thead><tbody ${body}>`;
    data.forEach((member) => {
    	// Give second rounds a gold background
        text += `<tr ${formatRows(member.member.startsWith('<'), styles, 'second')}>`;
        // left align name and club columns
        fields.forEach((field) => {
            text += `<td${formatCols(field, styles, ['left'])}>${member[field]}</td>`;
        });
        text += "</tr>";
    });
    text += "</tbody></table>";
    elem.innerHTML = text;
}

// 1.
// get the data
// add our own fields
// create the table
Bgr.memberData({name: "and$", // All those whose name ends in "and"
		members: [103, 139, 170, 324, 1248, 2067, 2309], // plus the committee members
		second: true, // and second rounds
		sortCol: 'date' // sort by date (ascending is default)
	})
    .then((data) => Bgr.augmentData(data, links, "report"))
    .then((data) => {
        createTable(target,
            data,
             // remove report if you don't need it
            ['member', 'name', 'date', 'time', 'direction', 'route', 'report', 'club'],
            Bgr.copyright(),
            {
                table: "bgr_table ten_block", // ten_block gives a stronger bottom border to every tenth row.
                header: "bgr_header",
                caption: "bgr_caption",
                rows_second: "second_round", // Give second rounds a gold background
                left: "cell_left",
                cols_left: ['club', 'name'] // left align name and club columns
            });
    })
    .catch((error) => console.log(error));


The above call results in the following table.

The table uses a separate CSS file from the main site, it's deliberately “different”, so if you want to be really lazy you can just link that and copy the above code into a local JavaScript file and everything will just work! You’d just have to:

  • Link to the CSS file at “http://www.bobgrahamclub.org.uk/api/bgr.css” or copy it and change the colours, etc to suit your site
  • Replace the name: “and$” property with your club name.
  • Remove the members, second and sortCol properties.
  • Replace “Bgr.augmentData(data)” with just “data” and remove the “report” column if you don’t need to add anything.

Debugging

You can turn on some basic logging by setting the debug property to true in the call to memberData. This will appear in the console log that is enabled in your browser by turning on the developer tools. The debug shows: what the code thinks you’ve asked it to do; the amount of data it starts with; the number of items the options result in.

It’s worth starting off with one of the above examples since they are known to work. As a rough progression we’d suggest the following:

  • {club: “your club name”}
  • {club: “^your club name”}, fix the name to the start of the search term.
  • {club: “your club name”, year: “a known value”}, i.e. choose a year you know someone from your club did the round
  • {club: “your club name”, name: “smith”}, again someone’s name you know has done the round.
  • {club: “your club name”, members: [1,2,3]}
  • {club: “your club name”, members: [1,2,3], second: true}
  • {club: “your club name”, members: [1,2,3], second: true, sortCol: ‘date’}

Once you’ve verified that everything is working using one of the above examples then tweek the call to suit your needs.

The search for club name simply looks for that text in the club𔄩s name but see the first example higher on this page for how to fix the search term to the start or end of the name. Something like “harriers” or “fell” will return a lot of entries!

You also need to be aware of caching issues when developing, sometimes you need to force the browser to get the file with your changes. Fortunately the developer mode of modern browsers bust the cache as a default action when you refresh but sometimes you may need a “hard” reload.

The code is based on the code behind the members page so if that page works then first check to see if the request has been blocked for security reasons. The console in the developer bar/window of your browser is the place to look.

There’s a test page to let you play around with various combinations of values that can be passed to the code.

Summary

Hopefully the above will help you get started in embedding “live” BGR data on your website. The data that this code uses is updated around the end of each year unless there’s a number of errata to publish. If anything isn’t clear then contact the Membership Secretary, details on the contacts page.