{"spec_id":"sequence-logo-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// sequence-logo-basic: Sequence Logo for Motif Visualization\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 60, bottom: 100, left: 120 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: relative letter frequencies at each aligned position of a\n// synthetic transcription-factor binding-site motif (10 positions, DNA) -----\nconst motif = [\n  { position: 1, freqs: { A: 0.7, C: 0.1, G: 0.1, T: 0.1 } },\n  { position: 2, freqs: { A: 0.05, C: 0.05, G: 0.8, T: 0.1 } },\n  { position: 3, freqs: { A: 0.05, C: 0.05, G: 0.05, T: 0.85 } },\n  { position: 4, freqs: { A: 0.3, C: 0.25, G: 0.25, T: 0.2 } },\n  { position: 5, freqs: { A: 0.1, C: 0.75, G: 0.1, T: 0.05 } },\n  { position: 6, freqs: { A: 0.9, C: 0.04, G: 0.03, T: 0.03 } },\n  { position: 7, freqs: { A: 0.4, C: 0.3, G: 0.2, T: 0.1 } },\n  { position: 8, freqs: { A: 0.2, C: 0.1, G: 0.65, T: 0.05 } },\n  { position: 9, freqs: { A: 0.1, C: 0.25, G: 0.1, T: 0.55 } },\n  { position: 10, freqs: { A: 0.28, C: 0.27, G: 0.24, T: 0.21 } },\n];\n\n// Shannon-entropy information content: DNA alphabet ceiling is log2(4) = 2 bits.\nconst MAX_BITS = 2;\nconst infoContent = (freqs) => {\n  const entropy = Object.values(freqs).reduce((sum, p) => (p > 0 ? sum - p * Math.log2(p) : sum), 0);\n  return MAX_BITS - entropy;\n};\n\nconst stacks = motif.map((d) => {\n  const infoBits = infoContent(d.freqs);\n  const letters = Object.entries(d.freqs)\n    .sort((a, b) => a[1] - b[1]) // ascending: least-frequent letter drawn first (bottom), most-frequent lands on top\n    .map(([letter, freq]) => ({ letter, height: freq * infoBits }));\n  return { position: d.position, infoBits, letters };\n});\n\n// Most-conserved position (highest information content) — storytelling focal point.\nconst peak = stacks.reduce((max, s) => (s.infoBits > max.infoBits ? s : max));\n\n// Standard nucleotide color convention (A green / C blue / G ochre / T red),\n// mapped onto the closest Imprint hues — a domain-convention semantic exception.\nconst LETTER_COLOR = { A: t.palette[0], C: t.palette[2], G: t.palette[3], T: t.palette[4] };\n\n// Distinctive typeface pairing: serif display face for the title, workhorse\n// sans-serif for chrome/glyphs.\nconst TITLE_FONT = \"Georgia, 'Times New Roman', serif\";\nconst CHROME_FONT = \"-apple-system, 'Segoe UI', Helvetica, Arial, sans-serif\";\n\n// --- Scales ------------------------------------------------------------\nconst x = d3\n  .scaleBand()\n  .domain(motif.map((d) => d.position))\n  .range([0, iw])\n  .padding(0.18);\nconst y = d3.scaleLinear().domain([0, MAX_BITS]).range([ih, 0]);\n\n// --- SVG mount -----------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Subtle y-axis gridlines only (bar-like stacked chart)\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(4))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Letter stacks: glyphs stretched to fill their allocated cell ----------\nconst colWidth = x.bandwidth();\nfor (const stack of stacks) {\n  let cumulative = 0;\n  const colX = x(stack.position);\n  for (const item of stack.letters) {\n    const yTop = y(cumulative + item.height);\n    const yBottom = y(cumulative);\n    const pixelHeight = yBottom - yTop;\n    cumulative += item.height;\n    if (pixelHeight <= 0) continue;\n\n    const glyph = g\n      .append(\"text\")\n      .attr(\"x\", 0)\n      .attr(\"y\", 0)\n      .style(\"font-family\", CHROME_FONT)\n      .style(\"font-weight\", \"900\")\n      .style(\"fill\", LETTER_COLOR[item.letter])\n      .text(item.letter);\n\n    const bbox = glyph.node().getBBox();\n    const scaleX = colWidth / bbox.width;\n    const scaleY = pixelHeight / bbox.height;\n    const tx = colX - bbox.x * scaleX;\n    const ty = yTop - bbox.y * scaleY;\n    glyph.attr(\"transform\", `translate(${tx},${ty}) scale(${scaleX},${scaleY})`);\n  }\n}\n\n// --- Axes ------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickSize(0).tickPadding(12));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-family\", CHROME_FONT).style(\"font-size\", \"16px\");\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(4).tickSize(0).tickPadding(12));\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-family\", CHROME_FONT).style(\"font-size\", \"16px\");\nyAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Axis labels -------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-family\", CHROME_FONT)\n  .style(\"font-size\", \"18px\")\n  .text(\"Position\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -85)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-family\", CHROME_FONT)\n  .style(\"font-size\", \"18px\")\n  .text(\"Information content (bits)\");\n\n// --- Storytelling callout: flag the single most-conserved position ---------\nconst peakX = x(peak.position) + colWidth / 2;\nconst peakTopY = y(peak.infoBits);\n\ng.append(\"line\")\n  .attr(\"x1\", peakX)\n  .attr(\"x2\", peakX)\n  .attr(\"y1\", peakTopY - 34)\n  .attr(\"y2\", peakTopY - 6)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"3,3\");\n\ng.append(\"text\")\n  .attr(\"x\", peakX)\n  .attr(\"y\", peakTopY - 42)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-family\", CHROME_FONT)\n  .style(\"font-style\", \"italic\")\n  .style(\"font-size\", \"14px\")\n  .text(\"Most conserved position\");\n\n// --- Title ---------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-family\", TITLE_FONT)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"700\")\n  .text(\"sequence-logo-basic · javascript · d3 · anyplot.ai\");\n"}