Charts
Chart cards on the dashboard — line, timeline, and multi-series visualizations for mining operations
Chart components specialized for Bitcoin mining operations data visualization, including hashrate trends, power consumption, power mode timelines, and multi-container comparisons.
Prerequisites
ChartWrapper
Wrapper component for charts with built-in loading and empty state handling.
Import
import { ChartWrapper } from '@tetherto/mdk-react-devkit/foundation'Props
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
children | Optional | ReactNode | none | Chart content rendered when data is present |
data | Optional | object | array | none | Chart data for LineChart-style datasets; used by useChartDataCheck |
dataset | Optional | object | array | none | Chart dataset for BarChart-style data; used by useChartDataCheck |
isLoading | Optional | boolean | false | When true, shows the loading overlay with default <Loader /> (animated dots) or customLoader, and hides chart children |
showNoDataPlaceholder | Optional | boolean | true | When true, shows empty placeholder when data/dataset has no values |
customLoader | Optional | ReactNode | <Loader /> | Node shown in the loading overlay (default core Loader) |
customNoDataMessage | Optional | string | ReactNode | none | Custom empty state message or component |
minHeight | Optional | number | 400 | Minimum height in pixels for empty state |
loadingMinHeight | Optional | number | minHeight | Minimum height in pixels for the loading overlay (default <Loader /> or customLoader) |
className | Optional | string | none | Root class name from the host app |
Chart children (LineChart, BarChart) come from @tetherto/mdk-react-devkit/core.
import { LineChart, BarChart } from '@tetherto/mdk-react-devkit/core'
const CustomSpinner = () => <div className="my-spinner" aria-hidden />Basic usage
<ChartWrapper
data={chartData}
isLoading={isLoading}
minHeight={400}
>
<LineChart data={chartData} />
</ChartWrapper>With custom loader
<ChartWrapper
data={chartData}
isLoading={isLoading}
customLoader={<CustomSpinner />}
minHeight={300}
>
<LineChart data={chartData} />
</ChartWrapper>With custom empty message
Pass an empty dataset so ChartWrapper shows the empty placeholder instead of the chart. With non-empty dataset, children render and the custom
message is not shown.
import { BarChart } from '@tetherto/mdk-react-devkit/core'
const emptyBarData = { labels: [], datasets: [] }
<ChartWrapper
dataset={emptyBarData}
isLoading={false}
customNoDataMessage="No hashrate data available for this period"
minHeight={300}
>
<BarChart data={emptyBarData} />
</ChartWrapper>States
The component handles three states automatically. Chart children are hidden while isLoading is true or when data / dataset is empty (and showNoDataPlaceholder is true):
- Loading: Shows default
<Loader />(animated dots) orcustomLoader - No data: Shows
EmptyStateplaceholder (orcustomNoDataMessage) - Has data: Shows chart content
Styling
.mdk-chart-wrapper: Root element.mdk-chart-wrapper__content: Chart content container.mdk-chart-wrapper__content--hidden: Hidden content state.mdk-chart-wrapper__empty: Empty state container.mdk-chart-wrapper__loading: Loading state container
ContainerCharts
Container-level chart dashboard with combination selector and multiple chart types.
Import
import { ContainerCharts } from '@tetherto/mdk-react-devkit/foundation'Props
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
featureEnabled | Optional | boolean | true | When false, shows disabledMessage instead of charts |
combinations | Required | { value: string; label: string }[] | none | Select options (value / label pairs) |
chartRawData | Optional | ChartEntry[] | null | Raw time-series rows; shape depends on selected container type |
selectedCombination | Optional | string | null | none | Controlled selected combinations[].value |
defaultSelectedCombination | Optional | string | null | null | Initial selection when uncontrolled |
onSelectedCombinationChange | Optional | function | none | Called with the new combination value (or null) when the user changes selection |
isLoadingCombinations | Optional | boolean | false | When true, shows loading state on the combination selector |
isLoadingCharts | Optional | boolean | false | When true, shows loading state on the chart grid |
disabledMessage | Optional | string | 'Container Charts feature is not enabled' | Message when featureEnabled is false |
title | Optional | string | 'Container Charts' | Section heading |
getDatasetBorderColor | Optional | function | none | Resolver for dataset border colors; receives dataset metadata from the builder |
ChartEntry type
type EntryData = {
[key: string]: number | null | undefined
}
type ChartEntry = {
ts: number | string
container_specific_stats_group_aggr: Record<string, EntryData | null | undefined>
}Each row’s container_specific_stats_group_aggr keys use complete container type ids (for example container-bd-d40-m30, container-as-hk3). Stat field names inside each entry depend on the container family.
Bitdeer (container-bd-… keys) — field names per chart block:
| Chart block | Stat field pattern |
|---|---|
| Liquid Temp H | hot_temp_c_w_{n}_group |
| Liquid Temp L | cold_temp_c_w_{n}_group |
| Oil Temp | cold_temp_c_{n}_group (no _w_) |
| Pressure | tank{n}_bar_group |
Hydro (container-as-hk3): supply_liquid_temp_group, supply_liquid_pressure_group.
Combination value strings must contain the real prefix patterns the component detects (bd-…, as-hk3…, as-immersion…, mbt-…). Values such as bitmain_hydro_b2 (underscores) do not match isAntspaceHydro() and show the wrong chart blocks.
const chartData: ChartEntry[] = [
{
ts: 1700000000,
container_specific_stats_group_aggr: {
'container-bd-d40-m30': {
hot_temp_c_w_1_group: 52,
hot_temp_c_w_2_group: 54,
cold_temp_c_w_1_group: 38,
cold_temp_c_w_2_group: 39,
cold_temp_c_1_group: 36,
cold_temp_c_2_group: 37,
tank1_bar_group: 2.4,
tank2_bar_group: 2.5,
},
'container-as-hk3': {
supply_liquid_temp_group: 44,
supply_liquid_pressure_group: 2.8,
},
},
},
]Basic usage
<ContainerCharts
combinations={[
{ value: 'bd-d40-m30_wm-m30sp', label: 'Bitdeer M30SP' },
{ value: 'as-hk3_am-s19xp_h', label: 'Bitmain Hydro S19XP' },
]}
defaultSelectedCombination="bd-d40-m30_wm-m30sp"
chartRawData={chartData}
onSelectedCombinationChange={setSelected}
/>Container type charts
Charts displayed vary by container type:
- Bitdeer: Liquid Temp H, Liquid Temp L, Oil Temp, Pressure
- Bitmain Hydro: Liquid Temp L, Pressure
- Bitmain Immersion: Liquid Temp L, Oil Temp
- MicroBT: Liquid Temp L, Pressure
Styling
.mdk-container-charts: Root element.mdk-container-charts__title: Section title.mdk-container-charts__select-row: Combination selector row.mdk-container-charts__select-label: Selector label.mdk-container-charts__select: Select dropdown.mdk-container-charts__layout: Charts grid layout.mdk-container-charts__chart-block: Individual chart container.mdk-container-charts__chart-title: Chart block title.mdk-container-charts__loading: Loading state container
LineChartCard
Composable card with timeline selector, legend, line chart, and stats footer.
Import
import { LineChartCard } from '@tetherto/mdk-react-devkit/foundation'Props
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
data | Optional | LineChartCardData | none | Pre-processed chart payload passed to the inner line chart |
rawData | Optional | unknown | none | Raw API payload; use with dataAdapter instead of data |
dataAdapter | Optional | function | none | Maps rawData to LineChartCardData |
timelineOptions | Optional | TimelineOption[] | none | Timeline range buttons |
timeline | Optional | string | none | Controlled active timeline value |
defaultTimeline | Optional | string | '5m' | Initial timeline when uncontrolled |
onTimelineChange | Optional | function | none | Called when the user selects a new timeline range |
title | Optional | string | none | Chart title in the card header |
detailLegends | Optional | boolean | false | When true, shows detailed legends below the title |
isLoading | Optional | boolean | false | When true, shows loading state |
shouldResetZoom | Optional | boolean | true | When true, resets chart zoom when the timeline changes |
minHeight | Optional | number | 350 | Minimum chart body height in pixels |
className | Optional | string | none | Root class name from the host app |
Basic usage
<LineChartCard
title="Temperature"
data={chartData}
timelineOptions={[
{ label: '5m', value: '5m' },
{ label: '1h', value: '1h' },
{ label: '24h', value: '24h' },
]}
defaultTimeline="1h"
/>With data adapter
<LineChartCard
title="Power Consumption"
rawData={rawStats}
dataAdapter={(raw) => processStatsToChartData(raw)}
timelineOptions={timelineOptions}
defaultTimeline="24h"
isLoading={isLoading}
/>Styling
.mdk-line-chart-card: Root element.mdk-line-chart-card__legends: Detail legends container
PowerModeTimelineChart
Visualizes power mode changes over time using TimelineChart.
Mining-specific wrapper around TimelineChart. Use TimelineChart directly when you supply custom row labels and segment data.
Import
import { PowerModeTimelineChart } from '@tetherto/mdk-react-devkit/foundation'Props
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
data | Optional | PowerModeTimelineEntry[] | [] | Historical power mode segments for TimelineChart |
dataUpdates | Optional | PowerModeTimelineEntry[] | [] | New segments merged into the chart when updates arrive |
timezone | Optional | string | 'UTC' | IANA timezone for segment timestamps |
title | Optional | string | 'Power Mode Timeline' | Chart title passed to the inner timeline |
isLoading | Optional | boolean | false | When true, shows loading state inside the chart card |
PowerModeTimelineEntry type
type PowerModeTimelineEntry = {
ts?: number
power_mode_group_aggr?: Record<string, string>
status_group_aggr?: Record<string, string>
}Basic usage
<PowerModeTimelineChart
data={powerModeHistory}
timezone="America/New_York"
isLoading={isLoading}
/>With real-time updates
<PowerModeTimelineChart
data={powerModeHistory}
dataUpdates={realtimeUpdates}
timezone={userTimezone}
/>TimelineChart
Horizontal timeline visualization for time-based events like power mode changes.
Gantt-like timeline for multiple rows. labels lists row names (Y axis). Each dataset groups colored segments; each segment's y must
match one of labels, and x is [startMs, endMs].
Import
import { TimelineChart, emptyTimelineChartData } from '@tetherto/mdk-react-devkit/foundation'
import type { TimelineChartData } from '@tetherto/mdk-react-devkit/foundation'Empty initial state
Use emptyTimelineChartData ({ labels: [], datasets: [] }) for the first render before live timeline data arrives. Pair with isLoading while fetching if needed.
<TimelineChart
initialData={emptyTimelineChartData}
title="Miner state"
isLoading={isLoading}
/>Props
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
initialData | Required | TimelineChartData | none | Row labels and segment datasets shown on first render |
newData | Optional | TimelineChartData | none | Extra labels and datasets merged when skipUpdates is false |
skipUpdates | Optional | boolean | false | When true, ignores newData |
range | Optional | ChartRange | none | Visible time window (min / max as Date or epoch ms) |
axisTitleText | Optional | AxisTitleText | { x: 'Time', y: '' } | Axis title strings |
isLoading | Optional | boolean | false | Shows loading state inside ChartContainer |
title | Optional | string | none | Chart title passed to ChartContainer |
height | Optional | number | string | none | Root height (CSS length or pixels) |
TimelineChartData type
See also Timeline chart types.
type TimelineChartDataPoint = {
x: [number, number] // segment start and end (epoch ms)
y: string | undefined // must match an entry in labels
}
type TimelineChartDataset = {
label: string
data: TimelineChartDataPoint[]
borderColor?: string[]
backgroundColor?: string[]
color?: string
}
type TimelineChartData = {
labels: string[]
datasets: TimelineChartDataset[]
}
type ChartRange = {
min: Date | number
max: Date | number
}Basic usage
const start = Date.now() - 2 * 60 * 60 * 1000
<TimelineChart
initialData={{
labels: ['Task A', 'Task B', 'Task C'],
datasets: [
{
label: 'In Progress',
color: '#22afff',
data: [
{ x: [start, start + 30 * 60 * 1000], y: 'Task A' },
{ x: [start + 20 * 60 * 1000, start + 50 * 60 * 1000], y: 'Task B' },
],
},
{
label: 'Completed',
color: '#72f59e',
data: [{ x: [start + 30 * 60 * 1000, start + 60 * 60 * 1000], y: 'Task A' }],
},
],
}}
title="Project timeline"
axisTitleText={{ x: 'Time', y: 'Tasks' }}
/>With newData updates
<TimelineChart
initialData={baseline}
newData={streamingUpdate}
skipUpdates={false}
/>When newData arrives, labels are unioned and datasets are appended. Set skipUpdates to keep only initialData.
Styling
.mdk-timeline-chart: Root element.mdk-timeline-chart__loading: Loading container.mdk-timeline-chart__legend,.mdk-timeline-chart__legend-item,.mdk-timeline-chart__legend-color,.mdk-timeline-chart__legend-label: Legend row.mdk-timeline-chart__visualization: Scrollable chart body
WidgetTopRow
Dashboard header row displaying title, power consumption, and alarm indicators.
Import
import { WidgetTopRow } from '@tetherto/mdk-react-devkit/foundation'Props
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
title | Required | string | none | Widget title shown in the header row |
power | Optional | number | none | Power consumption value; formatted with unit when set |
unit | Optional | string | none | Unit suffix for power (for example kW) |
statsErrorMessage | Optional | string | ErrorWithTimestamp[] | none | Replaces the power readout with - and a formatErrors tooltip |
alarms | Optional | object | none | Map keyed by liquidAlarms, leakageAlarms, pressureAlarms, otherAlarms; values are Alert arrays |
className | Optional | string | none | Root class name from the host app |
Composition
WidgetTopRow renders the title, then four fixed alarm slots in order (liquid, leakage, pressure, other), then the power readout. Each slot reads the matching alarms key from the table below; when the array is missing or empty, that slot is not shown. Tooltip text lists each Alert with timestamp and description.
| Slot label | alarms key | Icon role |
|---|---|---|
| Liquid | liquidAlarms | Temperature / liquid alarm |
| Leakage | leakageAlarms | Fluid / leakage alarm |
| Pressure | pressureAlarms | Pressure alarm |
| Other | otherAlarms | Other alarm |
When power and unit are set and statsErrorMessage is unset, the power label shows formatNumber(unitToKilo(power)) followed by the unit in a nested <span>.
Basic usage
<WidgetTopRow
title="Container A1"
power={125000}
unit="kW"
/>More examples
Styling
.mdk-widget-top-row: Root element.mdk-widget-top-row__inner: Inner container.mdk-widget-top-row__title: Title text.mdk-widget-top-row__alarm-info-icon: Alarm slot icon (tooltip trigger).mdk-widget-top-row__alarm-info-title: Alarm tooltip heading ({slot} issues).mdk-widget-top-row__power: Power display or error placeholder