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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

improves display of small/tiled visualizations #14

Merged
merged 8 commits into from
Feb 6, 2024
220 changes: 107 additions & 113 deletions example/QueryManager.tsx
Original file line number Diff line number Diff line change
@@ -1,113 +1,107 @@
import { ActionIcon, Badge, rem } from "@mantine/core";
import { IconEdit } from "@tabler/icons-react";
import React, { useCallback, useMemo, useState } from "react";
import { QueryEditor } from "./QueryEditor";
import { useStoredState } from "./useStorage";

export const QueryManager = (props: {
currentQuery: string;
onChange: (nextQuery: string) => void;
}) => {
const {currentQuery, onChange} = props;

const [currentEdit, setCurrentEdit] = useState<null | string>(null);
const [queries, setQueries] = useStoredState<string[]>("queries", []);

const createHandler = useCallback(() => {
const newKey = Math.random().toString(16).slice(2, 10);
setQueries([...queries, newKey]);
setCurrentEdit(newKey);
}, [queries]);

const clearHandler = useCallback(() => {
setQueries([]);
}, []);

const closeHandler = useCallback(() => {
if (!currentEdit) return;
setCurrentEdit(null);
onChange(currentEdit);
}, [currentEdit, onChange]);

const queryPickers = useMemo(() => queries.map(item =>
<QueryPicker
key={item}
active={item === currentQuery}
onSelect={() => onChange(item)}
onEdit={() => setCurrentEdit(item)}
label={item}
/>
), [currentQuery, queries]);

return (
<div className="query-manager">
<div style={{
display: "flex",
gap: "0.5rem",
padding: "0.5rem",
}}>
<button onClick={createHandler}>New query</button>
<button onClick={clearHandler}>Clear</button>
{queryPickers}
</div>
<dialog open={currentEdit != null}>
<QueryEditor id={currentEdit || ""} onClose={closeHandler} />
</dialog>
<style>{`
dialog {
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
margin: auto;
box-shadow: 0 0 1em rgba(0, 0, 0, 0.4);
border-width: 1px;
border-radius: 3px;
}
dialog::backdrop {
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(0.2);
}

dialog textarea {
display: block;
width: 50vw;
height: 50vh;
box-sizing: border-box;
}
`}</style>
</div>
);
};

function QueryPicker(props: {
active: boolean;
label: string;
onEdit: () => void;
onSelect: () => void;
}) {
const removeButton = (
<ActionIcon
size="xs"
color="blue"
radius="xl"
variant={props.active ? "filled" : "transparent"}
onClick={props.onEdit}
>
<IconEdit size={rem(16)} />
</ActionIcon>
);

return (
<Badge
variant={props.active ? "filled" : "outline"}
size="lg"
pr={3}
rightSection={removeButton}
onClick={props.onSelect}
>
{props.label}
</Badge>
)
}
import { ActionIcon, Badge, Flex, rem } from "@mantine/core";
import { IconEdit } from "@tabler/icons-react";
import React, { useCallback, useMemo, useState } from "react";
import { QueryEditor } from "./QueryEditor";
import { useStoredState } from "./useStorage";

export const QueryManager = (props: {
currentQuery: string;
onChange: (nextQuery: string) => void;
}) => {
const {currentQuery, onChange} = props;

const [currentEdit, setCurrentEdit] = useState<null | string>(null);
const [queries, setQueries] = useStoredState<string[]>("queries", []);

const createHandler = useCallback(() => {
const newKey = Math.random().toString(16).slice(2, 10);
setQueries([...queries, newKey]);
setCurrentEdit(newKey);
}, [queries]);

const clearHandler = useCallback(() => {
setQueries([]);
}, []);

const closeHandler = useCallback(() => {
if (!currentEdit) return;
setCurrentEdit(null);
onChange(currentEdit);
}, [currentEdit, onChange]);

const queryPickers = useMemo(() => queries.map(item =>
<QueryPicker
key={item}
active={item === currentQuery}
onSelect={() => onChange(item)}
onEdit={() => setCurrentEdit(item)}
label={item}
/>
), [currentQuery, queries]);

return (
<Flex direction="column" w="100%" gap="sm" className="query-manager">
<button onClick={createHandler}>New query</button>
{queryPickers}
<button onClick={clearHandler}>Clear</button>
<dialog open={currentEdit != null}>
<QueryEditor id={currentEdit || ""} onClose={closeHandler} />
</dialog>
<style>{`
dialog {
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
margin: auto;
box-shadow: 0 0 1em rgba(0, 0, 0, 0.4);
border-width: 1px;
border-radius: 3px;
}
dialog::backdrop {
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(0.2);
}

dialog textarea {
display: block;
width: 50vw;
height: 50vh;
box-sizing: border-box;
}
`}</style>
</Flex>
);
};

function QueryPicker(props: {
active: boolean;
label: string;
onEdit: () => void;
onSelect: () => void;
}) {
const removeButton = (
<ActionIcon
size="xs"
color="blue"
radius="xl"
variant={props.active ? "filled" : "transparent"}
onClick={props.onEdit}
>
<IconEdit size={rem(16)} />
</ActionIcon>
);

return (
<Badge
variant={props.active ? "filled" : "outline"}
size="lg"
pr={3}
rightSection={removeButton}
onClick={props.onSelect}
>
{props.label}
</Badge>
)
}
61 changes: 38 additions & 23 deletions example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import {useQuery} from "./useQuery";
import {useQueryParams} from "./useQueryParams";
import {useDisclosure} from "@mantine/hooks";
import {Button, Flex} from "@mantine/core";
import {D3plusContext} from "d3plus-react";

const d3plusConfig = {
colorScalePosition: "bottom"
};

const topojsonArray = [
{
Expand Down Expand Up @@ -80,34 +85,44 @@ function Demo() {
const [result, error] = useQuery(query);

return (
<Flex direction="column">
<Flex gap="sm" align="center" px="sm">
<Flex direction="row" h="100vh">
<Flex direction="column" gap="sm" align="center" p="sm" w={300} miw={300}>
<Button compact uppercase color="indigo" onClick={setDebugger.toggle}>
{Vizwrapper.name}
</Button>
<QueryManager currentQuery={currentQuery} onChange={setCurrentQuery} />
</Flex>
{result && <Vizwrapper
queries={result}
downloadFormats={["svg", "png"]}
defaultLocale="ar"
allowedChartTypes={[
"barchart",
"barchartyear",
"donut",
"geomap",
"histogram",
"lineplot",
"pie",
"stacked",
"treemap"
]}
topojsonConfig={topojsonConfig}
translations={translations}
userConfig={{
locale: "ar-SA"
}}
/>}
<div id="viz-scroller" style={{
overflowY: "scroll",
flex: "1 0 auto",
padding: "0.75rem",
boxSizing: "border-box"
}}>
<D3plusContext.Provider value={d3plusConfig}>
{result && <Vizwrapper
queries={result}
downloadFormats={["svg", "png"]}
defaultLocale="ar"
allowedChartTypes={[
"barchart",
"barchartyear",
"donut",
"geomap",
"histogram",
"lineplot",
"pie",
"stacked",
"treemap"
]}
topojsonConfig={topojsonConfig}
translations={translations}
userConfig={{
locale: "ar-SA",
scrollContainer: "#viz-scroller",
}}
/>}
</D3plusContext.Provider>
</div>
</Flex>
);
}
Expand Down
2 changes: 1 addition & 1 deletion example/useOlapSchema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Client, Cube, TesseractDataSource } from "@datawheel/olap-client";
import { useEffect, useState } from "react";

const ds = new TesseractDataSource("https://api.datasaudi.datawheel.us/tesseract/");
const ds = new TesseractDataSource("/tesseract/");
export const client = new Client(ds);

export function useOlapSchema() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ChartCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const ChartCard = props => {
const ButtonIcon = focused ? IconArrowsMinimize : IconArrowsMaximize;
const buttonText = focused ? translate("action_close") : translate("action_enlarge");
const buttonVariant = focused ? "filled" : "light";
const height = focused ? "calc(100vh - 3rem)" : isSingleChart ? "75vh" : 375;
const height = focused ? "calc(100vh - 3rem)" : isSingleChart ? "75vh" : 300;

return (
<Box className="vb-chart-card" h={height} w="100%" style={{overflow: "hidden"}}>
Expand Down
21 changes: 14 additions & 7 deletions src/components/Vizbuilder.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {useCharts} from "../toolbox/useCharts";
import {TranslationProvider} from "../toolbox/useTranslation";
import {ChartCard} from "./ChartCard";
import NonIdealState from "./NonIdealState";
import {Grid, Modal, Paper} from "@mantine/core";
import {Grid, Modal, Paper, useMantineTheme} from "@mantine/core";
import {useMediaQuery} from "@mantine/hooks";

/** @type {React.FC<VizBldr.VizbuilderProps>} */
export const Vizbuilder = props => {
Expand All @@ -18,24 +19,30 @@ export const Vizbuilder = props => {

const isSingleChart = charts.length === 1;

const theme = useMantineTheme();
const screenIsXL = useMediaQuery(`(min-width: ${theme.breakpoints.xl})`);
const screenIsLG = useMediaQuery(`(min-width: ${theme.breakpoints.lg})`);
const screenIsMD = useMediaQuery(`(min-width: ${theme.breakpoints.md})`);

const content = useMemo(() => {
const measureConfig = normalizeMeasureConfig(props.measureConfig);
const filteredCharts = charts
.filter((d, i) => charts.findIndex(c => c.key === d.key) === i); // removes duplicate charts

const colProps = filteredCharts.length === 1 ? {span: 12}
: filteredCharts.length === 2 ? {lg: 6}
: filteredCharts.length === 3 ? {lg: 6, xl: 4}
: {lg: 6, xl: 4, xxl: 3};
const columns = filteredCharts.length === 1 ? 1
: filteredCharts.length === 2 ? screenIsMD ? 2 : 1
: filteredCharts.length === 3 ? screenIsLG ? 3 : screenIsMD ? 2 : 1
: screenIsXL ? 4 : screenIsLG ? 3 : screenIsMD ? 2 : 1;

if (filteredCharts.length > 0) {
return (
<Grid
columns={columns}
w="100%"
className={cls("vb-charts-wrapper", {unique: filteredCharts.length === 1})}
>
{filteredCharts.map(chart =>
<Grid.Col key={chart.key} {...colProps}>
<Grid.Col key={chart.key} span={1}>
<Paper shadow="xs">
<ChartCard
chart={chart}
Expand All @@ -58,7 +65,7 @@ export const Vizbuilder = props => {

const Notice = props.nonIdealState || NonIdealState;
return <Notice />;
}, [currentChart, currentPeriod, charts, props.showConfidenceInt]);
}, [currentChart, currentPeriod, charts, props.showConfidenceInt, screenIsXL, screenIsLG, screenIsMD]);

const focusContent = useMemo(() => {
const measureConfig = normalizeMeasureConfig(props.measureConfig);
Expand Down