Skip to content

Commit

Permalink
Optimize - simplify binning logic and dynamic label based on success …
Browse files Browse the repository at this point in the history
…criteria (#4180)

Co-authored-by: Cole Blanchard <[email protected]>
Co-authored-by: Shawn Yama <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cole Blanchard <[email protected]>
  • Loading branch information
5 people authored Jul 19, 2024
1 parent 04aa307 commit 2d95b29
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 53 deletions.
107 changes: 54 additions & 53 deletions packages/client/hmi-client/src/services/charts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mean, isEmpty } from 'lodash';
import { isEmpty } from 'lodash';

const VEGALITE_SCHEMA = 'https://vega.github.io/schema/vega-lite/v5.json';

Expand Down Expand Up @@ -260,62 +260,63 @@ export interface OptimizeChartOptions {

const binCount = 5;

function formatSuccessChartData(riskResults: any, targetVariable: string, threshold: number, isMinimized: boolean) {
export function formatSuccessChartData(
riskResults: any,
targetVariable: string,
threshold: number,
isMinimized: boolean
) {
const targetState = `${targetVariable}_state`;
const data = riskResults[targetState]?.qoi || [];

const minValue = Math.min(...data);
const maxValue = Math.max(...data);
const stepSize = (maxValue - minValue) / binCount;
const bins: { range: string; count: number; tag: 'in' | 'out' }[] = [];
for (let i = binCount; i > 0; i--) {
let rangeStart = minValue + stepSize * (i - 1);
let rangeEnd = minValue + stepSize * i;

// Handle edge case where stepSize is 0, and give the range a thickness so it can be seen
if (stepSize === 0) {
rangeStart = minValue - 1;
rangeEnd = maxValue + 1;
}

let tag;
if (isMinimized) {
tag = rangeEnd < threshold ? 'in' : 'out';
} else {
tag = rangeStart > threshold ? 'in' : 'out';
}

bins.push({
range: `${rangeStart.toFixed(4)}-${rangeEnd.toFixed(4)}`,
count: 0,
tag
});
if (isEmpty(data)) {
return [];
}

const toBinIndex = (value: number) => {
if (value < minValue || value > maxValue) return -1;
// return first bin in cases where the max value is the same as the incoming value or when the min and max data values are the same (stepsize = 0)
if (stepSize === 0 || value === maxValue) return 0;
const index = binCount - 1 - Math.abs(Math.floor((value - minValue) / stepSize));
return index;
const min = Math.min(...data);
const max = Math.max(...data);
const ranges: { range: string; count: number; tag: 'in' | 'out' }[] = [];

// see whether ranges are in or out of the threshold
const determineTag = (
start: number,
end: number,
thresholdValue: number,
isMinimizedValue: boolean
): 'in' | 'out' => {
if (isMinimizedValue) {
return end < thresholdValue ? 'in' : 'out';
}
return start > thresholdValue ? 'in' : 'out';
};

const avgArray: number[] = [];

// Fill bins:
data.forEach((ele) => {
const index = toBinIndex(ele);
if (index !== -1) {
bins[index].count += 1;
if (bins[index].tag === 'out') {
avgArray.push(ele);
}
if (min === max) {
// case where there is only one value or all values are the same. we want to give the bar some thickness so that the user can see it.
const start = min - 1;
const end = min + 1;
const count = data.length;

ranges.push({
range: `${start.toFixed(4)}-${end.toFixed(4)}`,
count,
tag: determineTag(start, end, threshold, isMinimized)
});
} else {
const rangeSize = (max - min) / binCount;

for (let i = 0; i < binCount; i++) {
const start = min + i * rangeSize;
const end = i === binCount - 1 ? max : start + rangeSize;
// count the number of values in the range, some logic here to handle the last bin which is inclusive of the max value
const count = data.filter((num) => num >= start && (i === binCount - 1 ? num <= end : num < end)).length;

ranges.push({
range: `${start.toFixed(4)}-${end.toFixed(4)}`,
count,
tag: determineTag(start, end, threshold, isMinimized)
});
}
});

const avg = mean(avgArray);
}

return { data: bins, avg };
return ranges;
}

export function createOptimizeChart(
Expand All @@ -324,7 +325,7 @@ export function createOptimizeChart(
threshold: number,
isMinimized: boolean
): any {
const { data } = formatSuccessChartData(riskResults, targetVariable, threshold, isMinimized);
const data = formatSuccessChartData(riskResults, targetVariable, threshold, isMinimized);

return {
$schema: VEGALITE_SCHEMA,
Expand Down Expand Up @@ -355,14 +356,14 @@ export function createOptimizeChart(
y: {
field: 'start',
type: 'quantitative',
title: 'Min value at all times'
title: `${isMinimized ? 'Max' : 'Min'} value of ${targetVariable} at all times`
},
y2: { field: 'end' },
x: {
aggregate: 'sum',
field: 'count',
type: 'quantitative',
title: 'Count'
title: 'Number of samples'
},
color: {
field: 'tag',
Expand Down
43 changes: 43 additions & 0 deletions packages/client/hmi-client/tests/unit/utils/chart.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import { formatSuccessChartData } from '@/services/charts';

const riskResults = {
S_state: {
qoi: [0],
risk: 0
}
};
describe('chart util tests', () => {
it('should correctly format success chart data', () => {
riskResults.S_state.qoi = [0, 0, 0.5, 0.7, 1, 1.2, 2, 3, 3, 4, 5, 5, 5, 5];
expect(formatSuccessChartData(riskResults, 'S', 2.5, true)).to.deep.equal([
{ range: '0.0000-1.0000', count: 4, tag: 'in' },
{ range: '1.0000-2.0000', count: 2, tag: 'in' },
{ range: '2.0000-3.0000', count: 1, tag: 'out' },
{ range: '3.0000-4.0000', count: 2, tag: 'out' },
{ range: '4.0000-5.0000', count: 5, tag: 'out' }
]);

expect(formatSuccessChartData(riskResults, 'S', 2.5, false)).to.deep.equal([
{ range: '0.0000-1.0000', count: 4, tag: 'out' },
{ range: '1.0000-2.0000', count: 2, tag: 'out' },
{ range: '2.0000-3.0000', count: 1, tag: 'out' },
{ range: '3.0000-4.0000', count: 2, tag: 'in' },
{ range: '4.0000-5.0000', count: 5, tag: 'in' }
]);
});

it('should correctly format success chart data with 1 value', () => {
riskResults.S_state.qoi = [5];
expect(formatSuccessChartData(riskResults, 'S', 3, true)).to.deep.equal([
{ range: '4.0000-6.0000', count: 1, tag: 'out' }
]);
});

it('should correctly format success chart data with all the same values', () => {
riskResults.S_state.qoi = [5, 5, 5, 5, 5, 5];
expect(formatSuccessChartData(riskResults, 'S', 3, true)).to.deep.equal([
{ range: '4.0000-6.0000', count: 6, tag: 'out' }
]);
});
});

0 comments on commit 2d95b29

Please sign in to comment.