From 13254ca209e22987ffe0e4c05938e6ec8fe4e0af Mon Sep 17 00:00:00 2001 From: mwbernard Date: Thu, 14 Dec 2023 13:51:02 -0500 Subject: [PATCH 01/32] get food group stats on data load --- vacs-map-app/src/App.vue | 10 +++--- vacs-map-app/src/stores/cropYields.js | 44 ++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/vacs-map-app/src/App.vue b/vacs-map-app/src/App.vue index cc4df7d..8f52d42 100644 --- a/vacs-map-app/src/App.vue +++ b/vacs-map-app/src/App.vue @@ -23,16 +23,18 @@ const documentHeight = () => { } onMounted(() => { - gridStore.load() - cropYieldsStore.load() - cropInformationStore.load() + // need to load the crop information file first, then load other data + cropInformationStore.load().then(() => { + gridStore.load() + cropYieldsStore.load() + }) window.addEventListener('resize', documentHeight) documentHeight }) onUnmounted(() => { - window.removeEventListener('resize') + window.removeEventListener('resize', documentHeight) }) diff --git a/vacs-map-app/src/stores/cropYields.js b/vacs-map-app/src/stores/cropYields.js index 6fdf219..3e50a9b 100644 --- a/vacs-map-app/src/stores/cropYields.js +++ b/vacs-map-app/src/stores/cropYields.js @@ -1,12 +1,16 @@ import * as d3 from 'd3' import { ref } from 'vue' -import { defineStore } from 'pinia' +import { defineStore, storeToRefs } from 'pinia' import { getDataUrl } from '@/constants/data-load' +import { useCropInformationStore } from '@/stores/cropInformation' export const useCropYieldsStore = defineStore('cropYields', () => { const data = ref(null) const loading = ref(false) + const cropInformationStore = useCropInformationStore() + const { data: cropInfo } = storeToRefs(cropInformationStore) + const load = async () => { if (loading.value || data.value) return false loading.value = true @@ -17,6 +21,7 @@ export const useCropYieldsStore = defineStore('cropYields', () => { const yieldKeys = Object.keys(transformedData[0]).filter((k) => k.startsWith('yield')) + // add yield ratio columns for all crops and scenarios transformedData = transformedData.map((d, i) => { const rowWithYields = Object.fromEntries( Object.entries(d).filter(([k, v]) => v !== null) @@ -45,6 +50,43 @@ export const useCropYieldsStore = defineStore('cropYields', () => { return rowWithYields }) + // get max/min yield ratios (and which crops they correspond to) + // for each food group at each grid cell + const cropGroups = Array.from(new Set(cropInfo.value?.map((d) => d.crop_group))) + const futureScenarios = ['future_ssp126', 'future_ssp370'] + + transformedData = transformedData.map((d, i) => { + const rowWithGroupValues = { ...d } + futureScenarios.forEach((s) => { + cropGroups.forEach((g) => { + const groupYieldRatioKeys = cropInfo.value.filter((c) => c.crop_group === g).map((c) => { + return ['yieldratio', c.id, s].join('_') + }) + const obj = { + maxCrop: null, + minCrop: null, + maxVal: -10000, + minVal: 10000 + } + groupYieldRatioKeys.forEach((k) => { + if (d[k] && d[k] > obj.maxVal) { + obj.maxVal = d[k] + obj.maxCrop = k.split('_')[1] + } + if (d[k] && d[k] < obj.minVal) { + obj.minVal = d[k] + obj.minCrop = k.split('_')[1] + } + }) + if (obj.maxVal === -10000) obj.maxVal = null; + if (obj.minVal === 10000) obj.minVal = null; + const groupKey = [g,s].join('_') + rowWithGroupValues[groupKey] = obj; + }) + }) + return rowWithGroupValues; + }) + data.value = Object.freeze(transformedData) } From 635b530c840b90c4a3643de201925e36009f1db6 Mon Sep 17 00:00:00 2001 From: mwbernard Date: Thu, 14 Dec 2023 14:30:30 -0500 Subject: [PATCH 02/32] get max increase and min decrease --- vacs-map-app/src/stores/cropYields.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/vacs-map-app/src/stores/cropYields.js b/vacs-map-app/src/stores/cropYields.js index 3e50a9b..787eeda 100644 --- a/vacs-map-app/src/stores/cropYields.js +++ b/vacs-map-app/src/stores/cropYields.js @@ -65,8 +65,8 @@ export const useCropYieldsStore = defineStore('cropYields', () => { const obj = { maxCrop: null, minCrop: null, - maxVal: -10000, - minVal: 10000 + maxVal: null, + minVal: null } groupYieldRatioKeys.forEach((k) => { if (d[k] && d[k] > obj.maxVal) { @@ -78,8 +78,6 @@ export const useCropYieldsStore = defineStore('cropYields', () => { obj.minCrop = k.split('_')[1] } }) - if (obj.maxVal === -10000) obj.maxVal = null; - if (obj.minVal === 10000) obj.minVal = null; const groupKey = [g,s].join('_') rowWithGroupValues[groupKey] = obj; }) From 28b7255a37ca92e3764f7e88d9912767a44f5b13 Mon Sep 17 00:00:00 2001 From: mwbernard Date: Mon, 18 Dec 2023 10:25:07 -0500 Subject: [PATCH 03/32] Load group data once ready --- vacs-map-app/src/components/BaseMap.vue | 4 +- vacs-map-app/src/components/GridSource.vue | 2 +- vacs-map-app/src/stores/cropYields.js | 101 ++++++++++++--------- vacs-map-app/src/stores/joinedCropData.js | 32 +++---- 4 files changed, 76 insertions(+), 63 deletions(-) diff --git a/vacs-map-app/src/components/BaseMap.vue b/vacs-map-app/src/components/BaseMap.vue index 3a4f23a..0547e34 100644 --- a/vacs-map-app/src/components/BaseMap.vue +++ b/vacs-map-app/src/components/BaseMap.vue @@ -48,8 +48,8 @@ onMounted(() => { }) onUnmounted(() => { - map.value.remove(); -}); + map.value.remove() +}) watch(mapPadding, () => { if (mapPadding.value) map.value.setPadding(mapPadding.value) diff --git a/vacs-map-app/src/components/GridSource.vue b/vacs-map-app/src/components/GridSource.vue index 2e93688..d07f9d7 100644 --- a/vacs-map-app/src/components/GridSource.vue +++ b/vacs-map-app/src/components/GridSource.vue @@ -30,7 +30,7 @@ const cropYieldsStore = useCropYieldsStore() const gridStore = useGridStore() const joinedCropDataStore = useJoinedCropDataStore() -const { gridFeatureCollection } = storeToRefs(joinedCropDataStore); +const { gridFeatureCollection } = storeToRefs(joinedCropDataStore) onMounted(() => { gridStore.load() diff --git a/vacs-map-app/src/stores/cropYields.js b/vacs-map-app/src/stores/cropYields.js index 787eeda..7578bb4 100644 --- a/vacs-map-app/src/stores/cropYields.js +++ b/vacs-map-app/src/stores/cropYields.js @@ -1,5 +1,5 @@ import * as d3 from 'd3' -import { ref } from 'vue' +import { ref, watch } from 'vue' import { defineStore, storeToRefs } from 'pinia' import { getDataUrl } from '@/constants/data-load' import { useCropInformationStore } from '@/stores/cropInformation' @@ -8,9 +8,6 @@ export const useCropYieldsStore = defineStore('cropYields', () => { const data = ref(null) const loading = ref(false) - const cropInformationStore = useCropInformationStore() - const { data: cropInfo } = storeToRefs(cropInformationStore) - const load = async () => { if (loading.value || data.value) return false loading.value = true @@ -23,9 +20,7 @@ export const useCropYieldsStore = defineStore('cropYields', () => { // add yield ratio columns for all crops and scenarios transformedData = transformedData.map((d, i) => { - const rowWithYields = Object.fromEntries( - Object.entries(d).filter(([k, v]) => v !== null) - ) + const rowWithYields = Object.fromEntries(Object.entries(d).filter(([k, v]) => v !== null)) yieldKeys.forEach((k) => { const [_, crop, timeframe, scenario] = k.split('_') @@ -39,8 +34,13 @@ export const useCropYieldsStore = defineStore('cropYields', () => { const yieldRatioKey = ['yieldratio', crop, timeframe, scenario].join('_') let yieldRatioValue = null - if (rowWithYields[k] !== null && rowWithYields[historicalKey] !== null && rowWithYields[historicalKey]) { - yieldRatioValue = (rowWithYields[k] - rowWithYields[historicalKey]) / rowWithYields[historicalKey] + if ( + rowWithYields[k] !== null && + rowWithYields[historicalKey] !== null && + rowWithYields[historicalKey] + ) { + yieldRatioValue = + (rowWithYields[k] - rowWithYields[historicalKey]) / rowWithYields[historicalKey] } if (yieldRatioValue === null) return @@ -50,41 +50,6 @@ export const useCropYieldsStore = defineStore('cropYields', () => { return rowWithYields }) - // get max/min yield ratios (and which crops they correspond to) - // for each food group at each grid cell - const cropGroups = Array.from(new Set(cropInfo.value?.map((d) => d.crop_group))) - const futureScenarios = ['future_ssp126', 'future_ssp370'] - - transformedData = transformedData.map((d, i) => { - const rowWithGroupValues = { ...d } - futureScenarios.forEach((s) => { - cropGroups.forEach((g) => { - const groupYieldRatioKeys = cropInfo.value.filter((c) => c.crop_group === g).map((c) => { - return ['yieldratio', c.id, s].join('_') - }) - const obj = { - maxCrop: null, - minCrop: null, - maxVal: null, - minVal: null - } - groupYieldRatioKeys.forEach((k) => { - if (d[k] && d[k] > obj.maxVal) { - obj.maxVal = d[k] - obj.maxCrop = k.split('_')[1] - } - if (d[k] && d[k] < obj.minVal) { - obj.minVal = d[k] - obj.minCrop = k.split('_')[1] - } - }) - const groupKey = [g,s].join('_') - rowWithGroupValues[groupKey] = obj; - }) - }) - return rowWithGroupValues; - }) - data.value = Object.freeze(transformedData) } @@ -121,6 +86,54 @@ export const useCropYieldsStore = defineStore('cropYields', () => { ] } + const cropInformationStore = useCropInformationStore() + const { data: cropInfo } = storeToRefs(cropInformationStore) + + watch(cropInfo, () => { + // get max/min yield ratios (and which crops they correspond to) + // for each food group at each grid cell + const cropGroups = Array.from(new Set(cropInfo.value?.map((d) => d.crop_group))) + const futureScenarios = ['future_ssp126', 'future_ssp370'] + + const dataWithCropGroups = data.value.map((d, i) => { + const rowWithGroupValues = { ...d } + futureScenarios.forEach((s) => { + cropGroups.forEach((g) => { + const groupYieldRatioKeys = cropInfo.value + .filter((c) => c.crop_group === g) + .map((c) => { + return ['yieldratio', c.id, s].join('_') + }) + const rowHasYieldRatios = Object.keys(d).filter((k) => + groupYieldRatioKeys.includes(k) + ).length + if (!rowHasYieldRatios) return + + const obj = { + maxCrop: 'none', + minCrop: 'none', + maxVal: null, + minVal: null + } + groupYieldRatioKeys.forEach((k) => { + if (d[k] && d[k] > obj.maxVal) { + obj.maxVal = d[k] + obj.maxCrop = k.split('_')[1] + } + if (d[k] && d[k] < obj.minVal) { + obj.minVal = d[k] + obj.minCrop = k.split('_')[1] + } + }) + const groupKey = [g, s].join('_') + rowWithGroupValues[groupKey] = obj + }) + }) + return rowWithGroupValues + }) + data.value = dataWithCropGroups + }) + return { data, loading, diff --git a/vacs-map-app/src/stores/joinedCropData.js b/vacs-map-app/src/stores/joinedCropData.js index f0d636c..550094b 100644 --- a/vacs-map-app/src/stores/joinedCropData.js +++ b/vacs-map-app/src/stores/joinedCropData.js @@ -1,38 +1,38 @@ -import { computed } from 'vue'; +import { computed } from 'vue' import { defineStore, storeToRefs } from 'pinia' import { point, featureCollection } from '@turf/helpers' -import { useCropYieldsStore } from '@/stores/cropYields'; -import { useGridStore } from '@/stores/grid'; +import { useCropYieldsStore } from '@/stores/cropYields' +import { useGridStore } from '@/stores/grid' export const useJoinedCropDataStore = defineStore('joinedCropData', () => { - const gridStore = useGridStore(); - const cropYieldsStore = useCropYieldsStore(); + const gridStore = useGridStore() + const cropYieldsStore = useCropYieldsStore() - const { data: cropYieldsData } = storeToRefs(cropYieldsStore); - const { data: gridData } = storeToRefs(gridStore); + const { data: cropYieldsData } = storeToRefs(cropYieldsStore) + const { data: gridData } = storeToRefs(gridStore) // Provide the feature collection of grid data in one unified place so we // don't have to recalculate it const gridFeatureCollection = computed(() => { - if (!(gridData.value && cropYieldsData.value)) return featureCollection([]); + if (!(gridData.value && cropYieldsData.value)) return featureCollection([]) const gridMap = Object.fromEntries(gridData.value.map((row) => [row.id, row])) const yieldMap = Object.fromEntries(cropYieldsData.value.map((row) => [row.id, row])) - const points = Object.keys(gridMap).map(id => { - const g = gridMap[id]; + const points = Object.keys(gridMap).map((id) => { + const g = gridMap[id] return point( [g.X, g.Y], { ...g, - ...yieldMap[id], + ...yieldMap[id] }, { id } - ); - }); + ) + }) return Object.freeze(featureCollection(points)) - }); + }) - return { gridFeatureCollection }; -}); + return { gridFeatureCollection } +}) From 2bf4fd3b55ed470760d2f87bfdb401a538df28c2 Mon Sep 17 00:00:00 2001 From: mwbernard Date: Mon, 18 Dec 2023 10:26:01 -0500 Subject: [PATCH 04/32] Add crop group map options --- vacs-map-app/src/components/GridOverlay.vue | 60 ++++++++++++++++++- .../MapContainerColorAcrossScenarios.vue | 36 +++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/vacs-map-app/src/components/GridOverlay.vue b/vacs-map-app/src/components/GridOverlay.vue index ecc66ea..b5b30a7 100644 --- a/vacs-map-app/src/components/GridOverlay.vue +++ b/vacs-map-app/src/components/GridOverlay.vue @@ -13,6 +13,26 @@ const props = defineProps({ default: '' }, + useCropGroupMap: { + type: Boolean, + default: false + }, + + cropGroupColumn: { + type: String, + default: '' + }, + + cropGroupCrops: { + type: Array, + default: () => [] + }, + + cropGroupMetric: { + type: String, + default: 'max' + }, + colorColumn: { type: String, default: '' @@ -71,6 +91,10 @@ const props = defineProps({ const { id, + useCropGroupMap, + cropGroupColumn, + cropGroupCrops, + cropGroupMetric, colorColumn, colorColumnExtent, colorColumnQuintiles, @@ -88,7 +112,7 @@ const mapExploreStore = useMapExploreStore() const { hoveredId } = storeToRefs(mapExploreStore) const colorStore = useColorStore() -const { diverging: divergingScheme } = storeToRefs(colorStore) +const { diverging: divergingScheme, ordinal: ordinalScheme } = storeToRefs(colorStore) const addLayer = () => { if (!map.value || !mapReady.value || map.value.getLayer(id.value)) return @@ -278,6 +302,27 @@ const getCircleColorDiverging = (extent, center) => { ] } +const getCircleColorByCrop = () => { + if (!cropGroupColumn.value) return 'transparent' + + const cases = ['case'] + .concat( + cropGroupCrops.value + .map((crop, i) => { + return [ + ['==', ['get', cropGroupMetric.value + 'Crop', ['get', cropGroupColumn.value]], crop], + ordinalScheme.value[i] + ] + }) + .flat() + ) + .concat(['#777']) + + return ['case', ['!=', ['get', cropGroupColumn.value], null], cases, 'transparent'] + + return cases +} + const getCircleFillColor = () => { if (!fill.value) return 'transparent' return getCircleColor() @@ -285,10 +330,15 @@ const getCircleFillColor = () => { const getCircleStrokeColor = () => { if (!stroke.value) return 'transparent' + return getCircleColor() } const getCircleColor = () => { + if (useCropGroupMap.value) { + return getCircleColorByCrop() + } + if (colorDiverging.value) { // < 0, decrease // 0 = no change @@ -322,6 +372,14 @@ watch(divergingScheme, () => { updateLayer() }) +watch(useCropGroupMap, () => { + updateLayer() +}) + +watch(cropGroupMetric, () => { + updateLayer() +}) + watch(hoveredId, (current, prev) => { updateHoveredFeatureState(prev, false) updateHoveredFeatureState(current, true) diff --git a/vacs-map-app/src/components/MapContainerColorAcrossScenarios.vue b/vacs-map-app/src/components/MapContainerColorAcrossScenarios.vue index 97b247c..97229c0 100644 --- a/vacs-map-app/src/components/MapContainerColorAcrossScenarios.vue +++ b/vacs-map-app/src/components/MapContainerColorAcrossScenarios.vue @@ -16,6 +16,10 @@ { if (!selectedMetric.value || !selectedCrop.value || !selectedModel.value) { return null @@ -56,6 +68,30 @@ const selectedColumn = computed(() => { return [selectedMetric.value, selectedCrop.value, selectedModel.value].join('_') }) +const selectedCropGroup = computed(() => { + if (!cropInfo.value || !selectedCrop.value) { + return null + } + + return cropInfo.value.find((c) => c.id === selectedCrop.value).crop_group +}) + +const cropGroupColumn = computed(() => { + if (!selectedCropGroup.value || !selectedModel.value) { + return null + } + + return [selectedCropGroup.value, selectedModel.value].join('_') +}) + +const cropGroupCrops = computed(() => { + if (!cropInfo.value || !selectedCropGroup.value) { + return null + } + + return cropInfo.value.filter((c) => c.crop_group === selectedCropGroup.value).map((c) => c.id) +}) + // to get extent across scenarios const scenarios = computed(() => { if (selectedMetric.value === 'yieldratio') { From 18b6d0aa055ce3f04e0413dddbf9dabbc9dcb013 Mon Sep 17 00:00:00 2001 From: mwbernard Date: Mon, 18 Dec 2023 10:26:25 -0500 Subject: [PATCH 05/32] Load data better way --- vacs-map-app/src/App.vue | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/vacs-map-app/src/App.vue b/vacs-map-app/src/App.vue index 8f52d42..ba0919b 100644 --- a/vacs-map-app/src/App.vue +++ b/vacs-map-app/src/App.vue @@ -23,11 +23,9 @@ const documentHeight = () => { } onMounted(() => { - // need to load the crop information file first, then load other data - cropInformationStore.load().then(() => { - gridStore.load() - cropYieldsStore.load() - }) + cropInformationStore.load() + gridStore.load() + cropYieldsStore.load() window.addEventListener('resize', documentHeight) documentHeight From e4ec2e156f7996602bcd8fe1c89c11360f9856bf Mon Sep 17 00:00:00 2001 From: mwbernard Date: Mon, 18 Dec 2023 10:26:58 -0500 Subject: [PATCH 06/32] Add simple temporary UI --- vacs-map-app/src/components/ExploreSidebar.vue | 14 +++++++++++--- vacs-map-app/src/stores/mapExplore.js | 5 ++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/vacs-map-app/src/components/ExploreSidebar.vue b/vacs-map-app/src/components/ExploreSidebar.vue index 5c117d4..e25c435 100644 --- a/vacs-map-app/src/components/ExploreSidebar.vue +++ b/vacs-map-app/src/components/ExploreSidebar.vue @@ -29,7 +29,11 @@