From 21f0e48990261b063fbc4a8494ce8995453c9a9f Mon Sep 17 00:00:00 2001 From: lublagg Date: Fri, 1 Sep 2023 16:35:35 -0400 Subject: [PATCH 01/21] Test deployment. --- src/components/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/app.tsx b/src/components/app.tsx index d7688ec..d3222bd 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -2,7 +2,7 @@ import React from "react"; function App() { return ( -
Hello world
+
Hello world, this is a small change I made to test if deployment is working correctly
); } From 2f73e87379931871e853783adeaf85f54fc96e0e Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 6 Sep 2023 15:37:30 -0400 Subject: [PATCH 02/21] Create skeleton for plugin (PT-185950946) --- package-lock.json | 11 + package.json | 1 + src/assets/arrow-drop-down.svg | 6 + src/assets/info.svg | 6 + src/components/app.scss | 120 +++++ src/components/app.tsx | 52 +- src/components/dropdown.scss | 79 +++ src/components/dropdown.tsx | 33 ++ src/components/global.d.ts | 2 + src/components/information.scss | 43 ++ src/components/information.tsx | 27 + src/components/vars.scss | 8 + src/index.css | 13 - src/index.html | 5 +- src/index.scss | 3 + src/index.tsx | 2 +- src/scripts/Attribute.js | 174 +++++++ src/scripts/app.CODAPconnect.js | 266 ++++++++++ src/scripts/app.DBconnect.js | 171 +++++++ src/scripts/app.constants.js | 42 ++ src/scripts/app.js | 146 ++++++ src/scripts/app.ui.js | 420 ++++++++++++++++ src/scripts/app.userActions.js | 161 ++++++ src/scripts/attributeConfig.js | 864 ++++++++++++++++++++++++++++++++ src/scripts/pluginHelper.js | 162 ++++++ src/types.tsx | 69 --- 26 files changed, 2799 insertions(+), 87 deletions(-) create mode 100644 src/assets/arrow-drop-down.svg create mode 100644 src/assets/info.svg create mode 100644 src/components/app.scss create mode 100644 src/components/dropdown.scss create mode 100644 src/components/dropdown.tsx create mode 100644 src/components/global.d.ts create mode 100644 src/components/information.scss create mode 100644 src/components/information.tsx create mode 100644 src/components/vars.scss delete mode 100644 src/index.css create mode 100644 src/index.scss create mode 100644 src/scripts/Attribute.js create mode 100644 src/scripts/app.CODAPconnect.js create mode 100644 src/scripts/app.DBconnect.js create mode 100644 src/scripts/app.constants.js create mode 100644 src/scripts/app.js create mode 100644 src/scripts/app.ui.js create mode 100644 src/scripts/app.userActions.js create mode 100644 src/scripts/attributeConfig.js create mode 100644 src/scripts/pluginHelper.js diff --git a/package-lock.json b/package-lock.json index 5462d96..1444d47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "classnames": "^2.3.2", "iframe-phone": "^1.3.1", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -6553,6 +6554,11 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/clean-css": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz", @@ -24192,6 +24198,11 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "clean-css": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz", diff --git a/package.json b/package.json index cd5d9a3..790d284 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "classnames": "^2.3.2", "iframe-phone": "^1.3.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/src/assets/arrow-drop-down.svg b/src/assets/arrow-drop-down.svg new file mode 100644 index 0000000..c319f3c --- /dev/null +++ b/src/assets/arrow-drop-down.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/info.svg b/src/assets/info.svg new file mode 100644 index 0000000..9184f95 --- /dev/null +++ b/src/assets/info.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/app.scss b/src/components/app.scss new file mode 100644 index 0000000..2edc7c5 --- /dev/null +++ b/src/components/app.scss @@ -0,0 +1,120 @@ +@import "./vars.scss"; + +body { + margin: 0; + padding: 0; + overflow-y: hidden; +} + +/* remove Chrome blue halo */ +input:focus, +select:focus, +textarea:focus, +button:focus { + outline: none; +} + +/* basic page style */ +.pluginContent { + font-family: 'Montserrat', sans-serif; + font-size: 12px; + font-weight: 400; + color: $text; + background-color: white; + padding: 8px 12px; + height: 95vh; + position: absolute; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: stretch; +} + +.hide { + display: none; +} + +.sectionHeaderLine { + display: flex; + align-items: baseline; + justify-content: space-between; + margin: 6px 0; + .sectionHeaderText { + width: 88%; + } +} + +.infoButton { + display: inline-block; + min-width: 20px; + height: 20px; + background-image: url("../assets/info.svg"); + cursor: pointer; +} + +.scrollArea { + border-bottom: thin solid $teal-light; + flex-basis: 300px; + flex-grow: 10; + overflow-y: scroll; + margin-top: 6px; + margin-bottom: 0; +} + +.summary { + flex-basis: 56px; + flex-grow: 0; + flex-shrink: 0; + display: flex; + align-items: baseline; + justify-content: space-between; +} + +.introSection { + flex-basis: 47px; + flex-grow: 0; + flex-shrink: 10; + border-bottom: thin solid $text; +} + +button { + margin: 6px; + padding: 6px; + border-radius: 3px; + border: solid 1px $teal-light; + background-color: white; + font-family: 'Montserrat', sans-serif; + font-size: 12px; + font-weight: 500; + &:hover { + cursor: pointer; + background: $teal-light-12; + } +} + +a { + color: $teal-dark; +} + +.scrollArea { + .dropdown { + border-bottom: thin #dddddd solid; + :last-child { + border-bottom: none; + } + } +} + +.summary { + font-family: 'icomoon' !important; + font-size: 20px; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/src/components/app.tsx b/src/components/app.tsx index d3222bd..106a7e1 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -1,8 +1,56 @@ -import React from "react"; +import React, {useState} from "react"; +import { Dropdown } from "./dropdown"; +import css from "./app.scss"; +import classnames from "classnames"; +import { Information } from "./information"; function App() { + const [showInfo, setShowInfo] = useState(false); + + const handleInfoClick = () => { + setShowInfo(true); + }; + return ( -
Hello world, this is a small change I made to test if deployment is working correctly
+
+ { showInfo && + setShowInfo(false)}/> + } +
+
+ + Retrieve data on U.S. agricultural statitistics at the state or county level. + + +
+
+
+ + + +
+
+ + +
+
+ ); } diff --git a/src/components/dropdown.scss b/src/components/dropdown.scss new file mode 100644 index 0000000..10f47c6 --- /dev/null +++ b/src/components/dropdown.scss @@ -0,0 +1,79 @@ +@import "./vars.scss"; + +.dropdown { + border-bottom: thin #dddddd solid; +} + +.dropdownBody { + padding-left: 0; + padding-right: 0; + max-height: 1000px; + transition: max-height; + transition-duration: .5s; + transition-timing-function: ease-in; + &.hidden { + max-height: 0; + } +} + +.dropdownBody .sectionHeaderLine .sectionTitle { + flex-grow: 5; +} + +.sectionHeaderLine { + display: flex; + align-items: baseline; + justify-content: space-between; + margin: 6px 0; + .sectionTitle { + min-width: 102px; + font-weight: 600; + flex-basis: 75px; + flex-shrink: 0; + flex-grow: 0 + } +} + +.dropdown .dropdownBody { + padding: 0 12px; + overflow-y: hidden; + transition-property: max-height; + transition-duration: .5s; + transition-timing-function: ease-in; +} + +.dropdownIndicator{ + display: inline-block; + width: 20px; + height: 20px; + flex-basis: 20px; + flex-grow: 0; + flex-shrink: 0; + background-image: url("../assets/arrow-drop-down.svg"); + &.up { + transform: rotate(180deg); + } +} + +.dropdownHeader { + cursor: pointer; +} + +.userSelection { + font-family: 'Montserrat', sans-serif; + font-size: 12px; + color: $teal-dark; + flex-grow: 10; + padding: 0 6px; +} + +.selectionCount { + padding: 1.5px 5px; + border-radius: 3px; + background-color: $teal-light-25; + flex-basis: 16px; + flex-grow: 0; + flex-shrink: 0; + text-align: center; + height: 16px; +} \ No newline at end of file diff --git a/src/components/dropdown.tsx b/src/components/dropdown.tsx new file mode 100644 index 0000000..6e61c1e --- /dev/null +++ b/src/components/dropdown.tsx @@ -0,0 +1,33 @@ +import React, { useState } from "react"; +import classnames from "classnames"; +import css from "./dropdown.scss"; + +interface IProps { + sectionName: string + sectionAltText: string + sectionDescription: string +} + +export const Dropdown: React.FC = (props) => { + const {sectionName, sectionAltText, sectionDescription} = props; + const [showItems, setShowItems] = useState(false); + + const handleClick = () => { + setShowItems(!showItems); + }; + + return ( +
+
+ {sectionName} + + +
+
+

{sectionDescription}

+
+
+
+
+ ); +}; diff --git a/src/components/global.d.ts b/src/components/global.d.ts new file mode 100644 index 0000000..8c9bf89 --- /dev/null +++ b/src/components/global.d.ts @@ -0,0 +1,2 @@ +declare module "*.scss"; +declare module "*.svg"; diff --git a/src/components/information.scss b/src/components/information.scss new file mode 100644 index 0000000..1aaf93b --- /dev/null +++ b/src/components/information.scss @@ -0,0 +1,43 @@ +@import "./vars.scss"; + +.popUpContent { + position: absolute; + left: 12px; + right: 12px; + top: 35px; + z-index: 3; + + display: flex; + flex-direction: column; + overflow-y: hidden; + background-color: white; + + border: 0 solid $teal-dark; + border-radius: 6px; + + max-height: 1000px; + border-width: 1px; +} + +.popUpFooter { + flex-basis: 50px; + .controlRow { + display: block; + text-align: right; + } +} + +.cover { + position: absolute; + background-color: rgba(0,0,0,0.32); + z-index: 2; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.popUpBody { + background-color: white; + padding: 12px; +} \ No newline at end of file diff --git a/src/components/information.tsx b/src/components/information.tsx new file mode 100644 index 0000000..f06d75a --- /dev/null +++ b/src/components/information.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import css from "./information.scss"; + +interface IProps { + closeInfo: () => void; +} + +export const Information: React.FC = (props) => { + const {closeInfo} = props; + return ( + <> +
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ipsum velit, pellentesque eget turpis non, vestibulum egestas tellus. Fusce sed dolor hendrerit, rutrum ligula et, imperdiet est. Nam tincidunt leo a ultricies elementum. Quisque ornare eget massa ac congue. Curabitur et nisi orci. Nulla mollis lacus eu velit mollis, in vehicula lectus ultricies. Nulla facilisi. +
+
+
+ +
+
+
+
+ + ); +}; diff --git a/src/components/vars.scss b/src/components/vars.scss new file mode 100644 index 0000000..0d05c95 --- /dev/null +++ b/src/components/vars.scss @@ -0,0 +1,8 @@ + +$text: #222222; +$warning-color: #c5b200; +$teal-dark: #177991; +$teal-light: #72bfca; +$teal-dark-75: rgba(23, 121, 145, 0.75); +$teal-light-25: #72bfca40; +$teal-light-12: rgba(114, 191, 202, 0.12); diff --git a/src/index.css b/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/src/index.html b/src/index.html index 64373d0..b16d716 100644 --- a/src/index.html +++ b/src/index.html @@ -1,12 +1,13 @@ + NASS Plugin - +
- + \ No newline at end of file diff --git a/src/index.scss b/src/index.scss new file mode 100644 index 0000000..0447d35 --- /dev/null +++ b/src/index.scss @@ -0,0 +1,3 @@ +body { + font-family: 'Montserrat', sans-serif; +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index abcfb75..a41f680 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,7 @@ import React from "react"; import { render } from "react-dom"; import App from "./components/app"; -import "./index.css"; +import "./index.scss"; render( , diff --git a/src/scripts/Attribute.js b/src/scripts/Attribute.js new file mode 100644 index 0000000..decdd8d --- /dev/null +++ b/src/scripts/Attribute.js @@ -0,0 +1,174 @@ +/* + * ========================================================================== + * Copyright (c) 2018 by eeps media. + * Last modified 8/21/18 8:16 PM + * + * Created by Tim Erickson on 8/21/18 8:16 PM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * ========================================================================== + * + */ + + +class Attribute { + + constructor(iRecord, iAttributeAssignment, attributeMap) { + if (iAttributeAssignment) { + if (iAttributeAssignment.displayMe == null) iAttributeAssignment.displayMe = true; + } else { + iAttributeAssignment = {displayMe: false}; + } + if (!iRecord) {iRecord = {};} + this.name = iRecord.name; + // Starting position in the data string. Value comes from codebook. + this.startPos = (iRecord.startPos != null)?Number(iRecord.startPos):undefined; + // Width in characters in the data string. Value comes from codebook + this.width = iRecord.width; + // Whether categorical or numeric. Codebook value can be overridden + this.format = iAttributeAssignment.format || iRecord.format; + // If categorical, mapping of numeric codes to string values. + this.categories = iAttributeAssignment.categories || iRecord.categories; + // Attributes are grouped + this.groupNumber = iAttributeAssignment.group; + // Description of attribute + this.description = iAttributeAssignment.description || iRecord.description; + // no longer used + this.chosen = iAttributeAssignment.defCheck; + // can't be changed + this.readonly = iAttributeAssignment.readonly; + + this.displayMe = iAttributeAssignment.displayMe; //Boolean(iRecord.defshow); + this.hasCheckbox = this.displayMe; + // if the order of attribute is important we can convey the order of categories to CODAP + this.hasCategoryMap = iAttributeAssignment.hasCategoryMap; + // remapping of numeric codes to be applied before mapping to a category string + this.rangeMap = iAttributeAssignment.rangeMap; + this.multirangeMap = iAttributeAssignment.multirangeMap; + + // title is the CODAP attribute string + this.title = iAttributeAssignment.title || iRecord.labl; + if (!this.title) { + this.title = this.name; + } + // the DOM element ID + this.checkboxID = 'attr-' + this.title; + // we can create formula based attributes. A formula attribute is derived from + // values of other attributes as known to CODAP, and not directly from the source data. + this.formula = iAttributeAssignment.formula; + // a comma delimited list of attributes that must be present in the document + // along with this attribute + this.formulaDependents = iAttributeAssignment.formulaDependents; + + this.originalAttr = iAttributeAssignment.originalAttr; + this.attributeMap = attributeMap; + } + + getRawValue(dataObject) { + return dataObject[this.name]; + } + + isRecode() { + return (this.rangeMap || this.multirangeMap); + } + + getAttributesRawValue (attributeName, dataString) { + if (this.startPos != null) { + return this.getRawValue(dataString); + } + else { + let attribute = this.attributeMap && this.attributeMap[attributeName]; + return attribute && attribute.getRawValue(dataString); + } + } + recodeValue(dataString) { + let result = null; + let originalAttr = this.originalAttr; + let found = null; + if (this.multirangeMap) { + if (!Array.isArray(originalAttr)) {originalAttr = [originalAttr];} + let rawValues = this.originalAttr.map(function (name) { + return this.getAttributesRawValue(name, dataString); + }.bind(this)); + found = this.multirangeMap.find(function (constraint) { + return originalAttr.reduce(function(prior, attrName, attrIx) { + let value = rawValues[attrIx]; + let test = constraint.range[attrName]; + if (!(value && value.trim().length)) { + return false; + } + if (test) { + return prior && (test.from <= value && test.to >= value); + } else { + return prior; + } + }, true); + }); + } + else if (this.rangeMap) { + let rawValue = this.getAttributesRawValue(originalAttr, dataString); + if (rawValue.trim()) { + found = this.rangeMap.find(function(range) { + return (range.from<=rawValue && range.to >= rawValue); + }); + } + } + if (found) { + result = found.recodeTo; + } + return result; + } + + decodeValue(dataString) { + let result; + let rawValue; + if (this.isRecode()) { + rawValue = this.recodeValue(dataString); + } else { + rawValue = this.getRawValue(dataString); + } + + if (rawValue == null) { + result = ''; + } else if (this.format === 'categorical' && this.categories) { + result = this.categories[Number(rawValue)]; + if (result == null) {result = rawValue;} + } else { + result = rawValue; + } + return result; + } + + /** + * Creates a category map based on the categories listed and assigning colors + * in order. + */ + getCategoryMap () { + const kKellyColors = [ + '#FFB300', '#803E75', '#FF6800', '#A6BDD7', '#C10020', '#CEA262', + '#817066', '#007D34', '#00538A', '#F13A13', '#53377A', '#FF8E00', + '#B32851', '#F4C800', '#7F180D', '#93AA00', '#593315', '#232C16', + '#FF7A5C', '#F6768E']; + let order = Array.isArray(this.categories)? this.categories: Object.values(this.categories); + let categoryMap = {}; + let index = 0; + if (order) { + order.forEach(function (categoryName) { + categoryMap[categoryName] = kKellyColors[index++ % kKellyColors.length]; + }); + } + categoryMap.__order = order; + return categoryMap; + } +} + +export {Attribute}; diff --git a/src/scripts/app.CODAPconnect.js b/src/scripts/app.CODAPconnect.js new file mode 100644 index 0000000..e17b3eb --- /dev/null +++ b/src/scripts/app.CODAPconnect.js @@ -0,0 +1,266 @@ +/* + * ========================================================================== + * Copyright (c) 2018 by eeps media. + * Last modified 8/21/18 8:32 AM + * + * Created by Tim Erickson on 8/21/18 8:32 AM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + * ========================================================================== + * + */ + +// import codapInterface from "../common/codapInterface"; +import {constants} from "./app.constants.js"; + +let CODAPconnect = { + + initialize: async function (/*iCallback*/) { + try { + await codapInterface.init(this.iFrameDescriptor, null); + } catch (e) { + console.log('Error connecting to CODAP: ' + e); + window.app.state = Object.assign({}, window.app.freshState); + return; + } + // restore the state if possible + + app.state = await codapInterface.getInteractiveState(); + + if (jQuery.isEmptyObject(app.state)) { + await codapInterface.updateInteractiveState(app.freshState); + console.log("app: getting a fresh state"); + } + console.log("app.state is " + JSON.stringify(app.state)); // .length + " chars"); + + // now update the iframe to be mutable... + + const tMessage = { + "action": "update", + "resource": "interactiveFrame", + "values": { + "preventBringToFront": false, + "preventDataContextReorg": false + } + }; + + return await codapInterface.sendRequest(tMessage); + }, + + logAction: function (iMessage) { + codapInterface.sendRequest({ + action: 'notify', + resource: 'logMessage', + values: { + formatStr: iMessage + } + }); + }, + + makeCODAPAttributeDef: function (attr) { + return { + name: attr.title, + title: attr.title, + description: attr.description, + type: attr.format, + formula: attr.formula + } + }, + + saveCasesToCODAP: async function (iValues) { + const makeItemsMessage = { + action : "create", + resource : "dataContext[" + constants.datasetName + "].item", + values : iValues + }; + + return await codapInterface.sendRequest(makeItemsMessage); + }, + + deleteAllCases: async function () { + let theMessage = { + action: 'delete', + resource : "dataContext[" + constants.datasetName + "].allCases" + }; + return await codapInterface.sendRequest(theMessage); + }, + + guaranteeDataset: async function () { + let datasetResource = 'dataContext[' + constants.datasetName + + ']'; + let response = await codapInterface.sendRequest({ + action: 'get', + resource: datasetResource}); + if (!response.success) { + await this.createNewMicrodataDataset(app.allAttributes); + response = await codapInterface.sendRequest({ + action: 'get', + resource: datasetResource}); + } + return await this.makeNewAttributesIfNecessary(); + }, + + makeNewAttributesIfNecessary : async function() { + async function getCODAPAttrList() { + let attrListResource = 'dataContext[' + constants.datasetName + + ']'; + let response = + await codapInterface.sendRequest({ + action: 'get', + resource: attrListResource}); + if (response.success) { + let attrArrays = response.values.collections.map(function (collection) { + collection.attrs.forEach(function (attr) { + attr.collectionID = collection.guid; + }); + return collection.attrs; + }); + return attrArrays.flat(); + } + } + + let theAttributes = app.state.selectedAttributes.map(function (attrName) { + return app.allAttributes[attrName]; + }); + let existingAttributeList = await getCODAPAttrList(); + let existingAttributeNames = existingAttributeList.map(function (attr) { + return attr.title; + }); + let codapRequests = []; + + theAttributes.forEach(function (attr) { + if (!existingAttributeNames.includes(attr.title)) { + let attrResource = 'dataContext[' + constants.datasetName + '].collection[' + + constants.datasetChildCollectionName + '].attribute'; + let req = { + action: 'create', + resource: attrResource, + values: this.makeCODAPAttributeDef(attr) + }; + if (attr.hasCategoryMap) { + req.values._categoryMap = attr.getCategoryMap(); + } + codapRequests.push(req); + } + }.bind(this)); + if (app.state.priorAttributes) { + app.state.priorAttributes.forEach(function (attrName) { + if (!app.state.selectedAttributes.includes(attrName)) { + let codapAttr = existingAttributeList.find(function (cAttr) {return attrName === cAttr.name;}); + if (codapAttr) { + let attrResource = 'dataContext[' + constants.datasetName + + '].collection[' + codapAttr.collectionID + '].attribute[' + codapAttr.name + ']'; + let req = { + action: 'delete', resource: attrResource + }; + codapRequests.push(req); + } + } + }); + } + app.state.priorAttributes = app.state.selectedAttributes.slice(); + if (codapRequests.length > 0) { + return await codapInterface.sendRequest(codapRequests); + } else { + return {success: true}; + } + }, + + makeCaseTableAppear : async function() { + const theMessage = { + action : "create", + resource : "component", + values : { + type : 'caseTable', + dataContext : constants.datasetName, + name : constants.caseTableName, + 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); + } + return makeCaseTableResult.success && makeCaseTableResult.values.id; + }, + + autoscaleComponent: async function (name) { + return await codapInterface.sendRequest({ + action: 'notify', + resource: `component[${name}]`, + values: { + request: 'autoScale' + } + }) + }, + + createNewMicrodataDataset: async function (attributeList) { + + return codapInterface.sendRequest({ + action: 'create', + resource: 'dataContext', + values: { + name: constants.datasetName, + title: constants.datasetTitle, + description: constants.datasetDescription, + collections: [{ + name: constants.datasetParentCollectionName, + attrs: [this.makeCODAPAttributeDef( + attributeList['State']), this.makeCODAPAttributeDef( + attributeList['Boundaries'])] + }, { + name: constants.datasetChildCollectionName, + parent: constants.datasetParentCollectionName, + labels: { + singleCase: "person", + pluralCase: "people", + setOfCasesWithArticle: "a sample of people" + }, + + attrs: [ // note how this is an array of objects. + {name: "sample", type: "categorical", description: "sample number"},] + }] + } + }); + }, + myCODAPIDd: null, + selectSelf: async function () { + if (this.myCODAPId == null) { + let r1 = await codapInterface.sendRequest({action: 'get', resource: 'interactiveFrame'}); + if (r1.success) { + this.myCODAPId = r1.values.id; + } + } + if (this.myCODAPId != null) { + return await codapInterface.sendRequest({ + action: 'notify', + resource: `component[${this.myCODAPId}]`, + values: {request: 'select' + } + }); + } + }, + + iFrameDescriptor: { + version: constants.version, + name: constants.appName, + title: constants.appTitle, + dimensions: { + width: constants.appDefaultWidth, + height: constants.appDefaultHeight + }, + preventDataContextReorg: false, + cannotClose: false + } +}; + +export {CODAPconnect}; diff --git a/src/scripts/app.DBconnect.js b/src/scripts/app.DBconnect.js new file mode 100644 index 0000000..dd8d5f8 --- /dev/null +++ b/src/scripts/app.DBconnect.js @@ -0,0 +1,171 @@ +/* + * ========================================================================== + * Copyright (c) 2018 by eeps media. + * Last modified 8/21/18 8:32 AM + * + * Created by Tim Erickson on 8/21/18 8:32 AM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + * ========================================================================== + * + */ +import {userActions} from "./app.userActions.js"; + +/*global Papa:true */ +let DBconnect = { + + /** + * Retrieves sample data from the server. + * + * Will retrieve a subsample for each state/year combination. + * Each retrieval will return 1000 records. The actual number needed will be + * randomly selected (without replacement) from this set. + * + * @param iAtts Selected attributes. Unused in current implementation. + * @param iStateCodes + * @param iYears + * @param iAllAttributes {{}} All possible attributes indexed by attribute name. + * @return {Promise<*[]|[]>} + */ + getCasesFromDB: async function (iAtts, iStateCodes, iYears, iAllAttributes) { + function computeSubsample(data, size) { + if (data.length <= size) { + return data; + } + let randomizedData = data.map(function (item) { return {r: Math.random(), d: item};}); + randomizedData.sort(function(a, b) { return a.r - b.r; }); + let filteredData = randomizedData.filter(function (item, ix) {return (ix < size);}) + return filteredData.map(function (item) { return item.d;}); + } + + function fetchSubsampleChunk(stateName, year, chunkSize) { + return new Promise(function (resolve, reject) { + try { + let dataset = _this.metadata.datasets.find(function (ds) {return ds.name === String(year);}) + let presetCount = dataset && dataset.presetCount; + let presetIndex = Math.floor(Math.random() * presetCount); + let filePrefix = _this.metadata.filenamePrefix || 'preset-'; + let fileSuffix = _this.metadata.filenameSuffix || '.csv'; + let presetName = filePrefix + presetIndex + fileSuffix; + let dataExistsForYearAndState = _this.yearHasState(year, stateName); + if (dataExistsForYearAndState) { + let presetURL = `${_this.metadata.baseURL}/${year}/${stateName}/${presetName}`; + + // fetch chunks then randomly pick selection set. + app.addLog('Send request: ' + presetURL); + Papa.parse(presetURL, { + header: true, /* converts CSV rows to objects as defined by the header line */ + download: true, /* indicates this is a url to fetch */ + complete: function (response) { + if (response.errors.length === 0) { + app.addLog('Good response: ' + (response.data?response.data.length: '')); + resolve(computeSubsample(response.data, chunkSize)); + } else { + let msg = `Errors fetching ${presetURL}: ${response.errors.join( + ', ')}`; + app.addLog(msg); + reject(msg); + } + }, error: function (error/*, file*/) { + app.addLog(error); + reject(error); + } + }) + } else { + resolve([]); + } + } catch(ex) { + reject(ex); + } + }); + } + + + let _this = this; + const tSampleSize = userActions.getSelectedSampleSize(); + + iStateCodes = iStateCodes || []; + let stateAttribute = iAllAttributes.State; + let stateMap = stateAttribute.categories; + let stateNames = iStateCodes.length? + iStateCodes.map(function (sc) { return stateMap[sc]; }): + ['all']; + + iYears = iYears || []; + + let chunks = stateNames.length * iYears.length; + if (!chunks) { + return Promise.resolve([]); + } + let chunkSize = tSampleSize/chunks; + let fetchPromises = []; + stateNames.forEach(function (stateName) { + iYears.forEach(function (year) { + // if chunk size is large get double, so we can get a unique subsample + if (chunkSize > 900) { + fetchPromises.push(fetchSubsampleChunk(stateName, year, chunkSize/2)); + fetchPromises.push(fetchSubsampleChunk(stateName, year, chunkSize/2)); + } else { + fetchPromises.push(fetchSubsampleChunk(stateName, year, chunkSize)); + } + }); + }); + return Promise.all(fetchPromises); + }, + + getDatasetNames: function () { + if (this.metadata.datasets) { + let names = this.metadata.datasets.map(function (ds) { return ds.name;}); + return names; + } else { + return []; + } + }, + getStateNames: function () { + let stateSet = {}; + if (this.metadata.datasets) { + this.metadata.datasets.forEach(function (ds) { + ds.presetCollections.forEach(function (name) { + stateSet[name] = name; + }); + }); + return Object.keys(stateSet); + } else { + return []; + } + }, + + yearHasState: function (year, stateName) { + year = String(year); + if (this.metadata.datasets) { + let yearDataset = this.metadata.datasets.find(function (ds) { return ds.name === year}); + return (yearDataset && (yearDataset.presetCollections.indexOf(stateName) >= 0)); + } + }, + + getDBInfo: async function (iType, metadataURL) { + if (!this.metadata) { + let response = await fetch(metadataURL); + if (response.ok) { + this.metadata = await response.json(); + } else { + this.metadata = {}; + console.warn(`Metadata Fetch error: ${response.statusText}`); + } + } + if (iType === 'getYears') { + return this.getDatasetNames(); + } + else if (iType === 'getStates') { + return this.getStateNames(); + } + } +}; + +export {DBconnect}; diff --git a/src/scripts/app.constants.js b/src/scripts/app.constants.js new file mode 100644 index 0000000..515c780 --- /dev/null +++ b/src/scripts/app.constants.js @@ -0,0 +1,42 @@ +/* + * ========================================================================== + * Copyright (c) 2018 by eeps media. + * Last modified 8/21/18 9:18 AM + * + * Created by Tim Erickson on 8/21/18 9:18 AM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + * ========================================================================== + * + */ +let constants = { + version: "v0006", + appName: 'Microdata Portal', + appTitle: 'Microdata Portal', + appDefaultWidth: 380, + appDefaultHeight: 520, + + metadataURL: './assets/data/metadata.json', + + datasetName: "US Microdata", + datasetTitle: "US Microdata", + datasetDescription: 'US Population Microdata from the Microdata Portal', + datasetParentCollectionName: "places", + datasetChildCollectionName: "people", + caseTableName: "People", + + kMinCases: 0, + kMaxCases: 1000, + kDefaultSampleSize: 100, + defaultSelectedYears: [2017], + defaultSelectedStates: [], + defaultSelectedAttributes: ['Sex', 'Age', 'Year', 'State', 'Boundaries'] +}; + +export {constants}; diff --git a/src/scripts/app.js b/src/scripts/app.js new file mode 100644 index 0000000..f4498fb --- /dev/null +++ b/src/scripts/app.js @@ -0,0 +1,146 @@ +/* + * ========================================================================== + * Copyright (c) 2018 by eeps media. + * Last modified 8/18/18 9:03 PM + * + * Created by Tim Erickson on 8/18/18 9:03 PM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * ========================================================================== + * + */ +/* global: xml2js */ + +import * as attributeConfig from './attributeConfig.js'; +import {ui} from './app.ui.js'; +import {userActions} from "./app.userActions.js"; +import {CODAPconnect} from "./app.CODAPconnect.js"; +import {DBconnect} from "./app.DBconnect.js"; +import {Attribute} from "./Attribute.js" +import {constants} from "./app.constants.js"; + +window.app = { + state: null, + allAttributes: {}, // object containing all Attributes (a class), keyed by NAME. + + freshState: { + sampleNumber: 1, + sampleSize: 16, + selectedYears: constants.defaultSelectedYears, + selectedStates: constants.defaultSelectedStates, + selectedAttributes: constants.defaultSelectedAttributes, + keepExistingData: false, + activityLog: [] + }, + + logConnectionInfo: function () { + let info = navigator.connection || navigator.mozConnection || navigator.webkitConnection; + if (info) { + this.addLog('Connection: ' + [info.type, info.effectiveType, + info.saveData, info.rtt, info.downlink, info.downlinkMax].join('/') ); + ui.updateWholeUI(); + } + }, + + initialize: async function () { + // function handleError(message) { + // console.warn("Initializing Microdata Portal: " + message); + // } + ui.displayStatus('initializing', "Initializing"); + await CODAPconnect.initialize(null); + app.logConnectionInfo(); + await app.getAllAttributes(); + app.years = await DBconnect.getDBInfo("getYears", constants.metadataURL); + app.states = await DBconnect.getDBInfo('getStates', constants.metadataURL); + ui.init(); + ui.displayStatus('success', "Ready"); + }, + + updateStateFromDOM: function (logMessage) { + if (!this.state) { + // initialize state from CODAP, then update state + } + else { + this.state.selectedYears = userActions.getSelectedYears(); + this.state.selectedStates = userActions.getSelectedStates(); + this.state.selectedAttributes = userActions.getSelectedAttrs(); + this.state.requestedSampleSize = userActions.getRequestedSampleSize(); + if (logMessage) { + CODAPconnect.logAction(logMessage); + } + } + ui.updateWholeUI(); + }, + + addLog: function (logMessage) { + if (this.state) { + if (!this.state.activityLog) { + this.state.activityLog = []; + } + this.state.activityLog.push({time:new Date().toLocaleString(), message: logMessage}); + } + }, + + getDataDictionary: function (codebook) { + const kSpecialNumeric = ['FAMSIZE', 'AGE']; + let tCbkObject = xml2js(codebook, {}), + tDescriptions = tCbkObject.elements[1].elements[3]; + return tDescriptions.elements.map(function (iDesc) { + let tCats = {}; + iDesc.elements.forEach(function (iElement) { + if (iElement.name === 'catgry') { + let key = Number(iElement.elements[0].elements[0].text); + tCats[key] = iElement.elements[1].elements[0].text; + } + }); + return { + name: iDesc.attributes.name, + startPos: iDesc.elements[0].attributes.StartPos, + width: Number(iDesc.elements[0].attributes.width), + labl: iDesc.elements[1].elements[0].text, + description: iDesc.elements[2].elements[0].cdata, + format: (Object.keys(tCats).length === 0 || kSpecialNumeric.indexOf( + iDesc.attributes.name) >= 0) ? 'numeric' : 'categorical', + categories: tCats + } + }); + }, + + getPartitionCount: function () { + let numStates = app.state.selectedStates.length || 1; + let numYears = app.state.selectedYears.length || 1; + return numStates * numYears; + }, + + getAllAttributes: async function () { + let result = await fetch('./assets/data/codebook.xml'); + if (result.ok) { + let codeBook = await result.text(); + let dataDictionary = this.getDataDictionary(codeBook); + attributeConfig.attributeAssignment.forEach(function (configAttr) { + let codebookDef = dataDictionary.find(function (def) { + return def.name === configAttr.ipumsName; + }); + let tA = new Attribute(codebookDef, configAttr, app.allAttributes); + app.allAttributes[tA.title] = tA; + }); + + $("#chooseAttributeDiv").html(ui.makeAttributeListHTML()); + return app.allAttributes; + } else { + console.log('CodeBook fetch failed'); + } + } + +}; + +app.initialize(); diff --git a/src/scripts/app.ui.js b/src/scripts/app.ui.js new file mode 100644 index 0000000..49b5878 --- /dev/null +++ b/src/scripts/app.ui.js @@ -0,0 +1,420 @@ +/* + * ========================================================================== + * Copyright (c) 2018 by eeps media. + * Last modified 8/21/18 9:10 AM + * + * Created by Tim Erickson on 8/21/18 9:10 AM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + * ========================================================================== + * + */ +/* global app */ + +import * as attributeConfig from './attributeConfig.js'; +import {constants} from './app.constants.js'; +import {userActions} from "./app.userActions.js"; + +let ui = (function () { + function findAncestorElementWithClass(el, myClass) { + while (el !== null && el.parentElement !== el) { + if (el.classList.contains(myClass)) { + return el; + } + el = el.parentElement; + } + } + + function setEventHandler (selector, event, handler) { + const elements = document.querySelectorAll(selector); + if (!elements) { return; } + elements.forEach(function (el) { + el.addEventListener(event, handler); + }); + } + + function toggleClass(el, myClass) { + let isOpen = el.classList.contains(myClass); + if (isOpen) { + el.classList.remove(myClass); + } else { + el.classList.add(myClass); + } + } + + function togglePopUp(el) { + toggleClass(el, 'wx-open'); + } + + function togglePopOver(el) { + toggleClass(el, 'wx-open'); + } + + function toggleDescriptions(el) { + toggleClass(el, 'show-descriptions'); + } + + /** + * A utility to create a DOM element with classes and content. + * @param tag {string} + * @param [classList] {[string]} + * @param [content] {[Node]} + * @return {Element} + */ + function createElement(tag, classList, content) { + let el = document.createElement(tag); + if (classList) { + if (typeof classList === 'string') classList = [classList]; + classList.forEach( function (cl) {el.classList.add(cl);}); + } + if (content) { + if (!Array.isArray(content)) { content = [content];} + content.forEach(function(c) { + if (c instanceof Attr) { + el.setAttributeNode(c); + } else { + el.append(c); + } + }); + } + return el; + } + + /** + * A utility to create a DOM attribute node. + * @param name {string} + * @param value {*} + * @return {Attr} + */ + function createAttribute(name, value) { + let attr = document.createAttribute(name); + attr.value = value; + return attr; + } + + + return { + initialized: false, + + init: function () { + setEventHandler('#chooseAttributeDiv input', 'change', + userActions.changeAttributeCheckbox) + + $('#chooseSampleYearsDiv').html(ui.makeYearListHTML()); + + setEventHandler('#chooseSampleYearsDiv input', 'change', + userActions.changeSampleYearsCheckbox); + + document.getElementById('chooseStatesDiv').append(ui.makeStateList()); + + setEventHandler('#chooseStatesDiv input','change', + userActions.changeSampleStateCheckbox); + + setEventHandler('#sampleSizeInput', 'change', function (/*ev*/) { + userActions.updateRequestedSampleSize('Sample size change.'); + }); + + setEventHandler('#getCasesButton', 'click', + userActions.pressGetCasesButton); + + setEventHandler('#keepExistingDataCheckbox', 'change', + userActions.getKeepExistingDataOption); + + setEventHandler('.wx-dropdown-header', 'click', function (/*ev*/) { + let dropdownGroup = findAncestorElementWithClass(this, 'wx-dropdown-group'); + let sectionEl = findAncestorElementWithClass(this, 'wx-dropdown'); + let isClosed = sectionEl.classList.contains('wx-up'); + let dropDowns = (dropdownGroup||document).querySelectorAll('.wx-dropdown.wx-down'); + if (dropDowns) { + dropDowns.forEach(function (el) { + el.classList.remove('wx-down'); + el.classList.add('wx-up'); + }) + } + if (isClosed) { + sectionEl.classList.remove('wx-up'); + sectionEl.classList.add('wx-down'); + } + }); + + setEventHandler('.wx-pop-up-anchor,#wx-info-close-button', 'click', function (/*ev*/) { + let parentEl = findAncestorElementWithClass(this, 'wx-pop-up'); + togglePopUp(parentEl); + }); + + setEventHandler('.wx-pop-over-anchor', 'click', function (/*ev*/) { + let parentEl = findAncestorElementWithClass(this, 'wx-pop-over'); + togglePopOver(parentEl); + }); + + setEventHandler('.show-attr-description-checkbox', 'click', function (/*ev*/) { + let parentEl = findAncestorElementWithClass(this, 'attributeCheckboxes'); + toggleDescriptions(parentEl); + }); + + setEventHandler('html', 'click', userActions.selectHandler, true); + + this.initialized = true; + this.updateWholeUI(); + }, + + makeStateList: function () { + function makeItem(label, value, myClass, checked) { + let inputEl = createElement('input', [myClass], + [ + createAttribute('type', 'radio'), + createAttribute('name', 'state'), + createAttribute('id', value) + ] + ); + if (checked) { + inputEl.setAttributeNode(createAttribute('checked', 'checked')); + } + return createElement('label', null, [inputEl, label]); + } + + let out = createElement('div'); + out.append(makeItem('all states', 'state-all', 'select-all', true)); + // noinspection JSUnresolvedVariable + let stateAttribute = app.allAttributes.State; + let stateMap = stateAttribute.categories; + Object.keys(stateMap).forEach(function (stateCode) { + let stateName = stateMap[stateCode]; + if (app.states.indexOf(stateName) >= 0) { + let id = 'state-' + stateCode; + out.append(makeItem(stateName, id, 'select-item', false)); + } + }); + return out; + }, + + makeYearListHTML: function () { + let out = ''; + let checked = ''; + if (app.years) { + app.years.forEach(function (year) { + let id = 'year-' + year; + out += '
' + '
'; + checked = ''; + }); + } + return out; + }, + + makeAttributeListHTML: function () { + let out = ""; + + attributeConfig.attributeGroups.forEach( (g)=>{ + out += '
\n'; + out += `
`; + out += ` ${g.title}`; + out += ' '; + out += ' '; + out += ' '; + out += '
'; + out += '
'; + out += this.makeAttributeGroupHTML(g); + out += '
'; + out += '
'; + }); + + return out; + }, + + makeAttributeGroupHTML: function (iGroupObject) { + let out = ""; + + out += '\n'; + out += '' + + ''; + for (let attName in app.allAttributes) { + if (app.allAttributes.hasOwnProperty(attName)) { + const tAtt = app.allAttributes[attName]; // the attribute + let tReadonlyClause = tAtt.readonly? 'disabled="true" readonly="true"': ''; + // noinspection EqualityComparisonWithCoercionJS + if (tAtt.groupNumber == iGroupObject.number) { // not === because one may be a string + if (tAtt.displayMe) { + tAtt.hasCheckbox = true; // redundant + out += ''; + out += '\n'; + out += '\n"; + } + } + } + } + out += "
 Attribute
' + + tAtt.title + '
'; + out += '
' + tAtt.description + '
\n'; + out += "
\n"; + return out; + }, + + updateWholeUI: function () { + if (!this.initialized) return; + ui.refreshAttributeCheckboxes(); + ui.refreshStateCheckboxes(); + ui.refreshYearCheckboxes(); + ui.refreshSampleSummary(); + ui.refreshText(); + ui.refreshLog(); + }, + + refreshAttributeCheckboxes: function () { + let activeAttributes = app.state.selectedAttributes; + $('#chooseAttributeDiv .select-item').prop('checked', false); + if (activeAttributes) { + activeAttributes.forEach(function (attrName) { + $('#attr-' + attrName).prop('checked', true); + }); + } + + Object.values(app.allAttributes).forEach(function (attr) { + if (attr.formulaDependents) { + let $el = $('#' + attr.checkboxID); + if (!this.checkDependentSelected(attr.formulaDependents.split(','))) { + $el.prop('checked', false); + $el.prop('disabled', true); + } else { + $el.prop('disabled', false); + } + } + }.bind(this)); + }, + + refreshStateCheckboxes: function () { + let activeStateCodes = app.state.selectedStates; + $('#states .select-item, #states .select-all').prop('checked', false); + if (!activeStateCodes || (activeStateCodes.length === 0)) { + $('#state-all').prop('checked', true); + } else { + activeStateCodes.forEach(function (stateCode) { + $('#state-' + stateCode).prop('checked', true); + }); + } + }, + + refreshYearCheckboxes: function () { + let activeYears = app.state.selectedYears; + $('#sampleYears .select-item').prop('checked', false); + if (activeYears) { + activeYears.forEach(function (year) { + $('#year-' + year).prop('checked', true); + }); + } + }, + + refreshSampleSummary: function () { + function makeList(array) { + let rtn = ''; + if (array && array.length > 0) { + let length = array.length; + if (length === 1) { + rtn = array[0]; + } else if (length === 2) { + rtn = `${array[0]} and ${array[1]}`; + } else { + rtn = array.join(', '); + } + } + return rtn; + } + + // noinspection JSUnresolvedVariable + const stateAttr = app.allAttributes.State; + let states = app.state.selectedStates.map(function (st) { return stateAttr.categories[Number(st)]; }); + + let statesLength = states.length || ' '; + if (states.length === 0) { + states = ['all']; + } + + let years = app.state.selectedYears; + let attrs = app.state.selectedAttributes; + + let attrCountEl = document.querySelector('#attribute-section .wx-selection-count'); + let attrListEl = document.querySelector('#attribute-section .wx-user-selection'); + let statesCountEl = document.querySelector('#states-section .wx-selection-count'); + let statesListEl = document.querySelector('#states-section .wx-user-selection'); + let yearsCountEl = document.querySelector('#years-section .wx-selection-count'); + let yearsListEl = document.querySelector('#years-section .wx-user-selection'); + + if (attrCountEl && attrs) attrCountEl.innerHTML = '' + attrs.length; + if (attrListEl && attrs) attrListEl.innerHTML = makeList(attrs); + if (statesCountEl && states) statesCountEl.innerHTML = statesLength; + if (statesListEl && states) statesListEl.innerHTML = makeList(states); + if (yearsCountEl && years) yearsCountEl.innerHTML = '' + years.length; + if (yearsListEl && years) yearsListEl.innerHTML = makeList(years); + let subsectionCountEls = document.querySelectorAll( + '#chooseAttributeDiv .wx-selection-count'); + + subsectionCountEls.forEach(function (el) { + let parentEl = findAncestorElementWithClass(el, 'wx-dropdown'); + let checkedEls = parentEl.querySelectorAll('.select-item:checked'); + el.innerHTML = '' + checkedEls.length; + }); + }, + + refreshText: function () { + $('#sampleSizeInput').val(app.state.requestedSampleSize || constants.kDefaultSampleSize); + $('#keepExistingDataCheckbox')[0].checked = app.state.keepExistingData; + }, + + refreshLog: function () { + let activityLog = app.state.activityLog; + let tabContentNode = $('#log .wx-dropdown-body'); + let tableRows = activityLog && activityLog.map(function (logEntry) { + return $('').append($('').text(logEntry.time)).append($('').text(logEntry.message)); + }); + let table = $('').append(tableRows); + tabContentNode.empty().append(table); + }, + + /** + * Expects an array of attr names. Returns true if they are all selected. + * @param dependents + */ + checkDependentSelected: function (dependents) { + let selectedAttributes = app.state && app.state.selectedAttributes; + if (!selectedAttributes) { + return false; + } + return dependents.every(function (dep) { + return (selectedAttributes.includes(dep)); + }); + }, + + /** + * + * @param status {'initializing', 'inactive', 'retrieving', 'transferring', 'success', 'failure'} + * @param message + */ + displayStatus: function (status, message) { + let el = document.querySelector('.wx-summary'); + let statusClass = { + initializing: 'wx-transfer-in-progress', + inactive: '', + retrieving: 'wx-transfer-in-progress', + transferring: 'wx-transfer-in-progress', + success: 'wx-transfer-success', + failure: 'wx-transfer-failure' + }[status]||''; + el.classList.remove( + 'wx-transfer-in-progress', + 'wx-transfer-success', + 'wx-transfer-failure'); + if (statusClass) { el.classList.add(statusClass); } + $('#status').text(message); + }, + + }; +})(); + +export {ui}; diff --git a/src/scripts/app.userActions.js b/src/scripts/app.userActions.js new file mode 100644 index 0000000..514bfd3 --- /dev/null +++ b/src/scripts/app.userActions.js @@ -0,0 +1,161 @@ +/* + * ========================================================================== + * Copyright (c) 2018 by eeps media. + * Last modified 8/21/18 9:07 AM + * + * Created by Tim Erickson on 8/21/18 9:07 AM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + * ========================================================================== + * + */ + +import {constants} from './app.constants.js'; +import {CODAPconnect} from "./app.CODAPconnect.js"; +import {DBconnect} from "./app.DBconnect.js"; +import {ui} from "./app.ui.js" + +let userActions = { + + pressGetCasesButton : async function() { + try { + console.log("get cases!"); + let oData = []; + ui.displayStatus('retrieving', 'Fetching data...'); + let tData = await DBconnect.getCasesFromDB(app.state.selectedAttributes, + app.state.selectedStates, app.state.selectedYears, app.allAttributes); + + // If tData is empty, there must have been an error. We are relying on + // lower layers to log the failure. + if (!tData) { + ui.displayStatus('failure', 'Fetch Error. Please retry.'); + ui.updateWholeUI(); + return; + } + + tData = tData.flat(1); + + let counter = 0; + // okay, tData is an Array of objects whose keys are the variable names. + // now we have to translate names and values... + ui.displayStatus('transferring', 'Formatting data...'); + tData.forEach( function(c) { + // c is a case object + let sampleData = c; + if (!sampleData) { return; } + let o = { sample : app.state.sampleNumber }; + app.state.selectedAttributes.forEach(function (attrTitle) { + let attr = app.allAttributes[attrTitle]; + o[attr.title] = attr.decodeValue(sampleData); + }); + oData.push(o); + counter ++; + }); + + await CODAPconnect.guaranteeDataset(); + + // make sure the case table is showing + ui.displayStatus('transferring', 'Opening case table...'); + let id = await CODAPconnect.makeCaseTableAppear(); + + ui.displayStatus('transferring', 'Sending data to codap...'); + if (!app.state.keepExistingData) { + await CODAPconnect.deleteAllCases(); + } + await CODAPconnect.saveCasesToCODAP( oData ); + ui.displayStatus('success', `Selected a random sample of ${counter} people`); + setTimeout(function () {CODAPconnect.autoscaleComponent(id);}, 1000); + app.state.sampleNumber++; + } catch (ex) { + console.log(ex); + ui.displayStatus('failure', 'Fetch Error. Please retry.'); + } + ui.updateWholeUI(); + }, + + changeAttributeCheckbox : function(/*iAttName*/) { + // const tAtt = app.allAttributes[iAttName]; + // + // tAtt.chosen = !tAtt.chosen; + app.updateStateFromDOM('Attribute selection changed.'); + }, + + changeSampleYearsCheckbox : function (/*event*/) { + app.updateStateFromDOM('Sample years changed.'); + }, + + changeSampleStateCheckbox : function (/*event*/) { + // record change of status for selected states and potentially toggle 'all' option + if ($(this).hasClass('select-all')) { + $('#chooseStatesDiv .select-item').prop('checked', false); + } + // noinspection JSJQueryEfficiency + let $itemBoxes = $('#chooseStatesDiv .select-item'); + let $allBox = $('#chooseStatesDiv .select-all'); + if ($itemBoxes.filter(':checked').length > 0) { + $allBox.prop('checked',false); + } else { + $allBox.prop('checked', true); + } + app.updateStateFromDOM('sample state changed'); + }, + + getSelectedAttrs: function () { + let rslt = []; + $('#chooseAttributeDiv .select-item:checked').each(function (ix, el) { + rslt.push(el.id.replace('attr-', '')); + }); + return rslt; + }, + + getSelectedYears: function () { + let rslt = []; + $('#chooseSampleYearsDiv .select-item:checked').each(function (ix, el) { + rslt.push(el.id.replace('year-', '')); + }); + return rslt; + }, + + getSelectedStates: function () { + let rslt = []; + $('#chooseStatesDiv .select-item:checked').each(function (ix, el) { + rslt.push(el.id.replace('state-', '')); + }); + return rslt; + }, + + /** + * This is the raw request, not the quantity we will actually return + */ + getRequestedSampleSize: function () { + return $("#sampleSizeInput")[0].value; + }, + + getSelectedSampleSize: function () { + let requestedSize = $("#sampleSizeInput")[0].value; + let numPartitions = app.getPartitionCount(); + let constrainedSize = Math.max(constants.kMinCases, Math.min(constants.kMaxCases, requestedSize)); + let partitionSize = Math.round(constrainedSize/numPartitions) || 1; + return partitionSize * numPartitions; + }, + + getKeepExistingDataOption: function () { + app.state.keepExistingData = $('#keepExistingDataCheckbox').is(':checked'); + }, + updateRequestedSampleSize: function () { + app.state.requestedSampleSize = $("#sampleSizeInput")[0].value; + ui.updateWholeUI(); + }, + selectHandler: function () { + CODAPconnect.selectSelf(); + } + +}; + +export {userActions}; diff --git a/src/scripts/attributeConfig.js b/src/scripts/attributeConfig.js new file mode 100644 index 0000000..8b06e68 --- /dev/null +++ b/src/scripts/attributeConfig.js @@ -0,0 +1,864 @@ +// ========================================================================== +// +// Author: jsandoe +// +// Copyright (c) 2018 by The Concord Consortium, Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================================================================== +/** + * + * Configuration properties. + */ + + /* + * The following declarations are specific to the data set. + * Should consider moving to separate json. + */ +let attributeGroups = [ + { + number: 1, + open: false, + title: 'Basic demographics', + tooltip: 'Choose characteristics like age, sex, and marital status' + }, + { + number: 2, + open: false, + title: 'Race, ancestry, origins', + tooltip: "Choose characteristics relating to person's place of birth and ethnicity" + }, + { + number: 3, + open: false, + title: 'Work & employment', + tooltip: "Choose characteristics relating to person's work" + }, + { + number: 4, + open: false, + title: 'Income', + tooltip: "Choose characteristics relating to person's income" + }, + { + number: 5, + open: false, + title: 'Geography', + tooltip: "Choose characteristics relating to where the person resides" + }, + { + number: 6, + open: false, + title: 'Other', + tooltip: "Choose other characteristics" + }]; + +let attributeAssignment = [{ + ipumsName: 'AGE', + title: 'Age', + group: 1, + defCheck: true, + description: 'The individual\'s age in years as of the ' + + 'last birthday. Values range from 0 (less than 1 year old) to 90 ' + + 'and above. See codebook for special codes.' + }, + { + title: 'Age_recode', + group: 1, + originalAttr: 'Age', + format: 'categorical', + defCheck: false, + description: 'The individual’s age in years as of the ' + + 'last birthday. Recodes the Age variable into 8 age categories.', + rangeMap: [ + {from: 0, to: 17, recodeTo: 0}, + {from: 18, to: 24, recodeTo: 1}, + {from: 25, to: 34, recodeTo: 2}, + {from: 35, to: 44, recodeTo: 3}, + {from: 45, to: 54, recodeTo: 4}, + {from: 55, to: 64, recodeTo: 5}, + {from: 65, to: 74, recodeTo: 6}, + {from: 75, to: 999, recodeTo: 7}, + ], + categories: [ + 'under 18', + '18-24', + '25-34', + '35-44', + '45-54', + '55-64', + '65-74', + '75 and older' + ] + }, + { + ipumsName: 'SEX', + title: 'Sex', + group: 1, + defCheck: true, + description: 'Each individual\'s biological sex as male or female.' + }, + { + ipumsName: 'MARST', + title: 'Marital_status', + group: 1, + defCheck: false, + description: 'Each individual’s current marital status, with ' + + '6 possible categories.' + }, + { + ipumsName: 'NCHILD', + title: 'Number_of_children', + group: 1, + defCheck: false, + description: 'Counts the number of own children (of any age or ' + + 'marital status) residing with each individual. Values range from ' + + '0 to a top code of 9+.' + }, + { + ipumsName: 'FAMSIZE', + title: 'Family_size', + group: 1, + defCheck: false, + description: 'The number of own family members residing ' + + 'with each individual, including the person her/himself. Values ' + + 'range from 1 to as high as 26 or higher in some years.' + }, + { + ipumsName: 'EDUC', + title: 'Education-years', + hasCategoryMap: true, + displayMe: false, + group: 1, + defCheck: false, + description: 'The individual’s level of educational ' + + 'attainment based on the highest level or year of school completed.', + categories: [ + "N/A or no schooling", + "Nursery school to grade 4", + "Grade 5, 6, 7, or 8", + "Grade 9", + "Grade 10", + "Grade 11", + "Grade 12", + "1 year of college", + "2 years of college", + "3 years of college", + "4 years of college", + "5+ years of college", + ] + }, + { + ipumsName: 'EDUCD', + title: 'Education-degree_recode', + group: 1, + format: 'categorical', + hasCategoryMap: true, + defCheck: false, + description: 'The individual’s level of educational ' + + 'attainment based on the highest degree completed, in years for which ' + + 'this information is available.', + rangeMap: [ + {from: 0, to: 2, recodeTo: 0}, + {from: 3, to: 50, recodeTo: 1}, + {from: 51, to: 59, recodeTo: 11}, + {from: 60, to: 60, recodeTo: 2}, + {from: 61, to: 61, recodeTo: 1}, + {from: 62, to: 64, recodeTo: 3}, + {from: 65, to: 71, recodeTo: 4}, + {from: 72, to: 79, recodeTo: 11}, + {from: 80, to: 80, recodeTo: 5}, + {from: 81, to: 83, recodeTo: 7}, + {from: 84, to: 89, recodeTo: 11}, + {from: 90, to: 90, recodeTo: 5}, + {from: 91, to: 99, recodeTo: 11}, + {from: 100, to: 100, recodeTo: 5}, + {from: 101, to: 101, recodeTo: 8}, + {from: 102, to: 109, recodeTo: 11}, + {from: 110, to: 113, recodeTo: 6}, + {from: 114, to: 115, recodeTo: 9}, + {from: 116, to: 116, recodeTo: 10}, + ], + categories: [ + 'N/A or no schooling completed', + 'Some schooling, no high school diploma', + 'Completed Grade 12, diploma not identified', + 'High school diploma or GED', + '1 or more years of college, no degree', + '2-4 years of college, degree not identified', + '5+ years of college, degree not identified', + 'Associate’s degree', + 'Bachelor’s degree', + 'Master’s or professional degree', + 'Doctoral degree', + 'unknown' + ] + }, + { + ipumsName: 'RACE', + title: 'Race-multi', + displayMe: false, + group: 2, + defCheck: false, + description: 'Each individual’s race according to 9 categories, ' + + 'including categories for mixed-race individuals. Caution needed when ' + + 'making comparisons over time.' + }, + { + ipumsName: 'RACESING', + title: 'Race-single', + group: 2, + displayMe: false, + defCheck: false, + description: 'Assigns individuals to one of 5 race categories and ' + + 'assigns a single race to multiple-race people. Comparable over time, ' + + 'but not available after 2014.' + }, + { + originalAttr: ['Race-multi','Hispanic'], + title: 'Race_ethnicity-multi_recode', + group: 2, + format: 'categorical', + defCheck: false, + description: 'Each respondent’s combined race and Hispanic ' + + 'ethnicity status, grouped into 7 primary categories. Caution needed ' + + 'when making comparisons over time.', + // formula: "(Hispanic!='Not Hispanic')?" + + // "'Hispanic':(`Race-multi`='White')?'Non-Hispanic White':" + + // "(`Race-multi`='Black/African American/Negro')?'Non-Hispanic Black':" + + // "(`Race-multi`='Other Asian or Pacific Islander')?'Non-Hispanic Asian or Pacific Islander':" + + // "(`Race-multi`='Chinese')?'Non-Hispanic Asian or Pacific Islander':" + + // "(`Race-multi`='Japanese')?'Non-Hispanic Asian or Pacific Islander':" + + // "(`Race-multi`='American Indian or Alaska Native')?'Non-Hispanic American Indian/Alaska Native':" + + // "(`Race-multi`='Three or more major races')?'Non-Hispanic two or more major races':" + + // "(`Race-multi`='Two major races')?'Non-Hispanic two or more major races':" + + // "'Non-Hispanic Other race'", + // formulaDependents: 'Hispanic,Race-multi', + multirangeMap: [ + {range:{Hispanic:{from:1, to:4}}, recodeTo: 0}, + {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 1, to: 1}}, recodeTo: 1}, + {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 2, to: 2}}, recodeTo: 2}, + {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 3, to: 3}}, recodeTo: 3}, + {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 4, to: 6}}, recodeTo: 4}, + {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 7, to: 7}}, recodeTo: 6}, + {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 8, to: 9}}, recodeTo: 5} + ], + categories: [ +/*0*/ 'Hispanic', +/*1*/ 'Non-Hispanic White', +/*2*/ 'Non-Hispanic Black', +/*3*/ 'Non-Hispanic American Indian/Alaska Native', +/*4*/ 'Non-Hispanic Asian and/or Pacific Islander', +/*5*/ 'Non-Hispanic two or more major races', +/*6*/ 'Non-Hispanic Other race' + ] + }, + { + originalAttr: ['Race-single', 'Hispanic'], + title: 'Race_ethnicity-single_recode', + group: 2, + format: 'categorical', + defCheck: false, + description: 'Each respondent’s combined race and Hispanic ' + + 'ethnicity status, grouped into 6 primary categories. Comparable over ' + + 'time, but not available after 2014.', + // formula: "(Hispanic!='Not Hispanic')?'Hispanic':" + + // "(`Race-single`='')?'Non-Hispanic':" + + // "(`Race-single`='White')?'Non-Hispanic White':" + + // "(`Race-single`='Black')?'Non-Hispanic Black':" + + // "(`Race-single`='American Indian/Alaska Native')?'Non-Hispanic American Indian/Alaska Native':" + + // "(`Race-single`='Asian and/or Pacific Islander')?'Non-Hispanic Asian or Pacific Islander':" + + // "(`Race-single`='Other race, non-Hispanic')?'Non-Hispanic Other race':" + + // "'Non-Hispanic Other'", + // formulaDependents: 'Hispanic,Race-single', + multirangeMap: [ + {range:{Hispanic:{from:1, to:4}}, recodeTo: 0}, + {range:{Hispanic:{from:0, to:0}, 'Race-single':{from: 1, to: 1}}, recodeTo: 1}, + {range:{Hispanic:{from:0, to:0}, 'Race-single':{from: 2, to: 2}}, recodeTo: 2}, + {range:{Hispanic:{from:0, to:0}, 'Race-single':{from: 3, to: 3}}, recodeTo: 3}, + {range:{Hispanic:{from:0, to:0}, 'Race-single':{from: 4, to: 4}}, recodeTo: 4}, + {range:{Hispanic:{from:0, to:0}, 'Race-single':{from: 5, to: 5}}, recodeTo: 5} + ], + categories: [ + 'Hispanic', + 'Non-Hispanic White', + 'Non-Hispanic Black', + 'Non-Hispanic American Indian/Alaska Native', + 'Non-Hispanic Asian and/or Pacific Islander', + 'Non-Hispanic Other race' + ] + }, + + { + ipumsName: 'HISPAN', + title: 'Hispanic', + group: 2, + defCheck: false, + description: 'Identifies individuals of Hispanic, Spanish, or Latino ' + + 'origin and classifies them according to their country of origin. ' + + 'The U.S. Census considers Hispanic origin to be an ethnic rather than a ' + + 'racial classification; individuals of Hispanic origin can therefore ' + + 'be of any race.' + }, + { + title: 'Hispanic_recode', + originalAttr: 'Hispanic', + group: 2, + defCheck: false, + description: 'Identifies whether individuals are ' + + 'of Hispanic, Spanish, or Latino origin. Recodes the Hispanic variable ' + + 'into two codes.', + format: 'categorical', + rangeMap: [ + {from: 0, to: 0, recodeTo: 0}, + {from: 1, to: 4, recodeTo: 1} + ], + categories: [ + 'Not of Hispanic, Spanish, or Latino origin', + 'Hispanic, Spanish, or Latino origin' + ] + }, + { + title: 'Hispanic-detailed_recode', + ipumsName: 'HISPAND', + group: 2, + defCheck: false, + format: 'categorical', + description: 'Identifies individuals of Hispanic, Spanish, or Latino ' + + 'origin and classifies them according to their country of origin. ' + + 'Recodes a detailed version of Hispanic into 15 categories.', + rangeMap: [ + {from: 0, to: 99, recodeTo: 0}, + {from: 100, to: 107, recodeTo: 1}, + {from: 108, to: 199, recodeTo: 15}, + {from: 200, to: 200, recodeTo: 2}, + {from: 201, to: 299, recodeTo: 15}, + {from: 300, to: 300, recodeTo: 4}, + {from: 301, to: 399, recodeTo: 15}, + {from: 401, to: 411, recodeTo: 8}, + {from: 412, to: 412, recodeTo: 5}, + {from: 413, to: 413, recodeTo: 6}, + {from: 414, to: 415, recodeTo: 8}, + {from: 416, to: 416, recodeTo: 7}, + {from: 417, to: 417, recodeTo: 8}, + {from: 418, to: 419, recodeTo: 15}, + {from: 420, to: 422, recodeTo: 12}, + {from: 423, to: 423, recodeTo: 9}, + {from: 424, to: 424, recodeTo: 10}, + {from: 425, to: 425, recodeTo: 12}, + {from: 426, to: 426, recodeTo: 11}, + {from: 427, to: 431, recodeTo: 12}, + {from: 432, to: 449, recodeTo: 15}, + {from: 450, to: 459, recodeTo: 13}, + {from: 460, to: 460, recodeTo: 3}, + {from: 461, to: 464, recodeTo: 15}, + {from: 465, to: 499, recodeTo: 14}, + {from: 500, to: 999, recodeTo: 15} + ], + categories: [ + 'Not of Hispanic, Spanish, or Latino origin', + 'Mexican', + 'Puerto Rican', + 'Dominican', + 'Cuban', + 'Guatemalan', + 'Honduran', + 'Salvadoran', + 'Other Central American (Costa Rican, Nicaraguan, Panamanian, and others)', + 'Columbian', + 'Equadoran', + 'Peruvian', + 'Other South American (Argentinean, Bolivian, Chilean, Paraguayan, ' + + 'Uruguayan, Venezuelan, and others)', + 'Spaniard', + 'Other Hispanic' + ] + }, + { + ipumsName: 'CITIZEN', + title: 'Citizen', + group: 2, + defCheck: false, + description: 'Identifies the citizenship status of individuals, with ' + + '6 categories.' + }, + { + originalAttr: 'Citizen', + title: 'Citizen_recode', + group: 2, + defCheck: false, + format: 'categorical', + description: 'Individual is a U.S. citizen. Recodes the Citizen ' + + 'variable into two primary codes for almost all years available.', + rangeMap: [ + {from: 0, to: 2, recodeTo: 1}, + {from: 3, to: 4, recodeTo: 0}, + {from: 5, to: 9, recodeTo: 2} + ], + categories: [ + 'Not a U.S. citizen', + 'U.S. citizen', + 'Foreign born, citizenship status not reported' + ] + }, + { + ipumsName: 'YRIMMIG', + title: 'Immigrate-year', + group: 2, + defCheck: false, + description: 'The year in which a foreign-born person entered the U.S.' + }, + { + ipumsName: 'BPL', + title: 'Birthplace', + group: 2, + displayMe: false, + defCheck: false, + description: 'Where in the world the respondent was born. ' + + 'Includes up to 188 location categories. Consider working instead ' + + 'with a recoded and simplified version of this variable, called ' + + 'Birth region.' + }, + { + originalAttr: 'Birthplace', + title: 'Birthplace_recode', + group: 2, + format: 'categorical', + defCheck: false, + description: 'Where in the world the person was born. Recodes ' + + 'the Birthplace variable into 9 categories.', + rangeMap: [ + {from: 1, to: 120, recodeTo: 0}, + {from: 150, to: 199, recodeTo: 1}, + {from: 200, to: 300, recodeTo: 2}, + {from: 400, to: 429, recodeTo: 3}, + {from: 430, to: 499, recodeTo: 4}, + {from: 500, to: 524, recodeTo: 5}, + {from: 530, to: 549, recodeTo: 6}, + {from: 550, to: 550, recodeTo: 5}, + {from: 599, to: 599, recodeTo: 5}, + {from: 600, to: 600, recodeTo: 7}, + {from: 700, to: 800, recodeTo: 1}, + {from: 900, to: 999, recodeTo: 8} + ], + categories: [ + 'U.S. state, territory, or outlying region', + 'Canada, Australia, New Zealand, or Pacific Islands', + 'Mexico, Central America, South America, or the Caribbean', + 'Northern or Western Europe', + 'Southern Europe, Central/Eastern Europe, or Russia', + 'East, Southeast, or South Asia', + 'Middle East or Southwest Asia', + 'Africa', + 'Unknown' + ] + }, + { + ipumsName: 'SPEAKENG', + title: 'Speaks_English', + group: 2, + defCheck: false, + description: 'Whether the individual speaks English, ' + + 'speaks only English at home, or how well the individual speaks ' + + 'English. There have been up to 8 codes over time.' + }, + { + ipumsName: 'EMPSTAT', + title: 'Employment_status', + group: 3, + defCheck: false, + description: 'Whether the individual was a part of the ' + + 'labor force, i.e., working or seeking work, and if yes, whether ' + + 'the person was currently unemployed.' + }, + { + ipumsName: 'LABFORCE', + title: 'Labor_force_status', + group: 3, + defCheck: false, + description: 'Whether a person participated in the labor force.' + }, + { + ipumsName: 'CLASSWKR', + title: 'Class_of_worker', + group: 3, + defCheck: false, + description: 'Class of worker indicates whether individuals were ' + + 'self-employed or worked for wages as an employee.' + }, { + ipumsName: 'UHRSWORK', + title: 'Usual_hours_worked', + group: 3, + defCheck: false, + description: 'The number of hours per week that the ' + + 'individual usually worked, if the person worked during the ' + + 'previous year. Values range from 0 (or N/A) to 99 (the top code).' + }, + { + ipumsName: 'WKSWORK2', + title: 'Weeks_worked', + group: 3, + defCheck: false, + description: 'The number of weeks that the individual ' + + 'worked the previous year, with six primary categories.' + }, { + ipumsName: 'WORKEDYR', + title: 'Worked_last_year', + group: 3, + defCheck: false, + description: 'Indicates whether the person worked during the previous year.' + }, + { + ipumsName: 'OCC1950', + title: 'Occupation_1950_basis', + displayMe: false, + group: 3, + defCheck: false, + description: 'The person’s primary occupation, using ' + + 'the Census Bureau’s 1950 occupational classification system. ' + + 'There are several hundred occupation categories.' + }, + { + originalAttr: 'Occupation_1950_basis', + title: 'Occupation_1950_basis_recode', + group: 3, + format: 'categorical', + defCheck: false, + description: 'The person’s primary occupation, using a ' + + 'simplified version of the Census Bureau’s 1950 occupational ' + + 'classification system. There are 8 primary categories.', + rangeMap: [ + {from: 0, to: 99, recodeTo: 1}, + {from: 100, to: 100, recodeTo: 6}, + {from: 123, to: 123, recodeTo: 6}, + {from: 200, to: 290, recodeTo: 2}, + {from: 300, to: 490, recodeTo: 3}, + {from: 500, to: 594, recodeTo: 4}, + {from: 595, to: 595, recodeTo: 7}, + {from: 600, to: 690, recodeTo: 4}, + {from: 700, to: 790, recodeTo: 5}, + {from: 810, to: 970, recodeTo: 6}, + {from: 980, to: 995, recodeTo: 8}, + {from: 997, to: 997, recodeTo: 9}, + {from: 999, to: 999, recodeTo: 10} + ], + categories: [ + '', + 'Professional, technical', + 'Managers, officials, and proprietors', + 'Sales and clerical', + 'Craftsmen and operatives', + 'Service workers', + 'Farmers and laborers', + 'Members of the armed services', + 'Non-occupational response', + 'Occupation missing/unknown', + 'N/A (blank)' + ] + }, + { + ipumsName: 'OCC1990', + title: 'Occupation_1990_basis', + group: 3, + displayMe: false, + defCheck: false, + description: 'The person’s primary occupation, using a ' + + 'modified version of the 1990 Census Bureau occupational ' + + 'classification scheme, from 1950 forward. There are several hundred ' + + 'occupation categories. This attribute is not available for data ' + + 'collected prior to 1950.' + }, + { + originalAttr: 'Occupation_1990_basis', + title: 'Occupation_1990_basis_recode', + group: 3, + format: 'categorical', + defCheck: false, + description: 'The person’s primary occupation, using a ' + + 'simplified version of the Census Bureau’s 1990 occupational ' + + 'classification system. There are 7 primary categories. This ' + + 'attribute is not available for data collected prior to 1950.', + rangeMap: [ + {from: 3, to: 200, recodeTo: 1}, + {from: 203, to: 389, recodeTo: 2}, + {from: 405, to: 469, recodeTo: 3}, + {from: 473, to: 498, recodeTo: 4}, + {from: 503, to: 699, recodeTo: 5}, + {from: 703, to: 889, recodeTo: 6}, + {from: 905, to: 905, recodeTo: 7}, + {from: 991, to: 991, recodeTo: 8}, + {from: 999, to: 999, recodeTo: 9} + ], + categories: [ + '', + 'Managerial and professional   ', + 'Technical, sales, and administrative  ', + 'Service  ', + 'Farming, forestry, and fishing  ', + 'Precision production, craft, and repairers  ', + 'Operators and laborers  ', + 'Military occupations  ', + 'Unemployed', + 'N/A and unknown' + ] + }, + { + ipumsName: 'IND1950', + title: 'Industry_1950', + group: 3, + displayMe: false, + defCheck: false, + description: 'The industry of the individual, using the 1950 Census Bureau industrial classification system.' + }, + { + originalAttr: 'Industry_1950', + title: 'Industry_1950_recode', + group: 3, + format: 'categorical', + defCheck: false, + description: 'The person’s primary occupation, using a simplified version of the Census Bureau’s 1950 industry classification system. There are 9 primary categories.', + rangeMap: [ + {from: 100, to: 126, recodeTo: 1}, + {from: 206, to: 499, recodeTo: 2}, + {from: 506, to: 598, recodeTo: 3}, + {from: 606, to: 699, recodeTo: 4}, + {from: 716, to: 817, recodeTo: 5}, + {from: 826, to: 859, recodeTo: 6}, + {from: 868, to: 899, recodeTo: 7}, + {from: 906, to: 946, recodeTo: 8}, + {from: 976, to: 995, recodeTo: 9}, + {from: 0, to: 0, recodeTo: 10}, + {from: 997, to: 999, recodeTo: 10} + ], + categories: [ + '', + 'Agriculture, forestry, and fishing', + 'Construction, Mining, and Manufacturing', + 'Transportation, Communication, and Other Utilities', + 'Wholesale and Retail Trade', + 'Finance, Business, and Repair Services', + 'Entertainment, Recreation, and Personal Services', + 'Professional and Related Services', + 'Public Administration', + 'Other', + 'Non-classifiable, not reported, or blank, N/A, etc.' + ] + }, + { + ipumsName: 'IND1990', + title: 'Industry_1990', + displayMe: false, + group: 3, + defCheck: false, + description: 'The industry of the individual, using the 1990 Census Bureau industrial classification system. There are several hundred industry categories. This attribute is not available for data collected prior to 1950.' + }, + { + originalAttr: 'Industry_1990', + title: 'Industry_1990_recode', + group: 3, + format: 'categorical', + defCheck: false, + description: 'The industry of the individual, using a simplified version of the 1990 Census Bureau industrial classification system, from 1950 forward. There are 9 primary categories. This attribute is not available for data collected prior to 1950.', + rangeMap: [ + {from: 10, to: 32, recodeTo: 1}, + {from: 40, to: 392, recodeTo: 2}, + {from: 400, to: 472, recodeTo: 3}, + {from: 500, to: 691, recodeTo: 4}, + {from: 700, to: 760, recodeTo: 5}, + {from: 761, to: 810, recodeTo: 6}, + {from: 812, to: 893, recodeTo: 7}, + {from: 900, to: 932, recodeTo: 8}, + {from: 940, to: 960, recodeTo: 9}, + {from: 992, to: 992, recodeTo: 10}, + {from: 999, to: 999, recodeTo: 10}, + {from: 0, to: 0, recodeTo: 11}, + ], + categories: [ + '', + 'Agriculture, forestry, and fishing', + 'Construction, Mining, and Manufacturing', + 'Transportation, Communications, and Other Utilities', + 'Wholesale and Retail Trade', + 'Finance, Business, and Repair Services', + 'Entertainment, Recreation, and Personal Services', + 'Professional and Related Services', + 'Public Administration', + 'Military', + 'Unemployed not classified by industry, or did not respond', + 'Not applicable' + ] + }, + { + ipumsName: 'VETSTAT', + title: 'Veteran_status', + group: 3, + defCheck: false, + description: 'Indicates whether individuals served in the military forces of the U.S. (Army, Navy, Air Force, Marine Corps, or Coast Guard) in time of war or peace. Service includes active duty for any length of time and at any place at home or abroad.' + }, + { + ipumsName: 'INCTOT', + title: 'Income-total', + group: 4, + defCheck: false, + description: 'Each respondent’s total pre-tax income. Total income is the sum of the amounts reported for multiple types of income.' + }, + { + ipumsName: 'INCWAGE', + title: 'Income-wages', + group: 4, + defCheck: false, + description: 'Each respondent’s pre-tax wage or salary income received for work performed as an employee. Amounts are expressed in contemporary dollars.' + }, + { + ipumsName: 'FTOTINC', + title: 'Income-family_total', + group: 4, + defCheck: false, + description: 'Total reports the sum of the pre-tax incomes of all respondents 15 years old and over related to Person 1 in the questionnaire. Amounts are expressed in contemporary dollars.' + }, + // { + // ipumsName: 'INCEARN', + // title: 'Income-earnings', + // group: 4, + // defCheck: false, + // description: 'Income earned from wages or a person\'s own business or farm for the previous year. Amounts are expressed in contemporary dollars.' + // }, + { + ipumsName: 'CPI99', + title: 'CPI99', + group: 4, + defCheck: false, + description: 'The adjustment factor that converts contemporary dollars to constant 1999 dollars. It is a 5-digit numeric variable that has three implied decimals. For example, a CPI99 value of 15423 should be interpreted as 15.423.' + }, + { + ipumsName: 'INCWELFR', + title: 'Income-welfare', + group: 4, + defCheck: false, + description: 'How much pre-tax income (if any) the respondent received during the previous year from various public assistance programs commonly referred to as "welfare." Assistance from private charities is not included. Amounts are expressed in contemporary dollars.' + }, + { + ipumsName: 'POVERTY', + title: 'Poverty', + group: 4, + defCheck: false, + description: 'Each family\'s total income for the previous year as a percentage of the official U.S. poverty threshold, adjusted for inflation.' + }, + { + ipumsName: 'REGION', + title: 'Region', + group: 5, + displayMe: false, + defCheck: false, + description: 'Identifies the U.S. Census region and division where the individual lives. There are four primary regions and nine primary divisions of the U.S., with additional categories for mixed divisions. Consider using Region_recode or Region-division_recode for less detailed versions of this variable.' + }, + { + originalAttr: 'Region', + title: 'Region_recode', + group: 5, + format: 'categorical', + defCheck: false, + description: 'The U.S. Census region where the person lives, with 4 region categories.', + rangeMap: [ + {from: 11, to: 13, recodeTo: 1}, + {from: 21, to: 23, recodeTo: 2}, + {from: 31, to: 34, recodeTo: 3}, + {from: 41, to: 43, recodeTo: 4}, + {from: 91, to: 99, recodeTo: 5} + ], + categories: [ + '', + 'Northeast', + 'Midwest', + 'South', + 'West', + 'Other/not identified', + ] + }, + { + originalAttr: 'Region', + title: 'Region-division_recode', + group: 5, + format: 'categorical', + defCheck: false, + description: 'The U.S. Census division where the person lives, with 9 division categories.', + rangeMap: [ + {from: 11, to: 11, recodeTo: 1}, + {from: 12, to: 12, recodeTo: 2}, + {from: 13, to: 13, recodeTo: 10}, + {from: 21, to: 21, recodeTo: 3}, + {from: 22, to: 22, recodeTo: 4}, + {from: 23, to: 23, recodeTo: 10}, + {from: 31, to: 31, recodeTo: 5}, + {from: 32, to: 32, recodeTo: 6}, + {from: 33, to: 33, recodeTo: 7}, + {from: 34, to: 34, recodeTo: 10}, + {from: 41, to: 41, recodeTo: 8}, + {from: 42, to: 42, recodeTo: 9}, + {from: 43, to: 43, recodeTo: 10}, + {from: 91, to: 99, recodeTo: 11} + ], + categories: [ + '', + 'New England', + 'Middle Atlantic', + 'East North Central', + 'West North Central', + 'South Atlantic', + 'East South Central', + 'West South Central', + 'Mountain', + 'Pacific', + 'Mixed divisions ', + 'Other/not identified', + ] + }, + { + ipumsName: 'STATEFIP', + title: 'State', + group: 5, + defCheck: true, + displayMe: true, + readonly: true, + description: 'The state in which the individual lives, using a federal coding scheme that lists states alphabetically. Note that you must select this attribute if you want to display state names in your case table or graphs.' + }, + { + ipumsName: 'STATEFIP', + title: 'Boundaries', + group: 5, + defCheck: true, + displayMe: true, + readonly: true, + description: 'State boundaries. Requires that the State attribute also be selected.', + formula: 'lookupBoundary(US_state_boundaries, State)', + formulaDependents: 'State' + }, + { + ipumsName: 'MIGRATE1', + title: 'Moved', + group: 5, + defCheck: false, + description: 'Whether the person had moved to a different house within the past year, with several categories.' + }, + { + ipumsName: 'YEAR', + title: 'Year', + group: 6, + format: 'categorical', + defCheck: true, + displayMe: true, + readonly: true, + description: 'The four-digit year of the decennial census or ACS for each person\'s questionnaire responses. Note that you must select this attribute if you want to display year indicators in your case table or graphs.' + }] + +export {attributeGroups, attributeAssignment}; diff --git a/src/scripts/pluginHelper.js b/src/scripts/pluginHelper.js new file mode 100644 index 0000000..fab10ec --- /dev/null +++ b/src/scripts/pluginHelper.js @@ -0,0 +1,162 @@ +/** + * Created by tim on 1/19/17. + + + ========================================================================== + pluginHelper.js in gamePrototypes. + + Author: Tim Erickson + + Copyright (c) 2016 by The Concord Consortium, Inc. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ========================================================================== + + + */ + +var pluginHelper = { + + + /** + * Create a new data set (data context) using the input object + * @param iDataSetDescription the object that describes the data set. See the API documentation. + * @returns {Promise} which, when resolved, means that the data set exists + */ + initDataSet: function (iDataSetDescription) { + return new Promise( function( resolve, reject ) { + var tDataContextResourceString = 'dataContext[' + iDataSetDescription.name + ']'; + var tMessage = { action: 'get', resource: tDataContextResourceString }; + + // if the data set already exists, we will not ask CODAP to create one. So we check... + var tAlreadyExistsPromise = codapInterface.sendRequest(tMessage); + + tAlreadyExistsPromise.then( + // iValue is the result of the resolved "get dataContext" call + function( iValue ) { + if (iValue.success) { + console.log("dataContext[" + iDataSetDescription.name + "] already exists"); + resolve( iValue ); + } else { + // the data set did not exist. (Since get dataContext returned success = false) + console.log("Creating dataContext[" + iDataSetDescription.name + "]" ); + tMessage = { + action: 'create', + resource: 'dataContext', + values: iDataSetDescription + }; + codapInterface.sendRequest(tMessage).then( + // iValue is the result of the resolved "create dataContext" call. + function( iValue ) { + resolve( iValue ); + } + ); + } + } + ).catch (function (msg) { + console.log('warning in pluginHelper.initDataSet: ' + msg); + reject( msg ); + }); + }); + }, + + /** + * Create new data items (broader than cases; see the documentation for the API) + * Notes: (1) this refers only to the data context, not to any collections. Right? Has to. + * (2) notice how the values array does not have a "values" key inside it as with createCases. + * + * @param iValuesArray the array (or not) of objects, each of which will be an item. The keys are attribute names. + * @param iDataContextName the name of the data set (or "data context"). + */ + createItems : function(iValuesArray, iDataContextName, iCallback) { + return new Promise( function(resolve, reject) { + iValuesArray = pluginHelper.arrayify( iValuesArray ); + + var tResourceString = iDataContextName ? "dataContext[" + iDataContextName + "].item" : "item"; + + var tMessage = { + action : 'create', + resource : tResourceString, + values : iValuesArray + }; + + var tCreateItemsPromise = codapInterface.sendRequest(tMessage); + resolve( tCreateItemsPromise ); + }) + }, + + createCases : function(iValues, iCollection, iDataContext, iCallback) { + iValues = pluginHelper.arrayify( iValues ); + console.log("DO NOT CALL pluginHelper.createCases YET!!"); + }, + + /** + * + * @param IDs array of case IDs to be selected + * @param iDataContextName name of the data context in which these things live. OK if absent. + * @returns {Promise} + */ + selectCasesByIDs: function (IDs, iDataContextName) { + return new Promise( function( resolve, reject ) { + IDs = pluginHelper.arrayify( IDs ); + + var tResourceString = "selectionList"; + + if (typeof iDataContextName !== 'undefined') { + tResourceString = 'dataContext[' + iDataContextName + '].' + tResourceString; + } + + var tMessage = { + action: 'create', + resource: tResourceString, + values: IDs + }; + + var tSelectCasesPromise = codapInterface.sendRequest(tMessage); + resolve( tSelectCasesPromise ); + }) + }, + + getCaseValuesByCaseID: function (iCaseID, iDataContext) { + return new Promise(function (resolve, reject) { + var tMessage = { + action: 'get', + resource: "dataContext[" + + iDataContext + "].caseByID[" + + iCaseID + "]" + }; + + codapInterface.sendRequest(tMessage).then( + function (iResult) { + if (iResult.success) { + var tCaseValues = iResult.values.case.values; + resolve(tCaseValues); + } + } + ) + }) + }, + + /** + * Change the input to an array if it is not one! + * @param iValuesArray the thing which might be an array + * @returns {*} if it was not an array, a single-item array with the thing. Otherwise, the array. + */ + arrayify : function( iValuesArray ) { + if (iValuesArray && !Array.isArray(iValuesArray)) { + iValuesArray = [iValuesArray]; + } + return iValuesArray; + } + +} \ No newline at end of file diff --git a/src/types.tsx b/src/types.tsx index b7b6931..e69de29 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -1,69 +0,0 @@ -export interface IDataSet { - guid: number, - id: number, - name: string, - title: string -} - -export interface ICollection { - areParentChildLinksConfigured: boolean, - attrs: Array, - caseName: string, - cases: Array, - childAttrName: string, - collapseChildren: boolean, - defaults: any, - guid: number, - id: number, - labels: any, - name: string, - parent: number, - title: string, - type: string -} - -export type ICollections = Array; - -export interface ICaseObjCommon { - collection: { - name: string, - id: number - }, - id: number, - parent: number, - values: IValues -} - -export type IValues = Record; - -export interface ICaseObj extends ICaseObjCommon { - children: Array -} - -export interface IProcessedCaseObj extends ICaseObjCommon { - children: Array -} - -export interface ICollectionClass { - collectionName: string; - className: string; -} - -export interface ITableProps { - showHeaders: boolean, - collectionClasses: Array, - getClassName: (caseObj: IProcessedCaseObj) => string, - selectedDataSet: IDataSet, - collections: Array, - mapCellsFromValues: (values: IValues) => void, - mapHeadersFromValues: (values: IValues) => void, - getValueLength: (firstRow: Array) => number - paddingStyle: Record -} - -export interface IBoundingBox { - top: number; - left: number; - width: number; - height: number; -} From c98492b5c0ea3865c6954e9e4a3b0c561fe3485d Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 7 Sep 2023 16:38:38 -0400 Subject: [PATCH 03/21] User can select place / state options. --- src/assets/check-box-outline-blank.svg | 6 ++ src/assets/check-box.svg | 6 ++ src/assets/radio-button-checked.svg | 6 ++ src/assets/radio-button-unchecked.svg | 6 ++ src/components/app.tsx | 14 ++++ src/components/constants.ts | 65 ++++++++++++++++++ src/components/dropdown.scss | 8 ++- src/components/dropdown.tsx | 22 ++++++- src/components/options.scss | 54 +++++++++++++++ src/components/place-options.tsx | 91 ++++++++++++++++++++++++++ 10 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 src/assets/check-box-outline-blank.svg create mode 100644 src/assets/check-box.svg create mode 100644 src/assets/radio-button-checked.svg create mode 100644 src/assets/radio-button-unchecked.svg create mode 100644 src/components/constants.ts create mode 100644 src/components/options.scss create mode 100644 src/components/place-options.tsx diff --git a/src/assets/check-box-outline-blank.svg b/src/assets/check-box-outline-blank.svg new file mode 100644 index 0000000..9b1e50b --- /dev/null +++ b/src/assets/check-box-outline-blank.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/check-box.svg b/src/assets/check-box.svg new file mode 100644 index 0000000..b8830e9 --- /dev/null +++ b/src/assets/check-box.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/radio-button-checked.svg b/src/assets/radio-button-checked.svg new file mode 100644 index 0000000..e843eec --- /dev/null +++ b/src/assets/radio-button-checked.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/radio-button-unchecked.svg b/src/assets/radio-button-unchecked.svg new file mode 100644 index 0000000..859fecb --- /dev/null +++ b/src/assets/radio-button-unchecked.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/app.tsx b/src/components/app.tsx index 106a7e1..20a2974 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -3,9 +3,17 @@ import { Dropdown } from "./dropdown"; import css from "./app.scss"; import classnames from "classnames"; import { Information } from "./information"; +import { defaultSelectedOptions } from "./constants"; + function App() { const [showInfo, setShowInfo] = useState(false); + const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); + + const handleSetSelectedOptions = (option: string, value: string | string[]) => { + const newSelectedOptions = {...selectedOptions, [option]: value}; + setSelectedOptions(newSelectedOptions); + }; const handleInfoClick = () => { setShowInfo(true); @@ -33,16 +41,22 @@ function App() { sectionName={"Place"} sectionAltText={"Place alt text"} sectionDescription={"Place description"} + handleSetSelectedOptions={handleSetSelectedOptions} + selectedOptions={selectedOptions} />
diff --git a/src/components/constants.ts b/src/components/constants.ts new file mode 100644 index 0000000..22822be --- /dev/null +++ b/src/components/constants.ts @@ -0,0 +1,65 @@ +export const placeOptions = { + label: "Size of area for data", + options : ["State", "County"] +}; + +export const stateOptions = { + label: "Choose states to include in your dataset from the list below", + options: [ + "Alabama", + "Alaska", + "Arizona", + "Arkansas", + "California", + "Colorado", + "Connecticut", + "Delaware", + "Florida", + "Georgia", + "Hawaii", + "Idaho", + "Illinois", + "Indiana", + "Iowa", + "Kansas", + "Kentucky", + "Louisiana", + "Maine", + "Maryland", + "Massachusetts", + "Michigan", + "Minnesota", + "Mississippi", + "Missouri", + "Montana", + "Nebraska", + "Nevada", + "New Hampshire", + "New Jersey", + "New Mexico", + "New York", + "North Carolina", + "North Dakota", + "Ohio", + "Oklahoma", + "Oregon", + "Pennsylvania", + "Rhode Island", + "South Carolina", + "South Dakota", + "Tennessee", + "Texas", + "Utah", + "Vermont", + "Virginia", + "Washington", + "West Virginia", + "Wisconsin", + "Wyoming" + ] +}; + +export const defaultSelectedOptions = { + place: null, + states: [] +}; diff --git a/src/components/dropdown.scss b/src/components/dropdown.scss index 10f47c6..3e7db65 100644 --- a/src/components/dropdown.scss +++ b/src/components/dropdown.scss @@ -35,7 +35,7 @@ } .dropdown .dropdownBody { - padding: 0 12px; + padding: 0px 12px; overflow-y: hidden; transition-property: max-height; transition-duration: .5s; @@ -76,4 +76,10 @@ flex-shrink: 0; text-align: center; height: 16px; +} + +.options { + display: flex; + flex-direction: column; + padding: 12px 0px; } \ No newline at end of file diff --git a/src/components/dropdown.tsx b/src/components/dropdown.tsx index 6e61c1e..23ae174 100644 --- a/src/components/dropdown.tsx +++ b/src/components/dropdown.tsx @@ -1,21 +1,37 @@ import React, { useState } from "react"; import classnames from "classnames"; import css from "./dropdown.scss"; +import { PlaceOptions } from "./place-options"; +import { defaultSelectedOptions } from "./constants"; interface IProps { sectionName: string sectionAltText: string sectionDescription: string + handleSetSelectedOptions: (option: string, value: string|string[]) => void + selectedOptions: typeof defaultSelectedOptions; } export const Dropdown: React.FC = (props) => { - const {sectionName, sectionAltText, sectionDescription} = props; + const {sectionName, sectionAltText, sectionDescription, handleSetSelectedOptions, selectedOptions} = props; const [showItems, setShowItems] = useState(false); const handleClick = () => { setShowItems(!showItems); }; + const renderOptions = () => { + if (sectionName === "Place") { + return ( + + ); + } + }; + return (
@@ -24,8 +40,8 @@ export const Dropdown: React.FC = (props) => {
-

{sectionDescription}

-
+
+ {renderOptions()}
diff --git a/src/components/options.scss b/src/components/options.scss new file mode 100644 index 0000000..0d0d78e --- /dev/null +++ b/src/components/options.scss @@ -0,0 +1,54 @@ +.radioOptions, .checkOptions { + margin-top: 10px; + margin-bottom: 10px; +} + +.radioOptions { + display: flex; + flex-direction: row; + gap: 20px; +} + +.option { + display: flex; + align-items: center; + height: 24px; + flex-basis: 120px; + padding: 6px; + label { + padding-top: 3px; + } +} + +.radio { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 17px; + height: 17px; + font-size: 17px; + background-image: url('../assets/radio-button-unchecked.svg'); + color: var(--teal-dark-75); + &:checked { + background-image: url('../assets/radio-button-checked.svg'); + } +} + +.checkbox { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 17px; + height: 17px; + font-size: 17px; + background-image: url('../assets/check-box-outline-blank.svg'); + color: var(--teal-dark-75); + &:checked { + background-image: url('../assets/check-box.svg'); + } +} + +.checkOptions { + flex-wrap: wrap; + display: flex; +} \ No newline at end of file diff --git a/src/components/place-options.tsx b/src/components/place-options.tsx new file mode 100644 index 0000000..0774649 --- /dev/null +++ b/src/components/place-options.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { placeOptions, stateOptions } from "./constants"; + +import css from "./options.scss"; + +interface IProps { + handleSetSelectedOptions: (option: string, value: string|string[]) => void; + selectedPlace: string|null; + selectedStates: string[]; +} + +export const PlaceOptions: React.FC = (props) => { + const {handleSetSelectedOptions, selectedPlace, selectedStates} = props; + + const isStateSelected = (state: string) => { + return selectedStates.indexOf(state) > - 1; + }; + + const handleSelectPlace = (e: React.ChangeEvent) => { + handleSetSelectedOptions("place", e.target.value); + }; + + const handleSelectState = (e: React.ChangeEvent) => { + let newSelectedStates = [...selectedStates]; + if (e.currentTarget.checked) { + newSelectedStates.push(e.target.value); + newSelectedStates.sort(); + + } else { + if (isStateSelected(e.target.value)) { + newSelectedStates = newSelectedStates.filter((s) => s !== e.target.value); + } + } + handleSetSelectedOptions("states", newSelectedStates); + }; + + const createPlaceOptions = (options: string[]) => { + return ( + <> + {options.map((o) => { + return ( +
+ + +
+ ); + })} + + ); + }; + + const createStateOptions = (options: string[]) => { + return ( + <> + {options.map((o) => { + return ( +
+ + +
+ ); + })} + + ); + }; + + return ( + <> +
{placeOptions.label}:
+
{createPlaceOptions(placeOptions.options)}
+
{stateOptions.label}:
+
{createStateOptions(stateOptions.options)}
+ + ); +}; From 79c8e13fa8eeafc6e2bf9091b12921ecdd9cc4a8 Mon Sep 17 00:00:00 2001 From: lublagg Date: Fri, 8 Sep 2023 17:13:46 -0400 Subject: [PATCH 04/21] User sees preview of place selections. --- src/components/dropdown.tsx | 13 +- src/scripts/Attribute.js | 174 ------- src/scripts/app.CODAPconnect.js | 266 ---------- src/scripts/app.DBconnect.js | 171 ------- src/scripts/app.constants.js | 42 -- src/scripts/app.js | 146 ------ src/scripts/app.ui.js | 420 ---------------- src/scripts/app.userActions.js | 161 ------ src/scripts/attributeConfig.js | 864 -------------------------------- 9 files changed, 12 insertions(+), 2245 deletions(-) delete mode 100644 src/scripts/Attribute.js delete mode 100644 src/scripts/app.CODAPconnect.js delete mode 100644 src/scripts/app.DBconnect.js delete mode 100644 src/scripts/app.constants.js delete mode 100644 src/scripts/app.js delete mode 100644 src/scripts/app.ui.js delete mode 100644 src/scripts/app.userActions.js delete mode 100644 src/scripts/attributeConfig.js diff --git a/src/components/dropdown.tsx b/src/components/dropdown.tsx index 23ae174..5f4650b 100644 --- a/src/components/dropdown.tsx +++ b/src/components/dropdown.tsx @@ -13,13 +13,23 @@ interface IProps { } export const Dropdown: React.FC = (props) => { - const {sectionName, sectionAltText, sectionDescription, handleSetSelectedOptions, selectedOptions} = props; + const {sectionName, sectionAltText, handleSetSelectedOptions, selectedOptions} = props; const [showItems, setShowItems] = useState(false); const handleClick = () => { setShowItems(!showItems); }; + const renderSummary = () => { + if (sectionName === "Place") { + const place = selectedOptions.place || ""; + const states = selectedOptions.states.join(`, `); + return ( + `${place}: ${states}` + ); + } + } + const renderOptions = () => { if (sectionName === "Place") { return ( @@ -36,6 +46,7 @@ export const Dropdown: React.FC = (props) => {
{sectionName} +
{renderSummary()}
diff --git a/src/scripts/Attribute.js b/src/scripts/Attribute.js deleted file mode 100644 index decdd8d..0000000 --- a/src/scripts/Attribute.js +++ /dev/null @@ -1,174 +0,0 @@ -/* - * ========================================================================== - * Copyright (c) 2018 by eeps media. - * Last modified 8/21/18 8:16 PM - * - * Created by Tim Erickson on 8/21/18 8:16 PM - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * ========================================================================== - * - */ - - -class Attribute { - - constructor(iRecord, iAttributeAssignment, attributeMap) { - if (iAttributeAssignment) { - if (iAttributeAssignment.displayMe == null) iAttributeAssignment.displayMe = true; - } else { - iAttributeAssignment = {displayMe: false}; - } - if (!iRecord) {iRecord = {};} - this.name = iRecord.name; - // Starting position in the data string. Value comes from codebook. - this.startPos = (iRecord.startPos != null)?Number(iRecord.startPos):undefined; - // Width in characters in the data string. Value comes from codebook - this.width = iRecord.width; - // Whether categorical or numeric. Codebook value can be overridden - this.format = iAttributeAssignment.format || iRecord.format; - // If categorical, mapping of numeric codes to string values. - this.categories = iAttributeAssignment.categories || iRecord.categories; - // Attributes are grouped - this.groupNumber = iAttributeAssignment.group; - // Description of attribute - this.description = iAttributeAssignment.description || iRecord.description; - // no longer used - this.chosen = iAttributeAssignment.defCheck; - // can't be changed - this.readonly = iAttributeAssignment.readonly; - - this.displayMe = iAttributeAssignment.displayMe; //Boolean(iRecord.defshow); - this.hasCheckbox = this.displayMe; - // if the order of attribute is important we can convey the order of categories to CODAP - this.hasCategoryMap = iAttributeAssignment.hasCategoryMap; - // remapping of numeric codes to be applied before mapping to a category string - this.rangeMap = iAttributeAssignment.rangeMap; - this.multirangeMap = iAttributeAssignment.multirangeMap; - - // title is the CODAP attribute string - this.title = iAttributeAssignment.title || iRecord.labl; - if (!this.title) { - this.title = this.name; - } - // the DOM element ID - this.checkboxID = 'attr-' + this.title; - // we can create formula based attributes. A formula attribute is derived from - // values of other attributes as known to CODAP, and not directly from the source data. - this.formula = iAttributeAssignment.formula; - // a comma delimited list of attributes that must be present in the document - // along with this attribute - this.formulaDependents = iAttributeAssignment.formulaDependents; - - this.originalAttr = iAttributeAssignment.originalAttr; - this.attributeMap = attributeMap; - } - - getRawValue(dataObject) { - return dataObject[this.name]; - } - - isRecode() { - return (this.rangeMap || this.multirangeMap); - } - - getAttributesRawValue (attributeName, dataString) { - if (this.startPos != null) { - return this.getRawValue(dataString); - } - else { - let attribute = this.attributeMap && this.attributeMap[attributeName]; - return attribute && attribute.getRawValue(dataString); - } - } - recodeValue(dataString) { - let result = null; - let originalAttr = this.originalAttr; - let found = null; - if (this.multirangeMap) { - if (!Array.isArray(originalAttr)) {originalAttr = [originalAttr];} - let rawValues = this.originalAttr.map(function (name) { - return this.getAttributesRawValue(name, dataString); - }.bind(this)); - found = this.multirangeMap.find(function (constraint) { - return originalAttr.reduce(function(prior, attrName, attrIx) { - let value = rawValues[attrIx]; - let test = constraint.range[attrName]; - if (!(value && value.trim().length)) { - return false; - } - if (test) { - return prior && (test.from <= value && test.to >= value); - } else { - return prior; - } - }, true); - }); - } - else if (this.rangeMap) { - let rawValue = this.getAttributesRawValue(originalAttr, dataString); - if (rawValue.trim()) { - found = this.rangeMap.find(function(range) { - return (range.from<=rawValue && range.to >= rawValue); - }); - } - } - if (found) { - result = found.recodeTo; - } - return result; - } - - decodeValue(dataString) { - let result; - let rawValue; - if (this.isRecode()) { - rawValue = this.recodeValue(dataString); - } else { - rawValue = this.getRawValue(dataString); - } - - if (rawValue == null) { - result = ''; - } else if (this.format === 'categorical' && this.categories) { - result = this.categories[Number(rawValue)]; - if (result == null) {result = rawValue;} - } else { - result = rawValue; - } - return result; - } - - /** - * Creates a category map based on the categories listed and assigning colors - * in order. - */ - getCategoryMap () { - const kKellyColors = [ - '#FFB300', '#803E75', '#FF6800', '#A6BDD7', '#C10020', '#CEA262', - '#817066', '#007D34', '#00538A', '#F13A13', '#53377A', '#FF8E00', - '#B32851', '#F4C800', '#7F180D', '#93AA00', '#593315', '#232C16', - '#FF7A5C', '#F6768E']; - let order = Array.isArray(this.categories)? this.categories: Object.values(this.categories); - let categoryMap = {}; - let index = 0; - if (order) { - order.forEach(function (categoryName) { - categoryMap[categoryName] = kKellyColors[index++ % kKellyColors.length]; - }); - } - categoryMap.__order = order; - return categoryMap; - } -} - -export {Attribute}; diff --git a/src/scripts/app.CODAPconnect.js b/src/scripts/app.CODAPconnect.js deleted file mode 100644 index e17b3eb..0000000 --- a/src/scripts/app.CODAPconnect.js +++ /dev/null @@ -1,266 +0,0 @@ -/* - * ========================================================================== - * Copyright (c) 2018 by eeps media. - * Last modified 8/21/18 8:32 AM - * - * Created by Tim Erickson on 8/21/18 8:32 AM - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - * ========================================================================== - * - */ - -// import codapInterface from "../common/codapInterface"; -import {constants} from "./app.constants.js"; - -let CODAPconnect = { - - initialize: async function (/*iCallback*/) { - try { - await codapInterface.init(this.iFrameDescriptor, null); - } catch (e) { - console.log('Error connecting to CODAP: ' + e); - window.app.state = Object.assign({}, window.app.freshState); - return; - } - // restore the state if possible - - app.state = await codapInterface.getInteractiveState(); - - if (jQuery.isEmptyObject(app.state)) { - await codapInterface.updateInteractiveState(app.freshState); - console.log("app: getting a fresh state"); - } - console.log("app.state is " + JSON.stringify(app.state)); // .length + " chars"); - - // now update the iframe to be mutable... - - const tMessage = { - "action": "update", - "resource": "interactiveFrame", - "values": { - "preventBringToFront": false, - "preventDataContextReorg": false - } - }; - - return await codapInterface.sendRequest(tMessage); - }, - - logAction: function (iMessage) { - codapInterface.sendRequest({ - action: 'notify', - resource: 'logMessage', - values: { - formatStr: iMessage - } - }); - }, - - makeCODAPAttributeDef: function (attr) { - return { - name: attr.title, - title: attr.title, - description: attr.description, - type: attr.format, - formula: attr.formula - } - }, - - saveCasesToCODAP: async function (iValues) { - const makeItemsMessage = { - action : "create", - resource : "dataContext[" + constants.datasetName + "].item", - values : iValues - }; - - return await codapInterface.sendRequest(makeItemsMessage); - }, - - deleteAllCases: async function () { - let theMessage = { - action: 'delete', - resource : "dataContext[" + constants.datasetName + "].allCases" - }; - return await codapInterface.sendRequest(theMessage); - }, - - guaranteeDataset: async function () { - let datasetResource = 'dataContext[' + constants.datasetName + - ']'; - let response = await codapInterface.sendRequest({ - action: 'get', - resource: datasetResource}); - if (!response.success) { - await this.createNewMicrodataDataset(app.allAttributes); - response = await codapInterface.sendRequest({ - action: 'get', - resource: datasetResource}); - } - return await this.makeNewAttributesIfNecessary(); - }, - - makeNewAttributesIfNecessary : async function() { - async function getCODAPAttrList() { - let attrListResource = 'dataContext[' + constants.datasetName + - ']'; - let response = - await codapInterface.sendRequest({ - action: 'get', - resource: attrListResource}); - if (response.success) { - let attrArrays = response.values.collections.map(function (collection) { - collection.attrs.forEach(function (attr) { - attr.collectionID = collection.guid; - }); - return collection.attrs; - }); - return attrArrays.flat(); - } - } - - let theAttributes = app.state.selectedAttributes.map(function (attrName) { - return app.allAttributes[attrName]; - }); - let existingAttributeList = await getCODAPAttrList(); - let existingAttributeNames = existingAttributeList.map(function (attr) { - return attr.title; - }); - let codapRequests = []; - - theAttributes.forEach(function (attr) { - if (!existingAttributeNames.includes(attr.title)) { - let attrResource = 'dataContext[' + constants.datasetName + '].collection[' - + constants.datasetChildCollectionName + '].attribute'; - let req = { - action: 'create', - resource: attrResource, - values: this.makeCODAPAttributeDef(attr) - }; - if (attr.hasCategoryMap) { - req.values._categoryMap = attr.getCategoryMap(); - } - codapRequests.push(req); - } - }.bind(this)); - if (app.state.priorAttributes) { - app.state.priorAttributes.forEach(function (attrName) { - if (!app.state.selectedAttributes.includes(attrName)) { - let codapAttr = existingAttributeList.find(function (cAttr) {return attrName === cAttr.name;}); - if (codapAttr) { - let attrResource = 'dataContext[' + constants.datasetName + - '].collection[' + codapAttr.collectionID + '].attribute[' + codapAttr.name + ']'; - let req = { - action: 'delete', resource: attrResource - }; - codapRequests.push(req); - } - } - }); - } - app.state.priorAttributes = app.state.selectedAttributes.slice(); - if (codapRequests.length > 0) { - return await codapInterface.sendRequest(codapRequests); - } else { - return {success: true}; - } - }, - - makeCaseTableAppear : async function() { - const theMessage = { - action : "create", - resource : "component", - values : { - type : 'caseTable', - dataContext : constants.datasetName, - name : constants.caseTableName, - 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); - } - return makeCaseTableResult.success && makeCaseTableResult.values.id; - }, - - autoscaleComponent: async function (name) { - return await codapInterface.sendRequest({ - action: 'notify', - resource: `component[${name}]`, - values: { - request: 'autoScale' - } - }) - }, - - createNewMicrodataDataset: async function (attributeList) { - - return codapInterface.sendRequest({ - action: 'create', - resource: 'dataContext', - values: { - name: constants.datasetName, - title: constants.datasetTitle, - description: constants.datasetDescription, - collections: [{ - name: constants.datasetParentCollectionName, - attrs: [this.makeCODAPAttributeDef( - attributeList['State']), this.makeCODAPAttributeDef( - attributeList['Boundaries'])] - }, { - name: constants.datasetChildCollectionName, - parent: constants.datasetParentCollectionName, - labels: { - singleCase: "person", - pluralCase: "people", - setOfCasesWithArticle: "a sample of people" - }, - - attrs: [ // note how this is an array of objects. - {name: "sample", type: "categorical", description: "sample number"},] - }] - } - }); - }, - myCODAPIDd: null, - selectSelf: async function () { - if (this.myCODAPId == null) { - let r1 = await codapInterface.sendRequest({action: 'get', resource: 'interactiveFrame'}); - if (r1.success) { - this.myCODAPId = r1.values.id; - } - } - if (this.myCODAPId != null) { - return await codapInterface.sendRequest({ - action: 'notify', - resource: `component[${this.myCODAPId}]`, - values: {request: 'select' - } - }); - } - }, - - iFrameDescriptor: { - version: constants.version, - name: constants.appName, - title: constants.appTitle, - dimensions: { - width: constants.appDefaultWidth, - height: constants.appDefaultHeight - }, - preventDataContextReorg: false, - cannotClose: false - } -}; - -export {CODAPconnect}; diff --git a/src/scripts/app.DBconnect.js b/src/scripts/app.DBconnect.js deleted file mode 100644 index dd8d5f8..0000000 --- a/src/scripts/app.DBconnect.js +++ /dev/null @@ -1,171 +0,0 @@ -/* - * ========================================================================== - * Copyright (c) 2018 by eeps media. - * Last modified 8/21/18 8:32 AM - * - * Created by Tim Erickson on 8/21/18 8:32 AM - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - * ========================================================================== - * - */ -import {userActions} from "./app.userActions.js"; - -/*global Papa:true */ -let DBconnect = { - - /** - * Retrieves sample data from the server. - * - * Will retrieve a subsample for each state/year combination. - * Each retrieval will return 1000 records. The actual number needed will be - * randomly selected (without replacement) from this set. - * - * @param iAtts Selected attributes. Unused in current implementation. - * @param iStateCodes - * @param iYears - * @param iAllAttributes {{}} All possible attributes indexed by attribute name. - * @return {Promise<*[]|[]>} - */ - getCasesFromDB: async function (iAtts, iStateCodes, iYears, iAllAttributes) { - function computeSubsample(data, size) { - if (data.length <= size) { - return data; - } - let randomizedData = data.map(function (item) { return {r: Math.random(), d: item};}); - randomizedData.sort(function(a, b) { return a.r - b.r; }); - let filteredData = randomizedData.filter(function (item, ix) {return (ix < size);}) - return filteredData.map(function (item) { return item.d;}); - } - - function fetchSubsampleChunk(stateName, year, chunkSize) { - return new Promise(function (resolve, reject) { - try { - let dataset = _this.metadata.datasets.find(function (ds) {return ds.name === String(year);}) - let presetCount = dataset && dataset.presetCount; - let presetIndex = Math.floor(Math.random() * presetCount); - let filePrefix = _this.metadata.filenamePrefix || 'preset-'; - let fileSuffix = _this.metadata.filenameSuffix || '.csv'; - let presetName = filePrefix + presetIndex + fileSuffix; - let dataExistsForYearAndState = _this.yearHasState(year, stateName); - if (dataExistsForYearAndState) { - let presetURL = `${_this.metadata.baseURL}/${year}/${stateName}/${presetName}`; - - // fetch chunks then randomly pick selection set. - app.addLog('Send request: ' + presetURL); - Papa.parse(presetURL, { - header: true, /* converts CSV rows to objects as defined by the header line */ - download: true, /* indicates this is a url to fetch */ - complete: function (response) { - if (response.errors.length === 0) { - app.addLog('Good response: ' + (response.data?response.data.length: '')); - resolve(computeSubsample(response.data, chunkSize)); - } else { - let msg = `Errors fetching ${presetURL}: ${response.errors.join( - ', ')}`; - app.addLog(msg); - reject(msg); - } - }, error: function (error/*, file*/) { - app.addLog(error); - reject(error); - } - }) - } else { - resolve([]); - } - } catch(ex) { - reject(ex); - } - }); - } - - - let _this = this; - const tSampleSize = userActions.getSelectedSampleSize(); - - iStateCodes = iStateCodes || []; - let stateAttribute = iAllAttributes.State; - let stateMap = stateAttribute.categories; - let stateNames = iStateCodes.length? - iStateCodes.map(function (sc) { return stateMap[sc]; }): - ['all']; - - iYears = iYears || []; - - let chunks = stateNames.length * iYears.length; - if (!chunks) { - return Promise.resolve([]); - } - let chunkSize = tSampleSize/chunks; - let fetchPromises = []; - stateNames.forEach(function (stateName) { - iYears.forEach(function (year) { - // if chunk size is large get double, so we can get a unique subsample - if (chunkSize > 900) { - fetchPromises.push(fetchSubsampleChunk(stateName, year, chunkSize/2)); - fetchPromises.push(fetchSubsampleChunk(stateName, year, chunkSize/2)); - } else { - fetchPromises.push(fetchSubsampleChunk(stateName, year, chunkSize)); - } - }); - }); - return Promise.all(fetchPromises); - }, - - getDatasetNames: function () { - if (this.metadata.datasets) { - let names = this.metadata.datasets.map(function (ds) { return ds.name;}); - return names; - } else { - return []; - } - }, - getStateNames: function () { - let stateSet = {}; - if (this.metadata.datasets) { - this.metadata.datasets.forEach(function (ds) { - ds.presetCollections.forEach(function (name) { - stateSet[name] = name; - }); - }); - return Object.keys(stateSet); - } else { - return []; - } - }, - - yearHasState: function (year, stateName) { - year = String(year); - if (this.metadata.datasets) { - let yearDataset = this.metadata.datasets.find(function (ds) { return ds.name === year}); - return (yearDataset && (yearDataset.presetCollections.indexOf(stateName) >= 0)); - } - }, - - getDBInfo: async function (iType, metadataURL) { - if (!this.metadata) { - let response = await fetch(metadataURL); - if (response.ok) { - this.metadata = await response.json(); - } else { - this.metadata = {}; - console.warn(`Metadata Fetch error: ${response.statusText}`); - } - } - if (iType === 'getYears') { - return this.getDatasetNames(); - } - else if (iType === 'getStates') { - return this.getStateNames(); - } - } -}; - -export {DBconnect}; diff --git a/src/scripts/app.constants.js b/src/scripts/app.constants.js deleted file mode 100644 index 515c780..0000000 --- a/src/scripts/app.constants.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * ========================================================================== - * Copyright (c) 2018 by eeps media. - * Last modified 8/21/18 9:18 AM - * - * Created by Tim Erickson on 8/21/18 9:18 AM - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - * ========================================================================== - * - */ -let constants = { - version: "v0006", - appName: 'Microdata Portal', - appTitle: 'Microdata Portal', - appDefaultWidth: 380, - appDefaultHeight: 520, - - metadataURL: './assets/data/metadata.json', - - datasetName: "US Microdata", - datasetTitle: "US Microdata", - datasetDescription: 'US Population Microdata from the Microdata Portal', - datasetParentCollectionName: "places", - datasetChildCollectionName: "people", - caseTableName: "People", - - kMinCases: 0, - kMaxCases: 1000, - kDefaultSampleSize: 100, - defaultSelectedYears: [2017], - defaultSelectedStates: [], - defaultSelectedAttributes: ['Sex', 'Age', 'Year', 'State', 'Boundaries'] -}; - -export {constants}; diff --git a/src/scripts/app.js b/src/scripts/app.js deleted file mode 100644 index f4498fb..0000000 --- a/src/scripts/app.js +++ /dev/null @@ -1,146 +0,0 @@ -/* - * ========================================================================== - * Copyright (c) 2018 by eeps media. - * Last modified 8/18/18 9:03 PM - * - * Created by Tim Erickson on 8/18/18 9:03 PM - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and limitations under the License. - * ========================================================================== - * - */ -/* global: xml2js */ - -import * as attributeConfig from './attributeConfig.js'; -import {ui} from './app.ui.js'; -import {userActions} from "./app.userActions.js"; -import {CODAPconnect} from "./app.CODAPconnect.js"; -import {DBconnect} from "./app.DBconnect.js"; -import {Attribute} from "./Attribute.js" -import {constants} from "./app.constants.js"; - -window.app = { - state: null, - allAttributes: {}, // object containing all Attributes (a class), keyed by NAME. - - freshState: { - sampleNumber: 1, - sampleSize: 16, - selectedYears: constants.defaultSelectedYears, - selectedStates: constants.defaultSelectedStates, - selectedAttributes: constants.defaultSelectedAttributes, - keepExistingData: false, - activityLog: [] - }, - - logConnectionInfo: function () { - let info = navigator.connection || navigator.mozConnection || navigator.webkitConnection; - if (info) { - this.addLog('Connection: ' + [info.type, info.effectiveType, - info.saveData, info.rtt, info.downlink, info.downlinkMax].join('/') ); - ui.updateWholeUI(); - } - }, - - initialize: async function () { - // function handleError(message) { - // console.warn("Initializing Microdata Portal: " + message); - // } - ui.displayStatus('initializing', "Initializing"); - await CODAPconnect.initialize(null); - app.logConnectionInfo(); - await app.getAllAttributes(); - app.years = await DBconnect.getDBInfo("getYears", constants.metadataURL); - app.states = await DBconnect.getDBInfo('getStates', constants.metadataURL); - ui.init(); - ui.displayStatus('success', "Ready"); - }, - - updateStateFromDOM: function (logMessage) { - if (!this.state) { - // initialize state from CODAP, then update state - } - else { - this.state.selectedYears = userActions.getSelectedYears(); - this.state.selectedStates = userActions.getSelectedStates(); - this.state.selectedAttributes = userActions.getSelectedAttrs(); - this.state.requestedSampleSize = userActions.getRequestedSampleSize(); - if (logMessage) { - CODAPconnect.logAction(logMessage); - } - } - ui.updateWholeUI(); - }, - - addLog: function (logMessage) { - if (this.state) { - if (!this.state.activityLog) { - this.state.activityLog = []; - } - this.state.activityLog.push({time:new Date().toLocaleString(), message: logMessage}); - } - }, - - getDataDictionary: function (codebook) { - const kSpecialNumeric = ['FAMSIZE', 'AGE']; - let tCbkObject = xml2js(codebook, {}), - tDescriptions = tCbkObject.elements[1].elements[3]; - return tDescriptions.elements.map(function (iDesc) { - let tCats = {}; - iDesc.elements.forEach(function (iElement) { - if (iElement.name === 'catgry') { - let key = Number(iElement.elements[0].elements[0].text); - tCats[key] = iElement.elements[1].elements[0].text; - } - }); - return { - name: iDesc.attributes.name, - startPos: iDesc.elements[0].attributes.StartPos, - width: Number(iDesc.elements[0].attributes.width), - labl: iDesc.elements[1].elements[0].text, - description: iDesc.elements[2].elements[0].cdata, - format: (Object.keys(tCats).length === 0 || kSpecialNumeric.indexOf( - iDesc.attributes.name) >= 0) ? 'numeric' : 'categorical', - categories: tCats - } - }); - }, - - getPartitionCount: function () { - let numStates = app.state.selectedStates.length || 1; - let numYears = app.state.selectedYears.length || 1; - return numStates * numYears; - }, - - getAllAttributes: async function () { - let result = await fetch('./assets/data/codebook.xml'); - if (result.ok) { - let codeBook = await result.text(); - let dataDictionary = this.getDataDictionary(codeBook); - attributeConfig.attributeAssignment.forEach(function (configAttr) { - let codebookDef = dataDictionary.find(function (def) { - return def.name === configAttr.ipumsName; - }); - let tA = new Attribute(codebookDef, configAttr, app.allAttributes); - app.allAttributes[tA.title] = tA; - }); - - $("#chooseAttributeDiv").html(ui.makeAttributeListHTML()); - return app.allAttributes; - } else { - console.log('CodeBook fetch failed'); - } - } - -}; - -app.initialize(); diff --git a/src/scripts/app.ui.js b/src/scripts/app.ui.js deleted file mode 100644 index 49b5878..0000000 --- a/src/scripts/app.ui.js +++ /dev/null @@ -1,420 +0,0 @@ -/* - * ========================================================================== - * Copyright (c) 2018 by eeps media. - * Last modified 8/21/18 9:10 AM - * - * Created by Tim Erickson on 8/21/18 9:10 AM - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - * ========================================================================== - * - */ -/* global app */ - -import * as attributeConfig from './attributeConfig.js'; -import {constants} from './app.constants.js'; -import {userActions} from "./app.userActions.js"; - -let ui = (function () { - function findAncestorElementWithClass(el, myClass) { - while (el !== null && el.parentElement !== el) { - if (el.classList.contains(myClass)) { - return el; - } - el = el.parentElement; - } - } - - function setEventHandler (selector, event, handler) { - const elements = document.querySelectorAll(selector); - if (!elements) { return; } - elements.forEach(function (el) { - el.addEventListener(event, handler); - }); - } - - function toggleClass(el, myClass) { - let isOpen = el.classList.contains(myClass); - if (isOpen) { - el.classList.remove(myClass); - } else { - el.classList.add(myClass); - } - } - - function togglePopUp(el) { - toggleClass(el, 'wx-open'); - } - - function togglePopOver(el) { - toggleClass(el, 'wx-open'); - } - - function toggleDescriptions(el) { - toggleClass(el, 'show-descriptions'); - } - - /** - * A utility to create a DOM element with classes and content. - * @param tag {string} - * @param [classList] {[string]} - * @param [content] {[Node]} - * @return {Element} - */ - function createElement(tag, classList, content) { - let el = document.createElement(tag); - if (classList) { - if (typeof classList === 'string') classList = [classList]; - classList.forEach( function (cl) {el.classList.add(cl);}); - } - if (content) { - if (!Array.isArray(content)) { content = [content];} - content.forEach(function(c) { - if (c instanceof Attr) { - el.setAttributeNode(c); - } else { - el.append(c); - } - }); - } - return el; - } - - /** - * A utility to create a DOM attribute node. - * @param name {string} - * @param value {*} - * @return {Attr} - */ - function createAttribute(name, value) { - let attr = document.createAttribute(name); - attr.value = value; - return attr; - } - - - return { - initialized: false, - - init: function () { - setEventHandler('#chooseAttributeDiv input', 'change', - userActions.changeAttributeCheckbox) - - $('#chooseSampleYearsDiv').html(ui.makeYearListHTML()); - - setEventHandler('#chooseSampleYearsDiv input', 'change', - userActions.changeSampleYearsCheckbox); - - document.getElementById('chooseStatesDiv').append(ui.makeStateList()); - - setEventHandler('#chooseStatesDiv input','change', - userActions.changeSampleStateCheckbox); - - setEventHandler('#sampleSizeInput', 'change', function (/*ev*/) { - userActions.updateRequestedSampleSize('Sample size change.'); - }); - - setEventHandler('#getCasesButton', 'click', - userActions.pressGetCasesButton); - - setEventHandler('#keepExistingDataCheckbox', 'change', - userActions.getKeepExistingDataOption); - - setEventHandler('.wx-dropdown-header', 'click', function (/*ev*/) { - let dropdownGroup = findAncestorElementWithClass(this, 'wx-dropdown-group'); - let sectionEl = findAncestorElementWithClass(this, 'wx-dropdown'); - let isClosed = sectionEl.classList.contains('wx-up'); - let dropDowns = (dropdownGroup||document).querySelectorAll('.wx-dropdown.wx-down'); - if (dropDowns) { - dropDowns.forEach(function (el) { - el.classList.remove('wx-down'); - el.classList.add('wx-up'); - }) - } - if (isClosed) { - sectionEl.classList.remove('wx-up'); - sectionEl.classList.add('wx-down'); - } - }); - - setEventHandler('.wx-pop-up-anchor,#wx-info-close-button', 'click', function (/*ev*/) { - let parentEl = findAncestorElementWithClass(this, 'wx-pop-up'); - togglePopUp(parentEl); - }); - - setEventHandler('.wx-pop-over-anchor', 'click', function (/*ev*/) { - let parentEl = findAncestorElementWithClass(this, 'wx-pop-over'); - togglePopOver(parentEl); - }); - - setEventHandler('.show-attr-description-checkbox', 'click', function (/*ev*/) { - let parentEl = findAncestorElementWithClass(this, 'attributeCheckboxes'); - toggleDescriptions(parentEl); - }); - - setEventHandler('html', 'click', userActions.selectHandler, true); - - this.initialized = true; - this.updateWholeUI(); - }, - - makeStateList: function () { - function makeItem(label, value, myClass, checked) { - let inputEl = createElement('input', [myClass], - [ - createAttribute('type', 'radio'), - createAttribute('name', 'state'), - createAttribute('id', value) - ] - ); - if (checked) { - inputEl.setAttributeNode(createAttribute('checked', 'checked')); - } - return createElement('label', null, [inputEl, label]); - } - - let out = createElement('div'); - out.append(makeItem('all states', 'state-all', 'select-all', true)); - // noinspection JSUnresolvedVariable - let stateAttribute = app.allAttributes.State; - let stateMap = stateAttribute.categories; - Object.keys(stateMap).forEach(function (stateCode) { - let stateName = stateMap[stateCode]; - if (app.states.indexOf(stateName) >= 0) { - let id = 'state-' + stateCode; - out.append(makeItem(stateName, id, 'select-item', false)); - } - }); - return out; - }, - - makeYearListHTML: function () { - let out = ''; - let checked = ''; - if (app.years) { - app.years.forEach(function (year) { - let id = 'year-' + year; - out += '
' + '
'; - checked = ''; - }); - } - return out; - }, - - makeAttributeListHTML: function () { - let out = ""; - - attributeConfig.attributeGroups.forEach( (g)=>{ - out += '
\n'; - out += `
`; - out += ` ${g.title}`; - out += ' '; - out += ' '; - out += ' '; - out += '
'; - out += '
'; - out += this.makeAttributeGroupHTML(g); - out += '
'; - out += '
'; - }); - - return out; - }, - - makeAttributeGroupHTML: function (iGroupObject) { - let out = ""; - - out += '
\n'; - out += '' + - ''; - for (let attName in app.allAttributes) { - if (app.allAttributes.hasOwnProperty(attName)) { - const tAtt = app.allAttributes[attName]; // the attribute - let tReadonlyClause = tAtt.readonly? 'disabled="true" readonly="true"': ''; - // noinspection EqualityComparisonWithCoercionJS - if (tAtt.groupNumber == iGroupObject.number) { // not === because one may be a string - if (tAtt.displayMe) { - tAtt.hasCheckbox = true; // redundant - out += ''; - out += '\n'; - out += '\n"; - } - } - } - } - out += "
 Attribute
' - + tAtt.title + '
'; - out += '
' + tAtt.description + '
\n'; - out += "
\n"; - return out; - }, - - updateWholeUI: function () { - if (!this.initialized) return; - ui.refreshAttributeCheckboxes(); - ui.refreshStateCheckboxes(); - ui.refreshYearCheckboxes(); - ui.refreshSampleSummary(); - ui.refreshText(); - ui.refreshLog(); - }, - - refreshAttributeCheckboxes: function () { - let activeAttributes = app.state.selectedAttributes; - $('#chooseAttributeDiv .select-item').prop('checked', false); - if (activeAttributes) { - activeAttributes.forEach(function (attrName) { - $('#attr-' + attrName).prop('checked', true); - }); - } - - Object.values(app.allAttributes).forEach(function (attr) { - if (attr.formulaDependents) { - let $el = $('#' + attr.checkboxID); - if (!this.checkDependentSelected(attr.formulaDependents.split(','))) { - $el.prop('checked', false); - $el.prop('disabled', true); - } else { - $el.prop('disabled', false); - } - } - }.bind(this)); - }, - - refreshStateCheckboxes: function () { - let activeStateCodes = app.state.selectedStates; - $('#states .select-item, #states .select-all').prop('checked', false); - if (!activeStateCodes || (activeStateCodes.length === 0)) { - $('#state-all').prop('checked', true); - } else { - activeStateCodes.forEach(function (stateCode) { - $('#state-' + stateCode).prop('checked', true); - }); - } - }, - - refreshYearCheckboxes: function () { - let activeYears = app.state.selectedYears; - $('#sampleYears .select-item').prop('checked', false); - if (activeYears) { - activeYears.forEach(function (year) { - $('#year-' + year).prop('checked', true); - }); - } - }, - - refreshSampleSummary: function () { - function makeList(array) { - let rtn = ''; - if (array && array.length > 0) { - let length = array.length; - if (length === 1) { - rtn = array[0]; - } else if (length === 2) { - rtn = `${array[0]} and ${array[1]}`; - } else { - rtn = array.join(', '); - } - } - return rtn; - } - - // noinspection JSUnresolvedVariable - const stateAttr = app.allAttributes.State; - let states = app.state.selectedStates.map(function (st) { return stateAttr.categories[Number(st)]; }); - - let statesLength = states.length || ' '; - if (states.length === 0) { - states = ['all']; - } - - let years = app.state.selectedYears; - let attrs = app.state.selectedAttributes; - - let attrCountEl = document.querySelector('#attribute-section .wx-selection-count'); - let attrListEl = document.querySelector('#attribute-section .wx-user-selection'); - let statesCountEl = document.querySelector('#states-section .wx-selection-count'); - let statesListEl = document.querySelector('#states-section .wx-user-selection'); - let yearsCountEl = document.querySelector('#years-section .wx-selection-count'); - let yearsListEl = document.querySelector('#years-section .wx-user-selection'); - - if (attrCountEl && attrs) attrCountEl.innerHTML = '' + attrs.length; - if (attrListEl && attrs) attrListEl.innerHTML = makeList(attrs); - if (statesCountEl && states) statesCountEl.innerHTML = statesLength; - if (statesListEl && states) statesListEl.innerHTML = makeList(states); - if (yearsCountEl && years) yearsCountEl.innerHTML = '' + years.length; - if (yearsListEl && years) yearsListEl.innerHTML = makeList(years); - let subsectionCountEls = document.querySelectorAll( - '#chooseAttributeDiv .wx-selection-count'); - - subsectionCountEls.forEach(function (el) { - let parentEl = findAncestorElementWithClass(el, 'wx-dropdown'); - let checkedEls = parentEl.querySelectorAll('.select-item:checked'); - el.innerHTML = '' + checkedEls.length; - }); - }, - - refreshText: function () { - $('#sampleSizeInput').val(app.state.requestedSampleSize || constants.kDefaultSampleSize); - $('#keepExistingDataCheckbox')[0].checked = app.state.keepExistingData; - }, - - refreshLog: function () { - let activityLog = app.state.activityLog; - let tabContentNode = $('#log .wx-dropdown-body'); - let tableRows = activityLog && activityLog.map(function (logEntry) { - return $('').append($('').text(logEntry.time)).append($('').text(logEntry.message)); - }); - let table = $('').append(tableRows); - tabContentNode.empty().append(table); - }, - - /** - * Expects an array of attr names. Returns true if they are all selected. - * @param dependents - */ - checkDependentSelected: function (dependents) { - let selectedAttributes = app.state && app.state.selectedAttributes; - if (!selectedAttributes) { - return false; - } - return dependents.every(function (dep) { - return (selectedAttributes.includes(dep)); - }); - }, - - /** - * - * @param status {'initializing', 'inactive', 'retrieving', 'transferring', 'success', 'failure'} - * @param message - */ - displayStatus: function (status, message) { - let el = document.querySelector('.wx-summary'); - let statusClass = { - initializing: 'wx-transfer-in-progress', - inactive: '', - retrieving: 'wx-transfer-in-progress', - transferring: 'wx-transfer-in-progress', - success: 'wx-transfer-success', - failure: 'wx-transfer-failure' - }[status]||''; - el.classList.remove( - 'wx-transfer-in-progress', - 'wx-transfer-success', - 'wx-transfer-failure'); - if (statusClass) { el.classList.add(statusClass); } - $('#status').text(message); - }, - - }; -})(); - -export {ui}; diff --git a/src/scripts/app.userActions.js b/src/scripts/app.userActions.js deleted file mode 100644 index 514bfd3..0000000 --- a/src/scripts/app.userActions.js +++ /dev/null @@ -1,161 +0,0 @@ -/* - * ========================================================================== - * Copyright (c) 2018 by eeps media. - * Last modified 8/21/18 9:07 AM - * - * Created by Tim Erickson on 8/21/18 9:07 AM - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - * ========================================================================== - * - */ - -import {constants} from './app.constants.js'; -import {CODAPconnect} from "./app.CODAPconnect.js"; -import {DBconnect} from "./app.DBconnect.js"; -import {ui} from "./app.ui.js" - -let userActions = { - - pressGetCasesButton : async function() { - try { - console.log("get cases!"); - let oData = []; - ui.displayStatus('retrieving', 'Fetching data...'); - let tData = await DBconnect.getCasesFromDB(app.state.selectedAttributes, - app.state.selectedStates, app.state.selectedYears, app.allAttributes); - - // If tData is empty, there must have been an error. We are relying on - // lower layers to log the failure. - if (!tData) { - ui.displayStatus('failure', 'Fetch Error. Please retry.'); - ui.updateWholeUI(); - return; - } - - tData = tData.flat(1); - - let counter = 0; - // okay, tData is an Array of objects whose keys are the variable names. - // now we have to translate names and values... - ui.displayStatus('transferring', 'Formatting data...'); - tData.forEach( function(c) { - // c is a case object - let sampleData = c; - if (!sampleData) { return; } - let o = { sample : app.state.sampleNumber }; - app.state.selectedAttributes.forEach(function (attrTitle) { - let attr = app.allAttributes[attrTitle]; - o[attr.title] = attr.decodeValue(sampleData); - }); - oData.push(o); - counter ++; - }); - - await CODAPconnect.guaranteeDataset(); - - // make sure the case table is showing - ui.displayStatus('transferring', 'Opening case table...'); - let id = await CODAPconnect.makeCaseTableAppear(); - - ui.displayStatus('transferring', 'Sending data to codap...'); - if (!app.state.keepExistingData) { - await CODAPconnect.deleteAllCases(); - } - await CODAPconnect.saveCasesToCODAP( oData ); - ui.displayStatus('success', `Selected a random sample of ${counter} people`); - setTimeout(function () {CODAPconnect.autoscaleComponent(id);}, 1000); - app.state.sampleNumber++; - } catch (ex) { - console.log(ex); - ui.displayStatus('failure', 'Fetch Error. Please retry.'); - } - ui.updateWholeUI(); - }, - - changeAttributeCheckbox : function(/*iAttName*/) { - // const tAtt = app.allAttributes[iAttName]; - // - // tAtt.chosen = !tAtt.chosen; - app.updateStateFromDOM('Attribute selection changed.'); - }, - - changeSampleYearsCheckbox : function (/*event*/) { - app.updateStateFromDOM('Sample years changed.'); - }, - - changeSampleStateCheckbox : function (/*event*/) { - // record change of status for selected states and potentially toggle 'all' option - if ($(this).hasClass('select-all')) { - $('#chooseStatesDiv .select-item').prop('checked', false); - } - // noinspection JSJQueryEfficiency - let $itemBoxes = $('#chooseStatesDiv .select-item'); - let $allBox = $('#chooseStatesDiv .select-all'); - if ($itemBoxes.filter(':checked').length > 0) { - $allBox.prop('checked',false); - } else { - $allBox.prop('checked', true); - } - app.updateStateFromDOM('sample state changed'); - }, - - getSelectedAttrs: function () { - let rslt = []; - $('#chooseAttributeDiv .select-item:checked').each(function (ix, el) { - rslt.push(el.id.replace('attr-', '')); - }); - return rslt; - }, - - getSelectedYears: function () { - let rslt = []; - $('#chooseSampleYearsDiv .select-item:checked').each(function (ix, el) { - rslt.push(el.id.replace('year-', '')); - }); - return rslt; - }, - - getSelectedStates: function () { - let rslt = []; - $('#chooseStatesDiv .select-item:checked').each(function (ix, el) { - rslt.push(el.id.replace('state-', '')); - }); - return rslt; - }, - - /** - * This is the raw request, not the quantity we will actually return - */ - getRequestedSampleSize: function () { - return $("#sampleSizeInput")[0].value; - }, - - getSelectedSampleSize: function () { - let requestedSize = $("#sampleSizeInput")[0].value; - let numPartitions = app.getPartitionCount(); - let constrainedSize = Math.max(constants.kMinCases, Math.min(constants.kMaxCases, requestedSize)); - let partitionSize = Math.round(constrainedSize/numPartitions) || 1; - return partitionSize * numPartitions; - }, - - getKeepExistingDataOption: function () { - app.state.keepExistingData = $('#keepExistingDataCheckbox').is(':checked'); - }, - updateRequestedSampleSize: function () { - app.state.requestedSampleSize = $("#sampleSizeInput")[0].value; - ui.updateWholeUI(); - }, - selectHandler: function () { - CODAPconnect.selectSelf(); - } - -}; - -export {userActions}; diff --git a/src/scripts/attributeConfig.js b/src/scripts/attributeConfig.js deleted file mode 100644 index 8b06e68..0000000 --- a/src/scripts/attributeConfig.js +++ /dev/null @@ -1,864 +0,0 @@ -// ========================================================================== -// -// Author: jsandoe -// -// Copyright (c) 2018 by The Concord Consortium, Inc. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========================================================================== -/** - * - * Configuration properties. - */ - - /* - * The following declarations are specific to the data set. - * Should consider moving to separate json. - */ -let attributeGroups = [ - { - number: 1, - open: false, - title: 'Basic demographics', - tooltip: 'Choose characteristics like age, sex, and marital status' - }, - { - number: 2, - open: false, - title: 'Race, ancestry, origins', - tooltip: "Choose characteristics relating to person's place of birth and ethnicity" - }, - { - number: 3, - open: false, - title: 'Work & employment', - tooltip: "Choose characteristics relating to person's work" - }, - { - number: 4, - open: false, - title: 'Income', - tooltip: "Choose characteristics relating to person's income" - }, - { - number: 5, - open: false, - title: 'Geography', - tooltip: "Choose characteristics relating to where the person resides" - }, - { - number: 6, - open: false, - title: 'Other', - tooltip: "Choose other characteristics" - }]; - -let attributeAssignment = [{ - ipumsName: 'AGE', - title: 'Age', - group: 1, - defCheck: true, - description: 'The individual\'s age in years as of the ' + - 'last birthday. Values range from 0 (less than 1 year old) to 90 ' + - 'and above. See codebook for special codes.' - }, - { - title: 'Age_recode', - group: 1, - originalAttr: 'Age', - format: 'categorical', - defCheck: false, - description: 'The individual’s age in years as of the ' + - 'last birthday. Recodes the Age variable into 8 age categories.', - rangeMap: [ - {from: 0, to: 17, recodeTo: 0}, - {from: 18, to: 24, recodeTo: 1}, - {from: 25, to: 34, recodeTo: 2}, - {from: 35, to: 44, recodeTo: 3}, - {from: 45, to: 54, recodeTo: 4}, - {from: 55, to: 64, recodeTo: 5}, - {from: 65, to: 74, recodeTo: 6}, - {from: 75, to: 999, recodeTo: 7}, - ], - categories: [ - 'under 18', - '18-24', - '25-34', - '35-44', - '45-54', - '55-64', - '65-74', - '75 and older' - ] - }, - { - ipumsName: 'SEX', - title: 'Sex', - group: 1, - defCheck: true, - description: 'Each individual\'s biological sex as male or female.' - }, - { - ipumsName: 'MARST', - title: 'Marital_status', - group: 1, - defCheck: false, - description: 'Each individual’s current marital status, with ' + - '6 possible categories.' - }, - { - ipumsName: 'NCHILD', - title: 'Number_of_children', - group: 1, - defCheck: false, - description: 'Counts the number of own children (of any age or ' + - 'marital status) residing with each individual. Values range from ' + - '0 to a top code of 9+.' - }, - { - ipumsName: 'FAMSIZE', - title: 'Family_size', - group: 1, - defCheck: false, - description: 'The number of own family members residing ' + - 'with each individual, including the person her/himself. Values ' + - 'range from 1 to as high as 26 or higher in some years.' - }, - { - ipumsName: 'EDUC', - title: 'Education-years', - hasCategoryMap: true, - displayMe: false, - group: 1, - defCheck: false, - description: 'The individual’s level of educational ' + - 'attainment based on the highest level or year of school completed.', - categories: [ - "N/A or no schooling", - "Nursery school to grade 4", - "Grade 5, 6, 7, or 8", - "Grade 9", - "Grade 10", - "Grade 11", - "Grade 12", - "1 year of college", - "2 years of college", - "3 years of college", - "4 years of college", - "5+ years of college", - ] - }, - { - ipumsName: 'EDUCD', - title: 'Education-degree_recode', - group: 1, - format: 'categorical', - hasCategoryMap: true, - defCheck: false, - description: 'The individual’s level of educational ' + - 'attainment based on the highest degree completed, in years for which ' + - 'this information is available.', - rangeMap: [ - {from: 0, to: 2, recodeTo: 0}, - {from: 3, to: 50, recodeTo: 1}, - {from: 51, to: 59, recodeTo: 11}, - {from: 60, to: 60, recodeTo: 2}, - {from: 61, to: 61, recodeTo: 1}, - {from: 62, to: 64, recodeTo: 3}, - {from: 65, to: 71, recodeTo: 4}, - {from: 72, to: 79, recodeTo: 11}, - {from: 80, to: 80, recodeTo: 5}, - {from: 81, to: 83, recodeTo: 7}, - {from: 84, to: 89, recodeTo: 11}, - {from: 90, to: 90, recodeTo: 5}, - {from: 91, to: 99, recodeTo: 11}, - {from: 100, to: 100, recodeTo: 5}, - {from: 101, to: 101, recodeTo: 8}, - {from: 102, to: 109, recodeTo: 11}, - {from: 110, to: 113, recodeTo: 6}, - {from: 114, to: 115, recodeTo: 9}, - {from: 116, to: 116, recodeTo: 10}, - ], - categories: [ - 'N/A or no schooling completed', - 'Some schooling, no high school diploma', - 'Completed Grade 12, diploma not identified', - 'High school diploma or GED', - '1 or more years of college, no degree', - '2-4 years of college, degree not identified', - '5+ years of college, degree not identified', - 'Associate’s degree', - 'Bachelor’s degree', - 'Master’s or professional degree', - 'Doctoral degree', - 'unknown' - ] - }, - { - ipumsName: 'RACE', - title: 'Race-multi', - displayMe: false, - group: 2, - defCheck: false, - description: 'Each individual’s race according to 9 categories, ' + - 'including categories for mixed-race individuals. Caution needed when ' + - 'making comparisons over time.' - }, - { - ipumsName: 'RACESING', - title: 'Race-single', - group: 2, - displayMe: false, - defCheck: false, - description: 'Assigns individuals to one of 5 race categories and ' + - 'assigns a single race to multiple-race people. Comparable over time, ' + - 'but not available after 2014.' - }, - { - originalAttr: ['Race-multi','Hispanic'], - title: 'Race_ethnicity-multi_recode', - group: 2, - format: 'categorical', - defCheck: false, - description: 'Each respondent’s combined race and Hispanic ' + - 'ethnicity status, grouped into 7 primary categories. Caution needed ' + - 'when making comparisons over time.', - // formula: "(Hispanic!='Not Hispanic')?" + - // "'Hispanic':(`Race-multi`='White')?'Non-Hispanic White':" + - // "(`Race-multi`='Black/African American/Negro')?'Non-Hispanic Black':" + - // "(`Race-multi`='Other Asian or Pacific Islander')?'Non-Hispanic Asian or Pacific Islander':" + - // "(`Race-multi`='Chinese')?'Non-Hispanic Asian or Pacific Islander':" + - // "(`Race-multi`='Japanese')?'Non-Hispanic Asian or Pacific Islander':" + - // "(`Race-multi`='American Indian or Alaska Native')?'Non-Hispanic American Indian/Alaska Native':" + - // "(`Race-multi`='Three or more major races')?'Non-Hispanic two or more major races':" + - // "(`Race-multi`='Two major races')?'Non-Hispanic two or more major races':" + - // "'Non-Hispanic Other race'", - // formulaDependents: 'Hispanic,Race-multi', - multirangeMap: [ - {range:{Hispanic:{from:1, to:4}}, recodeTo: 0}, - {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 1, to: 1}}, recodeTo: 1}, - {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 2, to: 2}}, recodeTo: 2}, - {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 3, to: 3}}, recodeTo: 3}, - {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 4, to: 6}}, recodeTo: 4}, - {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 7, to: 7}}, recodeTo: 6}, - {range:{Hispanic:{from:0, to:0}, 'Race-multi':{from: 8, to: 9}}, recodeTo: 5} - ], - categories: [ -/*0*/ 'Hispanic', -/*1*/ 'Non-Hispanic White', -/*2*/ 'Non-Hispanic Black', -/*3*/ 'Non-Hispanic American Indian/Alaska Native', -/*4*/ 'Non-Hispanic Asian and/or Pacific Islander', -/*5*/ 'Non-Hispanic two or more major races', -/*6*/ 'Non-Hispanic Other race' - ] - }, - { - originalAttr: ['Race-single', 'Hispanic'], - title: 'Race_ethnicity-single_recode', - group: 2, - format: 'categorical', - defCheck: false, - description: 'Each respondent’s combined race and Hispanic ' + - 'ethnicity status, grouped into 6 primary categories. Comparable over ' + - 'time, but not available after 2014.', - // formula: "(Hispanic!='Not Hispanic')?'Hispanic':" + - // "(`Race-single`='')?'Non-Hispanic':" + - // "(`Race-single`='White')?'Non-Hispanic White':" + - // "(`Race-single`='Black')?'Non-Hispanic Black':" + - // "(`Race-single`='American Indian/Alaska Native')?'Non-Hispanic American Indian/Alaska Native':" + - // "(`Race-single`='Asian and/or Pacific Islander')?'Non-Hispanic Asian or Pacific Islander':" + - // "(`Race-single`='Other race, non-Hispanic')?'Non-Hispanic Other race':" + - // "'Non-Hispanic Other'", - // formulaDependents: 'Hispanic,Race-single', - multirangeMap: [ - {range:{Hispanic:{from:1, to:4}}, recodeTo: 0}, - {range:{Hispanic:{from:0, to:0}, 'Race-single':{from: 1, to: 1}}, recodeTo: 1}, - {range:{Hispanic:{from:0, to:0}, 'Race-single':{from: 2, to: 2}}, recodeTo: 2}, - {range:{Hispanic:{from:0, to:0}, 'Race-single':{from: 3, to: 3}}, recodeTo: 3}, - {range:{Hispanic:{from:0, to:0}, 'Race-single':{from: 4, to: 4}}, recodeTo: 4}, - {range:{Hispanic:{from:0, to:0}, 'Race-single':{from: 5, to: 5}}, recodeTo: 5} - ], - categories: [ - 'Hispanic', - 'Non-Hispanic White', - 'Non-Hispanic Black', - 'Non-Hispanic American Indian/Alaska Native', - 'Non-Hispanic Asian and/or Pacific Islander', - 'Non-Hispanic Other race' - ] - }, - - { - ipumsName: 'HISPAN', - title: 'Hispanic', - group: 2, - defCheck: false, - description: 'Identifies individuals of Hispanic, Spanish, or Latino ' + - 'origin and classifies them according to their country of origin. ' + - 'The U.S. Census considers Hispanic origin to be an ethnic rather than a ' + - 'racial classification; individuals of Hispanic origin can therefore ' + - 'be of any race.' - }, - { - title: 'Hispanic_recode', - originalAttr: 'Hispanic', - group: 2, - defCheck: false, - description: 'Identifies whether individuals are ' + - 'of Hispanic, Spanish, or Latino origin. Recodes the Hispanic variable ' + - 'into two codes.', - format: 'categorical', - rangeMap: [ - {from: 0, to: 0, recodeTo: 0}, - {from: 1, to: 4, recodeTo: 1} - ], - categories: [ - 'Not of Hispanic, Spanish, or Latino origin', - 'Hispanic, Spanish, or Latino origin' - ] - }, - { - title: 'Hispanic-detailed_recode', - ipumsName: 'HISPAND', - group: 2, - defCheck: false, - format: 'categorical', - description: 'Identifies individuals of Hispanic, Spanish, or Latino ' + - 'origin and classifies them according to their country of origin. ' + - 'Recodes a detailed version of Hispanic into 15 categories.', - rangeMap: [ - {from: 0, to: 99, recodeTo: 0}, - {from: 100, to: 107, recodeTo: 1}, - {from: 108, to: 199, recodeTo: 15}, - {from: 200, to: 200, recodeTo: 2}, - {from: 201, to: 299, recodeTo: 15}, - {from: 300, to: 300, recodeTo: 4}, - {from: 301, to: 399, recodeTo: 15}, - {from: 401, to: 411, recodeTo: 8}, - {from: 412, to: 412, recodeTo: 5}, - {from: 413, to: 413, recodeTo: 6}, - {from: 414, to: 415, recodeTo: 8}, - {from: 416, to: 416, recodeTo: 7}, - {from: 417, to: 417, recodeTo: 8}, - {from: 418, to: 419, recodeTo: 15}, - {from: 420, to: 422, recodeTo: 12}, - {from: 423, to: 423, recodeTo: 9}, - {from: 424, to: 424, recodeTo: 10}, - {from: 425, to: 425, recodeTo: 12}, - {from: 426, to: 426, recodeTo: 11}, - {from: 427, to: 431, recodeTo: 12}, - {from: 432, to: 449, recodeTo: 15}, - {from: 450, to: 459, recodeTo: 13}, - {from: 460, to: 460, recodeTo: 3}, - {from: 461, to: 464, recodeTo: 15}, - {from: 465, to: 499, recodeTo: 14}, - {from: 500, to: 999, recodeTo: 15} - ], - categories: [ - 'Not of Hispanic, Spanish, or Latino origin', - 'Mexican', - 'Puerto Rican', - 'Dominican', - 'Cuban', - 'Guatemalan', - 'Honduran', - 'Salvadoran', - 'Other Central American (Costa Rican, Nicaraguan, Panamanian, and others)', - 'Columbian', - 'Equadoran', - 'Peruvian', - 'Other South American (Argentinean, Bolivian, Chilean, Paraguayan, ' + - 'Uruguayan, Venezuelan, and others)', - 'Spaniard', - 'Other Hispanic' - ] - }, - { - ipumsName: 'CITIZEN', - title: 'Citizen', - group: 2, - defCheck: false, - description: 'Identifies the citizenship status of individuals, with ' + - '6 categories.' - }, - { - originalAttr: 'Citizen', - title: 'Citizen_recode', - group: 2, - defCheck: false, - format: 'categorical', - description: 'Individual is a U.S. citizen. Recodes the Citizen ' + - 'variable into two primary codes for almost all years available.', - rangeMap: [ - {from: 0, to: 2, recodeTo: 1}, - {from: 3, to: 4, recodeTo: 0}, - {from: 5, to: 9, recodeTo: 2} - ], - categories: [ - 'Not a U.S. citizen', - 'U.S. citizen', - 'Foreign born, citizenship status not reported' - ] - }, - { - ipumsName: 'YRIMMIG', - title: 'Immigrate-year', - group: 2, - defCheck: false, - description: 'The year in which a foreign-born person entered the U.S.' - }, - { - ipumsName: 'BPL', - title: 'Birthplace', - group: 2, - displayMe: false, - defCheck: false, - description: 'Where in the world the respondent was born. ' + - 'Includes up to 188 location categories. Consider working instead ' + - 'with a recoded and simplified version of this variable, called ' + - 'Birth region.' - }, - { - originalAttr: 'Birthplace', - title: 'Birthplace_recode', - group: 2, - format: 'categorical', - defCheck: false, - description: 'Where in the world the person was born. Recodes ' + - 'the Birthplace variable into 9 categories.', - rangeMap: [ - {from: 1, to: 120, recodeTo: 0}, - {from: 150, to: 199, recodeTo: 1}, - {from: 200, to: 300, recodeTo: 2}, - {from: 400, to: 429, recodeTo: 3}, - {from: 430, to: 499, recodeTo: 4}, - {from: 500, to: 524, recodeTo: 5}, - {from: 530, to: 549, recodeTo: 6}, - {from: 550, to: 550, recodeTo: 5}, - {from: 599, to: 599, recodeTo: 5}, - {from: 600, to: 600, recodeTo: 7}, - {from: 700, to: 800, recodeTo: 1}, - {from: 900, to: 999, recodeTo: 8} - ], - categories: [ - 'U.S. state, territory, or outlying region', - 'Canada, Australia, New Zealand, or Pacific Islands', - 'Mexico, Central America, South America, or the Caribbean', - 'Northern or Western Europe', - 'Southern Europe, Central/Eastern Europe, or Russia', - 'East, Southeast, or South Asia', - 'Middle East or Southwest Asia', - 'Africa', - 'Unknown' - ] - }, - { - ipumsName: 'SPEAKENG', - title: 'Speaks_English', - group: 2, - defCheck: false, - description: 'Whether the individual speaks English, ' + - 'speaks only English at home, or how well the individual speaks ' + - 'English. There have been up to 8 codes over time.' - }, - { - ipumsName: 'EMPSTAT', - title: 'Employment_status', - group: 3, - defCheck: false, - description: 'Whether the individual was a part of the ' + - 'labor force, i.e., working or seeking work, and if yes, whether ' + - 'the person was currently unemployed.' - }, - { - ipumsName: 'LABFORCE', - title: 'Labor_force_status', - group: 3, - defCheck: false, - description: 'Whether a person participated in the labor force.' - }, - { - ipumsName: 'CLASSWKR', - title: 'Class_of_worker', - group: 3, - defCheck: false, - description: 'Class of worker indicates whether individuals were ' + - 'self-employed or worked for wages as an employee.' - }, { - ipumsName: 'UHRSWORK', - title: 'Usual_hours_worked', - group: 3, - defCheck: false, - description: 'The number of hours per week that the ' + - 'individual usually worked, if the person worked during the ' + - 'previous year. Values range from 0 (or N/A) to 99 (the top code).' - }, - { - ipumsName: 'WKSWORK2', - title: 'Weeks_worked', - group: 3, - defCheck: false, - description: 'The number of weeks that the individual ' + - 'worked the previous year, with six primary categories.' - }, { - ipumsName: 'WORKEDYR', - title: 'Worked_last_year', - group: 3, - defCheck: false, - description: 'Indicates whether the person worked during the previous year.' - }, - { - ipumsName: 'OCC1950', - title: 'Occupation_1950_basis', - displayMe: false, - group: 3, - defCheck: false, - description: 'The person’s primary occupation, using ' + - 'the Census Bureau’s 1950 occupational classification system. ' + - 'There are several hundred occupation categories.' - }, - { - originalAttr: 'Occupation_1950_basis', - title: 'Occupation_1950_basis_recode', - group: 3, - format: 'categorical', - defCheck: false, - description: 'The person’s primary occupation, using a ' + - 'simplified version of the Census Bureau’s 1950 occupational ' + - 'classification system. There are 8 primary categories.', - rangeMap: [ - {from: 0, to: 99, recodeTo: 1}, - {from: 100, to: 100, recodeTo: 6}, - {from: 123, to: 123, recodeTo: 6}, - {from: 200, to: 290, recodeTo: 2}, - {from: 300, to: 490, recodeTo: 3}, - {from: 500, to: 594, recodeTo: 4}, - {from: 595, to: 595, recodeTo: 7}, - {from: 600, to: 690, recodeTo: 4}, - {from: 700, to: 790, recodeTo: 5}, - {from: 810, to: 970, recodeTo: 6}, - {from: 980, to: 995, recodeTo: 8}, - {from: 997, to: 997, recodeTo: 9}, - {from: 999, to: 999, recodeTo: 10} - ], - categories: [ - '', - 'Professional, technical', - 'Managers, officials, and proprietors', - 'Sales and clerical', - 'Craftsmen and operatives', - 'Service workers', - 'Farmers and laborers', - 'Members of the armed services', - 'Non-occupational response', - 'Occupation missing/unknown', - 'N/A (blank)' - ] - }, - { - ipumsName: 'OCC1990', - title: 'Occupation_1990_basis', - group: 3, - displayMe: false, - defCheck: false, - description: 'The person’s primary occupation, using a ' + - 'modified version of the 1990 Census Bureau occupational ' + - 'classification scheme, from 1950 forward. There are several hundred ' + - 'occupation categories. This attribute is not available for data ' + - 'collected prior to 1950.' - }, - { - originalAttr: 'Occupation_1990_basis', - title: 'Occupation_1990_basis_recode', - group: 3, - format: 'categorical', - defCheck: false, - description: 'The person’s primary occupation, using a ' + - 'simplified version of the Census Bureau’s 1990 occupational ' + - 'classification system. There are 7 primary categories. This ' + - 'attribute is not available for data collected prior to 1950.', - rangeMap: [ - {from: 3, to: 200, recodeTo: 1}, - {from: 203, to: 389, recodeTo: 2}, - {from: 405, to: 469, recodeTo: 3}, - {from: 473, to: 498, recodeTo: 4}, - {from: 503, to: 699, recodeTo: 5}, - {from: 703, to: 889, recodeTo: 6}, - {from: 905, to: 905, recodeTo: 7}, - {from: 991, to: 991, recodeTo: 8}, - {from: 999, to: 999, recodeTo: 9} - ], - categories: [ - '', - 'Managerial and professional   ', - 'Technical, sales, and administrative  ', - 'Service  ', - 'Farming, forestry, and fishing  ', - 'Precision production, craft, and repairers  ', - 'Operators and laborers  ', - 'Military occupations  ', - 'Unemployed', - 'N/A and unknown' - ] - }, - { - ipumsName: 'IND1950', - title: 'Industry_1950', - group: 3, - displayMe: false, - defCheck: false, - description: 'The industry of the individual, using the 1950 Census Bureau industrial classification system.' - }, - { - originalAttr: 'Industry_1950', - title: 'Industry_1950_recode', - group: 3, - format: 'categorical', - defCheck: false, - description: 'The person’s primary occupation, using a simplified version of the Census Bureau’s 1950 industry classification system. There are 9 primary categories.', - rangeMap: [ - {from: 100, to: 126, recodeTo: 1}, - {from: 206, to: 499, recodeTo: 2}, - {from: 506, to: 598, recodeTo: 3}, - {from: 606, to: 699, recodeTo: 4}, - {from: 716, to: 817, recodeTo: 5}, - {from: 826, to: 859, recodeTo: 6}, - {from: 868, to: 899, recodeTo: 7}, - {from: 906, to: 946, recodeTo: 8}, - {from: 976, to: 995, recodeTo: 9}, - {from: 0, to: 0, recodeTo: 10}, - {from: 997, to: 999, recodeTo: 10} - ], - categories: [ - '', - 'Agriculture, forestry, and fishing', - 'Construction, Mining, and Manufacturing', - 'Transportation, Communication, and Other Utilities', - 'Wholesale and Retail Trade', - 'Finance, Business, and Repair Services', - 'Entertainment, Recreation, and Personal Services', - 'Professional and Related Services', - 'Public Administration', - 'Other', - 'Non-classifiable, not reported, or blank, N/A, etc.' - ] - }, - { - ipumsName: 'IND1990', - title: 'Industry_1990', - displayMe: false, - group: 3, - defCheck: false, - description: 'The industry of the individual, using the 1990 Census Bureau industrial classification system. There are several hundred industry categories. This attribute is not available for data collected prior to 1950.' - }, - { - originalAttr: 'Industry_1990', - title: 'Industry_1990_recode', - group: 3, - format: 'categorical', - defCheck: false, - description: 'The industry of the individual, using a simplified version of the 1990 Census Bureau industrial classification system, from 1950 forward. There are 9 primary categories. This attribute is not available for data collected prior to 1950.', - rangeMap: [ - {from: 10, to: 32, recodeTo: 1}, - {from: 40, to: 392, recodeTo: 2}, - {from: 400, to: 472, recodeTo: 3}, - {from: 500, to: 691, recodeTo: 4}, - {from: 700, to: 760, recodeTo: 5}, - {from: 761, to: 810, recodeTo: 6}, - {from: 812, to: 893, recodeTo: 7}, - {from: 900, to: 932, recodeTo: 8}, - {from: 940, to: 960, recodeTo: 9}, - {from: 992, to: 992, recodeTo: 10}, - {from: 999, to: 999, recodeTo: 10}, - {from: 0, to: 0, recodeTo: 11}, - ], - categories: [ - '', - 'Agriculture, forestry, and fishing', - 'Construction, Mining, and Manufacturing', - 'Transportation, Communications, and Other Utilities', - 'Wholesale and Retail Trade', - 'Finance, Business, and Repair Services', - 'Entertainment, Recreation, and Personal Services', - 'Professional and Related Services', - 'Public Administration', - 'Military', - 'Unemployed not classified by industry, or did not respond', - 'Not applicable' - ] - }, - { - ipumsName: 'VETSTAT', - title: 'Veteran_status', - group: 3, - defCheck: false, - description: 'Indicates whether individuals served in the military forces of the U.S. (Army, Navy, Air Force, Marine Corps, or Coast Guard) in time of war or peace. Service includes active duty for any length of time and at any place at home or abroad.' - }, - { - ipumsName: 'INCTOT', - title: 'Income-total', - group: 4, - defCheck: false, - description: 'Each respondent’s total pre-tax income. Total income is the sum of the amounts reported for multiple types of income.' - }, - { - ipumsName: 'INCWAGE', - title: 'Income-wages', - group: 4, - defCheck: false, - description: 'Each respondent’s pre-tax wage or salary income received for work performed as an employee. Amounts are expressed in contemporary dollars.' - }, - { - ipumsName: 'FTOTINC', - title: 'Income-family_total', - group: 4, - defCheck: false, - description: 'Total reports the sum of the pre-tax incomes of all respondents 15 years old and over related to Person 1 in the questionnaire. Amounts are expressed in contemporary dollars.' - }, - // { - // ipumsName: 'INCEARN', - // title: 'Income-earnings', - // group: 4, - // defCheck: false, - // description: 'Income earned from wages or a person\'s own business or farm for the previous year. Amounts are expressed in contemporary dollars.' - // }, - { - ipumsName: 'CPI99', - title: 'CPI99', - group: 4, - defCheck: false, - description: 'The adjustment factor that converts contemporary dollars to constant 1999 dollars. It is a 5-digit numeric variable that has three implied decimals. For example, a CPI99 value of 15423 should be interpreted as 15.423.' - }, - { - ipumsName: 'INCWELFR', - title: 'Income-welfare', - group: 4, - defCheck: false, - description: 'How much pre-tax income (if any) the respondent received during the previous year from various public assistance programs commonly referred to as "welfare." Assistance from private charities is not included. Amounts are expressed in contemporary dollars.' - }, - { - ipumsName: 'POVERTY', - title: 'Poverty', - group: 4, - defCheck: false, - description: 'Each family\'s total income for the previous year as a percentage of the official U.S. poverty threshold, adjusted for inflation.' - }, - { - ipumsName: 'REGION', - title: 'Region', - group: 5, - displayMe: false, - defCheck: false, - description: 'Identifies the U.S. Census region and division where the individual lives. There are four primary regions and nine primary divisions of the U.S., with additional categories for mixed divisions. Consider using Region_recode or Region-division_recode for less detailed versions of this variable.' - }, - { - originalAttr: 'Region', - title: 'Region_recode', - group: 5, - format: 'categorical', - defCheck: false, - description: 'The U.S. Census region where the person lives, with 4 region categories.', - rangeMap: [ - {from: 11, to: 13, recodeTo: 1}, - {from: 21, to: 23, recodeTo: 2}, - {from: 31, to: 34, recodeTo: 3}, - {from: 41, to: 43, recodeTo: 4}, - {from: 91, to: 99, recodeTo: 5} - ], - categories: [ - '', - 'Northeast', - 'Midwest', - 'South', - 'West', - 'Other/not identified', - ] - }, - { - originalAttr: 'Region', - title: 'Region-division_recode', - group: 5, - format: 'categorical', - defCheck: false, - description: 'The U.S. Census division where the person lives, with 9 division categories.', - rangeMap: [ - {from: 11, to: 11, recodeTo: 1}, - {from: 12, to: 12, recodeTo: 2}, - {from: 13, to: 13, recodeTo: 10}, - {from: 21, to: 21, recodeTo: 3}, - {from: 22, to: 22, recodeTo: 4}, - {from: 23, to: 23, recodeTo: 10}, - {from: 31, to: 31, recodeTo: 5}, - {from: 32, to: 32, recodeTo: 6}, - {from: 33, to: 33, recodeTo: 7}, - {from: 34, to: 34, recodeTo: 10}, - {from: 41, to: 41, recodeTo: 8}, - {from: 42, to: 42, recodeTo: 9}, - {from: 43, to: 43, recodeTo: 10}, - {from: 91, to: 99, recodeTo: 11} - ], - categories: [ - '', - 'New England', - 'Middle Atlantic', - 'East North Central', - 'West North Central', - 'South Atlantic', - 'East South Central', - 'West South Central', - 'Mountain', - 'Pacific', - 'Mixed divisions ', - 'Other/not identified', - ] - }, - { - ipumsName: 'STATEFIP', - title: 'State', - group: 5, - defCheck: true, - displayMe: true, - readonly: true, - description: 'The state in which the individual lives, using a federal coding scheme that lists states alphabetically. Note that you must select this attribute if you want to display state names in your case table or graphs.' - }, - { - ipumsName: 'STATEFIP', - title: 'Boundaries', - group: 5, - defCheck: true, - displayMe: true, - readonly: true, - description: 'State boundaries. Requires that the State attribute also be selected.', - formula: 'lookupBoundary(US_state_boundaries, State)', - formulaDependents: 'State' - }, - { - ipumsName: 'MIGRATE1', - title: 'Moved', - group: 5, - defCheck: false, - description: 'Whether the person had moved to a different house within the past year, with several categories.' - }, - { - ipumsName: 'YEAR', - title: 'Year', - group: 6, - format: 'categorical', - defCheck: true, - displayMe: true, - readonly: true, - description: 'The four-digit year of the decennial census or ACS for each person\'s questionnaire responses. Note that you must select this attribute if you want to display year indicators in your case table or graphs.' - }] - -export {attributeGroups, attributeAssignment}; From 03ab1644ca868fd58088258bf80e25411c2e5c26 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 13 Sep 2023 12:54:04 -0400 Subject: [PATCH 05/21] Create attribute dropdown menu. --- package-lock.json | 100 +++++++++++++++++++-------- package.json | 1 + src/components/app.tsx | 8 +-- src/components/attribute-options.tsx | 52 ++++++++++++++ src/components/constants.ts | 47 ++++++++++++- src/components/dropdown.tsx | 23 +++--- src/components/options.scss | 25 +++++++ src/components/options.tsx | 65 +++++++++++++++++ src/components/place-options.tsx | 94 ++++++------------------- src/components/types.ts | 19 +++++ 10 files changed, 318 insertions(+), 116 deletions(-) create mode 100644 src/components/attribute-options.tsx create mode 100644 src/components/options.tsx create mode 100644 src/components/types.ts diff --git a/package-lock.json b/package-lock.json index 1444d47..4eda71c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.5.0", "classnames": "^2.3.2", "iframe-phone": "^1.3.1", "react": "^17.0.2", @@ -5717,8 +5718,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -5789,14 +5789,33 @@ "dev": true }, "node_modules/axios": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", - "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", - "dev": true, + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", + "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", "dependencies": { - "follow-redirects": "^1.14.7" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/babel-jest": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", @@ -6714,7 +6733,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -7550,7 +7568,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -9378,7 +9395,6 @@ "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "dev": true, "funding": [ { "type": "individual", @@ -14809,7 +14825,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -14818,7 +14833,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -18681,6 +18695,15 @@ "node": ">=10.0.0" } }, + "node_modules/wait-on/node_modules/axios": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.7" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -23590,8 +23613,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "at-least-node": { "version": "1.0.0", @@ -23631,12 +23653,30 @@ "dev": true }, "axios": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", - "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", - "dev": true, + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", + "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", "requires": { - "follow-redirects": "^1.14.7" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + } } }, "babel-jest": { @@ -24321,7 +24361,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -24971,8 +25010,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "depd": { "version": "2.0.0", @@ -26350,8 +26388,7 @@ "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "dev": true + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" }, "for-each": { "version": "0.3.3", @@ -30435,14 +30472,12 @@ "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "requires": { "mime-db": "1.52.0" } @@ -33338,6 +33373,17 @@ "lodash": "^4.17.21", "minimist": "^1.2.5", "rxjs": "^7.5.4" + }, + "dependencies": { + "axios": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.7" + } + } } }, "walker": { diff --git a/package.json b/package.json index 790d284..6b3d26e 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.5.0", "classnames": "^2.3.2", "iframe-phone": "^1.3.1", "react": "^17.0.2", diff --git a/src/components/app.tsx b/src/components/app.tsx index 20a2974..6ad8457 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -1,14 +1,15 @@ import React, {useState} from "react"; import { Dropdown } from "./dropdown"; -import css from "./app.scss"; import classnames from "classnames"; import { Information } from "./information"; import { defaultSelectedOptions } from "./constants"; +import { IStateOptions } from "./types"; +import css from "./app.scss"; function App() { const [showInfo, setShowInfo] = useState(false); - const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); + const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); const handleSetSelectedOptions = (option: string, value: string | string[]) => { const newSelectedOptions = {...selectedOptions, [option]: value}; @@ -40,21 +41,18 @@ function App() { diff --git a/src/components/attribute-options.tsx b/src/components/attribute-options.tsx new file mode 100644 index 0000000..3b5f603 --- /dev/null +++ b/src/components/attribute-options.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { Options } from "./options"; +import { attributeOptions } from "./constants"; +import classnames from "classnames"; +import { IStateOptions } from "./types"; + +import css from "./options.scss"; + +interface IProps { + handleSetSelectedOptions: (option: string, value: string|string[]) => void; + selectedOptions: IStateOptions; +} + +export const AttributeOptions: React.FC = (props) => { + const {handleSetSelectedOptions, selectedOptions} = props; + + const commonProps = { + inputType: "checkbox" as "checkbox"|"radio", + handleSetSelectedOptions, + selectedOptions + }; + + const getClasses = (key: string) => { + if (key === "cropUnits" || key === "crops") { + return classnames(css.checkOptions, css.narrow); + } else { + return classnames(css.checkOptions, css.vertical) + } + } + + return ( + <> + { + attributeOptions.map((attr) => { + return ( + <> + {attr.label &&
{attr.label}
} + {attr.instructions &&
{attr.instructions}
} +
+ +
+ + ) + }) + } + + ); +}; diff --git a/src/components/constants.ts b/src/components/constants.ts index 22822be..f0ef1fe 100644 --- a/src/components/constants.ts +++ b/src/components/constants.ts @@ -1,3 +1,5 @@ +import { IAttrOptions, IStateOptions } from "./types"; + export const placeOptions = { label: "Size of area for data", options : ["State", "County"] @@ -59,7 +61,46 @@ export const stateOptions = { ] }; -export const defaultSelectedOptions = { - place: null, - states: [] +const farmerOptions: IAttrOptions = { + key: "farmerDemographics", + label: "Farmer Demographics", + options: ["Total Farmers", "Age", "Race", "Gender"], + instructions: null +}; +const farmOptions: IAttrOptions = { + key: "farmDemographics", + label: "Farm Demographics", + options: ["Total Farms", "Organization Type", "Economic Class", "Acres Operated", "Organic"], + instructions: null +}; +const economicOptions: IAttrOptions = { + key: "economicsAndWages", + label: "Economics & Wages", + options: ["Labor Status", "Wages", "Time Worked"], + instructions: null +}; +const cropUnitOptions: IAttrOptions = { + key: "cropUnits", + label: "Production", + options: ["Area Harvested", "Yield"], + instructions: "(Choose units)" +}; +const cropOptions: IAttrOptions = { + key: "crops", + label: null, + options: ["Corn", "Cotton", "Grapes", "Grasses", "Oats", "Soybeans", "Wheat"], + instructions: "(Choose crops)" +}; + +export const attributeOptions = [farmerOptions, farmOptions, economicOptions, cropUnitOptions, cropOptions]; + +export const defaultSelectedOptions: IStateOptions = { + "geographicLevel": "", + "states": [], + "farmerDemographics": [], + "farmDemographics": [], + "economicsAndWages": [], + "cropUnits": "", + "crops": [], + "years": [] }; diff --git a/src/components/dropdown.tsx b/src/components/dropdown.tsx index 5f4650b..e485d0c 100644 --- a/src/components/dropdown.tsx +++ b/src/components/dropdown.tsx @@ -1,13 +1,14 @@ import React, { useState } from "react"; import classnames from "classnames"; -import css from "./dropdown.scss"; import { PlaceOptions } from "./place-options"; import { defaultSelectedOptions } from "./constants"; +import { AttributeOptions } from "./attribute-options"; + +import css from "./dropdown.scss"; interface IProps { sectionName: string sectionAltText: string - sectionDescription: string handleSetSelectedOptions: (option: string, value: string|string[]) => void selectedOptions: typeof defaultSelectedOptions; } @@ -22,22 +23,28 @@ export const Dropdown: React.FC = (props) => { const renderSummary = () => { if (sectionName === "Place") { - const place = selectedOptions.place || ""; + const place = selectedOptions.geographicLevel || ""; const states = selectedOptions.states.join(`, `); return ( `${place}: ${states}` ); + } else if (sectionName === "Attributes") { + return ( + "" + ) } } + const commonProps = {handleSetSelectedOptions, selectedOptions}; + const renderOptions = () => { if (sectionName === "Place") { return ( - + + ); + } else if (sectionName === "Attributes") { + return ( + ); } }; diff --git a/src/components/options.scss b/src/components/options.scss index 0d0d78e..70b558b 100644 --- a/src/components/options.scss +++ b/src/components/options.scss @@ -51,4 +51,29 @@ .checkOptions { flex-wrap: wrap; display: flex; + &.vertical { + flex-direction: column; + .option { + flex-basis: auto; + padding: 0px; + } + } + &.narrow { + margin: 0px; + .option { + flex-basis: auto; + padding: 3px; + margin-top: 0px; + } + } +} + +.category { + font-weight: 600; +} + +.instructions { + padding-left: 6px; + margin-top: 6px; + } \ No newline at end of file diff --git a/src/components/options.tsx b/src/components/options.tsx new file mode 100644 index 0000000..76b4f19 --- /dev/null +++ b/src/components/options.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { IStateOptions, OptionKey } from "./types"; + +import css from "./options.scss"; + +export interface IOptions { + options: string[], + selectedOptions: IStateOptions, + inputType: "radio" | "checkbox", + handleSetSelectedOptions: (option: string, value: string|string[]) => void, + optionKey: OptionKey +} + + +export const Options: React.FC = (props) => { + const {options, selectedOptions, inputType, handleSetSelectedOptions, optionKey} = props; + + const isOptionSelected = (option: string) => { + if (Array.isArray(selectedOptions[optionKey])) { + return selectedOptions[optionKey].indexOf(option) > -1; + } else { + return selectedOptions[optionKey] === option; + } + }; + + const handleSelectState = (e: React.ChangeEvent) => { + console.log({optionKey}, "e.target.value", e.target.value); + if (Array.isArray(selectedOptions[optionKey])) { + let newArray = [...selectedOptions[optionKey]]; + if (e.currentTarget.checked) { + newArray.push(e.target.value); + newArray.sort(); + + } else { + if (isOptionSelected(e.target.value)) { + newArray = newArray.filter((o) => o !== e.target.value); + } + } + handleSetSelectedOptions(optionKey, newArray); + } else if (optionKey === "geographicLevel" || optionKey === "cropUnits") { + handleSetSelectedOptions(optionKey, e.target.value); + } + }; + + return ( + <> + {options.map((o) => { + return ( +
+ handleSelectState(e)} + /> + +
+ ); + })} + + ); +}; diff --git a/src/components/place-options.tsx b/src/components/place-options.tsx index 0774649..ed3269b 100644 --- a/src/components/place-options.tsx +++ b/src/components/place-options.tsx @@ -1,91 +1,39 @@ import React from "react"; import { placeOptions, stateOptions } from "./constants"; +import { IStateOptions } from "./types" +import { Options } from "./options"; import css from "./options.scss"; interface IProps { handleSetSelectedOptions: (option: string, value: string|string[]) => void; - selectedPlace: string|null; - selectedStates: string[]; + selectedOptions: IStateOptions; } export const PlaceOptions: React.FC = (props) => { - const {handleSetSelectedOptions, selectedPlace, selectedStates} = props; - - const isStateSelected = (state: string) => { - return selectedStates.indexOf(state) > - 1; - }; - - const handleSelectPlace = (e: React.ChangeEvent) => { - handleSetSelectedOptions("place", e.target.value); - }; - - const handleSelectState = (e: React.ChangeEvent) => { - let newSelectedStates = [...selectedStates]; - if (e.currentTarget.checked) { - newSelectedStates.push(e.target.value); - newSelectedStates.sort(); - - } else { - if (isStateSelected(e.target.value)) { - newSelectedStates = newSelectedStates.filter((s) => s !== e.target.value); - } - } - handleSetSelectedOptions("states", newSelectedStates); - }; - - const createPlaceOptions = (options: string[]) => { - return ( - <> - {options.map((o) => { - return ( -
- - -
- ); - })} - - ); - }; - - const createStateOptions = (options: string[]) => { - return ( - <> - {options.map((o) => { - return ( -
- - -
- ); - })} - - ); - }; + const {handleSetSelectedOptions, selectedOptions} = props; + const commonProps = {selectedOptions, handleSetSelectedOptions} return ( <>
{placeOptions.label}:
-
{createPlaceOptions(placeOptions.options)}
+
+ +
{stateOptions.label}:
-
{createStateOptions(stateOptions.options)}
+
+ +
); }; diff --git a/src/components/types.ts b/src/components/types.ts new file mode 100644 index 0000000..cfa1fe2 --- /dev/null +++ b/src/components/types.ts @@ -0,0 +1,19 @@ +export interface IStateOptions { + "geographicLevel": string, + "states": string[] + "farmerDemographics": string[], + "farmDemographics": string[], + "economicsAndWages": string[], + "cropUnits": string, + "crops": string[] + "years": string[] +} + +export type OptionKey = keyof IStateOptions; + +export interface IAttrOptions { + key: keyof IStateOptions, + label: string|null, + options: string[] + instructions: string|null +} \ No newline at end of file From 2007ab975b7b693a29cdf0106e77d10013145a39 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 13 Sep 2023 12:56:11 -0400 Subject: [PATCH 06/21] Remove console.log --- src/components/options.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/options.tsx b/src/components/options.tsx index 76b4f19..85dc513 100644 --- a/src/components/options.tsx +++ b/src/components/options.tsx @@ -24,7 +24,6 @@ export const Options: React.FC = (props) => { }; const handleSelectState = (e: React.ChangeEvent) => { - console.log({optionKey}, "e.target.value", e.target.value); if (Array.isArray(selectedOptions[optionKey])) { let newArray = [...selectedOptions[optionKey]]; if (e.currentTarget.checked) { From 7d8dee093408ddacb889e8b961bd3c7cf8524713 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 13 Sep 2023 12:58:42 -0400 Subject: [PATCH 07/21] Fix linting errors. --- src/components/attribute-options.tsx | 6 +++--- src/components/dropdown.tsx | 4 ++-- src/components/place-options.tsx | 5 +++-- src/components/types.ts | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/attribute-options.tsx b/src/components/attribute-options.tsx index 3b5f603..4dbdc65 100644 --- a/src/components/attribute-options.tsx +++ b/src/components/attribute-options.tsx @@ -24,9 +24,9 @@ export const AttributeOptions: React.FC = (props) => { if (key === "cropUnits" || key === "crops") { return classnames(css.checkOptions, css.narrow); } else { - return classnames(css.checkOptions, css.vertical) + return classnames(css.checkOptions, css.vertical); } - } + }; return ( <> @@ -44,7 +44,7 @@ export const AttributeOptions: React.FC = (props) => { /> - ) + ); }) } diff --git a/src/components/dropdown.tsx b/src/components/dropdown.tsx index e485d0c..29a237a 100644 --- a/src/components/dropdown.tsx +++ b/src/components/dropdown.tsx @@ -31,9 +31,9 @@ export const Dropdown: React.FC = (props) => { } else if (sectionName === "Attributes") { return ( "" - ) + ); } - } + }; const commonProps = {handleSetSelectedOptions, selectedOptions}; diff --git a/src/components/place-options.tsx b/src/components/place-options.tsx index ed3269b..58a0ad4 100644 --- a/src/components/place-options.tsx +++ b/src/components/place-options.tsx @@ -1,6 +1,6 @@ import React from "react"; import { placeOptions, stateOptions } from "./constants"; -import { IStateOptions } from "./types" +import { IStateOptions } from "./types"; import { Options } from "./options"; import css from "./options.scss"; @@ -13,7 +13,8 @@ interface IProps { export const PlaceOptions: React.FC = (props) => { const {handleSetSelectedOptions, selectedOptions} = props; - const commonProps = {selectedOptions, handleSetSelectedOptions} + const commonProps = {selectedOptions, handleSetSelectedOptions}; + return ( <>
{placeOptions.label}:
diff --git a/src/components/types.ts b/src/components/types.ts index cfa1fe2..d9f23a9 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -16,4 +16,4 @@ export interface IAttrOptions { label: string|null, options: string[] instructions: string|null -} \ No newline at end of file +} From cf9e36cc9998c7ec66f218d7a853b84636e675e4 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 13 Sep 2023 17:59:03 -0400 Subject: [PATCH 08/21] User can select a year from dropdown menu. --- package-lock.json | 11 ++ package.json | 1 + src/components/app.tsx | 38 +++--- src/components/attribute-options.tsx | 7 +- src/components/constants.ts | 36 +++++- src/components/dropdown.tsx | 36 ++---- src/components/options.tsx | 4 +- src/components/place-options.tsx | 40 +++--- src/components/summary.tsx | 53 ++++++++ src/components/types.ts | 2 +- src/components/years-options.tsx | 26 ++++ src/scripts/api.ts | 23 ++++ src/scripts/query-headers.ts | 182 +++++++++++++++++++++++++++ 13 files changed, 379 insertions(+), 80 deletions(-) create mode 100644 src/components/summary.tsx create mode 100644 src/components/years-options.tsx create mode 100644 src/scripts/api.ts create mode 100644 src/scripts/query-headers.ts diff --git a/package-lock.json b/package-lock.json index 4eda71c..801b9e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.5.0", "classnames": "^2.3.2", + "fetch-jsonp": "^1.3.0", "iframe-phone": "^1.3.1", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -9247,6 +9248,11 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-jsonp": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fetch-jsonp/-/fetch-jsonp-1.3.0.tgz", + "integrity": "sha512-hxCYGvmANEmpkHpeWY8Kawfa5Z1t2csTpIClIDG/0S92eALWHRU1RnGaj86Tf5Cc0QF+afSa4SQ4pFB2rFM5QA==" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -26279,6 +26285,11 @@ "pend": "~1.2.0" } }, + "fetch-jsonp": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fetch-jsonp/-/fetch-jsonp-1.3.0.tgz", + "integrity": "sha512-hxCYGvmANEmpkHpeWY8Kawfa5Z1t2csTpIClIDG/0S92eALWHRU1RnGaj86Tf5Cc0QF+afSa4SQ4pFB2rFM5QA==" + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", diff --git a/package.json b/package.json index 6b3d26e..b801767 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.5.0", "classnames": "^2.3.2", + "fetch-jsonp": "^1.3.0", "iframe-phone": "^1.3.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/src/components/app.tsx b/src/components/app.tsx index 6ad8457..51613dc 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -1,16 +1,21 @@ -import React, {useState} from "react"; +import React, {useEffect, useState} from "react"; import { Dropdown } from "./dropdown"; import classnames from "classnames"; import { Information } from "./information"; -import { defaultSelectedOptions } from "./constants"; +import { categories, defaultSelectedOptions } from "./constants"; import { IStateOptions } from "./types"; import css from "./app.scss"; +import { runTestQuery } from "../scripts/api"; function App() { const [showInfo, setShowInfo] = useState(false); const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); + useEffect(() => { + runTestQuery(); + }, []) + const handleSetSelectedOptions = (option: string, value: string | string[]) => { const newSelectedOptions = {...selectedOptions, [option]: value}; setSelectedOptions(newSelectedOptions); @@ -38,24 +43,17 @@ function App() {
- - - + {categories.map((cat) => { + return ( + + ) + })}
diff --git a/src/components/attribute-options.tsx b/src/components/attribute-options.tsx index 4dbdc65..6e373a5 100644 --- a/src/components/attribute-options.tsx +++ b/src/components/attribute-options.tsx @@ -34,10 +34,11 @@ export const AttributeOptions: React.FC = (props) => { attributeOptions.map((attr) => { return ( <> - {attr.label &&
{attr.label}
} - {attr.instructions &&
{attr.instructions}
} -
+ {attr.label &&
{attr.label}
} + {attr.instructions &&
{attr.instructions}
} +
= 1910; year--) { + yearsArray.push(`${year}`); +} + +export const yearsOptions: IAttrOptions = { + key: "years", + label: "Years", + options: yearsArray, + instructions: null +} + +export const categories = [ + {header: "Place", options: placeOptions, altText: ""}, + {header: "Attributes", options: attributeOptions, altText: ""}, + {header: "Years", options: yearsOptions, altText: ""} +] + export const defaultSelectedOptions: IStateOptions = { "geographicLevel": "", "states": [], @@ -103,4 +127,4 @@ export const defaultSelectedOptions: IStateOptions = { "cropUnits": "", "crops": [], "years": [] -}; +}; \ No newline at end of file diff --git a/src/components/dropdown.tsx b/src/components/dropdown.tsx index 29a237a..2ab0357 100644 --- a/src/components/dropdown.tsx +++ b/src/components/dropdown.tsx @@ -5,55 +5,37 @@ import { defaultSelectedOptions } from "./constants"; import { AttributeOptions } from "./attribute-options"; import css from "./dropdown.scss"; +import { YearsOptions } from "./years-options"; +import { Summary } from "./summary"; interface IProps { - sectionName: string + category: string sectionAltText: string handleSetSelectedOptions: (option: string, value: string|string[]) => void selectedOptions: typeof defaultSelectedOptions; } export const Dropdown: React.FC = (props) => { - const {sectionName, sectionAltText, handleSetSelectedOptions, selectedOptions} = props; + const {category, sectionAltText, handleSetSelectedOptions, selectedOptions} = props; const [showItems, setShowItems] = useState(false); const handleClick = () => { setShowItems(!showItems); }; - const renderSummary = () => { - if (sectionName === "Place") { - const place = selectedOptions.geographicLevel || ""; - const states = selectedOptions.states.join(`, `); - return ( - `${place}: ${states}` - ); - } else if (sectionName === "Attributes") { - return ( - "" - ); - } - }; - const commonProps = {handleSetSelectedOptions, selectedOptions}; const renderOptions = () => { - if (sectionName === "Place") { - return ( - - ); - } else if (sectionName === "Attributes") { - return ( - - ); - } + return category === "Place" ? : + category === "Attributes" ? : + }; return (
- {sectionName} -
{renderSummary()}
+ {category} +
diff --git a/src/components/options.tsx b/src/components/options.tsx index 85dc513..023c494 100644 --- a/src/components/options.tsx +++ b/src/components/options.tsx @@ -29,7 +29,9 @@ export const Options: React.FC = (props) => { if (e.currentTarget.checked) { newArray.push(e.target.value); newArray.sort(); - + if (optionKey === "years") { + newArray.reverse(); + } } else { if (isOptionSelected(e.target.value)) { newArray = newArray.filter((o) => o !== e.target.value); diff --git a/src/components/place-options.tsx b/src/components/place-options.tsx index 58a0ad4..f165fbd 100644 --- a/src/components/place-options.tsx +++ b/src/components/place-options.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { placeOptions, stateOptions } from "./constants"; +import { placeOptions } from "./constants"; import { IStateOptions } from "./types"; import { Options } from "./options"; @@ -12,29 +12,25 @@ interface IProps { export const PlaceOptions: React.FC = (props) => { const {handleSetSelectedOptions, selectedOptions} = props; - - const commonProps = {selectedOptions, handleSetSelectedOptions}; - return ( <> -
{placeOptions.label}:
-
- -
-
{stateOptions.label}:
-
- -
+ {placeOptions.map((placeOpt) => { + return ( + <> +
{placeOpt.instructions}:
+
+ +
+ + ); + })} ); }; diff --git a/src/components/summary.tsx b/src/components/summary.tsx new file mode 100644 index 0000000..f02a931 --- /dev/null +++ b/src/components/summary.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { IStateOptions } from "./types"; +import { attributeOptions } from "./constants"; + +interface IProps { + category: string; + selectedOptions: IStateOptions; +} + +export const Summary: React.FC = ({category, selectedOptions}) => { + + const getSummaryText = () => { + if (category === "Place") { + const place = selectedOptions.geographicLevel || ""; + const states = selectedOptions.states.join(`, `); + const colon = place && states ? ": " : ""; + return ( + `${place}${colon}${states}` + ); + } else if (category === "Attributes") { + const resultString = attributeOptions.filter((attr) => { + const value = selectedOptions[attr.key]; + const valueIsArrayWithLength = Array.isArray(value) && value.length > 0; + const valueIsDefined = typeof value === "string" && value; + return valueIsArrayWithLength || valueIsDefined; + }).map((attr) => { + const value = selectedOptions[attr.key]; + const valueIsArrayWithLength = Array.isArray(value) && value.length > 0; + const valueIsDefined = typeof value === "string" && value; + const label = attr.label && (valueIsArrayWithLength || valueIsDefined) ? `${attr.label}: ` : ""; + + if (Array.isArray(value) && value.length > 0) { + return `${label}${value.join(", ")}`; + } else if (value) { + return `${attr.label}: ${value}`; + } else { + return null; + } + }) + .filter((item) => item !== null) + const finalString = resultString.join(", "); + return finalString; + } else if (category === "Years") { + return selectedOptions.years.join(", ") + } + } + + return ( +
+ {getSummaryText()} +
+ ) +} \ No newline at end of file diff --git a/src/components/types.ts b/src/components/types.ts index d9f23a9..6eaa4a9 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -14,6 +14,6 @@ export type OptionKey = keyof IStateOptions; export interface IAttrOptions { key: keyof IStateOptions, label: string|null, - options: string[] + options: string[], instructions: string|null } diff --git a/src/components/years-options.tsx b/src/components/years-options.tsx new file mode 100644 index 0000000..d9b605a --- /dev/null +++ b/src/components/years-options.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { yearsOptions } from "./constants"; +import { IStateOptions } from "./types"; +import { Options } from "./options"; + +import css from "./options.scss"; + +interface IProps { + handleSetSelectedOptions: (option: string, value: string|string[]) => void; + selectedOptions: IStateOptions; +} + +export const YearsOptions: React.FC = (props) => { + const {handleSetSelectedOptions, selectedOptions} = props; + return ( +
+ +
+ ); +}; diff --git a/src/scripts/api.ts b/src/scripts/api.ts new file mode 100644 index 0000000..5cfd9ef --- /dev/null +++ b/src/scripts/api.ts @@ -0,0 +1,23 @@ +import fetchJsonp from "fetch-jsonp"; +import { queryData } from "./query-headers"; + +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) => { + const queryParams = queryData.find((d) => d.plugInAttribute === attribute); + const {sector, group, commodity, category, domains, dataItem} = queryParams!; + const req = `${baseURL}§_desc=${sector}&group_desc=${group}&commodity_desc=${commodity}&statisticcat_desc=${category}&domain_desc=${domains}&short_desc=${dataItem}&agg_level_desc=${geoLevel}&state_name=${location}&year=${year}`; + return req; +}; + +export const runTestQuery = () => { + const request = createRequest("Total Farmers", "STATE", "CALIFORNIA", "2017"); + fetchJsonp(request) + .then(function(response) { + return response.json(); + }).then(function(json) { + console.log("parsed json", json); + }).catch(function(ex) { + console.log("parsing failed", ex); + }) +}; diff --git a/src/scripts/query-headers.ts b/src/scripts/query-headers.ts new file mode 100644 index 0000000..e487f4a --- /dev/null +++ b/src/scripts/query-headers.ts @@ -0,0 +1,182 @@ + +interface IQueryHeaders { + plugInAttribute: string, + numberOfAttributeColumnsInCodap: number|string, + sector: string, + group: string, + commodity: string, + category: string + dataItem: string|string[], + domains: string, + geographicLevels?: string, + years?: string +} + +const sharedDemographicHeaders = { +sector: "Demographics", +group: "Producers", +commodity: "Producers", +category: "Producers", +domains: "Total", +}; + +const sharedEconomicHeaders = { +sector: "Economics", +group: "Farms & Land & Assets", +commodity: "Farm Operations", +category: "Operations" +}; + +const sharedLaborHeaders = { +sector: "Economics", +group: "Expenses", +commodity: "Labor", +}; + +export const queryData: Array = [ +{ + plugInAttribute: "Total Farmers", + numberOfAttributeColumnsInCodap: 1, + ...sharedDemographicHeaders, + dataItem: "PRODUCERS, (ALL) - NUMBER OF PRODUCERS", +}, +{ + plugInAttribute: "Age", + numberOfAttributeColumnsInCodap: 7, + ...sharedDemographicHeaders, + dataItem: [ + "PRODUCERS, AGE LT 25 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE 25 TO 34 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE 35 TO 44 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE 45 TO 54 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE 55 TO 64 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE 65 TO 74 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE GE 75 - NUMBER OF PRODUCERS" + ], + +}, +{ + plugInAttribute: "Gender", + numberOfAttributeColumnsInCodap: 2, + ...sharedDemographicHeaders, + dataItem: [ + "PRODUCERS, (ALL), FEMALE - NUMBER OF PRODUCERS", + "PRODUCERS, (ALL), MALE - NUMBER OF PRODUCERS" + ], +}, +{ + plugInAttribute: "Race", + numberOfAttributeColumnsInCodap: 7, + ...sharedDemographicHeaders, + dataItem: [ + "PRODUCERS, AMERICAN INDIAN OR ALASKAN NATIVE - NUMBER OF PRODUCERS", + "PRODUCERS, ASIAN - NUMBER OF PRODUCERS", + "PRODUCERS, BLACK OR AFRICAN AMERICAN - NUMBER OF PRODUCERS", + "PRODUCERS, HISPANIC - NUMBER OF PRODUCERS", + "PRODUCERS, MULTI-RACE - NUMBER OF PRODUCERS", + "PRODUCERS, NATIVE HAWAIIAN OR OTHER PACIFIC ISLANDERS - NUMBER OF PRODUCERS", + "PRODUCERS, WHITE - NUMBER OF PRODUCERS" + ] +}, +{ + plugInAttribute: "Total Farms", + numberOfAttributeColumnsInCodap: 1, + ...sharedEconomicHeaders, + dataItem: "Farm Operations - Number of Operations", + domains: "Total", + geographicLevels: "State, County", + years: "1910 - 2022" +}, +{ + plugInAttribute: "Organization Type", + numberOfAttributeColumnsInCodap: 5, + sector: "Demographics", + group: "Farms & Land & Assets", + commodity: "Farm Operations", + category: "Operations", + dataItem: [ + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, CORPORATION (EXCL FAMILY HELD) - NUMBER OF OPERATIONS", + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, CORPORATION, FAMILY HELD - NUMBER OF OPERATIONS", + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, FAMILY & INDIVIDUAL - NUMBER OF OPERATIONS", + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, INSTITUTIONAL & RESEARCH & RESERVATION & OTHER - NUMBER OF OPERATIONS", + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, PARTNERSHIP - NUMBER OF OPERATIONS" + ], + domains: "Total", + geographicLevels: "State, County", + years: "County: 1997, 2002, 2007, 2012, 2017\nState: 1910 - 2022" +}, +{ + plugInAttribute: "Economic Class", + numberOfAttributeColumnsInCodap: "3 - 6", + ...sharedEconomicHeaders, + dataItem: "Farm Operations - Number of Operations", + domains: "Economic Class", + geographicLevels: "State ", + years: "1987 - 2022" +}, +{ + plugInAttribute: "Acres Operated", + numberOfAttributeColumnsInCodap: 14, + sector: "Economics", + group: "Farms & Land & Assets", + commodity: "Farm Operations", + category: "Area Operated", + dataItem: "Farm Operations - Acres Operated", + domains: "Area Operated", + geographicLevels: "State, County", + years: "1997, 2002, 2007, 2012, 2017" +}, +{ + plugInAttribute: "Organic", + numberOfAttributeColumnsInCodap: 1, + ...sharedEconomicHeaders, + dataItem: "Farm Operations, Organic - Number of Operations", + domains: "Organic Status" +}, +{ + plugInAttribute: "Labor Status", + numberOfAttributeColumnsInCodap: 3, + ...sharedLaborHeaders, + category: "Workers", + dataItem: [ + "LABOR, MIGRANT - NUMBER OF WORKERS", + "LABOR, UNPAID - NUMBER OF WORKERS", + "LABOR, HIRED - NUMBER OF WORKERS" + ], + domains: "Total", + geographicLevels: "State, County", + years: "2012, 2017" +}, +{ + plugInAttribute: "Wages", + numberOfAttributeColumnsInCodap: 1, + ...sharedLaborHeaders, + category: "Wage Rate", + dataItem: "LABOR, HIRED - WAGE RATE, MEASURED IN $/HOUR", + domains: "Total", + geographicLevels: "Region: Multi-state", + years: "1989 - 2022", +}, +{ + plugInAttribute: "Time Worked", + numberOfAttributeColumnsInCodap: 1, + ...sharedLaborHeaders, + category: "Wage Rate", + dataItem: "Labor, Hired - Time Worked, Measured in Hours/Week", + domains: "Total", + geographicLevels: "Region: Multi-state", + years: "1989 - 2022", +}, +{ + plugInAttribute: "Corn", + numberOfAttributeColumnsInCodap: 1, + sector: "Crops", + group: "Field Crops", + commodity: "Corn", + category: "Yield", + dataItem: "Corn, Grain - Yield, measured in BU / acre", + domains: "Total", + geographicLevels: "State, County", + years: "County: 1910 - 2022\nState:1866 - 2022 (only add 1910 - 2022)", +} +]; From 4f69f8e803c880fde6e75f7a1ce0a4e17c3d33bb Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 14 Sep 2023 15:17:16 -0400 Subject: [PATCH 09/21] Check if years are valid. --- src/components/years-options.tsx | 37 +++++- src/scripts/query-headers.ts | 220 ++++++++++++++++++++++++++++--- 2 files changed, 240 insertions(+), 17 deletions(-) diff --git a/src/components/years-options.tsx b/src/components/years-options.tsx index d9b605a..524b1f7 100644 --- a/src/components/years-options.tsx +++ b/src/components/years-options.tsx @@ -1,9 +1,10 @@ import React from "react"; -import { yearsOptions } from "./constants"; +import { attributeOptions, yearsOptions } from "./constants"; import { IStateOptions } from "./types"; import { Options } from "./options"; import css from "./options.scss"; +import { queryData } from "../scripts/query-headers"; interface IProps { handleSetSelectedOptions: (option: string, value: string|string[]) => void; @@ -12,6 +13,38 @@ interface IProps { export const YearsOptions: React.FC = (props) => { const {handleSetSelectedOptions, selectedOptions} = props; + + const handleSelectYear = (yearKey: string, years: string|string[]) => { + handleSetSelectedOptions(yearKey, years); + + // check if any attributeOption keys have values in selectedOptions + const attrKeys = attributeOptions.map((attr) => attr.key); + const selectedAttrKeys = attrKeys.filter((key) => selectedOptions[key].length > 0); + const areAnyAttrsSelected = selectedAttrKeys.length > 0; + // if any attributes are selected, check that selected year data is available for that selected attribute + if (areAnyAttrsSelected) { + const selectedYears = Array.isArray(years) ? years : [years]; + const selectedAttrs = selectedAttrKeys.map((key) => selectedOptions[key]); + selectedAttrs.forEach((attr) => { + if (Array.isArray(attr)) { + attr.forEach((subAttr) => { + const subAttrData = queryData.find((d) => d.plugInAttribute === subAttr); + const yearKeyToUse = selectedOptions.geographicLevel === "county" ? "county" : "state"; + const availableYears = subAttrData?.years[yearKeyToUse]; + if (availableYears) { + // check -- are selectedYears included in queryParams?.years[yearKeyTouse] ? + const areYearsValid = selectedYears.map((y) => availableYears.indexOf(y) > -1).indexOf(false) < 0; + console.log({areYearsValid}); + console.log({availableYears}); + console.log({selectedYears}); + // do something if years are not valid + } + }) + } + }) + } + } + return (
= (props) => { optionKey={yearsOptions.key} inputType={"checkbox"} selectedOptions={selectedOptions} - handleSetSelectedOptions={handleSetSelectedOptions} + handleSetSelectedOptions={handleSelectYear} />
); diff --git a/src/scripts/query-headers.ts b/src/scripts/query-headers.ts index e487f4a..311211f 100644 --- a/src/scripts/query-headers.ts +++ b/src/scripts/query-headers.ts @@ -1,3 +1,9 @@ +const areaHarvested = "Area Harvested"; +const yieldInBU = "Yield"; +interface ICropDataItem { + [areaHarvested]: string, + [yieldInBU]: string +} interface IQueryHeaders { plugInAttribute: string, @@ -5,11 +11,14 @@ interface IQueryHeaders { sector: string, group: string, commodity: string, - category: string - dataItem: string|string[], + category: string|ICropDataItem, + dataItem: string|string[]|ICropDataItem, domains: string, geographicLevels?: string, - years?: string + years: { + county: string[] + state: string[] + } } const sharedDemographicHeaders = { @@ -33,12 +42,21 @@ group: "Expenses", commodity: "Labor", }; +const allYears = []; +for (let year = 2022; year >= 1910; year--) { + allYears.push(`${year}`); +} + export const queryData: Array = [ { plugInAttribute: "Total Farmers", numberOfAttributeColumnsInCodap: 1, ...sharedDemographicHeaders, dataItem: "PRODUCERS, (ALL) - NUMBER OF PRODUCERS", + years: { + county: ["2017"], + state: ["2017"] + } }, { plugInAttribute: "Age", @@ -53,6 +71,10 @@ export const queryData: Array = [ "PRODUCERS, AGE 65 TO 74 - NUMBER OF PRODUCERS", "PRODUCERS, AGE GE 75 - NUMBER OF PRODUCERS" ], + years: { + county: ["2017"], + state: ["2017"] + } }, { @@ -63,6 +85,10 @@ export const queryData: Array = [ "PRODUCERS, (ALL), FEMALE - NUMBER OF PRODUCERS", "PRODUCERS, (ALL), MALE - NUMBER OF PRODUCERS" ], + years: { + county: ["2017"], + state: ["2017"] + } }, { plugInAttribute: "Race", @@ -76,7 +102,11 @@ export const queryData: Array = [ "PRODUCERS, MULTI-RACE - NUMBER OF PRODUCERS", "PRODUCERS, NATIVE HAWAIIAN OR OTHER PACIFIC ISLANDERS - NUMBER OF PRODUCERS", "PRODUCERS, WHITE - NUMBER OF PRODUCERS" - ] + ], + years: { + county: ["2017"], + state: ["2017"] + } }, { plugInAttribute: "Total Farms", @@ -85,7 +115,10 @@ export const queryData: Array = [ dataItem: "Farm Operations - Number of Operations", domains: "Total", geographicLevels: "State, County", - years: "1910 - 2022" + years: { + county: allYears, + state: allYears + } }, { plugInAttribute: "Organization Type", @@ -103,7 +136,10 @@ export const queryData: Array = [ ], domains: "Total", geographicLevels: "State, County", - years: "County: 1997, 2002, 2007, 2012, 2017\nState: 1910 - 2022" + years: { + county: ["1997", "2002", "2007", "2012", "2017"], + state: allYears + } }, { plugInAttribute: "Economic Class", @@ -112,7 +148,10 @@ export const queryData: Array = [ dataItem: "Farm Operations - Number of Operations", domains: "Economic Class", geographicLevels: "State ", - years: "1987 - 2022" + years: { + county: allYears.filter(y => Number(y) >= 1987), + state: allYears.filter(y => Number(y) >= 1987) + } }, { plugInAttribute: "Acres Operated", @@ -124,14 +163,21 @@ export const queryData: Array = [ dataItem: "Farm Operations - Acres Operated", domains: "Area Operated", geographicLevels: "State, County", - years: "1997, 2002, 2007, 2012, 2017" + years: { + county: ["1997", "2002", "2007", "2012", "2017"], + state: ["1997", "2002", "2007", "2012", "2017"] + } }, { plugInAttribute: "Organic", numberOfAttributeColumnsInCodap: 1, ...sharedEconomicHeaders, dataItem: "Farm Operations, Organic - Number of Operations", - domains: "Organic Status" + domains: "Organic Status", + years: { + county: ["2008", "2011", "2012", "2014", "2015", "2016", "2017", "2019", "2021"], + state: ["2008", "2011", "2012", "2014", "2015", "2016", "2017", "2019", "2021"] + } }, { plugInAttribute: "Labor Status", @@ -145,7 +191,10 @@ export const queryData: Array = [ ], domains: "Total", geographicLevels: "State, County", - years: "2012, 2017" + years: { + county: ["2012", "2017"], + state: ["2012", "2017"] + } }, { plugInAttribute: "Wages", @@ -155,7 +204,10 @@ export const queryData: Array = [ dataItem: "LABOR, HIRED - WAGE RATE, MEASURED IN $/HOUR", domains: "Total", geographicLevels: "Region: Multi-state", - years: "1989 - 2022", + years: { + county: allYears.filter(y => Number(y) >= 1989), + state: allYears.filter(y => Number(y) >= 1989) + } }, { plugInAttribute: "Time Worked", @@ -165,7 +217,10 @@ export const queryData: Array = [ dataItem: "Labor, Hired - Time Worked, Measured in Hours/Week", domains: "Total", geographicLevels: "Region: Multi-state", - years: "1989 - 2022", + years: { + county: allYears.filter(y => Number(y) >= 1989), + state: allYears.filter(y => Number(y) >= 1989) + } }, { plugInAttribute: "Corn", @@ -173,10 +228,145 @@ export const queryData: Array = [ sector: "Crops", group: "Field Crops", commodity: "Corn", - category: "Yield", - dataItem: "Corn, Grain - Yield, measured in BU / acre", + category: { + [areaHarvested]: "Area Harvested", + [yieldInBU]: "Yield" + }, + dataItem: { + [areaHarvested]: "Corn, Grain - Acres Harvested", + [yieldInBU]: "Corn, Grain - Yield, measured in BU / acre" + }, domains: "Total", geographicLevels: "State, County", - years: "County: 1910 - 2022\nState:1866 - 2022 (only add 1910 - 2022)", + years: { + county: allYears, + state: allYears + } +}, +{ + plugInAttribute: "Cotton", + numberOfAttributeColumnsInCodap: 1, + sector: "Crops", + group: "Field Crops", + commodity: "Cotton", + category: { + [areaHarvested]: "Area Harvested", + [yieldInBU]: "Yield" + }, + dataItem: { + [areaHarvested]: "Cotton - Acres Harvested", + [yieldInBU]: "Cotton - Yield, measured in LB / acre" + }, + domains: "Total", + geographicLevels: "State, County", + years: { + county: allYears, + state: allYears + } +}, +{ + plugInAttribute: "Grapes", + numberOfAttributeColumnsInCodap: 1, + sector: "Crops", + group: "Fruit & Tree Nuts", + commodity: "Grapes", + category: { + [areaHarvested]: "Area Harvested", + [yieldInBU]: "Yield" + }, + dataItem: { + [areaHarvested]: "Grapes, Organic - Acres Harvested", + [yieldInBU]: "Grapes - Yield, measured in tons / acre" + }, + domains: "Total", + geographicLevels: "State, County", + years: { + county: allYears, + state: allYears + } +}, +{ + plugInAttribute: "Grasses", + numberOfAttributeColumnsInCodap: 1, + sector: "Crops", + group: "Field Crops", + commodity: "Grasses", + category: { + [areaHarvested]: "Area Harvested", + [yieldInBU]: "Yield" + }, + dataItem: { + [areaHarvested]: "", + [yieldInBU]: "" + }, + domains: "Total", + geographicLevels: "State, County", + years: { + county: allYears, + state: allYears + } +}, +{ + plugInAttribute: "Oats", + numberOfAttributeColumnsInCodap: 1, + sector: "Crops", + group: "Field Crops", + commodity: "Oats", + category: { + [areaHarvested]: "Area Harvested", + [yieldInBU]: "Yield" + }, + dataItem: { + [areaHarvested]: "Oats - Acres Harvested", + [yieldInBU]: "Oats - Yield, measured in BU / acre" + }, + domains: "Total", + geographicLevels: "State, County", + years: { + county: allYears, + state: allYears + } +}, +{ + plugInAttribute: "Soybeans", + numberOfAttributeColumnsInCodap: 1, + sector: "Crops", + group: "Field Crops", + commodity: "Soybeans", + category: { + [areaHarvested]: "Area Harvested", + [yieldInBU]: "Yield" + }, + dataItem: { + [areaHarvested]: "Soybeans - Acres Harvested", + [yieldInBU]: "Soybeans - Yield, measured in BU / acre" + }, + domains: "Total", + geographicLevels: "State, County", + years: { + county: allYears, + state: allYears + } +}, +{ + plugInAttribute: "Wheat", + numberOfAttributeColumnsInCodap: 1, + sector: "Crops", + group: "Field Crops", + commodity: "Wheat", + category: { + [areaHarvested]: "Area Harvested", + [yieldInBU]: "Yield" + }, + dataItem: { + [areaHarvested]: "Wheat - Acres Harvested", + [yieldInBU]: "Wheat - Yield, measured in BU / acre" + }, + domains: "Total", + geographicLevels: "State, County", + years: { + county: allYears, + state: allYears + } } ]; From a24f45aa2b21cf59e54d7a46ce42381f344ffdcf Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 14 Sep 2023 21:49:46 -0400 Subject: [PATCH 10/21] Checkpoint --- src/components/app.tsx | 27 +++++++++-- src/components/constants.ts | 11 +++-- src/components/dropdown.tsx | 2 +- src/components/summary.tsx | 10 ++-- src/components/years-options.tsx | 6 +-- src/scripts/api.ts | 83 +++++++++++++++++++++++++++++--- src/scripts/connect.js | 82 ++++++++++++++++++++++++++++++- 7 files changed, 195 insertions(+), 26 deletions(-) diff --git a/src/components/app.tsx b/src/components/app.tsx index 51613dc..0cce018 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -4,17 +4,22 @@ import classnames from "classnames"; import { Information } from "./information"; import { categories, defaultSelectedOptions } from "./constants"; import { IStateOptions } from "./types"; +import { createQueryFromSelections } from "../scripts/api"; +import { connect } from "../scripts/connect"; + import css from "./app.scss"; -import { runTestQuery } from "../scripts/api"; function App() { const [showInfo, setShowInfo] = useState(false); const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); useEffect(() => { - runTestQuery(); - }, []) + const init = async () => { + await connect.initialize(); + }; + init(); + }, []); const handleSetSelectedOptions = (option: string, value: string | string[]) => { const newSelectedOptions = {...selectedOptions, [option]: value}; @@ -25,6 +30,18 @@ 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(); + }; + return (
{ showInfo && @@ -52,12 +69,12 @@ function App() { handleSetSelectedOptions={handleSetSelectedOptions} selectedOptions={selectedOptions} /> - ) + ); })}
- +
diff --git a/src/components/constants.ts b/src/components/constants.ts index 6cf2da9..ef141c0 100644 --- a/src/components/constants.ts +++ b/src/components/constants.ts @@ -12,6 +12,7 @@ export const stateOptions: IAttrOptions = { key: "states", instructions: "Choose states to include in your dataset from the list below", options: [ + "All States", "Alabama", "Alaska", "Arizona", @@ -110,21 +111,21 @@ export const yearsOptions: IAttrOptions = { label: "Years", options: yearsArray, instructions: null -} +}; export const categories = [ {header: "Place", options: placeOptions, altText: ""}, {header: "Attributes", options: attributeOptions, altText: ""}, {header: "Years", options: yearsOptions, altText: ""} -] +]; export const defaultSelectedOptions: IStateOptions = { - "geographicLevel": "", - "states": [], + "geographicLevel": "State", + "states": ["All States"], "farmerDemographics": [], "farmDemographics": [], "economicsAndWages": [], "cropUnits": "", "crops": [], "years": [] -}; \ No newline at end of file +}; diff --git a/src/components/dropdown.tsx b/src/components/dropdown.tsx index 2ab0357..27e56a3 100644 --- a/src/components/dropdown.tsx +++ b/src/components/dropdown.tsx @@ -28,7 +28,7 @@ export const Dropdown: React.FC = (props) => { const renderOptions = () => { return category === "Place" ? : category === "Attributes" ? : - + ; }; return ( diff --git a/src/components/summary.tsx b/src/components/summary.tsx index f02a931..43a32a7 100644 --- a/src/components/summary.tsx +++ b/src/components/summary.tsx @@ -37,17 +37,17 @@ export const Summary: React.FC = ({category, selectedOptions}) => { return null; } }) - .filter((item) => item !== null) + .filter((item) => item !== null); const finalString = resultString.join(", "); return finalString; } else if (category === "Years") { - return selectedOptions.years.join(", ") + return selectedOptions.years.join(", "); } - } + }; return (
{getSummaryText()}
- ) -} \ No newline at end of file + ); +}; diff --git a/src/components/years-options.tsx b/src/components/years-options.tsx index 524b1f7..924bd47 100644 --- a/src/components/years-options.tsx +++ b/src/components/years-options.tsx @@ -39,11 +39,11 @@ export const YearsOptions: React.FC = (props) => { console.log({selectedYears}); // do something if years are not valid } - }) + }); } - }) + }); } - } + }; return (
diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 5cfd9ef..e773ac6 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -1,23 +1,94 @@ import fetchJsonp from "fetch-jsonp"; import { queryData } from "./query-headers"; +import { IStateOptions } from "../components/types"; +import { connect } from "./connect"; 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) => { const queryParams = queryData.find((d) => d.plugInAttribute === attribute); const {sector, group, commodity, category, domains, dataItem} = queryParams!; - const req = `${baseURL}§_desc=${sector}&group_desc=${group}&commodity_desc=${commodity}&statisticcat_desc=${category}&domain_desc=${domains}&short_desc=${dataItem}&agg_level_desc=${geoLevel}&state_name=${location}&year=${year}`; - return req; + + 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 = []; + + if (Array.isArray(dataItem)) { + dataItem.forEach(item => { + reqs.push(`${baseReq}&short_desc=${item}`); + }); + } else { + reqs.push(`${baseReq}&short_desc=${dataItem}`); + } + + return reqs; +}; + +export const createQueryFromSelections = (selectedOptions: IStateOptions) => { + const {geographicLevel, states, years, ...subOptions} = selectedOptions; + const multipleStatesSelected = states.length > 1 || states[0] === "All States"; + const multipleYearsSelected = years.length > 1; + + if (multipleStatesSelected) { + states.forEach((state) => { + if (multipleYearsSelected) { + years.forEach(year => { + // do something + console.log("multiple years and mulitple states selected"); + }); + } else { + console.log("multiple states selected with one year"); + } + }); + } 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); + } + } + } }; -export const runTestQuery = () => { - const request = createRequest("Total Farmers", "STATE", "CALIFORNIA", "2017"); - fetchJsonp(request) +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); - }) + }); + }); }; + +const formatDataForCODAP = (res: any) => { + console.log({res}); + return res; +}; + +// 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); +// }) +// }) +// }; diff --git a/src/scripts/connect.js b/src/scripts/connect.js index 0ac3759..22a45a1 100644 --- a/src/scripts/connect.js +++ b/src/scripts/connect.js @@ -1,10 +1,90 @@ import { codapInterface } from "./codapInterface"; +const dataSetName = "NASS Quickstats Data"; +const dataSetTitle = "NASS Quickstats Data"; + export const connect = { initialize: async function () { return await codapInterface.init(this.iFrameDescriptor, null); }, + makeCODAPAttributeDef: function (attr) { + return { + name: attr.title, + title: attr.title, + description: attr.description, + type: attr.format, + formula: attr.formula + } + }, + + createNewDataset: async function (geoLevel) { + const geoLabel = geoLevel === "State" ? "States" : "Counties"; + 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"} + ] + }] + } + }); + }, + + + guaranteeDataset: async function (geoLevel) { + let datasetResource = 'dataContext[' + dataSetName + + ']'; + await this.createNewDataset(geoLevel); + const response = await codapInterface.sendRequest({ + action: 'get', + resource: datasetResource}); + 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); + } + return makeCaseTableResult.success && makeCaseTableResult.values.id; + }, + createNewCollection: async function(dSName, collName) { const message = { "action": "create", @@ -33,6 +113,6 @@ export const connect = { iFrameDescriptor: { version: '0.0.1', name: 'nass-plugin', - title: 'MultiData' + title: 'NASS Quickstats Data' }, } From b5d6406ffd6323adf181ab1177583b2b98f5d39e Mon Sep 17 00:00:00 2001 From: lublagg Date: Fri, 15 Sep 2023 15:31:04 -0400 Subject: [PATCH 11/21] Limit years options and "get data" button availability. --- src/components/app.scss | 2 +- src/components/app.tsx | 17 ++++++++- src/components/constants.ts | 16 ++++---- src/components/types.ts | 16 ++++---- src/components/utils.ts | 4 ++ src/components/years-options.tsx | 65 +++++++++++++++++--------------- src/scripts/connect.js | 2 +- 7 files changed, 71 insertions(+), 51 deletions(-) create mode 100644 src/components/utils.ts diff --git a/src/components/app.scss b/src/components/app.scss index 2edc7c5..b9c2a4e 100644 --- a/src/components/app.scss +++ b/src/components/app.scss @@ -86,7 +86,7 @@ button { font-family: 'Montserrat', sans-serif; font-size: 12px; font-weight: 500; - &:hover { + &:hover:not(:disabled) { cursor: pointer; background: $teal-light-12; } diff --git a/src/components/app.tsx b/src/components/app.tsx index 0cce018..d8228a7 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useState} from "react"; import { Dropdown } from "./dropdown"; import classnames from "classnames"; import { Information } from "./information"; -import { categories, defaultSelectedOptions } from "./constants"; +import { attributeOptions, categories, defaultSelectedOptions, yearsOptions } from "./constants"; import { IStateOptions } from "./types"; import { createQueryFromSelections } from "../scripts/api"; import { connect } from "../scripts/connect"; @@ -13,6 +13,8 @@ import css from "./app.scss"; function App() { const [showInfo, setShowInfo] = useState(false); const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); + const [getDataDisabled, setGetDataDisabled] = useState(true); + const {farmerDemographics, farmDemographics, crops, economicsAndWages} = selectedOptions; useEffect(() => { const init = async () => { @@ -21,6 +23,17 @@ function App() { init(); }, []); + useEffect(() => { + const {geographicLevel, states, years} = selectedOptions; + const attrKeys = attributeOptions.filter((attr) => attr.key !== "cropUnits").map((attr) => attr.key); + const selectedAttrKeys = attrKeys.filter((key) => selectedOptions[key].length > 0); + if (selectedAttrKeys.length && geographicLevel && states.length && years.length) { + setGetDataDisabled(false); + } else { + setGetDataDisabled(true); + } + }, [selectedOptions]); + const handleSetSelectedOptions = (option: string, value: string | string[]) => { const newSelectedOptions = {...selectedOptions, [option]: value}; setSelectedOptions(newSelectedOptions); @@ -74,7 +87,7 @@ function App() {
- +
diff --git a/src/components/constants.ts b/src/components/constants.ts index ef141c0..b1061cb 100644 --- a/src/components/constants.ts +++ b/src/components/constants.ts @@ -120,12 +120,12 @@ export const categories = [ ]; export const defaultSelectedOptions: IStateOptions = { - "geographicLevel": "State", - "states": ["All States"], - "farmerDemographics": [], - "farmDemographics": [], - "economicsAndWages": [], - "cropUnits": "", - "crops": [], - "years": [] + geographicLevel: "State", + states: ["All States"], + farmerDemographics: [], + farmDemographics: [], + economicsAndWages: [], + cropUnits: "", + crops: [], + years: [] }; diff --git a/src/components/types.ts b/src/components/types.ts index 6eaa4a9..5e78030 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -1,12 +1,12 @@ export interface IStateOptions { - "geographicLevel": string, - "states": string[] - "farmerDemographics": string[], - "farmDemographics": string[], - "economicsAndWages": string[], - "cropUnits": string, - "crops": string[] - "years": string[] + geographicLevel: string, + states: string[] + farmerDemographics: string[], + farmDemographics: string[], + economicsAndWages: string[], + cropUnits: string, + crops: string[] + years: string[] } export type OptionKey = keyof IStateOptions; diff --git a/src/components/utils.ts b/src/components/utils.ts new file mode 100644 index 0000000..5d9f3b0 --- /dev/null +++ b/src/components/utils.ts @@ -0,0 +1,4 @@ +export const flatten = (arr: any[]): any[] => { + return arr.reduce((acc: any[], val: any) => + Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), []); +} diff --git a/src/components/years-options.tsx b/src/components/years-options.tsx index 924bd47..8bfd780 100644 --- a/src/components/years-options.tsx +++ b/src/components/years-options.tsx @@ -1,10 +1,11 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { attributeOptions, yearsOptions } from "./constants"; import { IStateOptions } from "./types"; import { Options } from "./options"; +import { queryData } from "../scripts/query-headers"; +import { flatten } from "./utils"; import css from "./options.scss"; -import { queryData } from "../scripts/query-headers"; interface IProps { handleSetSelectedOptions: (option: string, value: string|string[]) => void; @@ -13,47 +14,49 @@ interface IProps { export const YearsOptions: React.FC = (props) => { const {handleSetSelectedOptions, selectedOptions} = props; + const [availableYearOptions, setAvailableYearOptions] = useState([]); + const {farmerDemographics, farmDemographics, crops, economicsAndWages} = selectedOptions; - const handleSelectYear = (yearKey: string, years: string|string[]) => { - handleSetSelectedOptions(yearKey, years); - - // check if any attributeOption keys have values in selectedOptions - const attrKeys = attributeOptions.map((attr) => attr.key); + useEffect(() => { + const attrKeys = attributeOptions.filter((attr) => attr.key !== "cropUnits").map((attr) => attr.key); const selectedAttrKeys = attrKeys.filter((key) => selectedOptions[key].length > 0); - const areAnyAttrsSelected = selectedAttrKeys.length > 0; - // if any attributes are selected, check that selected year data is available for that selected attribute - if (areAnyAttrsSelected) { - const selectedYears = Array.isArray(years) ? years : [years]; - const selectedAttrs = selectedAttrKeys.map((key) => selectedOptions[key]); - selectedAttrs.forEach((attr) => { - if (Array.isArray(attr)) { - attr.forEach((subAttr) => { - const subAttrData = queryData.find((d) => d.plugInAttribute === subAttr); - const yearKeyToUse = selectedOptions.geographicLevel === "county" ? "county" : "state"; - const availableYears = subAttrData?.years[yearKeyToUse]; - if (availableYears) { - // check -- are selectedYears included in queryParams?.years[yearKeyTouse] ? - const areYearsValid = selectedYears.map((y) => availableYears.indexOf(y) > -1).indexOf(false) < 0; - console.log({areYearsValid}); - console.log({availableYears}); - console.log({selectedYears}); - // do something if years are not valid - } - }); - } - }); + + if (!selectedAttrKeys.length) { + return; } + + const yearKeyToUse = selectedOptions.geographicLevel === "County" ? "county" : "state"; + const allSelectedAttrs = flatten(selectedAttrKeys.map((key) => selectedOptions[key])); + const newAvailableYears = allSelectedAttrs.reduce((years, attr) => { + const subAttrData = queryData.find((d) => d.plugInAttribute === attr); + const availableYears = subAttrData?.years[yearKeyToUse]; + if (availableYears) { + availableYears.forEach((y) => { + years.add(y); + }); + } + return years; + }, new Set()); + + setAvailableYearOptions(Array.from(newAvailableYears)); + }, [farmerDemographics, farmDemographics, crops, economicsAndWages]) + + const handleSelectYear = (yearKey: string, years: string|string[]) => { + handleSetSelectedOptions(yearKey, years); }; return (
+ {availableYearOptions.length === 0 ? +
Please select attributes to see available years.
+ : + />}
); }; diff --git a/src/scripts/connect.js b/src/scripts/connect.js index 22a45a1..6a648b7 100644 --- a/src/scripts/connect.js +++ b/src/scripts/connect.js @@ -19,7 +19,7 @@ export const connect = { }, createNewDataset: async function (geoLevel) { - const geoLabel = geoLevel === "State" ? "States" : "Counties"; + const geoLabel = geoLevel === "State" ? states : "Counties"; return codapInterface.sendRequest({ action: 'create', resource: 'dataContext', From 1b402243ef527ba4fd7953858057af00ec8d2a75 Mon Sep 17 00:00:00 2001 From: lublagg Date: Sat, 16 Sep 2023 18:38:18 -0400 Subject: [PATCH 12/21] 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 } From 1c0c59c58ebe391d5b40778442dc228abb47bcc3 Mon Sep 17 00:00:00 2001 From: lublagg Date: Sun, 17 Sep 2023 17:59:45 -0400 Subject: [PATCH 13/21] Encode parameters and unselect unavailable years. --- src/components/app.tsx | 3 +- src/components/attribute-options.tsx | 8 +- src/components/information.tsx | 2 +- src/components/options.scss | 2 +- src/components/place-options.tsx | 5 +- src/components/types.ts | 2 +- src/components/utils.ts | 2 +- src/components/years-options.tsx | 16 +- src/scripts/api.ts | 76 +++++---- src/scripts/query-headers.ts | 228 +++++++++++++-------------- 10 files changed, 179 insertions(+), 165 deletions(-) diff --git a/src/components/app.tsx b/src/components/app.tsx index 7564101..542371c 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useState} from "react"; import { Dropdown } from "./dropdown"; import classnames from "classnames"; import { Information } from "./information"; -import { attributeOptions, categories, defaultSelectedOptions, yearsOptions } from "./constants"; +import { attributeOptions, categories, defaultSelectedOptions } from "./constants"; import { IStateOptions } from "./types"; import { createTableFromSelections } from "../scripts/api"; import { connect } from "../scripts/connect"; @@ -14,7 +14,6 @@ function App() { const [showInfo, setShowInfo] = useState(false); const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); const [getDataDisabled, setGetDataDisabled] = useState(true); - const {farmerDemographics, farmDemographics, crops, economicsAndWages} = selectedOptions; useEffect(() => { const init = async () => { diff --git a/src/components/attribute-options.tsx b/src/components/attribute-options.tsx index 6e373a5..939aeee 100644 --- a/src/components/attribute-options.tsx +++ b/src/components/attribute-options.tsx @@ -34,8 +34,12 @@ export const AttributeOptions: React.FC = (props) => { attributeOptions.map((attr) => { return ( <> - {attr.label &&
{attr.label}
} - {attr.instructions &&
{attr.instructions}
} + {attr.label &&
{attr.label}
} + {attr.instructions && +
+ {attr.instructions} +
+ }
= (props) => {
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ipsum velit, pellentesque eget turpis non, vestibulum egestas tellus. Fusce sed dolor hendrerit, rutrum ligula et, imperdiet est. Nam tincidunt leo a ultricies elementum. Quisque ornare eget massa ac congue. Curabitur et nisi orci. Nulla mollis lacus eu velit mollis, in vehicula lectus ultricies. Nulla facilisi. + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ipsum velit, pellentesque eget turpis.
diff --git a/src/components/options.scss b/src/components/options.scss index 70b558b..bb1821e 100644 --- a/src/components/options.scss +++ b/src/components/options.scss @@ -68,7 +68,7 @@ } } -.category { +.statisticcat_desc { font-weight: 600; } diff --git a/src/components/place-options.tsx b/src/components/place-options.tsx index f165fbd..7e5bb04 100644 --- a/src/components/place-options.tsx +++ b/src/components/place-options.tsx @@ -18,7 +18,10 @@ export const PlaceOptions: React.FC = (props) => { return ( <>
{placeOpt.instructions}:
-
+
{ return arr.reduce((acc: any[], val: any) => Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), []); -} +}; diff --git a/src/components/years-options.tsx b/src/components/years-options.tsx index 8bfd780..bd0ab11 100644 --- a/src/components/years-options.tsx +++ b/src/components/years-options.tsx @@ -15,7 +15,6 @@ interface IProps { export const YearsOptions: React.FC = (props) => { const {handleSetSelectedOptions, selectedOptions} = props; const [availableYearOptions, setAvailableYearOptions] = useState([]); - const {farmerDemographics, farmDemographics, crops, economicsAndWages} = selectedOptions; useEffect(() => { const attrKeys = attributeOptions.filter((attr) => attr.key !== "cropUnits").map((attr) => attr.key); @@ -37,9 +36,20 @@ export const YearsOptions: React.FC = (props) => { } return years; }, new Set()); + const newSet: string[] = Array.from(newAvailableYears); + setAvailableYearOptions(newSet); - setAvailableYearOptions(Array.from(newAvailableYears)); - }, [farmerDemographics, farmDemographics, crops, economicsAndWages]) + // if selected years includes years not in available options, remove them from selection + const selectionsNotAvailable = selectedOptions.years.filter(year => !newSet.includes(year)); + if (selectionsNotAvailable.length) { + const newSelectedYears = [...selectedOptions.years]; + selectionsNotAvailable.forEach((year) => { + newSelectedYears.splice(newSelectedYears.indexOf(year), 1); + }); + handleSetSelectedOptions("years", newSelectedYears); + } + + }, [selectedOptions, handleSetSelectedOptions]); const handleSelectYear = (yearKey: string, years: string|string[]) => { handleSetSelectedOptions(yearKey, years); diff --git a/src/scripts/api.ts b/src/scripts/api.ts index ba10ec7..b44fbfa 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -1,5 +1,5 @@ import fetchJsonp from "fetch-jsonp"; -import { ICropDataItem, queryData } from "./query-headers"; +import { ICropCategory, ICropDataItem, queryData } from "./query-headers"; import { IStateOptions } from "../components/types"; import { connect } from "./connect"; import { cropOptions } from "../components/constants"; @@ -24,30 +24,41 @@ interface IGetAttrDataParams { 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!; - - 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(dItem => { - req = req + `&short_desc=${dItem}`; - }); - } else { - req = req + `&short_desc=${item}`; + if (!queryParams) { + throw new Error("Invalid attribute"); } + const { + sect_desc, + group_desc, + commodity_desc, + statisticcat_desc, + domain_desc, + short_desc, + } = queryParams; + + const item = cropCategory ? + (queryParams?.short_desc as ICropDataItem)[cropCategory] : + short_desc as string[]; + const cat = cropCategory ? + (queryParams?.statisticcat_desc as ICropCategory)[cropCategory] : + statisticcat_desc; + + const baseReq = `${baseURL}` + + `§_desc=${encodeURIComponent(sect_desc)}` + + `&group_desc=${encodeURIComponent(group_desc)}` + + `&commodity_desc=${encodeURIComponent(commodity_desc)}` + + `&statisticcat_desc=${encodeURIComponent(cat as string)}` + + `&domain_desc=${encodeURIComponent(domain_desc)}` + + `&agg_level_desc=${geographicLevel}` + + `&state_name=${location}` + + `&year=${year}`; + + let req = baseReq; + item.forEach(subItem => { + req = req + `&short_desc=${encodeURIComponent(subItem)}`; + }); return req; }; @@ -62,11 +73,14 @@ export const createTableFromSelections = async (selectedOptions: IStateOptions) 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); + if (!queryParams) { + throw new Error("Invalid attribute"); + } + const {short_desc} = queryParams; + if (Array.isArray(short_desc)) { + allAttrs.push(...short_desc); } else { - allAttrs.push(dataItem); + allAttrs.push(short_desc); } } } @@ -74,7 +88,7 @@ export const createTableFromSelections = async (selectedOptions: IStateOptions) const items = await getItems(selectedOptions); await connect.createItems(items); await connect.makeCaseTableAppear(); -} +}; const getItems = async (selectedOptions: IStateOptions) => { const {states, years} = selectedOptions; @@ -108,7 +122,7 @@ const getDataForSingleYearAndState = async (selectedOptions: IStateOptions, stat let item: any = { "State": state, "Year": year, - } + }; for (const key in subOptions) { const value = subOptions[key as keyof typeof subOptions]; @@ -136,12 +150,12 @@ const getAttrData = async (params: IGetAttrDataParams) => { const {data} = res; data.forEach((dataItem: any) => { values[dataItem.short_desc] = dataItem.Value; - }) + }); } else { console.log("error"); } return values; -} +}; export const fetchData = async (req: string) => { try { @@ -152,4 +166,4 @@ export const fetchData = async (req: string) => { console.log("parsing failed", error); throw error; } -}; \ No newline at end of file +}; diff --git a/src/scripts/query-headers.ts b/src/scripts/query-headers.ts index 2151acf..32110af 100644 --- a/src/scripts/query-headers.ts +++ b/src/scripts/query-headers.ts @@ -1,19 +1,24 @@ const areaHarvested = "Area Harvested"; const yieldInBU = "Yield"; -export interface ICropDataItem { + +export interface ICropCategory { [areaHarvested]: string, [yieldInBU]: string } +export interface ICropDataItem { + [areaHarvested]: string[], + [yieldInBU]: string[] +} -interface IQueryHeaders { +export interface IQueryHeaders { plugInAttribute: string, numberOfAttributeColumnsInCodap: number|string, - sector: string, - group: string, - commodity: string, - category: string|ICropDataItem, - dataItem: string|string[]|ICropDataItem, - domains: string, + sect_desc: string, + group_desc: string, + commodity_desc: string, + statisticcat_desc: string|ICropCategory, + short_desc: string[]|ICropDataItem, + domain_desc: string, geographicLevels?: string, years: { county: string[] @@ -22,24 +27,24 @@ interface IQueryHeaders { } const sharedDemographicHeaders = { -sector: "Demographics", -group: "Producers", -commodity: "Producers", -category: "Producers", -domains: "Total", +sect_desc: "Demographics", +group_desc: "Producers", +commodity_desc: "Producers", +statisticcat_desc: "Producers", +domain_desc: "Total", }; const sharedEconomicHeaders = { -sector: "Economics", -group: "Farms & Land & Assets", -commodity: "Farm Operations", -category: "Operations" +sect_desc: "Economics", +group_desc: "Farms & Land & Assets", +commodity_desc: "Farm Operations", +statisticcat_desc: "Operations" }; const sharedLaborHeaders = { -sector: "Economics", -group: "Expenses", -commodity: "Labor", +sect_desc: "Economics", +group_desc: "Expenses", +commodity_desc: "Labor", }; const allYears = []; @@ -52,7 +57,7 @@ export const queryData: Array = [ plugInAttribute: "Total Farmers", numberOfAttributeColumnsInCodap: 1, ...sharedDemographicHeaders, - dataItem: "PRODUCERS, (ALL) - NUMBER OF PRODUCERS", + short_desc: ["PRODUCERS, (ALL) - NUMBER OF PRODUCERS"], years: { county: ["2017"], state: ["2017"] @@ -62,7 +67,7 @@ export const queryData: Array = [ plugInAttribute: "Age", numberOfAttributeColumnsInCodap: 7, ...sharedDemographicHeaders, - dataItem: [ + short_desc: [ "PRODUCERS, AGE LT 25 - NUMBER OF PRODUCERS", "PRODUCERS, AGE 25 TO 34 - NUMBER OF PRODUCERS", "PRODUCERS, AGE 35 TO 44 - NUMBER OF PRODUCERS", @@ -81,7 +86,7 @@ export const queryData: Array = [ plugInAttribute: "Gender", numberOfAttributeColumnsInCodap: 2, ...sharedDemographicHeaders, - dataItem: [ + short_desc: [ "PRODUCERS, (ALL), FEMALE - NUMBER OF PRODUCERS", "PRODUCERS, (ALL), MALE - NUMBER OF PRODUCERS" ], @@ -94,7 +99,7 @@ export const queryData: Array = [ plugInAttribute: "Race", numberOfAttributeColumnsInCodap: 7, ...sharedDemographicHeaders, - dataItem: [ + short_desc: [ "PRODUCERS, AMERICAN INDIAN OR ALASKAN NATIVE - NUMBER OF PRODUCERS", "PRODUCERS, ASIAN - NUMBER OF PRODUCERS", "PRODUCERS, BLACK OR AFRICAN AMERICAN - NUMBER OF PRODUCERS", @@ -112,8 +117,8 @@ export const queryData: Array = [ plugInAttribute: "Total Farms", numberOfAttributeColumnsInCodap: 1, ...sharedEconomicHeaders, - dataItem: "Farm Operations - Number of Operations", - domains: "Total", + short_desc: ["FARM OPERATIONS - NUMBER OF OPERATIONS"], + domain_desc: "Total", geographicLevels: "State, County", years: { county: allYears, @@ -123,30 +128,30 @@ export const queryData: Array = [ { plugInAttribute: "Organization Type", numberOfAttributeColumnsInCodap: 5, - sector: "Demographics", - group: "Farms & Land & Assets", - commodity: "Farm Operations", - category: "Operations", - dataItem: [ + sect_desc: "Demographics", + group_desc: "Farms & Land & Assets", + commodity_desc: "Farm Operations", + statisticcat_desc: "Operations", + short_desc: [ "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, CORPORATION (EXCL FAMILY HELD) - NUMBER OF OPERATIONS", "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, CORPORATION, FAMILY HELD - NUMBER OF OPERATIONS", "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, FAMILY & INDIVIDUAL - NUMBER OF OPERATIONS", "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, INSTITUTIONAL & RESEARCH & RESERVATION & OTHER - NUMBER OF OPERATIONS", "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, PARTNERSHIP - NUMBER OF OPERATIONS" ], - domains: "Total", - geographicLevels: "State, County", + domain_desc: "Total", + geographicLevels: "County", years: { county: ["1997", "2002", "2007", "2012", "2017"], - state: allYears + state: [] } }, { plugInAttribute: "Economic Class", numberOfAttributeColumnsInCodap: "3 - 6", ...sharedEconomicHeaders, - dataItem: "Farm Operations - Number of Operations", - domains: "Economic Class", + short_desc: ["FARM OPERATIONS - NUMBER OF OPERATIONS"], + domain_desc: "Economic Class", geographicLevels: "State ", years: { county: allYears.filter(y => Number(y) >= 1987), @@ -156,12 +161,12 @@ export const queryData: Array = [ { plugInAttribute: "Acres Operated", numberOfAttributeColumnsInCodap: 14, - sector: "Economics", - group: "Farms & Land & Assets", - commodity: "Farm Operations", - category: "Area Operated", - dataItem: "Farm Operations - Acres Operated", - domains: "Area Operated", + sect_desc: "Economics", + group_desc: "Farms & Land & Assets", + commodity_desc: "Farm Operations", + statisticcat_desc: "Area Operated", + short_desc: ["FARM OPERATIONS - ACRES OPERATED"], + domain_desc: "Area Operated", geographicLevels: "State, County", years: { county: ["1997", "2002", "2007", "2012", "2017"], @@ -172,8 +177,8 @@ export const queryData: Array = [ plugInAttribute: "Organic", numberOfAttributeColumnsInCodap: 1, ...sharedEconomicHeaders, - dataItem: "Farm Operations, Organic - Number of Operations", - domains: "Organic Status", + short_desc: ["FARM OPERATIONS, ORGANIC - NUMBER OF OPERATIONS"], + domain_desc: "Organic Status", years: { county: ["2008", "2011", "2012", "2014", "2015", "2016", "2017", "2019", "2021"], state: ["2008", "2011", "2012", "2014", "2015", "2016", "2017", "2019", "2021"] @@ -183,13 +188,13 @@ export const queryData: Array = [ plugInAttribute: "Labor Status", numberOfAttributeColumnsInCodap: 3, ...sharedLaborHeaders, - category: "Workers", - dataItem: [ + statisticcat_desc: "Workers", + short_desc: [ "LABOR, MIGRANT - NUMBER OF WORKERS", "LABOR, UNPAID - NUMBER OF WORKERS", "LABOR, HIRED - NUMBER OF WORKERS" ], - domains: "Total", + domain_desc: "Total", geographicLevels: "State, County", years: { county: ["2012", "2017"], @@ -200,9 +205,9 @@ export const queryData: Array = [ plugInAttribute: "Wages", numberOfAttributeColumnsInCodap: 1, ...sharedLaborHeaders, - category: "Wage Rate", - dataItem: "LABOR, HIRED - WAGE RATE, MEASURED IN $/HOUR", - domains: "Total", + statisticcat_desc: "Wage Rate", + short_desc: ["LABOR, HIRED - WAGE RATE, MEASURED IN $/HOUR"], + domain_desc: "Total", geographicLevels: "Region: Multi-state", years: { county: allYears.filter(y => Number(y) >= 1989), @@ -213,9 +218,9 @@ export const queryData: Array = [ plugInAttribute: "Time Worked", numberOfAttributeColumnsInCodap: 1, ...sharedLaborHeaders, - category: "Wage Rate", - dataItem: "Labor, Hired - Time Worked, Measured in Hours/Week", - domains: "Total", + statisticcat_desc: "Wage Rate", + short_desc: ["LABOR, HIRED - TIME WORKED, MEASURED IN HOURS/WEEK"], + domain_desc: "Total", geographicLevels: "Region: Multi-state", years: { county: allYears.filter(y => Number(y) >= 1989), @@ -225,18 +230,18 @@ export const queryData: Array = [ { plugInAttribute: "Corn", numberOfAttributeColumnsInCodap: 1, - sector: "Crops", - group: "Field Crops", - commodity: "Corn", - category: { + sect_desc: "Crops", + group_desc: "Field Crops", + commodity_desc: "Corn", + statisticcat_desc: { [areaHarvested]: "Area Harvested", [yieldInBU]: "Yield" }, - dataItem: { - [areaHarvested]: "Corn, Grain - Acres Harvested", - [yieldInBU]: "Corn, Grain - Yield, measured in BU / acre" + short_desc: { + [areaHarvested]: ["CORN, GRAIN - ACRES HARVESTED"], + [yieldInBU]: ["CORN, GRAIN - YIELD, MEASURED IN BU / ACRE"] }, - domains: "Total", + domain_desc: "Total", geographicLevels: "State, County", years: { county: allYears, @@ -246,18 +251,18 @@ export const queryData: Array = [ { plugInAttribute: "Cotton", numberOfAttributeColumnsInCodap: 1, - sector: "Crops", - group: "Field Crops", - commodity: "Cotton", - category: { + sect_desc: "Crops", + group_desc: "Field Crops", + commodity_desc: "Cotton", + statisticcat_desc: { [areaHarvested]: "Area Harvested", [yieldInBU]: "Yield" }, - dataItem: { - [areaHarvested]: "Cotton - Acres Harvested", - [yieldInBU]: "Cotton - Yield, measured in LB / acre" + short_desc: { + [areaHarvested]: ["COTTON - ACRES HARVESTED"], + [yieldInBU]: ["COTTON - YIELD, MEASURED IN LB / ACRE"] }, - domains: "Total", + domain_desc: "Total", geographicLevels: "State, County", years: { county: allYears, @@ -267,39 +272,18 @@ export const queryData: Array = [ { plugInAttribute: "Grapes", numberOfAttributeColumnsInCodap: 1, - sector: "Crops", - group: "Fruit & Tree Nuts", - commodity: "Grapes", - category: { - [areaHarvested]: "Area Harvested", - [yieldInBU]: "Yield" - }, - dataItem: { - [areaHarvested]: "Grapes, Organic - Acres Harvested", - [yieldInBU]: "Grapes - Yield, measured in tons / acre" - }, - domains: "Total", - geographicLevels: "State, County", - years: { - county: allYears, - state: allYears - } -}, -{ - plugInAttribute: "Grasses", - numberOfAttributeColumnsInCodap: 1, - sector: "Crops", - group: "Field Crops", - commodity: "Grasses", - category: { + sect_desc: "Crops", + group_desc: "Fruit & Tree Nuts", + commodity_desc: "Grapes", + statisticcat_desc: { [areaHarvested]: "Area Harvested", [yieldInBU]: "Yield" }, - dataItem: { - [areaHarvested]: "", - [yieldInBU]: "" + short_desc: { + [areaHarvested]: ["GRAPES, ORGANIC - ACRES HARVESTED"], + [yieldInBU]: ["GRAPES - YIELD, MEASURED IN TONS / ACRE"] }, - domains: "Total", + domain_desc: "Total", geographicLevels: "State, County", years: { county: allYears, @@ -309,18 +293,18 @@ export const queryData: Array = [ { plugInAttribute: "Oats", numberOfAttributeColumnsInCodap: 1, - sector: "Crops", - group: "Field Crops", - commodity: "Oats", - category: { + sect_desc: "Crops", + group_desc: "Field Crops", + commodity_desc: "Oats", + statisticcat_desc: { [areaHarvested]: "Area Harvested", [yieldInBU]: "Yield" }, - dataItem: { - [areaHarvested]: "Oats - Acres Harvested", - [yieldInBU]: "Oats - Yield, measured in BU / acre" + short_desc: { + [areaHarvested]: ["Oats - Acres Harvested"], + [yieldInBU]: ["Oats - Yield, measured in BU / acre"] }, - domains: "Total", + domain_desc: "Total", geographicLevels: "State, County", years: { county: allYears, @@ -330,18 +314,18 @@ export const queryData: Array = [ { plugInAttribute: "Soybeans", numberOfAttributeColumnsInCodap: 1, - sector: "Crops", - group: "Field Crops", - commodity: "Soybeans", - category: { + sect_desc: "Crops", + group_desc: "Field Crops", + commodity_desc: "Soybeans", + statisticcat_desc: { [areaHarvested]: "Area Harvested", [yieldInBU]: "Yield" }, - dataItem: { - [areaHarvested]: "Soybeans - Acres Harvested", - [yieldInBU]: "Soybeans - Yield, measured in BU / acre" + short_desc: { + [areaHarvested]: ["Soybeans - Acres Harvested"], + [yieldInBU]: ["Soybeans - Yield, measured in BU / acre"] }, - domains: "Total", + domain_desc: "Total", geographicLevels: "State, County", years: { county: allYears, @@ -351,18 +335,18 @@ export const queryData: Array = [ { plugInAttribute: "Wheat", numberOfAttributeColumnsInCodap: 1, - sector: "Crops", - group: "Field Crops", - commodity: "Wheat", - category: { + sect_desc: "Crops", + group_desc: "Field Crops", + commodity_desc: "Wheat", + statisticcat_desc: { [areaHarvested]: "Area Harvested", [yieldInBU]: "Yield" }, - dataItem: { - [areaHarvested]: "Wheat - Acres Harvested", - [yieldInBU]: "Wheat - Yield, measured in BU / acre" + short_desc: { + [areaHarvested]: ["Wheat - Acres Harvested"], + [yieldInBU]: ["Wheat - Yield, measured in BU / acre"] }, - domains: "Total", + domain_desc: "Total", geographicLevels: "State, County", years: { county: allYears, From c879b5e68e2cd9046d27606bed1f4acdfd3f4a55 Mon Sep 17 00:00:00 2001 From: lublagg Date: Mon, 18 Sep 2023 21:22:32 -0400 Subject: [PATCH 14/21] Add support for counties. --- src/components/app.tsx | 4 +- src/components/attribute-options.tsx | 4 +- src/components/dropdown.tsx | 2 +- src/components/options.tsx | 2 +- src/components/place-options.tsx | 4 +- src/components/summary.tsx | 4 +- src/components/utils.ts | 4 - src/components/years-options.tsx | 11 +- src/{components => constants}/constants.ts | 106 +- src/constants/counties.ts | 3285 ++++++++++++++++++++ src/constants/query-headers.ts | 352 +++ src/{components => constants}/types.ts | 33 +- src/scripts/api.ts | 118 +- src/scripts/connect.js | 14 +- src/scripts/query-headers.ts | 356 --- src/scripts/utils.ts | 10 + 16 files changed, 3837 insertions(+), 472 deletions(-) delete mode 100644 src/components/utils.ts rename src/{components => constants}/constants.ts (72%) create mode 100644 src/constants/counties.ts create mode 100644 src/constants/query-headers.ts rename src/{components => constants}/types.ts (68%) delete mode 100644 src/scripts/query-headers.ts create mode 100644 src/scripts/utils.ts diff --git a/src/components/app.tsx b/src/components/app.tsx index 542371c..57539de 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -2,8 +2,8 @@ import React, {useEffect, useState} from "react"; import { Dropdown } from "./dropdown"; import classnames from "classnames"; import { Information } from "./information"; -import { attributeOptions, categories, defaultSelectedOptions } from "./constants"; -import { IStateOptions } from "./types"; +import { attributeOptions, categories, defaultSelectedOptions } from "../constants/constants"; +import { IStateOptions } from "../constants/types"; import { createTableFromSelections } from "../scripts/api"; import { connect } from "../scripts/connect"; diff --git a/src/components/attribute-options.tsx b/src/components/attribute-options.tsx index 939aeee..3a47779 100644 --- a/src/components/attribute-options.tsx +++ b/src/components/attribute-options.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Options } from "./options"; -import { attributeOptions } from "./constants"; +import { attributeOptions } from "../constants/constants"; import classnames from "classnames"; -import { IStateOptions } from "./types"; +import { IStateOptions } from "../constants/types"; import css from "./options.scss"; diff --git a/src/components/dropdown.tsx b/src/components/dropdown.tsx index 27e56a3..974314e 100644 --- a/src/components/dropdown.tsx +++ b/src/components/dropdown.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import classnames from "classnames"; import { PlaceOptions } from "./place-options"; -import { defaultSelectedOptions } from "./constants"; +import { defaultSelectedOptions } from "../constants/constants"; import { AttributeOptions } from "./attribute-options"; import css from "./dropdown.scss"; diff --git a/src/components/options.tsx b/src/components/options.tsx index 023c494..008dd5b 100644 --- a/src/components/options.tsx +++ b/src/components/options.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { IStateOptions, OptionKey } from "./types"; +import { IStateOptions, OptionKey } from "../constants/types"; import css from "./options.scss"; diff --git a/src/components/place-options.tsx b/src/components/place-options.tsx index 7e5bb04..6294bea 100644 --- a/src/components/place-options.tsx +++ b/src/components/place-options.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { placeOptions } from "./constants"; -import { IStateOptions } from "./types"; +import { placeOptions } from "../constants/constants"; +import { IStateOptions } from "../constants/types"; import { Options } from "./options"; import css from "./options.scss"; diff --git a/src/components/summary.tsx b/src/components/summary.tsx index 43a32a7..71d60ea 100644 --- a/src/components/summary.tsx +++ b/src/components/summary.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { IStateOptions } from "./types"; -import { attributeOptions } from "./constants"; +import { IStateOptions } from "../constants/types"; +import { attributeOptions } from "../constants/constants"; interface IProps { category: string; diff --git a/src/components/utils.ts b/src/components/utils.ts deleted file mode 100644 index 3f05872..0000000 --- a/src/components/utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const flatten = (arr: any[]): any[] => { - return arr.reduce((acc: any[], val: any) => - Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), []); -}; diff --git a/src/components/years-options.tsx b/src/components/years-options.tsx index bd0ab11..8413fbc 100644 --- a/src/components/years-options.tsx +++ b/src/components/years-options.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from "react"; -import { attributeOptions, yearsOptions } from "./constants"; -import { IStateOptions } from "./types"; +import { attributeOptions, yearsOptions } from "../constants/constants"; +import { IStateOptions } from "../constants/types"; import { Options } from "./options"; -import { queryData } from "../scripts/query-headers"; -import { flatten } from "./utils"; +import { queryData } from "../constants/query-headers"; +import { flatten } from "../scripts/utils"; import css from "./options.scss"; @@ -24,11 +24,10 @@ export const YearsOptions: React.FC = (props) => { return; } - const yearKeyToUse = selectedOptions.geographicLevel === "County" ? "county" : "state"; const allSelectedAttrs = flatten(selectedAttrKeys.map((key) => selectedOptions[key])); const newAvailableYears = allSelectedAttrs.reduce((years, attr) => { const subAttrData = queryData.find((d) => d.plugInAttribute === attr); - const availableYears = subAttrData?.years[yearKeyToUse]; + const availableYears = subAttrData?.years[selectedOptions.geographicLevel]; if (availableYears) { availableYears.forEach((y) => { years.add(y); diff --git a/src/components/constants.ts b/src/constants/constants.ts similarity index 72% rename from src/components/constants.ts rename to src/constants/constants.ts index 23aaecf..234e08c 100644 --- a/src/components/constants.ts +++ b/src/constants/constants.ts @@ -7,62 +7,66 @@ export const geographicLevelOptions: IAttrOptions = { options : ["State", "County"] }; +export const fiftyStates = [ + "Alabama", + "Alaska", + "Arizona", + "Arkansas", + "California", + "Colorado", + "Connecticut", + "Delaware", + "Florida", + "Georgia", + "Hawaii", + "Idaho", + "Illinois", + "Indiana", + "Iowa", + "Kansas", + "Kentucky", + "Louisiana", + "Maine", + "Maryland", + "Massachusetts", + "Michigan", + "Minnesota", + "Mississippi", + "Missouri", + "Montana", + "Nebraska", + "Nevada", + "New Hampshire", + "New Jersey", + "New Mexico", + "New York", + "North Carolina", + "North Dakota", + "Ohio", + "Oklahoma", + "Oregon", + "Pennsylvania", + "Rhode Island", + "South Carolina", + "South Dakota", + "Tennessee", + "Texas", + "Utah", + "Vermont", + "Virginia", + "Washington", + "West Virginia", + "Wisconsin", + "Wyoming" +] + export const stateOptions: IAttrOptions = { label: null, key: "states", instructions: "Choose states to include in your dataset from the list below", options: [ "All States", - "Alabama", - "Alaska", - "Arizona", - "Arkansas", - "California", - "Colorado", - "Connecticut", - "Delaware", - "Florida", - "Georgia", - "Hawaii", - "Idaho", - "Illinois", - "Indiana", - "Iowa", - "Kansas", - "Kentucky", - "Louisiana", - "Maine", - "Maryland", - "Massachusetts", - "Michigan", - "Minnesota", - "Mississippi", - "Missouri", - "Montana", - "Nebraska", - "Nevada", - "New Hampshire", - "New Jersey", - "New Mexico", - "New York", - "North Carolina", - "North Dakota", - "Ohio", - "Oklahoma", - "Oregon", - "Pennsylvania", - "Rhode Island", - "South Carolina", - "South Dakota", - "Tennessee", - "Texas", - "Utah", - "Vermont", - "Virginia", - "Washington", - "West Virginia", - "Wisconsin", - "Wyoming" + ...fiftyStates ] }; @@ -128,4 +132,4 @@ export const defaultSelectedOptions: IStateOptions = { cropUnits: "", crops: [], years: [] -}; +}; \ No newline at end of file diff --git a/src/constants/counties.ts b/src/constants/counties.ts new file mode 100644 index 0000000..f97563e --- /dev/null +++ b/src/constants/counties.ts @@ -0,0 +1,3285 @@ +export const countyData: {[state: string]: string[]} = { + "Alabama": [ + "AUTAUGA", + "BALDWIN", + "BARBOUR", + "BIBB", + "BLOUNT", + "BULLOCK", + "BUTLER", + "CALHOUN", + "CHAMBERS", + "CHEROKEE", + "CHILTON", + "CHOCTAW", + "CLARKE", + "CLAY", + "CLEBURNE", + "COFFEE", + "COLBERT", + "CONECUH", + "COOSA", + "COVINGTON", + "CRENSHAW", + "CULLMAN", + "DALE", + "DALLAS", + "DE KALB", + "ELMORE", + "ESCAMBIA", + "ETOWAH", + "FAYETTE", + "FRANKLIN", + "GENEVA", + "GREENE", + "HALE", + "HENRY", + "HOUSTON", + "JACKSON", + "JEFFERSON", + "LAMAR", + "LAUDERDALE", + "LAWRENCE", + "LEE", + "LIMESTONE", + "LOWNDES", + "MACON", + "MADISON", + "MARENGO", + "MARION", + "MARSHALL", + "MOBILE", + "MONROE", + "MONTGOMERY", + "MORGAN", + + "OTHER COUNTIES", + "PERRY", + "PICKENS", + "PIKE", + "RANDOLPH", + "RUSSELL", + "SAINT CLAIR", + "SHELBY", + "SUMTER", + "TALLADEGA", + "TALLAPOOSA", + "TUSCALOOSA", + "WALKER", + "WASHINGTON", + "WILCOX", + "WINSTON" + ], + "Alaska": [ + "ALEUTIAN ISLANDS", + "ANCHORAGE", + "FAIRBANKS NORTH STAR", + "JUNEAU", + "KENAI PENINSULA", + + "OTHER COUNTIES" + ], + "Arizona": [ + "APACHE", + "COCHISE", + "COCONINO", + "GILA", + "GRAHAM", + "GREENLEE", + "LAPAZ", + "MARICOPA", + "MOHAVE", + "NAVAJO", + + "OTHER COUNTIES", + "PIMA", + "PINAL", + "SANTA CRUZ", + "YAVAPAI", + "YUMA" + ], + "Arkansas": [ + "ARKANSAS", + "ASHLEY", + "BAXTER", + "BENTON", + "BOONE", + "BRADLEY", + "CALHOUN", + "CARROLL", + "CHICOT", + "CLARK", + "CLAY", + "CLEBURNE", + "CLEVELAND", + "COLUMBIA", + "CONWAY", + "CRAIGHEAD", + "CRAWFORD", + "CRITTENDEN", + "CROSS", + "DALLAS", + "DESHA", + "DREW", + "FAULKNER", + "FRANKLIN", + "FULTON", + "GARLAND", + "GRANT", + "GREENE", + "HEMPSTEAD", + "HOT SPRING", + "HOWARD", + "INDEPENDENCE", + "IZARD", + "JACKSON", + "JEFFERSON", + "JOHNSON", + "LAFAYETTE", + "LAWRENCE", + "LEE", + "LINCOLN", + "LITTLE RIVER", + "LOGAN", + "LONOKE", + "MADISON", + "MARION", + "MILLER", + "MISSISSIPPI", + "MONROE", + "MONTGOMERY", + "NEVADA", + "NEWTON", + + "OTHER COUNTIES", + "OUACHITA", + "PERRY", + "PHILLIPS", + "PIKE", + "POINSETT", + "POLK", + "POPE", + "PRAIRIE", + "PULASKI", + "RANDOLPH", + "SAINT FRANCIS", + "SALINE", + "SCOTT", + "SEARCY", + "SEBASTIAN", + "SEVIER", + "SHARP", + "STONE", + "UNION", + "VAN BUREN", + "WASHINGTON", + "WHITE", + "WOODRUFF", + "YELL" +], + "California": [ + "ALAMEDA", + "ALPINE", + "AMADOR", + "BUTTE", + "CALAVERAS", + "COLUSA", + "CONTRA COSTA", + "DEL NORTE", + "EL DORADO", + "FRESNO", + "GLENN", + "HUMBOLDT", + "IMPERIAL", + "INYO", + "KERN", + "KINGS", + "LAKE", + "LASSEN", + "LOS ANGELES", + "MADERA", + "MARIN", + "MARIPOSA", + "MENDOCINO", + "MERCED", + "MODOC", + "MONO", + "MONTEREY", + "NAPA", + "NEVADA", + "ORANGE", + + "OTHER COUNTIES", + "PLACER", + "PLUMAS", + "RIVERSIDE", + "SACRAMENTO", + "SAN BENITO", + "SAN BERNARDINO", + "SAN DIEGO", + "SAN FRANCISCO", + "SAN JOAQUIN", + "SAN LUIS OBISPO", + "SAN MATEO", + "SANTA BARBARA", + "SANTA CLARA", + "SANTA CRUZ", + "SHASTA", + "SIERRA", + "SISKIYOU", + "SOLANO", + "SONOMA", + "STANISLAUS", + "SUTTER", + "TEHAMA", + "TRINITY", + "TULARE", + "TUOLUMNE", + "VENTURA", + "YOLO", + "YUBA" + ], + "Colorado": [ + "ADAMS", + "ALAMOSA", + "ARAPAHOE", + "ARCHULETA", + "BACA", + "BENT", + "BOULDER", + "BROOMFIELD", + "CHAFFEE", + "CHEYENNE", + "CLEAR CREEK", + "CONEJOS", + "COSTILLA", + "CROWLEY", + "CUSTER", + "DELTA", + "DENVER", + "DOLORES", + "DOUGLAS", + "EAGLE", + "EL PASO", + "ELBERT", + "FREMONT", + "GARFIELD", + "GILPIN", + "GRAND", + "GUNNISON", + "HINSDALE", + "HUERFANO", + "JACKSON", + "JEFFERSON", + "KIOWA", + "KIT CARSON", + "LA PLATA", + "LAKE", + "LARIMER", + "LAS ANIMAS", + "LINCOLN", + "LOGAN", + "MESA", + "MINERAL", + "MOFFAT", + "MONTEZUMA", + "MONTROSE", + "MORGAN", + "OTERO", + + "OTHER COUNTIES", + "OURAY", + "PARK", + "PHILLIPS", + "PITKIN", + "PROWERS", + "PUEBLO", + "RIO BLANCO", + "RIO GRANDE", + "ROUTT", + "SAGUACHE", + "SAN JUAN", + "SAN MIGUEL", + "SEDGWICK", + "SUMMIT", + "TELLER", + "WASHINGTON", + "WELD", + "YUMA" +], + "Connecticut": [ + "FAIRFIELD", + "HARTFORD", + "LITCHFIELD", + "MIDDLESEX", + "NEW HAVEN", + "NEW LONDON", + + "OTHER COUNTIES", + "TOLLAND", + "WINDHAM" +], + "Delaware": [ + "KENT", + "NEW CASTLE", + "OTHER COUNTIES", + "SUSSEX" +], + "Florida": [ + "ALACHUA", + "BAKER", + "BAY", + "BRADFORD", + "BREVARD", + "BROWARD", + "CALHOUN", + "CHARLOTTE", + "CITRUS", + "CLAY", + "COLLIER", + "COLUMBIA", + "DE SOTO", + "DIXIE", + "DUVAL", + "ESCAMBIA", + "FLAGLER", + "FRANKLIN", + "GADSDEN", + "GILCHRIST", + "GLADES", + "GULF", + "HAMILTON", + "HARDEE", + "HENDRY", + "HERNANDO", + "HIGHLANDS", + "HILLSBOROUGH", + "HOLMES", + "INDIAN RIVER", + "JACKSON", + "JEFFERSON", + "LAFAYETTE", + "LAKE", + "LEE", + "LEON", + "LEVY", + "LIBERTY", + "MADISON", + "MANATEE", + "MARION", + "MARTIN", + "MIAMI-DADE", + "MONROE", + "NASSAU", + "OKALOOSA", + "OKEECHOBEE", + "ORANGE", + "OSCEOLA", + + "OTHER COUNTIES", + "PALM BEACH", + "PASCO", + "PINELLAS", + "POLK", + "PUTNAM", + "SANTA ROSA", + "SARASOTA", + "SEMINOLE", + "ST. JOHNS", + "ST. LUCIE", + "SUMTER", + "SUWANNEE", + "TAYLOR", + "UNION", + "VOLUSIA", + "WAKULLA", + "WALTON", + "WASHINGTON" +], + "Georgia": [ + "APPLING", + "ATKINSON", + "BACON", + "BAKER", + "BALDWIN", + "BANKS", + "BARROW", + "BARTOW", + "BEN HILL", + "BERRIEN", + "BIBB", + "BLECKLEY", + "BRANTLEY", + "BROOKS", + "BRYAN", + "BULLOCH", + "BURKE", + "BUTTS", + "CALHOUN", + "CAMDEN", + "CANDLER", + "CARROLL", + "CATOOSA", + "CHARLTON", + "CHATHAM", + "CHATTAHOOCHEE", + "CHATTOOGA", + "CHEROKEE", + "CLARKE", + "CLAY", + "CLAYTON", + "CLINCH", + "COBB", + "COFFEE", + "COLQUITT", + "COLUMBIA", + "COOK", + "COWETA", + "CRAWFORD", + "CRISP", + "DADE", + "DAWSON", + "DE KALB", + "DECATUR", + "DODGE", + "DOOLY", + "DOUGHERTY", + "DOUGLAS", + "EARLY", + "ECHOLS", + "EFFINGHAM", + "ELBERT", + "EMANUEL", + "EVANS", + "FANNIN", + "FAYETTE", + "FLOYD", + "FORSYTH", + "FRANKLIN", + "FULTON", + "GILMER", + "GLASCOCK", + "GLYNN", + "GORDON", + "GRADY", + "GREENE", + "GWINNETT", + "HABERSHAM", + "HALL", + "HANCOCK", + "HARALSON", + "HARRIS", + "HART", + "HEARD", + "HENRY", + "HOUSTON", + "IRWIN", + "JACKSON", + "JASPER", + "JEFF DAVIS", + "JEFFERSON", + "JENKINS", + "JOHNSON", + "JONES", + "LAMAR", + "LANIER", + "LAURENS", + "LEE", + "LIBERTY", + "LINCOLN", + "LONG", + "LOWNDES", + "LUMPKIN", + "MACON", + "MADISON", + "MARION", + "MCDUFFIE", + "MCINTOSH", + "MERIWETHER", + "MILLER", + "MITCHELL", + "MONROE", + "MONTGOMERY", + "MORGAN", + "MURRAY", + "MUSCOGEE", + "NEWTON", + "OCONEE", + "OGLETHORPE", + + "OTHER COUNTIES", + "PAULDING", + "PEACH", + "PICKENS", + "PIERCE", + "PIKE", + "POLK", + "PULASKI", + "PUTNAM", + "QUITMAN", + "RABUN", + "RANDOLPH", + "RICHMOND", + "ROCKDALE", + "SCHLEY", + "SCREVEN", + "SEMINOLE", + "SPALDING", + "STEPHENS", + "STEWART", + "SUMTER", + "TALBOT", + "TALIAFERRO", + "TATTNALL", + "TAYLOR", + "TELFAIR", + "TERRELL", + "THOMAS", + "TIFT", + "TOOMBS", + "TOWNS", + "TREUTLEN", + "TROUP", + "TURNER", + "TWIGGS", + "UNION", + "UPSON", + "WALKER", + "WALTON", + "WARE", + "WARREN", + "WASHINGTON", + "WAYNE", + "WEBSTER", + "WHEELER", + "WHITE", + "WHITFIELD", + "WILCOX", + "WILKES", + "WILKINSON", + "WORTH" +], + "Hawaii": [ + "HAWAII", + "HONOLULU", + "KAUAI", + "MAUI & KALWAO", + + "OTHER COUNTIES" +], + "Idaho": [ + "ADA", + "ADAMS", + "BANNOCK", + "BEAR LAKE", + "BENEWAH", + "BINGHAM", + "BLAINE", + "BOISE", + "BONNER", + "BONNEVILLE", + "BOUNDARY", + "BUTTE", + "CAMAS", + "CANYON", + "CARIBOU", + "CASSIA", + "CLARK", + "CLEARWATER", + "CUSTER", + "ELMORE", + "FRANKLIN", + "FREMONT", + "GEM", + "GOODING", + "IDAHO", + "JEFFERSON", + "JEROME", + "KOOTENAI", + "LATAH", + "LEMHI", + "LEWIS", + "LINCOLN", + "MADISON", + "MINIDOKA", + "NEZ PERCE", + "ONEIDA", + + "OTHER COUNTIES", + "OWYHEE", + "PAYETTE", + "POWER", + "SHOSHONE", + "TETON", + "TWIN FALLS", + "VALLEY", + "WASHINGTON" +], + "Illinois": [ + "ADAMS", + "ALEXANDER", + "BOND", + "BOONE", + "BROWN", + "BUREAU", + "CALHOUN", + "CARROLL", + "CASS", + "CHAMPAIGN", + "CHRISTIAN", + "CLARK", + "CLAY", + "CLINTON", + "COLES", + "COOK", + "CRAWFORD", + "CUMBERLAND", + "DE KALB", + "DE WITT", + "DOUGLAS", + "DU PAGE", + "EDGAR", + "EDWARDS", + "EFFINGHAM", + "FAYETTE", + "FORD", + "FRANKLIN", + "FULTON", + "GALLATIN", + "GREENE", + "GRUNDY", + "HAMILTON", + "HANCOCK", + "HARDIN", + "HENDERSON", + "HENRY", + "IROQUOIS", + "JACKSON", + "JASPER", + "JEFFERSON", + "JERSEY", + "JO DAVIESS", + "JOHNSON", + "KANE", + "KANKAKEE", + "KENDALL", + "KNOX", + "LA SALLE", + "LAKE", + "LAWRENCE", + "LEE", + "LIVINGSTON", + "LOGAN", + "MACON", + "MACOUPIN", + "MADISON", + "MARION", + "MARSHALL", + "MASON", + "MASSAC", + "MCDONOUGH", + "MCHENRY", + "MCLEAN", + "MENARD", + "MERCER", + "MONROE", + "MONTGOMERY", + "MORGAN", + "MOULTRIE", + "OGLE", + + "OTHER COUNTIES", + "PEORIA", + "PERRY", + "PIATT", + "PIKE", + "POPE", + "PULASKI", + "PUTNAM", + "RANDOLPH", + "RICHLAND", + "ROCK ISLAND", + "SALINE", + "SANGAMON", + "SCHUYLER", + "SCOTT", + "SHELBY", + "ST CLAIR", + "STARK", + "STEPHENSON", + "TAZEWELL", + "UNION", + "VERMILION", + "WABASH", + "WARREN", + "WASHINGTON", + "WAYNE", + "WHITE", + "WHITESIDE", + "WILL", + "WILLIAMSON", + "WINNEBAGO", + "WOODFORD" +], + "Indiana": [ + "ADAMS", + "ALLEN", + "BARTHOLOMEW", + "BENTON", + "BLACKFORD", + "BOONE", + "BROWN", + "CARROLL", + "CASS", + "CLARK", + "CLAY", + "CLINTON", + "CRAWFORD", + "DAVIESS", + "DE KALB", + "DEARBORN", + "DECATUR", + "DELAWARE", + "DUBOIS", + "ELKHART", + "FAYETTE", + "FLOYD", + "FOUNTAIN", + "FRANKLIN", + "FULTON", + "GIBSON", + "GRANT", + "GREENE", + "HAMILTON", + "HANCOCK", + "HARRISON", + "HENDRICKS", + "HENRY", + "HOWARD", + "HUNTINGTON", + "JACKSON", + "JASPER", + "JAY", + "JEFFERSON", + "JENNINGS", + "JOHNSON", + "KNOX", + "KOSCIUSKO", + "LA PORTE", + "LAGRANGE", + "LAKE", + "LAWRENCE", + "MADISON", + "MARION", + "MARSHALL", + "MARTIN", + "MIAMI", + "MONROE", + "MONTGOMERY", + "MORGAN", + "NEWTON", + "NOBLE", + "OHIO", + "ORANGE", + + "OTHER COUNTIES", + "OWEN", + "PARKE", + "PERRY", + "PIKE", + "PORTER", + "POSEY", + "PULASKI", + "PUTNAM", + "RANDOLPH", + "RIPLEY", + "RUSH", + "SCOTT", + "SHELBY", + "SPENCER", + "ST. JOSEPH", + "STARKE", + "STEUBEN", + "SULLIVAN", + "SWITZERLAND", + "TIPPECANOE", + "TIPTON", + "UNION", + "VANDERBURGH", + "VERMILLION", + "VIGO", + "WABASH", + "WARREN", + "WARRICK", + "WASHINGTON", + "WAYNE", + "WELLS", + "WHITE", + "WHITLEY" +], + "Iowa": [ + "ADAIR", + "ADAMS", + "ALLAMAKEE", + "APPANOOSE", + "AUDUBON", + "BENTON", + "BLACK HAWK", + "BOONE", + "BREMER", + "BUCHANAN", + "BUENA VISTA", + "BUTLER", + "CALHOUN", + "CARROLL", + "CASS", + "CEDAR", + "CERRO GORDO", + "CHEROKEE", + "CHICKASAW", + "CLARKE", + "CLAY", + "CLAYTON", + "CLINTON", + "CRAWFORD", + "DALLAS", + "DAVIS", + "DECATUR", + "DELAWARE", + "DES MOINES", + "DICKINSON", + "DUBUQUE", + "EMMET", + "FAYETTE", + "FLOYD", + "FRANKLIN", + "FREMONT", + "GREENE", + "GRUNDY", + "GUTHRIE", + "HAMILTON", + "HANCOCK", + "HARDIN", + "HARRISON", + "HENRY", + "HOWARD", + "HUMBOLDT", + "IDA", + "IOWA", + "JACKSON", + "JASPER", + "JEFFERSON", + "JOHNSON", + "JONES", + "KEOKUK", + "KOSSUTH", + "LEE", + "LINN", + "LOUISA", + "LUCAS", + "LYON", + "MADISON", + "MAHASKA", + "MARION", + "MARSHALL", + "MILLS", + "MITCHELL", + "MONONA", + "MONROE", + "MONTGOMERY", + "MUSCATINE", + "O BRIEN", + "OSCEOLA", + + "OTHER COUNTIES", + "PAGE", + "PALO ALTO", + "PLYMOUTH", + "POCAHONTAS", + "POLK", + "POTTAWATTAMIE", + "POWESHIEK", + "RINGGOLD", + "SAC", + "SCOTT", + "SHELBY", + "SIOUX", + "STORY", + "TAMA", + "TAYLOR", + "UNION", + "VAN BUREN", + "WAPELLO", + "WARREN", + "WASHINGTON", + "WAYNE", + "WEBSTER", + "WINNEBAGO", + "WINNESHIEK", + "WOODBURY", + "WORTH", + "WRIGHT" +], + "Kansas": [ + "ALLEN", + "ANDERSON", + "ATCHISON", + "BARBER", + "BARTON", + "BOURBON", + "BROWN", + "BUTLER", + "CHASE", + "CHAUTAUQUA", + "CHEROKEE", + "CHEYENNE", + "CLARK", + "CLAY", + "CLOUD", + "COFFEY", + "COMANCHE", + "COWLEY", + "CRAWFORD", + "DECATUR", + "DICKINSON", + "DONIPHAN", + "DOUGLAS", + "EDWARDS", + "ELK", + "ELLIS", + "ELLSWORTH", + "FINNEY", + "FORD", + "FRANKLIN", + "GEARY", + "GOVE", + "GRAHAM", + "GRANT", + "GRAY", + "GREELEY", + "GREENWOOD", + "HAMILTON", + "HARPER", + "HARVEY", + "HASKELL", + "HODGEMAN", + "JACKSON", + "JEFFERSON", + "JEWELL", + "JOHNSON", + "KEARNY", + "KINGMAN", + "KIOWA", + "LABETTE", + "LANE", + "LEAVENWORTH", + "LINCOLN", + "LINN", + "LOGAN", + "LYON", + "MARION", + "MARSHALL", + "MCPHERSON", + "MEADE", + "MIAMI", + "MITCHELL", + "MONTGOMERY", + "MORRIS", + "MORTON", + "NEMAHA", + "NEOSHO", + "NESS", + "NORTON", + "OSAGE", + "OSBORNE", + + "OTHER COUNTIES", + "OTTAWA", + "PAWNEE", + "PHILLIPS", + "POTTAWATOMIE", + "PRATT", + "RAWLINS", + "RENO", + "REPUBLIC", + "RICE", + "RILEY", + "ROOKS", + "RUSH", + "RUSSELL", + "SALINE", + "SCOTT", + "SEDGWICK", + "SEWARD", + "SHAWNEE", + "SHERIDAN", + "SHERMAN", + "SMITH", + "STAFFORD", + "STANTON", + "STEVENS", + "SUMNER", + "THOMAS", + "TREGO", + "WABAUNSEE", + "WALLACE", + "WASHINGTON", + "WICHITA", + "WILSON", + "WOODSON", + "WYANDOTTE" +], + "Kentucky": [ + "ADAIR", + "ALLEN", + "ANDERSON", + "BALLARD", + "BARREN", + "BATH", + "BELL", + "BOONE", + "BOURBON", + "BOYD", + "BOYLE", + "BRACKEN", + "BREATHITT", + "BRECKINRIDGE", + "BULLITT", + "BUTLER", + "CALDWELL", + "CALLOWAY", + "CAMPBELL", + "CARLISLE", + "CARROLL", + "CARTER", + "CASEY", + "CHRISTIAN", + "CLARK", + "CLAY", + "CLINTON", + "CRITTENDEN", + "CUMBERLAND", + "DAVIESS", + "EDMONSON", + "ELLIOTT", + "ESTILL", + "FAYETTE", + "FLEMING", + "FLOYD", + "FRANKLIN", + "FULTON", + "GALLATIN", + "GARRARD", + "GRANT", + "GRAVES", + "GRAYSON", + "GREEN", + "GREENUP", + "HANCOCK", + "HARDIN", + "HARLAN", + "HARRISON", + "HART", + "HENDERSON", + "HENRY", + "HICKMAN", + "HOPKINS", + "JACKSON", + "JEFFERSON", + "JESSAMINE", + "JOHNSON", + "KENTON", + "KNOTT", + "KNOX", + "LARUE", + "LAUREL", + "LAWRENCE", + "LEE", + "LESLIE", + "LETCHER", + "LEWIS", + "LINCOLN", + "LIVINGSTON", + "LOGAN", + "LYON", + "MADISON", + "MAGOFFIN", + "MARION", + "MARSHALL", + "MARTIN", + "MASON", + "MCCRACKEN", + "MCCREARY", + "MCLEAN", + "MEADE", + "MENIFEE", + "MERCER", + "METCALFE", + "MONROE", + "MONTGOMERY", + "MORGAN", + "MUHLENBERG", + "NELSON", + "NICHOLAS", + "OHIO", + "OLDHAM", + + "OTHER COUNTIES", + "OWEN", + "OWSLEY", + "PENDLETON", + "PERRY", + "PIKE", + "POWELL", + "PULASKI", + "ROBERTSON", + "ROCKCASTLE", + "ROWAN", + "RUSSELL", + "SCOTT", + "SHELBY", + "SIMPSON", + "SPENCER", + "TAYLOR", + "TODD", + "TRIGG", + "TRIMBLE", + "UNION", + "WARREN", + "WASHINGTON", + "WAYNE", + "WEBSTER", + "WHITLEY", + "WOLFE", + "WOODFORD" +], + "Louisiana": [ + "ACADIA", + "ALLEN", + "ASCENSION", + "ASSUMPTION", + "AVOYELLES", + "BEAUREGARD", + "BIENVILLE", + "BOSSIER", + "CADDO", + "CALCASIEU", + "CALDWELL", + "CAMERON", + "CATAHOULA", + "CLAIBORNE", + "CONCORDIA", + "DE SOTO", + "EAST BATON ROUGE", + "EAST CARROLL", + "EAST FELICIANA", + "EVANGELINE", + "FRANKLIN", + "GRANT", + "IBERIA", + "IBERVILLE", + "JACKSON", + "JEFFERSON", + "JEFFERSON DAVIS", + "LA SALLE", + "LAFAYETTE", + "LAFOURCHE", + "LINCOLN", + "LIVINGSTON", + "MADISON", + "MOREHOUSE", + "NATCHITOCHES", + "ORLEANS", + + "OTHER COUNTIES", + "OUACHITA", + "PLAQUEMINES", + "POINTE COUPEE", + "RAPIDES", + "RED RIVER", + "RICHLAND", + "SABINE", + "SAINT BERNARD", + "SAINT CHARLES", + "SAINT HELENA", + "SAINT JAMES", + "SAINT LANDRY", + "SAINT MARTIN", + "SAINT MARY", + "SAINT TAMMANY", + "ST. JOHN THE BAPTIST", + "TANGIPAHOA", + "TENSAS", + "TERREBONNE", + "UNION", + "VERMILION", + "VERNON", + "WASHINGTON", + "WEBSTER", + "WEST BATON ROUGE", + "WEST CARROLL", + "WEST FELICIANA", + "WINN" +], + "Maine": [ + "ANDROSCOGGIN", + "AROOSTOOK", + "CUMBERLAND", + "FRANKLIN", + "HANCOCK", + "KENNEBEC", + "KNOX", + "LINCOLN", + + "OTHER COUNTIES", + "OXFORD", + "PENOBSCOT", + "PISCATAQUIS", + "SAGADAHOC", + "SOMERSET", + "WALDO", + "WASHINGTON", + "YORK" +], + "Maryland": [ + "ALLEGANY", + "ANNE ARUNDEL", + "BALTIMORE", + "CALVERT", + "CAROLINE", + "CARROLL", + "CECIL", + "CHARLES", + "DORCHESTER", + "FREDERICK", + "GARRETT", + "HARFORD", + "HOWARD", + "KENT", + "MONTGOMERY", + + "OTHER COUNTIES", + "PRINCE GEORGES", + "QUEEN ANNES", + "SOMERSET", + "ST MARYS", + "TALBOT", + "WASHINGTON", + "WICOMICO", + "WORCESTER" +], + "Massachusetts": [ + "BARNSTABLE", + "BERKSHIRE", + "BRISTOL", + "DUKES", + "ESSEX", + "FRANKLIN", + "HAMPDEN", + "HAMPSHIRE", + "MIDDLESEX", + "NANTUCKET", + "NORFOLK", + + "OTHER COUNTIES", + "PLYMOUTH", + "SUFFOLK", + "WORCESTER" +], + "Michigan": [ + "ALCONA", + "ALGER", + "ALLEGAN", + "ALPENA", + "ANTRIM", + "ARENAC", + "BARAGA", + "BARRY", + "BAY", + "BENZIE", + "BERRIEN", + "BRANCH", + "CALHOUN", + "CASS", + "CHARLEVOIX", + "CHEBOYGAN", + "CHIPPEWA", + "CLARE", + "CLINTON", + "CRAWFORD", + "DELTA", + "DICKINSON", + "EATON", + "EMMET", + "GENESEE", + "GLADWIN", + "GOGEBIC", + "GRAND TRAVERSE", + "GRATIOT", + "HILLSDALE", + "HOUGHTON", + "HURON", + "INGHAM", + "IONIA", + "IOSCO", + "IRON", + "ISABELLA", + "JACKSON", + "KALAMAZOO", + "KALKASKA", + "KENT", + "KEWEENAW", + "LAKE", + "LAPEER", + "LEELANAU", + "LENAWEE", + "LIVINGSTON", + "LUCE", + "MACKINAC", + "MACOMB", + "MANISTEE", + "MARQUETTE", + "MASON", + "MECOSTA", + "MENOMINEE", + "MIDLAND", + "MISSAUKEE", + "MONROE", + "MONTCALM", + "MONTMORENCY", + "MUSKEGON", + "NEWAYGO", + "OAKLAND", + "OCEANA", + "OGEMAW", + "ONTONAGON", + "OSCEOLA", + "OSCODA", + + "OTHER COUNTIES", + "OTSEGO", + "OTTAWA", + "PRESQUE ISLE", + "ROSCOMMON", + "SAGINAW", + "SANILAC", + "SCHOOLCRAFT", + "SHIAWASSEE", + "ST CLAIR", + "ST JOSEPH", + "TUSCOLA", + "VAN BUREN", + "WASHTENAW", + "WAYNE", + "WEXFORD" +], + "Minnesota": [ + "AITKIN", + "ANOKA", + "BECKER", + "BELTRAMI", + "BENTON", + "BIG STONE", + "BLUE EARTH", + "BROWN", + "CARLTON", + "CARVER", + "CASS", + "CHIPPEWA", + "CHISAGO", + "CLAY", + "CLEARWATER", + "COOK", + "COTTONWOOD", + "CROW WING", + "DAKOTA", + "DODGE", + "DOUGLAS", + "FARIBAULT", + "FILLMORE", + "FREEBORN", + "GOODHUE", + "GRANT", + "HENNEPIN", + "HOUSTON", + "HUBBARD", + "ISANTI", + "ITASCA", + "JACKSON", + "KANABEC", + "KANDIYOHI", + "KITTSON", + "KOOCHICHING", + "LAC QUI PARLE", + "LAKE", + "LAKE OF THE WOODS", + "LE SUEUR", + "LINCOLN", + "LYON", + "MAHNOMEN", + "MARSHALL", + "MARTIN", + "MCLEOD", + "MEEKER", + "MILLE LACS", + "MORRISON", + "MOWER", + "MURRAY", + "NICOLLET", + "NOBLES", + "NORMAN", + "OLMSTED", + + "OTHER COUNTIES", + "OTTER TAIL", + "PENNINGTON", + "PINE", + "PIPESTONE", + "POLK", + "POPE", + "RAMSEY", + "RED LAKE", + "REDWOOD", + "RENVILLE", + "RICE", + "ROCK", + "ROSEAU", + "SCOTT", + "SHERBURNE", + "SIBLEY", + "ST. LOUIS", + "STEARNS", + "STEELE", + "STEVENS", + "SWIFT", + "TODD", + "TRAVERSE", + "WABASHA", + "WADENA", + "WASECA", + "WASHINGTON", + "WATONWAN", + "WILKIN", + "WINONA", + "WRIGHT", + "YELLOW MEDICINE" +], + "Mississippi": [ + "ADAMS", + "ALCORN", + "AMITE", + "ATTALA", + "BENTON", + "BOLIVAR", + "CALHOUN", + "CARROLL", + "CHICKASAW", + "CHOCTAW", + "CLAIBORNE", + "CLARKE", + "CLAY", + "COAHOMA", + "COPIAH", + "COVINGTON", + "DE SOTO", + "FORREST", + "FRANKLIN", + "GEORGE", + "GREENE", + "GRENADA", + "HANCOCK", + "HARRISON", + "HINDS", + "HOLMES", + "HUMPHREYS", + "ISSAQUENA", + "ITAWAMBA", + "JACKSON", + "JASPER", + "JEFFERSON", + "JEFFERSON DAVIS", + "JONES", + "KEMPER", + "LAFAYETTE", + "LAMAR", + "LAUDERDALE", + "LAWRENCE", + "LEAKE", + "LEE", + "LEFLORE", + "LINCOLN", + "LOWNDES", + "MADISON", + "MARION", + "MARSHALL", + "MONROE", + "MONTGOMERY", + "NESHOBA", + "NEWTON", + "NOXUBEE", + "OKTIBBEHA", + + "OTHER COUNTIES", + "PANOLA", + "PEARL RIVER", + "PERRY", + "PIKE", + "PONTOTOC", + "PRENTISS", + "QUITMAN", + "RANKIN", + "SCOTT", + "SHARKEY", + "SIMPSON", + "SMITH", + "STONE", + "SUNFLOWER", + "TALLAHATCHIE", + "TATE", + "TIPPAH", + "TISHOMINGO", + "TUNICA", + "UNION", + "WALTHALL", + "WARREN", + "WASHINGTON", + "WAYNE", + "WEBSTER", + "WILKINSON", + "WINSTON", + "YALOBUSHA", + "YAZOO" +], + "Missouri": [ + "ADAIR", + "ANDREW", + "ATCHISON", + "AUDRAIN", + "BARRY", + "BARTON", + "BATES", + "BENTON", + "BOLLINGER", + "BOONE", + "BUCHANAN", + "BUTLER", + "CALDWELL", + "CALLAWAY", + "CAMDEN", + "CAPE GIRARDEAU", + "CARROLL", + "CARTER", + "CASS", + "CEDAR", + "CHARITON", + "CHRISTIAN", + "CLARK", + "CLAY", + "CLINTON", + "COLE", + "COOPER", + "CRAWFORD", + "DADE", + "DALLAS", + "DAVIESS", + "DE KALB", + "DENT", + "DOUGLAS", + "DUNKLIN", + "FRANKLIN", + "GASCONADE", + "GENTRY", + "GREENE", + "GRUNDY", + "HARRISON", + "HENRY", + "HICKORY", + "HOLT", + "HOWARD", + "HOWELL", + "IRON", + "JACKSON", + "JASPER", + "JEFFERSON", + "JOHNSON", + "KNOX", + "LACLEDE", + "LAFAYETTE", + "LAWRENCE", + "LEWIS", + "LINCOLN", + "LINN", + "LIVINGSTON", + "MACON", + "MADISON", + "MARIES", + "MARION", + "MCDONALD", + "MERCER", + "MILLER", + "MISSISSIPPI", + "MONITEAU", + "MONROE", + "MONTGOMERY", + "MORGAN", + "NEW MADRID", + "NEWTON", + "NODAWAY", + "OREGON", + "OSAGE", + + "OTHER COUNTIES", + "OZARK", + "PEMISCOT", + "PERRY", + "PETTIS", + "PHELPS", + "PIKE", + "PLATTE", + "POLK", + "PULASKI", + "PUTNAM", + "RALLS", + "RANDOLPH", + "RAY", + "REYNOLDS", + "RIPLEY", + "SALINE", + "SCHUYLER", + "SCOTLAND", + "SCOTT", + "SHANNON", + "SHELBY", + "ST CHARLES", + "ST CLAIR", + "ST FRANCOIS", + "ST LOUIS", + "STE GENEVIEVE", + "STE. GENEVIEVE", + "STODDARD", + "STONE", + "SULLIVAN", + "TANEY", + "TEXAS", + "VERNON", + "WARREN", + "WASHINGTON", + "WAYNE", + "WEBSTER", + "WORTH", + "WRIGHT" +], + "Montana": [ + "BEAVERHEAD", + "BIG HORN", + "BLAINE", + "BROADWATER", + "CARBON", + "CARTER", + "CASCADE", + "CHOUTEAU", + "CUSTER", + "DANIELS", + "DAWSON", + "DEER LODGE", + "FALLON", + "FERGUS", + "FLATHEAD", + "GALLATIN", + "GARFIELD", + "GLACIER", + "GOLDEN VALLEY", + "GRANITE", + "HILL", + "JEFFERSON", + "JUDITH BASIN", + "LAKE", + "LEWIS AND CLARK", + "LIBERTY", + "LINCOLN", + "MADISON", + "MCCONE", + "MEAGHER", + "MINERAL", + "MISSOULA", + "MUSSELSHELL", + + "OTHER COUNTIES", + "PARK", + "PETROLEUM", + "PHILLIPS", + "PONDERA", + "POWDER RIVER", + "POWELL", + "PRAIRIE", + "RAVALLI", + "RICHLAND", + "ROOSEVELT", + "ROSEBUD", + "SANDERS", + "SHERIDAN", + "SILVER BOW", + "STILLWATER", + "SWEET GRASS", + "TETON", + "TOOLE", + "TREASURE", + "VALLEY", + "WHEATLAND", + "WIBAUX", + "YELLOWSTONE" +], + "Nebraska": [ + "ADAMS", + "ANTELOPE", + "ARTHUR", + "BANNER", + "BLAINE", + "BOONE", + "BOX BUTTE", + "BOYD", + "BROWN", + "BUFFALO", + "BURT", + "BUTLER", + "CASS", + "CEDAR", + "CHASE", + "CHERRY", + "CHEYENNE", + "CLAY", + "COLFAX", + "CUMING", + "CUSTER", + "DAKOTA", + "DAWES", + "DAWSON", + "DEUEL", + "DIXON", + "DODGE", + "DOUGLAS", + "DUNDY", + "FILLMORE", + "FRANKLIN", + "FRONTIER", + "FURNAS", + "GAGE", + "GARDEN", + "GARFIELD", + "GOSPER", + "GRANT", + "GREELEY", + "HALL", + "HAMILTON", + "HARLAN", + "HAYES", + "HITCHCOCK", + "HOLT", + "HOOKER", + "HOWARD", + "JEFFERSON", + "JOHNSON", + "KEARNEY", + "KEITH", + "KEYA PAHA", + "KIMBALL", + "KNOX", + "LANCASTER", + "LINCOLN", + "LOGAN", + "LOUP", + "MADISON", + "MCPHERSON", + "MERRICK", + "MORRILL", + "NANCE", + "NEMAHA", + "NUCKOLLS", + + "OTHER COUNTIES", + "OTOE", + "PAWNEE", + "PERKINS", + "PHELPS", + "PIERCE", + "PLATTE", + "POLK", + "RED WILLOW", + "RICHARDSON", + "ROCK", + "SALINE", + "SARPY", + "SAUNDERS", + "SCOTTS BLUFF", + "SEWARD", + "SHERIDAN", + "SHERMAN", + "SIOUX", + "STANTON", + "THAYER", + "THOMAS", + "THURSTON", + "VALLEY", + "WASHINGTON", + "WAYNE", + "WEBSTER", + "WHEELER", + "YORK" +], + "Nevada": [ + "CARSON CITY", + "CHURCHILL", + "CLARK", + "DOUGLAS", + "ELKO", + "ESMERALDA", + "EUREKA", + "HUMBOLDT", + "LANDER", + "LINCOLN", + "LYON", + "MINERAL", + "NYE", + "ORMSBY", + + "OTHER COUNTIES", + "PERSHING", + "STOREY", + "WASHOE", + "WHITE PINE" +], + "New Hampshire": [ + "BELKNAP", + "CARROLL", + "CHESHIRE", + "COOS", + "GRAFTON", + "HILLSBOROUGH", + "MERRIMACK", + + "OTHER COUNTIES", + "ROCKINGHAM", + "STRAFFORD", + "SULLIVAN" +], + "New Jersey": [ + "ATLANTIC", + "BERGEN", + "BURLINGTON", + "CAMDEN", + "CAPE MAY", + "CUMBERLAND", + "ESSEX", + "GLOUCESTER", + "HUDSON", + "HUNTERDON", + "MERCER", + "MIDDLESEX", + "MONMOUTH", + "MORRIS", + "OCEAN", + + "OTHER COUNTIES", + "PASSAIC", + "SALEM", + "SOMERSET", + "SUSSEX", + "UNION", + "WARREN" +], + "New Mexico": [ + "BERNALILLO", + "CATRON", + "CHAVES", + "CIBOLA", + "COLFAX", + "CURRY", + "DE BACA", + "DONA ANA", + "EDDY", + "GRANT", + "GUADALUPE", + "HARDING", + "HIDALGO", + "LEA", + "LINCOLN", + "LOS ALAMOS", + "LUNA", + "MCKINLEY", + "MORA", + "OTERO", + + "OTHER COUNTIES", + "QUAY", + "RIO ARRIBA", + "ROOSEVELT", + "SAN JUAN", + "SAN MIGUEL", + "SANDOVAL", + "SANTA FE", + "SIERRA", + "SOCORRO", + "TAOS", + "TORRANCE", + "UNION", + "VALENCIA" +], + "New York": [ + "ALBANY", + "ALLEGANY", + "BRONX", + "BROOME", + "CATTARAUGUS", + "CAYUGA", + "CHAUTAUQUA", + "CHEMUNG", + "CHENANGO", + "CLINTON", + "COLUMBIA", + "CORTLAND", + "DELAWARE", + "DUTCHESS", + "ERIE", + "ESSEX", + "FRANKLIN", + "FULTON", + "GENESEE", + "GREENE", + "HAMILTON", + "HERKIMER", + "JEFFERSON", + "KINGS", + "LEWIS", + "LIVINGSTON", + "MADISON", + "MONROE", + "MONTGOMERY", + "NASSAU", + "NEW YORK", + "NIAGARA", + "ONEIDA", + "ONONDAGA", + "ONTARIO", + "ORANGE", + "ORLEANS", + "OSWEGO", + + "OTHER COUNTIES", + "OTSEGO", + "PUTNAM", + "QUEENS", + "RENSSELAER", + "RICHMOND", + "ROCKLAND", + "SARATOGA", + "SCHENECTADY", + "SCHOHARIE", + "SCHUYLER", + "SENECA", + "ST LAWRENCE", + "STEUBEN", + "SUFFOLK", + "SULLIVAN", + "TIOGA", + "TOMPKINS", + "ULSTER", + "WARREN", + "WASHINGTON", + "WAYNE", + "WESTCHESTER", + "WYOMING", + "YATES" +], + "North Carolina": [ + "ALAMANCE", + "ALEXANDER", + "ALLEGHANY", + "ANSON", + "ASHE", + "AVERY", + "BEAUFORT", + "BERTIE", + "BLADEN", + "BRUNSWICK", + "BUNCOMBE", + "BURKE", + "CABARRUS", + "CALDWELL", + "CAMDEN", + "CARTERET", + "CASWELL", + "CATAWBA", + "CHATHAM", + "CHEROKEE", + "CHOWAN", + "CLAY", + "CLEVELAND", + "COLUMBUS", + "CRAVEN", + "CUMBERLAND", + "CURRITUCK", + "DARE", + "DAVIDSON", + "DAVIE", + "DUPLIN", + "DURHAM", + "EDGECOMBE", + "FORSYTH", + "FRANKLIN", + "GASTON", + "GATES", + "GRAHAM", + "GRANVILLE", + "GREENE", + "GUILFORD", + "HALIFAX", + "HARNETT", + "HAYWOOD", + "HENDERSON", + "HERTFORD", + "HOKE", + "HYDE", + "IREDELL", + "JACKSON", + "JOHNSTON", + "JONES", + "LEE", + "LENOIR", + "LINCOLN", + "MACON", + "MADISON", + "MARTIN", + "MCDOWELL", + "MECKLENBURG", + "MITCHELL", + "MONTGOMERY", + "MOORE", + "NASH", + "NEW HANOVER", + "NORTHAMPTON", + "ONSLOW", + "ORANGE", + + "OTHER COUNTIES", + "PAMLICO", + "PASQUOTANK", + "PENDER", + "PERQUIMANS", + "PERSON", + "PITT", + "POLK", + "RANDOLPH", + "RICHMOND", + "ROBESON", + "ROCKINGHAM", + "ROWAN", + "RUTHERFORD", + "SAMPSON", + "SCOTLAND", + "STANLY", + "STOKES", + "SURRY", + "SWAIN", + "TRANSYLVANIA", + "TYRRELL", + "UNION", + "VANCE", + "WAKE", + "WARREN", + "WASHINGTON", + "WATAUGA", + "WAYNE", + "WILKES", + "WILSON", + "YADKIN", + "YANCEY" +], + "North Dakota": [ + "ADAMS", + "BARNES", + "BENSON", + "BILLINGS", + "BOTTINEAU", + "BOWMAN", + "BURKE", + "BURLEIGH", + "CASS", + "CAVALIER", + "DICKEY", + "DIVIDE", + "DUNN", + "EDDY", + "EMMONS", + "FOSTER", + "GOLDEN VALLEY", + "GRAND FORKS", + "GRANT", + "GRIGGS", + "HETTINGER", + "KIDDER", + "LA MOURE", + "LOGAN", + "MCHENRY", + "MCINTOSH", + "MCKENZIE", + "MCLEAN", + "MERCER", + "MORTON", + "MOUNTRAIL", + "NELSON", + "OLIVER", + + "OTHER COUNTIES", + "PEMBINA", + "PIERCE", + "RAMSEY", + "RANSOM", + "RENVILLE", + "RICHLAND", + "ROLETTE", + "SARGENT", + "SHERIDAN", + "SIOUX", + "SLOPE", + "STARK", + "STEELE", + "STUTSMAN", + "TOWNER", + "TRAILL", + "WALSH", + "WARD", + "WELLS", + "WILLIAMS" +], + "Ohio": [ + "ADAMS", + "ALLEN", + "ASHLAND", + "ASHTABULA", + "ATHENS", + "AUGLAIZE", + "BELMONT", + "BROWN", + "BUTLER", + "CARROLL", + "CHAMPAIGN", + "CLARK", + "CLERMONT", + "CLINTON", + "COLUMBIANA", + "COSHOCTON", + "CRAWFORD", + "CUYAHOGA", + "DARKE", + "DEFIANCE", + "DELAWARE", + "ERIE", + "FAIRFIELD", + "FAYETTE", + "FRANKLIN", + "FULTON", + "GALLIA", + "GEAUGA", + "GREENE", + "GUERNSEY", + "HAMILTON", + "HANCOCK", + "HARDIN", + "HARRISON", + "HENRY", + "HIGHLAND", + "HOCKING", + "HOLMES", + "HURON", + "JACKSON", + "JEFFERSON", + "KNOX", + "LAKE", + "LAWRENCE", + "LICKING", + "LOGAN", + "LORAIN", + "LUCAS", + "MADISON", + "MAHONING", + "MARION", + "MEDINA", + "MEIGS", + "MERCER", + "MIAMI", + "MONROE", + "MONTGOMERY", + "MORGAN", + "MORROW", + "MUSKINGUM", + "NOBLE", + + "OTHER COUNTIES", + "OTTAWA", + "PAULDING", + "PERRY", + "PICKAWAY", + "PIKE", + "PORTAGE", + "PREBLE", + "PUTNAM", + "RICHLAND", + "ROSS", + "SANDUSKY", + "SCIOTO", + "SENECA", + "SHELBY", + "STARK", + "SUMMIT", + "TRUMBULL", + "TUSCARAWAS", + "UNION", + "VAN WERT", + "VINTON", + "WARREN", + "WASHINGTON", + "WAYNE", + "WILLIAMS", + "WOOD", + "WYANDOT" +], + "Oklahoma": [ + "ADAIR", + "ALFALFA", + "ATOKA", + "BEAVER", + "BECKHAM", + "BLAINE", + "BRYAN", + "CADDO", + "CANADIAN", + "CARTER", + "CHEROKEE", + "CHOCTAW", + "CIMARRON", + "CLEVELAND", + "COAL", + "COMANCHE", + "COTTON", + "CRAIG", + "CREEK", + "CUSTER", + "DELAWARE", + "DEWEY", + "ELLIS", + "GARFIELD", + "GARVIN", + "GRADY", + "GRANT", + "GREER", + "HARMON", + "HARPER", + "HASKELL", + "HUGHES", + "JACKSON", + "JEFFERSON", + "JOHNSTON", + "KAY", + "KINGFISHER", + "KIOWA", + "LATIMER", + "LEFLORE", + "LINCOLN", + "LOGAN", + "LOVE", + "MAJOR", + "MARSHALL", + "MAYES", + "MCCLAIN", + "MCCURTAIN", + "MCINTOSH", + "MURRAY", + "MUSKOGEE", + "NOBLE", + "NOWATA", + "OKFUSKEE", + "OKLAHOMA", + "OKMULGEE", + "OSAGE", + + "OTHER COUNTIES", + "OTTAWA", + "PAWNEE", + "PAYNE", + "PITTSBURG", + "PONTOTOC", + "POTTAWATOMIE", + "PUSHMATAHA", + "ROGER MILLS", + "ROGERS", + "SEMINOLE", + "SEQUOYAH", + "STEPHENS", + "TEXAS", + "TILLMAN", + "TULSA", + "WAGONER", + "WASHINGTON", + "WASHITA", + "WOODS", + "WOODWARD" +], + "Oregon": [ + "BAKER", + "BENTON", + "CLACKAMAS", + "CLATSOP", + "COLUMBIA", + "COOS", + "CROOK", + "CURRY", + "DESCHUTES", + "DOUGLAS", + "GILLIAM", + "GRANT", + "HARNEY", + "HOOD RIVER", + "JACKSON", + "JEFFERSON", + "JOSEPHINE", + "KLAMATH", + "LAKE", + "LANE", + "LINCOLN", + "LINN", + "MALHEUR", + "MARION", + "MORROW", + "MULTNOMAH", + + "OTHER COUNTIES", + "POLK", + "SHERMAN", + "TILLAMOOK", + "UMATILLA", + "UNION", + "WALLOWA", + "WASCO", + "WASHINGTON", + "WHEELER", + "YAMHILL" +], + "Pennsylvania": [ + "ADAMS", + "ALLEGHENY", + "ARMSTRONG", + "BEAVER", + "BEDFORD", + "BERKS", + "BLAIR", + "BRADFORD", + "BUCKS", + "BUTLER", + "CAMBRIA", + "CAMERON", + "CARBON", + "CENTRE", + "CHESTER", + "CLARION", + "CLEARFIELD", + "CLINTON", + "COLUMBIA", + "CRAWFORD", + "CUMBERLAND", + "DAUPHIN", + "DELAWARE", + "ELK", + "ERIE", + "FAYETTE", + "FOREST", + "FRANKLIN", + "FULTON", + "GREENE", + "HUNTINGDON", + "INDIANA", + "JEFFERSON", + "JUNIATA", + "LACKAWANNA", + "LANCASTER", + "LAWRENCE", + "LEBANON", + "LEHIGH", + "LUZERNE", + "LYCOMING", + "MCKEAN", + "MERCER", + "MIFFLIN", + "MONROE", + "MONTGOMERY", + "MONTOUR", + "NORTHAMPTON", + "NORTHUMBERLAND", + + "OTHER COUNTIES", + "PERRY", + "PHILADELPHIA", + "PIKE", + "POTTER", + "SCHUYLKILL", + "SNYDER", + "SOMERSET", + "SULLIVAN", + "SUSQUEHANNA", + "TIOGA", + "UNION", + "VENANGO", + "WARREN", + "WASHINGTON", + "WAYNE", + "WESTMORELAND", + "WYOMING", + "YORK" +], + "Rhode Island": [ + "BRISTOL", + "KENT", + "NEWPORT", + "PROVIDENCE", + "WASHINGTON" +], + "South Carolina": [ + "ABBEVILLE", + "AIKEN", + "ALLENDALE", + "ANDERSON", + "BAMBERG", + "BARNWELL", + "BEAUFORT", + "BERKELEY", + "CALHOUN", + "CHARLESTON", + "CHEROKEE", + "CHESTER", + "CHESTERFIELD", + "CLARENDON", + "COLLETON", + "DARLINGTON", + "DILLON", + "DORCHESTER", + "EDGEFIELD", + "FAIRFIELD", + "FLORENCE", + "GEORGETOWN", + "GREENVILLE", + "GREENWOOD", + "HAMPTON", + "HORRY", + "JASPER", + "KERSHAW", + "LANCASTER", + "LAURENS", + "LEE", + "LEXINGTON", + "MARION", + "MARLBORO", + "MCCORMICK", + "NEWBERRY", + "OCONEE", + "ORANGEBURG", + + "OTHER COUNTIES", + "PICKENS", + "RICHLAND", + "SALUDA", + "SPARTANBURG", + "SUMTER", + "UNION", + "WILLIAMSBURG", + "YORK" +], + "South Dakota": [ + "AURORA", + "BEADLE", + "BENNETT", + "BON HOMME", + "BROOKINGS", + "BROWN", + "BRULE", + "BUFFALO", + "BUTTE", + "CAMPBELL", + "CHARLES MIX", + "CLARK", + "CLAY", + "CODINGTON", + "CORSON", + "CUSTER", + "DAVISON", + "DAY", + "DEUEL", + "DEWEY", + "DOUGLAS", + "EDMUNDS", + "FALL RIVER", + "FAULK", + "GRANT", + "GREGORY", + "HAAKON", + "HAMLIN", + "HAND", + "HANSON", + "HARDING", + "HUGHES", + "HUTCHINSON", + "HYDE", + "JACKSON", + "JERAULD", + "JONES", + "KINGSBURY", + "LAKE", + "LAWRENCE", + "LINCOLN", + "LYMAN", + "MARSHALL", + "MCCOOK", + "MCPHERSON", + "MEADE", + "MELLETTE", + "MINER", + "MINNEHAHA", + "MOODY", + "OGLALA LAKOTA", + + "OTHER COUNTIES", + "PENNINGTON", + "PERKINS", + "POTTER", + "ROBERTS", + "SANBORN", + "SPINK", + "STANLEY", + "SULLY", + "TODD", + "TRIPP", + "TURNER", + "UNION", + "WALWORTH", + "WASHABAUGH", + "WASHINGTON", + "YANKTON", + "ZIEBACH" +], + "Tennessee": [ + "ANDERSON", + "BEDFORD", + "BENTON", + "BLEDSOE", + "BLOUNT", + "BRADLEY", + "CAMPBELL", + "CANNON", + "CARROLL", + "CARTER", + "CHEATHAM", + "CHESTER", + "CLAIBORNE", + "CLAY", + "COCKE", + "COFFEE", + "CROCKETT", + "CUMBERLAND", + "DAVIDSON", + "DE KALB", + "DECATUR", + "DICKSON", + "DYER", + "FAYETTE", + "FENTRESS", + "FRANKLIN", + "GIBSON", + "GILES", + "GRAINGER", + "GREENE", + "GRUNDY", + "HAMBLEN", + "HAMILTON", + "HANCOCK", + "HARDEMAN", + "HARDIN", + "HAWKINS", + "HAYWOOD", + "HENDERSON", + "HENRY", + "HICKMAN", + "HOUSTON", + "HUMPHREYS", + "JACKSON", + "JEFFERSON", + "JOHNSON", + "KNOX", + "LAKE", + "LAUDERDALE", + "LAWRENCE", + "LEWIS", + "LINCOLN", + "LOUDON", + "MACON", + "MADISON", + "MARION", + "MARSHALL", + "MAURY", + "MCMINN", + "MCNAIRY", + "MEIGS", + "MONROE", + "MONTGOMERY", + "MOORE", + "MORGAN", + "OBION", + + "OTHER COUNTIES", + "OVERTON", + "PERRY", + "PICKETT", + "POLK", + "PUTNAM", + "RHEA", + "ROANE", + "ROBERTSON", + "RUTHERFORD", + "SCOTT", + "SEQUATCHIE", + "SEVIER", + "SHELBY", + "SMITH", + "STEWART", + "SULLIVAN", + "SUMNER", + "TIPTON", + "TROUSDALE", + "UNICOI", + "UNION", + "VAN BUREN", + "WARREN", + "WASHINGTON", + "WAYNE", + "WEAKLEY", + "WHITE", + "WILLIAMSON", + "WILSON" +], + "Texas": [ + "ANDERSON", + "ANDREWS", + "ANGELINA", + "ARANSAS", + "ARCHER", + "ARMSTRONG", + "ATASCOSA", + "AUSTIN", + "BAILEY", + "BANDERA", + "BASTROP", + "BAYLOR", + "BEE", + "BELL", + "BEXAR", + "BLANCO", + "BORDEN", + "BOSQUE", + "BOWIE", + "BRAZORIA", + "BRAZOS", + "BREWSTER", + "BRISCOE", + "BROOKS", + "BROWN", + "BURLESON", + "BURNET", + "CALDWELL", + "CALHOUN", + "CALLAHAN", + "CAMERON", + "CAMP", + "CARSON", + "CASS", + "CASTRO", + "CHAMBERS", + "CHEROKEE", + "CHILDRESS", + "CLAY", + "COCHRAN", + "COKE", + "COLEMAN", + "COLLIN", + "COLLINGSWORTH", + "COLORADO", + "COMAL", + "COMANCHE", + "CONCHO", + "COOKE", + "CORYELL", + "COTTLE", + "CRANE", + "CROCKETT", + "CROSBY", + "CULBERSON", + "DALLAM", + "DALLAS", + "DAWSON", + "DE WITT", + "DEAF SMITH", + "DELTA", + "DENTON", + "DICKENS", + "DIMMIT", + "DONLEY", + "DUVAL", + "EASTLAND", + "ECTOR", + "EDWARDS", + "EL PASO", + "ELLIS", + "ERATH", + "FALLS", + "FANNIN", + "FAYETTE", + "FISHER", + "FLOYD", + "FOARD", + "FORT BEND", + "FRANKLIN", + "FREESTONE", + "FRIO", + "GAINES", + "GALVESTON", + "GARZA", + "GILLESPIE", + "GLASSCOCK", + "GOLIAD", + "GONZALES", + "GRAY", + "GRAYSON", + "GREGG", + "GRIMES", + "GUADALUPE", + "HALE", + "HALL", + "HAMILTON", + "HANSFORD", + "HARDEMAN", + "HARDIN", + "HARRIS", + "HARRISON", + "HARTLEY", + "HASKELL", + "HAYS", + "HEMPHILL", + "HENDERSON", + "HIDALGO", + "HILL", + "HOCKLEY", + "HOOD", + "HOPKINS", + "HOUSTON", + "HOWARD", + "HUDSPETH", + "HUNT", + "HUTCHINSON", + "IRION", + "JACK", + "JACKSON", + "JASPER", + "JEFF DAVIS", + "JEFFERSON", + "JIM HOGG", + "JIM WELLS", + "JOHNSON", + "JONES", + "KARNES", + "KAUFMAN", + "KENDALL", + "KENEDY", + "KENT", + "KERR", + "KIMBLE", + "KING", + "KINNEY", + "KLEBERG", + "KNOX", + "LA SALLE", + "LAMAR", + "LAMB", + "LAMPASAS", + "LAVACA", + "LEE", + "LEON", + "LIBERTY", + "LIMESTONE", + "LIPSCOMB", + "LIVE OAK", + "LLANO", + "LOVING", + "LUBBOCK", + "LYNN", + "MADISON", + "MARION", + "MARTIN", + "MASON", + "MATAGORDA", + "MAVERICK", + "MCCULLOCH", + "MCLENNAN", + "MCMULLEN", + "MEDINA", + "MENARD", + "MIDLAND", + "MILAM", + "MILLS", + "MITCHELL", + "MONTAGUE", + "MONTGOMERY", + "MOORE", + "MORRIS", + "MOTLEY", + "NACOGDOCHES", + "NAVARRO", + "NEWTON", + "NOLAN", + "NUECES", + "OCHILTREE", + "OLDHAM", + "ORANGE", + + "OTHER COUNTIES", + "PALO PINTO", + "PANOLA", + "PARKER", + "PARMER", + "PECOS", + "POLK", + "POTTER", + "PRESIDIO", + "RAINS", + "RANDALL", + "REAGAN", + "REAL", + "RED RIVER", + "REEVES", + "REFUGIO", + "ROBERTS", + "ROBERTSON", + "ROCKWALL", + "RUNNELS", + "RUSK", + "SABINE", + "SAN AUGUSTINE", + "SAN JACINTO", + "SAN PATRICIO", + "SAN SABA", + "SCHLEICHER", + "SCURRY", + "SHACKELFORD", + "SHELBY", + "SHERMAN", + "SMITH", + "SOMERVELL", + "STARR", + "STEPHENS", + "STERLING", + "STONEWALL", + "SUTTON", + "SWISHER", + "TARRANT", + "TAYLOR", + "TERRELL", + "TERRY", + "THROCKMORTON", + "TITUS", + "TOM GREEN", + "TRAVIS", + "TRINITY", + "TYLER", + "UPSHUR", + "UPTON", + "UVALDE", + "VAL VERDE", + "VAN ZANDT", + "VICTORIA", + "WALKER", + "WALLER", + "WARD", + "WASHINGTON", + "WEBB", + "WHARTON", + "WHEELER", + "WICHITA", + "WILBARGER", + "WILLACY", + "WILLIAMSON", + "WILSON", + "WINKLER", + "WISE", + "WOOD", + "YOAKUM", + "YOUNG", + "ZAPATA", + "ZAVALA" +], + "Utah": [ + "BEAVER", + "BOX ELDER", + "CACHE", + "CARBON", + "DAGGETT", + "DAVIS", + "DUCHESNE", + "EMERY", + "GARFIELD", + "GRAND", + "IRON", + "JUAB", + "KANE", + "MILLARD", + "MORGAN", + + "OTHER COUNTIES", + "PIUTE", + "RICH", + "SALT LAKE", + "SAN JUAN", + "SANPETE", + "SEVIER", + "SUMMIT", + "TOOELE", + "UINTAH", + "UTAH", + "WASATCH", + "WASHINGTON", + "WAYNE", + "WEBER" +], + "Vermont": [ + "ADDISON", + "BENNINGTON", + "CALEDONIA", + "CHITTENDEN", + "ESSEX", + "FRANKLIN", + "GRAND ISLE", + "LAMOILLE", + "ORANGE", + "ORLEANS", + + "OTHER COUNTIES", + "RUTLAND", + "WASHINGTON", + "WINDHAM", + "WINDSOR" +], + "Virginia": [ + "ACCOMACK", + "ALBEMARLE", + "ALLEGHANY", + "AMELIA", + "AMHERST", + "APPOMATTOX", + "ARLINGTON", + "AUGUSTA", + "BATH", + "BEDFORD", + "BLAND", + "BOTETOURT", + "BRUNSWICK", + "BUCHANAN", + "BUCKINGHAM", + "CAMPBELL", + "CAROLINE", + "CARROLL", + "CHARLES CITY", + "CHARLOTTE", + "CHESAPEAKE CITY", + "CHESTERFIELD", + "CLARKE", + "CRAIG", + "CULPEPER", + "CUMBERLAND", + "DICKENSON", + "DINWIDDIE", + "ESSEX", + "FAIRFAX", + "FAUQUIER", + "FLOYD", + "FLUVANNA", + "FRANKLIN", + "FREDERICK", + "GILES", + "GLOUCESTER", + "GOOCHLAND", + "GRAYSON", + "GREENE", + "GREENSVILLE", + "HALIFAX", + "HAMPTON CITY", + "HANOVER", + "HENRICO", + "HENRY", + "HIGHLAND", + "ISLE OF WIGHT", + "JAMES CITY", + "KING AND QUEEN", + "KING GEORGE", + "KING WILLIAM", + "LANCASTER", + "LEE", + "LOUDOUN", + "LOUISA", + "LUNENBURG", + "MADISON", + "MATHEWS", + "MECKLENBURG", + "MIDDLESEX", + "MONTGOMERY", + "NANSEMOND", + "NELSON", + "NEW KENT", + "NEWPORT NEWS CITY", + "NORTHAMPTON", + "NORTHUMBERLAND", + "NOTTOWAY", + "ORANGE", + + "OTHER COUNTIES", + "PAGE", + "PATRICK", + "PITTSYLVANIA", + "POWHATAN", + "PRINCE EDWARD", + "PRINCE GEORGE", + "PRINCE WILLIAM", + "PULASKI", + "RAPPAHANNOCK", + "RICHMOND", + "ROANOKE", + "ROCKBRIDGE", + "ROCKINGHAM", + "RUSSELL", + "SCOTT", + "SHENANDOAH", + "SMYTH", + "SOUTHAMPTON", + "SPOTSYLVANIA", + "STAFFORD", + "SUFFOLK CITY", + "SURRY", + "SUSSEX", + "TAZEWELL", + "VIRGINIA BEACH CITY", + "WARREN", + "WASHINGTON", + "WESTMORELAND", + "WISE", + "WYTHE", + "YORK" +], + "Washington": [ + "ADAMS", + "ASOTIN", + "BENTON", + "CHELAN", + "CLALLAM", + "CLARK", + "COLUMBIA", + "COWLITZ", + "DOUGLAS", + "FERRY", + "FRANKLIN", + "GARFIELD", + "GRANT", + "GRAYS HARBOR", + "ISLAND", + "JEFFERSON", + "KING", + "KITSAP", + "KITTITAS", + "KLICKITAT", + "LEWIS", + "LINCOLN", + "MASON", + "OKANOGAN", + + "OTHER COUNTIES", + "PACIFIC", + "PEND OREILLE", + "PIERCE", + "SAN JUAN", + "SKAGIT", + "SKAMANIA", + "SNOHOMISH", + "SPOKANE", + "STEVENS", + "THURSTON", + "WAHKIAKUM", + "WALLA WALLA", + "WHATCOM", + "WHITMAN", + "YAKIMA" +], + "West Virginia": [ + "BARBOUR", + "BERKELEY", + "BOONE", + "BRAXTON", + "BROOKE", + "CABELL", + "CALHOUN", + "CLAY", + "DODDRIDGE", + "FAYETTE", + "GILMER", + "GRANT", + "GREENBRIER", + "HAMPSHIRE", + "HANCOCK", + "HARDY", + "HARRISON", + "JACKSON", + "JEFFERSON", + "KANAWHA", + "LEWIS", + "LINCOLN", + "LOGAN", + "MARION", + "MARSHALL", + "MASON", + "MCDOWELL", + "MERCER", + "MINERAL", + "MINGO", + "MONONGALIA", + "MONROE", + "MORGAN", + "NICHOLAS", + "OHIO", + + "OTHER COUNTIES", + "PENDLETON", + "PLEASANTS", + "POCAHONTAS", + "PRESTON", + "PUTNAM", + "RALEIGH", + "RANDOLPH", + "RITCHIE", + "ROANE", + "SUMMERS", + "TAYLOR", + "TUCKER", + "TYLER", + "UPSHUR", + "WAYNE", + "WEBSTER", + "WETZEL", + "WIRT", + "WOOD", + "WYOMING" +], + "Wisconsin": [ + "ADAMS", + "ASHLAND", + "BARRON", + "BAYFIELD", + "BROWN", + "BUFFALO", + "BURNETT", + "CALUMET", + "CHIPPEWA", + "CLARK", + "COLUMBIA", + "CRAWFORD", + "DANE", + "DODGE", + "DOOR", + "DOUGLAS", + "DUNN", + "EAU CLAIRE", + "FLORENCE", + "FOND DU LAC", + "FOREST", + "GRANT", + "GREEN", + "GREEN LAKE", + "IOWA", + "IRON", + "JACKSON", + "JEFFERSON", + "JUNEAU", + "KENOSHA", + "KEWAUNEE", + "LA CROSSE", + "LAFAYETTE", + "LANGLADE", + "LINCOLN", + "MANITOWOC", + "MARATHON", + "MARINETTE", + "MARQUETTE", + "MENOMINEE", + "MILWAUKEE", + "MONROE", + "OCONTO", + "ONEIDA", + + "OTHER COUNTIES", + "OUTAGAMIE", + "OZAUKEE", + "PEPIN", + "PIERCE", + "POLK", + "PORTAGE", + "PRICE", + "RACINE", + "RICHLAND", + "ROCK", + "RUSK", + "SAUK", + "SAWYER", + "SHAWANO", + "SHEBOYGAN", + "ST CROIX", + "TAYLOR", + "TREMPEALEAU", + "VERNON", + "VILAS", + "WALWORTH", + "WASHBURN", + "WASHINGTON", + "WAUKESHA", + "WAUPACA", + "WAUSHARA", + "WINNEBAGO", + "WOOD" +], + "Wyoming": [ + "ALBANY", + "BIG HORN", + "CAMPBELL", + "CARBON", + "CONVERSE", + "CROOK", + "FREMONT", + "GOSHEN", + "HOT SPRINGS", + "JOHNSON", + "LARAMIE", + "LINCOLN", + "NATRONA", + "NIOBRARA", + + "OTHER COUNTIES", + "PARK", + "PLATTE", + "SHERIDAN", + "SUBLETTE", + "SWEETWATER", + "TETON", + "UINTA", + "WASHAKIE", + "WESTON" +] +} \ No newline at end of file diff --git a/src/constants/query-headers.ts b/src/constants/query-headers.ts new file mode 100644 index 0000000..dcac5f7 --- /dev/null +++ b/src/constants/query-headers.ts @@ -0,0 +1,352 @@ +import { fiftyStates } from "./constants"; +import { IQueryHeaders } from "./types"; + +const allYears = []; +for (let year = 2022; year >= 1910; year--) { + allYears.push(`${year}`); +} + + +const sharedDemographicHeaders = { + sect_desc: "Demographics", + group_desc: "Producers", + commodity_desc: "Producers", + statisticcat_desc: "Producers", + domain_desc: "Total", + geographicAreas: ["State", "County"] +}; + +const sharedEconomicHeaders = { + sect_desc: "Economics", + group_desc: "Farms & Land & Assets", + commodity_desc: "Farm Operations", + statisticcat_desc: "Operations" +}; + +const sharedLaborHeaders = { + sect_desc: "Economics", + group_desc: "Expenses", + commodity_desc: "Labor", +}; + +const sharedCropHeaders = { + sect_desc: "Crops", + statisticcat_desc: { + ["Area Harvested"]: "Area Harvested", + ["Yield"]: "Yield" + }, + domain_desc: "Total", + geographicAreas: ["State", "County"], + years: { + "County": allYears, + "State": allYears + } +} + +export const queryData: Array = [ + { + plugInAttribute: "Total Farmers", + ...sharedDemographicHeaders, + short_desc: ["PRODUCERS, (ALL) - NUMBER OF PRODUCERS"], + years: { + "County": ["2017"], + "State": ["2017"] + } + }, + { + plugInAttribute: "Age", + ...sharedDemographicHeaders, + short_desc: [ + "PRODUCERS, AGE LT 25 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE 25 TO 34 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE 35 TO 44 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE 45 TO 54 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE 55 TO 64 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE 65 TO 74 - NUMBER OF PRODUCERS", + "PRODUCERS, AGE GE 75 - NUMBER OF PRODUCERS" + ], + years: { + "County": ["2017"], + "State": ["2017"] + } + + }, + { + plugInAttribute: "Gender", + ...sharedDemographicHeaders, + short_desc: [ + "PRODUCERS, (ALL), FEMALE - NUMBER OF PRODUCERS", + "PRODUCERS, (ALL), MALE - NUMBER OF PRODUCERS" + ], + years: { + "County": ["2017"], + "State": ["2017"] + } + }, + { + plugInAttribute: "Race", + ...sharedDemographicHeaders, + short_desc: [ + "PRODUCERS, AMERICAN INDIAN OR ALASKAN NATIVE - NUMBER OF PRODUCERS", + "PRODUCERS, ASIAN - NUMBER OF PRODUCERS", + "PRODUCERS, BLACK OR AFRICAN AMERICAN - NUMBER OF PRODUCERS", + "PRODUCERS, HISPANIC - NUMBER OF PRODUCERS", + "PRODUCERS, MULTI-RACE - NUMBER OF PRODUCERS", + "PRODUCERS, NATIVE HAWAIIAN OR OTHER PACIFIC ISLANDERS - NUMBER OF PRODUCERS", + "PRODUCERS, WHITE - NUMBER OF PRODUCERS" + ], + years: { + "County": ["2017"], + "State": ["2017"] + } + }, + { + plugInAttribute: "Total Farms", + ...sharedEconomicHeaders, + short_desc: ["FARM OPERATIONS - NUMBER OF OPERATIONS"], + domain_desc: "Total", + geographicAreas: ["State", "County"], + years: { + "County": allYears, + "State": allYears + } + }, + { + plugInAttribute: "Organization Type", + sect_desc: "Demographics", + group_desc: "Farms & Land & Assets", + commodity_desc: "Farm Operations", + statisticcat_desc: "Operations", + short_desc: [ + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, CORPORATION (EXCL FAMILY HELD) - NUMBER OF OPERATIONS", + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, CORPORATION, FAMILY HELD - NUMBER OF OPERATIONS", + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, FAMILY & INDIVIDUAL - NUMBER OF OPERATIONS", + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, INSTITUTIONAL & RESEARCH & RESERVATION & OTHER - NUMBER OF OPERATIONS", + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, PARTNERSHIP - NUMBER OF OPERATIONS" + ], + domain_desc: "Total", + geographicAreas: ["County"], + years: { + "County": ["1997", "2002", "2007", "2012", "2017"], + "State": [] + } + }, + { + plugInAttribute: "Economic Class", + ...sharedEconomicHeaders, + short_desc: ["FARM OPERATIONS - NUMBER OF OPERATIONS"], + domain_desc: "Economic Class", + geographicAreas: ["State"], + years: { + "County": allYears.filter(y => Number(y) >= 1987), + "State": allYears.filter(y => Number(y) >= 1987) + } + }, + { + plugInAttribute: "Acres Operated", + sect_desc: "Economics", + group_desc: "Farms & Land & Assets", + commodity_desc: "Farm Operations", + statisticcat_desc: "Area Operated", + short_desc: ["FARM OPERATIONS - ACRES OPERATED"], + domain_desc: "Area Operated", + geographicAreas: ["State", "County"], + years: { + "County": ["1997", "2002", "2007", "2012", "2017"], + "State": ["1997", "2002", "2007", "2012", "2017"] + } + }, + { + plugInAttribute: "Organic", + ...sharedEconomicHeaders, + short_desc: ["FARM OPERATIONS, ORGANIC - NUMBER OF OPERATIONS"], + domain_desc: "Organic Status", + geographicAreas: ["State", "County"], + years: { + "County": ["2008", "2011", "2012", "2014", "2015", "2016", "2017", "2019", "2021"], + "State": ["2008", "2011", "2012", "2014", "2015", "2016", "2017", "2019", "2021"] + } + }, + { + plugInAttribute: "Labor Status", + ...sharedLaborHeaders, + statisticcat_desc: "Workers", + short_desc: [ + "LABOR, MIGRANT - NUMBER OF WORKERS", + "LABOR, UNPAID - NUMBER OF WORKERS", + "LABOR, HIRED - NUMBER OF WORKERS" + ], + domain_desc: "Total", + geographicAreas: ["State", "County"], + years: { + "County": ["2012", "2017"], + "State": ["2012", "2017"] + } + }, + { + plugInAttribute: "Wages", + ...sharedLaborHeaders, + statisticcat_desc: "Wage Rate", + short_desc: ["LABOR, HIRED - WAGE RATE, MEASURED IN $ / HOUR"], + domain_desc: "Total", + geographicAreas: ["REGION : MULTI-STATE"], + years: { + "County": allYears.filter(y => Number(y) >= 1989), + "State": allYears.filter(y => Number(y) >= 1989) + } + }, + { + plugInAttribute: "Time Worked", + ...sharedLaborHeaders, + statisticcat_desc: "Wage Rate", + short_desc: ["LABOR, HIRED - TIME WORKED, MEASURED IN HOURS/WEEK"], + domain_desc: "Total", + geographicAreas: ["REGION : MULTI-STATE"], + years: { + "County": allYears.filter(y => Number(y) >= 1989), + "State": allYears.filter(y => Number(y) >= 1989) + } + }, + { + plugInAttribute: "Corn", + group_desc: "Field Crops", + commodity_desc: "Corn", + short_desc: { + ["Area Harvested"]: ["CORN, GRAIN - ACRES HARVESTED"], + ["Yield"]: ["CORN, GRAIN - YIELD, MEASURED IN BU / ACRE"] + }, + ...sharedCropHeaders + }, + { + plugInAttribute: "Cotton", + group_desc: "Field Crops", + commodity_desc: "Cotton", + short_desc: { + ["Area Harvested"]: ["COTTON - ACRES HARVESTED"], + ["Yield"]: ["COTTON - YIELD, MEASURED IN LB / ACRE"] + }, + ...sharedCropHeaders + }, + { + plugInAttribute: "Grapes", + group_desc: "Fruit & Tree Nuts", + commodity_desc: "Grapes", + short_desc: { + ["Area Harvested"]: ["GRAPES, ORGANIC - ACRES HARVESTED"], + ["Yield"]: ["GRAPES - YIELD, MEASURED IN TONS / ACRE"] + }, + ...sharedCropHeaders + }, + { + plugInAttribute: "Oats", + group_desc: "Field Crops", + commodity_desc: "Oats", + short_desc: { + ["Area Harvested"]: ["Oats - Acres Harvested"], + ["Yield"]: ["Oats - Yield, measured in BU / acre"] + }, + ...sharedCropHeaders + }, + { + plugInAttribute: "Soybeans", + group_desc: "Field Crops", + commodity_desc: "Soybeans", + short_desc: { + ["Area Harvested"]: ["Soybeans - Acres Harvested"], + ["Yield"]: ["Soybeans - Yield, measured in BU / acre"] + }, + ...sharedCropHeaders + }, + { + plugInAttribute: "Wheat", + group_desc: "Field Crops", + commodity_desc: "Wheat", + + short_desc: { + ["Area Harvested"]: ["Wheat - Acres Harvested"], + ["Yield"]: ["Wheat - Yield, measured in BU / acre"] + }, + ...sharedCropHeaders + } +]; + +interface IRegion { + "Region": string + "States": string[] +} + + +export const multiRegions: IRegion[] = [ + { + "Region": "Pacific", + "States": ["Washington", "Oregon"] + }, + { + "Region": "Mountain I", + "States": ["Montana", "Idaho", "Wyoming"] + }, + { + "Region": "Mountain II", + "States": ["Nevada", "Utah", "Colorado"] + }, + { + "Region": "Mountain III", + "States": ["Arizona", "New Mexico"] + }, + { + "Region": "Northern Plains", + "States": ["North Dakota", "South Dakota", "Kansas", "Nebraska"] + }, + { + "Region": "Southern Plains", + "States": ["Oklahoma", "Texas"] + }, + { + "Region": "Lake", + "States": ["Minnesota", "Wisconsin", "Michigan"] + }, + { + "Region": "Cornbelt I", + "States": ["Illinois", "Indiana", "Ohio"] + }, + { + "Region": "Cornbelt II", + "States": ["Iowa", "Missouri"] + }, + { + "Region": "Delta", + "States": ["Mississippi", "Louisiana", "Arkansas"] + }, + { + "Region": "Appalachian I", + "States": ["Virginia", "North Carolina"] + }, + { + "Region": "Appalachian II", + "States": ["West Virginia", "Kentucky", "Tennessee"] + }, + { + "Region": "Southeast", + "States": ["South Carolina", "Alabama", "Georgia"] + }, + { + "Region": "Northeast I", + "States": ["Maine", "New Hampshire", "Vermont", "Massachusetts", "Connecticut", "Rhode Island", "New York"] + }, + { + "Region": "Northeast II", + "States": ["Pennsylvania", "New Jersey", "Delaware", "Maryland"] + }, + { + "Region": "California", + "States": ["California"] + }, + { + "Region": "Florida", + "States": ["Florida"] + }, + { + "Region": "Hawaii", + "States": ["Hawaii"] + } +]; diff --git a/src/components/types.ts b/src/constants/types.ts similarity index 68% rename from src/components/types.ts rename to src/constants/types.ts index fc058bd..1c3b2f5 100644 --- a/src/components/types.ts +++ b/src/constants/types.ts @@ -1,6 +1,10 @@ +import { fiftyStates } from "./constants"; + + + export interface IStateOptions { - geographicLevel: string, - states: string[] + geographicLevel: "County"|"State", + states: string[], farmerDemographics: string[], farmDemographics: string[], economicsAndWages: string[], @@ -59,3 +63,28 @@ export interface IResData { year: number; zip_5: string; }; + +export interface ICropCategory { + ["Area Harvested"]: string, + ["Yield"]: string +}; + +export interface ICropDataItem { + ["Area Harvested"]: string[], + ["Yield"]: string[] +}; + +export interface IQueryHeaders { + plugInAttribute: string, + sect_desc: string, + group_desc: string, + commodity_desc: string, + statisticcat_desc: string|ICropCategory, + short_desc: string[]|ICropDataItem, + domain_desc: string, + geographicAreas: string[], + years: { + "County": string[] + "State": string[] + } +}; diff --git a/src/scripts/api.ts b/src/scripts/api.ts index b44fbfa..5ec688a 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -1,8 +1,10 @@ import fetchJsonp from "fetch-jsonp"; -import { ICropCategory, ICropDataItem, queryData } from "./query-headers"; -import { IStateOptions } from "../components/types"; +import { multiRegions, queryData } from "../constants/query-headers"; +import { ICropCategory, ICropDataItem, IStateOptions } from "../constants/types"; import { connect } from "./connect"; -import { cropOptions } from "../components/constants"; +import { cropOptions } from "../constants/constants"; +import { countyData } from "../constants/counties"; +import { getQueryParams } from "./utils"; const baseURL = `https://quickstats.nass.usda.gov/api/api_GET/?key=9ED0BFB8-8DDD-3609-9940-A2341ED6A9E3`; @@ -11,19 +13,32 @@ interface IRequestParams { geographicLevel: string, location: string, year: string, - cropCategory?: keyof ICropDataItem + cropCategory?: keyof ICropDataItem, + state?: string } interface IGetAttrDataParams { attribute: string, geographicLevel: string, cropUnits: string, - state: string, + location: string, year: string + state?: string } -export const createRequest = ({attribute, geographicLevel, location, year, cropCategory}: IRequestParams) => { - const queryParams = queryData.find((d) => d.plugInAttribute === attribute); +export const fetchData = async (req: string) => { + try { + const response = await fetchJsonp(req, {timeout: 10000}); + const json = await response.json(); + return json; + } catch (error) { + console.log("parsing failed", error); + throw error; + } +}; + +export const createRequest = ({attribute, geographicLevel, location, year, cropCategory, state}: IRequestParams) => { + const queryParams = getQueryParams(attribute); if (!queryParams) { throw new Error("Invalid attribute"); @@ -45,17 +60,25 @@ export const createRequest = ({attribute, geographicLevel, location, year, cropC (queryParams?.statisticcat_desc as ICropCategory)[cropCategory] : statisticcat_desc; + const locationHeader = geographicLevel === "REGION : MULTI-STATE" ? "region_desc" : geographicLevel === "County" ? "county_name" : "state_name"; + const baseReq = `${baseURL}` + `§_desc=${encodeURIComponent(sect_desc)}` + `&group_desc=${encodeURIComponent(group_desc)}` + `&commodity_desc=${encodeURIComponent(commodity_desc)}` + `&statisticcat_desc=${encodeURIComponent(cat as string)}` + `&domain_desc=${encodeURIComponent(domain_desc)}` + - `&agg_level_desc=${geographicLevel}` + - `&state_name=${location}` + + `&agg_level_desc=${encodeURIComponent(geographicLevel)}` + + `&${locationHeader}=${encodeURIComponent(location)}` + `&year=${year}`; let req = baseReq; + + // if we are creating a request at the county level, we need to also pass in a state + if (state) { + req += `&state_name=${state}` + } + item.forEach(subItem => { req = req + `&short_desc=${encodeURIComponent(subItem)}`; }); @@ -65,14 +88,13 @@ export const createRequest = ({attribute, geographicLevel, location, year, cropC export const createTableFromSelections = async (selectedOptions: IStateOptions) => { const {geographicLevel, states, cropUnits, years, ...subOptions} = selectedOptions; await connect.getNewDataContext(); - await connect.createTopCollection(); + await connect.createTopCollection(geographicLevel); - // 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 queryParams = getQueryParams(attribute); if (!queryParams) { throw new Error("Invalid attribute"); } @@ -84,24 +106,35 @@ export const createTableFromSelections = async (selectedOptions: IStateOptions) } } } - await connect.createSubCollection(allAttrs); + await connect.createSubCollection(geographicLevel, allAttrs); const items = await getItems(selectedOptions); await connect.createItems(items); await connect.makeCaseTableAppear(); + // await connect.createItems(items); }; const getItems = async (selectedOptions: IStateOptions) => { const {states, years} = selectedOptions; const multipleStatesSelected = states.length > 1 || states[0] === "All States"; const multipleYearsSelected = years.length > 1; + const countySelected = selectedOptions.geographicLevel === "County"; const items = []; + if (multipleStatesSelected) { for (const state of states) { if (multipleYearsSelected) { for (const year of years) { - const item = await getDataForSingleYearAndState(selectedOptions, state, year); - items.push(item); + if (countySelected) { + const allCounties = countyData[state]; + for (const county of allCounties) { + const item = await getDataForSingleYearAndState(selectedOptions, county, year, state); + items.push(item); + } + } else { + const item = await getDataForSingleYearAndState(selectedOptions, state, year); + items.push(item); + } } } else { const item = await getDataForSingleYearAndState(selectedOptions, state, years[0]); @@ -109,18 +142,25 @@ const getItems = async (selectedOptions: IStateOptions) => { } } } else { - const item = await getDataForSingleYearAndState(selectedOptions, states[0], years[0]); - items.push(item); + if (countySelected) { + const allCounties = countyData[states[0]]; + for (const county of allCounties) { + const item = await getDataForSingleYearAndState(selectedOptions, county, years[0], states[0]); + items.push(item); + } + } else { + const item = await getDataForSingleYearAndState(selectedOptions, states[0], years[0]); + items.push(item); + } } - return items; }; -const getDataForSingleYearAndState = async (selectedOptions: IStateOptions, state: string, year: string) => { +const getDataForSingleYearAndState = async (selectedOptions: IStateOptions, countyOrState: string, year: string, state?: string) => { const {geographicLevel, states, years, cropUnits, ...subOptions} = selectedOptions; let item: any = { - "State": state, + [geographicLevel]: countyOrState, "Year": year, }; @@ -128,8 +168,23 @@ const getDataForSingleYearAndState = async (selectedOptions: IStateOptions, stat 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}; + const queryParams = getQueryParams(attribute); + const yearAvailable = queryParams?.years[geographicLevel].includes(year); + const isMultiStateRegion = queryParams?.geographicAreas[0] === "REGION : MULTI-STATE"; + const geoLevel = isMultiStateRegion ? "REGION : MULTI-STATE" : geographicLevel; + if (yearAvailable) { + let location = countyOrState; + if (isMultiStateRegion) { + const itemToCheck = state ? state : countyOrState; + location = multiRegions.find((region) => region.States.includes(itemToCheck))!.Region; + } + const params: IGetAttrDataParams = {attribute, geographicLevel: geoLevel, location, year, cropUnits}; + if (geoLevel === "County") { + params.state = state; + } + const attrData = await getAttrData(params); + item = {...item, ...attrData}; + } } } } @@ -138,8 +193,8 @@ const getDataForSingleYearAndState = async (selectedOptions: IStateOptions, stat }; const getAttrData = async (params: IGetAttrDataParams) => { - const {attribute, geographicLevel, state, year, cropUnits} = params; - const reqParams: IRequestParams = {attribute, geographicLevel, location: state, year}; + const {attribute, geographicLevel, location, year, cropUnits, state} = params; + const reqParams: IRequestParams = {attribute, geographicLevel, location, year, state}; if (cropOptions.options.includes(attribute) && cropUnits) { reqParams.cropCategory = cropUnits as keyof ICropDataItem; } @@ -148,22 +203,11 @@ const getAttrData = async (params: IGetAttrDataParams) => { const values: any = {}; if (res) { const {data} = res; - data.forEach((dataItem: any) => { - values[dataItem.short_desc] = dataItem.Value; + data.map((dataItem: any) => { + return values[dataItem.short_desc] = dataItem.Value; }); } else { console.log("error"); } return values; }; - -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; - } -}; diff --git a/src/scripts/connect.js b/src/scripts/connect.js index 0b24088..8676f21 100644 --- a/src/scripts/connect.js +++ b/src/scripts/connect.js @@ -48,19 +48,20 @@ export const connect = { return res; }, - createTopCollection: async function() { + createTopCollection: async function(geoLevel) { + const plural = geoLevel === "State" ? "States" : "Counties"; const message = { "action": "create", "resource": `dataContext[${dataSetName}].collection`, "values": { - "name": "States", + "name": plural, "parent": "_root_", "attributes": [{ - "name": "State", + "name": geoLevel, }, { "name": "Boundary", - "formula": "lookupBoundary(US_state_boundaries, State)", + "formula": `lookupBoundary(US_${geoLevel.toLowerCase()}_boundaries, ${geoLevel})`, "formulaDependents": "State" }] } @@ -68,13 +69,14 @@ export const connect = { await codapInterface.sendRequest(message); }, - createSubCollection: async function(attrs) { + createSubCollection: async function(geoLevel, attrs) { + const plural = geoLevel === "State" ? "States" : "Counties"; const message = { "action": "create", "resource": `dataContext[${dataSetName}].collection`, "values": { "name": "Data", - "parent": "States", + "parent": plural, "attributes": attrs.map((attr) => this.makeCODAPAttributeDef(attr)) } }; diff --git a/src/scripts/query-headers.ts b/src/scripts/query-headers.ts deleted file mode 100644 index 32110af..0000000 --- a/src/scripts/query-headers.ts +++ /dev/null @@ -1,356 +0,0 @@ -const areaHarvested = "Area Harvested"; -const yieldInBU = "Yield"; - -export interface ICropCategory { - [areaHarvested]: string, - [yieldInBU]: string -} -export interface ICropDataItem { - [areaHarvested]: string[], - [yieldInBU]: string[] -} - -export interface IQueryHeaders { - plugInAttribute: string, - numberOfAttributeColumnsInCodap: number|string, - sect_desc: string, - group_desc: string, - commodity_desc: string, - statisticcat_desc: string|ICropCategory, - short_desc: string[]|ICropDataItem, - domain_desc: string, - geographicLevels?: string, - years: { - county: string[] - state: string[] - } -} - -const sharedDemographicHeaders = { -sect_desc: "Demographics", -group_desc: "Producers", -commodity_desc: "Producers", -statisticcat_desc: "Producers", -domain_desc: "Total", -}; - -const sharedEconomicHeaders = { -sect_desc: "Economics", -group_desc: "Farms & Land & Assets", -commodity_desc: "Farm Operations", -statisticcat_desc: "Operations" -}; - -const sharedLaborHeaders = { -sect_desc: "Economics", -group_desc: "Expenses", -commodity_desc: "Labor", -}; - -const allYears = []; -for (let year = 2022; year >= 1910; year--) { - allYears.push(`${year}`); -} - -export const queryData: Array = [ -{ - plugInAttribute: "Total Farmers", - numberOfAttributeColumnsInCodap: 1, - ...sharedDemographicHeaders, - short_desc: ["PRODUCERS, (ALL) - NUMBER OF PRODUCERS"], - years: { - county: ["2017"], - state: ["2017"] - } -}, -{ - plugInAttribute: "Age", - numberOfAttributeColumnsInCodap: 7, - ...sharedDemographicHeaders, - short_desc: [ - "PRODUCERS, AGE LT 25 - NUMBER OF PRODUCERS", - "PRODUCERS, AGE 25 TO 34 - NUMBER OF PRODUCERS", - "PRODUCERS, AGE 35 TO 44 - NUMBER OF PRODUCERS", - "PRODUCERS, AGE 45 TO 54 - NUMBER OF PRODUCERS", - "PRODUCERS, AGE 55 TO 64 - NUMBER OF PRODUCERS", - "PRODUCERS, AGE 65 TO 74 - NUMBER OF PRODUCERS", - "PRODUCERS, AGE GE 75 - NUMBER OF PRODUCERS" - ], - years: { - county: ["2017"], - state: ["2017"] - } - -}, -{ - plugInAttribute: "Gender", - numberOfAttributeColumnsInCodap: 2, - ...sharedDemographicHeaders, - short_desc: [ - "PRODUCERS, (ALL), FEMALE - NUMBER OF PRODUCERS", - "PRODUCERS, (ALL), MALE - NUMBER OF PRODUCERS" - ], - years: { - county: ["2017"], - state: ["2017"] - } -}, -{ - plugInAttribute: "Race", - numberOfAttributeColumnsInCodap: 7, - ...sharedDemographicHeaders, - short_desc: [ - "PRODUCERS, AMERICAN INDIAN OR ALASKAN NATIVE - NUMBER OF PRODUCERS", - "PRODUCERS, ASIAN - NUMBER OF PRODUCERS", - "PRODUCERS, BLACK OR AFRICAN AMERICAN - NUMBER OF PRODUCERS", - "PRODUCERS, HISPANIC - NUMBER OF PRODUCERS", - "PRODUCERS, MULTI-RACE - NUMBER OF PRODUCERS", - "PRODUCERS, NATIVE HAWAIIAN OR OTHER PACIFIC ISLANDERS - NUMBER OF PRODUCERS", - "PRODUCERS, WHITE - NUMBER OF PRODUCERS" - ], - years: { - county: ["2017"], - state: ["2017"] - } -}, -{ - plugInAttribute: "Total Farms", - numberOfAttributeColumnsInCodap: 1, - ...sharedEconomicHeaders, - short_desc: ["FARM OPERATIONS - NUMBER OF OPERATIONS"], - domain_desc: "Total", - geographicLevels: "State, County", - years: { - county: allYears, - state: allYears - } -}, -{ - plugInAttribute: "Organization Type", - numberOfAttributeColumnsInCodap: 5, - sect_desc: "Demographics", - group_desc: "Farms & Land & Assets", - commodity_desc: "Farm Operations", - statisticcat_desc: "Operations", - short_desc: [ - "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, CORPORATION (EXCL FAMILY HELD) - NUMBER OF OPERATIONS", - "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, CORPORATION, FAMILY HELD - NUMBER OF OPERATIONS", - "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, FAMILY & INDIVIDUAL - NUMBER OF OPERATIONS", - "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, INSTITUTIONAL & RESEARCH & RESERVATION & OTHER - NUMBER OF OPERATIONS", - "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, PARTNERSHIP - NUMBER OF OPERATIONS" - ], - domain_desc: "Total", - geographicLevels: "County", - years: { - county: ["1997", "2002", "2007", "2012", "2017"], - state: [] - } -}, -{ - plugInAttribute: "Economic Class", - numberOfAttributeColumnsInCodap: "3 - 6", - ...sharedEconomicHeaders, - short_desc: ["FARM OPERATIONS - NUMBER OF OPERATIONS"], - domain_desc: "Economic Class", - geographicLevels: "State ", - years: { - county: allYears.filter(y => Number(y) >= 1987), - state: allYears.filter(y => Number(y) >= 1987) - } -}, -{ - plugInAttribute: "Acres Operated", - numberOfAttributeColumnsInCodap: 14, - sect_desc: "Economics", - group_desc: "Farms & Land & Assets", - commodity_desc: "Farm Operations", - statisticcat_desc: "Area Operated", - short_desc: ["FARM OPERATIONS - ACRES OPERATED"], - domain_desc: "Area Operated", - geographicLevels: "State, County", - years: { - county: ["1997", "2002", "2007", "2012", "2017"], - state: ["1997", "2002", "2007", "2012", "2017"] - } -}, -{ - plugInAttribute: "Organic", - numberOfAttributeColumnsInCodap: 1, - ...sharedEconomicHeaders, - short_desc: ["FARM OPERATIONS, ORGANIC - NUMBER OF OPERATIONS"], - domain_desc: "Organic Status", - years: { - county: ["2008", "2011", "2012", "2014", "2015", "2016", "2017", "2019", "2021"], - state: ["2008", "2011", "2012", "2014", "2015", "2016", "2017", "2019", "2021"] - } -}, -{ - plugInAttribute: "Labor Status", - numberOfAttributeColumnsInCodap: 3, - ...sharedLaborHeaders, - statisticcat_desc: "Workers", - short_desc: [ - "LABOR, MIGRANT - NUMBER OF WORKERS", - "LABOR, UNPAID - NUMBER OF WORKERS", - "LABOR, HIRED - NUMBER OF WORKERS" - ], - domain_desc: "Total", - geographicLevels: "State, County", - years: { - county: ["2012", "2017"], - state: ["2012", "2017"] - } -}, -{ - plugInAttribute: "Wages", - numberOfAttributeColumnsInCodap: 1, - ...sharedLaborHeaders, - statisticcat_desc: "Wage Rate", - short_desc: ["LABOR, HIRED - WAGE RATE, MEASURED IN $/HOUR"], - domain_desc: "Total", - geographicLevels: "Region: Multi-state", - years: { - county: allYears.filter(y => Number(y) >= 1989), - state: allYears.filter(y => Number(y) >= 1989) - } -}, -{ - plugInAttribute: "Time Worked", - numberOfAttributeColumnsInCodap: 1, - ...sharedLaborHeaders, - statisticcat_desc: "Wage Rate", - short_desc: ["LABOR, HIRED - TIME WORKED, MEASURED IN HOURS/WEEK"], - domain_desc: "Total", - geographicLevels: "Region: Multi-state", - years: { - county: allYears.filter(y => Number(y) >= 1989), - state: allYears.filter(y => Number(y) >= 1989) - } -}, -{ - plugInAttribute: "Corn", - numberOfAttributeColumnsInCodap: 1, - sect_desc: "Crops", - group_desc: "Field Crops", - commodity_desc: "Corn", - statisticcat_desc: { - [areaHarvested]: "Area Harvested", - [yieldInBU]: "Yield" - }, - short_desc: { - [areaHarvested]: ["CORN, GRAIN - ACRES HARVESTED"], - [yieldInBU]: ["CORN, GRAIN - YIELD, MEASURED IN BU / ACRE"] - }, - domain_desc: "Total", - geographicLevels: "State, County", - years: { - county: allYears, - state: allYears - } -}, -{ - plugInAttribute: "Cotton", - numberOfAttributeColumnsInCodap: 1, - sect_desc: "Crops", - group_desc: "Field Crops", - commodity_desc: "Cotton", - statisticcat_desc: { - [areaHarvested]: "Area Harvested", - [yieldInBU]: "Yield" - }, - short_desc: { - [areaHarvested]: ["COTTON - ACRES HARVESTED"], - [yieldInBU]: ["COTTON - YIELD, MEASURED IN LB / ACRE"] - }, - domain_desc: "Total", - geographicLevels: "State, County", - years: { - county: allYears, - state: allYears - } -}, -{ - plugInAttribute: "Grapes", - numberOfAttributeColumnsInCodap: 1, - sect_desc: "Crops", - group_desc: "Fruit & Tree Nuts", - commodity_desc: "Grapes", - statisticcat_desc: { - [areaHarvested]: "Area Harvested", - [yieldInBU]: "Yield" - }, - short_desc: { - [areaHarvested]: ["GRAPES, ORGANIC - ACRES HARVESTED"], - [yieldInBU]: ["GRAPES - YIELD, MEASURED IN TONS / ACRE"] - }, - domain_desc: "Total", - geographicLevels: "State, County", - years: { - county: allYears, - state: allYears - } -}, -{ - plugInAttribute: "Oats", - numberOfAttributeColumnsInCodap: 1, - sect_desc: "Crops", - group_desc: "Field Crops", - commodity_desc: "Oats", - statisticcat_desc: { - [areaHarvested]: "Area Harvested", - [yieldInBU]: "Yield" - }, - short_desc: { - [areaHarvested]: ["Oats - Acres Harvested"], - [yieldInBU]: ["Oats - Yield, measured in BU / acre"] - }, - domain_desc: "Total", - geographicLevels: "State, County", - years: { - county: allYears, - state: allYears - } -}, -{ - plugInAttribute: "Soybeans", - numberOfAttributeColumnsInCodap: 1, - sect_desc: "Crops", - group_desc: "Field Crops", - commodity_desc: "Soybeans", - statisticcat_desc: { - [areaHarvested]: "Area Harvested", - [yieldInBU]: "Yield" - }, - short_desc: { - [areaHarvested]: ["Soybeans - Acres Harvested"], - [yieldInBU]: ["Soybeans - Yield, measured in BU / acre"] - }, - domain_desc: "Total", - geographicLevels: "State, County", - years: { - county: allYears, - state: allYears - } -}, -{ - plugInAttribute: "Wheat", - numberOfAttributeColumnsInCodap: 1, - sect_desc: "Crops", - group_desc: "Field Crops", - commodity_desc: "Wheat", - statisticcat_desc: { - [areaHarvested]: "Area Harvested", - [yieldInBU]: "Yield" - }, - short_desc: { - [areaHarvested]: ["Wheat - Acres Harvested"], - [yieldInBU]: ["Wheat - Yield, measured in BU / acre"] - }, - domain_desc: "Total", - geographicLevels: "State, County", - years: { - county: allYears, - state: allYears - } -} -]; diff --git a/src/scripts/utils.ts b/src/scripts/utils.ts new file mode 100644 index 0000000..d8296e6 --- /dev/null +++ b/src/scripts/utils.ts @@ -0,0 +1,10 @@ +import { queryData } from "../constants/query-headers"; + +export const flatten = (arr: any[]): any[] => { + return arr.reduce((acc: any[], val: any) => + Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), []); +}; + +export const getQueryParams = (attribute: string) => { + return queryData.find((d) => d.plugInAttribute === attribute); +} \ No newline at end of file From 7ac5795336d09753ee6cad096fed69700034a25c Mon Sep 17 00:00:00 2001 From: lublagg Date: Tue, 19 Sep 2023 17:11:03 -0400 Subject: [PATCH 15/21] Various UI improvements. --- src/components/attribute-options.tsx | 2 +- src/components/options.tsx | 19 +- src/components/place-options.tsx | 25 +++ src/components/years-options.tsx | 2 +- src/constants/codapMetadata.ts | 183 ++++++++++++++++++ src/constants/constants.ts | 7 +- src/constants/counties.ts | 2 +- .../{query-headers.ts => queryHeaders.ts} | 101 +--------- src/constants/regionData.ts | 79 ++++++++ src/constants/types.ts | 12 +- src/scripts/api.ts | 90 ++++++--- src/scripts/pluginHelper.js | 162 ---------------- src/scripts/utils.ts | 4 +- 13 files changed, 385 insertions(+), 303 deletions(-) create mode 100644 src/constants/codapMetadata.ts rename src/constants/{query-headers.ts => queryHeaders.ts} (78%) create mode 100644 src/constants/regionData.ts delete mode 100644 src/scripts/pluginHelper.js diff --git a/src/components/attribute-options.tsx b/src/components/attribute-options.tsx index 3a47779..a60328e 100644 --- a/src/components/attribute-options.tsx +++ b/src/components/attribute-options.tsx @@ -15,7 +15,6 @@ export const AttributeOptions: React.FC = (props) => { const {handleSetSelectedOptions, selectedOptions} = props; const commonProps = { - inputType: "checkbox" as "checkbox"|"radio", handleSetSelectedOptions, selectedOptions }; @@ -43,6 +42,7 @@ export const AttributeOptions: React.FC = (props) => {
= (props) => { let newArray = [...selectedOptions[optionKey]]; if (e.currentTarget.checked) { newArray.push(e.target.value); + // If user selects "Age", "Gender", or "Race", auto-select "Total Farmers" as well + if (optionKey === "farmerDemographics" && !newArray.includes("Total Farmers")) { + newArray.push("Total Farmers"); + } + // If user selects a state, de-select "All States" + if (optionKey === "states" && newArray.includes("All States")) { + newArray = newArray.filter((state) => state !== "All States"); + } newArray.sort(); if (optionKey === "years") { newArray.reverse(); } } else { if (isOptionSelected(e.target.value)) { - newArray = newArray.filter((o) => o !== e.target.value); + const includes = (opt: string) => selectedOptions.farmerDemographics.includes(opt); + // "Total Farmers" can only be unselected if race, gender, and age are unselected + if (optionKey === "farmerDemographics" && e.target.value === "Total Farmers") { + const shouldFilter = !includes("Race") && !includes("Gender") && !includes("Age"); + if (shouldFilter) { + newArray = newArray.filter((o) => o !== e.target.value); + } + } else { + newArray = newArray.filter((o) => o !== e.target.value); + } } } handleSetSelectedOptions(optionKey, newArray); diff --git a/src/components/place-options.tsx b/src/components/place-options.tsx index 6294bea..25d9012 100644 --- a/src/components/place-options.tsx +++ b/src/components/place-options.tsx @@ -12,6 +12,15 @@ interface IProps { export const PlaceOptions: React.FC = (props) => { const {handleSetSelectedOptions, selectedOptions} = props; + + const isAllStatesSelected = () => { + return selectedOptions.states[0] === "All States"; + }; + + const handleSelectAllStates = () => { + handleSetSelectedOptions("states", ["All States"]); + }; + return ( <> {placeOptions.map((placeOpt) => { @@ -22,6 +31,22 @@ export const PlaceOptions: React.FC = (props) => { key={`options-container-${placeOpt.key}`} className={placeOpt.key === "geographicLevel" ? css.radioOptions : css.checkOptions} > + {placeOpt.key === "states" && +
+ + +
+ } 74", + "unitInCodapTable": "# of Farmers" + }, + "PRODUCERS, (ALL), FEMALE - NUMBER OF PRODUCERS": { + "attributeNameInCodapTable": "Female", + "unitInCodapTable": "# of Farmers" + }, + "PRODUCERS, (ALL), MALE - NUMBER OF PRODUCERS": { + "attributeNameInCodapTable": "Male", + "unitInCodapTable": "# of Farmers" + }, + "PRODUCERS, AMERICAN INDIAN OR ALASKAN NATIVE - NUMBER OF PRODUCERS": { + "attributeNameInCodapTable": "American Indian or Alaskan Native", + "unitInCodapTable": "# of Farmers" + }, + "PRODUCERS, ASIAN - NUMBER OF PRODUCERS": { + "attributeNameInCodapTable": "Asian", + "unitInCodapTable": "# of Farmers" + }, + "PRODUCERS, BLACK OR AFRICAN AMERICAN - NUMBER OF PRODUCERS": { + "attributeNameInCodapTable": "Black or African American", + "unitInCodapTable": "# of Farmers" + }, + "PRODUCERS, HISPANIC - NUMBER OF PRODUCERS": { + "attributeNameInCodapTable": "Hispanic", + "unitInCodapTable": "# of Farmers" + }, + "PRODUCERS, MULTI-RACE - NUMBER OF PRODUCERS": { + "attributeNameInCodapTable": "Multi-race", + "unitInCodapTable": "# of Farmers" + }, + "PRODUCERS, NATIVE HAWAIIAN OR OTHER PACIFIC ISLANDERS - NUMBER OF PRODUCERS": { + "attributeNameInCodapTable": "Native Hawaiian or Other Pacific Islanders", + "unitInCodapTable": "# of Farmers" + }, + "PRODUCERS, WHITE - NUMBER OF PRODUCERS": { + "attributeNameInCodapTable": "White", + "unitInCodapTable": "# of Farmers" + }, + "FARM OPERATIONS - NUMBER OF OPERATIONS": { + "attributeNameInCodapTable": "Total Number of Farms", + "unitInCodapTable": "# of Farms" + }, + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, CORPORATION (EXCL FAMILY HELD) - NUMBER OF OPERATIONS": { + "attributeNameInCodapTable": "Corporate", + "unitInCodapTable": "# of Farms" + }, + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, CORPORATION, FAMILY HELD - NUMBER OF OPERATIONS": { + "attributeNameInCodapTable": "Corporate, Family Held", + "unitInCodapTable": "# of Farms" + }, + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, FAMILY & INDIVIDUAL - NUMBER OF OPERATIONS": { + "attributeNameInCodapTable": "Family & Individual", + "unitInCodapTable": "# of Farms" + }, + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, INSTITUTIONAL & RESEARCH & RESERVATION & OTHER - NUMBER OF OPERATIONS": { + "attributeNameInCodapTable": "Institutional, Research, Reservation, & Other", + "unitInCodapTable": "# of Farms" + }, + "FARM OPERATIONS, ORGANIZATION, TAX PURPOSES, PARTNERSHIP - NUMBER OF OPERATIONS": { + "attributeNameInCodapTable": "Partnership", + "unitInCodapTable": "# of Farms" + }, + "FARM OPERATIONS - ACRES OPERATED": { + "attributeNameInCodapTable": "", + "unitInCodapTable": "# of Farms" + }, + "FARM OPERATIONS, ORGANIC - NUMBER OF OPERATIONS": { + "attributeNameInCodapTable": "Organic", + "unitInCodapTable": "# of Farms" + }, + "LABOR, MIGRANT - NUMBER OF WORKERS": { + "attributeNameInCodapTable": "Migrant", + "unitInCodapTable": "# of Farm Laborers" + }, + "LABOR, UNPAID - NUMBER OF WORKERS": { + "attributeNameInCodapTable": "Unpaid", + "unitInCodapTable": "# of Farm Laborers" + }, + "LABOR, HIRED - NUMBER OF WORKERS": { + "attributeNameInCodapTable": "Hired", + "unitInCodapTable": "# of Farm Laborers" + }, + "LABOR, HIRED - WAGE RATE, MEASURED IN $ / HOUR": { + "attributeNameInCodapTable": "Wage Rate of Laborers", + "unitInCodapTable": "$/Hour" + }, + "LABOR, HIRED - TIME WORKED, MEASURED IN HOURS / WEEK": { + "attributeNameInCodapTable": "Time Worked by Laborers", + "unitInCodapTable": "Hours/Week" + }, + "CORN, GRAIN - YIELD, MEASURED IN BU / ACRE": { + "attributeNameInCodapTable": "Corn Yield", + "unitInCodapTable": "BU/Acre" + }, + "CORN, GRAIN - ACRES HARVESTED": { + "attributeNameInCodapTable": "Corn Area Harvested", + "unitInCodapTable": "Acres Harvested" + }, + "COTTON, - YIELD, MEASURED IN LB / ACRE": { + "attributeNameInCodapTable": "Cotton Yield", + "unitInCodapTable": "LB/Acre" + }, + "COTTON - ACRES HARVESTED": { + "attributeNameInCodapTable": "Cotton Area Harvested", + "unitInCodapTable": "Acres Harvested" + }, + "GRAPES - YIELD, MEASURED IN TONS / ACRE": { + "attributeNameInCodapTable": "Grapes Yield", + "unitInCodapTable": "Tons/Acre" + }, + "GRAPES, ORGANIC - ACRES HARVESTED": { + "attributeNameInCodapTable": "Grapes Area Harvested", + "unitInCodapTable": "Acres Harvested" + }, + "OATS - YIELD, MEASURED IN BU / ACRE": { + "attributeNameInCodapTable": "Oats Yield", + "unitInCodapTable": "BU/Acre" + }, + "OATS - ACRES HARVESTED": { + "attributeNameInCodapTable": "Oats Area Harvested", + "unitInCodapTable": "Acres Harvested" + }, + "SOYBEANS - YIELD MEASURED IN BU / ACRE": { + "attributeNameInCodapTable": "Soybeans Yield", + "unitInCodapTable": "BU/Acre" + }, + "SOYBEANS - ACRES HARVESTED": { + "attributeNameInCodapTable": "Soybeans Area Harvested", + "unitInCodapTable": "Acres Harvested" + }, + "WHEAT - YIELD MEASURED IN BU / ACRE": { + "attributeNameInCodapTable": "Wheat Yield", + "unitInCodapTable": "BU/Acre" + }, + "WHEAT - ACRES HARVESTED": { + "attributeNameInCodapTable": "Wheat Area Harvested", + "unitInCodapTable": "Acres Harvested" + } +}; diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 234e08c..f23e8c9 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -58,14 +58,13 @@ export const fiftyStates = [ "West Virginia", "Wisconsin", "Wyoming" -] +]; export const stateOptions: IAttrOptions = { label: null, key: "states", instructions: "Choose states to include in your dataset from the list below", options: [ - "All States", ...fiftyStates ] }; @@ -99,7 +98,7 @@ const cropUnitOptions: IAttrOptions = { export const cropOptions: IAttrOptions = { key: "crops", label: null, - options: ["Corn", "Cotton", "Grapes", "Grasses", "Oats", "Soybeans", "Wheat"], + options: ["Corn", "Cotton", "Grapes", "Oats", "Soybeans", "Wheat"], instructions: "(Choose crops)" }; @@ -132,4 +131,4 @@ export const defaultSelectedOptions: IStateOptions = { cropUnits: "", crops: [], years: [] -}; \ No newline at end of file +}; diff --git a/src/constants/counties.ts b/src/constants/counties.ts index f97563e..abd1d50 100644 --- a/src/constants/counties.ts +++ b/src/constants/counties.ts @@ -3282,4 +3282,4 @@ export const countyData: {[state: string]: string[]} = { "WASHAKIE", "WESTON" ] -} \ No newline at end of file +}; diff --git a/src/constants/query-headers.ts b/src/constants/queryHeaders.ts similarity index 78% rename from src/constants/query-headers.ts rename to src/constants/queryHeaders.ts index dcac5f7..b2a50ab 100644 --- a/src/constants/query-headers.ts +++ b/src/constants/queryHeaders.ts @@ -1,4 +1,3 @@ -import { fiftyStates } from "./constants"; import { IQueryHeaders } from "./types"; const allYears = []; @@ -6,7 +5,6 @@ for (let year = 2022; year >= 1910; year--) { allYears.push(`${year}`); } - const sharedDemographicHeaders = { sect_desc: "Demographics", group_desc: "Producers", @@ -41,7 +39,7 @@ const sharedCropHeaders = { "County": allYears, "State": allYears } -} +}; export const queryData: Array = [ { @@ -198,8 +196,8 @@ export const queryData: Array = [ { plugInAttribute: "Time Worked", ...sharedLaborHeaders, - statisticcat_desc: "Wage Rate", - short_desc: ["LABOR, HIRED - TIME WORKED, MEASURED IN HOURS/WEEK"], + statisticcat_desc: "Time Worked", + short_desc: ["LABOR, HIRED - TIME WORKED, MEASURED IN HOURS / WEEK"], domain_desc: "Total", geographicAreas: ["REGION : MULTI-STATE"], years: { @@ -242,8 +240,8 @@ export const queryData: Array = [ group_desc: "Field Crops", commodity_desc: "Oats", short_desc: { - ["Area Harvested"]: ["Oats - Acres Harvested"], - ["Yield"]: ["Oats - Yield, measured in BU / acre"] + ["Area Harvested"]: ["OATS - ACRES HARVESTED"], + ["Yield"]: ["OATS - YIELD, MEASURED IN BU / ACRE"] }, ...sharedCropHeaders }, @@ -252,8 +250,8 @@ export const queryData: Array = [ group_desc: "Field Crops", commodity_desc: "Soybeans", short_desc: { - ["Area Harvested"]: ["Soybeans - Acres Harvested"], - ["Yield"]: ["Soybeans - Yield, measured in BU / acre"] + ["Area Harvested"]: ["SOYBEANS - ACRES HARVESTED"], + ["Yield"]: ["SOYBEANS - YIELD MEASURED IN BU / ACRE"] }, ...sharedCropHeaders }, @@ -263,90 +261,9 @@ export const queryData: Array = [ commodity_desc: "Wheat", short_desc: { - ["Area Harvested"]: ["Wheat - Acres Harvested"], - ["Yield"]: ["Wheat - Yield, measured in BU / acre"] + ["Area Harvested"]: ["WHEAT - ACRES HARVESTED"], + ["Yield"]: ["WHEAT - YIELD MEASURED IN BU / ACRE"] }, ...sharedCropHeaders } ]; - -interface IRegion { - "Region": string - "States": string[] -} - - -export const multiRegions: IRegion[] = [ - { - "Region": "Pacific", - "States": ["Washington", "Oregon"] - }, - { - "Region": "Mountain I", - "States": ["Montana", "Idaho", "Wyoming"] - }, - { - "Region": "Mountain II", - "States": ["Nevada", "Utah", "Colorado"] - }, - { - "Region": "Mountain III", - "States": ["Arizona", "New Mexico"] - }, - { - "Region": "Northern Plains", - "States": ["North Dakota", "South Dakota", "Kansas", "Nebraska"] - }, - { - "Region": "Southern Plains", - "States": ["Oklahoma", "Texas"] - }, - { - "Region": "Lake", - "States": ["Minnesota", "Wisconsin", "Michigan"] - }, - { - "Region": "Cornbelt I", - "States": ["Illinois", "Indiana", "Ohio"] - }, - { - "Region": "Cornbelt II", - "States": ["Iowa", "Missouri"] - }, - { - "Region": "Delta", - "States": ["Mississippi", "Louisiana", "Arkansas"] - }, - { - "Region": "Appalachian I", - "States": ["Virginia", "North Carolina"] - }, - { - "Region": "Appalachian II", - "States": ["West Virginia", "Kentucky", "Tennessee"] - }, - { - "Region": "Southeast", - "States": ["South Carolina", "Alabama", "Georgia"] - }, - { - "Region": "Northeast I", - "States": ["Maine", "New Hampshire", "Vermont", "Massachusetts", "Connecticut", "Rhode Island", "New York"] - }, - { - "Region": "Northeast II", - "States": ["Pennsylvania", "New Jersey", "Delaware", "Maryland"] - }, - { - "Region": "California", - "States": ["California"] - }, - { - "Region": "Florida", - "States": ["Florida"] - }, - { - "Region": "Hawaii", - "States": ["Hawaii"] - } -]; diff --git a/src/constants/regionData.ts b/src/constants/regionData.ts new file mode 100644 index 0000000..4a5317c --- /dev/null +++ b/src/constants/regionData.ts @@ -0,0 +1,79 @@ +interface IRegion { + "Region": string + "States": string[] +} + +export const multiRegions: IRegion[] = [ + { + "Region": "Pacific", + "States": ["Washington", "Oregon"] + }, + { + "Region": "Mountain I", + "States": ["Montana", "Idaho", "Wyoming"] + }, + { + "Region": "Mountain II", + "States": ["Nevada", "Utah", "Colorado"] + }, + { + "Region": "Mountain III", + "States": ["Arizona", "New Mexico"] + }, + { + "Region": "Northern Plains", + "States": ["North Dakota", "South Dakota", "Kansas", "Nebraska"] + }, + { + "Region": "Southern Plains", + "States": ["Oklahoma", "Texas"] + }, + { + "Region": "Lake", + "States": ["Minnesota", "Wisconsin", "Michigan"] + }, + { + "Region": "Cornbelt I", + "States": ["Illinois", "Indiana", "Ohio"] + }, + { + "Region": "Cornbelt II", + "States": ["Iowa", "Missouri"] + }, + { + "Region": "Delta", + "States": ["Mississippi", "Louisiana", "Arkansas"] + }, + { + "Region": "Appalachian I", + "States": ["Virginia", "North Carolina"] + }, + { + "Region": "Appalachian II", + "States": ["West Virginia", "Kentucky", "Tennessee"] + }, + { + "Region": "Southeast", + "States": ["South Carolina", "Alabama", "Georgia"] + }, + { + "Region": "Northeast I", + "States": ["Maine", "New Hampshire", "Vermont", "Massachusetts", "Connecticut", "Rhode Island", "New York"] + }, + { + "Region": "Northeast II", + "States": ["Pennsylvania", "New Jersey", "Delaware", "Maryland"] + }, + { + "Region": "California", + "States": ["California"] + }, + { + "Region": "Florida", + "States": ["Florida"] + }, + { + "Region": "Hawaii", + "States": ["Hawaii"] + } +]; diff --git a/src/constants/types.ts b/src/constants/types.ts index 1c3b2f5..d210207 100644 --- a/src/constants/types.ts +++ b/src/constants/types.ts @@ -1,7 +1,3 @@ -import { fiftyStates } from "./constants"; - - - export interface IStateOptions { geographicLevel: "County"|"State", states: string[], @@ -62,17 +58,17 @@ export interface IResData { week_ending: string; year: number; zip_5: string; -}; +} export interface ICropCategory { ["Area Harvested"]: string, ["Yield"]: string -}; +} export interface ICropDataItem { ["Area Harvested"]: string[], ["Yield"]: string[] -}; +} export interface IQueryHeaders { plugInAttribute: string, @@ -87,4 +83,4 @@ export interface IQueryHeaders { "County": string[] "State": string[] } -}; +} diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 5ec688a..8dcd166 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -1,10 +1,11 @@ import fetchJsonp from "fetch-jsonp"; -import { multiRegions, queryData } from "../constants/query-headers"; import { ICropCategory, ICropDataItem, IStateOptions } from "../constants/types"; +import { multiRegions } from "../constants/regionData"; import { connect } from "./connect"; -import { cropOptions } from "../constants/constants"; +import { cropOptions, fiftyStates } from "../constants/constants"; import { countyData } from "../constants/counties"; import { getQueryParams } from "./utils"; +import { attrToCODAPColumnName } from "../constants/codapMetadata"; const baseURL = `https://quickstats.nass.usda.gov/api/api_GET/?key=9ED0BFB8-8DDD-3609-9940-A2341ED6A9E3`; @@ -26,15 +27,30 @@ interface IGetAttrDataParams { state?: string } -export const fetchData = async (req: string) => { - try { - const response = await fetchJsonp(req, {timeout: 10000}); - const json = await response.json(); - return json; - } catch (error) { - console.log("parsing failed", error); - throw error; +// export const fetchData = async (req: string) => { +// try { +// const response = await fetchJsonp(req, {timeout: 10000}); +// const json = await response.json(); +// return json; +// } catch (error) { +// console.log("parsing failed", error); +// throw error; +// } +// }; + +export const fetchDataWithRetry = async (req: string, maxRetries = 3) => { + let retries = 0; + while (retries < maxRetries) { + try { + const response = await fetchJsonp(req, { timeout: 30000 }); // Increase the timeout + const json = await response.json(); + return json; + } catch (error) { + console.log(`Request attempt ${retries + 1} failed:`, error); + retries++; + } } + throw new Error(`Request failed after ${maxRetries} attempts`); }; export const createRequest = ({attribute, geographicLevel, location, year, cropCategory, state}: IRequestParams) => { @@ -76,7 +92,7 @@ export const createRequest = ({attribute, geographicLevel, location, year, cropC // if we are creating a request at the county level, we need to also pass in a state if (state) { - req += `&state_name=${state}` + req += `&state_name=${state}`; } item.forEach(subItem => { @@ -91,6 +107,7 @@ export const createTableFromSelections = async (selectedOptions: IStateOptions) await connect.createTopCollection(geographicLevel); const allAttrs: Array = ["Year"]; + for (const key in subOptions) { const selections = subOptions[key as keyof typeof subOptions]; for (const attribute of selections) { @@ -100,26 +117,35 @@ export const createTableFromSelections = async (selectedOptions: IStateOptions) } const {short_desc} = queryParams; if (Array.isArray(short_desc)) { - allAttrs.push(...short_desc); - } else { - allAttrs.push(short_desc); + for (const desc of short_desc) { + console.log({desc}); + const codapColumnName = attrToCODAPColumnName[desc].attributeNameInCodapTable; + allAttrs.push(codapColumnName); + } + } else if (typeof short_desc === "object" && cropUnits) { + const attr = short_desc[cropUnits as keyof ICropDataItem][0]; + allAttrs.push(attrToCODAPColumnName[attr].attributeNameInCodapTable); } } } + await connect.createSubCollection(geographicLevel, allAttrs); const items = await getItems(selectedOptions); await connect.createItems(items); await connect.makeCaseTableAppear(); - // await connect.createItems(items); }; const getItems = async (selectedOptions: IStateOptions) => { - const {states, years} = selectedOptions; + let {states, years} = selectedOptions; const multipleStatesSelected = states.length > 1 || states[0] === "All States"; const multipleYearsSelected = years.length > 1; const countySelected = selectedOptions.geographicLevel === "County"; - const items = []; + if (states[0] === "All States") { + states = fiftyStates; + } + + const promises = []; if (multipleStatesSelected) { for (const state of states) { @@ -128,32 +154,32 @@ const getItems = async (selectedOptions: IStateOptions) => { if (countySelected) { const allCounties = countyData[state]; for (const county of allCounties) { - const item = await getDataForSingleYearAndState(selectedOptions, county, year, state); - items.push(item); + const item = getDataForSingleYearAndState(selectedOptions, county, year, state); + promises.push(item); } } else { - const item = await getDataForSingleYearAndState(selectedOptions, state, year); - items.push(item); + const item = getDataForSingleYearAndState(selectedOptions, state, year); + promises.push(item); } } } else { - const item = await getDataForSingleYearAndState(selectedOptions, state, years[0]); - items.push(item); + const item = getDataForSingleYearAndState(selectedOptions, state, years[0]); + promises.push(item); } } } else { if (countySelected) { const allCounties = countyData[states[0]]; for (const county of allCounties) { - const item = await getDataForSingleYearAndState(selectedOptions, county, years[0], states[0]); - items.push(item); + const item = getDataForSingleYearAndState(selectedOptions, county, years[0], states[0]); + promises.push(item); } } else { - const item = await getDataForSingleYearAndState(selectedOptions, states[0], years[0]); - items.push(item); + const item = getDataForSingleYearAndState(selectedOptions, states[0], years[0]); + promises.push(item); } } - return items; + return await Promise.all(promises); }; const getDataForSingleYearAndState = async (selectedOptions: IStateOptions, countyOrState: string, year: string, state?: string) => { @@ -176,7 +202,8 @@ const getDataForSingleYearAndState = async (selectedOptions: IStateOptions, coun let location = countyOrState; if (isMultiStateRegion) { const itemToCheck = state ? state : countyOrState; - location = multiRegions.find((region) => region.States.includes(itemToCheck))!.Region; + const regData = multiRegions.find((region) => region.States.includes(itemToCheck)); + location = regData?.Region ? regData.Region : countyOrState; } const params: IGetAttrDataParams = {attribute, geographicLevel: geoLevel, location, year, cropUnits}; if (geoLevel === "County") { @@ -199,12 +226,13 @@ const getAttrData = async (params: IGetAttrDataParams) => { reqParams.cropCategory = cropUnits as keyof ICropDataItem; } const req = createRequest(reqParams); - const res = await fetchData(req); + const res = await fetchDataWithRetry(req); const values: any = {}; if (res) { const {data} = res; data.map((dataItem: any) => { - return values[dataItem.short_desc] = dataItem.Value; + const codapColumnName = attrToCODAPColumnName[dataItem.short_desc].attributeNameInCodapTable; + return values[codapColumnName] = dataItem.Value; }); } else { console.log("error"); diff --git a/src/scripts/pluginHelper.js b/src/scripts/pluginHelper.js deleted file mode 100644 index fab10ec..0000000 --- a/src/scripts/pluginHelper.js +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Created by tim on 1/19/17. - - - ========================================================================== - pluginHelper.js in gamePrototypes. - - Author: Tim Erickson - - Copyright (c) 2016 by The Concord Consortium, Inc. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - ========================================================================== - - - */ - -var pluginHelper = { - - - /** - * Create a new data set (data context) using the input object - * @param iDataSetDescription the object that describes the data set. See the API documentation. - * @returns {Promise} which, when resolved, means that the data set exists - */ - initDataSet: function (iDataSetDescription) { - return new Promise( function( resolve, reject ) { - var tDataContextResourceString = 'dataContext[' + iDataSetDescription.name + ']'; - var tMessage = { action: 'get', resource: tDataContextResourceString }; - - // if the data set already exists, we will not ask CODAP to create one. So we check... - var tAlreadyExistsPromise = codapInterface.sendRequest(tMessage); - - tAlreadyExistsPromise.then( - // iValue is the result of the resolved "get dataContext" call - function( iValue ) { - if (iValue.success) { - console.log("dataContext[" + iDataSetDescription.name + "] already exists"); - resolve( iValue ); - } else { - // the data set did not exist. (Since get dataContext returned success = false) - console.log("Creating dataContext[" + iDataSetDescription.name + "]" ); - tMessage = { - action: 'create', - resource: 'dataContext', - values: iDataSetDescription - }; - codapInterface.sendRequest(tMessage).then( - // iValue is the result of the resolved "create dataContext" call. - function( iValue ) { - resolve( iValue ); - } - ); - } - } - ).catch (function (msg) { - console.log('warning in pluginHelper.initDataSet: ' + msg); - reject( msg ); - }); - }); - }, - - /** - * Create new data items (broader than cases; see the documentation for the API) - * Notes: (1) this refers only to the data context, not to any collections. Right? Has to. - * (2) notice how the values array does not have a "values" key inside it as with createCases. - * - * @param iValuesArray the array (or not) of objects, each of which will be an item. The keys are attribute names. - * @param iDataContextName the name of the data set (or "data context"). - */ - createItems : function(iValuesArray, iDataContextName, iCallback) { - return new Promise( function(resolve, reject) { - iValuesArray = pluginHelper.arrayify( iValuesArray ); - - var tResourceString = iDataContextName ? "dataContext[" + iDataContextName + "].item" : "item"; - - var tMessage = { - action : 'create', - resource : tResourceString, - values : iValuesArray - }; - - var tCreateItemsPromise = codapInterface.sendRequest(tMessage); - resolve( tCreateItemsPromise ); - }) - }, - - createCases : function(iValues, iCollection, iDataContext, iCallback) { - iValues = pluginHelper.arrayify( iValues ); - console.log("DO NOT CALL pluginHelper.createCases YET!!"); - }, - - /** - * - * @param IDs array of case IDs to be selected - * @param iDataContextName name of the data context in which these things live. OK if absent. - * @returns {Promise} - */ - selectCasesByIDs: function (IDs, iDataContextName) { - return new Promise( function( resolve, reject ) { - IDs = pluginHelper.arrayify( IDs ); - - var tResourceString = "selectionList"; - - if (typeof iDataContextName !== 'undefined') { - tResourceString = 'dataContext[' + iDataContextName + '].' + tResourceString; - } - - var tMessage = { - action: 'create', - resource: tResourceString, - values: IDs - }; - - var tSelectCasesPromise = codapInterface.sendRequest(tMessage); - resolve( tSelectCasesPromise ); - }) - }, - - getCaseValuesByCaseID: function (iCaseID, iDataContext) { - return new Promise(function (resolve, reject) { - var tMessage = { - action: 'get', - resource: "dataContext[" - + iDataContext + "].caseByID[" - + iCaseID + "]" - }; - - codapInterface.sendRequest(tMessage).then( - function (iResult) { - if (iResult.success) { - var tCaseValues = iResult.values.case.values; - resolve(tCaseValues); - } - } - ) - }) - }, - - /** - * Change the input to an array if it is not one! - * @param iValuesArray the thing which might be an array - * @returns {*} if it was not an array, a single-item array with the thing. Otherwise, the array. - */ - arrayify : function( iValuesArray ) { - if (iValuesArray && !Array.isArray(iValuesArray)) { - iValuesArray = [iValuesArray]; - } - return iValuesArray; - } - -} \ No newline at end of file diff --git a/src/scripts/utils.ts b/src/scripts/utils.ts index d8296e6..9450ea4 100644 --- a/src/scripts/utils.ts +++ b/src/scripts/utils.ts @@ -1,4 +1,4 @@ -import { queryData } from "../constants/query-headers"; +import { queryData } from "../constants/queryHeaders"; export const flatten = (arr: any[]): any[] => { return arr.reduce((acc: any[], val: any) => @@ -7,4 +7,4 @@ export const flatten = (arr: any[]): any[] => { export const getQueryParams = (attribute: string) => { return queryData.find((d) => d.plugInAttribute === attribute); -} \ No newline at end of file +}; From cf66a33411678c0c734e6f417490e6308f6ad6c3 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 20 Sep 2023 18:03:26 -0400 Subject: [PATCH 16/21] Add loading message when fetching data. --- src/assets/done.svg | 6 ++++ src/assets/progress-indicator.svg | 14 ++++++++ src/assets/warning.svg | 6 ++++ src/components/app.scss | 19 +++------- src/components/app.tsx | 22 ++++++++++-- src/constants/queryHeaders.ts | 4 +-- src/scripts/api.ts | 60 ++++++++++++++----------------- 7 files changed, 79 insertions(+), 52 deletions(-) create mode 100644 src/assets/done.svg create mode 100644 src/assets/progress-indicator.svg create mode 100644 src/assets/warning.svg diff --git a/src/assets/done.svg b/src/assets/done.svg new file mode 100644 index 0000000..6818b68 --- /dev/null +++ b/src/assets/done.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/progress-indicator.svg b/src/assets/progress-indicator.svg new file mode 100644 index 0000000..afa7b16 --- /dev/null +++ b/src/assets/progress-indicator.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/src/assets/warning.svg b/src/assets/warning.svg new file mode 100644 index 0000000..98e9d53 --- /dev/null +++ b/src/assets/warning.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/app.scss b/src/components/app.scss index b9c2a4e..a491999 100644 --- a/src/components/app.scss +++ b/src/components/app.scss @@ -68,6 +68,11 @@ button:focus { display: flex; align-items: baseline; justify-content: space-between; + .status { + display: flex; + align-items: center; + gap: 10px; + } } .introSection { @@ -104,17 +109,3 @@ a { } } } - -.summary { - font-family: 'icomoon' !important; - font-size: 20px; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} diff --git a/src/components/app.tsx b/src/components/app.tsx index 57539de..6b1db3d 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -6,7 +6,9 @@ import { attributeOptions, categories, defaultSelectedOptions } from "../constan import { IStateOptions } from "../constants/types"; import { createTableFromSelections } from "../scripts/api"; import { connect } from "../scripts/connect"; - +import ProgressIndicator from "../assets/progress-indicator.svg"; +import Checkmark from "../assets/done.svg"; +import Error from "../assets/warning.svg"; import css from "./app.scss"; @@ -14,6 +16,8 @@ function App() { const [showInfo, setShowInfo] = useState(false); const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); const [getDataDisabled, setGetDataDisabled] = useState(true); + const [statusMessage, setStatusMessage] = useState(""); + const [statusGraphic, setStatusGraphic] = useState(); useEffect(() => { const init = async () => { @@ -43,7 +47,16 @@ function App() { }; const handleGetData = async () => { - await createTableFromSelections(selectedOptions); + setStatusMessage("Fetching data..."); + setStatusGraphic(); + const res = await createTableFromSelections(selectedOptions); + if (res !== "success") { + setStatusMessage("Fetch Error. Please retry."); + setStatusGraphic() + } else { + setStatusMessage("Fetched data."); + setStatusGraphic(); + } }; return ( @@ -77,7 +90,10 @@ function App() { })}
- +
+
{statusGraphic}
+
{statusMessage}
+
diff --git a/src/constants/queryHeaders.ts b/src/constants/queryHeaders.ts index b2a50ab..1d56b40 100644 --- a/src/constants/queryHeaders.ts +++ b/src/constants/queryHeaders.ts @@ -136,8 +136,8 @@ export const queryData: Array = [ domain_desc: "Economic Class", geographicAreas: ["State"], years: { - "County": allYears.filter(y => Number(y) >= 1987), - "State": allYears.filter(y => Number(y) >= 1987) + "County": allYears.filter(y => Number(y) >= 1998), + "State": allYears.filter(y => Number(y) >= 1998) } }, { diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 8dcd166..9989cd9 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -27,17 +27,6 @@ interface IGetAttrDataParams { state?: string } -// export const fetchData = async (req: string) => { -// try { -// const response = await fetchJsonp(req, {timeout: 10000}); -// const json = await response.json(); -// return json; -// } catch (error) { -// console.log("parsing failed", error); -// throw error; -// } -// }; - export const fetchDataWithRetry = async (req: string, maxRetries = 3) => { let retries = 0; while (retries < maxRetries) { @@ -103,36 +92,41 @@ export const createRequest = ({attribute, geographicLevel, location, year, cropC export const createTableFromSelections = async (selectedOptions: IStateOptions) => { const {geographicLevel, states, cropUnits, years, ...subOptions} = selectedOptions; + await connect.getNewDataContext(); await connect.createTopCollection(geographicLevel); - const allAttrs: Array = ["Year"]; - for (const key in subOptions) { - const selections = subOptions[key as keyof typeof subOptions]; - for (const attribute of selections) { - const queryParams = getQueryParams(attribute); - if (!queryParams) { - throw new Error("Invalid attribute"); - } - const {short_desc} = queryParams; - if (Array.isArray(short_desc)) { - for (const desc of short_desc) { - console.log({desc}); - const codapColumnName = attrToCODAPColumnName[desc].attributeNameInCodapTable; - allAttrs.push(codapColumnName); + try { + for (const key in subOptions) { + const selections = subOptions[key as keyof typeof subOptions]; + for (const attribute of selections) { + const queryParams = getQueryParams(attribute); + if (!queryParams) { + throw new Error("Invalid attribute"); + } + const {short_desc} = queryParams; + if (Array.isArray(short_desc)) { + for (const desc of short_desc) { + console.log({desc}); + const codapColumnName = attrToCODAPColumnName[desc].attributeNameInCodapTable; + allAttrs.push(codapColumnName); + } + } else if (typeof short_desc === "object" && cropUnits) { + const attr = short_desc[cropUnits as keyof ICropDataItem][0]; + allAttrs.push(attrToCODAPColumnName[attr].attributeNameInCodapTable); } - } else if (typeof short_desc === "object" && cropUnits) { - const attr = short_desc[cropUnits as keyof ICropDataItem][0]; - allAttrs.push(attrToCODAPColumnName[attr].attributeNameInCodapTable); } } - } - await connect.createSubCollection(geographicLevel, allAttrs); - const items = await getItems(selectedOptions); - await connect.createItems(items); - await connect.makeCaseTableAppear(); + await connect.createSubCollection(geographicLevel, allAttrs); + const items = await getItems(selectedOptions); + await connect.createItems(items); + await connect.makeCaseTableAppear(); + return "success"; + } catch (error) { + return error; + } }; const getItems = async (selectedOptions: IStateOptions) => { From 9afc2246db00730a7f029c452f7ed4c5051d3818 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 20 Sep 2023 19:05:20 -0400 Subject: [PATCH 17/21] Implementing review fixes. --- src/components/dropdown.tsx | 4 ++-- src/components/summary.tsx | 2 +- src/scripts/api.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/dropdown.tsx b/src/components/dropdown.tsx index 974314e..6eea11a 100644 --- a/src/components/dropdown.tsx +++ b/src/components/dropdown.tsx @@ -3,11 +3,11 @@ import classnames from "classnames"; import { PlaceOptions } from "./place-options"; import { defaultSelectedOptions } from "../constants/constants"; import { AttributeOptions } from "./attribute-options"; - -import css from "./dropdown.scss"; import { YearsOptions } from "./years-options"; import { Summary } from "./summary"; +import css from "./dropdown.scss"; + interface IProps { category: string sectionAltText: string diff --git a/src/components/summary.tsx b/src/components/summary.tsx index 71d60ea..e362f28 100644 --- a/src/components/summary.tsx +++ b/src/components/summary.tsx @@ -29,7 +29,7 @@ export const Summary: React.FC = ({category, selectedOptions}) => { const valueIsDefined = typeof value === "string" && value; const label = attr.label && (valueIsArrayWithLength || valueIsDefined) ? `${attr.label}: ` : ""; - if (Array.isArray(value) && value.length > 0) { + if (valueIsArrayWithLength) { return `${label}${value.join(", ")}`; } else if (value) { return `${attr.label}: ${value}`; diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 9989cd9..27a23ec 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -35,6 +35,7 @@ export const fetchDataWithRetry = async (req: string, maxRetries = 3) => { const json = await response.json(); return json; } catch (error) { + // eslint-disable-next-line console.log(`Request attempt ${retries + 1} failed:`, error); retries++; } @@ -108,7 +109,6 @@ export const createTableFromSelections = async (selectedOptions: IStateOptions) const {short_desc} = queryParams; if (Array.isArray(short_desc)) { for (const desc of short_desc) { - console.log({desc}); const codapColumnName = attrToCODAPColumnName[desc].attributeNameInCodapTable; allAttrs.push(codapColumnName); } @@ -229,7 +229,7 @@ const getAttrData = async (params: IGetAttrDataParams) => { return values[codapColumnName] = dataItem.Value; }); } else { - console.log("error"); + console.log(`Error: did not receive response for item with these params: ${params}`); } return values; }; From 690988da328d7dcb0582519060a44c175f68c7e9 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 20 Sep 2023 19:08:28 -0400 Subject: [PATCH 18/21] Fix unexpected 'eslint-disable-next-line' comment. --- src/scripts/api.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 27a23ec..9e8589f 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -35,7 +35,7 @@ export const fetchDataWithRetry = async (req: string, maxRetries = 3) => { const json = await response.json(); return json; } catch (error) { - // eslint-disable-next-line + // eslint-disable-next-line no-console console.log(`Request attempt ${retries + 1} failed:`, error); retries++; } @@ -93,12 +93,15 @@ export const createRequest = ({attribute, geographicLevel, location, year, cropC export const createTableFromSelections = async (selectedOptions: IStateOptions) => { const {geographicLevel, states, cropUnits, years, ...subOptions} = selectedOptions; + try { + const items = await getItems(selectedOptions); + if (items.length > 4000) { - await connect.getNewDataContext(); - await connect.createTopCollection(geographicLevel); - const allAttrs: Array = ["Year"]; + } + await connect.getNewDataContext(); + await connect.createTopCollection(geographicLevel); + const allAttrs: Array = ["Year"]; - try { for (const key in subOptions) { const selections = subOptions[key as keyof typeof subOptions]; for (const attribute of selections) { @@ -120,7 +123,6 @@ export const createTableFromSelections = async (selectedOptions: IStateOptions) } await connect.createSubCollection(geographicLevel, allAttrs); - const items = await getItems(selectedOptions); await connect.createItems(items); await connect.makeCaseTableAppear(); return "success"; @@ -229,6 +231,7 @@ const getAttrData = async (params: IGetAttrDataParams) => { return values[codapColumnName] = dataItem.Value; }); } else { + // eslint-disable-next-line no-console console.log(`Error: did not receive response for item with these params: ${params}`); } return values; From 5a6988d181392667d3a595f54bfa76ecddcf1bc8 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 20 Sep 2023 21:08:43 -0400 Subject: [PATCH 19/21] User is warned if query generates over 4000 rows. --- src/components/app.tsx | 25 +++++++++++- src/components/options.scss | 6 +++ src/components/warning.scss | 46 +++++++++++++++++++++ src/components/warning.tsx | 31 ++++++++++++++ src/components/years-options.tsx | 3 +- src/scripts/api.ts | 69 ++++++++++++++++++++------------ 6 files changed, 151 insertions(+), 29 deletions(-) create mode 100644 src/components/warning.scss create mode 100644 src/components/warning.tsx diff --git a/src/components/app.tsx b/src/components/app.tsx index 6b1db3d..d510891 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -2,9 +2,10 @@ import React, {useEffect, useState} from "react"; import { Dropdown } from "./dropdown"; import classnames from "classnames"; import { Information } from "./information"; +import { Warning } from "./warning"; import { attributeOptions, categories, defaultSelectedOptions } from "../constants/constants"; import { IStateOptions } from "../constants/types"; -import { createTableFromSelections } from "../scripts/api"; +import { createTableFromSelections, getNumberOfItems } from "../scripts/api"; import { connect } from "../scripts/connect"; import ProgressIndicator from "../assets/progress-indicator.svg"; import Checkmark from "../assets/done.svg"; @@ -18,6 +19,7 @@ function App() { const [getDataDisabled, setGetDataDisabled] = useState(true); const [statusMessage, setStatusMessage] = useState(""); const [statusGraphic, setStatusGraphic] = useState(); + const [showWarning, setShowWarning] = useState(false); useEffect(() => { const init = async () => { @@ -46,7 +48,7 @@ function App() { setShowInfo(true); }; - const handleGetData = async () => { + const getData = async () => { setStatusMessage("Fetching data..."); setStatusGraphic(); const res = await createTableFromSelections(selectedOptions); @@ -57,8 +59,24 @@ function App() { setStatusMessage("Fetched data."); setStatusGraphic(); } + } + + const handleGetData = async () => { + const numberOfRows = getNumberOfItems(selectedOptions); + if (numberOfRows > 4000) { + setShowWarning(true); + } else { + await getData(); + } }; + const handleCloseWarning = async (getDataAnyway: boolean) => { + setShowWarning(false); + if (getDataAnyway) { + await getData(); + } + } + return (
{ showInfo && @@ -96,6 +114,9 @@ function App() {
+ { showWarning && + + }
); diff --git a/src/components/options.scss b/src/components/options.scss index bb1821e..2651587 100644 --- a/src/components/options.scss +++ b/src/components/options.scss @@ -58,6 +58,12 @@ padding: 0px; } } + &.years { + padding: 0px; + .option { + flex-basis: 60px; + } + } &.narrow { margin: 0px; .option { diff --git a/src/components/warning.scss b/src/components/warning.scss new file mode 100644 index 0000000..afc4aa4 --- /dev/null +++ b/src/components/warning.scss @@ -0,0 +1,46 @@ +@import "./vars.scss"; + +.popUp { + position: absolute; + z-index: 2; + left: calc(50% - 175px); + top: calc(50% - 125px); + width: 350px; + height: 250; + .popUpContent { + position: absolute; + left: 12px; + right: 12px; + top: 35px; + z-index: 3; + display: flex; + flex-direction: column; + overflow-y: hidden; + background-color: white; + border: 0 solid $teal-dark; + border-radius: 6px; + border-width: 1px; + .header { + display: flex; + justify-content: center; + padding: 5px; + background-color: $teal-dark; + color: white; + font-size: 14px; + font-weight: bold; + gap: 5px; + } + .popUpBody { + background-color: white; + padding: 12px; + } + .popUpFooter { + justify-content: space-around; + display: flex; + } + } +} + + + + diff --git a/src/components/warning.tsx b/src/components/warning.tsx new file mode 100644 index 0000000..ad2f4f2 --- /dev/null +++ b/src/components/warning.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import WarningIcon from "../assets/warning.svg"; +import css from "./warning.scss"; + +interface IProps { + handleCloseWarning: (getDataAnyway: boolean) => void +} + +export const Warning: React.FC = (props) => { + const {handleCloseWarning} = props; + return ( +
+
+
+ Warning +
+
+
+ The number of attributes you have selected is over 4000 rows and may lead to the program running slowly. +
+
+
+
+ + +
+
+
+
+ ); +}; diff --git a/src/components/years-options.tsx b/src/components/years-options.tsx index 761a6ff..a23c8ae 100644 --- a/src/components/years-options.tsx +++ b/src/components/years-options.tsx @@ -4,6 +4,7 @@ import { IStateOptions } from "../constants/types"; import { Options } from "./options"; import { queryData } from "../constants/queryHeaders"; import { flatten } from "../scripts/utils"; +import classnames from "classnames"; import css from "./options.scss"; @@ -55,7 +56,7 @@ export const YearsOptions: React.FC = (props) => { }; return ( -
+
{availableYearOptions.length === 0 ?
Please select attributes to see available years.
: diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 9e8589f..3ab0385 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -4,7 +4,7 @@ import { multiRegions } from "../constants/regionData"; import { connect } from "./connect"; import { cropOptions, fiftyStates } from "../constants/constants"; import { countyData } from "../constants/counties"; -import { getQueryParams } from "./utils"; +import { flatten, getQueryParams } from "./utils"; import { attrToCODAPColumnName } from "../constants/codapMetadata"; const baseURL = `https://quickstats.nass.usda.gov/api/api_GET/?key=9ED0BFB8-8DDD-3609-9940-A2341ED6A9E3`; @@ -91,42 +91,59 @@ export const createRequest = ({attribute, geographicLevel, location, year, cropC return req; }; -export const createTableFromSelections = async (selectedOptions: IStateOptions) => { +export const getAllAttrs = (selectedOptions: IStateOptions) => { const {geographicLevel, states, cropUnits, years, ...subOptions} = selectedOptions; - try { - const items = await getItems(selectedOptions); - if (items.length > 4000) { - - } - await connect.getNewDataContext(); - await connect.createTopCollection(geographicLevel); - const allAttrs: Array = ["Year"]; + const allAttrs: Array = ["Year"]; - for (const key in subOptions) { - const selections = subOptions[key as keyof typeof subOptions]; - for (const attribute of selections) { - const queryParams = getQueryParams(attribute); - if (!queryParams) { - throw new Error("Invalid attribute"); - } - const {short_desc} = queryParams; - if (Array.isArray(short_desc)) { - for (const desc of short_desc) { - const codapColumnName = attrToCODAPColumnName[desc].attributeNameInCodapTable; - allAttrs.push(codapColumnName); - } - } else if (typeof short_desc === "object" && cropUnits) { - const attr = short_desc[cropUnits as keyof ICropDataItem][0]; - allAttrs.push(attrToCODAPColumnName[attr].attributeNameInCodapTable); + for (const key in subOptions) { + const selections = subOptions[key as keyof typeof subOptions]; + for (const attribute of selections) { + const queryParams = getQueryParams(attribute); + if (!queryParams) { + throw new Error("Invalid attribute"); + } + const {short_desc} = queryParams; + if (Array.isArray(short_desc)) { + for (const desc of short_desc) { + const codapColumnName = attrToCODAPColumnName[desc].attributeNameInCodapTable; + allAttrs.push(codapColumnName); } + } else if (typeof short_desc === "object" && cropUnits) { + const attr = short_desc[cropUnits as keyof ICropDataItem][0]; + allAttrs.push(attrToCODAPColumnName[attr].attributeNameInCodapTable); } } + } + return allAttrs; +} + +export const getNumberOfItems = (selectedOptions: IStateOptions) => { + let {states, years} = selectedOptions; + const countySelected = selectedOptions.geographicLevel === "County"; + if (states[0] === "All States") { + states = fiftyStates; + } + if (countySelected) { + return flatten(states.map((state: string) => countyData[state])).length * years.length; + } else { + return states.length * years.length; + } +} +export const createTableFromSelections = async (selectedOptions: IStateOptions) => { + const {geographicLevel, states, cropUnits, years, ...subOptions} = selectedOptions; + try { + const allAttrs = getAllAttrs(selectedOptions); + const items = await getItems(selectedOptions); + await connect.getNewDataContext(); + await connect.createTopCollection(geographicLevel); await connect.createSubCollection(geographicLevel, allAttrs); await connect.createItems(items); await connect.makeCaseTableAppear(); return "success"; } catch (error) { + // eslint-disable-next-line no-console + console.log("Error creating CODAP Table from API data:", error); return error; } }; From 7ae7163e06e8e66bd07ca2417161fcffd88b49c1 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 20 Sep 2023 21:57:52 -0400 Subject: [PATCH 20/21] Return promises in one loop instead of in nested lopo. --- src/scripts/api.ts | 139 ++++++++++++++++++++------------------------- 1 file changed, 63 insertions(+), 76 deletions(-) diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 3ab0385..ac2fbde 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -18,13 +18,11 @@ interface IRequestParams { state?: string } -interface IGetAttrDataParams { - attribute: string, +interface IGetItemParams { + requestString: string, + countyOrState: string, geographicLevel: string, - cropUnits: string, - location: string, year: string - state?: string } export const fetchDataWithRetry = async (req: string, maxRetries = 3) => { @@ -134,7 +132,14 @@ export const createTableFromSelections = async (selectedOptions: IStateOptions) const {geographicLevel, states, cropUnits, years, ...subOptions} = selectedOptions; try { const allAttrs = getAllAttrs(selectedOptions); - const items = await getItems(selectedOptions); + const requests = getAllRequests(selectedOptions); + const promises = []; + for (const req of requests) { + const {requestString, year, countyOrState} = req; + const res = getItem({requestString, year, countyOrState, geographicLevel: req.geographicLevel}); + promises.push(res); + } + const items = await Promise.all(promises); await connect.getNewDataContext(); await connect.createTopCollection(geographicLevel); await connect.createSubCollection(geographicLevel, allAttrs); @@ -148,97 +153,79 @@ export const createTableFromSelections = async (selectedOptions: IStateOptions) } }; -const getItems = async (selectedOptions: IStateOptions) => { +const getAllRequests = (selectedOptions: IStateOptions) => { let {states, years} = selectedOptions; - const multipleStatesSelected = states.length > 1 || states[0] === "All States"; - const multipleYearsSelected = years.length > 1; const countySelected = selectedOptions.geographicLevel === "County"; if (states[0] === "All States") { states = fiftyStates; } - const promises = []; - - if (multipleStatesSelected) { - for (const state of states) { - if (multipleYearsSelected) { - for (const year of years) { - if (countySelected) { - const allCounties = countyData[state]; - for (const county of allCounties) { - const item = getDataForSingleYearAndState(selectedOptions, county, year, state); - promises.push(item); - } - } else { - const item = getDataForSingleYearAndState(selectedOptions, state, year); - promises.push(item); - } + const requests = []; + + for (const state of states) { + const locations = countySelected ? countyData[state] : [state]; + for (const year of years) { + for (const location of locations) { + const req = getRequest(selectedOptions, location, year, state); + if (req) { + requests.push(req); } - } else { - const item = getDataForSingleYearAndState(selectedOptions, state, years[0]); - promises.push(item); } } - } else { - if (countySelected) { - const allCounties = countyData[states[0]]; - for (const county of allCounties) { - const item = getDataForSingleYearAndState(selectedOptions, county, years[0], states[0]); - promises.push(item); - } - } else { - const item = getDataForSingleYearAndState(selectedOptions, states[0], years[0]); - promises.push(item); - } } - return await Promise.all(promises); -}; -const getDataForSingleYearAndState = async (selectedOptions: IStateOptions, countyOrState: string, year: string, state?: string) => { - const {geographicLevel, states, years, cropUnits, ...subOptions} = selectedOptions; + return requests; +}; - let item: any = { - [geographicLevel]: countyOrState, - "Year": year, - }; +const getItem = async ({requestString, countyOrState, geographicLevel, year}: IGetItemParams) => { + const attrData = await getAttrData(requestString); + if (Object.keys(attrData).length) { + return { + [geographicLevel]: countyOrState, + "Year": year, + ...attrData + }; + } +}; +const getRequest = (selectedOptions: IStateOptions, countyOrState: string, year: string, state?: string) => { + const {geographicLevel, states, years, cropUnits, ...subOptions} = selectedOptions; for (const key in subOptions) { const value = subOptions[key as keyof typeof subOptions]; - if (value && Array.isArray(value)) { - for (const attribute of value) { - const queryParams = getQueryParams(attribute); - const yearAvailable = queryParams?.years[geographicLevel].includes(year); - const isMultiStateRegion = queryParams?.geographicAreas[0] === "REGION : MULTI-STATE"; - const geoLevel = isMultiStateRegion ? "REGION : MULTI-STATE" : geographicLevel; - if (yearAvailable) { - let location = countyOrState; - if (isMultiStateRegion) { - const itemToCheck = state ? state : countyOrState; - const regData = multiRegions.find((region) => region.States.includes(itemToCheck)); - location = regData?.Region ? regData.Region : countyOrState; - } - const params: IGetAttrDataParams = {attribute, geographicLevel: geoLevel, location, year, cropUnits}; - if (geoLevel === "County") { - params.state = state; - } - const attrData = await getAttrData(params); - item = {...item, ...attrData}; + for (const attribute of value) { + const queryParams = getQueryParams(attribute); + const yearAvailable = queryParams?.years[geographicLevel].includes(year); + const isMultiStateRegion = queryParams?.geographicAreas[0] === "REGION : MULTI-STATE"; + const geoLevel = isMultiStateRegion ? "REGION : MULTI-STATE" : geographicLevel; + if (yearAvailable) { + let location = countyOrState; + if (isMultiStateRegion) { + const itemToCheck = state ? state : countyOrState; + const regData = multiRegions.find((region) => region.States.includes(itemToCheck)); + location = regData?.Region ? regData.Region : countyOrState; + } + const params: IRequestParams = {attribute, geographicLevel: geoLevel, location, year}; + if (geoLevel === "County") { + params.state = state; } + if (cropOptions.options.includes(attribute) && cropUnits) { + params.cropCategory = cropUnits as keyof ICropDataItem; + } + return { + requestString: createRequest(params), + countyOrState, + geographicLevel, + year + }; } } - } - return item; + } }; -const getAttrData = async (params: IGetAttrDataParams) => { - const {attribute, geographicLevel, location, year, cropUnits, state} = params; - const reqParams: IRequestParams = {attribute, geographicLevel, location, year, state}; - if (cropOptions.options.includes(attribute) && cropUnits) { - reqParams.cropCategory = cropUnits as keyof ICropDataItem; - } - const req = createRequest(reqParams); + +const getAttrData = async (req: string) => { const res = await fetchDataWithRetry(req); const values: any = {}; if (res) { @@ -249,7 +236,7 @@ const getAttrData = async (params: IGetAttrDataParams) => { }); } else { // eslint-disable-next-line no-console - console.log(`Error: did not receive response for item with these params: ${params}`); + console.log(`Error: did not receive response for this request:`, req); } return values; }; From 8a79770720a5974cfae355197519f5bc600b9e7d Mon Sep 17 00:00:00 2001 From: lublagg Date: Fri, 22 Sep 2023 16:04:59 -0400 Subject: [PATCH 21/21] Add information text to pop-up. --- src/components/app.tsx | 6 +++--- src/components/information.scss | 14 +++++++------- src/components/information.tsx | 18 +++++++++++++++++- src/components/warning.tsx | 12 ++++++++++-- src/scripts/api.ts | 6 +++--- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/components/app.tsx b/src/components/app.tsx index d510891..ef33265 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -54,12 +54,12 @@ function App() { const res = await createTableFromSelections(selectedOptions); if (res !== "success") { setStatusMessage("Fetch Error. Please retry."); - setStatusGraphic() + setStatusGraphic(); } else { setStatusMessage("Fetched data."); setStatusGraphic(); } - } + }; const handleGetData = async () => { const numberOfRows = getNumberOfItems(selectedOptions); @@ -75,7 +75,7 @@ function App() { if (getDataAnyway) { await getData(); } - } + }; return (
diff --git a/src/components/information.scss b/src/components/information.scss index 1aaf93b..071e851 100644 --- a/src/components/information.scss +++ b/src/components/information.scss @@ -2,8 +2,8 @@ .popUpContent { position: absolute; - left: 12px; - right: 12px; + width: 310px; + left: calc(50% - 155px); top: 35px; z-index: 3; @@ -17,6 +17,11 @@ max-height: 1000px; border-width: 1px; + .popUpBody { + background-color: white; + padding: 12px; + font-size: 13px; + } } .popUpFooter { @@ -35,9 +40,4 @@ left: 0; right: 0; bottom: 0; -} - -.popUpBody { - background-color: white; - padding: 12px; } \ No newline at end of file diff --git a/src/components/information.tsx b/src/components/information.tsx index ed61e25..610e13b 100644 --- a/src/components/information.tsx +++ b/src/components/information.tsx @@ -13,7 +13,23 @@ export const Information: React.FC = (props) => {
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ipsum velit, pellentesque eget turpis. +

+ Source: This data comes from the National Agricultural Statistics Service (NASS) supplied + by the U.S. Department of Agriculture. The NASS conducts hundreds of surveys every year, curates + and makes public the data collected, and prepares reports covering virtually every aspect of U.S. + agriculture. The data collected includes production and supplies of food, farm labor and wages, + and changes in the demographics of U.S. producers. +

+

Learn more about the NASS

+

Access the full NASS dataset

+

Read reports published by NASS

+

+ Acknowledgements: Brought to you by the DataPBL project, a collaboration between + Concord Consortium, EL Education, and the University of Colorado. This material is supported + by the National Science Foundation under Grant No. DRL-2200887. Any opinions, findings, and + conclusions or recommendations expressed in this material are those of the authors and + do not necessarily reflect the views of the NSF. +

diff --git a/src/components/warning.tsx b/src/components/warning.tsx index ad2f4f2..bd0e2f3 100644 --- a/src/components/warning.tsx +++ b/src/components/warning.tsx @@ -21,8 +21,16 @@ export const Warning: React.FC = (props) => {
- - + + + + + +
diff --git a/src/scripts/api.ts b/src/scripts/api.ts index ac2fbde..6238561 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -113,7 +113,7 @@ export const getAllAttrs = (selectedOptions: IStateOptions) => { } } return allAttrs; -} +}; export const getNumberOfItems = (selectedOptions: IStateOptions) => { let {states, years} = selectedOptions; @@ -126,10 +126,10 @@ export const getNumberOfItems = (selectedOptions: IStateOptions) => { } else { return states.length * years.length; } -} +}; export const createTableFromSelections = async (selectedOptions: IStateOptions) => { - const {geographicLevel, states, cropUnits, years, ...subOptions} = selectedOptions; + const {geographicLevel} = selectedOptions; try { const allAttrs = getAllAttrs(selectedOptions); const requests = getAllRequests(selectedOptions);