{"spec_id":"sequence-logo-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// sequence-logo-basic: Sequence Logo for Motif Visualization\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// A 10-position DNA transcription-factor binding motif (TATA-box-like core),\n// highly conserved at positions 2-6, more variable at the flanks, and a\n// zero-information (perfectly flat) position 8 to show a position with no\n// informative letters — the standard behaviour of a real sequence logo.\nconst letters = [\"A\", \"C\", \"G\", \"T\"];\nconst freqMatrix = [\n  [0.7, 0.1, 0.1, 0.1],\n  [0.05, 0.05, 0.05, 0.85],\n  [0.85, 0.05, 0.05, 0.05],\n  [0.05, 0.05, 0.05, 0.85],\n  [0.8, 0.05, 0.05, 0.1],\n  [0.75, 0.08, 0.07, 0.1],\n  [0.4, 0.15, 0.15, 0.3],\n  [0.25, 0.25, 0.25, 0.25],\n  [0.4, 0.2, 0.15, 0.25],\n  [0.45, 0.25, 0.15, 0.15],\n];\nconst positions = freqMatrix.map((_, i) => String(i + 1));\nconst MAX_BITS = 2; // log2(4) — the DNA information-content ceiling\n\n// Shannon information content per position, then each letter's stack height\n// (frequency * information content), sorted ascending so the most frequent\n// letter lands on top of the stack.\nconst stacks = freqMatrix.map((freqs) => {\n  const entropy = freqs.reduce((sum, p) => (p > 0 ? sum - p * Math.log2(p) : sum), 0);\n  const infoContent = Math.max(0, MAX_BITS - entropy);\n  return letters\n    .map((letter, i) => ({ letter, height: freqs[i] * infoContent }))\n    .sort((a, b) => a.height - b.height);\n});\n\n// Standard DNA color convention (A green / C blue / G orange / T red),\n// mapped onto the closest Imprint categorical hues (semantic exception).\nconst LETTER_COLOR = {\n  A: t.palette[0], // brand green\n  C: t.palette[2], // blue\n  G: t.palette[3], // ochre (orange)\n  T: t.palette[4], // matte red\n};\n\n// --- Chart -------------------------------------------------------------------\n// No data series is used for the visible marks — the axes only establish the\n// position/bits coordinate system. Each letter is drawn as an SVG glyph via\n// the core Highcharts renderer, scaled with translateX/Y + scaleX/Y so it\n// exactly fills its allocated position/height box (per spec: \"letters\n// rendered as scaled glyphs, stretched to fill their allocated height\").\nlet glyphs = [];\n\n// Very-low-information letters (e.g. minor bases at positions 9-10) would\n// otherwise scale down to a near-invisible sliver. Give every drawn letter a\n// minimum on-screen height, converted from px into bits via the y-axis\n// scale, so it still reads as a distinct glyph. The floor is applied to the\n// stacking cursor too (not just the render box), so heights stay additive\n// and no glyph overlaps its neighbor.\nconst MIN_GLYPH_PX = 8;\n\nfunction drawLetters(chart) {\n  glyphs.forEach((el) => el.destroy());\n  glyphs = [];\n\n  const xAxis = chart.xAxis[0];\n  const yAxis = chart.yAxis[0];\n  const bandWidth = xAxis.toPixels(1) - xAxis.toPixels(0);\n  const letterWidth = bandWidth * 0.8;\n  const pxPerBit = yAxis.toPixels(0) - yAxis.toPixels(1);\n  const minBits = MIN_GLYPH_PX / pxPerBit;\n\n  stacks.forEach((stack, i) => {\n    const xCenter = xAxis.toPixels(i);\n    const boxLeft = xCenter - letterWidth / 2;\n    let bottomBits = 0;\n\n    stack.forEach(({ letter, height }) => {\n      if (height > 0.01) {\n        const renderHeight = Math.max(height, minBits);\n        const topBits = bottomBits + renderHeight;\n        const yTop = yAxis.toPixels(topBits);\n        const yBottom = yAxis.toPixels(bottomBits);\n        const boxHeight = yBottom - yTop;\n\n        const glyph = chart.renderer\n          .text(letter, 0, 0)\n          .attr({ x: 0, y: 0, fill: LETTER_COLOR[letter] })\n          .css({ fontFamily: \"Arial, Helvetica, sans-serif\", fontWeight: \"700\" })\n          .add();\n\n        const bbox = glyph.getBBox(true);\n        const scaleX = letterWidth / bbox.width;\n        const scaleY = boxHeight / bbox.height;\n        glyph.attr({\n          translateX: boxLeft - scaleX * bbox.x,\n          translateY: yTop - scaleY * bbox.y,\n          scaleX,\n          scaleY,\n        });\n        glyphs.push(glyph);\n        bottomBits += renderHeight;\n      } else {\n        bottomBits += height;\n      }\n    });\n  });\n}\n\nHighcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: { render() { drawLetters(this); } },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"sequence-logo-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    categories: positions,\n    min: 0,\n    max: positions.length - 1,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    title: { text: \"Position\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    // Subtle band calling out the highly-conserved TATA-box-like core\n    // (positions 2-6) so the motif's story reads at a glance, without\n    // competing with the letter glyphs drawn on top.\n    plotBands: [\n      {\n        from: 0.5,\n        to: 5.5,\n        color: `${t.amber}1f`,\n        label: {\n          text: \"Conserved core\",\n          align: \"center\",\n          verticalAlign: \"top\",\n          y: 14,\n          style: { color: t.inkSoft, fontSize: \"12px\" },\n        },\n      },\n    ],\n  },\n  yAxis: {\n    min: 0,\n    max: MAX_BITS,\n    tickInterval: 0.5,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    title: { text: \"Information content (bits)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n  },\n  legend: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  // Invisible series: gives Highcharts real data extremes so it reserves\n  // proper margins for the axis titles/labels (letters are drawn separately).\n  series: [\n    {\n      type: \"scatter\",\n      data: positions.map((_, i) => [i, 0]),\n      marker: { enabled: false },\n      lineWidth: 0,\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n  ],\n});\n"}