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 "";

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

Function Calls


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.

	// 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.

	// predefined filters
	filtered: "", // Allows a user defined filtering function to be passed.

	// 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.

	// debugging
	debug: "none", // set to "console" to get console logging; "file" to log to file; "console file" will do both.

	// the path to your logging directory
	logPath: "",
	// the filename you wish to use.
	filename: "log_errors.txt" // the default is log_errors.txt in the root folder of your site.

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 since 2014)
	postcode: // The first part of the postcode before the space.

	// 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

The filtered option may be used to pass a function to filter the data set in a different way to what may be achieved by the built-in options. This function is passed to the standard JS filter function which will run it against every element in the array. If this option is set then any other options such as name or club are ignored. If the value passed in is not a function then an error is raised.

	// filter for Kendal based clubs
	(({club}) => club.startsWith('Kendal') || club.startsWith('Helm'))
	// get all those in their 50s
	(({age}) => age > 49 && age < 60)
	// get all men in their 50s
	(({age, gender}) => gender === "M" && age > 49 && age < 60)
	// get all anticlockwise rounds that visited Sergeant Man first
	(({dir, opt}) => dir === "A" && opt === "1")

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",
	postcode: "LA22",
   	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 "console" you will see log messages in the browser console.


	members: [] // An array of comma separated membership numbers.

This call returns how an individual has helped other members

Information returned


	leg_five: // the number to times help has been provided on each leg.
	mem_num: // membership number
	name: // full name
	road: // the number of times help provided at road crossings.
	total: // total number of members helped
	assists: // a space separated list of membership numbers helped by the individual

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.

Also note that if someone hasn't helped on other rounds then the function will return "undefined" rather than zero for them. You might wish to clean this up. See the example at the foot of the page.

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",
	assists: "49,33 34 44 47 49 59 68 69 71 72 73 74 75 78 98 
			99 111 112 113 114 118 140 141 143 144 147 148 149 
			222 226 227 284 309 312 315 324 365 366 392 430 432
			433 486 491 645 769 771 772 811" 


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


	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”.


If you want a totals row then call this function


	data: [], // the array to total
	config: {} // a configuration object. See the next section for a description

The configuration object has two properties. The first an array of checks, one for each column to sum over. The second is a pre-initialised object, totals, with properties that match a row in the data array.

	checks: [
            column: "age",
            test: "less than", // or "equals", "greater than", "greater than equals", "lesser than equals"
            value: "25"

	totals: {
		name: "Totals",
		age: 0,
		// etc.

The above check will each age property in the data array that it is less than 25. If the test property is missing then the default check is for equality. The value to test for should be the same underlying type, i.e. string or number as that in the data array otherwise the check will fail.

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) => {
        ['member', 'name', 'date', 'time', 'direction', 'route', 'report']);
    showCopyright(copy_target, Bgr.copyright());


Displays a copyright notice.

Takes two optional parameters. The first determines the type of notice to display: There are three options depending on how you use the data.

"basic" // The default so no requirement to specify this. Use when displaying tables of the supplied data.
"derived" // Use when the data being displayed is derived from the supplied data: averages, etc.
"graph" // Use below any graphs based on the supplied data.

The second optional parameter is an array of strings of names of CSS classes from your website that are applied to the links within the notice. Use these to style those links in line with your website's look and feel.


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());

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 () {
    Bgr.memberData({year: this.value, club: "Ambleside"})
        .then((text) => {
            createTable(target, text);
            showCopyright(copy_target, Bgr.copyright());

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 assists API function is also called and this data is added to the main table of data for display. 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 '';

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

function getAssists(text, column) {
    const members = [];

    text.forEach(({member}) => {
    	// handle second rounds which are of the form "< nn >"
        if (member.charAt(0) === '<') {
            members.push(parseInt(member.substring(1, member.indexOf('>')), 10));
        } else {
            members.push(parseInt(member, 10));

    return Bgr.assists({members: members})
        .then((data) => {
            data.forEach((assist) => {
                text.forEach((mem) => {
                    let membership = parseInt(mem.member, 10);
                    if (mem.member.charAt(0) === '<') {
                        membership = parseInt(mem.member.substring(1, mem.member.indexOf('>')), 10);
                    if (parseInt(assist.mem_num, 10) === parseInt(membership, 10)) {
                        mem[column] =;
            return text;
        .then((d) => {
        // Tidy things up since if a member hasn't supported anyone then the value is undefined.
            d.forEach((elem) => {
                if(elem[column] === undefined) {
                    elem[column] = 0;
            return d;

// 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) => getAssists(data, "assists"))
    .then((data) => {
             // remove report if you don't need it
            ['member', 'name', 'date', 'time', 'direction', 'route', 'assists', 'club'],
                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

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 “” or copy it and change the colours, etc to suit your site
  • Remove the name: “and$” property, add the club: 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.


You can turn on some basic logging by setting the debug property to "console" 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.

If you wish to keep an eye on your usage in case of errors then set logging to "file" and set up the path and filename of where to log the data. It defaults to "log_errors.txt" in the root folder of your site.

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.


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.