mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-14 21:32:04 +00:00
Compare commits
2 Commits
feat/histo
...
perses
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54b429acae | ||
|
|
2c948ef9f6 |
1466
docs/earlier-thoughts/dashboard-v6-schema.json
Normal file
1466
docs/earlier-thoughts/dashboard-v6-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
436
docs/earlier-thoughts/perses-feasibility-analysis.md
Normal file
436
docs/earlier-thoughts/perses-feasibility-analysis.md
Normal file
@@ -0,0 +1,436 @@
|
||||
# Feasibility Analysis: Adopting Perses.dev Specification for SigNoz Dashboards
|
||||
|
||||
## Executive Summary
|
||||
|
||||
SigNoz's dashboard JSON has been a free-form `map[string]interface{}` with no schema enforcement. This document evaluates adopting [Perses.dev](https://perses.dev/) (a CNCF Sandbox project) as a structured dashboard specification. The conclusion is that **wholesale adoption is not recommended**, but several Perses design patterns should be borrowed into a SigNoz-native v6 dashboard schema.
|
||||
|
||||
---
|
||||
|
||||
## 1. Current State: SigNoz Dashboard JSON
|
||||
|
||||
### 1.1 Structure Overview
|
||||
|
||||
The dashboard is stored as `StorableDashboardData = map[string]interface{}` in Go (see `pkg/types/dashboardtypes/dashboard.go`). Top-level fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `title` | `string` | Dashboard display name |
|
||||
| `description` | `string` | Dashboard description text |
|
||||
| `tags` | `string[]` | Categorization tags (e.g., `["redis", "database"]`) |
|
||||
| `image` | `string` | Base64 or URL-encoded SVG for dashboard icon/thumbnail |
|
||||
| `version` | `string` | Schema version: `"v4"` or `"v5"` |
|
||||
| `layout` | `Layout[]` | React-grid-layout positioning for each widget |
|
||||
| `panelMap` | `Record<string, {widgets: Layout[], collapsed: boolean}>` | Groups panels under row widgets for collapsible sections |
|
||||
| `widgets` | `Widget[]` | Array of panel/widget definitions |
|
||||
| `variables` | `Record<string, IDashboardVariable>` | Dashboard variable definitions |
|
||||
| `uploadedGrafana` | `boolean` | Flag indicating if imported from Grafana |
|
||||
|
||||
### 1.2 Panel/Widget Types
|
||||
|
||||
| Panel Type | Constant | API Request Type |
|
||||
|-----------|----------|-----------------|
|
||||
| Time Series | `graph` | `time_series` |
|
||||
| Bar Chart | `bar` | `time_series` |
|
||||
| Table | `table` | `scalar` |
|
||||
| Pie Chart | `pie` | `scalar` |
|
||||
| Single Value | `value` | `scalar` |
|
||||
| List/Logs | `list` | `raw` |
|
||||
| Trace | `trace` | `trace` |
|
||||
| Histogram | `histogram` | `distribution` |
|
||||
| Row (group header) | `row` | N/A |
|
||||
|
||||
### 1.3 Query System
|
||||
|
||||
Each widget carries a `query` object with three modes simultaneously:
|
||||
|
||||
```json
|
||||
{
|
||||
"queryType": "builder|clickhouse_sql|promql",
|
||||
"builder": {
|
||||
"queryData": [],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [],
|
||||
"promql": []
|
||||
}
|
||||
```
|
||||
|
||||
Only `queryType` determines which mode is active; the other sections carry empty placeholder defaults.
|
||||
|
||||
#### Builder Query v4 (legacy, widely used)
|
||||
- `aggregateOperator` + `aggregateAttribute` as separate fields
|
||||
- `filters` with structured `items[]` containing key objects with synthetic IDs
|
||||
- `having: []` as array
|
||||
|
||||
#### Builder Query v5 (newer, integration dashboards)
|
||||
- `aggregations[]` array with `metricName`, `timeAggregation`, `spaceAggregation` combined
|
||||
- `filter` with expression string (e.g., `"host_name IN $host_name"`)
|
||||
- `having: {expression: ""}` as object
|
||||
|
||||
### 1.4 Query Range V5 API
|
||||
|
||||
The V5 execution API wraps queries in a `QueryEnvelope` discriminated union:
|
||||
|
||||
```
|
||||
QueryEnvelope = {type: QueryType, spec: any}
|
||||
```
|
||||
|
||||
Seven query types: `builder_query`, `builder_formula`, `builder_sub_query`, `builder_join`, `builder_trace_operator`, `promql`, `clickhouse_sql`
|
||||
|
||||
Six request types: `scalar`, `time_series`, `raw`, `raw_stream`, `trace`, `distribution`
|
||||
|
||||
Three signals with distinct aggregation models:
|
||||
- **Metrics**: `metricName + temporality + timeAggregation + spaceAggregation`
|
||||
- **Traces**: Expression-based (e.g., `"COUNT()"`, `"p99(duration_nano)"`)
|
||||
- **Logs**: Expression-based (e.g., `"COUNT()"`, `"count_distinct(host.name)"`)
|
||||
|
||||
17 post-processing functions, server-side formula evaluation, SQL-style joins, and trace span relationship operators.
|
||||
|
||||
### 1.5 Documented Pain Points
|
||||
|
||||
1. **`StorableDashboardData` is `map[string]interface{}`**: All nested property access requires manual type assertions with fragile `ok` checks.
|
||||
|
||||
2. **Two incompatible query schema versions coexisting**: v4 and v5 query formats coexist in the same codebase. The backend migration layer (`pkg/transition/migrate_common.go`) converts at execution time.
|
||||
|
||||
3. **Massive boilerplate**: Every widget carries `selectedLogFields` and `selectedTracesFields` arrays even for metrics-only panels. Identical 5-element arrays copy-pasted hundreds of times.
|
||||
|
||||
4. **Duplicate query slots**: Every widget carries all three query types (`builder`, `clickhouse_sql`, `promql`) with empty placeholders for inactive types.
|
||||
|
||||
5. **Variable key inconsistency**: Variables keyed by human-readable name (e.g., `"Account"`) OR UUID depending on dashboard.
|
||||
|
||||
6. **Variables coupled to ClickHouse SQL**: Variable queries use raw `SELECT ... FROM signoz_metrics.distributed_time_series_v4_1day`, coupling dashboard definitions to internal storage schema.
|
||||
|
||||
7. **Redundant synthetic IDs**: Filter keys contain derived `id` fields like `"cloud_account_id--string--tag--false"`.
|
||||
|
||||
8. **Spelling errors baked in**: `"timePreferance"` (misspelled) is embedded in the serialized JSON contract.
|
||||
|
||||
9. **Layout/widget coupling implicit**: Layout items reference widgets by matching `i` to `id` with no schema enforcement. `panelMap` adds another implicit layer.
|
||||
|
||||
10. **No schema validation**: Dashboard data has no Go struct for validation. Relies entirely on frontend TypeScript types with extensive optional markers.
|
||||
|
||||
---
|
||||
|
||||
## 2. Perses.dev Specification Overview
|
||||
|
||||
### 2.1 What is Perses?
|
||||
|
||||
Perses is a CNCF Sandbox project providing:
|
||||
- An **open dashboard specification** (implemented in Go, CUE, TypeScript)
|
||||
- A **plugin-based extension model** for panels, queries, datasources, and variables
|
||||
- **Dashboard-as-Code** via CUE and Go SDKs
|
||||
- **Static validation** via `percli` CLI
|
||||
- **Grafana migration** tooling
|
||||
|
||||
Adopters: Chronosphere, RedHat, SAP, Amadeus.
|
||||
|
||||
### 2.2 Dashboard Structure
|
||||
|
||||
```yaml
|
||||
kind: Dashboard
|
||||
metadata:
|
||||
name: "..."
|
||||
project: "..."
|
||||
spec:
|
||||
display: {name, description}
|
||||
datasources: {name: DatasourceSpec} # inline or referenced
|
||||
variables: [Variable] # ordered list
|
||||
panels: {id: Panel} # map of panel definitions
|
||||
layouts: [Layout] # separate from panels
|
||||
duration: "5m"
|
||||
refreshInterval: "30s"
|
||||
```
|
||||
|
||||
### 2.3 Core Design: Plugin = `{kind: string, spec: any}`
|
||||
|
||||
The universal extension point. Panels, queries, datasources, and variables are all plugins:
|
||||
|
||||
```go
|
||||
type Plugin struct {
|
||||
Kind string `json:"kind"`
|
||||
Metadata *PluginMetadata `json:"metadata,omitempty"`
|
||||
Spec any `json:"spec"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Panel Structure
|
||||
|
||||
```yaml
|
||||
kind: Panel
|
||||
spec:
|
||||
display: {name, description}
|
||||
plugin: {kind: "TimeSeriesChart", spec: {...}}
|
||||
queries:
|
||||
- kind: TimeSeriesQuery
|
||||
spec:
|
||||
plugin:
|
||||
kind: PrometheusTimeSeriesQuery
|
||||
spec: {query: "up", datasource: "$ds"}
|
||||
```
|
||||
|
||||
### 2.5 Layout System
|
||||
|
||||
Panels separated from layout. Grid-based positioning with JSON `$ref` pointers:
|
||||
|
||||
```yaml
|
||||
kind: Grid
|
||||
spec:
|
||||
display:
|
||||
title: "Section Name"
|
||||
collapse: {open: true}
|
||||
items:
|
||||
- x: 0, y: 0, width: 6, height: 6
|
||||
content: {"$ref": "#/spec/panels/my_panel"}
|
||||
```
|
||||
|
||||
### 2.6 Supported Datasources
|
||||
|
||||
| Datasource | Plugin Kind | Protocol |
|
||||
|---|---|---|
|
||||
| Prometheus | `PrometheusDatasource` | PromQL |
|
||||
| Tempo | `TempoDatasource` | TraceQL |
|
||||
| Loki | `LokiDatasource` | LogQL |
|
||||
| Pyroscope | `PyroscopeDatasource` | Pyroscope API |
|
||||
| ClickHouse | Community plugin | SQL |
|
||||
| VictoriaLogs | Community plugin | VictoriaLogs API |
|
||||
|
||||
### 2.7 Plugin System
|
||||
|
||||
Five plugin categories: datasource, query, panel, variable, explore. Each distributes as a compressed archive with CUE schemas, React components (via module federation), and optional Grafana migration logic.
|
||||
|
||||
---
|
||||
|
||||
## 3. Feasibility Assessment
|
||||
|
||||
### 3.1 Support for Logs/Metrics/Traces/Events/Profiles
|
||||
|
||||
| Signal | Perses Status | SigNoz Requirement | Gap |
|
||||
|---|---|---|---|
|
||||
| **Metrics** | Prometheus plugin (mature) | ClickHouse-backed with dual aggregation model (time + space) | **Significant** - Perses assumes PromQL |
|
||||
| **Traces** | Tempo plugin (exists) | ClickHouse-backed with trace operators, span-level queries, joins | **Significant** - Perses Tempo does basic TraceQL |
|
||||
| **Logs** | Loki plugin (exists) | ClickHouse-backed with builder queries, raw list views, streaming | **Moderate** - Perses Loki uses LogQL |
|
||||
| **Profiles** | Pyroscope plugin (exists) | Not yet core in SigNoz dashboards | Low gap |
|
||||
| **Events** | No plugin | Future SigNoz need | Would require custom plugin |
|
||||
|
||||
Perses has plugins for all four pillars, but each assumes a specific backend protocol (PromQL, TraceQL, LogQL). SigNoz uses a **unified query builder** abstracting over ClickHouse. This is a fundamental architectural mismatch.
|
||||
|
||||
### 3.2 Extensibility
|
||||
|
||||
Perses's plugin architecture is genuinely extensible. SigNoz could create custom plugins (`SigNozDatasource`, `SigNozBuilderQuery`, etc.). However, this means:
|
||||
- Writing and maintaining a **full Perses plugin ecosystem** for SigNoz
|
||||
- Plugin must handle all 7 query types and 3 signal types
|
||||
- CUE schema definitions for all SigNoz query structures
|
||||
- Tracking Perses upstream changes (still a Sandbox project, not graduated)
|
||||
|
||||
### 3.3 Coupling Analysis
|
||||
|
||||
| Dimension | Current SigNoz | With Perses | Assessment |
|
||||
|---|---|---|---|
|
||||
| Dashboard to Storage | Variables use raw ClickHouse SQL | Would need SigNoz query plugin | Improvement possible |
|
||||
| Dashboard to Frontend | Widget types tightly coupled to React | Perses separates panel spec from rendering | Improvement |
|
||||
| Dashboard to Query API | Widgets carry full query objects | Plugin-typed, referenced via datasource | Improvement, but adds indirection |
|
||||
| Dashboard to Perses | N/A | Depends on Perses versioning, plugin compat, CUE toolchain | **New coupling** |
|
||||
|
||||
### 3.4 Support for Query Range V5
|
||||
|
||||
This is the **most critical gap**:
|
||||
|
||||
| SigNoz V5 Feature | Perses Equivalent | Plugin Solvable? |
|
||||
|---|---|---|
|
||||
| `builder_query` with signal-specific aggregation | Plugin `spec: any` | Yes, but SigNoz-specific |
|
||||
| `builder_formula` (cross-query math) | No formula concept | **Partially** - needs custom panel logic |
|
||||
| `builder_join` (SQL-style cross-signal joins) | No equivalent | **No** - fundamentally different model |
|
||||
| `builder_trace_operator` (span relationships) | No equivalent | **No** - unique to SigNoz |
|
||||
| `builder_sub_query` (nested queries) | No equivalent | Would need plugin extension |
|
||||
| Multiple query types per panel | Single-typed queries | Would need wrapper plugin |
|
||||
| Post-processing functions (ewma, anomaly, timeShift) | No equivalent | Would need to be in plugin spec |
|
||||
|
||||
---
|
||||
|
||||
## 4. Why NOT Adopt Perses Wholesale
|
||||
|
||||
### 4.1 SigNoz-inside-Perses
|
||||
|
||||
Every SigNoz query feature would live inside `spec: any` blobs within Perses plugin wrappers:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozBuilderQuery",
|
||||
"spec": { /* ALL SigNoz-specific content here */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Perses validates the envelope (`kind: TimeSeriesQuery` exists, `plugin.kind` is registered). But the actual content is opaque to Perses. You'd still need your own validation for everything inside `spec`.
|
||||
|
||||
### 4.2 Formulas, Joins, and Trace Operators Have No Home
|
||||
|
||||
Perses model: `Panel -> queries[] -> Query`. Each query is independent.
|
||||
|
||||
SigNoz model: `Panel -> compositeQuery -> {queries[], formulas[], joins[], traceOperators[]}`. Queries reference each other by name. Formulas combine results. Joins cross signals.
|
||||
|
||||
You'd have to either:
|
||||
- Shove the entire compositeQuery into a single plugin spec (making Perses query structure meaningless)
|
||||
- Fork/extend Perses core spec (ongoing merge conflicts)
|
||||
|
||||
### 4.3 Plugin Maintenance Burden
|
||||
|
||||
Required custom plugins:
|
||||
|
||||
| Plugin | Purpose |
|
||||
|---|---|
|
||||
| `SigNozDatasource` | Points to SigNoz query-service |
|
||||
| `SigNozBuilderQuery` | Wraps v5 builder queries for metrics/logs/traces |
|
||||
| `SigNozFormulaQuery` | Wraps formula evaluation |
|
||||
| `SigNozTraceOperatorQuery` | Wraps trace structural operators |
|
||||
| `SigNozJoinQuery` | Wraps cross-signal joins |
|
||||
| `SigNozSubQuery` | Wraps nested queries |
|
||||
| `SigNozAttributeValuesVariable` | Variable from attribute values |
|
||||
| `SigNozQueryVariable` | Variable from query results |
|
||||
|
||||
Each needs: CUE schema, React component, Grafana migration handler, and tests. Every v5 feature addition requires plugin schema updates.
|
||||
|
||||
### 4.4 Community Mismatch
|
||||
|
||||
Perses adopters are primarily Prometheus-centric. A `SigNozDatasource` plugin is useful only to SigNoz. You'd be the sole maintainer of the plugin suite.
|
||||
|
||||
### 4.5 The Counterargument
|
||||
|
||||
The one strong argument FOR wholesale adoption: **you get out of the "dashboard spec" business entirely**. Even if 80% is SigNoz-specific plugins, the 20% Perses handles (metadata, layout, display, variable ordering, versioning, RBAC scoping) is real work you don't have to maintain. If Perses graduates from CNCF sandbox, ecosystem benefits compound.
|
||||
|
||||
This trade-off doesn't justify the ongoing plugin maintenance tax, especially since those patterns are straightforward to implement natively. However, if SigNoz plans to eventually expose PromQL/TraceQL/LogQL-compatible endpoints, the calculus changes significantly.
|
||||
|
||||
---
|
||||
|
||||
## 5. Recommendation: Borrow Patterns, Build Native
|
||||
|
||||
### 5.1 Patterns to Adopt from Perses
|
||||
|
||||
| Perses Pattern | SigNoz Adoption |
|
||||
|---|---|
|
||||
| **`kind` + `spec` envelope** | Use for query types, panel types, variables. Consistent with v5 `QueryEnvelope`. |
|
||||
| **Panels separated from Layout** | `panels: {}` map + `layouts: []` referencing by ID. |
|
||||
| **Ordered variables as array** | Move from `variables: {name: {...}}` to `variables: [...]`. |
|
||||
| **CUE or JSON Schema validation** | Define formal schema for dashboards. Use for CI and import/export. |
|
||||
| **Dashboard-as-Code SDK** | Go/TypeScript SDK for programmatic dashboard generation. |
|
||||
| **Metadata structure** | `kind` + `apiVersion` + `metadata` + `spec` top-level (Kubernetes-style). |
|
||||
|
||||
### 5.2 Proposed v6 Dashboard Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "signoz.io/v1",
|
||||
"metadata": {
|
||||
"name": "redis-overview",
|
||||
"title": "Redis Overview",
|
||||
"description": "...",
|
||||
"tags": ["redis", "database"],
|
||||
"image": "..."
|
||||
},
|
||||
"spec": {
|
||||
"defaults": {
|
||||
"timeRange": "5m",
|
||||
"refreshInterval": "30s"
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"kind": "QueryVariable",
|
||||
"spec": {
|
||||
"name": "host_name",
|
||||
"signal": "metrics",
|
||||
"attributeName": "host_name",
|
||||
"multiSelect": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"panels": {
|
||||
"hit_rate": {
|
||||
"kind": "TimeSeriesPanel",
|
||||
"spec": {
|
||||
"title": "Hit Rate",
|
||||
"description": "Cache hit rate across hosts",
|
||||
"display": {
|
||||
"yAxisUnit": "percent",
|
||||
"legend": {"position": "bottom"}
|
||||
},
|
||||
"query": {
|
||||
"type": "composite",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [{"metricName": "redis_keyspace_hits", "timeAggregation": "rate", "spaceAggregation": "sum"}],
|
||||
"filter": {"expression": "host_name IN $host_name"}
|
||||
}
|
||||
}
|
||||
],
|
||||
"formulas": [
|
||||
{"type": "builder_formula", "spec": {"expression": "A / (A + B) * 100"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layouts": [
|
||||
{
|
||||
"kind": "Grid",
|
||||
"spec": {
|
||||
"title": "Overview",
|
||||
"collapsible": true,
|
||||
"collapsed": false,
|
||||
"items": [
|
||||
{"panel": "hit_rate", "x": 0, "y": 0, "w": 6, "h": 6}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Key Improvements Over Current Format
|
||||
|
||||
| Issue | Current | v6 |
|
||||
|---|---|---|
|
||||
| No validation | `map[string]interface{}` | JSON Schema enforced |
|
||||
| Boilerplate | Every widget has selectedLogFields, all query modes | Only active query mode stored, display options per panel type |
|
||||
| Variable ordering | `order` field inside map entries | Array position |
|
||||
| Variable keys | Name or UUID inconsistently | `name` field in spec, array position |
|
||||
| Layout coupling | Implicit `i` matches `id` | Explicit `panel` reference in layout items |
|
||||
| Spelling errors | `timePreferance` | `timePreference` (fixed) |
|
||||
| Query structure | Flat list of all queries + formulas | Typed envelope matching v5 API |
|
||||
| Row grouping | Separate `panelMap` with duplicate layout entries | Integrated into `layouts[]` with `collapsible` flag |
|
||||
|
||||
### 5.4 Migration Path
|
||||
|
||||
1. **v4/v5 to v6 migration**: Build a Go migration function that transforms existing dashboards to v6 format. Handle both v4 and v5 query formats.
|
||||
2. **Backward compatibility**: Support reading v4/v5 dashboards with automatic upgrade to v6 on save.
|
||||
3. **Frontend**: Update TypeScript interfaces to match v6 schema. Remove legacy response converters once v5 API is fully adopted.
|
||||
4. **Validation**: Add JSON Schema validation on dashboard create/update API endpoints.
|
||||
5. **Integration dashboards**: Regenerate all dashboards in `SigNoz/dashboards` repo using v6 format.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Perses Homepage](https://perses.dev/)
|
||||
- [Perses Dashboard API](https://perses.dev/perses/docs/api/dashboard/)
|
||||
- [Perses Open Specification](https://perses.dev/perses/docs/concepts/open-specification/)
|
||||
- [Perses Plugin Creation](https://perses.dev/perses/docs/plugins/creation/)
|
||||
- [Perses Prometheus Plugin Model](https://perses.dev/plugins/docs/prometheus/model/)
|
||||
- [Perses GitHub Repository](https://github.com/perses/perses)
|
||||
- [SigNoz Dashboards Repository](https://github.com/SigNoz/dashboards)
|
||||
- SigNoz source: `pkg/types/dashboardtypes/dashboard.go`
|
||||
- SigNoz source: `pkg/types/querybuildertypes/querybuildertypesv5/`
|
||||
- SigNoz source: `frontend/src/types/api/dashboard/getAll.ts`
|
||||
- SigNoz source: `frontend/src/types/api/queryBuilder/queryBuilderData.ts`
|
||||
- SigNoz source: `frontend/src/types/api/v5/queryRange.ts`
|
||||
- SigNoz source: `pkg/transition/migrate_common.go`
|
||||
1463
docs/rfc-perses-native-dashboard.md
Normal file
1463
docs/rfc-perses-native-dashboard.md
Normal file
File diff suppressed because it is too large
Load Diff
378
docs/signoz-perses-plugins.cue
Normal file
378
docs/signoz-perses-plugins.cue
Normal file
@@ -0,0 +1,378 @@
|
||||
// SigNoz Perses Plugin Schemas
|
||||
//
|
||||
// CUE schemas for all SigNoz-specific Perses plugin types.
|
||||
// These define the `spec` shape inside Perses Plugin wrappers:
|
||||
// { "kind": "<PluginKind>", "spec": <validated by this file> }
|
||||
//
|
||||
// Perses core types (Dashboard, Panel, Layout, Variable envelopes)
|
||||
// are validated by Perses itself. This file only covers SigNoz plugins.
|
||||
//
|
||||
// Usage:
|
||||
// percli lint --schema-dir ./signoz-perses-plugins dashboard.json
|
||||
//
|
||||
// Reference: https://perses.dev/perses/docs/api/plugin/
|
||||
|
||||
package signoz
|
||||
|
||||
// ============================================================================
|
||||
// Datasource Plugin
|
||||
// ============================================================================
|
||||
|
||||
// SigNozDatasource configures the connection to a SigNoz query service.
|
||||
// Used inside: DatasourceSpec.plugin { kind: "SigNozDatasource", spec: ... }
|
||||
#SigNozDatasource: {
|
||||
// Direct URL for embedded mode (SigNoz serves its own dashboards).
|
||||
// The frontend calls SigNoz APIs directly at this base URL.
|
||||
directUrl?: string & =~"^/|^https?://"
|
||||
|
||||
// HTTP proxy config for external mode (standalone Perses connecting to SigNoz).
|
||||
proxy?: #HTTPProxy
|
||||
}
|
||||
|
||||
#HTTPProxy: {
|
||||
kind: "HTTPProxy"
|
||||
spec: {
|
||||
url: string & =~"^https?://"
|
||||
allowedEndpoints?: [...#AllowedEndpoint]
|
||||
}
|
||||
}
|
||||
|
||||
#AllowedEndpoint: {
|
||||
endpointPattern: string
|
||||
method: "GET" | "POST" | "PUT" | "DELETE"
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query Plugins
|
||||
// ============================================================================
|
||||
|
||||
// SigNozBuilderQuery is a single builder query for one signal.
|
||||
// Used inside: TimeSeriesQuery.spec.plugin { kind: "SigNozBuilderQuery", spec: ... }
|
||||
// Use this when the panel has independent queries (no formulas, joins, or trace operators).
|
||||
#SigNozBuilderQuery: {
|
||||
name: #QueryName
|
||||
signal: #Signal
|
||||
source?: string
|
||||
|
||||
disabled?: bool | *false
|
||||
|
||||
// Metrics use structured aggregations; traces/logs use expression aggregations
|
||||
aggregations?: [...#MetricAggregation | #ExpressionAggregation]
|
||||
|
||||
filter?: #Filter
|
||||
groupBy?: [...#GroupByKey]
|
||||
order?: [...#OrderBy]
|
||||
having?: #Having
|
||||
|
||||
selectFields?: [...#TelemetryFieldKey]
|
||||
|
||||
limit?: int & >=0 & <=10000
|
||||
limitBy?: #LimitBy
|
||||
offset?: int & >=0
|
||||
cursor?: string
|
||||
|
||||
secondaryAggregations?: [...#SecondaryAggregation]
|
||||
functions?: [...#PostProcessingFunction]
|
||||
|
||||
legend?: string
|
||||
reduceTo?: #ReduceTo
|
||||
stepInterval?: #StepInterval
|
||||
}
|
||||
|
||||
// SigNozCompositeQuery bundles multiple queries with formulas, joins, or trace operators.
|
||||
// Used inside: TimeSeriesQuery.spec.plugin { kind: "SigNozCompositeQuery", spec: ... }
|
||||
// Use this when a panel needs cross-query references (A/B formulas, joins, trace operators).
|
||||
#SigNozCompositeQuery: {
|
||||
queries: [...#CompositeQueryEntry] & [_, ...] // at least one query
|
||||
|
||||
formulas?: [...#FormulaEntry]
|
||||
joins?: [...#JoinEntry]
|
||||
traceOperators?: [...#TraceOperatorEntry]
|
||||
}
|
||||
|
||||
// A query within a composite. Same shape as SigNozBuilderQuery.
|
||||
#CompositeQueryEntry: #SigNozBuilderQuery
|
||||
|
||||
// A formula referencing other queries by name.
|
||||
#FormulaEntry: {
|
||||
name: #QueryName
|
||||
expression: string & !="" // e.g. "A/B * 100"
|
||||
|
||||
disabled?: bool | *false
|
||||
|
||||
order?: [...#OrderBy]
|
||||
limit?: int & >=0 & <=10000
|
||||
having?: #Having
|
||||
functions?: [...#PostProcessingFunction]
|
||||
legend?: string
|
||||
}
|
||||
|
||||
// A join combining results from two queries.
|
||||
#JoinEntry: {
|
||||
name: #QueryName
|
||||
|
||||
disabled?: bool | *false
|
||||
|
||||
left: #QueryRef
|
||||
right: #QueryRef
|
||||
joinType: "inner" | "left" | "right" | "full" | "cross"
|
||||
on: string & !="" // join condition expression
|
||||
|
||||
aggregations?: [...#MetricAggregation | #ExpressionAggregation]
|
||||
selectFields?: [...#TelemetryFieldKey]
|
||||
filter?: #Filter
|
||||
groupBy?: [...#GroupByKey]
|
||||
having?: #Having
|
||||
order?: [...#OrderBy]
|
||||
limit?: int & >=0 & <=10000
|
||||
|
||||
secondaryAggregations?: [...#SecondaryAggregation]
|
||||
functions?: [...#PostProcessingFunction]
|
||||
}
|
||||
|
||||
// A trace operator expressing span relationships.
|
||||
#TraceOperatorEntry: {
|
||||
name: #QueryName
|
||||
|
||||
disabled?: bool | *false
|
||||
|
||||
// Operators: => (direct descendant), -> (indirect descendant),
|
||||
// && (AND), || (OR), NOT (exclude). Example: "A => B && C"
|
||||
expression: string & !=""
|
||||
|
||||
filter?: #Filter
|
||||
returnSpansFrom?: string
|
||||
|
||||
order?: [...#TraceOrderBy]
|
||||
aggregations?: [...#ExpressionAggregation]
|
||||
stepInterval?: #StepInterval
|
||||
groupBy?: [...#GroupByKey]
|
||||
having?: #Having
|
||||
limit?: int & >=0 & <=10000
|
||||
offset?: int & >=0
|
||||
cursor?: string
|
||||
legend?: string
|
||||
selectFields?: [...#TelemetryFieldKey]
|
||||
functions?: [...#PostProcessingFunction]
|
||||
}
|
||||
|
||||
// SigNozPromQL wraps a raw PromQL query.
|
||||
// Used inside: TimeSeriesQuery.spec.plugin { kind: "SigNozPromQL", spec: ... }
|
||||
#SigNozPromQL: {
|
||||
name: string
|
||||
query: string & !=""
|
||||
|
||||
disabled?: bool | *false
|
||||
step?: #StepInterval
|
||||
stats?: bool | *false
|
||||
legend?: string
|
||||
}
|
||||
|
||||
// SigNozClickHouseSQL wraps a raw ClickHouse SQL query.
|
||||
// Used inside: TimeSeriesQuery.spec.plugin { kind: "SigNozClickHouseSQL", spec: ... }
|
||||
#SigNozClickHouseSQL: {
|
||||
name: string
|
||||
query: string & !=""
|
||||
|
||||
disabled?: bool | *false
|
||||
legend?: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Variable Plugins
|
||||
// ============================================================================
|
||||
|
||||
// SigNozQueryVariable resolves variable values using a builder query.
|
||||
// Used inside: ListVariable.spec.plugin { kind: "SigNozQueryVariable", spec: ... }
|
||||
#SigNozQueryVariable: {
|
||||
// The query that produces variable values.
|
||||
// Uses the same builder query model as panels.
|
||||
query: #SigNozBuilderQuery | #SigNozCompositeQuery | #SigNozPromQL | #SigNozClickHouseSQL
|
||||
}
|
||||
|
||||
// SigNozAttributeValues resolves variable values from attribute autocomplete.
|
||||
// Used inside: ListVariable.spec.plugin { kind: "SigNozAttributeValues", spec: ... }
|
||||
// This is a simpler alternative to SigNozQueryVariable for common cases
|
||||
// like "list all values of host.name for metric X".
|
||||
#SigNozAttributeValues: {
|
||||
signal: #Signal
|
||||
metricName?: string // required when signal is "metrics"
|
||||
attributeName: string & !=""
|
||||
|
||||
filter?: #Filter // optional pre-filter
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Signals
|
||||
// ============================================================================
|
||||
|
||||
#Signal: "metrics" | "traces" | "logs"
|
||||
// Extensible to "events" | "profiles" in future
|
||||
|
||||
// ============================================================================
|
||||
// Aggregations
|
||||
// ============================================================================
|
||||
|
||||
// MetricAggregation defines the two-level aggregation model for metrics:
|
||||
// time aggregation (within each time bucket) then space aggregation (across dimensions).
|
||||
#MetricAggregation: {
|
||||
metricName: string & !=""
|
||||
|
||||
temporality?: #MetricTemporality
|
||||
timeAggregation?: #TimeAggregation
|
||||
spaceAggregation?: #SpaceAggregation
|
||||
reduceTo?: #ReduceTo
|
||||
}
|
||||
|
||||
// ExpressionAggregation uses ClickHouse aggregate function syntax for traces/logs.
|
||||
// Examples: "count()", "sum(item_price)", "p99(duration_nano)", "countIf(day > 10)"
|
||||
#ExpressionAggregation: {
|
||||
expression: string & !=""
|
||||
alias?: string
|
||||
}
|
||||
|
||||
#MetricTemporality: "delta" | "cumulative" | "unspecified"
|
||||
|
||||
#TimeAggregation:
|
||||
"latest" | "sum" | "avg" | "min" | "max" |
|
||||
"count" | "count_distinct" | "rate" | "increase"
|
||||
|
||||
#SpaceAggregation:
|
||||
"sum" | "avg" | "min" | "max" | "count" |
|
||||
"p50" | "p75" | "p90" | "p95" | "p99"
|
||||
|
||||
#ReduceTo: "sum" | "count" | "avg" | "min" | "max" | "last" | "median"
|
||||
|
||||
// ============================================================================
|
||||
// Filters
|
||||
// ============================================================================
|
||||
|
||||
// Filter uses expression syntax instead of structured items with synthetic IDs.
|
||||
// Supports: =, !=, >, >=, <, <=, IN, NOT IN, LIKE, NOT LIKE, ILIKE, NOT ILIKE,
|
||||
// BETWEEN, NOT BETWEEN, EXISTS, NOT EXISTS, REGEXP, NOT REGEXP, CONTAINS, NOT CONTAINS.
|
||||
// Variable interpolation: $variable_name
|
||||
#Filter: {
|
||||
expression: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Group By, Order By, Having
|
||||
// ============================================================================
|
||||
|
||||
#GroupByKey: {
|
||||
name: string & !=""
|
||||
signal?: #Signal
|
||||
fieldContext?: #FieldContext
|
||||
fieldDataType?: #FieldDataType
|
||||
}
|
||||
|
||||
#OrderBy: {
|
||||
key: #OrderByKey
|
||||
direction: "asc" | "desc"
|
||||
}
|
||||
|
||||
#OrderByKey: {
|
||||
name: string & !=""
|
||||
signal?: #Signal
|
||||
fieldContext?: #FieldContext
|
||||
fieldDataType?: #FieldDataType
|
||||
}
|
||||
|
||||
#TraceOrderBy: {
|
||||
key: {
|
||||
name: "span_count" | "trace_duration"
|
||||
}
|
||||
direction: "asc" | "desc"
|
||||
}
|
||||
|
||||
// Having applies a post-aggregation filter.
|
||||
// Example: "count() > 100"
|
||||
#Having: {
|
||||
expression: string & !=""
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Secondary Aggregations & Limits
|
||||
// ============================================================================
|
||||
|
||||
#SecondaryAggregation: {
|
||||
expression: string
|
||||
alias?: string
|
||||
|
||||
stepInterval?: #StepInterval
|
||||
groupBy?: [...#GroupByKey]
|
||||
order?: [...#OrderBy]
|
||||
limit?: int & >=0 & <=10000
|
||||
limitBy?: #LimitBy
|
||||
}
|
||||
|
||||
#LimitBy: {
|
||||
keys: [...string] & [_, ...] // at least one key
|
||||
value: string // max rows per group (string for compatibility)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Post-Processing Functions
|
||||
// ============================================================================
|
||||
|
||||
#PostProcessingFunction: {
|
||||
name: #FunctionName
|
||||
args?: [...#FunctionArg]
|
||||
}
|
||||
|
||||
#FunctionName:
|
||||
// Threshold functions
|
||||
"cutOffMin" | "cutOffMax" | "clampMin" | "clampMax" |
|
||||
// Math functions
|
||||
"absolute" | "runningDiff" | "log2" | "log10" | "cumulativeSum" |
|
||||
// Smoothing functions (exponentially weighted moving average)
|
||||
"ewma3" | "ewma5" | "ewma7" |
|
||||
// Smoothing functions (sliding median window)
|
||||
"median3" | "median5" | "median7" |
|
||||
// Time functions
|
||||
"timeShift" |
|
||||
// Analysis functions
|
||||
"anomaly" |
|
||||
// Gap filling
|
||||
"fillZero"
|
||||
|
||||
#FunctionArg: {
|
||||
name?: string
|
||||
value: number | string | bool
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Telemetry Field References
|
||||
// ============================================================================
|
||||
|
||||
#TelemetryFieldKey: {
|
||||
name: string & !=""
|
||||
description?: string
|
||||
unit?: string
|
||||
signal?: #Signal
|
||||
fieldContext?: #FieldContext
|
||||
fieldDataType?: #FieldDataType
|
||||
}
|
||||
|
||||
#FieldContext:
|
||||
"resource" | "scope" | "span" | "event" | "link" |
|
||||
"log" | "metric" | "body" | "trace"
|
||||
|
||||
#FieldDataType:
|
||||
"string" | "int64" | "float64" | "bool" |
|
||||
"array(string)" | "array(int64)" | "array(float64)" | "array(bool)"
|
||||
|
||||
// ============================================================================
|
||||
// Common Types
|
||||
// ============================================================================
|
||||
|
||||
// QueryName must be a valid identifier: starts with letter, contains letters/digits/underscores.
|
||||
#QueryName: =~"^[A-Za-z][A-Za-z0-9_]*$"
|
||||
|
||||
// QueryRef references another query by name within a composite query.
|
||||
#QueryRef: {
|
||||
name: string & !=""
|
||||
}
|
||||
|
||||
// StepInterval accepts seconds (numeric) or duration string ("15s", "1m", "1h").
|
||||
#StepInterval: number & >=0 | =~"^[0-9]+(ns|us|ms|s|m|h)$"
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useCallback } from 'react';
|
||||
import ChartWrapper from 'container/DashboardContainer/visualization/charts/ChartWrapper/ChartWrapper';
|
||||
import BarChartTooltip from 'lib/uPlotV2/components/Tooltip/BarChartTooltip';
|
||||
import {
|
||||
BarTooltipProps,
|
||||
TooltipRenderArgs,
|
||||
} from 'lib/uPlotV2/components/types';
|
||||
|
||||
import { useBarChartStacking } from '../../hooks/useBarChartStacking';
|
||||
import { BarChartProps } from '../types';
|
||||
|
||||
export default function BarChart(props: BarChartProps): JSX.Element {
|
||||
const { children, isStackedBarChart, config, data, ...rest } = props;
|
||||
|
||||
const chartData = useBarChartStacking({
|
||||
data,
|
||||
isStackedBarChart,
|
||||
config,
|
||||
});
|
||||
|
||||
const renderTooltip = useCallback(
|
||||
(props: TooltipRenderArgs): React.ReactNode => {
|
||||
const tooltipProps: BarTooltipProps = {
|
||||
...props,
|
||||
timezone: rest.timezone,
|
||||
yAxisUnit: rest.yAxisUnit,
|
||||
decimalPrecision: rest.decimalPrecision,
|
||||
isStackedBarChart: isStackedBarChart,
|
||||
};
|
||||
return <BarChartTooltip {...tooltipProps} />;
|
||||
},
|
||||
[rest.timezone, rest.yAxisUnit, rest.decimalPrecision, isStackedBarChart],
|
||||
);
|
||||
|
||||
return (
|
||||
<ChartWrapper
|
||||
{...rest}
|
||||
config={config}
|
||||
data={chartData}
|
||||
renderTooltip={renderTooltip}
|
||||
>
|
||||
{children}
|
||||
</ChartWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import uPlot, { AlignedData } from 'uplot';
|
||||
|
||||
/**
|
||||
* Stack data cumulatively (top-down: first series = top, last = bottom).
|
||||
* When `omit(seriesIndex)` returns true, that series is excluded from stacking.
|
||||
*/
|
||||
export function stackSeries(
|
||||
data: AlignedData,
|
||||
omit: (seriesIndex: number) => boolean,
|
||||
): { data: AlignedData; bands: uPlot.Band[] } {
|
||||
const timeAxis = data[0];
|
||||
const pointCount = timeAxis.length;
|
||||
const valueSeriesCount = data.length - 1; // exclude time axis
|
||||
|
||||
const stackedSeries = buildStackedSeries({
|
||||
data,
|
||||
valueSeriesCount,
|
||||
pointCount,
|
||||
omit,
|
||||
});
|
||||
const bands = buildFillBands(valueSeriesCount + 1, omit); // +1 for 1-based series indices
|
||||
|
||||
return {
|
||||
data: [timeAxis, ...stackedSeries] as AlignedData,
|
||||
bands,
|
||||
};
|
||||
}
|
||||
|
||||
interface BuildStackedSeriesParams {
|
||||
data: AlignedData;
|
||||
valueSeriesCount: number;
|
||||
pointCount: number;
|
||||
omit: (seriesIndex: number) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accumulate from last series upward: last series = raw values, first = total.
|
||||
* Omitted series are copied as-is (no accumulation).
|
||||
*/
|
||||
function buildStackedSeries({
|
||||
data,
|
||||
valueSeriesCount,
|
||||
pointCount,
|
||||
omit,
|
||||
}: BuildStackedSeriesParams): (number | null)[][] {
|
||||
const stackedSeries: (number | null)[][] = Array(valueSeriesCount);
|
||||
const cumulativeSums = Array(pointCount).fill(0) as number[];
|
||||
|
||||
for (let seriesIndex = valueSeriesCount; seriesIndex >= 1; seriesIndex--) {
|
||||
const rawValues = data[seriesIndex] as (number | null)[];
|
||||
|
||||
if (omit(seriesIndex)) {
|
||||
stackedSeries[seriesIndex - 1] = rawValues;
|
||||
} else {
|
||||
stackedSeries[seriesIndex - 1] = rawValues.map((rawValue, pointIndex) => {
|
||||
const numericValue = rawValue == null ? 0 : Number(rawValue);
|
||||
return (cumulativeSums[pointIndex] += numericValue);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return stackedSeries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bands define fill between consecutive visible series for stacked appearance.
|
||||
* uPlot format: [upperSeriesIdx, lowerSeriesIdx].
|
||||
*/
|
||||
function buildFillBands(
|
||||
seriesLength: number,
|
||||
omit: (seriesIndex: number) => boolean,
|
||||
): uPlot.Band[] {
|
||||
const bands: uPlot.Band[] = [];
|
||||
|
||||
for (let seriesIndex = 1; seriesIndex < seriesLength; seriesIndex++) {
|
||||
if (omit(seriesIndex)) {
|
||||
continue;
|
||||
}
|
||||
const nextVisibleSeriesIndex = findNextVisibleSeriesIndex(
|
||||
seriesLength,
|
||||
seriesIndex,
|
||||
omit,
|
||||
);
|
||||
if (nextVisibleSeriesIndex !== -1) {
|
||||
bands.push({ series: [seriesIndex, nextVisibleSeriesIndex] });
|
||||
}
|
||||
}
|
||||
|
||||
return bands;
|
||||
}
|
||||
|
||||
function findNextVisibleSeriesIndex(
|
||||
seriesLength: number,
|
||||
afterIndex: number,
|
||||
omit: (seriesIndex: number) => boolean,
|
||||
): number {
|
||||
for (let i = afterIndex + 1; i < seriesLength; i++) {
|
||||
if (!omit(i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns band indices for initial stacked state (no series omitted).
|
||||
* Top-down: first series at top, band fills between consecutive series.
|
||||
* uPlot band format: [upperSeriesIdx, lowerSeriesIdx].
|
||||
*/
|
||||
export function getInitialStackedBands(seriesCount: number): uPlot.Band[] {
|
||||
const bands: uPlot.Band[] = [];
|
||||
for (let seriesIndex = 1; seriesIndex < seriesCount; seriesIndex++) {
|
||||
bands.push({ series: [seriesIndex, seriesIndex + 1] });
|
||||
}
|
||||
return bands;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
MutableRefObject,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { has } from 'lodash-es';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { stackSeries } from '../charts/utils/stackSeriesUtils';
|
||||
|
||||
/** Returns true if the series at the given index is hidden (e.g. via legend toggle). */
|
||||
function isSeriesHidden(plot: uPlot, seriesIndex: number): boolean {
|
||||
return !plot.series[seriesIndex]?.show;
|
||||
}
|
||||
|
||||
function canApplyStacking(
|
||||
unstackedData: uPlot.AlignedData | null,
|
||||
plot: uPlot,
|
||||
isUpdating: boolean,
|
||||
): boolean {
|
||||
return (
|
||||
!isUpdating &&
|
||||
!!unstackedData &&
|
||||
!!plot.data &&
|
||||
unstackedData[0]?.length === plot.data[0]?.length
|
||||
);
|
||||
}
|
||||
|
||||
function setupStackingHooks(
|
||||
config: UPlotConfigBuilder,
|
||||
applyStackingToChart: (plot: uPlot) => void,
|
||||
isUpdatingRef: MutableRefObject<boolean>,
|
||||
): () => void {
|
||||
const onDataChange = (plot: uPlot): void => {
|
||||
if (!isUpdatingRef.current) {
|
||||
applyStackingToChart(plot);
|
||||
}
|
||||
};
|
||||
|
||||
const onSeriesVisibilityChange = (
|
||||
plot: uPlot,
|
||||
_seriesIdx: number | null,
|
||||
opts: uPlot.Series,
|
||||
): void => {
|
||||
if (!has(opts, 'focus')) {
|
||||
applyStackingToChart(plot);
|
||||
}
|
||||
};
|
||||
|
||||
const removeSetDataHook = config.addHook('setData', onDataChange);
|
||||
const removeSetSeriesHook = config.addHook(
|
||||
'setSeries',
|
||||
onSeriesVisibilityChange,
|
||||
);
|
||||
|
||||
return (): void => {
|
||||
removeSetDataHook?.();
|
||||
removeSetSeriesHook?.();
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseBarChartStackingParams {
|
||||
data: uPlot.AlignedData;
|
||||
isStackedBarChart?: boolean;
|
||||
config: UPlotConfigBuilder | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles stacking for bar charts: computes initial stacked data and re-stacks
|
||||
* when data or series visibility changes (e.g. legend toggles).
|
||||
*/
|
||||
export function useBarChartStacking({
|
||||
data,
|
||||
isStackedBarChart = false,
|
||||
config,
|
||||
}: UseBarChartStackingParams): uPlot.AlignedData {
|
||||
// Store unstacked source data so uPlot hooks can access it (hooks run outside React's render cycle)
|
||||
const unstackedDataRef = useRef<uPlot.AlignedData | null>(null);
|
||||
unstackedDataRef.current = isStackedBarChart ? data : null;
|
||||
|
||||
// Prevents re-entrant calls when we update chart data (avoids infinite loop in setData hook)
|
||||
const isUpdatingChartRef = useRef(false);
|
||||
|
||||
const chartData = useMemo((): uPlot.AlignedData => {
|
||||
if (!isStackedBarChart || !data || data.length < 2) {
|
||||
return data;
|
||||
}
|
||||
const noSeriesHidden = (): boolean => false; // include all series in initial stack
|
||||
const { data: stacked } = stackSeries(data, noSeriesHidden);
|
||||
return stacked;
|
||||
}, [data, isStackedBarChart]);
|
||||
|
||||
const applyStackingToChart = useCallback((plot: uPlot): void => {
|
||||
const unstacked = unstackedDataRef.current;
|
||||
if (
|
||||
!unstacked ||
|
||||
!canApplyStacking(unstacked, plot, isUpdatingChartRef.current)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldExcludeSeries = (idx: number): boolean =>
|
||||
isSeriesHidden(plot, idx);
|
||||
const { data: stacked, bands } = stackSeries(unstacked, shouldExcludeSeries);
|
||||
|
||||
plot.delBand(null);
|
||||
bands.forEach((band: uPlot.Band) => plot.addBand(band));
|
||||
|
||||
isUpdatingChartRef.current = true;
|
||||
plot.setData(stacked);
|
||||
isUpdatingChartRef.current = false;
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!isStackedBarChart || !config) {
|
||||
return undefined;
|
||||
}
|
||||
return setupStackingHooks(config, applyStackingToChart, isUpdatingChartRef);
|
||||
}, [isStackedBarChart, config, applyStackingToChart]);
|
||||
|
||||
return chartData;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import ContextMenu from 'periscope/components/ContextMenu';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import BarChart from '../../charts/BarChart/BarChart';
|
||||
import ChartManager from '../../components/ChartManager/ChartManager';
|
||||
import { usePanelContextMenu } from '../../hooks/usePanelContextMenu';
|
||||
import { prepareBarPanelConfig, prepareBarPanelData } from './utils';
|
||||
|
||||
import '../Panel.styles.scss';
|
||||
|
||||
function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const {
|
||||
panelMode,
|
||||
queryResponse,
|
||||
widget,
|
||||
onDragSelect,
|
||||
isFullViewMode,
|
||||
onToggleModelHandler,
|
||||
} = props;
|
||||
const uPlotRef = useRef<uPlot | null>(null);
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
useEffect(() => {
|
||||
if (toScrollWidgetId === widget.id) {
|
||||
graphRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
graphRef.current?.focus();
|
||||
setToScrollWidgetId('');
|
||||
}
|
||||
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [queryResponse]);
|
||||
|
||||
const {
|
||||
coordinates,
|
||||
popoverPosition,
|
||||
onClose,
|
||||
menuItemsConfig,
|
||||
clickHandlerWithContextMenu,
|
||||
} = usePanelContextMenu({
|
||||
widget,
|
||||
queryResponse,
|
||||
});
|
||||
|
||||
const config = useMemo(() => {
|
||||
return prepareBarPanelConfig({
|
||||
widget,
|
||||
isDarkMode,
|
||||
currentQuery: widget.query,
|
||||
onClick: clickHandlerWithContextMenu,
|
||||
onDragSelect,
|
||||
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
|
||||
timezone,
|
||||
panelMode,
|
||||
minTimeScale: minTimeScale,
|
||||
maxTimeScale: maxTimeScale,
|
||||
});
|
||||
}, [
|
||||
widget,
|
||||
isDarkMode,
|
||||
queryResponse?.data?.payload,
|
||||
clickHandlerWithContextMenu,
|
||||
onDragSelect,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
timezone,
|
||||
panelMode,
|
||||
]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!queryResponse?.data?.payload) {
|
||||
return [];
|
||||
}
|
||||
return prepareBarPanelData(queryResponse?.data?.payload);
|
||||
}, [queryResponse?.data?.payload]);
|
||||
|
||||
const layoutChildren = useMemo(() => {
|
||||
if (!isFullViewMode) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ChartManager
|
||||
config={config}
|
||||
alignedData={chartData}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
onCancel={onToggleModelHandler}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
isFullViewMode,
|
||||
config,
|
||||
chartData,
|
||||
widget.yAxisUnit,
|
||||
onToggleModelHandler,
|
||||
]);
|
||||
|
||||
const onPlotDestroy = useCallback(() => {
|
||||
uPlotRef.current = null;
|
||||
}, []);
|
||||
|
||||
const onPlotRef = useCallback((plot: uPlot | null): void => {
|
||||
uPlotRef.current = plot;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="panel-container" ref={graphRef}>
|
||||
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
|
||||
<BarChart
|
||||
config={config}
|
||||
legendConfig={{
|
||||
position: widget?.legendPosition ?? LegendPosition.BOTTOM,
|
||||
}}
|
||||
plotRef={onPlotRef}
|
||||
onDestroy={onPlotDestroy}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
timezone={timezone.value}
|
||||
data={chartData as uPlot.AlignedData}
|
||||
width={containerDimensions.width}
|
||||
height={containerDimensions.height}
|
||||
layoutChildren={layoutChildren}
|
||||
isStackedBarChart={widget.stackedBarChart ?? false}
|
||||
>
|
||||
<ContextMenu
|
||||
coordinates={coordinates}
|
||||
popoverPosition={popoverPosition}
|
||||
title={menuItemsConfig.header as string}
|
||||
items={menuItemsConfig.items}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</BarChart>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BarPanel;
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getInitialStackedBands } from 'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import {
|
||||
DrawStyle,
|
||||
LineInterpolation,
|
||||
LineStyle,
|
||||
VisibilityMode,
|
||||
} from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import { AlignedData } from 'uplot';
|
||||
|
||||
import { PanelMode } from '../types';
|
||||
import { fillMissingXAxisTimestamps, getXAxisTimestamps } from '../utils';
|
||||
import { buildBaseConfig } from '../utils/baseConfigBuilder';
|
||||
|
||||
export function prepareBarPanelData(
|
||||
apiResponse: MetricRangePayloadProps,
|
||||
): AlignedData {
|
||||
const seriesList = apiResponse?.data?.result || [];
|
||||
const timestampArr = getXAxisTimestamps(seriesList);
|
||||
const yAxisValuesArr = fillMissingXAxisTimestamps(timestampArr, seriesList);
|
||||
return [timestampArr, ...yAxisValuesArr];
|
||||
}
|
||||
|
||||
export function prepareBarPanelConfig({
|
||||
widget,
|
||||
isDarkMode,
|
||||
currentQuery,
|
||||
onClick,
|
||||
onDragSelect,
|
||||
apiResponse,
|
||||
timezone,
|
||||
panelMode,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
}: {
|
||||
widget: Widgets;
|
||||
isDarkMode: boolean;
|
||||
currentQuery: Query;
|
||||
onClick: OnClickPluginOpts['onClick'];
|
||||
onDragSelect: (startTime: number, endTime: number) => void;
|
||||
apiResponse: MetricRangePayloadProps;
|
||||
timezone: Timezone;
|
||||
panelMode: PanelMode;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
}): UPlotConfigBuilder {
|
||||
const builder = buildBaseConfig({
|
||||
widget,
|
||||
isDarkMode,
|
||||
onClick,
|
||||
onDragSelect,
|
||||
apiResponse,
|
||||
timezone,
|
||||
panelMode,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
});
|
||||
|
||||
builder.setCursor({
|
||||
focus: {
|
||||
prox: 1e3,
|
||||
},
|
||||
});
|
||||
|
||||
if (widget.stackedBarChart) {
|
||||
const seriesCount = (apiResponse?.data?.result?.length ?? 0) + 1; // +1 for 1-based uPlot series indices
|
||||
builder.setBands(getInitialStackedBands(seriesCount));
|
||||
}
|
||||
|
||||
const seriesList: QueryData[] = apiResponse?.data?.result || [];
|
||||
seriesList.forEach((series) => {
|
||||
const baseLabelName = getLabelName(
|
||||
series.metric,
|
||||
series.queryName || '', // query
|
||||
series.legend || '',
|
||||
);
|
||||
|
||||
const label = currentQuery
|
||||
? getLegend(series, currentQuery, baseLabelName)
|
||||
: baseLabelName;
|
||||
|
||||
builder.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: DrawStyle.Bar,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
label: label,
|
||||
colorMapping: widget.customLegendColors ?? {},
|
||||
spanGaps: false,
|
||||
lineStyle: LineStyle.Solid,
|
||||
lineInterpolation: LineInterpolation.Spline,
|
||||
showPoints: VisibilityMode.Never,
|
||||
pointSize: 5,
|
||||
isDarkMode,
|
||||
});
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { BarTooltipProps, TooltipContentItem } from '../types';
|
||||
import Tooltip from './Tooltip';
|
||||
import { buildTooltipContent } from './utils';
|
||||
|
||||
export default function BarChartTooltip(props: BarTooltipProps): JSX.Element {
|
||||
const content = useMemo(
|
||||
(): TooltipContentItem[] =>
|
||||
buildTooltipContent({
|
||||
data: props.uPlotInstance.data,
|
||||
series: props.uPlotInstance.series,
|
||||
dataIndexes: props.dataIndexes,
|
||||
activeSeriesIndex: props.seriesIndex,
|
||||
uPlotInstance: props.uPlotInstance,
|
||||
yAxisUnit: props.yAxisUnit ?? '',
|
||||
decimalPrecision: props.decimalPrecision,
|
||||
isStackedBarChart: props.isStackedBarChart,
|
||||
}),
|
||||
[
|
||||
props.uPlotInstance,
|
||||
props.seriesIndex,
|
||||
props.dataIndexes,
|
||||
props.yAxisUnit,
|
||||
props.decimalPrecision,
|
||||
props.isStackedBarChart,
|
||||
],
|
||||
);
|
||||
|
||||
return <Tooltip {...props} content={content} />;
|
||||
}
|
||||
@@ -25,16 +25,28 @@ export function getTooltipBaseValue({
|
||||
index,
|
||||
dataIndex,
|
||||
isStackedBarChart,
|
||||
series,
|
||||
}: {
|
||||
data: AlignedData;
|
||||
index: number;
|
||||
dataIndex: number;
|
||||
isStackedBarChart?: boolean;
|
||||
series?: Series[];
|
||||
}): number | null {
|
||||
let baseValue = data[index][dataIndex] ?? null;
|
||||
if (isStackedBarChart && index + 1 < data.length && baseValue !== null) {
|
||||
const nextValue = data[index + 1][dataIndex] ?? null;
|
||||
if (nextValue !== null) {
|
||||
// Top-down stacking (first series at top): raw = stacked[i] - stacked[nextVisible].
|
||||
// When series are hidden, we must use the next *visible* series, not index+1,
|
||||
// since hidden series keep raw values and would produce negative/wrong results.
|
||||
if (isStackedBarChart && baseValue !== null && series) {
|
||||
let nextVisibleIdx = -1;
|
||||
for (let j = index + 1; j < series.length; j++) {
|
||||
if (series[j]?.show) {
|
||||
nextVisibleIdx = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (nextVisibleIdx >= 1) {
|
||||
const nextValue = data[nextVisibleIdx][dataIndex] ?? 0;
|
||||
baseValue = baseValue - nextValue;
|
||||
}
|
||||
}
|
||||
@@ -80,6 +92,7 @@ export function buildTooltipContent({
|
||||
index,
|
||||
dataIndex,
|
||||
isStackedBarChart,
|
||||
series,
|
||||
});
|
||||
|
||||
const isActive = index === activeSeriesIndex;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import uPlot, { Series } from 'uplot';
|
||||
|
||||
import {
|
||||
BarAlignment,
|
||||
ConfigBuilder,
|
||||
DrawStyle,
|
||||
LineInterpolation,
|
||||
@@ -43,18 +45,13 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
|
||||
}
|
||||
|
||||
private buildLineConfig({
|
||||
lineColor,
|
||||
lineWidth,
|
||||
lineStyle,
|
||||
lineCap,
|
||||
resolvedLineColor,
|
||||
}: {
|
||||
lineColor: string;
|
||||
lineWidth?: number;
|
||||
lineStyle?: LineStyle;
|
||||
lineCap?: Series.Cap;
|
||||
resolvedLineColor: string;
|
||||
}): Partial<Series> {
|
||||
const { lineWidth, lineStyle, lineCap } = this.props;
|
||||
const lineConfig: Partial<Series> = {
|
||||
stroke: lineColor,
|
||||
stroke: resolvedLineColor,
|
||||
width: lineWidth ?? 2,
|
||||
};
|
||||
|
||||
@@ -65,21 +62,26 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
|
||||
if (lineCap) {
|
||||
lineConfig.cap = lineCap;
|
||||
}
|
||||
|
||||
if (this.props.panelType === PANEL_TYPES.BAR) {
|
||||
lineConfig.fill = resolvedLineColor;
|
||||
}
|
||||
|
||||
return lineConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build path configuration
|
||||
*/
|
||||
private buildPathConfig({
|
||||
pathBuilder,
|
||||
drawStyle,
|
||||
lineInterpolation,
|
||||
}: {
|
||||
pathBuilder?: Series.PathBuilder | null;
|
||||
drawStyle: DrawStyle;
|
||||
lineInterpolation?: LineInterpolation;
|
||||
}): Partial<Series> {
|
||||
private buildPathConfig(): Partial<Series> {
|
||||
const {
|
||||
pathBuilder,
|
||||
drawStyle,
|
||||
lineInterpolation,
|
||||
barAlignment,
|
||||
barMaxWidth,
|
||||
barWidthFactor,
|
||||
} = this.props;
|
||||
if (pathBuilder) {
|
||||
return { paths: pathBuilder };
|
||||
}
|
||||
@@ -96,7 +98,13 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
|
||||
idx0: number,
|
||||
idx1: number,
|
||||
): Series.Paths | null => {
|
||||
const pathsBuilder = getPathBuilder(drawStyle, lineInterpolation);
|
||||
const pathsBuilder = getPathBuilder({
|
||||
drawStyle,
|
||||
lineInterpolation,
|
||||
barAlignment,
|
||||
barMaxWidth,
|
||||
barWidthFactor,
|
||||
});
|
||||
|
||||
return pathsBuilder(self, seriesIdx, idx0, idx1);
|
||||
},
|
||||
@@ -110,25 +118,21 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
|
||||
* Build points configuration
|
||||
*/
|
||||
private buildPointsConfig({
|
||||
lineColor,
|
||||
lineWidth,
|
||||
pointSize,
|
||||
pointsBuilder,
|
||||
pointsFilter,
|
||||
drawStyle,
|
||||
showPoints,
|
||||
resolvedLineColor,
|
||||
}: {
|
||||
lineColor: string;
|
||||
lineWidth?: number;
|
||||
pointSize?: number;
|
||||
pointsBuilder: Series.Points.Show | null;
|
||||
pointsFilter: Series.Points.Filter | null;
|
||||
drawStyle: DrawStyle;
|
||||
showPoints?: VisibilityMode;
|
||||
resolvedLineColor: string;
|
||||
}): Partial<Series.Points> {
|
||||
const {
|
||||
lineWidth,
|
||||
pointSize,
|
||||
pointsBuilder,
|
||||
pointsFilter,
|
||||
drawStyle,
|
||||
showPoints,
|
||||
} = this.props;
|
||||
const pointsConfig: Partial<Series.Points> = {
|
||||
stroke: lineColor,
|
||||
fill: lineColor,
|
||||
stroke: resolvedLineColor,
|
||||
fill: resolvedLineColor,
|
||||
size: !pointSize || pointSize < (lineWidth ?? 2) ? undefined : pointSize,
|
||||
filter: pointsFilter || undefined,
|
||||
};
|
||||
@@ -162,44 +166,16 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
|
||||
}
|
||||
|
||||
getConfig(): Series {
|
||||
const {
|
||||
drawStyle,
|
||||
pathBuilder,
|
||||
pointsBuilder,
|
||||
pointsFilter,
|
||||
lineInterpolation,
|
||||
lineWidth,
|
||||
lineStyle,
|
||||
lineCap,
|
||||
showPoints,
|
||||
pointSize,
|
||||
scaleKey,
|
||||
label,
|
||||
spanGaps,
|
||||
show = true,
|
||||
} = this.props;
|
||||
const { scaleKey, label, spanGaps, show = true } = this.props;
|
||||
|
||||
const lineColor = this.getLineColor();
|
||||
const resolvedLineColor = this.getLineColor();
|
||||
|
||||
const lineConfig = this.buildLineConfig({
|
||||
lineColor,
|
||||
lineWidth,
|
||||
lineStyle,
|
||||
lineCap,
|
||||
});
|
||||
const pathConfig = this.buildPathConfig({
|
||||
pathBuilder,
|
||||
drawStyle,
|
||||
lineInterpolation,
|
||||
resolvedLineColor,
|
||||
});
|
||||
const pathConfig = this.buildPathConfig();
|
||||
const pointsConfig = this.buildPointsConfig({
|
||||
lineColor,
|
||||
lineWidth,
|
||||
pointSize,
|
||||
pointsBuilder: pointsBuilder ?? null,
|
||||
pointsFilter: pointsFilter ?? null,
|
||||
drawStyle,
|
||||
showPoints,
|
||||
resolvedLineColor,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -227,15 +203,36 @@ interface PathBuilders {
|
||||
/**
|
||||
* Get path builder based on draw style and interpolation
|
||||
*/
|
||||
function getPathBuilder(
|
||||
style: DrawStyle,
|
||||
lineInterpolation?: LineInterpolation,
|
||||
): Series.PathBuilder {
|
||||
function getPathBuilder({
|
||||
drawStyle,
|
||||
lineInterpolation,
|
||||
barAlignment = BarAlignment.Center,
|
||||
barWidthFactor = 0.6,
|
||||
barMaxWidth = 200,
|
||||
}: {
|
||||
drawStyle: DrawStyle;
|
||||
lineInterpolation?: LineInterpolation;
|
||||
barAlignment?: BarAlignment;
|
||||
barMaxWidth?: number;
|
||||
barWidthFactor?: number;
|
||||
}): Series.PathBuilder {
|
||||
if (!builders) {
|
||||
throw new Error('Required uPlot path builders are not available');
|
||||
}
|
||||
|
||||
if (style === DrawStyle.Line) {
|
||||
if (drawStyle === DrawStyle.Bar) {
|
||||
const pathBuilders = uPlot.paths;
|
||||
const barsConfigKey = `bars|${barAlignment}|${barWidthFactor}|${barMaxWidth}`;
|
||||
if (!builders[barsConfigKey] && pathBuilders.bars) {
|
||||
builders[barsConfigKey] = pathBuilders.bars({
|
||||
size: [barWidthFactor, barMaxWidth],
|
||||
align: barAlignment,
|
||||
});
|
||||
}
|
||||
return builders[barsConfigKey];
|
||||
}
|
||||
|
||||
if (drawStyle === DrawStyle.Line) {
|
||||
if (lineInterpolation === LineInterpolation.StepBefore) {
|
||||
return builders.stepBefore;
|
||||
}
|
||||
|
||||
@@ -126,7 +126,45 @@ export enum VisibilityMode {
|
||||
Never = 'never',
|
||||
}
|
||||
|
||||
export interface SeriesProps {
|
||||
/**
|
||||
* Props for configuring lines
|
||||
*/
|
||||
export interface LineConfig {
|
||||
lineColor?: string;
|
||||
lineInterpolation?: LineInterpolation;
|
||||
lineStyle?: LineStyle;
|
||||
lineWidth?: number;
|
||||
lineCap?: Series.Cap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alignment of bars
|
||||
*/
|
||||
export enum BarAlignment {
|
||||
After = 1,
|
||||
Before = -1,
|
||||
Center = 0,
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for configuring bars
|
||||
*/
|
||||
export interface BarConfig {
|
||||
barAlignment?: BarAlignment;
|
||||
barMaxWidth?: number;
|
||||
barWidthFactor?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for configuring points
|
||||
*/
|
||||
export interface PointsConfig {
|
||||
pointColor?: string;
|
||||
pointSize?: number;
|
||||
showPoints?: VisibilityMode;
|
||||
}
|
||||
|
||||
export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
|
||||
scaleKey: string;
|
||||
label?: string;
|
||||
panelType: PANEL_TYPES;
|
||||
@@ -137,20 +175,7 @@ export interface SeriesProps {
|
||||
pointsBuilder?: Series.Points.Show;
|
||||
show?: boolean;
|
||||
spanGaps?: boolean;
|
||||
|
||||
isDarkMode?: boolean;
|
||||
|
||||
// Line config
|
||||
lineColor?: string;
|
||||
lineInterpolation?: LineInterpolation;
|
||||
lineStyle?: LineStyle;
|
||||
lineWidth?: number;
|
||||
lineCap?: Series.Cap;
|
||||
|
||||
// Points config
|
||||
pointColor?: string;
|
||||
pointSize?: number;
|
||||
showPoints?: VisibilityMode;
|
||||
}
|
||||
|
||||
export interface LegendItem {
|
||||
|
||||
Reference in New Issue
Block a user