{"spec_id":"heatmap-rainflow","library":"echarts","language":"javascript","code":"// anyplot.ai\n// heatmap-rainflow: Rainflow Counting Matrix for Fatigue Analysis\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-08-25\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: rainflow counting matrix -----------------------------------------\n// Simulated rainflow cycle counting from a wind-turbine blade-root flapwise\n// bending-moment load history: many small-amplitude cycles spread across a\n// wide range of mean values, progressively fewer and more mean-centered\n// cycles as the amplitude grows (the classic \"diamond\" shape of a measured\n// variable-amplitude load spectrum).\nlet seed = 1337;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst N_AMP = 20;\nconst N_MEAN = 20;\nconst AMP_STEP = 10; // kN·m\nconst MEAN_STEP = 10; // kN·m\nconst ampCenters = Array.from({ length: N_AMP }, (_, i) => 5 + i * AMP_STEP); // 5..195 kN·m\nconst meanCenters = Array.from({ length: N_MEAN }, (_, i) => -95 + i * MEAN_STEP); // -95..95 kN·m\n\n// Physical envelope constraint: a cycle's peak and valley (mean +/- amplitude)\n// are bounded by the blade root's overall load range, so |mean| <= ENV_LIMIT -\n// amplitude. This is what produces the classic tapering \"diamond\" shape of a\n// measured rainflow matrix — wide mean spread at low amplitude, narrowing to\n// zero mean spread at the highest amplitudes.\nconst ENV_LIMIT = 150; // kN·m\nconst AMP_DECAY = 70; // kN·m, controls how fast cycle counts fall off with amplitude\nconst PEAK_COUNT = 4500;\n\nconst cells = [];\nlet maxCount = 0;\nfor (let ai = 0; ai < N_AMP; ai++) {\n  const amp = ampCenters[ai];\n  const meanLimit = ENV_LIMIT - amp;\n  const ampFactor = Math.exp(-amp / AMP_DECAY);\n  for (let mi = 0; mi < N_MEAN; mi++) {\n    const mean = meanCenters[mi];\n    let count = 0;\n    if (meanLimit > 0) {\n      // Parabolic falloff within the envelope: peaks at mean=0, reaches\n      // exactly 0 at the envelope edges +/- meanLimit.\n      const meanFactor = Math.max(0, 1 - (mean / meanLimit) ** 2);\n      const jitter = 0.75 + 0.5 * lcg();\n      count = Math.round(PEAK_COUNT * ampFactor * meanFactor * jitter);\n    }\n    if (count > maxCount) maxCount = count;\n    // Zero/near-zero bins are skipped entirely so they stay visually distinct\n    // (transparent background) rather than rendered as a \"dark zero\" cell.\n    if (count > 2) {\n      cells.push([mi, ai, Math.log10(count + 1)]);\n    }\n  }\n}\nconst maxLog = Math.log10(maxCount + 1);\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: \"heatmap-rainflow · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  tooltip: {\n    formatter: (p) =>\n      `Mean: ${meanCenters[p.value[0]]} kN·m<br/>Amplitude: ${ampCenters[p.value[1]]} kN·m<br/>Cycles: ${Math.round(10 ** p.value[2] - 1)}`,\n  },\n  grid: {\n    left: 150,\n    right: 260,\n    top: 140,\n    bottom: 150,\n  },\n  xAxis: {\n    type: \"category\",\n    data: meanCenters,\n    name: \"Mean bending moment (kN·m)\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 18 },\n    axisLabel: { color: t.inkSoft, fontSize: 13, interval: 1, rotate: 45 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitArea: { show: false },\n  },\n  yAxis: {\n    type: \"category\",\n    data: ampCenters,\n    name: \"Cycle amplitude, half-range (kN·m)\",\n    nameLocation: \"middle\",\n    nameGap: 90,\n    nameTextStyle: { color: t.ink, fontSize: 18 },\n    axisLabel: { color: t.inkSoft, fontSize: 13, interval: 1 },\n    axisLine: { show: false },\n    axisTick: { show: false },\n    splitArea: { show: false },\n  },\n  visualMap: {\n    type: \"continuous\",\n    min: 0,\n    max: maxLog,\n    calculable: true,\n    orient: \"vertical\",\n    right: 40,\n    top: \"middle\",\n    itemHeight: 620,\n    inRange: { color: t.seq },\n    text: [\"High\", \"Low\"],\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    formatter: (value) => `${Math.round(Math.pow(10, value) - 1)}`,\n  },\n  series: [\n    {\n      type: \"heatmap\",\n      data: cells,\n      itemStyle: { borderColor: t.pageBg, borderWidth: 1 },\n      emphasis: {\n        itemStyle: { borderColor: t.ink, borderWidth: 2 },\n      },\n    },\n  ],\n});\n"}