Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 82 additions & 2 deletions e2e/screenshots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,42 @@ const MOCK_DASHBOARD_RESPONSE = {
},
status: { componentDataStatus: 'DATA', refreshStatus: 'IDLE' },
},
{
componentId: 'comp-metric',
reportResult: {
factMap: {
'T!T': { aggregates: [{ label: '42', value: 42 }] },
},
groupingsDown: { groupings: [] },
groupingsAcross: { groupings: [] },
reportExtendedMetadata: {
aggregateColumnInfo: {
RowCount: { dataType: 'int', label: 'Record Count' },
},
detailColumnInfo: {},
},
reportMetadata: { aggregates: ['RowCount'], detailColumns: [] },
},
status: { componentDataStatus: 'DATA', refreshStatus: 'IDLE' },
},
{
componentId: 'comp-gauge',
reportResult: {
factMap: {
'T!T': { aggregates: [{ label: '135', value: 135 }] },
},
groupingsDown: { groupings: [] },
groupingsAcross: { groupings: [] },
reportExtendedMetadata: {
aggregateColumnInfo: {
RowCount: { dataType: 'int', label: 'Record Count' },
},
detailColumnInfo: {},
},
reportMetadata: { aggregates: ['RowCount'], detailColumns: [] },
},
status: { componentDataStatus: 'DATA', refreshStatus: 'IDLE' },
},
Comment thread
nicomiguelino marked this conversation as resolved.
{
componentId: 'comp-table',
reportResult: {
Expand Down Expand Up @@ -176,8 +212,50 @@ const MOCK_DASHBOARD_RESPONSE = {
},
},
{
id: 'comp-table',
id: 'comp-metric',
componentData: 3,
header: 'Total Cases',
title: null,
reportId: '00O000000000005',
type: 'Report',
properties: {
visualizationType: 'Metric',
visualizationProperties: {},
aggregates: [{ name: 'RowCount' }],
groupings: null,
},
},
{
id: 'comp-gauge',
componentData: 4,
header: 'Number of Cases',
title: null,
reportId: '00O000000000004',
type: 'Report',
properties: {
visualizationType: 'Gauge',
visualizationProperties: {
breakPoints: [
{
aggregateName: 'RowCount',
breaks: [
{ color: 'ae4034', lowerBound: 0, upperBound: 54 },
{ color: 'f2a91e', lowerBound: 54, upperBound: 108 },
{ color: '217c3e', lowerBound: 108, upperBound: 200 },
],
},
],
showPercentages: false,
showRange: true,
showTotal: false,
},
aggregates: [{ name: 'RowCount' }],
groupings: null,
},
},
{
id: 'comp-table',
componentData: 5,
header: 'Account List',
title: null,
reportId: '00O000000000002',
Expand All @@ -202,7 +280,9 @@ const MOCK_DASHBOARD_RESPONSE = {
{ column: 0, row: 0, colspan: 4, rowspan: 8 },
{ column: 4, row: 0, colspan: 4, rowspan: 8 },
{ column: 8, row: 0, colspan: 4, rowspan: 8 },
{ column: 0, row: 8, colspan: 12, rowspan: 6 },
{ column: 0, row: 8, colspan: 3, rowspan: 8 },
{ column: 3, row: 8, colspan: 5, rowspan: 8 },
{ column: 8, row: 8, colspan: 4, rowspan: 8 },
],
numColumns: 12,
rowHeight: 36,
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@
"npm-run-all2": "^8.0.4",
"prettier": "^3.8.1",
"typescript": "^5.9.3"
},
"dependencies": {
"chartjs-plugin-datalabels": "^2.2.0"
}
}
Binary file modified screenshots/dashboard-1080x1920.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/dashboard-1280x720.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/dashboard-1920x1080.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/dashboard-2160x3840.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/dashboard-2160x4096.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/dashboard-3840x2160.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/dashboard-4096x2160.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/dashboard-480x800.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/dashboard-720x1280.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/dashboard-800x480.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/error-2160x3840.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/error-3840x2160.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/error-not-found-3840x2160.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/report-2160x3840.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/report-3840x2160.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions src/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ main {
flex: 1;
position: relative;
min-height: 0;
container-type: size;
}

.report-chart-card {
Expand Down Expand Up @@ -197,6 +198,21 @@ main {
word-break: break-word;
}

/* Metric component */

.metric-value {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: clamp(2rem, 30cqh, 20rem);
font-weight: 700;
letter-spacing: -0.04em;
color: var(--color-text-primary);
text-align: center;
}

/* Empty state */

.empty-state {
Expand Down
52 changes: 38 additions & 14 deletions src/render/chart.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Chart, registerables } from 'chart.js'
import ChartDataLabels from 'chartjs-plugin-datalabels'
import type { ReportResult } from '../types'
import { renderEmpty } from './utils'

Chart.register(...registerables)
Chart.register(...registerables, ChartDataLabels)

Chart.defaults.animation = false

Expand Down Expand Up @@ -62,6 +63,40 @@ function extractChartData(reportResult: ReportResult): {
return { labels, values }
}

function buildDatalabelsConfig(chartType: 'bar' | 'line' | 'pie' | 'doughnut') {
const base = {
color: '#ffffff' as const,
font: { weight: 'bold' as const, size: 20 },
formatter: (value: number) => (value === 0 ? '' : value.toLocaleString()),
}
if (chartType === 'bar') {
return {
...base,
anchor: 'end' as const,
align: 'start' as const,
clamp: true,
}
}
if (chartType === 'line') {
return {
...base,
font: { weight: 'bold' as const, size: 15 },
anchor: 'end' as const,
align: 'top' as const,
backgroundColor: 'rgba(0,0,0,0.55)',
borderRadius: 3,
padding: { top: 2, bottom: 2, left: 4, right: 4 },
}
}
return { ...base, anchor: 'center' as const, align: 'center' as const }
}

function buildScalesConfig(chartType: 'bar' | 'line' | 'pie' | 'doughnut') {
if (chartType !== 'bar' && chartType !== 'line') return undefined
const axis = { ticks: { color: '#ffffff' }, grid: { color: '#ffffff22' } }
return { x: axis, y: axis }
}

export function renderChart(
container: HTMLElement,
componentId: string,
Expand Down Expand Up @@ -105,20 +140,9 @@ export function renderChart(
maintainAspectRatio: false,
plugins: {
legend: { labels: { color: '#ffffff' } },
datalabels: buildDatalabelsConfig(chartType),
},
scales:
chartType === 'bar' || chartType === 'line'
? {
x: {
ticks: { color: '#ffffff' },
grid: { color: '#ffffff22' },
},
y: {
ticks: { color: '#ffffff' },
grid: { color: '#ffffff22' },
},
}
: undefined,
scales: buildScalesConfig(chartType),
},
})
}
53 changes: 47 additions & 6 deletions src/render/gauge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ function drawGaugeNeedle(
cx: number,
cy: number,
outerR: number,
pct: number,
value: number
pct: number
): void {
const angle = Math.PI + pct * Math.PI
const safePct = Number.isFinite(pct) ? Math.max(0, Math.min(1, pct)) : 0
const angle = Math.PI + safePct * Math.PI
ctx.save()
ctx.beginPath()
ctx.moveTo(cx, cy)
Expand All @@ -37,10 +37,40 @@ function drawGaugeNeedle(
ctx.arc(cx, cy, outerR * 0.06, 0, 2 * Math.PI)
ctx.fillStyle = '#ffffff'
ctx.fill()
ctx.font = `bold ${Math.floor(outerR * 0.22)}px sans-serif`
ctx.restore()
}

function drawGaugeBreakLabels(
ctx: CanvasRenderingContext2D,
cx: number,
cy: number,
outerR: number,
innerR: number,
min: number,
max: number,
breaks: { lowerBound: number; upperBound: number }[]
): void {
const range = max - min
if (range <= 0) return

const labelR = (outerR + innerR) / 2
const fontSize = Math.max(13, Math.floor(outerR * 0.14))
ctx.save()
ctx.font = `bold ${fontSize}px sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(String(value), cx, cy + outerR * 0.35)

for (const b of breaks) {
const midVal = (b.lowerBound + b.upperBound) / 2
const pct = (midVal - min) / range
const angle = Math.PI + pct * Math.PI
const x = cx + labelR * Math.cos(angle)
const y = cy + labelR * Math.sin(angle)

ctx.fillStyle = '#ffffff'
ctx.fillText(String(b.upperBound), x, y)
}

ctx.restore()
}

Expand Down Expand Up @@ -77,7 +107,17 @@ export function renderGauge(
const { ctx } = chart
const arcEl = chart.getDatasetMeta(0).data[0] as ArcElement
if (!arcEl) return
drawGaugeNeedle(ctx, arcEl.x, arcEl.y, arcEl.outerRadius, pct, value)
drawGaugeBreakLabels(
ctx,
arcEl.x,
arcEl.y,
arcEl.outerRadius,
arcEl.innerRadius,
min,
max,
breaks
)
drawGaugeNeedle(ctx, arcEl.x, arcEl.y, arcEl.outerRadius, pct)
},
}

Expand All @@ -101,6 +141,7 @@ export function renderGauge(
plugins: {
legend: { display: false },
tooltip: { enabled: false },
datalabels: { display: false },
},
},
plugins: [needlePlugin],
Expand Down
23 changes: 23 additions & 0 deletions src/render/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ComponentDataItem,
ReportResult,
} from '../types'

import { CHART_TYPES, renderChart } from './chart'
import { renderGauge } from './gauge'
import { renderTable } from './table'
Expand All @@ -25,6 +26,8 @@ function renderComponent(

if (sfTypeLower === 'gauge') {
renderGauge(container, item.componentId, item.reportResult, meta)
} else if (sfTypeLower === 'metric') {
renderMetric(container, item.reportResult, meta)
} else if (CHART_TYPES.has(sfTypeLower)) {
renderChart(container, item.componentId, item.reportResult, sfType, title)
} else {
Expand Down Expand Up @@ -105,6 +108,26 @@ function renderStat(
container.appendChild(stat)
}

function renderMetric(
container: HTMLElement,
reportResult: ReportResult,
_meta: DashboardMetadataComponent
): void {
const entry = reportResult.factMap?.['T!T']

if (!entry) {
renderEmpty(container)
return
}

const value = entry.aggregates?.[0]?.value ?? 0

const el = document.createElement('div')
el.className = 'metric-value'
el.textContent = Number(value).toLocaleString()
container.appendChild(el)
}

function hasReportDetailRows(reportResult: ReportResult): boolean {
const rows = Object.values(reportResult.factMap ?? {}).flatMap(
(entry) => entry.rows ?? []
Expand Down