Skip to content

Commit

Permalink
Merge pull request #2578 from evidence-dev/map-legends
Browse files Browse the repository at this point in the history
Map legends
  • Loading branch information
kwongz authored Oct 3, 2024
2 parents 4347a47 + 888cd48 commit 3a4be6e
Show file tree
Hide file tree
Showing 21 changed files with 1,111 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/loud-grapes-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@evidence-dev/core-components': patch
---

Implemented categorical and scalar map legends
13 changes: 13 additions & 0 deletions packages/lib/component-utilities/src/colours.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,16 @@ export const chartColours = [
'#e3e4e5',
'#f3f3f3'
];

export const mapColours = [
'hsla(207, 65%, 39%, 1)', // Navy
'hsla(195, 49%, 51%, 1)', // Teal
'hsla(207, 69%, 79%, 1)', // Light Blue
'hsla(202, 28%, 65%, 1)', // Grey
'hsla(179, 37%, 65%, 1)', // Light Green
'hsla(40, 30%, 75%, 1)', // Tan
'hsla(38, 89%, 62%, 1)', // Yellow
'hsla(342, 40%, 40%, 1)', // Maroon
'hsla(207, 86%, 70%, 1)', // Blue
'hsla(160, 40%, 46%, 1)' // Green
];
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

<div
width="100%"
class="grid grid-rows-auto box-content grid-cols-1 justify-center bg-red-50 text-grey-700 font-ui font-normal rounded border border-red-200 min-h-[{minHeight}px] py-5 px-8 my-5 print:break-inside-avoid"
class="grid grid-rows-auto box-content grid-cols-1 justify-center bg-red-50 text-grey-700 font-ui font-normal rounded border border-red-200 min-h-[{minHeight}px] py-5 px-8 my-5 print:break-inside-avoid relative z-[500]"
>
<div class="m-auto w-full">
<div class="font-bold text-center text-lg">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import { query } from '@evidence-dev/universal-sql/client-duckdb';
import { getInputContext } from '@evidence-dev/sdk/utils/svelte';
import { Dropdown, DropdownOption } from '../../../atoms/inputs/dropdown/index.js';
import AreaMap from './AreaMap.svelte';
import { screen, userEvent, within } from '@storybook/test';
Expand All @@ -33,6 +32,25 @@
`select * from la_zip_sales where zip_code <> 90704 order by 1 limit 100`,
slowQuery
);
const grouped_locations = Query.create(
`SELECT
*,
CASE
WHEN id BETWEEN 0 AND 500 THEN 'Hotels'
WHEN id BETWEEN 501 AND 1000 THEN 'Restaurants'
WHEN id BETWEEN 1001 AND 1500 THEN 'Golf Courses'
WHEN id BETWEEN 1501 AND 2000 THEN 'Shops'
WHEN id BETWEEN 2001 AND 2500 THEN 'Bars'
WHEN id BETWEEN 2501 AND 3000 THEN 'Entertainment'
WHEN id BETWEEN 3001 AND 4000 THEN 'Banks'
END AS Category
FROM la_zip_sales
WHERE zip_code <> 90704
ORDER BY 1;
`,
query
);
</script>

<Story name="Basic Usage" parameters={{ chromatic: { disableSnapshot: true } }}>
Expand Down Expand Up @@ -80,3 +98,54 @@
value="sales"
/>
</Story>

<Story name="Legend Usage" parameters={{ chromatic: { disableSnapshot: true } }}>
<AreaMap
legendType="categorical"
legendPosition="bottomLeft"
data={grouped_locations}
lat="lat"
long="long"
value="Category"
geoId="ZCTA5CE10"
areaCol="zip_code"
colorPalette={['red', 'green', 'blue', 'purple', 'orange', 'yellow', 'brown']}
/>
<div class="h-32"></div>
<AreaMap
legendType="scalar"
legendPosition="bottomLeft"
data={grouped_locations}
lat="lat"
long="long"
value="sales"
geoId="ZCTA5CE10"
areaCol="zip_code"
colorPalette={['red', 'yellow', 'green']}
/>
<div class="h-32"></div>
</Story>
<Story name="Legend Usage no color palette" parameters={{ chromatic: { disableSnapshot: true } }}>
<AreaMap
legendType="categorical"
legendPosition="bottomLeft"
data={grouped_locations}
lat="lat"
long="long"
value="Category"
geoId="ZCTA5CE10"
areaCol="zip_code"
/>
<div class="h-32"></div>
<AreaMap
legendType="scalar"
legendPosition="bottomLeft"
data={grouped_locations}
lat="lat"
long="long"
value="sales"
geoId="ZCTA5CE10"
areaCol="zip_code"
/>
<div class="h-32"></div>
</Story>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import { QueryLoad } from '../../../atoms/query-load';
import { Query } from '@evidence-dev/sdk/usql';
import Skeleton from '../../../atoms/skeletons/Skeleton.svelte';
import Legend from './components/Legend.svelte';
/** @type {'pass' | 'warn' | 'error' | undefined} */
export let emptySet = undefined;
Expand Down Expand Up @@ -59,6 +60,13 @@
/** @type {string|undefined} */
export let title = undefined;
/** @type {'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'} */
export let legendPosition = 'bottomLeft';
/** @type {'categorical' | 'scalar' | undefined} */
export let legendType = undefined;
/** @type {string} */
export let legendTitle = 'Legend';
const chartType = 'Area Map';
const initialHash = Query.isQuery(data) ? data.hash : undefined;
Expand All @@ -75,6 +83,7 @@
</div>
<BaseMap {startingLat} {startingLong} {startingZoom} {height} {basemap} {title}>
<Areas data={loaded} {geoJsonUrl} {geoId} {areaCol} {...$$restProps} />
<Areas data={loaded} {geoJsonUrl} {geoId} {areaCol} {legendType} {...$$restProps} />
<Legend {legendPosition} {legendType} {legendTitle} />
</BaseMap>
</QueryLoad>
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@
display: none;
}
div :global(.leaflet-tooltip) {
div :global(.leaflet-tooltip),
div :global(.legend-font) {
font-family: 'Inter', sans-serif;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@
`select * from la_locations order by 1 limit 100`,
slowQuery
);
const grouped_locations = Query.create(
`SELECT
*,
CASE
WHEN id BETWEEN 0 AND 4 THEN 'Hotels'
WHEN id BETWEEN 5 AND 9 THEN 'Restaurants'
WHEN id BETWEEN 10 AND 14 THEN 'Golf Courses'
WHEN id BETWEEN 15 AND 19 THEN 'Shops'
WHEN id BETWEEN 20 AND 24 THEN 'Bars'
WHEN id BETWEEN 25 AND 29 THEN 'Entertainment'
WHEN id BETWEEN 30 AND 34 THEN 'Banks'
END AS Category
FROM la_locations`,
query
);
</script>

<Story name="Basic Usage" parameters={{ chromatic: { disableSnapshot: true } }}>
Expand All @@ -34,3 +50,37 @@
<Story name="Loading" parameters={{ chromatic: { disableSnapshot: true } }}>
<BubbleMap data={slow_la_locations} lat="lat" long="long" size="sales" />
</Story>

<Story name="Legend Usage" parameters={{ chromatic: { disableSnapshot: true } }}>
<BubbleMap
legendType="categorical"
legendPosition="bottomLeft"
data={grouped_locations}
lat="lat"
long="long"
value="Category"
size="sales"
colorPalette={['red', 'green', 'blue', 'purple', 'orange', 'yellow', 'brown']}
tooltipType="hover"
tooltip={[
{ id: 'point_name', showColumnName: false, valueClass: 'text-lg font-semibold' },
{ id: 'sales', fmt: 'usd', fieldClass: 'text-[grey]', valueClass: 'text-[green]' }
]}
/>
<div class="h-32"></div>
<BubbleMap
data={grouped_locations}
legendType="scalar"
lat="lat"
long="long"
value="sales"
size="sales"
colorPalette={['red', 'blue', 'green']}
tooltipType="hover"
tooltip={[
{ id: 'point_name', showColumnName: false, valueClass: 'text-lg font-semibold' },
{ id: 'sales', fmt: 'usd', fieldClass: 'text-[grey]', valueClass: 'text-[green]' }
]}
/>
<div class="h-32"></div>
</Story>
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import EmptyChart from '../core/EmptyChart.svelte';
import { QueryLoad } from '../../../atoms/query-load';
import { Query } from '@evidence-dev/sdk/usql';
import Legend from './components/Legend.svelte';
/** @type {'pass' | 'warn' | 'error' | undefined} */
export let emptySet = undefined;
Expand Down Expand Up @@ -58,6 +59,13 @@
/** @type {string|undefined} */
export let title = undefined;
/** @type {'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'} */
export let legendPosition = 'bottomLeft';
/** @type {'categorical' | 'scalar' | undefined} */
export let legendType = undefined;
/** @type {string} */
export let legendTitle = 'Legend';
const chartType = 'Bubble Map';
const initialHash = Query.isQuery(data) ? data.hash : undefined;
Expand All @@ -69,6 +77,7 @@
<ErrorChart let:loaded slot="error" {chartType} error={error ?? loaded.error.message} />
<BaseMap {startingLat} {startingLong} {startingZoom} {height} {basemap} {title}>
<Bubbles data={loaded} {lat} {long} {size} {...$$restProps} />
<Bubbles data={loaded} {lat} {long} {size} {legendType} {...$$restProps} />
<Legend {legendPosition} {legendType} {legendTitle} />
</BaseMap>
</QueryLoad>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { fmt } from '@evidence-dev/component-utilities/formatting';
import formatTitle from '@evidence-dev/component-utilities/formatTitle';
import { initSmoothZoom } from './LeafletSmoothZoom';
import { writable, derived, readonly } from 'svelte/store';
import chroma from 'chroma-js';
import { uiColours } from '@evidence-dev/component-utilities/colours';

/** @template T @typedef {import('svelte/store').Writable<T>} Writable<T> */
/** @template T @typedef {import('svelte/store').Readable<T>} Readable<T> */
Expand Down Expand Up @@ -33,6 +35,14 @@ export class EvidenceMap {
/** @type {HTMLDivElement | undefined} */
#mapEl;

/** @type {import('svelte/store').Writable<{ values: string[], colorPalette: string[], minValue: number, maxValue: number }>} */
#legendData = writable({
values: [],
colorPalette: [],
minValue: 0,
maxValue: 0
});

/** Handles the promises associated with the initialization of the map component. */
#sharedPromise = sharedPromise();

Expand Down Expand Up @@ -431,4 +441,76 @@ export class EvidenceMap {

return data;
}

handleLegendValues(colorPalette, values, legendType) {
//determine legend style
if (legendType === 'categorical') {
let uniqueValues = new Set(values);
values = [...uniqueValues];
let i = 0;

while (colorPalette.length < values.length) {
if (i >= colorPalette.length) i = 0;
colorPalette.push(colorPalette[i]);
i++;
}
} else if (legendType === 'scalar') {
values.forEach((value) => {
if (typeof value !== 'number' && value !== null) {
throw new Error('Scalar legend requires numeric values or null.');
}
if (typeof value === 'number' && isNaN(value)) {
throw new Error('Scalar legend requires valid numeric values.');
}
});
}
return values;
}

handleFillColor(item, value, values, colorPalette, colorScale) {
if (!value) return uiColours.blue700;

if (item[value]) {
if (typeof item[value] === 'string') {
return colorPalette[values.indexOf(item[value])];
} else {
return colorScale(item[value]);
}
}
}

//handle legend data

buildLegend(colorPalette, arrayOfStringValues, minValue, maxValue) {
this.#legendData.update((legendData) => ({
...legendData, // Keep existing data
colorPalette,
values: arrayOfStringValues, // Make sure to update this property
minValue,
maxValue
}));
}

get legendData() {
return readonly(this.#legendData);
}

async initializeData(
data,
{ corordinates, value, checkInputs, min, max, colorPalette, legendType }
) {
await data.fetch();
checkInputs(data, corordinates);
let values = data.map((d) => d[value]);
let minValue = Math.min(...values);
let maxValue = Math.max(...values);
let colorScale = chroma.scale(colorPalette).domain([min ?? minValue, max ?? maxValue]);
colorPalette = colorPalette.map((item) => chroma(item).hex());
if (legendType) {
values = this.handleLegendValues(colorPalette, values, legendType);
this.buildLegend(colorPalette, values, minValue, maxValue);
}
// Return the values, minValue, and maxValue for sharing with other functions
return { values, minValue, maxValue, colorScale, colorPalette };
}
}
Loading

0 comments on commit 3a4be6e

Please sign in to comment.