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
2 changes: 1 addition & 1 deletion backend/tests/test_sql_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sqlfluff.core import FluffConfig, Linter

from api.models import FilterSource, RangeFilter
from sql_render import (
from app_utils.sql_render import (
compile_filters,
compile_where,
filter_clauses,
Expand Down
1 change: 0 additions & 1 deletion frontend/src/app/data-viewer/MetricsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export function MetricsPanel({ metrics }: { metrics: Record<string, number> }) {
<Grid>
{METRICS.map(({ label, field, prefix }) => (
<Grid.Col key={field} span={{ base: 6, md: 2.4 }}>
s
<Text style={metricValueStyle}>
{prefix}
{formatNumber(metrics[field])}
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/app/data-viewer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
useApplyFilters,
buildFilters,
} from '@/components/FilterUI/useApplyFilters';
import { DataRow, ChartPayload, ChartMetadata } from '@/types/cachedCharts';

import { motion } from 'motion/react';
import classes from './Tabs.module.css';
Expand Down Expand Up @@ -583,9 +584,9 @@ export default function DataViewerPage() {
charts={visibleItems}
action="add"
userInterests={interests}
defIds={visibleItems.map(
(c) => c.chartParams?.defId as string | undefined,
)}
defIds={visibleItems
.map((c) => c.chartParams?.defId)
.filter((id): id is string => id !== undefined)}
view="gallery"
/>
</Box>
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/app/resources/data-sources/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,21 @@ import {
import { ArrowLeftIcon, MagnifyingGlassIcon } from '@phosphor-icons/react';
import * as motion from 'motion/react-client';
import {
DATA_SOURCES,
Category,
Dataset,
DATA_SOURCES,
Variable,
} from './source_description';

const COLOR = {
spruce: '#1B3A2F',
spruceDeep: '#122820',
slate: '#40525A',
birch: '#F6F5EF',
amberSoft: '#E7B563',
line: 'rgba(27, 58, 47, 0.14)',
};

import { COLORS, FONTS } from '@/app/theme';

// -----------------------------------------------------------------------------
Expand Down
13 changes: 6 additions & 7 deletions frontend/src/app/working-report/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ import {
XIcon,
} from '@phosphor-icons/react';
import { ChartDef, chartDefs } from '@/components/Charts/configs/ChartDefs';
import { ChartItem, ChartMetadata, DataRow } from '@/types/cachedCharts';
import {
ChartItem,
ChartMetadata,
DataRow,
ChartPayload,
} from '@/types/cachedCharts';
import { COLORS, FONTS } from '@/app/theme';

// one chart's backend payload, keyed by chart def id in state below
type ChartPayload = {
data: DataRow[];
metadata?: ChartMetadata;
tableData?: DataRow[];
};
import { createChartItem, createTableItem } from '@/utils/itemFactory';
import { useItems } from '@/components/ItemsProvider';
import { PdfModeContext } from '@/contexts/PdfModeContext';
Expand Down
239 changes: 159 additions & 80 deletions frontend/src/components/Charts/Bar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useEffect } from 'react';

// recharts
import {
Expand Down Expand Up @@ -214,24 +214,54 @@ const CompareDiffPerXBarChartSVG = ({

const CompareDiffPerXBarChart = ({
chart,
view,
onPlotData,
}: {
chart: CompareDiffChartItem;
view?: 'gallery' | 'report';
onPlotData?: (rows: DataRow[]) => void;
}) => {
const isPdfMode = usePdfMode();
if (isPdfMode) return <CompareDiffPerXBarChartSVG chart={chart} />;

const includeCategories = chart.chartParams?.includeCategories;
const filteredData = includeCategories
? chart.data.filter((entry: any) =>
includeCategories.includes(entry[chart.xField]),
)
: chart.data;

const filteredCompareData = includeCategories
? chart.compareData.filter((entry: any) =>
includeCategories.includes(entry[chart.xField]),
)
: chart.compareData;

// Filter primary dataset based on categories if specified
const filteredData = useMemo(() => {
return includeCategories
? chart.data.filter((entry: any) =>
includeCategories.includes(entry[chart.xField]),
)
: chart.data;
}, [chart.data, includeCategories, chart.xField]);

// Filter comparison dataset based on categories if specified
const filteredCompareData = useMemo(() => {
return includeCategories && chart.compareData
? chart.compareData.filter((entry: any) =>
includeCategories.includes(entry[chart.xField]),
)
: (chart.compareData ?? []);
}, [chart.compareData, includeCategories, chart.xField]);

// Derive exact plottable rows and report back to ChartCard for TableView
const plotData = useMemo(() => {
return filteredData.map((entry: any, i: number) => {
const cmpEntry = filteredCompareData[i];
return {
[chart.xField]: entry[chart.xField],
[chart.yField]: entry[chart.yField],
...(cmpEntry
? { [`${chart.yField} (cmp)`]: cmpEntry[chart.yField] }
: {}),
};
});
}, [filteredData, filteredCompareData, chart.xField, chart.yField]);

useEffect(() => {
onPlotData?.(plotData);
}, [plotData, onPlotData]);

if (isPdfMode) return <CompareDiffPerXBarChartSVG chart={chart} />;

const labels = filteredData.map((entry: any) => entry[chart.xField]);

Expand All @@ -252,6 +282,7 @@ const CompareDiffPerXBarChart = ({
chart.yField,
`${chart.yField} (compare)`,
];

const data = {
labels,
datasets: [
Expand Down Expand Up @@ -356,35 +387,40 @@ const cleanUseType = (useType: string) => useType.replace(/_/g, ' ');

const ZoningAllowanceStackedBarChart = ({
chart,
onPlotData,
}: {
chart: CompareDiffChartItem;
onPlotData?: (rows: DataRow[]) => void;
}) => {
// const isPdfMode = usePdfMode();
// if (isPdfMode) return <ZoningAllowanceStackedBarChartSVG chart={chart} />;

const mainRows = ((chart.data || []) as AllowanceRow[]).filter((r) =>
INCLUDED_USE_TYPES.has(r.use_type),
const mainRows = useMemo(
() =>
((chart.data || []) as AllowanceRow[]).filter((r) =>
INCLUDED_USE_TYPES.has(r.use_type),
),
[chart.data],
);

const compareRows = ((chart.compareData || []) as AllowanceRow[]).filter(
(r) => INCLUDED_USE_TYPES.has(r.use_type),
const compareRows = useMemo(
() =>
((chart.compareData || []) as AllowanceRow[]).filter((r) =>
INCLUDED_USE_TYPES.has(r.use_type),
),
[chart.compareData],
);

type PivotRow = { use_type: string } & Record<string, number | string>;
const pivot = (rows: AllowanceRow[]): Record<string, PivotRow> => {
const map: Record<string, PivotRow> = {};

for (const r of rows) {
const useType = cleanUseType(r.use_type);
const group = groupVal(r.val);

if (!map[useType]) {
map[useType] = { use_type: useType };
}
if (!map[useType]) map[useType] = { use_type: useType };
map[useType][group] =
((map[useType][group] as number) || 0) + (Number(r.Acres) || 0);
}

return map;
};

Expand All @@ -401,6 +437,32 @@ const ZoningAllowanceStackedBarChart = ({

const stackKeys = VAL_ORDER;

const plotData = useMemo(() => {
return labels.map((useType) => {
const row: DataRow = { 'Residential Type': useType };

stackKeys.forEach((key) => {
row[key] = main[useType]?.[key] ?? 0;
});

if (Object.keys(compare).length > 0) {
stackKeys.forEach((key) => {
row[`${key} (cmp)`] = compare[useType]?.[key] ?? 0;
});
}

return row;
});
}, [labels, main, compare, stackKeys]);

// 3. Serialize plotData in the dependency array to break reference-equality loops!
const serializedPlotData = JSON.stringify(plotData);

useEffect(() => {
onPlotData?.(plotData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [serializedPlotData, onPlotData]);

const colorForGroup = (group: string) => VAL_GROUP_COLORS[group] ?? '#999999';

const mutedColor = (hex: string) => {
Expand Down Expand Up @@ -429,68 +491,85 @@ const ZoningAllowanceStackedBarChart = ({
[stackKeys, labels, main, compare, colorForGroup],
);

const data = { labels, datasets };
const data = useMemo(
() => ({
labels,
datasets,
}),
[labels, datasets],
);

const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: {
generateLabels: (chart: any) =>
stackKeys.map((key) => {
const mainIndex = chart.data.datasets.findIndex(
(ds: any) => ds.label === `${key}`,
);
const compareIndex = chart.data.datasets.findIndex(
(ds: any) => ds.label === `${key}`,
);

const mainMeta = chart.getDatasetMeta(mainIndex);
const compareMeta = chart.getDatasetMeta(compareIndex);

const hidden =
(mainMeta.hidden ?? chart.data.datasets[mainIndex].hidden) &&
(compareMeta.hidden ??
chart.data.datasets[compareIndex].hidden);

return {
text: key,
fillStyle: colorForGroup(key),
strokeStyle: colorForGroup(key),
lineWidth: 1,
hidden,
datasetIndex: mainIndex,
};
}),
},
onClick: (_e: any, legendItem: any, legend: any) => {
const chart = legend.chart;
const key = legendItem.text;

chart.data.datasets.forEach((ds: any, idx: number) => {
if (ds.label?.startsWith(key)) {
const meta = chart.getDatasetMeta(idx);
meta.hidden = !(meta.hidden ?? false);
}
});

chart.update();
const options = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
animation: { duration: 250 },
transitions: {
active: {
animation: {
duration: 0, // Disables jerky animation when hovering over bars
},
},
},
tooltip: {
callbacks: {
label: (ctx: any) =>
`${ctx.dataset.label}: ${ctx.raw?.toLocaleString?.() ?? ctx.raw}`,
plugins: {
legend: {
display: true,
labels: {
generateLabels: (chart: any) =>
stackKeys.map((key) => {
const mainIndex = chart.data.datasets.findIndex(
(ds: any) => ds.label === `${key}`,
);
const compareIndex = chart.data.datasets.findIndex(
(ds: any) => ds.label === `${key}`,
);

const mainMeta = chart.getDatasetMeta(mainIndex);
const compareMeta = chart.getDatasetMeta(compareIndex);

const hidden =
(mainMeta.hidden ?? chart.data.datasets[mainIndex].hidden) &&
(compareMeta.hidden ??
chart.data.datasets[compareIndex].hidden);

return {
text: key,
fillStyle: colorForGroup(key),
strokeStyle: colorForGroup(key),
lineWidth: 1,
hidden,
datasetIndex: mainIndex,
};
}),
},
onClick: (_e: any, legendItem: any, legend: any) => {
const chart = legend.chart;
const key = legendItem.text;

chart.data.datasets.forEach((ds: any, idx: number) => {
if (ds.label?.startsWith(key)) {
const meta = chart.getDatasetMeta(idx);
meta.hidden = !(meta.hidden ?? false);
}
});

chart.update();
},
},
tooltip: {
callbacks: {
label: (ctx: any) =>
`${ctx.dataset.label}: ${ctx.raw?.toLocaleString?.() ?? ctx.raw}`,
},
},
},
},
scales: {
x: { stacked: true },
y: { stacked: true },
},
};
scales: {
x: { stacked: true },
y: { stacked: true },
},
}),
[stackKeys],
);

return <BarJS data={data} options={options} />;
};
Expand Down
Loading
Loading