Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AG-12978 Normalised stacked area nulls and zeroes #3121

Draft
wants to merge 6 commits into
base: latest
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions packages/ag-charts-community/src/chart/data/dataModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,8 @@ export class DataModel<

const { rawData, columns, invalidData } = processedData;
for (const processor of groupProcessors) {
console.group();
console.log(processor);
const valueIndexes = this.valueGroupIdxLookup(processor);
const adjustFn = processor.adjust()();

Expand All @@ -920,6 +922,7 @@ export class DataModel<

processedData.domain.values[valueIndex] = domain.getDomain();
}
console.groupEnd();
}
}

Expand Down
117 changes: 79 additions & 38 deletions packages/ag-charts-community/src/chart/data/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function groupAccumulativeValueProperty<K>(
) {
return [
valueProperty(propName, scaleType, opts),
accumulateGroup(opts.groupId, mode, sum, opts.separateNegative),
accumulateGroup(opts.groupId, mode, sum, opts),
...(opts.rangeId != null ? [range(opts.rangeId, opts.groupId)] : []),
];
}
Expand Down Expand Up @@ -184,9 +184,18 @@ export const SORT_DOMAIN_GROUPS: ProcessorOutputPropertyDefinition<'sortedGroupD
}),
};

function normaliseFnBuilder({ normaliseTo, mode }: { normaliseTo: number; mode: 'sum' | 'range' }) {
const normalise = (val: null | number, extent: number) => {
if (extent === 0) return null;
function normaliseFnBuilder({
normaliseTo,
mode,
invalidValue,
}: {
normaliseTo: number;
mode: 'sum' | 'range';
invalidValue: any;
}) {
const normalise = (val: null | number, extent: number | undefined) => {
if (extent == null) return invalidValue;
if (extent === 0) return 0;
const result = ((val ?? 0) * normaliseTo) / extent;
if (result >= 0) {
return Math.min(normaliseTo, result);
Expand All @@ -198,11 +207,9 @@ function normaliseFnBuilder({ normaliseTo, mode }: { normaliseTo: number; mode:
const extent = normaliseFindExtent(mode, columns, valueIndexes, datumIndex);
for (const valueIdx of valueIndexes) {
const column = columns[valueIdx];
const value: null | number | number[] | (null | number)[] = column[datumIndex];
if (value == null) {
column[datumIndex] = undefined;
continue;
}
const value: null | number | number[] = column[datumIndex];
if (value == null) continue;

column[datumIndex] =
// eslint-disable-next-line sonarjs/no-nested-functions
typeof value === 'number' ? normalise(value, extent) : value.map((v) => normalise(v, extent));
Expand All @@ -211,13 +218,16 @@ function normaliseFnBuilder({ normaliseTo, mode }: { normaliseTo: number; mode:
}

function normaliseFindExtent(mode: 'sum' | 'range', columns: any[][], valueIndexes: number[], datumIndex: number) {
const valuesExtent = [0, 0];
let valuesExtent: [number, number] | undefined;
for (const valueIdx of valueIndexes) {
const column = columns[valueIdx];
const value: null | number | (null | number)[] = column[datumIndex];
const value: null | any[] = column[datumIndex];
if (value == null) continue;
// Note - Array.isArray(new Float64Array) is false, and this type is used for stack accumulators
const valueExtent = typeof value === 'number' ? value : Math.max(...value.map((v) => v ?? 0));
const valueExtent = typeof value === 'number' ? value : value[value.length - 1];
if (!Number.isFinite(valueExtent)) continue;

valuesExtent ??= [0, 0];
const valIdx = valueExtent < 0 ? 0 : 1;
if (mode === 'sum') {
valuesExtent[valIdx] += valueExtent;
Expand All @@ -228,18 +238,21 @@ function normaliseFindExtent(mode: 'sum' | 'range', columns: any[][], valueIndex
}
}

if (valuesExtent == null) return;

return Math.max(Math.abs(valuesExtent[0]), valuesExtent[1]);
}

export function normaliseGroupTo(
matchGroupIds: string[],
normaliseTo: number,
mode: 'sum' | 'range' = 'sum'
mode: 'sum' | 'range' = 'sum',
{ invalidValue = null }: { invalidValue?: any } = {}
): GroupValueProcessorDefinition<any, any> {
return {
type: 'group-value-processor',
matchGroupIds,
adjust: memo({ normaliseTo, mode }, normaliseFnBuilder),
adjust: memo({ normaliseTo, mode, invalidValue }, normaliseFnBuilder),
};
}

Expand Down Expand Up @@ -363,24 +376,46 @@ export function animationValidation(valueKeyIds?: string[]): ProcessorOutputProp
};
}

function buildGroupAccFn({ mode, separateNegative }: { mode: 'normal' | 'trailing'; separateNegative?: boolean }) {
function buildGroupAccFn({
mode,
separateNegative,
invalidValue,
}: {
mode: 'normal' | 'trailing';
separateNegative?: boolean;
invalidValue: number;
}) {
return () => () => (columns: any[][], valueIndexes: number[], datumIndex: number) => {
// Datum scope.
const acc = [0, 0];

for (const valueIdx of valueIndexes) {
const column = columns[valueIdx];
const currentVal = column[datumIndex];
const accIndex = isNegative(currentVal) && separateNegative ? 0 : 1;
if (!isFiniteNumber(currentVal)) continue;
let currentVal = column[datumIndex];
if (!Number.isFinite(currentVal)) currentVal = invalidValue;

const accIndex = !Number.isFinite(currentVal) || (isNegative(currentVal) && separateNegative) ? 0 : 1;
const nextAcc = acc[accIndex] + currentVal;

if (mode === 'normal') acc[accIndex] += currentVal;
column[datumIndex] = acc[accIndex];
if (mode === 'trailing') acc[accIndex] += currentVal;
if (Number.isFinite(nextAcc)) {
column[datumIndex] = mode === 'normal' ? nextAcc : acc[accIndex];
acc[accIndex] = nextAcc;
} else {
column[datumIndex] = invalidValue;
}
}
};
}

function buildGroupWindowAccFn({ mode, sum }: { mode: 'normal' | 'trailing'; sum: 'current' | 'last' }) {
function buildGroupWindowAccFn({
mode,
sum,
invalidValue,
}: {
mode: 'normal' | 'trailing';
sum: 'current' | 'last';
invalidValue: number;
}) {
return () => {
// Entire data-set scope.
const lastValues: any[] = [];
Expand All @@ -392,22 +427,28 @@ function buildGroupWindowAccFn({ mode, sum }: { mode: 'normal' | 'trailing'; sum
let acc = 0;
for (const valueIdx of valueIndexes) {
const column = columns[valueIdx];
const currentVal = column[datumIndex];
const lastValue = firstRow && sum === 'current' ? 0 : lastValues[valueIdx];
lastValues[valueIdx] = currentVal;

const sumValue = sum === 'current' ? currentVal : lastValue;
if (!isFiniteNumber(currentVal) || !isFiniteNumber(lastValue)) {
column[datumIndex] = acc;
continue;
let currentVal = column[datumIndex];
if (!Number.isFinite(currentVal)) currentVal = invalidValue;

let sumValue: number;
if (sum === 'current') {
sumValue = currentVal;
} else if (!firstRow) {
sumValue = lastValues[valueIdx];
} else if (Number.isFinite(currentVal)) {
sumValue = 0;
} else {
sumValue = invalidValue;
}
lastValues[valueIdx] = currentVal;

if (mode === 'normal') {
acc += sumValue;
}
column[datumIndex] = acc;
if (mode === 'trailing') {
acc += sumValue;
const nextAcc = acc + sumValue;
if (Number.isFinite(nextAcc)) {
column[datumIndex] = mode === 'normal' ? nextAcc : acc;
acc = nextAcc;
} else {
column[datumIndex] = invalidValue;
}
}

Expand All @@ -421,14 +462,14 @@ export function accumulateGroup(
matchGroupId: string,
mode: 'normal' | 'trailing' | 'window' | 'window-trailing',
sum: 'current' | 'last',
separateNegative = false
{ separateNegative = false, invalidValue = 0 } = {}
): GroupValueProcessorDefinition<any, any> {
let adjust;
if (mode.startsWith('window')) {
const modeParam = mode.endsWith('-trailing') ? 'trailing' : 'normal';
adjust = memo({ mode: modeParam, sum }, buildGroupWindowAccFn);
adjust = memo({ mode: modeParam, sum, invalidValue }, buildGroupWindowAccFn);
} else {
adjust = memo({ mode: mode as 'normal' | 'trailing', separateNegative }, buildGroupAccFn);
adjust = memo({ mode: mode as 'normal' | 'trailing', separateNegative, invalidValue }, buildGroupAccFn);
}

return {
Expand Down
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,18 @@ const EXAMPLES: Record<string, CartesianOrPolarTestCase & { skip?: boolean }> =
],
skipWarningsReversed: false,
}),
NORMALISED_AREA_STACKED: {
NORMALISED_STACKED_AREA: {
options: examples.NORMALISED_STACKED_AREA,
assertions: cartesianChartAssertions({ axisTypes: ['category', 'number'], seriesTypes: repeat('area', 4) }),
},
NORMALISED_STACKED_AREA_WITH_ZEROES: {
options: examples.NORMALISED_STACKED_AREA_WITH_ZEROES,
assertions: cartesianChartAssertions({ axisTypes: ['category', 'number'], seriesTypes: repeat('area', 4) }),
},
NORMALISED_STACKED_AREA_CONNECTED: {
options: examples.NORMALISED_STACKED_AREA_CONNECTED,
assertions: cartesianChartAssertions({ axisTypes: ['category', 'number'], seriesTypes: repeat('area', 4) }),
},
}),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { extent } from '../../../util/array';
import { mergeDefaults } from '../../../util/object';
import { isDefined } from '../../../util/type-guards';
import type { RequireOptional } from '../../../util/types';
import { isContinuous } from '../../../util/value';
import { LogAxis } from '../../axis/logAxis';
import { TimeAxis } from '../../axis/timeAxis';
import { ChartAxisDirection } from '../../chartAxisDirection';
Expand Down Expand Up @@ -198,21 +197,28 @@ export class AreaSeries extends CartesianSeries<
marker: `area-stack-${groupIndex}-yValues-marker`,
};

const common: Partial<DatumPropertyDefinition<unknown>> = { invalidValue: NaN };
const extraProps = [];
if (isDefined(normalizedTo)) {
extraProps.push(normaliseGroupTo(Object.values(idMap), normalizedTo, 'range'));

if (connectMissingData) {
if (isDefined(normalizedTo)) {
common.invalidValue = 0;
extraProps.push(normaliseGroupTo(Object.values(idMap), normalizedTo, 'range', { invalidValue: 0 }));
} else if (stackCount > 1) {
common.invalidValue = 0;
}
} else if (isDefined(normalizedTo)) {
extraProps.push(normaliseGroupTo(Object.values(idMap), normalizedTo, 'range', { invalidValue: NaN }));
}

if (animationEnabled) {
extraProps.push(animationValidation());
}

const common: Partial<DatumPropertyDefinition<unknown>> = { invalidValue: null };
if ((isDefined(normalizedTo) || connectMissingData) && stackCount > 1) {
common.invalidValue = 0;
}
if (!visible) {
common.forceValue = 0;
}

await this.requestDataModel<any, any, true>(dataController, data, {
props: [
keyProperty(xKey, xScaleType, { id: 'xValue' }),
Expand Down Expand Up @@ -312,9 +318,6 @@ export class AreaSeries extends CartesianSeries<
} = this.properties;
const { scale: xScale } = xAxis;
const { scale: yScale } = yAxis;

const { isContinuousY } = this.getScaleInformation({ xScale, yScale });

const xOffset = (xScale.bandwidth ?? 0) / 2;

const xValues = dataModel.resolveKeysById(this, 'xValue', processedData);
Expand All @@ -325,25 +328,6 @@ export class AreaSeries extends CartesianSeries<
yFilterKey != null ? dataModel.resolveColumnById(this, 'yFilterRaw', processedData) : undefined;
const yStackValues = dataModel.resolveColumnById<number[]>(this, 'yValueStack', processedData);

const createMarkerCoordinate = (xDatum: any, yEnd: number, rawYDatum: any): SizedPoint => {
let currY;

// if not normalized, the invalid data points will be processed as `undefined` in processData()
// if normalized, the invalid data points will be processed as 0 rather than `undefined`
// check if unprocessed datum is valid as we only want to show markers for valid points
if (
isDefined(this.properties.normalizedTo) ? isContinuousY && isContinuous(rawYDatum) : !isNaN(rawYDatum)
) {
currY = yEnd;
}

return {
x: xScale.convert(xDatum) + xOffset,
y: yScale.convert(currY),
size: marker.size,
};
};

const labelData: LabelSelectionDatum[] = [];
const markerData: MarkerSelectionDatum[] = [];
const { visibleSameStackCount } = this.ctx.seriesStateManager.getVisiblePeerGroupIndex(this);
Expand All @@ -362,8 +346,11 @@ export class AreaSeries extends CartesianSeries<

const validPoint = Number.isFinite(yDatum);

// marker data
const point = createMarkerCoordinate(xDatum, +yValueCumulative, yDatum);
const point: SizedPoint = {
x: xScale.convert(xDatum) + xOffset,
y: yScale.convert(+yValueCumulative),
size: marker.size,
};

const selected = yFilterValues != null ? yFilterValues[datumIndex] === yDatum : undefined;
if (selected === false) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export class LineSeries extends CartesianSeries<
);

if (isDefined(normalizedTo)) {
props.push(normaliseGroupTo([ids[0], ids[1], ids[2]], normalizedTo, 'range'));
props.push(normaliseGroupTo([ids[0], ids[1], ids[2]], normalizedTo, 'range', { invalidValue: 0 }));
}
}

Expand Down
Loading
Loading