資料轉換
自 Apache EChartsTM 5 開始,ECharts 支援了“資料轉換”(data transform
)。在 ECharts 中,資料轉換
是指,從使用者提供的原始資料,透過一些轉換方法,生成新的資料。這個功能使得使用者可以用宣告式的方式進行資料處理,並且 ECharts 內建了一些常用的“轉換方法”,使得這些任務可以“開箱即用”。(為保證上下文一致,本文統一使用“轉換”而非“變換”)。
資料轉換的抽象公式為:outData = f(inputData)
。其中轉換方法 f
可以是 filter
、sort
、regression
、boxplot
、cluster
、aggregate
(todo) 等。藉助這些轉換方法,使用者可以實現下面這些功能:
- 將資料分割成多個系列。
- 進行一些統計並可視化結果。
- 將一些視覺化演算法應用於資料並展示結果。
- 對資料進行排序。
- 移除或篩選某些空資料或特殊資料。
- ...
資料轉換入門
在 ECharts 中,資料轉換是基於 dataset 的概念實現的。可以在 dataset 例項中配置一個 dataset.transform,來表明此 dataset 是由這個 transform
生成的。例如:
var option = { dataset: [ { // This dataset is on `datasetIndex: 0`. source: [ ['Product', 'Sales', 'Price', 'Year'], ['Cake', 123, 32, 2011], ['Cereal', 231, 14, 2011], ['Tofu', 235, 5, 2011], ['Dumpling', 341, 25, 2011], ['Biscuit', 122, 29, 2011], ['Cake', 143, 30, 2012], ['Cereal', 201, 19, 2012], ['Tofu', 255, 7, 2012], ['Dumpling', 241, 27, 2012], ['Biscuit', 102, 34, 2012], ['Cake', 153, 28, 2013], ['Cereal', 181, 21, 2013], ['Tofu', 395, 4, 2013], ['Dumpling', 281, 31, 2013], ['Biscuit', 92, 39, 2013], ['Cake', 223, 29, 2014], ['Cereal', 211, 17, 2014], ['Tofu', 345, 3, 2014], ['Dumpling', 211, 35, 2014], ['Biscuit', 72, 24, 2014] ] // id: 'a' }, { // This dataset is on `datasetIndex: 1`. // A `transform` is configured to indicate that the // final data of this dataset is transformed via this // transform function. transform: { type: 'filter', config: { dimension: 'Year', value: 2011 } } // There can be optional properties `fromDatasetIndex` or `fromDatasetId` // to indicate that where is the input data of the transform from. // For example, `fromDatasetIndex: 0` specify the input data is from // the dataset on `datasetIndex: 0`, or `fromDatasetId: 'a'` specify the // input data is from the dataset having `id: 'a'`. // [DEFAULT_RULE] // If both `fromDatasetIndex` and `fromDatasetId` are omitted, // `fromDatasetIndex: 0` are used by default. }, { // This dataset is on `datasetIndex: 2`. // Similarly, if neither `fromDatasetIndex` nor `fromDatasetId` is // specified, `fromDatasetIndex: 0` is used by default transform: { // The "filter" transform filters and gets data items only match // the given condition in property `config`. type: 'filter', // Transforms has a property `config`. In this "filter" transform, // the `config` specify the condition that each result data item // should be satisfied. In this case, this transform get all of // the data items that the value on dimension "Year" equals to 2012. config: { dimension: 'Year', value: 2012 } } }, { // This dataset is on `datasetIndex: 3` transform: { type: 'filter', config: { dimension: 'Year', value: 2013 } } } ], series: [ { type: 'pie', radius: 50, center: ['25%', '50%'], // In this case, each "pie" series reference to a dataset that has // the result of its "filter" transform. datasetIndex: 1 }, { type: 'pie', radius: 50, center: ['50%', '50%'], datasetIndex: 2 }, { type: 'pie', radius: 50, center: ['75%', '50%'], datasetIndex: 3 } ] };
我們總結一下使用資料轉換的關鍵點:
- 在一個“空白”的 dataset 中,透過宣告
transform
、fromDatasetIndex
/fromDatasetId
,從而基於已有的資料來源生成新資料。 - 系列(series)引用這些 dataset 來展示結果。
進階用法
管道轉換
我們可以使用一種“管道(pipe)”的語法糖,例如:
option = {
dataset: [
{
source: [] // The original data
},
{
// Declare transforms in an array to pipe multiple transforms,
// which makes them execute one by one and take the output of
// the previous transform as the input of the next transform.
transform: [
{
type: 'filter',
config: { dimension: 'Product', value: 'Tofu' }
},
{
type: 'sort',
config: { dimension: 'Year', order: 'desc' }
}
]
}
],
series: {
type: 'pie',
// Display the result of the piped transform.
datasetIndex: 1
}
};
注意:理論上,任何型別的轉換都可以有多個輸入資料和多個輸出資料。但是,當轉換被用於“管道”中時,除了管道中的第一個轉換以外,其餘的轉換隻能接收一個輸入;除了管道中最後一個轉換以外,其餘的轉換隻能產生一個輸出。
輸出多個數據
大多數情況下,轉換方法只需要產生一個數據。但確實存在一個轉換方法需要產生多個數據的場景,其中每個資料可能被不同的系列使用。
例如,在內建的箱型圖(boxplot)轉換中,除了產生箱型圖資料外,還會產生異常值資料,這些異常值資料可以用在散點圖中。參見此示例。
我們使用屬性 dataset.fromTransformResult 來滿足這個需求。例如:
option = {
dataset: [
{
// Original source data.
source: []
},
{
transform: {
type: 'boxplot'
}
// After this "boxplot transform" two result data generated:
// result[0]: The boxplot data
// result[1]: The outlier data
// By default, when series or other dataset reference this dataset,
// only result[0] can be visited.
// If we need to visit result[1], we have to use another dataset
// as follows:
},
{
// This extra dataset references the dataset above, and retrieves
// the result[1] as its own data. Thus series or other dataset can
// reference this dataset to get the data from result[1].
fromDatasetIndex: 1,
fromTransformResult: 1
}
],
xAxis: {
type: 'category'
},
yAxis: {},
series: [
{
name: 'boxplot',
type: 'boxplot',
// Reference the data from result[0].
datasetIndex: 1
},
{
name: 'outlier',
type: 'scatter',
// Reference the data from result[1].
datasetIndex: 2
}
]
};
此外,dataset.fromTransformResult 和 dataset.transform 可以同時出現在一個 dataset 中,這意味著該轉換的輸入來自於 fromTransformResult
指定的上游轉換結果。例如:
{
fromDatasetIndex: 1,
fromTransformResult: 1,
transform: {
type: 'sort',
config: { dimension: 2, order: 'desc' }
}
}
在開發環境中除錯
當使用資料轉換時,我們可能會遇到最終圖表顯示不正確,但又不知道配置哪裡出了問題的麻煩。這種情況下,可以使用 transform.print
屬性來輔助除錯。(transform.print
僅在開發環境中可用)。
option = {
dataset: [
{
source: []
},
{
transform: {
type: 'filter',
config: {},
// The result of this transform will be printed
// in dev tool via `console.log`.
print: true
}
}
]
};
篩選轉換(Filter Transform)
轉換型別 “filter” 是一個內建的轉換,它能根據指定的條件篩選資料。基本配置如下:
option = { dataset: [ { source: [ ['Product', 'Sales', 'Price', 'Year'], ['Cake', 123, 32, 2011], ['Latte', 231, 14, 2011], ['Tofu', 235, 5, 2011], ['Milk Tee', 341, 25, 2011], ['Porridge', 122, 29, 2011], ['Cake', 143, 30, 2012], ['Latte', 201, 19, 2012], ['Tofu', 255, 7, 2012], ['Milk Tee', 241, 27, 2012], ['Porridge', 102, 34, 2012], ['Cake', 153, 28, 2013], ['Latte', 181, 21, 2013], ['Tofu', 395, 4, 2013], ['Milk Tee', 281, 31, 2013], ['Porridge', 92, 39, 2013], ['Cake', 223, 29, 2014], ['Latte', 211, 17, 2014], ['Tofu', 345, 3, 2014], ['Milk Tee', 211, 35, 2014], ['Porridge', 72, 24, 2014] ] }, { transform: { type: 'filter', config: { dimension: 'Year', '=': 2011 } // The config is the "condition" of this filter. // This transform traverse the source data and // and retrieve all the items that the "Year" // is `2011`. } } ], series: { type: 'pie', datasetIndex: 1 } };
這是另一個篩選轉換的例子:
關於維度(dimension)
config.dimension
可以是:
- 在 dataset 中宣告的維度名稱,例如
config: { dimension: 'Year', '=': 2011 }
。維度名稱宣告不是必需的。 - 維度索引(從 0 開始),例如
config: { dimension: 3, '=': 2011 }
。
關於關係運算符
關係運算符可以是:>
(gt
)、>=
(gte
)、<
(lt
)、<=
(lte
)、=
(eq
)、!=
(ne
, <>
)、reg
。(括號中的名稱是別名)。它們遵循通用的語義。除常見的數值比較外,還有一些額外功能:
- 一個 {} 項中可以出現多個運算子,如
{ dimension: 'Price', '>=': 20, '<': 30 }
,這表示邏輯“與”(Price >= 20 and Price < 30)。 - 資料值可以是“數值型字串”。數值型字串是指可以轉換為數字的字串,如 ' 123 '。轉換時會自動去除空白字元和換行符。
- 如果我們需要比較 JS
Date
例項或日期字串(如 '2012-05-12'),我們需要手動指定parser: 'time'
,例如config: { dimension: 3, lt: '2012-05-12', parser: 'time' }
。 - 支援純字串比較,但只能用於
=
、!=
。>
、>=
、<
、<=
不支援純字串比較(這四個運算子的“右值”不能是“字串”)。 - 運算子
reg
可用於進行正則表示式測試。例如使用{ dimension: 'Name', reg: /\s+Müller\s*$/ }
來選擇“Name”維度中所有姓氏為 Müller 的資料項。
關於邏輯關係
有時我們也需要表達邏輯關係 (and
/ or
/ not
)
option = {
dataset: [
{
source: [
// ...
]
},
{
transform: {
type: 'filter',
config: {
// Use operator "and".
// Similarly, we can also use "or", "not" in the same place.
// But "not" should be followed with a {...} rather than `[...]`.
and: [
{ dimension: 'Year', '=': 2011 },
{ dimension: 'Price', '>=': 20, '<': 30 }
]
}
// The condition is "Year" is 2011 and "Price" is greater
// or equal to 20 but less than 30.
}
}
],
series: {
type: 'pie',
datasetIndex: 1
}
};
and
/or
/not
可以巢狀使用,例如:
transform: {
type: 'filter',
config: {
or: [{
and: [{
dimension: 'Price', '>=': 10, '<': 20
}, {
dimension: 'Sales', '<': 100
}, {
not: { dimension: 'Product', '=': 'Tofu' }
}]
}, {
and: [{
dimension: 'Price', '>=': 10, '<': 20
}, {
dimension: 'Sales', '<': 100
}, {
not: { dimension: 'Product', '=': 'Cake' }
}]
}]
}
}
關於解析器(parser)
在進行值比較時可以指定一些“解析器”。目前只支援:
parser: 'time'
:在比較前將值解析為日期時間。解析規則與echarts.time.parse
相同,支援將 JSDate
例項、時間戳數字(毫秒)和時間字串(如'2012-05-12 03:11:22'
)解析為時間戳數字,而其他值將被解析為NaN
。parser: 'trim'
:在比較前對字串進行修剪。對於非字串,返回原始值。parser: 'number'
:在比較前強制將值轉換為數字。如果無法轉換為有意義的數字,則轉換為NaN
。在大多數情況下這是不必要的,因為預設情況下,如果可能,值在比較前會自動轉換為數字。但預設轉換是嚴格的,而此解析器提供了寬鬆的策略。如果遇到帶有單位字尾的數字字串(如'33%'
、12px
),我們應該使用parser: 'number'
在比較前將它們轉換為數字。
這是一個展示 parser: 'time'
的例子:
option = {
dataset: [
{
source: [
['Product', 'Sales', 'Price', 'Date'],
['Milk Tee', 311, 21, '2012-05-12'],
['Cake', 135, 28, '2012-05-22'],
['Latte', 262, 36, '2012-06-02'],
['Milk Tee', 359, 21, '2012-06-22'],
['Cake', 121, 28, '2012-07-02'],
['Latte', 271, 36, '2012-06-22']
// ...
]
},
{
transform: {
type: 'filter',
config: {
dimension: 'Date',
'>=': '2012-05',
'<': '2012-06',
parser: 'time'
}
}
}
]
};
正式定義
最後,我們在此給出篩選轉換配置的正式定義:
type FilterTransform = {
type: 'filter';
config: ConditionalExpressionOption;
};
type ConditionalExpressionOption =
| true
| false
| RelationalExpressionOption
| LogicalExpressionOption;
type RelationalExpressionOption = {
dimension: DimensionName | DimensionIndex;
parser?: 'time' | 'trim' | 'number';
lt?: DataValue; // less than
lte?: DataValue; // less than or equal
gt?: DataValue; // greater than
gte?: DataValue; // greater than or equal
eq?: DataValue; // equal
ne?: DataValue; // not equal
'<'?: DataValue; // lt
'<='?: DataValue; // lte
'>'?: DataValue; // gt
'>='?: DataValue; // gte
'='?: DataValue; // eq
'!='?: DataValue; // ne
'<>'?: DataValue; // ne (SQL style)
reg?: RegExp | string; // RegExp
};
type LogicalExpressionOption = {
and?: ConditionalExpressionOption[];
or?: ConditionalExpressionOption[];
not?: ConditionalExpressionOption;
};
type DataValue = string | number | Date;
type DimensionName = string;
type DimensionIndex = number;
請注意,當使用按需引入時,如果需要使用此內建轉換,除了
Dataset
元件外,還必須引入Transform
元件。
import {
DatasetComponent,
TransformComponent
} from 'echarts/components';
echarts.use([
DatasetComponent,
TransformComponent
]);
排序轉換(Sort Transform)
另一個內建的轉換是“sort”。
option = {
dataset: [
{
dimensions: ['name', 'age', 'profession', 'score', 'date'],
source: [
[' Hannah Krause ', 41, 'Engineer', 314, '2011-02-12'],
['Zhao Qian ', 20, 'Teacher', 351, '2011-03-01'],
[' Jasmin Krause ', 52, 'Musician', 287, '2011-02-14'],
['Li Lei', 37, 'Teacher', 219, '2011-02-18'],
[' Karle Neumann ', 25, 'Engineer', 253, '2011-04-02'],
[' Adrian Groß', 19, 'Teacher', null, '2011-01-16'],
['Mia Neumann', 71, 'Engineer', 165, '2011-03-19'],
[' Böhm Fuchs', 36, 'Musician', 318, '2011-02-24'],
['Han Meimei ', 67, 'Engineer', 366, '2011-03-12']
]
},
{
transform: {
type: 'sort',
// Sort by score.
config: { dimension: 'score', order: 'asc' }
}
}
],
series: {
type: 'bar',
datasetIndex: 1
}
// ...
};
關於“排序轉換”的一些額外功能:
- 支援按多個維度排序。見下面的示例。
- 排序規則:
- 預設情況下,“數值型”(即數字和數值型字串,如
' 123 '
)能夠按數值順序排序。 - 否則,“非數值型字串”之間也能夠排序。這在對具有相同標籤的資料項進行分組時可能會有幫助,尤其是在多個維度參與排序時(見下例)。
- 當“數值型”與“非數值型字串”比較時,或它們中任何一個與其他型別的值比較時,它們是不可比較的。因此,我們稱後者為“不可比較的”,並根據
incomparable: 'min' | 'max'
屬性將其視作“最小值”或“最大值”。此功能通常有助於決定是將空值(如null
、undefined
、NaN
、''
、'-'
)或其他非法值放在開頭還是末尾。
- 預設情況下,“數值型”(即數字和數值型字串,如
- 可以使用
parser: 'time' | 'trim' | 'number'
,與“篩選轉換”相同。- 如果打算對時間值(JS
Date
例項或時間字串,如'2012-03-12 11:13:54'
)進行排序,應指定parser: 'time'
。例如config: { dimension: 'date', order: 'desc', parser: 'time' }
- 如果打算對帶有單位字尾的值(如
'33%'
、'16px'
)進行排序,需要使用parser: 'number'
。
- 如果打算對時間值(JS
看一個多重排序的例子:
option = {
dataset: [
{
dimensions: ['name', 'age', 'profession', 'score', 'date'],
source: [
[' Hannah Krause ', 41, 'Engineer', 314, '2011-02-12'],
['Zhao Qian ', 20, 'Teacher', 351, '2011-03-01'],
[' Jasmin Krause ', 52, 'Musician', 287, '2011-02-14'],
['Li Lei', 37, 'Teacher', 219, '2011-02-18'],
[' Karle Neumann ', 25, 'Engineer', 253, '2011-04-02'],
[' Adrian Groß', 19, 'Teacher', null, '2011-01-16'],
['Mia Neumann', 71, 'Engineer', 165, '2011-03-19'],
[' Böhm Fuchs', 36, 'Musician', 318, '2011-02-24'],
['Han Meimei ', 67, 'Engineer', 366, '2011-03-12']
]
},
{
transform: {
type: 'sort',
config: [
// Sort by the two dimensions.
{ dimension: 'profession', order: 'desc' },
{ dimension: 'score', order: 'desc' }
]
}
}
],
series: {
type: 'bar',
datasetIndex: 1
}
// ...
};
最後,我們在此給出排序轉換配置的正式定義:
type SortTransform = {
type: 'sort';
config: OrderExpression | OrderExpression[];
};
type OrderExpression = {
dimension: DimensionName | DimensionIndex;
order: 'asc' | 'desc';
incomparable?: 'min' | 'max';
parser?: 'time' | 'trim' | 'number';
};
type DimensionName = string;
type DimensionIndex = number;
請注意,當使用按需引入時,如果需要使用此內建轉換,除了
Dataset
元件外,還必須引入Transform
元件。
import {
DatasetComponent,
TransformComponent
} from 'echarts/components';
echarts.use([
DatasetComponent,
TransformComponent
]);
使用外部轉換
除了內建的轉換(如 'filter'、'sort'),我們還可以使用外部轉換來提供更強大的功能。這裡我們使用第三方庫 ecStat 作為一個例子:
這個案例展示瞭如何透過 ecStat 生成一條迴歸線:
// Register the external transform at first.
echarts.registerTransform(ecStatTransform(ecStat).regression);
option = {
dataset: [
{
source: rawData
},
{
transform: {
// Reference the registered external transform.
// Note that external transform has a namespace (like 'ecStat:xxx'
// has namespace 'ecStat').
// built-in transform (like 'filter', 'sort') does not have a namespace.
type: 'ecStat:regression',
config: {
// Parameters needed by the external transform.
method: 'exponential'
}
}
}
],
xAxis: { type: 'category' },
yAxis: {},
series: [
{
name: 'scatter',
type: 'scatter',
datasetIndex: 0
},
{
name: 'regression',
type: 'line',
symbol: 'none',
datasetIndex: 1
}
]
};
使用 echarts-stat 的示例