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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spatial index widgets #34

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
608d5f2
feat(source): extend source options in widget constructor with spatia…
juandjara Nov 29, 2024
e317a9b
feat(widgets): read spatialDataColumn and spatialDataType from props …
juandjara Nov 29, 2024
6932b37
feat(widgets): allow passing spatialFiltersResolution to all model calls
juandjara Nov 29, 2024
51622c2
feat(widgets): allow passing spatialFiltersMode to all model calls
juandjara Nov 29, 2024
341e691
fix(source): fix source input types
juandjara Nov 29, 2024
76fde7a
feat(model): add dataResolution prop
juandjara Nov 29, 2024
aceef33
feat(utils): add getSpatialFiltersResolution util
juandjara Nov 29, 2024
c4d7332
fix vector source types
juandjara Dec 2, 2024
7c74ae2
export spatial index utils
juandjara Dec 2, 2024
25ae33d
prettier format
juandjara Dec 2, 2024
d50d904
Spatial index widgets second proposal (#35)
juandjara Dec 4, 2024
cbe99fc
chore: run prettier
juandjara Dec 4, 2024
f85f818
dont export functions from spatial-index util file
juandjara Dec 4, 2024
a186251
pass view state to model call in widgets
juandjara Dec 4, 2024
05ea3bd
use new params in inner model call
juandjara Dec 4, 2024
c684a72
example with h3 widgets
juandjara Dec 4, 2024
6628b01
chore: run prettier
juandjara Dec 4, 2024
046a077
add .env to .gitignore
juandjara Dec 5, 2024
8251cea
simplify viewstate passing
juandjara Dec 5, 2024
c41dbe9
simplify source prop type in getSpatialFiltersResolution
juandjara Dec 5, 2024
2f98cbc
simplify viewState prop type in getSpatialFiltersResolution
juandjara Dec 5, 2024
adb0bf7
chore: remove geoColumn
juandjara Dec 5, 2024
62c890e
chore: run prettier
juandjara Dec 5, 2024
5703d12
Refactor model call query params (#41)
juandjara Dec 10, 2024
cbea41b
run prettier
juandjara Dec 10, 2024
b0af864
query parameters default
juandjara Dec 10, 2024
e3bbde6
chore(release): v0.4.1-alpha.0
juandjara Dec 10, 2024
aeb8df5
Merge branch 'main' of https://github.com/CartoDB/carto-api-client in…
juandjara Dec 10, 2024
125385b
chore(release): v0.4.1-alpha.1
juandjara Dec 10, 2024
1d47f7b
chore(release): v0.4.1-alpha.0
juandjara Dec 10, 2024
26b8948
pass dataResolution from source to widgets
juandjara Dec 12, 2024
898740a
Merge branch 'main' of https://github.com/CartoDB/carto-api-client in…
juandjara Dec 12, 2024
f7220a0
run prettier
juandjara Dec 12, 2024
cb8711f
chore(release): v0.4.2-alpha.0
juandjara Dec 12, 2024
2bb52df
Update src/models/model.ts
juandjara Dec 18, 2024
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
124 changes: 124 additions & 0 deletions examples/05-react-h3/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, {useEffect, useMemo, useState} from 'react';
import {Map} from 'react-map-gl/maplibre';
import DeckGL from '@deck.gl/react';
import { h3TableSource, Filters } from '@carto/api-client';
import {
CategoryWidget,
FormulaWidget,
HistogramWidget,
PieWidget,
ScatterWidget,
TableWidget,
} from '../components/index-react.js';
import {MapView} from '@deck.gl/core';
import {H3TileLayer} from '@deck.gl/carto';
import {FilterEvent} from '../components/types.js';

const MAP_VIEW = new MapView({repeat: true});
const MAP_STYLE =
'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json';
const INITIAL_VIEW_STATE = {latitude: 37.3753636, longitude: -5.9962577, zoom: 6};

export function App(): JSX.Element {
const [viewState, setViewState] = useState({...INITIAL_VIEW_STATE});
const [filters, setFilters] = useState<Filters>({});
const [attributionHTML, setAttributionHTML] = useState('');

// Update sources.
const data = useMemo(() => {
return h3TableSource({
accessToken: import.meta.env.VITE_CARTO_ACCESS_TOKEN,
connectionName: 'carto_dw',
tableName:
'carto-demo-data.demo_tables.derived_spatialfeatures_esp_h3res8_v1_yearly_v2',
filters,
aggregationExp: 'sum(population) as population',
})
}, [filters]);

// Update layers.
const layers = useMemo(() => {
return [
new H3TileLayer({
id: 'retail_stores',
data,
pointRadiusMinPixels: 4,
getFillColor: [200, 0, 80],
}),
];
}, [data]);

useEffect(() => {
data?.then(({attribution}) => setAttributionHTML(attribution));
}, [data]);

return (
<>
<header>
<h1>React</h1>
<a href="../">← Back</a>
</header>
<section id="view">
<DeckGL
layers={layers}
views={MAP_VIEW}
initialViewState={INITIAL_VIEW_STATE}
controller={{dragRotate: false}}
onViewStateChange={({viewState}) => setViewState(viewState)}
>
<Map reuseMaps mapStyle={MAP_STYLE} />
</DeckGL>
</section>
<section id="rail">
<FormulaWidget
data={data}
viewState={viewState}
header="Total population"
operation="count"
></FormulaWidget>

<CategoryWidget
data={data}
viewState={viewState}
header="Urbanity"
operation="count"
column="urbanity"
onfilter={(e) => setFilters((e as FilterEvent).detail.filters)}
></CategoryWidget>
<PieWidget
data={data}
viewState={viewState}
header="Urbanity"
operation="count"
column="urbanity"
onfilter={(e) => setFilters((e as FilterEvent).detail.filters)}
></PieWidget>
<TableWidget
data={data}
viewState={viewState}
header="Pop. Distribution"
columns={['population', 'male', 'female']}
sortBy='population'
></TableWidget>
<ScatterWidget
data={data}
viewState={viewState}
header="Education vs. Healthcare"
xAxisColumn="education"
yAxisColumn="healthcare"
></ScatterWidget>
<HistogramWidget
data={data}
viewState={viewState}
header="Population distribution"
column="population"
ticks={[100, 500, 1000, 5000]}
></HistogramWidget>
</section>
<footer
id="footer"
dangerouslySetInnerHTML={{__html: attributionHTML}}
></footer>
</>
);
}
12 changes: 12 additions & 0 deletions examples/05-react-h3/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>Examples / React</title>
<link rel="stylesheet" href="../style.css" />
</head>
<body>
<main id="app"></main>
<script type="module" src="./react.tsx"></script>
</body>
</html>
6 changes: 6 additions & 0 deletions examples/05-react-h3/react.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import {createRoot} from 'react-dom/client';
import {App} from './app';

const container = document.querySelector('#app')!;
createRoot(container).render(<App />);
7 changes: 7 additions & 0 deletions examples/components/widgets/category-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ export class CategoryWidget extends BaseWidget {
spatialFilter: this.getSpatialFilterOrViewState(),
operation,
column,
viewState: this.viewState
? {
zoom: this.viewState.zoom,
latitude: this.viewState.latitude,
longitude: this.viewState.longitude,
}
: undefined,
juandjara marked this conversation as resolved.
Show resolved Hide resolved
});
},
args: () =>
Expand Down
7 changes: 7 additions & 0 deletions examples/components/widgets/formula-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ export class FormulaWidget extends BaseWidget {
operation,
column,
spatialFilter: this.getSpatialFilterOrViewState(),
viewState: this.viewState
? {
zoom: this.viewState.zoom,
latitude: this.viewState.latitude,
longitude: this.viewState.longitude,
}
: undefined,
});
return response.value;
},
Expand Down
7 changes: 7 additions & 0 deletions examples/components/widgets/histogram-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ export class HistogramWidget extends BaseWidget {
column,
operation,
ticks,
viewState: this.viewState
? {
zoom: this.viewState.zoom,
latitude: this.viewState.latitude,
longitude: this.viewState.longitude,
}
: undefined,
});
},
args: () =>
Expand Down
7 changes: 7 additions & 0 deletions examples/components/widgets/scatter-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ export class ScatterWidget extends BaseWidget {
xAxisJoinOperation,
yAxisColumn,
yAxisJoinOperation,
viewState: this.viewState
? {
zoom: this.viewState.zoom,
latitude: this.viewState.latitude,
longitude: this.viewState.longitude,
}
: undefined,
});
},
args: () =>
Expand Down
14 changes: 12 additions & 2 deletions examples/components/widgets/table-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ export class TableWidget extends BaseWidget {
...(sortBy && {sortBy, sortDirection}),
limit,
spatialFilter: this.getSpatialFilterOrViewState(),
viewState: this.viewState
? {
zoom: this.viewState.zoom,
latitude: this.viewState.latitude,
longitude: this.viewState.longitude,
}
: undefined,
});
},
args: () =>
Expand Down Expand Up @@ -136,11 +143,14 @@ function renderTableRow(row: unknown[]) {
</tr>`;
}

const _numberFormatter = new Intl.NumberFormat();
const _numberFormatter = new Intl.NumberFormat('en-US', {
maximumFractionDigits: 2,
notation: 'compact',
});
function renderTableCell(value: unknown) {
let formattedValue: string;
if (typeof value === 'number') {
return _numberFormatter.format(value);
formattedValue = _numberFormatter.format(value);
} else {
formattedValue = String(value);
}
Expand Down
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ <h2>Frameworks</h2>
<li><a href="./02-react/index.html">react</a></li>
<li><a href="./03-svelte/index.html">svelte</a></li>
<li><a href="./04-vue/index.html">vue</a></li>
<li><a href="./05-react-h3/index.html">react-h3</a></li>
<li>angular (TODO)</li>
</ol>
</body>
Expand Down
3 changes: 1 addition & 2 deletions src/api/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {buildQueryUrl} from './endpoints';
import {requestWithParameters} from './request-with-parameters';
import {APIErrorContext} from './carto-api-error';

export type QueryOptions = SourceOptions &
Omit<QuerySourceOptions, 'spatialDataColumn'>;
export type QueryOptions = SourceOptions & QuerySourceOptions;
type UrlParameters = {q: string; queryParameters?: string};

export const query = async function (
Expand Down
27 changes: 24 additions & 3 deletions src/models/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {$TODO} from '../types-internal.js';
import {assert} from '../utils.js';
import {ModelRequestOptions, makeCall} from './common.js';
import {ApiVersion} from '../constants.js';
import {SpatialDataType, SpatialFilterPolyfillMode} from '../sources/types.js';

/** @internalRemarks Source: @carto/react-api */
const AVAILABLE_MODELS = [
Expand Down Expand Up @@ -38,6 +39,12 @@ export interface ModelSource {
geoColumn?: string;
spatialFilter?: SpatialFilter;
queryParameters?: QueryParameters;
spatialDataColumn?: string;
spatialDataType?: SpatialDataType;
spatialFiltersResolution?: number;
spatialFiltersMode?: SpatialFilterPolyfillMode;
/** original resolution of the spatial index data as stored in the DW */
dataResolution?: number;
}

const {V3} = ApiVersion;
Expand Down Expand Up @@ -79,7 +86,9 @@ export function executeModel(props: {
data,
filters,
filtersLogicalOperator = 'and',
geoColumn = DEFAULT_GEO_COLUMN,
spatialDataType = 'geo',
spatialFiltersMode = 'intersects',
spatialFiltersResolution = 0,
} = source;

const queryParameters = source.queryParameters
Expand All @@ -96,18 +105,30 @@ export function executeModel(props: {
filtersLogicalOperator,
};

const spatialDataColumn =
source.spatialDataColumn || source.geoColumn || DEFAULT_GEO_COLUMN;

// Picking Model API requires 'spatialDataColumn'.
if (model === 'pick') {
queryParams.spatialDataColumn = geoColumn;
queryParams.spatialDataColumn = spatialDataColumn;
}

// API supports multiple filters, we apply it only to geoColumn
const spatialFilters = source.spatialFilter
? {[geoColumn]: source.spatialFilter}
? {[spatialDataColumn]: source.spatialFilter}
: undefined;

if (spatialFilters) {
queryParams.spatialFilters = JSON.stringify(spatialFilters);
queryParams.spatialDataColumn = spatialDataColumn;
queryParams.spatialDataType = spatialDataType;
}

if (spatialDataType !== 'geo') {
if (spatialFiltersResolution > 0) {
queryParams.spatialFiltersResolution = String(spatialFiltersResolution);
juandjara marked this conversation as resolved.
Show resolved Hide resolved
}
queryParams.spatialFiltersMode = spatialFiltersMode;
}

const urlWithSearchParams =
Expand Down
8 changes: 7 additions & 1 deletion src/sources/h3-query-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type H3QuerySourceOptions = SourceOptions &
QuerySourceOptions &
AggregationOptions &
FilterOptions;

type UrlParameters = {
aggregationExp: string;
aggregationResLevel?: string;
Expand Down Expand Up @@ -59,7 +60,12 @@ export const h3QuerySource = async function (
return baseSource<UrlParameters>('query', options, urlParameters).then(
(result) => ({
...(result as TilejsonResult),
widgetSource: new WidgetQuerySource(options),
widgetSource: new WidgetQuerySource({
...options,
// NOTE: passing redundant spatialDataColumn here to apply the default value 'h3'
spatialDataColumn,
donmccurdy marked this conversation as resolved.
Show resolved Hide resolved
spatialDataType: 'h3',
}),
})
);
};
7 changes: 6 additions & 1 deletion src/sources/h3-table-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ export const h3TableSource = async function (
return baseSource<UrlParameters>('table', options, urlParameters).then(
(result) => ({
...(result as TilejsonResult),
widgetSource: new WidgetTableSource(options),
widgetSource: new WidgetTableSource({
...options,
// NOTE: passing redundant spatialDataColumn here to apply the default value 'h3'
spatialDataColumn,
spatialDataType: 'h3',
}),
})
);
};
7 changes: 6 additions & 1 deletion src/sources/quadbin-query-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ export const quadbinQuerySource = async function (
return baseSource<UrlParameters>('query', options, urlParameters).then(
(result) => ({
...(result as TilejsonResult),
widgetSource: new WidgetQuerySource(options),
widgetSource: new WidgetQuerySource({
...options,
// NOTE: passing redundant spatialDataColumn here to apply the default value 'quadbin'
spatialDataColumn,
spatialDataType: 'quadbin',
}),
})
);
};
7 changes: 6 additions & 1 deletion src/sources/quadbin-table-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ export const quadbinTableSource = async function (
return baseSource<UrlParameters>('table', options, urlParameters).then(
(result) => ({
...(result as TilejsonResult),
widgetSource: new WidgetTableSource(options),
widgetSource: new WidgetTableSource({
...options,
// NOTE: passing redundant spatialDataColumn here to apply the default value 'quadbin'
spatialDataColumn,
spatialDataType: 'quadbin',
}),
})
);
};
Loading