Skip to content

Commit

Permalink
Dynamic loading script as content and custom maps
Browse files Browse the repository at this point in the history
  • Loading branch information
InfinityXTech authored Jan 18, 2025
1 parent af00010 commit c9d8c41
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 89 deletions.
4 changes: 2 additions & 2 deletions resources/dist/filament-world-map-widget.js

Large diffs are not rendered by default.

119 changes: 59 additions & 60 deletions resources/js/index.js
Original file line number Diff line number Diff line change
@@ -1,85 +1,84 @@
import jsVectorMap from 'jsvectormap';
import 'jsvectormap/dist/maps/world.js';
import 'jsvectormap/dist/maps/world-merc.js';

/**
* Initializes the world map widget with the given options.
* @param {object} options - Options for initializing the map
* @param {object} options.stats - The stats data, with country codes as keys and values as view counts
* @param {string} options.tooltipText - Text to display next to the stats in the tooltip
* @param {string} options.map - The name of the map to use
* @param {string} options.customMapUrl - The name of the map to use
* @param {array} options.color - RGB array for the region color
* @param {string} options.selector - The CSS selector for the HTML element to attach the map
* @param {object} options.additionalOptions - Additional options to override or extend the default configuration
*/
export default function initWorldMapWidget({
stats,
tooltipText,
map,
color,
selector,
additionalOptions = {}
}) {
import { loadScript } from './scriptLoader';

export default function initWorldMapWidget({ stats, tooltipText, map, customMapUrl, color, selector, additionalOptions = {} }) {
return {
stats,

async init() {
init() {
const self = this;
const dataValues = self.stats;
const minValue = Math.min(...Object.values(dataValues));
const maxValue = Math.max(...Object.values(dataValues));
const scriptUrl = customMapUrl ?? `https://raw.githubusercontent.com/themustafaomar/jsvectormap/master/src/maps/${map.replace(/_/g, '-')}.js`;

loadScript(scriptUrl, () => {
// Initialize jsVectorMap after the script is loaded
const dataValues = self.stats;
const minValue = Math.min(...Object.values(dataValues));
const maxValue = Math.max(...Object.values(dataValues));

const normalizeOpacity = (value, min, max) => 0.3 + ((value - min) / (max - min)) * (1 - 0.3);
const normalizeOpacity = (value, min, max) =>
0.3 + ((value - min) / (max - min)) * (1 - 0.3);

const regionScales = Object.fromEntries(
Object.entries(dataValues).map(([code, value]) => {
const opacity = normalizeOpacity(value, minValue, maxValue);
return [code, `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${opacity.toFixed(2)})`];
})
);
const regionScales = Object.fromEntries(
Object.entries(dataValues).map(([code, value]) => {
const opacity = normalizeOpacity(value, minValue, maxValue);
return [code, `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${opacity.toFixed(2)})`];
})
);

const regionValues = Object.fromEntries(
Object.entries(dataValues).map(([code]) => [code, code])
);
const regionValues = Object.fromEntries(
Object.entries(dataValues).map(([code]) => [code, code])
);

// Default options
const options = {
selector: selector,
map: map,
series: {
regions: [{
attribute: 'fill',
scale: regionScales,
values: regionValues,
}]
},
showTooltip: true,
onRegionTooltipShow(event, tooltip, code) {
const stats = self.stats[code.toUpperCase()] || 0;
tooltip.text(
`<h5>${tooltip.text()}: ${stats} ${tooltipText}</h5>`,
true // Enables HTML
);
}
};
const options = {
selector: selector,
map: map,
series: {
regions: [{
attribute: 'fill',
scale: regionScales,
values: regionValues,
}]
},
showTooltip: true,
onRegionTooltipShow(event, tooltip, code) {
const stats = self.stats[code.toUpperCase()] || 0;
tooltip.text(
`<h5>${tooltip.text()}: ${stats} ${tooltipText}</h5>`,
true // Enable HTML in the tooltip
);
}
};

// Merge options with additionalOptions, where additionalOptions can override default options
const mergedOptions = {
...options,
...additionalOptions,
series: {
regions: [
{
...options.series.regions[0],
...additionalOptions.series?.regions?.[0],
}
],
},
};
const mergedOptions = {
...options,
...additionalOptions,
series: {
regions: [
{
...options.series.regions[0],
...additionalOptions.series?.regions?.[0],
}
],
},
};

// Initialize the map with merged options
new jsVectorMap(mergedOptions);
}
// Initialize the map
new jsVectorMap(mergedOptions);
});

},
};
}

Expand Down
86 changes: 86 additions & 0 deletions resources/js/scriptLoader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// scriptLoader.js

const scriptCallbacks = {};
const scriptLoaded = {};

/**
* Checks if a script with the given URL is already injected
* @param {string} scriptUrl - The URL of the script
* @returns {boolean} - True if already injected, false otherwise
*/
const isScriptInjected = (scriptUrl) => {
return document.querySelector(`script[src="${scriptUrl}"]`) !== null;
};

/**
* Adds a callback function to the callbacks for a specific script
* @param {string} scriptUrl - The URL of the script
* @param {function} callback - The callback to execute when the script is loaded
*/
const addScriptCallback = (scriptUrl, callback) => {
if (!scriptCallbacks[scriptUrl]) {
scriptCallbacks[scriptUrl] = [];
}
scriptCallbacks[scriptUrl].push(callback);
};

/**
* Runs all callbacks for a specific script
* @param {string} scriptUrl - The URL of the script
*/
const runScriptCallbacks = (scriptUrl) => {
if (scriptLoaded[scriptUrl]) {
while (scriptCallbacks[scriptUrl]?.length) {
const callback = scriptCallbacks[scriptUrl].shift();
callback();
}
}
};

export const addScriptToDocument = (scriptContent) => {
// Create a script tag with the fetched content
const script = document.createElement('script');
script.textContent = scriptContent;

// Append the script to the document
document.head.appendChild(script);
}

/**
* Fetches the script content and injects it as an inline script tag
* @param {string} scriptUrl - The URL of the script to load
* @param {function} callback - The callback to execute when the script is loaded
*/
export const loadScript = (scriptUrl, callback) => {
addScriptCallback(scriptUrl, callback);

if (scriptLoaded[scriptUrl]) {
runScriptCallbacks(scriptUrl);
return;
}

if (!isScriptInjected(scriptUrl)) {
fetch(scriptUrl)
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch script: ${scriptUrl}`);
}
return response.text();
})
.then((scriptContent) => {
// Add script to doc
addScriptToDocument(scriptContent);

// Mark the script as loaded
scriptLoaded[scriptUrl] = true;
runScriptCallbacks(scriptUrl);
})
.catch((error) => {
console.error(`Failed to load script content: ${scriptUrl}`, {
error, // Logs the error event object for additional context
src: scriptUrl, // Logs the URL of the script
currentTime: new Date() // Logs the time of the error for debugging purposes
});
});
}
};
3 changes: 2 additions & 1 deletion resources/views/widgets/world-map-widget.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
x-data="initWorldMapWidget({
stats: JSON.parse('{{ json_encode($this->stats()) }}'),
tooltipText: '{{ $this->tooltip() }}',
map: '{{ $this->map()->value }}',
map: '{{ is_string($this->map()) ? $this->map() : $this->map()->value }}',
color: JSON.parse('{{ json_encode($this->color()) }}'),
selector: '#map',
additionalOptions: JSON.parse('{{ json_encode($this->additionalOptions()) }}'),
customMapUrl: '{{ $this->customMapUrl() }}'
})"
x-init="init()">
<x-filament::section>
Expand Down
20 changes: 10 additions & 10 deletions src/Enums/Map.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
enum Map: string
{
case WORLD = 'world';
// case WORLD_MERC = 'world_merc';
// case US_MILL_EN = 'us_mill_en';
// case US_MERC_EN = 'us_merc_en';
// case US_LCC_EN = 'us_lcc_en';
// case US_AEA_EN = 'us_aea_en';
// case SPAIN = 'spain';
// case RUSSIA = 'russia';
// case CANADA = 'canada';
// case IRAQ = 'iraq';
// case BRASIL = 'brasil';
case WORLD_MERC = 'world_merc';
case US_MILL_EN = 'us_mill_en';
case US_MERC_EN = 'us_merc_en';
case US_LCC_EN = 'us_lcc_en';
case US_AEA_EN = 'us_aea_en';
case SPAIN = 'spain';
case RUSSIA = 'russia';
case CANADA = 'canada';
case IRAQ = 'iraq';
case BRASIL = 'brasil';
}
98 changes: 82 additions & 16 deletions src/Widgets/WorldMapWidget.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,109 @@
use Illuminate\View\View;
use InfinityXTech\FilamentWorldMapWidget\Enums\Map;

/**
* Class WorldMapWidget
* Represents a Filament widget for displaying a world map with configurable options.
*/
class WorldMapWidget extends Widget
{
/**
* @var string $view
* The view file that renders the world map widget.
*/
protected static string $view = 'filament-world-map-widget::widgets.world-map-widget';

public function stats () {
/**
* Returns the stats to be displayed on the map.
* Keys represent country codes, and values are the associated data points.
*
* @return array
*/
public function stats(): array
{
return [
'US' => 35000,
'RS' => 15000
'US' => 35000, // Data for the United States
'RS' => 15000 // Data for Serbia
];
}

public function heading ():string|Htmlable|null {
return 'World Map';
/**
* Provides the heading text for the widget.
*
* @return string|Htmlable|null The heading text or HTML content.
*/
public function heading(): string|Htmlable|null
{
return 'World Map'; // Default heading for the widget
}

/**
* Provides the tooltip text for the widget.
*
* @return string|Htmlable Tooltip content or HTML.
*/
public function tooltip(): string|Htmlable
{
return 'stats'; // Tooltip text displayed on hover
}

public function tooltip ():string|Htmlable {
return 'stats';
/**
* Defines the map type to be displayed.
*
* @return Map|string The map type, defaults to the WORLD map.
*/
public function map(): Map|string
{
return Map::WORLD; // Enum value for the world map
}

public function map ():Map {
return Map::WORLD;
/**
* Provides a custom URL for the map data, if any.
*
* @return string|null The custom map URL or null if not provided.
*/
public function customMapUrl(): ?string
{
return null; // No custom map URL by default
}

public function color ():array {
return [0, 120, 215];
/**
* Specifies the RGB color values for the map.
*
* @return array The RGB color values as an array [R, G, B].
*/
public function color(): array
{
return [0, 120, 215]; // Default blue color for the map
}

public function height ():string {
return '332px';
/**
* Specifies the height of the widget container.
*
* @return string The height value in CSS-compatible format.
*/
public function height(): string
{
return '332px'; // Default widget height
}

public function additionalOptions ():array {
return [];
/**
* Additional configuration options for the widget.
*
* @return array An array of additional options.
*/
public function additionalOptions(): array
{
return []; // No additional options by default
}

/**
* Renders the widget view.
*
* @return View The rendered view instance.
*/
public function render(): View
{
return view(static::$view);
return view(static::$view); // Render the specified widget view
}
}

0 comments on commit c9d8c41

Please sign in to comment.