fractalkitty looking for math? fractalkitty.com

Coded Poems

haiku explorer

#random#trig#physics#text#interactive#animation#svg#dom#data#poem#tool

data from: https://www.fractalkitty.com/serendipitous-oulipo-haiku/

getOptimalDimensionspaddingsvghaikuDataGlobalcreateVisualizationrootpackrootNodenodescolorScalecircleshighlightSharedHaikusresetHighlightingshowHaikuCloudmodalSizemodalSvgcenterXcenterYradiuscenterGroupsimulationnodeGroupshaikuIndexhaiku

const getOptimalDimensions = () => {
  const padding = window.innerWidth <= 480 ? 10 : 40;
  return {
    width: window.innerWidth - padding,
    height: window.innerHeight - padding
  };
};
let {
  width,
  height
} = getOptimalDimensions();
const svg = d3.select("#main-viz")
  .attr("viewBox", `0 0 ${width} ${height}`)
  .attr("preserveAspectRatio", "xMidYMid meet")
  .style("width", "100%")
  .style("height", "100vh");
let haikuDataGlobal = [];
Promise.all([
  d3.json("/assets/haiku_data.json"),
  d3.json("/assets/haiku_word_cloud.json")
]).then(([haikuData, cloudData]) => {
  haikuDataGlobal = haikuData.haikus;
  createVisualization(cloudData);
}).catch(error => console.error("Error loading data:", error));

function createVisualization(data) {
  const root = {
    name: "words",
    children: data.map(d => ({
      name: d.text,
      value: d.size,
      haiku_indices: d.haiku_indices
    }))
  };
  const pack = d3.pack()
    .size([width, height])
    .padding(3);
  const rootNode = d3.hierarchy(root)
    .sum(d => d.value)
    .sort((a, b) => b.value - a.value);
  const nodes = pack(rootNode).descendants().slice(1);
  const colorScale = d3.scaleSequential()
    .domain([0, d3.max(data, d => d.size)])
    .interpolator(d3.interpolateBlues);
  const circles = svg.selectAll("g")
    .data(nodes)
    .join("g")
    .attr("transform", d => `translate(${d.x},${d.y})`);
  circles.append("circle")
    .attr("class", "word-circle")
    .attr("r", d => d.r)
    .style("fill", d => colorScale(d.data.value))
    .on("click", (event, d) => {
      showHaikuCloud(d.data.name, d.data.value, d.data.haiku_indices);
    })
    .on("mouseover", (event, d) => {
      highlightSharedHaikus(d.data.haiku_indices);
    })
    .on("mouseout", () => {
      resetHighlighting();
    });
  circles.append("text")
    .attr("class", "word-label")
    .attr("text-anchor", "middle")
    .attr("dy", "0.3em")
    .style("font-size", d => Math.min(d.r * 0.8, 16) + "px")
    .style("fill", d => d.data.value > d3.max(data, d => d.size) / 2 ? "white" : "black")
    .text(d => d.data.name);

  function highlightSharedHaikus(haikuIndices) {
    svg.selectAll(".word-circle")
      .classed("dimmed", true)
      .classed("highlighted", false);
    svg.selectAll("g")
      .filter(d => {
        if (!d || !d.data || !d.data.haiku_indices) return false;
        return d.data.haiku_indices.some(index =>
          haikuIndices.includes(index)
        );
      })
      .select(".word-circle")
      .classed("dimmed", false)
      .classed("highlighted", true);
  }

  function resetHighlighting() {
    svg.selectAll(".word-circle")
      .classed("dimmed", false)
      .classed("highlighted", false);
  }
}

function showHaikuCloud(word, size, haikuIndices) {
  document.getElementById("haiku-text").innerHTML = "";
  modal.style.display = "block";
  modal.offsetHeight;
  modal.classList.add("visible");
  const modalSize = window.innerWidth <= 480 ?
    Math.min(350, window.innerWidth * 0.9, window.innerHeight * 0.7) :
    Math.min(500, window.innerWidth * 0.9, window.innerHeight * 0.8);
  const modalSvg = d3.select("#modal-viz")
    .attr("width", modalSize)
    .attr("height", modalSize);
  modalSvg.selectAll("*").remove();
  const centerX = modalSize / 2;
  const centerY = modalSize / 2;
  const radius = modalSize * 0.35;
  const centerGroup = modalSvg.append("g")
    .attr("transform", `translate(${centerX}, ${centerY})`);
  centerGroup.append("circle")
    .attr("r", modalSize * 0.15)
    .style("fill", d3.interpolateBlues(0.7));
  centerGroup.append("text")
    .attr("class", "center-word")
    .attr("dy", "-0.2em")
    .style("fill", "white")
    .style("font-size", "24px")
    .style("font-weight", "bold")
    .text(word);
  centerGroup.append("text")
    .attr("class", "center-count")
    .attr("dy", "1.2em")
    .style("fill", "white")
    .text(`${size} times`);
  const nodes = haikuIndices.map(index => ({
    index: index,
    x: centerX + Math.cos(Math.random() * 2 * Math.PI) * radius,
    y: centerY + Math.sin(Math.random() * 2 * Math.PI) * radius,
    r: modalSize * 0.04
  }));
  const simulation = d3.forceSimulation(nodes)
    .force("center", d3.forceCenter(centerX, centerY))
    .force("charge", d3.forceManyBody().strength(-50))
    .force("collide", d3.forceCollide().radius(d => d.r * 1.5))
    .force("radial", d3.forceRadial(radius * 0.6, centerX, centerY).strength(1))
    .on("tick", updatePositions);
  const nodeGroups = modalSvg.selectAll(".haiku-node")
    .data(nodes)
    .join("g")
    .attr("class", "haiku-node");
  nodeGroups.append("circle")
    .attr("r", d => d.r)
    .style("fill", d3.interpolateBlues(0.3))
    .style("cursor", "pointer")
    .on("mouseover", function(event, d) {
      d3.select(this).style("fill", d3.interpolateBlues(0.6));
      const haikuIndex = haikuIndices[d.index];
      const haiku = haikuDataGlobal.find(h => Number(h.index) === haikuIndex);
      const haikuText = haiku ? haiku.text.replace(/\n/g, "<br><br>") : "<em>Haiku not found</em>";
      document.getElementById("haiku-text").innerHTML = `<strong>Haiku #${haikuIndex}</strong><br>${haikuText}`;
    })
    .on("mouseout", function() {
      d3.select(this).style("fill", d3.interpolateBlues(0.3));
    });
  nodeGroups.append("text")
    .attr("text-anchor", "middle")
    .attr("dy", "0.35em")
    .style("font-size", d => `${Math.max(d.r * 0.4, 10)}px`)
    .text(d => haikuIndices[d.index]);

  function updatePositions() {
    nodeGroups.attr("transform", d => `translate(${d.x},${d.y})`);
  }
  const drag = d3.drag()
    .on("start", (event, d) => {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    })
    .on("drag", (event, d) => {
      d.fx = event.x;
      d.fy = event.y;
    })
    .on("end", (event, d) => {
      if (!event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    });
  nodeGroups.call(drag);
}

function closeModal() {
  modal.classList.remove("visible");
  document.querySelector(".modal-content").classList.remove("visible");
  setTimeout(() => {
    modal.style.display = "none";
  }, 300);
}
document.querySelector(".close").onclick = closeModal;
window.onclick = (event) => {
  if (event.target === modal) closeModal();
};
let resizeTimeout;
window.addEventListener("resize", () => {
  clearTimeout(resizeTimeout);
  resizeTimeout = setTimeout(() => {
    const {
      width: newWidth,
      height: newHeight
    } = getOptimalDimensions();
    width = newWidth;
    height = newHeight;
    svg.attr("viewBox", `0 0 ${width} ${height}`);
  }, 250);
});