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

feat: full width table #1979

Merged
merged 2 commits into from
Oct 25, 2024
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
10 changes: 7 additions & 3 deletions packages/malloy-render/src/component/table/table-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,17 @@ export function getTableLayout(
};
layout.maxDepth = Math.max(layout.maxDepth, layoutEntry.depth);
const {tag} = field.tagParse();
const columnTag = tag.tag('column');

// Allow overriding size
const textWidth = tag.text('width');
const textWidth = columnTag?.text('width');
const numericWidth = columnTag?.numeric('width');
if (textWidth && NAMED_COLUMN_WIDTHS[textWidth])
layoutEntry.width = NAMED_COLUMN_WIDTHS[textWidth];
else if (tag.numeric('width')) layoutEntry.width = tag.numeric('width')!;
else if (numericWidth) layoutEntry.width = numericWidth;

if (tag.numeric('height')) layoutEntry.height = tag.numeric('height')!;
if (columnTag?.numeric('height'))
layoutEntry.height = columnTag.numeric('height')!;

layout.fields[key] = layoutEntry;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/malloy-render/src/component/table/table.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
);
}

.malloy-table.root.full-width {
width: 100%;
}

.malloy-table:not(.root) {
grid-column: 1 / span var(--table-row-span);
}
Expand Down
77 changes: 44 additions & 33 deletions packages/malloy-render/src/component/table/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const Cell = (props: {
}) => {
const style = () => {
const layout = useTableContext()!.layout;
const columnTag = props.field.tagParse().tag.tag('column');
const width = layout.fieldLayout(props.field).width;
const height = layout.fieldLayout(props.field).height;
const style: JSX.CSSProperties = {};
Expand All @@ -48,9 +49,12 @@ const Cell = (props: {
style.width = `${width}px`;
style['min-width'] = `${width}px`;
style['max-width'] = `${width}px;`;
if (typeof height === 'number') {
style.height = `${height}px;`;
}
}
if (height) {
style.height = `${height}px`;
}
if (columnTag?.text('word_break') === 'break_all') {
style['word-break'] = 'break-all';
}
}
return style;
Expand Down Expand Up @@ -183,7 +187,6 @@ const TableField = (props: {field: Field; row: DataRecord}) => {
style.position = 'sticky';
style.top = `var(--malloy-render--table-header-cumulative-height-${fieldLayout.depth})`;
}
// const fieldKey = metadata.getFieldKey()

const tableGutterLeft = fieldLayout.depth > 0 && isFirstChild(props.field);
const tableGutterRight = fieldLayout.depth > 0 && isLastChild(props.field);
Expand Down Expand Up @@ -225,6 +228,9 @@ const MalloyTableRoot = (_props: {
const props = mergeProps({rowLimit: Infinity}, _props);
const tableCtx = useTableContext()!;
const resultMetadata = useResultContext();
const shouldFillWidth =
tableCtx.root &&
props.data.field.tagParse().tag.tag('table')?.text('size') === 'fill';

const pinnedFields = createMemo(() => {
const fields = Object.entries(tableCtx.layout.fieldHeaderRangeMap)
Expand Down Expand Up @@ -287,6 +293,7 @@ const MalloyTableRoot = (_props: {
const templateColumns = fieldsToSize()
.map(([key]) => {
const maybeSize = tableCtx.store.columnWidths[key];
if (shouldFillWidth) return maybeSize ? maybeSize + 'px' : 'auto';
return `minmax(${
maybeSize ? maybeSize + 'px' : 'auto'
}, max-content)`;
Expand Down Expand Up @@ -412,59 +419,62 @@ const MalloyTableRoot = (_props: {
}, 2000);
};

// We want an initial measurement even if not scrolling
let measureInitial = true;
// Observe column width sizes
function updateColumnWidths() {
const pinnedHeaders = pinnedHeaderRow.querySelectorAll(
'[data-pinned-header]'
);
const updates: [string, number][] = [];
pinnedHeaders.forEach(node => {
const key = node.getAttribute('data-pinned-header')!;
const value = node.clientWidth;
const currWidth = tableCtx.store.columnWidths[key];
if (typeof currWidth === 'undefined' || value > currWidth)
updates.push([key, value]);
});
if (updates.length > 0) {
tableCtx.setStore(
'columnWidths',
produce((widths: Record<string, number>) => {
updates.forEach(([key, value]) => (widths[key] = value));
})
);
}
}

// Observe column width sizes and save them as they expand on scroll. Don't let them shrink as its jarring.
onMount(() => {
if (pinnedHeaderRow) {
const resizeObserver = new ResizeObserver(() => {
// select all nodes with data-pinned-header attribute
if (isScrolling || measureInitial) {
const pinnedHeaders = pinnedHeaderRow.querySelectorAll(
'[data-pinned-header]'
);
const updates: [string, number][] = [];
pinnedHeaders.forEach(node => {
const key = node.getAttribute('data-pinned-header')!;
const value = node.clientWidth;
const currWidth = tableCtx.store.columnWidths[key];
if (typeof currWidth === 'undefined' || value > currWidth)
updates.push([key, value]);
});
if (updates.length > 0) {
tableCtx.setStore(
'columnWidths',
produce((widths: Record<string, number>) => {
updates.forEach(([key, value]) => (widths[key] = value));
})
);
}
// Update measureInitial on next tick so that table sizes don't immediately get cleared by other ResizeObserver
setTimeout(() => (measureInitial = false), 0);
if (isScrolling) {
// Measure while scrolling
updateColumnWidths();
}
});
resizeObserver.observe(pinnedHeaderRow);
// Initial measurement
requestAnimationFrame(() => updateColumnWidths());
}
});

// Observe table width resize
// Clear width cache if table changes size due to something besides scroll position (fetching new data)
// Meant to handle when the table resizes due to less available real estate, like a viewport change
// TODO find a better way to handle this scenario
onMount(() => {
if (tableCtx.root) {
let priorWidth: number | null = null;
const resizeObserver = new ResizeObserver(entries => {
if (!isScrolling && !measureInitial) {
const [entry] = entries;
const [entry] = entries;
// Not scrolling and skip the initial measurement, it's handled by header row observer
if (!isScrolling && priorWidth !== null) {
if (priorWidth !== entry.contentRect.width) {
priorWidth = entry.contentRect.width;
tableCtx.setStore(s => ({
...s,
columnWidths: {},
}));
}
}
priorWidth = entry.contentRect.width;
});
resizeObserver.observe(scrollEl);
}
Expand All @@ -482,6 +492,7 @@ const MalloyTableRoot = (_props: {
}}
classList={{
'root': tableCtx.root,
'full-width': shouldFillWidth,
'pinned': pinned(),
}}
part={tableCtx.root ? 'table-container' : ''}
Expand Down
81 changes: 79 additions & 2 deletions packages/malloy-render/src/stories/tables.stories.malloy
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
source: products is duckdb.table("data/products.parquet") extend {

#(story)
view: products_table is{
view: products_table is {
select: *
limit: 1000
order_by: id
Expand All @@ -11,8 +11,10 @@ source: products is duckdb.table("data/products.parquet") extend {
view: long_column is {
select:
brand,
# width=lg
# column {width=lg}
name
# column {width=200 word_break=break_all }
f is 'asdfasdfasdfasdfasdfasdflkahdfsgoaisdfoijadsfgoiahjosdijgaodfsgijao;sdijfgoaidjsgaodsigjao;dsfgija;odfigjoaisdfj'
}

# bar_chart
Expand Down Expand Up @@ -145,6 +147,81 @@ source: products is duckdb.table("data/products.parquet") extend {
`@2001-02-03 04:05:06.001.year` is @2001-02-03 04:05:06.001.year
limit: 1
}

#(story)
# table.size=fill
view: products_table_full_width is {
select: *
limit: 1000
order_by: id
}

#(story)
# table.size=fill
view: nested_full_width is {
group_by: category
aggregate: avg_retail is retail_price.avg()
nest:
nested_column_1 is {
group_by: brand
aggregate: avg_retail is retail_price.avg()
limit: 10
}
another_nested is {
group_by: department
aggregate: avg_retail is retail_price.avg()
nest:
deeply_nested is {
group_by: `sku`
aggregate: total_cost is cost.sum()
limit: 3
}
limit: 5
}
record is {
nest: nested_record is {
group_by: id
aggregate: total_cost is cost.sum()
limit: 5
}
}
another_nested2 is {
group_by: department
aggregate: avg_retail is retail_price.avg()
nest: deeply_nested is {
group_by: `sku`
aggregate: total_cost is cost.sum()
limit: 3
}
limit: 5
}
}

measure: sales is retail_price.sum()

#(story)
view: perf is {
group_by: brand
aggregate: sales
-- # bar_chartx
nest: a is {
group_by: category
aggregate: sales
limit: 24
},
nest: b is {
group_by: category
aggregate: sales
# bar_chart { size=spark }
nest: c is {
group_by: id
aggregate: sales
limit: 24
}
limit: 5
}
limit: 10
}
}

source: null_test is duckdb.sql("select unnest([1,null,3]) as i") extend {
Expand Down