Skip to content

Commit 046b8cf

Browse files
committed
NASAEARTH-16 Merge branch 'main' into NASAEARTH-16-time-core-graph
2 parents faab628 + 281b2fe commit 046b8cf

File tree

7 files changed

+299
-83
lines changed

7 files changed

+299
-83
lines changed

playwright/in-codap.spec.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,15 @@ test("App inside of CODAP", async ({ baseURL, page }) => {
7777
await expect(page.getByTestId("case-table")).toHaveCount(1);
7878
await expect(page.getByTestId("codap-slider")).toHaveCount(1);
7979
await expect(page.getByTestId("codap-map")).toHaveCount(1);
80+
await expect(page.getByTestId("codap-graph")).toHaveCount(1);
8081

81-
// Need to wait for fix in CODAP to get the correct graph
8282
//make sure the graph opens and have the correct axes attributes
83-
// await expect(page.getByTestId("codap-graph")).toHaveCount(1);
84-
// await expect(page.locator(".Graph-title-bar")).toContainText("Rainfall Chart");
85-
// await expect(page.getByTestId("axis-legend-attribute-button-bottom")).toContainText("label");
86-
// await expect(page.getByTestId("axis-legend-attribute-button-left")).toContainText("date");
87-
// await expect(page.getByTestId("axis-legend-attribute-button-legend")).toContainText("color");
83+
await expect(page.locator(".Graph-title-bar").nth(0)).toContainText("Rainfall Plot");
84+
await expect(page.getByTestId("axis-legend-attribute-button-bottom").nth(0)).toContainText("date");
85+
await expect(page.getByTestId("axis-legend-attribute-button-left").nth(0)).toContainText("value");
86+
await expect(page.getByTestId("axis-legend-attribute-button-legend").nth(0)).toContainText("label");
87+
page.getByTestId("codap-graph").nth(0).click({ position: { x: 10, y: 10 } });
88+
await expect(page.getByTestId("graph-display-values-button")).toBeVisible();
89+
page.getByTestId("graph-display-values-button").nth(0).click();
90+
await expect(page.getByTestId("adornment-checkbox-connecting-lines")).toBeChecked();
8891
});

src/components/app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect } from "react";
2+
import { initializeNeoPlugin } from "../models/plugin-state";
23
import { isNonEmbedded } from "../utils/embed-check";
3-
import { initializeNeoPlugin } from "../utils/codap-utils";
44
import { TabContainer } from "./tabs/tab-container";
55
import { Provider } from "./ui/provider";
66

src/components/tabs/dataset-tab.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
} from "@concord-consortium/codap-plugin-api";
66
import { reaction } from "mobx";
77
import { observer } from "mobx-react-lite";
8+
import { kDataContextName } from "../../data/constants";
89
import { kNeoDatasets } from "../../models/neo-datasets";
9-
import { kDataContextName, dataManager } from "../../models/data-manager";
10+
import { dataManager } from "../../models/data-manager";
1011
import { pluginState } from "../../models/plugin-state";
1112
import { isNonEmbedded } from "../../utils/embed-check";
1213
import { DatasetSelector } from "../dataset-selector/dataset-selector";

src/data/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export const kPinLatAttributeName = "pinLat";
1010
export const kPinLongAttributeName = "pinLong";
1111
export const kPinColorAttributeName = "pinColor";
1212

13+
export const kDataContextName = "NEOPluginData";
14+
export const kXYGraphName = "NEOPlugin XY Graph";
15+
export const kChartGraphName = "NEOPlugin Chart Graph";
16+
export const kMapPinsCollectionName = "Map Pins";
17+
1318
export const kMapName = "NeoMap";
1419

1520
export const kSliderComponentName = "NeoSlider";

src/models/data-manager.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,15 @@ import {
1111
sendMessage,
1212
} from "@concord-consortium/codap-plugin-api";
1313
import { decodePng } from "@concord-consortium/png-codec";
14-
import { kPinColorAttributeName } from "../data/constants";
15-
import { createGraph, createOrUpdateDateSlider, createOrUpdateMap, addConnectingLinesToGraph,
16-
deleteExistingGraphs, addRegionOfInterestToGraphs,
17-
updateGraphRegionOfInterest} from "../utils/codap-utils";
14+
import { kChartGraphName, kDataContextName, kMapPinsCollectionName, kXYGraphName } from "../data/constants";
15+
import { createOrUpdateGraphs, createOrUpdateDateSlider, createOrUpdateMap, addConnectingLinesToGraph,
16+
addRegionOfInterestToGraphs, updateGraphRegionOfInterest, updateLocationColorMap, rescaleGraph
17+
} from "../utils/codap-utils";
1818
import { GeoImage } from "./geo-image";
1919
import { NeoDataset, NeoImageInfo } from "./neo-types";
2020
import { kImageLoadDelay, kMaxSerialImages, kParallelLoad } from "./config";
2121
import { pinLabel, pluginState } from "./plugin-state";
2222

23-
export const kDataContextName = "NEOPluginData";
24-
const kMapPinsCollectionName = "Map Pins";
2523
const kDatesCollectionName = "Available Dates";
2624

2725
async function clearExistingCases(): Promise<void> {
@@ -152,13 +150,16 @@ export class DataManager {
152150
};
153151

154152
pluginState.pins.forEach(pin => {
155-
const color = geoImage.extractColor(pin.lat, pin.long);
153+
const extractedColor = geoImage.extractColor(pin.lat, pin.long);
156154
const label = pinLabel(pin);
157-
const paletteIndex = this.reversePalette?.[GeoImage.rgbToNumber(color)] ?? -1;
155+
const paletteIndex = this.reversePalette?.[GeoImage.rgbToNumber(extractedColor)] ?? -1;
156+
const paletteValue = neoDataset.paletteToValue(paletteIndex);
157+
const color = paletteValue === null ? {r: 148, g: 148, b: 148} : extractedColor;
158+
158159
neoDatasetImage.pins[label] = {
159160
color: GeoImage.rgbToHex(color),
160161
paletteIndex,
161-
value: neoDataset.paletteToValue(paletteIndex),
162+
value: paletteValue,
162163
label,
163164
pinColor: pin.color,
164165
};
@@ -285,21 +286,38 @@ export class DataManager {
285286
});
286287
});
287288

289+
// FIXME: Change pin lat lon to geoname
290+
const pinColorMap: Record<string, string> = {};
291+
pluginState.pins.forEach(pin => {
292+
pinColorMap[`${parseFloat(pin.lat.toFixed(2))}, ${parseFloat(pin.long.toFixed(2))}`] = pin.color;
293+
});
294+
288295
await updateDataContextTitle(neoDataset.label);
289296
await this.createMapPinsCollection();
290297
await this.createDatesChildCollection();
291298
await clearExistingCases();
292-
await deleteExistingGraphs();
293299
await createItems(kDataContextName, items);
294300
await createTable(kDataContextName);
301+
// The codap-plugin-api does not apply colormap property to attributes
302+
// so we update the attribute after the collection is created
303+
await updateLocationColorMap(pinColorMap);
295304
// We can't add the connecting lines on the first graph creation so we update it later
296-
await createGraph(kDataContextName, `${neoDataset.label} Plot`,
297-
{xAttrName: "date", yAttrName: "value", legendAttrName: kPinColorAttributeName});
305+
await createOrUpdateGraphs(kDataContextName,
306+
[ { name: kXYGraphName,
307+
title: `${neoDataset.label} Plot`,
308+
xAttrName: "date",
309+
yAttrName: "value",
310+
legendAttrName: "label"
311+
}
312+
]);
298313
await this.createOrUpdateSlider();
299314
await this.updateMapAndGraphs();
300-
await addConnectingLinesToGraph(kDataContextName, `${neoDataset.label} Plot`,{showConnectingLines: true});
315+
await addConnectingLinesToGraph();
301316
const roiPosition = getTimestamp(this.loadedImages[0]);
302-
await addRegionOfInterestToGraphs(kDataContextName, neoDataset.label, roiPosition);
317+
await addRegionOfInterestToGraphs(roiPosition);
318+
await rescaleGraph(kXYGraphName);
319+
await rescaleGraph(kChartGraphName);
320+
303321
} catch (error) {
304322
console.error("Failed to process dataset:", error);
305323
throw error;
@@ -341,7 +359,7 @@ export class DataManager {
341359
// to the slider movement.
342360
if (!skipGraphs && pins.length > 0) {
343361
const startTime = getTimestamp(item);
344-
await updateGraphRegionOfInterest(kDataContextName, neoDataset.label, startTime);
362+
await updateGraphRegionOfInterest(kDataContextName, startTime);
345363
}
346364

347365
await createOrUpdateMap(`${neoDataset.label} - ${item.date}`, item.url);
@@ -381,8 +399,10 @@ export class DataManager {
381399
}
382400

383401
private async createMapPinsCollection(): Promise<void> {
402+
// The codap-plugin-api does not support colormap property to attributes
403+
// so we update the attribute after the collection is created
384404
await createParentCollection(kDataContextName, kMapPinsCollectionName, [
385-
{ name: "label" },
405+
{ name: "label", type: "categorical"},
386406
{ name: "pinColor", type: "color" }
387407
]);
388408
}

src/models/plugin-state.ts

Lines changed: 161 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,67 @@
1-
import { getAllItems, IResult } from "@concord-consortium/codap-plugin-api";
2-
import { makeAutoObservable } from "mobx";
1+
import { addDataContextChangeListener, getAllItems, getCaseByID, getCaseBySearch, getSelectionList, initializePlugin,
2+
IResult, sendMessage } from "@concord-consortium/codap-plugin-api";
3+
import { makeAutoObservable, reaction } from "mobx";
34
import {
4-
kPinColorAttributeName, kPinDataContextName, kPinLatAttributeName, kPinLongAttributeName
5+
kDataContextName,
6+
kInitialDimensions,
7+
kMapPinsCollectionName,
8+
kPinColorAttributeName, kPinDataContextName, kPinLatAttributeName, kPinLongAttributeName,
9+
kPluginName,
10+
kVersion
511
} from "../data/constants";
12+
import { createOrUpdateMap, createSelectionList, deleteSelectionList, updateSelectionList } from "../utils/codap-utils";
613
import { NeoDataset } from "./neo-types";
714

8-
interface IMapPin {
15+
export interface IMapPin {
916
color: string;
1017
id: string;
1118
lat: number;
1219
long: number;
1320
}
1421

22+
export interface ILocationCase {
23+
label: string;
24+
pinColor: string;
25+
}
26+
1527
export function pinLabel(pin: IMapPin) {
1628
return `${pin.lat.toFixed(2)}, ${pin.long.toFixed(2)}`;
1729
}
1830

31+
1932
class PluginState {
2033
neoDataset: NeoDataset | undefined;
2134
neoDatasetName = "";
2235
pins: IMapPin[] = [];
36+
selectedPins: IMapPin[] = [];
37+
selectedCases: any[] =[];
2338

2439
constructor() {
2540
makeAutoObservable(this);
41+
// Reaction to changes in selectedPins from MapPinDataContext
42+
reaction(
43+
() => this.selectedPins,
44+
(selectedPins) => {
45+
this.handleSelectionChange(
46+
selectedPins,
47+
kDataContextName,
48+
kMapPinsCollectionName,
49+
(pin) => `label == ${pinLabel(pin)}`
50+
);
51+
}
52+
);
53+
// Reaction to changes in selectedCases NEOPluginDataContext
54+
reaction(
55+
() => this.selectedCases,
56+
(selectedCases) => {
57+
this.handleSelectionChange(
58+
selectedCases,
59+
kPinDataContextName,
60+
kMapPinsCollectionName,
61+
(sCase) => `pinColor == ${sCase.pinColor}`
62+
);
63+
}
64+
);
2665
}
2766

2867
setNeoDataset(neoDataset: NeoDataset | undefined) {
@@ -45,6 +84,124 @@ class PluginState {
4584
});
4685
}
4786
}
87+
88+
setSelectedPins(selectedPins: IMapPin[]) {
89+
this.selectedPins = selectedPins;
90+
}
91+
92+
setSelectedCases(selectedCases: any[]) {
93+
this.selectedCases = selectedCases;
94+
}
95+
96+
async handleSelectionChange<T>(
97+
selectedItems: T[], dataContextName: string, collectionName: string, searchQueryFn: (item: T) => string
98+
): Promise<void> {
99+
if (selectedItems.length === 0) {
100+
deleteSelectionList(dataContextName);
101+
return;
102+
}
103+
104+
for (const item of selectedItems) {
105+
const searchQuery = searchQueryFn(item);
106+
const result = await getCaseBySearch(dataContextName, collectionName, searchQuery);
107+
108+
if (result.success) {
109+
const selectedItemIds = result.values.map((val: any) => val.id);
110+
if (selectedItems.length === 1) {
111+
createSelectionList(dataContextName, selectedItemIds);
112+
return;
113+
} else {
114+
const updateSelection = await updateSelectionList(dataContextName, selectedItemIds);
115+
if (!updateSelection.success) {
116+
createSelectionList(dataContextName, selectedItemIds);
117+
}
118+
}
119+
}
120+
}
121+
}
122+
}
123+
124+
export async function initializeNeoPlugin() {
125+
await initializePlugin({ pluginName: kPluginName, version: kVersion, dimensions: kInitialDimensions });
126+
127+
// Create the pin dataset
128+
await sendMessage("create", `dataContext`, {
129+
name: kPinDataContextName,
130+
collections: [
131+
{
132+
name: "Map Pins",
133+
attrs: [
134+
{ name: kPinLatAttributeName, type: "numeric" },
135+
{ name: kPinLongAttributeName, type: "numeric" },
136+
{ name: kPinColorAttributeName, type: "color" }
137+
]
138+
}
139+
]
140+
});
141+
142+
// Create map if it doesn't exist
143+
await createOrUpdateMap("Map");
144+
145+
// See if there are any existing pins
146+
pluginState.updatePins();
147+
148+
// Set up a listener for changes to the pin dataset
149+
addDataContextChangeListener(kPinDataContextName, notification => {
150+
const { operation } = notification.values;
151+
152+
if (["createCases", "deleteCases", "updateCases"].includes(operation)) {
153+
pluginState.updatePins();
154+
}
155+
});
156+
// Set up a listener for pin selection
157+
addDataContextChangeListener(kPinDataContextName, async notification => {
158+
const { operation, result } = notification.values;
159+
if (operation === "selectCases" && result.success) {
160+
const selectedPins = await getSelectionList(kPinDataContextName);
161+
const selectedPinValues: IMapPin[] = await Promise.all(
162+
selectedPins.values.map(async (pin: any) => {
163+
const pinItem = await getCaseByID(kPinDataContextName, pin.caseID);
164+
if (pinItem.success) {
165+
const pinValues = pinItem.values;
166+
const pinCase = (pinValues as any).case;
167+
return {
168+
id: pinCase.id,
169+
lat: pinCase.values.pinLat,
170+
long: pinCase.values.pinLong,
171+
color: pinCase.values.pinColor,
172+
};
173+
}
174+
return null;
175+
})
176+
);
177+
pluginState.setSelectedPins(selectedPinValues);
178+
}
179+
});
180+
181+
// Set up a listener for case selection
182+
addDataContextChangeListener(kDataContextName, async notification => {
183+
const { operation, result } = notification.values;
184+
if (operation === "selectCases" && result.success) {
185+
const selectedCases = await getSelectionList(kDataContextName);
186+
const selectedPinCases = selectedCases.values
187+
.filter((sCase: any) => sCase.collectionName === kMapPinsCollectionName);
188+
const selectedCaseValues: any[] = await Promise.all(
189+
selectedPinCases.map(async (sCase: any) => {
190+
const caseItem = await getCaseByID(kDataContextName, sCase.caseID);
191+
if (caseItem.success) {
192+
const caseValues = caseItem.values;
193+
return {
194+
id: caseValues.id,
195+
label: caseValues.case.values.label,
196+
pinColor: caseValues.case.values.pinColor,
197+
};
198+
}
199+
return null;
200+
})
201+
);
202+
pluginState.setSelectedCases(selectedCaseValues);
203+
}
204+
});
48205
}
49206

50207
export const pluginState = new PluginState();

0 commit comments

Comments
 (0)