Skip to content
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
2 changes: 2 additions & 0 deletions packages/malloy-render/src/api/malloy-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {RenderPluginFactory} from './plugin-types';
import {MalloyViz} from './malloy-viz';
import {LineChartPluginFactory} from '@/plugins/line-chart/line-chart-plugin';
import {BarChartPluginFactory} from '@/plugins/bar-chart/bar-chart-plugin';
import {WaterfallChartPluginFactory} from '@/plugins/waterfall-chart/waterfall-chart-plugin';

export class MalloyRenderer {
private globalOptions: MalloyRendererOptions;
Expand All @@ -20,6 +21,7 @@ export class MalloyRenderer {
this.pluginRegistry = [
LineChartPluginFactory,
BarChartPluginFactory,
WaterfallChartPluginFactory,
...(options.plugins || []),
];
}
Expand Down
1 change: 1 addition & 0 deletions packages/malloy-render/src/data_tree/cells/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
}

cellAtPath(path: string[]): Cell {
console.log({path, cell: this});

Check warning on line 166 in packages/malloy-render/src/data_tree/cells/base.ts

View workflow job for this annotation

GitHub Actions / main / main

Unexpected console statement
if (path.length === 0) {
return this.asCell();
}
Expand Down
1 change: 1 addition & 0 deletions packages/malloy-render/src/data_tree/fields/nest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
}

fieldAtPath(path: string[]): Field {
console.log('fieldAtPath', path);

Check warning on line 47 in packages/malloy-render/src/data_tree/fields/nest.ts

View workflow job for this annotation

GitHub Actions / main / main

Unexpected console statement
if (path.length === 0) {
return this.asField();
} else {
Expand Down
5 changes: 5 additions & 0 deletions packages/malloy-render/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ export {
BarChartPluginFactory,
type BarChartPluginInstance,
} from './bar-chart/bar-chart-plugin';

export {
WaterfallChartPluginFactory,
type WaterfallChartPluginInstance,
} from './waterfall-chart/waterfall-chart-plugin';
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import type {
MalloyDataToChartDataHandler,
VegaChartProps,
} from '@/component/types';
import {convertLegacyToVizTag} from '@/component/tag-utils';
import type {RenderMetadata} from '@/component/render-result-metadata';
import {Field, RepeatedRecordCell} from '@/data_tree';

Check failure on line 7 in packages/malloy-render/src/plugins/waterfall-chart/generate-waterfall_chart-vega-spec.ts

View workflow job for this annotation

GitHub Actions / main / main

All imports in the declaration are only used as types. Use `import type`
import type {RecordCell} from '@/data_tree';
import type {Spec} from 'vega';
import type {WaterfallChartPluginInstance} from './waterfall-chart-plugin';
import {getChartLayoutSettings} from '@/component/chart/chart-layout-settings';
import type {NestField} from '@/data_tree';

Check failure on line 12 in packages/malloy-render/src/plugins/waterfall-chart/generate-waterfall_chart-vega-spec.ts

View workflow job for this annotation

GitHub Actions / main / main

@/data_tree type import is duplicated

// Helper function to extract synthetic data for chart layout calculation
function getSyntheticDataForLayout(
explore: NestField,
settings: any,

Check failure on line 17 in packages/malloy-render/src/plugins/waterfall-chart/generate-waterfall_chart-vega-spec.ts

View workflow job for this annotation

GitHub Actions / main / main

Unexpected any. Specify a different type
data?: {rows: RecordCell[]}
): {
xField: Field;
yField: Field;
getYMinMax: () => [number, number];
} {
// For x-axis, we need to consider all x labels including 'start', 'end', 'Others*', and actual category values
const nestPath = JSON.parse(settings.xField).slice(0, 1);
const xPath = JSON.parse(settings.xField).slice(-1);

Check failure on line 26 in packages/malloy-render/src/plugins/waterfall-chart/generate-waterfall_chart-vega-spec.ts

View workflow job for this annotation

GitHub Actions / main / main

'xPath' is assigned a value but never used. Allowed unused vars must match /^_/u
const yPath = JSON.parse(settings.yField).slice(-1);

// Get the nested field to access x values
const nestedField = explore.fieldAt([nestPath]);
const xField = nestedField.isRepeatedRecord() ? nestedField.fields[0] : explore.fields[0];

Check failure on line 31 in packages/malloy-render/src/plugins/waterfall-chart/generate-waterfall_chart-vega-spec.ts

View workflow job for this annotation

GitHub Actions / main / main

Replace `·?·nestedField.fields[0]` with `⏎····?·nestedField.fields[0]⏎···`
const yField = nestedField.isRepeatedRecord() ? nestedField.fields[1] : explore.fields[1];

Check failure on line 32 in packages/malloy-render/src/plugins/waterfall-chart/generate-waterfall_chart-vega-spec.ts

View workflow job for this annotation

GitHub Actions / main / main

Replace `·?·nestedField.fields[1]` with `⏎····?·nestedField.fields[1]⏎···`

// Collect all possible x-axis labels
const xLabels = new Set<string>(['start', 'end', 'Others*']);

// Add actual x values from field metadata if available
if (xField.valueSet && xField.valueSet.size > 0) {
for (const val of xField.valueSet) {
xLabels.add(String(val));
}
}

// Find the longest x label
let maxXString = '';
for (const label of xLabels) {
if (label.length > maxXString.length) {
maxXString = label;
}
}

// Create lazy calculation for y-axis min/max
const getYMinMax = (): [number, number] => {
// Use field metadata if available
if (yField.minNumber !== undefined && yField.maxNumber !== undefined) {
// Add some padding for the waterfall visualization
const range = yField.maxNumber - yField.minNumber;
return [
Math.min(0, yField.minNumber - range * 0.1),
yField.maxNumber + range * 0.1

Check failure on line 60 in packages/malloy-render/src/plugins/waterfall-chart/generate-waterfall_chart-vega-spec.ts

View workflow job for this annotation

GitHub Actions / main / main

Insert `,`
];
}

// Fallback to data calculation if provided
let min = 0;
let max = 0;

if (data && data.rows) {
for (const row of data.rows as RecordCell[]) {
const startVal = row.cellAt(settings.startField).value as number;
const endVal = row.cellAt(settings.endField).value as number;

min = Math.min(min, startVal, endVal);
max = Math.max(max, startVal, endVal);

// Calculate intermediate values
let current = startVal;
const nested = row.cellAt([nestPath]) as RepeatedRecordCell;
if (nested && nested.rows) {
for (const nRow of nested.rows) {
const yVal = nRow.cellAt(yPath).value as number;
current += yVal;
min = Math.min(min, current);
max = Math.max(max, current);
}
}
}
}

return [min, max];
};

// Create a synthetic x field with the max string value
const syntheticXField = {
...xField,
maxString: maxXString,
valueSet: xLabels,
} as Field;

return {
xField: syntheticXField,
yField,
getYMinMax,
};
}

export function generateWaterfallChartVegaSpec(
metadata: RenderMetadata,
plugin: WaterfallChartPluginInstance
): VegaChartProps {
const {field: explore} = plugin;
const settings = plugin.getMetadata().settings;
const tag = convertLegacyToVizTag(explore.tag);
const chartTag = tag.tag('viz');
if (!chartTag)
throw new Error(
'Malloy Waterfall Chart: Tried to render a waterfall chart, but no viz=waterfall tag was found'
);

// Pre-process data to get synthetic values for chart layout calculation
const syntheticData = getSyntheticDataForLayout(explore, settings);

// Get chart layout settings with synthetic data
const chartSettings = getChartLayoutSettings(explore, chartTag, {
metadata,
xField: syntheticData.xField,
yField: syntheticData.yField,
chartType: 'waterfall',
getYMinMax: syntheticData.getYMinMax,
});

const spec: Spec = {
$schema: 'https://vega.github.io/schema/vega/v5.json',
width: chartSettings.plotWidth,
height: chartSettings.plotHeight,
padding: chartSettings.padding,
data: [{name: 'values'}],
autosize: {
type: 'none',
resize: true,
contains: 'padding',
},
scales: [
{
name: 'x',
type: 'band',
domain: {data: 'values', field: 'x'},
range: 'width',
padding: 0.1,
},
{
name: 'y',
type: 'linear',
domain: chartSettings.yScale.domain || {data: 'values', fields: ['start', 'end']},

Check failure on line 154 in packages/malloy-render/src/plugins/waterfall-chart/generate-waterfall_chart-vega-spec.ts

View workflow job for this annotation

GitHub Actions / main / main

Replace `data:·'values',·fields:·['start',·'end']` with `⏎··········data:·'values',⏎··········fields:·['start',·'end'],⏎········`
nice: true,
range: 'height',
},
],
axes: [
{
scale: 'x',
orient: 'bottom' as const,
labelAngle: chartSettings.xAxis.labelAngle,
labelAlign: chartSettings.xAxis.labelAlign,
labelBaseline: chartSettings.xAxis.labelBaseline,
labelLimit: chartSettings.xAxis.labelLimit,
title: syntheticData.xField.name || 'Category',
titlePadding: 10,
},
{
scale: 'y',
orient: 'left' as const,
tickCount: chartSettings.yAxis.tickCount ?? 'ceil(height/40)',
labelLimit: chartSettings.yAxis.width + 10,
title: syntheticData.yField.name || 'Value',
},
].filter(axis => {

Check failure on line 177 in packages/malloy-render/src/plugins/waterfall-chart/generate-waterfall_chart-vega-spec.ts

View workflow job for this annotation

GitHub Actions / main / main

'axis' is defined but never used. Allowed unused args must match /^_/u
// Hide axes for spark charts
if (chartSettings.isSpark) {
return false;
}
return true;
}),
marks: [
{
type: 'rect',
from: {data: 'values'},
encode: {
update: {
x: {scale: 'x', field: 'x'},
width: {scale: 'x', band: 1, offset: -1},
y: {scale: 'y', signal: 'max(datum.start, datum.end)'},
y2: {scale: 'y', signal: 'min(datum.start, datum.end)'},
fill: {signal: 'datum.value >= 0 ? "#1877F2" : "#DC3545"'},
},
},
},
],
};

const mapMalloyDataToChartData: MalloyDataToChartDataHandler = data => {
const records: {x: string; value: number; start: number; end: number}[] =
[];

for (const row of data.rows as RecordCell[]) {
const startVal = row.cellAt(settings.startField).value as number;
const endVal = row.cellAt(settings.endField).value as number;
let current = startVal;
let sumOfIntermediates = 0;

records.push({x: 'start', value: startVal, start: 0, end: startVal});
const nestPath = JSON.parse(settings.xField).slice(0, 1);
const xPath = JSON.parse(settings.xField).slice(-1);
const yPath = JSON.parse(settings.yField).slice(-1);
const nested = row.cellAt([nestPath]) as RepeatedRecordCell;
for (const nRow of nested.rows) {
const xVal = nRow.cellAt(xPath).value;
const yVal = nRow.cellAt(yPath).value as number;
const start = current;
const end = current + yVal;
records.push({x: String(xVal), value: yVal, start, end});
current = end;
sumOfIntermediates += yVal;
}

// Check if sum of intermediate values equals difference between start and end
const expectedDiff = endVal - startVal;
const actualDiff = sumOfIntermediates;
if (Math.abs(expectedDiff - actualDiff) > 0.0001) {
// Using small epsilon for floating point comparison
const othersValue = expectedDiff - actualDiff;
const start = current;
const end = current + othersValue;
records.push({x: 'Others*', value: othersValue, start, end});
current = end;
}

records.push({x: 'end', value: endVal, start: 0, end: endVal});
}

return {data: records, isDataLimited: false};
};

return {
spec,
plotWidth: chartSettings.plotWidth,
plotHeight: chartSettings.plotHeight,
totalWidth: chartSettings.totalWidth,
totalHeight: chartSettings.totalHeight,
chartType: 'waterfall',
chartTag,
mapMalloyDataToChartData,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type {Tag} from '@malloydata/malloy-tag';
import type {NestField} from '@/data_tree';
import {type WaterfallChartSettings} from './waterfall-chart-settings';

export function getWaterfallChartSettings(
explore: NestField,
tagOverride?: Tag
): WaterfallChartSettings {
const tag = tagOverride ?? explore.tag;
const vizTag = tag.tag('viz')!;

const startRef = vizTag.text('start');
const endRef = vizTag.text('end');
const xRef = vizTag.text('x');
const yRef = vizTag.text('y');

if (!startRef || !endRef || !xRef || !yRef) {
throw new Error(
'Malloy Waterfall Chart: start, end, x and y must be specified'
);
}

return {
startField: JSON.stringify(startRef.split('.')),
endField: JSON.stringify(endRef.split('.')),
xField: JSON.stringify(xRef.split('.')),
yField: JSON.stringify(yRef.split('.')),
};
}

export type {WaterfallChartSettings};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Tag} from '@malloydata/malloy-tag';
import type {WaterfallChartSettings} from './waterfall-chart-settings';

export function waterfallChartSettingsToTag(
settings: WaterfallChartSettings
): Tag {
let tag = new Tag({properties: {viz: {eq: 'waterfall'}}});
if (settings.startField) tag = tag.set(['viz', 'start'], settings.startField);
if (settings.endField) tag = tag.set(['viz', 'end'], settings.endField);
if (settings.xField) tag = tag.set(['viz', 'x'], settings.xField);
if (settings.yField) tag = tag.set(['viz', 'y'], settings.yField);
return tag;
}
Loading