mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Lens] Allow non-numeric metrics for metric vis (#169258)
## Summary Fixes #137756 Allows to set non numeric metrics for metric visualization for primary and secondary metric. <img width="369" alt="Screenshot 2023-10-19 at 13 45 47" src="d6f00cd1
-3be8-4c07-abe0-5bd15e2e9813"> <img width="350" alt="Screenshot 2023-10-19 at 13 46 37" src="01978149
-ca40-44c2-ba73-9698335e819a"> Doesn't include coloring by terms. When primary metric is non-numeric: 1. when maximum value is empty, we hide maximum value group 2. when maximum value has a value we set an error message on dimension 3. we don’t allow to use a palette for coloring 4. we don’t allow to set a trendline <img width="681" alt="Screenshot 2023-10-19 at 13 30 16" src="7464f9cc
-c09c-42cd-bd44-f55ffc1dfad9"> <img width="456" alt="Screenshot 2023-10-19 at 13 30 22" src="e5726ab9
-a748-4417-9b66-5bf4d708d833"> ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
beae7b1448
commit
c7e785383a
13 changed files with 667 additions and 477 deletions
|
@ -20,12 +20,14 @@ import {
|
|||
MetricWTrend,
|
||||
MetricWNumber,
|
||||
SettingsProps,
|
||||
MetricWText,
|
||||
} from '@elastic/charts';
|
||||
import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils';
|
||||
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
|
||||
import type {
|
||||
Datatable,
|
||||
DatatableColumn,
|
||||
DatatableRow,
|
||||
IInterpreterRenderHandlers,
|
||||
RenderMode,
|
||||
} from '@kbn/expressions-plugin/common';
|
||||
|
@ -65,6 +67,28 @@ function enhanceFieldFormat(serializedFieldFormat: SerializedFieldFormat | undef
|
|||
return serializedFieldFormat ?? { id: formatId };
|
||||
}
|
||||
|
||||
const renderSecondaryMetric = (
|
||||
columns: DatatableColumn[],
|
||||
row: DatatableRow,
|
||||
config: Pick<VisParams, 'metric' | 'dimensions'>
|
||||
) => {
|
||||
let secondaryMetricColumn: DatatableColumn | undefined;
|
||||
let formatSecondaryMetric: ReturnType<typeof getMetricFormatter>;
|
||||
if (config.dimensions.secondaryMetric) {
|
||||
secondaryMetricColumn = getColumnByAccessor(config.dimensions.secondaryMetric, columns);
|
||||
formatSecondaryMetric = getMetricFormatter(config.dimensions.secondaryMetric, columns);
|
||||
}
|
||||
const secondaryPrefix = config.metric.secondaryPrefix ?? secondaryMetricColumn?.name;
|
||||
return (
|
||||
<span>
|
||||
{secondaryPrefix}
|
||||
{secondaryMetricColumn
|
||||
? `${secondaryPrefix ? ' ' : ''}${formatSecondaryMetric!(row[secondaryMetricColumn.id])}`
|
||||
: undefined}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const getMetricFormatter = (
|
||||
accessor: ExpressionValueVisDimension | string,
|
||||
columns: Datatable['columns']
|
||||
|
@ -149,13 +173,6 @@ export const MetricVis = ({
|
|||
const primaryMetricColumn = getColumnByAccessor(config.dimensions.metric, data.columns)!;
|
||||
const formatPrimaryMetric = getMetricFormatter(config.dimensions.metric, data.columns);
|
||||
|
||||
let secondaryMetricColumn: DatatableColumn | undefined;
|
||||
let formatSecondaryMetric: ReturnType<typeof getMetricFormatter>;
|
||||
if (config.dimensions.secondaryMetric) {
|
||||
secondaryMetricColumn = getColumnByAccessor(config.dimensions.secondaryMetric, data.columns);
|
||||
formatSecondaryMetric = getMetricFormatter(config.dimensions.secondaryMetric, data.columns);
|
||||
}
|
||||
|
||||
let breakdownByColumn: DatatableColumn | undefined;
|
||||
let formatBreakdownValue: FieldFormatConvertFunction;
|
||||
if (config.dimensions.breakdownBy) {
|
||||
|
@ -172,28 +189,32 @@ export const MetricVis = ({
|
|||
const metricConfigs: MetricSpec['data'][number] = (
|
||||
breakdownByColumn ? data.rows : data.rows.slice(0, 1)
|
||||
).map((row, rowIdx) => {
|
||||
const value: number = row[primaryMetricColumn.id] !== null ? row[primaryMetricColumn.id] : NaN;
|
||||
const value: number | string =
|
||||
row[primaryMetricColumn.id] !== null ? row[primaryMetricColumn.id] : NaN;
|
||||
const title = breakdownByColumn
|
||||
? formatBreakdownValue(row[breakdownByColumn.id])
|
||||
: primaryMetricColumn.name;
|
||||
const subtitle = breakdownByColumn ? primaryMetricColumn.name : config.metric.subtitle;
|
||||
const secondaryPrefix = config.metric.secondaryPrefix ?? secondaryMetricColumn?.name;
|
||||
|
||||
if (typeof value !== 'number') {
|
||||
const nonNumericMetric: MetricWText = {
|
||||
value: formatPrimaryMetric(value),
|
||||
title: String(title),
|
||||
subtitle,
|
||||
icon: config.metric?.icon ? getIcon(config.metric?.icon) : undefined,
|
||||
extra: renderSecondaryMetric(data.columns, row, config),
|
||||
color: config.metric.color ?? defaultColor,
|
||||
};
|
||||
return nonNumericMetric;
|
||||
}
|
||||
|
||||
const baseMetric: MetricWNumber = {
|
||||
value,
|
||||
valueFormatter: formatPrimaryMetric,
|
||||
title: String(title),
|
||||
subtitle,
|
||||
icon: config.metric?.icon ? getIcon(config.metric?.icon) : undefined,
|
||||
extra: (
|
||||
<span>
|
||||
{secondaryPrefix}
|
||||
{secondaryMetricColumn
|
||||
? `${secondaryPrefix ? ' ' : ''}${formatSecondaryMetric!(
|
||||
row[secondaryMetricColumn.id]
|
||||
)}`
|
||||
: undefined}
|
||||
</span>
|
||||
),
|
||||
extra: renderSecondaryMetric(data.columns, row, config),
|
||||
color:
|
||||
config.metric.palette && value != null
|
||||
? getColor(
|
||||
|
|
|
@ -224,6 +224,33 @@ describe('LayerPanel', () => {
|
|||
const group = instance.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]');
|
||||
expect(group).toHaveLength(1);
|
||||
});
|
||||
it('does not render a hidden group', async () => {
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: [],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
},
|
||||
{
|
||||
groupLabel: 'B',
|
||||
groupId: 'b',
|
||||
accessors: [],
|
||||
filterOperations: () => true,
|
||||
isHidden: true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { instance } = await mountWithProvider(<LayerPanel {...getDefaultProps()} />);
|
||||
const group = instance.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]');
|
||||
expect(group).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the required warning when only one group is configured', async () => {
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
|
|
|
@ -427,239 +427,244 @@ export function LayerPanel(
|
|||
)}
|
||||
</header>
|
||||
|
||||
{dimensionGroups.map((group, groupIndex) => {
|
||||
let errorText: string = '';
|
||||
{dimensionGroups
|
||||
.filter((group) => !group.isHidden)
|
||||
.map((group, groupIndex) => {
|
||||
let errorText: string = '';
|
||||
|
||||
if (!isEmptyLayer) {
|
||||
if (
|
||||
group.requiredMinDimensionCount &&
|
||||
group.requiredMinDimensionCount > group.accessors.length
|
||||
) {
|
||||
if (group.requiredMinDimensionCount > 1) {
|
||||
if (!isEmptyLayer) {
|
||||
if (
|
||||
group.requiredMinDimensionCount &&
|
||||
group.requiredMinDimensionCount > group.accessors.length
|
||||
) {
|
||||
if (group.requiredMinDimensionCount > 1) {
|
||||
errorText = i18n.translate(
|
||||
'xpack.lens.editorFrame.requiresTwoOrMoreFieldsWarningLabel',
|
||||
{
|
||||
defaultMessage: 'Requires {requiredMinDimensionCount} fields',
|
||||
values: {
|
||||
requiredMinDimensionCount: group.requiredMinDimensionCount,
|
||||
},
|
||||
}
|
||||
);
|
||||
} else {
|
||||
errorText = i18n.translate('xpack.lens.editorFrame.requiresFieldWarningLabel', {
|
||||
defaultMessage: 'Requires field',
|
||||
});
|
||||
}
|
||||
} else if (group.dimensionsTooMany && group.dimensionsTooMany > 0) {
|
||||
errorText = i18n.translate(
|
||||
'xpack.lens.editorFrame.requiresTwoOrMoreFieldsWarningLabel',
|
||||
'xpack.lens.editorFrame.tooManyDimensionsSingularWarningLabel',
|
||||
{
|
||||
defaultMessage: 'Requires {requiredMinDimensionCount} fields',
|
||||
defaultMessage:
|
||||
'Please remove {dimensionsTooMany, plural, one {a dimension} other {{dimensionsTooMany} dimensions}}',
|
||||
values: {
|
||||
requiredMinDimensionCount: group.requiredMinDimensionCount,
|
||||
dimensionsTooMany: group.dimensionsTooMany,
|
||||
},
|
||||
}
|
||||
);
|
||||
} else {
|
||||
errorText = i18n.translate('xpack.lens.editorFrame.requiresFieldWarningLabel', {
|
||||
defaultMessage: 'Requires field',
|
||||
});
|
||||
}
|
||||
} else if (group.dimensionsTooMany && group.dimensionsTooMany > 0) {
|
||||
errorText = i18n.translate(
|
||||
'xpack.lens.editorFrame.tooManyDimensionsSingularWarningLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Please remove {dimensionsTooMany, plural, one {a dimension} other {{dimensionsTooMany} dimensions}}',
|
||||
values: {
|
||||
dimensionsTooMany: group.dimensionsTooMany,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
const isOptional = !group.requiredMinDimensionCount && !group.suggestedValue;
|
||||
return (
|
||||
<EuiFormRow
|
||||
className="lnsLayerPanel__row"
|
||||
fullWidth
|
||||
label={
|
||||
<>
|
||||
{group.groupLabel}
|
||||
{group.groupTooltip && (
|
||||
<>
|
||||
<EuiIconTip
|
||||
color="subdued"
|
||||
content={group.groupTooltip}
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
position="top"
|
||||
size="s"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
labelAppend={
|
||||
isOptional ? (
|
||||
<EuiText color="subdued" size="xs" data-test-subj="lnsGroup_optional">
|
||||
{i18n.translate('xpack.lens.editorFrame.optionalDimensionLabel', {
|
||||
defaultMessage: 'Optional',
|
||||
})}
|
||||
</EuiText>
|
||||
) : null
|
||||
}
|
||||
labelType="legend"
|
||||
key={group.groupId}
|
||||
isInvalid={Boolean(errorText)}
|
||||
error={errorText}
|
||||
>
|
||||
<>
|
||||
{group.accessors.length ? (
|
||||
<ReorderProvider className={'lnsLayerPanel__group'} dataTestSubj="lnsDragDrop">
|
||||
{group.accessors.map((accessorConfig, accessorIndex) => {
|
||||
const { columnId } = accessorConfig;
|
||||
|
||||
const messages =
|
||||
props?.getUserMessages?.('dimensionButton', {
|
||||
dimensionId: columnId,
|
||||
}) ?? [];
|
||||
|
||||
return (
|
||||
<DraggableDimensionButton
|
||||
activeVisualization={activeVisualization}
|
||||
registerNewButtonRef={registerNewButtonRef}
|
||||
order={[2, layerIndex, groupIndex, accessorIndex]}
|
||||
target={{
|
||||
id: columnId,
|
||||
layerId,
|
||||
columnId,
|
||||
groupId: group.groupId,
|
||||
filterOperations: group.filterOperations,
|
||||
prioritizedOperation: group.prioritizedOperation,
|
||||
isMetricDimension: group?.isMetricDimension,
|
||||
indexPatternId: layerDatasource
|
||||
? layerDatasource.getUsedDataView(layerDatasourceState, layerId)
|
||||
: activeVisualization.getUsedDataView?.(
|
||||
visualizationState,
|
||||
layerId
|
||||
),
|
||||
humanData: {
|
||||
label: columnLabelMap?.[columnId] ?? '',
|
||||
groupLabel: group.groupLabel,
|
||||
position: accessorIndex + 1,
|
||||
layerNumber: layerIndex + 1,
|
||||
},
|
||||
const isOptional = !group.requiredMinDimensionCount && !group.suggestedValue;
|
||||
return (
|
||||
<EuiFormRow
|
||||
className="lnsLayerPanel__row"
|
||||
fullWidth
|
||||
label={
|
||||
<>
|
||||
{group.groupLabel}
|
||||
{group.groupTooltip && (
|
||||
<>
|
||||
<EuiIconTip
|
||||
color="subdued"
|
||||
content={group.groupTooltip}
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
group={group}
|
||||
key={columnId}
|
||||
state={layerDatasourceState}
|
||||
layerDatasource={layerDatasource}
|
||||
datasourceLayers={framePublicAPI.datasourceLayers}
|
||||
onDragStart={() => setHideTooltip(true)}
|
||||
onDragEnd={() => setHideTooltip(false)}
|
||||
onDrop={onDrop}
|
||||
indexPatterns={dataViews.indexPatterns}
|
||||
>
|
||||
<DimensionButton
|
||||
accessorConfig={accessorConfig}
|
||||
label={columnLabelMap?.[accessorConfig.columnId] ?? ''}
|
||||
groupLabel={group.groupLabel}
|
||||
onClick={(id: string) => {
|
||||
setActiveDimension({
|
||||
isNew: false,
|
||||
activeGroup: group,
|
||||
activeId: id,
|
||||
});
|
||||
}}
|
||||
onRemoveClick={(id: string) => {
|
||||
props.onRemoveDimension({ columnId: id, layerId });
|
||||
removeButtonRef(id);
|
||||
}}
|
||||
message={{
|
||||
severity: messages[0]?.severity,
|
||||
content: messages[0]?.shortMessage || messages[0]?.longMessage,
|
||||
position="top"
|
||||
size="s"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
labelAppend={
|
||||
isOptional ? (
|
||||
<EuiText color="subdued" size="xs" data-test-subj="lnsGroup_optional">
|
||||
{i18n.translate('xpack.lens.editorFrame.optionalDimensionLabel', {
|
||||
defaultMessage: 'Optional',
|
||||
})}
|
||||
</EuiText>
|
||||
) : null
|
||||
}
|
||||
labelType="legend"
|
||||
key={group.groupId}
|
||||
isInvalid={Boolean(errorText)}
|
||||
error={errorText}
|
||||
>
|
||||
<>
|
||||
{group.accessors.length ? (
|
||||
<ReorderProvider
|
||||
className={'lnsLayerPanel__group'}
|
||||
dataTestSubj="lnsDragDrop"
|
||||
>
|
||||
{group.accessors.map((accessorConfig, accessorIndex) => {
|
||||
const { columnId } = accessorConfig;
|
||||
|
||||
const messages =
|
||||
props?.getUserMessages?.('dimensionButton', {
|
||||
dimensionId: columnId,
|
||||
}) ?? [];
|
||||
|
||||
return (
|
||||
<DraggableDimensionButton
|
||||
activeVisualization={activeVisualization}
|
||||
registerNewButtonRef={registerNewButtonRef}
|
||||
order={[2, layerIndex, groupIndex, accessorIndex]}
|
||||
target={{
|
||||
id: columnId,
|
||||
layerId,
|
||||
columnId,
|
||||
groupId: group.groupId,
|
||||
filterOperations: group.filterOperations,
|
||||
prioritizedOperation: group.prioritizedOperation,
|
||||
isMetricDimension: group?.isMetricDimension,
|
||||
indexPatternId: layerDatasource
|
||||
? layerDatasource.getUsedDataView(layerDatasourceState, layerId)
|
||||
: activeVisualization.getUsedDataView?.(
|
||||
visualizationState,
|
||||
layerId
|
||||
),
|
||||
humanData: {
|
||||
label: columnLabelMap?.[columnId] ?? '',
|
||||
groupLabel: group.groupLabel,
|
||||
position: accessorIndex + 1,
|
||||
layerNumber: layerIndex + 1,
|
||||
},
|
||||
}}
|
||||
group={group}
|
||||
key={columnId}
|
||||
state={layerDatasourceState}
|
||||
layerDatasource={layerDatasource}
|
||||
datasourceLayers={framePublicAPI.datasourceLayers}
|
||||
onDragStart={() => setHideTooltip(true)}
|
||||
onDragEnd={() => setHideTooltip(false)}
|
||||
onDrop={onDrop}
|
||||
indexPatterns={dataViews.indexPatterns}
|
||||
>
|
||||
{layerDatasource ? (
|
||||
<>
|
||||
{layerDatasource.DimensionTriggerComponent({
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessorConfig.columnId,
|
||||
groupId: group.groupId,
|
||||
filterOperations: group.filterOperations,
|
||||
indexPatterns: dataViews.indexPatterns,
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{activeVisualization?.DimensionTriggerComponent?.({
|
||||
columnId,
|
||||
label: columnLabelMap?.[columnId] ?? '',
|
||||
hideTooltip,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</DimensionButton>
|
||||
</DraggableDimensionButton>
|
||||
);
|
||||
})}
|
||||
</ReorderProvider>
|
||||
) : null}
|
||||
<DimensionButton
|
||||
accessorConfig={accessorConfig}
|
||||
label={columnLabelMap?.[accessorConfig.columnId] ?? ''}
|
||||
groupLabel={group.groupLabel}
|
||||
onClick={(id: string) => {
|
||||
setActiveDimension({
|
||||
isNew: false,
|
||||
activeGroup: group,
|
||||
activeId: id,
|
||||
});
|
||||
}}
|
||||
onRemoveClick={(id: string) => {
|
||||
props.onRemoveDimension({ columnId: id, layerId });
|
||||
removeButtonRef(id);
|
||||
}}
|
||||
message={{
|
||||
severity: messages[0]?.severity,
|
||||
content: messages[0]?.shortMessage || messages[0]?.longMessage,
|
||||
}}
|
||||
>
|
||||
{layerDatasource ? (
|
||||
<>
|
||||
{layerDatasource.DimensionTriggerComponent({
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessorConfig.columnId,
|
||||
groupId: group.groupId,
|
||||
filterOperations: group.filterOperations,
|
||||
indexPatterns: dataViews.indexPatterns,
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{activeVisualization?.DimensionTriggerComponent?.({
|
||||
columnId,
|
||||
label: columnLabelMap?.[columnId] ?? '',
|
||||
hideTooltip,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</DimensionButton>
|
||||
</DraggableDimensionButton>
|
||||
);
|
||||
})}
|
||||
</ReorderProvider>
|
||||
) : null}
|
||||
|
||||
{group.fakeFinalAccessor && (
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: ${euiThemeVars.euiBorderRadius};
|
||||
min-height: ${euiThemeVars.euiSizeXL};
|
||||
{group.fakeFinalAccessor && (
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: ${euiThemeVars.euiBorderRadius};
|
||||
min-height: ${euiThemeVars.euiSizeXL};
|
||||
|
||||
cursor: default !important;
|
||||
background-color: ${euiThemeVars.euiColorLightShade} !important;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 ${euiThemeVars.euiSizeS};
|
||||
`}
|
||||
>
|
||||
<DimensionTrigger
|
||||
label={group.fakeFinalAccessor.label}
|
||||
id="lns-fakeDimension"
|
||||
data-test-subj="lns-fakeDimension"
|
||||
cursor: default !important;
|
||||
background-color: ${euiThemeVars.euiColorLightShade} !important;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 ${euiThemeVars.euiSizeS};
|
||||
`}
|
||||
>
|
||||
<DimensionTrigger
|
||||
label={group.fakeFinalAccessor.label}
|
||||
id="lns-fakeDimension"
|
||||
data-test-subj="lns-fakeDimension"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{group.supportsMoreColumns ? (
|
||||
<EmptyDimensionButton
|
||||
activeVisualization={activeVisualization}
|
||||
order={[2, layerIndex, groupIndex, group.accessors.length]}
|
||||
group={group}
|
||||
target={{
|
||||
layerId,
|
||||
groupId: group.groupId,
|
||||
filterOperations: group.filterOperations,
|
||||
prioritizedOperation: group.prioritizedOperation,
|
||||
isNewColumn: true,
|
||||
isMetricDimension: group?.isMetricDimension,
|
||||
indexPatternId: layerDatasource
|
||||
? layerDatasource.getUsedDataView(layerDatasourceState, layerId)
|
||||
: activeVisualization.getUsedDataView?.(visualizationState, layerId),
|
||||
humanData: {
|
||||
groupLabel: group.groupLabel,
|
||||
layerNumber: layerIndex + 1,
|
||||
position: group.accessors.length + 1,
|
||||
label: i18n.translate('xpack.lens.indexPattern.emptyDimensionButton', {
|
||||
defaultMessage: 'Empty dimension',
|
||||
}),
|
||||
},
|
||||
}}
|
||||
layerDatasource={layerDatasource}
|
||||
state={layerDatasourceState}
|
||||
datasourceLayers={framePublicAPI.datasourceLayers}
|
||||
onClick={(id) => {
|
||||
props.onEmptyDimensionAdd(id, group);
|
||||
setActiveDimension({
|
||||
activeGroup: group,
|
||||
activeId: id,
|
||||
isNew: !group.supportStaticValue && Boolean(layerDatasource),
|
||||
});
|
||||
}}
|
||||
onDrop={onDrop}
|
||||
indexPatterns={dataViews.indexPatterns}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{group.supportsMoreColumns ? (
|
||||
<EmptyDimensionButton
|
||||
activeVisualization={activeVisualization}
|
||||
order={[2, layerIndex, groupIndex, group.accessors.length]}
|
||||
group={group}
|
||||
target={{
|
||||
layerId,
|
||||
groupId: group.groupId,
|
||||
filterOperations: group.filterOperations,
|
||||
prioritizedOperation: group.prioritizedOperation,
|
||||
isNewColumn: true,
|
||||
isMetricDimension: group?.isMetricDimension,
|
||||
indexPatternId: layerDatasource
|
||||
? layerDatasource.getUsedDataView(layerDatasourceState, layerId)
|
||||
: activeVisualization.getUsedDataView?.(visualizationState, layerId),
|
||||
humanData: {
|
||||
groupLabel: group.groupLabel,
|
||||
layerNumber: layerIndex + 1,
|
||||
position: group.accessors.length + 1,
|
||||
label: i18n.translate('xpack.lens.indexPattern.emptyDimensionButton', {
|
||||
defaultMessage: 'Empty dimension',
|
||||
}),
|
||||
},
|
||||
}}
|
||||
layerDatasource={layerDatasource}
|
||||
state={layerDatasourceState}
|
||||
datasourceLayers={framePublicAPI.datasourceLayers}
|
||||
onClick={(id) => {
|
||||
props.onEmptyDimensionAdd(id, group);
|
||||
setActiveDimension({
|
||||
activeGroup: group,
|
||||
activeId: id,
|
||||
isNew: !group.supportStaticValue && Boolean(layerDatasource),
|
||||
});
|
||||
}}
|
||||
onDrop={onDrop}
|
||||
indexPatterns={dataViews.indexPatterns}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
})}
|
||||
) : null}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
})}
|
||||
</EuiPanel>
|
||||
</section>
|
||||
{(layerDatasource?.LayerSettingsComponent || activeVisualization?.LayerSettingsComponent) && (
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { DragContextState, DragContextValue } from '@kbn/dom-drag-drop';
|
||||
import { DatatableColumnType } from '@kbn/expressions-plugin/common';
|
||||
import { createMockDataViewsState } from '../data_views_service/mocks';
|
||||
import { FramePublicAPI, FrameDatasourceAPI } from '../types';
|
||||
export { mockDataPlugin } from './data_plugin_mock';
|
||||
|
@ -83,3 +84,26 @@ export function createMockedDragDropContext(
|
|||
setState ? setState : jest.fn(),
|
||||
];
|
||||
}
|
||||
|
||||
export function generateActiveData(
|
||||
json: Array<{
|
||||
id: string;
|
||||
rows: Array<Record<string, number | null>>;
|
||||
}>
|
||||
) {
|
||||
return json.reduce((memo, { id, rows }) => {
|
||||
const columns = Object.keys(rows[0]).map((columnId) => ({
|
||||
id: columnId,
|
||||
name: columnId,
|
||||
meta: {
|
||||
type: typeof rows[0][columnId]! as DatatableColumnType,
|
||||
},
|
||||
}));
|
||||
memo[id] = {
|
||||
type: 'datatable' as const,
|
||||
columns,
|
||||
rows,
|
||||
};
|
||||
return memo;
|
||||
}, {} as NonNullable<FramePublicAPI['activeData']>);
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ export function CollapseSetting({
|
|||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
id="lns-indexPattern-collapse-by"
|
||||
label={
|
||||
<EuiToolTip
|
||||
delay="long"
|
||||
|
|
|
@ -821,6 +821,7 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & {
|
|||
paramEditorCustomProps?: ParamEditorCustomProps;
|
||||
enableFormatSelector?: boolean;
|
||||
labels?: { buttonAriaLabel: string; buttonLabel: string };
|
||||
isHidden?: boolean;
|
||||
};
|
||||
|
||||
export interface VisualizationDimensionChangeProps<T> {
|
||||
|
|
|
@ -55,6 +55,7 @@ Object {
|
|||
"groupId": "max",
|
||||
"groupLabel": "Maximum value",
|
||||
"groupTooltip": "If the maximum value is specified, the minimum value is fixed at zero.",
|
||||
"isHidden": false,
|
||||
"paramEditorCustomProps": Object {
|
||||
"headingLabel": "Value",
|
||||
},
|
||||
|
@ -100,6 +101,10 @@ Array [
|
|||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"isBucketed": false,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
|
@ -109,6 +114,10 @@ Array [
|
|||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"isBucketed": false,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
|
@ -118,6 +127,10 @@ Array [
|
|||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"isBucketed": false,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers';
|
|||
import { LayoutDirection } from '@elastic/charts';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EuiColorPickerOutput } from '@elastic/eui/src/components/color_picker/color_picker';
|
||||
import { createMockFramePublicAPI } from '../../mocks';
|
||||
import { createMockFramePublicAPI, generateActiveData } from '../../mocks';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
import { DebouncedInput } from '@kbn/visualization-ui-components';
|
||||
|
@ -47,6 +47,18 @@ const SELECTORS = {
|
|||
BREAKDOWN_EDITOR: '[data-test-subj="lnsMetricDimensionEditor_breakdown"]',
|
||||
};
|
||||
|
||||
const nonNumericMetricFrame = createMockFramePublicAPI({
|
||||
activeData: generateActiveData([
|
||||
{
|
||||
id: 'first',
|
||||
rows: Array(3).fill({
|
||||
'metric-col-id': 'nonNumericData',
|
||||
'max-col-id': 1000,
|
||||
}),
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
// see https://github.com/facebook/jest/issues/4402#issuecomment-534516219
|
||||
const expectCalledBefore = (mock1: jest.Mock, mock2: jest.Mock) =>
|
||||
expect(mock1.mock.invocationCallOrder[0]).toBeLessThan(mock2.mock.invocationCallOrder[0]);
|
||||
|
@ -102,7 +114,14 @@ describe('dimension editor', () => {
|
|||
} as unknown as DatasourcePublicAPI,
|
||||
removeLayer: jest.fn(),
|
||||
addLayer: jest.fn(),
|
||||
frame: createMockFramePublicAPI(),
|
||||
frame: createMockFramePublicAPI({
|
||||
activeData: generateActiveData([
|
||||
{
|
||||
id: 'first',
|
||||
rows: Array(3).fill({ 'metric-col-id': 100, 'secondary-metric-col-id': 1 }),
|
||||
},
|
||||
]),
|
||||
}),
|
||||
setState: jest.fn(),
|
||||
panelRef: {} as React.MutableRefObject<HTMLDivElement | null>,
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
|
@ -112,27 +131,9 @@ describe('dimension editor', () => {
|
|||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('primary metric dimension', () => {
|
||||
const accessor = 'primary-metric-col-id';
|
||||
const accessor = 'metric-col-id';
|
||||
const metricAccessorState = { ...fullState, metricAccessor: accessor };
|
||||
|
||||
beforeEach(() => {
|
||||
props.frame.activeData = {
|
||||
first: {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{
|
||||
id: accessor,
|
||||
name: 'foo',
|
||||
meta: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
],
|
||||
rows: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
class Harness {
|
||||
public _wrapper;
|
||||
|
||||
|
@ -146,6 +147,10 @@ describe('dimension editor', () => {
|
|||
return this._wrapper.find(DimensionEditor);
|
||||
}
|
||||
|
||||
public get colorModeSwitch() {
|
||||
return this._wrapper.find('EuiButtonGroup[data-test-subj="lnsMetric_color_mode_buttons"]');
|
||||
}
|
||||
|
||||
public get colorPicker() {
|
||||
return this._wrapper.find(EuiColorPicker);
|
||||
}
|
||||
|
@ -163,11 +168,16 @@ describe('dimension editor', () => {
|
|||
|
||||
const mockSetState = jest.fn();
|
||||
|
||||
const getHarnessWithState = (state: MetricVisualizationState, datasource = props.datasource) =>
|
||||
const getHarnessWithState = (
|
||||
state: MetricVisualizationState,
|
||||
datasource = props.datasource,
|
||||
propsOverrides: Partial<VisualizationDimensionEditorProps<MetricVisualizationState>> = {}
|
||||
) =>
|
||||
new Harness(
|
||||
mountWithIntl(
|
||||
<DimensionEditor
|
||||
{...props}
|
||||
{...propsOverrides}
|
||||
datasource={datasource}
|
||||
state={state}
|
||||
setState={mockSetState}
|
||||
|
@ -191,6 +201,15 @@ describe('dimension editor', () => {
|
|||
expect(component.exists(SELECTORS.BREAKDOWN_EDITOR)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('Color mode switch is not shown when the primary metric is non-numeric', () => {
|
||||
expect(getHarnessWithState(fullState, undefined).colorModeSwitch.exists()).toBeTruthy();
|
||||
expect(
|
||||
getHarnessWithState(fullState, undefined, {
|
||||
frame: nonNumericMetricFrame,
|
||||
}).colorModeSwitch.exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('static color controls', () => {
|
||||
it('is hidden when dynamic coloring is enabled', () => {
|
||||
const harnessWithPalette = getHarnessWithState({ ...metricAccessorState, palette });
|
||||
|
@ -202,6 +221,13 @@ describe('dimension editor', () => {
|
|||
});
|
||||
expect(harnessNoPalette.colorPicker.exists()).toBeTruthy();
|
||||
});
|
||||
it('is visible when metric is non-numeric even if palette is set', () => {
|
||||
expect(
|
||||
getHarnessWithState(fullState, undefined, {
|
||||
frame: nonNumericMetricFrame,
|
||||
}).colorPicker.exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('fills with default value', () => {
|
||||
const localHarness = getHarnessWithState({
|
||||
|
@ -240,24 +266,6 @@ describe('dimension editor', () => {
|
|||
describe('secondary metric dimension', () => {
|
||||
const accessor = 'secondary-metric-col-id';
|
||||
|
||||
beforeEach(() => {
|
||||
props.frame.activeData = {
|
||||
first: {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{
|
||||
id: accessor,
|
||||
name: 'foo',
|
||||
meta: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
],
|
||||
rows: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('renders when the accessor matches', () => {
|
||||
const component = shallow(
|
||||
<DimensionEditor
|
||||
|
@ -331,7 +339,7 @@ describe('dimension editor', () => {
|
|||
const setState = jest.fn();
|
||||
const localState = {
|
||||
...fullState,
|
||||
secondaryPrefix: 'foo',
|
||||
secondaryPrefix: 'secondary-metric-col-id2',
|
||||
secondaryMetricAccessor: accessor,
|
||||
};
|
||||
const component = mount(
|
||||
|
@ -341,7 +349,7 @@ describe('dimension editor', () => {
|
|||
const buttonGroup = component.find(EuiButtonGroup);
|
||||
|
||||
// make sure that if the user was to select the "custom" option, they would get the default value
|
||||
expect(buttonGroup.props().options[1].value).toBe('foo');
|
||||
expect(buttonGroup.props().options[1].value).toBe('secondary-metric-col-id');
|
||||
|
||||
const newVal = 'bar';
|
||||
|
||||
|
@ -461,7 +469,7 @@ describe('dimension editor', () => {
|
|||
});
|
||||
|
||||
describe('additional section', () => {
|
||||
const accessor = 'primary-metric-col-id';
|
||||
const accessor = 'metric-col-id';
|
||||
const metricAccessorState = { ...fullState, metricAccessor: accessor };
|
||||
|
||||
class Harness {
|
||||
|
@ -473,6 +481,10 @@ describe('dimension editor', () => {
|
|||
this._wrapper = wrapper;
|
||||
}
|
||||
|
||||
public get wrapper() {
|
||||
return this._wrapper;
|
||||
}
|
||||
|
||||
private get rootComponent() {
|
||||
return this._wrapper.find(DimensionEditorAdditionalSection);
|
||||
}
|
||||
|
@ -520,11 +532,16 @@ describe('dimension editor', () => {
|
|||
|
||||
const mockSetState = jest.fn();
|
||||
|
||||
const getHarnessWithState = (state: MetricVisualizationState, datasource = props.datasource) =>
|
||||
const getHarnessWithState = (
|
||||
state: MetricVisualizationState,
|
||||
datasource = props.datasource,
|
||||
propsOverrides: Partial<VisualizationDimensionEditorProps<MetricVisualizationState>> = {}
|
||||
) =>
|
||||
new Harness(
|
||||
mountWithIntl(
|
||||
<DimensionEditorAdditionalSection
|
||||
{...props}
|
||||
{...propsOverrides}
|
||||
datasource={datasource}
|
||||
state={state}
|
||||
setState={mockSetState}
|
||||
|
@ -623,6 +640,13 @@ describe('dimension editor', () => {
|
|||
} as DatasourcePublicAPI).isDisabled('trendline')
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('should not show a trendline button group when primary metric dimension is non-numeric', () => {
|
||||
expect(
|
||||
getHarnessWithState(fullState, undefined, {
|
||||
frame: nonNumericMetricFrame,
|
||||
}).wrapper.isEmptyRender()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('responding to buttons', () => {
|
||||
it('enables trendline', () => {
|
||||
|
|
|
@ -143,78 +143,77 @@ function SecondaryMetricEditor({ accessor, idPrefix, frame, layerId, setState, s
|
|||
const defaultPrefix = columnName || '';
|
||||
|
||||
return (
|
||||
<div data-test-subj="lnsMetricDimensionEditor_secondary_metric">
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.metric.prefixText.label', {
|
||||
defaultMessage: 'Prefix',
|
||||
})}
|
||||
>
|
||||
<>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
buttonSize="compressed"
|
||||
legend={i18n.translate('xpack.lens.metric.prefix.label', {
|
||||
defaultMessage: 'Prefix',
|
||||
})}
|
||||
data-test-subj="lnsMetric_prefix_buttons"
|
||||
options={[
|
||||
{
|
||||
id: `${idPrefix}auto`,
|
||||
label: i18n.translate('xpack.lens.metric.prefix.auto', {
|
||||
defaultMessage: 'Auto',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_prefix_auto',
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
id: `${idPrefix}custom`,
|
||||
label: i18n.translate('xpack.lens.metric.prefix.custom', {
|
||||
defaultMessage: 'Custom',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_prefix_custom',
|
||||
value: defaultPrefix,
|
||||
},
|
||||
{
|
||||
id: `${idPrefix}none`,
|
||||
label: i18n.translate('xpack.lens.metric.prefix.none', {
|
||||
defaultMessage: 'None',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_prefix_none',
|
||||
value: '',
|
||||
},
|
||||
]}
|
||||
idSelected={`${idPrefix}${
|
||||
state.secondaryPrefix === undefined
|
||||
? 'auto'
|
||||
: state.secondaryPrefix === ''
|
||||
? 'none'
|
||||
: 'custom'
|
||||
}`}
|
||||
onChange={(_id, secondaryPrefix) => {
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.metric.prefixText.label', {
|
||||
defaultMessage: 'Prefix',
|
||||
})}
|
||||
>
|
||||
<>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
buttonSize="compressed"
|
||||
legend={i18n.translate('xpack.lens.metric.prefix.label', {
|
||||
defaultMessage: 'Prefix',
|
||||
})}
|
||||
data-test-subj="lnsMetric_prefix_buttons"
|
||||
options={[
|
||||
{
|
||||
id: `${idPrefix}auto`,
|
||||
label: i18n.translate('xpack.lens.metric.prefix.auto', {
|
||||
defaultMessage: 'Auto',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_prefix_auto',
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
id: `${idPrefix}custom`,
|
||||
label: i18n.translate('xpack.lens.metric.prefix.custom', {
|
||||
defaultMessage: 'Custom',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_prefix_custom',
|
||||
value: defaultPrefix,
|
||||
},
|
||||
{
|
||||
id: `${idPrefix}none`,
|
||||
label: i18n.translate('xpack.lens.metric.prefix.none', {
|
||||
defaultMessage: 'None',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_prefix_none',
|
||||
value: '',
|
||||
},
|
||||
]}
|
||||
idSelected={`${idPrefix}${
|
||||
state.secondaryPrefix === undefined
|
||||
? 'auto'
|
||||
: state.secondaryPrefix === ''
|
||||
? 'none'
|
||||
: 'custom'
|
||||
}`}
|
||||
onChange={(_id, secondaryPrefix) => {
|
||||
setState({
|
||||
...state,
|
||||
secondaryPrefix,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
{state.secondaryPrefix && (
|
||||
<DebouncedInput
|
||||
data-test-subj="lnsMetric_prefix_custom_input"
|
||||
compressed
|
||||
value={state.secondaryPrefix}
|
||||
onChange={(newPrefix) => {
|
||||
setState({
|
||||
...state,
|
||||
secondaryPrefix,
|
||||
secondaryPrefix: newPrefix,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
{state.secondaryPrefix && (
|
||||
<DebouncedInput
|
||||
compressed
|
||||
value={state.secondaryPrefix}
|
||||
onChange={(newPrefix) => {
|
||||
setState({
|
||||
...state,
|
||||
secondaryPrefix: newPrefix,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -225,11 +224,13 @@ function PrimaryMetricEditor(props: SubProps) {
|
|||
|
||||
const currentData = frame.activeData?.[state.layerId];
|
||||
|
||||
if (accessor == null || !isNumericFieldForDatatable(currentData, accessor)) {
|
||||
const isMetricNumeric = isNumericFieldForDatatable(currentData, accessor);
|
||||
|
||||
if (accessor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasDynamicColoring = Boolean(state?.palette);
|
||||
const hasDynamicColoring = Boolean(isMetricNumeric && state?.palette);
|
||||
|
||||
const supportsPercentPalette = Boolean(
|
||||
state.maxAccessor ||
|
||||
|
@ -265,62 +266,65 @@ function PrimaryMetricEditor(props: SubProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.metric.dynamicColoring.label', {
|
||||
defaultMessage: 'Color mode',
|
||||
})}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
buttonSize="compressed"
|
||||
legend={i18n.translate('xpack.lens.metric.colorMode.label', {
|
||||
{isMetricNumeric && (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.metric.dynamicColoring.label', {
|
||||
defaultMessage: 'Color mode',
|
||||
})}
|
||||
data-test-subj="lnsMetric_color_mode_buttons"
|
||||
options={[
|
||||
{
|
||||
id: `${idPrefix}static`,
|
||||
label: i18n.translate('xpack.lens.metric.colorMode.static', {
|
||||
defaultMessage: 'Static',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_color_mode_static',
|
||||
},
|
||||
{
|
||||
id: `${idPrefix}dynamic`,
|
||||
label: i18n.translate('xpack.lens.metric.colorMode.dynamic', {
|
||||
defaultMessage: 'Dynamic',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_color_mode_dynamic',
|
||||
},
|
||||
]}
|
||||
idSelected={`${idPrefix}${state.palette ? 'dynamic' : 'static'}`}
|
||||
onChange={(id) => {
|
||||
const colorMode = id.replace(idPrefix, '') as 'static' | 'dynamic';
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
buttonSize="compressed"
|
||||
legend={i18n.translate('xpack.lens.metric.colorMode.label', {
|
||||
defaultMessage: 'Color mode',
|
||||
})}
|
||||
data-test-subj="lnsMetric_color_mode_buttons"
|
||||
options={[
|
||||
{
|
||||
id: `${idPrefix}static`,
|
||||
label: i18n.translate('xpack.lens.metric.colorMode.static', {
|
||||
defaultMessage: 'Static',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_color_mode_static',
|
||||
},
|
||||
{
|
||||
id: `${idPrefix}dynamic`,
|
||||
label: i18n.translate('xpack.lens.metric.colorMode.dynamic', {
|
||||
defaultMessage: 'Dynamic',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_color_mode_dynamic',
|
||||
},
|
||||
]}
|
||||
idSelected={`${idPrefix}${state.palette ? 'dynamic' : 'static'}`}
|
||||
onChange={(id) => {
|
||||
const colorMode = id.replace(idPrefix, '') as 'static' | 'dynamic';
|
||||
|
||||
const params =
|
||||
colorMode === 'dynamic'
|
||||
? {
|
||||
palette: {
|
||||
...activePalette,
|
||||
params: {
|
||||
...activePalette.params,
|
||||
stops: displayStops,
|
||||
const params =
|
||||
colorMode === 'dynamic'
|
||||
? {
|
||||
palette: {
|
||||
...activePalette,
|
||||
params: {
|
||||
...activePalette.params,
|
||||
stops: displayStops,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
palette: undefined,
|
||||
};
|
||||
setState({
|
||||
...state,
|
||||
color: undefined,
|
||||
...params,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
color: undefined,
|
||||
}
|
||||
: {
|
||||
palette: undefined,
|
||||
color: undefined,
|
||||
};
|
||||
setState({
|
||||
...state,
|
||||
...params,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{!hasDynamicColoring && <StaticColorControls {...props} />}
|
||||
{hasDynamicColoring && (
|
||||
<EuiFormRow
|
||||
|
@ -404,10 +408,18 @@ function PrimaryMetricEditor(props: SubProps) {
|
|||
);
|
||||
}
|
||||
|
||||
function StaticColorControls({ state, setState }: Pick<Props, 'state' | 'setState'>) {
|
||||
function StaticColorControls({
|
||||
state,
|
||||
setState,
|
||||
frame,
|
||||
}: Pick<Props, 'state' | 'setState' | 'frame'>) {
|
||||
const colorLabel = i18n.translate('xpack.lens.metric.color', {
|
||||
defaultMessage: 'Color',
|
||||
});
|
||||
const currentData = frame.activeData?.[state.layerId];
|
||||
const isMetricNumeric = Boolean(
|
||||
state.metricAccessor && isNumericFieldForDatatable(currentData, state.metricAccessor)
|
||||
);
|
||||
|
||||
const setColor = useCallback(
|
||||
(color: string) => {
|
||||
|
@ -420,7 +432,7 @@ function StaticColorControls({ state, setState }: Pick<Props, 'state' | 'setStat
|
|||
useDebouncedValue<string>(
|
||||
{
|
||||
onChange: setColor,
|
||||
value: state.color || getDefaultColor(state),
|
||||
value: state.color || getDefaultColor(state, isMetricNumeric),
|
||||
},
|
||||
{ allowFalsyValue: true }
|
||||
);
|
||||
|
@ -448,10 +460,12 @@ export function DimensionEditorAdditionalSection({
|
|||
addLayer,
|
||||
removeLayer,
|
||||
accessor,
|
||||
frame,
|
||||
}: VisualizationDimensionEditorProps<MetricVisualizationState>) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
if (accessor !== state.metricAccessor) {
|
||||
const currentData = frame.activeData?.[state.layerId];
|
||||
if (accessor !== state.metricAccessor || !isNumericFieldForDatatable(currentData, accessor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -566,7 +580,6 @@ export function DimensionEditorAdditionalSection({
|
|||
}`}
|
||||
onChange={(id) => {
|
||||
const supportingVisualizationType = id.split('--')[1] as SupportingVisType;
|
||||
|
||||
switch (supportingVisualizationType) {
|
||||
case 'trendline':
|
||||
setState({
|
||||
|
|
|
@ -90,6 +90,10 @@ export const toExpression = (
|
|||
const datasource = datasourceLayers[state.layerId];
|
||||
const datasourceExpression = datasourceExpressionsByLayers[state.layerId];
|
||||
|
||||
const isMetricNumeric = Boolean(
|
||||
state.metricAccessor &&
|
||||
datasource?.getOperationForColumnId(state.metricAccessor)?.dataType === 'number'
|
||||
);
|
||||
const maxPossibleTiles =
|
||||
// if there's a collapse function, no need to calculate since we're dealing with a single tile
|
||||
state.breakdownByAccessor && !state.collapseFn
|
||||
|
@ -142,15 +146,16 @@ export const toExpression = (
|
|||
trendline: trendlineExpression ? [trendlineExpression] : [],
|
||||
subtitle: state.subtitle ?? undefined,
|
||||
progressDirection: state.progressDirection as LayoutDirection,
|
||||
color: state.color || getDefaultColor(state),
|
||||
color: state.color || getDefaultColor(state, isMetricNumeric),
|
||||
icon: state.icon,
|
||||
palette: state.palette?.params
|
||||
? [
|
||||
paletteService
|
||||
.get(CUSTOM_PALETTE)
|
||||
.toExpression(computePaletteParams(state.palette.params as CustomPaletteParams)),
|
||||
]
|
||||
: [],
|
||||
palette:
|
||||
isMetricNumeric && state.palette?.params
|
||||
? [
|
||||
paletteService
|
||||
.get(CUSTOM_PALETTE)
|
||||
.toExpression(computePaletteParams(state.palette.params as CustomPaletteParams)),
|
||||
]
|
||||
: [],
|
||||
maxCols: state.maxCols ?? DEFAULT_MAX_COLUMNS,
|
||||
minTiles: maxPossibleTiles ?? undefined,
|
||||
inspectorTableId: state.layerId,
|
||||
|
|
|
@ -10,7 +10,7 @@ import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring';
|
|||
import { ExpressionAstExpression, ExpressionAstFunction } from '@kbn/expressions-plugin/common';
|
||||
import { euiLightVars, euiThemeVars } from '@kbn/ui-theme';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../../mocks';
|
||||
import { createMockDatasource, createMockFramePublicAPI, generateActiveData } from '../../mocks';
|
||||
import {
|
||||
DatasourceLayers,
|
||||
DatasourcePublicAPI,
|
||||
|
@ -82,7 +82,14 @@ describe('metric visualization', () => {
|
|||
...trendlineProps,
|
||||
};
|
||||
|
||||
const mockFrameApi = createMockFramePublicAPI();
|
||||
const mockFrameApi = createMockFramePublicAPI({
|
||||
activeData: generateActiveData([
|
||||
{
|
||||
id: 'first',
|
||||
rows: Array(3).fill({ 'metric-col-id': 20, 'max-metric-col-id': 100 }),
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
test('returns a default state', () => {
|
||||
|
@ -268,6 +275,7 @@ describe('metric visualization', () => {
|
|||
mockDatasource.publicAPIMock.getMaxPossibleNumValues.mockReturnValue(maxPossibleNumValues);
|
||||
mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
|
||||
isStaticValue: false,
|
||||
dataType: 'number',
|
||||
} as OperationDescriptor);
|
||||
|
||||
datasourceLayers = {
|
||||
|
@ -616,7 +624,7 @@ describe('metric visualization', () => {
|
|||
it('always applies max function to static max dimensions', () => {
|
||||
(
|
||||
datasourceLayers.first as jest.Mocked<DatasourcePublicAPI>
|
||||
).getOperationForColumnId.mockReturnValueOnce({
|
||||
).getOperationForColumnId.mockReturnValue({
|
||||
isStaticValue: true,
|
||||
} as OperationDescriptor);
|
||||
|
||||
|
@ -648,6 +656,9 @@ describe('metric visualization', () => {
|
|||
"type": "function",
|
||||
}
|
||||
`);
|
||||
(
|
||||
datasourceLayers.first as jest.Mocked<DatasourcePublicAPI>
|
||||
).getOperationForColumnId.mockClear();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1109,4 +1120,28 @@ describe('metric visualization', () => {
|
|||
noPadding: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserMessages', () => {
|
||||
it('returns error for non numeric primary metric if maxAccessor exists', () => {
|
||||
const frame = createMockFramePublicAPI({
|
||||
activeData: generateActiveData([
|
||||
{
|
||||
id: 'first',
|
||||
rows: Array(3).fill({ 'metric-col-id': '100', 'max-metric-col-id': 100 }),
|
||||
},
|
||||
]),
|
||||
});
|
||||
expect(visualization.getUserMessages!(fullState, { frame })).toHaveLength(1);
|
||||
|
||||
const frameNoErrors = createMockFramePublicAPI({
|
||||
activeData: generateActiveData([
|
||||
{
|
||||
id: 'first',
|
||||
rows: Array(3).fill({ 'metric-col-id': 30, 'max-metric-col-id': 100 }),
|
||||
},
|
||||
]),
|
||||
});
|
||||
expect(visualization.getUserMessages!(fullState, { frame: frameNoErrors })).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import { LayoutDirection } from '@elastic/charts';
|
|||
import { euiLightVars, euiThemeVars } from '@kbn/ui-theme';
|
||||
import { IconChartMetric } from '@kbn/chart-icons';
|
||||
import { AccessorConfig } from '@kbn/visualization-ui-components';
|
||||
import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils';
|
||||
import { CollapseFunction } from '../../../common/expressions';
|
||||
import type { LayerType } from '../../../common/types';
|
||||
import { layerTypes } from '../../../common/layer_types';
|
||||
|
@ -25,6 +26,7 @@ import {
|
|||
VisualizationConfigProps,
|
||||
VisualizationDimensionGroupConfig,
|
||||
Suggestion,
|
||||
UserMessage,
|
||||
} from '../../types';
|
||||
import { GROUP_ID, LENS_METRIC_ID } from './constants';
|
||||
import { DimensionEditor, DimensionEditorAdditionalSection } from './dimension_editor';
|
||||
|
@ -40,8 +42,11 @@ export const showingBar = (
|
|||
): state is MetricVisualizationState & { showBar: true; maxAccessor: string } =>
|
||||
Boolean(state.showBar && state.maxAccessor);
|
||||
|
||||
export const getDefaultColor = (state: MetricVisualizationState) =>
|
||||
showingBar(state) ? euiLightVars.euiColorPrimary : euiThemeVars.euiColorLightestShade;
|
||||
export const getDefaultColor = (state: MetricVisualizationState, isMetricNumeric?: boolean) => {
|
||||
return showingBar(state) && isMetricNumeric
|
||||
? euiLightVars.euiColorPrimary
|
||||
: euiThemeVars.euiColorLightestShade;
|
||||
};
|
||||
|
||||
export interface MetricVisualizationState {
|
||||
layerId: string;
|
||||
|
@ -70,7 +75,13 @@ export interface MetricVisualizationState {
|
|||
trendlineBreakdownByAccessor?: string;
|
||||
}
|
||||
|
||||
export const supportedDataTypes = new Set(['number']);
|
||||
export const supportedDataTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']);
|
||||
|
||||
const isSupportedMetric = (op: OperationMetadata) =>
|
||||
!op.isBucketed && supportedDataTypes.has(op.dataType);
|
||||
|
||||
const isSupportedDynamicMetric = (op: OperationMetadata) =>
|
||||
!op.isBucketed && supportedDataTypes.has(op.dataType) && !op.isStaticValue;
|
||||
|
||||
export const metricLabel = i18n.translate('xpack.lens.metric.label', {
|
||||
defaultMessage: 'Metric',
|
||||
|
@ -84,29 +95,25 @@ const getMetricLayerConfiguration = (
|
|||
): {
|
||||
groups: VisualizationDimensionGroupConfig[];
|
||||
} => {
|
||||
const isSupportedMetric = (op: OperationMetadata) =>
|
||||
!op.isBucketed && supportedDataTypes.has(op.dataType);
|
||||
const currentData = props.frame.activeData?.[props.state.layerId];
|
||||
|
||||
const isSupportedDynamicMetric = (op: OperationMetadata) =>
|
||||
!op.isBucketed && supportedDataTypes.has(op.dataType) && !op.isStaticValue;
|
||||
const isMetricNumeric = Boolean(
|
||||
props.state.metricAccessor &&
|
||||
isNumericFieldForDatatable(currentData, props.state.metricAccessor)
|
||||
);
|
||||
|
||||
const getPrimaryAccessorDisplayConfig = (): Partial<AccessorConfig> => {
|
||||
const hasDynamicColoring = Boolean(isMetricNumeric && props.state.palette);
|
||||
const stops = props.state.palette?.params?.stops || [];
|
||||
const hasStaticColoring = !!props.state.color;
|
||||
const hasDynamicColoring = !!props.state.palette;
|
||||
|
||||
return hasDynamicColoring
|
||||
? {
|
||||
triggerIconType: 'colorBy',
|
||||
palette: stops.map(({ color }) => color),
|
||||
}
|
||||
: hasStaticColoring
|
||||
? {
|
||||
triggerIconType: 'color',
|
||||
color: props.state.color,
|
||||
}
|
||||
: {
|
||||
triggerIconType: 'color',
|
||||
color: getDefaultColor(props.state),
|
||||
color: props.state.color ?? getDefaultColor(props.state, isMetricNumeric),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -180,6 +187,7 @@ const getMetricLayerConfiguration = (
|
|||
},
|
||||
]
|
||||
: [],
|
||||
isHidden: !props.state.maxAccessor && !isMetricNumeric,
|
||||
supportsMoreColumns: !props.state.maxAccessor,
|
||||
filterOperations: isSupportedMetric,
|
||||
enableDimensionEditor: true,
|
||||
|
@ -632,7 +640,7 @@ export const getMetricVisualization = ({
|
|||
return suggestion;
|
||||
},
|
||||
|
||||
getVisualizationInfo(state) {
|
||||
getVisualizationInfo(state, frame) {
|
||||
const dimensions = [];
|
||||
if (state.metricAccessor) {
|
||||
dimensions.push({
|
||||
|
@ -676,6 +684,11 @@ export const getMetricVisualization = ({
|
|||
const hasStaticColoring = !!state.color;
|
||||
const hasDynamicColoring = !!state.palette;
|
||||
|
||||
const currentData = frame?.activeData?.[state.layerId];
|
||||
const isMetricNumeric = Boolean(
|
||||
state.metricAccessor && isNumericFieldForDatatable(currentData, state.metricAccessor)
|
||||
);
|
||||
|
||||
return {
|
||||
layers: [
|
||||
{
|
||||
|
@ -688,10 +701,34 @@ export const getMetricVisualization = ({
|
|||
? stops.map(({ color }) => color)
|
||||
: hasStaticColoring
|
||||
? [state.color]
|
||||
: [getDefaultColor(state)]
|
||||
: [getDefaultColor(state, isMetricNumeric)]
|
||||
).filter(nonNullable),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
getUserMessages(state, { frame }) {
|
||||
const currentData = frame.activeData?.[state.layerId];
|
||||
|
||||
const errors: UserMessage[] = [];
|
||||
|
||||
if (state.maxAccessor) {
|
||||
const isMetricNonNumeric = Boolean(
|
||||
state.metricAccessor && !isNumericFieldForDatatable(currentData, state.metricAccessor)
|
||||
);
|
||||
if (isMetricNonNumeric) {
|
||||
errors.push({
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'dimensionButton', dimensionId: state.maxAccessor }],
|
||||
shortMessage: i18n.translate('xpack.lens.lnsMetric_maxDimensionPanel.nonNumericError', {
|
||||
defaultMessage: 'Primary metric must be numeric to set a maximum value.',
|
||||
}),
|
||||
longMessage: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,25 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FramePublicAPI } from '../../types';
|
||||
import { computeOverallDataDomain, getStaticValue } from './reference_line_helpers';
|
||||
import { XYDataLayerConfig } from './types';
|
||||
|
||||
function getActiveData(json: Array<{ id: string; rows: Array<Record<string, number | null>> }>) {
|
||||
return json.reduce((memo, { id, rows }) => {
|
||||
const columns = Object.keys(rows[0]).map((columnId) => ({
|
||||
id: columnId,
|
||||
name: columnId,
|
||||
meta: { type: 'number' as const },
|
||||
}));
|
||||
memo[id] = {
|
||||
type: 'datatable' as const,
|
||||
columns,
|
||||
rows,
|
||||
};
|
||||
return memo;
|
||||
}, {} as NonNullable<FramePublicAPI['activeData']>);
|
||||
}
|
||||
import { generateActiveData } from '../../mocks';
|
||||
|
||||
describe('reference_line helpers', () => {
|
||||
describe('getStaticValue', () => {
|
||||
|
@ -41,7 +25,7 @@ describe('reference_line helpers', () => {
|
|||
[],
|
||||
'x',
|
||||
{
|
||||
activeData: getActiveData([
|
||||
activeData: generateActiveData([
|
||||
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
||||
]),
|
||||
},
|
||||
|
@ -54,7 +38,7 @@ describe('reference_line helpers', () => {
|
|||
[{ layerId: 'id-a', seriesType: 'area' } as XYDataLayerConfig], // missing xAccessor for groupId == x
|
||||
'x',
|
||||
{
|
||||
activeData: getActiveData([
|
||||
activeData: generateActiveData([
|
||||
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
||||
]),
|
||||
},
|
||||
|
@ -73,7 +57,7 @@ describe('reference_line helpers', () => {
|
|||
], // missing hit of accessor "d" in data
|
||||
'yLeft',
|
||||
{
|
||||
activeData: getActiveData([
|
||||
activeData: generateActiveData([
|
||||
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
||||
]),
|
||||
},
|
||||
|
@ -92,7 +76,7 @@ describe('reference_line helpers', () => {
|
|||
], // missing yConfig fallbacks to left axis, but the requested group is yRight
|
||||
'yRight',
|
||||
{
|
||||
activeData: getActiveData([
|
||||
activeData: generateActiveData([
|
||||
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
||||
]),
|
||||
},
|
||||
|
@ -111,7 +95,7 @@ describe('reference_line helpers', () => {
|
|||
], // same as above with x groupId
|
||||
'x',
|
||||
{
|
||||
activeData: getActiveData([
|
||||
activeData: generateActiveData([
|
||||
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
||||
]),
|
||||
},
|
||||
|
@ -134,7 +118,7 @@ describe('reference_line helpers', () => {
|
|||
],
|
||||
'yRight',
|
||||
{
|
||||
activeData: getActiveData([
|
||||
activeData: generateActiveData([
|
||||
{
|
||||
id: 'id-a',
|
||||
rows: [{ a: -30 }, { a: 10 }],
|
||||
|
@ -159,7 +143,7 @@ describe('reference_line helpers', () => {
|
|||
],
|
||||
'yLeft',
|
||||
{
|
||||
activeData: getActiveData([
|
||||
activeData: generateActiveData([
|
||||
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
||||
]),
|
||||
},
|
||||
|
@ -182,7 +166,7 @@ describe('reference_line helpers', () => {
|
|||
],
|
||||
'yRight',
|
||||
{
|
||||
activeData: getActiveData([
|
||||
activeData: generateActiveData([
|
||||
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
||||
]),
|
||||
},
|
||||
|
@ -192,7 +176,7 @@ describe('reference_line helpers', () => {
|
|||
});
|
||||
|
||||
it('should correctly distribute axis on left and right with different formatters when in auto', () => {
|
||||
const tables = getActiveData([
|
||||
const tables = generateActiveData([
|
||||
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 200, c: 100 }) },
|
||||
]);
|
||||
tables['id-a'].columns[0].meta.params = { id: 'number' }; // a: number formatter
|
||||
|
@ -230,7 +214,7 @@ describe('reference_line helpers', () => {
|
|||
});
|
||||
|
||||
it('should ignore hasHistogram for left or right axis', () => {
|
||||
const tables = getActiveData([
|
||||
const tables = generateActiveData([
|
||||
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 200, c: 100 }) },
|
||||
]);
|
||||
tables['id-a'].columns[0].meta.params = { id: 'number' }; // a: number formatter
|
||||
|
@ -285,7 +269,7 @@ describe('reference_line helpers', () => {
|
|||
],
|
||||
'x', // this is influenced by the callback
|
||||
{
|
||||
activeData: getActiveData([
|
||||
activeData: generateActiveData([
|
||||
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
||||
]),
|
||||
},
|
||||
|
@ -312,7 +296,7 @@ describe('reference_line helpers', () => {
|
|||
],
|
||||
'x',
|
||||
{
|
||||
activeData: getActiveData([
|
||||
activeData: generateActiveData([
|
||||
{
|
||||
id: 'id-a',
|
||||
rows: Array(3)
|
||||
|
@ -334,7 +318,7 @@ describe('reference_line helpers', () => {
|
|||
computeOverallDataDomain(
|
||||
[{ layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] } as XYDataLayerConfig],
|
||||
['a', 'b', 'c'],
|
||||
getActiveData([
|
||||
generateActiveData([
|
||||
{
|
||||
id: 'id-a',
|
||||
rows: Array(3)
|
||||
|
@ -360,7 +344,7 @@ describe('reference_line helpers', () => {
|
|||
computeOverallDataDomain(
|
||||
[{ layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] } as XYDataLayerConfig],
|
||||
['a', 'b', 'c'],
|
||||
getActiveData([
|
||||
generateActiveData([
|
||||
{
|
||||
id: 'id-a',
|
||||
rows: Array(3)
|
||||
|
@ -385,7 +369,7 @@ describe('reference_line helpers', () => {
|
|||
{ layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] },
|
||||
] as XYDataLayerConfig[],
|
||||
['a', 'b', 'c', 'd', 'e', 'f'],
|
||||
getActiveData([
|
||||
generateActiveData([
|
||||
{ id: 'id-a', rows: [{ a: 25, b: 100, c: 100 }] },
|
||||
{ id: 'id-b', rows: [{ d: 50, e: 50, f: 50 }] },
|
||||
])
|
||||
|
@ -399,7 +383,7 @@ describe('reference_line helpers', () => {
|
|||
{ layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] },
|
||||
] as XYDataLayerConfig[],
|
||||
['a', 'b', 'c', 'd', 'e', 'f'],
|
||||
getActiveData([
|
||||
generateActiveData([
|
||||
{
|
||||
id: 'id-a',
|
||||
rows: Array(3)
|
||||
|
@ -435,7 +419,7 @@ describe('reference_line helpers', () => {
|
|||
{ layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] },
|
||||
] as XYDataLayerConfig[],
|
||||
['a', 'b', 'c', 'd', 'e', 'f'],
|
||||
getActiveData([
|
||||
generateActiveData([
|
||||
{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) },
|
||||
{ id: 'id-b', rows: Array(3).fill({ d: 50, e: 50, f: 50 }) },
|
||||
])
|
||||
|
@ -453,7 +437,7 @@ describe('reference_line helpers', () => {
|
|||
{ layerId: 'id-b', seriesType: stackedSeries, accessors: ['d', 'e', 'f'] },
|
||||
] as XYDataLayerConfig[],
|
||||
['a', 'b', 'c', 'd', 'e', 'f'],
|
||||
getActiveData([
|
||||
generateActiveData([
|
||||
{ id: 'id-a', rows: [{ a: 100, b: 100, c: 100 }] },
|
||||
{ id: 'id-b', rows: [{ d: 50, e: 50, f: 50 }] },
|
||||
])
|
||||
|
@ -475,7 +459,7 @@ describe('reference_line helpers', () => {
|
|||
{ layerId: 'id-b', seriesType, xAccessor: 'f', accessors: ['d', 'e'] },
|
||||
] as XYDataLayerConfig[],
|
||||
['a', 'b', 'd', 'e'],
|
||||
getActiveData([
|
||||
generateActiveData([
|
||||
{
|
||||
id: 'id-a',
|
||||
rows: Array(3)
|
||||
|
@ -502,7 +486,7 @@ describe('reference_line helpers', () => {
|
|||
{ layerId: 'id-b', seriesType, accessors: ['f'] },
|
||||
] as XYDataLayerConfig[],
|
||||
['c', 'f'],
|
||||
getActiveData([
|
||||
generateActiveData([
|
||||
{
|
||||
id: 'id-a',
|
||||
rows: Array(3)
|
||||
|
@ -530,7 +514,7 @@ describe('reference_line helpers', () => {
|
|||
{ layerId: 'id-b', seriesType, xAccessor: 'f', accessors: ['d', 'e'] },
|
||||
] as XYDataLayerConfig[],
|
||||
['a', 'b', 'd', 'e'],
|
||||
getActiveData([
|
||||
generateActiveData([
|
||||
{
|
||||
id: 'id-a',
|
||||
rows: Array(3)
|
||||
|
@ -560,7 +544,7 @@ describe('reference_line helpers', () => {
|
|||
} as XYDataLayerConfig,
|
||||
],
|
||||
['a', 'b', 'c'],
|
||||
getActiveData([
|
||||
generateActiveData([
|
||||
{
|
||||
id: 'id-a',
|
||||
rows: Array(3)
|
||||
|
@ -584,7 +568,7 @@ describe('reference_line helpers', () => {
|
|||
} as XYDataLayerConfig,
|
||||
],
|
||||
['a', 'b', 'c'],
|
||||
getActiveData([
|
||||
generateActiveData([
|
||||
{
|
||||
id: 'id-a',
|
||||
rows: Array(3)
|
||||
|
@ -605,7 +589,7 @@ describe('reference_line helpers', () => {
|
|||
computeOverallDataDomain(
|
||||
[],
|
||||
['a', 'b', 'c'],
|
||||
getActiveData([{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }])
|
||||
generateActiveData([{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }])
|
||||
)
|
||||
).toEqual({ min: undefined, max: undefined });
|
||||
});
|
||||
|
@ -618,7 +602,7 @@ describe('reference_line helpers', () => {
|
|||
{ layerId: 'id-b', seriesType: 'line', accessors: ['d', 'e', 'f'] },
|
||||
] as XYDataLayerConfig[],
|
||||
['a', 'b'],
|
||||
getActiveData([{ id: 'id-c', rows: [{ a: 100, b: 100 }] }]) // mind the layer id here
|
||||
generateActiveData([{ id: 'id-c', rows: [{ a: 100, b: 100 }] }]) // mind the layer id here
|
||||
)
|
||||
).toEqual({ min: undefined, max: undefined });
|
||||
|
||||
|
@ -629,7 +613,7 @@ describe('reference_line helpers', () => {
|
|||
{ layerId: 'id-b', seriesType: 'bar_stacked' },
|
||||
] as XYDataLayerConfig[],
|
||||
['a', 'b'],
|
||||
getActiveData([])
|
||||
generateActiveData([])
|
||||
)
|
||||
).toEqual({ min: undefined, max: undefined });
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue