@record-evolution/widget-linechart 1.6.34 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,6 +21,10 @@ export type XAxisLabel = string;
21
21
  * Label text displayed beside the vertical axis. Describes the measured values (e.g., 'Temperature (°C)', 'Count', 'Revenue ($)').
22
22
  */
23
23
  export type YAxisLabel = string;
24
+ /**
25
+ * Label text displayed at the top of the secondary vertical axis on the right side of the chart. Describes the values measured on the right axis (e.g., 'Pressure (bar)', 'Humidity (%)'). The right y-axis only appears when at least one data series has its 'Y-Axis Assignment' (dataseries[].yAxis) set to 'right'; this label is ignored otherwise.
26
+ */
27
+ export type RightYAxisLabel = string;
24
28
  /**
25
29
  * When enabled, displays a legend identifying each data series by color and label. Essential for multi-series charts.
26
30
  */
@@ -38,9 +42,13 @@ export type ShowBoxFrame = boolean;
38
42
  */
39
43
  export type TimeseriesChart = boolean;
40
44
  /**
41
- * When multiple charts are drawn (via chartName grouping), this controls their arrangement. Enabled = stacked vertically, Disabled = arranged horizontally side by side.
45
+ * The direction in which bars grow and the chart is read. 'vertical' (the default) places the x-values (categories or timestamps) along the horizontal axis at the bottom and the measured y-values along the vertical axis, so bars grow upwards. 'horizontal' swaps the two axes: the x-values run down the left-hand vertical axis, first data point at the top, and the y-values grow to the right, producing a horizontal bar chart. Choose 'horizontal' for ranked or frequency charts, and whenever category names are long enough to be unreadable when squeezed under a vertical axis. This setting affects only the drawing direction, not the meaning of the other options: 'X-Axis Label' always names the x-value dimension and 'Y-Axis Label' always names the measured values, whichever side of the chart they end up on. Applies to all series in the chart, including line and scatter series.
42
46
  */
43
- export type VerticalLayout = boolean;
47
+ export type BarDirection = "vertical" | "horizontal";
48
+ /**
49
+ * Controls how SEPARATE charts are arranged relative to each other when the series are grouped into more than one chart via 'Chart Name' (dataseries[].advanced.chartName). Enabled = the charts are stacked one above the other, Disabled = the charts sit side by side. This is a page-layout option and has no effect on a single chart; to change the direction bars grow in, use 'Bar Direction' (axis.orientation) instead.
50
+ */
51
+ export type StackMultipleChartsVertically = boolean;
44
52
  /**
45
53
  * When enabled, displays the x-axis line, ticks, and labels. Disable to remove bottom axis and its padding for minimal designs.
46
54
  */
@@ -57,6 +65,10 @@ export type ShowYAxis = boolean;
57
65
  * When enabled, the y-axis range automatically adjusts to fit the data (min to max). When disabled, the y-axis always starts at 0, which is better for comparing absolute values.
58
66
  */
59
67
  export type YAxisScaling = boolean;
68
+ /**
69
+ * When enabled, the right y-axis range automatically adjusts to fit its assigned series (min to max). When disabled (the default), the right y-axis always starts at 0, which is better for comparing absolute values. This setting is independent of the left axis 'Y-Axis Scaling' and only has an effect when at least one data series is assigned to the right axis.
70
+ */
71
+ export type RightYAxisScaling = boolean;
60
72
  /**
61
73
  * Display name for this data series shown in legends and tooltips. Should clearly identify what data this series represents.
62
74
  */
@@ -65,6 +77,10 @@ export type Label = string;
65
77
  * The visual representation type: 'line' for continuous line charts (best for trends), 'bar' for vertical bar charts (best for comparisons), 'scatter' for individual data points (best for correlation analysis).
66
78
  */
67
79
  export type DrawingStyle = "bar" | "line" | "scatter";
80
+ /**
81
+ * Selects which vertical axis this series is plotted against: 'left' (the default) uses the primary y-axis on the left side of the chart, 'right' uses a secondary y-axis on the right side with its own independent scale. Assign 'right' to series whose value range differs strongly from the other series (e.g., temperature 0-40 °C on the left and pressure 900-1100 hPa on the right) so each group remains readable. The right axis appears automatically as soon as one series in a chart is set to 'right' and is labeled/scaled via the global 'Right Y-Axis Label' and 'Right Y-Axis Scaling' settings. Note: all series in one chart share a single inferred axis data type (numeric or categorical), so do not mix numeric and categorical y-values across the two axes.
82
+ */
83
+ export type YAxisAssignment = "left" | "right";
68
84
  /**
69
85
  * When enabled for line charts, fills the area between the line and the x-axis, creating an area chart. Useful for visualizing volume or cumulative values.
70
86
  */
@@ -81,6 +97,10 @@ export type LineDashStyle = "solid" | "dashed" | "dotted";
81
97
  * Shape of data point markers: 'circle', 'rect' (square), 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', or 'none' to hide markers. Different shapes help distinguish series.
82
98
  */
83
99
  export type SymbolStyle = "circle" | "rect" | "roundRect" | "triangle" | "diamond" | "pin" | "arrow" | "none";
100
+ /**
101
+ * When enabled, each data point's y-value is printed as text directly on the chart: above the top of every bar in a vertical bar chart, just past the end of the bar in a horizontal one, and above the marker for line and scatter series. Enable this whenever the reader is meant to read exact numbers off the chart rather than estimate them against the axis — the usual expectation for bar charts of counts, totals or frequencies, and the setting to use when someone asks for the values to be shown on or above the bars. Numbers are rounded to two decimal places; non-numeric values are printed unchanged. Leave it disabled for charts with many data points, where the labels would overlap each other.
102
+ */
103
+ export type ShowValueLabels = boolean;
84
104
  /**
85
105
  * Z-index controlling which series appears on top when overlapping. Lower numbers are drawn on top. Use to ensure important series aren't hidden behind others.
86
106
  */
@@ -123,6 +143,7 @@ export type Dataseries = {
123
143
  type?: DrawingStyle;
124
144
  backgroundColor?: FillColor;
125
145
  borderColor?: LineColor;
146
+ yAxis?: YAxisAssignment;
126
147
  styling?: Styling;
127
148
  advanced?: AdvancedSettings;
128
149
  data?: Data;
@@ -130,9 +151,9 @@ export type Dataseries = {
130
151
  }[];
131
152
 
132
153
  /**
133
- * A versatile chart widget for visualizing data as line charts, bar charts, or scatter plots. Use this widget for time-series data, trend analysis, comparisons, and general data visualization. Supports multiple data series with different chart types in the same view, automatic pivot-based series generation, zoom controls, and flexible styling options. The ECharts-based rendering provides smooth animations and interactive tooltips. Ideal for monitoring dashboards, historical data analysis, and any scenario requiring visual data representation over continuous or categorical dimensions.
154
+ * A versatile chart widget for visualizing data as line charts, bar charts, or scatter plots. Despite the package name, this is also THE bar chart widget: set dataseries[].type to 'bar' for vertical bars and axis.orientation to 'horizontal' for horizontal ones. Use this widget for time-series data, trend analysis, comparisons, frequency distributions, and general data visualization. Supports multiple data series with different chart types in the same view, automatic pivot-based series generation, zoom controls, and flexible styling options. The ECharts-based rendering provides smooth animations and interactive tooltips. Ideal for monitoring dashboards, historical data analysis, and any scenario requiring visual data representation over continuous or categorical dimensions.
134
155
  */
135
- export interface InputData {
156
+ export interface ChartConfiguration {
136
157
  title?: Title;
137
158
  subTitle?: Subtitle;
138
159
  axis?: Configuration;
@@ -145,15 +166,18 @@ export interface InputData {
145
166
  export interface Configuration {
146
167
  xAxisLabel?: XAxisLabel;
147
168
  yAxisLabel?: YAxisLabel;
169
+ yAxisLabelRight?: RightYAxisLabel;
148
170
  showLegend?: ShowLegend;
149
171
  showTitle?: ShowTitle;
150
172
  showBox?: ShowBoxFrame;
151
173
  timeseries?: TimeseriesChart;
152
- columnLayout?: VerticalLayout;
174
+ orientation?: BarDirection;
175
+ columnLayout?: StackMultipleChartsVertically;
153
176
  showXAxis?: ShowXAxis;
154
177
  xAxisZoom?: XAxisZoomTool;
155
178
  showYAxis?: ShowYAxis;
156
179
  yAxisScaling?: YAxisScaling;
180
+ yAxisScalingRight?: RightYAxisScaling;
157
181
  [k: string]: unknown;
158
182
  }
159
183
  /**
@@ -176,6 +200,7 @@ export interface Styling {
176
200
  borderWidth?: LineWidth;
177
201
  borderDash?: LineDashStyle;
178
202
  pointStyle?: SymbolStyle;
203
+ showValueLabels?: ShowValueLabels;
179
204
  [k: string]: unknown;
180
205
  }
181
206
  /**
@@ -1,6 +1,27 @@
1
1
  {
2
- "title": "inputData",
3
- "description": "A versatile chart widget for visualizing data as line charts, bar charts, or scatter plots. Use this widget for time-series data, trend analysis, comparisons, and general data visualization. Supports multiple data series with different chart types in the same view, automatic pivot-based series generation, zoom controls, and flexible styling options. The ECharts-based rendering provides smooth animations and interactive tooltips. Ideal for monitoring dashboards, historical data analysis, and any scenario requiring visual data representation over continuous or categorical dimensions.",
2
+ "title": "Chart Configuration",
3
+ "description": "A versatile chart widget for visualizing data as line charts, bar charts, or scatter plots. Despite the package name, this is also THE bar chart widget: set dataseries[].type to 'bar' for vertical bars and axis.orientation to 'horizontal' for horizontal ones. Use this widget for time-series data, trend analysis, comparisons, frequency distributions, and general data visualization. Supports multiple data series with different chart types in the same view, automatic pivot-based series generation, zoom controls, and flexible styling options. The ECharts-based rendering provides smooth animations and interactive tooltips. Ideal for monitoring dashboards, historical data analysis, and any scenario requiring visual data representation over continuous or categorical dimensions.",
4
+ "aiSelection": {
5
+ "dataShape": "One or more series of (x, y) pairs. x is a timestamp, a number, or a category label and defines the position along the x-value dimension; y is the measured quantity. All series in a chart share one x dimension. An optional third column bound to data[].pivot splits a single series into one line or bar per distinct value of that column, and an optional data[].r sizes the marker for a bubble chart. Needs at least two columns of data; a single scalar has no shape to plot.",
6
+ "useWhen": [
7
+ "A quantity is measured repeatedly over time and the trend, shape or direction of change is the point (axis.timeseries: true, dataseries[].type: 'line').",
8
+ "A quantity is compared across a handful of discrete categories — counts, totals, frequency distributions, per-machine or per-site comparisons (dataseries[].type: 'bar', axis.timeseries: false). This is the correct widget for any bar chart; there is no separate bar chart widget.",
9
+ "Categories are being ranked, or their names are long enough to be unreadable side by side under a vertical axis (dataseries[].type: 'bar', axis.orientation: 'horizontal').",
10
+ "Two numeric variables are being correlated as a cloud of individual points (dataseries[].type: 'scatter'), optionally with data[].r encoding a third variable as marker size.",
11
+ "Several quantities with different units or wildly different ranges belong on one chart (assign the second group dataseries[].yAxis: 'right').",
12
+ "The same measurement is broken down by an entity — one line per machine, per site, per program (bind the breakdown column to data[].pivot)."
13
+ ],
14
+ "notFor": [
15
+ "Parts of a whole, shares, or percentages that add up to 100% — use widget-doughnut.",
16
+ "A single current number, reading or KPI with no second dimension to plot — use widget-value, or widget-gauge when it should be read against a min/max range.",
17
+ "Records the user needs to read row by row, or data with more columns than an x and a y — use widget-table.",
18
+ "Intensity or density across two categorical dimensions at once, i.e. a matrix or calendar grid — use widget-heatmap.",
19
+ "How long an entity stayed in each discrete state over time (state timelines, machine mode history, up/down periods drawn as spans). A bar chart of state durations is the wrong shape — use widget-statehistory.",
20
+ "Anything positioned by geographic coordinates — use widget-mapbox.",
21
+ "A single binary or few-valued status shown as an indicator rather than plotted — use widget-signal, or widget-switch when the user must also be able to toggle it.",
22
+ "Free text, headings or explanatory prose — use widget-label, widget-textbox or widget-markdown."
23
+ ]
24
+ },
4
25
  "type": "object",
5
26
  "properties": {
6
27
  "title": {
@@ -35,6 +56,13 @@
35
56
  "dataDrivenDisabled": false,
36
57
  "order": 1
37
58
  },
59
+ "yAxisLabelRight": {
60
+ "title": "Right Y-Axis Label",
61
+ "description": "Label text displayed at the top of the secondary vertical axis on the right side of the chart. Describes the values measured on the right axis (e.g., 'Pressure (bar)', 'Humidity (%)'). The right y-axis only appears when at least one data series has its 'Y-Axis Assignment' (dataseries[].yAxis) set to 'right'; this label is ignored otherwise.",
62
+ "type": "string",
63
+ "dataDrivenDisabled": false,
64
+ "order": 1.05
65
+ },
38
66
  "showLegend": {
39
67
  "title": "Show Legend",
40
68
  "description": "When enabled, displays a legend identifying each data series by color and label. Essential for multi-series charts.",
@@ -66,9 +94,18 @@
66
94
  "dataDrivenDisabled": true,
67
95
  "order": 4
68
96
  },
97
+ "orientation": {
98
+ "title": "Bar Direction",
99
+ "description": "The direction in which bars grow and the chart is read. 'vertical' (the default) places the x-values (categories or timestamps) along the horizontal axis at the bottom and the measured y-values along the vertical axis, so bars grow upwards. 'horizontal' swaps the two axes: the x-values run down the left-hand vertical axis, first data point at the top, and the y-values grow to the right, producing a horizontal bar chart. Choose 'horizontal' for ranked or frequency charts, and whenever category names are long enough to be unreadable when squeezed under a vertical axis. This setting affects only the drawing direction, not the meaning of the other options: 'X-Axis Label' always names the x-value dimension and 'Y-Axis Label' always names the measured values, whichever side of the chart they end up on. Applies to all series in the chart, including line and scatter series.",
100
+ "type": "string",
101
+ "enum": ["vertical", "horizontal"],
102
+ "default": "vertical",
103
+ "dataDrivenDisabled": true,
104
+ "order": 4.5
105
+ },
69
106
  "columnLayout": {
70
- "title": "Vertical Layout",
71
- "description": "When multiple charts are drawn (via chartName grouping), this controls their arrangement. Enabled = stacked vertically, Disabled = arranged horizontally side by side.",
107
+ "title": "Stack Multiple Charts Vertically",
108
+ "description": "Controls how SEPARATE charts are arranged relative to each other when the series are grouped into more than one chart via 'Chart Name' (dataseries[].advanced.chartName). Enabled = the charts are stacked one above the other, Disabled = the charts sit side by side. This is a page-layout option and has no effect on a single chart; to change the direction bars grow in, use 'Bar Direction' (axis.orientation) instead.",
72
109
  "type": "boolean",
73
110
  "dataDrivenDisabled": true,
74
111
  "order": 5
@@ -104,6 +141,13 @@
104
141
  "type": "boolean",
105
142
  "dataDrivenDisabled": true,
106
143
  "order": 11
144
+ },
145
+ "yAxisScalingRight": {
146
+ "title": "Right Y-Axis Scaling",
147
+ "description": "When enabled, the right y-axis range automatically adjusts to fit its assigned series (min to max). When disabled (the default), the right y-axis always starts at 0, which is better for comparing absolute values. This setting is independent of the left axis 'Y-Axis Scaling' and only has an effect when at least one data series is assigned to the right axis.",
148
+ "type": "boolean",
149
+ "dataDrivenDisabled": true,
150
+ "order": 11.1
107
151
  }
108
152
  }
109
153
  },
@@ -146,6 +190,15 @@
146
190
  "color": true,
147
191
  "order": 4
148
192
  },
193
+ "yAxis": {
194
+ "title": "Y-Axis Assignment",
195
+ "description": "Selects which vertical axis this series is plotted against: 'left' (the default) uses the primary y-axis on the left side of the chart, 'right' uses a secondary y-axis on the right side with its own independent scale. Assign 'right' to series whose value range differs strongly from the other series (e.g., temperature 0-40 °C on the left and pressure 900-1100 hPa on the right) so each group remains readable. The right axis appears automatically as soon as one series in a chart is set to 'right' and is labeled/scaled via the global 'Right Y-Axis Label' and 'Right Y-Axis Scaling' settings. Note: all series in one chart share a single inferred axis data type (numeric or categorical), so do not mix numeric and categorical y-values across the two axes.",
196
+ "type": "string",
197
+ "enum": ["left", "right"],
198
+ "default": "left",
199
+ "dataDrivenDisabled": true,
200
+ "order": 4.5
201
+ },
149
202
  "styling": {
150
203
  "title": "Styling",
151
204
  "description": "Additional visual styling options for the series.",
@@ -195,6 +248,14 @@
195
248
  "none"
196
249
  ],
197
250
  "order": 5
251
+ },
252
+ "showValueLabels": {
253
+ "title": "Show Value Labels",
254
+ "description": "When enabled, each data point's y-value is printed as text directly on the chart: above the top of every bar in a vertical bar chart, just past the end of the bar in a horizontal one, and above the marker for line and scatter series. Enable this whenever the reader is meant to read exact numbers off the chart rather than estimate them against the axis — the usual expectation for bar charts of counts, totals or frequencies, and the setting to use when someone asks for the values to be shown on or above the bars. Numbers are rounded to two decimal places; non-numeric values are printed unchanged. Leave it disabled for charts with many data points, where the labels would overlap each other.",
255
+ "type": "boolean",
256
+ "default": false,
257
+ "dataDrivenDisabled": true,
258
+ "order": 6
198
259
  }
199
260
  }
200
261
  },
@@ -30,7 +30,7 @@ echarts.use([
30
30
  LegacyGridContainLabel
31
31
  ])
32
32
 
33
- import { InputData } from './definition-schema'
33
+ import { ChartConfiguration } from './definition-schema'
34
34
  import { EChartsOption, SeriesOption } from 'echarts'
35
35
  import { TitleOption } from 'echarts/types/dist/shared'
36
36
 
@@ -39,11 +39,23 @@ type Theme = {
39
39
  theme_object: any
40
40
  }
41
41
 
42
- type SeriesOptionX = SeriesOption & { minDate?: number; maxDate?: number; drawOrder: number }
42
+ type SeriesOptionX = SeriesOption & {
43
+ minDate?: number
44
+ maxDate?: number
45
+ drawOrder: number
46
+ xAxisIndex?: number
47
+ yAxisIndex?: number
48
+ // Which value axis (0 = primary, 1 = secondary) this series belongs to,
49
+ // independent of orientation. In a vertical chart the value axes are the
50
+ // y axes (left/right), in a horizontal one they are the x axes (bottom/top),
51
+ // so xAxisIndex/yAxisIndex alone cannot be read without knowing the
52
+ // orientation — this field can.
53
+ valueAxisIndex?: number
54
+ }
43
55
  @customElement('widget-linechart-versionplaceholder')
44
56
  export class WidgetLinechart extends LitElement {
45
57
  @property({ type: Object })
46
- inputData?: InputData
58
+ inputData?: ChartConfiguration
47
59
 
48
60
  @property({ type: Object })
49
61
  theme?: Theme
@@ -143,21 +155,45 @@ export class WidgetLinechart extends LitElement {
143
155
  fontSize: 14
144
156
  }
145
157
  },
146
- yAxis: {
147
- type: 'value',
148
- nameLocation: 'middle',
149
- name: 'Temperature (°C)',
150
- nameGap: 30,
151
- axisLabel: {
152
- fontSize: 14
153
- },
154
- axisLine: {
155
- lineStyle: {
156
- width: undefined
157
- }
158
+ // index 0 = left (primary) axis, index 1 = right (secondary) axis.
159
+ // Must stay function-free: the template is copied via structuredClone.
160
+ yAxis: [
161
+ {
162
+ type: 'value',
163
+ nameLocation: 'middle',
164
+ name: 'Temperature (°C)',
165
+ nameGap: 30,
166
+ position: 'left',
167
+ axisLabel: {
168
+ fontSize: 14
169
+ },
170
+ axisLine: {
171
+ lineStyle: {
172
+ width: undefined
173
+ }
174
+ },
175
+ scale: false
158
176
  },
159
- scale: false
160
- },
177
+ {
178
+ type: 'value',
179
+ position: 'right',
180
+ show: false,
181
+ name: '',
182
+ nameGap: 30,
183
+ axisLabel: {
184
+ fontSize: 14
185
+ },
186
+ axisLine: {
187
+ lineStyle: {
188
+ width: undefined
189
+ }
190
+ },
191
+ splitLine: {
192
+ show: false
193
+ },
194
+ scale: false
195
+ }
196
+ ],
161
197
  series: [
162
198
  {
163
199
  name: 'Highest',
@@ -241,9 +277,49 @@ export class WidgetLinechart extends LitElement {
241
277
  echarts.registerTheme(theme.theme_name, filteredTheme)
242
278
  }
243
279
 
280
+ /** True when bars grow to the right instead of upwards (axis.orientation). */
281
+ isHorizontal(): boolean {
282
+ return this.inputData?.axis?.orientation === 'horizontal'
283
+ }
284
+
285
+ /**
286
+ * Index of the measured y-value inside a data point's `value` tuple.
287
+ *
288
+ * A point is stored as `[x, y, r]` for a vertical chart. A horizontal chart
289
+ * swaps the first two entries to `[y, x, r]`, because ECharts always maps
290
+ * tuple index 0 to the x axis and index 1 to the y axis — and in a
291
+ * horizontal chart the measured value is what belongs on the x axis.
292
+ */
293
+ private valueIndex(): number {
294
+ return this.isHorizontal() ? 0 : 1
295
+ }
296
+
297
+ /** Index of the x-value (category/timestamp) inside a data point's `value` tuple. */
298
+ private categoryIndex(): number {
299
+ return this.isHorizontal() ? 1 : 0
300
+ }
301
+
302
+ /**
303
+ * Formatter for the on-chart value labels (styling.showValueLabels).
304
+ * Rounds to two decimals to match the numeric axis labels, and leaves
305
+ * categorical y-values untouched.
306
+ */
307
+ private valueLabelFormatter(valueIdx: number) {
308
+ return (params: any) => {
309
+ const raw = Array.isArray(params?.value) ? params.value[valueIdx] : params?.value
310
+ if (raw === undefined || raw === null || raw === '') return ''
311
+ const num = Number(raw)
312
+ return isNaN(num) ? String(raw) : String(Math.round(num * 100) / 100)
313
+ }
314
+ }
315
+
244
316
  transformData() {
245
317
  if (!this?.inputData?.dataseries?.length) return
246
318
 
319
+ const horizontal = this.isHorizontal()
320
+ const valueIdx = this.valueIndex()
321
+ const categoryIdx = this.categoryIndex()
322
+
247
323
  // reset all existing chart dataseries
248
324
  this.canvasList.forEach((chartM) => {
249
325
  chartM.series = []
@@ -280,22 +356,29 @@ export class WidgetLinechart extends LitElement {
280
356
  : derivedBgColors[i]
281
357
  : undefined
282
358
  const data = distincts.length === 1 ? ds.data : ds.data?.filter((d) => d.pivot === piv)
359
+ // The value tuple is [x, y, r] for a vertical chart and [y, x, r]
360
+ // for a horizontal one — ECharts always reads index 0 as the x
361
+ // axis, and in a horizontal chart the measured value lives there.
362
+ const toTuple = (x: any, y: any, r: any) => (horizontal ? [y, x, r] : [x, y, r])
283
363
  let data2 = this.inputData?.axis?.timeseries
284
- ? (data?.map((d) => ({ name: d.x, value: [new Date(d.x ?? '').getTime(), d.y, d.r] })) ??
285
- [])
286
- : (data?.map((d) => ({ name: d.x, value: [d.x, d.y, d.r] })) ?? [])
364
+ ? (data?.map((d) => ({
365
+ name: d.x,
366
+ value: toTuple(new Date(d.x ?? '').getTime(), d.y, d.r)
367
+ })) ?? [])
368
+ : (data?.map((d) => ({ name: d.x, value: toTuple(d.x, d.y, d.r) })) ?? [])
287
369
 
288
370
  let minDate: number = 0,
289
371
  maxDate: number = 0,
290
372
  extraData: (string | number | undefined)[][] = []
291
373
  if (this.xAxisType() === 'time' && data2) {
292
- const dates = data2.map((d: any) => d.value[0] as number)
374
+ const dates = data2.map((d: any) => d.value[categoryIdx] as number)
293
375
  minDate = Math.min(...dates)
294
376
  maxDate = Math.max(...dates)
295
377
  // extraData = (pds?.data as any[][])?.filter((d: any) => d[0] < minDate) ?? []
296
378
  // data2.unshift(...extraData) // add old data to the beginning of the new data
297
379
  // data2 = [...extraData, ...data2] // leave old data in for smooth shifting animation and delete after draw
298
380
  }
381
+ const valueAxisIndex = ds.yAxis === 'right' ? 1 : 0
299
382
  const pds: SeriesOptionX = {
300
383
  id: name,
301
384
  name: name,
@@ -317,8 +400,21 @@ export class WidgetLinechart extends LitElement {
317
400
  symbol: ds.styling?.pointStyle ?? 'circle',
318
401
  symbolSize: (d: any[]) => d[2] ?? 0,
319
402
  showSymbol: ds.styling?.pointStyle === 'none' ? false : true,
403
+ label: {
404
+ show: ds.styling?.showValueLabels ?? false,
405
+ // Bars grow upwards when vertical and rightwards when
406
+ // horizontal, so the label sits past the growing end.
407
+ position: horizontal ? 'right' : 'top',
408
+ fontSize: 12,
409
+ formatter: this.valueLabelFormatter(valueIdx)
410
+ },
320
411
  data: data2 ?? [],
321
- drawOrder: ds.advanced?.drawOrder ?? 0
412
+ drawOrder: ds.advanced?.drawOrder ?? 0,
413
+ valueAxisIndex: valueAxisIndex,
414
+ // The secondary value axis is the right y axis when vertical
415
+ // and the top x axis when horizontal.
416
+ xAxisIndex: horizontal ? valueAxisIndex : 0,
417
+ yAxisIndex: horizontal ? 0 : valueAxisIndex
322
418
  }
323
419
  let chartName = ds.advanced?.chartName ?? ''
324
420
  chartName = chartName.replace('#split#', prefix)
@@ -455,6 +551,7 @@ export class WidgetLinechart extends LitElement {
455
551
  const modifier = 1
456
552
  // Sort chartContainer children by drawOrder and label
457
553
  if (!this.chartContainer) return
554
+ const horizontal = this.isHorizontal()
458
555
  for (const canvas of this.canvasList.values()) {
459
556
  if (canvas.element) this.chartContainer.appendChild(canvas.element)
460
557
  }
@@ -479,10 +576,18 @@ export class WidgetLinechart extends LitElement {
479
576
  yAxisLabel: this.inputData?.axis?.yAxisLabel,
480
577
  xAxisZoom: this.inputData?.axis?.xAxisZoom,
481
578
  yAxisScaling: this.inputData?.axis?.yAxisScaling,
579
+ yAxisLabelRight: this.inputData?.axis?.yAxisLabelRight,
580
+ yAxisScalingRight: this.inputData?.axis?.yAxisScalingRight,
482
581
  xAxisType: this.xAxisType(),
483
582
  yAxisType: this.yAxisType(),
583
+ // Flipping the orientation swaps which option key holds the
584
+ // single category axis and which holds the value-axis array,
585
+ // so it must force a notMerge rebuild rather than a merge.
586
+ orientation: this.inputData?.axis?.orientation ?? 'vertical',
484
587
  seriesCount: chart.series.length,
485
- seriesNames: chart.series.map((s) => s.name).join(',')
588
+ seriesNames: chart.series.map((s) => s.name).join(','),
589
+ seriesAxes: chart.series.map((s) => s.valueAxisIndex ?? 0).join(','),
590
+ seriesLabels: chart.series.map((s) => ((s as any).label?.show ? 1 : 0)).join(',')
486
591
  })
487
592
  const configChanged = chart.lastConfig !== currentConfig
488
593
  chart.lastConfig = currentConfig
@@ -520,31 +625,114 @@ export class WidgetLinechart extends LitElement {
520
625
  }
521
626
  }
522
627
 
523
- option.dataZoom[0].show = this.inputData?.axis?.xAxisZoom ?? false
524
- option.toolbox.show = this.inputData?.axis?.xAxisZoom ?? false
628
+ const showZoom = this.inputData?.axis?.xAxisZoom ?? false
629
+ option.dataZoom[0].show = showZoom
630
+ option.toolbox.show = showZoom
631
+
632
+ // Y axes: index 0 = left (primary), index 1 = right (secondary).
633
+ // The template holds an array, but getOption() (merge path) also returns
634
+ // component options normalized to arrays — handle both shapes.
635
+ const yAxes: any[] = Array.isArray(option.yAxis) ? option.yAxis : [option.yAxis ?? {}]
636
+ while (yAxes.length < 2) yAxes.push({})
637
+ option.yAxis = yAxes
638
+
639
+ const hasValueLabels = chart.series.some((s) => !!(s as any).label?.show)
640
+ const rightAxisUsed = chart.series.some((s) => (s.valueAxisIndex ?? 0) === 1)
641
+ const leftAxisUsed =
642
+ chart.series.length === 0 || chart.series.some((s) => (s.valueAxisIndex ?? 0) === 0)
643
+ const showLeftAxis = showYAxis && leftAxisUsed
644
+ const showRightAxis = showYAxis && rightAxisUsed
525
645
 
526
646
  const yAxisLabel = this.inputData?.axis?.yAxisLabel ?? ''
527
- const hasYAxisLabel = showYAxis && !!yAxisLabel
528
- option.yAxis.type = this.yAxisType()
529
- option.yAxis.name = yAxisLabel
530
- option.yAxis.scale = this.inputData?.axis?.yAxisScaling ?? false
531
- option.yAxis.show = showYAxis
532
- option.yAxis.nameLocation = 'end'
533
- option.yAxis.nameGap = 10
534
- option.yAxis.nameTextStyle = { align: 'left' }
535
- option.yAxis.axisLine = { show: true }
536
- if (['value', 'log'].includes(option.yAxis.type))
537
- option.yAxis.axisLabel = {
538
- 'font-size': 14,
539
- formatter: (value: number) => Math.round(value * 100) / 100
647
+ const yAxisLabelRight = this.inputData?.axis?.yAxisLabelRight ?? ''
648
+ const hasYAxisLabel = showLeftAxis && !!yAxisLabel
649
+ const hasYAxisLabelRight = showRightAxis && !!yAxisLabelRight
650
+
651
+ const yType = this.yAxisType()
652
+ const numericAxisLabel = ['value', 'log'].includes(yType ?? '')
653
+ ? { fontSize: 14, formatter: (value: number) => Math.round(value * 100) / 100 }
654
+ : undefined
655
+
656
+ Object.assign(yAxes[0], {
657
+ type: yType,
658
+ name: yAxisLabel,
659
+ scale: this.inputData?.axis?.yAxisScaling ?? false,
660
+ show: showLeftAxis,
661
+ position: 'left',
662
+ nameLocation: 'end',
663
+ nameGap: 10,
664
+ nameTextStyle: { align: 'left' },
665
+ axisLine: { show: true }
666
+ })
667
+ if (numericAxisLabel) yAxes[0].axisLabel = numericAxisLabel
668
+
669
+ Object.assign(yAxes[1], {
670
+ type: yType,
671
+ name: showRightAxis ? yAxisLabelRight : '',
672
+ scale: this.inputData?.axis?.yAxisScalingRight ?? false,
673
+ show: showRightAxis,
674
+ position: 'right',
675
+ nameLocation: 'end',
676
+ nameGap: 10,
677
+ nameTextStyle: { align: 'right' },
678
+ axisLine: { show: true },
679
+ splitLine: { show: false }
680
+ })
681
+ if (numericAxisLabel) yAxes[1].axisLabel = numericAxisLabel
682
+
683
+ // Orientation swap. Everything above builds one category axis (the
684
+ // x-values) and a two-entry array of value axes (the y-values). A
685
+ // vertical chart puts them where they were built; a horizontal one
686
+ // moves the category axis onto `yAxis` and the value axes onto
687
+ // `xAxis`. The axis *labels* keep their meaning either way:
688
+ // 'X-Axis Label' names the x-value dimension wherever it is drawn.
689
+ if (horizontal) {
690
+ const catAxis: any = Array.isArray(option.xAxis) ? option.xAxis[0] : option.xAxis
691
+ option.yAxis = {
692
+ ...catAxis,
693
+ position: 'left',
694
+ // Read top-to-bottom, so the first data point is the top
695
+ // bar — the convention for ranked horizontal bar charts.
696
+ inverse: true,
697
+ // 'start' is the top of an inverted axis, which keeps the
698
+ // axis name clear of the value axis running along the bottom.
699
+ // When a secondary value axis is in use it is drawn along
700
+ // the top, so the name has to clear its tick labels too.
701
+ nameLocation: 'start',
702
+ nameGap: showRightAxis ? 30 : 10,
703
+ nameTextStyle: { align: 'left' }
540
704
  }
705
+ option.xAxis = yAxes.map((axis: any, index: number) => ({
706
+ ...axis,
707
+ position: index === 0 ? 'bottom' : 'top',
708
+ nameLocation: 'middle',
709
+ nameGap: 27,
710
+ nameTextStyle: { align: 'center' }
711
+ }))
712
+ // Zooming still applies to the x-value dimension, which now
713
+ // lives on the y axis.
714
+ option.dataZoom = [
715
+ { ...(option.dataZoom?.[0] ?? {}), xAxisIndex: undefined, yAxisIndex: [0] }
716
+ ]
717
+ }
541
718
 
542
719
  option.series = chart.series
543
720
  option.legend.show = showLegend
544
721
 
545
722
  // Dynamic grid padding based on visible elements
546
- // Add extra top space when Y-axis label is shown at 'end' position
547
- const topPadding = showTitle ? 30 : hasYAxisLabel ? 30 : 0
723
+ // Add extra top space when Y-axis label is shown at 'end' position.
724
+ // The right axis name and the legend both live in the top-right corner,
725
+ // so reserve an extra row when both are visible.
726
+ const topPadding =
727
+ (showTitle || hasYAxisLabel || hasYAxisLabelRight ? 30 : 0) +
728
+ (showLegend && hasYAxisLabelRight ? 25 : 0)
729
+ // An ECharts slider dataZoom is ~30px thick and sits outside the
730
+ // grid's own bookkeeping, so nothing reserves room for it. Left
731
+ // unaccounted it is drawn straight over the plot, clipping the foot
732
+ // of every bar and washing out the axis labels underneath.
733
+ const ZOOM_THICKNESS = 40
734
+ const zoomPadBottom = showZoom && !horizontal ? ZOOM_THICKNESS : 0
735
+ const zoomPadRight = showZoom && horizontal ? ZOOM_THICKNESS : 0
548
736
  option.grid = {
549
737
  ...option.grid,
550
738
  show: showBox,
@@ -552,12 +740,31 @@ export class WidgetLinechart extends LitElement {
552
740
  borderWidth: showBox ? 1 : 0,
553
741
  borderColor: this.themeTitleColor ?? '#ccc',
554
742
  top: topPadding,
555
- bottom: showXAxis ? 20 : 0,
556
- left: showYAxis ? 20 : 0,
557
- right: 0,
743
+ // The category axis sits at the bottom when vertical and on the
744
+ // left when horizontal; the value axis is the other way round.
745
+ // The zoom slider is not accounted for by `containLabel`, so its
746
+ // thickness has to be reserved here as well — see ZOOM_THICKNESS.
747
+ bottom: ((horizontal ? showLeftAxis : showXAxis) ? 20 : 0) + zoomPadBottom,
748
+ left: (horizontal ? showXAxis : showLeftAxis) ? 20 : 0,
749
+ // A horizontal chart grows towards the right edge, so the last
750
+ // axis tick and any value label printed past the end of a bar
751
+ // need room that `containLabel` does not reserve for them.
752
+ right: (horizontal ? (hasValueLabels ? 45 : 15) : 0) + zoomPadRight,
558
753
  containLabel: showXAxis || showYAxis
559
754
  }
560
755
 
756
+ // Anchor the slider to the edge whose dimension it zooms: the bottom
757
+ // for a vertical chart, the right-hand side for a horizontal one
758
+ // (where it controls the y axis and ECharts draws it upright).
759
+ if (showZoom) {
760
+ Object.assign(
761
+ option.dataZoom[0],
762
+ horizontal
763
+ ? { right: 0, top: topPadding, bottom: 20, left: undefined }
764
+ : { bottom: 0, left: undefined, right: undefined, top: undefined }
765
+ )
766
+ }
767
+
561
768
  // Calculate animation duration based on update frequency
562
769
  const animationDuration = this.updateThresholdMs //this.calculateAnimationDuration(chart, label)
563
770
  option.animation = true