Public
Edited
Sep 6, 2023
Insert cell
Insert cell
Insert cell
Insert cell
// viewof ex2 = pack({
// threshold: Range([minSimilarity, maxSimilarity + 0.0001], {label: "Similarity", step:stepValue, value: initialValue})
// })
viewof ex2 = pack({
threshold: Range([minSimilarity, maxSimilarity + 0.0001], {label: "Similarity", step:stepValue, value: 0.409})
})
Insert cell
Insert cell
dispatch.on("circle-clicked", (obj) => {
ReactDOM.render(
React.createElement(Gallery, obj),
document.getElementById(obj.tag)
);
});
Insert cell
<style>
.flex-parent-element {
display: flex;
width: 100%;
align-items: center; /* add this */
}

.flex-child-element {
flex: 1;
/* border: 2px solid black; */
margin: 0px;
}

.flex-child-element:first-child {
margin-right: 10px;
flex-basis: 20%;
}
.flex-child-element.green {
height: 100%;
/* justify-content: center; */
}
</style>
Insert cell
Insert cell
Insert cell
Insert cell
function createChart(finalJson) {
const root = d3.hierarchy(finalJson);

root.x0 = dy / 2;
root.y0 = 0;
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
if (d.depth && d.data.name.length !== 7) d.children = null;
});

const svg = d3.create("svg")
.attr("viewBox", [-margin.left, -margin.top, width, dx])
.style("font", "25px sans-serif")
.style("user-select", "none");

const linkStrokeWidthBase = 3;
const gLink = svg.append("g")
.attr("fill", "none")
.attr("stroke-opacity", 0.7)
.attr("stroke-width", linkStrokeWidthBase);

const gNode = svg.append("g")
.attr("cursor", "pointer")
.attr("pointer-events", "all");
let idQueue = []; // Initialize node queue to an empty array


function getImagesIdx(nodeData) {
console.log("data:", nodeData);
// if (!nodeData.children || nodeData.img_ids.length !== 0) {
// console.log("data image ids:", nodeData.img_ids);
// }
}

function calculateAverageSimilarity(data) {
// console.log("similarity array:", data.similarity_score);
if (!data.similarity_score || data.similarity_score.length === 0) {
return 0;
}
const sum = data.similarity_score.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
const average = sum / data.similarity_score.length;
// console.log("similarity avg:", average);
return average;
}

const updateLinks = (node) => {

// Check if ID already exists in the queue
for (let i = 0; i < idQueue.length; i++) {
if (idQueue[i].node.id === node.id) {
// console.log(`ID ${node.id} already exists in the queue`);
return;
}
}
if (Array.isArray(node.data.children) && node.data.children.length === 0) {
return; // return if the node has an empty children array
}

// console.log(node.data)
// let imageIdxs = getImagesIdx(node.data);
let imageIdstoShow = null;
if (node.data.img_ids){
// console.log("data image ids: ", node.data.img_ids)
imageIdstoShow = node.data.img_ids
}else if(node.data.children){
imageIdstoShow = Array.from(new Set(node.data.children.reduce((acc, obj) => { return acc.concat(obj.img_ids); }, [])));
// console.log("data children: ", imageIdstoShow)
}

// console.log(node.data.labels);
if (imageIdstoShow === null) {
return;
}
// console.log("selected images: ", imageIdstoShow);
let imgTopic = getImgTid(node.data.name);
if (imgTopic === null) {
return;
}

//In this new version use image ID in the data

// Assign a color to new object based on the color of the previous object
let color;
let divTag;
if (idQueue.length === 0) {
color = "purple";
divTag = "app1";
} else if (idQueue[idQueue.length - 1].color === "purple") {
color = "red";
divTag = "app2";
} else {
color = "purple";
divTag = "app1";
}

// Add new object to queue
if (idQueue.length < 2) {
idQueue.push({ node: node, color: color, divTag: divTag });
} else {
// Replace oldest object with new object
idQueue[0] = idQueue[1];
idQueue[1] = { node: node, color: color, divTag: divTag };
}

gNode.selectAll("circle").attr("fill", d => {
const queueNode = idQueue.find(qNode => qNode.node.id === d.id); // find the corresponding node in idQueue
if (queueNode) {
return queueNode.color; // if node exists in idQueue, use its color
} else {
return d._children ? "#555" : "#999"; // otherwise, use default color
}
}).attr("r", d => {
const queueNode = idQueue.find(qNode => qNode.node.id === d.id); // find the corresponding node in idQueue
if (queueNode) {
return 10; // if node exists in idQueue, use its color
} else {
return 6; // otherwise, use the default size
}
});

let ancestors = null;
ancestors = node.ancestors();

// console.log(idQueue);
gLink.selectAll("path")
.filter(link => !(ancestors.includes(link.source) && ancestors.includes(link.target)))
.attr("stroke", node => {
// console.log("similarity scores:", calculateAverageSimilarity(node.target.data));
return node.target.data.similarity_score ?
simColorScale(calculateAverageSimilarity(node.target.data)) : avgColorScale(avgSimilarity[node.target.data.id]);
})
.attr("stroke-width", linkStrokeWidthBase)
.attr("stroke-opacity", 0.7);

for (let i = 0; i < idQueue.length; i++) {
// console.log(idQueue[i].node);
ancestors = idQueue[i].node.ancestors();

gLink.selectAll("path")
.filter(link => ancestors.includes(link.source) && ancestors.includes(link.target))
.attr("stroke-width", 6)
.attr("stroke-opacity", 1)
.attr("stroke", idQueue[i].color);
}
dispatch.call("circle-clicked", null, { id: imgTopic, tag: divTag, images:imageIdstoShow, labels: node.data.labels, color:color});
};

function update(source) {
const duration = d3.event && d3.event.altKey ? 2500 : 250;
const nodes = root.descendants().reverse();
const links = root.links();
// Compute the new tree layout.
tree(root);

let left = root;
let right = root;
root.eachBefore(node => {
if (node.x < left.x) left = node;
if (node.x > right.x) right = node;
});

const height = right.x - left.x + margin.top + margin.bottom;
const transition = svg.transition()
.duration(duration)
.attr("viewBox", [-margin.left, left.x - margin.top, width, height])
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));

// Update the nodes…
const node = gNode.selectAll("g")
.data(nodes, d => d.id);

// Enter any new nodes at the parent's previous position.
const nodeEnter = node.enter().append("g")
.attr("transform", d => `translate(${source.y0},${source.x0})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.on("click", (event, d) => {
// console.log(d.data.labels);
d.children = d.children ? null : d._children;
update(d);
// selectImage(d);
updateLinks(d);
});

nodeEnter.append("circle")
.attr("r", 6)
.attr("fill", d => d._children ? "#555" : "#999")
.attr("stroke-width", 15);

nodeEnter.append("text")
.attr("dy", "-0.31em")
.attr("x", -40)
.attr("text-anchor", "start")
.text(d => d.data.labels ? d.data.labels.slice(0, 2).join(', ') : "")
.clone(true).lower()
.attr("stroke-linejoin", "round")
.attr("stroke-width", 5)
.attr("stroke", "white");
// Add title element to show labels information
nodeEnter.append("title")
.text(d => {
let labelStr = d.data.labels ? d.data.labels.join(", ") : "";
let simStr = d.data.similarity_score ? "\nSimilarity: " + calculateAverageSimilarity(d.data) : "";
let imgTopicStr = d.data.img_tid ? ", Image Topic#: " + d.data.img_tid : "";
return labelStr + simStr + imgTopicStr;
});


// Transition nodes to their new position.
const nodeUpdate = node.merge(nodeEnter).transition(transition)
.attr("transform", d => `translate(${d.y},${d.x})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);

// Transition exiting nodes to the parent's new position.
const nodeExit = node.exit().transition(transition).remove()
.attr("transform", d => `translate(${source.y},${source.x})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);

const diagonal = d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x);
// Update the links…
const link = gLink.selectAll("path")
.data(links, d => d.target.id);

// Enter any new links at the parent's previous position.
const linkEnter = link.enter().append("path")
.attr("d", d => {
const o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
}).attr("stroke", d => d.target.data.similarity_score ?
simColorScale(calculateAverageSimilarity(d.target.data)) : avgColorScale(avgSimilarity[d.target.data.id]))
.attr("stroke-width", linkStrokeWidthBase)
.attr("fill", "none")
.attr("stroke-linejoin", "round");

// Transition links to their new position.
link.merge(linkEnter).transition(transition)
.attr("d", diagonal);


// Transition exiting nodes to the parent's new position.
link.exit().transition(transition).remove()
.attr("d", d => {
const o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
});

// Stash the old positions for transition.
root.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}

update(root);

// dispatchImg.on("img-clicked", (imageTopic) => {
// // console.log(imageTopic);
// });
return svg.node();
}
Insert cell
margin = ({top: 25, right: 50, bottom: 10, left: 7.5})
Insert cell
dy = width / 2.4
Insert cell
dx = 50
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
dispatch = d3.dispatch("circle-clicked");

Insert cell
dispatchImg = d3.dispatch("img-clicked");
Insert cell
diagonal = d3.linkHorizontal().x(d => d.y).y(d => d.x)
Insert cell
tree = d3.tree().nodeSize([dx, dy])
Insert cell
groupedData = d3.group(imageCsv, d => d.topic);
Insert cell
filteredImageTopics = new Map([...groupedData].filter(([key, value]) => uniqueImageTopics.includes(key)));
Insert cell
randomImageByTopicsMap = new Map(
Array.from(filteredImageTopics.entries()).map(([key, values]) => {
if (values.length < ex2.imagePerTopics) {
return [key, values];
}
return [key, d3.shuffle(values).slice(0, ex2.imagePerTopics)];
})
);
Insert cell
flattenedRandomSelection = Array.from(randomImageByTopicsMap, ([key, values]) =>
values.map(value => ({ ...value }))
).flatMap(array => array);
Insert cell
imageCsv[0]
Insert cell
imageCsv = d3.csv(
"https://raw.githubusercontent.com/AI-EnabledSoftwareEngineering-AISE/RE4VIS/main/data/images_df_av.csv",
d3.autoType
)
Insert cell
uniqueImageTopics = Array.from(new Set(filteredData.flatMap(obj => obj.children.map(child => child.img_tid))));

Insert cell
filteredData_old = sampleData.children.map(child => {
if (child.children) {
let filteredChildren = child.children.filter(grandchild => {
return grandchild.similarity && grandchild.similarity >= ex2.threshold;
});
return {...child, children: filteredChildren};
} else {
return child;
}
});
Insert cell
filteredData = sampleData.children.map(child => {
if (child.children) {
const filteredChildren = child.children.filter(grandchild => {
return grandchild.similarity_score.some(score => score >= ex2.threshold);
}).map(grandchild => ({
...grandchild,
img_ids: grandchild.img_ids.filter((id, index) => grandchild.similarity_score[index] >= ex2.threshold),
similarity_score: grandchild.similarity_score.filter(score => score >= ex2.threshold)
}));
return { ...child, children: filteredChildren };
} else {
return child;
}
});


Insert cell
finalJson = {
return {
name:"",
children:filteredData
}
}
Insert cell
avgSimilarity_old = finalJson.children.map(entry => {
const sum = entry.children.reduce((acc, child) => acc + child.similarity, 0);
const avg = sum / entry.children.length || 0; // to avoid division by zero
// return { id: entry.id, avgSimilarity: avg };
return avg ;
});
Insert cell
avgSimilarity = finalJson.children.map(entry => {
const avgScores = entry.children.map(child => {
const sum = child.similarity_score.reduce((acc, score) => acc + score, 0);
return sum / child.similarity_score.length || 0; // to avoid division by zero
});

const totalAvg = avgScores.reduce((acc, avg) => acc + avg, 0);
return totalAvg / avgScores.length || 0; // to avoid division by zero
});
Insert cell
similarities = finalJson.children.flatMap(d =>
d.children.flatMap(c => c.similarity_score)
).sort();
Insert cell
similarities_old = finalJson.children.flatMap(d => d.children.map(c => c.similarity)).sort()
Insert cell
avgSimilaritiesSorted = [...avgSimilarity].sort()
Insert cell
// gLink.selectAll("path")
// .filter(link => !(ancestors.includes(link.source) && ancestors.includes(link.target)))
// .attr("stroke", node => node.target.data.similarity_score ?
// simColorScale(node.target.data.similarity_score) : avgColorScale(avgSimilarity[node.target.data.id]))
// .attr("stroke-width", linkStrokeWidthBase)
// .attr("stroke-opacity", 0.7);
Insert cell
// Create the quantile scale
// https://observablehq.com/@d3/color-schemes
avgColorScale = d3.scaleQuantile()
.domain([d3.quantileSorted(avgSimilaritiesSorted, 0.25), d3.quantileSorted(avgSimilaritiesSorted, 0.5), d3.quantileSorted(avgSimilaritiesSorted, 0.75)])
.range(["#9ebcda", "#6baed6", "#4292c6", "#08519c"]);

Insert cell
simColorScale = d3.scaleQuantile()
.domain([d3.quantileSorted(similarities, 0.25), d3.quantileSorted(similarities, 0.5), d3.quantileSorted(similarities, 0.75)])
.range(["#9ebcda", "#6baed6", "#4292c6", "#08519c"]);
Insert cell
avgWidthScale = d3.scaleQuantile()
.domain([d3.quantileSorted(avgSimilaritiesSorted, 0.25), d3.quantileSorted(avgSimilaritiesSorted, 0.5), d3.quantileSorted(avgSimilaritiesSorted, 0.75)])
.range([1.5, 2.5, 3.5, 4.5]);

Insert cell
simWidthScale = d3.scaleQuantile()
.domain([d3.quantileSorted(similarities, 0.25), d3.quantileSorted(similarities, 0.5), d3.quantileSorted(similarities, 0.75)])
.range([1.5, 2.5, 3.5, 4.5]);

Insert cell
maxSimilarity = d3.max(sampleData.children, d =>
d3.max(d.children, child => d3.max(child.similarity_score))
);
Insert cell
minSimilarity = d3.min(sampleData.children, d =>
d3.min(d.children, child => d3.min(child.similarity_score))
);
Insert cell
// minSimilarity = d3.min(sampleData.children, d => d3.min(d.children, child => child.similarity));
Insert cell
stepValue = (maxSimilarity - minSimilarity) / 10
Insert cell
initialValue = minSimilarity + 7* stepValue
Insert cell
sampleData = d3.json("https://raw.githubusercontent.com/AI-EnabledSoftwareEngineering-AISE/RE4VIS/main/data/flare_av.json")
Insert cell
getIndexOfObjectWithTopic(7)
Insert cell
function getIndexOfObjectWithTopic(topic) {
for (let i = 0; i < flattenedRandomSelection.length; i++) {
if (flattenedRandomSelection[i].topic === topic) {
return i;
}
}
return -1; // topic not found in any object in data
}
Insert cell
getImgTid(1)
Insert cell
function getImgTid(name) {
let maxSim = -Infinity;
let imgTid = null;
if (name === '') {
// Search for entry with the highest similarity
finalJson.children.forEach(child => {
child.children.forEach(grandchild => {
if (grandchild.similarity > maxSim) {
maxSim = grandchild.similarity;
imgTid = grandchild.img_tid;
}
});
});
} else {
const foundGrandchild = finalJson.children.flatMap(child => child.children).find(grandchild => grandchild.id == name);
if (foundGrandchild) {
imgTid = foundGrandchild.img_tid;
} else {
const foundChild = finalJson.children.find(child => child.name == name);
if (foundChild) {
foundChild.children.forEach(grandchild => {
if (grandchild.similarity > maxSim) {
maxSim = grandchild.similarity;
imgTid = grandchild.img_tid;
}
});
}
}
}
return imgTid;
}
Insert cell
Insert cell
// margin = ({top: 10, right: 120, bottom: 10, left: 40})
Insert cell
d3 = require("d3@6")
Insert cell
import {render, component, jsx, memo, forwardRef, React, ReactDOM, createElement, Children, createRef, createContext, lazy, Fragment, StrictMode, Suspense, cloneElement, useCallback, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useDebugValue, createPortal, __SECRET_SWITCHER} from "@j-f1/react"
Insert cell
import {Range, pack} from "@esperanc/range-input-variations"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more