From ee03c39c83c134194d756c04b0eaf2d0b74c90b4 Mon Sep 17 00:00:00 2001 From: William Finzer Date: Thu, 6 Jun 2024 14:36:28 -0700 Subject: [PATCH] [#187751261] Feature: User can plot a normal curve on top of a histogram Fortunately, it just takes a few tweaks to normal-curve-adornment-component.tsx to get the curve to use the count axis and histogram bin width in place of the dots and overlap to get this to work. * Also fixed a dependency error in background.tsx --- .../data-display/components/background.tsx | 2 +- .../normal-curve-adornment-component.tsx | 39 ++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/v3/src/components/data-display/components/background.tsx b/v3/src/components/data-display/components/background.tsx index 7b8044a8c2..f7afe3c34d 100644 --- a/v3/src/components/data-display/components/background.tsx +++ b/v3/src/components/data-display/components/background.tsx @@ -108,7 +108,7 @@ export const Background = forwardRef((prop }) } marqueeState.setMarqueeRect({x: startX.current, y: startY.current, width: 0, height: 0}) - }, [datasetsArray, marqueeState, pixiPointsArrayRef]), + }, [bgRef, datasetsArray, marqueeState, pixiPointsArrayRef]), onDrag = useCallback((event: { dx: number; dy: number }) => { if (event.dx !== 0 || event.dy !== 0 && datasetsArray.length) { diff --git a/v3/src/components/graph/adornments/univariate-measures/normal-curve/normal-curve-adornment-component.tsx b/v3/src/components/graph/adornments/univariate-measures/normal-curve/normal-curve-adornment-component.tsx index 0a2d873d4f..733ec089c9 100644 --- a/v3/src/components/graph/adornments/univariate-measures/normal-curve/normal-curve-adornment-component.tsx +++ b/v3/src/components/graph/adornments/univariate-measures/normal-curve/normal-curve-adornment-component.tsx @@ -44,7 +44,9 @@ export const NormalCurveAdornmentComponent = observer( const helper = useMemo(() => { return new UnivariateMeasureAdornmentHelper(cellKey, layout, model, plotHeight, plotWidth, containerId) }, [cellKey, containerId, layout, model, plotHeight, plotWidth]) + const isHistogram = graphModel.pointDisplayType === "histogram" const numericScale = isVertical.current ? helper.xScale : helper.yScale + const countScale = isHistogram ? (isVertical.current ? helper.yScale : helper.xScale) : undefined const {cellCounts} = useAdornmentCells(model, cellKey) const isBlockingOtherMeasure = dataConfig && helper.blocksOtherMeasure({adornmentsStore, attrId: numericAttrId, dataConfig, isVertical: isVertical.current}) @@ -174,7 +176,6 @@ export const NormalCurveAdornmentComponent = observer( * - the line segment representing a specified number of standard errors on each side of the mean */ const symbolPathF = (p: { x: number, y: number, width: number, cellHeight: number }, iIsHorizontal: boolean) => { - const normalF = (x: number) => { return normal(x, amplitude, mean, stdDev) } @@ -184,27 +185,37 @@ export const NormalCurveAdornmentComponent = observer( return iIsHorizontal ? p.y - tStackCoord : p.x + tStackCoord } + const countToScreenCoordFromHistogram = (iCount: number) => { + if (!countScale) { + return 0 + } else { + return isVertical.current ? countScale(iCount) / cellCounts.y : countScale(iCount) / cellCounts.x + } + } + + const countAxisFunc = isHistogram ? countToScreenCoordFromHistogram : countToScreenCoordFromDotPlot, + sqrtTwoPi = Math.sqrt(2 * Math.PI), + pointRadius = graphModel.getPointRadius(), + numCellsNumeric = isVertical.current ? cellCounts.x : cellCounts.y, + overlap = graphModel.pointOverlap, + binWidth = isHistogram ? graphModel.binWidth + : Math.abs(numericScale.invert(pointRadius * 2) - numericScale.invert(0)) + + if (!countAxisFunc || binWidth === undefined) return "" + let path = '' /* let sESegment = '', sESegmentPixelLength: number */ - - const sqrtTwoPi = Math.sqrt(2 * Math.PI), - // isHistogram = false, //this.getPath('model.plotModel.dotsAreFused'), - countAxisFunc = /*isHistogram ? tCountAxisView.dataToCoordinate.bind(tCountAxisView) - :*/ countToScreenCoordFromDotPlot, - pointRadius = graphModel.getPointRadius(), - numCellsNumeric = isVertical.current ? cellCounts.x : cellCounts.y, - overlap = graphModel.pointOverlap, - binWidth = /*isHistogram ? tParentPlotView.getPath('model.width') - :*/ Math.abs(numericScale.invert(pointRadius * 2) - numericScale.invert(0)), + const pixelRange = numericScale.range(), pixelMin = isVertical.current ? pixelRange[0] : pixelRange[1], pixelMax = isVertical.current ? pixelRange[1] : pixelRange[0], + numeratorForAmplitude = isHistogram ? 1 : numCellsNumeric, // todo: For a gaussian fit amplitude is a fitted parameter - amplitude = (numCellsNumeric / (stdDev * sqrtTwoPi)) * caseCount * binWidth, + amplitude = (numeratorForAmplitude / (stdDev * sqrtTwoPi)) * caseCount * binWidth, points = [], kPixelGap = 1, meanSegmentPixelLength = countAxisFunc(normalF(mean)) - countAxisFunc(0), @@ -272,8 +283,8 @@ export const NormalCurveAdornmentComponent = observer( .attr("id", `${helper.generateIdString("path")}`) .attr("data-testid", `${helper.measureSlug}-normal-curve`) .attr("d", theSymbolPath) - }, [caseCount, cellCounts.x, cellCounts.y, graphModel, helper, isVertical, - layout.plotHeight, mean, numericScale, stdDev, valueRef]) + }, [caseCount, cellCounts.x, cellCounts.y, countScale, graphModel, helper, isHistogram, isVertical, + layout.plotHeight, mean, numericScale, stdDev, valueRef]) const addAdornmentElements = useCallback((measure: IMeasureInstance, selectionsObj: INormalCurveSelections, labelObj: ILabel) => {