Skip to content

Commit

Permalink
User can specify number of standard errors (#1273)
Browse files Browse the repository at this point in the history
* [#187626777] Feature: User can specify how many standard errors are shown in the standard error bar

* We add a NumberInput to the Standard Error checkbox item in standard-error-adornment-registration.tsx
* We do a considerable amount of "futzing" to make sure undo/redo works properly and the user can't get in trouble by deleting the numeric value
* Note that the formatting of this input is not good. I'm adding a PT story about it.

---------

Co-authored-by: Ethan McElroy <[email protected]>
  • Loading branch information
bfinzer and emcelroy authored May 21, 2024
1 parent 73d3f02 commit 707cc74
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 18 deletions.
2 changes: 2 additions & 0 deletions v3/src/components/graph/adornments/adornment-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@ describe("Deserialization", () => {
const testModel4 = M.create(snap3)
expect(isMovableValueAdornment(testModel4.adornment) && testModel4.adornment.values).toBeDefined()

const consoleSpy = jest.spyOn(console, "warn").mockImplementation()
const unknownAdornment = UnknownAdornmentModel.create()
testModel.setAdornment(unknownAdornment)
expect(testModel.adornment.type).toEqual("Unknown")
const snap4 = getSnapshot(testModel)
const testModel5 = M.create(snap4)
expect(testModel5.adornment.type).toEqual("Unknown")
expect(consoleSpy).toHaveBeenCalledWith(`Unknown adornment type: ${unknownAdornment.type}`)
})
})
7 changes: 4 additions & 3 deletions v3/src/components/graph/adornments/adornments-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AdornmentsStore } from "./adornments-store"
import * as contentInfo from "./adornment-content-info"
import { IGraphDataConfigurationModel } from "../models/graph-data-configuration-model"
import { kMovableValueType } from "./movable-value/movable-value-adornment-types"
import { kCountType } from "./count/count-adornment-types"

jest.spyOn(contentInfo, "getAdornmentTypes").mockReturnValue(
[
Expand All @@ -22,7 +23,7 @@ const mockAdornment = {
cellKey: () => ({}),
setVisibility: () => true,
updateCategories: () => ({}),
type: "Mock Adornment"
type: kCountType
}
const mockMovableValueAdornment = {
cellCount: () => ({x: 1, y: 1}),
Expand Down Expand Up @@ -121,9 +122,9 @@ describe("AdornmentsStore", () => {
adornmentsStore.addAdornment(mockAdornment, mockUpdateCategoriesOptions)
expect(adornmentsStore.adornments.length).toBe(1)
expect(adornmentsStore.adornments[0].isVisible).toBe(false)
adornmentsStore.showAdornment(adornmentsStore.adornments[0], "Mock Adornment")
adornmentsStore.showAdornment(adornmentsStore.adornments[0], kCountType)
expect(adornmentsStore.adornments[0].isVisible).toBe(true)
adornmentsStore.hideAdornment("Mock Adornment")
adornmentsStore.hideAdornment(kCountType)
expect(adornmentsStore.adornments[0].isVisible).toBe(false)
})
it("can trigger a callback function when updateAdornments is called", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export const StandardErrorAdornmentComponent = observer(
selectionsObj: IStandardErrorSelections, labelObj: ILabel) => {
if (!numericAttrId || !dataConfig) return
const value = model.measureValue(numericAttrId, cellKey, dataConfig)
if (value === undefined || isNaN(value)) return
if (value === undefined || isNaN(value) || isNaN(range.min) || isNaN(range.max)) return

addErrorBar(valueObjRef.current)

Expand All @@ -268,8 +268,8 @@ export const StandardErrorAdornmentComponent = observer(
} else {
addTextTip(range.max, textContent, selectionsObj)
}
}, [numericAttrId, dataConfig, model, cellKey, addErrorBar, helper, isVertical, showLabel, addLabels,
range.max, addTextTip])
}, [numericAttrId, dataConfig, model, cellKey, range.min, range.max, addErrorBar, helper,
isVertical, showLabel, addLabels, addTextTip])

// Add the lines and their associated covers and labels
const refreshValues = useCallback(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,29 @@ export const StandardErrorAdornmentModel = UnivariateMeasureAdornmentModel
.props({
type: types.optional(types.literal(kStandardErrorType), kStandardErrorType),
labelTitle: types.optional(types.literal(kStandardErrorValueTitleKey), kStandardErrorValueTitleKey),
numStErrs: types.optional(types.number, 1)
_numStErrs: types.optional(types.number, 1)
})
.volatile(self => ({
dynamicNumStErrs: undefined as number | undefined
}))
.actions(self => ({
setNumStErrs(numStErrs: number) {
self.numStErrs = numStErrs
self._numStErrs = numStErrs
self.dynamicNumStErrs = undefined
},
setDynamicNumStErrs(numStErrs: number | undefined) {
self.dynamicNumStErrs = numStErrs
}
}))
.views(self => ({
get numStErrs() {
return self.dynamicNumStErrs ?? self._numStErrs
},
get hasRange() {
return true
},
}))
.views(self => ({
computeMeasureValue(attrId: string, cellKey: Record<string, string>, dataConfig: IGraphDataConfigurationModel) {
// The measure value is the standard error of the mean.
const caseValues = self.getCaseValues(attrId, cellKey, dataConfig)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,85 @@
import React from "react"
import { Flex, NumberDecrementStepper, NumberIncrementStepper, NumberInput, NumberInputField, NumberInputStepper }
from "@chakra-ui/react"
import { translate } from "../../../../../utilities/translation/translate"
import { AdornmentCheckbox } from "../../adornment-checkbox"
import { useGraphContentModelContext } from "../../../hooks/use-graph-content-model-context"
import { registerAdornmentComponentInfo } from "../../adornment-component-info"
import { registerAdornmentContentInfo } from "../../adornment-content-info"
import { StandardErrorAdornmentModel } from "./standard-error-adornment-model"
import { kStandardErrorClass, kStandardErrorLabelKey, kStandardErrorType, kStandardErrorPrefix,
kStandardErrorUndoAddKey, kStandardErrorRedoAddKey, kStandardErrorRedoRemoveKey,
kStandardErrorUndoRemoveKey} from "./standard-error-adornment-types"
import { AdornmentCheckbox } from "../../adornment-checkbox"
import {
kStandardErrorClass, kStandardErrorLabelKey, kStandardErrorType, kStandardErrorPrefix,
kStandardErrorUndoAddKey, kStandardErrorRedoAddKey, kStandardErrorRedoRemoveKey,
kStandardErrorUndoRemoveKey
} from "./standard-error-adornment-types"
import { IStandardErrorAdornmentModel, StandardErrorAdornmentModel } from "./standard-error-adornment-model"
import { StandardErrorAdornmentComponent } from "./standard-error-adornment-component"

const Controls = () => {
const graphModel = useGraphContentModelContext()
const adornmentsStore = graphModel.adornmentsStore
const existingAdornment =
adornmentsStore.findAdornmentOfType<IStandardErrorAdornmentModel>(kStandardErrorType)

const handleBlur = () => {
if (existingAdornment) {
graphModel.applyModelChange(
() => {
const numStErrs = existingAdornment.numStErrs // Can be NaN if user cleared value
if (isFinite(numStErrs)) {
existingAdornment.setNumStErrs(numStErrs)
}
else {
existingAdornment?.setDynamicNumStErrs(undefined)
}
},
{
undoStringKey: 'DG.Undo.graph.setNumStErrs',
redoStringKey: 'DG.Undo.graph.setNumStErrs'
}
)
}
}

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
handleBlur()
} else if (e.key === 'Escape') {
e.preventDefault()
if (existingAdornment) {
existingAdornment.setDynamicNumStErrs(undefined)
}
}
}

return (
<AdornmentCheckbox
classNameValue={kStandardErrorClass}
labelKey={kStandardErrorLabelKey}
type={kStandardErrorType}
/>
<Flex direction="row">
<AdornmentCheckbox
classNameValue={kStandardErrorClass}
labelKey={''}
type={kStandardErrorType}
/>
<NumberInput min={0} size={"xs"} variant={"outline"}
data-testid={`adornment-number-input-${kStandardErrorClass}`}
focusInputOnChange={true}
focusBorderColor={"blue.500"}
isDisabled={!existingAdornment?.isVisible}
defaultValue={existingAdornment?.numStErrs ?? 1}
onFocus={(e) => e.target.select()}
onChange={(_, valueAsNumber) => {
existingAdornment?.setDynamicNumStErrs(valueAsNumber)
}}
onBlur={handleBlur}
onKeyDown={(e) => handleKeyDown(e)}
>
<NumberInputField/>
<NumberInputStepper>
<NumberIncrementStepper/>
<NumberDecrementStepper/>
</NumberInputStepper>
</NumberInput>
<span>{translate(kStandardErrorLabelKey)}</span>
</Flex>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

.measure-items {
margin-left: 20px;
font-size: 12px;

.measure-movable-value-button {
font-size: 12px;
Expand Down

0 comments on commit 707cc74

Please sign in to comment.