Skip to content

Commit

Permalink
feat(react,vue): Add filter support to CategoryWidget
Browse files Browse the repository at this point in the history
  • Loading branch information
donmccurdy committed Sep 3, 2024
1 parent 7ab65e1 commit b12ccb0
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 35 deletions.
43 changes: 43 additions & 0 deletions packages/create-common/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
--card-background: #ffffff;
--card-background-surface: #f8f9f9;
--text-primary: #2c3032;
--text-link: rgb(3, 111, 226);
--border-color: #e0e0e0;

--app-bar-height: 48px;
Expand Down Expand Up @@ -146,6 +147,17 @@ figure {
margin-bottom: 1rem;
}

a {
color: var(--text-link);
}

button {
color: var(--text-link);
font-size: 13px;
line-height: 20px;
letter-spacing: 0.25px;
}

/******************************************************************************
* PAGE LAYOUT
*/
Expand Down Expand Up @@ -297,6 +309,8 @@ ul.category-list {

li.category-item {
margin-bottom: var(--padding);
cursor: pointer;
user-select: none;
}

li.category-item:last-child {
Expand All @@ -318,6 +332,24 @@ li.category-item:last-child {
.category-item-meter {
display: block;
width: 100%;
transition: opacity 0.2s ease;
}

ul.category-list:has(.selected) li.category-item {
filter: saturate(0);
}

ul.category-list:has(.selected) li.category-item.selected {
filter: saturate(1);
}

li.category-item:hover {
filter: saturate(2.5);
}

ul.category-list + button {
float: right;
margin-top: 1em;
}

/******************************************************************************
Expand Down Expand Up @@ -474,3 +506,14 @@ input[type='checkbox'] {
vertical-align: middle;
margin-right: 1em;
}

button {
background: none;
border: none;
cursor: pointer;
padding: 0;
}

button:hover {
text-decoration: underline;
}
2 changes: 1 addition & 1 deletion packages/create-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"dependencies": {
"@auth0/auth0-react": "^2.2.4",
"@carto/api-client": "^0.0.1-2",
"@carto/api-client": "^0.0.44",
"@carto/create-common": "^0.0.7",
"@deck.gl/aggregation-layers": "^9.0.24",
"@deck.gl/carto": "^9.0.24",
Expand Down
8 changes: 6 additions & 2 deletions packages/create-react/src/components/views/Default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Map } from 'react-map-gl/maplibre';
import DeckGL from '@deck.gl/react';
import { AccessorFunction, Color, MapView, MapViewState } from '@deck.gl/core';
import { colorCategories, VectorTileLayer } from '@deck.gl/carto';
import { vectorQuerySource } from '@carto/api-client';
import { Filter, vectorQuerySource } from '@carto/api-client';
import { Legend } from '../common/Legend';
import { Card } from '../common/Card';
import { Layers } from '../common/Layers';
Expand All @@ -31,6 +31,7 @@ const RADIO_COLORS: AccessorFunction<unknown, Color> = colorCategories({
});

export default function Default() {
const [filters, setFilters] = useState({} as Record<string, Filter>);
const [attributionHTML, setAttributionHTML] = useState('');
const [viewState, setViewState] = useDebouncedState(INITIAL_VIEW_STATE, 200);

Expand All @@ -44,8 +45,9 @@ export default function Default() {
connectionName: 'carto_dw',
sqlQuery:
'SELECT * FROM `carto-demo-data.demo_tables.cell_towers_worldwide`',
filters,
});
}, []);
}, [filters]);

/****************************************************************************
* Layers (https://deck.gl/docs/api-reference/carto/overview#carto-layers)
Expand Down Expand Up @@ -106,6 +108,8 @@ export default function Default() {
column={'radio'}
operation={'count'}
viewState={viewState}
filters={filters}
onFiltersChange={setFilters}
/>
</Card>
</aside>
Expand Down
82 changes: 61 additions & 21 deletions packages/create-react/src/components/widgets/CategoryWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,49 @@ import { MapViewState } from '@deck.gl/core';
import {
AggregationType,
CategoryResponse,
Filter,
FilterType,
WidgetSource,
removeFilter,
getFilter,
hasFilter,
} from '@carto/api-client';
import { useEffect, useMemo, useState } from 'react';
import {
createSpatialFilter,
WidgetStatus,
numberFormatter,
} from '../../utils';
import { useToggleFilter } from '../../hooks';

const { IN } = FilterType;

export interface CategoryWidgetProps {
data: Promise<{ widgetSource: WidgetSource }>;
column: string;
operation?: AggregationType;
viewState?: MapViewState;
filters?: Record<string, Filter>;
onFiltersChange?: (filters: Record<string, Filter>) => void;
}

export function CategoryWidget({
data,
column,
operation,
viewState,
filters,
onFiltersChange,
}: CategoryWidgetProps) {
const [owner] = useState<string>(crypto.randomUUID());
const [status, setStatus] = useState<WidgetStatus>('loading');
const [response, setResponse] = useState<CategoryResponse>([]);
const toggleFilter = useToggleFilter({
column,
owner,
filters,
onChange: onFiltersChange,
});

useEffect(() => {
const abortController = new AbortController();
Expand All @@ -37,6 +56,7 @@ export function CategoryWidget({
operation,
spatialFilter: viewState && createSpatialFilter(viewState),
abortController,
filterOwner: owner,
}),
)
.then((response) => {
Expand All @@ -52,7 +72,7 @@ export function CategoryWidget({
setStatus('loading');

return () => abortController.abort();
}, [data, column, operation, viewState]);
}, [data, column, operation, viewState, owner]);

const [min, max] = useMemo(() => {
let min = Infinity;
Expand All @@ -64,6 +84,11 @@ export function CategoryWidget({
return [min, max];
}, [response]);

const selectedCategories = useMemo(() => {
const filter = filters && getFilter(filters, { column, owner, type: IN });
return new Set((filter?.values || []) as string[]);
}, [filters, column, owner]);

if (status === 'loading') {
return <span className="title">...</span>;
}
Expand All @@ -76,26 +101,41 @@ export function CategoryWidget({
return <span className="title">No data</span>;
}

function onClearFilters() {
if (filters && onFiltersChange) {
onFiltersChange({ ...removeFilter(filters, { column, owner }) });
}
}

return (
<ul className="category-list">
{response.map(({ name, value }) => (
<li key={name} className="category-item">
<div className="category-item-row">
<span className="category-item-label body1 strong">{name}</span>
<data className="category-item-value body1" value={value}>
{numberFormatter.format(value)}
</data>
</div>
<div className="category-item-row">
<meter
className="category-item-meter"
min={min}
max={max}
value={value}
></meter>
</div>
</li>
))}
</ul>
<>
<ul className="category-list">
{response.map(({ name, value }) => (
<li
key={name}
className={`category-item ${selectedCategories.has(name) ? 'selected' : ''}`}
onClick={() => toggleFilter(name)}
>
<div className="category-item-row">
<span className="category-item-label body1 strong">{name}</span>
<data className="category-item-value body1" value={value}>
{numberFormatter.format(value)}
</data>
</div>
<div className="category-item-row">
<meter
className="category-item-meter"
min={min}
max={max}
value={value}
></meter>
</div>
</li>
))}
</ul>
{filters && onFiltersChange && hasFilter(filters, { column }) && (
<button onClick={onClearFilters}>Clear</button>
)}
</>
);
}
54 changes: 54 additions & 0 deletions packages/create-react/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import {
addFilter,
Filter,
FilterType,
getFilter,
removeFilter,
} from '@carto/api-client';
import { useState, useEffect, useRef } from 'react';

type Timeout = ReturnType<typeof setTimeout>;
Expand Down Expand Up @@ -88,3 +95,50 @@ export function useDebouncedEffect(
[delay, ...deps],
);
}

export type ToggleFilterProps = {
column: string;
owner: string;
filters?: Record<string, Filter>;
onChange?: (filters: Record<string, Filter>) => void;
};

export function useToggleFilter({
column,
owner,
filters,
onChange,
}: ToggleFilterProps): (category: string) => void {
const { IN } = FilterType;

return function onToggleFilter(category: string): void {
if (!filters || !onChange) return;

const filter = getFilter(filters, { column, type: IN, owner });

let values: string[];

if (!filter) {
values = [category];
} else if ((filter.values as string[]).includes(category)) {
values = (filter.values as string[]).filter(
(v: string) => v !== category,
);
} else {
values = [...(filter.values as string[]), category];
}

if (values.length > 0) {
filters = addFilter(filters, {
column,
type: IN,
owner,
values: values,
});
} else {
filters = removeFilter(filters, { column, owner });
}

onChange({ ...filters });
};
}
2 changes: 1 addition & 1 deletion packages/create-vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"dependencies": {
"@auth0/auth0-vue": "^2.3.3",
"@carto/api-client": "^0.0.1-2",
"@carto/api-client": "^0.0.44",
"@carto/create-common": "workspace:^",
"@deck.gl/aggregation-layers": "^9.0.24",
"@deck.gl/carto": "^9.0.24",
Expand Down
16 changes: 14 additions & 2 deletions packages/create-vue/src/components/views/Default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { refDebounced } from '@vueuse/core';
import { Map } from 'maplibre-gl';
import { AccessorFunction, Deck, MapViewState, Color } from '@deck.gl/core';
import { colorCategories, VectorTileLayer } from '@deck.gl/carto';
import { vectorQuerySource } from '@carto/api-client';
import { vectorQuerySource, Filter } from '@carto/api-client';
import Layers from '../common/Layers.vue';
import Legend from '../common/Legend.vue';
import Card from '../common/Card.vue';
Expand Down Expand Up @@ -39,12 +39,19 @@ const RADIO_COLORS: AccessorFunction<unknown, Color> = colorCategories({
* Sources (https://deck.gl/docs/api-reference/carto/data-sources)
*/
const filters = ref<Record<string, Filter>>({});
const onFiltersChange = (_filters: Record<string, Filter>) => {
filters.value = _filters;
};
const data = computed(() =>
vectorQuerySource({
accessToken: import.meta.env.VITE_CARTO_ACCESS_TOKEN,
connectionName: 'carto_dw',
sqlQuery:
'SELECT * FROM `carto-demo-data.demo_tables.cell_towers_worldwide`',
filters: filters.value,
}),
);
Expand Down Expand Up @@ -90,7 +97,10 @@ watchEffect(() => {
});
watchEffect(() => {
data.value?.then(({ attribution }) => (attributionHTML.value = attribution));
data.value?.then(
({ attribution }: { attribution: string }) =>
(attributionHTML.value = attribution),
);
});
onMounted(() => {
Expand Down Expand Up @@ -146,6 +156,8 @@ onUnmounted(() => {
:column="'radio'"
:operation="'count'"
:viewState="viewStateDebounced as MapViewState"
:filters
:onFiltersChange
/>
</Card>
</aside>
Expand Down
Loading

0 comments on commit b12ccb0

Please sign in to comment.