<template>
  <div>
    <div class="wrap">
      <h1>When each location is actually busy</h1>
      <p class="lead">
        Six day-by-hour heatmaps under one colour scale and one gradient legend.
        The commuter double-peak, the campus midday hump and the weekend-shaped
        park all read at a glance, and so does the plain fact that the park is a
        fraction of the size of the station.
      </p>

      <div class="chart-wrap">
        <div id="chart">
          <apexchart
            type="heatmap"
            height="620"
            :options="chartOptions"
            :series="series"
          ></apexchart>
        </div>
      </div>

      <div class="note">
        The trellis derives the colour domain over the union of all panels and
        pushes an absolute <code>colorScale.min</code>/<code>max</code> into
        every one, so a given count lands on exactly the same fill in every
        panel. The grid then draws ONE gradient strip for the whole thing
        instead of six identical legends, and hovering a cell traces the same
        value in every other panel. Asking for
        <code>scales.color: 'independent'</code> is refused with a warning:
        per-panel shading is the classic way a heatmap grid misleads. The cell
        tooltip is one line (<code>tooltip.compact</code>), because a full card
        would cover the row it is captioning. Trellis is a premium feature;
        without a license it renders with a trial watermark.
      </div>
    </div>
  </div>
</template>

<script>
import VueApexCharts from 'vue-apexcharts'

import 'apexcharts/features/trellis'

// Six locations, each a real heatmap of day x hour check-ins, all six drawn
// against ONE colour scale derived from the union of every panel's values.
// That is the whole point of the grid: the same fill means the same count
// everywhere, so the quiet sites read as quiet. Six independently shaded
// heatmaps would give every panel its own darkest cell and the comparison
// would be a lie.
//
// Deterministic counts so the e2e snapshot is stable.
function mulberry32(seed) {
  return function () {
    seed |= 0
    seed = (seed + 0x6d2b79f5) | 0
    var t = Math.imul(seed ^ (seed >>> 15), 1 | seed)
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296
  }
}

var DAYS = ['Sun', 'Sat', 'Fri', 'Thu', 'Wed', 'Tue', 'Mon']
var HOURS = ['6a', '8a', '10a', '12p', '2p', '4p', '6p', '8p']

// Each location has its own rhythm AND its own overall volume: the commuter
// sites are busiest, the campus site is a midday hump, the lakeside site is
// small and evening-heavy. Volume differences are the point of one scale.
var LOCATIONS = [
  {
    name: 'Central Station',
    seed: 7,
    volume: 1.0,
    peaks: [1, 6],
    weekend: 0.45,
  },
  { name: 'Harbor Point', seed: 19, volume: 0.82, peaks: [1, 6], weekend: 0.6 },
  {
    name: 'University Campus',
    seed: 31,
    volume: 0.66,
    peaks: [3, 4],
    weekend: 0.3,
  },
  {
    name: 'Riverside Mall',
    seed: 43,
    volume: 0.58,
    peaks: [4, 5],
    weekend: 1.15,
  },
  {
    name: 'Airport North',
    seed: 59,
    volume: 0.74,
    peaks: [0, 7],
    weekend: 0.95,
  },
  {
    name: 'Lakeside Park',
    seed: 71,
    volume: 0.31,
    peaks: [5, 6],
    weekend: 1.4,
  },
]

function hourlyRow(rand, loc, dayIndex) {
  // DAYS runs Sun..Mon top to bottom, so weekend factor keys off the ends.
  var isWeekend = dayIndex === 0 || dayIndex === 1
  var dayFactor = isWeekend ? loc.weekend : 0.85 + rand() * 0.3
  return HOURS.map(function (hour, hi) {
    var near = Math.min(
      Math.abs(hi - loc.peaks[0]),
      Math.abs(hi - loc.peaks[1]),
    )
    var shape = Math.max(0.12, 1 - near * 0.28)
    var v = 130 * loc.volume * dayFactor * shape * (0.85 + rand() * 0.3)
    return { x: hour, y: Math.round(v) }
  })
}

var activitySeries = []
LOCATIONS.forEach(function (loc) {
  var rand = mulberry32(loc.seed)
  DAYS.forEach(function (day, di) {
    activitySeries.push({
      name: day,
      location: loc.name,
      data: hourlyRow(rand, loc, di),
    })
  })
})

export default {
  components: {
    apexchart: VueApexCharts,
  },
  data: function () {
    return {
      series: activitySeries,
      chartOptions: {
        chart: {
          id: 'activityTrellis',
          type: 'heatmap',
          height: 620,
          animations: {
            enabled: false,
          },
        },
        trellis: {
          by: 'location',
          columns: 3,
          minPanelWidth: 300,
          gap: 14,
        },
        colors: ['#0E7490'],
        plotOptions: {
          heatmap: {
            radius: 2,
            enableShades: true,
            shadeIntensity: 0.55,
            colorScale: {
              gradientLegend: {
                formatter: function (val) {
                  return Math.round(Number(val)) + ' check-ins'
                },
              },
            },
          },
        },
        dataLabels: {
          enabled: false,
        },
        stroke: {
          width: 1,
          colors: ['#fff'],
        },
        xaxis: {
          type: 'category',
        },
        tooltip: {
          compact: true,
          y: {
            formatter: function (val) {
              return val + ' check-ins'
            },
          },
        },
      },
    }
  },
}
</script>

<style>
body {
  font-family:
    -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #fafbfc;
  color: #2c3e50;
  margin: 0;
  padding: 32px 16px;
}
.wrap {
  max-width: 1100px;
  margin: 0 auto;
}
h1 {
  font-size: 22px;
  margin: 0 0 8px;
}
.lead {
  color: #5b6b78;
  line-height: 1.5;
  margin: 0 0 20px;
}
.chart-wrap {
  background: #fff;
  border-radius: 8px;
  padding: 16px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
}
.note {
  background: #f0fdfa;
  border-left: 3px solid #0e7490;
  padding: 12px 16px;
  font-size: 13px;
  color: #333;
  border-radius: 2px;
  line-height: 1.6;
  margin: 26px auto 0;
}
.note code {
  background: #ccfbf1;
  padding: 1px 5px;
  border-radius: 3px;
}
</style>