Column-to-style

The column-to-style feature allows binding data columns to individual item properties like style,label,emphasis,etc. See series.data for the full list.
There are two conditions to set this up:

  1. have a data column for each customized property
  2. set series$encode$data with a value and properties pointing to column names

Here is an example on how to use it. Some explanations are in the code.

comments <- "
value 'pos' is the coordinate of each item in singleAxis
'symbol' and 'symbolSize' are data properties and data columns with the same name are automatically bound to them, see https://echarts.apache.org/en/option.html#series-scatter.data.symbolSize
emphasis$label$fontSize is explicitly bound to column 'lftSize'
'country','tw','c2' data columns are also copied to each item's data, to use in tooltip,etc.
"
data <- data.frame(
  country= c('China','USA','India','Russia','Japan'),
  c2= c('cn','us','in','ru','jp'),
  tw= c(8990,4145,1532,1024,913)
) |> mutate(
  symbolSize= 2*sqrt(tw/pi) *1.5,   # area of circle * scale
  pos= cumsum(symbolSize),
  symbol= paste0('image://https://hatscripts.github.io/circle-flags/flags/',c2,'.svg'),
  lftSize= c(14,12,10,10,10)
)
ec.init( data,
  grid=list(show=T, backgroundColor=
    list(type="linear", x=0, y=0, x2=0, y2=1, 
         colorStops= list(
           list(offset=0, color="#EDDD53aa"), 
           list(offset=0.5, color='#57C785aa'), 
           list(offset=1, color="#2A7B9Baa"))
  )),
  title= list(text='⚡️ Electricity consumption in 2024, \nleading countries (in terawatt-hours)', 
      subtext='source Statista',
      sublink='https://www.statista.com/statistics/267081/electricity-consumption-in-selected-countries-worldwide/'),
  singleAxis= list(type='value' , 
      left='-2%', width='99%', splitLine= list(show=F),
      axisLine= list(show=F), axisLabel= list(show=F), axisTick= list(show=F) ), 
  series.param= list(type='scatter', coordinateSystem= 'singleAxis', 
      label= list(show=T, position='bottom', formatter= ec.clmn('%L@', 'tw'), fontWeight='bold'),
      encode= list(data= list(
        value= c('pos'),    # value is required, defines which columns correspond to the chart axes
        emphasis= list(label= list(color= 'purple', fontSize='lftSize'))) 
      )),
  tooltip= list(formatter= ec.clmn('%@<br> %L@ TWh', 'country','tw'))
)

ECharts v.6 was released in July 2025 introducing some new chart types.
Here are a few examples made with echarty.

Matrix

Correlation heatmap in table format

mtx <- cor(swiss)
cols <- colnames(mtx)
mtx[upper.tri(mtx)] <- NA
datam <- as.data.frame(mtx)
#datam <- tibble::rownames_to_column(datam, 'x')
datam <- datam |> mutate(x= rownames(datam)); rownames(datam) <- NULL
# Convert to long format
long_data_base <- reshape( datam, direction= "long",
  idvar = "x",
  varying = list(colnames(mtx)),
  v.names = "value",
  timevar = "y",
  times = cols # Custom labels for timevar
)
datam <- na.omit(long_data_base)
row.names(datam) <- NULL
vals <- lapply(cols, \(x) { list(value=x) })

ec.init(
  title= list(text= 'demo: new matrix chart from ECharts v.6.0'),
  matrix= list(x= list(data=vals), y= list(data=vals)),
  visualMap= list(type='continuous', min=-1,max=1, dimension=3,
                 calculable=TRUE, orient='horizontal', bottom=0, left='center'),
  series= list(list(type= 'heatmap', coordinateSystem= 'matrix',
                    data= ec.data(datam),
    label= list(show=TRUE, formatter= ec.clmn('%R2@', 3))
  ))
)

Matrix mini-bars

Matrix is a coordinate system. It could be used to position charts in flexible layouts. Rewritten in R from the original. See also a more advanced example

jscode <- "function makeRenderItem() {  
  return function (params, api) {
    xDim = params.encode.x[0];
    yDim = params.encode.y[0];
    valDim = xDim +1;
    const dataExtent = [0, 10000]; 
    const xval = api.value(xDim);
    const yval = api.value(yDim);
    const labelVal = api.value(valDim);
    const rect = api.layout([xval, yval]).rect;
    if (!rect) {
      return;
    }
    const height = rect.height * 0.2;
    const barY = rect.y + ((rect.height - height) / 4) * 3;
    const barX = rect.x + rect.width * 0.15;
    const widthMax = rect.width * 0.8;
    const width = linearMap(labelVal, dataExtent, [0, widthMax]);
      //console.log(rect, labelVal, width);
    return {
      type: 'group',
      children: [
        {
          type: 'rect',
          shape: { x: barX, y: barY, width, height },
          style: api.style({
            fill: '#0ca8df'
          })
        },
        {
          type: 'text',
          x: barX,
          y: rect.y + (rect.height / 4) * 1.5,
          style: {
            text: labelVal + '',
            fill: '#333',
            align: 'left',
            verticalAlign: 'middle'
          }
        }
      ]
    };
  };
}
function linearMap(val, domain, range) {
  const d0 = domain[0];
  const d1 = domain[1];
  const r0 = range[0];
  const r1 = range[1];
  const subDomain = d1 - d0;
  const subRange = r1 - r0;
  return subDomain === 0
    ? subRange === 0
      ? r0
      : (r0 + r1) / 2
    : val === d0
    ? r0
    : val === d1
    ? r1
    : ((val - d0) / subDomain) * subRange + r0;
}
"
ec.init(
  dataset= list(source= list(
    list("2021-02-01", "amount", 1212, "file", 2321, "Q", 1412), 
    list("2021-02-02", "amount", 7181, "file", 2114, "Q", 1402), 
    list("2021-02-03", "amount", 2763, "file", 4212, "Q", 8172), 
    list("2021-02-04", "amount", 6122, "file", 2942, "Q", 6121), 
    list("2021-02-05", "amount", 4221, "file", 3411, "Q", 1987) )), 
  matrix = list(x= list(levelSize= 50, itemStyle= list(color= "#f0f8ff"), label= list(fontWeight = "bold"))), 
  series = list(
    list(type= "custom", coordinateSystem= "matrix", encode= list(x= 2, y= 1), renderItem= JS("makeRenderItem()")),
    list(type= "custom", coordinateSystem= "matrix", encode= list(x= 4, y= 1), renderItem= JS("makeRenderItem()")),
    list(type= "custom", coordinateSystem= "matrix", encode= list(x= 6, y= 1), renderItem= JS("makeRenderItem()"))
  )
) |>
prependContent(HTML(paste("<script>",jscode,"</script>")))

Segmented Donut

ec.init(
  load= 'https://cdn.jsdelivr.net/gh/apache/echarts-custom-series@main/custom-series/segmentedDoughnut/dist/index.auto.js',    # custom chart source code
  ask= 'loadRemote',
  series.param= list(
    type= 'custom', renderItem= 'segmentedDoughnut',
    coordinateSystem= 'none',
    itemPayload= list(
      radius= list('50%','65%'), segmentCount= 8,
      label= list(show=T, formatter= '{c}/{b}', fontSize=35, color= '#555')
    ),
    data= list(5) )
)

Fisheye Lens

Local zoom-in functionality for large-data charts. Tested with line and scatter charts. Uses the new “broken axis” feature from v.6. Rewritten in R from the original example

#---- fisheye v.6 -----
# https://echarts.apache.org/examples/en/editor.html?c=line-fisheye-lens&edit=1&reset=1
jscode <- "function initAxisBreakInteraction() {
  GRID_TOP = 120;
  GRID_BOTTOM = 80
  GRID_LEFT = 60;
  GRID_RIGHT = 60;
  function roundXYValue(val) {
    return +(+val).toFixed(0);
  }
  var _brushingEl = null;
  myChart = ec_chart(echwid);

  myChart.getZr().on('mousedown', function (params) {
    _brushingEl = new echarts.graphic.Rect({
      shape: { x: params.offsetX, y: params.offsetY },
      style: { stroke: 'none', fill: '#ccc' },
      ignore: true
    });
    myChart.getZr().add(_brushingEl);
  });
  myChart.getZr().on('mousemove', function (params) { //debugger;
    if (!_brushingEl) {
      return;
    }
    var initX = _brushingEl.shape.x;
    var initY = _brushingEl.shape.y;
    var currPoint = [params.offsetX, params.offsetY];
    _brushingEl.setShape('width', currPoint[0] - initX);
    _brushingEl.setShape('height', currPoint[1] - initY);
    _brushingEl.ignore = false;
  });
  document.addEventListener('mouseup', function (params) {
    if (!_brushingEl) {
      return;
    }
    var initX = _brushingEl.shape.x;
    var initY = _brushingEl.shape.y;
    var currPoint = [params.offsetX, params.offsetY];
    var xPixelSpan = Math.abs(currPoint[0] - initX);
    var yPixelSpan = Math.abs(currPoint[1] - initY);
    if (xPixelSpan > 2 && yPixelSpan > 2) {
      updateAxisBreak(
        myChart,
        [initX, initY],
        currPoint,
        xPixelSpan,
        yPixelSpan
      );
    }
    myChart.getZr().remove(_brushingEl);
    _brushingEl = null;
  });
  function updateAxisBreak(myChart, initXY, currPoint, xPixelSpan, yPixelSpan) {
    var dataXY0 = myChart.convertFromPixel({ gridIndex: 0 }, initXY);
    var dataXY1 = myChart.convertFromPixel({ gridIndex: 0 }, currPoint);
    function makeDataRange(v0, v1) {
      var dataRange = [roundXYValue(v0), roundXYValue(v1)];
      if (dataRange[0] > dataRange[1]) {
        dataRange.reverse();
      }
      return dataRange;
    }
    var xDataRange = makeDataRange(dataXY0[0], dataXY1[0]);
    var yDataRange = makeDataRange(dataXY0[1], dataXY1[1]);
    var xySpan = getXYAxisPixelSpan(myChart);
    var xGapPercentStr = (xPixelSpan / xySpan[0]) * 100 + '%';
    var yGapPercentStr = (yPixelSpan / xySpan[1]) * 100 + '%';
    function makeOption(xGapPercentStr, yGapPercentStr) {
      return {
        xAxis: {
          breaks: [
            {
              start: xDataRange[0],
              end: xDataRange[1],
              gap: xGapPercentStr
            }
          ]
        },
        yAxis: {
          breaks: [
            {
              start: yDataRange[0],
              end: yDataRange[1],
              gap: yGapPercentStr
            }
          ]
        }
      };
    }
    // This is to make a transition animation effect - firstly create axis break
    // on the brushed area, then collapse it to a small gap.
    myChart.setOption(makeOption(xGapPercentStr, yGapPercentStr));
    setTimeout(() => {
      var option = makeOption('80%', '80%');
      addClearButtonUpdateOption(option, true);
      myChart.setOption(option);
    }, 0);
  }
  function getXYAxisPixelSpan(myChart) {
    return [
      myChart.getWidth() - GRID_LEFT - GRID_RIGHT,
      myChart.getHeight() - GRID_BOTTOM - GRID_TOP
    ];
  }
} // End of initAxisBreakInteraction
setTimeout(initAxisBreakInteraction, 111);

function addClearButtonUpdateOption(option, show) {
  option.graphic = [
    {
      elements: [
        {
          type: 'rect',
          ignore: !show,
          name: 'clearAxisBreakBtn',
          top: 5,
          left: 5,
          shape: { r: 3, width: 70, height: 30 },
          style: { fill: '#eee', stroke: '#999', lineWidth: 1 },
          textContent: {
            type: 'text',
            style: {
              text: 'Reset',
              fontSize: 15,
              fontWeight: 'bold'
            }
          },
          textConfig: { position: 'inside' }
        }
      ]
    }
  ];
}"

# rewritten from JS to R by AI
generateSeriesData <- function() {
  seriesData <- list()
  DATA_COUNT <- 1000
  reset1 <- TRUE
  reset2 <- TRUE
  yVal <- 0
  
  for (idx in 0:(DATA_COUNT - 1)) {
    if (idx < DATA_COUNT / 4) {
      yVal <- makeRandom(yVal, c(100, 10000), 50000)
    } else if (idx < (2 * DATA_COUNT) / 3) {
      if (reset1) {
        yVal <- 110010
        reset1 <- FALSE
      }
      yVal <- makeRandom(yVal, c(100000, 105000), 50000)
    } else {
      if (reset2) {
        yVal <- 300100
        reset2 <- FALSE
      }
      yVal <- makeRandom(yVal, c(300000, 305000), 20000)
    }
    seriesData[[idx + 1]] <- c(idx, yVal)
  }
  
  return(seriesData)
}

makeRandom <- function(lastYVal, range, factor) {
  lastYVal <- lastYVal - range[1]
  delta <- (runif(1) - 0.5 * sin(lastYVal / factor)) * 
           (range[2] - range[1]) * 0.8
  return(roundXYValue(lastYVal + delta + range[1]))
}
roundXYValue <- function(value, digits=0) {
  return(round(value, digits))
}

library(htmlwidgets); library(htmltools)
GRID_TOP <- 120
GRID_BOTTOM <- 80
GRID_LEFT <- GRID_RIGHT <- 60
breakAreaStyle <- list(expandOnClick=F, zigzagZ= 200, zigzagAmplitude= 0, 
                       itemStyle= list(borderColor= '#777', opacity= 0)
)
ec.init(title= list(text= 'Fisheye Lens on Line Chart', 
      subtext= 'Brush to magnify the details', left='center',
      textStyle= list(fontSize=20),subtextStyle=list(color='#175ce5',fontSize=15,fontWeight='bold')),
  tooltip= list(trigger='axis'),
  legend= list(show=T), grid=list(top=GRID_TOP,bottom=GRID_BOTTOM,left=GRID_LEFT,right=GRID_RIGHT),
  xAxis= list(splitLine=list(show=FALSE), breakArea= breakAreaStyle),
  yAxis= list(axisTick=list(show=TRUE), breakArea= breakAreaStyle),
  series.param= list(type='line',name='Data A',symbol='circle',showSymbol=FALSE,symbolSize=5, data= generateSeriesData()),
  on= list(list(event='click', handler= JS("function (params) {
    if (params.name === 'clearAxisBreakBtn') {
      var option = {
        xAxis: { breaks: [] },
        yAxis: { breaks: [] }
      };
      addClearButtonUpdateOption(option, false);
      this.setOption(option);
    }
  }") ))
) |> 
appendContent(HTML(paste("<script>",jscode,"</script>")))

Chord chart

ec.init(
  tooltip = list(show=T), legend= list(show=T), 
  series.param= list(type = "chord", name = "test",
    startAngle = 90, endAngle = -270, clockwise = FALSE, 
    lineStyle = list(color = "target"), 
    data = list(
      list(name= "A"), list(name= "B"), list(name= "C"), list(name= "D")), 
    links = list(list(source = "A", target = "B", value = 40), 
                 list(source = "A", target = "C", value = 20, lineStyle= list(color = "source")), 
                 list(source = "A", target = "D", value = 10) ))
)

You can read about the new v.6 features here.