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

Stacked Bar: Fixing how stacked values are handled #525

Merged
merged 3 commits into from
Jan 29, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { VisXYContainer, VisScatter, VisAxis, VisLine } from '@unovis/react'

import { XYDataRecord } from '@src/utils/data'
import { ExampleViewerDurationProps } from '@src/components/ExampleViewer/index'
import type { WithOptional } from '@unovis/ts/lib/types/misc'

export const title = 'Scatter with Line'
export const subTitle = 'And undefined segments'
Expand All @@ -11,7 +12,7 @@ export const component = (props: ExampleViewerDurationProps): JSX.Element => {
(d: XYDataRecord) => d.y,
]

const data: XYDataRecord[] = [
const data: WithOptional<XYDataRecord, 'y'>[] = [
{ x: 0, y: 9.4 },
{ x: 1, y: 8.6 },
{ x: 2, y: undefined },
Expand All @@ -25,7 +26,7 @@ export const component = (props: ExampleViewerDurationProps): JSX.Element => {
{ x: 14, y: 5.2 },
]
return (
<VisXYContainer<XYDataRecord> data={data} margin={{ top: 5, left: 5 }} xDomain={[-1, 15]}>
<VisXYContainer data={data} margin={{ top: 5, left: 5 }} xDomain={[-1, 15]}>
<VisScatter x={d => d.x} y={accessors} duration={props.duration}/>
<VisLine x={d => d.x} y={accessors} duration={props.duration}/>
<VisAxis type='x' numTicks={15} tickFormat={(x: number) => `${x}`} duration={props.duration}/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useRef } from 'react'
import { VisXYContainer, VisStackedBar, VisAxis, VisTooltip, VisCrosshair } from '@unovis/react'

import { XYDataRecord, generateXYDataRecords } from '@src/utils/data'
import { ExampleViewerDurationProps } from '@src/components/ExampleViewer/index'

export const title = 'Stacked Bar: Negative Values'
export const subTitle = 'Positive and Negative Stack'
export const component = (props: ExampleViewerDurationProps): JSX.Element => {
const tooltipRef = useRef(null)
const data = generateXYDataRecords(15)
const accessors = [
(d: XYDataRecord) => (d.y1 || 0) - 3,
(d: XYDataRecord, i: number) => i % 4 ? d.y : -d.y,
(d: XYDataRecord, i: number) => i % 2 ? d.y1 : -(d.y1 as number),
]

return (
<VisXYContainer<XYDataRecord> data={data} margin={{ top: 5, left: 5 }}>
<VisStackedBar x={d => d.x} y={accessors} duration={props.duration}/>
<VisAxis type='x' numTicks={3} tickFormat={(x: number) => `${x}ms`} duration={props.duration}/>
<VisAxis type='y' tickFormat={(y: number) => `${y}bps`} duration={props.duration}/>
<VisCrosshair template={(d: XYDataRecord) => `${d.x}`} />
<VisTooltip ref={tooltipRef} />
</VisXYContainer>
)
}
2 changes: 1 addition & 1 deletion packages/dev/src/utils/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const randomNumberGenerator = new Seedramdon('unovis')

export type XYDataRecord = {
x: number;
y: number | undefined;
y: number;
y1?: number;
y2?: number;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/ts/src/components/area/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class Area<Datum> extends XYComponentCore<Datum, AreaConfigInterface<Datu
const areaDataX = data.map((d, i) => this.xScale(getNumber(d, config.x, i)))

const stacked = getStackedData(data, config.baseline, yAccessors, this._prevNegative)
this._prevNegative = stacked.map(s => !!s.negative)
this._prevNegative = stacked.map(s => !!s.isMostlyNegative)
const stackedData: AreaDatum[][] = stacked.map(
arr => arr.map(
(d, j) => ({
Expand Down
18 changes: 12 additions & 6 deletions packages/ts/src/components/stacked-bar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class StackedBar<Datum> extends XYComponentCore<Datum, StackedBarConfigIn

const yAccessors = this.getAccessors()
const stacked = getStackedData(this._barData, 0, yAccessors, this._prevNegative)
this._prevNegative = stacked.map(s => !!s.negative)
this._prevNegative = stacked.map(s => !!s.isMostlyNegative)

const barGroups = this.g
.selectAll<SVGGElement, Datum>(`.${s.barGroup}`)
Expand Down Expand Up @@ -126,8 +126,15 @@ export class StackedBar<Datum> extends XYComponentCore<Datum, StackedBarConfigIn
// Render Bars
const bars = barGroupsMerged
.selectAll<SVGPathElement, StackedBarDataRecord<Datum>>(`.${s.bar}`)
.data((d, j) => stacked.map((s) =>
({ ...d, _stacked: s[j], _negative: s.negative, _ending: s.ending }))
.data((d, j) => stacked.map((s, stackIndex) =>
({
...d,
_index: j,
_stacked: s[j],
// Ending bar if the next stack is not the same as the current one
_ending: (stackIndex === stacked.length - 1) ||
((stackIndex <= stacked.length - 1) && stacked[stackIndex + 1][j][0] !== s[j][1]),
}))
)

const barsEnter = bars.enter().append('path')
Expand Down Expand Up @@ -201,10 +208,9 @@ export class StackedBar<Datum> extends XYComponentCore<Datum, StackedBarConfigIn
const yAccessors = this.getAccessors()
const barWidth = this._getBarWidth()

const isNegative = d._negative
const isNegative = d._stacked[1] < 0
const isEnding = d._ending // The most top bar or, if the value is negative, the most bottom bar
// Todo: Find a way to pass the datum index to `getNumber` below
const value = getNumber(d, yAccessors[accessorIndex])
const value = getNumber(d, yAccessors[accessorIndex], d._index)
const height = isEntering ? 0 : Math.abs(this.valueScale(d._stacked[0]) - this.valueScale(d._stacked[1]))
const h = !isEntering && config.barMinHeight1Px && (height < 1) && isFinite(value) && (value !== config.barMinHeightZeroValue) ? 1 : height
const y = isEntering
Expand Down
2 changes: 1 addition & 1 deletion packages/ts/src/components/stacked-bar/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type StackedBarDataRecord<D> = D & {
_index: number;
_stacked: [number, number];
_negative: boolean;
_ending: boolean;
}
2 changes: 1 addition & 1 deletion packages/ts/src/types/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
export type GenericDataRecord = Record<string, unknown>

/** Extension of a numbers array that carries additional information required for plotting stacked data */
export type StackValuesRecord = Array<[number, number]> & { negative?: boolean; ending?: boolean }
export type StackValuesRecord = Array<[number, number]> & { isMostlyNegative: boolean }
16 changes: 3 additions & 13 deletions packages/ts/src/utils/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,13 @@ export function getStackedData<Datum> (
return (average === 0 && Array.isArray(prevNegative)) ? prevNegative[j] : average < 0
})

const stackedData: StackValuesRecord[] = acs.map(() => [])
const stackedData = acs.map(() => [] as StackValuesRecord)
data.forEach((d, i) => {
let positiveStack = baselineValues[i]
let negativeStack = baselineValues[i]
acs.forEach((a, j) => {
const value = getNumber(d, a, i) || 0
if (!isNegativeStack[j]) {
if (value >= 0) {
stackedData[j].push([positiveStack, positiveStack += value])
} else {
stackedData[j].push([negativeStack, negativeStack += value])
Expand All @@ -279,19 +279,9 @@ export function getStackedData<Datum> (

// Fill in additional stack information
stackedData.forEach((stack, i) => {
stack.negative = isNegativeStack[i]
stack.isMostlyNegative = isNegativeStack[i]
})

stackedData.filter(s => s.negative)
.forEach((s, i, arr) => {
s.ending = i === arr.length - 1
})

stackedData.filter(s => !s.negative)
.forEach((s, i, arr) => {
s.ending = i === arr.length - 1
})

return stackedData
}

Expand Down
Loading