Skip to content

Commit

Permalink
181910061 Numeric Legend Color Picker (#1594)
Browse files Browse the repository at this point in the history
* Change low and high attribute colors using pickers from eye menu.

* Update legend and graph dots when attribute colors change.
  • Loading branch information
tealefristoe authored Nov 8, 2024
1 parent 78e8673 commit beafe64
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ScaleQuantile, scaleQuantile, schemeBlues} from "d3"
import {reaction} from "mobx"
import {comparer, reaction} from "mobx"
import {observer} from "mobx-react-lite"
import React, {useCallback, useEffect, useRef, useState} from "react"
import { mstReaction } from "../../../../utilities/mst-reaction"
Expand All @@ -22,12 +22,13 @@ export const NumericLegend =
quantileScale = useRef<ScaleQuantile<string>>(scaleQuantile()),
[choroplethElt, setChoroplethElt] = useState<SVGGElement | null>(null),
valuesRef = useRef<number[]>([]),
metadata = dataConfiguration?.metadata,
legendAttrID = dataConfiguration?.attributeID("legend") ?? "",

getLabelHeight = useCallback(() => {
const labelFont = vars.labelFont,
legendAttrID = dataConfiguration?.attributeID('legend') ?? ''
const labelFont = vars.labelFont
return getStringBounds(dataConfiguration?.dataset?.attrFromID(legendAttrID)?.name ?? '', labelFont).height
}, [dataConfiguration]),
}, [dataConfiguration, legendAttrID]),

refreshScale = useCallback(() => {
const numberHeight = getStringBounds('0').height
Expand Down Expand Up @@ -63,7 +64,7 @@ export const NumericLegend =
}

setDesiredExtent(layerIndex, computeDesiredExtent())
quantileScale.current.domain(valuesRef.current).range(schemeBlues[5])
quantileScale.current.domain(valuesRef.current).range(dataConfiguration?.quantileScaleColors ?? schemeBlues[5])
choroplethLegend(quantileScale.current, choroplethElt,
{
isDate: dataConfiguration?.attributeType('legend') === 'date',
Expand Down Expand Up @@ -115,6 +116,13 @@ export const NumericLegend =
{name: "NumericLegend respondToHiddenCaseChange"}, dataConfiguration)
}, [dataConfiguration, refreshScale])

useEffect(function respondToColorChange() {
return mstReaction(
() => metadata?.getAttributeColorRange(legendAttrID),
refreshScale, { name: "NumericLegend respondToColorChange", equals: comparer.structural }, metadata
)
}, [legendAttrID, metadata, refreshScale])

// todo: This reaction is not being triggered when a legend attribute value is changed.
// It should be.
useEffect(function respondToNumericValuesChange() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React, {useRef} from "react"
import {observer} from "mobx-react-lite"
import {Checkbox, Flex, FormControl, FormLabel, Slider, SliderThumb, SliderTrack
} from "@chakra-ui/react"
import React, { useRef } from "react"
import { observer } from "mobx-react-lite"
import { Checkbox, Flex, FormControl, FormLabel, Slider, SliderThumb, SliderTrack } from "@chakra-ui/react"
import {
kDefaultHighAttributeColor, kDefaultLowAttributeColor
} from "../../../models/shared/shared-case-metadata-constants"
import { t } from "../../../utilities/translation/translate"
import { IDataConfigurationModel } from "../models/data-configuration-model"
import { IDisplayItemDescriptionModel } from "../models/display-item-description-model"
import { PointColorSetting } from "./point-color-setting"
import {IDataConfigurationModel} from "../models/data-configuration-model"
import {IDisplayItemDescriptionModel} from "../models/display-item-description-model"
import {t} from "../../../utilities/translation/translate"

import "./inspector-panel.scss"

Expand All @@ -28,6 +30,7 @@ export const DisplayItemFormatControl = observer(function PointFormatControl(pro
const attrType = dataConfiguration?.dataset?.attrFromID(legendAttrID ?? "")?.type
const categoriesRef = useRef<string[] | undefined>()
categoriesRef.current = dataConfiguration?.categoryArrayForAttrRole('legend')
const metadata = dataConfiguration.metadata

const handlePointColorChange = (color: string) => {
displayItemDescription.applyModelChange(() => {
Expand All @@ -39,6 +42,26 @@ export const DisplayItemFormatControl = observer(function PointFormatControl(pro
})
}

const handleLowAttributeColorChange = (color: string) => {
metadata?.applyModelChange(() => {
metadata.setAttributeColor(legendAttrID, color, "low")
}, {
undoStringKey: "DG.Undo.graph.changeAttributeColor",
redoStringKey: "DG.Redo.graph.changeAttributeColor",
log: "Changed attribute color"
})
}

const handleHighAttributeColorChange = (color: string) => {
metadata?.applyModelChange(() => {
metadata.setAttributeColor(legendAttrID, color, "high")
}, {
undoStringKey: "DG.Undo.graph.changeAttributeColor",
redoStringKey: "DG.Redo.graph.changeAttributeColor",
log: "Changed attribute color"
})
}

const handleCatPointColorChange = (color: string, cat: string) => {
dataConfiguration.applyModelChange(
() => {
Expand Down Expand Up @@ -116,6 +139,7 @@ export const DisplayItemFormatControl = observer(function PointFormatControl(pro
}
}

const colorRange = metadata?.getAttributeColorRange(legendAttrID)
return (
<Flex className="palette-form" direction="column">

Expand Down Expand Up @@ -149,12 +173,16 @@ export const DisplayItemFormatControl = observer(function PointFormatControl(pro
<FormLabel className="form-label color-picker">{t("DG.Inspector.legendColor")}</FormLabel>
{/* Sets the min and max colors for numeric legend. Currently not implemented so
this sets the same color for all the points*/}
<PointColorSetting propertyLabel={t("DG.Inspector.legendColor")}
onColorChange={(color) => handlePointColorChange(color)}
swatchBackgroundColor={displayItemDescription.pointColor}/>
<PointColorSetting propertyLabel={t("DG.Inspector.legendColor")}
onColorChange={(color) => handlePointColorChange(color)}
swatchBackgroundColor={displayItemDescription.pointColor}/>
<PointColorSetting
propertyLabel={t("DG.Inspector.legendColor")}
onColorChange={(color) => handleLowAttributeColorChange(color)}
swatchBackgroundColor={colorRange?.low ?? kDefaultLowAttributeColor}
/>
<PointColorSetting
propertyLabel={t("DG.Inspector.legendColor")}
onColorChange={(color) => handleHighAttributeColorChange(color)}
swatchBackgroundColor={colorRange?.high ?? kDefaultHighAttributeColor}
/>
</Flex>
</FormControl>
:(
Expand Down
37 changes: 31 additions & 6 deletions v3/src/components/data-display/models/data-configuration-model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {scaleQuantile, ScaleQuantile, schemeBlues} from "d3"
import {scaleQuantile, ScaleQuantile} from "d3"
import {comparer, observable, reaction} from "mobx"
import {
addDisposer, getEnv, getSnapshot, hasEnv, IAnyStateTreeNode, Instance, ISerializedActionCall,
Expand All @@ -17,12 +17,15 @@ import {ISharedCaseMetadata, SharedCaseMetadata} from "../../../models/shared/sh
import {isSetCaseValuesAction} from "../../../models/data/data-set-actions"
import {FilteredCases, IFilteredChangedCases} from "../../../models/data/filtered-cases"
import {Formula, IFormula} from "../../../models/formula/formula"
import {
kDefaultHighAttributeColor, kDefaultLowAttributeColor
} from "../../../models/shared/shared-case-metadata-constants"
import {hashStringSets, typedId, uniqueId} from "../../../utilities/js-utils"
import {missingColor} from "../../../utilities/color-utils"
import {getQuantileScale, missingColor} from "../../../utilities/color-utils"
import { numericSortComparator } from "../../../utilities/data-utils"
import {GraphPlace} from "../../axis-graph-shared"
import {CaseData} from "../d3-types"
import {AttrRole, TipAttrRoles, graphPlaceToAttrRole} from "../data-display-types"
import {GraphPlace} from "../../axis-graph-shared"
import { numericSortComparator } from "../../../utilities/data-utils"

export const AttributeDescription = types
.model('AttributeDescription', {
Expand Down Expand Up @@ -398,23 +401,45 @@ export const DataConfigurationModel = types
)
return joinedCaseData
},
get lowColor() {
const attrId = self.attributeID("legend")
return self.metadata?.getAttributeColorRange(attrId).low
},
get highColor() {
const attrId = self.attributeID("legend")
return self.metadata?.getAttributeColorRange(attrId).high
}
}))
.views(self => ({
// observable hash of rendered case ids
get caseDataHash() {
return hashStringSets(self.filteredCases.map(cases => cases.caseIds))
},
get quantileScaleColors() {
return getQuantileScale(
self.lowColor ?? kDefaultLowAttributeColor,
self.highColor ?? kDefaultHighAttributeColor
)
}
}))
.extend(self => {
// TODO: This is a hack to get around the fact that MST doesn't seem to cache this as expected
// when implemented as simple view.
let quantileScale: ScaleQuantile<string> | undefined = undefined
let previousLowAttributeColor: string | undefined
let previousHighAttributeColor: string | undefined

return {
views: {
get legendQuantileScale() {
if (!quantileScale) {
quantileScale = scaleQuantile(self.numericValuesForAttrRole('legend'), schemeBlues[5])
if (
!quantileScale ||
previousLowAttributeColor !== self.lowColor ||
previousHighAttributeColor !== self.highColor
) {
previousLowAttributeColor = self.lowColor
previousHighAttributeColor = self.highColor
quantileScale = scaleQuantile(self.numericValuesForAttrRole('legend'), self.quantileScaleColors)
}
return quantileScale
},
Expand Down
77 changes: 33 additions & 44 deletions v3/src/components/graph/hooks/use-plot.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useEffect} from "react"
import { useCallback, useEffect } from "react"
import { comparer, reaction } from "mobx"
import {isAlive} from "mobx-state-tree"
import {onAnyAction} from "../../../utilities/mst-utils"
Expand Down Expand Up @@ -52,13 +52,27 @@ export const usePlotResponders = (props: IPlotResponderProps) => {
startAnimation = graphModel.startAnimation,
layout = useGraphLayoutContext(),
dataConfiguration = graphModel.dataConfiguration,
legendAttrID = dataConfiguration?.attributeID("legend"),
dataset = dataConfiguration?.dataset,
metadata = dataConfiguration?.metadata,
instanceId = useInstanceIdContext()

const callRefreshPointPositions = useDebouncedCallback((selectedOnly: boolean) => {
refreshPointPositions(selectedOnly)
})

const callMatchCirclesToData = useCallback(() => {
pixiPoints && matchCirclesToData({
dataConfiguration,
pointRadius: graphModel.getPointRadius(),
pointColor: graphModel.pointDescription.pointColor,
pointDisplayType: graphModel.pointDisplayType,
pointStrokeColor: graphModel.pointDescription.pointStrokeColor,
pixiPoints,
startAnimation, instanceId
})
}, [dataConfiguration, graphModel, instanceId, pixiPoints, startAnimation])

// Refresh point positions when pixiPoints become available to fix this bug:
// https://www.pivotaltracker.com/story/show/188333898
// This might be a workaround for the fact that useDebouncedCallback may not be updated when pixiPoints
Expand Down Expand Up @@ -140,30 +154,21 @@ export const usePlotResponders = (props: IPlotResponderProps) => {
if (!pixiPoints) {
return
}
matchCirclesToData({
dataConfiguration,
pointRadius: graphModel.getPointRadius(),
pointColor: graphModel.pointDescription.pointColor,
pointDisplayType: graphModel.pointDisplayType,
pointStrokeColor: graphModel.pointDescription.pointStrokeColor,
pixiPoints,
startAnimation, instanceId
})
callMatchCirclesToData()
callRefreshPointPositions(false)
}, {name: "respondToHiddenCasesChange"}, dataConfiguration
)
return () => disposer()
}, [callRefreshPointPositions, dataConfiguration, graphModel, instanceId, pixiPoints, startAnimation])
}, [callMatchCirclesToData, callRefreshPointPositions, dataConfiguration, pixiPoints])

// respond to axis range changes (e.g. component resizing)
useEffect(() => {
const disposer = reaction(
return reaction(
() => [layout.getAxisLength('left'), layout.getAxisLength('bottom')],
() => {
callRefreshPointPositions(false)
}, {name: "usePlot [axis range]"}
)
return () => disposer()
}, [layout, callRefreshPointPositions])

// respond to selection changes
Expand All @@ -179,24 +184,15 @@ export const usePlotResponders = (props: IPlotResponderProps) => {
const allCases = dataset.items.map(c => c.__id__)
const updatedHiddenCases = allCases.filter(caseID => !selectedCases.includes(caseID))
dataConfiguration?.setHiddenCases(updatedHiddenCases)
pixiPoints && matchCirclesToData({
dataConfiguration,
pointRadius: graphModel.getPointRadius(),
pointColor: graphModel.pointDescription.pointColor,
pointDisplayType: graphModel.pointDisplayType,
pointStrokeColor: graphModel.pointDescription.pointStrokeColor,
pixiPoints,
startAnimation, instanceId
})
callMatchCirclesToData()
callRefreshPointPositions(false)
}
refreshPointSelection()
},
{name: "useSubAxis.respondToSelectionChanges"}, dataConfiguration
)
}
}, [callRefreshPointPositions, dataConfiguration, dataset, graphModel, instanceId, pixiPoints,
refreshPointSelection, startAnimation])
}, [callMatchCirclesToData, callRefreshPointPositions, dataConfiguration, dataset, refreshPointSelection])

// respond to value changes
useEffect(() => {
Expand Down Expand Up @@ -232,20 +228,12 @@ export const usePlotResponders = (props: IPlotResponderProps) => {
graphModel?.setPointConfig("points")
}

matchCirclesToData({
dataConfiguration,
pointRadius: graphModel.getPointRadius(),
pointColor: graphModel.pointDescription.pointColor,
pointDisplayType: graphModel.pointDisplayType,
pointStrokeColor: graphModel.pointDescription.pointStrokeColor,
pixiPoints,
startAnimation, instanceId
})
callMatchCirclesToData()
callRefreshPointPositions(false)
}
}) || (() => true)
return () => disposer()
}, [dataset, dataConfiguration, startAnimation, graphModel, callRefreshPointPositions, instanceId, pixiPoints])
}, [callMatchCirclesToData, dataset, dataConfiguration, graphModel, callRefreshPointPositions, pixiPoints])

// respond to pointDisplayType changes
useEffect(function respondToPointConfigChange() {
Expand All @@ -254,19 +242,11 @@ export const usePlotResponders = (props: IPlotResponderProps) => {
() => {
if (!pixiPoints) return

matchCirclesToData({
dataConfiguration,
pointRadius: graphModel.getPointRadius(),
pointColor: graphModel.pointDescription.pointColor,
pointDisplayType: graphModel.pointDisplayType,
pointStrokeColor: graphModel.pointDescription.pointStrokeColor,
pixiPoints,
startAnimation, instanceId
})
callMatchCirclesToData()
callRefreshPointPositions(false)
}, {name: "usePlot [pointDisplayType]"}, graphModel
)
}, [callRefreshPointPositions, dataConfiguration, graphModel, instanceId, pixiPoints, startAnimation])
}, [callMatchCirclesToData, callRefreshPointPositions, graphModel, pixiPoints])

useEffect(() => {
return mstReaction(
Expand Down Expand Up @@ -296,4 +276,13 @@ export const usePlotResponders = (props: IPlotResponderProps) => {
{name: "respondToPointVisualChange", equals: comparer.structural}, graphModel
)
}, [callRefreshPointPositions, graphModel])

// respond to attribute color change
useEffect(function respondToColorChange() {
return mstReaction(
() => metadata?.getAttributeColorRange(legendAttrID),
() => callRefreshPointPositions(false),
{ name: "usePlotResponders respondToColorChange", equals: comparer.structural }, metadata
)
}, [callRefreshPointPositions, legendAttrID, metadata])
}
1 change: 1 addition & 0 deletions v3/src/models/codap/create-codap-document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ describe("createCodapDocument", () => {
},
[caseMetadata.id]: {
sharedModel: {
attributeColorRanges: {},
categories: {},
collections: {},
data: "test-5",
Expand Down
4 changes: 4 additions & 0 deletions v3/src/models/shared/shared-case-metadata-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { schemeBlues } from "d3"

export const kDefaultLowAttributeColor = schemeBlues[5][0]
export const kDefaultHighAttributeColor = schemeBlues[5][4]
Loading

0 comments on commit beafe64

Please sign in to comment.