Skip to content

Commit

Permalink
Merge pull request #274 from chrishiguto/feature/add-react-data-viz
Browse files Browse the repository at this point in the history
feat: add react data viz package and first map component
  • Loading branch information
andreneto97 authored Jan 7, 2025
2 parents bcc5ac1 + 212e4ad commit 2feb827
Show file tree
Hide file tree
Showing 13 changed files with 655 additions and 2 deletions.
31 changes: 31 additions & 0 deletions copy-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');

function createDirIfNotExist(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}

function copyCSSFiles() {
// Glob pattern to match all .css files in src directories
const srcPattern = 'packages/*/src/**/*.css';

glob(srcPattern, (err, files) => {
if (err) {
console.error('Error reading CSS files:', err);
return;
}

files.forEach((file) => {
const distDir = file.replace('src', 'dist');
createDirIfNotExist(path.dirname(distDir));

fs.copyFileSync(file, distDir);
console.log(`Copied ${file} to ${distDir}`);
});
});
}

copyCSSFiles();
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"scripts": {
"postinstall": "husky install",
"clean": "./node_modules/.bin/rimraf packages/*/dist packages/*/tsconfig.tsbuildinfo",
"build": "./node_modules/.bin/tsc --build",
"build": "./node_modules/.bin/tsc --build && yarn copy:css",
"copy:css": "node copy-css.js",
"watch": "yarn build && ./node_modules/.bin/tsc --build --watch",
"prepublish": "yarn clean && yarn build",
"lint": "eslint \"packages/**/{src,test}/**/*.{ts,js,json}\"",
Expand Down
55 changes: 55 additions & 0 deletions packages/react-data-viz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# @concepta/react-data-viz

## Introduction

The primary goal of this package is to provide easy-to-use, out-of-the-box data visualization components. Aligned with the mission of Rockets to offer a comprehensive end-to-end application, this package contributes by providing maps and charts for data visualization management.

We offer maps and charts that can be used with any dataset, as well as pre-integrated components designed to work seamlessly with third-party data visualization platforms.

## Folder Structure

The folder structure is straightforward and designed with simplicity in mind. We separate `charts` and `maps`, allowing them to be imported directly from their respective folders.

`import { MapMarkerCluster } from '@rockets/react-data-viz/maps`

Alternatively, they can be imported from the main entry point:

`import { MapMarkerCluster } from '@rockets/react-data-viz`

```
react-data-viz
├── src
│ ├── charts
│ ├── maps
│ │ └── MapMarkerCluster.tsx
│ ├── integrations
│ │ └── cube
│ │ └── MapMarkerCluster.tsx
│ └── index.ts
```

The integrations with third-party libraries are located in the `integrations` folder. These integrations consist of chart and map components built on top of specific third-party APIs.

This structure keeps the core functionality decoupled from the integrations, ensuring smooth extensibility and easier integration of third-party libraries.

## Peer dependencies

This package leverages both `leaflet` and `react-leaflet` package to implement the map components. While the map components are included in the package, their dependencies are not bundled as required dependencies, nor are they installed by default.

To use the map components, developers must manually install the `leaflet` and `react-leaflet` dependencies in their project. For detailed installation instructions, refer to the official documentation: [React-Leaflet Start Guide](https://react-leaflet.js.org/docs/start-introduction/).

This approach helps maintain a smaller bundle size by exporting only the necessary code and avoiding unnecessary dependencies in the core package.

## Installation

To install `@concepta/react-data-viz`, run the following command:

```bash
npm install @concepta/react-data-viz
```

or with yarn:

```bash
yarn add @concepta/react-data-viz
```
54 changes: 54 additions & 0 deletions packages/react-data-viz/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@concepta/react-data-viz",
"version": "2.0.0-alpha.20",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "BSD-3-Clause",
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
},
"optionalDependencies": {
"@cubejs-client/core": "^1.0.0",
"@cubejs-client/react": "^1.0.0",
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3"
},
"peerDependencies": {
"@cubejs-client/core": "^1.0.0",
"@cubejs-client/react": "^1.0.0",
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"dependencies": {
"@react-leaflet/core": "^3.0.0",
"react-leaflet": "4.2.1"
},
"devDependencies": {
"@types/leaflet": "^1",
"@types/leaflet.markercluster": "^1",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-leaflet": "^3.0.0"
},
"scripts": {
"test": "jest"
},
"jest": {
"transform": {
"^.+\\.(ts|tsx|js|jsx)$": "ts-jest"
}
}
}
1 change: 1 addition & 0 deletions packages/react-data-viz/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './maps';
32 changes: 32 additions & 0 deletions packages/react-data-viz/src/maps/Map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { MapContainer, MapContainerProps, TileLayer } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet.markercluster/dist/MarkerCluster.css';
import { PropsWithChildren } from 'react';
import { LatLngTuple } from 'leaflet';

const GLOBE_CENTER: LatLngTuple = [0, 0];

const Map = ({
children,
center = GLOBE_CENTER,
...props
}: PropsWithChildren<MapContainerProps>) => {
return (
<MapContainer
style={{ height: '100%', width: '100%' }}
zoom={2}
minZoom={2}
center={center}
scrollWheelZoom={true}
{...props}
>
<TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{children}
</MapContainer>
);
};

export default Map;
40 changes: 40 additions & 0 deletions packages/react-data-viz/src/maps/MapMarkerCluster.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { MapContainerProps, Marker } from 'react-leaflet';
import { MarkerClusterGroupOptions } from 'leaflet';

import MarkerClusterGroup from './MarkerClusterGroup';
import Map from './Map';

import 'leaflet.markercluster/dist/MarkerCluster.css';

type Position = {
lat: number;
lon: number;
};

export type MapMarkerClusterProps = {
data: Position[];
markerClusterProps?: MarkerClusterGroupOptions;
} & MapContainerProps;

const MapMarkerCluster = ({
data,
markerClusterProps,
...props
}: MapMarkerClusterProps) => (
<Map {...props}>
<MarkerClusterGroup showCoverageOnHover={false} {...markerClusterProps}>
{data.map((address, index) => {
const { lat, lon } = address;

// Loose equality to check for `undefined` or `null`
if (lat == undefined || lon == undefined) {
return null;
}

return <Marker key={`${lat}-${lon}-${index}`} position={[lat, lon]} />;
})}
</MarkerClusterGroup>
</Map>
);

export default MapMarkerCluster;
73 changes: 73 additions & 0 deletions packages/react-data-viz/src/maps/MarkerClusterGroup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// The following code is based on the implementation from the repository:
// https://github.com/yuzhva/react-leaflet-markercluster
// Due to its simplicity, we decided to integrate it directly into our project
// for better maintainability and to reduce external dependencies.

import React from 'react';
import { createPathComponent } from '@react-leaflet/core';
import L from 'leaflet';
import 'leaflet.markercluster';

import 'leaflet.markercluster/dist/MarkerCluster.css';
import './styles.css';

L.MarkerClusterGroup.include({
_flushLayerBuffer() {
this.addLayers(this._layerBuffer);
this._layerBuffer = [];
},

addLayer(layer) {
if (this._layerBuffer.length === 0) {
setTimeout(this._flushLayerBuffer.bind(this), 50);
}
this._layerBuffer.push(layer);
},
});

L.MarkerClusterGroup.addInitHook(function () {
this._layerBuffer = [];
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function createMarkerCluster({ children: _c, ...props }, context) {
const clusterProps: L.MarkerClusterGroupOptions = {};
const clusterEvents: L.LayerEvent = {} as L.LayerEvent;

// Splitting props and events to different objects
Object.entries(props).forEach(([propName, prop]) =>
propName.startsWith('on')
? (clusterEvents[propName] = prop)
: (clusterProps[propName] = prop),
);
const instance = new L.MarkerClusterGroup(clusterProps);

// Initializing event listeners
Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`;
instance.on(clusterEvent, callback);
});
return {
instance,
context: {
...context,
layerContainer: instance,
},
};
}

const MarkerCluster = createPathComponent(createMarkerCluster);

const withRemovedNullishChildren = <P extends object>(
Component: React.ComponentType<P>,
) => {
return ({ children, ...props }: { children?: React.ReactNode } & P) => {
// Filter out nullish or invalid children
const validChildren = React.Children.toArray(children).filter(Boolean);

// Spread props excluding `children`
return <Component {...(props as P)}>{validChildren}</Component>;
};
};

export default withRemovedNullishChildren(MarkerCluster);
Loading

0 comments on commit 2feb827

Please sign in to comment.