Skip to content

Instantly share code, notes, and snippets.

@ThomasRohde
Created October 30, 2022 12:47
Show Gist options
  • Save ThomasRohde/6382de4e0eda1a96ee229f90a871afc4 to your computer and use it in GitHub Desktop.
Save ThomasRohde/6382de4e0eda1a96ee229f90a871afc4 to your computer and use it in GitHub Desktop.
Charting framework for Archi #Archi #JArchi #ArchiMateTool
/*
Author: Thomas Klok Rohde
Description:
Helper function to create HTML producing a Plotly chart.
This chart shows the distribution of ArchiMate objects used in the current model.
Place in __DIR__/lib directory
History:
October 29, 2022 : Created
*/
function createChart(pred, opts) {
let buckets = new Map();
let labels = [];
let dataset = [];
let title = opts.title ? opts.title : "No Title";
$("*").not("folder").each(o => {
if (pred(o.type, o.name, o)) {
let count = parseInt(buckets.get(o.type));
if (!count) count = 0;
buckets.set(o.type, count + 1);
}
});
for (let [key, value] of buckets) {
labels.push(key);
dataset.push(value);
}
let chartContent = `var chart = {
"data": [
{
"y": ${JSON.stringify(dataset)},
"x": ${JSON.stringify(labels)},
"type": "${opts.type}"
}],
"layout": {
"title": {
"text": "${title}",
"font": {
"size": 24
}
},
"layout.title.font.size" : 22,
"xaxis" : {
automargin: true
}
}
}
plot = Plotly.newPlot(
'plotly_div',
chart.data,
chart.layout, {displayModeBar: false})
`;
return chartContent;
}
/*
Author: Thomas Klok Rohde
Description:
Helper function to create HTML producing a Plotly chart.
This chart shows the distribution of ArchiMate objects used in the current model.
Place in __DIR__/lib directory
History:
October 29, 2022 : Created
*/
function createChart(pred, opts) {
let title = opts.title ? opts.title : "No Title";
let data = [];
let cont = true;
let ranges = [[0.1, "red"],[0.4, "yellow"],[1.0,"green"]];
$("*").not("folder").each(o => {
if (cont && pred(o.type, o.name, o)) {
const prop = o.prop(opts.property);
const value = parseInt(prop);
if (prop && value != NaN) {
let indicator = {
value: value,
mode: "number+gauge",
gauge: { shape: `${opts.type}`, bar: { color: "black" }, axis: { range: [opts.min, opts.max] } },
type: "indicator",
title: { text: `${opts.property}`}
}
steps = [];
current = 0;
ranges.forEach(r => {
let range = {
range: [current, Math.floor(opts.max * r[0])],
color: r[1]
}
steps.push(range);
current = Math.floor(opts.max * r[0]);
});
indicator.gauge.steps = steps;
data.push(indicator);
cont = false;
}
}
});
let chartContent = `
const labelLength = ${title.length * 14};
var chart = {
"data": ${JSON.stringify(data)},
"layout": {
"title": {
"text": "${title}",
"font": {
"size": 24
}
}
}
}
if (chart.data[0].gauge.shape == "bullet") {
chart.layout.margin = {"l": labelLength }
}
plot = Plotly.newPlot(
'plotly_div',
chart.data,
chart.layout, {displayModeBar: false})
`;
return chartContent;
}
/*
Author: Thomas Klok Rohde
Description:
Helper function to create HTML producing a Plotly chart.
This chart shows a table with all the properties and their values.
You might have to resize the containing note to fit all the data, or filter the source size using a predicate function
Place in __DIR__/lib directory
History:
October 29, 2022 : Created
*/
function createChart(pred, opts) {
let title = opts.title ? opts.title : "No Title";
let headers = [["<b>Name</b>"]];
let props = new Set();
let targets = [];
$("*").not("folder").each(o => {
if (pred(o.type, o.name, o)) {
targets.push(o);
o.prop().forEach(p => { if (p.length > 0) props.add(p.split(":")[0]) })
}
});
props.forEach(p => {
headers.push([`<b>${p}</b>`])
})
let values = [];
let names = [];
targets.forEach(o => {
names.push(($(o).is("relationship") ? `${o.type} (${o.source.name} - ${o.target.name})` : o.name))
});
values.push(names);
props.forEach(p => {
let column = [];
targets.forEach(o => {
let value = o.prop(p);
if (!value) value = "-"; else value = value.toString();
column.push(value);
})
values.push(column);
});
chartContent = `var chart = {
"data": [{
type: 'table',
header: {
values: ${JSON.stringify(headers)},
align: "center",
line: { width: 1, color: 'black' },
fill: { color: "grey" },
font: { family: "Arial", size: 12, color: "white" }
},
cells: {
values: ${JSON.stringify(values)},
align: "center",
line: { color: "black", width: 1 },
font: { family: "Arial", size: 11, color: ["black"] }
}
}],
"layout": {
"title": {
"text": "${title}",
"font": {
"size": 24
}
},
"height": ${opts.height},
"layout.title.font.size" : 22
}
}
plot = Plotly.newPlot(
'plotly_div',
chart.data,
chart.layout, {displayModeBar: false})
`;
return chartContent;
}
/*
Author: Thomas Klok Rohde
Description: Insert chart to view
History:
October 11, 2022 : Created with base set of scripts
*/
console.show();
console.clear();
let note = $(selection).filter("diagram-model-note").first();
// Get a list of all defined properties in the model
let props = new Set();
$("*").not("folder").each(o => {
o.prop().forEach(p => {
if (p.length) props.add(p);
})
})
let properties = "";
props.forEach(p => {
properties += `<option value="${p}">${p}</option>
`;
});
if (note == undefined)
throw new Error("No note selected");
let chartTemplate = {};
let chartFilter = "";
if (note.prop("Chart")) {
chartTemplate = JSON.parse(note.prop("Chart"));
chartFilter = chartTemplate.filter ? chartTemplate.filter : "";
console.log(JSON.stringify(chartTemplate,null,3))
}
const SWT = Java.type('org.eclipse.swt.SWT');
const FillLayout = Java.type('org.eclipse.swt.layout.FillLayout');
const Shell = Java.type('org.eclipse.swt.widgets.Shell');
const Browser = Java.type('org.eclipse.swt.browser.Browser');
const ProgressAdapter = Java.extend(Java.type('org.eclipse.swt.browser.ProgressAdapter'));
const LocationAdapter = Java.extend(Java.type('org.eclipse.swt.browser.LocationAdapter'));
const CustomFunction = Java.extend(Java.type('org.eclipse.swt.browser.BrowserFunction'));
const IArchiImages = Java.type('com.archimatetool.editor.ui.IArchiImages');
const ImageFactory = Java.type('com.archimatetool.editor.ui.ImageFactory');
const Rectangle = Java.type('org.eclipse.swt.graphics.Rectangle');
const Monitor = Java.type('org.eclipse.swt.widgets.Monitor');
let display = shell.getDisplay();
let newShell = new Shell(display, SWT.MODAL | SWT.TITLE | SWT.ON_TOP);
newShell.setText("Insert chart");
newShell.setLayout(new FillLayout());
html = `<html>
<title>Insert chart</title>
<style>
*,
*:before,
*:after {
box-sizing: border-box;
font-family: sans-serif;
font-size: 12px;
}
input[type=text],
input[type=number],
input[type=email],
select {
padding: 5px;
margin: 5px 0;
border-radius: 5px;
border-width: 2px;
width: 100%;
}
.option {
display: none;
margin: 10px;
border-width: 2px;
border-radius: 5px;
}
button {
margin: 10px;
padding: 5px;
width: 100px;
}
</style>
<body>
<input id="title1" type="text" value="${chartTemplate.title}" placeholder="Insert chart title">
<input id="filter1" type="text" value="" placeholder="Insert javascript expression - name, type and obj are available" onchange="validate('#filter1')">
<input name="chart" type="radio" value="chartArchiDistribution" onchange="toggleOptions('#options1')">
Distribution of ArchiMate types
<fieldset id="options1" disabled="true" class="option">
<legend>Select chart type</legend>
<select id="charttype1">
<option value="bar">Bar</option>
<option value="lines">Lines</option>
<select>
</fieldset>
</input>
<br>
<input name="chart" type="radio" value="chartGauges" onchange="toggleOptions('#options2')">
Show gauge for a numerical property
<fieldset id="options2" disabled="true" class="option">
<legend>Select chart options</legend>
Chart type
<select id="charttype2">
<option value="bullet">Bullet</option>
<option value="gauge">Gauge</option>
<select>
Property
<select id="properties">
${properties}
<select>
Minimum value<input type="number" id="minGauge" value=0 name="minGauge"></input>Maximum value<input type="number" id="maxGauge" value=100 name="maxGauge"></input>
</fieldset>
</input>
<br>
<input name="chart" type="radio" value="chartPropertyTable" onchange="hideOptions(['#options1','#options2'])">
Table of defined attributes
</input>
<br>
<button id="insertbutton" disabled="true" onclick="insertPressed()">Insert</button><button onclick="cancelPressed()">Cancel</button>
<script type="text/javascript">
function insertPressed() {
let chartButtons = document.getElementsByName('chart');
let chartName = null;
let chartType = null;
let chartProperty = null;
let chart = null;
let chartMin = 0;
let chartMax = 100;
for (let i = 0; i < chartButtons.length; i++) {
if (chartButtons[i].checked) {
chartName = chartButtons[i].value;
if (chartName == "chartArchiDistribution") chartType = document.querySelector('#charttype1').value;
if (chartName == "chartPropertyTable") chartType = "table";
if (chartName == "chartGauges") {
chartType = document.querySelector('#charttype2').value;
chartProperty = document.querySelector('#properties').value;
}
}
}
if (!chartName) chart = null;
else
chart = {
chart: chartName,
type: chartType
}
if (chartProperty) {
chart.property = chartProperty;
chart.min = chartMin;
chart.max = chartMax;
}
let filter = document.querySelector('#filter1').value;
if (filter) chart.filter = filter;
let title = document.querySelector('#title1').value;
if (title) chart.title = title;
insertPressedEvent(JSON.stringify(chart));
}
function cancelPressed() {
cancelPressedEvent();
}
function toggleOptions(id) {
let options = ['#options1','#options2'];
let element = document.querySelector(id);
element.disabled = !element.disabled;
if (element.disabled) element.style.display = 'none';
else {
element.style.display = 'block';
options.forEach(o => {
if (o != id) hideOptions([o]);
})
}
document.querySelector('#insertbutton').disabled = false;
}
function hideOptions(ids) {
ids.forEach(id => {
let element = document.querySelector(id);
element.disabled = true;
element.style.display = 'none';
});
document.querySelector('#insertbutton').disabled = false;
}
let chartButtons = document.getElementsByName('chart');
let current = "${chartTemplate.chart}";
for (let i = 0; i < chartButtons.length; i++) {
if (chartButtons[i].value == current) chartButtons[i].checked = true;
}
if (current == "chartArchiDistribution") {
toggleOptions('#options1');
document.getElementById('charttype1').value = '${chartTemplate.type}';
}
if (current == "chartGauges") {
toggleOptions('#options2');
document.getElementById('charttype2').value = '${chartTemplate.type}';
document.getElementById('properties').value = '${chartTemplate.property}';
}
document.getElementById('filter1').value = '${chartFilter}';
</script>
</body>`;
var insertPressed = false;
var cancelPressed = false;
let browser = new Browser(newShell, SWT.NONE);
var chart;
browser.addProgressListener(new ProgressAdapter({
completed: function (event) {
let fncOk = new CustomFunction(browser, "insertPressedEvent", {
function: function (args) {
chart = args[0];
insertPressed = true;
}
});
let fncCancel = new CustomFunction(browser, "cancelPressedEvent", {
function: function (args) {
cancelPressed = true;
}
});
browser.addLocationListener(new LocationAdapter({
changed: function (e) {
browser.removeLocationListener(this);
fncOk.dispose();
fncCancel.dispose();
}
}));
}
}));
// Write the HTML to a temporary file, so we are allowed to execute a local script
let System = Java.type('java.lang.System');
let tmpfile = System.getProperty("java.io.tmpdir") + "layout.html";
$.fs.writeFile(tmpfile, html);
browser.setUrl("file:///" + tmpfile);
// Set icon to Archi icon, in case shell has a style which displays icons
newShell.setImage(IArchiImages.ImageFactory.getImage(IArchiImages.ICON_APP));
newShell.setSize(800, 775);
// Center dialog on screen
const primary = display.getPrimaryMonitor();
const bounds = primary.getBounds();
const rect = newShell.getBounds();
const x = bounds.x + (bounds.width - rect.width) / 2;
const y = bounds.y + (bounds.height - rect.height) / 2;
newShell.setLocation(x, y);
newShell.open();
while (!newShell.isDisposed() && !insertPressed && !cancelPressed) {
if (!display.readAndDispatch()) display.sleep();
}
if (insertPressed && chart != undefined) {
note.prop("Chart", chart);
newShell.dispose();
load(__DIR__ + "Refresh chart.ajs");
}
else if (cancelPressed) {
console.log('Dialog cancelled.');
newShell.dispose();
}
/*
Author: Thomas Klok Rohde
Description: Refresh selected chart
History:
October 26, 2022 : Created
*/
console.show();
console.clear();
// Avoid name clashes by encapsulation.
(function () {
let note = $(selection).filter("diagram-model-note").first();
if (note == undefined)
throw new Error("No note selected.");
const SWT = Java.type('org.eclipse.swt.SWT');
const FillLayout = Java.type('org.eclipse.swt.layout.FillLayout');
const Shell = Java.type('org.eclipse.swt.widgets.Shell');
const Browser = Java.type('org.eclipse.swt.browser.Browser');
const ProgressAdapter = Java.extend(Java.type('org.eclipse.swt.browser.ProgressAdapter'));
const LocationAdapter = Java.extend(Java.type('org.eclipse.swt.browser.LocationAdapter'));
const CustomFunction = Java.extend(Java.type('org.eclipse.swt.browser.BrowserFunction'));
const IArchiImages = Java.type('com.archimatetool.editor.ui.IArchiImages');
const ImageFactory = Java.type('com.archimatetool.editor.ui.ImageFactory');
let display = shell.getDisplay();
let newShell = new Shell(display, SWT.SHELL_TRIM | SWT.ON_TOP | SWT.APPLICATION_MODAL);
newShell.setText("Refresh chart");
newShell.setLayout(new FillLayout());
let width = note.bounds.width;
let height = note.bounds.height;
if (!note.prop("Chart")) {
throw new Error("Selected note does not contain chart property.");
}
const chartOptions = JSON.parse(note.prop("Chart"));
chartOptions.width = width;
chartOptions.height = height;
let predicate = function (type, name, obj) { return true };
let chartContent;
if (chartOptions.filter) {
let newPredicate = new Function("tp", "nm", "obj", "return " + chartOptions.filter);
try {
e = $("*").first();
$(e).each(o => {
let p = newPredicate(o.type, o.name, o);
})
predicate = newPredicate;
}
catch (error) {
console.log("Error in predicate. Skipping")
}
}
//try {
load(__DIR__ + "/lib/" + chartOptions.chart + ".js");
chartContent = createChart(predicate, chartOptions);
//}
//catch (error) {
// throw new Error("Wrong chart type: " + chartOptions.chart)
//}
let html = `<html>
<script src="${__DIR__ + "/lib/plotly.js"}"></script>
<style>
:root {
font-family: Sans-serif;
}
button {
margin-right: 10px;
padding: 5px;
width: 100px;
}
table
{
font-size:0.8em;
}
</style>
<div id="buttons">
<button onclick="insertPressed()">Insert</button><button onclick="cancelPressed()">Cancel</button>
</div>
<div id="plotly_div" ></div>
<script>
function insertPressed() {
plot
.then(function (gd) {
Plotly.toImage(gd, { format: 'png', height: ${height}, width: ${width} })
.then(function (url) {
insertPressedEvent(url);
})
});
}
function cancelPressed() {
cancelPressedEvent();
}
${chartContent}
</script>
</html>`;
var insertPressed = false;
var cancelPressed = false;
let browser = new Browser(newShell, SWT.NONE);
var image;
browser.addProgressListener(new ProgressAdapter({
completed: function (event) {
let fncOk = new CustomFunction(browser, "insertPressedEvent", {
function: (args) => {
image = args[0];
insertPressed = true;
}
});
let fncCancel = new CustomFunction(browser, "cancelPressedEvent", {
function: (args) => {
cancelPressed = true;
}
});
browser.addLocationListener(new LocationAdapter({
changed: (e) => {
browser.removeLocationListener(this);
fncOk.dispose();
fncCancel.dispose();
}
}));
}
}));
// Write the HTML to a temporary file, so we are allowed to execute a local script
let System = Java.type('java.lang.System');
let tmpfile = System.getProperty("java.io.tmpdir") + "creategraph.html";
$.fs.writeFile(tmpfile, html);
browser.setUrl("file:///" + tmpfile);
// Set icon to Archi icon
newShell.setImage(IArchiImages.ImageFactory.getImage(IArchiImages.ICON_APP));
newShell.setSize(Math.floor(width * 2.4) + 200, Math.floor(height * 2.4));
newShell.open();
while (!newShell.isDisposed() && !insertPressed && !cancelPressed) {
if (!display.readAndDispatch()) display.sleep();
}
if (insertPressed) {
let data = image.replace(/^data:image\/\w+;base64,/, "");
let tmpimagefile = System.getProperty("java.io.tmpdir") + "chartimage.png";
$.fs.writeFile(tmpimagefile, data, "BASE64");
note.imageSource = IMAGE_SOURCE.CUSTOM;
note.image = model.createImage(tmpimagefile);
note.imagePosition = IMAGE_POSITION.MIDDLE_CENTRE;
note.borderType = BORDER.RECTANGLE;
}
else if (cancelPressed)
console.log('Cancelled.')
newShell.dispose();
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment