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 @@ -55,20 +55,3 @@
transform: translateY(var(--actions-height));
}
}

.content {
display: flex;
flex-shrink: 0;
flex-direction: column;
padding: 28px;
}

.footer {
flex-shrink: 0;
padding: 0 28px 18px;

[data-loading="true"] & {
visibility: hidden;
pointer-events: none;
}
}
17 changes: 7 additions & 10 deletions dataweaver/apps/web/src/components/elements/card/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,21 @@ interface CardAction {

/** @default false */
isDisabled?: boolean;

/** @default false */
isActive?: boolean;
}

interface CardProps extends CardState {
actions: CardAction[];
content: ReactNode;

/** **Note**: This isn't shown while `isLoading`. */
footer?: ReactNode;
children: ReactNode;
}

export const CardBase = ({
isLoading,
selection,
actions,
content,
footer,
children,
}: CardProps) => {
const getCachedCanScroll = useCachedResizeValues((element: HTMLElement) => {
return element.scrollHeight > element.clientHeight;
Expand All @@ -62,6 +61,7 @@ export const CardBase = ({
variant="flat"
tone="card-action"
aria-label={action.label}
aria-pressed={action.isActive}
// Prevent tldraw from triggering canvas gestures (e.g. dragging)
onPointerDown={(event) => event.stopPropagation()}
onClick={action.onClick}
Expand All @@ -80,10 +80,7 @@ export const CardBase = ({
}
}}
>
<div className={s.content}>{content}</div>
<div className={s.footer} inert={isLoading}>
{footer}
</div>
{children}
</div>
</article>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
.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 {
@include type-title;
}
.title {
@include type-title;
}

.description {
@include type-body-medium;
.description {
@include type-body-medium;
}
}
151 changes: 117 additions & 34 deletions dataweaver/apps/web/src/components/elements/card/chart/chart.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
'use client';

import { AnimatePresence } from 'motion/react';
import { 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 { Skeleton } from '~/components/elements/skeleton';
import { IconLineGraph } from '~/components/primitives/icons/line_graph';
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 { useExportActions } from '~/components/scopes/atlas/export_provider';
import { useQueryActions } from '~/components/scopes/atlas/query_provider';
import s from './chart.module.scss';
import { ConditionalTabs } from './conditional_tabs';
import { DataChartLine } from './data_chart_line';
import { DataTable } from './data_table';
import { type ChartStyle, MenuChartOptions } from './menu_chart_options';

export interface ChartDatum {
year: number;
Expand All @@ -18,54 +30,125 @@ export interface ChartDatum {
const CHART_WIDTH = 356;
const CHART_HEIGHT = 200;

export interface CardChartProps extends Pick<CardState, 'isLoading'> {
export interface CardChartProps extends CardState {
id: TLShapeId;
title?: string;
description?: string;
followUp?: string;

// TODO: Atm data rendered within the card is very specific to the emissions
// dataset. Let's make it more generic once we have real data to work with
data?: ChartDatum[];
}

export const CardChart = ({
id,
isLoading,
data,
selection,
title,
description,
followUp,
data,
}: CardChartProps) => {
const editor = useEditor();

const { open: openExport } = useExportActions();
const { runPrompt } = useQueryActions();

// TODO: Support the different chart styles (for now we always show bar chart)
const [selectedStyle, setSelectedStyle] =
useState<ChartStyle>('bar-vertical');
const [isStyleMenuOpen, setIsStyleMenuOpen] = useState(false);

return (
<>
{(title || description) && (
<div className={s['header-container']}>
{title && <h2 className={s.title}>{title}</h2>}
{description && <p className={s.description}>{description}</p>}
</div>
)}
<Card.Base
isLoading={isLoading}
selection={selection}
actions={[
{
icon: IconBarChartOutlined,
label: 'Chart options',
isDisabled: isLoading,
isActive: isStyleMenuOpen,
onClick: () => setIsStyleMenuOpen((isOpen) => !isOpen),
},
{
icon: IconExport,
label: 'Export',
isDisabled: isLoading,
onClick: () => {
editor.select(id);
openExport();
},
},
{
icon: IconDelete,
label: 'Delete',
onClick: () => editor.deleteShapes([id]),
},
]}
>
<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>
)}

{isLoading || !data ? (
<Skeleton />
) : (
<ConditionalTabs
tabs={[
{
icon: IconLineGraph,
label: 'Chart',
children: (
<DataChartLine
data={data}
width={CHART_WIDTH}
height={CHART_HEIGHT}
/>
),
},
{
icon: IconTable,
label: 'Table',
children: <DataTable data={data} />,
},
]}
/>
{isLoading || !data ? (
<Skeleton />
) : (
<ConditionalTabs
tabs={[
{
icon: IconLineGraphSingle,
label: 'Chart',
children: (
<DataChartLine
data={data}
width={CHART_WIDTH}
height={CHART_HEIGHT}
/>
),
},
{
icon: IconTable,
label: 'Table',
children: <DataTable data={data} />,
},
]}
/>
)}
</div>

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

<AnimatePresence>
{isStyleMenuOpen && (
<MenuChartOptions
value={selectedStyle}
onConfirmSelectionChange={(newStyle) => {
setSelectedStyle(newStyle);
setIsStyleMenuOpen(false);
}}
onClose={() => setIsStyleMenuOpen(false)}
/>
)}
</AnimatePresence>
</Card.Base>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
.container {
position: absolute;
inset: 0;
z-index: $z-index-above-content;
display: grid;
background: rgb(var(--color-card-overlay-backdrop) / 30%);
}

.backdrop {
z-index: $z-index-behind-content;
grid-area: 1 / 1;
height: 100%;
background: rgb(var(--color-card-accent-subtle) / 30%);
}

.content-container {
display: flex;
flex-direction: column;
grid-area: 1 / 1;
place-self: center center;
width: 300px;
max-width: 100%;
background: rgb(var(--color-card-surface));
border-radius: var(--corner-size);
box-shadow: var(--shadow-elevated);
}

.title {
@include type-title;

padding: 16px 16px 10px;
color: rgb(var(--color-card-content));
}

.options-container {
display: flex;
flex-direction: column;
row-gap: 2px;
padding-inline: 12px;

.option-container {
column-gap: 12px;
align-items: center;
height: 40px;
padding-inline: 12px;
border-radius: var(--border-radius-small);
transition: background 0.2s $ease-linear;

@include hover {
background: rgb(var(--color-card-accent-subtle));
}

.option-icon {
flex-shrink: 0;
width: 24px;
height: 24px;
color: rgb(var(--color-card-content));
}

.option-label {
@include type-body-medium;

color: rgb(var(--color-card-content));
}
}
}

.actions-container {
display: flex;
gap: 8px;
justify-content: flex-end;
padding: 32px 18px 20px 0;
}
Loading