Skip to content

Commit

Permalink
Export legend component
Browse files Browse the repository at this point in the history
  • Loading branch information
RafaelSzmarowski committed Sep 20, 2023
1 parent e3ed3a0 commit 5854348
Show file tree
Hide file tree
Showing 14 changed files with 191 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CategoryLegend as _CategoryLegend, CategoryLegendOptions } from '@opendatasoft/visualizations';
import { FC } from 'react';
import { SimpleProps } from './Props';
import wrap from './ReactSimpleImpl';

// Explicit name and type are needed for storybook
const CategoryLegend: FC<SimpleProps<CategoryLegendOptions>> = wrap(_CategoryLegend);
export default CategoryLegend;
5 changes: 5 additions & 0 deletions packages/visualizations-react/src/components/Props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ export interface Props<Data, Options> extends HTMLAttributes<HTMLElement> {
options: Options;
tag?: string;
}

export interface SimpleProps<Options> extends HTMLAttributes<HTMLElement> {
options: Options;
tag?: string;
}
55 changes: 55 additions & 0 deletions packages/visualizations-react/src/components/ReactSimpleImpl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { SimpleComponent } from '@opendatasoft/visualizations';
import React, { FC, ForwardedRef, forwardRef, useEffect, useRef } from 'react';
import { useMergeRefs } from 'use-callback-ref';
import { SimpleProps } from './Props';

// FIXME: Test the wrap method

// Represent one of our simple component class's constructor like CategoryLegend
type ComponentConstructor<Options, ComponentClass extends SimpleComponent<Options>> =
new (container: HTMLElement, options: Options) => ComponentClass;

// The wrapper build a function component for the given component class
export default function wrap<Options, ComponentClass extends SimpleComponent<Options>>(
ComponentConstructor: ComponentConstructor<Options, ComponentClass>
): FC<SimpleProps<Options>> {
// We use forwardRef to forward the actual ref of the container
return forwardRef((props: SimpleProps< Options>, forwardedRef: ForwardedRef<HTMLElement>) => {
const { tag, options, ...elementProps } = props;

// This ref will hold our SDK component instance
const componentRef = useRef<ComponentClass | null>(null);
// This ref will hold the container element
const containerRef = useRef<HTMLElement | null>(null);
// By merging container ref and forwarded ref parent component could also access the container ref !
const ref = useMergeRefs([forwardedRef, containerRef]);

// Update options (put before creating the component to skip the initial render)
useEffect(() => {
componentRef.current?.updateOptions(options);
}, [options]);

// Create and destroy
useEffect(() => {
const container = containerRef.current;
if (container) {
const component = new ComponentConstructor(container, options);
componentRef.current = component;
return () => {
component.destroy();
componentRef.current = null;
};
}

throw new Error('Container was expected to be available in useEffect. This is a bug.');
// We only want to create or destroy on mount, hot reload or tag change.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tag]);

// With React 17 we should be able to just use jsx runtime.
return React.createElement(tag || 'div', {
...elementProps, // such as style...
ref,
});
});
}
3 changes: 2 additions & 1 deletion packages/visualizations-react/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export type { Props } from './components/Props';
export type { Props, SimpleProps } from './components/Props';
export { default as Chart } from './components/Chart';
export { default as MarkdownText } from './components/MarkdownText';
export { default as KpiCard } from './components/KpiCard';
export { default as ChoroplethGeoJson } from './components/ChoroplethGeoJson';
export { default as ChoroplethVectorTiles } from './components/ChoroplethVectorTiles';
export { default as ChoroplethSvg } from './components/ChoroplethSvg';
export { default as PoiMap } from './components/PoiMap';
export { default as CategoryLegend } from './components/CategoryLegend';
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ComponentMeta } from '@storybook/react';
import type { CategoryLegendOptions } from '@opendatasoft/visualizations';
import { CATEGORY_ITEM_VARIANT } from '@opendatasoft/visualizations';
import { CategoryLegend, SimpleProps } from '../../src';
import CategoryLegendTemplate from './CategoryLegendTemplate';

const meta: ComponentMeta<typeof CategoryLegend> = {
title: 'Legend',
component: CategoryLegend,
};

export default meta;

export const CategoryCircleLegend = CategoryLegendTemplate.bind({});
const CategoryCircleLegendArgs: SimpleProps<CategoryLegendOptions> = {
options: {
type: 'category' as const,
title: "I Am Legend",
items: [
{ label: 'category 1', color: '#F5C2C1', borderColor: 'red', variant: CATEGORY_ITEM_VARIANT.Circle },
{ label: 'category 2', color: '#90EE90', borderColor: 'green', variant: CATEGORY_ITEM_VARIANT.Circle },
{ label: 'category 3', color: '#ADD8E6', borderColor: 'blue', variant: CATEGORY_ITEM_VARIANT.Circle },
],
align: 'start' as const,
},
};
CategoryCircleLegend.args = CategoryCircleLegendArgs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { ComponentStory } from '@storybook/react';
import { CategoryLegendOptions } from '@opendatasoft/visualizations';
import { CategoryLegend, SimpleProps } from '../../src';

const CategoryLegendTemplate: ComponentStory<typeof CategoryLegend> = (
args: SimpleProps<CategoryLegendOptions>
) => (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<CategoryLegend {...args} style={{ width: '60vw' }} />
</div>
);

export default CategoryLegendTemplate;
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script lang="ts">
import CategoryLegendItem from './CategoryLegend/Item/CategoryLegendItem.svelte';
import type { CategoryLegend, CategoryItem } from './types';
import type { CategoryLegendOptions, CategoryItem } from './types';
export let legendOptions: CategoryLegend;
export let options: CategoryLegendOptions;
let items: CategoryItem[] = [];
let title: string | undefined;
Expand All @@ -16,7 +16,7 @@
: [...refinedSeries, index];
};
$: ({ items, title, align = 'center' } = legendOptions);
$: ({ items, title, align = 'center' } = options);
</script>

<div class="legend-container" style="--align: {align}">
Expand Down
9 changes: 9 additions & 0 deletions packages/visualizations/src/components/Legend/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import CategoryLegendImpl from './CategoryLegend.svelte';
import SvelteSimpleImpl from '../SvelteSimpleImpl';
import type { CategoryLegendOptions } from './types';

export default class CategoryLegendComponent extends SvelteSimpleImpl<CategoryLegendOptions> {
protected getSvelteSimpleComponentClass(): typeof CategoryLegendImpl {
return CategoryLegendImpl;
}
}
2 changes: 1 addition & 1 deletion packages/visualizations/src/components/Legend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export type LineCategoryItem = BaseCategoryItem & {

export type CategoryItem = CircleCategoryItem | BoxCategoryItem | LineCategoryItem;

export type CategoryLegend = {
export type CategoryLegendOptions = {
type: 'category';
items: CategoryItem[];
title?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
<p id={mapId.toString()} class="a11y-invisible-description">{description}</p>
{/if}
{#if legend}
<CategoryLegend legendOptions={legend} />
<CategoryLegend options={legend} />
{/if}
{#if sourceLink}
<SourceLink source={sourceLink} />
Expand Down
4 changes: 2 additions & 2 deletions packages/visualizations/src/components/MapPoi/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CircleLayerSpecification, StyleSpecification } from 'maplibre-gl';
import type { BBox } from 'geojson';
import type { Color, Source } from '../types';
import type { CategoryLegend } from '../Legend/types';
import type { CategoryLegendOptions } from '../Legend/types';

// To render data layers on the map
export type PoiMapData = Partial<{
Expand All @@ -28,7 +28,7 @@ export interface PoiMapOptions {
title?: string;
subtitle?: string;
description?: string;
legend?: CategoryLegend;
legend?: CategoryLegendOptions;
/** Link button to source */
sourceLink?: Source;
}
Expand Down
29 changes: 29 additions & 0 deletions packages/visualizations/src/components/SvelteSimpleImpl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { SvelteComponent } from 'svelte';
import { SimpleComponent } from '../types';

export default abstract class SvelteImpl<Options> extends SimpleComponent<Options> {
protected abstract getSvelteSimpleComponentClass(): typeof SvelteComponent;

private svelteComponent: SvelteComponent | undefined;

protected onCreate() {
const ComponentClass = this.getSvelteSimpleComponentClass();
this.svelteComponent = new ComponentClass({
target: this.container,
props: {
options: this.options,
},
});
}

protected onOptionsUpdated() {
this.svelteComponent?.$$set?.({
options: this.options,
});
}

protected onDestroy() {
this.svelteComponent?.$destroy();
this.svelteComponent = undefined;
}
}
1 change: 1 addition & 0 deletions packages/visualizations/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { default as KpiCard } from './components/KpiCard';
export { ChoroplethGeoJson, ChoroplethVectorTiles } from './components/Map/WebGl';
export { default as ChoroplethSvg } from './components/Map/Svg';
export { default as PoiMap } from './components/MapPoi';
export { default as CategoryLegend } from './components/Legend';

export * from './types';
export * from './components/types';
Expand Down
28 changes: 28 additions & 0 deletions packages/visualizations/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,31 @@ export abstract class BaseComponent<Data, Options> {

protected abstract onDestroy(): void;
}

export abstract class SimpleComponent<Options> {
readonly container: HTMLElement;

protected options: Options;

constructor(container: HTMLElement, options: Options) {
this.container = container;
this.options = options;
this.onCreate();
}

public updateOptions(newOptions: Options): void {
const oldOptions = this.options;
this.options = newOptions;
this.onOptionsUpdated(oldOptions);
}

public destroy(): void {
this.onDestroy();
}

protected abstract onCreate(): void;

protected abstract onOptionsUpdated(oldOptions: Options): void;

protected abstract onDestroy(): void;
}

0 comments on commit 5854348

Please sign in to comment.