Vue-ApexChart Set color on individual bars based on another series value - vue.js

I'm using ApexChart-Vue and my graph has two series one bar and one line. The line is the goal for that specific date and the bar is the value for that date. I'm trying to set the color of the bar to red if the value is lower than the goal and to green if the value is equal or higher that the goal.
I understand that a can add a function to the colors array, but only one function per series.
Basically I want to do way the doc says here: https://apexcharts.com/docs/options/colors/
colors: [function({ value, seriesIndex, w }) {
if (value < 55) {
return '#7E36AF'
} else {
return '#D9534F'
}
}, function({ value, seriesIndex, w }) {
if (value < 111) {
return '#7E36AF'
} else {
return '#D9534F'
}
}]
But I need to pass the the goal for the current date into the function some how.
Any ideas?

As I understand it, you wish to be able to customize the values 55 and 111 based on some changing goal eg the target on Bar 1 is 10 but on Bar 2 is 20.
According to the source code, color functions receive the following object:
{
seriesIndex: this.seriesIndex, // the data series being coloured
dataPointIndex: opts.dataPointIndex, // the index for that bar
value: opts.value, // the value for that bar
w: w // ??
}
My approach would be to store the targets as an array then just access the threshold for each date using the index value
colors: [
function ({ value, dataPointIndex }) {
if (targets[dataPointIndex] > ) {
return Colors.red
} else {
return Colors.green
}
}]

Related

set the position of data label according to vertical bar chart column height

I am using vue chat js with chart js 2.9.4 package. And chart js plugin datalabels also plugged in to show data labels. I created a vertical bar chart and when some columns height is lower, data label is overlapped with x-axis. In order to avoid that I did as follows.
plugins: {
datalabels: {
align(context) {
const index = context.dataIndex
const value = context.dataset.data[index]
return value < 10 ? 'top' : 'center'
},
}
}
There as you can see I had to add a condition manually to set the position of the data label. If the value is less than 10, then data label should be on top and if it's more than or equal to 10 it should be on center. But you know that value 10 in the condition should be dynamic. So is there any way to identify this overlap behavior in the used plugin or any good equation to fix this issue?
Take the max value of the dataset you are showing
const data = context.dataset.data
const max = Math.max( ...data )
Then, if for the given bar it is lower than the 10% (or some other percentil) of the max bar value then just do what you where doing.
All together:
const index = context.dataIndex
const data = context.dataset.data
const max = Math.max( ...data )
const percentil = 0.10
const value = data[index]
return value < max * percentil ? 'top' : 'center'

How to create v-autocomplete which first shows items startsWith and then shows items indexOf

I would like to create a vuetify autocomplete with a custom filter that first shows the hits that start with the searchtext and then show the hits that not start with the searchtext, but have the searchtext somewhere in the middle.
I now have a custom filter like this, but this filter is not prioritizing words that start with the searchtext:
customFilter(item, queryText) {
const textOne = item.description.toLowerCase();
const textTwo = item.code.toLowerCase();
const searchText = queryText.toLowerCase();
return (
textOne.indexOf(searchText) > -1 || textTwo.indexOf(searchText) > -1
);
}
},
Codepen (Type 'ma' for example)
I believe you need to sort it manually, filter returns only true/false whether item is a match.
// https://stackoverflow.com/a/36114029/1981247
var _sortByTerm = function (data, term) {
return data.sort(function (a, b) {
// cast to lowercase
return a.toLowerCase().indexOf(term) < b.toLowerCase().indexOf(term) ? -1 : 1;
});
};
Then pass sorted array to items prop
<v-autocomplete :items="computedItems"
...
computed: {
computedItems() {
return _sortByTerm(this.states, this.search.toLowerCase())
},
},
Note this is just to get you started, and you might need to change code a bit according to the data (and filters) you are using, e.g. _sortByTerm works only on array of strings, but in the link there is a solution for sorting arrays of objects also.

Vue.js: error setting a computed property

in the following code (jsbin available here) I have two input elements, a range and a text, bound together via a computed property.
var vm = new Vue({
el: '#main-container',
data: {
sliderValue: 100,
},
computed: {
actualValue: {
get: function() {
if (this.sliderValue <= 100) {
return this.sliderValue;
} else {
return Math.round(this.sliderValue * 12.5 - 1150);
}
},
/* set won't work for val > 100*/
set: function(val) {
if (val <= 100) {
this.sliderValue = val;
} else {
this.sliderValue = Math.round((val + 1150)/12.5);
}
}
}
},
methods: {
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id="main-container">
<input type="range" v-model="sliderValue" min=1 max=132>
<input type="text" v-model="actualValue">
<p>Slider: {{sliderValue}}</p>
<p>Actual: {{actualValue}}</p>
</div>
The range goes from 1 to 132, and its range is mapped [1..500] in the text input, with a simple transformation (basically it's a linear mapping with two different slopes for [1..100] and [101..132]) using the actualValue computed property.
Getting actualValue works as expected: dragging the slider correctly updates the input text with appropriate values in the range [1..500].
I'm not able to find a way to set actualValue, though. I'd like to be able to type a value in the text input, and make the slider's thumb update accordingly to the inverse transformation (val + 1150)/12.5.
It works as long as the typed number is in the range [1..100], but it "explodes" for numbers >100, e.g. 101 makes the sliderValue jump at 80892 and actualValue is then re-calculated as 1010000. As far as I understand, it's a looping-feedback scenario.
I've tried also alternative approaches (watch; v-on:change in the text input; using a third variable) to no avail.
Thanks in advance for any suggestion!
It's an amazing puzzle, and challenged me for a long time!
Look at the screenshot below. Your sliderValue and actualValue are strings, not integers. When you set actualValue as 101, you are actually setting it as a string value of "101"
Now, your sliderValue = ((actualValue + 1150)/12.5)
"101" + 1150 = "1011150" (another string!, try it in the developer console)
That messes up your entire calculation. Now you know how to fix it :-)
And you need to get that Vue devtools from here: https://github.com/vuejs/vue-devtools
EDIT: Response to comment #3
Here is the modified jsBin: http://jsbin.com/mahejozeye/1/edit?html,js,output
The only difference is introduction of two console.log statements in your map2 function. This helps you identify if your non-linear mapping function is working correctly or not. If you keep your developer console open, you will see what is happening in this function.
Case 1: When you set the value radius = 25 using the text box, your sliderRadius gets set to 111.55518394648828 and your radius again gets re-calculated as 25. So it comes around in a full circle and everything is stable here.
Case 2: When you set the value radius = 55, your sliderRadius gets set to 173.03607214428857 through your non-linear map2() function, which resets radius to 51.29869180420927
Clearly there is a circular dependency issue. Your sliderRadius and radius are very much dependent on each other and therefore radius is unable to take the value between 51 and 58.
You need to evaluate why it happens, as it has a lot to do with the non-linear mapping function that you have defined. The moment radius can take stable values at 55 (through the map2 function), then your current problem will be resolved.
The simplest fix is to set your input type to number:
<input type="number" v-model="actualValue">
or you can convert your value to an integer with something like:
set: function(val) {
var intVal = parseInt(val, 10);
if (!isNaN(intVal)) {
if (intVal <= 100) {
this.sliderValue = Math.max(1, intVal);
} else {
this.sliderValue = Math.min(132, Math.round((intVal + 1150) / 12.5));
}
}
}

making a linegraph that shows population decay with dc.js and crossfilter

I am creating a dashboard in DC.js. One of the visualizations is a survival curve showing the percentage of survival on the y-axis and the time in weeks on the x-axis
Each record in the dataset contains a deathAfter column called recidiefNa. This shows the number of weeks after death occurred, and shows -99 for survival.
See sketches for example dataset and desired chart form:
I created this code to create the dimensions and groups and draw the desired chart.
var recDim = cf1.dimension(dc.pluck('recidiefNa'));//sets dimension
var recGroup = recDim.group().reduceCount();
var resDim = cf1.dimension(dc.pluck('residuNa'));
var resGroup = resDim.group().reduceCount();
var scChart = dc.compositeChart("#scStepChart");
scChart
.width(600)
.height(400)
.x(d3.scale.linear().domain([0,52]))
.y(d3.scale.linear().domain([0,100]))
.clipPadding(10)
.brushOn(false)
.xAxisLabel("tijd in weken")
.yAxisLabel("percentage vrij van residu/recidief")
.compose([
dc.lineChart(scChart)
.dimension(recDim)
.group(recGroup)
.interpolate("step-after")
.renderDataPoints(true)
.renderTitle(true)
.keyAccessor(function(d){return d.key;})
.valueAccessor(function(d){return (d.value/cf1.groupAll().reduceCount().value()*100);}),
dc.lineChart(scChart)
.dimension(resDim)
.group(resGroup)
.interpolate("step-after")
.renderDataPoints(true)
.colors(['orange'])
.renderTitle(true)
.keyAccessor(function(d){return d.key;})
.valueAccessor(function(d){return (d.value/cf1.groupAll().reduceCount().value()*100 );})
])
.xAxis().ticks(4);
scChart.render();
This gives the following result:
As you can see my first problem is that I need the line to extend until the y-axis showing x=0weeks and y=100% as the first datapoint.
So that's question number one: is there a way to get that line to look more like my sketch(starting on the y-axis at 100%?
My second and bigger problem is that it is showing the inverse of the percentage I need (eg. 38 instead of 62). This is because of the way the data is structured (which is somehting i rather not change)
First I tried changing the valueaccessor to 100-*calculated number. Which is obviously the normal way to solve this issue. However my result was this:
As you can see now the survival curve is a positive incline which is never possible in a survival curve. This is my second question. Any ideas how to fix this?
Ah, it wasn't clear from the particular example that each data point should be based on the last, but your comment makes that clear. It sounds like what you are looking for is a kind of cumulative sum - in your case, a cumulative subtraction.
There is an entry in the FAQ for this.
Adapting that code to your use case:
function accumulate_subtract_from_100_group(source_group) {
return {
all:function () {
var cumulate = 100;
return source_group.all().map(function(d) {
cumulate -= d.value;
return {key:d.key, value:cumulate};
});
}
};
}
Use it like this:
var decayRecGroup = accumulate_subtract_from_100_group(recGroup)
// ...
dc.lineChart(scChart)
// ...
.group(decayRecGroup)
and similarly for the resGroup
While we're at it, we can concatenate the data to the initial point, to answer your first question:
function accumulate_subtract_from_100_and_prepend_start_point_group(source_group) {
return {
all:function () {
var cumulate = 100;
return [{key: 0, value: cumulate}]
.concat(source_group.all().map(function(d) {
cumulate -= d.value;
return {key:d.key, value:cumulate};
}));
}
};
}
(ridiculous function name for exposition only!)
EDIT: here is #Erik's final adapted answer with the percentage conversion built in, and a couple of performance improvements:
function fakeGrouper(source_group) {
var groupAll = cf1.groupAll().reduceCount();
return {
all:function () {
var cumulate = 100;
var total = groupAll.value();
return [{key: 0, value: cumulate}]
.concat(source_group.all().map(function(d) {
if(d.key > 0) {
cumulate -= (d.value/total*100).toFixed(0);
}
return {key:d.key, value:cumulate};
}));
}
};
}

Adding x axis labels when using dojox.charting.DataSeries

I'm creating a Dojo line chart from a dojo.data.ItemFileReadStore using a dojox.charting.DataSeries. I'm using the third parameter (value) of the constructor of DataSeries to specify a method which will generate the points on the chart. e.g.
function formatLineGraphItem(store,item)
{
var o = {
x: graphIndex++,
y: store.getValue(item, "fileSize"),
};
return o;
}
The graphIndex is an integer which is incremented for every fileSize value. This gives me a line chart with the fileSize shown against a numeric count. This works fine.
What I'd like is to be able to specify the x axis label to use instead of the value of graphIndex i.e. the under lying data will still be 1,2,3,4 but the label will show text (in this case the time at which the file size was captured).
I can do this by passing in an array of labels into the x asis when I call chart.addAxis() but this requires me to know the the values before I iterate through the data. e.g.
var dataSeriesConfig = {query: {id: "*"}};
var xAxisLabels = [{text:"2011-11-20",value:1},{text:"2011-11-21",value:2},{text:"2011-11-22",value:3}];
var chart1 = new dojox.charting.Chart("chart1");
chart1.addPlot("default", {type: "Lines", tension: "4"});
chart1.addAxis("x", {labels: xAxisLabels});
chart1.addAxis("y", {vertical: true});
chart1.addSeries("Values", new dojox.charting.DataSeries(dataStore, dataSeriesConfig, formatLineGraphItem));
chart1.render();
The xAxisLabels array can be created by preparsing the dataSeries but it's not a very nice work around.
Does anyone have any ideas how the formatLineGraphItem method could be extended to provide the x axis labels. Or does anyone have any documentation on what values the object o can contain?
Thanks in advance!
This will take a unix timestamp, multiply the value by 1000 (so that it has microseconds for JavaScript, and then pass the value to dojo date to format it).
You shouldn't have any problems editing this to the format you need.
You provided examples that your dates are like "1", "2", "3", which is clearly wrong. Those aren't dates.. so this is the best you can do unless you edit your question.
chart1.addAxis("x",{
labelFunc: function(n){
if(isNaN(dojo.number.parse(n)) || dojo.number.parse(n) % 1 != 0){
return " ";
}
else {
// I am assuming that your timestamp needs to be multiplied by 1000.
var date = new Date(dojo.number.parse(n) * 1000);
return dojo.date.locale.format(date, {
selector: "date",
datePattern: "dd MMMM",
locale: "en"
});
}
},
maxLabelSize: 100
}