Skip to content

Commit 2feb827

Browse files
authored
Merge pull request #274 from chrishiguto/feature/add-react-data-viz
feat: add react data viz package and first map component
2 parents bcc5ac1 + 212e4ad commit 2feb827

File tree

13 files changed

+655
-2
lines changed

13 files changed

+655
-2
lines changed

Diff for: copy-css.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const glob = require('glob');
4+
5+
function createDirIfNotExist(dir) {
6+
if (!fs.existsSync(dir)) {
7+
fs.mkdirSync(dir, { recursive: true });
8+
}
9+
}
10+
11+
function copyCSSFiles() {
12+
// Glob pattern to match all .css files in src directories
13+
const srcPattern = 'packages/*/src/**/*.css';
14+
15+
glob(srcPattern, (err, files) => {
16+
if (err) {
17+
console.error('Error reading CSS files:', err);
18+
return;
19+
}
20+
21+
files.forEach((file) => {
22+
const distDir = file.replace('src', 'dist');
23+
createDirIfNotExist(path.dirname(distDir));
24+
25+
fs.copyFileSync(file, distDir);
26+
console.log(`Copied ${file} to ${distDir}`);
27+
});
28+
});
29+
}
30+
31+
copyCSSFiles();

Diff for: package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
"scripts": {
6060
"postinstall": "husky install",
6161
"clean": "./node_modules/.bin/rimraf packages/*/dist packages/*/tsconfig.tsbuildinfo",
62-
"build": "./node_modules/.bin/tsc --build",
62+
"build": "./node_modules/.bin/tsc --build && yarn copy:css",
63+
"copy:css": "node copy-css.js",
6364
"watch": "yarn build && ./node_modules/.bin/tsc --build --watch",
6465
"prepublish": "yarn clean && yarn build",
6566
"lint": "eslint \"packages/**/{src,test}/**/*.{ts,js,json}\"",

Diff for: packages/react-data-viz/README.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# @concepta/react-data-viz
2+
3+
## Introduction
4+
5+
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.
6+
7+
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.
8+
9+
## Folder Structure
10+
11+
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.
12+
13+
`import { MapMarkerCluster } from '@rockets/react-data-viz/maps`
14+
15+
Alternatively, they can be imported from the main entry point:
16+
17+
`import { MapMarkerCluster } from '@rockets/react-data-viz`
18+
19+
```
20+
react-data-viz
21+
├── src
22+
│ ├── charts
23+
│ ├── maps
24+
│ │ └── MapMarkerCluster.tsx
25+
│ ├── integrations
26+
│ │ └── cube
27+
│ │ └── MapMarkerCluster.tsx
28+
│ └── index.ts
29+
```
30+
31+
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.
32+
33+
This structure keeps the core functionality decoupled from the integrations, ensuring smooth extensibility and easier integration of third-party libraries.
34+
35+
## Peer dependencies
36+
37+
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.
38+
39+
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/).
40+
41+
This approach helps maintain a smaller bundle size by exporting only the necessary code and avoiding unnecessary dependencies in the core package.
42+
43+
## Installation
44+
45+
To install `@concepta/react-data-viz`, run the following command:
46+
47+
```bash
48+
npm install @concepta/react-data-viz
49+
```
50+
51+
or with yarn:
52+
53+
```bash
54+
yarn add @concepta/react-data-viz
55+
```

Diff for: packages/react-data-viz/package.json

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "@concepta/react-data-viz",
3+
"version": "2.0.0-alpha.20",
4+
"main": "dist/index.js",
5+
"types": "dist/index.d.ts",
6+
"license": "BSD-3-Clause",
7+
"publishConfig": {
8+
"access": "public"
9+
},
10+
"files": [
11+
"dist"
12+
],
13+
"peerDependenciesMeta": {
14+
"@types/react": {
15+
"optional": true
16+
},
17+
"@types/react-dom": {
18+
"optional": true
19+
}
20+
},
21+
"optionalDependencies": {
22+
"@cubejs-client/core": "^1.0.0",
23+
"@cubejs-client/react": "^1.0.0",
24+
"leaflet": "^1.9.4",
25+
"leaflet.markercluster": "^1.5.3"
26+
},
27+
"peerDependencies": {
28+
"@cubejs-client/core": "^1.0.0",
29+
"@cubejs-client/react": "^1.0.0",
30+
"leaflet": "^1.9.4",
31+
"leaflet.markercluster": "^1.5.3",
32+
"react": "^18.2.0",
33+
"react-dom": "^18.2.0"
34+
},
35+
"dependencies": {
36+
"@react-leaflet/core": "^3.0.0",
37+
"react-leaflet": "4.2.1"
38+
},
39+
"devDependencies": {
40+
"@types/leaflet": "^1",
41+
"@types/leaflet.markercluster": "^1",
42+
"@types/react": "^18.2.0",
43+
"@types/react-dom": "^18.2.0",
44+
"@types/react-leaflet": "^3.0.0"
45+
},
46+
"scripts": {
47+
"test": "jest"
48+
},
49+
"jest": {
50+
"transform": {
51+
"^.+\\.(ts|tsx|js|jsx)$": "ts-jest"
52+
}
53+
}
54+
}

Diff for: packages/react-data-viz/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './maps';

Diff for: packages/react-data-viz/src/maps/Map.tsx

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { MapContainer, MapContainerProps, TileLayer } from 'react-leaflet';
2+
import 'leaflet/dist/leaflet.css';
3+
import 'leaflet.markercluster/dist/MarkerCluster.css';
4+
import { PropsWithChildren } from 'react';
5+
import { LatLngTuple } from 'leaflet';
6+
7+
const GLOBE_CENTER: LatLngTuple = [0, 0];
8+
9+
const Map = ({
10+
children,
11+
center = GLOBE_CENTER,
12+
...props
13+
}: PropsWithChildren<MapContainerProps>) => {
14+
return (
15+
<MapContainer
16+
style={{ height: '100%', width: '100%' }}
17+
zoom={2}
18+
minZoom={2}
19+
center={center}
20+
scrollWheelZoom={true}
21+
{...props}
22+
>
23+
<TileLayer
24+
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
25+
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
26+
/>
27+
{children}
28+
</MapContainer>
29+
);
30+
};
31+
32+
export default Map;
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { MapContainerProps, Marker } from 'react-leaflet';
2+
import { MarkerClusterGroupOptions } from 'leaflet';
3+
4+
import MarkerClusterGroup from './MarkerClusterGroup';
5+
import Map from './Map';
6+
7+
import 'leaflet.markercluster/dist/MarkerCluster.css';
8+
9+
type Position = {
10+
lat: number;
11+
lon: number;
12+
};
13+
14+
export type MapMarkerClusterProps = {
15+
data: Position[];
16+
markerClusterProps?: MarkerClusterGroupOptions;
17+
} & MapContainerProps;
18+
19+
const MapMarkerCluster = ({
20+
data,
21+
markerClusterProps,
22+
...props
23+
}: MapMarkerClusterProps) => (
24+
<Map {...props}>
25+
<MarkerClusterGroup showCoverageOnHover={false} {...markerClusterProps}>
26+
{data.map((address, index) => {
27+
const { lat, lon } = address;
28+
29+
// Loose equality to check for `undefined` or `null`
30+
if (lat == undefined || lon == undefined) {
31+
return null;
32+
}
33+
34+
return <Marker key={`${lat}-${lon}-${index}`} position={[lat, lon]} />;
35+
})}
36+
</MarkerClusterGroup>
37+
</Map>
38+
);
39+
40+
export default MapMarkerCluster;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// The following code is based on the implementation from the repository:
2+
// https://github.com/yuzhva/react-leaflet-markercluster
3+
// Due to its simplicity, we decided to integrate it directly into our project
4+
// for better maintainability and to reduce external dependencies.
5+
6+
import React from 'react';
7+
import { createPathComponent } from '@react-leaflet/core';
8+
import L from 'leaflet';
9+
import 'leaflet.markercluster';
10+
11+
import 'leaflet.markercluster/dist/MarkerCluster.css';
12+
import './styles.css';
13+
14+
L.MarkerClusterGroup.include({
15+
_flushLayerBuffer() {
16+
this.addLayers(this._layerBuffer);
17+
this._layerBuffer = [];
18+
},
19+
20+
addLayer(layer) {
21+
if (this._layerBuffer.length === 0) {
22+
setTimeout(this._flushLayerBuffer.bind(this), 50);
23+
}
24+
this._layerBuffer.push(layer);
25+
},
26+
});
27+
28+
L.MarkerClusterGroup.addInitHook(function () {
29+
this._layerBuffer = [];
30+
});
31+
32+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
33+
function createMarkerCluster({ children: _c, ...props }, context) {
34+
const clusterProps: L.MarkerClusterGroupOptions = {};
35+
const clusterEvents: L.LayerEvent = {} as L.LayerEvent;
36+
37+
// Splitting props and events to different objects
38+
Object.entries(props).forEach(([propName, prop]) =>
39+
propName.startsWith('on')
40+
? (clusterEvents[propName] = prop)
41+
: (clusterProps[propName] = prop),
42+
);
43+
const instance = new L.MarkerClusterGroup(clusterProps);
44+
45+
// Initializing event listeners
46+
Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
47+
const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`;
48+
instance.on(clusterEvent, callback);
49+
});
50+
return {
51+
instance,
52+
context: {
53+
...context,
54+
layerContainer: instance,
55+
},
56+
};
57+
}
58+
59+
const MarkerCluster = createPathComponent(createMarkerCluster);
60+
61+
const withRemovedNullishChildren = <P extends object>(
62+
Component: React.ComponentType<P>,
63+
) => {
64+
return ({ children, ...props }: { children?: React.ReactNode } & P) => {
65+
// Filter out nullish or invalid children
66+
const validChildren = React.Children.toArray(children).filter(Boolean);
67+
68+
// Spread props excluding `children`
69+
return <Component {...(props as P)}>{validChildren}</Component>;
70+
};
71+
};
72+
73+
export default withRemovedNullishChildren(MarkerCluster);

0 commit comments

Comments
 (0)