Skip to content

Commit

Permalink
feat: new variant ('circle') for category legend
Browse files Browse the repository at this point in the history
And side changes:
- Add legend component to POI maps
- Add source link, title, subtitle and description for poi maps
- Add source link for choropleth maps
- Update bottom padding for title and subtitle for charts and maps

---------

Co-authored-by: Kevin Fabre <[email protected]>
  • Loading branch information
RafaelSzmarowski and KevinFabre-ods committed Sep 22, 2023
1 parent 9658e0f commit 86ed7ba
Show file tree
Hide file tree
Showing 24 changed files with 484 additions and 141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@opendatasoft/visualizations';
import { ChoroplethGeoJson, Props } from '../../src';
import { shapes, multiPolygonShapes, worldCopies } from './data';
import { IMAGES } from '../utils';
import { IMAGES, defaultSource } from '../utils';

const meta: ComponentMeta<typeof ChoroplethGeoJson> = {
title: 'Map/Choropleth',
Expand Down Expand Up @@ -378,6 +378,7 @@ const StudioChoroplethNavigationMapButtonsArgs: Props<DataFrame, ChoroplethGeoJs
title: 'I Am Legend',
},
navigationMaps: [...makeMiniMaps(15),],
sourceLink: defaultSource,
},
};
StudioChoroplethNavigationMapButtons.args = StudioChoroplethNavigationMapButtonsArgs;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@opendatasoft/visualizations';
import { ChoroplethVectorTiles, Props } from '../../src';
import { shapesTiles, regShapes, dataReg } from './data';
import { defaultSource } from '../utils';

const meta: ComponentMeta<typeof ChoroplethVectorTiles> = {
title: 'Map/ChoroplethVector',
Expand Down Expand Up @@ -305,6 +306,7 @@ const StudioChoroplethNavigationMapButtonsArgs: Props<DataFrame, ChoroplethVecto
},
bbox: [-17.529298, 38.79776, 23.889159, 52.836618],
navigationMaps: [...makeMiniMaps(15),],
sourceLink: defaultSource,
},
};
StudioChoroplethNavigationMapButtons.args = StudioChoroplethNavigationMapButtonsArgs;
Expand Down
80 changes: 70 additions & 10 deletions packages/visualizations-react/stories/Poi/PoiMap.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react';
import { BBox } from 'geojson';
import { CATEGORY_ITEM_VARIANT, PopupDisplayTypes } from '@opendatasoft/visualizations';
import { ComponentMeta, ComponentStory } from '@storybook/react';

import type { Layer } from '@opendatasoft/visualizations';
import { PopupDisplayTypes } from '@opendatasoft/visualizations';

import { defaultSource, timeout } from '../utils';

import sources from './sources';
import { PoiMap } from '../../src';
import { timeout } from '../utils';

const BASE_STYLE = 'https://demotiles.maplibre.org/style.json';

Expand All @@ -16,6 +16,7 @@ const layer1: Layer = {
source: 'cities',
type: 'circle',
color: 'black',
borderColor: 'white',
popup: {
display: PopupDisplayTypes.Tooltip,
getContent: async (_, properties) => {
Expand All @@ -32,6 +33,7 @@ const layer2: Layer = {
source: 'battles',
type: 'circle',
color: 'red',
borderColor: 'white',
popup: {
display: PopupDisplayTypes.Sidebar,
getContent: async (_, properties) => {
Expand All @@ -52,10 +54,52 @@ const layers = [layer1, layer2];
const citiesColorMatch = {
key: 'key',
colors: { Paris: 'blue', Nantes: 'yellow', Bordeaux: 'purple', Marseille: 'lightblue' },
borderColors: { Paris: 'white', Nantes: 'black', Bordeaux: 'white', Marseille: 'black' },
};

const bbox: BBox = [-6.855469, 41.343825, 11.645508, 51.37178];

const legend = {
type: 'category' as const,
title: 'French cities',
items: [
{
label: 'Paris',
color: citiesColorMatch.colors.Paris,
borderColor: citiesColorMatch.borderColors.Paris,
variant: CATEGORY_ITEM_VARIANT.Circle,
},
{
label: 'Nantes',
color: citiesColorMatch.colors.Nantes,
borderColor: citiesColorMatch.borderColors.Nantes,
variant: CATEGORY_ITEM_VARIANT.Circle,
},
{
label: 'Bordeaux',
color: citiesColorMatch.colors.Bordeaux,
borderColor: citiesColorMatch.borderColors.Bordeaux,
variant: CATEGORY_ITEM_VARIANT.Circle,
},
{
label: 'Marseille',
color: citiesColorMatch.colors.Marseille,
borderColor: citiesColorMatch.borderColors.Marseille,
variant: CATEGORY_ITEM_VARIANT.Circle,
},
],
align: 'start' as const,
};

const options = {
style: BASE_STYLE,
bbox,
title: 'Lorem Ipsum',
subtitle: 'Dolor Sit Amet',
desciption: 'More aria description',
sourceLink: defaultSource,
};

const meta: ComponentMeta<typeof PoiMap> = {
title: 'Poi/PoiMap',
component: PoiMap,
Expand Down Expand Up @@ -83,7 +127,7 @@ const Template: ComponentStory<typeof PoiMap> = (args) => (
export const PoiMapNoLayersParams: ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapNoLayersParamsArgs = {
data: {},
options: { style: BASE_STYLE, bbox },
options,
};
PoiMapNoLayersParams.args = PoiMapNoLayersParamsArgs;

Expand All @@ -93,11 +137,7 @@ PoiMapNoLayersParams.args = PoiMapNoLayersParamsArgs;
export const PoiMapNonInteractive: ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapNonInteractiveArgs = {
data: { value: { layers, sources } },
options: {
style: BASE_STYLE,
bbox,
interactive: false,
},
options: { ...options, interactive: false },
};
PoiMapNonInteractive.args = PoiMapNonInteractiveArgs;

Expand All @@ -107,6 +147,26 @@ PoiMapNonInteractive.args = PoiMapNonInteractiveArgs;
export const PoiMapMatchExpression: ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapMatchExpressionArgs = {
data: { value: { layers: [{ ...layer1, colorMatch: citiesColorMatch }, layer2], sources } },
options: { style: BASE_STYLE, bbox },
options,
};
PoiMapMatchExpression.args = PoiMapMatchExpressionArgs;

/**
* STORY: With legend on start align
*/
export const PoiMapLegendStart: ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapLegendStartArgs = {
data: { value: { layers: [{ ...layer1, colorMatch: citiesColorMatch }, layer2], sources } },
options: { ...options, legend },
};
PoiMapLegendStart.args = PoiMapLegendStartArgs;

/**
* STORY: With legend on center align
*/
export const PoiMapLegendCenter: ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapLegendCenterArgs = {
data: { value: { layers: [{ ...layer1, colorMatch: citiesColorMatch }, layer2], sources } },
options: { ...options, legend: { ...legend, align: 'center' as const } },
};
PoiMapLegendCenter.args = PoiMapLegendCenterArgs;
21 changes: 10 additions & 11 deletions packages/visualizations/src/components/Chart/Chart.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { defaultValue } from './utils';
import toDataset from './datasets';
import buildScales from './scales';
import { buildLegend, buildCustomLegend } from './legend';
import { buildLegend, buildPieAndDoughnutCustomLegend } from './legend';
export let data: Async<DataFrame>;
export let options: ChartOptions;
Expand Down Expand Up @@ -186,8 +186,11 @@
$: legendPosition =
clientWidth <= 375 ? 'bottom' : defaultValue(options?.legend?.position, 'bottom');
let legendOptions: CategoryLegendType;
$: if (options?.legend?.custom) {
legendOptions = buildCustomLegend({ chart, options, chartConfig });
$: if (
[ChartSeriesType.Pie, ChartSeriesType.Doughnut].includes(options.series[0].type) &&
options?.legend?.custom
) {
legendOptions = buildPieAndDoughnutCustomLegend({ chart, options, chartConfig });
}
</script>

Expand Down Expand Up @@ -238,6 +241,10 @@
}
.header {
width: 100%;
margin: 0 0 1em 0;
}
.header h3,
.header p {
margin: 0;
}
Expand All @@ -255,14 +262,6 @@
flex-direction: row;
}
figcaption {
display: grid;
justify-content: center;
grid-gap: 3px 13px;
grid-template-columns: repeat(auto-fit, minmax(120px, max-content));
padding: 13px 0;
}
/* Suitable for elements that are used via aria-describedby or aria-labelledby */
.a11y-invisible-description {
display: none;
Expand Down
15 changes: 12 additions & 3 deletions packages/visualizations/src/components/Chart/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import type { Options as DataLabelsOptions } from 'chartjs-plugin-datalabels/typ
import type { ChartSeries, DataLabelsConfiguration, FillConfiguration } from './types';
import type { DataFrame } from '../types';
import { defaultCompactNumberFormat } from '../utils/formatter';
import { defaultValue, singleChartJsColor, multipleChartJsColors } from './utils';
import {
defaultValue,
singleChartJsColor,
multipleChartJsColors,
DEFAULT_GREY_COLOR,
} from './utils';

function chartJsFill(fill: FillConfiguration | undefined) {
if (fill === undefined) return false;
Expand Down Expand Up @@ -73,7 +78,9 @@ export default function toDataset(df: DataFrame, s: ChartSeries): ChartDataset {
type: 'pie',
label: defaultValue(s.label, ''),
data: df.map((entry) => entry[s.valueColumn]),
backgroundColor: multipleChartJsColors(s.backgroundColor),
backgroundColor: multipleChartJsColors(
s.backgroundColor?.length ? s.backgroundColor : [DEFAULT_GREY_COLOR]
),
datalabels: chartJsDataLabels(s.dataLabels),
};
}
Expand All @@ -96,7 +103,9 @@ export default function toDataset(df: DataFrame, s: ChartSeries): ChartDataset {
type: 'doughnut',
label: defaultValue(s.label, ''),
data: df.map((entry) => entry[s.valueColumn]),
backgroundColor: multipleChartJsColors(s.backgroundColor),
backgroundColor: multipleChartJsColors(
s.backgroundColor?.length ? s.backgroundColor : DEFAULT_GREY_COLOR
),
datalabels: chartJsDataLabels(s.dataLabels),
};
}
Expand Down
14 changes: 10 additions & 4 deletions packages/visualizations/src/components/Chart/legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { LegendOptions, ChartTypeRegistry, ChartConfiguration, Chart } from
import type { _DeepPartialObject } from 'chart.js/types/utils';
import type { ChartOptions } from './types';
import { assureMaxLength } from '../utils/formatter';
import { defaultValue } from './utils';
import { defaultValue, DEFAULT_GREY_COLOR } from './utils';
import { CATEGORY_ITEM_VARIANT } from '../Legend/types';

const LEGEND_MAX_LENGTH = 50;

Expand Down Expand Up @@ -80,7 +81,7 @@ function buildLegendLabels(
return `${chartConfig.data.labels?.[index]}`;
}

export function buildCustomLegend({
export function buildPieAndDoughnutCustomLegend({
chart,
options,
chartConfig,
Expand All @@ -90,12 +91,17 @@ export function buildCustomLegend({
chartConfig: ChartConfiguration;
}) {
const { series } = options;
const backgroundColors = series[0].backgroundColor?.length
? series[0].backgroundColor
: [DEFAULT_GREY_COLOR];
return {
type: 'category' as const,
position: defaultValue(options?.legend?.position, 'bottom'),
align: defaultValue(options?.legend?.align, 'center'),
items: chartConfig.data.datasets[0].data.map((_data, i) => ({
color: series[0].backgroundColor?.[i],
borderDashed: false,
color: backgroundColors[i % backgroundColors.length],
variant: CATEGORY_ITEM_VARIANT.Box,
dashed: false,
label: buildLegendLabels(i, options, chartConfig),
onClick: (index: number) => {
if (chart) {
Expand Down
2 changes: 2 additions & 0 deletions packages/visualizations/src/components/Chart/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Color } from '../types';

export const DEFAULT_GREY_COLOR = '#F0F0F0';

export function defaultValue<T>(value: T | undefined, fallback: T): T {
if (value === undefined) return fallback;
return value;
Expand Down
Loading

0 comments on commit 86ed7ba

Please sign in to comment.