{"spec_id":"sequence-logo-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// sequence-logo-basic: Sequence Logo for Motif Visualization\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// 12-position DNA motif: degenerate flanks around a conserved CACGTG (E-box) core\nconst positions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];\nconst motif = [\n  { A: 0.28, C: 0.24, G: 0.22, T: 0.26 },\n  { A: 0.32, C: 0.2, G: 0.18, T: 0.3 },\n  { A: 0.15, C: 0.55, G: 0.1, T: 0.2 },\n  { A: 0.05, C: 0.85, G: 0.05, T: 0.05 },\n  { A: 0.8, C: 0.07, G: 0.07, T: 0.06 },\n  { A: 0.04, C: 0.88, G: 0.04, T: 0.04 },\n  { A: 0.05, C: 0.05, G: 0.85, T: 0.05 },\n  { A: 0.06, C: 0.06, G: 0.06, T: 0.82 },\n  { A: 0.04, C: 0.05, G: 0.87, T: 0.04 },\n  { A: 0.2, C: 0.3, G: 0.25, T: 0.25 },\n  { A: 0.3, C: 0.22, G: 0.2, T: 0.28 },\n  { A: 0.27, C: 0.25, G: 0.23, T: 0.25 },\n];\nconst MAX_BITS = 2; // DNA: log2(4 letters)\n\n// DNA colors — semantic exception: standard A/C/G/T convention mapped onto Imprint\nconst LETTER_COLOR = {\n  A: t.palette[0],\n  C: t.palette[2],\n  G: t.palette[3],\n  T: t.palette[4],\n};\n\n// Approximate Helvetica/Arial-Bold uppercase glyph metrics (AFM widths, cap height)\n// as a fraction of font size — used to stretch each glyph to fill its cell exactly.\nconst GLYPH_WIDTH_RATIO = { A: 0.722, C: 0.722, G: 0.778, T: 0.611 };\nconst CAP_HEIGHT_RATIO = 0.718;\nconst REFERENCE_FONT_SIZE = 100;\n\n// Below this rendered height, four stacked glyphs smear into an illegible\n// band — draw a thin color-coded sliver instead so the stack stays honest.\nconst MIN_LETTER_HEIGHT = 8;\n// Below this, the letter is still drawn but gets an ink-color stroke: the\n// muted ochre/matte-red hues fall under the WCAG contrast floor and need the\n// extra edge right where the small scale already strains legibility.\nconst MIN_STROKE_HEIGHT = 24;\n\n// Per position: Shannon-entropy information content (bits), letters stacked\n// bottom-to-top in ascending frequency order (so the most frequent letter is on top).\nconst stackData = [];\npositions.forEach((pos, posIdx) => {\n  const freqs = motif[posIdx];\n  const entropy = Object.values(freqs).reduce(\n    (sum, f) => (f > 0 ? sum - f * Math.log2(f) : sum),\n    0,\n  );\n  const infoContent = MAX_BITS - entropy;\n  const orderedLetters = Object.keys(freqs).sort((a, b) => freqs[a] - freqs[b]);\n  let cumulative = 0;\n  orderedLetters.forEach((letter) => {\n    const freq = freqs[letter];\n    const height = infoContent * freq;\n    const y0 = cumulative;\n    const y1 = cumulative + height;\n    cumulative = y1;\n    stackData.push([posIdx, y0, y1, letter, freq]);\n  });\n});\n\n// --- Custom render: stretch each letter glyph to fill its stack cell ------------\nfunction renderItem(params, api) {\n  const posIdx = api.value(0);\n  const y0 = api.value(1);\n  const y1 = api.value(2);\n  const letter = api.value(3);\n\n  const bottomLeft = api.coord([posIdx, y0]);\n  const topLeft = api.coord([posIdx, y1]);\n  const bandWidth = api.size([1, 0])[0];\n\n  const targetHeight = bottomLeft[1] - topLeft[1];\n  const targetWidth = bandWidth * 0.82;\n  if (targetHeight < 0.5 || targetWidth < 1) {\n    return { type: \"text\", style: { text: \"\" } };\n  }\n\n  const centerX = bottomLeft[0];\n  const centerY = (bottomLeft[1] + topLeft[1]) / 2;\n\n  // Near-zero-information positions: four glyphs this small would overlap\n  // into a muddy smear, so show a plain color-coded sliver instead.\n  if (targetHeight < MIN_LETTER_HEIGHT) {\n    return {\n      type: \"rect\",\n      shape: {\n        x: centerX - targetWidth / 2,\n        y: topLeft[1],\n        width: targetWidth,\n        height: targetHeight,\n      },\n      style: { fill: LETTER_COLOR[letter] },\n    };\n  }\n\n  const scaleX =\n    targetWidth / (REFERENCE_FONT_SIZE * GLYPH_WIDTH_RATIO[letter]);\n  const scaleY = targetHeight / (REFERENCE_FONT_SIZE * CAP_HEIGHT_RATIO);\n  // Stroke width is drawn in the pre-transform coordinate space, so\n  // compensate for the scaleY shrink to keep the rendered edge ~1px.\n  const needsStroke = targetHeight < MIN_STROKE_HEIGHT;\n  const strokeWidth = needsStroke ? 1 / scaleY : 0;\n\n  return {\n    type: \"text\",\n    x: centerX,\n    y: centerY,\n    scaleX: scaleX,\n    scaleY: scaleY,\n    style: {\n      text: letter,\n      fontSize: REFERENCE_FONT_SIZE,\n      fontWeight: \"bold\",\n      fontFamily: \"Arial, Helvetica, sans-serif\",\n      fill: LETTER_COLOR[letter],\n      stroke: needsStroke ? t.ink : \"none\",\n      lineWidth: strokeWidth,\n      textAlign: \"center\",\n      textVerticalAlign: \"middle\",\n    },\n  };\n}\n\n// --- Init ---------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"sequence-logo-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    textStyle: { color: t.ink },\n    formatter: (params) => {\n      const [posIdx, , , letter, freq] = params.value;\n      return `Position ${posIdx + 1}<br/>${letter}: ${Math.round(freq * 100)}%`;\n    },\n  },\n  grid: { left: 100, right: 60, top: 110, bottom: 100, containLabel: true },\n  xAxis: {\n    type: \"category\",\n    data: positions.map(String),\n    name: \"Position\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    min: 0,\n    max: MAX_BITS,\n    name: \"Information content (bits)\",\n    nameLocation: \"middle\",\n    nameGap: 55,\n    nameRotate: 90,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      renderItem: renderItem,\n      clip: true,\n      data: stackData,\n    },\n  ],\n});\n"}