Building HTML Tables in Text Blocks
Text blocks in ChartHop dashboards support raw HTML, which means you can build a fully formatted, styled table using a <table class="ch-table-1"> element combined with CQLT (template expressions). This is especially useful when you need a custom multi-row table that responds to dashboard filters — but where the native chart types don't give you exactly the layout you want.
When to Use This
- You want a table that displays computed metrics side by side (e.g., review scores across multiple categories)
- You need 20+ versions of the same dashboard with different filters — and want one flexible text block that adapts to jobFilter, rather than rebuilding native chart tables for each version
- The data you're displaying comes from long or complex CQL expressions (like custom scoring calculations) that don't map cleanly to a native chart formula
How It Works
Inside a Text Block, you can write HTML directly. ChartHop will render it as formatted content. Wrap any CQL expression in {{ }} to have it evaluated dynamically — just like you would in any other template expression.
The class ch-table-1 applies ChartHop's standard table styling, so your table visually matches the rest of the dashboard.
Basic Structure
<table class="ch-table-1">
<thead>
<tr>
<th>Category</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr>
<td>Overall Performance</td>
<td>{{ findAnswersByAssessment('managerReviewRating', 'H1 Performance Review').filter{target.department=jobFilter}.mean{value} }}</td>
</tr>
<tr>
<td>Values Alignment</td>
<td>{{ findAnswersByAssessment('valuesRatingManager', 'H1 Performance Review').filter{target.department=jobFilter}.mean{value} }}</td>
</tr>
</tbody>
</table>Available Variables
Text blocks have access to three key dashboard variables that make your table dynamic and filter-aware:
Variable | What It Represents |
|---|---|
jobFilter | The audience/filter the dashboard viewer has currently applied (e.g., filtered to Engineering, or a specific manager's team) |
intervalFrom | The start of the reporting period selected on the dashboard (inclusive) |
intervalUntil | The end of the reporting period selected on the dashboard (exclusive — i.e. the day after the last day in the period) |
Using all three means your table responds fully to whatever the viewer has configured — both in terms of who is in scope and when.
These are the same variables used in time-series chart formulas, so if you're already familiar with intervalFrom/intervalUntil from building charts, they work exactly the same way here.
Example: using all three variables
<td>{{ findHires(intervalFrom, intervalUntil).filter{jobFilter}.count() }}</td>This counts new hires within the selected date range, scoped to the active filter.
Using jobFilter Inside the Table
Because this is a Text Block, the jobFilter variable is available — it reflects whatever filter the dashboard viewer has currently applied (e.g., filtered to Engineering, or a specific manager's team).
You can pass jobFilter directly into your CQL expression:
<td>{{ db.job.find(jobFilter).mean{baseComp.annualized} }}</td>This means a single text block table can power 20+ filtered dashboard versions without any changes — just apply a different audience or department filter on the dashboard itself.
Real-World Example: Multi-Score Review Summary Table
This example displays average scores across several review dimensions, filtered to whoever is in scope:
<table class="ch-table-1">
<thead>
<tr>
<th>Review Dimension</th>
<th>Avg Score</th>
</tr>
</thead>
<tbody>
<tr>
<td>Overall Performance</td>
<td>{{ findAnswersByAssessment('managerReviewRating', 'H1 2025 Review').filter{jobFilter}.mean{value} }}</td>
</tr>
<tr>
<td>Impact</td>
<td>{{ findAnswersByAssessment('impactCompetency', 'H1 2025 Review').filter{jobFilter}.mean{value} }}</td>
</tr>
<tr>
<td>Collaboration</td>
<td>{{ findAnswersByAssessment('collaborationCompetency', 'H1 2025 Review').filter{jobFilter}.mean{value} }}</td>
</tr>
<tr>
<td>Knowledge</td>
<td>{{ findAnswersByAssessment('knowledgeCompetency', 'H1 2025 Review').filter{jobFilter}.mean{value} }}</td>
</tr>
</tbody>
</table>Tips
- Always use ch-table-1 as the class — this applies ChartHop's built-in table styling and keeps it visually consistent with native chart tables.
- Column headers are static HTML — only the cell values need template expressions. Keep headers as plain text in <th> tags.
- Expressions are evaluated server-side — if a formula returns no data (e.g., no responses exist for a filter), the cell will render as empty rather than erroring.
- This does not replace native Table charts for most use cases — if the built-in Table chart type works for you, use it. This approach shines when you need custom layouts, multi-source data in one table, or highly specific expression logic that the native chart type can't express.
