human-product · Stage 3

静的グラフィックスを同等の情報構造として設計する

roadmapとquantitative chartをsemantic HTMLとCSSで表し、色や形だけに依存せず、同じ関係と値をテキスト構造から検証できるようにする。

学習時間
320分
難易度
advanced
更新日
2026-07-30
到達証拠
成果物・説明・判断根拠・転用

到達目標

  1. データ、幾何、rasterization、合成というgraphics pipelineと表示上の制約を説明できる

    • 視覚表現と同等のテキスト構造を持つ静的データ図
    • graphics pipeline、SVG 2、semantic HTML、知覚encodingを説明する5分発表
  2. roadmapのnodeとedge、chartのlabelとvalueをsemantic HTMLと同等のテキスト構造へ保持できる

    • 視覚表現と同等のテキスト構造を持つ静的データ図
    • 視覚encodingと情報同等性の欠落を診断する回答
  3. quantitative valueを共通尺度上の位置で表し、colorだけに依存しない表示へ移行できる

    • 視覚encodingと情報同等性の欠落を診断する回答
    • display modeだけを変えて情報同等性を再検証した記録

能力の進行

  1. recognize

    データ、mark、channel、semantic structure、text equivalentを区別できる

    証拠: 視覚表現と同等のテキスト構造を持つ静的データ図

  2. explain

    OpenGL 4.6とSVG 2の描画機構、ClevelandとMunznerの知覚設計を役割別に説明できる

    証拠: graphics pipeline、SVG 2、semantic HTML、知覚encodingを説明する5分発表

  3. apply

    roadmapとquantitative chartをCSS付きsemantic HTMLと同等テキストへ変換できる

    証拠: 視覚表現と同等のテキスト構造を持つ静的データ図

  4. diagnose

    欠落したedge、値、単位、順序、color-only encodingをデータから検出できる

    証拠: 視覚encodingと情報同等性の欠落を診断する回答

  5. lead

    視覚品質、知覚精度、アクセシビリティ、保守性の境界を関係者と合意できる

    証拠: display modeだけを変えて情報同等性を再検証した記録

なぜ重要か

図は関係や量を一目で伝える一方、情報をpixel、位置、colorだけへ閉じ込めると、読み上げ、monochrome印刷、CSS無効化、低視力、検索、差分reviewでは意味を失う。静的な教材では、視覚表現と同じdataからsemantic HTMLと同等のテキスト構造を導出し、どの経路でも同じ判断へ到達できるようにする。

OpenGL 4.6はgraphics pipelineの機構、SVG 2は文書内のvector表現を定める。Clevelandのgraphical perceptionとMunznerの分析設計は、読者のtaskとdata typeに合うencodingを選ぶ根拠になる。W3C Complex Imagesは、短い要約だけでは足りない複雑な関係や値をtextへ残す実践を示す。

メンタルモデル

dataを唯一の情報源とし、視覚表現、semantic HTML、同等textを別々のviewとして導出する。roadmapではnodeとdirected edge、quantitative chartではlabel、value、unit、比較結果を情報契約にする。CSSは配置と装飾を担当し、関係や値そのものを生成しない。

Clevelandが示した知覚taskを踏まえ、量の比較では共通尺度上のpositionを優先する。colorは群や強調の補助にできるが、colorだけで状態や順位を符号化しない。Munznerのwhat、why、howを使い、data、読者task、encodingを混同しない。

一つのdata modelから視覚と同等textへ分岐するrendering pipeline

注記

図を読む際の補足情報です。

  1. Data:
  2. Transform:
  3. Visual:
  4. Equivalent:
  5. Verify:

一つのdata modelから視覚表現と同等textを作り、どの不変条件を照合するか。

  1. Data
    roadmapのnodeとedge、chartのlabelとvalueをID付きで定義する。
    • Data → Transform: typed values
  2. Transform
    並び、scale、minimum、maximumを決定的に計算する。
    • Transform → Visual: 同じtransformから決定的layout
    • Transform → Equivalent: 同じtransformから同等text
  3. Visual
    semantic HTMLとCSSでroadmapとquantitative chartを配置する。
    • Visual → Verify: 視覚側のID・値を照合
  4. Equivalent
    node関係list、caption付きtable、summaryを同じdataから導出する。
    • Equivalent → Verify: text側のID・値を照合
  5. Verify
    ID、edge、row、値、集約、display modeの不変条件を照合する。

Data、Transform、Visual、Equivalent、Verifyの順序を説明し、別目的の0–100% worked-example chartを二重計上しない。

動く例で考える

roadmapとquantitative chartの同等性を検証する

前提
4つのroadmap node、3つのdirected edge、3点のsynthetic chart dataを固定する。production計画や実測値ではない。
入力
node ID、label、edgeのfromとto、chart pointのID、label、valueを使う。display modeはcolorをbaselineとする。
操作
roadmapのsemantic HTML用node IDとtext equivalentのnode、edgeを同じdataから導出する。chartは共通尺度上のpositionをencodingとし、同じrowをtableとtextへ複製する。
観測
roadmapのnodeとedge、chartの3 row、maximum、minimumが一致する。colorからmonochromeへdisplay modeだけを変えてもdataとtext equivalenceは不変である。
結論
視覚表現は情報そのものではなくdataのviewである。同等textを同じ変換から生成し、欠落mutationを失敗させることでdriftを検出できる。
同じ0–100%尺度で比較する学習活動の時間

0%

100%

color表示

  1. 学習: 12 hours (60%)
    60%
  2. 実践: 20 hours (100%)
    100%
  3. 復習: 8 hours (40%)
    40%

monochrome表示

  1. 学習: 12 hours (60%)
    60%
  2. 実践: 20 hours (100%)
    100%
  3. 復習: 8 hours (40%)
    40%
barと同じfixtureから導出した活動別時間
活動 ID 時間
学習 learn 12 hours
実践 practice 20 hours
復習 review 8 hours
python3.13 - <<'PY'
from html import escape
from html.parser import HTMLParser
from hashlib import sha256
import json

HARNESS = "graphics_semantic_equivalence_lab_v1"
ROADMAP_NODES = [
    {"id": "foundation", "label": "基礎"},
    {"id": "build", "label": "構築"},
    {"id": "operate", "label": "運用"},
    {"id": "lead", "label": "牽引"},
]
ROADMAP_EDGES = [
    {"from": "foundation", "to": "build"},
    {"from": "build", "to": "operate"},
    {"from": "operate", "to": "lead"},
]
CHART_DATA = [
    {"id": "learn", "label": "学習", "value": 12},
    {"id": "practice", "label": "実践", "value": 20},
    {"id": "review", "label": "復習", "value": 8},
]
ROADMAP_CSS = (
    ".roadmap { border: 1px solid currentColor; padding: 1rem; } "
    ".roadmap-nodes { display: grid; gap: 0.5rem; } "
    ".roadmap-edges { list-style: none; padding: 0; }"
)
CHART_CSS = (
    ".quantitative-chart { border: 1px solid currentColor; padding: 1rem; } "
    ".chart-bars { list-style: none; padding: 0; } "
    ".chart-bar { inline-size: 100%; accent-color: #245d63; } "
    ".quantitative-chart table { border-collapse: collapse; width: 100%; } "
    ".quantitative-chart th, .quantitative-chart td { "
    "border: 1px solid currentColor; padding: 0.25rem; }"
)
DISPLAY_MODE_PRESENTATIONS = {
    "color": {
        "class_name": "display-color",
        "css": (
            ".display-color { color: CanvasText; background: Canvas; } "
            ".display-color .roadmap, "
            ".display-color .quantitative-chart { "
            "border-style: solid; } "
            ".display-color .chart-bar { accent-color: #245d63; }"
        ),
    },
    "monochrome": {
        "class_name": "display-monochrome",
        "css": (
            ".display-monochrome { "
            "color: CanvasText; background: Canvas; } "
            ".display-monochrome .roadmap, "
            ".display-monochrome .quantitative-chart { "
            "border-style: double; } "
            ".display-monochrome .chart-bar { "
            "accent-color: CanvasText; border-style: double; "
            "background-image: repeating-linear-gradient("
            "135deg, CanvasText 0 2px, Canvas 2px 4px); }"
        ),
    },
}
UNSAFE_CSS_TOKENS = (
    "@import",
    "url(",
    "javascript:",
    "expression(",
    "</style",
    "<!--",
    "-->",
)
ARTIFACT_TAG_ATTRIBUTES = {
    "section": {"class"},
    "figure": {"class"},
    "figcaption": set(),
    "ol": {"class"},
    "li": {"id", "data-from", "data-to"},
    "span": set(),
    "table": set(),
    "caption": set(),
    "thead": set(),
    "tbody": set(),
    "tr": set(),
    "th": {"scope"},
    "td": set(),
    "meter": {
        "class",
        "data-id",
        "data-raw-value",
        "min",
        "max",
        "value",
    },
}

class ArtifactParser(HTMLParser):
    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.tags = []
        self.ids = []
        self.edge_pairs = []
        self.table_header_scopes = []
        self.figure_caption_count = 0
        self.table_caption_count = 0
        self.table_rows = []
        self.meter_values = []
        self.section_classes = []
        self.unsafe = []
        self.in_tbody = False
        self.current_row = None
        self.current_cell = None

    def handle_starttag(self, tag, attrs):
        normalized_tag = tag.casefold()
        values = {
            name.casefold(): value or ""
            for name, value in attrs
        }
        self.tags.append(normalized_tag)
        allowed_attributes = ARTIFACT_TAG_ATTRIBUTES.get(normalized_tag)
        if allowed_attributes is None:
            self.unsafe.append(f"tag:{normalized_tag}")
        else:
            supplied_attributes = {
                name.casefold()
                for name, _ in attrs
            }
            if (
                len(supplied_attributes) != len(attrs)
                or not supplied_attributes <= allowed_attributes
            ):
                self.unsafe.append(f"attributes:{normalized_tag}")
        if values.get("id"):
            self.ids.append(values["id"])
        if normalized_tag == "section":
            self.section_classes.append(values.get("class", ""))
        if normalized_tag == "li" and {
            "data-from",
            "data-to",
        }.issubset(values):
            self.edge_pairs.append(
                [values["data-from"], values["data-to"]]
            )
        if normalized_tag == "th":
            self.table_header_scopes.append(values.get("scope", ""))
        if normalized_tag == "figcaption":
            self.figure_caption_count += 1
        if normalized_tag == "caption":
            self.table_caption_count += 1
        if normalized_tag == "meter":
            self.meter_values.append(
                {
                    "id": values.get("data-id", ""),
                    "min": int(values.get("min", "0")),
                    "max": int(values.get("max", "0")),
                    "value": int(values.get("value", "0")),
                    "raw_value": int(values.get("data-raw-value", "0")),
                }
            )
        if normalized_tag == "tbody":
            self.in_tbody = True
        if normalized_tag == "tr" and self.in_tbody:
            self.current_row = []
        if (
            normalized_tag in {"th", "td"}
            and self.current_row is not None
        ):
            self.current_cell = []
        if normalized_tag in {
            "script",
            "style",
            "iframe",
            "object",
            "embed",
        }:
            self.unsafe.append(normalized_tag)
        for name, value in attrs:
            normalized_name = name.casefold()
            candidate = value or ""
            if (
                normalized_name == "style"
                or normalized_name.startswith("on")
                or "://" in candidate
            ):
                self.unsafe.append(normalized_name)

    def handle_data(self, data):
        if self.current_cell is not None:
            self.current_cell.append(data)

    def handle_endtag(self, tag):
        normalized_tag = tag.casefold()
        if (
            normalized_tag in {"th", "td"}
            and self.current_cell is not None
            and self.current_row is not None
        ):
            self.current_row.append(
                " ".join("".join(self.current_cell).split())
            )
            self.current_cell = None
        if (
            normalized_tag == "tr"
            and self.current_row is not None
        ):
            if len(self.current_row) == 3:
                self.table_rows.append(
                    {
                        "id": self.current_row[1],
                        "label": self.current_row[0],
                        "value": int(self.current_row[2]),
                    }
                )
            self.current_row = None
        if normalized_tag == "tbody":
            self.in_tbody = False

def render_roadmap_html(nodes, edges):
    node_items = "".join(
        (
            f'<li id="{escape(node["id"])}">'
            f'{escape(node["label"])}</li>'
        )
        for node in nodes
    )
    edge_items = "".join(
        (
            f'<li data-from="{escape(edge["from"])}" '
            f'data-to="{escape(edge["to"])}">'
            f'{escape(edge["from"])} to {escape(edge["to"])}'
            "</li>"
        )
        for edge in edges
    )
    return (
        '<figure class="roadmap">'
        "<figcaption>学習roadmapと依存関係</figcaption>"
        f'<ol class="roadmap-nodes">{node_items}</ol>'
        f'<ol class="roadmap-edges">{edge_items}</ol>'
        "</figure>"
    )

def render_meter_rows(chart_data, scale_max):
    # All marks share one maximum so bar length preserves quantitative ratios;
    # visible labels keep the raw value available without relying on color.
    raw_max = max(point["value"] for point in chart_data)
    return "".join(
        (
            "<li>"
            f'<span>{escape(point["label"])}: '
            f'{point["value"]} hours '
            f'({round(point["value"] / raw_max * scale_max)}%)'
            "</span>"
            f'<meter class="chart-bar" data-id="{escape(point["id"])}" '
            f'data-raw-value="{point["value"]}" min="0" '
            f'max="{scale_max}" '
            f'value="{round(point["value"] / raw_max * scale_max)}">'
            f'{round(point["value"] / raw_max * scale_max)}%'
            "</meter>"
            "</li>"
        )
        for point in chart_data
    )

def render_chart_html(chart_data):
    scale_max = 100
    meter_rows = render_meter_rows(chart_data, scale_max)
    rows = "".join(
        (
            "<tr>"
            f'<th scope="row">{escape(point["label"])}</th>'
            f'<td>{escape(point["id"])}</td>'
            f'<td>{point["value"]}</td>'
            "</tr>"
        )
        for point in chart_data
    )
    return (
        '<figure class="quantitative-chart">'
        "<figcaption>学習活動の時間比較</figcaption>"
        f'<ol class="chart-bars">{meter_rows}</ol>'
        "<table>"
        "<caption>活動別時間の quantitative chart</caption>"
        "<thead><tr>"
        '<th scope="col">活動</th>'
        '<th scope="col">ID</th>'
        '<th scope="col">時間</th>'
        "</tr></thead>"
        f"<tbody>{rows}</tbody>"
        "</table>"
        "</figure>"
    )

def validate_roadmap_artifact(html, node_ids, edge_pairs):
    parser = ArtifactParser()
    parser.feed(html)
    parser.close()
    valid = (
        {"figure", "figcaption", "ol", "li"}.issubset(parser.tags)
        and parser.ids == node_ids
        and parser.edge_pairs == edge_pairs
        and parser.figure_caption_count == 1
        and not parser.unsafe
    )
    return {
        "valid": valid,
        "node_ids": parser.ids,
        "edge_pairs": parser.edge_pairs,
        "unsafe": parser.unsafe,
    }

def validate_chart_artifact(html, expected_rows):
    parser = ArtifactParser()
    parser.feed(html)
    parser.close()
    required_tags = {
        "figure",
        "figcaption",
        "table",
        "caption",
        "thead",
        "tbody",
        "tr",
        "th",
        "td",
    }
    raw_max = max(row["value"] for row in expected_rows)
    expected_meters = [
        {
            "id": row["id"],
            "min": 0,
            "max": 100,
            "value": round(row["value"] / raw_max * 100),
            "raw_value": row["value"],
        }
        for row in expected_rows
    ]
    semantic_valid = (
        required_tags.issubset(parser.tags)
        and parser.figure_caption_count == 1
        and parser.table_caption_count == 1
        and bool(parser.table_header_scopes)
        and all(
            scope in {"col", "row"}
            for scope in parser.table_header_scopes
        )
        and parser.table_rows == expected_rows
        and not parser.unsafe
    )
    scale_valid = (
        "meter" in parser.tags
        and parser.meter_values == expected_meters
    )
    return {
        "valid": semantic_valid and scale_valid,
        "semantic_valid": semantic_valid,
        "scale_valid": scale_valid,
        "table_rows": parser.table_rows,
        "meter_values": parser.meter_values,
        "header_scopes": parser.table_header_scopes,
        "unsafe": parser.unsafe,
    }

def artifact_data_fingerprint(nodes, edges, chart_data):
    # Canonical JSON proves that the transfer changes presentation only,
    # rather than silently changing the roadmap or quantitative evidence.
    canonical = json.dumps(
        {
            "nodes": nodes,
            "edges": edges,
            "chart_data": chart_data,
        },
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    )
    return sha256(canonical.encode("utf-8")).hexdigest()

def validate_display_artifact(
    html,
    css,
    expected_class,
    expected_css,
):
    parser = ArtifactParser()
    parser.feed(html)
    parser.close()
    unsafe_css = [
        token
        for token in UNSAFE_CSS_TOKENS
        if token in css.casefold()
    ]
    html_valid = (
        not parser.unsafe
        and parser.section_classes == [expected_class]
        and parser.figure_caption_count == 2
        and parser.table_caption_count == 1
    )
    # CSS is executable presentation input. Exact comparison against the
    # selected fixed rule set prevents selector or declaration injection
    # from being mistaken for a validated artifact.
    css_valid = (
        type(css) is str
        and css == expected_css
        and not unsafe_css
    )
    return {
        "valid": html_valid and css_valid,
        "html_valid": html_valid,
        "css_valid": css_valid,
        "section_classes": parser.section_classes,
        "unsafe_html": parser.unsafe,
        "unsafe_css": unsafe_css,
    }

def render_display_artifact(
    display_mode,
    roadmap_artifact,
    chart_artifact,
    data_fingerprint,
):
    assert (
        type(display_mode) is str
        and display_mode in DISPLAY_MODE_PRESENTATIONS
    ), "graphics-display-mode-invariant"
    assert (
        roadmap_artifact["css"] == ROADMAP_CSS
        and chart_artifact["css"] == CHART_CSS
    ), "graphics-css-invariant"
    presentation = DISPLAY_MODE_PRESENTATIONS[display_mode]
    class_name = presentation["class_name"]
    html = (
        f'<section class="{class_name}">'
        f'{roadmap_artifact["html"]}{chart_artifact["html"]}'
        "</section>"
    )
    css = ROADMAP_CSS + CHART_CSS + presentation["css"]
    validation = validate_display_artifact(
        html,
        css,
        class_name,
        ROADMAP_CSS + CHART_CSS + presentation["css"],
    )
    assert validation["html_valid"], "graphics-artifact-invariant"
    assert validation["css_valid"], "graphics-css-invariant"
    return {
        "display_mode": display_mode,
        "html": html,
        "css": css,
        "data_fingerprint": data_fingerprint,
        "validation": validation,
        "valid": validation["valid"],
    }

def derive_roadmap(nodes, edges):
    node_ids = [node["id"] for node in nodes]
    edge_pairs = [
        [edge["from"], edge["to"]]
        for edge in edges
    ]
    roadmap_html = render_roadmap_html(nodes, edges)
    artifact_validation = validate_roadmap_artifact(
        roadmap_html,
        node_ids,
        edge_pairs,
    )
    assert artifact_validation["valid"], "graphics-artifact-invariant"
    return {
        "data": {
            "nodes": [dict(node) for node in nodes],
            "edges": [dict(edge) for edge in edges],
        },
        "semantic_html": {
            "element": "ol",
            "node_ids": list(node_ids),
            "css_role": "layout-and-decoration-only",
        },
        "text_equivalent": {
            "node_ids": list(node_ids),
            "edge_pairs": edge_pairs,
            "format": "ordered-node-list-and-directed-relations",
        },
        "artifact": {
            "html": roadmap_html,
            "css": ROADMAP_CSS,
        },
        "artifact_validation": artifact_validation,
    }

def derive_chart(chart_data):
    expected_rows = [
        {
            "id": point["id"],
            "label": point["label"],
            "value": point["value"],
        }
        for point in chart_data
    ]
    text_rows = [dict(point) for point in chart_data]
    assert text_rows == expected_rows, "graphics-causal-invariant"
    chart_html = render_chart_html(chart_data)
    artifact_validation = validate_chart_artifact(
        chart_html,
        expected_rows,
    )
    assert (
        artifact_validation["semantic_valid"]
    ), "graphics-artifact-invariant"
    assert artifact_validation["scale_valid"], "graphics-scale-invariant"
    maximum = max(chart_data, key=lambda point: point["value"])
    minimum = min(chart_data, key=lambda point: point["value"])
    raw_max = maximum["value"]
    return {
        "data": [dict(point) for point in chart_data],
        "semantic_html": {
            "element": "table",
            "caption": "学習活動の時間比較",
            "table_rows": expected_rows,
        },
        "text_equivalent": {
            "rows": text_rows,
            "summary": (
                f"maximum={maximum['id']}; minimum={minimum['id']}"
            ),
        },
        "summary": {
            "maximum_id": maximum["id"],
            "minimum_id": minimum["id"],
        },
        "encoding": {
            "quantitative": "position-on-common-scale",
            "color_only": False,
            "unit": "hours",
        },
        "visual_encoding": {
            "mark": "meter-bar",
            "scale": "normalized-common-zero-to-100",
            "scale_min": 0,
            "scale_max": 100,
            "raw_max": raw_max,
            "visible_value_labels": True,
            "non_color_cue": "length-pattern-border-and-text",
        },
        "artifact": {
            "html": chart_html,
            "css": CHART_CSS,
        },
        "artifact_validation": artifact_validation,
    }

def mastery_evidence():
    return {
        "lab_steps": [
            {"step": 1, "evidence": "ID付きnode、edge、chart fixture"},
            {"step": 2, "evidence": "semantic HTML nodeとCSS責務"},
            {"step": 3, "evidence": "共通尺度とcaption付きtable rows"},
            {"step": 4, "evidence": "node、edge、row、集約の等価検証"},
            {"step": 5, "evidence": "display-modeだけを変えたtransfer"},
        ],
        "assessments": [
            {"assessment": 1, "evidence": "directed edgeの同等text"},
            {"assessment": 2, "evidence": "position encodingとcolor-only回避"},
        ],
        "rubric_dimensions": [
            "technical-correctness",
            "judgment",
            "evidence",
            "communication",
        ],
        "transfer": {
            "task": (
                "color表示からmonochrome表示へ変え、roadmapとchartの"
                "同等情報を再検証する"
            ),
            "changed_assumption": "display-mode",
            "evidence": "同じdataとtext equivalentで表示modeだけを変更",
        },
    }

def main():
    roadmap = derive_roadmap(ROADMAP_NODES, ROADMAP_EDGES)
    chart = derive_chart(CHART_DATA)
    data_fingerprint = artifact_data_fingerprint(
        ROADMAP_NODES,
        ROADMAP_EDGES,
        CHART_DATA,
    )
    baseline_artifact = render_display_artifact(
        "color",
        roadmap["artifact"],
        chart["artifact"],
        data_fingerprint,
    )
    transferred_artifact = render_display_artifact(
        "monochrome",
        roadmap["artifact"],
        chart["artifact"],
        data_fingerprint,
    )
    assert (
        baseline_artifact["valid"]
        and transferred_artifact["valid"]
    ), "graphics-artifact-invariant"
    assert (
        baseline_artifact["data_fingerprint"]
        == transferred_artifact["data_fingerprint"]
    ), "graphics-artifact-invariant"
    expected_node_ids = [node["id"] for node in ROADMAP_NODES]
    expected_edges = [
        [edge["from"], edge["to"]]
        for edge in ROADMAP_EDGES
    ]
    assert (
        roadmap["text_equivalent"]["node_ids"] == expected_node_ids
    ), "graphics-causal-invariant"
    assert (
        roadmap["text_equivalent"]["edge_pairs"] == expected_edges
    ), "graphics-causal-invariant"
    report = {
        "harness": HARNESS,
        "roadmap": roadmap,
        "quantitative_chart": chart,
        "display_mode_transfer": {
            "changed_assumption": "display-mode",
            "changed_fields": ["display_mode"],
            "baseline_mode": "color",
            "transferred_mode": "monochrome",
            "data_unchanged": (
                baseline_artifact["data_fingerprint"]
                == transferred_artifact["data_fingerprint"]
            ),
            "text_equivalence_preserved": (
                roadmap["semantic_html"]["node_ids"]
                == roadmap["text_equivalent"]["node_ids"]
                and chart["semantic_html"]["table_rows"]
                == chart["text_equivalent"]["rows"]
            ),
            "baseline_artifact": baseline_artifact,
            "transferred_artifact": transferred_artifact,
        },
        "fixture_metadata": {
            "kind": "synthetic",
            "provenance": "lesson-defined roadmap and chart records",
            "limitations": (
                "small deterministic fixture; not measured learner data"
            ),
        },
        "runtime_bound": {
            "records": (
                len(ROADMAP_NODES)
                + len(ROADMAP_EDGES)
                + len(CHART_DATA)
            ),
            "subprocesses": 0,
        },
        "mastery_evidence": mastery_evidence(),
        "external_network_used": False,
    }
    return report

print(json.dumps(main(), ensure_ascii=False, indent=2))
PY

トレードオフと失敗モード

読者taskから静的encodingを選ぶ decision table
task 主encoding 同等text 避ける設計
依存関係をたどる nodeとdirected edge 順序付きnode listとfrom-to関係 線の位置だけで関係を表す
量を正確に比較する 共通尺度上のposition caption付きtableとmaximum、minimum 面積やcolorだけで値を表す
状態を素早く識別する label、形、pattern、補助color 状態名と説明 赤と緑だけで区別する
  • 誤診: SVG 2で描いたので自動的にsemanticでaccessibileな図になる。反証: SVGの要素だけではreader task、directed edge、値、要約の同等性を保証しない。dataからsemantic HTMLと同等のテキスト構造を導出して照合する。
  • 誤診: color legendを追加すればcolorだけのencodingでも十分である。反証: legendもcolor識別を要求する。label、位置、形、pattern、tableを併用し、monochromeでも同じ関係と値を取得できる必要がある。
  • 失敗モード: 視覚図とtableを別々に更新すると、node削除や値変更でdriftし、異なる結論を伝える。
  • 失敗モード: 3D効果や面積で一次元の量を表すと、装飾が比較taskへ不要な知覚誤差を加える。

知識チェック

  1. OpenGL 4.6とSVG 2は、この教材の情報設計でどの異なる層を扱うか。
  2. roadmapの同等textにnode labelだけでなくedgeが必要な理由は何か。
  3. Clevelandの知覚taskを量の比較へどう適用するか。
  4. colorからmonochromeへ変える時に同等性をどう反証するか。

出典と次の学習

rendering機構はThe OpenGL Graphics System Version 4.6 Core ProfileのMay 5, 2022版、文書内vector表現はScalable Vector Graphics 2へ版を固定する。encodingの知覚根拠はClevelandとMcGillのGraphical Perception、分析設計はMunznerのVisualization Analysis and Design、複雑図のtext実践はW3C Complex Imagesを参照する。

次は、視覚化したproduct仮説を反証可能なsuccess metric、guardrail、stop conditionへ変換し、simulated dataで継続判断を再現する。

実践ラボ

roadmapとquantitative chartの視覚・意味同等性を作る

提出成果物: 視覚表現と同等のテキスト構造を持つ静的データ図

  1. 固定synthetic dataからroadmapのnodeとdirected edge、chartのlabel、value、単位を定義する
  2. roadmapをsemantic HTMLの順序付きnodeと関係listへ変換し、CSSを表示の責務に限定する
  3. quantitative chartを共通尺度上のpositionで表し、同じ値をcaption付きtableとtext summaryへ変換する
  4. node、edge、row、maximum、minimumが視覚dataと同等のテキスト構造で一致することを検証する
  5. 同じdataとtext equivalentのままcolorからmonochromeへdisplay modeだけを変え、情報同等性を再検証する

説明して理解を確かめる

5分で、OpenGL 4.6のgraphics pipeline、SVG 2の文書表現、Clevelandのgraphical perception、Munznerの分析設計、W3C Complex Imagesのtext equivalentを役割別に説明する。

アセスメント

  1. 問い: 色分けしたroadmapが見栄え良く表示されるが、読み上げでは依存関係が分からない。何を直すか。

    期待する証拠: data由来のnode ID、directed edge、semantic HTML、同等のテキスト構造、CSS責務の分離

  2. 問い: barの面積と彩度だけで3つの値を比較するchartをどう再設計するか。

    期待する証拠: 共通尺度上の位置、label、value、単位、table、summary、color-only回避

別問題へ転用する

color表示からmonochrome表示へ変え、roadmapとchartの同等情報を再検証する

復習スケジュール

  1. 1日後

    roadmapのvisual orderと依存関係dataを分ける理由は何か

  2. 7日後

    quantitative valueの比較に共通尺度上のpositionを使う利点は何か

  3. 30日後

    monochrome transferで不変にすべき情報を三つ挙げる

  4. 90日後

    roadmapのvisual orderと依存関係dataを分ける理由は何か

評価ルーブリック

4段階の評価基準
観点未達発展途上熟達卓越
technical-correctness視覚位置と色だけに情報を持たせ、元dataとの対応がないdataと図は対応するがedge、値、単位のいずれかがtextから欠けるnode、edge、row、値、集約をdataから一貫して導出する表示変換、順序、精度、fallbackの境界でも同等性を維持する
judgment装飾性だけでencodingを選び読者の比較タスクを考慮しない複数encodingを使うが知覚精度と複雑性を比較しない読者のtask、data type、知覚精度、保守性からencodingを選ぶ密度、国際化、印刷、支援技術の緊張を再評価条件へ含める
evidence画像だけを提出し元dataと検証結果を残さない元dataはあるが視覚表現とtext equivalentを別々に手書きする同じfixtureから視覚用dataと同等textを再現可能に導出するtext欠落mutationを失敗させ、最大値と関係も機械的に照合する
communication凡例、単位、captionがなく図の意図を説明できない説明はあるが意思決定に必要な比較と関係が散在するcaption、summary、table、関係listで同じ結論へ到達できる視覚利用者と非視覚利用者が同じ根拠をレビューできる

出典

以下の外部資料は利用者が選択したときだけ開きます。