Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[proof-of-principle] allow metadata to be fetched from an API #1207

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions scripts/get-data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,6 @@ do
done

echo "The local data directory ./data now contains up-to-date datasets from http://data.nextstrain.org"

# TMP
curl http://staging.nextstrain.org/zika-tutorial-metadata-via-api.json --compressed -o data/zika-tutorial-metadata-via-api.json
77 changes: 77 additions & 0 deletions src/middleware/extraMetadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as types from "../actions/types";
import { infoNotification } from "../actions/notifications";
import { fetchJSON } from "../util/serverInteraction";
import { changeColorBy } from "../actions/colors";

/**
* EXPERIMENTAL ONLY -- NOT FOR PRODUCTION
* This function is a proof-of-principle approach for spiking in metadata
* via an API call, rather than storing it within the JSON.
* It currently only works when changing to a new colorBy.
* Cacheing is not implemented.
*/
export const extraMetadataMiddleware = (store) => (next) => async (action) => {

if (action.type === types.NEW_COLORS) {
const {metadata, tree} = store.getState();
const coloringSpecifiesApiEndpoint = metadata.colorings && metadata.colorings[action.colorBy] && metadata.colorings[action.colorBy].EXPERIMENTAL_google_sheets_id;
const colorScaleIsUndefined = action.colorScale.legendValues.length === 0; // stops unnecessary fetches
if (coloringSpecifiesApiEndpoint && colorScaleIsUndefined) {
store.dispatch(infoNotification({message: "Fetching colors, hold on!", details: "Should use spinner or similar UI"}));
try {
const colorByData = await getGoogleSheetData(metadata.colorings[action.colorBy].EXPERIMENTAL_google_sheets_id, action.colorBy);
insertDataIntoTree(tree, action.colorBy, colorByData);
store.dispatch(changeColorBy(action.colorBy)); // re-dispatch, the original one won't have gone through!
} catch (error) {
console.error(error);
}
return;
}
}
next(action); // send action to other middleware / reducers
};


/** get & parse data from (public) google sheets.
* Google is moving to v4 of their API, however that seems (?) to require API keys even to access public
* data. Thus i'm using the v3 API, which will go offline on Sept 30, but allows anonymous API calls.
* As such, the parsing function is rudimentary. We only parse the field `keyName`, but cacheing of the response
* (within auspice?) will make this acceptable
*/
async function getGoogleSheetData(id, keyName) {
const sheet = await fetchJSON(`https://spreadsheets.google.com/feeds/cells/${id}/1/public/full?alt=json`);
const cells = sheet.feed.entry.map((e) => e["gs$cell"]);

/* work out what column represents the keyName */
let keyColumnId;
try {
keyColumnId = (cells.filter((c) => c.row==="1" && c.inputValue===keyName)[0]).col;
} catch (e) {
console.error(`Couldn't find a column name of ${keyName} in the google sheet`);
throw e;
}

/* create a Map of strain -> keyValue out of the google sheets JSON */
const rowToStrain = new Map();
cells.filter((cell) => cell.row!=="1" && cell.col==="1") // assume strain is col 1
.forEach((cell) => {rowToStrain.set(cell.row, cell.inputValue);});
const strainMap = new Map();
cells.filter((cell) => cell.row!=="1" && cell.col===keyColumnId)
.filter((cell) => rowToStrain.has(cell.row))
.forEach((cell) => {
strainMap.set(rowToStrain.get(cell.row), cell.inputValue);
});

return strainMap;
}

/** this should be done in a reducer, but simply modifying the internal data within
* the tree is a shortcut and is possible since it's an Object.
*/
function insertDataIntoTree(tree, key, data) {
tree.nodes.forEach((n) => {
if (data.has(n.name)) {
n.node_attrs[key] = {value: data.get(n.name)};
}
});
}
3 changes: 3 additions & 0 deletions src/store/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import { changeURLMiddleware } from "../middleware/changeURL";
import { extraMetadataMiddleware } from "../middleware/extraMetadata";

import rootReducer from "../reducers";
import { loggingMiddleware } from "../middleware/logActions"; // eslint-disable-line no-unused-vars

const configureStore = (initialState) => {
const middleware = [
thunk,
extraMetadataMiddleware,
changeURLMiddleware, // eslint-disable-line comma-dangle
// loggingMiddleware
];
Expand Down