Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/components/InlineHint/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { InfoCircleOutlined } from "@ant-design/icons";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied from TFE. Appears as a small hoverable or tab-focusable tooltip.

Image

import { Tooltip } from "antd";
import React, { ReactElement, ReactNode, useRef } from "react";

type InlineHintProps = {
title?: ReactNode;
};

/** An icon that can be hovered or focused to show an informational tooltip. */
export default function InlineHint(props: InlineHintProps): ReactElement {
const popupContainerRef = useRef<HTMLDivElement>(null);

return (
<div ref={popupContainerRef}>
<Tooltip
trigger={["focus", "hover"]}
title={props.title}
getPopupContainer={() => popupContainerRef.current ?? document.body}
>
<button
style={{
background: "none",
border: "none",
padding: 0,
margin: 0,
cursor: "help",
color: "unset",
}}
aria-label="More information"
>
<InfoCircleOutlined></InfoCircleOutlined>
</button>
</Tooltip>
</div>
);
}
14 changes: 9 additions & 5 deletions src/components/LabeledSlider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { InputNumber, Row } from "antd";
import Slider, { SliderSingleProps } from "antd/es/slider";
import React, { ReactElement, useEffect, useState } from "react";
import React, { ReactElement, ReactNode, useEffect, useId, useState } from "react";

type LabeledSliderProps = {
sliderProps: SliderSingleProps;
label: string;
label: string | ReactNode;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to allow the inline hint to also be shown here.

I'd love to pull in a bunch of the components I've written in TFE, especially for handling input and settings organization......

labelWidth?: string;
id?: string;
inputMax?: number;
Expand All @@ -25,7 +25,8 @@ export default function LabeledSlider(props: LabeledSliderProps): ReactElement {
}
};

const inputId = props.id || `labeled-slider-${props.label.toLowerCase().replace(/\s+/g, "-")}`;
const generatedId = useId();
const inputId = props.id || `labeled-slider-${generatedId}`;

useEffect(() => {
setInputValue(props.sliderProps.value);
Expand All @@ -43,7 +44,10 @@ export default function LabeledSlider(props: LabeledSliderProps): ReactElement {
}}
wrap={false}
>
<label htmlFor={inputId} style={{ width: props.labelWidth }}>
<label
htmlFor={inputId}
style={{ width: props.labelWidth, flexBasis: props.labelWidth, flexShrink: 0 }}
>
{props.label}
</label>

Expand All @@ -61,7 +65,7 @@ export default function LabeledSlider(props: LabeledSliderProps): ReactElement {
onBlur={onConfirmInputValue}
></InputNumber>

<div style={{ width: "100%" }} ref={containerRef}>
<div style={{ width: "100%", flexGrow: 1, flexShrink: 1 }} ref={containerRef}>
<Slider
{...props.sliderProps}
tooltip={{
Expand Down
134 changes: 134 additions & 0 deletions src/components/PlotLineSettings/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, { ReactElement } from "react";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

import { connect } from "react-redux";
import { ActionCreator } from "redux";

import {
SetLineAverageWindowAction,
SetLineDefaultColorAction,
SetLineWidthAction,
} from "../../state/selection/types";
import { State } from "../../state/types";
import { useDebouncedSetter } from "../../hooks";
import LabeledSlider from "../LabeledSlider";
import {
getLineDefaultColor,
getLineMovingAverageWindow,
getLineWidth,
} from "../../state/selection/selectors";
import {
setConnectLineDefaultColor,
setConnectLineAverageWindow,
setConnectLineWidth,
} from "../../state/selection/actions";
import ResettableColorPicker from "../ResettableColorPicker";
import { GENERAL_PLOT_SETTINGS, PALETTE } from "../../constants";
import InlineHint from "../InlineHint";

type PropsFromState = {
lineAverageWindow: number;
lineWidth: number;
lineDefaultColor: string;
};

type DispatchProps = {
handleSetLineAverageWindow: ActionCreator<SetLineAverageWindowAction>;
handleSetLineWidth: ActionCreator<SetLineWidthAction>;
handleSetLineDefaultColor: ActionCreator<SetLineDefaultColorAction>;
};

type PlotLineSettingsProps = PropsFromState &
DispatchProps & {
labelWidth?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a comment on what labelWidth is for? (I'm wondering: why is it a string? what label?)

@ShrimpCryptid ShrimpCryptid Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, can do. It's a string because it's intended to take a CSS specifier, e.g. "100px" or "100%". The label is the label for any settings inputs (sliders or numeric inputs) that sits to the left.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

labelsColumnCssWidth ?

};

const PlotLineSettings = (props: PlotLineSettingsProps): ReactElement => {
const [lineAverageWindow, setLineAverageWindow] = useDebouncedSetter(
props.lineAverageWindow,
props.handleSetLineAverageWindow
);
const [lineWidth, setLineWidth] = useDebouncedSetter(props.lineWidth, props.handleSetLineWidth);
const [lineDefaultColor, setLineDefaultColor] = useDebouncedSetter(
props.lineDefaultColor,
props.handleSetLineDefaultColor
);

return (
<div style={{ display: "flex", flexDirection: "column", width: "100%" }}>
<p style={{ fontWeight: 600, marginBottom: 0, color: PALETTE.white }}>Line settings</p>
<LabeledSlider
label={
<div style={{ display: "flex", flexDirection: "row", gap: "6px" }}>
Average window
<InlineHint title="Total number of points to average over, including past and future." />
</div>
}
id={"line-average-window-input"}
labelWidth={props.labelWidth}
sliderProps={{
value: lineAverageWindow,
onChange: setLineAverageWindow,
min: 1,
max: 31,
step: 2,
}}
inputMin={1}
inputMax={101}
Comment thread
ShrimpCryptid marked this conversation as resolved.
></LabeledSlider>
<LabeledSlider
label="Line width"
labelWidth={props.labelWidth}
sliderProps={{
value: lineWidth,
onChange: setLineWidth,
min: 0.1,
max: 3.5,
step: 0.1,
marks: { 1.5: <></> },
tooltip: {
formatter: (value) => value?.toFixed(1),
},
}}
inputMin={0}
inputMax={100}
Comment thread
ShrimpCryptid marked this conversation as resolved.
></LabeledSlider>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
marginTop: "4px",
}}
>
<label style={{ width: props.labelWidth }}>Default color</label>
<ResettableColorPicker
value={lineDefaultColor}
onChange={(color) => {
setLineDefaultColor(color.toHexString());
}}
size="small"
onReset={function (): void {
setLineDefaultColor(GENERAL_PLOT_SETTINGS.connectionLineDefaultColor);
}}
></ResettableColorPicker>
</div>
</div>
);
};

function mapStateToProps(state: State): PropsFromState {
return {
lineAverageWindow: getLineMovingAverageWindow(state),
lineWidth: getLineWidth(state),
lineDefaultColor: getLineDefaultColor(state),
};
}

const dispatchToPropsMap: DispatchProps = {
handleSetLineAverageWindow: setConnectLineAverageWindow,
handleSetLineDefaultColor: setConnectLineDefaultColor,
handleSetLineWidth: setConnectLineWidth,
};
export default connect<PropsFromState, DispatchProps, unknown, State>(
mapStateToProps,
dispatchToPropsMap
)(PlotLineSettings);
7 changes: 5 additions & 2 deletions src/components/PlotSettings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SetPointOpacityAction, SetPointRadiusAction } from "../../state/selecti
import { State } from "../../state/types";
import { useDebouncedSetter } from "../../hooks";
import LabeledSlider from "../LabeledSlider";
import PlotLineSettings from "../PlotLineSettings";

type PropsFromState = {
pointOpacity: number;
Expand All @@ -19,7 +20,8 @@ type DispatchProps = {
};

type PlotSettingsProps = PropsFromState & DispatchProps;
const SETTINGS_LABEL_WIDTH = "110px";
const SETTINGS_WIDTH = "280px";
const SETTINGS_LABEL_WIDTH = "135px";

const PlotSettings = (props: PlotSettingsProps): ReactElement => {
const { pointOpacity, pointRadius, handleSetPointOpacity, handleSetPointRadius } = props;
Expand All @@ -28,7 +30,7 @@ const PlotSettings = (props: PlotSettingsProps): ReactElement => {
const [radius, setRadius] = useDebouncedSetter(pointRadius, handleSetPointRadius);

return (
<div style={{ display: "flex", flexDirection: "column", width: "250px" }}>
<div style={{ display: "flex", flexDirection: "column", width: SETTINGS_WIDTH }}>
<LabeledSlider
label="Opacity"
labelWidth={SETTINGS_LABEL_WIDTH}
Expand Down Expand Up @@ -58,6 +60,7 @@ const PlotSettings = (props: PlotSettingsProps): ReactElement => {
inputMin={0}
inputMax={100}
></LabeledSlider>
<PlotLineSettings labelWidth={SETTINGS_LABEL_WIDTH} />
</div>
);
};
Expand Down
3 changes: 2 additions & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ export const GENERAL_PLOT_SETTINGS = {
cellName: CELL_ID_KEY,
chartParent: "ace-scatter-chart",
circleRadius: 4,
connectionLineWidth: 1,
connectionLineWidth: 1.5,
connectionLineDefaultColor: BASE_PALETTE_COLORS.mediumDarkGray as string,
histogramColor: BASE_PALETTE_COLORS.lightGray,
legend: {
font: {
Expand Down
31 changes: 23 additions & 8 deletions src/containers/MainPlotContainer/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
getFilteredConnectByFeatureValues,
getShowConnectLines,
getFilteredConnectByCategoryValues,
getLineMovingAverageWindow,
} from "../../state/selection/selectors";
import { MainPlotSettings, SelectedPointData, TickConversion } from "../../state/selection/types";
import {
Expand All @@ -56,6 +57,7 @@ import {
import { findFeature } from "../../state/util";
import { getGroupByTitle } from "../ColorByMenu/selectors";
import { PlotlyAnnotation } from "../../components/MainPlot";
import { getMovingAverage } from "../../util/math";

export const handleNullValues = (
inputXValues: (number | null)[],
Expand Down Expand Up @@ -179,6 +181,7 @@ export const getLinePlotData = createSelector(
getFilteredConnectByCategoryValues,
getFilteredConnectByFeatureValues,
getShowConnectLines,
getLineMovingAverageWindow,
],
calculateLinePlotData
);
Expand All @@ -188,7 +191,8 @@ export function calculateLinePlotData(
yValues: (number | null)[],
connectByCategoryValues: (number | null)[],
connectByFeatureValues: (number | null)[],
showConnectingLines: boolean
showConnectingLines: boolean,
movingAverageWindow: number
): LinePlotData[] | null {
if (!showConnectingLines) {
return null;
Expand Down Expand Up @@ -228,6 +232,14 @@ export function calculateLinePlotData(
lineData.push({ x: x, y: y });
}

// Apply moving average to each line
if (movingAverageWindow > 1) {
for (const line of lineData) {
line.y = getMovingAverage(line.y, movingAverageWindow, true);
line.x = getMovingAverage(line.x, movingAverageWindow, true);
}
}

return lineData;
}

Expand Down Expand Up @@ -381,7 +393,7 @@ function makeScatterPlotData(
}

// TODO: Add the ability to adjust the line settings via an additional selector
function makeLinePlotTrace(data: LinePlotData): Partial<PlotData> {
function makeLinePlotTrace(data: LinePlotData, settings: MainPlotSettings): Partial<PlotData> {
return {
type: "scattergl",
mode: "lines",
Expand All @@ -390,8 +402,8 @@ function makeLinePlotTrace(data: LinePlotData): Partial<PlotData> {
y: data.y,
showlegend: false,
line: {
width: GENERAL_PLOT_SETTINGS.connectionLineWidth,
color: PALETTE.mediumDarkGray,
width: settings.connectionLineWidth,
color: settings.connectionLineDefaultColor,
},
};
}
Expand Down Expand Up @@ -472,19 +484,22 @@ export const getScatterPlotDataArray = createSelector(
[composePlotlyData, getMainPlotSettings],
(allPlotData, mainPlotSettings): Partial<PlotData>[] => {
const { mainPlotData, selectedGroupPlotData } = allPlotData;
let data = [
let traces = [
makeHistogramPlotX(mainPlotData.x),
makeHistogramPlotY(mainPlotData.y),
makeScatterPlotData(mainPlotData, mainPlotSettings),
];
if (selectedGroupPlotData) {
data.push(makeScatterPlotData(selectedGroupPlotData, mainPlotSettings));
traces.push(makeScatterPlotData(selectedGroupPlotData, mainPlotSettings));
}
if (allPlotData.linePlotData) {
data = [...allPlotData.linePlotData.map(makeLinePlotTrace), ...data];
const lineTraces = allPlotData.linePlotData.map((line) =>
makeLinePlotTrace(line, mainPlotSettings)
);
traces = [...lineTraces, ...traces];
}

return data;
return traces;
}
);

Expand Down
Loading
Loading