diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 9e1d8eea96..bccd357bb4 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "6.70.8", + "version": "6.71.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.70.8", + "version": "6.71.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 30d6f24d4c..b02aa74fff 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "6.70.8", + "version": "6.71.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 13bfd7a7df..ed1fce005f 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,13 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version 6.71.0 +*Released*: 20 November 2025 +- Line chart trendline options for provided parameters to CalculateCurveFit API + - ChartBuilderModal update to include trendline option for provided parameters + - Include new 5 Parameter nonlinear curve fit option in trendline select + - Use column metadata displayWidth in app grid column render calcWidths + ### version 6.70.8 *Released*: 19 November 2025 - Merge from release25.11-SNAPSHOT to develop diff --git a/packages/components/src/internal/components/base/Grid.tsx b/packages/components/src/internal/components/base/Grid.tsx index 2b970c9176..cf14dcaa22 100644 --- a/packages/components/src/internal/components/base/Grid.tsx +++ b/packages/components/src/internal/components/base/Grid.tsx @@ -48,7 +48,7 @@ function processColumns(columns: List): List { raw: c, tableCell: c.tableCell, title: c.title || c.caption, - width: c.width, + width: c.width || c.displayWidth, }); }) .toList(); diff --git a/packages/components/src/internal/components/chart/Chart.tsx b/packages/components/src/internal/components/chart/Chart.tsx index 37fc366297..cde101b31a 100644 --- a/packages/components/src/internal/components/chart/Chart.tsx +++ b/packages/components/src/internal/components/chart/Chart.tsx @@ -175,7 +175,7 @@ export const SVGChart: FC = memo(({ api, chart, container, filters, query measureStore, trendlineData ); - setPlot(plots[0]); + if (plots) setPlot(plots[0]); } } }; diff --git a/packages/components/src/internal/components/chart/ChartBuilderModal.tsx b/packages/components/src/internal/components/chart/ChartBuilderModal.tsx index d39e674ae5..5445e6378d 100644 --- a/packages/components/src/internal/components/chart/ChartBuilderModal.tsx +++ b/packages/components/src/internal/components/chart/ChartBuilderModal.tsx @@ -74,6 +74,7 @@ export const getChartBuilderQueryConfig = ( viewName: savedConfig?.viewName || viewName, columns: Object.values(fieldValues) .filter(field => field?.value && typeof field.value === 'string') // just those fields with values + .filter(field => !field.equation) // exclude the trendlineType field (which has an equation value) .map(field => field.data?.fieldKey ?? field.value), // Issue 52050: use fieldKey for special characters sort: LABKEY_VIS.GenericChartHelper.getQueryConfigSortKey(chartConfig.measures), filterArray: savedConfig?.filterArray ?? [], @@ -140,6 +141,7 @@ export const getChartBuilderChartConfig = ( trendlineType: undefined, trendlineAsymptoteMin: undefined, trendlineAsymptoteMax: undefined, + trendlineParameters: undefined, ...savedConfig?.geomOptions, }, } as ChartConfig; @@ -186,6 +188,7 @@ export const getChartBuilderChartConfig = ( config.geomOptions.trendlineType = type === '' ? undefined : type; config.geomOptions.trendlineAsymptoteMin = fieldValues.trendlineAsymptoteMin?.value; config.geomOptions.trendlineAsymptoteMax = fieldValues.trendlineAsymptoteMax?.value; + config.geomOptions.trendlineParameters = fieldValues.trendlineParameters?.value; } if ( @@ -377,8 +380,10 @@ const ChartTypeQueryForm: FC = memo(props => { {hasTrendlineOption && ( )} @@ -673,6 +678,11 @@ export const ChartBuilderModal: FC = memo(({ actions, mo value: chartConfig.geomOptions.trendlineAsymptoteMax, }; } + if (chartConfig.geomOptions.trendlineParameters) { + fieldValues_['trendlineParameters'] = { + value: chartConfig.geomOptions.trendlineParameters, + }; + } } return fieldValues_; diff --git a/packages/components/src/internal/components/chart/ChartFieldOption.test.tsx b/packages/components/src/internal/components/chart/ChartFieldOption.test.tsx index 7cc9917dd7..c67e2e8455 100644 --- a/packages/components/src/internal/components/chart/ChartFieldOption.test.tsx +++ b/packages/components/src/internal/components/chart/ChartFieldOption.test.tsx @@ -13,12 +13,13 @@ import { SchemaQuery } from '../../../public/SchemaQuery'; import { QueryInfo } from '../../../public/QueryInfo'; import { ViewInfo } from '../../ViewInfo'; -import { ChartFieldOption, getSelectOptions } from './ChartFieldOption'; +import { ChartFieldOption } from './ChartFieldOption'; import { ChartFieldInfo, ChartTypeInfo } from './models'; LABKEY_VIS = { GenericChartHelper: { getAllowableTypes: () => ['int', 'double'], + isMeasureDimensionMatch: () => true, isNumericType: (type: string) => type === 'int', }, }; @@ -60,28 +61,6 @@ const model = makeTestQueryModel( 0 ); -describe('getSelectOptions', () => { - test('hasMatchingType', () => { - LABKEY_VIS.GenericChartHelper = { - ...LABKEY_VIS.GenericChartHelper, - isMeasureDimensionMatch: () => false, - }; - const field = { name: 'x' } as ChartFieldInfo; - const options = getSelectOptions(model, BAR_CHART_TYPE, field); - expect(options.length).toBe(2); - }); - - test('isMeasureDimensionMatch', () => { - LABKEY_VIS.GenericChartHelper = { - ...LABKEY_VIS.GenericChartHelper, - isMeasureDimensionMatch: () => true, - }; - const field = { name: 'x' } as ChartFieldInfo; - const options = getSelectOptions(model, BAR_CHART_TYPE, field); - expect(options.length).toBe(3); - }); -}); - describe('ChartFieldOption', () => { test('line chart for x, showFieldOptions for int', async () => { render( @@ -89,8 +68,10 @@ describe('ChartFieldOption', () => { field={{ name: 'x', label: 'X Axis', required: true } as ChartFieldInfo} fieldValues={{ x: { value: 'field1', data: { type: 'int' } } }} model={model} + onErrorBarChange={jest.fn()} onScaleChange={jest.fn()} onSelectFieldChange={jest.fn()} + scaleValues={undefined} selectedType={LINE_PLOT_TYPE} /> ); @@ -108,8 +89,10 @@ describe('ChartFieldOption', () => { field={{ name: 'x', label: 'X Axis', required: true } as ChartFieldInfo} fieldValues={{ x: { value: 'field1', data: { type: 'date' } } }} model={model} + onErrorBarChange={jest.fn()} onScaleChange={jest.fn()} onSelectFieldChange={jest.fn()} + scaleValues={undefined} selectedType={LINE_PLOT_TYPE} /> ); @@ -127,8 +110,10 @@ describe('ChartFieldOption', () => { field={{ name: 'x', label: 'X Axis', required: true } as ChartFieldInfo} fieldValues={{ x: { value: 'field1', data: { type: 'int' } } }} model={model} + onErrorBarChange={jest.fn()} onScaleChange={jest.fn()} onSelectFieldChange={jest.fn()} + scaleValues={undefined} selectedType={BAR_CHART_TYPE} /> ); @@ -146,8 +131,10 @@ describe('ChartFieldOption', () => { field={{ name: 'x', label: 'X Axis', required: false } as ChartFieldInfo} fieldValues={{ x: { value: 'field1', data: { type: 'date' } } }} model={model} + onErrorBarChange={jest.fn()} onScaleChange={jest.fn()} onSelectFieldChange={jest.fn()} + scaleValues={undefined} selectedType={LINE_PLOT_TYPE} /> ); @@ -163,8 +150,10 @@ describe('ChartFieldOption', () => { field={{ name: 'x', label: 'X Axis', required: true } as ChartFieldInfo} fieldValues={{ x: { value: 'field1', data: { type: 'int' } } }} model={model} + onErrorBarChange={jest.fn()} onScaleChange={jest.fn()} onSelectFieldChange={jest.fn()} + scaleValues={undefined} selectedType={LINE_PLOT_TYPE} /> ); @@ -194,6 +183,7 @@ describe('ChartFieldOption', () => { field={{ name: 'x', label: 'X Axis', required: true } as ChartFieldInfo} fieldValues={{ x: { value: 'field1', data: { type: 'int' } } }} model={model} + onErrorBarChange={jest.fn()} onScaleChange={jest.fn()} onSelectFieldChange={jest.fn()} scaleValues={{ trans: 'log', type: 'manual', min: '3', max: '20' }} @@ -231,6 +221,7 @@ describe('ChartFieldOption', () => { field={{ name: 'x', label: 'X Axis', required: true } as ChartFieldInfo} fieldValues={{ x: { value: 'field1', data: { type: 'int' } } }} model={model} + onErrorBarChange={jest.fn()} onScaleChange={jest.fn()} onSelectFieldChange={jest.fn()} scaleValues={{ trans: 'log', type: 'manual', min: '1', max: '0' }} diff --git a/packages/components/src/internal/components/chart/ChartFieldOption.tsx b/packages/components/src/internal/components/chart/ChartFieldOption.tsx index cc716ee5a6..26e01eda9b 100644 --- a/packages/components/src/internal/components/chart/ChartFieldOption.tsx +++ b/packages/components/src/internal/components/chart/ChartFieldOption.tsx @@ -5,37 +5,12 @@ import { SelectInput, SelectInputOption } from '../forms/input/SelectInput'; import { QueryModel } from '../../../public/QueryModel/QueryModel'; import { LABKEY_VIS } from '../../constants'; -import { naturalSortByProperty } from '../../../public/sort'; import { ChartFieldRangeScaleOptions } from './ChartFieldRangeScaleOptions'; import { ChartFieldInfo, ChartTypeInfo, ScaleType } from './models'; -import { getFieldDataType, shouldShowAggregateOptions, shouldShowRangeScaleOptions } from './utils'; +import { getFieldDataType, getSelectOptions, shouldShowAggregateOptions, shouldShowRangeScaleOptions } from './utils'; import { ChartFieldAggregateOptions } from './ChartFieldAggregateOptions'; -export const getSelectOptions = ( - model: QueryModel, - chartType: ChartTypeInfo, - field: ChartFieldInfo -): SelectInputOption[] => { - const allowableTypes = LABKEY_VIS.GenericChartHelper.getAllowableTypes(field); - - return model.queryInfo - .getDisplayColumns(model.viewName) - .filter(col => { - const colType = getFieldDataType(col); - const hasMatchingType = allowableTypes.indexOf(colType) > -1; - const isMeasureDimensionMatch = LABKEY_VIS.GenericChartHelper.isMeasureDimensionMatch( - chartType.name, - field, - col.measure, - col.dimension - ); - return hasMatchingType || isMeasureDimensionMatch; - }) - .sort(naturalSortByProperty('caption')) - .map(col => ({ label: col.caption, value: col.fieldKey, data: col })); -}; - const DEFAULT_SCALE_VALUES = { type: 'automatic', trans: 'linear' }; interface OwnProps { diff --git a/packages/components/src/internal/components/chart/CurveFitStatsGrid.tsx b/packages/components/src/internal/components/chart/CurveFitStatsGrid.tsx index a8a6583519..39599b6e11 100644 --- a/packages/components/src/internal/components/chart/CurveFitStatsGrid.tsx +++ b/packages/components/src/internal/components/chart/CurveFitStatsGrid.tsx @@ -102,7 +102,7 @@ interface NonlinearCurveFitData { } interface NonlinearCurveFit extends CurveFit, NonlinearCurveFitData { - type: '3 Parameter' | '4 Parameter' | 'Five Parameter' | 'Four Parameter' | 'Three Parameter'; + type: '3 Parameter' | '4 Parameter' | '5 Parameter' | 'Five Parameter' | 'Four Parameter' | 'Three Parameter'; } interface CurveFitData { @@ -138,7 +138,14 @@ type PossibleTrendlines = | Trendline | Trendline; -const nonLinearTrendlineTypes = ['3 Parameter', '4 Parameter', 'Five Parameter', 'Four Parameter', 'Three Parameter']; +const nonLinearTrendlineTypes = [ + '3 Parameter', + '4 Parameter', + '5 Parameter', + 'Five Parameter', + 'Four Parameter', + 'Three Parameter', +]; function isNonlinearTrendline( trendline: PossibleTrendlines ): trendline is Trendline { diff --git a/packages/components/src/internal/components/chart/TrendlineOption.test.tsx b/packages/components/src/internal/components/chart/TrendlineOption.test.tsx index 05c8831808..f80075bd5a 100644 --- a/packages/components/src/internal/components/chart/TrendlineOption.test.tsx +++ b/packages/components/src/internal/components/chart/TrendlineOption.test.tsx @@ -7,9 +7,15 @@ import { LABKEY_VIS } from '../../constants'; import { SchemaQuery } from '../../../public/SchemaQuery'; import { TrendlineOption } from './TrendlineOption'; +import { makeTestQueryModel } from '../../../public/QueryModel/testUtils'; +import { QueryInfo } from '../../../public/QueryInfo'; +import { ViewInfo } from '../../ViewInfo'; +import { ChartTypeInfo } from './models'; LABKEY_VIS = { GenericChartHelper: { + getAllowableTypes: () => ['text'], + isMeasureDimensionMatch: () => true, TRENDLINE_OPTIONS: [ { value: 'option1', label: 'Option 1', schemaPrefix: undefined }, { value: 'option2', label: 'Option 2', schemaPrefix: null }, @@ -19,13 +25,43 @@ LABKEY_VIS = { }, }; +const LINE_PLOT_TYPE = { + name: 'line_plot', +} as ChartTypeInfo; + +const columns = [ + { fieldKey: 'intCol', jsonType: 'int' }, + { fieldKey: 'doubleCol', jsonType: 'double' }, + { fieldKey: 'textCol', jsonType: 'string' }, +]; + +const model = makeTestQueryModel( + new SchemaQuery('schema', 'query', 'view'), + QueryInfo.fromJsonForTests( + { + columns, + name: 'query', + schemaName: 'schema', + views: [ + { columns, name: ViewInfo.DEFAULT_NAME }, + { columns, name: 'view' }, + ], + }, + true + ), + [], + 0 +); + describe('TrendlineOption', () => { test('hidden without x-axis value selected', async () => { render( ); await waitFor(() => { @@ -36,6 +72,7 @@ describe('TrendlineOption', () => { expect(document.querySelectorAll('.field-option-icon')).toHaveLength(0); expect(document.querySelectorAll('input[name="trendlineAsymptoteMin"]')).toHaveLength(0); expect(document.querySelectorAll('input[name="trendlineAsymptoteMax"]')).toHaveLength(0); + expect(document.querySelectorAll('input[name="trendlineParameters"]')).toHaveLength(0); }); test('shown with x-axis value selected, non-date', async () => { @@ -43,12 +80,15 @@ describe('TrendlineOption', () => { x: { data: { jsonType: 'int' }, value: 'field1' }, trendlineAsymptoteMin: { value: undefined }, trendlineAsymptoteMax: { value: undefined }, + trendlineParameters: { value: undefined }, }; render( ); await waitFor(() => { @@ -73,12 +113,15 @@ describe('TrendlineOption', () => { x: { data: { jsonType: 'date' }, value: 'field1' }, trendlineAsymptoteMin: { value: undefined }, trendlineAsymptoteMax: { value: undefined }, + trendlineParameters: { value: undefined }, }; render( ); await waitFor(() => { @@ -94,12 +137,15 @@ describe('TrendlineOption', () => { x: { data: { type: 'time' }, value: 'field1' }, trendlineAsymptoteMin: { value: undefined }, trendlineAsymptoteMax: { value: undefined }, + trendlineParameters: { value: undefined }, }; render( ); await waitFor(() => { @@ -116,12 +162,15 @@ describe('TrendlineOption', () => { trendlineType: { value: 'option1', showMin: true, showMax: true }, trendlineAsymptoteMin: { value: '0.1' }, trendlineAsymptoteMax: { value: '1.0' }, + trendlineParameters: { value: undefined }, }; render( ); await waitFor(() => { @@ -154,12 +203,15 @@ describe('TrendlineOption', () => { trendlineType: { value: 'option1', showMin: true, showMax: false }, trendlineAsymptoteMin: { value: '0.1' }, trendlineAsymptoteMax: { value: undefined }, + trendlineParameters: { value: undefined }, }; render( ); await waitFor(() => { @@ -185,4 +237,34 @@ describe('TrendlineOption', () => { expect(document.querySelectorAll('input[name="trendlineAsymptoteMax"]')).toHaveLength(0); expect(document.querySelector('input[name="trendlineAsymptoteMin"]').getAttribute('value')).toBe(''); }); + + test('show provided parameters in trendline gear tooltip', async () => { + const fieldValues = { + x: { data: { jsonType: 'int' }, value: 'field1' }, + trendlineType: { value: 'option1', showMin: true, showMax: true }, + trendlineAsymptoteMin: { value: undefined }, + trendlineAsymptoteMax: { value: undefined }, + trendlineParameters: { value: 'field1' }, + }; + render( + + ); + await waitFor(() => { + expect(document.querySelectorAll('.trendline-option')).toHaveLength(1); + }); + + expect(document.querySelectorAll('.select-input')).toHaveLength(1); // trendline type + expect(document.querySelectorAll('.field-option-icon')).toHaveLength(1); + + await userEvent.click(document.querySelector('.fa-gear')); + expect(document.querySelectorAll('.select-input')).toHaveLength(2); // trendline type and now provided parameters + expect(document.querySelectorAll('input[name="trendlineParameters"]')).toHaveLength(1); + expect(document.querySelector('input[name="trendlineParameters"]').getAttribute('value')).toBe('field1'); + }); }); diff --git a/packages/components/src/internal/components/chart/TrendlineOption.tsx b/packages/components/src/internal/components/chart/TrendlineOption.tsx index 045c402a51..aa2f3c9cce 100644 --- a/packages/components/src/internal/components/chart/TrendlineOption.tsx +++ b/packages/components/src/internal/components/chart/TrendlineOption.tsx @@ -10,8 +10,9 @@ import { OverlayTrigger } from '../../OverlayTrigger'; import { Popover } from '../../Popover'; import { RadioGroupInput, RadioGroupOption } from '../forms/input/RadioGroupInput'; -import { TrendlineType } from './models'; -import { getFieldDataType } from './utils'; +import { ChartFieldInfo, ChartTypeInfo, TrendlineType } from './models'; +import { getFieldDataType, getSelectOptions } from './utils'; +import { QueryModel } from '../../../public/QueryModel/QueryModel'; const ASYMPTOTE_TYPES = [ { value: 'automatic', label: 'Automatic' }, @@ -20,14 +21,16 @@ const ASYMPTOTE_TYPES = [ interface TrendlineOptionProps { fieldValues: Record; + model: QueryModel; onFieldChange: (key: string, value: SelectInputOption) => void; schemaQuery: SchemaQuery; + selectedType: ChartTypeInfo; } export const TrendlineOption: FC = memo(props => { const TRENDLINE_OPTIONS: TrendlineType[] = Object.values(LABKEY_VIS.GenericChartHelper.TRENDLINE_OPTIONS); - const { fieldValues, onFieldChange, schemaQuery } = props; - const showFieldOptions = fieldValues.trendlineType?.showMin || fieldValues.trendlineType?.showMax; + const { fieldValues, onFieldChange, schemaQuery, model, selectedType } = props; + const showFieldOptions = fieldValues.trendlineType && fieldValues.trendlineType.value !== ''; // hide the trendline option if no x-axis value selected and for date field selection on x-axis const hidden = useMemo(() => { @@ -39,10 +42,7 @@ export const TrendlineOption: FC = memo(props => { const [asymptoteType, setAsymptoteType] = useState('automatic'); const [asymptoteMin, setAsymptoteMin] = useState(''); const [asymptoteMax, setAsymptoteMax] = useState(''); - const invalidRange = useMemo( - () => !!asymptoteMin && !!asymptoteMax && asymptoteMax <= asymptoteMin, - [asymptoteMin, asymptoteMax] - ); + useEffect(() => { if (loadingTrendlineOptions && (!!fieldValues.trendlineAsymptoteMin || !!fieldValues.trendlineAsymptoteMax)) { setAsymptoteType('manual'); @@ -60,12 +60,6 @@ export const TrendlineOption: FC = memo(props => { setAsymptoteMax(event.target.value); }, []); - const applyTrendlineAsymptote = useCallback(() => { - if (invalidRange) return; - onFieldChange('trendlineAsymptoteMin', { value: asymptoteMin }); - onFieldChange('trendlineAsymptoteMax', { value: asymptoteMax }); - }, [onFieldChange, asymptoteMin, asymptoteMax, invalidRange]); - const clearTrendlineAsymptote = useCallback(() => { setAsymptoteMin(''); onFieldChange('trendlineAsymptoteMin', undefined); @@ -98,16 +92,6 @@ export const TrendlineOption: FC = memo(props => { [clearTrendlineAsymptote] ); - const asymptoteTypeOptions = useMemo(() => { - return ASYMPTOTE_TYPES.map( - option => - ({ - ...option, - selected: asymptoteType === option.value, - }) as RadioGroupOption - ); - }, [asymptoteType]); - if (hidden) return null; return ( @@ -129,64 +113,36 @@ export const TrendlineOption: FC = memo(props => {
{showFieldOptions && (
-
- - -
- {asymptoteType === 'manual' && ( -
- {fieldValues.trendlineType?.showMin && ( - - )} - {fieldValues.trendlineType?.showMin && - fieldValues.trendlineType?.showMax && -} - {fieldValues.trendlineType?.showMax && ( - - )} - {invalidRange && ( -
Invalid range (Max <= Min)
- )} -
- )} + } + triggerType="click" >
@@ -197,3 +153,142 @@ export const TrendlineOption: FC = memo(props => { ); }); TrendlineOption.displayName = 'TrendlineOption'; + +interface TrendlineOptionPopoverProps { + asymptoteMax: string; + asymptoteMin: string; + asymptoteType: string; + fieldValues: Record; + model: QueryModel; + onAsymptoteTypeChange: (selected: string) => void; + onFieldChange: (key: string, value: SelectInputOption) => void; + onTrendlineAsymptoteMax: (event: ChangeEvent) => void; + onTrendlineAsymptoteMin: (event: ChangeEvent) => void; + selectedType: ChartTypeInfo; +} + +const TrendlineOptionPopover: FC = props => { + const { + fieldValues, + model, + selectedType, + asymptoteType, + onAsymptoteTypeChange, + onFieldChange, + asymptoteMin, + asymptoteMax, + onTrendlineAsymptoteMin, + onTrendlineAsymptoteMax, + } = props; + const showAsymptoteOptions = fieldValues.trendlineType?.showMin || fieldValues.trendlineType?.showMax; + const invalidRange = useMemo( + () => !!asymptoteMin && !!asymptoteMax && asymptoteMax <= asymptoteMin, + [asymptoteMin, asymptoteMax] + ); + + const options = useMemo(() => { + const field = { + name: 'parameters', + textOnly: true, + label: 'Provided Parameters', + required: false, + } as ChartFieldInfo; + return getSelectOptions(model, selectedType, field); + }, [model, selectedType]); + // Issue 52050: use fieldKey for special characters + const parameterInputValue = useMemo(() => { + if (fieldValues?.trendlineParameters) { + return fieldValues.trendlineParameters.data?.fieldKey ?? fieldValues.trendlineParameters.value; + } + return undefined; + }, [fieldValues]); + + const asymptoteTypeOptions = useMemo(() => { + return ASYMPTOTE_TYPES.map( + option => + ({ + ...option, + selected: asymptoteType === option.value, + }) as RadioGroupOption + ); + }, [asymptoteType]); + + const applyTrendlineAsymptote = useCallback(() => { + if (invalidRange) return; + onFieldChange('trendlineAsymptoteMin', { value: asymptoteMin }); + onFieldChange('trendlineAsymptoteMax', { value: asymptoteMax }); + }, [onFieldChange, asymptoteMin, asymptoteMax, invalidRange]); + + const onParameterFieldChange = useCallback( + (name: string, _: string, selectedOption: SelectInputOption) => { + onFieldChange(name, selectedOption); + }, + [onFieldChange] + ); + + return ( +
+ {showAsymptoteOptions && ( + <> +
+ + +
+ {asymptoteType === 'manual' && ( +
+ {fieldValues.trendlineType?.showMin && ( + + )} + {fieldValues.trendlineType?.showMin && fieldValues.trendlineType?.showMax && ( + - + )} + {fieldValues.trendlineType?.showMax && ( + + )} + {invalidRange &&
Invalid range (Max <= Min)
} +
+ )} + + )} +
+ + +
+
+ ); +}; diff --git a/packages/components/src/internal/components/chart/utils.test.ts b/packages/components/src/internal/components/chart/utils.test.ts index b424cf42da..364a65dfe5 100644 --- a/packages/components/src/internal/components/chart/utils.test.ts +++ b/packages/components/src/internal/components/chart/utils.test.ts @@ -4,10 +4,23 @@ import { createHorizontalBarCountLegendData, createHorizontalBarLegendData, getFieldDataType, + getSelectOptions, shouldShowAggregateOptions, shouldShowRangeScaleOptions, } from './utils'; import { ChartFieldInfo, ChartTypeInfo } from './models'; +import { LABKEY_VIS } from '../../constants'; +import { makeTestQueryModel } from '../../../public/QueryModel/testUtils'; +import { SchemaQuery } from '../../../public/SchemaQuery'; +import { QueryInfo } from '../../../public/QueryInfo'; +import { ViewInfo } from '../../ViewInfo'; + +LABKEY_VIS = { + GenericChartHelper: { + getAllowableTypes: () => ['int', 'double'], + isNumericType: (type: string) => type === 'int', + }, +}; describe('createHorizontalBarLegendData', () => { test('all different', () => { @@ -357,3 +370,49 @@ describe('shouldShowAggregateOptions', () => { expect(shouldShowAggregateOptions(yField, LINE_PLOT_TYPE)).toBe(true); }); }); + +describe('getSelectOptions', () => { + const columns = [ + { fieldKey: 'intCol', jsonType: 'int' }, + { fieldKey: 'doubleCol', jsonType: 'double' }, + { fieldKey: 'textCol', jsonType: 'string' }, + ]; + + const model = makeTestQueryModel( + new SchemaQuery('schema', 'query', 'view'), + QueryInfo.fromJsonForTests( + { + columns, + name: 'query', + schemaName: 'schema', + views: [ + { columns, name: ViewInfo.DEFAULT_NAME }, + { columns, name: 'view' }, + ], + }, + true + ), + [], + 0 + ); + + test('hasMatchingType', () => { + LABKEY_VIS.GenericChartHelper = { + ...LABKEY_VIS.GenericChartHelper, + isMeasureDimensionMatch: () => false, + }; + const field = { name: 'x' } as ChartFieldInfo; + const options = getSelectOptions(model, BAR_CHART_TYPE, field); + expect(options.length).toBe(2); + }); + + test('isMeasureDimensionMatch', () => { + LABKEY_VIS.GenericChartHelper = { + ...LABKEY_VIS.GenericChartHelper, + isMeasureDimensionMatch: () => true, + }; + const field = { name: 'x' } as ChartFieldInfo; + const options = getSelectOptions(model, BAR_CHART_TYPE, field); + expect(options.length).toBe(3); + }); +}); diff --git a/packages/components/src/internal/components/chart/utils.ts b/packages/components/src/internal/components/chart/utils.ts index 14baf1acf0..0da1bc3662 100644 --- a/packages/components/src/internal/components/chart/utils.ts +++ b/packages/components/src/internal/components/chart/utils.ts @@ -1,5 +1,9 @@ import { Map } from 'immutable'; import { ChartFieldInfo, ChartTypeInfo } from './models'; +import { QueryModel } from '../../../public/QueryModel/QueryModel'; +import { SelectInputOption } from '../forms/input/SelectInput'; +import { naturalSortByProperty } from '../../../public/sort'; +import { LABKEY_VIS } from '../../constants'; export interface HorizontalBarData { backgroundColor?: string; @@ -91,3 +95,27 @@ export const shouldShowAggregateOptions = (field: ChartFieldInfo, selectedType: const isLine = selectedType.name === 'line_plot'; return field.name === 'y' && (isBar || isLine); }; + +export const getSelectOptions = ( + model: QueryModel, + chartType: ChartTypeInfo, + field: ChartFieldInfo +): SelectInputOption[] => { + const allowableTypes = LABKEY_VIS.GenericChartHelper.getAllowableTypes(field); + + return model.queryInfo + .getDisplayColumns(model.viewName) + .filter(col => { + const colType = getFieldDataType(col); + const hasMatchingType = allowableTypes.indexOf(colType) > -1; + const isMeasureDimensionMatch = LABKEY_VIS.GenericChartHelper.isMeasureDimensionMatch( + chartType.name, + field, + col.measure, + col.dimension + ); + return hasMatchingType || isMeasureDimensionMatch; + }) + .sort(naturalSortByProperty('caption')) + .map(col => ({ label: col.caption, value: col.fieldKey, data: col })); +}; diff --git a/packages/components/src/public/QueryColumn.ts b/packages/components/src/public/QueryColumn.ts index e241ecb1b5..1370b73d47 100644 --- a/packages/components/src/public/QueryColumn.ts +++ b/packages/components/src/public/QueryColumn.ts @@ -128,6 +128,7 @@ export class QueryColumn implements IQueryColumn { declare description: string; declare dimension: boolean; declare displayAsLookup: boolean; + declare displayWidth: number | string; // declare excludeFromShifting: boolean; // declare ext: any; declare facetingBehaviorType: string;