Charts icon indicating copy to clipboard operation
Charts copied to clipboard

Logarithmic Axis

Open zimonjonez opened this issue 10 years ago • 52 comments

Hi

First of all, thank you so much for creating this library. Even if I was working with an Objective C example and Android documentation, I could create something with ios-charts in no time. (Actually I'm surprised this library is not as popular as CorePlot, as ios-charts is much earlier to pick up in my point of view)

Just a quick question/suggestion. One of my projects requires plotting data in Log Scale on the X-Axis. I read through the documentation on MPAndroidChart, but looks like it's not supported.

Is this going to be supported in the near future?

Is there any existing solution that I could work with?

Thanks a lot!

Simon

zimonjonez avatar May 27 '15 11:05 zimonjonez

Well ios-charts is only about two months old! So I guess there are no new users for CorePlot...

By log scale you mean that you want to have inconsistent x-axis spacing?

On Wed, May 27, 2015 at 2:30 PM, zimonjonez [email protected] wrote:

Hi

First of all, thank you so much for creating this library. Even if I was working with an Objective C example and Android documentation, I could create something with ios-charts in no time. (Actually I'm surprised this library is not as popular as CorePlot, as ios-charts is much earlier to pick up in my point of view)

Just a quick question/suggestion. One of my projects requires plotting data in Log Scale on the X-Axis. I read through the documentation on MPAndroidChart, but looks like it's not supported.

Is this going to be supported in the near future?

Is there any existing solution that I could work with?

Thanks a lot!

Simon

— Reply to this email directly or view it on GitHub https://github.com/danielgindi/ios-charts/issues/109.

danielgindi avatar May 27 '15 11:05 danielgindi

Two months only?! You did a great job! I'll definitely going to spread the word

Yes, inconsistent x-axis spacing is what I'm looking for.

See images attached. This is what I did earlier using CorePlot.

(https://cloud.githubusercontent.com/assets/9811701/7835025/084b718c-04b9-11e5-9ff7-f63e340b69d1.png) screen shot 2015-05-27 at 9 40 10 pm

zimonjonez avatar May 27 '15 11:05 zimonjonez

You can see that it is on the table, it is planned... https://github.com/PhilJay/MPAndroidChart/issues/12

danielgindi avatar May 27 '15 12:05 danielgindi

From what I'm seeing when reading more on logarithmic axises, it seems like this term refers to an actually logarithmic Y axis - where we would have to logarithmically calculate the value sizes and positions. While what you want is just more control on the x-indexes... We are planning to move to x-values soon. But I'm going to work on logarithmic y axis too!

danielgindi avatar Jun 11 '15 08:06 danielgindi

I would love to see logarithmic scaling as well (for both x and y axis). Keep up the good work, this is an awesome project!

bdhammel avatar Dec 01 '15 06:12 bdhammel

@danielgindi can this be a part of in #194 ? since they are all behaving like y axis

liuxuan30 avatar Dec 01 '15 08:12 liuxuan30

+1

RyGuyM avatar Mar 16 '16 17:03 RyGuyM

it seems like this term refers to an actually logarithmic Y axis

@danielgindi , how to configure Y-axis for using a logarithmic scale? I haven't found any references wither in the github tickets or on the internet (StackOverflow, googling, etc.)

As far as I understood your reply earlier in this thread, the library supports log scale for Y axis. Please correct me if I got you wrong.

dodikk avatar Jul 05 '16 11:07 dodikk

@danielgindi , does the library support log scale for Y axis ?

rooparaman avatar Sep 05 '16 09:09 rooparaman

@xxxrr I suppose current master branch should support both X and Y? @danielgindi We could close this if v3 did the job?

liuxuan30 avatar Sep 06 '16 00:09 liuxuan30

@liuxuan30 Thanks a lot.. Can you let me know how to configure it ? I am not able to get any references ..

rooparaman avatar Sep 06 '16 01:09 rooparaman

hmm, I am not familiar with v3 yet.. You can check out v3 ChartsDemo - time line chart. v3 is a big change how x axis values are calculated

liuxuan30 avatar Sep 06 '16 01:09 liuxuan30

@liuxuan30 Thanks.. But not able to find things related to Logarithmic scale in it..Will look deeper into v3.. Thanks..

rooparaman avatar Sep 06 '16 02:09 rooparaman

I mean there is no logarithmic support yet, but seems like v3 can support such type easier than v2. The main difference is for x axis

liuxuan30 avatar Sep 08 '16 01:09 liuxuan30

Hi, any updates about this ?

ThomasGoujon avatar Nov 15 '16 09:11 ThomasGoujon

@ThomasGoujon I think this can be solved by Chart 3.0 (except for those axis styles)

liuxuan30 avatar Nov 16 '16 07:11 liuxuan30

Hi @liuxuan30 @danielgindi, how would I go about plotting Y values logarithmically using Charts 3.0?

ashrobo avatar Nov 27 '16 13:11 ashrobo

just calculate the x, y and draw it!

liuxuan30 avatar Nov 29 '16 01:11 liuxuan30

So if I plot a range of x, y values say between 0 and 1000, how to I get the Y axis to draw logarithmically, for example 0, 1, 10, 100, 1000?

ashrobo avatar Nov 29 '16 19:11 ashrobo

oh I see, you are talking about y axis labels. By default it can't, but you can override computeAxisValues() to feed your own.

liuxuan30 avatar Nov 30 '16 01:11 liuxuan30

Can anyone give me a hint on how to get logarithmic axes like the x-axis of @zimonjonez plot? Including the logarithmic spacing between the 0, 1, 10, 100, ... steps? https://de.wikipedia.org/wiki/Logarithmische_Darstellung#/media/File:Log-Achse.png

SpaceDuster avatar Dec 05 '16 15:12 SpaceDuster

@SpaceDuster like I said above, take a look at computeAxisValues(), and override a bunch of methods in axis renderer, to get what you want. It's not a easy task, since you have to understand how this library works.

liuxuan30 avatar Dec 06 '16 01:12 liuxuan30

@AshRobinson would you be willing to share how you overrode computeAxisValues() to draw the Y axis logarithmically?

brittanygraft avatar Dec 19 '16 18:12 brittanygraft

@danielgindi Great library! Very appreciative! Has the ability to scale the Y axis logarithmically been added yet?

brittanygraft avatar Dec 23 '16 05:12 brittanygraft

No, I guess it's not a feature that many people needs.

liuxuan30 avatar Dec 27 '16 01:12 liuxuan30

@liuxuan30 I overrode computeAxisValues() to show only the values I want to show - log base 10, but the chart is still using a linear scale. Do you know how to go about redrawing the scale - keeping the logarithmic intervals equidistant apart?

@SpaceDuster have you had any luck on this?

brittanygraft avatar Dec 30 '16 03:12 brittanygraft

@danielgindi could you give me an idea of where to start when trying to rescale the Y axis?

brittanygraft avatar Dec 30 '16 17:12 brittanygraft

@brittanygraft screenshot maybe? 'but the chart is still using a linear scale' is not clear to me. Ideally, you should check the data point it using to draw for each label.

liuxuan30 avatar Jan 03 '17 01:01 liuxuan30

What I figured out so far:

  1. modify the file XAxisRenderer (you find it in the folder: Charts -> Classes -> Renderers) as follows: right after the lines "open class XAxisRenderer: AxisRendererBase {" add:
    public var logarithmic: Bool = false
    public var maxOrderOfMagnitudeNegative = 0
    public var minOrderOfMagnitudePositive = 0
    public var logSetup: [Double] = []

in "open func drawLabels..." after the lines "if xAxis.isWordWrapEnabled { labelMaxSize.width = xAxis.wordWrapWidthPercent * valueToPixelMatrix.a }" add:

        let entries: [Double]
        if logarithmic == true {
            entries = logSetup.map{log10(abs($0)) * $0/abs($0)}
            xAxis.entries = logSetup
        } else {
            entries = xAxis.entries
        }

in the same "open func drawLabels" after " if viewPortHandler.isInBoundsX(position.x) {" add and modify as follows:

// **** fraction of entries[i]
                var frac: Double
                if xAxis.entries[i] > 0 {
                    frac = 10 ** Double(minOrderOfMagnitudePositive)
                } else {
                    frac = 10 ** Double(maxOrderOfMagnitudeNegative)
                }
                let label = xAxis.valueFormatter?.stringForValue(xAxis.entries[i]/frac, axis: xAxis) ?? ""

in "open override func renderGridLines" after the line "var position = CGPoint(x: 0.0, y: 0.0)" add:

        let entries: [Double]
        if logarithmic == true {
            entries = logSetup.map{log10(abs($0)) * $0/abs($0)}
        } else {
            entries = xAxis.entries
        }
  1. use following functions to setup your data and plot for logarithmic plotting:
    // function for calculating the order of magnitude (oom) of an Double: bsp. 0.02 -> oom=-2, 10 -> oom = 1
    func order(input: Double) -> (Int) {
        var order = 0
        var temp = abs(input)
        if temp < 10 {
            while temp < 10 {
                temp = temp * 10
                order = order - 1
            }
        } else if temp > 1 {
            while temp > 1 {
                temp = temp / 10
                order = order + 1
            }
        }
        return order
    }
    

    
// ********** function for calculating the highest (negative) order of the data sets entries < 0 ******************
    
    func negativeOrderOfMagnitudeOfDataSets(dataInput: [([Double], [Double])]) -> (Int, Int, Int, Int) {

        // create arrays to hold the positive and negative parts with an absolute value < 1 (log10 < 0) of all data sets
        var PositivePartArrX: [Double] = []
        var NegativePartArrX: [Double] = []
        var PositivePartArrY: [Double] = []
        var NegativePartArrY: [Double] = []
        
        
        // cycle through the data sets and write positive and negative entries (abs < 1) in the acording arrays
        for i in 0..<dataInput.count {
            for entry in dataInput[i].0 {
                if entry < 1 && entry > 0 {
                    PositivePartArrX.append(entry)
                } else if entry > -1 && entry < 0 {
                    NegativePartArrX.append(entry)
                }
            }
            
            for entry in dataInput[i].1 {
                if entry < 1 && entry > 0 {
                    PositivePartArrY.append(entry)
                } else if entry > -1 && entry < 0 {
                    NegativePartArrY.append(entry)
                }
            }
        }
        
        // calculate the highest absolute order of the positive and negative part (abs < 1). If the array is empty the order is 0
        var orderPositivePartX: Int = 0
        if PositivePartArrX != [] {
            orderPositivePartX = order(input: PositivePartArrX.min()!)
        }
        var orderPositivePartY: Int = 0
        if PositivePartArrY != [] {
            orderPositivePartY = order(input: PositivePartArrY.min()!)
        }
        
        var orderNegativePartX: Int = 0
        if NegativePartArrX != [] {
            orderNegativePartX = order(input: NegativePartArrX.min()!)
        }
        var orderNegativePartY: Int = 0
        if NegativePartArrY != [] {
            orderNegativePartY = order(input: NegativePartArrY.min()!)
        }


        return (orderPositivePartX, orderNegativePartX, orderPositivePartY, orderNegativePartY)
        
    }
// ********** ////////////////////////////////// ******************
    
    
    
// ********** function that prepates the data for plotting and setup labels, markers, colors, ... ******************
    public func prepareDataToPlot(dataInput: [([Double], [Double])], labels: [String] = [], marker: Array<ScatterChartDataSet.Shape> = [], color: Array<NSUIColor> = [], logX: Bool = false, logY: Bool = false )->([ScatterChartDataSet]) {
        print("start preparing data for plotting")
        
        
        // if there are no (or a different number than y-values) x values the x-values are substituted by a upcounting array [0, 1, 2, 3, ...]
        var dataInput = dataInput
        for i in 0..<dataInput.count{
            if dataInput[i].0.count != dataInput[i].1.count {
                var substitute: [Double] = []
                for i in 0..<dataInput[i].0.count {
                    substitute.append(Double(i))
                }
                dataInput[i].1 = substitute
            }
        }
        
        
        
        
        // make data logarythmic
        if logX == true {

            print("     start making X-axis data logarythmic")

            
            // get absolute values < 1 (log10 < 0) and split them into a negative and positive part
            var orderPositiveArr: [Double] = []
            var orderNegativeArr: [Double] = []
            for i in 0..<dataInput.count {
                for entry in dataInput[i].0 {
                    if entry < 1 && entry > 0 {
                        orderPositiveArr.append(entry)
                    } else if entry > -1 && entry < 0 {
                        orderNegativeArr.append(entry)
                    }
                }
            }
            
            // get the highest negative order of the positive and negative entries. bsp. 0.002 -> order -3
            var minOrderPositive: Int
            if orderPositiveArr != [] {
                minOrderPositive = order(input: orderPositiveArr.min()!)
            } else {
                minOrderPositive = 0
            }
            
            var maxOrderNegative: Int
            if orderNegativeArr != [] {
                maxOrderNegative = order(input: orderNegativeArr.min()!)
            } else {
                maxOrderNegative = 0
            }
            

            
            // cycle through the data, shift them into the positive (>1) range and calculate the log values. Both for the negative and positive part.
            // entries with x value = 0.0 are deleted (also the acording y entry)
            for i in 0..<dataInput.count {
                var xDataInputLogTemp: [Double] = []

                for j in 0..<dataInput[i].0.count {
                    let xEntry = dataInput[i].0[j]
                    if xEntry > 0 {
                        let logValue = (xEntry * (10 ** Double(abs(minOrderPositive))))
                        xDataInputLogTemp.append(log10(abs(logValue)) * logValue/abs(logValue))
                    } else if xEntry < 0 {
                        let logValue = (xEntry * (10 ** Double(abs(maxOrderNegative))))
                        xDataInputLogTemp.append(log10(abs(logValue)) * logValue/abs(logValue))
                    } else {
                        dataInput[i].1.remove(at: j)
                    }
                }
                dataInput[i].0 = xDataInputLogTemp
            }
            print("    done making X-axis data logarythmic)")
        }
        
        
        
        
        if logY == true {
            print("     start making Y-axis data logarythmic")
            
            
            // get absolute values < 1 (log10 < 0) and split them into a negative and positive part
            var orderPositiveArr: [Double] = []
            var orderNegativeArr: [Double] = []
            for i in 0..<dataInput.count {
                for entry in dataInput[i].1 {
                    if entry < 1 && entry > 0 {
                        orderPositiveArr.append(entry)
                    } else if entry > -1 && entry < 0 {
                        orderNegativeArr.append(entry)
                    }
                }
            }
            
            // get the highest negative order of the positive and negative entries. bsp. 0.002 -> order -3
            var minOrderPositive: Int
            if orderPositiveArr != [] {
                minOrderPositive = order(input: orderPositiveArr.min()!)
            } else {
                minOrderPositive = 0
            }
            
            var maxOrderNegative: Int
            if orderNegativeArr != [] {
                maxOrderNegative = order(input: orderNegativeArr.min()!)
            } else {
                maxOrderNegative = 0
            }
            
            
            
            // cycle through the data, shift them into the positive (>1) range and calculate the log values. Both for the negative and positive part.
            // entries with x value = 0.0 are deleted (also the acording y entry)
            for i in 0..<dataInput.count {
                var xDataInputLogTemp: [Double] = []
                for j in 0..<dataInput[i].1.count {
                    let xEntry = dataInput[i].1[j]
                    if xEntry > 0 {
                        let logValue = (xEntry * (10 ** Double(abs(minOrderPositive))))
                        xDataInputLogTemp.append(log10(abs(logValue)) * logValue/abs(logValue))
                    } else if xEntry < 0 {
                        let logValue = (xEntry * (10 ** Double(abs(maxOrderNegative))))
                        xDataInputLogTemp.append(log10(abs(logValue)) * logValue/abs(logValue))
                    } else {
                        dataInput[i].0.remove(at: j)
                    }
                }
                dataInput[i].1 = xDataInputLogTemp
            }
            print("     done making Y-axis data logarythmic")
        }
        
        

        
        
        
        
        var dataEntries: [[ChartDataEntry]] = []
        
        // setup of markers, colors, ... and their default values if no input is given
        var labels = labels
        var marker = marker
        var color = color
        var colors = [NSUIColor.black, NSUIColor.blue, NSUIColor.red, NSUIColor.cyan, NSUIColor.green, NSUIColor.magenta, NSUIColor.orange, NSUIColor.magenta, NSUIColor.brown, NSUIColor.yellow]
        
        // (Marker) Color set-up
        if color.count != dataInput.count {
            color = []
            if dataInput.count < colors.count {
                for i in 0..<dataInput.count {
                    color.append(colors[i])
                }
            } else {
                var counter = 0
                for _ in 0..<dataInput.count {
                    color.append(colors[counter])
                    
                    if counter == colors.endIndex {
                        counter = 0
                    }
                }
            }
        }
        
        
        
        
        // Marker set-up
        if marker.count != dataInput.count {
            marker = []
            for _ in 0..<dataInput.count {
                marker.append(ScatterChartDataSet.Shape.x)
            }
        }
        
        
        // Description lable set-up
        if labels.count != dataInput.count {
            labels = [String](repeating: "-", count: dataInput.count)
        }
        
        
        // chart data set-up
        for i in 0..<dataInput.count {
            var dataEntriesSet: [ChartDataEntry] = []
            for j in 0..<dataInput[i].0.count {
                dataEntriesSet.append(ChartDataEntry(x: dataInput[i].0[j], y: dataInput[i].1[j]))
            }
            dataEntries.append(dataEntriesSet)
        }
        
        
        
        // plot apperance set-up
        var scatterChartDataSets: [ScatterChartDataSet] = []
        for i in 0..<dataEntries.count {
            let scatterChartDataSetI = ScatterChartDataSet(values: dataEntries[i], label: labels[i])
            scatterChartDataSetI.setScatterShape(marker[i])
            scatterChartDataSetI.colors = [color[i]]
            scatterChartDataSets.append(scatterChartDataSetI)
        }
        

        // return processed data to be plotted by plot function
        print("done preparing data for plotting")
        
        return scatterChartDataSets
        
    }
    
    func negativeOrderOfMagnitudeOfDataSets(dataInput: [([Double], [Double])]) -> (Int, Int, Int, Int) {

        // create arrays to hold the positive and negative parts with an absolute value < 1 (log10 < 0) of all data sets
        var PositivePartArrX: [Double] = []
        var NegativePartArrX: [Double] = []
        var PositivePartArrY: [Double] = []
        var NegativePartArrY: [Double] = []
        
        
        // cycle through the data sets and write positive and negative entries (abs < 1) in the acording arrays
        for i in 0..<dataInput.count {
            for entry in dataInput[i].0 {
                if entry < 1 && entry > 0 {
                    PositivePartArrX.append(entry)
                } else if entry > -1 && entry < 0 {
                    NegativePartArrX.append(entry)
                }
            }
            
            for entry in dataInput[i].1 {
                if entry < 1 && entry > 0 {
                    PositivePartArrY.append(entry)
                } else if entry > -1 && entry < 0 {
                    NegativePartArrY.append(entry)
                }
            }
        }
        
        // calculate the highest absolute order of the positive and negative part (abs < 1). If the array is empty the order is 0
        var orderPositivePartX: Int = 0
        if PositivePartArrX != [] {
            orderPositivePartX = order(input: PositivePartArrX.min()!)
        }
        var orderPositivePartY: Int = 0
        if PositivePartArrY != [] {
            orderPositivePartY = order(input: PositivePartArrY.min()!)
        }
        
        var orderNegativePartX: Int = 0
        if NegativePartArrX != [] {
            orderNegativePartX = order(input: NegativePartArrX.min()!)
        }
        var orderNegativePartY: Int = 0
        if NegativePartArrY != [] {
            orderNegativePartY = order(input: NegativePartArrY.min()!)
        }


        return (orderPositivePartX, orderNegativePartX, orderPositivePartY, orderNegativePartY)
        
    }



// ********** setup the plot for log style plotting ******************

func logAxis(data: [([Double], [Double])], xAxis: Bool = false, yAxis: Bool = false) -> ([Double],[Double]) {
        print("start preparing plot for log axis")
        
        func logAxisRange(data: [Double]) -> ([Double]) {
            // create arrays to hold the positive and negative parts of the input data
            var dataPositive: [Double] = []
            var dataNegative: [Double] = []
            // cycle through data and copy the positive and negative entries in the regarding arrays
            for i in data {
                if i > 0 && i < Double.infinity {
                    dataPositive.append(i)
                } else if i < 0 && i > -Double.infinity {
                    dataNegative.append(i)
                }
            }

            // steps shown in the plot
            let arr = [1.0, 2.0, 4.0, 6.0, 8.0]
        
            // the data is split in positive and negative parts. The axis range for both parts are calculated seperatly
            var logAxisPositive: [Double] = []
            var logAxisNegative: [Double] = []
        
        
            // if data < 0 exists:
            if dataNegative != [] {
                // calculating minimun and maximum (range) of the negative part
                let minDataNegative = dataNegative.min()!
                let maxDataNegative = dataNegative.max()!

                // multiplying the step array with -1 and a multiple of 10 to get the drawn steps (gridlines and values) from the minimum to the maximum
                for i in order(input: minDataNegative) ... order(input: maxDataNegative)+1 {
                    logAxisNegative = logAxisNegative + arr.map{-1 * $0 * (10 ** Double(i))}
                }
                // if the datasets have entries abs < 1 (log10 negative) multiplying all entries with 10 ** abs(negative order) to shift into the positive log10 range.
                // this shift is later compensated and allows to distinguish from positive and negative data input by tho minus sign
                if order(input: maxDataNegative) < 0 {
                    logAxisNegative = logAxisNegative.map{$0 * (10 ** Double(abs(order(input: maxDataNegative))))}
                }
                // reverse the negative part so that the axis draws from right to left
                logAxisNegative.reverse()
            }
            // if data > 0 exists:
            if dataPositive != [] {
                // calculating minimun and maximum (range) of the negative part
                let minDataPositive = dataPositive.min()!
                let maxDataPositive = dataPositive.max()!
                // multiplying the step array with a multiple of 10 to get the drawn steps (gridlines and values) from the minimum to the maximum
                for i in order(input: maxDataPositive)-1 ... order(input: minDataPositive) {
                    logAxisPositive = logAxisPositive + arr.map{$0 * (10 ** Double(i))}
                }
                // if the datasets have entries < 1 (log10 negative) multiplying all entries with 10 ** abs(negative order) to shift into the positive log10 range.
                // this shift is later compensated and allows to distinguish from positive and negative data input by tho minus sign
                if order(input: minDataPositive) < 0 {
                    logAxisPositive = logAxisPositive.map{$0 * (10 ** Double(abs(order(input: minDataPositive))))}
                }
            }
            // the logarithmized axis is set together from the negative and positive parts
            let logAxis = logAxisNegative + logAxisPositive


            return logAxis
            
        }
        
        
        var logXAxis: [Double] = []
        var logYAxis: [Double] = []
        if xAxis == true {
            var xAxisData: [Double] = []
            for i in 0..<data.count {
                xAxisData = xAxisData + data[i].0
            }
            logXAxis = logAxisRange(data: xAxisData)
        }
        if yAxis == true {
            var yAxisData: [Double] = []
            for i in 0..<data.count {
                yAxisData = yAxisData + data[i].1
            }
            logYAxis = logAxisRange(data: yAxisData)
        }

        print("done preparing plot for log axis")
        return (logXAxis, logYAxis)
}

    
// ********** function to setup the plot for logarithmic plotting ******************

    func setupPlotForLog (data: [([Double], [Double])], logXAxis: Bool = false, logYAxis: Bool = false) -> () {
        if logXAxis == true {
            plotArea.xAxisRenderer.logarithmic = true
            plotArea.xAxisRenderer.logSetup = logAxis(data: data, xAxis: true).0
            plotArea.xAxisRenderer.minOrderOfMagnitudePositive = abs(negativeOrderOfMagnitudeOfDataSets(dataInput: data).0)
            plotArea.xAxisRenderer.maxOrderOfMagnitudeNegative = abs(negativeOrderOfMagnitudeOfDataSets(dataInput: data).1)
        }
 
        if logYAxis == true {
            plotArea.leftYAxisRenderer .logarithmic = true
            plotArea.leftYAxisRenderer.logSetup = logAxis(data: data, yAxis: true).1
            plotArea.leftYAxisRenderer.minOrderOfMagnitudePositive = abs(negativeOrderOfMagnitudeOfDataSets(dataInput: data).2)
            plotArea.leftYAxisRenderer.maxOrderOfMagnitudeNegative = abs(negativeOrderOfMagnitudeOfDataSets(dataInput: data).3)

        }
        
    }

possible that I forgot a step here. Please let me know if that works for you.

SpaceDuster avatar Jan 03 '17 09:01 SpaceDuster

hello

i have some errors on "order" Use of unresolved identifier 'order'

order(input: maxDataNegative) order(input: minDataNegative)-1 etc.....

thanks

thierryH91200 avatar Jan 03 '17 11:01 thierryH91200