Skip to content

Commit

Permalink
Provide standard error adornment (#1261)
Browse files Browse the repository at this point in the history
* [#186948720] Feature: User can display a standard error bar on a dot plot

* Duplicated `standard-deviation` folder and fill in with standard error
* Register the standard error adornment
* Prepare for v2 import of standard error adornment
* Created standard-error-adornment-component.tsx separate from univariate-measure-adornment-simple-component.tsx
* Pass a `spannerRef` down to adornments so that adornments can add d3 elements that won't be clipped by the subplots; e.g. the hover lines that get shown on hovering over standard error bar.
* Change measure tip style to more closely match V2
* When creating d3 elements for displaying the measure, make sure to remove possible previously created elements first
* Enhance useAdornmentAttributes to return additional stuff that can be used by UnivariateMeasureAdornmentSimpleComponent and StandardErrorAdornmentComponent
* Some localizable strings related to standard error that exist in the latest V2 had not yet made it into V3

* * Fix lint errors regarding dependencies in univariate-measure-adornment-simple-component.tsx
* Fix test in v2-adornment-importer.test.ts

* * Code review changes
  • Loading branch information
bfinzer authored May 16, 2024
1 parent 0c4ace2 commit 9f36d3b
Show file tree
Hide file tree
Showing 24 changed files with 680 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { INumericAxisModel } from "../../axis/models/axis-model"

export interface IAdornmentComponentProps {
cellKey: Record<string, string>
cellCoords: { row: number, col: number }
containerId: string
model: IAdornmentModel
plotHeight: number
plotWidth: number
xAxis?: INumericAxisModel
yAxis?: INumericAxisModel
spannerRef?: React.RefObject<SVGSVGElement>
}

export interface IAdornmentControlsProps {
Expand Down
12 changes: 9 additions & 3 deletions v3/src/components/graph/adornments/adornment-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { IMeanAdornmentModel, MeanAdornmentModel } from "./univariate-measures/m
import { IMedianAdornmentModel, MedianAdornmentModel } from "./univariate-measures/median/median-adornment-model"
import { IStandardDeviationAdornmentModel, StandardDeviationAdornmentModel }
from "./univariate-measures/standard-deviation/standard-deviation-adornment-model"
import { IStandardErrorAdornmentModel, StandardErrorAdornmentModel }
from "./univariate-measures/standard-error/standard-error-adornment-model"
import { IMeanAbsoluteDeviationAdornmentModel, MeanAbsoluteDeviationAdornmentModel }
from "./univariate-measures/mean-absolute-deviation/mean-absolute-deviation-adornment-model"
import { BoxPlotAdornmentModel, IBoxPlotAdornmentModel } from "./univariate-measures/box-plot/box-plot-adornment-model"
Expand All @@ -36,19 +38,23 @@ const adornmentTypeDispatcher = (adornmentSnap: IAdornmentModel) => {
case "Plotted Function": return PlottedFunctionAdornmentModel
case "Plotted Value": return PlottedValueAdornmentModel
case "Standard Deviation": return StandardDeviationAdornmentModel
default: return UnknownAdornmentModel
case "Standard Error": return StandardErrorAdornmentModel
default: {
console.warn(`Unknown adornment type: ${adornmentSnap.type}`)
return UnknownAdornmentModel
}
}
}

export const AdornmentModelUnion = types.union({ dispatcher: adornmentTypeDispatcher },
BoxPlotAdornmentModel, CountAdornmentModel, LSRLAdornmentModel, MeanAdornmentModel,
MeanAbsoluteDeviationAdornmentModel, MedianAdornmentModel, MovableValueAdornmentModel, MovableLineAdornmentModel,
MovablePointAdornmentModel, PlottedFunctionAdornmentModel, PlottedValueAdornmentModel,
StandardDeviationAdornmentModel, UnknownAdornmentModel)
StandardDeviationAdornmentModel, StandardErrorAdornmentModel, UnknownAdornmentModel)
export type IAdornmentModelUnion = IBoxPlotAdornmentModel | ICountAdornmentModel | ILSRLAdornmentModel |
IMeanAdornmentModel | IMeanAbsoluteDeviationAdornmentModel | IMedianAdornmentModel | IMovableValueAdornmentModel |
IMovableLineAdornmentModel | IMovablePointAdornmentModel | IPlottedFunctionAdornmentModel |
IPlottedValueAdornmentModel | IStandardDeviationAdornmentModel | IUnknownAdornmentModel
IPlottedValueAdornmentModel | IStandardDeviationAdornmentModel | IStandardErrorAdornmentModel | IUnknownAdornmentModel

export const ParentAdornmentTypes = ["Univariate Measure"] as const
export type ParentAdornmentType = typeof ParentAdornmentTypes[number]
1 change: 1 addition & 0 deletions v3/src/components/graph/adornments/adornment-ui-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const measures: IMeasures = {
{
title: "DG.Inspector.graphSpreadOptions", type: "Group", rulerStateKey: 'measuresOfSpread', items: [
{title: "DG.Inspector.graphPlottedStDev", type: "Standard Deviation"},
{title: "DG.Inspector.graphPlottedStErr", type: "Standard Error"},
{title: "DG.Inspector.graphPlottedMeanAbsDev", type: "Mean Absolute Deviation"},
]
},
Expand Down
7 changes: 6 additions & 1 deletion v3/src/components/graph/adornments/adornment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ import "./adornment.scss"
interface IProps {
adornment: IAdornmentModel
cellKey: Record<string, string>
cellCoords: { row: number, col: number }
spannerRef: React.RefObject<SVGSVGElement>
}

export const Adornment = observer(function Adornment({adornment, cellKey: _cellKey}: IProps) {
export const Adornment = observer(function Adornment({adornment, cellKey: _cellKey,
cellCoords, spannerRef}: IProps) {
const cellKey = useDeepCompareMemo(() => _cellKey, [_cellKey])
const graphModel = useGraphContentModelContext()
const { subPlotWidth, subPlotHeight } = useSubplotExtent()
Expand Down Expand Up @@ -47,13 +50,15 @@ export const Adornment = observer(function Adornment({adornment, cellKey: _cellK
>
<Component
cellKey={cellKey}
cellCoords={cellCoords}
containerId={adornmentKey}
key={adornmentKey}
model={adornment}
plotHeight={subPlotHeight}
plotWidth={subPlotWidth}
xAxis={graphModel.getNumericAxis('bottom')}
yAxis={graphModel.getNumericAxis('left')}
spannerRef={spannerRef}
/>
</div>
)
Expand Down
11 changes: 10 additions & 1 deletion v3/src/components/graph/adornments/adornments.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,13 @@
position: absolute;
top: 0;
width: 100%;
}
}

.adornment-spanner {
display: block;
position: absolute;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
22 changes: 16 additions & 6 deletions v3/src/components/graph/adornments/adornments.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from "react"
import React, { useEffect, useRef } from "react"
import { clsx } from "clsx"
import { observer } from "mobx-react-lite"
import { mstAutorun } from "../../../utilities/mst-autorun"
Expand All @@ -25,6 +25,7 @@ export const Adornments = observer(function Adornments() {
const { isTileSelected } = useTileModelContext()
const adornments = graphModel.adornmentsStore.adornments
const { left, top, width, height } = layout.computedBounds.plot
const spannerRef = useRef<SVGSVGElement>(null)

useEffect(function handleAdornmentBannerCountChange() {
return mstAutorun(() => {
Expand Down Expand Up @@ -56,6 +57,7 @@ export const Adornments = observer(function Adornments() {
const yAttrId = dataConfig?.attributeID("y")
const yAttrType = dataConfig?.attributeType("y")
const yCatValues = layout.getAxisMultiScale("left").categoryValues
// yCats is the array of categorical values for the y axis (the one on the left)
const yCats = yAttrType === "categorical" && yCatValues ? yCatValues : [""]
const topAttrId = dataConfig?.attributeID("topSplit")
const topCatValues = layout.getAxisMultiScale("top").categoryValues
Expand All @@ -71,7 +73,9 @@ export const Adornments = observer(function Adornments() {
// Inside each cell of the outer grid, we build an "inner grid" which is determined by the attributes
// on the bottom and left axes.
const outerGridCells: React.JSX.Element[] = []
// bottomRepetitions is the number of repetitions of the bottom axis brought about by categories on the top axis
const bottomRepetitions = dataConfig?.numRepetitionsForPlace('bottom') ?? 1
// leftRepetitions is the number of repetitions of the left axis brought about by categories on the right axis
const leftRepetitions = dataConfig?.numRepetitionsForPlace('left') ?? 1
const outerGridStyle = {
gridTemplateColumns: `repeat(${bottomRepetitions}, 1fr)`,
Expand All @@ -85,7 +89,6 @@ export const Adornments = observer(function Adornments() {
gridTemplateColumns: `repeat(${xCats.length}, 1fr)`,
gridTemplateRows: `repeat(${yCats.length}, 1fr)`,
}

for (let topIndex = 0; topIndex < bottomRepetitions; topIndex++) {
for (let rightIndex = 0; rightIndex < leftRepetitions; rightIndex++) {
const adornmentNodes = []
Expand All @@ -94,6 +97,8 @@ export const Adornments = observer(function Adornments() {
// The cellKey is an object that contains the attribute IDs and categorical values for the
// current graph cell. It's used to uniquely identify that cell.
let cellKey: Record<string, string> = {}
const cellCoords = { row: rightIndex * yCats.length + yIndex,
col: topIndex * xCats.length + xIndex}
if (topAttrId) {
cellKey = updateCellKey(cellKey, topAttrId, topCats[topIndex])
}
Expand Down Expand Up @@ -121,11 +126,12 @@ export const Adornments = observer(function Adornments() {
// skip adornments that don't support current plot type
const adornmentContentInfo = getAdornmentContentInfo(adornment.type)
if (!adornmentContentInfo.plots.includes(graphModel.plotType)) return

return <Adornment
key={`graph-adornment-${adornment.id}-${yIndex}-${xIndex}-${rightIndex}-${topIndex}`}
adornment={adornment}
cellKey={cellKey}
cellCoords={cellCoords}
spannerRef={spannerRef}
/>
})
}
Expand Down Expand Up @@ -153,9 +159,13 @@ export const Adornments = observer(function Adornments() {
{adornmentBanners}
</div>
}
<div className={containerClass} data-testid={kGraphAdornmentsClass} style={outerGridStyle}>
{outerGridCells}
</div>
<div className={containerClass} data-testid={kGraphAdornmentsClass} style={outerGridStyle}>
{outerGridCells}
</div>
<div className={'adornment-spanner'} style={outerGridStyle}>
{/*The following svg can be used by adornments that need to draw outside their grid cell*/}
<svg className="spanner-svg" ref={spannerRef}/>
</div>
</>
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const PlottedValueComponent = observer(
return (
<UnivariateMeasureAdornmentSimpleComponent
cellKey={cellKey}
cellCoords={{row: 0, col: 0}} // Not used in Plotted Value
containerId={containerId}
model={model}
plotHeight={plotHeight}
Expand Down
Loading

0 comments on commit 9f36d3b

Please sign in to comment.