Przejdź do treści
<code>pen.pl

Stacje sejsmiczne — tabela telemetryczna

Wizualizacja danych

Podgląd

Ładowanie podglądu…

Ciemny rejestr pomiarów z filtrem regionu, sortowaniem parametrów i czytelną paginacją.

0,0 / 5 Brak ocen

Kod źródłowy

Licencja MIT — także do użytku komercyjnego. Zachowaj informacje o autorze i licencji.

HTML
<main class="seis-shell" id="seismic-table">
  <header class="seis-head"><div><p class="eyebrow">SIEĆ S-14 / ODCZYT NA ŻYWO</p><h1>Rejestr drgań</h1></div><div class="pulse" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></div></header>
  <section class="seis-controls" aria-label="Filtry rejestru">
    <label>Znajdź stację lub miejsce<input type="search" data-search placeholder="np. Karpaty"></label>
    <label>Region<select data-filter id="seis-region"><option value="">Wszystkie regiony</option><option>Europa</option><option>Azja</option><option>Oceania</option><option>Ameryka</option></select></label>
    <button type="button" data-reset>Wyczyść</button>
  </section>
  <div class="seis-meta"><p role="status" aria-live="polite"></p><span>UTC+2 · 13.10.2026</span></div>
  <div class="table-scroll" tabindex="0" aria-label="Tabela odczytów sejsmicznych — przewijaj poziomo na małym ekranie">
    <table><caption>Ostatnie odczyty stacji sejsmicznych</caption><thead><tr>
      <th scope="col"><button type="button" data-sort="station">Stacja <span data-sort-mark aria-hidden="true">↕</span></button></th>
      <th scope="col"><button type="button" data-sort="place">Obszar <span data-sort-mark aria-hidden="true">↕</span></button></th>
      <th scope="col"><button type="button" data-sort="mag">Magnituda <span data-sort-mark aria-hidden="true">↕</span></button></th>
      <th scope="col"><button type="button" data-sort="depth">Głębokość <span data-sort-mark aria-hidden="true">↕</span></button></th>
      <th scope="col"><button type="button" data-sort="time">Czas <span data-sort-mark aria-hidden="true">↕</span></button></th>
    </tr></thead><tbody><tr><td colspan="5">Ładowanie odczytów…</td></tr></tbody></table>
  </div>
  <nav class="seis-pages" data-pages aria-label="Stronicowanie tabeli"></nav>
</main>
CSS
*{box-sizing:border-box}html{color-scheme:dark}body{margin:0;min-width:0;background:#07100e;color:#e8f0e7;font-family:"IBM Plex Mono","SFMono-Regular",Consolas,monospace}.seis-shell{min-height:100vh;padding:clamp(18px,4vw,48px);background:#07100e;border-top:6px solid #c9ff4a}.seis-head{display:flex;align-items:end;justify-content:space-between;gap:24px;border-bottom:1px solid #52615b;padding-bottom:18px}.eyebrow{margin:0 0 8px;color:#c9ff4a;font-size:12px;letter-spacing:.13em}.seis-head h1{margin:0;font-size:clamp(34px,7vw,76px);line-height:.9;font-weight:600;letter-spacing:-.06em}.pulse{display:flex;align-items:center;gap:4px;height:54px}.pulse i{display:block;width:4px;background:#c9ff4a}.pulse i:nth-child(1),.pulse i:nth-child(5){height:10px}.pulse i:nth-child(2),.pulse i:nth-child(4){height:28px}.pulse i:nth-child(3){height:52px}.seis-controls{display:grid;grid-template-columns:minmax(220px,1fr) minmax(170px,.45fr) auto;gap:12px;padding:18px 0}.seis-controls label{display:grid;gap:7px;color:#aab6b0;font-size:12px}.seis-controls input,.seis-controls select,.seis-controls button{min-height:44px;border:1px solid #52615b;border-radius:0;background:#101b17;color:#f6faf4;font:inherit;padding:0 12px}.seis-controls button{align-self:end;border-color:#c9ff4a;color:#c9ff4a;cursor:pointer}.seis-meta{display:flex;justify-content:space-between;gap:18px;color:#aab6b0;font-size:12px;padding:5px 0 10px}.seis-meta p{margin:0;color:#e8f0e7}.table-scroll{max-width:100%;overflow-x:auto;border:1px solid #52615b}.table-scroll:focus-visible{outline:3px solid #c9ff4a;outline-offset:3px}table{width:100%;min-width:720px;border-collapse:collapse}caption{text-align:left;padding:13px 14px;color:#c9ff4a;border-bottom:1px solid #52615b;font-weight:700}th,td{text-align:left;border-bottom:1px solid #33413c;padding:12px 14px;white-space:nowrap}thead{background:#101b17}th button{width:100%;min-height:44px;padding:0;border:0;background:transparent;color:#bdc8c2;font:inherit;text-align:left;cursor:pointer}th[aria-sort] button{color:#c9ff4a}tbody tr:hover{background:#111e19}tbody th{color:#c9ff4a;font-weight:600}td small{display:block;margin-top:3px;color:#8c9a93;font-size:10px}.mag{font-size:18px;font-weight:700}.empty-row{text-align:center;padding:38px;color:#bdc8c2}.seis-pages{display:flex;flex-wrap:wrap;gap:6px;padding-top:14px}.seis-pages button{min-width:44px;min-height:44px;border:1px solid #52615b;border-radius:0;background:transparent;color:#e8f0e7;font:inherit;padding:0 12px;cursor:pointer}.seis-pages button[aria-current="page"]{background:#c9ff4a;color:#07100e;border-color:#c9ff4a}.seis-pages button:disabled{opacity:.36;cursor:not-allowed}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #c9ff4a;outline-offset:2px}@media(max-width:620px){.seis-shell{padding:18px 14px}.seis-head{align-items:start}.pulse{transform:scale(.72);transform-origin:right top}.seis-controls{grid-template-columns:1fr}.seis-meta{align-items:start;flex-direction:column;gap:4px}.seis-pages button:first-child,.seis-pages button:last-child{flex:1}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
JavaScript
$(() => {
  const records = [{"station":"BESK-04","region":"Europa","place":"Beskid Niski","mag":2.1,"depth":7,"time":"08:14"},{"station":"TATR-11","region":"Europa","place":"Tatry Zachodnie","mag":1.6,"depth":5,"time":"08:37"},{"station":"RILA-02","region":"Europa","place":"Bułgaria","mag":3.4,"depth":18,"time":"09:05"},{"station":"ETNA-07","region":"Europa","place":"Sycylia","mag":2.8,"depth":11,"time":"09:42"},{"station":"ALPY-09","region":"Europa","place":"Tyrol","mag":1.9,"depth":6,"time":"10:18"},{"station":"KARP-15","region":"Europa","place":"Karpaty Wschodnie","mag":3.1,"depth":24,"time":"11:06"},{"station":"PIND-03","region":"Europa","place":"Grecja","mag":2.5,"depth":13,"time":"11:49"},{"station":"ISLD-08","region":"Europa","place":"Islandia","mag":4.2,"depth":9,"time":"12:22"},{"station":"HONS-18","region":"Azja","place":"Honsiu","mag":4.8,"depth":36,"time":"07:58"},{"station":"JAVA-06","region":"Azja","place":"Jawa","mag":5.1,"depth":52,"time":"10:44"},{"station":"MIND-12","region":"Azja","place":"Mindanao","mag":4.5,"depth":41,"time":"12:03"},{"station":"NZ-SO1","region":"Oceania","place":"Wyspa Południowa","mag":3.7,"depth":16,"time":"08:53"},{"station":"ANDY-21","region":"Ameryka","place":"Andy Środkowe","mag":4.6,"depth":61,"time":"09:29"},{"station":"CASC-10","region":"Ameryka","place":"Kaskady","mag":2.9,"depth":12,"time":"11:31"}];
  const numericColumns = new Set(["mag", "depth"]);
  const collator = new Intl.Collator('pl', { numeric: true, sensitivity: 'base' });
  const state = { query: '', sort: 'mag', direction: 'desc', page: 1 };
  const pageSize = 4;
  const $root = $('#seismic-table');
  const $body = $root.find('tbody');
  const $pages = $root.find('[data-pages]');
  const $status = $root.find('[role="status"]');

  function filteredRecords() {
    const query = state.query.trim().toLocaleLowerCase('pl');
    return records.filter((record) => {
      const matchesQuery = !query || Object.values(record).join(' ').toLocaleLowerCase('pl').includes(query);
      const region = $('#seis-region').val();
      const matchesFilters = !region || record.region === region;
      return matchesQuery && matchesFilters;
    });
  }

  function compare(left, right) {
    const key = state.sort;
    const result = numericColumns.has(key)
      ? Number(left[key]) - Number(right[key])
      : collator.compare(String(left[key]), String(right[key]));
    return state.direction === 'asc' ? result : -result;
  }

  function rowTemplate(record) {
    return `<tr><th scope="row">${record.station}</th><td>${record.place}<small>${record.region}</small></td><td class="mag">${record.mag.toFixed(1)}</td><td>${record.depth} km</td><td>${record.time}</td></tr>`;
  }

  function render() {
    const result = filteredRecords().sort(compare);
    const totalPages = Math.max(1, Math.ceil(result.length / pageSize));
    state.page = Math.min(state.page, totalPages);
    const visible = result.slice((state.page - 1) * pageSize, state.page * pageSize);

    $body.html(visible.length
      ? visible.map(rowTemplate).join('')
      : '<tr><td class="empty-row" colspan="5">Brak wyników. Zmień filtry lub wyczyść wyszukiwanie.</td></tr>');

    $root.find('thead th').removeAttr('aria-sort');
    $root.find('[data-sort]').each(function () {
      const active = this.dataset.sort === state.sort;
      $(this).find('[data-sort-mark]').text(active ? (state.direction === 'asc' ? '↑' : '↓') : '↕');
      if (active) $(this).closest('th').attr('aria-sort', state.direction === 'asc' ? 'ascending' : 'descending');
    });

    const buttons = [];
    buttons.push(`<button type="button" data-page="${state.page - 1}" ${state.page === 1 ? 'disabled' : ''}>Poprzednia</button>`);
    for (let page = 1; page <= totalPages; page += 1) {
      buttons.push(`<button type="button" data-page="${page}" ${page === state.page ? 'aria-current="page"' : ''} aria-label="Strona ${page}">${page}</button>`);
    }
    buttons.push(`<button type="button" data-page="${state.page + 1}" ${state.page === totalPages ? 'disabled' : ''}>Następna</button>`);
    $pages.html(buttons.join(''));

    const direction = state.direction === 'asc' ? 'rosnąco' : 'malejąco';
    $status.text(`Wyniki: ${result.length} · Strona ${state.page} z ${totalPages} · Sortowanie: ${direction}.`);
  }

  $root.on('input', '[data-search]', function () {
    state.query = this.value;
    state.page = 1;
    render();
  });
  $root.on('change', '[data-filter]', function () {
    state.page = 1;
    render();
  });
  $root.on('click', '[data-sort]', function () {
    const key = this.dataset.sort;
    state.direction = state.sort === key && state.direction === 'asc' ? 'desc' : 'asc';
    state.sort = key;
    state.page = 1;
    render();
    this.focus();
  });
  $root.on('click', '[data-page]', function () {
    if (this.disabled) return;
    state.page = Number(this.dataset.page);
    render();
    $pages.find(`[data-page="${state.page}"]`).focus();
  });
  $root.on('click', '[data-reset]', () => {
    $root.find('[data-search]').val('');
    $root.find('select[data-filter]').prop('selectedIndex', 0);
    $root.find('input[type="radio"][data-filter]').first().prop('checked', true);
    state.query = '';
    state.sort = 'mag';
    state.direction = 'desc';
    state.page = 1;
    render();
    $root.find('[data-search]').focus();
  });

  render();
});
Biblioteki

JavaScript

Edytuj swój pen

Podaj adres e-mail użyty przy zapisie. Wyślemy link do edycji.

Link jest ważny przez 30 minut.