Skip to content

Instantly share code, notes, and snippets.

@smileham
Last active January 19, 2026 14:10
Show Gist options
  • Select an option

  • Save smileham/4bbca832d8fe629b72beb4e2b9a4b7ea to your computer and use it in GitHub Desktop.

Select an option

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
/*
* 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