Skip to content
Open
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 @@ -38,10 +38,8 @@
display: flex;
flex-direction: column;
grid-area: 1 / 1;
height: fit-content;
max-height: 100%;
overflow-y: auto;
overscroll-behavior: none;
height: 100%;
overflow: clip;

// Dragging the card is reserved for the actions bar, so the content is a
// freely selectable text surface rather than a drag handle (this overrides
Expand Down
33 changes: 7 additions & 26 deletions dataweaver/apps/web/src/components/elements/card/base.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
'use client';

import {
type ComponentPropsWithRef,
type ComponentType,
type ReactNode,
useRef,
import type {
ComponentPropsWithRef,
ComponentType,
ReactNode,
RefObject,
} from 'react';
import type { TLShapeId } from 'tldraw';
import { Button } from '~/components/elements/button';
import { CARD_VARIANT_MAX } from '~/components/scopes/atlas/config';
import type { CardVariant } from '~/components/scopes/atlas/helpers';
import { useCachedResizeValues } from '~/hooks/use_cached_resize_values';
import s from './base.module.scss';
import { useCardAutoHeight } from './use_card_auto_height';
import { useCardClearTextSelection } from './use_card_clear_text_selection';
import { useCardDragHandle } from './use_card_drag_handle';
import { useCardTextClipboard } from './use_card_text_clipboard';
Expand Down Expand Up @@ -45,33 +41,25 @@ interface CardAction {

interface CardProps extends CardState {
id: TLShapeId;
variant: CardVariant;
childrenContainerRef: RefObject<HTMLDivElement | null>;
actions: CardAction[];
children: ReactNode;
}

export const CardBase = ({
id,
variant,
childrenContainerRef,
isLoading,
selection,
actions,
children,
}: CardProps) => {
const childrenContainerRef = useRef<HTMLDivElement>(null);

useCardAutoHeight(childrenContainerRef, id, CARD_VARIANT_MAX[variant].h);

useCardClearTextSelection(childrenContainerRef, id);

useCardTextClipboard(childrenContainerRef);

const startDragging = useCardDragHandle(id);

const getCachedCanScroll = useCachedResizeValues((element: HTMLElement) => {
return element.scrollHeight > element.clientHeight;
});

return (
<article
className={s.container}
Expand Down Expand Up @@ -99,13 +87,6 @@ export const CardBase = ({
<div
ref={childrenContainerRef}
className={s['children-container']}
// TLDraw captures all wheel events; this ensures that cards can be
// scrolled when children here can scroll
onWheelCapture={(event) => {
if (getCachedCanScroll(false, event.currentTarget)) {
event.stopPropagation();
}
}}
// Once the card is the single selection, reserve dragging for the
// actions bar: stop the canvas from starting a gesture so the content
// stays selectable/highlightable. While unselected or multi-selected we
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
.container {
display: flex;
flex-shrink: 0;
flex-direction: column;
padding: 28px;
}

.header-container {
display: flex;
flex-direction: column;
row-gap: 8px;
margin-bottom: 16px;
color: rgb(var(--color-card-content));

.title {
Expand Down
76 changes: 43 additions & 33 deletions dataweaver/apps/web/src/components/elements/card/chart/chart.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
'use client';

import { AnimatePresence } from 'motion/react';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { type TLShapeId, useEditor } from 'tldraw';
import { Button } from '~/components/elements/button';
import { Card } from '~/components/elements/card';

import type { CardState } from '~/components/elements/card/base';
import { useCardAutoHeight } from '~/components/elements/card/use_card_auto_height';
import { Skeleton } from '~/components/elements/skeleton';
import { IconBarChartOutlined } from '~/components/primitives/icons/bar_chart_outlined';
import { IconDelete } from '~/components/primitives/icons/delete';
import { IconExport } from '~/components/primitives/icons/export';
import { IconLineGraphSingle } from '~/components/primitives/icons/line_graph_single';
import { IconPencil } from '~/components/primitives/icons/pencil';
import { IconTable } from '~/components/primitives/icons/table';
import { CARD_VARIANT_SIZE_DEFAULT } from '~/components/scopes/atlas/config';
import { useExportActions } from '~/components/scopes/atlas/export_provider';
import { useQueryActions } from '~/components/scopes/atlas/query_provider';
import type { FacetInfo } from '~/server/types';
Expand All @@ -29,8 +31,7 @@ export interface ChartDatum {
value: number;
}

// TODO: Get dynamically instead of hard coding here
const CHART_WIDTH = 356;
/** Width is responsive (fills the resizable card); height stays fixed. */
const CHART_HEIGHT = 200;

export interface CardChartProps extends CardState {
Expand Down Expand Up @@ -60,6 +61,16 @@ export const CardChart = ({
const { open: openExport } = useExportActions();
const { runPrompt } = useQueryActions();

const containerRef = useRef<HTMLDivElement>(null);
const contentContainerRef = useRef<HTMLDivElement>(null);

useCardAutoHeight(
id,
containerRef,
contentContainerRef,
CARD_VARIANT_SIZE_DEFAULT.chart.h,
);

// TODO: Support the different chart styles (for now we always show bar chart)
const [selectedStyle, setSelectedStyle] =
useState<ChartStyle>('bar-vertical');
Expand All @@ -75,7 +86,7 @@ export const CardChart = ({
return (
<Card.Base
id={id}
variant="chart"
childrenContainerRef={containerRef}
isLoading={isLoading}
selection={selection}
actions={[
Expand All @@ -102,14 +113,17 @@ export const CardChart = ({
},
]}
>
<div className={s.container}>
{(title || description) && (
<div className={s['header-container']}>
{title && <h2 className={s.title}>{title}</h2>}
{description && <p className={s.description}>{description}</p>}
</div>
)}

<Card.Content
contentContainerRef={contentContainerRef}
title={
(title || description) && (
<div className={s['header-container']}>
{title && <h2 className={s.title}>{title}</h2>}
{description && <p className={s.description}>{description}</p>}
</div>
)
}
>
{isLoading || !chartData ? (
<Skeleton />
) : (
Expand All @@ -128,11 +142,7 @@ export const CardChart = ({
icon: IconLineGraphSingle,
label: 'Chart',
children: (
<DataChartLine
data={chartData}
width={CHART_WIDTH}
height={CHART_HEIGHT}
/>
<DataChartLine data={chartData} height={CHART_HEIGHT} />
),
},
{
Expand All @@ -144,22 +154,22 @@ export const CardChart = ({
/>
</>
)}
</div>

{relatedQuery && !isLoading && (
<Card.Footer>
<Button
size="small"
variant="flat"
tone="accent-subtle"
icon={IconPencil}
onPointerDown={(event) => event.stopPropagation()}
onClick={() => runPrompt(relatedQuery)}
>
{relatedQuery}
</Button>
</Card.Footer>
)}

{relatedQuery && !isLoading && (
<Card.Footer>
<Button
size="small"
variant="flat"
tone="accent-subtle"
icon={IconPencil}
onPointerDown={(event) => event.stopPropagation()}
onClick={() => runPrompt(relatedQuery)}
>
{relatedQuery}
</Button>
</Card.Footer>
)}
</Card.Content>

<AnimatePresence>
{isStyleMenuOpen && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
'use client';

import { COLORS } from '@package/tokens/ts';
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts';
import {
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
XAxis,
YAxis,
} from 'recharts';
import type { ChartDatum } from './chart';

const LINE_COLOR = `rgb(${COLORS['card-surface-selected']})`;
Expand All @@ -10,41 +17,39 @@ const AXIS_COLOR = `rgb(${COLORS['card-content-muted']})`;

interface ChartProps {
data: ChartDatum[];
width: number;

/** Fixed chart height, in px; width fills the (resizable) card. */
height: number;
}

const compactFormatter = new Intl.NumberFormat('en', { notation: 'compact' });

export const DataChartLine = ({ data, width, height }: ChartProps) => {
export const DataChartLine = ({ data, height }: ChartProps) => {
return (
<LineChart
width={width}
height={height}
data={data}
margin={{ top: 8, right: 8, bottom: 0, left: 0 }}
>
<CartesianGrid stroke={GRID_COLOR} vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tick={{ fontSize: 10, fill: AXIS_COLOR }}
/>
<YAxis
width={36}
tickLine={false}
axisLine={false}
tick={{ fontSize: 10, fill: AXIS_COLOR }}
tickFormatter={(v) => compactFormatter.format(v)}
/>
<Line
type="monotone"
dataKey="value"
stroke={LINE_COLOR}
strokeWidth={2}
dot={false}
/>
</LineChart>
<ResponsiveContainer width="100%" height={height}>
<LineChart data={data} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
<CartesianGrid stroke={GRID_COLOR} vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tick={{ fontSize: 10, fill: AXIS_COLOR }}
/>
<YAxis
width={36}
tickLine={false}
axisLine={false}
tick={{ fontSize: 10, fill: AXIS_COLOR }}
tickFormatter={(v) => compactFormatter.format(v)}
/>
<Line
type="monotone"
dataKey="value"
stroke={LINE_COLOR}
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.container {
display: flex;
flex-grow: 1;
flex-direction: column;
min-height: 0;
}

.header-container {
flex-shrink: 0;
padding: 28px 28px 16px;
border-bottom: 1px solid transparent;

@include prefers-motion {
transition: border-color 0.1s $ease-linear;
}

[data-has-scrolled="true"] & {
border-bottom-color: rgb(var(--color-card-divider));
}
}

.content-outer-container {
flex-grow: 1;
min-height: 0;
overflow-y: auto;
overscroll-behavior: none;
scrollbar-width: thin;
}

.content-inner-container {
display: flex;
flex-direction: column;
padding: 0 28px 28px;
}
Loading