About the Map
This is an interactive map made by Sharon Howard for the Alice Thornton’s Books project .
The map shows places associated with narratives of the 319 Events tagged by the project, with links to the project’s digital scholarly edition (DSE) .
Caveats:
There may be errors. All places (regardless of actual size) are mapped as single points.
Not all places mentioned in a narrative are equally significant to the event.
Conversely, a significant place might not be explicitly mentioned, especially in more brief and fragmentary passages.
Places mentioned outside text tagged as Events are not included.
ojsAtbGeoEvent = transpose (ojs_atb_geo_event)
ojsEvents = transpose (ojs_events)
earliest = d3. min (ojsAtbGeoEvent. map (d => d. start_year ));
latest = d3. max (ojsAtbGeoEvent. map (d => d. end_year ));
function createGeoDataEvent (data) {
const geoData = {
type : 'FeatureCollection' ,
crs : {type : 'name' , properties : {name : 'urn:ogc:def:crs:EPSG::27700' }},
features : []
};
data. map (h => {
geoData. features . push ({
type : 'Feature' ,
properties : {
// this lacks a uid i think. shouldn't matter
id : h. place_id ,
event_id : h. event_id ,
address : h. address ,
level : h. level ,
type : h. place_type ,
//category: h.category,
keyword : h. keyword ,
event : h. event ,
event_year : h. display_year ,
start_year : h. start_year ,
end_year : h. end_year ,
event_link_html : h. event_link_html ,
places_link_html : h. atb_places_link_html ,
event_text_html : h. place_event_html
},
geometry : {
type : 'Point' ,
coordinates : [h. long , h. lat ]
}
});
});
return geoData;
}
searched_event = search_events. map (d => d. event_id )
searched_place = search_events. map (d => d. place_id ) // i think you just want the event id for the search filter
labelEventKeywordData = [
... new Set (
ojsAtbGeoEvent // data source
. flatMap ((n) => n. keyword )
. sort ()
)
]
The Map
Features:
search event descriptions or keywords (table below for reference)
click on cluster numbers (or zoom in/out) to drill down into areas
click on single markers to view event details and texts below the map, with links to the DSE.
viewof search_events = Inputs. search (ojsAtbGeoEvent, {label : "Search events" , columns : ["event" , "keyword" ], width : 400 })
/*
viewof rangeYears = Inputs.form([
Inputs.range([earliest, latest], {label: "From", value: earliest, step: 1}), // start_year and end_year
Inputs.range([earliest, latest], {label: "To", value: latest, step: 1})]
);
*/
//viewof eventKeywordCheck = Inputs.checkbox(labelEventKeywordData, {label: "Event keyword", value: labelEventKeywordData})
map = {
// Make a container in the Observable DOM to hold the map
let parent = DOM. element ('div' , { style : `width: ${ width} px; height:500px` });
// Make sure Observable renders the parent container so Leaflet can figure out its size
yield parent;
let map = L. map (parent)
. setView ([54.3 , - 3.5 ], 6 ); // uk+ire // higher numbers are more zoomed in
// OSM fallback if maptiler fails (or simply slow to load!)
// possible to customise osm to simply be a bit more subtle?
// 2026-03-23 weird access blocked messages, though map shows. try reinstating maptiler- that's ok in mindseye.
let osmTileLayer = L. tileLayer ( 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' , {
attribution : '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>' ,
maxZoom : 16 ,
minZoom : 5
})
. addTo (map);
/*
*/
const key = 'Qm3IHfglBpG81gp4ZK4G' ;
let mtTileLayer = L. tileLayer (`https://api.maptiler.com/maps/base-v4/{z}/{x}/{y}.png?key= ${ key} ` , { //style URL
tileSize : 512 ,
zoomOffset : - 1 ,
minZoom : 4 ,
attribution : " \u003c a href= \" https://www.maptiler.com/copyright/ \" target= \" _blank \"\u003e\u0026 copy; MapTiler \u003c /a \u003e \u003c a href= \" https://www.openstreetmap.org/copyright \" target= \" _blank \"\u003e\u0026 copy; OpenStreetMap contributors \u003c /a \u003e " ,
crossOrigin : true
}). addTo (map);
const geojsonData = createGeoDataEvent (ojsAtbGeoEvent);
// tweak clustering...
/*
Customising the Clustered Markers
how do i just make the text of the markers a bit more prominent without changing anything else?
zoomToBoundsOnClick
whether to turn this off?
but you have no way of knowing when you've zoomed in far enough for clicking to open spiderfyed singles!
maxClusterRadius i think this def helps.
*/
const mcg = markerCluster ({
spiderfyDistanceMultiplier : 1.8 ,
zoomToBoundsOnClick : true ,
maxClusterRadius : 40
}). addTo (map);
// todo did i find code to have different markers for types/categories?
// single markers with a simple popup for testing. is L.geoJson the same as L.geoJSON ? seems to be
let markers = L. geoJSON (null , {
onEachFeature : function (feature, layer) {
const data = feature. properties ;
// tooltip shows brief info. event short much too short to be useful!
const tip = `<div class="popup"> ${ data. address } ( ${ data. event_year } )</div>` ;
layer. bindPopup (tip); // click.
//layer.bindTooltip(tip, {sticky: true}); // hover.
// make html for fuller details
// to handle NAs (undefined) need to start with an empty const. don't think there are any in this atb data though
//let linkedlleps = '';
//if(data.linked_llep) {linkedlleps = `<div><p><i>Examination(s):</i></p> <ul>${data.linked_llep} </ul></div>`; } // don't need else.
const html = `<h3> ${ data. address } </h3>` +
`<h4> ${ data. event } </h4>` +
`<p> ${ data. event_year } </p>` +
`<ul><li> ${ data. places_link_html } </li><li> ${ data. event_link_html } </li></ul>` +
`<div> ${ data. event_text_html } </div>`
//`<p>${data.page_link_html}</p>`
;
//would use textContent for plain text. innerHTML for formatted HTML
// this needs bindPopup to work nicely on clustered things.
layer. on ('click' , () => {
document . getElementById ('atb_info' ). innerHTML = html;
})
},
pointToLayer : function (feature, latlng) {
// it works! but comment out because you don't have category any more
/*const d = feature.properties;
let customColour = "red";
if (d.category=="home") customColour = "#CC6000";
else customColour = "#ff7800";
*/
return L. circleMarker (latlng, {
radius : 7 ,
//fillColor: customColour,
fillColor : "#CC6000" , // a bit darker than "#ff7800"
color : "#000" ,
weight : 1 , // width of the circle border.
opacity : 1 ,
fillOpacity : 1
});
}
}) ;
// Once you have populated your Leaflet GeoJSON Layer Group (typically with geojsonLayer.addData(geoJsonObject), then instead of adding that group to your map, simply add it into your MarkerClusterGroup:
markers. addData (geojsonData). addTo (mcg);
// This is to make these variables accessible in other Observable cells.
// It is not necessary for regular HTML web pages.
mutable mapVariables = [mcg, markers];
}
mapFilters = {
// This handles the changes
// In normal HTML environment, this would be inside the slider event handling function.
mapVariables[1 ]. eachLayer (l => {
if (
// single year in data: l.feature.properties.start_year >= rangeYears[0] && l.feature.properties.end_year <= rangeYears[1]
// year range in data
//l.feature.properties.start_year <= rangeYears[1] && l.feature.properties.end_year >= rangeYears[0]
//&&
//eventKeywordCheck.includes(l.feature.properties.keyword)
searched_event. includes (l. feature . properties . event_id )
)
{
l. addTo (mapVariables[0 ]);
} else {
l. removeFrom (mapVariables[0 ]);
}
})
return 'howdy' ; // do i need this return line at all? probably not...
}
mutable mapVariables = null ;
markerCluster = L, require ('leaflet.markercluster@1.1.0' ). catch (() => L. markerClusterGroup )
markerClusterCSS = html `<link href=' ${ resolve ('leaflet.markercluster@1.1.0/dist/MarkerCluster.Default.css' )} ' rel='stylesheet' />`