Skip to content

Commit

Permalink
feat: support date-time slider in v2 documents
Browse files Browse the repository at this point in the history
  • Loading branch information
kswenson committed Dec 20, 2024
1 parent 3039c50 commit 54824e3
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 11 deletions.
11 changes: 7 additions & 4 deletions v3/src/components/slider/slider-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { getGlobalValueManager, getSharedModelManager } from "../../models/tiles
import { ITileContentModel, TileContentModel } from "../../models/tiles/tile-content"
import { DateUnit, dateUnits, unitsStringToMilliseconds } from "../../utilities/date-utils"
import { kSliderTileType } from "./slider-defs"
import {AnimationDirection, AnimationDirections, AnimationMode, AnimationModes, FixValueFn, ISliderScaleType,
import {
AnimationDirection, AnimationDirections, AnimationMode, AnimationModes, FixValueFn, ISliderScaleType,
kDefaultAnimationDirection, kDefaultAnimationMode, kDefaultAnimationRate, kDefaultDateMultipleOfUnit,
kDefaultSliderScaleType, SliderScaleTypes} from "./slider-types"
kDefaultSliderAxisMax, kDefaultSliderAxisMin, kDefaultSliderScaleType, SliderScaleTypes
} from "./slider-types"

export const SliderModel = TileContentModel
.named("SliderModel")
Expand All @@ -25,7 +27,7 @@ export const SliderModel = TileContentModel
_animationRate: types.maybe(types.number), // frames per second
scaleType: types.optional(types.enumeration([...SliderScaleTypes]), kDefaultSliderScaleType),
axis: types.optional(types.union(NumericAxisModel, DateAxisModel),
() => NumericAxisModel.create({ place: 'bottom', min: -0.5, max: 11.5 }))
() => NumericAxisModel.create({ place: 'bottom', min: kDefaultSliderAxisMin, max: kDefaultSliderAxisMax }))
})
.views(self => ({
get name() {
Expand Down Expand Up @@ -169,7 +171,8 @@ export const SliderModel = TileContentModel
if (scaleType !== self.scaleType) {
switch (scaleType) {
case "numeric":
self.axis = NumericAxisModel.create({ place: 'bottom', min: -0.5, max: 11.5 })
self.axis = NumericAxisModel.create({
place: 'bottom', min: kDefaultSliderAxisMin, max: kDefaultSliderAxisMax })
self.setValue(0.5)
break
case "date": {
Expand Down
15 changes: 15 additions & 0 deletions v3/src/components/slider/slider-registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ describe("Slider registration", () => {
})
expect(tileWithNoSharedModel).toBeUndefined()

// export numeric slider in v2 format
const row = docContent.getRowByIndex(0) as IFreeTileRow
const sliderExport = exportV2Component({ tile, row, sharedModelManager })
expect(sliderExport?.type).toBe("DG.SliderView")
Expand All @@ -94,5 +95,19 @@ describe("Slider registration", () => {
expect(sliderStorage.animationMode).toBe(1)
expect(sliderStorage.maxPerSecond).toBeNull()
expect(sliderStorage.restrictToMultiplesOf).toBeNull()

// change to date-time slider and export in v2 format
sliderModel!.setScaleType("date")
sliderModel!.setDateMultipleOfUnit("day")
const dateSliderExport = exportV2Component({ tile, row, sharedModelManager })
expect(dateSliderExport?.type).toBe("DG.SliderView")
const dateSliderStorage = dateSliderExport!.componentStorage as ICodapV2SliderStorage
expect(dateSliderStorage._links_?.model).toBeDefined()
expect(dateSliderStorage.animationDirection).toBe(1)
expect(dateSliderStorage.animationMode).toBe(1)
expect(dateSliderStorage.maxPerSecond).toBeNull()
expect(dateSliderStorage.restrictToMultiplesOf).toBeNull()
expect(dateSliderStorage.v3?.scaleType).toBe("date")
expect(dateSliderStorage.v3?.dateMultipleOfUnit).toBe("day")
})
})
29 changes: 22 additions & 7 deletions v3/src/components/slider/slider-registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ITileLikeModel, registerTileContentInfo } from "../../models/tiles/tile
import { getGlobalValueManager } from "../../models/tiles/tile-environment"
import { ITileModelSnapshotIn } from "../../models/tiles/tile-model"
import { toV2Id, toV3GlobalId, toV3Id } from "../../utilities/codap-utils"
import { DateUnit, unitsStringToMilliseconds } from "../../utilities/date-utils"
import { isFiniteNumber } from "../../utilities/math-utils"
import { isAliveSafe } from "../../utilities/mst-utils"
import { t } from "../../utilities/translation/translate"
Expand All @@ -25,7 +26,7 @@ import { ISliderSnapshot, SliderModel, isSliderModel } from "./slider-model"
import { SliderTitleBar } from "./slider-title-bar"
import {
AnimationDirection, AnimationDirections, AnimationMode, AnimationModes,
kDefaultAnimationDirection, kDefaultAnimationMode
kDefaultAnimationDirection, kDefaultAnimationMode, kDefaultSliderAxisMax, kDefaultSliderAxisMin
} from "./slider-types"
import { kDefaultSliderName, kDefaultSliderValue } from "./slider-utils"

Expand Down Expand Up @@ -74,15 +75,17 @@ registerTileComponentInfo({
defaultHeight: 73
})

registerV2TileExporter((kSliderTileType), ({ tile }) => {
registerV2TileExporter(kSliderTileType, ({ tile }) => {
const sliderModel = isSliderModel(tile.content) ? tile.content : undefined
if (!sliderModel) return
const {
domain: [lowerBound, upperBound],
animationDirection,
animationMode,
_animationRate,
multipleOf
multipleOf,
dateMultipleOfUnit,
scaleType
} = sliderModel

const domain = isFiniteNumber(lowerBound) && isFiniteNumber(upperBound) ? { lowerBound, upperBound } : undefined
Expand All @@ -93,14 +96,21 @@ registerV2TileExporter((kSliderTileType), ({ tile }) => {
const getAnimationModeIndex = (mode?: AnimationMode) => {
return mode != null ? AnimationModes.findIndex(_mode => _mode === mode) : 1
}
// v2 doesn't support date-time sliders; convert to seconds instead
const restrictToMultiplesOf = scaleType === "date" && multipleOf != null
? multipleOf * unitsStringToMilliseconds(dateMultipleOfUnit) / 1000
: multipleOf
// v3 extensions: ignored by v2, but allows full round-trip for v3 save/restore
const v3: ICodapV2SliderStorage["v3"] = { scaleType, multipleOf, dateMultipleOfUnit }

const componentStorage: SetOptional<ICodapV2SliderStorage, keyof ICodapV2BaseComponentStorage> = {
_links_: { model: guidLink("DG.GlobalValue", toV2Id(tile.id)) },
...domain,
animationDirection: getAnimationDirectionIndex(animationDirection),
animationMode: getAnimationModeIndex(animationMode),
maxPerSecond: _animationRate ?? null,
restrictToMultiplesOf: multipleOf ?? null
restrictToMultiplesOf: restrictToMultiplesOf ?? null,
v3
}
return { type: "DG.SliderView", componentStorage }
})
Expand All @@ -116,7 +126,7 @@ registerV2TileImporter("DG.SliderView", ({ v2Component, v2Document, sharedModelM
guid: componentGuid,
componentStorage: {
name, title: v2Title = "", _links_, lowerBound, upperBound, animationDirection, animationMode,
restrictToMultiplesOf, maxPerSecond, userTitle, userSetTitle
restrictToMultiplesOf, maxPerSecond, userTitle, userSetTitle, v3
}
} = v2Component
const globalId = _links_.model.id
Expand All @@ -137,15 +147,20 @@ registerV2TileImporter("DG.SliderView", ({ v2Component, v2Document, sharedModelM
return AnimationModes[mode] || kDefaultAnimationMode
}

const axisType = v3?.scaleType ?? "numeric"
const axisMin = lowerBound ?? kDefaultSliderAxisMin
const axisMax = upperBound ?? kDefaultSliderAxisMax

// create slider model
const content: ISliderSnapshot = {
type: kSliderTileType,
globalValue: globalValue.id,
multipleOf: restrictToMultiplesOf ?? undefined,
multipleOf: v3?.multipleOf ?? restrictToMultiplesOf ?? undefined,
dateMultipleOfUnit: v3?.dateMultipleOfUnit as DateUnit ?? undefined,
animationDirection: getAnimationDirectionStr(animationDirection),
animationMode: getAnimationModeStr(animationMode),
_animationRate: maxPerSecond ?? undefined,
axis: { type: "numeric", place: "bottom", min: lowerBound ?? 0, max: upperBound ?? 12 }
axis: { type: axisType, place: "bottom", min: axisMin, max: axisMax }
}
const title = v2Title && (userTitle || userSetTitle) ? v2Title : undefined
const sliderTileSnap: ITileModelSnapshotIn = {
Expand Down
3 changes: 3 additions & 0 deletions v3/src/components/slider/slider-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export const kDefaultSliderAxisTop = 0
export const kDefaultSliderAxisHeight = 24
export const kDefaultSliderPadding = 10

export const kDefaultSliderAxisMin = -0.5
export const kDefaultSliderAxisMax = 11.5

// values are translation string keys; indices are v2 values
export const AnimationDirections = ["backAndForth", "lowToHigh", "highToLow"] as const
export type AnimationDirection = typeof AnimationDirections[number]
Expand Down
8 changes: 8 additions & 0 deletions v3/src/v2/codap-v2-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export interface ICodapV2BaseComponentStorage {
// In the CFM shared files there are more than 20,000 examples of cannotClose: true
// and more 20,000 examples cannotClose: false
cannotClose?: boolean
// allows v2 documents saved by v3 to contain v3-specific enhancements
v3?: object
}

export interface ICodapV2CalculatorStorage extends ICodapV2BaseComponentStorage {
Expand All @@ -264,6 +266,12 @@ export interface ICodapV2SliderStorage extends ICodapV2BaseComponentStorage {
// NOTE: v2 writes out the `userTitle` property, but reads in property `userChangedTitle`.
// It is also redundant with the `userSetTitle` property shared by all components. ¯\_(ツ)_/¯
userTitle?: boolean
// v3 enhancements
v3?: {
scaleType: "numeric" | "date",
multipleOf?: number
dateMultipleOfUnit?: string
}
}

export interface ICodapV2RowHeight {
Expand Down

0 comments on commit 54824e3

Please sign in to comment.