-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #274 from chrishiguto/feature/add-react-data-viz
feat: add react data viz package and first map component
- Loading branch information
Showing
13 changed files
with
655 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './maps'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' | ||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" | ||
/> | ||
{children} | ||
</MapContainer> | ||
); | ||
}; | ||
|
||
export default Map; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
73
packages/react-data-viz/src/maps/MarkerClusterGroup/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Oops, something went wrong.