apache / echarts

Apache ECharts is a powerful, interactive charting and data visualization library for browser
https://echarts.apache.org
Apache License 2.0
60.14k stars 19.6k forks source link

[Feature] Line style for varied segments and connectNulls #17138

Open Ovilia opened 2 years ago

Ovilia commented 2 years ago

What problem does this feature solve?

Current API lacks the ability to control line style for varied segments. For example, if we want to make different styles for a line series, we have to mock it using many series, which is not an elegant way to do.

052a24de686d38783a9723dbb

What does the proposed API look like?

The new option should give the developer the control over line styles for different line segments. The API could be:

{
  data: [{
    value: 10,
    lineStyle: {...}, // controls the line style after this data
    areaStyle: {...}
  }]
}

or something similar to visualMap.pieces:

{
  linePieces: [{
    index: [0, 2, 4], // the index of line segments
    lineStyle: {...},
    areaStyle: {...}
  }]
}

or callback rules:

{
  linePieces: [{
    rule: function(params) { return params.dataIndex % 4 === 1; },
    lineStyle: {...},
    areaStyle: {...}
  }]
}

We should also provide a way to set the lineStyle for the lines from connectNulls. The API may be

{
  nullLineStyle: {...},
  nullAreaStyle: {...}
}

or

{
  connectNullStyle: {
    lineStyle: {...},
    areaStyle: {...}
  }
}

or

{
  linePieces: [{
    rule: function(params) { return params.isConnectNulls },
    lineStyle: {...},
    areaStyle: {...}
  }]
}

This issue is a task for OSPP so it will probably be fixed by a student from the program. Discussion about the design and your own requirement is always welcomed.

Ovilia commented 2 years ago

See also: #14239 #10233

pe-2 commented 2 years ago

What about api like this? This is my initial idea. @Ovilia 😊

   series: [
       {
          data: [150, 230, 224, 218, 135, 147, 260],
          type: 'line',
           // itemStyle and areastyle is also like this
          lineStyle:{ 
                type: "dotted",  // global style
                styles:{  // part style
                      type:'solid',
                      scope:[0,1],
                },
          }
       }
    ]
pe-2 commented 2 years ago

sorry ,styles should be an array 😅

Ovilia commented 2 years ago

@pe-2 Thanks for your suggestion. Although we do not have something like this for other options, theoretically speeking, this could also be a solution. I would suggest comparing each of these solutions and getting to a conclusion which one is the best. The thinking behind the choice of API design is the most important thing for this task.

P.S. We don't have any application on this task yet. Please make sure you submit the application on time.

Thanks!

pe-2 commented 2 years ago

You’re welcome, It’s my pleasure. I will submit the application after I complete it. 😁

zz5840 commented 2 years ago

We should also provide a way to set the lineStyle for the lines from connectNulls. The API may be

{
  nullLineStyle: {...},
  nullAreaStyle: {...}
}

or

{
  connectNullStyle: {
    lineStyle: {...},
    areaStyle: {...}
  }
}

If we use these two APIs for setting lineStyle for null item, how could we set lineStyle for each null item individually?

zz5840 commented 2 years ago

@Ovilia I'd like to add following properties to option in LineSeries.ts#L72 for implementing the second proposal:


export interface LinePieceData {
    index: number
    leftValue: number
    rightValue: number
    lineStyle: LineStyleOption
    isConnectNulls: boolean
}

export interface LinePiecesOption {
    indexes?: number[]
    rules: (piece: LinePieceData) => boolean
    lineStyle?: LineStyleOption
}

export interface LineSeriesOption {
    // ...
    linePieces?: LinePiecesOption[]
}

The new API can be call by providing the linePieces option as you proposed:

or something similar to visualMap.pieces:

{
  linePieces: [{
    index: [0, 2, 4], // the index of line segments
    lineStyle: {...},
    areaStyle: {...}
  }]
}

or callback rules:

{
  linePieces: [{
    rule: function(params) { return params.dataIndex % 4 === 1; },
    lineStyle: {...},
    areaStyle: {...}
  }]
}

When using property rules to indicate which pieces should this style applied to, the callback function will receive the following arguments as a piece object:

export interface LinePieceData {
    // index of the line piece
    index: number
    // left endpoint value of the line piece
    leftValue: number
    // right endpoint value of the line piece
    rightValue: number
    // line style will be applied to the line piece
    lineStyle: LineStyleOption
    // indicates if the piece is used to connect null data
    isConnectNulls: boolean
}

What do you think of this implement?

Ovilia commented 2 years ago

@zz5840 Thanks for the update. I want to clearify that if linePieces.index is the index of the data? I'm not sure what does index: [0, 2, 4] mean? Does this mean the lines between (data[0], data[1]), (data[2], data[3]), (data[3], data[4]) appy this style?

We should better use names like startValue and endValue rather than leftValue and rightValue because if the axis is inversed, the order of data is not the same as in the chart.

areaStyle should also be provided in the API.

zz5840 commented 2 years ago

@Ovilia Thanks for reply.

I'd rather make linePieces.index be the index of the line segment, which is showed in the image below. The red number is line index and the blue is data index.

msedge_9d7gVn5wp0

Line index will be different from data index only if a null data exist. So if we use line index, all index will be valid, and things like index pointing to a null data will not happen.

But there still exist disadvantage in line index. For example, it's hard to control the style of data segment (eg. to set the style for all data whose x-axis data is small than a certain value) when using dynamic data and number of nulls is unknown.

So which index should we provide, or should we provide both?

Ovilia commented 2 years ago

If the index means the ordinal number of the lines, then you must be careful because when the data is dense enough with line series, the data item (empty circles) is not always visible. Do you wish to have

image

or

image

?

Also, I feel it strange to exclude null data when making this index. It would mean the nth non-null line. I'm not sure if this is helpful for developers. Can you think of an example when this way is more convenient?

it's hard to control the style of data segment (eg. to set the style for all data whose x-axis data is small than a certain value) when using dynamic data and number of nulls is unknown.

This is a common situation so I think we shouldn't exclude null data in the index unless we have some solid reasons.

zz5840 commented 2 years ago

Also, I feel it strange to exclude null data when making this index. It would mean the nth non-null line. I'm not sure if this is helpful for developers. Can you think of an example when this way is more convenient?

I didn't mean to exclude null data, I mean this is what will happen if we use the index of the left endpoint as the index of line segment. But as you say, it's very common to set the style for all data whose x-axis data is small than a certain value, so we must accept a inconsecutive index if we use choose this solution.

Also, if we choose this solution, only the following image will show the correct index.

Ovilia commented 2 years ago

@zz5840 Thanks for the clarification. Just a few updates on the API design:

export interface LineSeriesOption {
    lineSegments?: LineSegmentsOption[],
    nullItemStyle?: {...}, // item style for null data
    nullLineStyle?: {...},
    nullAreaStyle?: {...},
    // ...
}

export interface LineSegmentsOption {
    dataIndexRange: number[][]
    includeEndItem: boolean = true
    itemStyle?: ItemStyleOption
    lineStyle?: LineStyleOption
    areaStyle?: AreaStyleOption
}
  1. I don't think we should provide isConnectNulls in LinePieceData (or at least you can make an PR without this first and later add this feature when you have time). For now, isConnectNulls is a series-level option and it doesn't seem very necessary to provide a segment-level option to me.
  2. Please note that the type of data values of line series is not number only. It could be numbers like 123, or strings like '123' or arrays like ['2020-10-10 13:00:00', 123]. Use LineDataValue if you are referencing to input data types.
  3. dataIndex is a better name than index because the latter one can also mean series index or others.
  4. dataIndexRange is an array containing an array of length two. It works like Array.slice, where the first number means the starting index and the second number means the length. So [[2, 1], [7, 2]] means data with index 2, 7, and 8.
  5. For itemStyle, if includeEndIndex is set to be true, the data at the end of the segment will be applied the itemStyle. For example, if dataIndexRange is [[2, 1], [7, 2]] and includeEndIndex is false, then data item 2, 7, and 8 will apply the itemStyle. If includeEndIndex is true, then data item 2, 3, 7, 8 and 9 will apply the itemStyle.
  6. I removed the callback form in the API because it seems to complicate the situation. Developers have such situations can loop through the data and generate an array of lineSegments to implement the requirement. So I think we can go without it and see if it's necessary in the future.
  7. I added nullItemStyle, nullLineStyle, and nullAreaStyle in the API. When data contains null, we should automatically create a segment from the null data and use corresponding styles.
gwak commented 2 years ago

Maybe how PlottableJs handles property updates can be of help for you to decide which API is best. For each property that you can set in PlottableJs, you have the choice of either give the value statically or give an accessor function:

https://github.com/palantir/plottable/blob/develop/src/core/interfaces.ts#L13-L19

/**
 * Accesses a specific datum property. Users supply Accessors to their
 * plots' .x, .y, .attr, etc. functions.
 */
export interface IAccessor<T> {
    (datum: any, index: number, dataset: Dataset): T; // Inputs should be replaced by charts specific dataset data structures
}

Then when creating a Plot in Plottable:

const stackedBarPlot = new Plottable.Plots.StackedBar()
      .datasets(datasets)
      .attr('fill', (d: any, I: number, dataset: Dataset) => dataset.metadata().color);

How it would look like in echarts:

export interface LineSeriesOption .... {
  lineStyle: LineStyleOption | IAccessor<LineStyleOption>;
...
}

Then in user-land:

const baseLineStyle = {...};
...

lineStyle: (datum: any, index: number, dataset: Dataset) => {
  return {... baseLineStyle, dashOffset: index > 10 ? 0 : 4 };
}
...

That way a user of the library can very easily create its own logic, that even reusable because it's a simple function.

ArslanGhaffar21 commented 1 year ago

What about api like this? This is my initial idea. @Ovilia 😊

   series: [
       {
          data: [150, 230, 224, 218, 135, 147, 260],
          type: 'line',
           // itemStyle and areastyle is also like this
          lineStyle:{ 
                type: "dotted",  // global style
                styles:{  // part style
                      type:'solid',
                      scope:[0,1],
                },
          }
       }
    ]

there is no any styles in lineStyle , and did you submit application ?

DoubleCorner commented 1 year ago

Will this feature be implemented in the next version?

helgasoft commented 1 year ago

A workaround with renderItem - Demo Code image

xiziliang commented 1 year ago

I really need lineStyle!!! { data: [{ lineStyle: {...}, }] }

sgtsaughter commented 1 year ago

Is there a way to simply make lineStyle.color and lineStyle.type accept a function. itemStyle.color already accepts a function and can give you the ability to change color based on dataPoint and index.

user-et commented 5 months ago

Image

您好,最终能实现到可以根据第三维信息,通过visualmap获取映射对应的颜色;然后两点之间根据首尾节点的颜色做渐变的效果吗? 或者是输入和数据等长的信息,描述每个节点的颜色,然后折线段根据2个节点的颜色做渐变呢?

helgasoft commented 5 months ago

@user-et, gradient possible thru custom line - Demo

image

user-et commented 5 months ago

thank you, i'll give it a try

agraddy commented 1 month ago

I agree with @sgtsaughter. If lineStyle.color and lineStyle.type could accept a function that would solve the issue.

When I was looking through the documentation, I saw that symbolSize accepts a function so I assumed lineStyle would too. When it didn't accept a function, I searched the issue list and ended up here.

PS Really impressive project! Thanks for all of the consistent updates and documentation, I know that takes a lot of work!