From 1b402243ef527ba4fd7953858057af00ec8d2a75 Mon Sep 17 00:00:00 2001 From: lublagg Date: Sat, 16 Sep 2023 18:38:18 -0400 Subject: [PATCH] User can select attributes and create CODAP table. --- src/components/app.tsx | 14 +-- src/components/constants.ts | 2 +- src/components/types.ts | 42 ++++++++ src/scripts/api.ts | 185 +++++++++++++++++++++++------------ src/scripts/connect.js | 126 ++++++++++++------------ src/scripts/query-headers.ts | 2 +- 6 files changed, 235 insertions(+), 136 deletions(-) diff --git a/src/components/app.tsx b/src/components/app.tsx index d8228a7..7564101 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -4,7 +4,7 @@ import classnames from "classnames"; import { Information } from "./information"; import { attributeOptions, categories, defaultSelectedOptions, yearsOptions } from "./constants"; import { IStateOptions } from "./types"; -import { createQueryFromSelections } from "../scripts/api"; +import { createTableFromSelections } from "../scripts/api"; import { connect } from "../scripts/connect"; @@ -43,16 +43,8 @@ function App() { setShowInfo(true); }; - const handleGetData = () => { - createQueryFromSelections(selectedOptions); - - const makeDataSetAndTable = async () => { - const dS = await connect.guaranteeDataset(selectedOptions.geographicLevel); - if (dS.success) { - await connect.makeCaseTableAppear(); - } - }; - makeDataSetAndTable(); + const handleGetData = async () => { + await createTableFromSelections(selectedOptions); }; return ( diff --git a/src/components/constants.ts b/src/components/constants.ts index b1061cb..23aaecf 100644 --- a/src/components/constants.ts +++ b/src/components/constants.ts @@ -92,7 +92,7 @@ const cropUnitOptions: IAttrOptions = { options: ["Area Harvested", "Yield"], instructions: "(Choose units)" }; -const cropOptions: IAttrOptions = { +export const cropOptions: IAttrOptions = { key: "crops", label: null, options: ["Corn", "Cotton", "Grapes", "Grasses", "Oats", "Soybeans", "Wheat"], diff --git a/src/components/types.ts b/src/components/types.ts index 5e78030..d19581c 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -17,3 +17,45 @@ export interface IAttrOptions { options: string[], instructions: string|null } + +export interface IResData { + "CV (%)": string; + Value: string; + agg_level_desc: string; + asd_code: string; + asd_desc: string; + begin_code: string; + class_desc: string; + commodity_desc: string; + congr_district_code: string; + country_code: string; + country_name: string; + county_ansi: string; + county_code: string; + county_name: string; + domain_desc: string; + domaincat_desc: string; + end_code: string; + freq_desc: string; + group_desc: string; + load_time: string; + location_desc: string; + prodn_practice_desc: string; + reference_period_desc: string; + region_desc: string; + sector_desc: string; + short_desc: string; + source_desc: string; + state_alpha: string; + state_ansi: string; + state_fips_code: string; + state_name: string; + statisticcat_desc: string; + unit_desc: string; + util_practice_desc: string; + watershed_code: string; + watershed_desc: string; + week_ending: string; + year: number; + zip_5: string; +}; \ No newline at end of file diff --git a/src/scripts/api.ts b/src/scripts/api.ts index e773ac6..ba10ec7 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -1,94 +1,155 @@ import fetchJsonp from "fetch-jsonp"; -import { queryData } from "./query-headers"; +import { ICropDataItem, queryData } from "./query-headers"; import { IStateOptions } from "../components/types"; import { connect } from "./connect"; +import { cropOptions } from "../components/constants"; const baseURL = `https://quickstats.nass.usda.gov/api/api_GET/?key=9ED0BFB8-8DDD-3609-9940-A2341ED6A9E3`; -export const createRequest = (attribute: string, geoLevel: string, location: string, year: string) => { +interface IRequestParams { + attribute: string, + geographicLevel: string, + location: string, + year: string, + cropCategory?: keyof ICropDataItem +} + +interface IGetAttrDataParams { + attribute: string, + geographicLevel: string, + cropUnits: string, + state: string, + year: string +} + +export const createRequest = ({attribute, geographicLevel, location, year, cropCategory}: IRequestParams) => { const queryParams = queryData.find((d) => d.plugInAttribute === attribute); const {sector, group, commodity, category, domains, dataItem} = queryParams!; - const baseReq = `${baseURL}§_desc=${sector}&group_desc=${group}&commodity_desc=${commodity}&statisticcat_desc=${category}&domain_desc=${domains}&agg_level_desc=${geoLevel}&state_name=${location}&year=${year}`; - - const reqs = []; + let item; + let cat; + if (cropCategory) { + const cropDataItem = queryParams?.dataItem as ICropDataItem; + const cropCat = queryParams?.category as ICropDataItem; + item = cropDataItem[cropCategory]; + cat = cropCat[cropCategory]; + } else { + item = dataItem; + cat = category; + } + const baseReq = `${baseURL}§_desc=${sector}&group_desc=${group}&commodity_desc=${commodity}&statisticcat_desc=${cat}&domain_desc=${domains}&agg_level_desc=${geographicLevel}&state_name=${location}&year=${year}`; + let req = baseReq; if (Array.isArray(dataItem)) { - dataItem.forEach(item => { - reqs.push(`${baseReq}&short_desc=${item}`); + dataItem.forEach(dItem => { + req = req + `&short_desc=${dItem}`; }); } else { - reqs.push(`${baseReq}&short_desc=${dataItem}`); + req = req + `&short_desc=${item}`; } - return reqs; + return req; }; -export const createQueryFromSelections = (selectedOptions: IStateOptions) => { - const {geographicLevel, states, years, ...subOptions} = selectedOptions; +export const createTableFromSelections = async (selectedOptions: IStateOptions) => { + const {geographicLevel, states, cropUnits, years, ...subOptions} = selectedOptions; + await connect.getNewDataContext(); + await connect.createTopCollection(); + + // need to change this - instead of creating based on UI names, create based on dataItems in queryParams + const allAttrs: Array = ["Year"]; + for (const key in subOptions) { + const selections = subOptions[key as keyof typeof subOptions]; + for (const attribute of selections) { + const queryParams = queryData.find((d) => d.plugInAttribute === attribute); + const {dataItem} = queryParams!; + if (Array.isArray(dataItem)) { + allAttrs.push(...dataItem); + } else { + allAttrs.push(dataItem); + } + } + } + await connect.createSubCollection(allAttrs); + const items = await getItems(selectedOptions); + await connect.createItems(items); + await connect.makeCaseTableAppear(); +} + +const getItems = async (selectedOptions: IStateOptions) => { + const {states, years} = selectedOptions; const multipleStatesSelected = states.length > 1 || states[0] === "All States"; const multipleYearsSelected = years.length > 1; + const items = []; if (multipleStatesSelected) { - states.forEach((state) => { + for (const state of states) { if (multipleYearsSelected) { - years.forEach(year => { - // do something - console.log("multiple years and mulitple states selected"); - }); + for (const year of years) { + const item = await getDataForSingleYearAndState(selectedOptions, state, year); + items.push(item); + } } else { - console.log("multiple states selected with one year"); + const item = await getDataForSingleYearAndState(selectedOptions, state, years[0]); + items.push(item); } - }); + } } else { - for (const key in subOptions) { - const value = subOptions[key as keyof typeof subOptions]; - console.log("current value", value); - if (value && Array.isArray(value) && value.length > 1) { - console.log("you selected more than one value from a sub-category"); - } else if (value && Array.isArray(value) && value.length === 1) { - console.log("you selected only one value from a sub-category and it is this value", value); - const reqArray = createRequest(value[0], geographicLevel, states[0], years[0]); - console.log("REQUEST", reqArray[0]); - getDataAndCreateCodapTable(reqArray); + const item = await getDataForSingleYearAndState(selectedOptions, states[0], years[0]); + items.push(item); + } + + return items; +}; + +const getDataForSingleYearAndState = async (selectedOptions: IStateOptions, state: string, year: string) => { + const {geographicLevel, states, years, cropUnits, ...subOptions} = selectedOptions; + + let item: any = { + "State": state, + "Year": year, + } + + for (const key in subOptions) { + const value = subOptions[key as keyof typeof subOptions]; + if (value && Array.isArray(value)) { + for (const attribute of value) { + const attrData = await getAttrData({attribute, geographicLevel, state, year, cropUnits}); + item = {...item, ...attrData}; } } } -}; -export const getDataAndCreateCodapTable = (reqs: string[]) => { - reqs.forEach((req) => { - fetchJsonp(req) - .then(function(response) { - return response.json(); - }).then(function(json) { - console.log("parsed json", json); - const formattedData = formatDataForCODAP(json); - }).catch(function(ex) { - console.log("parsing failed", ex); - }); - }); + return item; }; -const formatDataForCODAP = (res: any) => { - console.log({res}); - return res; -}; +const getAttrData = async (params: IGetAttrDataParams) => { + const {attribute, geographicLevel, state, year, cropUnits} = params; + const reqParams: IRequestParams = {attribute, geographicLevel, location: state, year}; + if (cropOptions.options.includes(attribute) && cropUnits) { + reqParams.cropCategory = cropUnits as keyof ICropDataItem; + } + const req = createRequest(reqParams); + const res = await fetchData(req); + const values: any = {}; + if (res) { + const {data} = res; + data.forEach((dataItem: any) => { + values[dataItem.short_desc] = dataItem.Value; + }) + } else { + console.log("error"); + } + return values; +} -// export const runTestQuery = () => { -// const request1 = createRequest("Total Farmers", "STATE", "CALIFORNIA", "2017")[0]; -// const request2 = createRequest("Total Farmers", "STATE", "ARKANSAS", "2017")[0]; -// const request3 = createRequest("Total Farmers", "STATE", "ALABAMA", "2017")[0]; -// const request4 = createRequest("Total Farmers", "STATE", "MONTANA", "2017")[0]; -// const requests = [request1, request2, request3, request4]; -// requests.forEach((req) => { -// fetchJsonp(req) -// .then(function(response) { -// return response.json(); -// }).then(function(json) { -// console.log("parsed json", json); -// }).catch(function(ex) { -// console.log("parsing failed", ex); -// }) -// }) -// }; +export const fetchData = async (req: string) => { + try { + const response = await fetchJsonp(req); + const json = await response.json(); + return json; + } catch (error) { + console.log("parsing failed", error); + throw error; + } +}; \ No newline at end of file diff --git a/src/scripts/connect.js b/src/scripts/connect.js index 6a648b7..0b24088 100644 --- a/src/scripts/connect.js +++ b/src/scripts/connect.js @@ -10,106 +10,110 @@ export const connect = { makeCODAPAttributeDef: function (attr) { return { - name: attr.title, - title: attr.title, - description: attr.description, - type: attr.format, - formula: attr.formula + name: attr } }, - createNewDataset: async function (geoLevel) { - const geoLabel = geoLevel === "State" ? states : "Counties"; + createNewDataContext: async function () { return codapInterface.sendRequest({ action: 'create', resource: 'dataContext', values: { name: dataSetName, - title: dataSetTitle, - collections: [{ - name: geoLabel, - attrs: [ - { - name: geoLevel, - title: geoLevel, - description: `Selected ${geoLabel}` - }, - { - name: "Boundaries", - title: "Boundaries", - formula: 'lookupBoundary(US_state_boundaries, State)', - formulaDependents: 'State' - } - ] - }, { - name: "Data", - parent: geoLabel, - attrs: [ // note how this is an array of objects. - {name: "Year", title: "Year"} - ] - }] + title: dataSetTitle } }); }, + deleteOldDataContext: async function () { + return codapInterface.sendRequest({ + action: 'delete', + resource: `dataContext[${dataSetName}]` + }); + }, - guaranteeDataset: async function (geoLevel) { - let datasetResource = 'dataContext[' + dataSetName + - ']'; - await this.createNewDataset(geoLevel); + checkIfDataContextExists: async function () { const response = await codapInterface.sendRequest({ action: 'get', - resource: datasetResource}); + resource: `dataContext[${dataSetName}]`}); return response; }, - makeCaseTableAppear : async function() { - const theMessage = { - action : "create", - resource : "component", - values : { - type : 'caseTable', - dataContext : dataSetName, - name : dataSetName, - title: dataSetName, - cannotClose : true - } - }; - - const makeCaseTableResult = await codapInterface.sendRequest(theMessage); - if (makeCaseTableResult.success) { - console.log("Success creating case table: " + theMessage.title); - } else { - console.log("FAILED to create case table: " + theMessage.title); + getNewDataContext: async function () { + const doesDataContextExist = await this.checkIfDataContextExists(); + if (doesDataContextExist.success) { + await connect.deleteOldDataContext(); } - return makeCaseTableResult.success && makeCaseTableResult.values.id; + const res = await connect.createNewDataContext(); + return res; }, - createNewCollection: async function(dSName, collName) { + createTopCollection: async function() { const message = { "action": "create", - "resource": `dataContext[${dSName}].collection`, + "resource": `dataContext[${dataSetName}].collection`, "values": { - "name": collName, + "name": "States", + "parent": "_root_", "attributes": [{ - "name": "newAttr", + "name": "State", + }, + { + "name": "Boundary", + "formula": "lookupBoundary(US_state_boundaries, State)", + "formulaDependents": "State" }] } }; await codapInterface.sendRequest(message); }, - createNewAttribute: async function(dSName, collName, attrName) { + createSubCollection: async function(attrs) { const message = { "action": "create", - "resource": `dataContext[${dSName}].collection[${collName}].attribute`, + "resource": `dataContext[${dataSetName}].collection`, "values": { - "name": attrName, + "name": "Data", + "parent": "States", + "attributes": attrs.map((attr) => this.makeCODAPAttributeDef(attr)) } }; await codapInterface.sendRequest(message); }, + createItems: async function(items) { + for (const item of items) { + const message = { + "action": "create", + "resource": `dataContext[${dataSetName}].item`, + "values": item + }; + await codapInterface.sendRequest(message); + } + }, + + makeCaseTableAppear : async function() { + const theMessage = { + action : "create", + resource : "component", + values : { + type : 'caseTable', + dataContext : dataSetName, + name : dataSetName, + title: dataSetName, + cannotClose : false + } + }; + + const makeCaseTableResult = await codapInterface.sendRequest(theMessage); + if (makeCaseTableResult.success) { + console.log("Success creating case table: " + theMessage.title); + } else { + console.log("FAILED to create case table: " + theMessage.title); + } + return makeCaseTableResult.success && makeCaseTableResult.values.id; + }, + iFrameDescriptor: { version: '0.0.1', name: 'nass-plugin', diff --git a/src/scripts/query-headers.ts b/src/scripts/query-headers.ts index 311211f..2151acf 100644 --- a/src/scripts/query-headers.ts +++ b/src/scripts/query-headers.ts @@ -1,6 +1,6 @@ const areaHarvested = "Area Harvested"; const yieldInBU = "Yield"; -interface ICropDataItem { +export interface ICropDataItem { [areaHarvested]: string, [yieldInBU]: string }