Last active
January 19, 2026 14:10
-
-
Save smileham/4bbca832d8fe629b72beb4e2b9a4b7ea to your computer and use it in GitHub Desktop.
jArchi script to create Heatmap (set Red, Amber or Green background to element) based on the value of a given property. #jarchi
This file contains hidden or 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
| /* | |
| * Smart HeatMap (Numeric Gradient + Discrete) | |
| * * Version: 8.1 | |
| * * "VIBE CODED" with Gemini - it was quick, did it better than I could in the time I had, and seems to work... | |
| * * Changes: | |
| * - Added Multi-selection dialog for Scope (Components vs Relationships). | |
| * - Detects if a property is Numeric. | |
| * - If Numeric: Allows Min/Max color selection and generates a Gradient Heatmap. | |
| * - If Text (Discrete): Uses standard specific value coloring (limited to 10 unique values). | |
| * - Generates stepped legends for Gradients. | |
| * * * Usage: | |
| * 1. Select a View. | |
| * 2. Run Script. | |
| * 3. Select Scope (Components/Relationships). | |
| * 4. Select Property. | |
| * 5. Follow prompts (Gradient or Discrete flow). | |
| */ | |
| var MAX_DISCRETE_VALUES = 10; // Limit for text-based properties | |
| console.show(); | |
| console.clear(); | |
| console.log("> Starting Smart Heat Map"); | |
| // ============================================================================= | |
| // Helper Functions | |
| // ============================================================================= | |
| function rgbToHex(r, g, b) { | |
| return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); | |
| } | |
| function hexToRgb(hex) { | |
| var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); | |
| return result ? { | |
| r: parseInt(result[1], 16), | |
| g: parseInt(result[2], 16), | |
| b: parseInt(result[3], 16) | |
| } : null; | |
| } | |
| function interpolateColor(color1, color2, factor) { | |
| if (arguments.length < 3) { factor = 0.5; } | |
| var c1 = hexToRgb(color1); | |
| var c2 = hexToRgb(color2); | |
| if (!c1 || !c2) return null; | |
| var r = Math.round(c1.r + factor * (c2.r - c1.r)); | |
| var g = Math.round(c1.g + factor * (c2.g - c1.g)); | |
| var b = Math.round(c1.b + factor * (c2.b - c1.b)); | |
| return rgbToHex(r, g, b); | |
| } | |
| function colorDialog() { | |
| var ColorDialog = Java.type("org.eclipse.swt.widgets.ColorDialog"); | |
| var dialog = new ColorDialog(shell); | |
| var rgb = dialog.open(); | |
| return rgb ? rgbToHex(rgb.red, rgb.green, rgb.blue) : null; | |
| } | |
| function showMessage(msg) { | |
| var MessageDialog = Java.type("org.eclipse.jface.dialogs.MessageDialog"); | |
| MessageDialog.openInformation(shell, "HeatMap Info", msg); | |
| } | |
| function singleSelectionDialog(title, choices) { | |
| var ListSelectionDialog = Java.type("org.eclipse.ui.dialogs.ListSelectionDialog"); | |
| var LabelProvider = Java.type('org.eclipse.jface.viewers.LabelProvider'); | |
| var ArrayContentProvider = Java.type('org.eclipse.jface.viewers.ArrayContentProvider'); | |
| var dialog = new ListSelectionDialog(shell, choices, ArrayContentProvider.getInstance(), new LabelProvider(), title); | |
| dialog.setTitle("Property Selection"); | |
| dialog.setMessage("Select an item:"); | |
| if (dialog.open() === 0) { | |
| var result = Java.from(dialog.getResult()); | |
| return result.length > 0 ? result[0] : null; | |
| } | |
| return null; | |
| } | |
| function multiSelectionDialog(title, choices) { | |
| var ListSelectionDialog = Java.type("org.eclipse.ui.dialogs.ListSelectionDialog"); | |
| var LabelProvider = Java.type('org.eclipse.jface.viewers.LabelProvider'); | |
| var ArrayContentProvider = Java.type('org.eclipse.jface.viewers.ArrayContentProvider'); | |
| var dialog = new ListSelectionDialog(shell, choices, ArrayContentProvider.getInstance(), new LabelProvider(), title); | |
| dialog.setTitle("Scope Selection"); | |
| dialog.setMessage("Select elements to include:"); | |
| if (dialog.open() === 0) { | |
| return Java.from(dialog.getResult()); | |
| } | |
| return null; | |
| } | |
| // ============================================================================= | |
| // Main Logic | |
| // ============================================================================= | |
| var currentView = $(selection).filter("archimate-diagram-model").first(); | |
| if (!currentView) { | |
| window.alert("Please select a View (Diagram) first."); | |
| exit(); | |
| } | |
| // 0. Ask user what to include | |
| var scopeOptions = ["Components", "Relationships"]; | |
| var selectedScope = multiSelectionDialog("Select what to include in the Heatmap", scopeOptions); | |
| if (!selectedScope || selectedScope.length === 0) { | |
| console.log("> No scope selected. Exiting."); | |
| exit(); | |
| } | |
| var includeComponents = selectedScope.indexOf("Components") !== -1; | |
| var includeRelationships = selectedScope.indexOf("Relationships") !== -1; | |
| console.log("> Scope: " + selectedScope.join(" & ")); | |
| // 1. Harvest Data | |
| var propData = {}; // { "Cost": ["100", "500"], "Status": ["Good", "Bad"] } | |
| // Selector logic | |
| var elementsToScan = $(currentView).find().not("diagram-model-group").not("diagram-model-note"); | |
| if (includeComponents && includeRelationships) { | |
| // Keep as is (everything except notes/groups) | |
| } else if (includeComponents) { | |
| // Exclude relationships | |
| elementsToScan = elementsToScan.not("relationship"); | |
| } else if (includeRelationships) { | |
| // Keep ONLY relationships | |
| elementsToScan = elementsToScan.filter("relationship"); | |
| } else { | |
| // Nothing selected (though exit condition above should catch this) | |
| elementsToScan = $([]); | |
| } | |
| elementsToScan.each(function(e) { | |
| var properties = e.prop(); | |
| properties.forEach(function(key) { | |
| var val = e.prop(key); | |
| if (!propData[key]) propData[key] = []; | |
| if (propData[key].indexOf(val) === -1) propData[key].push(val); | |
| }); | |
| }); | |
| // 2. Analyze Properties (Numeric vs Discrete) | |
| var allKeys = Object.keys(propData); | |
| var candidateProps = []; | |
| var propTypes = {}; // { "Cost": "numeric", "Status": "discrete" } | |
| console.log("--- Analyzing Properties ---"); | |
| allKeys.forEach(function(key) { | |
| var values = propData[key]; | |
| // Check if ALL values for this property are numeric | |
| var isNumeric = values.every(function(v) { | |
| return !isNaN(parseFloat(v)) && isFinite(v); | |
| }); | |
| if (isNumeric) { | |
| propTypes[key] = "numeric"; | |
| candidateProps.push(key + " (Numeric range)"); | |
| console.log("Included '" + key + "': Numeric [" + values.length + " values]"); | |
| } else { | |
| // Fallback to max limit check for non-numeric | |
| if (values.length <= MAX_DISCRETE_VALUES) { | |
| propTypes[key] = "discrete"; | |
| candidateProps.push(key + " (Text values)"); | |
| } else { | |
| console.log("Excluded '" + key + "': Too many text values (" + values.length + ")"); | |
| } | |
| } | |
| }); | |
| console.log("----------------------------"); | |
| if (candidateProps.length === 0) { | |
| window.alert("No suitable properties found in the selected scope.\n(Numeric properties or Text properties with <= " + MAX_DISCRETE_VALUES + " values)"); | |
| exit(); | |
| } | |
| // 3. Select Property | |
| var selectionLabel = singleSelectionDialog("Select Property", candidateProps); | |
| if (!selectionLabel) exit(); | |
| // Extract actual key name (remove the " (Numeric...)" suffix) | |
| var selectedProp = selectionLabel.replace(" (Numeric range)", "").replace(" (Text values)", ""); | |
| var mode = propTypes[selectedProp]; | |
| var mappings = []; | |
| // 4. Branch Logic | |
| if (mode === "numeric") { | |
| // ================== GRADIENT MODE ================== | |
| console.log("> Mode: Numeric Gradient"); | |
| // Get Min/Max | |
| var values = propData[selectedProp].map(function(v) { return parseFloat(v); }); | |
| var minVal = Math.min.apply(null, values); | |
| var maxVal = Math.max.apply(null, values); | |
| showMessage("Selected Numeric Property: " + selectedProp + "\nMin: " + minVal + "\nMax: " + maxVal + "\n\nPlease select the Color for the MINIMUM value."); | |
| var minColor = colorDialog(); | |
| if (!minColor) exit(); | |
| showMessage("Now select the Color for the MAXIMUM value."); | |
| var maxColor = colorDialog(); | |
| if (!maxColor) exit(); | |
| // Apply Gradient Colors | |
| var shouldResetDefaults = window.confirm("Reset non-matching elements to default color?"); | |
| elementsToScan.each(function(e) { | |
| var valStr = e.prop(selectedProp); | |
| if (valStr !== null && valStr !== undefined) { | |
| var val = parseFloat(valStr); | |
| if (!isNaN(val)) { | |
| // Calculate ratio (0 to 1) | |
| var ratio = (maxVal === minVal) ? 1 : (val - minVal) / (maxVal - minVal); | |
| var finalColor = interpolateColor(minColor, maxColor, ratio); | |
| if ($(e).is("relationship")) { | |
| e.lineColor = finalColor; | |
| } else { | |
| e.fillColor = finalColor; | |
| } | |
| } | |
| } else if (shouldResetDefaults) { | |
| $(e).is("relationship") ? e.lineColor = null : e.fillColor = null; | |
| } | |
| }); | |
| // Create Gradient Legend (5 Steps) | |
| mappings = []; | |
| for (var i = 0; i <= 4; i++) { | |
| var r = i / 4; // 0, 0.25, 0.5, 0.75, 1.0 | |
| var valStep = minVal + (r * (maxVal - minVal)); | |
| var colStep = interpolateColor(minColor, maxColor, r); | |
| // Round value for display nicely | |
| valStep = Math.round(valStep * 100) / 100; | |
| mappings.push({ value: valStep, color: colStep }); | |
| } | |
| } else { | |
| // ================== DISCRETE MODE ================== | |
| console.log("> Mode: Discrete Selection"); | |
| var availableValues = propData[selectedProp].sort(); | |
| while (availableValues.length > 0) { | |
| var valToColor = singleSelectionDialog( | |
| "Select value to color (" + availableValues.length + " left)", | |
| availableValues | |
| ); | |
| if (!valToColor) break; | |
| var hexColor = colorDialog(); | |
| if (hexColor) { | |
| mappings.push({ value: valToColor, color: hexColor }); | |
| availableValues = availableValues.filter(function(v) { return v !== valToColor; }); | |
| } | |
| } | |
| if (mappings.length > 0) { | |
| var shouldResetDefaults = window.confirm("Reset non-matching elements to default color?"); | |
| elementsToScan.each(function(e) { | |
| var val = e.prop(selectedProp); | |
| var match = mappings.find(function(m) { return m.value == val; }); | |
| if (match) { | |
| $(e).is("relationship") ? e.lineColor = match.color : e.fillColor = match.color; | |
| } else if (shouldResetDefaults) { | |
| $(e).is("relationship") ? e.lineColor = null : e.fillColor = null; | |
| } | |
| }); | |
| } | |
| } | |
| // 5. Generate Legend (Common for both) | |
| if (mappings.length > 0) { | |
| console.log("> Generating Key..."); | |
| // Remove old key | |
| $(currentView).find("diagram-model-group").each(function(g) { | |
| if (g.name && g.name.startsWith("Legend: " + selectedProp)) g.delete(); | |
| }); | |
| var entryHeight = 30; | |
| var headerHeight = 30; | |
| var boxWidth = 200; | |
| var boxHeight = headerHeight + (mappings.length * entryHeight) + 10; | |
| var legendGroup = currentView.createObject("diagram-model-group", 10, 10, boxWidth, boxHeight); | |
| legendGroup.name = "Legend: " + selectedProp; | |
| legendGroup.borderType = 1; | |
| legendGroup.fillColor = "#FFFFFF"; | |
| for (var i = 0; i < mappings.length; i++) { | |
| var map = mappings[i]; | |
| var noteY = headerHeight + (i * entryHeight); | |
| var note = legendGroup.createObject("diagram-model-note", 10, noteY, boxWidth - 20, entryHeight - 5); | |
| note.fillColor = map.color; | |
| note.borderType = 1; | |
| // Label format depends on mode | |
| var label = (mode === "numeric") ? (selectedProp + " ~ " + map.value) : (selectedProp + ": " + map.value); | |
| note.text = label; | |
| note.textAlignment = 4; | |
| } | |
| } | |
| console.log("> Done."); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment