I'm trying to get multiple label per item on Kendo Column chart
Desired layout looks like this
I was able to get only this layout
import { Component } from '#angular/core';
import { groupBy, GroupResult } from '#progress/kendo-data-query';
import { ValueAxisLabels } from '#progress/kendo-angular-charts';
export type TrendItem = {
clientName: string;
periodName: string;
income: number;
};
#Component({
selector: 'my-app',
template: `
<kendo-chart>
<kendo-chart-category-axis>
<kendo-chart-category-axis-item [categories]="categories">
</kendo-chart-category-axis-item>
</kendo-chart-category-axis>
<kendo-chart-value-axis>
<kendo-chart-value-axis-item [labels]="valueAxisLabels">
</kendo-chart-value-axis-item>
</kendo-chart-value-axis>
<kendo-chart-series>
<kendo-chart-series-item *ngFor="let groupedResult of groupedTrendsByPeriod" [data]="groupedResult.items" field="income" type="column">
<kendo-chart-series-item-labels [content]="labelVisual">
</kendo-chart-series-item-labels>
</kendo-chart-series-item>
</kendo-chart-series>
</kendo-chart>
`,
})
export class AppComponent {
public valueAxisLabels: ValueAxisLabels = {
font: 'bold 16px Arial, sans-serif',
};
public trendItems: TrendItem[] = [
{
clientName: 'Client1',
periodName: 'Q1 2020',
income: 20,
},
{
clientName: 'Client1',
periodName: 'Q2 2020',
income: 15,
},
{
clientName: 'Client1',
periodName: 'Q3 2020',
income: 35,
},
{
clientName: 'Client1',
periodName: 'Q4 2020',
income: 40,
},
{
clientName: 'Client2',
periodName: 'Q1 2020',
income: 15,
},
{
clientName: 'Client2',
periodName: 'Q2 2020',
income: 20,
},
{
clientName: 'Client2',
periodName: 'Q3 2020',
income: 15,
},
{
clientName: 'Client2',
periodName: 'Q4 2020',
income: 30,
}
];
public categories = (groupBy(this.trendItems, [{ field: 'clientName' }]) as GroupResult[])
.map((e) => e.value);
public groupedTrendsByPeriod = groupBy(this.trendItems, [{ field: 'periodName' }]) as GroupResult[];
public labelVisual(e: { dataItem: TrendItem }) {
return `$${e.dataItem.income}\r\n${e.dataItem.periodName}`;
}
}
You can try this code here.
My current result look like this
So my question is how to display multiple labels per item like on the first picture?
My current obstacles.
I didn't find a way to add multiple <kendo-chart-series-item-labels> elements. Only one will be rendered, rest will be ignored.
I didn't find a way to position labels below column chart. For column chart it's only possible to use "center", "insideBase", "insideEnd", "outsideEnd" options (according to API Reference) but none of them gives me desired position.
You can use the notes feature of the KendoChartComponent.
You can see the implementation here.
You add noteTextField="periodName" [notes]="notesOptions" to your kendo-chart-series-item element.
You add [labels]="labelOptions" to your kendo-chart-category-axis-item element.
You add the following to your component:
public notesOptions = {
position: 'bottom',
icon: {
visible: false,
},
label: {
color: 'black',
content: (e) => e.dataItem.periodName,
font: "12px Arial",
margin: -30,
},
line: {
width: 0,
},
};
public labelOptions = {
margin: {
top: 10,
},
};
Change the labelVisual function return value to `$${e.dataItem.income}\`.
Note: Some changes can be performed and still give the desired design so if something isn't exactly right, it's probably fixable.
Here is an image of the resulting chart:
I don't think kendo provides any native solution for that but what I can suggest is to:
Use legends to display each bar meaning. like the example here.
Use some self generated labels and position them under the table which is risky for UI. I provided an example here.
Related
Given the sample code using ag-grid with Vue 3
<script setup lang="ts">
import "ag-grid-community/dist/styles/ag-grid.css";
import "ag-grid-community/dist/styles/ag-theme-alpine.css";
import { AgGridVue } from "ag-grid-vue3";
import { ref } from "vue";
const columnDefs = ref([
{
field: "person",
},
{
field: "car",
},
]);
const rowData = ref([
{
person: "person-1",
car: "car-1",
},
{
person: "person-1",
car: "car-2",
},
{
person: "person-1",
car: "car-3",
},
{
person: "person-2",
car: "car-4",
},
{
person: "person-3",
car: "car-5",
},
{
person: "person-3",
car: "car-6",
},
]);
</script>
<template>
<ag-grid-vue
style="width: 500px; height: 500px"
class="ag-theme-alpine"
:columnDefs="columnDefs"
:rowData="rowData"
>
</ag-grid-vue>
</template>
You will get the following output
I could group the table based on person but is there a way to merge cells in a single column?
I think I can't use row-spanning for a single column definition because inside the rowSpan function it is not possible to tell ag-grid to combine multiple cells of a single column.
Any ideas?
After spending a few hours on this requirement, I came up with the solution.
Here are the steps which I performed :
To implement rowSpan, You have to manipulate the rowData so that it will contain the person property only in the first occurance object. As a result, When we will apply the rowSpan based on the length it will not take that length for each same person name. This is how I manipulated the rowData :
const uniq = {};
const rowData = [{
person: "person-1",
car: "car-1",
}, {
person: "person-1",
car: "car-2",
}, {
person: "person-1",
car: "car-3",
}, {
person: "person-2",
car: "car-4",
}, {
person: "person-3",
car: "car-5",
}, {
person: "person-3",
car: "car-6",
}];
rowData.forEach(obj => {
!uniq[obj.person] ? uniq[obj.person] = true : delete obj.person;
});
console.log(rowData);
Now, Created a rowSpan function which is basically used to return the length of the objects contains occurrence of same person names.
function rowSpan(params) {
const person = params.data.person;
const gridData = getData(); // getData() with return the original rowData array.
return gridData.filter((e) => e.person === person).length;
}
At the end, for styling the rowSpan columns. I used these styles (refer from here)
.show-cell {
background: white;
border-left: 1px solid lightgrey !important;
border-right: 1px solid lightgrey !important;
border-bottom: 1px solid lightgrey !important;
}
This is how the columnDefs for person object looks like :
{
field: "person",
rowSpan: rowSpan,
cellClassRules: { 'show-cell': 'value !== undefined' }
}
Live Demo : Row Spanning Demo
Are you sure row-spanning will not work? Can you try to implement this?
function rowSpan(params) {
const person = params.data.person;
return rowData.value.filter((e) => e.person === person).length;
}
const columnDefs = ref([
{
field: "person",
rowSpan: rowSpan,
},
{
field: "car",
},
]);
I'm creating an interactive viz with React and I would like to add a slider in my viz.
import "./styles.css";
import React from "react";
import Sunburst from "react-zoomable-sunburst";
import { data } from "./data";
import { Slider } from "#mui/material";
class App extends React.Component {
onSelect(event) {
console.log(event);
}
render() {
return (
<div className="App">
<Sunburst
width="400"
height="400"
data={data.a}
count_member="size"
labelFunc={(node) => node.data.name}
_debug={false}
/>
<Slider
aria-label="Year"
defaultValue={2016}
valueLabelDisplay="auto"
step={1}
marks
min={2016}
max={2020}
/>
</div>
);
}
}
export default App;
Above is my code. I added my data from 2016 to 2020 in data.js file and I want my viz to change according to the year as I move my slider.
a: {
name: "2020",
children: [
{
name: "Campus",
children: [
{
name: "Liabilities",
children: [
{
name: "Current Liabilities",
children: [
{ name: "Accounts payable", size: 53010 },
{ name: "Accrued salaries", size: 23554 },
{ name: "Unearned revenue", size: 253322 },
{ name: "Commercial paper", size: 326008 },
{ name: "Current portion of long-term debt", size: 112431 },
{ name: "Funds held for others", size: 2500 },
{ name: "Other current liabilities", size: 71036 }
]
}
This is a part of my code in data.js and I named 2016 data as a, 2017 as b, and so on.
I wanted to name as 2016, 2017... instead of a,b... but this didn't work since data.2016 didn't work. (So I made it data.a in the code above)
So currently, my slider doesn't work. Is there any way to make dictionary or other method in React to resolve this problem?
Thanks.
use
data['2016']
That should work.
I am creating this tree map and I have different values What I want is different shades of colors(green for postive and red for negative) on different ranges of points. The issue is in my callback function I am accessing the value parameter but points is not available.
Also the second issue is I want the font size according to the size of the box. As there are some boxes which are very big but the font size looks very small. I think If i get that color part fix I can set the fontsize on my own
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.anychart.com/releases/v8/js/anychart-core.min.js"></script>
<script src="https:/cdn.anychart.com/releases/v8/js/anychart-treemap.min.js"></script>
<title>My First JavaScript Treemap Chart</title>
<style>
html,
body,
#container {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="container"></div>
<style>
span {
color: #fff;
font-size: 14px;
}
</style>
<script>
anychart.onDocumentReady(function () {
var chart = anychart.treeMap(getData(), "as-tree");
chart.maxDepth(2);
chart.hintDepth(1);
chart.hintOpacity(0.7);
chart.labels().useHtml(true);
chart.labels().format(
"<span style='font-weight:bold; color: #fff;'>{%name}</span><br>{%points}"
);
chart.tooltip().useHtml(true);
chart.tooltip().format(
"COMPANY : {%name}%<br>PERCENTAGE CHANGE: {%points}%<br>MARKET CAP: {%points}<br>VOLUME: {%points}<br>"
);
chart.container("container");
chart.draw();
var customColorScale = anychart.scales.linearColor();
customColorScale.colors(["green", "red",]);
console.log(chart)
chart.colorScale(customColorScale);
// chart.colorRange().enabled(true);
chart.colorRange().length("100%");
chart.fontColor = 'white'
chart.fill(coloringFunction);
function coloringFunction(value, points, name) {
console.log(value)
if (this.value < 0) {
return 'red'
}
else if (this.value > 0) {
return 'red'
}
}
/*chart.title().useHtml(true);
chart.title(
"<span style='font-size:18; font-style:bold'>PSX TREE MAP </span><br><i><span style='font-size:14; font-style:italic'>UPDATED(7:11 PM)</i>"
);*/
});
// get data
function getData() {
// create data
var data = [
{
name: "PAKISTAN STOCK EXCHANGE",
children: [
{
name: "TECH ",
children: [
{
name: "HUMNL",
value: 24,
points: 0.26
},
{
name: "TRG",
value: 6,
points: 2.3
,
points: 0.26
},
{
name: "SYS",
value: 55,
points: 4.26
}
]
},
{
name: "SUGAR & ALLIED INDUSTRIES ",
children: [
{
name: "AGSML",
value: 12,
points: 2.26
},
{
name: "MIRKS",
value: 14,
points: 3.26
,
},
{
name: "ALNRS",
value: 15,
points: 0.26
}
]
},
]
}
];
return data;
}
</script>
</body>
</html>
To achieve the coloring you should provide ABS value for the "size" field in the data. But the "value" field may reflect positive/negative values.
Like this:
{name: "Point 1", value: -11443830, size: 11443830}
Exactly the "value" field is used for linear and ordinal coloring. The treemap supports both, you can learn more about it in the documentation article.
In your case, you can create and adjust linear scale like this:
// create and configure a color scale.
var customColorScale = anychart.scales.linearColor();
customColorScale.minimum(-100000000).maximum(100000000);
customColorScale.colors(["red", "green"]);
// set the color scale as the color scale of the chart
chart.colorScale(customColorScale);
Min/max apply as you need or do not apply it to calculate them automatically.
To adjust label fontsize for every item simply enable adjustFontSize() setting:
chart.labels().adjustFontSize(true);
For details, check the sample we prepared.
anychart.onDocumentReady(function () {
// create data
var data = [
{name: "European Union", children: [
{name: "Belgium", value: -11443830, size: 11443830},
{name: "France", value: 64938716, size: 64938716},
{name: "Germany", value: 80636124, size: 80636124},
{name: "Greece", value: -10892931, size: 10892931},
{name: "Italy", value: 59797978, size: 59797978},
{name: "Netherlands", value: -17032845, size: 17032845},
{name: "Poland", value: 38563573, size: 38563573},
{name: "Romania", value: -19237513, size: 19237513},
{name: "Spain", value: 46070146, size: 46070146},
{name: "United Kingdom", value: 65511098, size: 65511098}
]}
];
// create a chart and set the data
var chart = anychart.treeMap(data, "as-tree");
// create and configure a color scale.
var customColorScale = anychart.scales.linearColor();
customColorScale.minimum(-100000000).maximum(100000000);
customColorScale.colors(["red", "green"]);
// set the color scale as the color scale of the chart
chart.colorScale(customColorScale);
// add a color range
chart.colorRange().enabled(true);
chart.colorRange().length("80%");
chart.labels().adjustFontSize(true);
// set the container id
chart.container("container");
// initiate drawing the chart
chart.draw();
});
html, body, #container {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
<script src="https://cdn.anychart.com/releases/8.10.0/js/anychart-bundle.min.js"></script>
<div id="container"></div>
I'm trying to create a list and append values to it that I've retrieved from xml http request. I've tested just a text block with the value of typeAssetProcess and it prints fine, but when I start trying to use a list is when everything starts to break. What am I doing wrong and how can I fix it?
import QtQuick 2.0
import "../controls" as Controls
Item {
Column {
id: column
width: parent.width
height: parent.height
}
ListView {
id: listView
width: parent.width
height: parent.height
model: ListModel {
ListElement {
name: qstr("Proccess: %1").arg(typeAssetProcess)
colorCode: "grey"
}
ListElement {
name: "Red"
colorCode: "red"
}
ListElement {
name: "Blue"
colorCode: "blue"
}
ListElement {
name: "Green"
colorCode: "green"
}
}
delegate: Item {
x: 5
width: 80
height: 40
Row {
id: row1
Rectangle {
width: 40
height: 40
color: colorCode
}
Text {
text: name
font.bold: true
anchors.verticalCenter: parent.verticalCenter
}
spacing: 10
}
}
}
}
This code is what is breaking:
ListElement {
name: qstr("Proccess: %1").arg(typeAssetProcess)
colorCode: "grey"
}
This is a known limitation to the ListElement type, containing a "collection of role definitions instead of properties". This is why you can not use script or property binding for these roles (otherwise you get the error ListElement: cannot use script for property value).
Improvements are frequently requested to the Qt team but as far as I know there is no implementation yet.
One thing you can do is dynamically initialize the model instead of using fixed ListElement:
ListView {
id: listView
readonly property var modelElements: [
{
name: qsTr("Proccess: %1").arg(typeAssetProcess),
colorCode: "grey"
},
{
name: "Red",
colorCode: "red"
},
{
name: "blue",
colorCode: "blue"
},
{
name: "Green",
colorCode: "green"
}]
Component.onCompleted: {
modelElements.forEach(function(element) {
model.append(element)
})
}
width: parent.width
height: parent.height
model: ListModel {}
delegate: ...
}
You can also choose to implement your own model in C++.
I'm wondering how can I pass an array in ListModel?
ok, In QML I have a ListView and I set it's ListModel like so:
model: ListModel
{
id: myList
ListElement
{
name: ""
card: 0
books: []
}
}
I can append to it by using:
myList.append({name:"terry", card:00100, books:["024589","865976","879582","215645"]});
but when I try to output it on screen I get this.
{
"card": 00100
"books": {
"objectName": "",
"count": 4,
"dynamicRoles": false
},
"name": "terry",
"name": "terry"
}
I'm not sure why I'm getting 2 names though! and how can I get the value of books?
I look up the QML documentation of ListModel and ListElement couldn't find anything related to passing an array, all the examples are integer or string.
Any idea how I can get the date?
I did work around it by calling the array in Delegate with Component.onCompleted:{} but I believe that's not a good/correct way since Delegate is not responsible for holding the data and should be done in Model, please do correct me if I'm wrong.
Thanks for your time.
Edit01: Thanks for the reply, here is the reason I need array:
I have a ComboBox in Delegate like so:
delegate: Rectangle
{
id: rowID
width: 50
height: 40
color: "#323232"
Row
{
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
Label{
id: nameID
text: name
font.pixelSize: 12
width: 200
wrapMode: Text.WrapAnywhere
anchors.verticalCenter: parent.verticalCenter
color: "#999"
}
Label{
anchors.verticalCenter: parent.verticalCenter
text: "out:"
font.pixelSize: 12
color: "#999"
}
ComboBox{
id: booksID
height: 20
width: 50
model: books
anchors.verticalCenter: parent.verticalCenter
}
}
}
as you can see I'm feeding the name to Label (id: nameID) and I want to feed the books to ComboBox (id: booksID) that has model, if I make books key as ListElement how can I feed all the values?
in QML ListModel or ListElement documentation didn't mention anything about getting all the key's value right? it only supports get(int index) which is based on an index number.
You did that wrong. Array members must be ListElement:
ListModel {
id: mod
ListElement {
name: "ali"
dic: [ ListElement{text:"asad-o-llah"; code: 14}, ListElement{text:"aboo torab"; code: 72}, ListElement{text:"amir al-momenin"; code: 110}]
}
}
ListView {
model: mod
anchors.fill: parent
delegate: Component {
Rectangle {
width: parent.width; height: 50
Row {
Text {
text: name
}
ComboBox {
width: 100; height: 30
model: dic //<-- set dic as model for combo box
textRole: "text" //<-- important!
onCurrentIndexChanged: {
console.log("current code is "+model.get(currentIndex).code); //<-- get code value
}
}
}
}
}
}
Component.onCompleted: {
var v = mod.get(0).dic.get(0).value; //<-- sample usage
console.log(v);
}
Do you want some thing similar to this:
Rectangle {
id: root
visible: true
width: 360
height: 360
ListModel
{
id: myList
ListElement {
name: "Story"
card: 3
books: [
ListElement { bookName: "Story 1" },
ListElement { bookName: "Story 2" },
ListElement { bookName: "Story 3" }
]
}
ListElement {
name: "Novel"
card: 3
books: [
ListElement { bookName: "Novel 1" },
ListElement { bookName: "Novel 2" },
ListElement { bookName: "Novel 3" }
]
}
}
Component {
id: displayDelegate
Rectangle
{
id: rowID
width: 300 //50
height: 40
color: "#323232"
border.color: "white"
Row
{
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
Text{
id: nameID
text: name
font.pixelSize: 12
width: 50 //200
wrapMode: Text.WrapAnywhere
/*anchors.verticalCenter: parent.verticalCenter*/
color: "white"//"#999"
}
Text{
/*anchors.verticalCenter: parent.verticalCenter*/
text: "out:"
font.pixelSize: 12
color: "white"//"#999"
}
/*ComboBox{
id: booksID
height: 20
width: 50
model: books
anchors.verticalCenter: parent.verticalCenter
}*/
Repeater {
model: books
Text { text: bookName + "\t"; color: "white" }
}
}
}
}
ListView {
id: disp
anchors.fill: parent
model: myList
delegate: displayDelegate
}
}
I have modified few line of the code which you have shared. I am not sure about your ComboBox implementation. Therefore, I have used my own implementation of Repeater. You can try to execute and check the result.
As an alternative to working with ListModel and ListElement, you can also have a look at the QSyncable JsonListModel QML type. (It is an open-source component by Ben Lau, you can find it on GitHub here: https://github.com/benlau/qsyncable)
The JsonListModel is a specialized ListModel type, that can handle JSON Arrays that you e.g. create in your QML or fetch from a REST service. It automatically synchronizes the JSON to a QML ListModel, so its very convenient to use:
ListView {
id: listView
anchors.fill: parent
// property for json data, used as source for JsonListModel
property var jsonData: []
// use JsonListModel as model
model: JsonListModel {
source: listView.jsonData
keyField: "id"
}
// delegate
delegate: DelegateItem { /* ... */ }
}
You can also find a comprehensive guide how it works here: JsonListModel guide