Skip to content

Commit

Permalink
182991523 date type precision menu (#1620)
Browse files Browse the repository at this point in the history
* Implements precision for both numeric and dates.

* fix attr format string for numerical

* chore: code review tweaks

---------

Co-authored-by: Kirk Swenson <[email protected]>
  • Loading branch information
eireland and kswenson authored Nov 15, 2024
1 parent 4c4ffda commit 1c55fc4
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 44 deletions.
4 changes: 2 additions & 2 deletions v3/src/components/case-table/use-columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export const useColumns = ({ data, indexColumn }: IUseColumnsProps) => {
const collection = data?.getCollection(collectionId)
const attrs: IAttribute[] = collection ? getCollectionAttrs(collection, data) : []
const visible: IAttribute[] = attrs.filter(attr => attr && !caseMetadata?.isHidden(attr.id))
return visible.map(({ id, name, type, userType, isEditable, hasFormula }) =>
({ id, name, type, userType, isEditable, hasFormula }))
return visible.map(({ id, name, type, userType, isEditable, hasFormula, precision }) =>
({ id, name, type, userType, isEditable, hasFormula, precision }))
},
entries => {
// column definitions
Expand Down
8 changes: 3 additions & 5 deletions v3/src/components/case-table/use-rows.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { format } from "d3"
import { reaction } from "mobx"
import { useCallback, useEffect, useRef } from "react"
import { useDebouncedCallback } from "use-debounce"
Expand All @@ -8,7 +7,6 @@ import { useDataSetContext } from "../../hooks/use-data-set-context"
import { useLoggingContext } from "../../hooks/use-log-context"
import { logMessageWithReplacement } from "../../lib/log-message"
import { appState } from "../../models/app-state"
import { kDefaultFormatStr } from "../../models/data/attribute-types"
import { isAddCasesAction, isRemoveCasesAction, isSetCaseValuesAction } from "../../models/data/data-set-actions"
import { createCasesNotification } from "../../models/data/data-set-notifications"
import {
Expand All @@ -18,6 +16,7 @@ import { isSetIsCollapsedAction } from "../../models/shared/shared-case-metadata
import { mstReaction } from "../../utilities/mst-reaction"
import { onAnyAction } from "../../utilities/mst-utils"
import { prf } from "../../utilities/profiler"
import { renderAttributeValue } from "../case-tile-common/attribute-format-utils"
import { applyCaseValueChanges } from "../case-tile-common/case-tile-utils"
import { kInputRowKey, symDom, TRow, TRowsChangeData } from "./case-table-types"
import { useCollectionTableModel } from "./use-collection-table-model"
Expand Down Expand Up @@ -86,9 +85,8 @@ export const useRows = (gridElement: HTMLDivElement | null) => {
if (data && caseId && attr && cellSpan) {
const strValue = data.getStrValue(caseId, attr.id)
const numValue = data.getNumeric(caseId, attr.id)
const formatStr = attr.format || kDefaultFormatStr
const formatted = (numValue != null) && isFinite(numValue) ? format(formatStr)(numValue) : strValue
cellSpan.textContent = formatted ?? ""
const { value } = renderAttributeValue(strValue, numValue, attr)
cellSpan.textContent = value
setCachedDomAttr(caseId, attr.id)
}
})
Expand Down
12 changes: 5 additions & 7 deletions v3/src/components/case-tile-common/attribute-format-utils.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { format } from "d3-format"
import React from "react"
import { IAttribute } from "../../models/data/attribute"
import { kDefaultFormatStr } from "../../models/data/attribute-types"
import { kDefaultNumPrecision } from "../../models/data/attribute-types"
import { parseColor } from "../../utilities/color-utils"
import { isStdISODateString } from "../../utilities/date-iso-utils"
import { parseDate } from "../../utilities/date-parser"
import { DatePrecision, formatDate } from "../../utilities/date-utils"
import { formatDate } from "../../utilities/date-utils"
import { kCaseTableBodyFont, kCaseTableHeaderFont, kMaxAutoColumnWidth,
kMinAutoColumnWidth } from "../case-table/case-table-types"
import { measureText } from "../../hooks/use-measure-text"
Expand All @@ -24,8 +24,7 @@ export const getNumFormatter = (formatStr: string) => {
}

export function renderAttributeValue(str = "", num = NaN, attr?: IAttribute, key?: number) {
const { type, userType } = attr || {}

const { type, userType, numPrecision, datePrecision } = attr || {}
// colors
const color = type === "color" || !userType ? parseColor(str, { colorNames: type === "color" }) : ""
if (color) {
Expand All @@ -41,7 +40,7 @@ export function renderAttributeValue(str = "", num = NaN, attr?: IAttribute, key

// numbers
if (isFinite(num)) {
const formatStr = attr?.format ?? kDefaultFormatStr
const formatStr = `.${numPrecision ?? kDefaultNumPrecision}~f`
const formatter = getNumFormatter(formatStr)
if (formatter) str = formatter(num)
}
Expand All @@ -57,8 +56,7 @@ export function renderAttributeValue(str = "", num = NaN, attr?: IAttribute, key
if (isStdISODateString(str) || userType === "date" && str !== "") {
const date = parseDate(str, true)
if (date) {
// TODO: add precision support for date formatting
const formattedDate = formatDate(date, DatePrecision.None)
const formattedDate = formatDate(date, datePrecision)
return {
value: str,
content: <span className="cell-span" key={key}>{formattedDate || `"${str}"`}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useDataSetContext } from "../../../hooks/use-data-set-context"
import { logMessageWithReplacement } from "../../../lib/log-message"
import { AttributeType, attributeTypes } from "../../../models/data/attribute"
import { updateAttributesNotification } from "../../../models/data/data-set-notifications"
import { DatePrecision } from "../../../utilities/date-utils"
import { uniqueName } from "../../../utilities/js-utils"
import { t } from "../../../utilities/translation/translate"
import { CodapModal } from "../../codap-modal"
Expand All @@ -31,7 +32,7 @@ export const EditAttributePropertiesModal = ({ attributeId, isOpen, onClose }: I
const [attributeName, setAttributeName] = useState(columnName)
const [description, setDescription] = useState("")
const [units, setUnits] = useState("")
const [precision, setPrecision] = useState("")
const [precision, setPrecision] = useState(attribute?.precision)
const [userType, setUserType] = useState<SelectableAttributeType>("none")
const [editable, setEditable] = useState<YesNoValue>("yes")

Expand All @@ -40,7 +41,7 @@ export const EditAttributePropertiesModal = ({ attributeId, isOpen, onClose }: I
setAttributeName(attribute?.name || "attribute")
setDescription(attribute?.description ?? "")
setUnits(attribute?.units ?? "")
setPrecision(`${attribute?.precision ?? ""}`)
setPrecision(attribute?.precision)
setUserType(attribute?.userType ?? "none")
setEditable(attribute?.editable ? "yes" : "no")
}, [attribute, isOpen])
Expand All @@ -63,8 +64,8 @@ export const EditAttributePropertiesModal = ({ attributeId, isOpen, onClose }: I
if (userType !== (attribute.userType ?? "none")) {
attribute.setUserType(userType === "none" ? undefined : userType)
}
if (precision !== `${attribute?.precision ?? ""}`) {
attribute.setPrecision(precision ? +precision : undefined)
if (precision !== attribute.precision) {
attribute.setPrecision(precision)
}
if ((editable === "yes") !== attribute.editable) {
attribute.setEditable(editable === "yes")
Expand Down Expand Up @@ -100,6 +101,53 @@ export const EditAttributePropertiesModal = ({ attributeId, isOpen, onClose }: I
{ label: t("DG.AttrFormView.applyBtnTitle"), onClick: applyChanges, default: true }
]

function toDatePrecision(pStr: string) {
return !pStr || isFinite(Number(pStr)) ? undefined : pStr as DatePrecision
}
function toDatePrecisionStr(p: typeof precision) {
return p == null || typeof p === "number" ? "" : p
}

function toNumPrecision(pStr: string) {
return isFinite(Number(pStr)) ? Number(pStr) : undefined
}
function toNumPrecisionStr(p: typeof precision) {
return p == null || typeof p === "string" ? "" : `${p}`
}

const getPrecisionMenu = () => {
if (attribute?.type === "date" || userType === "date") {
return (
<Select size="xs" ml={5} value={toDatePrecisionStr(precision)} data-testid="attr-precision-select"
onChange={(e) => setPrecision(toDatePrecision(e.target.value))}>
{Object.values(DatePrecision).map(p => {
return (
<option value={p} key={`precision-${p}`} data-testid={`attr-precision-option-${p}`}>
{p}
</option>
)
})}
</Select>
)
} else {
return (
<Select size="xs" ml={5} value={toNumPrecisionStr(precision)}
data-testid="attr-precision-select"
onChange={(e) => setPrecision(toNumPrecision(e.target.value))}>
<option value={""}></option>
{[...Array(10).keys()].map(pNum => {
const precisionStr = `${pNum}`
return (
<option value={pNum} key={`precision-${pNum}`} data-testid={`attr-precision-option-${pNum}`}>
{precisionStr}
</option>
)
})}
</Select>
)
}
}

return (
<CodapModal
isOpen={isOpen}
Expand Down Expand Up @@ -148,20 +196,7 @@ export const EditAttributePropertiesModal = ({ attributeId, isOpen, onClose }: I
/>
</FormLabel>
<FormLabel className="edit-attribute-form-row" mr={5}>{t("DG.CaseTable.attributeEditor.precision")}
<Select size="xs" ml={5} value={precision} data-testid="attr-precision-select"
onChange={(e) => setPrecision(e.target.value)}>
<option value={""}></option>
<option value={"0"} data-testid="attr-precision-option">0</option>
<option value={"1"} data-testid="attr-precision-option">1</option>
<option value={"2"} data-testid="attr-precision-option">2</option>
<option value={"3"} data-testid="attr-precision-option">3</option>
<option value={"4"} data-testid="attr-precision-option">4</option>
<option value={"5"} data-testid="attr-precision-option">5</option>
<option value={"6"} data-testid="attr-precision-option">6</option>
<option value={"7"} data-testid="attr-precision-option">7</option>
<option value={"8"} data-testid="attr-precision-option">8</option>
<option value={"9"} data-testid="attr-precision-option">9</option>
</Select>
{getPrecisionMenu()}
</FormLabel>
<FormLabel className="edit-attribute-form-row editable">{t("DG.CaseTable.attributeEditor.editable")}
<RadioGroup value={editable} ml={5} data-testid="attr-editable-radio"
Expand Down
3 changes: 2 additions & 1 deletion v3/src/models/data/attribute-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const kDefaultFormatStr = ".3~f"
export const kDefaultNumPrecision = 3
export const kDefaultNumFormatStr = `.${kDefaultNumPrecision}~f`

export const isDevelopment = () => process.env.NODE_ENV !== "production"
export const isProduction = () => process.env.NODE_ENV === "production"
Expand Down
5 changes: 0 additions & 5 deletions v3/src/models/data/attribute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { getSnapshot } from "mobx-state-tree"
import {
Attribute, IAttributeSnapshot, importValueToString, isAttributeType, isFormulaAttr, isValidFormulaAttr
} from "./attribute"
import { kDefaultFormatStr } from "./attribute-types"

describe("Attribute", () => {

Expand Down Expand Up @@ -202,10 +201,6 @@ describe("Attribute", () => {
expect(attribute.strValues).toEqual(["", "", "", "", "", ""])
expect(attribute.numValues).toEqual([NaN, NaN, NaN, NaN, NaN, NaN])

expect(attribute.format).toBe(kDefaultFormatStr)
attribute.setPrecision(2)
expect(attribute.format).toBe(".2~f")

expect(attribute.description).toBeUndefined()
attribute.setDescription("description")
expect(attribute.description).toBe("description")
Expand Down
16 changes: 10 additions & 6 deletions v3/src/models/data/attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ import { cachedFnFactory } from "../../utilities/mst-utils"
import { Formula, IFormula } from "../formula/formula"
import { applyModelChange } from "../history/apply-model-change"
import { withoutUndo } from "../history/without-undo"
import { isDevelopment, isProduction, IValueType, kDefaultFormatStr } from "./attribute-types"
import { isDevelopment, isProduction, IValueType } from "./attribute-types"
import { V2Model } from "./v2-model"
import { DatePrecision } from "../../utilities/date-utils"

export interface ISetValueOptions {
noInvalidate?: boolean
Expand Down Expand Up @@ -71,7 +72,7 @@ export const Attribute = V2Model.named("Attribute").props({
userType: types.maybe(types.enumeration([...attributeTypes])),
// userFormat: types.maybe(types.string),
units: types.maybe(types.string),
precision: types.maybe(types.number),
precision: types.maybe(types.union(types.number, types.enumeration(Object.values(DatePrecision)))),
deleteable: true,
editable: true,
formula: types.maybe(Formula),
Expand Down Expand Up @@ -114,6 +115,12 @@ export const Attribute = V2Model.named("Attribute").props({
if (value == null || value === "") return NaN
return Number(value)
},
get numPrecision() {
return typeof self.precision === "number" ? self.precision : undefined
},
get datePrecision() {
return typeof self.precision === "string" ? self.precision : undefined
},
getEmptyCount: cachedFnFactory<number>(() => {
// Note that `self.changeCount` is absolutely not necessary here. However, historically, this function used to be
// a MobX computed property, and `self.changeCount` was used to invalidate the cache. Also, there are tests
Expand Down Expand Up @@ -240,9 +247,6 @@ export const Attribute = V2Model.named("Attribute").props({

return "categorical"
},
get format() {
return self.precision != null ? `.${self.precision}~f` : kDefaultFormatStr
},
get isEditable() {
return self.editable && !self.hasFormula
},
Expand Down Expand Up @@ -279,7 +283,7 @@ export const Attribute = V2Model.named("Attribute").props({
// setUserFormat(precision: string) {
// self.userFormat = `.${precision}~f`
// },
setPrecision(precision?: number) {
setPrecision(precision?: number | DatePrecision) {
self.precision = precision
},
setDeleteable(deleteable: boolean) {
Expand Down

0 comments on commit 1c55fc4

Please sign in to comment.