Skip to content

Commit

Permalink
188646708 v3 Update Graph API (#1675)
Browse files Browse the repository at this point in the history
* Include attribute ids in get requests.

* Allow attributes to be specified by id when creating graph.

* Support update graph requests.
  • Loading branch information
tealefristoe authored Dec 11, 2024
1 parent 1b89e7b commit a001527
Show file tree
Hide file tree
Showing 13 changed files with 421 additions and 127 deletions.
150 changes: 134 additions & 16 deletions v3/src/components/graph/component-handler-graph.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// TODO Rename this file after the PR is approved

import { getSnapshot } from "mobx-state-tree"
import { IBaseNumericAxisModel } from "../axis/models/axis-model"
import { V2GetGraph } from "../../data-interactive/data-interactive-component-types"
import { V2GetGraph, V2Graph } from "../../data-interactive/data-interactive-component-types"
import { DIComponentInfo } from "../../data-interactive/data-interactive-types"
import { diComponentHandler } from "../../data-interactive/handlers/component-handler"
import { setupTestDataset, testCases } from "../../data-interactive/handlers/handler-test-utils"
import { testGetComponent } from "../../data-interactive/handlers/component-handler-test-utils"
import { appState } from "../../models/app-state"
import { toV3Id } from "../../utilities/codap-utils"
import { toV2Id, toV3Id } from "../../utilities/codap-utils"
import { kGraphIdPrefix } from "./graph-defs"
import "./graph-registration"
import { IGraphContentModel, isGraphContentModel } from "./models/graph-content-model"
Expand All @@ -21,6 +23,7 @@ describe("DataInteractive ComponentHandler Graph", () => {
const a1 = dataset.getAttributeByName("a1")!
const a2 = dataset.getAttributeByName("a2")!
const a3 = dataset.getAttributeByName("a3")!
const a4 = dataset.getAttributeByName("a4")!

it("create and get graph work", async () => {
// Create a graph tile with no options
Expand All @@ -46,9 +49,44 @@ describe("DataInteractive ComponentHandler Graph", () => {
type: "graph", dataContext: "data", rightSplitAttributeName: "a3", xAttributeName: "a3"
}).success).toBe(false)

// Create a graph with ids
const resultIds = handler.create!({}, {
type: "graph", cannotClose: true, dataContext: "data", xAttributeID: toV2Id(a3.id), yAttributeID: toV2Id(a2.id),
legendAttributeID: toV2Id(a1.id), captionAttributeID: toV2Id(a2.id), rightNumericAttributeID: toV2Id(a3.id),
rightSplitAttributeID: toV2Id(a1.id), topSplitAttributeID: toV2Id(a2.id), topSplitAttributeName: "a3",
enableNumberToggle: true, numberToggleLastMode: true
})
expect(resultIds.success).toBe(true)
expect(documentContent.tileMap.size).toBe(1)
const resultIdsValues = resultIds.values as DIComponentInfo
const tileIds = documentContent.tileMap.get(toV3Id(kGraphIdPrefix, resultIdsValues.id!))!
expect(tileIds).toBeDefined()
expect(isGraphContentModel(tileIds.content)).toBe(true)
const tileContentIds = tileIds.content as IGraphContentModel
expect(tileIds.cannotClose).toBe(true)
expect(tileContentIds.dataConfiguration.attributeDescriptionForRole("x")?.attributeID).toBe(a3.id)
expect(tileContentIds.dataConfiguration.attributeDescriptionForRole("y")?.attributeID).toBe(a2.id)
expect(tileContentIds.dataConfiguration.attributeDescriptionForRole("legend")?.attributeID).toBe(a1.id)
expect(tileContentIds.dataConfiguration.attributeDescriptionForRole("caption")?.attributeID).toBe(a2.id)
expect(tileContentIds.dataConfiguration.attributeDescriptionForRole("rightNumeric")?.attributeID).toBe(a3.id)
expect(tileContentIds.dataConfiguration.attributeDescriptionForRole("rightSplit")?.attributeID).toBe(a1.id)
// Id should trump name for topSplit
expect(tileContentIds.dataConfiguration.attributeDescriptionForRole("topSplit")?.attributeID).toBe(a2.id)
expect(tileContentIds.showParentToggles).toBe(true)
// Make sure numberToggleLastMode hid all appropriate cases
tileContentIds.layers.forEach(layer => {
const lastCaseId = dataset.itemIds[dataset.itemIds.length - 1]
dataset.itemIds.forEach(itemId => {
expect(layer.dataConfiguration.hiddenCases.includes(itemId)).toBe(itemId !== lastCaseId)
})
})
// Delete the graph when we're finished
handler.delete!({ component: tileIds })
expect(documentContent.tileMap.size).toBe(0)

// Create a graph with options
const result = handler.create!({}, {
type: "graph", cannotClose: true, dataContext: "data", xAttributeName: "a3", yAttributeName: "a2",
type: "graph", cannotClose: true, dataContext: "data", xAttributeName: "a3", yAttributeName: "a4",
legendAttributeName: "a1", captionAttributeName: "a2", rightNumericAttributeName: "a3",
rightSplitAttributeName: "a1", topSplitAttributeName: "a2", enableNumberToggle: true, numberToggleLastMode: true
})
Expand All @@ -59,14 +97,15 @@ describe("DataInteractive ComponentHandler Graph", () => {
expect(tile).toBeDefined()
expect(isGraphContentModel(tile.content)).toBe(true)
const tileContent = tile.content as IGraphContentModel
const dataConfig = tileContent.dataConfiguration
expect(tile.cannotClose).toBe(true)
expect(tileContent.dataConfiguration.attributeDescriptionForRole("x")?.attributeID).toBe(a3.id)
expect(tileContent.dataConfiguration.attributeDescriptionForRole("y")?.attributeID).toBe(a2.id)
expect(tileContent.dataConfiguration.attributeDescriptionForRole("legend")?.attributeID).toBe(a1.id)
expect(tileContent.dataConfiguration.attributeDescriptionForRole("caption")?.attributeID).toBe(a2.id)
expect(tileContent.dataConfiguration.attributeDescriptionForRole("rightNumeric")?.attributeID).toBe(a3.id)
expect(tileContent.dataConfiguration.attributeDescriptionForRole("rightSplit")?.attributeID).toBe(a1.id)
expect(tileContent.dataConfiguration.attributeDescriptionForRole("topSplit")?.attributeID).toBe(a2.id)
expect(dataConfig.attributeDescriptionForRole("x")?.attributeID).toBe(a3.id)
expect(dataConfig.attributeDescriptionForRole("y")?.attributeID).toBe(a4.id)
expect(dataConfig.attributeDescriptionForRole("legend")?.attributeID).toBe(a1.id)
expect(dataConfig.attributeDescriptionForRole("caption")?.attributeID).toBe(a2.id)
expect(dataConfig.attributeDescriptionForRole("rightNumeric")?.attributeID).toBe(a3.id)
expect(dataConfig.attributeDescriptionForRole("rightSplit")?.attributeID).toBe(a1.id)
expect(dataConfig.attributeDescriptionForRole("topSplit")?.attributeID).toBe(a2.id)
expect(tileContent.showParentToggles).toBe(true)
// Make sure numberToggleLastMode hid all appropriate cases
tileContent.layers.forEach(layer => {
Expand All @@ -76,12 +115,87 @@ describe("DataInteractive ComponentHandler Graph", () => {
})
})

// Update a graph's axis bounds
const xAxis = tileContent.getAxis("bottom") as IBaseNumericAxisModel
const yAxis = tileContent.getAxis("left") as IBaseNumericAxisModel
const y2Axis = tileContent.getAxis("rightNumeric") as IBaseNumericAxisModel
expect(xAxis.min).toBe(-.5)
expect(xAxis.max).toBe(7.5)
expect(yAxis.min).toBe(-6.5)
expect(yAxis.max).toBe(1.5)
expect(y2Axis.min).toBe(-.5)
expect(y2Axis.max).toBe(7.5)
const updateBoundsResult = handler.update!({ component: tile }, {
xLowerBound: 2, xUpperBound: 6, yLowerBound: -20, yUpperBound: 10, y2LowerBound: -3, y2UpperBound: 13
})
expect(updateBoundsResult.success).toBe(true)
expect(xAxis.min).toBe(2)
expect(xAxis.max).toBe(6)
expect(yAxis.min).toBe(-20)
expect(yAxis.max).toBe(10)
expect(y2Axis.min).toBe(-3)
expect(y2Axis.max).toBe(13)

// Update a graph to switch attributes
const updateResult1 = handler.update!({ component: tile }, {
xAttributeName: "a2", yAttributeName: "a1", legendAttributeName: "a2", captionAttributeName: "a1",
rightNumericAttributeName: "a4", rightSplitAttributeName: "a1", topSplitAttributeName: "a1",
enableNumberToggle: false, numberToggleLastMode: false
})
expect(updateResult1.success).toBe(true)
expect(dataConfig.attributeDescriptionForRole("x")?.attributeID).toBe(a2.id)
expect(dataConfig.attributeDescriptionForRole("y")?.attributeID).toBe(a1.id)
expect(dataConfig.attributeDescriptionForRole("legend")?.attributeID).toBe(a2.id)
expect(dataConfig.attributeDescriptionForRole("caption")?.attributeID).toBe(a1.id)
expect(dataConfig.attributeDescriptionForRole("rightNumeric")?.attributeID).toBe(a4.id)
expect(dataConfig.attributeDescriptionForRole("rightSplit")?.attributeID).toBe(a1.id)
expect(dataConfig.attributeDescriptionForRole("topSplit")?.attributeID).toBe(a1.id)
expect(tileContent.showParentToggles).toBe(false)

// Update a graph to remove attributes
const updateResult2 = handler.update!({ component: tile }, {
xAttributeName: null, yAttributeName: null, legendAttributeName: null, captionAttributeID: null,
rightNumericAttributeName: null, rightSplitAttributeName: null, topSplitAttributeName: null
} as V2Graph)
expect(updateResult2.success).toBe(true)
expect(dataConfig.attributeDescriptionForRole("x")?.attributeID).toBeUndefined()
expect(dataConfig.attributeDescriptionForRole("y")?.attributeID).toBeUndefined()
expect(dataConfig.attributeDescriptionForRole("legend")?.attributeID).toBeUndefined()
expect(dataConfig.attributeDescriptionForRole("caption")?.attributeID).toBeUndefined()
expect(dataConfig.attributeDescriptionForRole("rightNumeric")?.attributeID).toBeUndefined()
expect(dataConfig.attributeDescriptionForRole("rightSplit")?.attributeID).toBeUndefined()
expect(dataConfig.attributeDescriptionForRole("topSplit")?.attributeID).toBeUndefined()

// Update a graph to add attributes
const updateResult3 = handler.update!({ component: tile }, {
xAttributeID: toV2Id(a3.id), xAttributeName: "a2", yAttributeID: toV2Id(a2.id), legendAttributeID: toV2Id(a3.id),
captionAttributeID: toV2Id(a2.id), rightSplitAttributeID: toV2Id(a1.id), topSplitAttributeID: toV2Id(a2.id),
enableNumberToggle: true, numberToggleLastMode: true
})
expect(updateResult3.success).toBe(true)
// Id should trump name
expect(dataConfig.attributeDescriptionForRole("x")?.attributeID).toBe(a3.id)
expect(dataConfig.attributeDescriptionForRole("y")?.attributeID).toBe(a2.id)
expect(dataConfig.attributeDescriptionForRole("legend")?.attributeID).toBe(a3.id)
expect(dataConfig.attributeDescriptionForRole("caption")?.attributeID).toBe(a2.id)
expect(dataConfig.attributeDescriptionForRole("rightSplit")?.attributeID).toBe(a1.id)
expect(dataConfig.attributeDescriptionForRole("topSplit")?.attributeID).toBe(a2.id)
expect(tileContent.showParentToggles).toBe(true)

// We have to set a numeric x attribute before we can set the rightNumeric attribute
handler.update!({ component: tile }, {
rightNumericAttributeID: toV2Id(a3.id)
})
expect(dataConfig.attributeDescriptionForRole("rightNumeric")?.attributeID).toBe(a3.id)

// Get graph
testGetComponent(tile, handler, (graphTile, values) => {
const {
dataContext, enableNumberToggle, numberToggleLastMode, captionAttributeName, legendAttributeName,
rightSplitAttributeName, topSplitAttributeName, xAttributeName, xLowerBound, xUpperBound,
yAttributeName, yLowerBound, yUpperBound, y2AttributeName, y2LowerBound, y2UpperBound
dataContext, enableNumberToggle, numberToggleLastMode, captionAttributeID, captionAttributeName,
legendAttributeID, legendAttributeName, rightSplitAttributeID, rightSplitAttributeName,
topSplitAttributeID, topSplitAttributeName, xAttributeID, xAttributeName, xLowerBound, xUpperBound,
yAttributeID, yAttributeName, yLowerBound, yUpperBound,
y2AttributeID, y2AttributeName, y2LowerBound, y2UpperBound
} = values as V2GetGraph
const content = graphTile.content as IGraphContentModel
const graphDataset = content.dataset!
Expand All @@ -91,32 +205,36 @@ describe("DataInteractive ComponentHandler Graph", () => {
expect(numberToggleLastMode).toBe(content.showOnlyLastCase)

const captionAttributeId = dataConfiguration.attributeDescriptionForRole("caption")!.attributeID
expect(captionAttributeID).toBe(captionAttributeId)
expect(captionAttributeName).toBe(graphDataset.getAttribute(captionAttributeId)?.name)

const legendAttributeId = dataConfiguration.attributeDescriptionForRole("legend")!.attributeID
expect(legendAttributeID).toBe(legendAttributeId)
expect(legendAttributeName).toBe(graphDataset.getAttribute(legendAttributeId)?.name)

const rightSplitId = dataConfiguration.attributeDescriptionForRole("rightSplit")!.attributeID
expect(rightSplitAttributeID).toBe(rightSplitId)
expect(rightSplitAttributeName).toBe(graphDataset.getAttribute(rightSplitId)?.name)

const topSplitId = dataConfiguration.attributeDescriptionForRole("topSplit")!.attributeID
expect(topSplitAttributeID).toBe(topSplitId)
expect(topSplitAttributeName).toBe(graphDataset.getAttribute(topSplitId)?.name)

const xAttributeId = dataConfiguration.attributeDescriptionForRole("x")!.attributeID
expect(xAttributeID).toBe(xAttributeId)
expect(xAttributeName).toBe(graphDataset.getAttribute(xAttributeId)?.name)
const xAxis = content.getAxis("bottom") as IBaseNumericAxisModel
expect(xLowerBound).toBe(xAxis.min)
expect(xUpperBound).toBe(xAxis.max)

const yAttributeId = dataConfiguration.attributeDescriptionForRole("y")!.attributeID
expect(yAttributeID).toBe(yAttributeId)
expect(yAttributeName).toBe(graphDataset.getAttribute(yAttributeId)?.name)
const yAxis = content.getAxis("left") as IBaseNumericAxisModel
expect(yLowerBound).toBe(yAxis.min)
expect(yUpperBound).toBe(yAxis.max)

const y2AttributeId = dataConfiguration.attributeDescriptionForRole("rightNumeric")!.attributeID
expect(y2AttributeID).toBe(y2AttributeId)
expect(y2AttributeName).toBe(graphDataset.getAttribute(y2AttributeId)?.name)
const y2Axis = content.getAxis("rightNumeric") as IBaseNumericAxisModel
expect(y2LowerBound).toBe(y2Axis.min)
expect(y2UpperBound).toBe(y2Axis.max)
})
Expand Down
18 changes: 11 additions & 7 deletions v3/src/components/graph/components/graph.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {comparer} from "mobx"
import {observer} from "mobx-react-lite"
import {isAlive} from "mobx-state-tree"
import {IDisposer, isAlive} from "mobx-state-tree"
import React, {MutableRefObject, useCallback, useEffect, useMemo, useRef} from "react"
import {select} from "d3"
import {clsx} from "clsx"
Expand Down Expand Up @@ -233,12 +233,16 @@ export const Graph = observer(function Graph({graphController, graphRef, pixiPoi

// respond to assignment of new attribute ID
useEffect(function handleNewAttributeID() {
const disposer = graphModel && onAnyAction(graphModel, action => {
if (isSetAttributeIDAction(action)) {
startAnimation()
graphController?.handleAttributeAssignment()
}
})
let disposer: Maybe<IDisposer>
if (graphModel) {
disposer = onAnyAction(graphModel, action => {
const dataConfigActions = ["setAttribute", "replaceYAttribute", "removeYAttributeAtIndex"]
if (dataConfigActions.includes(action.name) || isSetAttributeIDAction(action)) {
startAnimation()
graphController?.handleAttributeAssignment()
}
})
}
return () => disposer?.()
}, [graphController, layout, graphModel, startAnimation])

Expand Down
Loading

0 comments on commit a001527

Please sign in to comment.