Show real-time data in your Vue pivot table tool.
Play with the demo on a larger screen: save this link for later or watch the video review now.
<script setup>
import { useTemplateRef } from "vue";
import Pivot from "vue-flexmonster/vue3";
import "flexmonster/flexmonster.css";
const pivotRef = useTemplateRef("pivot");
const report = {
dataSource: {
type: "json",
data: [
{
"ID": "ISK",
"Currency": "ISK",
"Bid Rate": 144.904417,
"Ask Rate": 144.904417
}
],
mapping: {
"ID": {
type: "id"
},
"Currency": {
type: "string"
},
"Bid Rate": {
type: "number"
},
"Ask Rate": {
type: "number"
}
}
},
slice: {
rows: [
{
uniqueName: "Currency",
sort: "asc"
}
],
columns: [
{
uniqueName: "[Measures]"
}
],
measures: [
{
uniqueName: "Bid Rate",
aggregation: "sum",
format: "rate_format"
},
{
uniqueName: "Ask Rate",
aggregation: "sum",
format: "rate_format"
},
{
uniqueName: "Spread",
formula: "(sum('Ask Rate') - sum('Bid Rate')",
caption: "Spread",
format: "spread_format"
}
],
flatOrder: ["Bid Rate", "Currency", "Ask Rate", "Spread"]
},
options: {
grid: {
type: "flat",
showFilter: false,
showHeaders: false,
showTotals: "off",
showGrandTotals: "off"
},
configuratorButton: false,
drillThrough: false
},
conditions: [
{
formula: "if(#updateDifference < 0, 'fm-updated-cell-down', 'fm-updated-cell-up')"
}
],
formats: [
{
name: "rate_format",
decimalPlaces: 4,
textAlign: "left",
thousandsSeparator: ",",
decimalSeparator: "."
},
{
name: "spread_format",
decimalPlaces: 2,
textAlign: "right"
}
],
tableSizes: {
columns: [
{
tuple: ["Currency"],
width: 200
},
{
tuple: ["Bid Rate"],
width: 200
},
{
tuple: ["Ask Rate"],
width: 200
},
{
tuple: ["Spread"],
width: 200
}
]
}
};
const currency_data = {
base: "EUR",
date: "2026-02-19",
rates: {
"AED": 4.315695,
"AFN": 73.196281,
"ALL": 96.41278,
"AMD": 442.699239,
"AOA": 1075.5504,
"ARS": 1637.8094,
"AUD": 1.668851,
"AWG": 2.103194,
"AZN": 1.997389,
"BAM": 1.95583,
"BBD": 2.349943,
"BCH": 0.00217389,
"BDT": 143.624622,
"BHD": 0.441754,
"BIF": 3484.9372,
"BMD": 1.174735,
"BND": 1.491319,
"BOB": 8.123182,
"BRL": 6.139918,
"BSD": 1.174774,
"BTC": 0.00001785,
"BTN": 107.071755,
"BWP": 15.569885,
"BZD": 2.366204,
"CAD": 1.610968,
"CDF": 2714.6536,
"CHF": 0.911323,
"CLP": 1019.4707,
"CNY": 8.117184,
"COP": 4341.444,
"CRC": 564.998862,
"CUC": 1.174898,
"CUP": 28.206249,
"CVE": 110.27,
"CZK": 24.263945,
"DJF": 209.051711,
"DKK": 7.471706,
"DOP": 72.326258,
"DZD": 152.8183896,
"EGP": 55.918178,
"ERN": 17.650672,
"ETB": 181.052726,
"ETH": 0.00061124,
"EUR": 1,
"FJD": 2.588981,
"FKP": 0.873974,
"GBP": 0.873974,
"GEL": 3.148256,
"GGP": 0.873974,
"GHS": 12.934981,
"GIP": 0.873974,
"GMD": 87.113153,
"GNF": 10329.61,
"GTQ": 9.028754,
"GYD": 246.2709298,
"HKD": 9.194832,
"HNL": 31.132507,
"HTG": 154.376922,
"HUF": 378.7144395,
"IDR": 19886.25,
"ILS": 3.684162,
"IMP": 0.873974,
"INR": 107.139324,
"IQD": 1542.0246,
"IRR": 1513627.74,
"ISK": 144.904417,
"JEP": 0.873974,
"JMD": 183.420735,
"JOD": 0.834364,
"JPY": 182.478885,
"KES": 151.918593,
"KGS": 102.952542,
"KHR": 4729.8484,
"KMF": 491.96775,
"KPW": 1059.6772,
"KRW": 1704.4228,
"KWD": 0.361109,
"KYD": 0.977261,
"KZT": 580.061951,
"LAK": 25229.85,
"LBP": 105696.5,
"LKR": 364.287964,
"LRD": 218.4740298,
"LSL": 19.0093097,
"LTL": 3.4528,
"LVL": 0.7028,
"LYD": 7.450847,
"MAD": 10.79119,
"MDL": 20.155611,
"MGA": 5122.2147,
"MKD": 61.600169,
"MMK": 2470.7194,
"MNT": 4201.1779,
"MOP": 9.4714,
"MRU": 46.910393,
"MUR": 54.285057,
"MVR": 18.1624696,
"MWK": 2041.4086,
"MXN": 20.296938,
"MYR": 4.598598,
"MZN": 75.125987,
"NAD": 19.010569,
"NGN": 1579.6115,
"NIO": 43.297921,
"NOK": 11.257276,
"NPR": 171.453562,
"NZD": 1.969253,
"OMR": 0.45244,
"PAB": 1.176271,
"PEN": 3.951717,
"PGK": 5.055734,
"PHP": 68.268117,
"PKR": 328.967861,
"PLN": 4.223936,
"PYG": 7637.6817,
"QAR": 4.281802,
"RON": 5.097529,
"RSD": 117.421545,
"RWF": 1718.5892,
"SAR": 4.411401,
"SBD": 9.462084,
"SCR": 17.047283,
"SDG": 706.077194,
"SEK": 10.667844,
"SGD": 1.492383,
"SHP": 0.873974,
"SLE": 27.002706,
"SLL": 27002.7,
"SOS": 670.180148,
"SPL": 0.196101,
"SRD": 44.258017,
"STN": 24.634009,
"SVC": 10.294256,
"SYP": 130.079984,
"SZL": 19.034846,
"THB": 36.66563,
"TJS": 11.097921,
"TMT": 4.115697,
"TND": 3.381045,
"TOP": 2.830983,
"TRY": 51.488771,
"TTD": 7.963055,
"TVD": 1.667513,
"TWD": 37.173483,
"TZS": 3045.3736,
"UAH": 50.8901,
"UGX": 4219.8786,
"USD": 1.176459,
"UYU": 45.654212,
"UZS": 14280.82,
"VES": 466.598574,
"VND": 30522.47,
"VUV": 140.00752,
"WST": 3.178346,
"XAF": 655.957,
"XAG": 0.015023,
"XAU": 0.00023519,
"XCD": 3.184203,
"XCG": 2.120881,
"XDR": 0.8541,
"XOF": 655.957,
"XPF": 119.331742,
"YER": 280.462391,
"ZAR": 19.038241,
"ZMK": 22332.84,
"ZMW": 22.33284,
"ZWG": 30.124114
}
};
const valids = [
"EUR",
"JPY",
"CZK",
"DKK",
"GBP",
"HUF",
"UAH",
"LTL",
"LVL",
"PLN",
"RON",
"SEK",
"CHF",
"NOK",
"HRK",
"TRY",
"AUD",
"BRL",
"CAD",
"CNY",
"HKD",
"IDR",
"ILS",
"INR",
"KRW",
"MXN",
"MYR",
"NZD",
"PHP",
"SGD",
"THB",
"ZAR",
"ISK"
];
const dataset = [];
function getData() {
const rates = currency_data.rates;
Object.entries(rates).forEach(([key, value]) => {
let record = {};
record["ID"] = key;
record["Currency"] = key;
record["Bid Rate"] = value;
record["Ask Rate"] = value;
if (valids.indexOf(record["Currency"]) > 0) {
dataset.push(record);
}
});
if (dataset.length > 0) {
pivotRef.value.flexmonster.updateData(
{
data: dataset
},
{
partial: true
}
);
}
}
function updateData() {
const numbers = [];
for (let i = 0; i < dataset.length; i++) {
// Generate an array of numbers that are used for indices
numbers[i] = i;
}
const shuffleArray = (arr) => {
return arr
.map((a) => [Math.random(), a])
.sort((a, b) => a[0] - b[0])
.map((a) => a[1]);
};
const random_permutation = shuffleArray(numbers);
random_permutation.forEach((index) => {
let item = dataset[index];
let prev_rate;
let new_rate;
if (index % 2 === 0) {
prev_rate = item["Bid Rate"];
new_rate = prev_rate + randomFloatBetween(0.01, 0.3, 3);
item["Bid Rate"] = new_rate;
item["Ask Rate"] = new_rate - randomFloatBetween(0.01, 0.3, 3);
dataset[index] = item;
} else {
prev_rate = item["Bid Rate"];
new_rate = prev_rate - randomFloatBetween(0.01, 0.2, 3);
if (new_rate > 0) {
item["Bid Rate"] = new_rate;
item["Ask Rate"] = new_rate + randomFloatBetween(0.01, 0.2, 3);
dataset[index] = item;
} else {
new_rate = prev_rate + randomFloatBetween(0.01, 0.2, 3);
item["Bid Rate"] = new_rate;
item["Ask Rate"] = new_rate - randomFloatBetween(0.01, 0.2, 3);
dataset[index] = item;
}
}
});
pivotRef.value.flexmonster.updateData(
{
data: dataset
},
{
partial: true
}
);
}
function randomFloatBetween(minValue, maxValue, precision) {
if (typeof precision === "undefined") {
precision = 4;
}
return parseFloat(
Math.min(
minValue + Math.random() * (maxValue - minValue),
maxValue
).toFixed(precision)
);
}
function customizeCellFunction(cell, data) {
if (data.hierarchy?.uniqueName === "Currency" && data.member) {
cell.addClass("header-cell");
const names = data.member.caption.split("/");
const flag = `<img class="flag" style="width:36px; height:24px;"
src="https://cdn.flexmonster.com/flags/${names[0].toLowerCase()}.svg">`;
cell.text = `<div style="display:flex; align-items:center; font-size:12px;
position:relative; bottom: 4px;">${flag} ${names[0]}</div>`;
}
}
function onReportComplete() {
pivotRef.value.flexmonster.off("reportcomplete");
getData();
updateData();
setInterval(() => {
updateData();
}, 6000);
}
</script>
<template>
<Pivot
ref="pivot"
height="450"
:report="report"
:customizeCell="customizeCellFunction"
:reportcomplete="onReportComplete"
/>
</template>
.header-cell {
background-color: #F7F7F7 !important;
border-bottom: 1px solid #E9E9E9 !important;
border-right: 1px solid #E9E9E9 !important;
}
#fm-pivot-view .fm-grid-layout div.fm-cell {
padding-left: 20px;
}
#fm-pivot-view .fm-header {
background-color: #e9e9e9 !important;
border-right: 1px solid #d5d5d5 !important;
border-bottom: 1px solid #d5d5d5 !important;
}
.fm-updated-cell-down {
color: #DF3800 !important;
font-weight: bold !important;
background-image: none !important;
-webkit-animation: fadeIt 5s ease-in-out;
-moz-animation: fadeIt 5s ease-in-out;
-o-animation: fadeIt 5s ease-in-out;
animation: fadeIt 5s ease-in-out;
}
.fm-updated-cell-down::before {
content: "\2193 \00a0";
position: relative;
bottom: 2px;
}
.fm-updated-cell-up {
color: #00A45A !important;
font-weight: bold !important;
background-image: none !important;
-webkit-animation: fadeIt1 5s ease-in-out;
-moz-animation: fadeIt1 5s ease-in-out;
-o-animation: fadeIt1 5s ease-in-out;
animation: fadeIt1 5s ease-in-out;
}
.fm-updated-cell-up::before {
content: '\2191 \00a0';
position: relative;
bottom: 2px;
}
@-webkit-keyframes fadeIt {
0% {
background-color: #FFFFFF;
}
20% {
background-color: #F9D7CC;
border-bottom: 1px solid #e9c5ba;
border-right: 1px solid #e9c5ba;
}
50% {
background-color: #F9D7CC;
border-bottom: 1px solid #e9c5ba;
border-right: 1px solid #e9c5ba;
}
100% {
background-color: #FFFFFF;
}
}
@-moz-keyframes fadeIt {
0% {
background-color: #FFFFFF;
}
20% {
background-color: #F9D7CC;
border-bottom: 1px solid #e9c5ba;
border-right: 1px solid #e9c5ba;
}
50% {
background-color: #F9D7CC;
border-bottom: 1px solid #e9c5ba;
border-right: 1px solid #e9c5ba;
}
100% {
background-color: #FFFFFF;
}
}
@-o-keyframes fadeIt {
0% {
background-color: #FFFFFF;
}
20% {
background-color: #F9D7CC;
border-bottom: 1px solid #e9c5ba;
border-right: 1px solid #e9c5ba;
}
50% {
background-color: #F9D7CC;
border-bottom: 1px solid #e9c5ba;
border-right: 1px solid #e9c5ba;
}
100% {
background-color: #FFFFFF;
}
}
@keyframes fadeIt {
0% {
background-color: #FFFFFF;
}
20% {
background-color: #F9D7CC;
border-bottom: 1px solid #e9c5ba;
border-right: 1px solid #e9c5ba;
}
50% {
background-color: #F9D7CC;
border-bottom: 1px solid #e9c5ba;
border-right: 1px solid #e9c5ba;
}
100% {
background-color: #FFFFFF;
}
}
@-webkit-keyframes fadeIt1 {
0% {
background-color: #FFFFFF;
}
20% {
background-color: #CCEDDE;
border-bottom: 1px solid #aedbc7;
border-right: 1px solid #aedbc7;
}
50% {
background-color: #CCEDDE;
border-bottom: 1px solid #aedbc7;
border-right: 1px solid #aedbc7;
}
100% {
background-color: #FFFFFF;
}
}
@-moz-keyframes fadeIt1 {
0% {
background-color: #FFFFFF;
}
20% {
background-color: #CCEDDE;
border-bottom: 1px solid #aedbc7;
border-right: 1px solid #aedbc7;
}
50% {
background-color: #CCEDDE;
border-bottom: 1px solid #aedbc7;
border-right: 1px solid #aedbc7;
}
100% {
background-color: #FFFFFF;
}
}
@-o-keyframes fadeIt1 {
0% {
background-color: #FFFFFF;
}
20% {
background-color: #CCEDDE;
border-bottom: 1px solid #aedbc7;
border-right: 1px solid #aedbc7;
}
50% {
background-color: #CCEDDE;
border-bottom: 1px solid #aedbc7;
border-right: 1px solid #aedbc7;
}
100% {
background-color: #FFFFFF;
}
}
@keyframes fadeIt1 {
0% {
background-color: #FFFFFF;
}
20% {
background-color: #CCEDDE;
border-bottom: 1px solid #aedbc7;
border-right: 1px solid #aedbc7;
}
50% {
background-color: #CCEDDE;
border-bottom: 1px solid #aedbc7;
border-right: 1px solid #aedbc7;
}
100% {
background-color: #FFFFFF;
}
}
In this demo, you can see the live streaming updates of JSON data. With the conditional formatting, we highlighted changes in values.
Using the updateData() method, you can update the data without changing your slice and formatting. With this approach, we increased the data loading speed, as there is no need to reload the report.