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
ConsumptionLineChart
Power consumption trend chart built on LineChartCard.
Import
import { ConsumptionLineChart } from '@tetherto/mdk-react-devkit/foundation'Props
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
data | Optional | TailLogEntry[] | [] | Consumption log entries for the line series |
tag | Optional | string | 't-miner' | Data tag that selects which aggregated power field to read |
timelineOptions | Optional | TimelineOption[] | none | Range buttons shown above the chart; omit to hide the selector |
timeline | Optional | string | none | Controlled active range; pair with onTimelineChange |
defaultTimeline | Optional | string | none | Initial range when uncontrolled; falls back to 5m (or 1m when isOneMinEnabled) |
onTimelineChange | Optional | function | none | Called with the new range value when the user changes timeline |
powerAttribute | Optional | string | none | Overrides the derived power field name on each log entry |
label | Optional | string | none | Legend label (default: total miner consumption text) |
rawConsumptionW | Optional | number | string | none | Raw consumption value for the detail legend |
isOneMinEnabled | Optional | boolean | none | When true, adds a 1m timeline option |
totalTransformerConsumption | Optional | boolean | none | When true, uses transformer total consumption semantics |
isDetailed | Optional | boolean | none | When true, shows the detail legend with the current value |
skipMinMaxAvg | Optional | boolean | false | When true, hides the min/max/avg footer row |
Basic usage
<ConsumptionLineChart
data={consumptionLogs}
timelineOptions={timelineOptions}
defaultTimeline="5m"
/>Controlled timeline
const [timeline, setTimeline] = useState('5m')
const handleTimelineChange = (value: string) => setTimeline(value)
<ConsumptionLineChart
data={consumptionLogs}
timelineOptions={timelineOptions}
timeline={timeline}
onTimelineChange={handleTimelineChange}
/>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
HashRateLineChartSelector
Hashrate over time visualization with timeline selector and real-time data support. Switches between a plain hashrate layout and a pool-overlay layout based on hasPoolLine.
All props are optional.
Import
import { HashRateLineChartSelector } from '@tetherto/mdk-react-devkit/foundation'Behaviour
HashRateLineChartSelector renders one of two layouts based on hasPoolLine:
hasPoolLine={false}(default) — a single hashrate line driven bydataandrealtimeHashrateData, with timeline range selector and a highlighted current-value readout.hasPoolLine={true}— a multi-series chart that overlays the miner's hashrate against per-pool breakdowns, driven byminerTailLogDataandminerPoolDataRaw.
Pass the props for the layout you want; props for the other layout have no effect.
Layout
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
hasPoolLine | Optional | boolean | false | When true, renders HashRateLineChartWithPool; when false, plain hashrate line |
Plain-layout props
Used when hasPoolLine is false.
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
data | Optional | HashRateLogEntry[] | [] | Historical hashrate log entries for the primary line |
realtimeHashrateData | Optional | HashRateLogEntry | none | Latest sample merged into the series for the current-value readout |
fixedTimezone | Optional | string | none | IANA timezone for axis tick formatting |
height | Optional | number | none | Chart height in pixels |
loading | Optional | boolean | none | When true, shows loading state and hides the footer readout |
Pool overlay layout props
Used when hasPoolLine is true.
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
minerTailLogData | Optional | HashRateLogEntry[] | [] | Miner hashrate samples (flat array or array of arrays) |
minerPoolDataRaw | Optional | MinerPoolDataItem[] | [] | Per-pool hashrate samples overlaid on the miner line |
isMinerTailLogLoading | Optional | boolean | false | When true, initial-load state for the miner series |
isMinerTailLogFetching | Optional | boolean | false | When true, background refresh indicator for the miner series |
isMinerpoolInitialLoading | Optional | boolean | false | When true, initial-load state for the pool overlay series |
Shared props
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
isOneMinEnabled | Optional | boolean | none | When true, adds a 1m option to the timeline range selector in both layouts |
HashRateLogEntry type
type HashRateLogEntry = {
ts: number
hashrate_mhs_1m_sum_aggr?: number
[key: string]: unknown
}MinerPoolDataItem type
type PoolStat = {
poolType: string
hashrate: number
}
type MinerPoolDataItem = {
stats: PoolStat[]
ts: number | string
}Basic usage
<HashRateLineChartSelector
data={hashrateHistory}
realtimeHashrateData={currentHashrate}
isOneMinEnabled
height={400}
loading={isLoading}
/>With fixed timezone
<HashRateLineChartSelector
data={hashrateHistory}
fixedTimezone="America/New_York"
height={400}
loading={false}
/>With pool overlay
<HashRateLineChartSelector
hasPoolLine
minerTailLogData={tailLog}
isMinerTailLogLoading={isInitialLoading}
isMinerTailLogFetching={isRefetching}
minerPoolDataRaw={poolSamples}
isMinerpoolInitialLoading={isPoolInitialLoading}
isOneMinEnabled
/>Features
- Timeline range selector:
5m,15m,1h,6h,24h,7d(plus1mwhenisOneMinEnabled) - Legend toggle: show/hide individual datasets
- Min/Max/Avg readout derived from the visible series
- Highlighted current-value readout (plain layout)
- Pool type breakdown overlay (pool overlay layout)
Styling
Built on ChartContainer and LineChart from @tetherto/mdk-react-devkit/core. The pool-overlay layout adds a .mdk-hash-rate-line-chart-with-pool root wrapper. See core chart styling for customization options.
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