For our first Connections Lab project, we're combining everything we've learned so far to create a site that interacts with data via an API. We're also incorporating P5 or any other library.
Ideas
I keep a list of project ideas so I first looked at that to see if anything would fit. There were a few interesting ones to which I added a bunch more. Dora shared a list of public APIs which led to even more ideas—16 in total (you can see them all on Notion).
Of these, I was really interested in doing something with an art database. I loved the Google Arts and Culture app that let you take a photo and find a piece of art that looked like it, but image recognition seemed a bridge too far.
What if you typed in your name and a related artwork showed up? This seems to keep the spirit of "find myself in the Met", but at a technical level I could maybe keep up with.
I quickly mocked this idea up in Figma—some notes on the design:
- A P5 sketch would create a fullscreen background animation
- Searching for a query would return a corresponding artwork from the Met's collection
- In the case of multiple results, I would first want to filter out the "uninteresting" ones (at the time of designing, unavailable works or pieces with no images), then randomly select one of the remaining results
- If no results, we show a message encouraging the viewer to make their own piece and get it into the Met
The Met's API
The Met has an open access API through which you can search for artworks (or "objects") by a query (the text we'll put in the search bar), or using a number of key/value pairs. The following are all booleans (true/false)
isHighlight
- is it a “highlight” artwork (display these first)?
isOnView
- is the artwork currently on view?
hasImages
- does this object have an image?
title
- does the search query match the title?
tags
- does the search query match any tags?
My thinking is that an artwork that is a highlight, on view and has images is a "better" result to show than one that just has images, or is just a highlight, so I considered using nested conditional statements to trap for all of those conditions. Eric taught me a better way of doing this, by writing a helper function that accepts all of those keys as arguments, which you can then run in a series of true/false permutations.
async function searchArtworks(isHighlight, isOnView, hasImages, title, tags) {
const url = new URL('https://collectionapi.metmuseum.org/public/collection/v1/search?');
if (isHighlight) {
url.searchParams.append('isHighlight', 'true')
}
if (hasImages) {
url.searchParams.append('hasImages', 'true')
}
if (isOnView) {
url.searchParams.append('isOnView', 'true')
}
if (title) {
url.searchParams.append('title', 'true')
}
if (tags) {
url.searchParams.append('tags', 'true')
}
url.searchParams.append('q', searchName)
In this function, we can append a given parameter to the search only if it's set to true. We can then call the function from another function that starts by running the narrowest search (all true), and runs wider and wider searches if no results.
async function getObjectID(searchName) {
// Fetch all objects with isHighlight = true, isOnView = true, hasImages = true, title = true and searchName
const data = await searchArtworks(true, true, true, true);
if (data) return data;
// HIghlights and nothing else
const data1b = await searchArtworks(true);
if (data1b) return data1b;
// Fetch all objects with isHighlight = false, isOnView = true, hasImages = true, title = true and searchName
const data2 = await searchArtworks(false, true, true, true);
if (data2) return data2;
// Fetch all objects with isHighlight = false, isOnView = false, hasImages = true, title = true and searchName
const data3 = await searchArtworks(false, false, true, true);
if (data3) return data3;
// Introduce tags if nothing matches title
const data4 = await searchArtworks(true, true, true, false, true);
if (data4) return data4;
// Tags but no highlight
const data5 = await searchArtworks(false, true, true, false, true);
if (data5) return data5;
// Tags but no highlight or on view
const data6 = await searchArtworks(false, false, true, false, true);
if (data6) return data6;
// Title but no image
const data7 = await searchArtworks(false, false, false, true);
if (data7) return data7;
// Only tags
const data8 = await searchArtworks(false, false, false, false, true);
if (data8) return data8;
// Nothing but still a match?
const data9 = await searchArtworks();
if (data9) return data9;
I'm finding this search a little more restrictive—I think in an ideal world, what I'd like to do is pass on all the results with images but give more "weight" to the ones that have more attributes so they are more likely to be picked at random than the others. As it stands, if one highlight piece is a match, you will only ever see that piece. I may roll it back to a broader search in future.
(Update Oct 7: I'm using a slightly broader search now—I start by looking for a match in the artwork title, then in the artist name, and finally in the artwork tags. What I'd like to do in the future is maybe use OR instead of AND type logic to group different permutations of attributes that I consider equivalent and create a pool of results that would be equally "fun" to serve up).
Choosing an Object
At each step in the search sequence, we will either have zero, one or more than one result. We use a conditional to either pick a random result from the array, use the single result or pass on the fact there are no matches.
return fetch(url).then(res => res.json())
.then(data => {
console.log(data);
// if array length > 1, return random object ID from array
if (data.objectIDs.length > 1) {
let randomIndex = Math.floor(Math.random() * data.objectIDs.length);
return data.objectIDs[randomIndex];
}
// if array length = 1, return object ID
if (data.objectIDs.length === 1) {
return data.objectIDs[0];
}
// if array length = 0, return null
if (data.objectIDs.length === 0) {
return null;
}
})
Displaying an Object
Once we get an objectID, I need to grab the following:
primaryImage
- URL for BG image
title
- What’s it called?
artistPrefix
(optional) - If the artist is a Dr. Ms, etc.
artistDisplayName
- Artist’s name
artistDisplayBio
(optional) - Place and date of birth, usually
objectDate
(optional) - When was the work (said to be) created?
medium
(optional) - What’s it made of?
Then, I need to create DOM elements to house each of these pieces.
I couldn't get this to work for the longest time, so I asked Jonny for help. He showed me his workflow for getting chat GPT to debug his code, and GPT suggested I use appendChild to append the new DOM elements I created to the existing HTML.
// Create a new Div for the metadata
let artworkInfo = document.createElement('div');
artworkInfo.setAttribute('class', 'artwork-info');
// Grab primaryImage and make it BG Image
console.log(data.primaryImage);
let bgImage = document.getElementById('bg-image');
bgImage.style.backgroundImage = `url(${data.primaryImage})`;
// Create a div element for artist name and bio
let artistInfo = document.createElement('div');
artistInfo.setAttribute('class', 'artist-info');
let artistName = document.createElement('h2');
artistName.innerHTML = data.artistDisplayName;
artistInfo.appendChild(artistName); // Append artistName to artistInfo div
Wiping and Unwiping
To move between search and results without a page transition, we have a removeSearch
function that takes the container element for search away.
function removeSearch() {
const element = document.getElementById('main');
element.remove();
}
Refreshing the page restores the original HTML, so we don't need an “unwipe” function, just a refresh button.
//create button to refresh page
let refresh = document.createElement('button');
refresh.setAttribute('class', 'button');
refresh.setAttribute('onclick', 'refresh()');
refresh.innerHTML = '← Start Again'; // Set the button text
artworkInfo.appendChild(refresh); // Append refresh to artworkInfo div
function refresh() {
location.reload();
}
No Results
This was another thing that gave me a headache. For some reason, the "else" part of my stack of conditionals wasn't firing, and the function to display the no results message was never invoked. Again I went to Jonny/GPT to help, but this time, neither of the two solutions I got back were close to working. However, one of the hallucinated solutions included a .catch(error)
at the end, something I hadn't included in the function I tried to write. I still can't quite understand why, but simply adding that was enough to fix the issue.
My original idea (see mockups above) was to serve up a random artwork when no results were found—I might still return to that but for now the P5 sketch continues in the background.
Next Steps
I'll definitely spend some more time on this project even though we're technically done. Here's what I'd like to fix:
- P5 background (ideally this would be dynamic each time the page loads) (currently using a sketch written via Copilot)
- Pressing return in the field runs the search rather than refreshing the page (I found many explanations that using the form's submit event should fix this, but it didn't)
- Get the "no results" part working (it looks like returning null doesn't fail the
if(id)
conditional) (done!) - Get more results coming back per search query (done but can still improve!)
- CSS animations to have things come in and out more smoothly
- Footer design
Update October 10
“Undefined” Error
It looks like for certain objects I get the following:
{ "message": "Not a valid object" }
I should be able to consider this a "no matches" situation and run displayNoMatchMessage, which should hopefully get rid of undefined results.
Results Without Images
From clicking through to the Met’s page, it looks like the issue is that some results don’t have image rights. Not sure if I can do anything about this as the search params don’t seem to give me this.
No Comments.